/* * The JTS Topology Suite is a collection of Java classes that * implement the fundamental operations required to validate a given * geo-spatial data set to a known topological specification. * * Copyright (C) 2001 Vivid Solutions * * 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; either * version 2.1 of the License, or (at your option) any later version. * * 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. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * For more information, contact: * * Vivid Solutions * Suite #1A * 2328 Government Street * Victoria BC V8T 5G5 * Canada * * (250)385-6040 * www.vividsolutions.com */ package com.revolsys.geometry.operation.linemerge; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Set; import java.util.TreeSet; import com.revolsys.geometry.model.Geometry; import com.revolsys.geometry.model.GeometryFactory; import com.revolsys.geometry.model.LineString; import com.revolsys.geometry.model.Lineal; import com.revolsys.geometry.model.Point; import com.revolsys.geometry.planargraph.DirectedEdge; import com.revolsys.geometry.planargraph.GraphComponent; import com.revolsys.geometry.planargraph.Node; import com.revolsys.geometry.planargraph.Subgraph; import com.revolsys.geometry.planargraph.algorithm.ConnectedSubgraphFinder; import com.revolsys.geometry.util.Assert; /** * Builds a sequence from a set of LineStrings so that * they are ordered end to end. * A sequence is a complete non-repeating list of the linear * components of the input. Each linestring is oriented * so that identical endpoints are adjacent in the list. * <p> * A typical use case is to convert a set of * unoriented geometric links * from a linear network * (e.g. such as block faces on a bus route) * into a continuous oriented path through the network. * <p> * The input linestrings may form one or more connected sets. * The input linestrings should be correctly noded, or the results may * not be what is expected. * The computed output is a single {@link Lineal} containing the ordered * linestrings in the sequence. * <p> * The sequencing employs the classic <b>Eulerian path</b> graph algorithm. * Since Eulerian paths are not uniquely determined, * further rules are used to * make the computed sequence preserve as much as possible of the input * ordering. * Within a connected subset of lines, the ordering rules are: * <ul> * <li>If there is degree-1 node which is the start * node of an linestring, use that node as the start of the sequence * <li>If there is a degree-1 node which is the end * node of an linestring, use that node as the end of the sequence * <li>If the sequence has no degree-1 nodes, use any node as the start * </ul> * * Note that not all arrangements of lines can be sequenced. * For a connected set of edges in a graph, * <i>Euler's Theorem</i> states that there is a sequence containing each edge once * <b>if and only if</b> there are no more than 2 nodes of odd degree. * If it is not possible to find a sequence, the {@link #isSequenceable()} method * will return <code>false</code>. * * @version 1.7 */ public class LineSequencer { private static Node findLowestDegreeNode(final Subgraph graph) { int minDegree = Integer.MAX_VALUE; Node minDegreeNode = null; for (final Iterator i = graph.nodeIterator(); i.hasNext();) { final Node node = (Node)i.next(); if (minDegreeNode == null || node.getDegree() < minDegree) { minDegree = node.getDegree(); minDegreeNode = node; } } return minDegreeNode; } /** * Finds an {@link DirectedEdge} for an unvisited edge (if any), * choosing the dirEdge which preserves orientation, if possible. * * @param node the node to examine * @return the dirEdge found, or <code>null</code> if none were unvisited */ private static DirectedEdge findUnvisitedBestOrientedDE(final Node node) { DirectedEdge wellOrientedDE = null; DirectedEdge unvisitedDE = null; for (final Object element : node.getOutEdges()) { final DirectedEdge de = (DirectedEdge)element; if (!de.getEdge().isVisited()) { unvisitedDE = de; if (de.getEdgeDirection()) { wellOrientedDE = de; } } } if (wellOrientedDE != null) { return wellOrientedDE; } return unvisitedDE; } /** * Tests whether a {@link Geometry} is sequenced correctly. * {@link LineString}s are trivially sequenced. * {@link Lineal}s are checked for correct sequencing. * Otherwise, <code>isSequenced</code> is defined * to be <code>true</code> for geometries that are not lineal. * * @param geom the geometry to test * @return <code>true</code> if the geometry is sequenced or is not lineal */ public static boolean isSequenced(final Geometry geom) { if (!(geom instanceof Lineal) || geom instanceof LineString) { return true; } final Lineal lineal = (Lineal)geom; // the nodes in all subgraphs which have been completely scanned final Set prevSubgraphNodes = new TreeSet(); Point lastNode = null; final List currNodes = new ArrayList(); for (int i = 0; i < lineal.getGeometryCount(); i++) { final LineString line = (LineString)lineal.getGeometry(i); final Point startNode = line.getPoint(0); final Point endNode = line.getPoint(line.getVertexCount() - 1); /** * If this linestring is connected to a previous subgraph, geom is not sequenced */ if (prevSubgraphNodes.contains(startNode)) { return false; } if (prevSubgraphNodes.contains(endNode)) { return false; } if (lastNode != null) { if (!startNode.equals(lastNode)) { // start new connected sequence prevSubgraphNodes.addAll(currNodes); currNodes.clear(); } } currNodes.add(startNode); currNodes.add(endNode); lastNode = endNode; } return true; } public static Geometry sequence(final Geometry geom) { final LineSequencer sequencer = new LineSequencer(); sequencer.add(geom); return sequencer.getSequencedLineStrings(); } // initialize with default, in case no lines are input private GeometryFactory factory = GeometryFactory.DEFAULT_3D; private final LineMergeGraph graph = new LineMergeGraph(); private boolean isRun = false; private boolean isSequenceable = false; private int lineCount = 0; private Geometry sequencedGeometry = null; /** * Adds a {@link Collection} of {@link Geometry}s to be sequenced. * May be called multiple times. * Any dimension of Geometry may be added; the constituent linework will be * extracted. * * @param geometries a Collection of geometries to add */ 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 sequenced. * May be called multiple times. * Any dimension of Geometry may be added; the constituent linework will be * extracted. * * @param geometry the geometry to add */ public void add(final Geometry geometry) { for (final LineString line : geometry.getGeometryComponents(LineString.class)) { addLine(line); } } private void addLine(final LineString lineString) { if (this.factory == null) { this.factory = lineString.getGeometryFactory(); } this.graph.addEdge(lineString); this.lineCount++; } private void addReverseSubpath(DirectedEdge de, final ListIterator lit, final boolean expectedClosed) { // trace an unvisited path *backwards* from this de final Node endNode = de.getToNode(); Node fromNode = null; while (true) { lit.add(de.getSym()); de.getEdge().setVisited(true); fromNode = de.getFromNode(); final DirectedEdge unvisitedOutDE = findUnvisitedBestOrientedDE(fromNode); // this must terminate, since we are continually marking edges as visited if (unvisitedOutDE == null) { break; } de = unvisitedOutDE.getSym(); } if (expectedClosed) { // the path should end at the toNode of this de, otherwise we have an // error Assert.isTrue(fromNode == endNode, "path not contiguous"); } } /** * Builds a geometry ({@link Lineal} ) * representing the sequence. * * @param sequences a List of Lists of DirectedEdges with * LineMergeEdges as their parent edges. * @return the sequenced geometry, or <code>null</code> if no sequence exists */ private Geometry buildSequencedGeometry(final List sequences) { final List lines = new ArrayList(); for (final Iterator i1 = sequences.iterator(); i1.hasNext();) { final List seq = (List)i1.next(); for (final Iterator i2 = seq.iterator(); i2.hasNext();) { final DirectedEdge de = (DirectedEdge)i2.next(); final LineMergeEdge e = (LineMergeEdge)de.getEdge(); final LineString line = e.getLine(); LineString lineToAdd = line; if (!de.getEdgeDirection() && !line.isClosed()) { lineToAdd = line.reverse(); } lines.add(lineToAdd); } } if (lines.size() == 0) { return this.factory.lineString(); } else { return this.factory.buildGeometry(lines); } } private void computeSequence() { if (this.isRun) { return; } this.isRun = true; final List sequences = findSequences(); if (sequences == null) { return; } this.sequencedGeometry = buildSequencedGeometry(sequences); this.isSequenceable = true; final int finalLineCount = this.sequencedGeometry.getGeometryCount(); Assert.isTrue(this.lineCount == finalLineCount, "Lines were missing from result"); Assert.isTrue(this.sequencedGeometry instanceof Lineal, "Result is not lineal"); } private List findSequence(final Subgraph graph) { GraphComponent.setVisited(graph.edgeIterator(), false); final Node startNode = findLowestDegreeNode(graph); final DirectedEdge startDE = startNode.getOutEdges().iterator().next(); final DirectedEdge startDESym = startDE.getSym(); final List seq = new LinkedList(); final ListIterator lit = seq.listIterator(); addReverseSubpath(startDESym, lit, false); while (lit.hasPrevious()) { final DirectedEdge prev = (DirectedEdge)lit.previous(); final DirectedEdge unvisitedOutDE = findUnvisitedBestOrientedDE(prev.getFromNode()); if (unvisitedOutDE != null) { addReverseSubpath(unvisitedOutDE.getSym(), lit, true); } } /** * At this point, we have a valid sequence of graph DirectedEdges, but it * is not necessarily appropriately oriented relative to the underlying * geometry. */ final List orientedSeq = orient(seq); return orientedSeq; } private List findSequences() { final List sequences = new ArrayList(); final ConnectedSubgraphFinder csFinder = new ConnectedSubgraphFinder(this.graph); final List subgraphs = csFinder.getConnectedSubgraphs(); for (final Iterator i = subgraphs.iterator(); i.hasNext();) { final Subgraph subgraph = (Subgraph)i.next(); if (hasSequence(subgraph)) { final List seq = findSequence(subgraph); sequences.add(seq); } else { // if any subgraph cannot be sequenced, abort return null; } } return sequences; } /** * Returns the {@link Lineal} * built by the sequencing process, if one exists. * * @return the sequenced linestrings, * or <code>null</code> if a valid sequence does not exist */ public Geometry getSequencedLineStrings() { computeSequence(); return this.sequencedGeometry; } /** * Tests whether a complete unique path exists in a graph * using Euler's Theorem. * * @param graph the subgraph containing the edges * @return <code>true</code> if a sequence exists */ private boolean hasSequence(final Subgraph graph) { int oddDegreeCount = 0; for (final Iterator i = graph.nodeIterator(); i.hasNext();) { final Node node = (Node)i.next(); if (node.getDegree() % 2 == 1) { oddDegreeCount++; } } return oddDegreeCount <= 2; } /** * Tests whether the arrangement of linestrings has a valid * sequence. * * @return <code>true</code> if a valid sequence exists. */ public boolean isSequenceable() { computeSequence(); return this.isSequenceable; } /** * Computes a version of the sequence which is optimally * oriented relative to the underlying geometry. * <p> * Heuristics used are: * <ul> * <li>If the path has a degree-1 node which is the start * node of an linestring, use that node as the start of the sequence * <li>If the path has a degree-1 node which is the end * node of an linestring, use that node as the end of the sequence * <li>If the sequence has no degree-1 nodes, use any node as the start * (NOTE: in this case could orient the sequence according to the majority of the * linestring orientations) * </ul> * * @param seq a List of DirectedEdges * @return a List of DirectedEdges oriented appropriately */ private List orient(final List seq) { final DirectedEdge startEdge = (DirectedEdge)seq.get(0); final DirectedEdge endEdge = (DirectedEdge)seq.get(seq.size() - 1); final Node startNode = startEdge.getFromNode(); final Node endNode = endEdge.getToNode(); boolean flipSeq = false; final boolean hasDegree1Node = startNode.getDegree() == 1 || endNode.getDegree() == 1; if (hasDegree1Node) { boolean hasObviousStartNode = false; // test end edge before start edge, to make result stable // (ie. if both are good starts, pick the actual start if (endEdge.getToNode().getDegree() == 1 && endEdge.getEdgeDirection() == false) { hasObviousStartNode = true; flipSeq = true; } if (startEdge.getFromNode().getDegree() == 1 && startEdge.getEdgeDirection() == true) { hasObviousStartNode = true; flipSeq = false; } // since there is no obvious start node, use any node of degree 1 if (!hasObviousStartNode) { // check if the start node should actually be the end node if (startEdge.getFromNode().getDegree() == 1) { flipSeq = true; // if the end node is of degree 1, it is properly the end node } } } // if there is no degree 1 node, just use the sequence as is // (Could insert heuristic of taking direction of majority of lines as // overall direction) if (flipSeq) { return reverse(seq); } return seq; } /** * Reverse the sequence. * This requires reversing the order of the dirEdges, and flipping * each dirEdge as well * * @param seq a List of DirectedEdges, in sequential order * @return the reversed sequence */ private List reverse(final List seq) { final LinkedList newSeq = new LinkedList(); for (final Iterator i = seq.iterator(); i.hasNext();) { final DirectedEdge de = (DirectedEdge)i.next(); newSeq.addFirst(de.getSym()); } return newSeq; } }