/**
* Copyright (C) 2008 - 2014 52°North Initiative for Geospatial Open Source
* Software GmbH
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published
* by the Free Software Foundation.
*
* If the program is linked with libraries which are licensed under one of
* the following licenses, the combination of the program with the linked
* library is not considered a "derivative work" of the program:
*
* - Apache License, version 2.0
* - Apache Software License, version 1.0
* - GNU Lesser General Public License, version 3
* - Mozilla Public License, versions 1.0, 1.1 and 2.0
* - Common Development and Distribution License (CDDL), version 1.0
*
* Therefore the distribution of the program linked with libraries licensed
* under the aforementioned licenses, is permitted by the copyright holders
* if the distribution is compliant with both the GNU General Public
* icense version 2 and the aforementioned licenses.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*/
package org.n52.ses.eml.v002.views;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.Map;
import org.n52.ses.api.event.MapEvent;
import org.n52.ses.eml.v002.filter.spatial.methods.ICreateBuffer;
import org.n52.ses.eml.v002.filter.spatial.methods.PostGisCreateBuffer;
import org.n52.ses.eml.v002.filterlogic.esper.customFunctions.SpatialMethods;
import org.n52.ses.util.common.ConfigurationRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.espertech.esper.client.EventBean;
import com.espertech.esper.client.EventType;
import com.espertech.esper.core.context.util.AgentInstanceViewFactoryChainContext;
import com.espertech.esper.core.service.StatementContext;
import com.espertech.esper.epl.expression.ExprConstantNode;
import com.espertech.esper.epl.expression.ExprIdentNode;
import com.espertech.esper.event.map.MapEventBean;
import com.espertech.esper.view.ViewSupport;
import com.espertech.esper.view.Viewable;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKTReader;
/**
* This is a custom view for esper to create a dynamic spatial buffer.
* The dynamic buffer gets updated by position information, thus
* enabling trajectory processing.
*
* @author matthes rieke
*
*/
/**
* @author matthes
*
*/
public class DynamicSpatialBufferView extends ViewSupport {
private static final Logger logger = LoggerFactory
.getLogger(DynamicSpatialBufferView.class);
public static final String SELECTFUNCTION_NAME = "SelectGeometryInDynamicBuffer";
public static final boolean ALLOW_ORIGINAL_MESSAGE = true;
public static final String SELECT_STRING = "*";
private StatementContext context;
private EventType eventType;
private Double bufferDistance;
private Geometry route;
Geometry buffer;
Object bufferMutex = new Object();
private String distanceUOM;
private String aircraftProperty;
private String notifyProperty;
private Point currentPosition;
private Point previousPosition;
private ICreateBuffer buffering;
private String crs;
private EventBean lastInsertStreamEvent;
private int currentSegmentPoint = 1;
private Geometry dynamicRoute;
private GeometryFactory geomFactory = new GeometryFactory();
/**
* @param ctx the statement context
* @param et the eventType from the factory
* @param geometry the route
* @param distance the buffer distance
* @param uom the distance uom
* @param crs the coordinate reference system for this buffer
* @param aircraftPos the aircraft updates
* @param notifyGeom the actual geometry
*/
public DynamicSpatialBufferView(AgentInstanceViewFactoryChainContext ctx,
EventType et, Object geometry, Object distance, Object uom, Object crs,
Object aircraftPos, Object notifyGeom) {
this.context = ctx.getStatementContext();
this.eventType = et;
this.bufferDistance = (Double) ((ExprConstantNode) distance).getValue();
this.distanceUOM = (String) ((ExprConstantNode) uom).getValue();;
this.aircraftProperty = (String) ((ExprIdentNode) aircraftPos).getFullUnresolvedName();
this.notifyProperty = (String) ((ExprIdentNode) notifyGeom).getFullUnresolvedName();
this.buffering = new PostGisCreateBuffer();
this.crs = (String) ((ExprConstantNode) crs).getValue();;
WKTReader wkt = new WKTReader();
String geomString = (String) ((ExprIdentNode) geometry).getUnresolvedPropertyName();
try {
this.route = wkt.read(geomString.substring(9, geomString.length() - 2));
this.dynamicRoute = this.route;
} catch (ParseException e) {
logger.warn(e.getMessage(), e);
}
if (this.route != null) {
this.buffer = this.buffering.buffer(this.route, this.bufferDistance, this.distanceUOM, this.crs);
}
}
@Override
public void update(EventBean[] newData, EventBean[] oldData) {
// The remove stream most post the same exact object references of events that were posted as the insert stream
EventBean[] removeStreamToPost;
if (this.lastInsertStreamEvent != null) {
removeStreamToPost = new EventBean[] {this.lastInsertStreamEvent};
}
else {
removeStreamToPost = new EventBean[] {populateMap(null)};
}
if (newData.length > 0) {
for (EventBean eventBean : newData) {
@SuppressWarnings("unchecked")
Map<String, Object> underlying = (Map<String, Object>) eventBean.getUnderlying();
MapEventBean positionEvent = null;
MapEventBean notifyEvent = null;
for (String type : underlying.keySet()) {
if (type.equals(this.aircraftProperty.substring(0, this.aircraftProperty.indexOf(".")))) {
/*
* we have a position update!
*/
positionEvent = (MapEventBean) underlying.get(type);
this.previousPosition = this.currentPosition;
this.currentPosition = (Point) positionEvent.getProperties().get(this.aircraftProperty.substring(
this.aircraftProperty.indexOf(".")+1, this.aircraftProperty.length()));
//check if our buffer needs an update
checkBufferUpdate();
}
else if (type.equals(this.notifyProperty.substring(0, this.notifyProperty.indexOf(".")))) {
/*
* we have a notification geometry
*/
notifyEvent = (MapEventBean) underlying.get(type);
}
}
if (notifyEvent != null && !areEqual(notifyEvent, positionEvent)) {
/*
* this is a different event, treat it as notification update
*/
if (withinBuffer(notifyEvent.getProperties())) {
/*
* it matched the dynamic buffer!
* populate to parent views -> this starts
* provision to the StatementListener
*/
if (this.hasViews()) {
EventBean newDataPost = populateMap(notifyEvent);
this.lastInsertStreamEvent = newDataPost;
updateChildren(new EventBean[] {newDataPost}, removeStreamToPost);
}
/*
* kann wieder raus, demokram
*/
if (Boolean.parseBoolean(ConfigurationRegistry.getInstance().getPropertyForKey("DEMO_MODE"))) {
final Geometry geom = (Geometry) notifyEvent.getProperties().get(MapEvent.GEOMETRY_KEY);
new Thread(new Runnable() {
@Override
public void run() {
try {
HttpURLConnection conn = (HttpURLConnection) (new URL("http://localhost:8082")).openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.connect();
OutputStream writer = conn.getOutputStream();
String msg = "Notification: "+ geom.toText();
writer.write(msg.getBytes());
writer.flush();
writer.close();
conn.getResponseCode();
conn.disconnect();
} catch (MalformedURLException e) {
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
}).start();
}
}
}
}
}
}
private boolean withinBuffer(Map<String, Object> properties) {
if (properties.get(MapEvent.GEOMETRY_KEY) == null) {
return false;
}
Geometry geom = (Geometry) properties.get(MapEvent.GEOMETRY_KEY);
if (this.buffer != null) {
synchronized (this.bufferMutex) {
return SpatialMethods.intersects(geom, this.buffer);
}
}
return false;
}
private boolean areEqual(MapEventBean notifyEvent,
MapEventBean positionEvent) {
//simplest case
if (notifyEvent != null && positionEvent == null) return false;
return notifyEvent.getProperties().get(MapEvent.ORIGNIAL_MESSAGE_KEY).equals(
positionEvent.getProperties().get(MapEvent.ORIGNIAL_MESSAGE_KEY));
}
/**
* This methods implements the algorithm to check
* if the buffer has to be recalculated.
*/
private synchronized void checkBufferUpdate() {
/*
* we follow a simple principle.
* - check the distance to the next route segement point.
* - compare previous distance to current distance.
* - if the distance is greater than the previous then we assume that
* the segment point is passed, since the distance is growing
*
* TODO Future work for dynamic algorithm:
* - take angles into account
* - in the case the aircraft is too fast (i.e. it gets beyond the
* current active coordinate's successor during position updates)
* check for "left-out" segment points
* - to ensure the order of position updates, consider the xml timestamp
* element
*/
if (this.previousPosition == null) {
return;
}
//only calculate an update if we have more then 1 segment.
if (this.route.getCoordinates().length > 2 && this.route.getCoordinates().length - 1 != this.currentSegmentPoint) {
Coordinate activePoint = this.route.getCoordinates()[this.currentSegmentPoint];
double preDist = activePoint.distance(this.previousPosition.getCoordinate());
double curDist = activePoint.distance(this.currentPosition.getCoordinate());
//check if the position moves away from the active coordinate
if (curDist > preDist) {
Coordinate[] origCoords = this.route.getCoordinates();
Coordinate[] points = new Coordinate[origCoords.length - this.currentSegmentPoint];
for (int i = 0; i < points.length; i++) {
points[i] = origCoords[i+this.currentSegmentPoint];
}
//increment the active coordinate
this.currentSegmentPoint++;
//create the new dynamic route
this.dynamicRoute = this.geomFactory.createLineString(points);
//calculate the actual buffer
synchronized (this.bufferMutex) {
this.buffer = this.buffering.buffer(this.dynamicRoute, this.bufferDistance, this.distanceUOM, this.crs);
}
//demo stuff
if (Boolean.parseBoolean(ConfigurationRegistry.getInstance().getPropertyForKey("DEMO_MODE"))) {
new Thread(new Runnable() {
@Override
public void run() {
try {
HttpURLConnection conn = (HttpURLConnection) (new URL("http://localhost:8082")).openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.connect();
OutputStream writer = conn.getOutputStream();
String msg = null;
synchronized (DynamicSpatialBufferView.this.bufferMutex) {
msg = "BufferUpdate="+ DynamicSpatialBufferView.this.buffer.toText();
}
if (msg != null) {
writer.write(msg.getBytes());
writer.flush();
writer.close();
conn.getResponseCode();
}
conn.disconnect();
} catch (MalformedURLException e) {
} catch (IOException e) {
}
}
}).start();
}
}
}
}
private EventBean populateMap(MapEventBean notifyEvent) {
EventBean result;
if (notifyEvent == null) {
result = new MapEventBean(this.eventType);
} else {
result = new MapEventBean(notifyEvent.getProperties(), notifyEvent.getEventType());
}
return result;
}
@Override
public EventType getEventType() {
return this.eventType;
}
@Override
public Iterator<EventBean> iterator() {
this.context.getDynamicReferenceEventTypes();
return null;
}
@Override
public void setParent(Viewable parent) {
super.setParent(parent);
if (parent != null) {
//TODO do what?
}
}
}