Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle Elasticsearch GeoJSON circles using interpolation #67

Merged
merged 1 commit into from
Apr 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.Polygon;
import java.awt.geom.Point2D;
import org.geotools.referencing.GeodeticCalculator;
import org.geotools.referencing.datum.DefaultEllipsoid;

/**
* Utilities for parsing Elasticsearch document source and field content to
* Utilities for parsing Elasticsearch document source and field content to
* extract values and create geometries.
*
*/
Expand All @@ -38,28 +41,45 @@ public class ElasticParserUtil {
private final static Logger LOGGER = Logging.getLogger(ElasticParserUtil.class);

private static final Pattern GEO_POINT_PATTERN;

static {
GEO_POINT_PATTERN = Pattern.compile("\\s*([-+]?\\d*\\.?\\d*)[^-+\\d\\.]+([-+]?\\d*\\.?\\d*)\\s*");
}

private static final Pattern GEO_HASH_PATTERN;

static {
GEO_HASH_PATTERN = Pattern.compile("[0123456789bcdefghjkmnpqrstuvwxyz]+");
}

private static final Pattern ELASTIC_DISTANCE_PATTERN;

static {
ELASTIC_DISTANCE_PATTERN = Pattern.compile("([0-9]+(\\.[0-9]+)?)([a-zA-Z]*)");
}

private static final double CIRCLE_INTERPOLATION_INTERVAL = 500.0;
private static final int MAX_CIRCLE_POINTS = 500;
private static final int MIN_CIRCLE_POINTS = 40;
private static final double MIN_CIRCLE_RADIUS_M = 0.001;

private final GeodeticCalculator geodeticCalculator;

private final GeometryFactory geometryFactory;

private boolean unsupportedEncodingMessage;

public ElasticParserUtil() {
this.geometryFactory = new GeometryFactory();
this.unsupportedEncodingMessage = false;
this.geodeticCalculator = new GeodeticCalculator(DefaultEllipsoid.WGS84);
}

/**
* Create point geometry given geo_point or geo_shape definition. GeoPoint
* can be defined by string, geohash, coordinate array or properties map.
* GeoShape is defined by properties map.
*
* @param obj GeoPoint or GeoShape definition
* @return Geometry
*/
Expand All @@ -73,28 +93,28 @@ public Geometry createGeometry(Object obj) {
// coordinate
final double y = Double.valueOf(listMatcher.group(1));
final double x = Double.valueOf(listMatcher.group(2));
geometry = geometryFactory.createPoint(new Coordinate(x,y));
geometry = geometryFactory.createPoint(new Coordinate(x, y));
} else if (GEO_HASH_PATTERN.matcher((String) obj).matches()) {
// geohash
final LatLong latLon = GeoHash.decodeHash((String) obj);
final Coordinate geoPoint = new Coordinate(latLon.getLon(), latLon.getLat());
final double lat = geoPoint.y;
final double lon = geoPoint.x;
geometry = geometryFactory.createPoint(new Coordinate(lon,lat));
geometry = geometryFactory.createPoint(new Coordinate(lon, lat));
} else {
geometry = null;
}
} else if (obj instanceof List && ((List<?>) obj).size()==2) {
} else if (obj instanceof List && ((List<?>) obj).size() == 2) {
// geo_point by coordinate array
final List<?> values = (List<?>) obj;
if (Number.class.isAssignableFrom(values.get(0).getClass())) {
final double x = ((Number) values.get(0)).doubleValue();
final double y = ((Number) values.get(1)).doubleValue();
geometry = geometryFactory.createPoint(new Coordinate(x,y));
geometry = geometryFactory.createPoint(new Coordinate(x, y));
} else if (values.get(0) instanceof String) {
final double x = Double.valueOf((String) values.get(0));
final double y = Double.valueOf((String) values.get(1));
geometry = geometryFactory.createPoint(new Coordinate(x,y));
geometry = geometryFactory.createPoint(new Coordinate(x, y));
} else {
geometry = null;
}
Expand All @@ -108,12 +128,13 @@ public Geometry createGeometry(Object obj) {
}

/**
* Create geometry given property map defining geo_shape type and coordinates
* or geo_point lat and lon.
* Create geometry given property map defining geo_shape type and
* coordinates or geo_point lat and lon.
*
* @param properties Properties
* @return Geometry
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@SuppressWarnings({"rawtypes", "unchecked"})
public Geometry createGeometry(final Map<String, Object> properties) {
final Geometry geometry;
switch (String.valueOf(properties.get("type")).toUpperCase()) {
Expand All @@ -123,83 +144,99 @@ public Geometry createGeometry(final Map<String, Object> properties) {
final Coordinate coordinate = createCoordinate(posList);
geometry = geometryFactory.createPoint(coordinate);
break;
} case "LINESTRING": {
}
case "LINESTRING": {
final List<List<Object>> posList;
posList = (List) properties.get("coordinates");
final Coordinate[] coordinates = createCoordinates(posList);
geometry = geometryFactory.createLineString(coordinates);
break;
} case "POLYGON": {
}
case "POLYGON": {
final List<List<List<Object>>> posList;
posList = (List) properties.get("coordinates");
geometry = createPolygon(posList);
break;
} case "MULTIPOINT": {
}
case "MULTIPOINT": {
final List<List<Object>> posList;
posList = (List) properties.get("coordinates");
final Coordinate[] coordinates = createCoordinates(posList);
geometry = geometryFactory.createMultiPoint(coordinates);
break;
} case "MULTILINESTRING": {
}
case "MULTILINESTRING": {
final List<List<List<Object>>> posList;
posList = (List) properties.get("coordinates");
final LineString[] lineStrings = new LineString[posList.size()];
for (int i=0; i<posList.size(); i++) {
for (int i = 0; i < posList.size(); i++) {
final Coordinate[] coordinates = createCoordinates(posList.get(i));
lineStrings[i] = geometryFactory.createLineString(coordinates);
}
geometry = geometryFactory.createMultiLineString(lineStrings);
break;
} case "MULTIPOLYGON": {
}
case "MULTIPOLYGON": {
final List<List<List<List<Object>>>> posList;
posList = (List) properties.get("coordinates");
final Polygon[] polygons = new Polygon[posList.size()];
for (int i=0; i<posList.size(); i++) {
for (int i = 0; i < posList.size(); i++) {
polygons[i] = createPolygon(posList.get(i));
}
geometry = geometryFactory.createMultiPolygon(polygons);
break;
} case "GEOMETRYCOLLECTION": {
final List<Map<String,Object>> list;
}
case "GEOMETRYCOLLECTION": {
final List<Map<String, Object>> list;
list = (List) properties.get("geometries");
final Geometry[] geometries = new Geometry[list.size()];
for (int i=0; i<geometries.length; i++) {
for (int i = 0; i < geometries.length; i++) {
geometries[i] = createGeometry(list.get(i));
}
geometry = geometryFactory.createGeometryCollection(geometries);
break;
} case "ENVELOPE": {
}
case "ENVELOPE": {
final List<List<Object>> posList;
posList = (List) properties.get("coordinates");
final Coordinate[] coords = createCoordinates(posList);
final Envelope envelope = new Envelope(coords[0], coords[1]);
geometry = geometryFactory.toGeometry(envelope);
break;
} default:
}
case "CIRCLE": {
final List posList;
posList = (List) properties.get("coordinates");
final String radius = (String) properties.get("radius");
final Coordinate coordinate = createCoordinate(posList);
geometry = createCircle(coordinate, radius);
break;
}
default:
// check if this is a geo_point
final Object latObj = properties.get("lat");
final Object lonObj = properties.get("lon");
if (latObj != null && lonObj != null) {
final Double lat;
if (latObj instanceof Number) {
lat = ((Number)latObj).doubleValue();
lat = ((Number) latObj).doubleValue();
} else if (latObj instanceof String) {
lat = new Double((String)latObj);
lat = new Double((String) latObj);
} else {
lat = null;
}

final Double lon;
if (lonObj instanceof Number) {
lon = ((Number)lonObj).doubleValue();
lon = ((Number) lonObj).doubleValue();
} else if (lonObj instanceof String) {
lon = new Double((String)lonObj);
lon = new Double((String) lonObj);
} else {
lon = null;
}

if (lat != null && lon != null) {
geometry = geometryFactory.createPoint(new Coordinate(lon,lat));
geometry = geometryFactory.createPoint(new Coordinate(lon, lat));
} else {
geometry = null;
}
Expand All @@ -214,17 +251,17 @@ public Geometry createGeometry(final Map<String, Object> properties) {
private Polygon createPolygon(final List<List<List<Object>>> posList) {
final Coordinate[] shellCoordinates = createCoordinates(posList.get(0));
final LinearRing shell = geometryFactory.createLinearRing(shellCoordinates);
final LinearRing[] holes = new LinearRing[posList.size()-1];
for (int i=1; i<posList.size(); i++) {
final LinearRing[] holes = new LinearRing[posList.size() - 1];
for (int i = 1; i < posList.size(); i++) {
final Coordinate[] coordinates = createCoordinates(posList.get(i));
holes[i-1] = geometryFactory.createLinearRing(coordinates);
holes[i - 1] = geometryFactory.createLinearRing(coordinates);
}
return geometryFactory.createPolygon(shell, holes);
}

private Coordinate[] createCoordinates(final List<List<Object>> posList) {
final Coordinate[] coordinates = new Coordinate[posList.size()];
for (int i=0; i<posList.size(); i++) {
for (int i = 0; i < posList.size(); i++) {
coordinates[i] = createCoordinate(posList.get(i));
}
return coordinates;
Expand All @@ -240,11 +277,12 @@ private Coordinate createCoordinate(final List<Object> posList) {
x = Double.valueOf(posList.get(0).toString());
y = Double.valueOf(posList.get(1).toString());
}
return new Coordinate(x,y);
return new Coordinate(x, y);
}

/**
* Read field from document source.
*
* @param source Source
* @param name Field to extract.
* @return List of values or empty list if not found
Expand All @@ -271,7 +309,7 @@ private void readField(Object entry, List<String> keys, List<Object> values) {
readField(object, keys, values);
}
} else if (!keys.isEmpty() && Map.class.isAssignableFrom(entry.getClass())) {
final Object nextEntry = ((Map<?,?>) entry).get(keys.get(0));
final Object nextEntry = ((Map<?, ?>) entry).get(keys.get(0));
final List<String> newKeys = keys.subList(1, keys.size());
readField(nextEntry, newKeys, values);
} else if (entry != null) {
Expand All @@ -285,7 +323,8 @@ public static boolean isGeoPointFeature(Map<String, Object> map) {
if (map.size() == 2 && map.containsKey("coordinates")) {
try {
result = "geo_point".equals(((Map) map.get("coordinates")).get("type"));
} catch (Exception e) { }
} catch (Exception e) {
}
}
return result;
}
Expand All @@ -311,4 +350,84 @@ public static String urlDecode(String value) {
return value;
}

/**
* Interpolates a JTS polygon from a circle definition. Assumes WGS84 CRS.
*
* @param centreCoord The centre of the circle
* @param radius Consists of a numeric value with a units string appended to
* it.
* @return A polygon that is an interpolated form of a circle
*/
private Geometry createCircle(Coordinate centreCoord, String radius) {

if (centreCoord == null) {
return null;
}

final double radM;
try {
radM = convertToMeters(radius);
}
catch(Exception e) {
return null;
}

// Reject circles with radii below an arbitrary minimum.
if (radM < MIN_CIRCLE_RADIUS_M) {
return null;
}

// Interpolate a circle on the surface of the ellipsoid at an arbitrary
// interval and then ensure that the number of interpolated points are
// within a specified range
final double circumferance = radM * 2.0 * Math.PI;
int numPoints = (int) (circumferance / CIRCLE_INTERPOLATION_INTERVAL);
numPoints = Math.max(MIN_CIRCLE_POINTS, numPoints);
numPoints = Math.min(MAX_CIRCLE_POINTS, numPoints);
final double angularIncrement = 360.0 / numPoints;
geodeticCalculator.setStartingGeographicPoint(centreCoord.x, centreCoord.y);
final Coordinate[] linearRingCoords = new Coordinate[numPoints + 1];
double angle = 0.0;
for (int i = 0; i < numPoints; i++) {
geodeticCalculator.setDirection(angle, radM);
Point2D point2D = geodeticCalculator.getDestinationGeographicPoint();
linearRingCoords[i] = new Coordinate(point2D.getX(), point2D.getY());
angle += angularIncrement;
}
linearRingCoords[numPoints] = linearRingCoords[0];
final LinearRing linearRing = geometryFactory.createLinearRing(linearRingCoords);
return geometryFactory.createPolygon(linearRing);
}

/**
* Converts an Elasticsearch distance string consisting of value and unit
* into metres.
* @param distanceWithUnit String of the form of a decimal number
* concatenated with a unit string as defined in
* {@link FilterToElasticHelper#UNITS_MAP}. If the unit string is missing
* then the number is assumed to be metres.
* @return distance in metres.
* @throws IllegalArgumentException
*/
static final double convertToMeters(String distanceWithUnit) throws IllegalArgumentException {
if (distanceWithUnit == null || distanceWithUnit.isEmpty()) {
throw new IllegalArgumentException("Null of zero length distance string argument");
}
final Matcher matcher = ELASTIC_DISTANCE_PATTERN.matcher(distanceWithUnit);
if (matcher.matches()) {
final double distance = Double.valueOf(matcher.group(1));
final String unit = matcher.group(3);
Double conversion = FilterToElasticHelper.UNITS_MAP.get(unit);
if (conversion == null) {
if (unit != null && ! unit.isEmpty()) {
throw new IllegalArgumentException("Illegal unit: " + unit);
} else {
conversion = new Double(1.0);
}
}
return distance * conversion;
} else {
throw new IllegalArgumentException("Distance string argument has incorrect format");
}
}
}
Loading