/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.data.collection; // J2SE interfaces import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.logging.Logger; import org.geotools.data.DataSourceException; import org.geotools.data.FeatureReader; import org.geotools.data.Query; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.feature.CollectionEvent; import org.geotools.feature.CollectionListener; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureIterator; import org.geotools.feature.collection.FeatureIteratorImpl; import org.geotools.feature.collection.SimpleFeatureIteratorImpl; import org.geotools.feature.collection.SubFeatureCollection; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.filter.SortBy2; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.util.NullProgressListener; import org.opengis.feature.Feature; import org.opengis.feature.FeatureVisitor; import org.opengis.feature.IllegalAttributeException; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.filter.Filter; import org.opengis.filter.sort.SortBy; import org.opengis.geometry.BoundingBox; /** * Origional implementation of FeatureCollection using a TreeMap for internal storage. * <p> * The features are maintained in an internal TreeMap resuling in a collection that is sorted by * feature id mimicking the contents of a shapefile. * <p> * This implementation of FeatureCollection is painfully correct. * <ul> * <li>To better mimic an actual data file each feature that is returned is a copy</li> * <li>The pain comes if you were expecting performance - the overhead associated with copying is * significant</li> * <li>Since a TreeSet (and not a spatial index) is used to store contents their this feature * collection does not support fast spatial operations.</li> * </ul> * With this in mind this implementation is recommended for being careful or you are encountering * problems between threads when debugging. It is excellent for its intended purpose of test cases. * * @author Ian Schneider * * @source $URL$ * http://svn.osgeo.org/geotools/trunk/modules/library/main/src/main/java/org/geotools/ * data/collection/TreeSetFeatureCollection.java $ * @version $Id$ */ public class TreeSetFeatureCollection implements SimpleFeatureCollection { protected static Logger LOGGER = org.geotools.util.logging.Logging .getLogger("org.geotools.data.collection"); /** * Contents of collection, referenced by FeatureID. * <p> * This use will result in collections that are sorted by FID, in keeping with shapefile etc... * </p> */ private SortedMap<String, SimpleFeature> contents = new TreeMap<String, SimpleFeature>(); /** Internal envelope of bounds. */ private ReferencedEnvelope bounds = null; /** * listeners */ protected List<CollectionListener> listeners = new ArrayList<CollectionListener>(); /** * id used when serialized to gml */ protected String id; /** * FeatureType of contents. */ protected SimpleFeatureType schema; /** * This constructor should not be used by client code. * * @param collection * SimpleFeatureCollection to copy into memory */ public TreeSetFeatureCollection(FeatureCollection<SimpleFeatureType, SimpleFeature> collection) { this(collection.getID(), collection.getSchema()); addAll(collection); } /** * This constructor should not be used by client code. * <p> * Opportunistic reuse is encouraged, but only for the purposes of testing or other specialized * uses. Normal creation should occur through * <code>org.geotools.core.FeatureCollections.newCollection()</code> allowing applications to * customize any generated collections. * </p> * * @param id * may be null ... feature id * @param featureType * optional, may be null */ public TreeSetFeatureCollection(String id, SimpleFeatureType memberType) { this.id = id == null ? "featureCollection" : id; this.schema = memberType; } /** * Gets the bounding box for the features in this feature collection. * * @return the envelope of the geometries contained by this feature collection. */ public ReferencedEnvelope getBounds() { if (bounds == null) { bounds = new ReferencedEnvelope(); for (Iterator i = contents.values().iterator(); i.hasNext();) { BoundingBox geomBounds = ((SimpleFeature) i.next()).getBounds(); // IanS - as of 1.3, JTS expandToInclude ignores "null" Envelope // and simply adds the new bounds... // This check ensures this behavior does not occur. if (!geomBounds.isEmpty()) { bounds.include(geomBounds); } } } return bounds; } /** * To let listeners know that something has changed. */ protected void fireChange(SimpleFeature[] features, int type) { bounds = null; CollectionEvent cEvent = new CollectionEvent(this, features, type); for (int i = 0, ii = listeners.size(); i < ii; i++) { ((CollectionListener) listeners.get(i)).collectionChanged(cEvent); } } protected void fireChange(SimpleFeature feature, int type) { fireChange(new SimpleFeature[] { feature }, type); } protected void fireChange(Collection<SimpleFeature> coll, int type) { SimpleFeature[] features = new SimpleFeature[coll.size()]; features = (SimpleFeature[]) coll.toArray(features); fireChange(features, type); } /** * Ensures that this collection contains the specified element (optional operation). Returns * <tt>true</tt> if this collection changed as a result of the call. (Returns <tt>false</tt> if * this collection does not permit duplicates and already contains the specified element.) * * <p> * Collections that support this operation may place limitations on what elements may be added * to this collection. In particular, some collections will refuse to add <tt>null</tt> * elements, and others will impose restrictions on the type of elements that may be added. * Collection classes should clearly specify in their documentation any restrictions on what * elements may be added. * </p> * * <p> * If a collection refuses to add a particular element for any reason other than that it already * contains the element, it <i>must</i> throw an exception (rather than returning <tt>false</tt> * ). This preserves the invariant that a collection always contains the specified element after * this call returns. * </p> * * @param o * element whose presence in this collection is to be ensured. * * @return <tt>true</tt> if this collection changed as a result of the call */ public boolean add(SimpleFeature o) { return add(o, true); } protected boolean add(SimpleFeature feature, boolean fire) { // This cast is neccessary to keep with the contract of Set! if (feature == null) return false; // cannot add null! final String ID = feature.getID(); if (ID == null) return false; // ID is required! if (contents.containsKey(ID)) return false; // feature all ready present if (this.schema == null) { this.schema = feature.getFeatureType(); } SimpleFeatureType childType = (SimpleFeatureType) getSchema(); // if ( childType==null ){ // //this.childType= // }else{ if (!feature.getFeatureType().equals(childType)) LOGGER.warning("Feature Collection contains a heterogeneous" + " mix of features"); // } // TODO check inheritance with FeatureType here!!! contents.put(ID, feature); if (fire) { fireChange(feature, CollectionEvent.FEATURES_ADDED); } return true; } /** * Adds all of the elements in the specified collection to this collection (optional operation). * The behavior of this operation is undefined if the specified collection is modified while the * operation is in progress. (This implies that the behavior of this call is undefined if the * specified collection is this collection, and this collection is nonempty.) * * @param collection * elements to be inserted into this collection. * * @return <tt>true</tt> if this collection changed as a result of the call * * @see #add(Object) */ @SuppressWarnings("unchecked") public boolean addAll(Collection collection) { // TODO check inheritance with FeatureType here!!! boolean changed = false; Iterator<SimpleFeature> iterator = collection.iterator(); try { List featuresAdded = new ArrayList(collection.size()); while (iterator.hasNext()) { SimpleFeature f = (SimpleFeature) iterator.next(); boolean added = add(f, false); changed |= added; if (added) featuresAdded.add(f); } if (changed) { fireChange(featuresAdded, CollectionEvent.FEATURES_ADDED); } return changed; } finally { if (collection instanceof FeatureCollection) { ((SimpleFeatureCollection) collection).close(iterator); } } } @SuppressWarnings("unchecked") public boolean addAll(FeatureCollection collection) { // TODO check inheritance with FeatureType here!!! boolean changed = false; Iterator<?> iterator = collection.iterator(); try { List<SimpleFeature> featuresAdded = new ArrayList<SimpleFeature>(collection.size()); while (iterator.hasNext()) { SimpleFeature f = (SimpleFeature) iterator.next(); boolean added = add(f, false); changed |= added; if (added) featuresAdded.add(f); } if (changed) { fireChange(featuresAdded, CollectionEvent.FEATURES_ADDED); } return changed; } finally { collection.close(iterator); } } /** * Removes all of the elements from this collection (optional operation). This collection will * be empty after this method returns unless it throws an exception. */ public void clear() { if (contents.isEmpty()) return; SimpleFeature[] oldFeatures = new SimpleFeature[contents.size()]; oldFeatures = (SimpleFeature[]) contents.values().toArray(oldFeatures); contents.clear(); fireChange(oldFeatures, CollectionEvent.FEATURES_REMOVED); } /** * Returns <tt>true</tt> if this collection contains the specified element. More formally, * returns <tt>true</tt> if and only if this collection contains at least one element <tt>e</tt> * such that <tt>(o==null ? * e==null : o.equals(e))</tt>. * * @param o * element whose presence in this collection is to be tested. * * @return <tt>true</tt> if this collection contains the specified element */ public boolean contains(Object o) { // The contract of Set doesn't say we have to cast here, but I think its // useful for client sanity to get a ClassCastException and not just a // false. if (!(o instanceof SimpleFeature)) return false; SimpleFeature feature = (SimpleFeature) o; final String ID = feature.getID(); return contents.containsKey(ID); // || contents.containsValue( feature ); } /** * Test for collection membership. * * @param collection * @return true if collection is completly covered */ public boolean containsAll(Collection collection) { Iterator iterator = collection.iterator(); try { while (iterator.hasNext()) { SimpleFeature feature = (SimpleFeature) iterator.next(); if (!contents.containsKey(feature.getID())) { return false; } } return true; } finally { if (collection instanceof FeatureCollection) { ((SimpleFeatureCollection) collection).close(iterator); } } } /** * Returns <tt>true</tt> if this collection contains no elements. * * @return <tt>true</tt> if this collection contains no elements */ public boolean isEmpty() { return contents.isEmpty(); } /** * Returns an iterator over the elements in this collection. There are no guarantees concerning * the order in which the elements are returned (unless this collection is an instance of some * class that provides a guarantee). * * @return an <tt>Iterator</tt> over the elements in this collection */ public Iterator<SimpleFeature> iterator() { final Iterator<SimpleFeature> iterator = contents.values().iterator(); return new Iterator<SimpleFeature>() { SimpleFeature currFeature = null; public boolean hasNext() { return iterator.hasNext(); } public SimpleFeature next() { currFeature = (SimpleFeature) iterator.next(); return currFeature; } public void remove() { iterator.remove(); fireChange(currFeature, CollectionEvent.FEATURES_REMOVED); } }; } /** * Gets a SimpleFeatureIterator of this feature collection. This allows iteration without having * to cast. * * @return the SimpleFeatureIterator for this collection. */ public SimpleFeatureIterator features() { return new SimpleFeatureIteratorImpl(this); } /** * Removes a single instance of the specified element from this collection, if it is present * (optional operation). More formally, removes an element <tt>e</tt> such that * <tt>(o==null ? e==null : * o.equals(e))</tt>, if this collection contains one or more such elements. Returns true if * this collection contained the specified element (or equivalently, if this collection changed * as a result of the call). * * @param o * element to be removed from this collection, if present. * * @return <tt>true</tt> if this collection changed as a result of the call */ public boolean remove(Object o) { if (!(o instanceof SimpleFeature)) return false; SimpleFeature f = (SimpleFeature) o; boolean changed = contents.values().remove(f); if (changed) { fireChange(f, CollectionEvent.FEATURES_REMOVED); } return changed; } /** * Removes all this collection's elements that are also contained in the specified collection * (optional operation). After this call returns, this collection will contain no elements in * common with the specified collection. * * @param collection * elements to be removed from this collection. * * @return <tt>true</tt> if this collection changed as a result of the call * * @see #remove(Object) * @see #contains(Object) */ @SuppressWarnings("unchecked") public boolean removeAll(Collection collection) { boolean changed = false; Iterator iterator = collection.iterator(); try { List removedFeatures = new ArrayList(collection.size()); while (iterator.hasNext()) { SimpleFeature f = (SimpleFeature) iterator.next(); boolean removed = contents.values().remove(f); if (removed) { changed = true; removedFeatures.add(f); } } if (changed) { fireChange(removedFeatures, CollectionEvent.FEATURES_REMOVED); } return changed; } finally { if (collection instanceof FeatureCollection) { ((SimpleFeatureCollection) collection).close(iterator); } } } /** * Retains only the elements in this collection that are contained in the specified collection * (optional operation). In other words, removes from this collection all of its elements that * are not contained in the specified collection. * * @param collection * elements to be retained in this collection. * * @return <tt>true</tt> if this collection changed as a result of the call * * @see #remove(Object) * @see #contains(Object) */ @SuppressWarnings("unchecked") public boolean retainAll(Collection collection) { List removedFeatures = new ArrayList(contents.size() - collection.size()); boolean modified = false; for (Iterator it = contents.values().iterator(); it.hasNext();) { SimpleFeature f = (SimpleFeature) it.next(); if (!collection.contains(f)) { it.remove(); modified = true; removedFeatures.add(f); } } if (modified) { fireChange(removedFeatures, CollectionEvent.FEATURES_REMOVED); } return modified; } /** * Returns the number of elements in this collection. If this collection contains more than * <tt>Integer.MAX_VALUE</tt> elements, returns <tt>Integer.MAX_VALUE</tt>. * * @return the number of elements in this collection */ public int size() { return contents.size(); } /** * Returns an array containing all of the elements in this collection. If the collection makes * any guarantees as to what order its elements are returned by its iterator, this method must * return the elements in the same order. * * <p> * The returned array will be "safe" in that no references to it are maintained by this * collection. (In other words, this method must allocate a new array even if this collection is * backed by an array). The caller is thus free to modify the returned array. * </p> * * <p> * This method acts as bridge between array-based and collection-based APIs. * </p> * * @return an array containing all of the elements in this collection */ public Object[] toArray() { return contents.values().toArray(); } /** * Returns an array containing all of the elements in this collection; the runtime type of the * returned array is that of the specified array. If the collection fits in the specified array, * it is returned therein. Otherwise, a new array is allocated with the runtime type of the * specified array and the size of this collection. * * <p> * If this collection fits in the specified array with room to spare (i.e., the array has more * elements than this collection), the element in the array immediately following the end of the * collection is set to <tt>null</tt>. This is useful in determining the length of this * collection <i>only</i> if the caller knows that this collection does not contain any * <tt>null</tt> elements.) * </p> * * <p> * If this collection makes any guarantees as to what order its elements are returned by its * iterator, this method must return the elements in the same order. * </p> * * <p> * Like the <tt>toArray</tt> method, this method acts as bridge between array-based and * collection-based APIs. Further, this method allows precise control over the runtime type of * the output array, and may, under certain circumstances, be used to save allocation costs * </p> * * <p> * Suppose <tt>l</tt> is a <tt>List</tt> known to contain only strings. The following code can * be used to dump the list into a newly allocated array of <tt>String</tt>: * * <pre> * String[] x = (String[]) v.toArray(new String[0]); * </pre> * * </p> * * <p> * Note that <tt>toArray(new Object[0])</tt> is identical in function to <tt>toArray()</tt>. * </p> * * @param a * the array into which the elements of this collection are to be stored, if it is * big enough; otherwise, a new array of the same runtime type is allocated for this * purpose. * * @return an array containing the elements of this collection */ @SuppressWarnings("unchecked") public Object[] toArray(Object[] a) { return contents.values().toArray(a != null ? a : new Object[contents.size()]); } public void close(FeatureIterator<SimpleFeature> close) { if (close instanceof FeatureIteratorImpl) { FeatureIteratorImpl<SimpleFeature> wrapper = (FeatureIteratorImpl<SimpleFeature>) close; wrapper.close(); } } public void close(Iterator close) { // nop } public FeatureReader<SimpleFeatureType, SimpleFeature> reader() throws IOException { final SimpleFeatureIterator iterator = features(); return new FeatureReader<SimpleFeatureType, SimpleFeature>() { public SimpleFeatureType getFeatureType() { return getSchema(); } public SimpleFeature next() throws IOException, IllegalAttributeException, NoSuchElementException { return iterator.next(); } public boolean hasNext() throws IOException { return iterator.hasNext(); } public void close() throws IOException { TreeSetFeatureCollection.this.close(iterator); } }; } public int getCount() throws IOException { return contents.size(); } public SimpleFeatureCollection collection() throws IOException { SimpleFeatureCollection copy = new TreeSetFeatureCollection(null, getSchema()); List<SimpleFeature> list = new ArrayList<SimpleFeature>(contents.size()); SimpleFeatureIterator iterator = features(); try { while (iterator.hasNext()) { SimpleFeature feature = iterator.next(); SimpleFeature duplicate; try { duplicate = SimpleFeatureBuilder.copy(feature); } catch (IllegalAttributeException e) { throw new DataSourceException("Unable to copy " + feature.getID(), e); } list.add(duplicate); } } finally { iterator.close(); } copy.addAll(list); return copy; } /** * Optimization time ... grab the fid set so other can quickly test membership during * removeAll/retainAll implementations. * * @return Set of fids. */ public Set<String> fids() { return Collections.unmodifiableSet(contents.keySet()); } public void accepts(org.opengis.feature.FeatureVisitor visitor, org.opengis.util.ProgressListener progress) { Iterator iterator = null; if (progress == null) progress = new NullProgressListener(); try { float size = size(); float position = 0; progress.started(); for (iterator = iterator(); !progress.isCanceled() && iterator.hasNext(); progress .progress(position++ / size)) { try { SimpleFeature feature = (SimpleFeature) iterator.next(); visitor.visit(feature); } catch (Exception erp) { progress.exceptionOccurred(erp); } } } finally { progress.complete(); close(iterator); } } /** * Will return an optimized subCollection based on access to the origional * MemoryFeatureCollection. * <p> * This method is intended in a manner similar to subList, example use: <code> * collection.subCollection( myFilter ).clear() * </code> * </p> * * @param filter * Filter used to determine sub collection. * @since GeoTools 2.2, Filter 1.1 */ public SimpleFeatureCollection subCollection(Filter filter) { if (filter == Filter.INCLUDE) { return this; } CollectionFeatureSource temp = new CollectionFeatureSource( this ); return temp.getFeatures(filter); } public SimpleFeatureCollection sort(SortBy order) { Query subQuery = new Query( getSchema().getTypeName() ); subQuery.setSortBy( new SortBy[]{ order } ); CollectionFeatureSource temp = new CollectionFeatureSource( this ); return temp.getFeatures(subQuery); } public void purge() { // no resources were harmed in the making of this FeatureCollection } public String getID() { return id; } public final void addListener(CollectionListener listener) throws NullPointerException { listeners.add(listener); } public final void removeListener(CollectionListener listener) throws NullPointerException { listeners.remove(listener); } public SimpleFeatureType getSchema() { return schema; } }