Ticket #21881: josm21881_cycle_dector_v2.patch

File josm21881_cycle_dector_v2.patch, 35.1 KB (added by gaben, 12 months ago)
  • new file src/org/openstreetmap/josm/data/validation/tests/CycleDetector.java

    Subject: [PATCH] Cycle detector #21881
    ---
    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/validation/tests/CycleDetector.java b/src/org/openstreetmap/josm/data/validation/tests/CycleDetector.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.validation.tests;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.util.ArrayDeque;
     7import java.util.ArrayList;
     8import java.util.Arrays;
     9import java.util.Collection;
     10import java.util.Collections;
     11import java.util.Deque;
     12import java.util.HashMap;
     13import java.util.HashSet;
     14import java.util.List;
     15import java.util.Map;
     16import java.util.Queue;
     17import java.util.Set;
     18import java.util.stream.Collectors;
     19
     20import org.openstreetmap.josm.data.osm.Node;
     21import org.openstreetmap.josm.data.osm.NodeGraph;
     22import org.openstreetmap.josm.data.osm.NodePair;
     23import org.openstreetmap.josm.data.osm.OsmPrimitive;
     24import org.openstreetmap.josm.data.osm.Way;
     25import org.openstreetmap.josm.data.osm.WaySegment;
     26import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
     27import org.openstreetmap.josm.data.validation.Severity;
     28import org.openstreetmap.josm.data.validation.Test;
     29import org.openstreetmap.josm.data.validation.TestError;
     30import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     31import org.openstreetmap.josm.spi.preferences.Config;
     32import org.openstreetmap.josm.tools.Pair;
     33
     34/**
     35 * Test for detecting <a href="https://en.wikipedia.org/wiki/Cycle_(graph_theory)">cycles</a> in a directed graph,
     36 * currently used for waterways only. The processed graph consists of ways labeled as waterway.
     37 *
     38 * @author gaben
     39 * @since xxx
     40 */
     41public class CycleDetector extends Test {
     42    public static final int CYCLE_DETECTED = 4200;
     43
     44    /** All waterways for cycle detection */
     45    private final Set<Way> usableWaterways = new HashSet<>();
     46
     47    /** Already visited primitives */
     48    private final Set<Way> visitedWays = new HashSet<>();
     49
     50    /** Currently used directional waterways from the OSM wiki */
     51    private static List<String> directionalWaterways;
     52
     53    protected static final String PREFIX = ValidatorPrefHelper.PREFIX + "." + CycleDetector.class.getSimpleName();
     54
     55    public CycleDetector() {
     56        super(tr("Cycle detector"), tr("Detects cycles in drainage systems."));
     57    }
     58
     59    @Override
     60    public boolean isPrimitiveUsable(OsmPrimitive p) {
     61        return p.isUsable() && (p instanceof Way) && (((Way) p).getNodesCount() > 1) && p.hasTag("waterway", directionalWaterways);
     62    }
     63
     64    @Override
     65    public void visit(Way w) {
     66        if (isPrimitiveUsable(w))
     67            usableWaterways.add(w);
     68    }
     69
     70    @Override
     71    public void startTest(ProgressMonitor progressMonitor) {
     72        super.startTest(progressMonitor);
     73        directionalWaterways = Config.getPref().getList(PREFIX + ".directionalWaterways",
     74            Arrays.asList("river", "stream", "tidal_channel", "drain", "ditch", "fish_pass", "fairway"));
     75    }
     76
     77    @Override
     78    public void endTest() {
     79        for (Collection<Way> graph : getGraphs()) {
     80            NodeGraph nodeGraph = NodeGraph.createDirectedGraphFromWays(graph);
     81            Tarjan tarjan = new Tarjan(nodeGraph);
     82            Collection<List<Node>> scc = tarjan.getSCC();
     83            Map<Node, List<Node>> graphMap = tarjan.getGraphMap();
     84
     85            for (Collection<Node> possibleCycle : scc) {
     86                // there is a cycle in the graph if a strongly connected component has more than one node
     87                if (possibleCycle.size() > 1) {
     88                    errors.add(
     89                        TestError.builder(this, Severity.ERROR, CYCLE_DETECTED)
     90                            .message(tr("Cycle in directional waterway network"))
     91                            .primitives(possibleCycle)
     92                            .highlightWaySegments(createSegments(graphMap, possibleCycle))
     93                            .build()
     94                    );
     95                }
     96            }
     97        }
     98
     99        super.endTest();
     100    }
     101
     102    @Override
     103    public void clear() {
     104        super.clear();
     105        usableWaterways.clear();
     106        visitedWays.clear();
     107    }
     108
     109    /**
     110     * Creates WaySegments from Nodes for the error highlight function.
     111     * @param graphMap the complete graph data
     112     * @param nodes nodes to build the way segments from
     113     * @return WaySegments from the Nodes
     114     */
     115    private static Collection<WaySegment> createSegments(Map<Node, List<Node>> graphMap, Collection<Node> nodes) {
     116        List<NodePair> pairs = new ArrayList<>();
     117
     118        // build new graph exclusively from SCC nodes
     119        for (Node node : nodes) {
     120            for (Node successor : graphMap.get(node)) {
     121                // check for outbound nodes
     122                if (nodes.contains(successor)) {
     123                    pairs.add(new NodePair(node, successor));
     124                }
     125            }
     126        }
     127
     128        Collection<WaySegment> segments = new ArrayList<>();
     129
     130        for (NodePair pair : pairs) {
     131            final Node n = pair.getA();
     132            final Node m = pair.getB();
     133
     134            if (n != null && m != null && !n.equals(m)) {
     135                List<Way> intersect = new ArrayList<>(n.getParentWays());
     136                List<Way> mWays = m.getParentWays();
     137                intersect.retainAll(mWays);
     138
     139                for (Way w : intersect) {
     140                    if (w.getNeighbours(n).contains(m) && getNodeIndex(w, n) + 1 == getNodeIndex(w, m)) {
     141                        segments.add(WaySegment.forNodePair(w, n, m));
     142                    }
     143                }
     144            }
     145        }
     146
     147        return segments;
     148    }
     149
     150    /**
     151     * Returns the way index of a node. Only the first occurrence is considered in case it's a closed way.
     152     * @param w parent way
     153     * @param n the node to look up
     154     * @return >=0 if the node is found or<br>-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            Collection<Way> graph = buildGraph(waterway);
     177
     178            if (!graph.isEmpty())
     179                graphs.add(graph);
     180        }
     181
     182        return graphs;
     183    }
     184
     185    /**
     186     * Returns a collection of ways, which belongs to the same graph.
     187     *
     188     * @param way starting way to extend the graph from
     189     * @return a collection of ways which belongs to the same graph
     190     */
     191    private Collection<Way> buildGraph(Way way) {
     192        if (visitedWays.contains(way))
     193            return Collections.emptySet();
     194
     195        final Set<Way> graph = new HashSet<>();
     196        Queue<Way> queue = new ArrayDeque<>();
     197        queue.offer(way);
     198
     199        while (!queue.isEmpty()) {
     200            Way currentWay = queue.poll();
     201            visitedWays.add(currentWay);
     202
     203            for (Node node : currentWay.getNodes()) {
     204                Collection<Way> referrers = node.referrers(Way.class)
     205                    .filter(this::isPrimitiveUsable)
     206                    .filter(candidate -> candidate != currentWay)
     207                    .collect(Collectors.toSet());
     208
     209                if (!referrers.isEmpty()) {
     210                    for (Way referrer : referrers) {
     211                        if (!visitedWays.contains(referrer)) {
     212                            queue.offer(referrer);
     213                            visitedWays.add(referrer);
     214                        }
     215                    }
     216                    graph.addAll(referrers);
     217                }
     218            }
     219        }
     220        return graph;
     221    }
     222
     223
     224
     225    /**
     226     * Tarjan's strongly connected components algorithm for JOSM.
     227     *
     228     * @see <a href="https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm">
     229     * Tarjan's strongly connected components algorithm</a>
     230     */
     231    public static final class Tarjan {
     232
     233        /**
     234         * Used to remember visited nodes and its metadata. Key is used for storing
     235         * the unique ID of the nodes instead of the full data to save space.
     236         */
     237        private final Map<Long, TarjanHelper> registry;
     238
     239        /** Used to store the graph data as a map. */
     240        private final Map<Node, List<Node>> graphMap;
     241
     242        /** Used to store strongly connected components. */
     243        private final Collection<List<Node>> scc = new HashSet<>();
     244
     245        /** Used on algorithm runtime to keep track discovery progress. */
     246        private final Deque<Node> stack = new ArrayDeque<>();
     247
     248        /** Used on algorithm runtime to keep track discovery progress. */
     249        private int index = 0;
     250
     251        public Tarjan(NodeGraph graph) {
     252            graphMap = graph.createMap();
     253
     254            this.registry = new HashMap<>((int) (graph.getEdges().size() / 0.75) + 1);
     255        }
     256
     257        /**
     258         * Returns the strongly connected components in the current graph.
     259         *
     260         * @return the strongly connected components in the current graph
     261         */
     262        public Collection<List<Node>> getSCC() {
     263            for (Node node : graphMap.keySet()) {
     264                if (!registry.containsKey(node.getUniqueId())) {
     265                    strongConnect(node);
     266                }
     267            }
     268            return scc;
     269        }
     270
     271        /**
     272         * Returns the graph data as a map.
     273         * @see NodeGraph#createMap()
     274         * @return the graph data as a map
     275         */
     276        public Map<Node, List<Node>> getGraphMap() {
     277            return graphMap;
     278        }
     279
     280        /**
     281         * Calculates strongly connected components available from the given node, in an iterative fashion.
     282         *
     283         * @param u0 the node to generate strongly connected components from
     284         */
     285        private void strongConnect(final Node u0) {
     286            final Deque<Pair<Node, Integer>> work = new ArrayDeque<>();
     287            work.push(new Pair<>(u0, 0));
     288            boolean recurse;
     289
     290            while (!work.isEmpty()) {
     291                Pair<Node, Integer> popped = work.remove();
     292                Node u = popped.a;
     293                int j = popped.b;
     294
     295                if (j == 0) {
     296                    index++;
     297                    registry.put(u.getUniqueId(), new TarjanHelper(index));
     298                    stack.push(u);
     299                }
     300
     301                recurse = false;
     302                List<Node> successors = getSuccessors(u);
     303
     304                for (int i = j; i < successors.size(); i++) {
     305                    Node v = successors.get(i);
     306                    if (!registry.containsKey(v.getUniqueId())) {
     307                        work.push(new Pair<>(u, i + 1));
     308                        work.push(new Pair<>(v, 0));
     309                        recurse = true;
     310                        break;
     311                    } else if (stack.contains(v)) {
     312                        TarjanHelper uHelper = registry.get(u.getUniqueId());
     313                        TarjanHelper vHelper = registry.get(v.getUniqueId());
     314                        uHelper.lowlink = Math.min(uHelper.lowlink, vHelper.index);
     315                    }
     316                }
     317
     318                if (!recurse) {
     319                    TarjanHelper uHelper = registry.get(u.getUniqueId());
     320                    if (uHelper.lowlink == uHelper.index) {
     321                        List<Node> currentSCC = new ArrayList<>();
     322                        Node v;
     323                        do {
     324                            v = stack.remove();
     325                            currentSCC.add(v);
     326                        } while (!v.equals(u));
     327                        scc.add(currentSCC);
     328                    }
     329                    if (!work.isEmpty()) {
     330                        Node v = u;
     331                        Pair<Node, Integer> peeked = work.peek();
     332                        u = peeked.a;
     333                        TarjanHelper vHelper = registry.get(v.getUniqueId());
     334                        uHelper = registry.get(u.getUniqueId());
     335                        uHelper.lowlink = Math.min(uHelper.lowlink, vHelper.lowlink);
     336                    }
     337                }
     338            }
     339        }
     340
     341        /**
     342         * Returns the next direct successors from the graph of the given node.
     343         *
     344         * @param node a node to start search from
     345         * @return direct successors of the node or an empty list, if it's a terminal node
     346         */
     347        private List<Node> getSuccessors(Node node) {
     348            return graphMap.getOrDefault(node, Collections.emptyList());
     349        }
     350
     351        /**
     352         * Helper class for storing the Tarjan algorithm runtime metadata.
     353         */
     354        private static class TarjanHelper {
     355            private final int index;
     356            private int lowlink;
     357
     358            private TarjanHelper(int index) {
     359                this.index = index;
     360                this.lowlink = index;
     361            }
     362        }
     363    }
     364}
  • new file test/data/CycleDetector_test_wikipedia.osm

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/data/CycleDetector_test_wikipedia.osm b/test/data/CycleDetector_test_wikipedia.osm
    new file mode 100644
    - +  
     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>
  • new file test/unit/org/openstreetmap/josm/data/validation/tests/CycleDetectorTest.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/data/validation/tests/CycleDetectorTest.java b/test/unit/org/openstreetmap/josm/data/validation/tests/CycleDetectorTest.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.validation.tests;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5
     6import java.nio.file.Files;
     7import java.nio.file.Paths;
     8
     9import org.junit.jupiter.api.Test;
     10import org.junit.jupiter.api.extension.RegisterExtension;
     11import org.openstreetmap.josm.TestUtils;
     12import org.openstreetmap.josm.data.osm.DataSet;
     13import org.openstreetmap.josm.io.OsmReader;
     14import org.openstreetmap.josm.testutils.JOSMTestRules;
     15
     16/**
     17 * JUnit test for {@link CycleDetector} validation test.
     18 */
     19class CycleDetectorTest {
     20    /**
     21     * Setup test.
     22     */
     23    @RegisterExtension
     24    public JOSMTestRules test = new JOSMTestRules();
     25
     26    @Test()
     27    void testCycleDetection() throws Exception {
     28        CycleDetector cycleDetector = new CycleDetector();
     29        DataSet ds = OsmReader.parseDataSet(
     30            Files.newInputStream(Paths.get(TestUtils.getTestDataRoot(), "CycleDetector_test_wikipedia.osm")), null);
     31        cycleDetector.startTest(null);
     32        cycleDetector.visit(ds.allPrimitives());
     33        cycleDetector.endTest();
     34
     35        // we have 4 cycles in the test file
     36        assertEquals(4, cycleDetector.getErrors().size());
     37    }
     38}
     39 No newline at end of file
  • src/org/openstreetmap/josm/data/osm/NodeGraph.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/NodeGraph.java b/src/org/openstreetmap/josm/data/osm/NodeGraph.java
    a b  
    2424
    2525/**
    2626 * A directed or undirected graph of nodes.
     27 *
    2728 * @since 12463 (extracted from CombineWayAction)
    2829 */
    2930public class NodeGraph {
     31    private final Set<NodePair> edges;
     32    private final Map<Node, List<NodePair>> successors = new LinkedHashMap<>();
     33    private final Map<Node, List<NodePair>> predecessors = new LinkedHashMap<>();
     34    private int numUndirectedEdges;
     35
     36    /**
     37     * The number of edges that were added.
     38     */
     39    private int addedEdges;
     40
     41    /**
     42     * Constructs a new {@code NodeGraph}.
     43     */
     44    public NodeGraph() {
     45        edges = new LinkedHashSet<>();
     46    }
    3047
    3148    /**
    3249     * Builds a list of pair of nodes from the given way.
    33      * @param way way
     50     * @param way      way
    3451     * @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 inversed copy)
     52     *                 if {@code false} each pair of nodes will occur twice (the pair and its inverse copy)
    3653     * @return a list of pair of nodes from the given way
    3754     */
    3855    public static List<NodePair> buildNodePairs(Way way, boolean directed) {
    3956        List<NodePair> pairs = new ArrayList<>();
    40         for (Pair<Node, Node> pair: way.getNodePairs(false /* don't sort */)) {
     57        for (Pair<Node, Node> pair : way.getNodePairs(false)) {
    4158            pairs.add(new NodePair(pair));
    4259            if (!directed) {
    4360                pairs.add(new NodePair(pair).swap());
     
    4865
    4966    /**
    5067     * Builds a list of pair of nodes from the given ways.
    51      * @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 inversed copy)
     68     * @param ways     ways
     69     * @param directed if {@code true} each pair of nodes will occur once, in the way nodes order.<p>
     70     *                 if {@code false} each pair of nodes will occur twice (the pair and its inverse copy)
    5471     * @return a list of pair of nodes from the given ways
    5572     */
    5673    public static List<NodePair> buildNodePairs(List<Way> ways, boolean directed) {
    5774        List<NodePair> pairs = new ArrayList<>();
    58         for (Way w: ways) {
     75        for (Way w : ways) {
    5976            pairs.addAll(buildNodePairs(w, directed));
    6077        }
    6178        return pairs;
    6279    }
    6380
    6481    /**
    65      * Builds a new list of pair nodes without the duplicated pairs (including inversed copies).
     82     * Builds a new list of pair nodes without the duplicated pairs (including inverse copies).
    6683     * @param pairs existing list of pairs
    6784     * @return a new list of pair nodes without the duplicated pairs
    6885     */
    6986    public static List<NodePair> eliminateDuplicateNodePairs(List<NodePair> pairs) {
    7087        List<NodePair> cleaned = new ArrayList<>();
    71         for (NodePair p: pairs) {
     88        for (NodePair p : pairs) {
    7289            if (!cleaned.contains(p) && !cleaned.contains(p.swap())) {
    7390                cleaned.add(p);
    7491            }
     
    7693        return cleaned;
    7794    }
    7895
     96    /**
     97     * Create a directed graph from the given node pairs.
     98     * @param pairs Node pairs to build the graph from
     99     * @return node graph structure
     100     */
    79101    public static NodeGraph createDirectedGraphFromNodePairs(List<NodePair> pairs) {
    80102        NodeGraph graph = new NodeGraph();
    81         for (NodePair pair: pairs) {
     103        for (NodePair pair : pairs) {
    82104            graph.add(pair);
    83105        }
    84106        return graph;
    85107    }
    86108
     109    /**
     110     * Create a directed graph from the given ways.
     111     * @param ways ways to build the graph from
     112     * @return node graph structure
     113     */
    87114    public static NodeGraph createDirectedGraphFromWays(Collection<Way> ways) {
    88115        NodeGraph graph = new NodeGraph();
    89         for (Way w: ways) {
    90             graph.add(buildNodePairs(w, true /* directed */));
     116        for (Way w : ways) {
     117            graph.add(buildNodePairs(w, true));
    91118        }
    92119        return graph;
    93120    }
     
    99126     */
    100127    public static NodeGraph createUndirectedGraphFromNodeList(List<NodePair> pairs) {
    101128        NodeGraph graph = new NodeGraph();
    102         for (NodePair pair: pairs) {
     129        for (NodePair pair : pairs) {
    103130            graph.add(pair);
    104131            graph.add(pair.swap());
    105132        }
     
    108135
    109136    /**
    110137     * Create an undirected graph from the given ways, but prevent reversing of all
    111      * non-new ways by fix one direction.
     138     * non-new ways by fixing one direction.
    112139     * @param ways Ways to build the graph from
    113140     * @return node graph structure
    114141     * @since 8181
    115142     */
    116143    public static NodeGraph createUndirectedGraphFromNodeWays(Collection<Way> ways) {
    117144        NodeGraph graph = new NodeGraph();
    118         for (Way w: ways) {
    119             graph.add(buildNodePairs(w, false /* undirected */));
     145        for (Way w : ways) {
     146            graph.add(buildNodePairs(w, false));
    120147        }
    121148        return graph;
    122149    }
    123150
     151    /**
     152     * Create a nearly undirected graph from the given ways, but prevent reversing of all
     153     * non-new ways by fixing one direction.
     154     * The first new way gives the direction of the graph.
     155     * @param ways Ways to build the graph from
     156     * @return node graph structure
     157     */
    124158    public static NodeGraph createNearlyUndirectedGraphFromNodeWays(Collection<Way> ways) {
    125159        boolean dir = true;
    126160        NodeGraph graph = new NodeGraph();
    127         for (Way w: ways) {
     161        for (Way w : ways) {
    128162            if (!w.isNew()) {
    129163                /* let the first non-new way give the direction (see #5880) */
    130164                graph.add(buildNodePairs(w, dir));
    131165                dir = false;
    132166            } else {
    133                 graph.add(buildNodePairs(w, false /* undirected */));
     167                graph.add(buildNodePairs(w, false));
    134168            }
    135169        }
    136170        return graph;
    137171    }
    138172
    139     private final Set<NodePair> edges;
    140     private int numUndirectedEges;
    141     /** counts the number of edges that were added */
    142     private int addedEdges;
    143     private final Map<Node, List<NodePair>> successors = new LinkedHashMap<>();
    144     private final Map<Node, List<NodePair>> predecessors = new LinkedHashMap<>();
     173    /**
     174     * Add a node pair.
     175     * @param pair node pair
     176     */
     177    public void add(NodePair pair) {
     178        addedEdges++;
     179        edges.add(pair);
     180    }
     181
     182    /**
     183     * Add a list of node pairs.
     184     * @param pairs collection of node pairs
     185     */
     186    public void add(Collection<NodePair> pairs) {
     187        for (NodePair pair : pairs) {
     188            add(pair);
     189        }
     190    }
     191
     192    /**
     193     * Return the edges containing the node pairs of the graph.
     194     * @return the edges containing the node pairs of the graph
     195     */
     196    public Set<NodePair> getEdges() {
     197        return edges;
     198    }
     199
     200    /**
     201     * Return the graph's nodes.
     202     * @return the graph's nodes
     203     */
     204    public Set<Node> getNodes() {
     205        Set<Node> nodes = new LinkedHashSet<>(2 * edges.size());
     206        for (NodePair pair : edges) {
     207            nodes.add(pair.getA());
     208            nodes.add(pair.getB());
     209        }
     210        return nodes;
     211    }
     212
     213    /**
     214     * Creates a lookup table from the existing edges to make querying possible.
     215     * @return a map containing all the graph data, where the key is queryable, values are direct successors of the key node
     216     * @since xxx
     217     */
     218    public Map<Node, List<Node>> createMap() {
     219        final Map<Node, List<Node>> result = new HashMap<>((int) (edges.size() / 0.75) + 1);
    145220
     221        for (NodePair edge : edges) {
     222            result.computeIfAbsent(edge.getA(), k -> new ArrayList<>()).add(edge.getB());
     223        }
     224
     225        return result;
     226    }
     227
     228    /**
     229     * See {@link #prepare()}
     230     */
    146231    protected void rememberSuccessor(NodePair pair) {
    147232        List<NodePair> l = successors.computeIfAbsent(pair.getA(), k -> new ArrayList<>());
    148233        if (!l.contains(pair)) {
     
    150235        }
    151236    }
    152237
     238    /**
     239     * See {@link #prepare()}
     240     */
    153241    protected void rememberPredecessors(NodePair pair) {
    154242        List<NodePair> l = predecessors.computeIfAbsent(pair.getB(), k -> new ArrayList<>());
    155243        if (!l.contains(pair)) {
     
    157245        }
    158246    }
    159247
     248    /**
     249     * Replies true if {@code n} is a terminal node of the graph. Internal variables should be initialized first.
     250     * @param n Node to check
     251     * @return {@code true} if it is a terminal node
     252     * @see #prepare()
     253     */
    160254    protected boolean isTerminalNode(Node n) {
    161255        if (successors.get(n) == null) return false;
    162256        if (successors.get(n).size() != 1) return false;
     
    174268        successors.clear();
    175269        predecessors.clear();
    176270
    177         for (NodePair pair: edges) {
     271        for (NodePair pair : edges) {
    178272            if (!undirectedEdges.contains(pair) && !undirectedEdges.contains(pair.swap())) {
    179273                undirectedEdges.add(pair);
    180274            }
    181275            rememberSuccessor(pair);
    182276            rememberPredecessors(pair);
    183277        }
    184         numUndirectedEges = undirectedEdges.size();
    185     }
    186 
    187     /**
    188      * Constructs a new {@code NodeGraph}.
    189      */
    190     public NodeGraph() {
    191         edges = new LinkedHashSet<>();
     278        numUndirectedEdges = undirectedEdges.size();
    192279    }
    193280
    194281    /**
    195      * Add a node pair.
    196      * @param pair node pair
     282     * Return the terminal nodes of the graph.
     283     * @return the terminal nodes of the graph
    197284     */
    198     public void add(NodePair pair) {
    199         addedEdges++;
    200         edges.add(pair);
    201     }
    202 
    203     /**
    204      * Add a list of node pairs.
    205      * @param pairs list of node pairs
    206      */
    207     public void add(Collection<NodePair> pairs) {
    208         for (NodePair pair: pairs) {
    209             add(pair);
    210         }
    211     }
    212 
    213285    protected Set<Node> getTerminalNodes() {
    214286        return getNodes().stream().filter(this::isTerminalNode).collect(Collectors.toCollection(LinkedHashSet::new));
    215287    }
     
    229301        return Optional.ofNullable(successors.get(node)).orElseGet(Collections::emptyList);
    230302    }
    231303
    232     protected Set<Node> getNodes() {
    233         Set<Node> nodes = new LinkedHashSet<>(2 * edges.size());
    234         for (NodePair pair: edges) {
    235             nodes.add(pair.getA());
    236             nodes.add(pair.getB());
    237         }
    238         return nodes;
    239     }
    240 
    241304    protected boolean isSpanningWay(Collection<NodePair> way) {
    242         return numUndirectedEges == way.size();
     305        return numUndirectedEdges == way.size();
    243306    }
    244307
    245308    protected List<Node> buildPathFromNodePairs(Deque<NodePair> path) {
     
    248311    }
    249312
    250313    /**
    251      * Tries to find a spanning path starting from node <code>startNode</code>.
    252      *
     314     * Tries to find a spanning path starting from node {@code startNode}.
     315     * <p>
    253316     * Traverses the path in depth-first order.
    254      *
    255317     * @param startNode the start node
    256318     * @return the spanning path; empty list if no path is found
    257319     */
     
    259321        if (startNode != null) {
    260322            Deque<NodePair> path = new ArrayDeque<>();
    261323            Set<NodePair> dupCheck = new HashSet<>();
    262             Deque<NodePair> nextPairs = new ArrayDeque<>();
    263             nextPairs.addAll(getOutboundPairs(startNode));
     324            Deque<NodePair> nextPairs = new ArrayDeque<>(getOutboundPairs(startNode));
    264325            while (!nextPairs.isEmpty()) {
    265326                NodePair cur = nextPairs.removeLast();
    266327                if (!dupCheck.contains(cur) && !dupCheck.contains(cur.swap())) {
     
    280341
    281342    /**
    282343     * 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>
     344     * the segment of a way) exactly once.<p>
     345     * <b>Note that duplicated edges are removed first!</b>
    285346     *
    286      * @return the path; null, if no path was found
     347     * @return the path; {@code null} if no path was found
    287348     */
    288349    public List<Node> buildSpanningPath() {
    289350        prepare();
    290         if (numUndirectedEges > 0 && isConnected()) {
    291             // try to find a path from each "terminal node", i.e. from a
    292             // node which is connected by exactly one undirected edges (or
    293             // two directed edges in opposite direction) to the graph. A
     351        if (numUndirectedEdges > 0 && isConnected()) {
     352            // Try to find a path from each "terminal node", i.e. from a
     353            // node which is connected by exactly one undirected edge (or
     354            // two directed edges in the opposite direction) to the graph. A
    294355            // graph built up from way segments is likely to include such
    295356            // nodes, unless the edges build one or more closed rings.
    296357            // We order the nodes to start with the best candidates, but
     
    324385
    325386    /**
    326387     * Find out if the graph is connected.
    327      * @return true if it is connected.
     388     * @return {@code true} if it is connected
    328389     */
    329390    private boolean isConnected() {
    330391        Set<Node> nodes = getNodes();
     
    350411
    351412    /**
    352413     * Sort the nodes by number of appearances in the edges.
    353      * @return set of nodes which can be start nodes in a spanning way.
     414     * @return set of nodes which can be start nodes in a spanning way
    354415     */
    355416    private Set<Node> getMostFrequentVisitedNodesFirst() {
    356417        if (edges.isEmpty())
    357418            return Collections.emptySet();
    358         // count appearance of nodes in edges
     419        // count the appearance of nodes in edges
    359420        Map<Node, Integer> counters = new HashMap<>();
    360421        for (NodePair pair : edges) {
    361422            Integer c = counters.get(pair.getA());
  • src/org/openstreetmap/josm/data/validation/OsmValidator.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/validation/OsmValidator.java b/src/org/openstreetmap/josm/data/validation/OsmValidator.java
    a b  
    4444import org.openstreetmap.josm.data.validation.tests.ConditionalKeys;
    4545import org.openstreetmap.josm.data.validation.tests.ConnectivityRelations;
    4646import org.openstreetmap.josm.data.validation.tests.CrossingWays;
     47import org.openstreetmap.josm.data.validation.tests.CycleDetector;
    4748import org.openstreetmap.josm.data.validation.tests.DirectionNodes;
    4849import org.openstreetmap.josm.data.validation.tests.DuplicateNode;
    4950import org.openstreetmap.josm.data.validation.tests.DuplicateRelation;
     
    154155        // 3700 .. 3799 is automatically removed since it clashed with pt_assistant.
    155156        SharpAngles.class, // 3800 .. 3899
    156157        ConnectivityRelations.class, // 3900 .. 3999
    157         DirectionNodes.class, // 4000-4099
     158        DirectionNodes.class, // 4000 .. 4099
    158159        RightAngleBuildingTest.class, // 4100 .. 4199
     160        CycleDetector.class, // 4200 .. 4299
    159161    };
    160162
    161163    /**