package org.geotools.data.collection; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import org.geotools.data.DataStore; import org.geotools.data.DataUtilities; import org.geotools.data.FeatureListener; import org.geotools.data.Query; import org.geotools.data.QueryCapabilities; import org.geotools.data.ResourceInfo; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.data.store.EmptyFeatureCollection; import org.geotools.data.store.ReTypingFeatureCollection; import org.geotools.data.store.ReprojectingFeatureCollection; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.collection.MaxSimpleFeatureCollection; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.geometry.jts.ReferencedEnvelope; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; 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.sort.SortBy; 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; /** * A FeatureSource using a spatial index to hold on to features and serve them up for fast display. * <p> * This is a port of Andrea's CachingFeatureSource (which is slightly more compliced and rebuilds * the cache as an origional feature source changes). Our implementation here knows up front that * the features are in memory and does its best to take advantage of the fact. A caching feature * source for fast data access. * <p> * Please note that this FeatureSource is strictly "read-only" and thus does not support feature * events. * </p> * * @source $URL: http://svn.osgeo.org/geotools/trunk/modules/library/main/src/main/java/org/geotools/data/collection/SpatialIndexFeatureSource.java $ */ public class SpatialIndexFeatureSource implements SimpleFeatureSource { SpatialIndexFeatureCollection contents; 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 SpatialIndexFeatureSource(SpatialIndexFeatureCollection original) { this.contents = original; } public void addFeatureListener(FeatureListener listener) { } public void removeFeatureListener(FeatureListener listener) { } public DataStore getDataStore() { return null; // not applicable } public ReferencedEnvelope getBounds() throws IOException { return contents.getBounds(); } public ReferencedEnvelope getBounds(Query query) throws IOException { return getFeatures(query).getBounds(); } public int getCount(Query query) throws IOException { return getFeatures(query).size(); } public SimpleFeatureType getSchema() { return contents.getSchema(); } public SimpleFeatureCollection getFeatures() throws IOException { return contents; } public SimpleFeatureCollection getFeatures(Filter filter) throws IOException { Query query = new Query(getSchema().getName().getLocalPart(), filter); return getFeatures(query); } public SimpleFeatureCollection getFeatures(Query query) throws IOException { Envelope bounds = getEnvelope(query.getFilter()); return getFeatureCollection(query, bounds); } private SimpleFeatureCollection getFeatureCollection(Query query, Envelope bounds) throws IOException { query = DataUtilities.resolvePropertyNames(query, getSchema()); final int offset = query.getStartIndex() != null ? query.getStartIndex() : 0; if (offset > 0 & query.getSortBy() == null) { if (!getQueryCapabilities().supportsSorting(query.getSortBy())) { throw new IllegalStateException("Feature source does not support this sorting " + "so there is no way a stable paging (offset/limit) can be performed"); } Query copy = new Query(query); copy.setSortBy(new SortBy[] { SortBy.NATURAL_ORDER }); query = copy; } SimpleFeatureCollection collection; // step one filter if (query.getFilter() != null && query.getFilter().equals(Filter.EXCLUDE)) { return new EmptyFeatureCollection(getSchema()); } if (query.getFilter() != null && query.getFilter().equals(Filter.INCLUDE)) { collection = contents; } if (query.getFilter() != null && query.getFilter().equals(Filter.INCLUDE)) { collection = contents; } else { collection = contents.subCollection(query.getFilter()); } // step two: reproject if (query.getCoordinateSystemReproject() != null) { collection = new ReprojectingFeatureCollection(collection, query .getCoordinateSystemReproject()); } // step two sort! (note this makes a sorted copy) if (query.getSortBy() != null && query.getSortBy().length != 0) { SimpleFeature array[] = collection.toArray(new SimpleFeature[collection.size()]); // Arrays sort is stable (not resorting equal elements) for (SortBy sortBy : query.getSortBy()) { Comparator<SimpleFeature> comparator = DataUtilities.sortComparator(sortBy); Arrays.sort(array, comparator); } ArrayList<SimpleFeature> list = new ArrayList<SimpleFeature>(Arrays.asList(array)); collection = new ListFeatureCollection(getSchema(), list); } // step three skip to start and return max number of fetaures if (offset > 0 || !query.isMaxFeaturesUnlimited()) { long max = Long.MAX_VALUE; if (!query.isMaxFeaturesUnlimited()) { max = query.getMaxFeatures(); } collection = new MaxSimpleFeatureCollection(collection, offset, max); } // step four - retyping // (It would be nice to do this earlier so as to not have all the baggage // of unneeded attributes) if (query.getPropertyNames() != Query.ALL_NAMES) { // rebuild the type and wrap the reader SimpleFeatureType schema = collection.getSchema(); SimpleFeatureType target = SimpleFeatureTypeBuilder.retype(schema, query .getPropertyNames()); if (!target.equals(schema)) { collection = new ReTypingFeatureCollection(collection, target); } } return collection; } Envelope getEnvelope(Filter filter) { Envelope result = new Envelope(); 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; } private BBOX bboxFilter(Envelope bbox) { return ff.bbox(contents.getSchema().getGeometryDescriptor().getLocalName(), bbox.getMinX(), bbox.getMinY(), bbox.getMaxX(), bbox.getMaxY(), null); } public ResourceInfo getInfo() { return null; } public Name getName() { return contents.getSchema().getName(); } public QueryCapabilities getQueryCapabilities() { return new QueryCapabilities() { @Override public boolean isOffsetSupported() { return true; } }; } public Set getSupportedHints() { HashSet hints = new HashSet(); return hints; } }