package mil.nga.giat.geowave.adapter.vector.plugin;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.io.Closeable;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.geotools.data.FeatureReader;
import org.geotools.data.Query;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.filter.FidFilterImpl;
import org.geotools.geometry.jts.Decimator;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.geotools.renderer.lite.RendererUtilities;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.filter.Filter;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
import com.google.common.collect.Iterators;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import mil.nga.giat.geowave.adapter.vector.plugin.transaction.GeoWaveTransaction;
import mil.nga.giat.geowave.adapter.vector.query.cql.CQLQuery;
import mil.nga.giat.geowave.adapter.vector.render.DistributedRenderAggregation;
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.FeatureStatistic;
import mil.nga.giat.geowave.adapter.vector.util.QueryIndexHelper;
import mil.nga.giat.geowave.core.geotime.GeometryUtils.GeoConstraintsWrapper;
import mil.nga.giat.geowave.core.geotime.index.dimension.LatitudeDefinition;
import mil.nga.giat.geowave.core.geotime.index.dimension.TimeDefinition;
import mil.nga.giat.geowave.core.geotime.store.query.SpatialQuery;
import mil.nga.giat.geowave.core.geotime.store.query.TemporalConstraintsSet;
import mil.nga.giat.geowave.core.index.ByteArrayId;
import mil.nga.giat.geowave.core.index.dimension.NumericDimensionDefinition;
import mil.nga.giat.geowave.core.store.CloseableIterator;
import mil.nga.giat.geowave.core.store.CloseableIteratorWrapper;
import mil.nga.giat.geowave.core.store.adapter.statistics.DataStatistics;
import mil.nga.giat.geowave.core.store.index.Index;
import mil.nga.giat.geowave.core.store.index.PrimaryIndex;
import mil.nga.giat.geowave.core.store.query.BasicQuery;
import mil.nga.giat.geowave.core.store.query.BasicQuery.Constraints;
import mil.nga.giat.geowave.core.store.query.DataIdQuery;
import mil.nga.giat.geowave.core.store.query.QueryOptions;
import mil.nga.giat.geowave.core.store.query.aggregate.CountAggregation;
import mil.nga.giat.geowave.core.store.query.aggregate.CountResult;
/**
* This class wraps a geotools data store as well as one for statistics (for
* example to display Heatmaps) into a GeoTools FeatureReader for simple feature
* data. It acts as a helper for GeoWave's GeoTools data store.
*
*/
public class GeoWaveFeatureReader implements
FeatureReader<SimpleFeatureType, SimpleFeature>
{
private final static Logger LOGGER = LoggerFactory.getLogger(GeoWaveFeatureReader.class);
private final GeoWaveDataStoreComponents components;
private final GeoWaveFeatureCollection featureCollection;
private final GeoWaveTransaction transaction;
private final Query query;
public GeoWaveFeatureReader(
final Query query,
final GeoWaveTransaction transaction,
final GeoWaveDataStoreComponents components )
throws IOException {
this.components = components;
this.transaction = transaction;
featureCollection = new GeoWaveFeatureCollection(
this,
query);
this.query = query;
}
public GeoWaveTransaction getTransaction() {
return transaction;
}
public GeoWaveDataStoreComponents getComponents() {
return components;
}
@Override
public void close()
throws IOException {
if (featureCollection.getOpenIterator() != null) {
featureCollection.closeIterator(featureCollection.getOpenIterator());
}
}
@Override
public SimpleFeatureType getFeatureType() {
return components.getAdapter().getFeatureType();
}
@Override
public boolean hasNext()
throws IOException {
Iterator<SimpleFeature> it = featureCollection.getOpenIterator();
if (it != null) {
// protect againt GeoTools forgetting to call close()
// on this FeatureReader, which causes a resource leak
if (!it.hasNext()) {
((CloseableIterator<?>) it).close();
}
return it.hasNext();
}
it = featureCollection.openIterator();
return it.hasNext();
}
@Override
public SimpleFeature next()
throws IOException,
IllegalArgumentException,
NoSuchElementException {
Iterator<SimpleFeature> it = featureCollection.getOpenIterator();
if (it != null) {
return it.next();
}
it = featureCollection.openIterator();
return it.next();
}
public CloseableIterator<SimpleFeature> getNoData() {
return new CloseableIterator.Empty<SimpleFeature>();
}
public long getCount() {
return featureCollection.getCount();
}
protected long getCountInternal(
final Geometry jtsBounds,
final TemporalConstraintsSet timeBounds,
final Filter filter,
final Integer limit ) {
final CountQueryIssuer countIssuer = new CountQueryIssuer(
filter,
limit);
issueQuery(
jtsBounds,
timeBounds,
countIssuer);
return countIssuer.count;
}
private BasicQuery getQuery(
final Map<ByteArrayId, DataStatistics<SimpleFeature>> statsMap,
final Geometry jtsBounds,
final TemporalConstraintsSet timeBounds ) {
final Constraints timeConstraints = QueryIndexHelper.composeTimeBoundedConstraints(
components.getAdapter().getFeatureType(),
components.getAdapter().getTimeDescriptors(),
statsMap,
timeBounds);
final GeoConstraintsWrapper geoConstraints = QueryIndexHelper.composeGeometricConstraints(
getFeatureType(),
statsMap,
jtsBounds);
/**
* NOTE: query to an index that requires a constraint and the constraint
* is missing equates to a full table scan. @see BasicQuery
*/
final BasicQuery query = composeQuery(
geoConstraints,
timeConstraints);
query.setExact(timeBounds.isExact());
return query;
}
public CloseableIterator<SimpleFeature> issueQuery(
final Geometry jtsBounds,
final TemporalConstraintsSet timeBounds,
final QueryIssuer issuer ) {
final List<CloseableIterator<SimpleFeature>> results = new ArrayList<CloseableIterator<SimpleFeature>>();
final Map<ByteArrayId, DataStatistics<SimpleFeature>> statsMap = transaction.getDataStatistics();
final BasicQuery query = getQuery(
statsMap,
jtsBounds,
timeBounds);
try (CloseableIterator<Index<?, ?>> indexIt = getComponents().getIndices(
statsMap,
query)) {
while (indexIt.hasNext()) {
final PrimaryIndex index = (PrimaryIndex) indexIt.next();
final CloseableIterator<SimpleFeature> it = issuer.query(
index,
query);
if (it != null) {
results.add(it);
}
}
}
catch (final IOException e) {
LOGGER.warn(
"unable to close index iterator for query",
e);
}
if (results.isEmpty()) {
return getNoData();
}
return interweaveTransaction(
issuer.getLimit(),
issuer.getFilter(),
new CloseableIteratorWrapper<SimpleFeature>(
new Closeable() {
@Override
public void close()
throws IOException {
for (final CloseableIterator<SimpleFeature> result : results) {
result.close();
}
}
},
Iterators.concat(results.iterator())));
}
protected static boolean hasAtLeastSpatial(
final PrimaryIndex index ) {
if ((index == null) || (index.getIndexStrategy() == null)
|| (index.getIndexStrategy().getOrderedDimensionDefinitions() == null)) {
return false;
}
boolean hasLatitude = false;
boolean hasLongitude = false;
for (final NumericDimensionDefinition dimension : index.getIndexStrategy().getOrderedDimensionDefinitions()) {
if (dimension instanceof LatitudeDefinition) {
hasLatitude = true;
}
if (dimension instanceof LatitudeDefinition) {
hasLongitude = true;
}
}
return hasLatitude && hasLongitude;
}
protected static boolean hasTime(
final PrimaryIndex index ) {
if ((index == null) || (index.getIndexStrategy() == null)
|| (index.getIndexStrategy().getOrderedDimensionDefinitions() == null)) {
return false;
}
for (final NumericDimensionDefinition dimension : index.getIndexStrategy().getOrderedDimensionDefinitions()) {
if (dimension instanceof TimeDefinition) {
return true;
}
}
return false;
}
private class BaseIssuer implements
QueryIssuer
{
final Filter filter;
final Integer limit;
public BaseIssuer(
final Filter filter,
final Integer limit ) {
super();
this.filter = filter;
this.limit = limit;
}
@Override
public CloseableIterator<SimpleFeature> query(
final PrimaryIndex index,
final BasicQuery query ) {
final QueryOptions queryOptions = new QueryOptions(
components.getAdapter(),
index,
limit,
null,
transaction.composeAuthorizations());
if (subsetRequested()) {
queryOptions.setFieldIds(
getSubset(),
components.getAdapter());
}
return components.getDataStore().query(
queryOptions,
CQLQuery.createOptimalQuery(
filter,
components.getAdapter(),
index,
query));
}
@Override
public Filter getFilter() {
return filter;
}
@Override
public Integer getLimit() {
return limit;
}
}
private class CountQueryIssuer extends
BaseIssuer implements
QueryIssuer
{
private long count = 0;
public CountQueryIssuer(
final Filter filter,
final Integer limit ) {
super(
filter,
limit);
}
@Override
public CloseableIterator<SimpleFeature> query(
final PrimaryIndex index,
final BasicQuery query ) {
final QueryOptions queryOptions = new QueryOptions(
components.getAdapter(),
index,
limit,
null,
transaction.composeAuthorizations());
queryOptions.setAggregation(
new CountAggregation(),
components.getAdapter());
try (final CloseableIterator<CountResult> result = components.getDataStore().query(
queryOptions,
CQLQuery.createOptimalQuery(
filter,
components.getAdapter(),
index,
query))) {
if (result.hasNext()) {
final CountResult cntResult = result.next();
if (cntResult != null) {
count = cntResult.getCount();
}
}
}
catch (final IOException e) {
LOGGER.warn(
"Unable to close count iterator",
e);
}
return null;
}
public long getCount() {
return count;
}
}
private class EnvelopeQueryIssuer extends
BaseIssuer implements
QueryIssuer
{
final ReferencedEnvelope envelope;
final int width;
final int height;
final double pixelSize;
public EnvelopeQueryIssuer(
final int width,
final int height,
final double pixelSize,
final Filter filter,
final Integer limit,
final ReferencedEnvelope envelope ) {
super(
filter,
limit);
this.width = width;
this.height = height;
this.pixelSize = pixelSize;
this.envelope = envelope;
}
@Override
public CloseableIterator<SimpleFeature> query(
final PrimaryIndex index,
final BasicQuery query ) {
final QueryOptions options = new QueryOptions(
components.getAdapter(),
index,
transaction.composeAuthorizations());
options.setLimit(limit);
if (subsetRequested()) {
options.setFieldIds(
getSubset(),
components.getAdapter());
}
final double east = envelope.getMaxX();
final double west = envelope.getMinX();
final double north = envelope.getMaxY();
final double south = envelope.getMinY();
try {
final AffineTransform worldToScreen = RendererUtilities.worldToScreenTransform(
new ReferencedEnvelope(
new Envelope(
west,
east,
south,
north),
CRS.decode("EPSG:4326")),
new Rectangle(
width,
height));
final MathTransform2D fullTransform = (MathTransform2D) ProjectiveTransform.create(worldToScreen);
// calculate spans
try {
final double[] spans = Decimator.computeGeneralizationDistances(
fullTransform.inverse(),
new Rectangle(
width,
height),
pixelSize);
options.setMaxResolutionSubsamplingPerDimension(spans);
return components.getDataStore().query(
options,
CQLQuery.createOptimalQuery(
filter,
components.getAdapter(),
index,
query));
}
catch (final TransformException e) {
throw new IllegalArgumentException(
"Unable to compute generalization distance",
e);
}
}
catch (MismatchedDimensionException | FactoryException e) {
throw new IllegalArgumentException(
"Unable to decode CRS EPSG:4326",
e);
}
}
}
private class RenderQueryIssuer extends
BaseIssuer implements
QueryIssuer
{
final DistributedRenderOptions renderOptions;
public RenderQueryIssuer(
final Filter filter,
final Integer limit,
final DistributedRenderOptions renderOptions ) {
super(
filter,
limit);
this.renderOptions = renderOptions;
}
@Override
public CloseableIterator<SimpleFeature> query(
final PrimaryIndex index,
final BasicQuery query ) {
final QueryOptions queryOptions = new QueryOptions(
components.getAdapter(),
index,
transaction.composeAuthorizations());
if (subsetRequested()) {
queryOptions.setFieldIds(
getSubset(),
components.getAdapter());
}
queryOptions.setAggregation(
new DistributedRenderAggregation(
renderOptions),
components.getAdapter());
try (CloseableIterator<DistributedRenderResult> resultIt = components.getDataStore().query(
queryOptions,
CQLQuery.createOptimalQuery(
filter,
components.getAdapter(),
index,
query))) {
if (resultIt.hasNext()) {
final DistributedRenderResult result = resultIt.next();
return new CloseableIterator.Wrapper(
Iterators.singletonIterator(SimpleFeatureBuilder.build(
GeoWaveFeatureCollection.getDistributedRenderFeatureType(),
new Object[] {
result,
renderOptions
},
"render")));
}
}
catch (final IOException e) {
LOGGER.warn(
"Unable to get distributed rendering result",
e);
}
return new CloseableIterator.Wrapper(
Iterators.emptyIterator());
}
}
public CloseableIterator<SimpleFeature> renderData(
final Geometry jtsBounds,
final TemporalConstraintsSet timeBounds,
final Filter filter,
final Integer limit,
final DistributedRenderOptions renderOptions ) {
return issueQuery(
jtsBounds,
timeBounds,
new RenderQueryIssuer(
filter,
limit,
renderOptions));
}
public CloseableIterator<SimpleFeature> getData(
final Geometry jtsBounds,
final TemporalConstraintsSet timeBounds,
final int width,
final int height,
final double pixelSize,
final Filter filter,
final ReferencedEnvelope envelope,
final Integer limit ) {
return issueQuery(
jtsBounds,
timeBounds,
new EnvelopeQueryIssuer(
width,
height,
pixelSize,
filter,
limit,
envelope));
}
public CloseableIterator<SimpleFeature> getData(
final Geometry jtsBounds,
final TemporalConstraintsSet timeBounds,
final Integer limit ) {
return issueQuery(
jtsBounds,
timeBounds,
new BaseIssuer(
null,
limit));
}
public CloseableIterator<SimpleFeature> getData(
final Geometry jtsBounds,
final TemporalConstraintsSet timeBounds,
final Filter filter,
final Integer limit ) {
if (filter instanceof FidFilterImpl) {
final Set<String> fids = ((FidFilterImpl) filter).getIDs();
final List<ByteArrayId> ids = new ArrayList<ByteArrayId>();
for (final String fid : fids) {
ids.add(new ByteArrayId(
fid));
}
final PrimaryIndex[] writeIndices = components.getAdapterIndices();
final PrimaryIndex queryIndex = ((writeIndices != null) && (writeIndices.length > 0)) ? writeIndices[0]
: null;
final QueryOptions queryOptions = new QueryOptions(
components.getAdapter(),
queryIndex,
limit,
null,
transaction.composeAuthorizations());
if (subsetRequested()) {
queryOptions.setFieldIds(
getSubset(),
components.getAdapter());
}
return components.getDataStore().query(
queryOptions,
new DataIdQuery(
components.getAdapter().getAdapterId(),
ids));
}
return issueQuery(
jtsBounds,
timeBounds,
new BaseIssuer(
filter,
limit));
}
public GeoWaveFeatureCollection getFeatureCollection() {
return featureCollection;
}
private CloseableIterator<SimpleFeature> interweaveTransaction(
final Integer limit,
final Filter filter,
final CloseableIterator<SimpleFeature> it ) {
return transaction.interweaveTransaction(
limit,
filter,
it);
}
protected List<DataStatistics<SimpleFeature>> getStatsFor(
final String name ) {
final List<DataStatistics<SimpleFeature>> stats = new LinkedList<DataStatistics<SimpleFeature>>();
final Map<ByteArrayId, DataStatistics<SimpleFeature>> statsMap = transaction.getDataStatistics();
for (final Map.Entry<ByteArrayId, DataStatistics<SimpleFeature>> stat : statsMap.entrySet()) {
if ((stat.getValue() instanceof FeatureStatistic)
&& ((FeatureStatistic) stat.getValue()).getFieldName().endsWith(
name)) {
stats.add(stat.getValue());
}
}
return stats;
}
protected TemporalConstraintsSet clipIndexedTemporalConstraints(
final TemporalConstraintsSet constraintsSet ) {
return QueryIndexHelper.clipIndexedTemporalConstraints(
transaction.getDataStatistics(),
components.getAdapter().getTimeDescriptors(),
constraintsSet);
}
protected Geometry clipIndexedBBOXConstraints(
final Geometry bbox ) {
return QueryIndexHelper.clipIndexedBBOXConstraints(
getFeatureType(),
bbox,
transaction.getDataStatistics());
}
private BasicQuery composeQuery(
final GeoConstraintsWrapper geoConstraints,
final Constraints temporalConstraints ) {
// TODO: this actually doesn't boost performance much, if at
// all, and one key is missing - the query geometry has to be
// topologically equivalent to its envelope and the ingested
// geometry has to be topologically equivalent to its envelope
// this could be kept as a statistic on ingest, but considering
// it doesn't boost performance it may not be worthwhile
// pursuing
// if (geoConstraints.isConstraintsMatchGeometry()) {
// return new BasicQuery(
// geoConstraints.getConstraints().merge(
// temporalConstraints));
// }
// else {
return new SpatialQuery(
geoConstraints.getConstraints().merge(
temporalConstraints),
geoConstraints.getGeometry());
// }
}
public Object convertToType(
final String attrName,
final Object value ) {
final SimpleFeatureType featureType = components.getAdapter().getFeatureType();
final AttributeDescriptor descriptor = featureType.getDescriptor(attrName);
if (descriptor == null) {
return value;
}
final Class<?> attrClass = descriptor.getType().getBinding();
if (attrClass.isInstance(value)) {
return value;
}
if (Number.class.isAssignableFrom(attrClass) && Number.class.isInstance(value)) {
if (Double.class.isAssignableFrom(attrClass)) {
return ((Number) value).doubleValue();
}
if (Float.class.isAssignableFrom(attrClass)) {
return ((Number) value).floatValue();
}
if (Long.class.isAssignableFrom(attrClass)) {
return ((Number) value).longValue();
}
if (Integer.class.isAssignableFrom(attrClass)) {
return ((Number) value).intValue();
}
if (Short.class.isAssignableFrom(attrClass)) {
return ((Number) value).shortValue();
}
if (Byte.class.isAssignableFrom(attrClass)) {
return ((Number) value).byteValue();
}
if (BigInteger.class.isAssignableFrom(attrClass)) {
return BigInteger.valueOf(((Number) value).longValue());
}
if (BigDecimal.class.isAssignableFrom(attrClass)) {
return BigDecimal.valueOf(((Number) value).doubleValue());
}
}
if (Calendar.class.isAssignableFrom(attrClass)) {
if (Date.class.isInstance(value)) {
final Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
c.setTime((Date) value);
return c;
}
}
if (Timestamp.class.isAssignableFrom(attrClass)) {
if (Date.class.isInstance(value)) {
final Timestamp ts = new Timestamp(
((Date) value).getTime());
return ts;
}
}
return value;
}
private boolean subsetRequested() {
if (query == null) {
return false;
}
return !(query.getPropertyNames() == Query.ALL_NAMES);
}
private List<String> getSubset() {
if (query == null) {
return Collections.emptyList();
}
return Arrays.asList(query.getPropertyNames());
}
}