/* uDig - User Friendly Desktop Internet GIS client
* http://udig.refractions.net
* (C) 2004, Refractions Research Inc.
*
* 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 net.refractions.linecleaner.cleansing;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import net.refractions.linecleaner.FeatureUtil;
import net.refractions.linecleaner.GeometryUtil;
import net.refractions.linecleaner.LoggingSystem;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.geotools.data.DefaultQuery;
import org.geotools.data.FeatureStore;
import org.geotools.data.Query;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.feature.AttributeType;
import org.geotools.feature.AttributeTypeFactory;
import org.geotools.feature.Feature;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureCollections;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.FeatureType;
import org.geotools.feature.FeatureTypeBuilder;
import org.geotools.feature.FeatureTypeFactory;
import org.geotools.feature.IllegalAttributeException;
import org.geotools.feature.SchemaException;
import org.geotools.filter.FidFilter;
import org.geotools.filter.Filter;
import org.geotools.filter.FilterFactory;
import org.geotools.filter.FilterFactoryFinder;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.Point;
/**
* Runs through all the nodes in a featurestore, adding nodes at the
* closest point on all nearby features. Basically we're adding nodes
* at strategic points so that they'll be collapsed later by
* the EndNodesProcessor.
*
* @author myronwu
*/
public class NodeInsertionProcessor extends AbstractProcessor {
double distanceTolerance;
NewNodes newNodes;
LoggingSystem loggingSystem = LoggingSystem.getInstance();
/**
* @param store
* @param distanceTolerance
*/
public NodeInsertionProcessor(net.refractions.udig.project.internal.Map map, FeatureStore store, double distanceTolerance) {
super(map, store);
this.distanceTolerance = distanceTolerance;
this.newNodes = new NewNodes();
}
/**
*
* @param monitor
* @throws IOException
*/
protected void runInternal(IProgressMonitor monitor, PauseMonitor pauseMonitor) throws IOException {
if (monitor == null) monitor = new NullProgressMonitor();
this.loggingSystem.setCurrentAction(LoggingSystem.NODE_INSERTION);
this.loggingSystem.begin();
monitor.beginTask("Node Insertion: ", 2);
if (monitor.isCanceled()) {
return;
}
pauseIfNecessary(pauseMonitor);
insertNodes(new SubProgressMonitor(monitor, 1, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK),
pauseMonitor);
if (monitor.isCanceled()) {
return;
}
pauseIfNecessary(pauseMonitor);
this.newNodes.commitChanges(new SubProgressMonitor(monitor, 1, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK),
pauseMonitor);
monitor.done();
this.loggingSystem.finish();
}
// debugging method
/*
public void go() throws IOException {
insertNodes();
this.newNodes.dump();
this.newNodes.commitChanges();
// dump the nodes we're adding in for debugging purposes
try {
this.newNodes.writeShp(new URL("file:///C:/nodes.shp"));
} catch (Exception e) {
throw (RuntimeException) new RuntimeException( ).initCause( e );
}
}*/
protected void insertNodes(IProgressMonitor monitor, PauseMonitor pauseMonitor) throws IOException {
monitor.beginTask("", 2 * this.featureStore.getCount(Query.FIDS));
monitor.subTask("Calculating Nodes");
String typename = this.featureStore.getSchema().getTypeName();
String defaultGeom = this.featureStore.getSchema().getDefaultGeometry().getName();
DefaultQuery query = new DefaultQuery(typename, Filter.NONE, new String[] {defaultGeom});
FeatureIterator i = this.featureStore.getFeatures(query).features();
try {
while (i.hasNext()) {
Feature f = i.next();
String fid = f.getID();
Geometry geom = f.getDefaultGeometry();
processNode(getStartPoint(geom), fid);
monitor.worked(1);
if (monitor.isCanceled()) {
break;
}
pauseIfNecessary(pauseMonitor);
processNode(getEndPoint(geom), fid);
monitor.worked(1);
if (monitor.isCanceled()) {
break;
}
pauseIfNecessary(pauseMonitor);
}
} finally {
i.close();
monitor.done();
}
}
/**
*
* @param node
* @param fid Fid of the feature that node belongs to
*/
protected void processNode(Point node, String fid) {
FeatureIterator i = getNearbyFeatures(node).features();
try {
while (i.hasNext()) {
Feature f = i.next();
// getNearbyFeatures also returns f itself--exclude.
if (!f.getID().equals(fid)) {
nodeClosestPoint(f, node);
}
}
} finally {
i.close();
}
}
/**
* Add a node to f at the closest point to node.
* @param f
* @param node
*/
protected void nodeClosestPoint(Feature f, Point node) {
Coordinate closestPoint =
GeometryUtil.getClosestPoint(node.getCoordinate(), f.getDefaultGeometry());
// The bbox filter seems to bring back some features outside
// the bbox? Make a sanity check here to make sure the closest point
// found is within tolerance.
Point p = node.getFactory().createPoint(closestPoint);
if (p.distance(node) < this.distanceTolerance) {
this.newNodes.insertNode(f, closestPoint);
}
}
protected FeatureCollection getNearbyFeatures(Point p) {
FeatureCollection nearbyFeatures = null;
String typename = this.featureStore.getSchema().getTypeName();
String geomName = this.featureStore.getSchema().getDefaultGeometry().getName();
try {
nearbyFeatures = this.featureStore.getFeatures(
new DefaultQuery(typename, getBBoxFilter(p), new String[] { geomName }));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return nearbyFeatures;
}
protected Filter getBBoxFilter(Point node) {
return GeometryUtil.getBBoxFilter(this.featureStore, node, this.distanceTolerance);
}
protected Point getStartPoint(Geometry g) {
Coordinate start = g.getCoordinates()[0];
return g.getFactory().createPoint(start);
}
protected Point getEndPoint(Geometry g) {
Coordinate[] coords = g.getCoordinates();
return g.getFactory().createPoint(coords[coords.length - 1]);
}
/**
* Grab a feature and all its attributes from the FeatureStore by fid. This is
* necessary since the filters we work with are optimized when bringing back only
* fids and geometries.
* @param fid
* @return The feature identified by fid.
* @throws NoSuchElementException
* @throws IOException
*/
protected Feature getFeature(String fid)
throws NoSuchElementException, IOException {
FilterFactory ff = FilterFactoryFinder.createFilterFactory();
FidFilter filter = ff.createFidFilter(fid);
FeatureIterator features = this.featureStore.getFeatures(filter).features();
try{
return features.next();
}finally{
if( features!=null)
features.close();
}
}
/**
* Subdivide the feature f at a set of coordinates.
* @param f
* @param points
* @return A FeatureCollection containing all subdivisions of f at the given points.
* @throws IllegalAttributeException
*/
protected FeatureCollection subdivide(Feature f, Collection<Coordinate> points)
throws IllegalAttributeException {
FeatureCollection fc = FeatureCollections.newCollection();
LineString line = GeometryUtil.extractLine(f.getDefaultGeometry());
if (line != null) {
Collection<LineString> lines = GeometryUtil.subdivide(line, points);
for (LineString ls: lines) {
Feature fcopy = FeatureUtil.copy(f);
// the following is expected by shapefiles
MultiLineString mls = GeometryUtil.wrapInMultiLineString(ls);
fcopy.setDefaultGeometry(mls);
fc.add(fcopy);
}
}
return fc;
}
/**
* A class to cache all the nodes we're going to add to each feature.
* This is necessary because we can't modify a FeatureStore as we traverse it.
*/
private class NewNodes {
Map<String, Set<Coordinate>> nodeIndex =
new HashMap<String, Set<Coordinate>>();
/**
* Divide the features at the nodes we've calculated.
* @param pauseMonitor
* @throws IOException
*/
public void commitChanges(IProgressMonitor monitor, PauseMonitor pauseMonitor) throws IOException {
monitor.beginTask("", nodeIndex.size());
monitor.subTask("Inserting Nodes");
String typename = featureStore.getSchema().getTypeName();
String defaultGeom = featureStore.getSchema().getDefaultGeometry().getName();
Filter fidFilter = FeatureUtil.fidsToFidFilter(this.nodeIndex.keySet());
Query query = new DefaultQuery(typename, fidFilter);
MemoryFeatureIterator iter = new MemoryFeatureIterator(featureStore, map, query);
try {
while (iter.hasNext()) {
Feature f = iter.next();
String fid = f.getID();
Collection<Coordinate> newNodes = this.nodeIndex.get(fid);
FeatureCollection fc;
try {
fc = subdivide(f, newNodes);
// Here we pop the first subdivided feature and replace
// the original feature with it, then add the rest of
// the subdivisions. Modifying is faster than removing
// features.
Feature first = pop(fc);
modifyGeometry(fid, first.getDefaultGeometry());
featureStore.addFeatures(fc.reader());
// System.out.print("Replacing " + f.getID() + " ");
// FeatureUtil.dumpFeature(f);
// System.out.print("with ");
// FeatureUtil.dumpFeatureCollection(fc);
} catch (IllegalAttributeException e1) {
// TODO Handle IllegalAttributeException
throw (RuntimeException) new RuntimeException( ).initCause( e1 );
}
monitor.worked(1);
if (monitor.isCanceled()) {
break;
}
pauseIfNecessary(pauseMonitor);
}
} finally {
iter.close();
monitor.done();
}
}
protected void modifyGeometry(String fid, Geometry newGeom)
throws IOException {
FilterFactory ff = FilterFactoryFinder.createFilterFactory();
Filter filter = ff.createFidFilter(fid);
String geomName = featureStore.getSchema().getDefaultGeometry().getName();
AttributeType geomType = featureStore.getSchema().getAttributeType(geomName);
featureStore.modifyFeatures(geomType, newGeom, filter);
}
protected Feature pop(FeatureCollection fc) {
FeatureIterator i = fc.features();
Feature f = null;
try {
if (i.hasNext()) {
f = i.next();
fc.remove(f);
}
} finally {
i.close();
}
return f;
}
/**
* Record a new node for f.
* @param f
* @param node
*/
public void insertNode(Feature f, Coordinate node) {
LineString line = GeometryUtil.extractLine(f.getDefaultGeometry());
if (line != null) {
getNodes(f.getID()).add(node);
}
}
protected Set<Coordinate> getNodes(String fid) {
if (hasNodes(fid)) {
return this.nodeIndex.get(fid);
}
Set<Coordinate> nodes = new HashSet<Coordinate>();
this.nodeIndex.put(fid, nodes);
return nodes;
}
protected boolean hasNodes(String fid) {
return this.nodeIndex.containsKey(fid);
}
// debugging method
public void dump() {
System.out.println("Dump:");
for (Map.Entry<String, Set<Coordinate>> e: nodeIndex.entrySet()) {
String fid = e.getKey();
Set<Coordinate> nodes = e.getValue();
System.out.println(fid);
for (Coordinate c: nodes) {
System.out.println(c);
}
}
}
// debugging method
public void writeShp(URL url) throws IOException, IllegalAttributeException {
ShapefileDataStoreFactory factory = new ShapefileDataStoreFactory();
ShapefileDataStore ds = (ShapefileDataStore)factory.createDataStore(url);
FeatureTypeBuilder builder = FeatureTypeFactory.newInstance("test");
AttributeType geom = AttributeTypeFactory.newAttributeType("the_geom", Point.class);
FeatureType type;
try {
builder.addType(geom);
type = builder.getFeatureType();
} catch (SchemaException e1) {
// TODO Handle SchemaException
throw (RuntimeException) new RuntimeException( ).initCause( e1 );
}
ds.createSchema(type);
FeatureStore store = (FeatureStore)ds.getFeatureSource();
FeatureCollection fc = FeatureCollections.newCollection();
int count = 0;
GeometryFactory gf = new GeometryFactory();
for (Map.Entry<String, Set<Coordinate>> e: nodeIndex.entrySet()) {
Set<Coordinate> nodes = e.getValue();
for (Coordinate node: nodes) {
Feature f = type.create(new Object[] {gf.createPoint(node)}, Integer.toString(count));
count++;
fc.add(f);
}
}
store.addFeatures(fc.reader());
}
}
}