Ticket #21881: 21881_github.patch
File 21881_github.patch, 34.6 KB (added by , 5 months ago) |
---|
-
src/org/openstreetmap/josm/data/algorithms/Tarjan.java
1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.algorithms; 3 4 import org.openstreetmap.josm.data.osm.Node; 5 import org.openstreetmap.josm.data.osm.NodeGraph; 6 import org.openstreetmap.josm.tools.Pair; 7 import org.openstreetmap.josm.tools.Utils; 8 9 import java.util.ArrayDeque; 10 import java.util.ArrayList; 11 import java.util.Collection; 12 import java.util.Collections; 13 import java.util.Deque; 14 import java.util.HashMap; 15 import java.util.List; 16 import java.util.Map; 17 18 /** 19 * Tarjan's strongly connected components algorithm for JOSM. 20 * 21 * @author gaben 22 * @see <a href="https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm"> 23 * Tarjan's strongly connected components algorithm</a> 24 * @since xxx 25 */ 26 public final class Tarjan { 27 28 /** 29 * Used to remember visited nodes and its metadata. Key is used for storing 30 * the unique ID of the nodes instead of the full data to save space. 31 */ 32 private final Map<Long, TarjanHelper> registry; 33 34 /** Used to store the graph data as a map. */ 35 private final Map<Node, List<Node>> graphMap; 36 37 /** Used to store strongly connected components. NOTE: single nodes are not stored to save memory. */ 38 private final Collection<List<Node>> scc = new ArrayList<>(); 39 40 /** Used on algorithm runtime to keep track discovery progress. */ 41 private final Deque<Node> stack = new ArrayDeque<>(); 42 43 /** Used on algorithm runtime to keep track discovery progress. */ 44 private int index; 45 46 /** 47 * Initialize the Tarjan's algorithm. 48 * 49 * @param graph graph data in NodeGraph object format 50 */ 51 public Tarjan(NodeGraph graph) { 52 graphMap = graph.createMap(); 53 54 this.registry = new HashMap<>(Utils.hashMapInitialCapacity(graph.getEdges().size())); 55 } 56 57 /** 58 * Returns the strongly connected components in the current graph. Single nodes are ignored to save memory. 59 * 60 * @return the strongly connected components in the current graph 61 */ 62 public Collection<List<Node>> getSCC() { 63 for (Node node : graphMap.keySet()) { 64 if (!registry.containsKey(node.getUniqueId())) { 65 strongConnect(node); 66 } 67 } 68 return scc; 69 } 70 71 /** 72 * Returns the graph data as a map. 73 * 74 * @return the graph data as a map 75 * @see NodeGraph#createMap() 76 */ 77 public Map<Node, List<Node>> getGraphMap() { 78 return graphMap; 79 } 80 81 /** 82 * Calculates strongly connected components available from the given node, in an iterative fashion. 83 * 84 * @param u0 the node to generate strongly connected components from 85 */ 86 private void strongConnect(final Node u0) { 87 final Deque<Pair<Node, Integer>> work = new ArrayDeque<>(); 88 work.push(new Pair<>(u0, 0)); 89 boolean recurse; 90 91 while (!work.isEmpty()) { 92 Pair<Node, Integer> popped = work.remove(); 93 Node u = popped.a; 94 int j = popped.b; 95 96 if (j == 0) { 97 index++; 98 registry.put(u.getUniqueId(), new TarjanHelper(index)); 99 stack.push(u); 100 } 101 102 recurse = false; 103 List<Node> successors = getSuccessors(u); 104 105 for (int i = j; i < successors.size(); i++) { 106 Node v = successors.get(i); 107 if (!registry.containsKey(v.getUniqueId())) { 108 work.push(new Pair<>(u, i + 1)); 109 work.push(new Pair<>(v, 0)); 110 recurse = true; 111 break; 112 } else if (stack.contains(v)) { 113 TarjanHelper uHelper = registry.get(u.getUniqueId()); 114 TarjanHelper vHelper = registry.get(v.getUniqueId()); 115 uHelper.lowlink = Math.min(uHelper.lowlink, vHelper.index); 116 } 117 } 118 119 if (!recurse) { 120 TarjanHelper uHelper = registry.get(u.getUniqueId()); 121 if (uHelper.lowlink == uHelper.index) { 122 List<Node> currentSCC = new ArrayList<>(); 123 Node v; 124 do { 125 v = stack.remove(); 126 currentSCC.add(v); 127 } while (!v.equals(u)); 128 129 // store the component only if it makes a cycle, otherwise it's a waste of memory 130 if (currentSCC.size() > 1) { 131 scc.add(currentSCC); 132 } 133 } 134 if (!work.isEmpty()) { 135 Node v = u; 136 Pair<Node, Integer> peeked = work.peek(); 137 u = peeked.a; 138 TarjanHelper vHelper = registry.get(v.getUniqueId()); 139 uHelper = registry.get(u.getUniqueId()); 140 uHelper.lowlink = Math.min(uHelper.lowlink, vHelper.lowlink); 141 } 142 } 143 } 144 } 145 146 /** 147 * Returns the next direct successors from the graph of the given node. 148 * 149 * @param node a node to start search from 150 * @return direct successors of the node or an empty list, if it's a terminal node 151 */ 152 private List<Node> getSuccessors(Node node) { 153 return graphMap.getOrDefault(node, Collections.emptyList()); 154 } 155 156 /** 157 * Helper class for storing the Tarjan algorithm runtime metadata. 158 */ 159 private static final class TarjanHelper { 160 private final int index; 161 private int lowlink; 162 163 private TarjanHelper(int index) { 164 this.index = index; 165 this.lowlink = index; 166 } 167 } 168 } -
src/org/openstreetmap/josm/data/algorithms/package-info.java
1 // License: GPL. For details, see LICENSE file. 2 3 /** 4 * General purpose algorithm classes for OSM data validation. 5 */ 6 package org.openstreetmap.josm.data.algorithms; -
src/org/openstreetmap/josm/data/osm/NodeGraph.java
21 21 import java.util.stream.Stream; 22 22 23 23 import org.openstreetmap.josm.tools.Pair; 24 import org.openstreetmap.josm.tools.Utils; 24 25 25 26 /** 26 * A directed or undirected graph of nodes. 27 * A directed or undirected graph of nodes. Nodes are connected via edges represented by NodePair instances. 28 * 27 29 * @since 12463 (extracted from CombineWayAction) 28 30 */ 29 31 public class NodeGraph { … … 32 34 * Builds a list of pair of nodes from the given way. 33 35 * @param way way 34 36 * @param directed if {@code true} each pair of nodes will occur once, in the way nodes order. 35 * if {@code false} each pair of nodes will occur twice (the pair and its inverse dcopy)37 * if {@code false} each pair of nodes will occur twice (the pair and its inverse copy) 36 38 * @return a list of pair of nodes from the given way 37 39 */ 38 40 public static List<NodePair> buildNodePairs(Way way, boolean directed) { 39 41 List<NodePair> pairs = new ArrayList<>(); 40 for (Pair<Node, Node> pair : way.getNodePairs(false /* don't sort */)) {42 for (Pair<Node, Node> pair : way.getNodePairs(false)) { 41 43 pairs.add(new NodePair(pair)); 42 44 if (!directed) { 43 45 pairs.add(new NodePair(pair).swap()); … … 49 51 /** 50 52 * Builds a list of pair of nodes from the given ways. 51 53 * @param ways ways 52 * @param directed if {@code true} each pair of nodes will occur once, in the way nodes order. 53 * if {@code false} each pair of nodes will occur twice (the pair and its inverse dcopy)54 * @param directed if {@code true} each pair of nodes will occur once, in the way nodes order.<br> 55 * if {@code false} each pair of nodes will occur twice (the pair and its inverse copy) 54 56 * @return a list of pair of nodes from the given ways 55 57 */ 56 58 public static List<NodePair> buildNodePairs(List<Way> ways, boolean directed) { 57 59 List<NodePair> pairs = new ArrayList<>(); 58 for (Way w : ways) {60 for (Way w : ways) { 59 61 pairs.addAll(buildNodePairs(w, directed)); 60 62 } 61 63 return pairs; … … 62 64 } 63 65 64 66 /** 65 * Builds a new list of pair nodes without the duplicated pairs (including inverse dcopies).67 * Builds a new list of pair nodes without the duplicated pairs (including inverse copies). 66 68 * @param pairs existing list of pairs 67 69 * @return a new list of pair nodes without the duplicated pairs 68 70 */ 69 71 public static List<NodePair> eliminateDuplicateNodePairs(List<NodePair> pairs) { 70 72 List<NodePair> cleaned = new ArrayList<>(); 71 for (NodePair p : pairs) {73 for (NodePair p : pairs) { 72 74 if (!cleaned.contains(p) && !cleaned.contains(p.swap())) { 73 75 cleaned.add(p); 74 76 } … … 76 78 return cleaned; 77 79 } 78 80 81 /** 82 * Create a directed graph from the given node pairs. 83 * @param pairs Node pairs to build the graph from 84 * @return node graph structure 85 */ 79 86 public static NodeGraph createDirectedGraphFromNodePairs(List<NodePair> pairs) { 80 87 NodeGraph graph = new NodeGraph(); 81 for (NodePair pair : pairs) {88 for (NodePair pair : pairs) { 82 89 graph.add(pair); 83 90 } 84 91 return graph; 85 92 } 86 93 94 /** 95 * Create a directed graph from the given ways. 96 * @param ways ways to build the graph from 97 * @return node graph structure 98 */ 87 99 public static NodeGraph createDirectedGraphFromWays(Collection<Way> ways) { 88 100 NodeGraph graph = new NodeGraph(); 89 for (Way w : ways) {90 graph.add(buildNodePairs(w, true /* directed */));101 for (Way w : ways) { 102 graph.add(buildNodePairs(w, true)); 91 103 } 92 104 return graph; 93 105 } … … 99 111 */ 100 112 public static NodeGraph createUndirectedGraphFromNodeList(List<NodePair> pairs) { 101 113 NodeGraph graph = new NodeGraph(); 102 for (NodePair pair : pairs) {114 for (NodePair pair : pairs) { 103 115 graph.add(pair); 104 116 graph.add(pair.swap()); 105 117 } … … 108 120 109 121 /** 110 122 * Create an undirected graph from the given ways, but prevent reversing of all 111 * non-new ways by fix one direction.123 * non-new ways by fixing one direction. 112 124 * @param ways Ways to build the graph from 113 125 * @return node graph structure 114 126 * @since 8181 … … 115 127 */ 116 128 public static NodeGraph createUndirectedGraphFromNodeWays(Collection<Way> ways) { 117 129 NodeGraph graph = new NodeGraph(); 118 for (Way w : ways) {119 graph.add(buildNodePairs(w, false /* undirected */));130 for (Way w : ways) { 131 graph.add(buildNodePairs(w, false)); 120 132 } 121 133 return graph; 122 134 } 123 135 136 /** 137 * Create a nearly undirected graph from the given ways, but prevent reversing of all 138 * non-new ways by fixing one direction. 139 * The first new way gives the direction of the graph. 140 * @param ways Ways to build the graph from 141 * @return node graph structure 142 */ 124 143 public static NodeGraph createNearlyUndirectedGraphFromNodeWays(Collection<Way> ways) { 125 144 boolean dir = true; 126 145 NodeGraph graph = new NodeGraph(); 127 for (Way w : ways) {146 for (Way w : ways) { 128 147 if (!w.isNew()) { 129 148 /* let the first non-new way give the direction (see #5880) */ 130 149 graph.add(buildNodePairs(w, dir)); 131 150 dir = false; 132 151 } else { 133 graph.add(buildNodePairs(w, false /* undirected */));152 graph.add(buildNodePairs(w, false)); 134 153 } 135 154 } 136 155 return graph; … … 137 156 } 138 157 139 158 private final Set<NodePair> edges; 140 private int numUndirectedE ges;141 /** counts the number of edges that were added*/159 private int numUndirectedEdges; 160 /** The number of edges that were added. */ 142 161 private int addedEdges; 143 162 private final Map<Node, List<NodePair>> successors = new LinkedHashMap<>(); 144 163 private final Map<Node, List<NodePair>> predecessors = new LinkedHashMap<>(); 145 164 165 /** 166 * Constructs a lookup table from the existing edges in the graph to enable efficient querying. 167 * This method creates a map where each node is associated with a list of nodes that are directly connected to it. 168 * 169 * @return A map representing the graph structure, where nodes are keys, and values are their direct successors. 170 * @since xxx 171 */ 172 public Map<Node, List<Node>> createMap() { 173 final Map<Node, List<Node>> result = new HashMap<>(Utils.hashMapInitialCapacity(edges.size())); 174 175 for (NodePair edge : edges) { 176 result.computeIfAbsent(edge.getA(), k -> new ArrayList<>()).add(edge.getB()); 177 } 178 179 return result; 180 } 181 182 /** 183 * See {@link #prepare()} 184 */ 146 185 protected void rememberSuccessor(NodePair pair) { 147 186 List<NodePair> l = successors.computeIfAbsent(pair.getA(), k -> new ArrayList<>()); 148 187 if (!l.contains(pair)) { … … 150 189 } 151 190 } 152 191 192 /** 193 * See {@link #prepare()} 194 */ 153 195 protected void rememberPredecessors(NodePair pair) { 154 196 List<NodePair> l = predecessors.computeIfAbsent(pair.getB(), k -> new ArrayList<>()); 155 197 if (!l.contains(pair)) { … … 157 199 } 158 200 } 159 201 202 /** 203 * Replies true if {@code n} is a terminal node of the graph. Internal variables should be initialized first. 204 * @param n Node to check 205 * @return {@code true} if it is a terminal node 206 * @see #prepare() 207 */ 160 208 protected boolean isTerminalNode(Node n) { 161 209 if (successors.get(n) == null) return false; 162 210 if (successors.get(n).size() != 1) return false; … … 174 222 successors.clear(); 175 223 predecessors.clear(); 176 224 177 for (NodePair pair : edges) {225 for (NodePair pair : edges) { 178 226 if (!undirectedEdges.contains(pair) && !undirectedEdges.contains(pair.swap())) { 179 227 undirectedEdges.add(pair); 180 228 } … … 181 229 rememberSuccessor(pair); 182 230 rememberPredecessors(pair); 183 231 } 184 numUndirectedE ges = undirectedEdges.size();232 numUndirectedEdges = undirectedEdges.size(); 185 233 } 186 234 187 235 /** … … 202 250 203 251 /** 204 252 * Add a list of node pairs. 205 * @param pairs listof node pairs253 * @param pairs collection of node pairs 206 254 */ 207 public void add( Collection<NodePair> pairs) {208 for (NodePair pair : pairs) {255 public void add(Iterable<NodePair> pairs) { 256 for (NodePair pair : pairs) { 209 257 add(pair); 210 258 } 211 259 } 212 260 261 /** 262 * Return the edges containing the node pairs of the graph. 263 * @return the edges containing the node pairs of the graph 264 */ 265 public Collection<NodePair> getEdges() { 266 return Collections.unmodifiableSet(edges); 267 } 268 269 /** 270 * Return the terminal nodes of the graph. 271 * @return the terminal nodes of the graph 272 */ 213 273 protected Set<Node> getTerminalNodes() { 214 274 return getNodes().stream().filter(this::isTerminalNode).collect(Collectors.toCollection(LinkedHashSet::new)); 215 275 } … … 229 289 return Optional.ofNullable(successors.get(node)).orElseGet(Collections::emptyList); 230 290 } 231 291 232 protected Set<Node> getNodes() { 292 /** 293 * Return the graph's nodes. 294 * @return the graph's nodes 295 */ 296 public Collection<Node> getNodes() { 233 297 Set<Node> nodes = new LinkedHashSet<>(2 * edges.size()); 234 for (NodePair pair : edges) {298 for (NodePair pair : edges) { 235 299 nodes.add(pair.getA()); 236 300 nodes.add(pair.getB()); 237 301 } … … 239 303 } 240 304 241 305 protected boolean isSpanningWay(Collection<NodePair> way) { 242 return numUndirectedE ges == way.size();306 return numUndirectedEdges == way.size(); 243 307 } 244 308 245 309 protected List<Node> buildPathFromNodePairs(Deque<NodePair> path) { … … 248 312 } 249 313 250 314 /** 251 * Tries to find a spanning path starting from node <code>startNode</code>.252 * 315 * Tries to find a spanning path starting from node {@code startNode}. 316 * <p> 253 317 * Traverses the path in depth-first order. 254 318 * 255 319 * @param startNode the start node … … 259 323 if (startNode != null) { 260 324 Deque<NodePair> path = new ArrayDeque<>(); 261 325 Set<NodePair> dupCheck = new HashSet<>(); 262 Deque<NodePair> nextPairs = new ArrayDeque<>(); 263 nextPairs.addAll(getOutboundPairs(startNode)); 326 Deque<NodePair> nextPairs = new ArrayDeque<>(getOutboundPairs(startNode)); 264 327 while (!nextPairs.isEmpty()) { 265 328 NodePair cur = nextPairs.removeLast(); 266 329 if (!dupCheck.contains(cur) && !dupCheck.contains(cur.swap())) { … … 280 343 281 344 /** 282 345 * Tries to find a path through the graph which visits each edge (i.e. 283 * the segment of a way) exactly once. 284 * < p><b>Note that duplicated edges are removed first!</b>346 * the segment of a way) exactly once.<p> 347 * <b>Note that duplicated edges are removed first!</b> 285 348 * 286 * @return the path; null, if no path was found349 * @return the path; {@code null}, if no path was found 287 350 */ 288 351 public List<Node> buildSpanningPath() { 289 352 prepare(); 290 if (numUndirectedE ges > 0 && isConnected()) {291 // try to find a path from each "terminal node", i.e. from a292 // node which is connected by exactly one undirected edge s(or293 // two directed edges in opposite direction) to the graph. A353 if (numUndirectedEdges > 0 && isConnected()) { 354 // Try to find a path from each "terminal node", i.e. from a 355 // node which is connected by exactly one undirected edge (or 356 // two directed edges in the opposite direction) to the graph. A 294 357 // graph built up from way segments is likely to include such 295 358 // nodes, unless the edges build one or more closed rings. 296 359 // We order the nodes to start with the best candidates, but … … 324 387 325 388 /** 326 389 * Find out if the graph is connected. 327 * @return true if it is connected.390 * @return {@code true} if it is connected 328 391 */ 329 392 private boolean isConnected() { 330 Set<Node> nodes = getNodes();393 Collection<Node> nodes = getNodes(); 331 394 if (nodes.isEmpty()) 332 395 return false; 333 396 Deque<Node> toVisit = new ArrayDeque<>(); … … 350 413 351 414 /** 352 415 * Sort the nodes by number of appearances in the edges. 353 * @return set of nodes which can be start nodes in a spanning way .416 * @return set of nodes which can be start nodes in a spanning way 354 417 */ 355 418 private Set<Node> getMostFrequentVisitedNodesFirst() { 356 419 if (edges.isEmpty()) 357 420 return Collections.emptySet(); 358 // count appearance of nodes in edges421 // count the appearance of nodes in edges 359 422 Map<Node, Integer> counters = new HashMap<>(); 360 423 for (NodePair pair : edges) { 361 424 Integer c = counters.get(pair.getA()); -
src/org/openstreetmap/josm/data/validation/OsmValidator.java
44 44 import org.openstreetmap.josm.data.validation.tests.ConditionalKeys; 45 45 import org.openstreetmap.josm.data.validation.tests.ConnectivityRelations; 46 46 import org.openstreetmap.josm.data.validation.tests.CrossingWays; 47 import org.openstreetmap.josm.data.validation.tests.CycleDetector; 47 48 import org.openstreetmap.josm.data.validation.tests.DirectionNodes; 48 49 import org.openstreetmap.josm.data.validation.tests.DuplicateNode; 49 50 import org.openstreetmap.josm.data.validation.tests.DuplicateRelation; … … 154 155 // 3700 .. 3799 is automatically removed since it clashed with pt_assistant. 155 156 SharpAngles.class, // 3800 .. 3899 156 157 ConnectivityRelations.class, // 3900 .. 3999 157 DirectionNodes.class, // 4000 -4099158 DirectionNodes.class, // 4000 .. 4099 158 159 RightAngleBuildingTest.class, // 4100 .. 4199 160 CycleDetector.class, // 4200 .. 4299 159 161 }; 160 162 161 163 /** -
src/org/openstreetmap/josm/data/validation/tests/CycleDetector.java
1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.validation.tests; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 import static org.openstreetmap.josm.tools.I18n.trc; 6 7 import java.util.ArrayDeque; 8 import java.util.ArrayList; 9 import java.util.Arrays; 10 import java.util.Collection; 11 import java.util.HashSet; 12 import java.util.List; 13 import java.util.Map; 14 import java.util.Queue; 15 import java.util.Set; 16 import java.util.stream.Collectors; 17 18 import org.openstreetmap.josm.data.osm.Node; 19 import org.openstreetmap.josm.data.osm.NodeGraph; 20 import org.openstreetmap.josm.data.osm.OsmPrimitive; 21 import org.openstreetmap.josm.data.osm.Way; 22 import org.openstreetmap.josm.data.osm.WaySegment; 23 import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper; 24 import org.openstreetmap.josm.data.validation.Severity; 25 import org.openstreetmap.josm.data.validation.Test; 26 import org.openstreetmap.josm.data.validation.TestError; 27 import org.openstreetmap.josm.data.algorithms.Tarjan; 28 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 29 import org.openstreetmap.josm.spi.preferences.Config; 30 import org.openstreetmap.josm.tools.Pair; 31 32 /** 33 * Test for detecting <a href="https://en.wikipedia.org/wiki/Cycle_(graph_theory)">cycles</a> in a directed graph, 34 * currently used for waterways only. The processed graph consists of ways labeled as waterway. 35 * 36 * @author gaben 37 * @since xxx 38 */ 39 public class CycleDetector extends Test { 40 public static final int CYCLE_DETECTED = 4200; 41 42 /** All waterways for cycle detection */ 43 private final Set<Way> usableWaterways = new HashSet<>(); 44 45 /** Already visited primitive unique IDs */ 46 private final Set<Long> visitedWays = new HashSet<>(); 47 48 /** Currently used directional waterways from the OSM wiki */ 49 private List<String> directionalWaterways; 50 51 protected static final String PREFIX = ValidatorPrefHelper.PREFIX + "." + CycleDetector.class.getSimpleName(); 52 53 public CycleDetector() { 54 super(tr("Cycle detector"), tr("Detects cycles in drainage systems.")); 55 } 56 57 @Override 58 public boolean isPrimitiveUsable(OsmPrimitive p) { 59 return p.isUsable() && (p instanceof Way) && (((Way) p).getNodesCount() > 1) && p.hasTag("waterway", directionalWaterways); 60 } 61 62 @Override 63 public void visit(Way w) { 64 if (isPrimitiveUsable(w)) 65 usableWaterways.add(w); 66 } 67 68 @Override 69 public void startTest(ProgressMonitor progressMonitor) { 70 super.startTest(progressMonitor); 71 directionalWaterways = Config.getPref().getList(PREFIX + ".directionalWaterways", 72 Arrays.asList("river", "stream", "tidal_channel", "drain", "ditch", "fish_pass", "fairway")); 73 } 74 75 @Override 76 public void endTest() { 77 for (Collection<Way> graph : getGraphs()) { 78 NodeGraph nodeGraph = NodeGraph.createDirectedGraphFromWays(graph); 79 Tarjan tarjan = new Tarjan(nodeGraph); 80 Collection<List<Node>> scc = tarjan.getSCC(); 81 Map<Node, List<Node>> graphMap = tarjan.getGraphMap(); 82 83 for (Collection<Node> possibleCycle : scc) { 84 // there is a cycle in the graph if a strongly connected component has more than one node 85 if (possibleCycle.size() > 1) { 86 errors.add( 87 TestError.builder(this, Severity.ERROR, CYCLE_DETECTED) 88 .message(trc("graph theory", "Cycle in directional waterway network")) 89 .primitives(possibleCycle) 90 .highlightWaySegments(createSegments(graphMap, possibleCycle)) 91 .build() 92 ); 93 } 94 } 95 } 96 97 super.endTest(); 98 } 99 100 @Override 101 public void clear() { 102 super.clear(); 103 usableWaterways.clear(); 104 visitedWays.clear(); 105 } 106 107 /** 108 * Creates WaySegments from Nodes for the error highlight function. 109 * 110 * @param graphMap the complete graph data 111 * @param nodes nodes to build the way segments from 112 * @return WaySegments from the Nodes 113 */ 114 private static Collection<WaySegment> createSegments(Map<Node, List<Node>> graphMap, Collection<Node> nodes) { 115 List<Pair<Node, Node>> pairs = new ArrayList<>(); 116 117 // build new graph exclusively from SCC nodes 118 for (Node node : nodes) { 119 for (Node successor : graphMap.get(node)) { 120 // check for outbound nodes 121 if (nodes.contains(successor)) { 122 pairs.add(new Pair<>(node, successor)); 123 } 124 } 125 } 126 127 Collection<WaySegment> segments = new ArrayList<>(); 128 129 for (Pair<Node, Node> pair : pairs) { 130 final Node n = pair.a; 131 final Node m = pair.b; 132 133 if (n != null && m != null && !n.equals(m)) { 134 List<Way> intersect = new ArrayList<>(n.getParentWays()); 135 List<Way> mWays = m.getParentWays(); 136 intersect.retainAll(mWays); 137 138 for (Way w : intersect) { 139 if (w.getNeighbours(n).contains(m) && getNodeIndex(w, n) + 1 == getNodeIndex(w, m)) { 140 segments.add(WaySegment.forNodePair(w, n, m)); 141 } 142 } 143 } 144 } 145 146 return segments; 147 } 148 149 /** 150 * Returns the way index of a node. Only the first occurrence is considered in case it's a closed way. 151 * 152 * @param w parent way 153 * @param n the node to look up 154 * @return {@code >=0} if the node is found or<br>{@code -1} if node not part of the way 155 */ 156 private static int getNodeIndex(Way w, Node n) { 157 for (int i = 0; i < w.getNodesCount(); i++) { 158 if (w.getNode(i).equals(n)) { 159 return i; 160 } 161 } 162 163 return -1; 164 } 165 166 /** 167 * Returns all directional waterways which connect to at least one other usable way. 168 * 169 * @return all directional waterways which connect to at least one other usable way 170 */ 171 private Collection<Collection<Way>> getGraphs() { 172 // HashSet doesn't make a difference here 173 Collection<Collection<Way>> graphs = new ArrayList<>(); 174 175 for (Way waterway : usableWaterways) { 176 if (visitedWays.contains(waterway.getUniqueId())) { 177 continue; 178 } 179 Collection<Way> graph = buildGraph(waterway); 180 181 if (!graph.isEmpty()) { 182 graphs.add(graph); 183 } 184 } 185 186 return graphs; 187 } 188 189 /** 190 * Returns a collection of ways, which belongs to the same graph. 191 * 192 * @param way starting way to extend the graph from 193 * @return a collection of ways which belongs to the same graph 194 */ 195 private Collection<Way> buildGraph(Way way) { 196 final Set<Way> graph = new HashSet<>(); 197 Queue<Way> queue = new ArrayDeque<>(); 198 queue.offer(way); 199 200 while (!queue.isEmpty()) { 201 Way currentWay = queue.poll(); 202 visitedWays.add(currentWay.getUniqueId()); 203 204 for (Node node : currentWay.getNodes()) { 205 Collection<Way> referrers = node.referrers(Way.class) 206 .filter(this::isPrimitiveUsable) 207 .filter(candidate -> candidate != currentWay) 208 .collect(Collectors.toList()); 209 210 if (!referrers.isEmpty()) { 211 for (Way referrer : referrers) { 212 if (!visitedWays.contains(referrer.getUniqueId())) { 213 queue.offer(referrer); 214 visitedWays.add(referrer.getUniqueId()); 215 } 216 } 217 graph.addAll(referrers); 218 } 219 } 220 } 221 return graph; 222 } 223 } -
test/data/regress/21881/CycleDetector_test_wikipedia.osm
1 <?xml version='1.0' encoding='UTF-8'?> 2 <osm version='0.6' upload='never' generator='JOSM'> 3 <node id='-137726' action='modify' visible='true' lat='47.74161657891' lon='17.37769604149' /> 4 <node id='-137727' action='modify' visible='true' lat='47.74160961975' lon='17.37612305842' /> 5 <node id='-137728' action='modify' visible='true' lat='47.74043350867' lon='17.37611270985' /> 6 <node id='-137731' action='modify' visible='true' lat='47.74043350867' lon='17.37771673864' /> 7 <node id='-137732' action='modify' visible='true' lat='47.74071883984' lon='17.37897926452' /> 8 <node id='-137733' action='modify' visible='true' lat='47.74045438662' lon='17.38021074469' /> 9 <node id='-137734' action='modify' visible='true' lat='47.74011337916' lon='17.37895856738' /> 10 <node id='-137735' action='modify' visible='true' lat='47.74163049723' lon='17.38024179041' /> 11 <node id='-137736' action='modify' visible='true' lat='47.74119902778' lon='17.38124560197' /> 12 <node id='-137737' action='modify' visible='true' lat='47.74161657891' lon='17.38222871639' /> 13 <node id='-137738' action='modify' visible='true' lat='47.7420091937' lon='17.38123761625' /> 14 <node id='-137746' action='modify' visible='true' lat='47.74044046799' lon='17.38222871639' /> 15 <node id='-137759' action='modify' visible='true' lat='47.73993243552' lon='17.38222871639' /> 16 <node id='-137760' action='modify' visible='true' lat='47.73994635429' lon='17.38319113367' /> 17 <node id='-137761' action='modify' visible='true' lat='47.74046134593' lon='17.38319113367' /> 18 <way id='-103300' action='modify' visible='true'> 19 <nd ref='-137726' /> 20 <nd ref='-137727' /> 21 <tag k='waterway' v='ditch' /> 22 </way> 23 <way id='-103301' action='modify' visible='true'> 24 <nd ref='-137727' /> 25 <nd ref='-137728' /> 26 <tag k='waterway' v='ditch' /> 27 </way> 28 <way id='-103302' action='modify' visible='true'> 29 <nd ref='-137728' /> 30 <nd ref='-137726' /> 31 <tag k='waterway' v='ditch' /> 32 </way> 33 <way id='-103305' action='modify' visible='true'> 34 <nd ref='-137731' /> 35 <nd ref='-137728' /> 36 <tag k='waterway' v='ditch' /> 37 </way> 38 <way id='-103306' action='modify' visible='true'> 39 <nd ref='-137731' /> 40 <nd ref='-137726' /> 41 <tag k='waterway' v='ditch' /> 42 </way> 43 <way id='-103307' action='modify' visible='true'> 44 <nd ref='-137733' /> 45 <nd ref='-137732' /> 46 <nd ref='-137731' /> 47 <tag k='waterway' v='ditch' /> 48 </way> 49 <way id='-103309' action='modify' visible='true'> 50 <nd ref='-137731' /> 51 <nd ref='-137734' /> 52 <nd ref='-137733' /> 53 <tag k='waterway' v='ditch' /> 54 </way> 55 <way id='-103311' action='modify' visible='true'> 56 <nd ref='-137733' /> 57 <nd ref='-137735' /> 58 <tag k='waterway' v='ditch' /> 59 </way> 60 <way id='-103312' action='modify' visible='true'> 61 <nd ref='-137735' /> 62 <nd ref='-137726' /> 63 <tag k='waterway' v='ditch' /> 64 </way> 65 <way id='-103313' action='modify' visible='true'> 66 <nd ref='-137735' /> 67 <nd ref='-137736' /> 68 <nd ref='-137737' /> 69 <tag k='waterway' v='ditch' /> 70 </way> 71 <way id='-103315' action='modify' visible='true'> 72 <nd ref='-137737' /> 73 <nd ref='-137738' /> 74 <nd ref='-137735' /> 75 <tag k='waterway' v='ditch' /> 76 </way> 77 <way id='-103324' action='modify' visible='true'> 78 <nd ref='-137746' /> 79 <nd ref='-137733' /> 80 <tag k='waterway' v='ditch' /> 81 </way> 82 <way id='-103325' action='modify' visible='true'> 83 <nd ref='-137746' /> 84 <nd ref='-137737' /> 85 <tag k='waterway' v='ditch' /> 86 </way> 87 <way id='-103359' action='modify' visible='true'> 88 <nd ref='-137746' /> 89 <nd ref='-137759' /> 90 <nd ref='-137760' /> 91 <nd ref='-137761' /> 92 <nd ref='-137746' /> 93 <tag k='waterway' v='ditch' /> 94 </way> 95 </osm> -
test/unit/org/openstreetmap/josm/data/validation/tests/CycleDetectorTest.java
1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.validation.tests; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 6 import org.junit.jupiter.api.Test; 7 import org.openstreetmap.josm.TestUtils; 8 import org.openstreetmap.josm.data.osm.DataSet; 9 import org.openstreetmap.josm.io.OsmReader; 10 import org.openstreetmap.josm.testutils.annotations.BasicPreferences; 11 12 /** 13 * JUnit test for {@link CycleDetector} validation test. 14 */ 15 @BasicPreferences 16 class CycleDetectorTest { 17 18 @Test 19 void testCycleDetection() throws Exception { 20 CycleDetector cycleDetector = new CycleDetector(); 21 DataSet ds = OsmReader.parseDataSet(TestUtils.getRegressionDataStream(21881, "CycleDetector_test_wikipedia.osm"), null); 22 cycleDetector.startTest(null); 23 cycleDetector.visit(ds.allPrimitives()); 24 cycleDetector.endTest(); 25 26 // we have 4 cycles in the test file 27 assertEquals(4, cycleDetector.getErrors().size()); 28 } 29 }