package mil.nga.giat.geowave.adapter.vector.plugin; import java.io.IOException; import java.util.Collection; import java.util.Iterator; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.geotools.data.DataUtilities; import org.geotools.data.FeatureReader; import org.geotools.data.Query; import org.geotools.data.store.DataFeatureCollection; import org.geotools.feature.FeatureIterator; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.feature.visitor.MaxVisitor; import org.geotools.feature.visitor.MinVisitor; import org.geotools.filter.spatial.BBOXImpl; import org.geotools.geometry.jts.ReferencedEnvelope; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.filter.Filter; import org.opengis.geometry.BoundingBox; import org.opengis.referencing.FactoryException; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import mil.nga.giat.geowave.adapter.vector.render.DistributedRenderOptions; import mil.nga.giat.geowave.adapter.vector.render.DistributedRenderResult; import mil.nga.giat.geowave.adapter.vector.stats.FeatureBoundingBoxStatistics; import mil.nga.giat.geowave.adapter.vector.stats.FeatureNumericRangeStatistics; import mil.nga.giat.geowave.adapter.vector.stats.FeatureTimeRangeStatistics; import mil.nga.giat.geowave.core.geotime.store.query.TemporalConstraintsSet; import mil.nga.giat.geowave.core.geotime.store.statistics.BoundingBoxDataStatistics; import mil.nga.giat.geowave.core.index.ByteArrayId; import mil.nga.giat.geowave.core.store.CloseableIterator; import mil.nga.giat.geowave.core.store.adapter.statistics.CountDataStatistics; import mil.nga.giat.geowave.core.store.adapter.statistics.DataStatistics; /** * This class is a helper for the GeoWave GeoTools data store. It represents a * collection of feature data by encapsulating a GeoWave reader and a query * object in order to open the appropriate cursor to iterate over data. It uses * Keys within the Query hints to determine whether to perform special purpose * queries such as decimation or distributed rendering. * */ public class GeoWaveFeatureCollection extends DataFeatureCollection { private final static Logger LOGGER = LoggerFactory.getLogger(GeoWaveFeatureCollection.class); private final GeoWaveFeatureReader reader; private CloseableIterator<SimpleFeature> featureCursor; private final Query query; private static SimpleFeatureType distributedRenderFeatureType; public GeoWaveFeatureCollection( final GeoWaveFeatureReader reader, final Query query ) { this.reader = reader; this.query = validateQuery( GeoWaveFeatureCollection.getSchema( reader, query).getTypeName(), query); } @Override public int getCount() { if (query.getFilter().equals( Filter.INCLUDE)) { // GEOWAVE-60 optimization final Map<ByteArrayId, DataStatistics<SimpleFeature>> statsMap = reader .getTransaction() .getDataStatistics(); if (statsMap.containsKey(CountDataStatistics.STATS_TYPE)) { final CountDataStatistics stats = (CountDataStatistics) statsMap.get(CountDataStatistics.STATS_TYPE); if ((stats != null) && stats.isSet()) { return (int) stats.getCount(); } } } else if (query.getFilter().equals( Filter.EXCLUDE)) { return 0; } QueryConstraints constraints; try { constraints = getQueryConstraints(); return (int) reader.getCountInternal( constraints.jtsBounds, constraints.timeBounds, getFilter(query), constraints.limit); } catch (TransformException | FactoryException e) { LOGGER.warn( "Unable to transform geometry, can't get count", e); } // fallback return 0; } @Override public ReferencedEnvelope getBounds() { double minx = Double.MAX_VALUE, maxx = -Double.MAX_VALUE, miny = Double.MAX_VALUE, maxy = -Double.MAX_VALUE; try { // GEOWAVE-60 optimization final Map<ByteArrayId, DataStatistics<SimpleFeature>> statsMap = reader .getTransaction() .getDataStatistics(); final ByteArrayId statId = FeatureBoundingBoxStatistics.composeId(reader .getFeatureType() .getGeometryDescriptor() .getLocalName()); if (statsMap.containsKey(statId)) { final BoundingBoxDataStatistics<SimpleFeature> stats = (BoundingBoxDataStatistics<SimpleFeature>) statsMap .get(statId); return new ReferencedEnvelope( stats.getMinX(), stats.getMaxX(), stats.getMinY(), stats.getMaxY(), GeoWaveGTDataStore.DEFAULT_CRS); } final Iterator<SimpleFeature> iterator = openIterator(); if (!iterator.hasNext()) { return null; } while (iterator.hasNext()) { final BoundingBox bbox = iterator.next().getBounds(); minx = Math.min( bbox.getMinX(), minx); maxx = Math.max( bbox.getMaxX(), maxx); miny = Math.min( bbox.getMinY(), miny); maxy = Math.max( bbox.getMaxY(), maxy); } close(iterator); } catch (final Exception e) { LOGGER.warn( "Error calculating bounds", e); return new ReferencedEnvelope( -180, 180, -90, 90, GeoWaveGTDataStore.DEFAULT_CRS); } return new ReferencedEnvelope( minx, maxx, miny, maxy, GeoWaveGTDataStore.DEFAULT_CRS); } @Override public SimpleFeatureType getSchema() { if (isDistributedRenderQuery()) { return getDistributedRenderFeatureType(); } return reader.getFeatureType(); } public static synchronized SimpleFeatureType getDistributedRenderFeatureType() { if (distributedRenderFeatureType == null) { distributedRenderFeatureType = createDistributedRenderFeatureType(); } return distributedRenderFeatureType; } private static SimpleFeatureType createDistributedRenderFeatureType() { final SimpleFeatureTypeBuilder typeBuilder = new SimpleFeatureTypeBuilder(); typeBuilder.setName("distributed_render"); typeBuilder.add( "result", DistributedRenderResult.class); typeBuilder.add( "options", DistributedRenderOptions.class); return typeBuilder.buildFeatureType(); } protected boolean isDistributedRenderQuery() { return GeoWaveFeatureCollection.isDistributedRenderQuery(query); } protected static final boolean isDistributedRenderQuery( final Query query ) { return query.getHints().containsKey( DistributedRenderProcess.OPTIONS); } private static SimpleFeatureType getSchema( final GeoWaveFeatureReader reader, final Query query ) { if (GeoWaveFeatureCollection.isDistributedRenderQuery(query)) { return getDistributedRenderFeatureType(); } return reader.getComponents().getAdapter().getFeatureType(); } private Filter getFilter( final Query query ) { final Filter filter = query.getFilter(); if (filter instanceof BBOXImpl) { final BBOXImpl bbox = ((BBOXImpl) filter); final String propName = bbox.getPropertyName(); if ((propName == null) || propName.isEmpty()) { bbox.setPropertyName(getSchema( reader, query).getGeometryDescriptor().getLocalName()); } } return filter; } protected QueryConstraints getQueryConstraints() throws TransformException, FactoryException { final ReferencedEnvelope referencedEnvelope = getEnvelope(query); final Geometry jtsBounds = getBBox( query, referencedEnvelope); final TemporalConstraintsSet timeBounds = getBoundedTime(query); Integer limit = getLimit(query); final Integer startIndex = getStartIndex(query); // limit becomes a 'soft' constraint since GeoServer will inforce // the limit final Long max = (limit != null) ? limit.longValue() + (startIndex == null ? 0 : startIndex.longValue()) : null; // limit only used if less than an integer max value. limit = ((max != null) && (max.longValue() < Integer.MAX_VALUE)) ? max.intValue() : null; return new QueryConstraints( jtsBounds, timeBounds, referencedEnvelope, limit); } @Override protected Iterator<SimpleFeature> openIterator() { try { return openIterator(getQueryConstraints()); } catch (TransformException | FactoryException e) { LOGGER.warn( "Unable to transform geometry", e); } return featureCursor; } private Iterator<SimpleFeature> openIterator( final QueryConstraints contraints ) { if (query.getFilter() == Filter.EXCLUDE) { featureCursor = reader.getNoData(); } else if (isDistributedRenderQuery()) { featureCursor = reader.renderData( contraints.jtsBounds, contraints.timeBounds, getFilter(query), contraints.limit, (DistributedRenderOptions) query.getHints().get( DistributedRenderProcess.OPTIONS)); } else if (query.getHints().containsKey( SubsampleProcess.OUTPUT_WIDTH) && query.getHints().containsKey( SubsampleProcess.OUTPUT_HEIGHT) && query.getHints().containsKey( SubsampleProcess.OUTPUT_BBOX)) { double pixelSize = 1; if (query.getHints().containsKey( SubsampleProcess.PIXEL_SIZE)) { pixelSize = (Double) query.getHints().get( SubsampleProcess.PIXEL_SIZE); } featureCursor = reader.getData( contraints.jtsBounds, contraints.timeBounds, (Integer) query.getHints().get( SubsampleProcess.OUTPUT_WIDTH), (Integer) query.getHints().get( SubsampleProcess.OUTPUT_HEIGHT), pixelSize, getFilter(query), contraints.referencedEnvelope, contraints.limit); } else { // get the data within the bounding box featureCursor = reader.getData( contraints.jtsBounds, contraints.timeBounds, getFilter(query), contraints.limit); } return featureCursor; } private ReferencedEnvelope getEnvelope( final Query query ) throws TransformException, FactoryException { if (query.getHints().containsKey( SubsampleProcess.OUTPUT_BBOX)) { return ((ReferencedEnvelope) query.getHints().get( SubsampleProcess.OUTPUT_BBOX)).transform( GeoWaveGTDataStore.DEFAULT_CRS, true); } return null; } private Geometry getBBox( final Query query, final ReferencedEnvelope envelope ) { if (envelope != null) { return new GeometryFactory().toGeometry(envelope); } ExtractGeometryFilterVisitorResult geoAndCompareOp = ExtractGeometryFilterVisitor.getConstraints( query.getFilter(), GeoWaveGTDataStore.DEFAULT_CRS); if (geoAndCompareOp == null) { return reader.clipIndexedBBOXConstraints(null); } else { return reader.clipIndexedBBOXConstraints(geoAndCompareOp.getGeometry()); } } private Query validateQuery( final String typeName, final Query query ) { return query == null ? new Query( typeName, Filter.EXCLUDE) : query; } private Integer getStartIndex( final Query query ) { return query.getStartIndex(); } private Integer getLimit( final Query query ) { if (!query.isMaxFeaturesUnlimited() && (query.getMaxFeatures() >= 0)) { return query.getMaxFeatures(); } return null; } @Override public void accepts( final org.opengis.feature.FeatureVisitor visitor, final org.opengis.util.ProgressListener progress ) throws IOException { if ((visitor instanceof MinVisitor)) { final ExtractAttributesFilter filter = new ExtractAttributesFilter(); final MinVisitor minVisitor = (MinVisitor) visitor; final Collection<String> attrs = (Collection<String>) minVisitor.getExpression().accept( filter, null); int acceptedCount = 0; for (final String attr : attrs) { for (final DataStatistics<SimpleFeature> stat : reader.getStatsFor(attr)) { if (stat instanceof FeatureTimeRangeStatistics) { minVisitor.setValue(reader.convertToType( attr, ((FeatureTimeRangeStatistics) stat).getMinTime())); acceptedCount++; } else if (stat instanceof FeatureNumericRangeStatistics) { minVisitor.setValue(reader.convertToType( attr, ((FeatureNumericRangeStatistics) stat).getMin())); acceptedCount++; } } } if (acceptedCount > 0) { if (progress != null) { progress.complete(); } return; } } else if ((visitor instanceof MaxVisitor)) { final ExtractAttributesFilter filter = new ExtractAttributesFilter(); final MaxVisitor maxVisitor = (MaxVisitor) visitor; final Collection<String> attrs = (Collection<String>) maxVisitor.getExpression().accept( filter, null); int acceptedCount = 0; for (final String attr : attrs) { for (final DataStatistics<SimpleFeature> stat : reader.getStatsFor(attr)) { if (stat instanceof FeatureTimeRangeStatistics) { maxVisitor.setValue(reader.convertToType( attr, ((FeatureTimeRangeStatistics) stat).getMaxTime())); acceptedCount++; } else if (stat instanceof FeatureNumericRangeStatistics) { maxVisitor.setValue(reader.convertToType( attr, ((FeatureNumericRangeStatistics) stat).getMax())); acceptedCount++; } } } if (acceptedCount > 0) { if (progress != null) { progress.complete(); } return; } } DataUtilities.visit( this, visitor, progress); } /** * Return constraints that are indexed * * @param query * @return */ protected TemporalConstraintsSet getBoundedTime( final Query query ) { if (query == null) { return null; } final TemporalConstraintsSet constraints = new ExtractTimeFilterVisitor( reader.getComponents().getAdapter().getTimeDescriptors()).getConstraints(query); return constraints.isEmpty() ? constraints : reader.clipIndexedTemporalConstraints(constraints); } @Override public FeatureReader<SimpleFeatureType, SimpleFeature> reader() { return reader; } @Override protected void closeIterator( final Iterator<SimpleFeature> close ) { try { featureCursor.close(); } catch (final IOException e) { LOGGER.warn( "Unable to close iterator", e); } } public Iterator<SimpleFeature> getOpenIterator() { return featureCursor; } @Override public void close( final FeatureIterator<SimpleFeature> iterator ) { featureCursor = null; super.close(iterator); } @Override public boolean isEmpty() { try { return !reader.hasNext(); } catch (final IOException e) { LOGGER.warn( "Error checking reader", e); } return true; } private static class QueryConstraints { Geometry jtsBounds; TemporalConstraintsSet timeBounds; ReferencedEnvelope referencedEnvelope; Integer limit; public QueryConstraints( final Geometry jtsBounds, final TemporalConstraintsSet timeBounds, final ReferencedEnvelope referencedEnvelope, final Integer limit ) { super(); this.jtsBounds = jtsBounds; this.timeBounds = timeBounds; this.referencedEnvelope = referencedEnvelope; this.limit = limit; } } }