package mil.nga.giat.geowave.core.geotime.store.filter; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import mil.nga.giat.geowave.core.geotime.GeometryUtils; import mil.nga.giat.geowave.core.geotime.store.dimension.GeometryWrapper; import mil.nga.giat.geowave.core.index.ByteArrayId; import mil.nga.giat.geowave.core.index.sfc.data.BasicNumericDataset; import mil.nga.giat.geowave.core.index.sfc.data.MultiDimensionalNumericData; import mil.nga.giat.geowave.core.index.sfc.data.NumericData; import mil.nga.giat.geowave.core.store.data.IndexedPersistenceEncoding; import mil.nga.giat.geowave.core.store.dimension.NumericDimensionField; import mil.nga.giat.geowave.core.store.filter.BasicQueryFilter; import mil.nga.giat.geowave.core.store.filter.GenericTypeResolver; import mil.nga.giat.geowave.core.store.index.CommonIndexModel; import com.google.common.collect.Interner; import com.google.common.collect.Interners; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.prep.PreparedGeometry; import com.vividsolutions.jts.geom.prep.PreparedGeometryFactory; /** * This filter can perform fine-grained acceptance testing (intersection test * with a query geometry) with JTS geometry * */ public class SpatialQueryFilter extends BasicQueryFilter { private static final Interner<GeometryImage> geometryImageInterner = Interners.newWeakInterner(); public static final PreparedGeometryFactory FACTORY = new PreparedGeometryFactory(); private GeometryImage preparedGeometryImage; protected interface SpatialQueryCompareOp { public boolean compare( final Geometry dataGeometry, final PreparedGeometry constraintGeometry ); public BasicQueryCompareOperation getBaseCompareOp(); } public enum CompareOperation implements SpatialQueryCompareOp { CONTAINS { @Override public boolean compare( final Geometry dataGeometry, final PreparedGeometry constraintGeometry ) { return constraintGeometry.contains(dataGeometry); } @Override public BasicQueryCompareOperation getBaseCompareOp() { return BasicQueryCompareOperation.CONTAINS; } }, OVERLAPS { @Override public boolean compare( final Geometry dataGeometry, final PreparedGeometry constraintGeometry ) { return constraintGeometry.overlaps(dataGeometry); } @Override public BasicQueryCompareOperation getBaseCompareOp() { return BasicQueryCompareOperation.OVERLAPS; } }, INTERSECTS { @Override public boolean compare( final Geometry dataGeometry, final PreparedGeometry constraintGeometry ) { return constraintGeometry.intersects(dataGeometry); } @Override public BasicQueryCompareOperation getBaseCompareOp() { return BasicQueryCompareOperation.INTERSECTS; } }, TOUCHES { @Override public boolean compare( final Geometry dataGeometry, final PreparedGeometry constraintGeometry ) { return constraintGeometry.touches(dataGeometry); } @Override public BasicQueryCompareOperation getBaseCompareOp() { return BasicQueryCompareOperation.TOUCHES; } }, WITHIN { @Override public boolean compare( final Geometry dataGeometry, final PreparedGeometry constraintGeometry ) { return constraintGeometry.within(dataGeometry); } @Override public BasicQueryCompareOperation getBaseCompareOp() { return BasicQueryCompareOperation.WITHIN; } }, DISJOINT { @Override public boolean compare( final Geometry dataGeometry, final PreparedGeometry constraintGeometry ) { return constraintGeometry.disjoint(dataGeometry); } @Override public BasicQueryCompareOperation getBaseCompareOp() { return BasicQueryCompareOperation.DISJOINT; } }, CROSSES { @Override public boolean compare( final Geometry dataGeometry, final PreparedGeometry constraintGeometry ) { return constraintGeometry.crosses(dataGeometry); } @Override public BasicQueryCompareOperation getBaseCompareOp() { return BasicQueryCompareOperation.CROSSES; } }, EQUALS { @Override public boolean compare( final Geometry dataGeometry, final PreparedGeometry constraintGeometry ) { // This method is same as Geometry.equalsTopo which is // computationally expensive. // See equalsExact for quick structural equality return constraintGeometry.getGeometry().equals( dataGeometry); } @Override public BasicQueryCompareOperation getBaseCompareOp() { return BasicQueryCompareOperation.EQUALS; } } }; private CompareOperation compareOperation = CompareOperation.INTERSECTS; private Set<ByteArrayId> geometryFieldIds; protected SpatialQueryFilter() { super(); } public SpatialQueryFilter( final MultiDimensionalNumericData query, final NumericDimensionField<?>[] orderedConstrainedDimensionDefinitions, final NumericDimensionField<?>[] unconstrainedDimensionDefinitions, final Geometry queryGeometry, final CompareOperation compareOp, final BasicQueryCompareOperation nonSpatialCompareOp ) { this( stripGeometry( query, orderedConstrainedDimensionDefinitions, unconstrainedDimensionDefinitions), queryGeometry, compareOp, nonSpatialCompareOp); } private SpatialQueryFilter( final StrippedGeometry strippedGeometry, final Geometry queryGeometry, final CompareOperation compareOp, final BasicQueryCompareOperation nonSpatialCompareOp ) { super( strippedGeometry.strippedQuery, strippedGeometry.strippedDimensionDefinitions, nonSpatialCompareOp); preparedGeometryImage = new GeometryImage( FACTORY.create(queryGeometry)); geometryFieldIds = strippedGeometry.geometryFieldIds; this.compareOperation = compareOp; } private static class StrippedGeometry { private final MultiDimensionalNumericData strippedQuery; private final NumericDimensionField<?>[] strippedDimensionDefinitions; private final Set<ByteArrayId> geometryFieldIds; public StrippedGeometry( final MultiDimensionalNumericData strippedQuery, final NumericDimensionField<?>[] strippedDimensionDefinitions, final Set<ByteArrayId> geometryFieldIds ) { this.strippedQuery = strippedQuery; this.strippedDimensionDefinitions = strippedDimensionDefinitions; this.geometryFieldIds = geometryFieldIds; } } private static StrippedGeometry stripGeometry( final MultiDimensionalNumericData query, final NumericDimensionField<?>[] orderedConstrainedDimensionDefinitions, final NumericDimensionField<?>[] unconstrainedDimensionDefinitions ) { final Set<ByteArrayId> geometryFieldIds = new HashSet<ByteArrayId>(); final List<NumericData> numericDataPerDimension = new ArrayList<NumericData>(); final List<NumericDimensionField<?>> fields = new ArrayList<NumericDimensionField<?>>(); final NumericData[] data = query.getDataPerDimension(); for (int d = 0; d < orderedConstrainedDimensionDefinitions.length; d++) { // if the type on the generic is assignable to geometry then save // the field ID for later filtering if (isSpatial(orderedConstrainedDimensionDefinitions[d])) { geometryFieldIds.add(orderedConstrainedDimensionDefinitions[d].getFieldId()); } else { numericDataPerDimension.add(data[d]); fields.add(orderedConstrainedDimensionDefinitions[d]); } } // we need to also add all geometry field IDs even if it is // unconstrained to be able to apply a geometry intersection (understand // that the bbox for a geometry can imply a full range based on its // envelope but the polygon may still need to be intersected with // results) for (int d = 0; d < unconstrainedDimensionDefinitions.length; d++) { if (isSpatial(unconstrainedDimensionDefinitions[d])) { geometryFieldIds.add(unconstrainedDimensionDefinitions[d].getFieldId()); } } return new StrippedGeometry( new BasicNumericDataset( numericDataPerDimension.toArray(new NumericData[numericDataPerDimension.size()])), fields.toArray(new NumericDimensionField<?>[fields.size()]), geometryFieldIds); } public static boolean isSpatial( final NumericDimensionField<?> d ) { final Class<?> commonIndexType = GenericTypeResolver.resolveTypeArgument( d.getClass(), NumericDimensionField.class); return GeometryWrapper.class.isAssignableFrom(commonIndexType); } @Override public boolean accept( final CommonIndexModel indexModel, final IndexedPersistenceEncoding<?> persistenceEncoding ) { if (preparedGeometryImage == null) { return true; } // we can actually get the geometry for the data and test the // intersection of the query geometry with that boolean geometryPasses = false; for (final ByteArrayId fieldId : geometryFieldIds) { final Object geomObj = persistenceEncoding.getCommonData().getValue( fieldId); if ((geomObj != null) && (geomObj instanceof GeometryWrapper)) { final GeometryWrapper geom = (GeometryWrapper) geomObj; if (geometryPasses(geom.getGeometry())) { geometryPasses = true; break; } } } if (!geometryPasses) { return false; } if (isSpatialOnly()) {// if this is only a spatial index, return // true return true; } // otherwise, if the geometry passes, and there are other dimensions, // check the other dimensions return super.accept( indexModel, persistenceEncoding); } private boolean geometryPasses( final Geometry dataGeometry ) { if (dataGeometry == null) { return false; } if (preparedGeometryImage != null) { return compareOperation.compare( dataGeometry, preparedGeometryImage.preparedGeometry); } return false; } protected boolean isSpatialOnly() { return (dimensionFields == null) || (dimensionFields.length == 0); } @Override public byte[] toBinary() { final byte[] geometryBinary = preparedGeometryImage.geometryBinary; int geometryFieldIdByteSize = 4; for (final ByteArrayId id : geometryFieldIds) { geometryFieldIdByteSize += (4 + id.getBytes().length); } final ByteBuffer geometryFieldIdBuffer = ByteBuffer.allocate(geometryFieldIdByteSize); geometryFieldIdBuffer.putInt(geometryFieldIds.size()); for (final ByteArrayId id : geometryFieldIds) { geometryFieldIdBuffer.putInt(id.getBytes().length); geometryFieldIdBuffer.put(id.getBytes()); } final byte[] theRest = super.toBinary(); final ByteBuffer buf = ByteBuffer.allocate(12 + geometryBinary.length + geometryFieldIdByteSize + theRest.length); buf.putInt(compareOperation.ordinal()); buf.putInt(geometryBinary.length); buf.putInt(geometryFieldIdByteSize); buf.put(geometryBinary); buf.put(geometryFieldIdBuffer.array()); buf.put(theRest); return buf.array(); } @Override public void fromBinary( final byte[] bytes ) { final ByteBuffer buf = ByteBuffer.wrap(bytes); compareOperation = CompareOperation.values()[buf.getInt()]; final byte[] geometryBinary = new byte[buf.getInt()]; final byte[] theRest = new byte[bytes.length - geometryBinary.length - buf.getInt() - 12]; buf.get(geometryBinary); final int fieldIdSize = buf.getInt(); geometryFieldIds = new HashSet<ByteArrayId>( fieldIdSize); for (int i = 0; i < fieldIdSize; i++) { final byte[] fieldId = new byte[buf.getInt()]; buf.get(fieldId); geometryFieldIds.add(new ByteArrayId( fieldId)); } buf.get(theRest); preparedGeometryImage = geometryImageInterner.intern(new GeometryImage( geometryBinary)); // build the the PreparedGeometry and underling Geometry if not // reconstituted yet; most likely occurs if this thread constructed the // image. preparedGeometryImage.init(); super.fromBinary(theRest); } /** * This class is used for interning a PreparedGeometry. Prepared geometries * cannot be interned since they do not extend Object.hashCode(). * * Interning a geometry assumes a geometry is already constructed on the * heap at the time interning begins. The byte image of geometry provides a * more efficient component to hash and associate with a single image of the * geometry. * * The approach of interning the Geometry prior to construction of a * PreparedGeometry lead to excessive memory use. Thus, this class is * constructed to hold the prepared geometry and prevent reconstruction of * the underlying geometry from a byte array if the Geometry has been * interned. * * Using this approach increased performance of a large query unit test by * 40% and reduced heap memory consumption by roughly 50%. * */ public static class GeometryImage { byte[] geometryBinary; PreparedGeometry preparedGeometry = null; public GeometryImage( final PreparedGeometry preparedGeometry ) { super(); this.preparedGeometry = preparedGeometry; geometryBinary = GeometryUtils.geometryToBinary(preparedGeometry.getGeometry()); } public GeometryImage( final byte[] geometryBinary ) { super(); this.geometryBinary = geometryBinary; } public synchronized void init() { if (preparedGeometry == null) { preparedGeometry = FACTORY.create(GeometryUtils.geometryFromBinary(geometryBinary)); } } public PreparedGeometry getGeometry() { return preparedGeometry; } @Override public int hashCode() { final int prime = 31; int result = 1; result = (prime * result) + Arrays.hashCode(geometryBinary); return result; } @Override public boolean equals( final Object obj ) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final GeometryImage other = (GeometryImage) obj; if (!Arrays.equals( geometryBinary, other.geometryBinary)) { return false; } return true; } } }