/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2009, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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
* Lesser General Public License for more details.
*/
package mil.nga.giat.data.elasticsearch;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.measure.unit.SI;
import static mil.nga.giat.data.elasticsearch.ElasticConstants.GEOMETRY_TYPE;
import static mil.nga.giat.data.elasticsearch.ElasticConstants.MATCH_ALL;
import mil.nga.giat.data.elasticsearch.ElasticAttribute.ElasticGeometryType;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.geometry.jts.JTS;
import org.locationtech.spatial4j.shape.SpatialRelation;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.spatial.BBOX;
import org.opengis.filter.spatial.Beyond;
import org.opengis.filter.spatial.BinarySpatialOperator;
import org.opengis.filter.spatial.Contains;
import org.opengis.filter.spatial.DWithin;
import org.opengis.filter.spatial.Disjoint;
import org.opengis.filter.spatial.DistanceBufferOperator;
import org.opengis.filter.spatial.Intersects;
import org.opengis.filter.spatial.Within;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateFilter;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryComponentFilter;
import com.vividsolutions.jts.geom.Polygon;
class FilterToElasticHelper {
private double lat, lon, distance;
private String key;
private Literal geometry;
private SpatialRelation shapeRelation;
private Map<String,Object> shapeBuilder;
/**
* Conversion factor from common units to meter
*/
protected static final Map<String, Double> UNITS_MAP = new HashMap<String, Double>() {
{
put("kilometers", 1000.0);
put("kilometer", 1000.0);
put("mm", 0.001);
put("millimeter", 0.001);
put("mi", 1609.344);
put("miles", 1609.344);
put("NM", 1852d);
put("feet", 0.3048);
put("ft", 0.3048);
put("in", 0.0254);
}
};
protected static final Envelope WORLD = new Envelope(-180, 180, -90, 90);
FilterToElastic delegate;
public FilterToElasticHelper(FilterToElastic delegate) {
this.delegate = delegate;
}
protected Object visitBinarySpatialOperator(BinarySpatialOperator filter,
PropertyName property, Literal geometry, boolean swapped,
Object extraData) {
if (filter instanceof DistanceBufferOperator) {
visitDistanceSpatialOperator((DistanceBufferOperator) filter,
property, geometry, swapped, extraData);
} else {
visitComparisonSpatialOperator(filter, property, geometry,
swapped, extraData);
}
return extraData;
}
protected Object visitBinarySpatialOperator(BinarySpatialOperator filter, Expression e1,
Expression e2, Object extraData) {
visitBinarySpatialOperator(filter, e1, e2, false, extraData);
return extraData;
}
protected void visitDistanceSpatialOperator(DistanceBufferOperator filter,
PropertyName property, Literal geometry, boolean swapped,
Object extraData) {
property.accept(delegate, extraData);
key = (String) delegate.field;
geometry.accept(delegate, extraData);
final Geometry geo = delegate.currentGeometry;
lat = geo.getCentroid().getY();
lon = geo.getCentroid().getX();
final double inputDistance = filter.getDistance();
final String inputUnits = filter.getDistanceUnits();
distance = Double.valueOf(toMeters(inputDistance, inputUnits));
delegate.queryBuilder = ImmutableMap.of("bool", ImmutableMap.of("must", MATCH_ALL,
"filter", ImmutableMap.of("geo_distance",
ImmutableMap.of("distance", distance+SI.METER.toString(), key, ImmutableList.of(lon,lat)))));
if ((filter instanceof DWithin && swapped)
|| (filter instanceof Beyond && !swapped)) {
delegate.queryBuilder = ImmutableMap.of("bool", ImmutableMap.of("must_not", delegate.queryBuilder));
}
}
private String toMeters(double distance, String unit) {
// only geography uses metric units
if(isCurrentGeography()) {
Double conversion = UNITS_MAP.get(unit);
if(conversion != null) {
return String.valueOf(distance * conversion);
}
}
// in case unknown unit or not geography, use as-is
return String.valueOf(distance);
}
void visitComparisonSpatialOperator(BinarySpatialOperator filter,
PropertyName property, Literal geometry, boolean swapped, Object extraData) {
// if geography case, sanitize geometry first
this.geometry = geometry;
if(isCurrentGeography()) {
this.geometry = clipToWorld(geometry);
}
visitBinarySpatialOperator(filter, (Expression)property, (Expression)this.geometry, swapped, extraData);
// if geography case, sanitize geometry first
if(isCurrentGeography()) {
if(isWorld(this.geometry)) {
// nothing to filter in this case
delegate.queryBuilder = MATCH_ALL;
return;
} else if(isEmpty(this.geometry)) {
if(!(filter instanceof Disjoint)) {
delegate.queryBuilder = ImmutableMap.of("bool", ImmutableMap.of("must_not", MATCH_ALL));
} else {
delegate.queryBuilder = MATCH_ALL;
}
return;
}
}
visitBinarySpatialOperator(filter, (Expression)property, (Expression)this.geometry, swapped, extraData);
}
void visitBinarySpatialOperator(BinarySpatialOperator filter, Expression e1, Expression e2,
boolean swapped, Object extraData) {
AttributeDescriptor attType;
attType = (AttributeDescriptor)e1.evaluate(delegate.featureType);
ElasticGeometryType geometryType;
geometryType = (ElasticGeometryType) attType.getUserData().get(GEOMETRY_TYPE);
if (geometryType == ElasticGeometryType.GEO_POINT) {
visitGeoPointBinarySpatialOperator(filter, e1, e2, swapped, extraData);
} else {
visitGeoShapeBinarySpatialOperator(filter, e1, e2, swapped, extraData);
}
}
void visitGeoShapeBinarySpatialOperator(BinarySpatialOperator filter, Expression e1, Expression e2,
boolean swapped, Object extraData) {
if (filter instanceof Disjoint) {
shapeRelation = SpatialRelation.DISJOINT;
} else if ((!swapped && filter instanceof Within) || (swapped && filter instanceof Contains)) {
shapeRelation = SpatialRelation.WITHIN;
} else if (filter instanceof Intersects || filter instanceof BBOX) {
shapeRelation = SpatialRelation.INTERSECTS;
} else {
FilterToElastic.LOGGER.fine(filter.getClass().getSimpleName()
+ " is unsupported for geo_shape types");
shapeRelation = null;
delegate.fullySupported = false;
}
if (shapeRelation != null) {
e1.accept(delegate, extraData);
key = (String) delegate.field;
e2.accept(delegate, extraData);
shapeBuilder = delegate.currentShapeBuilder;
}
if (shapeRelation != null && shapeBuilder != null) {
delegate.queryBuilder = ImmutableMap.of("bool", ImmutableMap.of("must", MATCH_ALL,
"filter", ImmutableMap.of("geo_shape",
ImmutableMap.of(key, ImmutableMap.of("shape", shapeBuilder, "relation", shapeRelation)))));
} else {
delegate.queryBuilder = MATCH_ALL;
}
}
void visitGeoPointBinarySpatialOperator(BinarySpatialOperator filter, Expression e1, Expression e2,
boolean swapped, Object extraData) {
e1.accept(delegate, extraData);
key = (String) delegate.field;
e2.accept(delegate, extraData);
final Geometry geometry = delegate.currentGeometry;
if (geometry instanceof Polygon &&
((!swapped && filter instanceof Within)
|| (swapped && filter instanceof Contains)
|| filter instanceof Intersects)) {
final Polygon polygon = (Polygon) geometry;
final List<List<Double>> points = new ArrayList<>();
for (final Coordinate coordinate : polygon.getCoordinates()) {
points.add(ImmutableList.of(coordinate.x, coordinate.y));
}
delegate.queryBuilder = ImmutableMap.of("bool", ImmutableMap.of("must", MATCH_ALL,
"filter", ImmutableMap.of("geo_polygon",
ImmutableMap.of(key, ImmutableMap.of("points", points)))));
} else if (filter instanceof BBOX) {
final Envelope envelope = geometry.getEnvelopeInternal();
final double minY = clipLat(envelope.getMinY());
final double maxY = clipLat(envelope.getMaxY());
final double minX, maxX;
if (envelope.getWidth() < 360) {
minX = clipLon(envelope.getMinX());
maxX = clipLon(envelope.getMaxX());
} else {
minX = -180;
maxX = 180;
}
delegate.queryBuilder = ImmutableMap.of("bool", ImmutableMap.of("must", MATCH_ALL,
"filter", ImmutableMap.of("geo_bounding_box", ImmutableMap.of(key,
ImmutableMap.of("top_left", ImmutableList.of(minX, maxY),
"bottom_right", ImmutableList.of(maxX, minY))))));
} else {
FilterToElastic.LOGGER.fine(filter.getClass().getSimpleName()
+ " is unsupported for geo_point types");
delegate.fullySupported = false;
delegate.queryBuilder = MATCH_ALL;
}
}
boolean isCurrentGeography() {
return true;
}
protected Literal clipToWorld(Literal geometry) {
if(geometry != null) {
Geometry g = geometry.evaluate(null, Geometry.class);
if(g != null) {
g.apply(new GeometryComponentFilter() {
@Override
public void filter(Geometry geom) {
geom.apply(new CoordinateFilter() {
@Override
public void filter(Coordinate coord) {
coord.setCoordinate(new Coordinate(clipLon(coord.x),clipLat(coord.y)));
}
});
}
});
geometry = CommonFactoryFinder.getFilterFactory(null).literal(g);
}
}
return geometry;
}
protected double clipLon(double lon) {
double x = Math.signum(lon)*(Math.abs(lon)%360);
return x = x>180 ? x-360 : (x<-180 ? x+360 : x);
}
protected double clipLat(double lat) {
return Math.min(90, Math.max(-90, lat));
}
/**
* Returns true if the geometry covers the entire world
* @param geometry
* @return
*/
protected boolean isWorld(Literal geometry) {
boolean result = false;
if(geometry != null) {
Geometry g = geometry.evaluate(null, Geometry.class);
if(g != null) {
result = JTS.toGeometry(WORLD).equalsTopo(g.union());
}
}
return result;
}
/**
* Returns true if the geometry is fully empty
* @param geometry
* @return
*/
protected boolean isEmpty(Literal geometry) {
boolean result = false;
if(geometry != null) {
Geometry g = geometry.evaluate(null, Geometry.class);
result = g == null || g.isEmpty();
}
return result;
}
}