package com.revolsys.geometry.dissolve; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Stack; import com.revolsys.geometry.edgegraph.HalfEdge; import com.revolsys.geometry.edgegraph.MarkHalfEdge; import com.revolsys.geometry.model.Geometry; import com.revolsys.geometry.model.GeometryFactory; import com.revolsys.geometry.model.LineString; import com.revolsys.geometry.model.PointList; /** * Dissolves the linear components * from a collection of {@link Geometry}s * into a set of maximal-length {@link Linestring}s * in which every unique segment appears once only. * The output linestrings run between node vertices * of the input, which are vertices which have * either degree 1, or degree 3 or greater. * <p> * Use cases for dissolving linear components * include generalization * (in particular, simplifying polygonal coverages), * and visualization * (in particular, avoiding symbology conflicts when * depicting shared polygon boundaries). * <p> * This class does <b>not</b> node the input lines. * If there are line segments crossing in the input, * they will still cross in the output. * * @author Martin Davis * */ public class LineDissolver { /** * Dissolves the linear components in a geometry. * * @param g the geometry to dissolve * @return the dissolved lines */ public static Geometry dissolve(final Geometry g) { final LineDissolver d = new LineDissolver(); d.add(g); return d.getResult(); } private GeometryFactory factory; private final DissolveEdgeGraph graph; private final List lines = new ArrayList(); private final Stack nodeEdgeStack = new Stack(); private Geometry result; private DissolveHalfEdge ringStartEdge; public LineDissolver() { this.graph = new DissolveEdgeGraph(); } /** * Adds a collection of Geometries to be processed. May be called multiple times. * Any dimension of Geometry may be added; the constituent linework will be * extracted. * * @param geometries the geometries to be line-merged */ public void add(final Collection geometries) { for (final Iterator i = geometries.iterator(); i.hasNext();) { final Geometry geometry = (Geometry)i.next(); add(geometry); } } /** * Adds a {@link Geometry} to be dissolved. * Any number of geometries may be added by calling this method multiple times. * Any type of Geometry may be added. The constituent linework will be * extracted to be dissolved. * * @param geometry geometry to be line-merged */ public void add(final Geometry geometry) { for (final LineString line : geometry.getGeometryComponents(LineString.class)) { add(line); } } private void add(final LineString lineString) { if (this.factory == null) { this.factory = lineString.getGeometryFactory(); } final LineString seq = lineString; for (int i = 1; i < seq.getVertexCount(); i++) { final DissolveHalfEdge e = (DissolveHalfEdge)this.graph.addEdge(seq.getPoint(i - 1), seq.getPoint(i)); /** * Record source initial segments, so that they can be reflected in output when needed * (i.e. during formation of isolated rings) */ if (i == 1) { e.setStart(); } } } private void addLine(final PointList line) { this.lines.add(this.factory.lineString(line.toPointArray())); } /** * Builds a line starting from the given edge. * The start edge origin is a node (valence = 1 or >= 3), * unless it is part of a pure ring. * A pure ring has no other incident lines. * In this case the start edge may occur anywhere on the ring. * * The line is built up to the next node encountered, * or until the start edge is re-encountered * (which happens if the edges form a ring). * * @param eStart */ private void buildLine(final HalfEdge eStart) { final PointList line = new PointList(); DissolveHalfEdge e = (DissolveHalfEdge)eStart; this.ringStartEdge = null; MarkHalfEdge.markBoth(e); line.add(e.orig().newPoint(), false); // scan along the path until a node is found (if one exists) while (e.sym().degree() == 2) { updateRingStartEdge(e); final DissolveHalfEdge eNext = (DissolveHalfEdge)e.next(); // check if edges form a ring - if so, we're done if (eNext == eStart) { buildRing(this.ringStartEdge); return; } // add point to line, and move to next edge line.add(eNext.orig().newPoint(), false); e = eNext; MarkHalfEdge.markBoth(e); } // add final node line.add(e.dest().newPoint(), false); // queue up the final node edges stackEdges(e.sym()); // store the scanned line addLine(line); } /** * For each edge in stack * (which must originate at a node) * extracts the line it initiates. */ private void buildLines() { while (!this.nodeEdgeStack.empty()) { final HalfEdge e = (HalfEdge)this.nodeEdgeStack.pop(); if (MarkHalfEdge.isMarked(e)) { continue; } buildLine(e); } } private void buildRing(final HalfEdge eStartRing) { final PointList line = new PointList(); HalfEdge e = eStartRing; line.add(e.orig().newPoint(), false); // scan along the path until a node is found (if one exists) while (e.sym().degree() == 2) { final HalfEdge eNext = e.next(); // check if edges form a ring - if so, we're done if (eNext == eStartRing) { break; } // add point to line, and move to next edge line.add(eNext.orig().newPoint(), false); e = eNext; } // add final node line.add(e.dest().newPoint(), false); // store the scanned line addLine(line); } private void computeResult() { final Collection edges = this.graph.getVertexEdges(); for (final Iterator i = edges.iterator(); i.hasNext();) { final HalfEdge e = (HalfEdge)i.next(); if (MarkHalfEdge.isMarked(e)) { continue; } process(e); } this.result = this.factory.buildGeometry(this.lines); } /** * Gets the dissolved result as a MultiLineString. * * @return the dissolved lines */ public Geometry getResult() { if (this.result == null) { computeResult(); } return this.result; } private void process(final HalfEdge e) { HalfEdge eNode = e.prevNode(); // if edge is in a ring, just process this edge if (eNode == null) { eNode = e; } stackEdges(eNode); // extract lines from node edges in stack buildLines(); } /** * Adds edges around this node to the stack. * * @param node */ private void stackEdges(final HalfEdge node) { HalfEdge e = node; do { if (!MarkHalfEdge.isMarked(e)) { this.nodeEdgeStack.add(e); } e = e.oNext(); } while (e != node); } /** * Updates the tracked ringStartEdge * if the given edge has a lower origin * (using the standard {@link Coordinates} ordering). * * Identifying the lowest starting node meets two goals: * <ul> * <li>It ensures that isolated input rings are created using the original node and orientation * <li>For isolated rings formed from multiple input linestrings, * it provides a canonical node and orientation for the output * (rather than essentially random, and thus hard to test). * </ul> * * @param e */ private void updateRingStartEdge(DissolveHalfEdge e) { if (!e.isStart()) { e = (DissolveHalfEdge)e.sym(); if (!e.isStart()) { return; } } // here e is known to be a start edge if (this.ringStartEdge == null) { this.ringStartEdge = e; return; } if (e.orig().compareTo(this.ringStartEdge.orig()) < 0) { this.ringStartEdge = e; } } }