package net.refractions.linecleaner.cleansing;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import net.refractions.linecleaner.GeometryUtil;
import net.refractions.linecleaner.LoggingSystem;
import net.refractions.udig.project.internal.Map;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultQuery;
import org.geotools.data.FeatureReader;
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.FeatureIterator;
import org.geotools.feature.FeatureType;
import org.geotools.feature.FeatureTypeFactory;
import org.geotools.feature.IllegalAttributeException;
import org.geotools.filter.FidFilter;
import org.geotools.filter.Filter;
import org.geotools.filter.FilterFactoryFinder;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
*
* See the file doc/end-nodes.txt for the algorithm that this is based on.
*
* @author rgould
*
*/
public class EndNodesProcessor extends AbstractProcessor {
public static final double DEFAULT_DISTANCE_TOLERANCE = 25;
public static final double DEFAULT_AREA_TOLERANCE = 3000;
private double distanceTolerance;
private ArrayList<NodeCollection> nodeCollections;
private java.util.Map<Node, NodeCollection> collectionIndex;
private double areaTolerance;
private long startTime;
LoggingSystem loggingSystem = LoggingSystem.getInstance();
private String typename;
private String defaultGeom;
/**
*
* @param dataStore - datastore to perform the operation on
* @param distanceTolerance - minimum distance at which end-nodes are snapped together
* @param areaTolerance - groups of end-nodes with area greater than this are flagged/ignored
* @param level - sets the level of the logger. defaults to Level.WARNING
*/
public EndNodesProcessor (Map map, FeatureStore featureStore, double distanceTolerance,
double areaTolerance) {
super(map, featureStore);
this.distanceTolerance = distanceTolerance;
this.areaTolerance = areaTolerance;
this.nodeCollections = new ArrayList<NodeCollection>();
this.collectionIndex = new HashMap<Node, NodeCollection>();
this.typename = featureStore.getSchema().getTypeName();
this.defaultGeom = featureStore.getSchema().getDefaultGeometry().getName();
}
public void printNodeCollections() {
System.out.println("Node Collections: Size: " +nodeCollections.size());
for (int i = 0; i < nodeCollections.size(); i++) {
NodeCollection nc = nodeCollections.get(i);
System.out.print("NodeCollection["+i+"] (Flagged:"+ nc.isFlagged() +") (Centroid: " + nc.calculateAveragePoint() + ") contains EndNodes: ");
Iterator iter = nc.iterator();
while (iter.hasNext()) {
Point point = (Point) iter.next();
String fid = (String) point.getUserData();
System.out.print(fid+" ");
}
System.out.println();
}
}
protected void runInternal(IProgressMonitor monitor, PauseMonitor pauseMonitor) throws IOException {
if (monitor == null) monitor = new NullProgressMonitor();
loggingSystem.setCurrentAction(LoggingSystem.END_NODES);
loggingSystem.begin();
monitor.beginTask("End Nodes: ", 2);
if (monitor.isCanceled()) {
return;
}
pauseIfNecessary(pauseMonitor);
setupNodeCollections(new SubProgressMonitor(monitor, 1, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK),
pauseMonitor);
if (isDebugging()) {
try {
dumpNodeCollections();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if (monitor.isCanceled()) {
return;
}
pauseIfNecessary(pauseMonitor);
processNodeCollections(new SubProgressMonitor(monitor, 1, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK),
pauseMonitor);
monitor.done();
loggingSystem.fine("####################################################");
int count = 0;
loggingSystem.fine("FLAGGED NODE COLLECTIONS: ");
for (NodeCollection collection : getNodeCollections()) {
if (collection.isFlagged()) {
count++;
Envelope bbox = collection.getEnvelope();
double area = bbox.getHeight() * bbox.getWidth();
loggingSystem.fine(collection.toString() + "Area size:"+ area);
}
}
loggingSystem.finish(count);
loggingSystem.fine("TOTAL FLAGGED NODE COLLECTIONS: " + count);
loggingSystem.fine("####################################################");
}
private void dumpNodeCollections() throws Exception {
AttributeType geom = AttributeTypeFactory.newAttributeType("the_geom", Polygon.class);
AttributeType flagged = AttributeTypeFactory.newAttributeType("flagged", Boolean.class);
AttributeType size = AttributeTypeFactory.newAttributeType("size", Integer.class);
FeatureType ftNode = FeatureTypeFactory.newFeatureType(new AttributeType[] {geom, flagged, size}, "nodeCollection");
ArrayList<Feature> features = new ArrayList<Feature>(nodeCollections.size());
GeometryFactory factory = new GeometryFactory();
for (NodeCollection collection : nodeCollections) {
Coordinate[] coords = new Coordinate[5];
Envelope envelope = collection.getEnvelope();
coords[0] = new Coordinate(envelope.getMinX(), envelope.getMinY());
coords[1] = new Coordinate(envelope.getMinX(), envelope.getMaxY());
coords[2] = new Coordinate(envelope.getMaxX(), envelope.getMaxY());
coords[3] = new Coordinate(envelope.getMaxX(), envelope.getMinY());
coords[4] = coords[0];
LinearRing ring = factory.createLinearRing(coords);
Polygon polygon = factory.createPolygon(ring, null);
Feature newFeature = ftNode.create(new Object[] { polygon, collection.isFlagged() , collection.size()} );
features.add(newFeature);
}
String typename = featureStore.getSchema().getTypeName();
String tmpDir = System.getProperty("java.io.tmpdir");
String separator = System.getProperty("file.separator");
File file = new File(tmpDir + separator + typename + "-pre"+getName()+"NCDUMP"+".shp");
ShapefileDataStoreFactory dsFactory = new ShapefileDataStoreFactory();
ShapefileDataStore ds = (ShapefileDataStore)dsFactory.createDataStore(file.toURL());
ds.createSchema(ftNode);
FeatureStore newStore = (FeatureStore) ds.getFeatureSource();
FeatureReader reader = DataUtilities.reader(features);
newStore.addFeatures(reader);
}
/**
* Run through the features and set up each end-node into a collection
* that includes other end-nodes that are nearby.
* @throws IOException
*
*/
public void setupNodeCollections(IProgressMonitor monitor, PauseMonitor pauseMonitor) throws IOException {
loggingSystem.fine("******************************");
loggingSystem.fine("Begin: Set up NodeCollections.");
monitor.beginTask("", 2*featureStore.getCount(Query.FIDS));
monitor.subTask("Setup");
Query query = new DefaultQuery(this.typename, Filter.NONE, new String[] {this.defaultGeom});
MemoryFeatureIterator iter = new MemoryFeatureIterator(featureStore, map, query);
try {
int count = 0;
while (iter.hasNext()) {
Feature feature = (Feature) iter.next();
Geometry geometry = feature.getDefaultGeometry();
Coordinate start = geometry.getCoordinates()[0];
Coordinate end = geometry.getCoordinates()[geometry.getCoordinates().length-1];
Point startPoint = geometry.getFactory().createPoint(start);
Point endPoint = geometry.getFactory().createPoint(end);
startPoint.setUserData(feature.getID());
endPoint.setUserData(feature.getID());
processEndNode(startPoint);
monitor.worked(1);
if (monitor.isCanceled()) {
break;
}
processEndNode(endPoint);
monitor.worked(1);
if (monitor.isCanceled()) {
break;
}
if ((count%100) == 0) {
double time = System.currentTimeMillis();
time = time-startTime;
loggingSystem.fine("[SetupNodeCollections]: Processing feature #"+count+". Time is at " + time + "ms.");
}
count++;
}
} finally {
iter.close();
monitor.done();
}
loggingSystem.fine("Finished setting up node collections. Total collections: " + getNodeCollections().size() );
}
/**
* Given an end-node (endNodeA), assign it to a NodeCollection for later
* processing.
*
* See the file doc/end-nodes.txt for the algorithm that this is based on.
*
* @param endNodeA
*/
private void processEndNode(Point endNodeA) {
loggingSystem.finest("Begin processing endNode: " + endNodeToString(endNodeA));
Collection<Point> nearby = findNearbyNodes(distanceTolerance, endNodeA);
NodeCollection collectionA = findNodeCollection(endNodeA);
Iterator nearbyIter = nearby.iterator();
while (nearbyIter.hasNext()) {
Point endNodeB = (Point) nearbyIter.next();
NodeCollection collectionB = findNodeCollection(endNodeB);
if (collectionA != null && collectionB != null) {
if (collectionA == collectionB) {
continue;
} else {
loggingSystem.fine("Merging nodecollections: "+ endNodeToString(endNodeA) + " and " + endNodeToString(endNodeB));
mergeCollections(collectionA, collectionB);
continue;
}
}
if (collectionA != null) {
loggingSystem.finest("Adding " + endNodeToString(endNodeB) + " to the collection of " + endNodeToString(endNodeA));
addToCollection(collectionA, endNodeB);
} else {
if (collectionB != null) {
loggingSystem.finest("Adding " + endNodeToString(endNodeA) + " to the collection of " + endNodeToString(endNodeB));
addToCollection(collectionB, endNodeA);
collectionA = collectionB; //This prevents us from having to do findNodeCollection(endNodeA) on every iteration. They ARE the same collections.
} else {
loggingSystem.finest("Creating a new NodeCollection for " + endNodeToString(endNodeA) + " and " +endNodeToString(endNodeB));
collectionA = createNewNodeCollection();
addToCollection(collectionA, endNodeA);
addToCollection(collectionA, endNodeB);
}
}
}
}
private void mergeCollections(NodeCollection collectionA, NodeCollection collectionB) {
collectionA.merge(collectionB);
getNodeCollections().remove(collectionB);
// update the index
Iterator i = collectionB.iterator();
while (i.hasNext()) {
Point p = (Point) i.next();
Node n = new Node(p, (String) p.getUserData());
collectionIndex.put(n, collectionA);
}
}
private void addToCollection(NodeCollection collectionA, Point endNodeB) {
collectionA.add(endNodeB);
collectionIndex.put(new Node(endNodeB, (String) endNodeB.getUserData()), collectionA);
}
/**
* Performs the real processing on the node collections.
*
* In short:
* Run through each nodeCollection. If its collective area
* is too large, flag it.
*
* Calculate an average point. If the collection is not flagged,
* move every end-node inside the collection to that point.
*
* See the file doc/end-nodes.txt for the algorithm that this is based on.
* @param monitor
* @throws IOException
*
*/
private void processNodeCollections(IProgressMonitor monitor, PauseMonitor pauseMonitor)
throws IOException {
loggingSystem.fine("******************************");
loggingSystem.fine("Begin: Processing Node Collections");
int count = 0;
monitor.beginTask("", getNodeCollections().size());
monitor.subTask("Processing");
for (NodeCollection collectionC : getNodeCollections()) {
Envelope bbox = collectionC.getEnvelope();
if (!isTolerableAreaSize(bbox)) {
double area = bbox.getHeight() * bbox.getWidth();
loggingSystem.info("Node Collection has a total area ("+area+") greater than " + this.areaTolerance +". Collection: "+ collectionC);
collectionC.setFlagged(true);
}
Point averagePoint = collectionC.calculateAveragePoint();
if (!collectionC.isFlagged()) {
repositionNodes(collectionC, averagePoint);
}
// double time = System.currentTimeMillis();
// time = time-startTime;
// LOGGER.fine("Finished processing NodeCollection #"+count+" Size: " +collectionC.size()+
// ". Time is at " + time + "ms.");
// Runtime runtime = Runtime.getRuntime();
// LOGGER.finer("MEMORY: Total: "+runtime.totalMemory()+" Max: "+runtime.maxMemory()+" Free: " + runtime.freeMemory());
count++;
monitor.worked(1);
if (monitor.isCanceled()) {
break;
}
pauseIfNecessary(pauseMonitor);
}
monitor.done();
// try {
// featureStore.getTransaction().commit();
// } catch (IOException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
}
/**
* For every node that is in collectionC, reposition it so that it is
* located at point "averagePoint".
*
* Note that this method does not call commit() or close() on the
* dataStore.
*
* @param collectionC
* @param averagePoint
* @throws IOException
*/
private void repositionNodes(NodeCollection collectionC, Point averagePoint) throws IOException {
loggingSystem.fine("Reposition Nodes to "+averagePoint+" for NodeCollection: " + collectionC);
Iterator iter = collectionC.iterator();
while (iter.hasNext()) {
Point point = (Point) iter.next();
String fid = (String) point.getUserData();
FidFilter fidFilter = FilterFactoryFinder.createFilterFactory().createFidFilter(fid);
Feature feature = null;
Query query = new DefaultQuery(this.typename, fidFilter, new String[] {this.defaultGeom});
MemoryFeatureIterator iter2 = new MemoryFeatureIterator(featureStore, map, query);
try {
feature = (Feature) iter2.next();
} finally {
iter2.close();
}
Geometry geometry = feature.getDefaultGeometry();
boolean beginning = false;
Coordinate[] coords = geometry.getCoordinates();
Coordinate start = coords[0];
if (start.equals2D(point.getCoordinate())) {
beginning = true;
}
loggingSystem.finest("REPOSITIONING: BEFORE: " + geometry);
if (beginning) {
geometry.getCoordinates()[0].setCoordinate(averagePoint.getCoordinate());
} else {
geometry.getCoordinates()[geometry.getCoordinates().length-1].setCoordinate(averagePoint.getCoordinate());
}
loggingSystem.finest("REPOSITIONING: AFTER: " + geometry);
String xpath = feature.getFeatureType().getDefaultGeometry().getName();
try {
feature.setAttribute(xpath, geometry);
} catch (IllegalAttributeException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
AttributeType attributeType = feature.getFeatureType().getAttributeType(xpath);
this.featureStore.modifyFeatures(attributeType, geometry, fidFilter);
}
}
/**
* Returns true of the area of bbox is less than this.areaTolerance.
*
* @param bbox
* @return
*/
private boolean isTolerableAreaSize(Envelope bbox) {
if (areaTolerance <= 0) {
return true;
}
double area = bbox.getHeight() * bbox.getWidth();
return area <= areaTolerance;
}
/**
* Searches all known NodeCollections for "endNode" and returns the
* containing NodeCollection, if it is found. Otherwise, it returns
* null.
*
* @param endNode
* @return
*/
private NodeCollection findNodeCollection( Point endNode ) {
Node n = new Node(endNode, (String) endNode.getUserData());
NodeCollection nc = null;
if (collectionIndex.containsKey(n)) {
nc = collectionIndex.get(n);
}
return nc;
}
public List<NodeCollection> getNodeCollections() {
return nodeCollections;
}
/**
* Creates a new NodeCollection and adds it to the local pool
*
* @return
*/
private NodeCollection createNewNodeCollection() {
NodeCollection newNC = new NodeCollection();
getNodeCollections().add(newNC);
return newNC;
}
/**
* Given a node (endNode), locate every other node that is within a
* certain distance (toleranceDistance). Return each of those endNodes.
*
* @param toleranceDistance
* @param endNode
* @return
*/
private Collection<Point> findNearbyNodes( double toleranceDistance, Point endNode ) {
ArrayList<Point> nearbyNodes = new ArrayList<Point>();
FeatureCollection featureCollection = null;
String geomName = featureStore.getSchema().getDefaultGeometry().getName();
try {
featureCollection = this.featureStore.getFeatures(
new DefaultQuery(typename, getBBoxFilter(endNode), new String[] { geomName }));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
FeatureIterator iter = featureCollection.features();
try {
while (iter.hasNext()) {
Feature feature = (Feature) iter.next();
Geometry geometry = feature.getDefaultGeometry();
Coordinate start = geometry.getCoordinates()[0];
Coordinate end = geometry.getCoordinates()[geometry.getCoordinates().length-1];
Point startPoint = geometry.getFactory().createPoint(start);
Point endPoint = geometry.getFactory().createPoint(end);
startPoint.setUserData(feature.getID());
endPoint.setUserData(feature.getID());
if (!endNode.equals(startPoint) &&
endNode.isWithinDistance(startPoint, toleranceDistance)) {
loggingSystem.finest(endNodeToString(endNode)+" is within distance of " + endNodeToString(startPoint));
nearbyNodes.add(startPoint);
}
if (!endNode.equals(endPoint) &&
endNode.isWithinDistance(endPoint, toleranceDistance)) {
loggingSystem.finest(endNodeToString(endNode)+" is within distance of " + endNodeToString(endPoint));
nearbyNodes.add(endPoint);
}
}
} finally {
featureCollection.close(iter);
}
return nearbyNodes;
}
public static String endNodeToString(Point endNode) {
return "[FID: '"+endNode.getUserData()+"' Coord: ("
+endNode.getCoordinate().x+", "
+endNode.getCoordinate().y+")]";
}
private Filter getBBoxFilter(Point endNode) {
return GeometryUtil.getBBoxFilter(this.featureStore, endNode, this.distanceTolerance);
}
// simple struct combining Point and fid to hash NodeCollection against.
private class Node {
public final Coordinate c; // use Coordinate. Point doesn't have hashCode()
public final String fid;
public Node(Point p, String fid) {
this.c = p.getCoordinate();
this.fid = fid;
}
@Override
public boolean equals(Object obj) {
boolean equals = false;
if (obj instanceof Node) {
Node q = (Node) obj;
equals = c.equals(q.c) && fid.equals(q.fid);
}
return equals;
}
@Override
public int hashCode() {
// TODO this is a valid hash, but is it a good one?
return c.hashCode() + fid.hashCode();
}
}
}