package mil.nga.giat.geowave.adapter.vector.plugin; import mil.nga.giat.geowave.core.geotime.GeometryUtils; import mil.nga.giat.geowave.core.geotime.store.filter.SpatialQueryFilter.CompareOperation; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.geotools.filter.visitor.NullFilterVisitor; import org.geotools.geometry.jts.ReferencedEnvelope; import org.opengis.filter.And; import org.opengis.filter.ExcludeFilter; import org.opengis.filter.Filter; import org.opengis.filter.Id; import org.opengis.filter.IncludeFilter; import org.opengis.filter.Not; import org.opengis.filter.Or; import org.opengis.filter.PropertyIsBetween; import org.opengis.filter.PropertyIsEqualTo; import org.opengis.filter.PropertyIsGreaterThan; import org.opengis.filter.PropertyIsGreaterThanOrEqualTo; import org.opengis.filter.PropertyIsLessThan; import org.opengis.filter.PropertyIsLessThanOrEqualTo; import org.opengis.filter.PropertyIsLike; import org.opengis.filter.PropertyIsNotEqualTo; import org.opengis.filter.PropertyIsNull; import org.opengis.filter.expression.Add; import org.opengis.filter.expression.Divide; import org.opengis.filter.expression.Function; import org.opengis.filter.expression.Literal; import org.opengis.filter.expression.Multiply; import org.opengis.filter.expression.NilExpression; import org.opengis.filter.expression.PropertyName; import org.opengis.filter.expression.Subtract; import org.opengis.filter.spatial.BBOX; import org.opengis.filter.spatial.Beyond; import org.opengis.filter.spatial.Contains; import org.opengis.filter.spatial.Crosses; import org.opengis.filter.spatial.DWithin; import org.opengis.filter.spatial.Disjoint; import org.opengis.filter.spatial.Equals; import org.opengis.filter.spatial.Intersects; import org.opengis.filter.spatial.Overlaps; import org.opengis.filter.spatial.Touches; import org.opengis.filter.spatial.Within; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; /** * This class is used to exact single query geometry and its associated * predicate from a CQL expression. There are three possible outcomes based on * the extracted results. 1) If CQL expression is simple then we are able to * extract query geometry and predicate successfully. 2) If CQL expression * combines multiple dissimilar geometric relationships (i.e. * "BBOX(geom,...) AND TOUCHES(geom,...)") then we wont be able combine that * into a single query geometry and predicate. In which case, we will only * return query geometry for the purpose of creating linear constraints and * predicate value will be null. However, we are able to combine multiple * geometric relationships into one query/predicate if their predicates are same * (i.e. "INTERSECTS(geom,...) AND INTERSECTS(geom,...)") 3) In some case, we * won't be able to extract query geometry and predicate at all. In that case, * we simply return null. This occurs if CQL expression doesn't contain any * geometric constraints or CQL expression has non-inclusive filter (i.e. NOT or * DISJOINT(...)). * */ public class ExtractGeometryFilterVisitor extends NullFilterVisitor { public static final NullFilterVisitor GEOMETRY_VISITOR = new ExtractGeometryFilterVisitor( GeoWaveGTDataStore.DEFAULT_CRS); private static Logger LOGGER = LoggerFactory.getLogger(ExtractGeometryFilterVisitor.class); private final CoordinateReferenceSystem crs; /** * This FilterVisitor is stateless - use * ExtractGeometryFilterVisitor.BOUNDS_VISITOR. You may also subclass in * order to reuse this functionality in your own FilterVisitor * implementation. */ public ExtractGeometryFilterVisitor( final CoordinateReferenceSystem crs ) { this.crs = crs; } /** * * @param filter * @param crs * @return null if empty constraint (infinite not supported) */ public static ExtractGeometryFilterVisitorResult getConstraints( final Filter filter, CoordinateReferenceSystem crs ) { final ExtractGeometryFilterVisitorResult geoAndCompareOpData = (ExtractGeometryFilterVisitorResult) filter .accept( new ExtractGeometryFilterVisitor( crs), null); Geometry geo = geoAndCompareOpData.getGeometry(); // empty or infinite geometry simply return null as we can't create // linear constraints from if ((geo == null) || geo.isEmpty()) { return null; } final double area = geo.getArea(); if (Double.isInfinite(area) || Double.isNaN(area)) { return null; } return geoAndCompareOpData; } /** * Produce an ReferencedEnvelope from the provided data parameter. * * @param data * @return ReferencedEnvelope */ private Geometry bbox( final Object data ) { try { if (data == null) { return null; } else if (data instanceof ReferencedEnvelope) { return new GeometryFactory().toGeometry(((ReferencedEnvelope) data).transform( crs, true)); } else if (data instanceof Envelope) { return new GeometryFactory().toGeometry((Envelope) data); } else if (data instanceof CoordinateReferenceSystem) { return new GeometryFactory().toGeometry(new ReferencedEnvelope( (CoordinateReferenceSystem) data).transform( crs, true)); } } catch (TransformException | FactoryException e) { LOGGER.warn( "Unable to transform geometry", e); return null; } throw new ClassCastException( "Could not cast data to ReferencedEnvelope"); } @Override public Object visit( final ExcludeFilter filter, final Object data ) { return new ExtractGeometryFilterVisitorResult( null, null); } @Override public Object visit( final IncludeFilter filter, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } private Geometry infinity() { return GeometryUtils.infinity(); } @SuppressWarnings("deprecation") @Override public Object visit( final BBOX filter, final Object data ) { final Geometry bbox = bbox(data); // consider doing reprojection here into data CRS? final Envelope bounds = new Envelope( filter.getMinX(), filter.getMaxX(), filter.getMinY(), filter.getMaxY()); if (bbox != null) { return bbox.union(new GeometryFactory().toGeometry(bounds)); } else { return new ExtractGeometryFilterVisitorResult( bbox(bounds), CompareOperation.INTERSECTS); } } /** * Please note we are only visiting literals involved in spatial operations. * * @param literal * , hopefully a Geometry or Envelope * @param data * Incoming BoundingBox (or Envelope or CRS) * * @return ReferencedEnvelope updated to reflect literal */ @Override public Object visit( final Literal expression, final Object data ) { final Object value = expression.getValue(); if (value instanceof Geometry) { final Geometry geometry = (Geometry) value; return geometry; } else { LOGGER.info("LiteralExpression ignored!"); } return bbox(data); } @Override public Object visit( final And filter, final Object data ) { ExtractGeometryFilterVisitorResult finalResult = null; for (final Filter f : filter.getChildren()) { final Object obj = f.accept( this, data); if ((obj != null) && (obj instanceof ExtractGeometryFilterVisitorResult)) { final ExtractGeometryFilterVisitorResult currentResult = (ExtractGeometryFilterVisitorResult) obj; final Geometry currentGeom = currentResult.getGeometry(); final double currentArea = currentGeom.getArea(); if (finalResult == null) { finalResult = currentResult; } else if (!Double.isInfinite(currentArea) && !Double.isNaN(currentArea)) { // if predicates match then we can combine the geometry as // well as predicate if (currentResult.matchPredicate(finalResult)) { finalResult = new ExtractGeometryFilterVisitorResult( finalResult.getGeometry().intersection( currentGeom), currentResult.getCompareOp()); } else { // if predicate doesn't match then still combine // geometry but set predicate to null finalResult = new ExtractGeometryFilterVisitorResult( finalResult.getGeometry().intersection( currentGeom), null); } } else { finalResult = new ExtractGeometryFilterVisitorResult( finalResult.getGeometry(), null); } } } return finalResult; } @Override public Object visit( final Not filter, final Object data ) { // no matter what we have to return an infinite envelope // rationale // !(finite envelope) -> an unbounded area -> infinite // !(non spatial filter) -> infinite (no spatial concern) // !(infinite) -> ... infinite, as the first infinite could be the // result // of !(finite envelope) return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final Or filter, final Object data ) { ExtractGeometryFilterVisitorResult finalResult = new ExtractGeometryFilterVisitorResult( new GeometryFactory().toGeometry(new Envelope()), null); for (final Filter f : filter.getChildren()) { final Object obj = f.accept( this, data); if ((obj != null) && (obj instanceof ExtractGeometryFilterVisitorResult)) { final ExtractGeometryFilterVisitorResult currentResult = (ExtractGeometryFilterVisitorResult) obj; final Geometry currentGeom = currentResult.getGeometry(); final double currentArea = currentGeom.getArea(); if (finalResult.getGeometry().isEmpty()) { finalResult = currentResult; } else if (!Double.isInfinite(currentArea) && !Double.isNaN(currentArea)) { if (currentResult.matchPredicate(finalResult)) { finalResult = new ExtractGeometryFilterVisitorResult( finalResult.getGeometry().union( currentGeom), currentResult.getCompareOp()); } else { finalResult = new ExtractGeometryFilterVisitorResult( finalResult.getGeometry().union( currentGeom), null); } } else { finalResult = new ExtractGeometryFilterVisitorResult( finalResult.getGeometry(), null); } } } if (finalResult.getGeometry().isEmpty()) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } return finalResult; } @Override public Object visit( final Beyond filter, final Object data ) { // beyond a certain distance from a finite object, no way to limit it return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final Contains filter, Object data ) { data = filter.getExpression1().accept( this, data); data = filter.getExpression2().accept( this, data); // since predicate is defined relative to the query geometry we are // using WITHIN // which is converse of CONTAINS operator // CQL Expression "CONTAINS(geo, QueryGeometry)" is equivalent to // QueryGeometry.WITHIN(geo) return new ExtractGeometryFilterVisitorResult( (Geometry) data, CompareOperation.WITHIN); } @Override public Object visit( final Crosses filter, Object data ) { data = filter.getExpression1().accept( this, data); data = filter.getExpression2().accept( this, data); return new ExtractGeometryFilterVisitorResult( (Geometry) data, CompareOperation.CROSSES); } @Override public Object visit( final Disjoint filter, Object data ) { // disjoint does not define a rectangle, but a hole in the // Cartesian plane, no way to limit it return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final DWithin filter, final Object data ) { final Geometry bbox = bbox(data); // we have to take the reference geometry bbox and // expand it by the distance. // We ignore the unit of measure for the moment Literal geometry = null; if ((filter.getExpression1() instanceof PropertyName) && (filter.getExpression2() instanceof Literal)) { geometry = (Literal) filter.getExpression2(); } if ((filter.getExpression2() instanceof PropertyName) && (filter.getExpression1() instanceof Literal)) { geometry = (Literal) filter.getExpression1(); } // we cannot desume a bbox from this filter if (geometry == null) { return null; } Geometry geom = geometry.evaluate( null, Geometry.class); if (geom == null) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } Pair<Geometry, Double> geometryAndDegrees; try { geometryAndDegrees = mil.nga.giat.geowave.adapter.vector.utils.GeometryUtils.buffer( crs, geom, filter.getDistanceUnits(), filter.getDistance()); } catch (TransformException e) { LOGGER.error( "Cannot transform geometry to CRS", e); geometryAndDegrees = Pair.of( geom, filter.getDistance()); } if (bbox != null) { return geometryAndDegrees.getLeft().union( bbox); } else { return new ExtractGeometryFilterVisitorResult( geometryAndDegrees.getLeft(), CompareOperation.INTERSECTS); } } @Override public Object visit( final Equals filter, Object data ) { data = filter.getExpression1().accept( this, data); data = filter.getExpression2().accept( this, data); return new ExtractGeometryFilterVisitorResult( (Geometry) data, CompareOperation.EQUALS); } @Override public Object visit( final Intersects filter, Object data ) { data = filter.getExpression1().accept( this, data); data = filter.getExpression2().accept( this, data); return new ExtractGeometryFilterVisitorResult( (Geometry) data, CompareOperation.INTERSECTS); } @Override public Object visit( final Overlaps filter, Object data ) { data = filter.getExpression1().accept( this, data); data = filter.getExpression2().accept( this, data); return new ExtractGeometryFilterVisitorResult( (Geometry) data, CompareOperation.OVERLAPS); } @Override public Object visit( final Touches filter, Object data ) { data = filter.getExpression1().accept( this, data); data = filter.getExpression2().accept( this, data); return new ExtractGeometryFilterVisitorResult( (Geometry) data, CompareOperation.TOUCHES); } @Override public Object visit( final Within filter, Object data ) { data = filter.getExpression1().accept( this, data); data = filter.getExpression2().accept( this, data); // since predicate is defined relative to the query geometry we are // using CONTAIN // which is converse of WITHIN operator // CQL Expression "WITHIN(geo, QueryGeometry)" is equivalent to // QueryGeometry.CONTAINS(geo) return new ExtractGeometryFilterVisitorResult( (Geometry) data, CompareOperation.CONTAINS); } @Override public Object visit( final Add expression, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final Divide expression, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final Function expression, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final Id filter, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final Multiply expression, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final NilExpression expression, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final PropertyIsBetween filter, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final PropertyIsEqualTo filter, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final PropertyIsGreaterThan filter, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final PropertyIsGreaterThanOrEqualTo filter, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final PropertyIsLessThan filter, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final PropertyIsLessThanOrEqualTo filter, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final PropertyIsLike filter, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final PropertyIsNotEqualTo filter, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final PropertyIsNull filter, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visit( final PropertyName expression, final Object data ) { return new ExtractGeometryFilterVisitorResult( null, null); } @Override public Object visit( final Subtract expression, final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } @Override public Object visitNullFilter( final Object data ) { return new ExtractGeometryFilterVisitorResult( infinity(), null); } }