Ticket #21881: josm21881_loop_detector_wip.patch
File josm21881_loop_detector_wip.patch, 17.8 KB (added by , 3 years ago) |
---|
-
src/org/openstreetmap/josm/data/validation/tests/LoopDetector.java
1 package org.openstreetmap.josm.data.validation.tests; 2 3 import static org.openstreetmap.josm.gui.MainApplication.getLayerManager; 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.util.ArrayDeque; 7 import java.util.ArrayList; 8 import java.util.Arrays; 9 import java.util.Collection; 10 import java.util.Deque; 11 import java.util.HashMap; 12 import java.util.HashSet; 13 import java.util.Map; 14 import java.util.Set; 15 import java.util.stream.Collectors; 16 17 import org.openstreetmap.josm.data.osm.IPrimitive; 18 import org.openstreetmap.josm.data.osm.Node; 19 import org.openstreetmap.josm.data.osm.NodeGraph; 20 import org.openstreetmap.josm.data.osm.NodePair; 21 import org.openstreetmap.josm.data.osm.OsmPrimitive; 22 import org.openstreetmap.josm.data.osm.Way; 23 import org.openstreetmap.josm.data.validation.Severity; 24 import org.openstreetmap.josm.data.validation.Test; 25 import org.openstreetmap.josm.data.validation.TestError; 26 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 27 import org.openstreetmap.josm.tools.Logging; 28 29 30 /** 31 * Test for detecting cycles in a directed graph, currently used for waterwoys only. 32 * The graph consist of OSM dataset ways. 33 * 34 * @since xxx 35 */ 36 public class LoopDetector extends Test { 37 public static final int LOOP_DETECTED = 4100; 38 39 /** 40 * Currently used directional waterways from the OSM wiki 41 */ 42 private static final Set<String> directionalWaterways = new HashSet<>( 43 Arrays.asList("river", "stream", "tidal_channel", "drain", "ditch", "fish_pass", "fairway")); 44 45 private final Set<Way> visitedWays = new HashSet<>(); 46 47 public LoopDetector() { 48 super(tr("Loop detector"), tr("Detects loops in connected directional ways.")); 49 } 50 51 @Override 52 public boolean isPrimitiveUsable(OsmPrimitive p) { 53 return p.isUsable() && (p instanceof Way) && ((Way) p).getNodesCount() > 1 && p.hasTag("waterway", directionalWaterways); 54 } 55 56 @Override 57 public void startTest(ProgressMonitor progressMonitor) { 58 super.startTest(progressMonitor); 59 setShowElements(true); 60 // TODO: osm partial selection validator doesn't work 61 62 Collection<Collection<Way>> graphs = getGraphs(); 63 64 // FIXME debug variables 65 int i =0, g = 0; 66 67 for (Collection<Way> graph : graphs) { 68 NodeGraph nodeGraph = NodeGraph.createDirectedGraphFromWays(graph); 69 Tarjan tarjan = new Tarjan(nodeGraph); 70 Logging.debug("Graph looping... " + ++g); 71 Collection<Collection<Node>> scc = tarjan.getSCC(); 72 for (Collection<Node> possibleCycle : scc) { 73 // contains a cycle (or loop) if the strongly connected components set size is larger than 1 74 if (possibleCycle.size() > 1) { 75 Logging.debug("Cycle detected! " + ++i); 76 errors.add( 77 TestError.builder(this, Severity.ERROR, LOOP_DETECTED) 78 .message(tr("Cycle in directional waterway network")) 79 .primitives(possibleCycle) 80 .build() 81 ); 82 } 83 } 84 progressMonitor.worked(1); 85 } 86 } 87 88 @Override 89 public void clear() { 90 super.clear(); 91 visitedWays.clear(); 92 } 93 94 /** 95 * Returns all directional waterways which connects to at least one other usable way. 96 * 97 * @return all directional waterways which connects to at least one other usable way 98 */ 99 private Collection<Collection<Way>> getGraphs() { 100 // 70127 waterway 101 Set<Way> usableWaterways = getLayerManager() 102 .getActiveDataSet() 103 .getWays() 104 .stream() 105 .filter(this::isPrimitiveUsable) 106 .collect(Collectors.toSet()); 107 108 // HashSet doesn't make a difference here 109 Collection<Collection<Way>> graphs = new ArrayList<>(); 110 111 for (Way current : usableWaterways) { 112 Collection<Way> graph = buildGraph(current); 113 114 if (!graph.isEmpty()) 115 graphs.add(graph); 116 } 117 118 Logging.debug("All usable waterways " + usableWaterways.size() + ", visited ways " + visitedWays.size() + ", graphs size " + graphs.size()); 119 usableWaterways.removeAll(visitedWays); 120 Logging.debug(String.format("Ignored ways (%s): %s", usableWaterways.size(), 121 usableWaterways.stream().map(IPrimitive::getId).collect(Collectors.toList()))); 122 123 return graphs; 124 } 125 126 /** 127 * Returns a collection of ways which belongs to the same graph. 128 * 129 * @param way starting way to extend the graph from 130 * @return a collection of ways which belongs to the same graph 131 */ 132 private Collection<Way> buildGraph(Way way) { 133 if (visitedWays.contains(way)) 134 return new HashSet<>(); 135 136 final Set<Way> graph = new HashSet<>(); 137 138 for (Node node : way.getNodes()) { 139 final Collection<Way> referrers = node.referrers(Way.class).filter(this::isPrimitiveUsable).collect(Collectors.toSet()); 140 referrers.remove(way); 141 142 if (!referrers.isEmpty()) { 143 visitedWays.add(way); 144 for (Way referrer : referrers) { 145 graph.addAll(buildGraph(referrer)); 146 } 147 } 148 graph.addAll(referrers); 149 } 150 return graph; 151 } 152 153 /** 154 * Tarjan's algorithm implementation for JOSM. 155 * 156 * @see <a href="https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm"> 157 * Tarjan's strongly connected components algorithm</a> 158 */ 159 public static class Tarjan { 160 161 /** 162 * Helper class for storing algorithm runtime metadata. 163 */ 164 private static class TarjanHelper { 165 private final int index; 166 private int lowlink; 167 168 public TarjanHelper(int index, int lowlink) { 169 this.index = index; 170 this.lowlink = lowlink; 171 } 172 } 173 174 /** 175 * A simple key-value registry to store visited nodes and its metadata 176 */ 177 private final Map<Node, TarjanHelper> registry = new HashMap<>(); 178 179 private int index = 0; 180 private final Collection<Collection<Node>> scc = new HashSet<>(); 181 private final Deque<Node> stack = new ArrayDeque<>(); 182 private final NodeGraph graph; 183 184 public Tarjan(NodeGraph graph) { 185 this.graph = graph; 186 } 187 188 /** 189 * Returns the strongly connected components in the current graph. 190 * 191 * @return the strongly connected components in the current graph 192 */ 193 public Collection<Collection<Node>> getSCC() { 194 for (Node node : graph.getNodes()) { 195 if (!registry.containsKey(node)) { 196 Logging.debug("Started SCC for n" + node.getId()); 197 strongConnect(node); 198 Logging.debug("...done"); 199 } 200 } 201 return scc; 202 } 203 204 /** 205 * Calculates strongly connected components available from the given node. 206 * 207 * @param v 208 */ 209 private void strongConnect(Node v) { 210 registry.put(v, new TarjanHelper(index, index)); 211 index++; 212 stack.push(v); 213 214 // FIXME: performance issue here, possibly because of constant getSuccessors() call 215 for (Node w : getSuccessors(v)) { 216 if (!registry.containsKey(w)) { 217 strongConnect(w); 218 TarjanHelper vHelper = registry.get(v); 219 TarjanHelper wHelper = registry.get(w); 220 vHelper.lowlink = Math.min(vHelper.lowlink, wHelper.lowlink); 221 } else if (stack.contains(w)) { 222 TarjanHelper vHelper = registry.get(v); 223 TarjanHelper wHelper = registry.get(w); 224 vHelper.lowlink = Math.min(vHelper.lowlink, wHelper.index); 225 } 226 } 227 228 final TarjanHelper vHelper = registry.get(v); 229 if (vHelper.lowlink == vHelper.index) { 230 Collection<Node> currentSCC = new HashSet<>(); 231 Node w; 232 do { 233 w = stack.remove(); 234 currentSCC.add(w); 235 } while (!w.equals(v)); 236 scc.add(currentSCC); 237 } 238 } 239 240 /** 241 * Returns direct successors from the graph of the given node. 242 * 243 * @param node a node to start search from 244 * @return a collection of nodes from the graph which are direct neighbors of the given node 245 */ 246 private Collection<Node> getSuccessors(Node node) { 247 final Collection<Node> successors = new HashSet<>(); 248 for (NodePair pair : graph.getEdges()) { 249 if (pair.getA().equals(node)) { 250 successors.add(pair.getB()); 251 } 252 } 253 return successors; 254 } 255 } 256 } -
src/org/openstreetmap/josm/data/validation/OsmValidator.java
53 53 import org.openstreetmap.josm.data.validation.tests.InternetTags; 54 54 import org.openstreetmap.josm.data.validation.tests.Lanes; 55 55 import org.openstreetmap.josm.data.validation.tests.LongSegment; 56 import org.openstreetmap.josm.data.validation.tests.LoopDetector; 56 57 import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker; 57 58 import org.openstreetmap.josm.data.validation.tests.MultipolygonTest; 58 59 import org.openstreetmap.josm.data.validation.tests.NameMismatch; … … 154 155 SharpAngles.class, // 3800 .. 3899 155 156 ConnectivityRelations.class, // 3900 .. 3999 156 157 DirectionNodes.class, // 4000-4099 158 LoopDetector.class, // 4100-4199 157 159 }; 158 160 159 161 /** … … 365 367 public static JTree buildJTreeList() { 366 368 DefaultMutableTreeNode root = new DefaultMutableTreeNode(tr("Ignore list")); 367 369 final Pattern elemId1Pattern = Pattern.compile(":([rwn])_"); 368 final Pattern elemId2Pattern = Pattern.compile("^ [0-9]+$");370 final Pattern elemId2Pattern = Pattern.compile("^\\d+$"); 369 371 for (Entry<String, String> e: ignoredErrors.entrySet()) { 370 372 String key = e.getKey(); 371 373 // key starts with a code, it maybe followed by a string (eg. a MapCSS rule) and … … 457 459 } 458 460 if (tr("Ignore list").equals(description)) 459 461 description = ""; 460 if (!key.matches("^ [0-9]+(_.*|$)")) {462 if (!key.matches("^\\d+(_.*|$)")) { 461 463 description = key; 462 464 key = ""; 463 465 } … … 470 472 } else if (item.matches("^([rwn])_.*")) { 471 473 // single element 472 474 entry = key + ":" + item; 473 } else if (item.matches("^ [0-9]+(_.*|)$")) {475 } else if (item.matches("^\\d+(_.*|)$")) { 474 476 // no element ids 475 477 entry = item; 476 478 } -
src/org/openstreetmap/josm/data/osm/NodeGraph.java
32 32 * Builds a list of pair of nodes from the given way. 33 33 * @param way way 34 34 * @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)35 * if {@code false} each pair of nodes will occur twice (the pair and its inverse copy) 36 36 * @return a list of pair of nodes from the given way 37 37 */ 38 38 public static List<NodePair> buildNodePairs(Way way, boolean directed) { 39 39 List<NodePair> pairs = new ArrayList<>(); 40 for (Pair<Node, Node> pair: way.getNodePairs(false /* don't sort */)) {40 for (Pair<Node, Node> pair: way.getNodePairs(false)) { 41 41 pairs.add(new NodePair(pair)); 42 42 if (!directed) { 43 43 pairs.add(new NodePair(pair).swap()); … … 50 50 * Builds a list of pair of nodes from the given ways. 51 51 * @param ways ways 52 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)53 * if {@code false} each pair of nodes will occur twice (the pair and its inverse copy) 54 54 * @return a list of pair of nodes from the given ways 55 55 */ 56 56 public static List<NodePair> buildNodePairs(List<Way> ways, boolean directed) { … … 62 62 } 63 63 64 64 /** 65 * Builds a new list of pair nodes without the duplicated pairs (including inverse dcopies).65 * Builds a new list of pair nodes without the duplicated pairs (including inverse copies). 66 66 * @param pairs existing list of pairs 67 67 * @return a new list of pair nodes without the duplicated pairs 68 68 */ … … 76 76 return cleaned; 77 77 } 78 78 79 /** 80 * Create a directed graph from the given node pairs. 81 * @param pairs Node pairs to build the graph from 82 * @return node graph structure 83 */ 79 84 public static NodeGraph createDirectedGraphFromNodePairs(List<NodePair> pairs) { 80 85 NodeGraph graph = new NodeGraph(); 81 86 for (NodePair pair: pairs) { … … 84 89 return graph; 85 90 } 86 91 92 /** 93 * Create a directed graph from the given ways. 94 * @param ways ways to build the graph from 95 * @return node graph structure 96 */ 87 97 public static NodeGraph createDirectedGraphFromWays(Collection<Way> ways) { 88 98 NodeGraph graph = new NodeGraph(); 89 99 for (Way w: ways) { 90 graph.add(buildNodePairs(w, true /* directed */));100 graph.add(buildNodePairs(w, true)); 91 101 } 92 102 return graph; 93 103 } … … 116 126 public static NodeGraph createUndirectedGraphFromNodeWays(Collection<Way> ways) { 117 127 NodeGraph graph = new NodeGraph(); 118 128 for (Way w: ways) { 119 graph.add(buildNodePairs(w, false /* undirected */));129 graph.add(buildNodePairs(w, false)); 120 130 } 121 131 return graph; 122 132 } … … 130 140 graph.add(buildNodePairs(w, dir)); 131 141 dir = false; 132 142 } else { 133 graph.add(buildNodePairs(w, false /* undirected */));143 graph.add(buildNodePairs(w, false)); 134 144 } 135 145 } 136 146 return graph; 137 147 } 138 148 139 private final Set<NodePair> edges; 140 private int numUndirectedEges; 149 public Set<NodePair> getEdges() { 150 return edges; 151 } 152 private int numUndirectedEdges; 153 141 154 /** counts the number of edges that were added */ 142 155 private int addedEdges; 156 private final Set<NodePair> edges; 143 157 private final Map<Node, List<NodePair>> successors = new LinkedHashMap<>(); 144 158 private final Map<Node, List<NodePair>> predecessors = new LinkedHashMap<>(); 145 159 … … 181 195 rememberSuccessor(pair); 182 196 rememberPredecessors(pair); 183 197 } 184 numUndirectedE ges = undirectedEdges.size();198 numUndirectedEdges = undirectedEdges.size(); 185 199 } 186 200 187 201 /** … … 202 216 203 217 /** 204 218 * Add a list of node pairs. 205 * @param pairs listof node pairs219 * @param pairs collection of node pairs 206 220 */ 207 221 public void add(Collection<NodePair> pairs) { 208 222 for (NodePair pair: pairs) { … … 229 243 return Optional.ofNullable(successors.get(node)).orElseGet(Collections::emptyList); 230 244 } 231 245 232 p rotectedSet<Node> getNodes() {246 public Set<Node> getNodes() { 233 247 Set<Node> nodes = new LinkedHashSet<>(2 * edges.size()); 234 248 for (NodePair pair: edges) { 235 249 nodes.add(pair.getA()); … … 239 253 } 240 254 241 255 protected boolean isSpanningWay(Collection<NodePair> way) { 242 return numUndirectedE ges == way.size();256 return numUndirectedEdges == way.size(); 243 257 } 244 258 245 259 protected List<Node> buildPathFromNodePairs(Deque<NodePair> path) { … … 287 301 */ 288 302 public List<Node> buildSpanningPath() { 289 303 prepare(); 290 if (numUndirectedE ges > 0 && isConnected()) {304 if (numUndirectedEdges > 0 && isConnected()) { 291 305 // try to find a path from each "terminal node", i.e. from a 292 306 // node which is connected by exactly one undirected edges (or 293 307 // two directed edges in opposite direction) to the graph. A … … 324 338 325 339 /** 326 340 * Find out if the graph is connected. 327 * @return true if it is connected.341 * @return {@code true} if it is connected 328 342 */ 329 343 private boolean isConnected() { 330 344 Set<Node> nodes = getNodes(); … … 350 364 351 365 /** 352 366 * Sort the nodes by number of appearances in the edges. 353 * @return set of nodes which can be start nodes in a spanning way .367 * @return set of nodes which can be start nodes in a spanning way 354 368 */ 355 369 private Set<Node> getMostFrequentVisitedNodesFirst() { 356 370 if (edges.isEmpty())