/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2009-2011, Open Source Geospatial Foundation (OSGeo)
*
* This file is hereby placed into the Public Domain. This means anyone is
* free to do whatever they wish with this file. Use it well and enjoy!
*/
package org.geotools.data;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.data.store.EmptyFeatureCollection;
import org.geotools.data.store.FilteringIterator;
import org.geotools.data.store.ReTypingIterator;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.Hints;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.collection.AbstractFeatureCollection;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.opengis.feature.Feature;
import org.opengis.feature.IllegalAttributeException;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.Name;
import org.opengis.filter.And;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.spatial.BBOX;
import org.opengis.filter.spatial.BinarySpatialOperator;
import org.opengis.filter.spatial.Contains;
import org.opengis.filter.spatial.Crosses;
import org.opengis.filter.spatial.DWithin;
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 com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.index.strtree.STRtree;
/**
* A caching feature source for fast data access.
* <p>
* This feature source is used as a wrapper offering a spatial index, for a quick
* user interface experience at the cost of memory, all data returned by the last query
* will be loaded in a in memory spatial index.
* <p>This class is meant as an example, it is not tested enough to be considered production
* worthy
*
* @author Andrea Aime - GeoSolutions
* @since 2.6
*
* @source $URL$
* @version $Id$
* @deprecated This class is not tested enough to be considered production ready
*/
public class CachingFeatureSource implements SimpleFeatureSource {
private SimpleFeatureSource wrapped;
private STRtree index;
private boolean dirty;
private Query cachedQuery;
private Envelope cachedBounds;
private SimpleFeatureType cachedSchema;
private Envelope originalBounds;
private static FilterFactory ff = CommonFactoryFinder.getFilterFactory(null);
private static final Set<Class> supportedFilterTypes = new HashSet<Class>(Arrays.asList(
BBOX.class, Contains.class, Crosses.class, DWithin.class, Equals.class,
Intersects.class, Overlaps.class, Touches.class, Within.class));
public CachingFeatureSource( FeatureSource original) throws IOException {
this( DataUtilities.simple( original ));
}
public CachingFeatureSource(SimpleFeatureSource original) throws IOException {
this.wrapped = original;
this.originalBounds = original.getBounds();
if (originalBounds == null)
originalBounds = new Envelope(-Double.MAX_VALUE, Double.MAX_VALUE, -Double.MAX_VALUE,
Double.MAX_VALUE);
}
private void fillCache(Query query) throws IOException {
Query cloned = new DefaultQuery(query);
cloned.getHints().remove(Hints.GEOMETRY_DISTANCE);
FeatureCollection features = wrapped.getFeatures(cloned);
FeatureIterator fi = features.features();
index = null;
STRtree newIndex = new STRtree();
while (fi.hasNext()) {
// consider turning all geometries into packed ones, to save space
Feature f = fi.next();
newIndex.insert(ReferencedEnvelope.reference(f.getBounds()), f);
}
fi.close();
index = newIndex;
cachedQuery = query;
cachedSchema = (SimpleFeatureType) features.getSchema();
cachedBounds = getEnvelope(query.getFilter());
dirty = false;
}
public void addFeatureListener(FeatureListener listener) {
wrapped.addFeatureListener(listener);
}
public void removeFeatureListener(FeatureListener listener) {
wrapped.removeFeatureListener(listener);
}
public DataStore getDataStore() {
return (DataStore) wrapped.getDataStore();
}
public ReferencedEnvelope getBounds() throws IOException {
return wrapped.getBounds();
}
public ReferencedEnvelope getBounds(Query query) throws IOException {
return wrapped.getBounds(query);
}
public int getCount(Query query) throws IOException {
return wrapped.getCount(query);
}
public SimpleFeatureType getSchema() {
return wrapped.getSchema();
}
public SimpleFeatureCollection getFeatures() throws IOException {
return getFeatures(Filter.INCLUDE);
}
public SimpleFeatureCollection getFeatures(Filter filter) throws IOException {
return getFeatures(new DefaultQuery(wrapped.getSchema().getName().getLocalPart(), filter));
}
public SimpleFeatureCollection getFeatures(Query query) throws IOException {
String schemaName = wrapped.getSchema().getName().getLocalPart();
if (query.getTypeName() != null && !schemaName.equals(query.getTypeName())) {
throw new DataSourceException("Typename mismatch, query asks for '"
+ query.getTypeName() + " but this feature source provides '" + schemaName
+ "'");
}
return getFeatureCollection(query, getEnvelope(query.getFilter()));
}
private SimpleFeatureCollection getFeatureCollection(Query query, Envelope bounds) throws IOException {
try {
SimpleFeatureType base = wrapped.getSchema();
SimpleFeatureType alternate = base;
if (query.getPropertyNames() != Query.ALL_NAMES) {
alternate = SimpleFeatureTypeBuilder.retype(base, query.getPropertyNames());
if (alternate.equals(base))
alternate = base;
}
return new CachingFeatureCollection(bounds, base, alternate, query);
} catch (Exception e) {
throw new DataSourceException(
"Error occurred extracting features from the spatial index", e);
}
}
/**
* Same as DataUtilities.reType, but without the cloning that uselessly wastes CPU cycles...
*
* @param featureType
* @param feature
* @return
* @throws IllegalAttributeException
*/
public static SimpleFeature reType(SimpleFeatureType featureType, SimpleFeature feature)
throws IllegalAttributeException {
FeatureType origional = feature.getFeatureType();
if (featureType.equals(origional)) {
return SimpleFeatureBuilder.copy(feature);
}
String id = feature.getID();
int numAtts = featureType.getAttributeCount();
Object[] attributes = new Object[numAtts];
String xpath;
for (int i = 0; i < numAtts; i++) {
AttributeDescriptor curAttType = featureType.getDescriptor(i);
attributes[i] = feature.getAttribute(curAttType.getLocalName());
}
return SimpleFeatureBuilder.build(featureType, attributes, id);
}
boolean isSubQuery(Query query) {
// no cached data?
if (cachedQuery == null)
return false;
// do we miss some properties?
String[] cachedPropNames = cachedQuery.getPropertyNames();
String[] propNames = query.getPropertyNames();
if (cachedPropNames != Query.ALL_NAMES
&& (propNames == Query.ALL_NAMES || !Arrays.asList(cachedPropNames).containsAll(
Arrays.asList(propNames))))
return false;
Filter[] filters = splitFilters(query);
Filter[] cachedFilters = splitFilters(cachedQuery);
if (!filters[0].equals(cachedFilters[0]))
return false;
Envelope envelope = getEnvelope(filters[1]);
return cachedBounds.contains(envelope);
}
Envelope getEnvelope(Filter filter) {
Envelope result = originalBounds;
if (filter instanceof And) {
Envelope bounds = new Envelope();
for (Iterator iter = ((And) filter).getChildren().iterator(); iter.hasNext();) {
Filter f = (Filter) iter.next();
Envelope e = getEnvelope(f);
if (e == null)
return null;
else
bounds.expandToInclude(e);
}
result = bounds;
} else if (filter instanceof BinarySpatialOperator) {
BinarySpatialOperator gf = (BinarySpatialOperator) filter;
if (supportedFilterTypes.contains(gf.getClass())) {
Expression lg = gf.getExpression1();
Expression rg = gf.getExpression2();
if (lg instanceof Literal) {
Geometry g = (Geometry) ((Literal) lg).getValue();
if (rg instanceof PropertyName)
result = g.getEnvelopeInternal();
} else if (rg instanceof Literal) {
Geometry g = (Geometry) ((Literal) rg).getValue();
if (lg instanceof PropertyName)
result = g.getEnvelopeInternal();
}
}
}
return result.intersection(originalBounds);
}
/**
* Splits a query into two parts, a spatial component that can be turned into a bbox filter (by
* including some more feature in the result) and a residual component that we cannot address
* with the spatial index
*
* @param query
*/
Filter[] splitFilters(Query query) {
Filter filter = query.getFilter();
if (filter == null || filter.equals(Filter.EXCLUDE)) {
return new Filter[] { Filter.EXCLUDE, bboxFilter(originalBounds) };
}
if (!(filter instanceof And)) {
Envelope envelope = getEnvelope(filter);
if (envelope == null)
return new Filter[] { Filter.EXCLUDE, bboxFilter(originalBounds) };
else
return new Filter[] { Filter.EXCLUDE, bboxFilter(envelope) };
}
And and = (And) filter;
List residuals = new ArrayList();
List bboxBacked = new ArrayList();
for (Iterator it = and.getChildren().iterator(); it.hasNext();) {
Filter child = (Filter) it.next();
if (getEnvelope(child) != null) {
bboxBacked.add(child);
} else {
residuals.add(child);
}
}
return new Filter[] { (Filter) ff.and(residuals), (Filter) ff.and(bboxBacked) };
}
private BBOX bboxFilter(Envelope bbox) {
return ff.bbox(wrapped.getSchema().getGeometryDescriptor().getLocalName(), bbox.getMinX(),
bbox.getMinY(), bbox.getMaxX(), bbox.getMaxY(), null);
}
public ResourceInfo getInfo() {
return wrapped.getInfo();
}
public Name getName() {
return wrapped.getName();
}
public QueryCapabilities getQueryCapabilities() {
return wrapped.getQueryCapabilities();
}
public Set getSupportedHints() {
HashSet hints = new HashSet(wrapped.getSupportedHints());
hints.remove(Hints.FEATURE_DETACHED);
return hints;
}
/**
* A custom feature collection directly peeking on the feature source spatial index
*
* @author Andrea Aime - Geosolutions
*/
final class CachingFeatureCollection extends AbstractFeatureCollection {
private SimpleFeatureType sourceSchema;
private SimpleFeatureType targetSchema;
private Query query;
private ReferencedEnvelope queryBounds = null;
protected CachingFeatureCollection(Envelope queryBounds, SimpleFeatureType sourceSchema,
SimpleFeatureType targetSchema, Query query) {
super(targetSchema);
this.sourceSchema = sourceSchema;
this.targetSchema = targetSchema;
this.query = query;
}
@Override
public int size() {
try {
return getCount(query);
} catch(Exception e) {
throw new RuntimeException("Failed to count features", e);
}
}
@Override
public synchronized ReferencedEnvelope getBounds() {
try {
return CachingFeatureSource.this.getBounds(query);
} catch(Exception e) {
throw new RuntimeException("Failed to count features", e);
}
}
@Override
protected Iterator openIterator() {
List features;
synchronized (CachingFeatureSource.this) {
try {
if (index == null || dirty || !isSubQuery(query)) {
fillCache(query);
}
if(queryBounds != null) {
features = index.query(queryBounds);
} else {
features = index.query((Envelope) index.getRoot().getBounds());
}
} catch(Exception e) {
throw new RuntimeException("Failed to get data", e);
}
}
Iterator it = features.iterator();
if(query.getFilter() != null && Filter.INCLUDE.equals(query.getFilter())) {
it = new FilteringIterator<Feature>(it, query.getFilter());
}
if(targetSchema != sourceSchema) {
it = new ReTypingIterator(it, sourceSchema, targetSchema);
}
return it;
}
@Override
public SimpleFeatureCollection subCollection(Filter filter) {
// get the new target envelope
Envelope filterEnvelope = getEnvelope(filter);
Envelope subEnvelope = queryBounds;
if(filterEnvelope != null) {
subEnvelope = subEnvelope.intersection(queryBounds);
}
if(subEnvelope.isNull()) {
return new EmptyFeatureCollection(targetSchema);
}
// mix filters
Query subQuery = new Query(query);
Filter baseFilter = query.getFilter();
if(baseFilter != null && !Filter.INCLUDE.equals(baseFilter)) {
Filter mixed = ff.and(baseFilter, filter);
subQuery.setFilter(mixed);
}
return new CachingFeatureCollection(subEnvelope, sourceSchema, targetSchema, subQuery);
}
}
}