/** * Copyright (c) 2013-2020 Contributors to the Eclipse Foundation * * <p> See the NOTICE file distributed with this work for additional information regarding copyright * ownership. All rights reserved. This program and the accompanying materials are made available * under the terms of the Apache License, Version 2.0 which accompanies this distribution and is * available at http://www.apache.org/licenses/LICENSE-2.0.txt */ package org.locationtech.geowave.adapter.vector.plugin; import java.io.IOException; import java.util.Iterator; import java.util.Map; 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.filter.spatial.BBOXImpl; import org.geotools.geometry.jts.ReferencedEnvelope; import org.locationtech.geowave.adapter.vector.render.DistributedRenderOptions; import org.locationtech.geowave.adapter.vector.render.DistributedRenderResult; import org.locationtech.geowave.core.geotime.store.query.TemporalConstraintsSet; import org.locationtech.geowave.core.geotime.store.query.api.VectorStatisticsQueryBuilder; import org.locationtech.geowave.core.geotime.store.statistics.BoundingBoxDataStatistics; import org.locationtech.geowave.core.geotime.util.ExtractGeometryFilterVisitor; import org.locationtech.geowave.core.geotime.util.ExtractGeometryFilterVisitorResult; import org.locationtech.geowave.core.geotime.util.ExtractTimeFilterVisitor; import org.locationtech.geowave.core.geotime.util.GeometryUtils; import org.locationtech.geowave.core.store.CloseableIterator; import org.locationtech.geowave.core.store.adapter.statistics.CountDataStatistics; import org.locationtech.geowave.core.store.adapter.statistics.InternalDataStatistics; import org.locationtech.geowave.core.store.adapter.statistics.StatisticsId; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 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 static final 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<StatisticsId, InternalDataStatistics<SimpleFeature, ?, ?>> statsMap = reader.getTransaction().getDataStatistics(); final StatisticsId id = CountDataStatistics.STATS_TYPE.newBuilder().build().getId(); if (statsMap.containsKey(id)) { final CountDataStatistics stats = (CountDataStatistics) statsMap.get(id); 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<StatisticsId, InternalDataStatistics<SimpleFeature, ?, ?>> statsMap = reader.getTransaction().getDataStatistics(); final StatisticsId statId = VectorStatisticsQueryBuilder.newBuilder().factory().bbox().fieldName( reader.getFeatureType().getGeometryDescriptor().getLocalName()).build().getId(); if (statsMap.containsKey(statId)) { final BoundingBoxDataStatistics<SimpleFeature, ?> stats = (BoundingBoxDataStatistics<SimpleFeature, ?>) statsMap.get(statId); return new ReferencedEnvelope( stats.getMinX(), stats.getMaxX(), stats.getMinY(), stats.getMaxY(), reader.getFeatureType().getCoordinateReferenceSystem()); } 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, GeometryUtils.getDefaultCRS()); } return new ReferencedEnvelope(minx, maxx, miny, maxy, GeometryUtils.getDefaultCRS()); } @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 constraints) { if ((constraints.jtsBounds != null && constraints.jtsBounds.isEmpty()) || (constraints.timeBounds != null && constraints.timeBounds.isEmpty())) { // return nothing if either constraint is empty featureCursor = reader.getNoData(); } else if (query.getFilter() == Filter.EXCLUDE) { featureCursor = reader.getNoData(); } else if (isDistributedRenderQuery()) { featureCursor = reader.renderData( constraints.jtsBounds, constraints.timeBounds, getFilter(query), constraints.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( constraints.jtsBounds, constraints.timeBounds, (Integer) query.getHints().get(SubsampleProcess.OUTPUT_WIDTH), (Integer) query.getHints().get(SubsampleProcess.OUTPUT_HEIGHT), pixelSize, getFilter(query), constraints.referencedEnvelope, constraints.limit); } else { featureCursor = reader.getData( constraints.jtsBounds, constraints.timeBounds, getFilter(query), constraints.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( reader.getFeatureType().getCoordinateReferenceSystem(), true); } return null; } private Geometry getBBox(final Query query, final ReferencedEnvelope envelope) { if (envelope != null) { return new GeometryFactory().toGeometry(envelope); } final String geomAtrributeName = reader.getComponents().getAdapter().getFeatureType().getGeometryDescriptor().getLocalName(); final ExtractGeometryFilterVisitorResult geoAndCompareOp = ExtractGeometryFilterVisitor.getConstraints( query.getFilter(), reader.getComponents().getAdapter().getFeatureType().getCoordinateReferenceSystem(), geomAtrributeName); 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 (!GeoWaveGTPluginUtils.accepts( visitor, progress, reader.getFeatureType(), reader.getTransaction().getDataStatistics())) { DataUtilities.visit(this, visitor, progress); } } /** * @param query the query * @return the temporal constraints of the query */ 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() ? null : reader.clipIndexedTemporalConstraints(constraints); } @Override public FeatureReader<SimpleFeatureType, SimpleFeature> reader() { return reader; } @Override protected void closeIterator(final Iterator<SimpleFeature> close) { featureCursor.close(); } 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; } } }