/**
* Created by Nicholas Hallahan on 1/7/15.
* nhallahan@spatialdev.com
*/
package com.spatialdev.osm.model;
import android.util.Log;
import com.mapbox.mapboxsdk.api.ILatLng;
import com.spatialdev.osm.marker.OSMMarker;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.index.quadtree.Quadtree;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class JTSModel {
private static final int TAP_PIXEL_TOLERANCE = 24;
private Map<String, OSMDataSet> dataSetHash;
private GeometryFactory geometryFactory;
private Quadtree spatialIndex;
public JTSModel() {
geometryFactory = new GeometryFactory();
spatialIndex = new Quadtree();
dataSetHash = new ConcurrentHashMap<>();
}
public synchronized void addOSMDataSet(String filePath, OSMDataSet ds) {
dataSetHash.put(filePath, ds);
addOSMClosedWays(ds);
addOSMOpenWays(ds);
addOSMStandaloneNodes(ds);
}
public synchronized void mergeEditedOSMDataSet(String absPath, OSMDataSet ds) {
Collection<OSMDataSet> dataSets = dataSetHash.values();
for (OSMDataSet existingDataSet : dataSets) {
// closed ways
removeWaysFromExistingDataSet(existingDataSet, ds.getClosedWays());
//open ways
removeWaysFromExistingDataSet(existingDataSet, ds.getOpenWays());
}
addOSMDataSet(absPath, ds);
}
/**
* Removes a specific OSM XML Data Set based off of the path of the file.
* * *
* @param absoluteFilePath
*/
public void removeDataSet(String absoluteFilePath) {
OSMDataSet ds = dataSetHash.get(absoluteFilePath);
List<OSMWay> closedWays = ds.getClosedWays();
List<OSMWay> openWays = ds.getOpenWays();
List<OSMNode> standaloneNodes = ds.getStandaloneNodes();
for (OSMWay w : closedWays) {
try {
Geometry geom = w.getJTSGeom();
Envelope env = geom.getEnvelopeInternal();
spatialIndex.remove(env, w);
} catch (Exception e) {
Log.e("NO_GEOM", "Cannot remove a closed way with no JTS geom.");
}
}
for (OSMWay w : openWays) {
try {
Geometry geom = w.getJTSGeom();
Envelope env = geom.getEnvelopeInternal();
spatialIndex.remove(env, w);
} catch (Exception e) {
Log.e("NO_GEOM", "Cannot remove an open way with no JTS geom.");
}
}
for (OSMNode n : standaloneNodes) {
try {
Geometry geom = n.getJTSGeom();
Envelope env = geom.getEnvelopeInternal();
spatialIndex.remove(env, n);
} catch (Exception e) {
Log.e("NO_GEOM", "Cannot remove a standalone node with no JTS geom.");
}
}
}
private void removeWaysFromExistingDataSet(OSMDataSet existingDataSet, List<OSMWay> ways) {
for (OSMWay w : ways) {
OSMWay oldWay = existingDataSet.getWay(w.getId());
if (oldWay != null) {
Geometry geom = oldWay.getJTSGeom();
if (geom != null) {
Envelope env = geom.getEnvelopeInternal();
spatialIndex.remove(env, oldWay);
}
}
}
}
public Envelope createTapEnvelope(ILatLng latLng, float zoom) {
return createTapEnvelope(latLng.getLatitude(), latLng.getLongitude(), zoom);
}
public Envelope createTapEnvelope(double lat, double lng, float zoom) {
Coordinate coord = new Coordinate(lng, lat);
return createTapEnvelope(coord, lat, lng, zoom);
}
public List<OSMElement> queryFromEnvelope(Envelope envelope) {
List<OSMElement> results = spatialIndex.query(envelope);
return results;
}
public OSMElement queryFromTap(ILatLng latLng, float zoom) {
double lat = latLng.getLatitude();
double lng = latLng.getLongitude();
Coordinate coord = new Coordinate(lng, lat);
Envelope envelope = createTapEnvelope(coord, lat, lng, zoom);
List results = spatialIndex.query(envelope);
int len = results.size();
if (len == 0 ) {
return null;
}
if (len == 1) {
return (OSMElement) results.get(0);
}
Point clickPoint = geometryFactory.createPoint(coord);
OSMElement closestElement = null;
double closestDist = Double.POSITIVE_INFINITY; // should be replaced in first for loop iteration
for (Object res : results) {
OSMElement el = (OSMElement) res;
if (closestElement == null) {
closestElement = el;
closestDist = el.getJTSGeom().distance(clickPoint);
continue;
}
Geometry geom = el.getJTSGeom();
double dist = geom.distance(clickPoint);
if (dist > closestDist) {
continue;
}
if (dist < closestDist) {
closestElement = el;
closestDist = dist;
continue;
}
// If we are here, then the distances are the same,
// so we prioritize which element is better based on their type.
closestElement = prioritizeElementByType(closestElement, el);
}
Geometry closestElementGeom = closestElement.getJTSGeom();
if (closestElementGeom != null && closestElementGeom.intersects(geometryFactory.createPoint(coord))) {
return closestElement;
}
return null;
}
private Envelope createTapEnvelope(Coordinate coord, double lat, double lng, float zoom) {
Envelope envelope = new Envelope(coord);
// Creating a reasonably sized envelope around the tap location.
// Tweak the TAP_PIXEL_TOLERANCE to get a better sized box for your needs.
double degreesLngPerPixel = degreesLngPerPixel(zoom);
double deltaX = degreesLngPerPixel * TAP_PIXEL_TOLERANCE;
double deltaY = scaledLatDeltaForMercator(deltaX, lat);
envelope.expandBy(deltaX, deltaY);
return envelope;
}
/**
* Prioritizes points over lines over polygons.
* @param el1
* @param el2
* @return the priority OSMElement type
*/
private OSMElement prioritizeElementByType(OSMElement el1, OSMElement el2) {
if (el1 instanceof OSMNode) {
return el1;
}
if (el2 instanceof OSMNode) {
return el2;
}
// It's gotta be a Way at this point...
if ( ! ((OSMWay)el1).isClosed() ) {
return el1;
}
return el2;
}
private void addOSMClosedWays(OSMDataSet ds) {
List<OSMWay> closedWays = ds.getClosedWays();
for (OSMWay w : closedWays) {
if (!w.isModified() && OSMWay.containsModifiedWay(w.getId())) {
continue;
}
// Don't render or index ways that do not have all of their referenced nodes.
if (w.incomplete()) {
continue;
}
List<OSMNode> nodes = w.getNodes();
Coordinate[] coords = coordArrayFromNodeList(nodes);
Polygon poly = geometryFactory.createPolygon(coords);
w.setJTSGeom(poly);
Envelope envelope = poly.getEnvelopeInternal();
spatialIndex.insert(envelope, w);
}
}
private void addOSMOpenWays(OSMDataSet ds) {
List<OSMWay> openWays = ds.getOpenWays();
for (OSMWay w : openWays) {
if (!w.isModified() && OSMWay.containsModifiedWay(w.getId())) {
continue;
}
// Don't render or index ways that do not have all of their referenced nodes.
if (w.incomplete()) {
continue;
}
List<OSMNode> nodes = w.getNodes();
Coordinate[] coords = coordArrayFromNodeList(nodes);
LineString line = geometryFactory.createLineString(coords);
w.setJTSGeom(line);
Envelope envelope = line.getEnvelopeInternal();
spatialIndex.insert(envelope, w);
}
}
private Coordinate[] coordArrayFromNodeList(List<OSMNode> nodes) {
Coordinate[] coords = new Coordinate[nodes.size()];
int i = 0;
for (OSMNode node : nodes) {
double lat = node.getLat();
double lng = node.getLng();
Coordinate coord = new Coordinate(lng, lat);
coords[i++] = coord;
}
return coords;
}
private void addOSMStandaloneNodes(OSMDataSet ds) {
List<OSMNode> standaloneNodes = ds.getStandaloneNodes();
for (OSMNode n : standaloneNodes) {
double lat = n.getLat();
double lng = n.getLng();
Coordinate coord = new Coordinate(lng, lat);
Point point = geometryFactory.createPoint(coord);
n.setJTSGeom(point);
Envelope envelope = point.getEnvelopeInternal();
spatialIndex.insert(envelope, n);
}
}
/**
* When the user adds an OSMNode POI to the map, we want to add that new
* node to JTSModel. This is done in OSMMap.
*
* @param n - the OSMNode
*/
public void addOSMStandaloneNode(OSMNode n) {
double lat = n.getLat();
double lng = n.getLng();
Coordinate coord = new Coordinate(lng, lat);
Point point = geometryFactory.createPoint(coord);
n.setJTSGeom(point);
Envelope envelope = point.getEnvelopeInternal();
spatialIndex.insert(envelope, n);
}
/**
* Removes the OSMElement from the Spatial Index
* if the element there.
*
* @param el - any OSMElement
*/
public void removeOSMElement(OSMElement el) {
Geometry geom = el.getJTSGeom();
if (geom != null) {
Envelope env = geom.getEnvelopeInternal();
spatialIndex.remove(env, el);
}
}
/**
* This is how degrees wide a given pixel is for a given zoom.
*
* @param zoom
* @return
*/
private static double degreesLngPerPixel(float zoom) {
double degreesPerTile = 360 / Math.pow(2, zoom);
return degreesPerTile / 256;
}
/**
* This is how many degrees high a given Lat Delta is for a given
* zoom in Spherical Mercator.
*
* http://en.wikipedia.org/wiki/Mercator_projection#Scale_factor
*
* @param deltaDeg, lng
* @return
*/
private static double scaledLatDeltaForMercator(double deltaDeg, double lat) {
double scale = 1 / Math.cos(Math.toRadians(lat));
return deltaDeg / scale;
}
}