Ticket #21881: josm21881_loop_detector_v1.patch

File josm21881_loop_detector_v1.patch, 31.4 KB (added by gaben, 3 years ago)

stable version, added tests

  • src/org/openstreetmap/josm/data/osm/NodeGraph.java

    Subject: [PATCH] josm21881_loop_detector_v1
    ---
    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     * counts 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    }
    145191
     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
     216     * @since xxx
     217     */
     218    public Map<Node, Set<Node>> createMap() {
     219        Map<Node, Set<Node>> result = new HashMap<>((int) (edges.size() / 0.75) + 1);
     220        for (NodePair edge : edges) {
     221            if (result.containsKey(edge.getA())) {
     222                result.get(edge.getA()).add(edge.getB());
     223            } else {
     224                result.put(edge.getA(), new HashSet<>(Collections.singletonList(edge.getB())));
     225            }
     226        }
     227
     228        return result;
     229    }
     230
     231    /**
     232     * See {@link #prepare()}
     233     */
    146234    protected void rememberSuccessor(NodePair pair) {
    147235        List<NodePair> l = successors.computeIfAbsent(pair.getA(), k -> new ArrayList<>());
    148236        if (!l.contains(pair)) {
     
    150238        }
    151239    }
    152240
     241    /**
     242     * See {@link #prepare()}
     243     */
    153244    protected void rememberPredecessors(NodePair pair) {
    154245        List<NodePair> l = predecessors.computeIfAbsent(pair.getB(), k -> new ArrayList<>());
    155246        if (!l.contains(pair)) {
     
    157248        }
    158249    }
    159250
     251    /**
     252     * Replies true if {@code n} is a terminal node of the graph. Internal variables should be initialized first.
     253     * @param n Node to check
     254     * @return {@code true} if it is a terminal node
     255     * @see #prepare()
     256     */
    160257    protected boolean isTerminalNode(Node n) {
    161258        if (successors.get(n) == null) return false;
    162259        if (successors.get(n).size() != 1) return false;
     
    174271        successors.clear();
    175272        predecessors.clear();
    176273
    177         for (NodePair pair: edges) {
     274        for (NodePair pair : edges) {
    178275            if (!undirectedEdges.contains(pair) && !undirectedEdges.contains(pair.swap())) {
    179276                undirectedEdges.add(pair);
    180277            }
    181278            rememberSuccessor(pair);
    182279            rememberPredecessors(pair);
    183280        }
    184         numUndirectedEges = undirectedEdges.size();
    185     }
    186 
    187     /**
    188      * Constructs a new {@code NodeGraph}.
    189      */
    190     public NodeGraph() {
    191         edges = new LinkedHashSet<>();
     281        numUndirectedEdges = undirectedEdges.size();
    192282    }
    193283
    194284    /**
    195      * Add a node pair.
    196      * @param pair node pair
     285     * Return the terminal nodes of the graph.
     286     * @return the terminal nodes of the graph
    197287     */
    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 
    213288    protected Set<Node> getTerminalNodes() {
    214289        return getNodes().stream().filter(this::isTerminalNode).collect(Collectors.toCollection(LinkedHashSet::new));
    215290    }
     
    229304        return Optional.ofNullable(successors.get(node)).orElseGet(Collections::emptyList);
    230305    }
    231306
    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 
    241307    protected boolean isSpanningWay(Collection<NodePair> way) {
    242         return numUndirectedEges == way.size();
     308        return numUndirectedEdges == way.size();
    243309    }
    244310
    245311    protected List<Node> buildPathFromNodePairs(Deque<NodePair> path) {
     
    248314    }
    249315
    250316    /**
    251      * Tries to find a spanning path starting from node <code>startNode</code>.
    252      *
     317     * Tries to find a spanning path starting from node {@code startNode}.
     318     * <p>
    253319     * Traverses the path in depth-first order.
    254      *
    255320     * @param startNode the start node
    256321     * @return the spanning path; empty list if no path is found
    257322     */
     
    283348     * the segment of a way) exactly once.
    284349     * <p><b>Note that duplicated edges are removed first!</b>
    285350     *
    286      * @return the path; null, if no path was found
     351     * @return the path; {@code null}, if no path was found
    287352     */
    288353    public List<Node> buildSpanningPath() {
    289354        prepare();
    290         if (numUndirectedEges > 0 && isConnected()) {
     355        if (numUndirectedEdges > 0 && isConnected()) {
    291356            // try to find a path from each "terminal node", i.e. from a
    292357            // node which is connected by exactly one undirected edges (or
    293358            // two directed edges in opposite direction) to the graph. A
     
    324389
    325390    /**
    326391     * Find out if the graph is connected.
    327      * @return true if it is connected.
     392     * @return {@code true} if it is connected
    328393     */
    329394    private boolean isConnected() {
    330395        Set<Node> nodes = getNodes();
     
    350415
    351416    /**
    352417     * Sort the nodes by number of appearances in the edges.
    353      * @return set of nodes which can be start nodes in a spanning way.
     418     * @return set of nodes which can be start nodes in a spanning way
    354419     */
    355420    private Set<Node> getMostFrequentVisitedNodesFirst() {
    356421        if (edges.isEmpty())
  • new file src/org/openstreetmap/josm/data/validation/tests/CycleDetector.java

    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.Map;
     15import java.util.Set;
     16import java.util.stream.Collectors;
     17
     18import org.openstreetmap.josm.data.osm.Node;
     19import org.openstreetmap.josm.data.osm.NodeGraph;
     20import org.openstreetmap.josm.data.osm.OsmPrimitive;
     21import org.openstreetmap.josm.data.osm.Way;
     22import org.openstreetmap.josm.data.validation.Severity;
     23import org.openstreetmap.josm.data.validation.Test;
     24import org.openstreetmap.josm.data.validation.TestError;
     25
     26/**
     27 * Test for detecting <a href="https://en.wikipedia.org/wiki/Cycle_(graph_theory)">cycles</a> in a directed graph,
     28 * currently used for waterways only. The graph consists of OSM dataset ways labeled as waterway.
     29 *
     30 * @author gaben
     31 * @since xxx
     32 */
     33public class CycleDetector extends Test {
     34    public static final int CYCLE_DETECTED = 4100;
     35
     36    /**
     37     * All waterways for cycle detection.
     38     */
     39    private final Set<Way> usableWaterways = new HashSet<>();
     40
     41    private final Set<Way> visitedWays = new HashSet<>();
     42
     43    /**
     44     * Currently used directional waterways from the OSM wiki
     45     */
     46    private static final Set<String> directionalWaterways = new HashSet<>(
     47            Arrays.asList("river", "stream", "tidal_channel", "drain", "ditch", "fish_pass", "fairway"));
     48
     49    public CycleDetector() {
     50        super(tr("Loop detector"), tr("Detects loops in connected, directional waterways."));
     51    }
     52
     53    @Override
     54    public boolean isPrimitiveUsable(OsmPrimitive p) {
     55        return p.isUsable() && (p instanceof Way) && ((Way) p).getNodesCount() > 1 && p.hasTag("waterway", directionalWaterways);
     56    }
     57
     58    @Override
     59    public void visit(Way w) {
     60        if (isPrimitiveUsable(w))
     61            usableWaterways.add(w);
     62    }
     63
     64    @Override
     65    public void endTest() {
     66        Collection<Collection<Way>> graphs = getGraphs();
     67
     68        for (Collection<Way> graph : graphs) {
     69            NodeGraph nodeGraph = NodeGraph.createDirectedGraphFromWays(graph);
     70            Tarjan tarjan = new Tarjan(nodeGraph);
     71            Collection<Collection<Node>> scc = tarjan.getSCC();
     72            for (Collection<Node> possibleCycle : scc) {
     73                if (possibleCycle.size() > 1) {
     74                    errors.add(
     75                            TestError.builder(this, Severity.ERROR, CYCLE_DETECTED)
     76                                    .message(tr("Cycle in directional waterway network"))
     77                                    .primitives(possibleCycle)
     78                                    .build()
     79                    );
     80                }
     81            }
     82        }
     83
     84        super.endTest();
     85    }
     86
     87    @Override
     88    public void clear() {
     89        super.clear();
     90        usableWaterways.clear();
     91        visitedWays.clear();
     92    }
     93
     94    /**
     95     * Returns all directional waterways which connect to at least one other usable way.
     96     *
     97     * @return all directional waterways which connect to at least one other usable way
     98     */
     99    private Collection<Collection<Way>> getGraphs() {
     100        // HashSet doesn't make a difference here
     101        Collection<Collection<Way>> graphs = new ArrayList<>();
     102
     103        for (Way waterway : usableWaterways) {
     104            Collection<Way> graph = buildGraph(waterway);
     105
     106            if (!graph.isEmpty())
     107                graphs.add(graph);
     108        }
     109
     110        return graphs;
     111    }
     112
     113    /**
     114     * Returns a collection of ways which belongs to the same graph.
     115     *
     116     * @param way starting way to extend the graph from
     117     * @return a collection of ways which belongs to the same graph
     118     */
     119    private Collection<Way> buildGraph(Way way) {
     120        if (visitedWays.contains(way))
     121            return Collections.emptySet();
     122
     123        final Set<Way> graph = new HashSet<>();
     124
     125        for (Node node : way.getNodes()) {
     126            Collection<Way> referrers = node.referrers(Way.class)
     127                    .filter(this::isPrimitiveUsable)
     128                    .filter(candidate -> candidate != way)
     129                    .collect(Collectors.toSet());
     130
     131            if (!referrers.isEmpty()) {
     132                visitedWays.add(way);
     133                for (Way referrer : referrers) {
     134                    graph.addAll(buildGraph(referrer));
     135                }
     136                graph.addAll(referrers);
     137            }
     138        }
     139        return graph;
     140    }
     141
     142    /**
     143     * Tarjan's algorithm implementation for JOSM.
     144     *
     145     * @see <a href="https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm">
     146     * Tarjan's strongly connected components algorithm</a>
     147     */
     148    public static final class Tarjan {
     149
     150        /**
     151         * A simple key-value registry to remember visited nodes and its metadata. Key is used for storing the unique
     152         * ID of the primitives instead of the full primitives to save space.
     153         */
     154        private final Map<Long, TarjanHelper> registry;
     155        private final Collection<Collection<Node>> scc = new HashSet<>();
     156        private final Deque<Node> stack = new ArrayDeque<>();
     157        private final NodeGraph graph;
     158        private final Map<Node, Set<Node>> graphMap;
     159        private int index = 0;
     160
     161        public Tarjan(NodeGraph graph) {
     162            this.graph = graph;
     163
     164            graphMap = this.graph.createMap();
     165
     166            this.registry = new HashMap<>((int) (this.graph.getEdges().size() / 0.75) + 1);
     167        }
     168
     169        /**
     170         * Returns the strongly connected components in the current graph.
     171         *
     172         * @return the strongly connected components in the current graph
     173         */
     174        public Collection<Collection<Node>> getSCC() {
     175            for (Node node : graph.getNodes()) {
     176                if (!registry.containsKey(node.getUniqueId())) {
     177                    strongConnect(node);
     178                }
     179            }
     180            return scc;
     181        }
     182
     183        /**
     184         * Calculates strongly connected components available from the given node.
     185         *
     186         * @param v the node to generate strongly connected components from
     187         */
     188        private void strongConnect(Node v) {
     189            registry.put(v.getUniqueId(), new TarjanHelper(index, index));
     190            index++;
     191            stack.push(v);
     192
     193            for (Node w : getSuccessors(v)) {
     194                if (!registry.containsKey(w.getUniqueId())) {
     195                    strongConnect(w);
     196                    TarjanHelper vHelper = registry.get(v.getUniqueId());
     197                    TarjanHelper wHelper = registry.get(w.getUniqueId());
     198                    vHelper.lowlink = Math.min(vHelper.lowlink, wHelper.lowlink);
     199                } else if (stack.contains(w)) {
     200                    TarjanHelper vHelper = registry.get(v.getUniqueId());
     201                    TarjanHelper wHelper = registry.get(w.getUniqueId());
     202                    vHelper.lowlink = Math.min(vHelper.lowlink, wHelper.index);
     203                }
     204            }
     205
     206            final TarjanHelper vHelper = registry.get(v.getUniqueId());
     207            if (vHelper.lowlink == vHelper.index) {
     208                Collection<Node> currentSCC = new HashSet<>();
     209                Node w;
     210                do {
     211                    w = stack.remove();
     212                    currentSCC.add(w);
     213                } while (!w.equals(v));
     214                scc.add(currentSCC);
     215            }
     216        }
     217
     218        /**
     219         * Returns the next direct successors from the graph of the given node.
     220         *
     221         * @param node a node to start search from
     222         * @return a collection of nodes from the graph which are direct neighbors of the given node
     223         */
     224        private Set<Node> getSuccessors(Node node) {
     225            return graphMap.getOrDefault(node, Collections.emptySet());
     226        }
     227
     228        /**
     229         * Helper class for storing the Tarjan algorithm runtime metadata.
     230         */
     231        private static final class TarjanHelper {
     232            private final int index;
     233            private int lowlink;
     234
     235            private TarjanHelper(int index, int lowlink) {
     236                this.index = index;
     237                this.lowlink = lowlink;
     238            }
     239        }
     240    }
     241}
  • 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  
    5353import org.openstreetmap.josm.data.validation.tests.InternetTags;
    5454import org.openstreetmap.josm.data.validation.tests.Lanes;
    5555import org.openstreetmap.josm.data.validation.tests.LongSegment;
     56import org.openstreetmap.josm.data.validation.tests.CycleDetector;
    5657import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker;
    5758import org.openstreetmap.josm.data.validation.tests.MultipolygonTest;
    5859import org.openstreetmap.josm.data.validation.tests.NameMismatch;
     
    154155        SharpAngles.class, // 3800 .. 3899
    155156        ConnectivityRelations.class, // 3900 .. 3999
    156157        DirectionNodes.class, // 4000-4099
     158        CycleDetector.class, // 4100-4199
    157159    };
    158160
    159161    /**
     
    607609    }
    608610
    609611    /**
    610      * Initialize grid details based on current projection system. Values based on
     612     * Initialize grid details based on the current projection system. Values based on
    611613     * the original value fixed for EPSG:4326 (10000) using heuristics (that is, test&amp;error
    612614     * until most bugs were discovered while keeping the processing time reasonable)
    613615     */
     
    636638    private static boolean testsInitialized;
    637639
    638640    /**
    639      * Initializes all tests if this operations hasn't been performed already.
     641     * Initializes all tests if this operation hasn't been performed already.
    640642     */
    641643    public static synchronized void initializeTests() {
    642644        if (!testsInitialized) {
  • 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    /**
     27     * Test all error cases manually created in direction-nodes.osm.
     28     *
     29     * @throws Exception in case of error
     30     */
     31    @Test
     32    void testCoverage() throws Exception {
     33        CycleDetector cycleDetector = new CycleDetector();
     34        ValidatorTestUtils.testSampleFile("test/data/CycleDetector_test_wikipedia.osm", DataSet::getWays, null, cycleDetector);
     35    }
     36
     37    @Test
     38    void testCycleDetection() throws Exception {
     39        CycleDetector cycleDetector = new CycleDetector();
     40        DataSet ds = OsmReader.parseDataSet(
     41                Files.newInputStream(Paths.get(TestUtils.getTestDataRoot(), "CycleDetector_test_wikipedia.osm")), null);
     42        cycleDetector.startTest(null);
     43        cycleDetector.visit(ds.allPrimitives());
     44        cycleDetector.endTest();
     45
     46        // we have 4 cycles in the test file
     47        assertEquals(4, cycleDetector.getErrors().size());
     48    }
     49}
     50 No newline at end of file
  • test/unit/org/openstreetmap/josm/data/validation/tests/DirectionNodesTest.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/DirectionNodesTest.java b/test/unit/org/openstreetmap/josm/data/validation/tests/DirectionNodesTest.java
    a b  
    88import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
    99
    1010/**
    11  * JUnit Test of Multipolygon validation test.
     11 * JUnit Test of {@link DirectionNodes} validation test.
    1212 */
    1313class DirectionNodesTest {
    1414
    15 
    1615    /**
    1716     * Setup test.
    1817     */
     
    2524     * @throws Exception in case of error
    2625     */
    2726    @Test
    28     void testDirectionsNodesTestFile() throws Exception {
     27    void testDirectionNodesTestFile() throws Exception {
    2928        final DirectionNodes test = new DirectionNodes();
    3029        ValidatorTestUtils.testSampleFile("nodist/data/direction-nodes.osm", ds -> ds.getNodes(), null, test);
    3130    }