Ticket #21881: 21881_github.patch

File 21881_github.patch, 34.6 KB (added by gaben, 5 months ago)

reupload with all the files

  • src/org/openstreetmap/josm/data/algorithms/Tarjan.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.algorithms;
     3
     4import org.openstreetmap.josm.data.osm.Node;
     5import org.openstreetmap.josm.data.osm.NodeGraph;
     6import org.openstreetmap.josm.tools.Pair;
     7import org.openstreetmap.josm.tools.Utils;
     8
     9import java.util.ArrayDeque;
     10import java.util.ArrayList;
     11import java.util.Collection;
     12import java.util.Collections;
     13import java.util.Deque;
     14import java.util.HashMap;
     15import java.util.List;
     16import java.util.Map;
     17
     18/**
     19 * Tarjan's strongly connected components algorithm for JOSM.
     20 *
     21 * @author gaben
     22 * @see <a href="https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm">
     23 * Tarjan's strongly connected components algorithm</a>
     24 * @since xxx
     25 */
     26public final class Tarjan {
     27
     28    /**
     29     * Used to remember visited nodes and its metadata. Key is used for storing
     30     * the unique ID of the nodes instead of the full data to save space.
     31     */
     32    private final Map<Long, TarjanHelper> registry;
     33
     34    /** Used to store the graph data as a map. */
     35    private final Map<Node, List<Node>> graphMap;
     36
     37    /** Used to store strongly connected components. NOTE: single nodes are not stored to save memory. */
     38    private final Collection<List<Node>> scc = new ArrayList<>();
     39
     40    /** Used on algorithm runtime to keep track discovery progress. */
     41    private final Deque<Node> stack = new ArrayDeque<>();
     42
     43    /** Used on algorithm runtime to keep track discovery progress. */
     44    private int index;
     45
     46    /**
     47     * Initialize the Tarjan's algorithm.
     48     *
     49     * @param graph graph data in NodeGraph object format
     50     */
     51    public Tarjan(NodeGraph graph) {
     52        graphMap = graph.createMap();
     53
     54        this.registry = new HashMap<>(Utils.hashMapInitialCapacity(graph.getEdges().size()));
     55    }
     56
     57    /**
     58     * Returns the strongly connected components in the current graph. Single nodes are ignored to save memory.
     59     *
     60     * @return the strongly connected components in the current graph
     61     */
     62    public Collection<List<Node>> getSCC() {
     63        for (Node node : graphMap.keySet()) {
     64            if (!registry.containsKey(node.getUniqueId())) {
     65                strongConnect(node);
     66            }
     67        }
     68        return scc;
     69    }
     70
     71    /**
     72     * Returns the graph data as a map.
     73     *
     74     * @return the graph data as a map
     75     * @see NodeGraph#createMap()
     76     */
     77    public Map<Node, List<Node>> getGraphMap() {
     78        return graphMap;
     79    }
     80
     81    /**
     82     * Calculates strongly connected components available from the given node, in an iterative fashion.
     83     *
     84     * @param u0 the node to generate strongly connected components from
     85     */
     86    private void strongConnect(final Node u0) {
     87        final Deque<Pair<Node, Integer>> work = new ArrayDeque<>();
     88        work.push(new Pair<>(u0, 0));
     89        boolean recurse;
     90
     91        while (!work.isEmpty()) {
     92            Pair<Node, Integer> popped = work.remove();
     93            Node u = popped.a;
     94            int j = popped.b;
     95
     96            if (j == 0) {
     97                index++;
     98                registry.put(u.getUniqueId(), new TarjanHelper(index));
     99                stack.push(u);
     100            }
     101
     102            recurse = false;
     103            List<Node> successors = getSuccessors(u);
     104
     105            for (int i = j; i < successors.size(); i++) {
     106                Node v = successors.get(i);
     107                if (!registry.containsKey(v.getUniqueId())) {
     108                    work.push(new Pair<>(u, i + 1));
     109                    work.push(new Pair<>(v, 0));
     110                    recurse = true;
     111                    break;
     112                } else if (stack.contains(v)) {
     113                    TarjanHelper uHelper = registry.get(u.getUniqueId());
     114                    TarjanHelper vHelper = registry.get(v.getUniqueId());
     115                    uHelper.lowlink = Math.min(uHelper.lowlink, vHelper.index);
     116                }
     117            }
     118
     119            if (!recurse) {
     120                TarjanHelper uHelper = registry.get(u.getUniqueId());
     121                if (uHelper.lowlink == uHelper.index) {
     122                    List<Node> currentSCC = new ArrayList<>();
     123                    Node v;
     124                    do {
     125                        v = stack.remove();
     126                        currentSCC.add(v);
     127                    } while (!v.equals(u));
     128
     129                    // store the component only if it makes a cycle, otherwise it's a waste of memory
     130                    if (currentSCC.size() > 1) {
     131                        scc.add(currentSCC);
     132                    }
     133                }
     134                if (!work.isEmpty()) {
     135                    Node v = u;
     136                    Pair<Node, Integer> peeked = work.peek();
     137                    u = peeked.a;
     138                    TarjanHelper vHelper = registry.get(v.getUniqueId());
     139                    uHelper = registry.get(u.getUniqueId());
     140                    uHelper.lowlink = Math.min(uHelper.lowlink, vHelper.lowlink);
     141                }
     142            }
     143        }
     144    }
     145
     146    /**
     147     * Returns the next direct successors from the graph of the given node.
     148     *
     149     * @param node a node to start search from
     150     * @return direct successors of the node or an empty list, if it's a terminal node
     151     */
     152    private List<Node> getSuccessors(Node node) {
     153        return graphMap.getOrDefault(node, Collections.emptyList());
     154    }
     155
     156    /**
     157     * Helper class for storing the Tarjan algorithm runtime metadata.
     158     */
     159    private static final class TarjanHelper {
     160        private final int index;
     161        private int lowlink;
     162
     163        private TarjanHelper(int index) {
     164            this.index = index;
     165            this.lowlink = index;
     166        }
     167    }
     168}
  • src/org/openstreetmap/josm/data/algorithms/package-info.java

     
     1// License: GPL. For details, see LICENSE file.
     2
     3/**
     4 * General purpose algorithm classes for OSM data validation.
     5 */
     6package org.openstreetmap.josm.data.algorithms;
  • src/org/openstreetmap/josm/data/osm/NodeGraph.java

     
    2121import java.util.stream.Stream;
    2222
    2323import org.openstreetmap.josm.tools.Pair;
     24import org.openstreetmap.josm.tools.Utils;
    2425
    2526/**
    26  * A directed or undirected graph of nodes.
     27 * A directed or undirected graph of nodes. Nodes are connected via edges represented by NodePair instances.
     28 *
    2729 * @since 12463 (extracted from CombineWayAction)
    2830 */
    2931public class NodeGraph {
     
    3234     * Builds a list of pair of nodes from the given way.
    3335     * @param way way
    3436     * @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)
     37     *                 if {@code false} each pair of nodes will occur twice (the pair and its inverse copy)
    3638     * @return a list of pair of nodes from the given way
    3739     */
    3840    public static List<NodePair> buildNodePairs(Way way, boolean directed) {
    3941        List<NodePair> pairs = new ArrayList<>();
    40         for (Pair<Node, Node> pair: way.getNodePairs(false /* don't sort */)) {
     42        for (Pair<Node, Node> pair : way.getNodePairs(false)) {
    4143            pairs.add(new NodePair(pair));
    4244            if (!directed) {
    4345                pairs.add(new NodePair(pair).swap());
     
    4951    /**
    5052     * Builds a list of pair of nodes from the given ways.
    5153     * @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)
     54     * @param directed if {@code true} each pair of nodes will occur once, in the way nodes order.<br>
     55     *                 if {@code false} each pair of nodes will occur twice (the pair and its inverse copy)
    5456     * @return a list of pair of nodes from the given ways
    5557     */
    5658    public static List<NodePair> buildNodePairs(List<Way> ways, boolean directed) {
    5759        List<NodePair> pairs = new ArrayList<>();
    58         for (Way w: ways) {
     60        for (Way w : ways) {
    5961            pairs.addAll(buildNodePairs(w, directed));
    6062        }
    6163        return pairs;
     
    6264    }
    6365
    6466    /**
    65      * Builds a new list of pair nodes without the duplicated pairs (including inversed copies).
     67     * Builds a new list of pair nodes without the duplicated pairs (including inverse copies).
    6668     * @param pairs existing list of pairs
    6769     * @return a new list of pair nodes without the duplicated pairs
    6870     */
    6971    public static List<NodePair> eliminateDuplicateNodePairs(List<NodePair> pairs) {
    7072        List<NodePair> cleaned = new ArrayList<>();
    71         for (NodePair p: pairs) {
     73        for (NodePair p : pairs) {
    7274            if (!cleaned.contains(p) && !cleaned.contains(p.swap())) {
    7375                cleaned.add(p);
    7476            }
     
    7678        return cleaned;
    7779    }
    7880
     81    /**
     82     * Create a directed graph from the given node pairs.
     83     * @param pairs Node pairs to build the graph from
     84     * @return node graph structure
     85     */
    7986    public static NodeGraph createDirectedGraphFromNodePairs(List<NodePair> pairs) {
    8087        NodeGraph graph = new NodeGraph();
    81         for (NodePair pair: pairs) {
     88        for (NodePair pair : pairs) {
    8289            graph.add(pair);
    8390        }
    8491        return graph;
    8592    }
    8693
     94    /**
     95     * Create a directed graph from the given ways.
     96     * @param ways ways to build the graph from
     97     * @return node graph structure
     98     */
    8799    public static NodeGraph createDirectedGraphFromWays(Collection<Way> ways) {
    88100        NodeGraph graph = new NodeGraph();
    89         for (Way w: ways) {
    90             graph.add(buildNodePairs(w, true /* directed */));
     101        for (Way w : ways) {
     102            graph.add(buildNodePairs(w, true));
    91103        }
    92104        return graph;
    93105    }
     
    99111     */
    100112    public static NodeGraph createUndirectedGraphFromNodeList(List<NodePair> pairs) {
    101113        NodeGraph graph = new NodeGraph();
    102         for (NodePair pair: pairs) {
     114        for (NodePair pair : pairs) {
    103115            graph.add(pair);
    104116            graph.add(pair.swap());
    105117        }
     
    108120
    109121    /**
    110122     * Create an undirected graph from the given ways, but prevent reversing of all
    111      * non-new ways by fix one direction.
     123     * non-new ways by fixing one direction.
    112124     * @param ways Ways to build the graph from
    113125     * @return node graph structure
    114126     * @since 8181
     
    115127     */
    116128    public static NodeGraph createUndirectedGraphFromNodeWays(Collection<Way> ways) {
    117129        NodeGraph graph = new NodeGraph();
    118         for (Way w: ways) {
    119             graph.add(buildNodePairs(w, false /* undirected */));
     130        for (Way w : ways) {
     131            graph.add(buildNodePairs(w, false));
    120132        }
    121133        return graph;
    122134    }
    123135
     136    /**
     137     * Create a nearly undirected graph from the given ways, but prevent reversing of all
     138     * non-new ways by fixing one direction.
     139     * The first new way gives the direction of the graph.
     140     * @param ways Ways to build the graph from
     141     * @return node graph structure
     142     */
    124143    public static NodeGraph createNearlyUndirectedGraphFromNodeWays(Collection<Way> ways) {
    125144        boolean dir = true;
    126145        NodeGraph graph = new NodeGraph();
    127         for (Way w: ways) {
     146        for (Way w : ways) {
    128147            if (!w.isNew()) {
    129148                /* let the first non-new way give the direction (see #5880) */
    130149                graph.add(buildNodePairs(w, dir));
    131150                dir = false;
    132151            } else {
    133                 graph.add(buildNodePairs(w, false /* undirected */));
     152                graph.add(buildNodePairs(w, false));
    134153            }
    135154        }
    136155        return graph;
     
    137156    }
    138157
    139158    private final Set<NodePair> edges;
    140     private int numUndirectedEges;
    141     /** counts the number of edges that were added */
     159    private int numUndirectedEdges;
     160    /** The number of edges that were added. */
    142161    private int addedEdges;
    143162    private final Map<Node, List<NodePair>> successors = new LinkedHashMap<>();
    144163    private final Map<Node, List<NodePair>> predecessors = new LinkedHashMap<>();
    145164
     165    /**
     166     * Constructs a lookup table from the existing edges in the graph to enable efficient querying.
     167     * This method creates a map where each node is associated with a list of nodes that are directly connected to it.
     168     *
     169     * @return A map representing the graph structure, where nodes are keys, and values are their direct successors.
     170     * @since xxx
     171     */
     172    public Map<Node, List<Node>> createMap() {
     173        final Map<Node, List<Node>> result = new HashMap<>(Utils.hashMapInitialCapacity(edges.size()));
     174
     175        for (NodePair edge : edges) {
     176            result.computeIfAbsent(edge.getA(), k -> new ArrayList<>()).add(edge.getB());
     177        }
     178
     179        return result;
     180    }
     181
     182    /**
     183     * See {@link #prepare()}
     184     */
    146185    protected void rememberSuccessor(NodePair pair) {
    147186        List<NodePair> l = successors.computeIfAbsent(pair.getA(), k -> new ArrayList<>());
    148187        if (!l.contains(pair)) {
     
    150189        }
    151190    }
    152191
     192    /**
     193     * See {@link #prepare()}
     194     */
    153195    protected void rememberPredecessors(NodePair pair) {
    154196        List<NodePair> l = predecessors.computeIfAbsent(pair.getB(), k -> new ArrayList<>());
    155197        if (!l.contains(pair)) {
     
    157199        }
    158200    }
    159201
     202    /**
     203     * Replies true if {@code n} is a terminal node of the graph. Internal variables should be initialized first.
     204     * @param n Node to check
     205     * @return {@code true} if it is a terminal node
     206     * @see #prepare()
     207     */
    160208    protected boolean isTerminalNode(Node n) {
    161209        if (successors.get(n) == null) return false;
    162210        if (successors.get(n).size() != 1) return false;
     
    174222        successors.clear();
    175223        predecessors.clear();
    176224
    177         for (NodePair pair: edges) {
     225        for (NodePair pair : edges) {
    178226            if (!undirectedEdges.contains(pair) && !undirectedEdges.contains(pair.swap())) {
    179227                undirectedEdges.add(pair);
    180228            }
     
    181229            rememberSuccessor(pair);
    182230            rememberPredecessors(pair);
    183231        }
    184         numUndirectedEges = undirectedEdges.size();
     232        numUndirectedEdges = undirectedEdges.size();
    185233    }
    186234
    187235    /**
     
    202250
    203251    /**
    204252     * Add a list of node pairs.
    205      * @param pairs list of node pairs
     253     * @param pairs collection of node pairs
    206254     */
    207     public void add(Collection<NodePair> pairs) {
    208         for (NodePair pair: pairs) {
     255    public void add(Iterable<NodePair> pairs) {
     256        for (NodePair pair : pairs) {
    209257            add(pair);
    210258        }
    211259    }
    212260
     261    /**
     262     * Return the edges containing the node pairs of the graph.
     263     * @return the edges containing the node pairs of the graph
     264     */
     265    public Collection<NodePair> getEdges() {
     266        return Collections.unmodifiableSet(edges);
     267    }
     268
     269    /**
     270     * Return the terminal nodes of the graph.
     271     * @return the terminal nodes of the graph
     272     */
    213273    protected Set<Node> getTerminalNodes() {
    214274        return getNodes().stream().filter(this::isTerminalNode).collect(Collectors.toCollection(LinkedHashSet::new));
    215275    }
     
    229289        return Optional.ofNullable(successors.get(node)).orElseGet(Collections::emptyList);
    230290    }
    231291
    232     protected Set<Node> getNodes() {
     292    /**
     293     * Return the graph's nodes.
     294     * @return the graph's nodes
     295     */
     296    public Collection<Node> getNodes() {
    233297        Set<Node> nodes = new LinkedHashSet<>(2 * edges.size());
    234         for (NodePair pair: edges) {
     298        for (NodePair pair : edges) {
    235299            nodes.add(pair.getA());
    236300            nodes.add(pair.getB());
    237301        }
     
    239303    }
    240304
    241305    protected boolean isSpanningWay(Collection<NodePair> way) {
    242         return numUndirectedEges == way.size();
     306        return numUndirectedEdges == way.size();
    243307    }
    244308
    245309    protected List<Node> buildPathFromNodePairs(Deque<NodePair> path) {
     
    248312    }
    249313
    250314    /**
    251      * Tries to find a spanning path starting from node <code>startNode</code>.
    252      *
     315     * Tries to find a spanning path starting from node {@code startNode}.
     316     * <p>
    253317     * Traverses the path in depth-first order.
    254318     *
    255319     * @param startNode the start node
     
    259323        if (startNode != null) {
    260324            Deque<NodePair> path = new ArrayDeque<>();
    261325            Set<NodePair> dupCheck = new HashSet<>();
    262             Deque<NodePair> nextPairs = new ArrayDeque<>();
    263             nextPairs.addAll(getOutboundPairs(startNode));
     326            Deque<NodePair> nextPairs = new ArrayDeque<>(getOutboundPairs(startNode));
    264327            while (!nextPairs.isEmpty()) {
    265328                NodePair cur = nextPairs.removeLast();
    266329                if (!dupCheck.contains(cur) && !dupCheck.contains(cur.swap())) {
     
    280343
    281344    /**
    282345     * Tries to find a path through the graph which visits each edge (i.e.
    283      * the segment of a way) exactly once.
    284      * <p><b>Note that duplicated edges are removed first!</b>
     346     * the segment of a way) exactly once.<p>
     347     * <b>Note that duplicated edges are removed first!</b>
    285348     *
    286      * @return the path; null, if no path was found
     349     * @return the path; {@code null}, if no path was found
    287350     */
    288351    public List<Node> buildSpanningPath() {
    289352        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
     353        if (numUndirectedEdges > 0 && isConnected()) {
     354            // Try to find a path from each "terminal node", i.e. from a
     355            // node which is connected by exactly one undirected edge (or
     356            // two directed edges in the opposite direction) to the graph. A
    294357            // graph built up from way segments is likely to include such
    295358            // nodes, unless the edges build one or more closed rings.
    296359            // We order the nodes to start with the best candidates, but
     
    324387
    325388    /**
    326389     * Find out if the graph is connected.
    327      * @return true if it is connected.
     390     * @return {@code true} if it is connected
    328391     */
    329392    private boolean isConnected() {
    330         Set<Node> nodes = getNodes();
     393        Collection<Node> nodes = getNodes();
    331394        if (nodes.isEmpty())
    332395            return false;
    333396        Deque<Node> toVisit = new ArrayDeque<>();
     
    350413
    351414    /**
    352415     * Sort the nodes by number of appearances in the edges.
    353      * @return set of nodes which can be start nodes in a spanning way.
     416     * @return set of nodes which can be start nodes in a spanning way
    354417     */
    355418    private Set<Node> getMostFrequentVisitedNodesFirst() {
    356419        if (edges.isEmpty())
    357420            return Collections.emptySet();
    358         // count appearance of nodes in edges
     421        // count the appearance of nodes in edges
    359422        Map<Node, Integer> counters = new HashMap<>();
    360423        for (NodePair pair : edges) {
    361424            Integer c = counters.get(pair.getA());
  • src/org/openstreetmap/josm/data/validation/OsmValidator.java

     
    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    /**
  • src/org/openstreetmap/josm/data/validation/tests/CycleDetector.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.validation.tests;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5import static org.openstreetmap.josm.tools.I18n.trc;
     6
     7import java.util.ArrayDeque;
     8import java.util.ArrayList;
     9import java.util.Arrays;
     10import java.util.Collection;
     11import java.util.HashSet;
     12import java.util.List;
     13import java.util.Map;
     14import java.util.Queue;
     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.osm.WaySegment;
     23import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
     24import org.openstreetmap.josm.data.validation.Severity;
     25import org.openstreetmap.josm.data.validation.Test;
     26import org.openstreetmap.josm.data.validation.TestError;
     27import org.openstreetmap.josm.data.algorithms.Tarjan;
     28import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     29import org.openstreetmap.josm.spi.preferences.Config;
     30import org.openstreetmap.josm.tools.Pair;
     31
     32/**
     33 * Test for detecting <a href="https://en.wikipedia.org/wiki/Cycle_(graph_theory)">cycles</a> in a directed graph,
     34 * currently used for waterways only. The processed graph consists of ways labeled as waterway.
     35 *
     36 * @author gaben
     37 * @since xxx
     38 */
     39public class CycleDetector extends Test {
     40    public static final int CYCLE_DETECTED = 4200;
     41
     42    /** All waterways for cycle detection */
     43    private final Set<Way> usableWaterways = new HashSet<>();
     44
     45    /** Already visited primitive unique IDs */
     46    private final Set<Long> visitedWays = new HashSet<>();
     47
     48    /** Currently used directional waterways from the OSM wiki */
     49    private List<String> directionalWaterways;
     50
     51    protected static final String PREFIX = ValidatorPrefHelper.PREFIX + "." + CycleDetector.class.getSimpleName();
     52
     53    public CycleDetector() {
     54        super(tr("Cycle detector"), tr("Detects cycles in drainage systems."));
     55    }
     56
     57    @Override
     58    public boolean isPrimitiveUsable(OsmPrimitive p) {
     59        return p.isUsable() && (p instanceof Way) && (((Way) p).getNodesCount() > 1) && p.hasTag("waterway", directionalWaterways);
     60    }
     61
     62    @Override
     63    public void visit(Way w) {
     64        if (isPrimitiveUsable(w))
     65            usableWaterways.add(w);
     66    }
     67
     68    @Override
     69    public void startTest(ProgressMonitor progressMonitor) {
     70        super.startTest(progressMonitor);
     71        directionalWaterways = Config.getPref().getList(PREFIX + ".directionalWaterways",
     72            Arrays.asList("river", "stream", "tidal_channel", "drain", "ditch", "fish_pass", "fairway"));
     73    }
     74
     75    @Override
     76    public void endTest() {
     77        for (Collection<Way> graph : getGraphs()) {
     78            NodeGraph nodeGraph = NodeGraph.createDirectedGraphFromWays(graph);
     79            Tarjan tarjan = new Tarjan(nodeGraph);
     80            Collection<List<Node>> scc = tarjan.getSCC();
     81            Map<Node, List<Node>> graphMap = tarjan.getGraphMap();
     82
     83            for (Collection<Node> possibleCycle : scc) {
     84                // there is a cycle in the graph if a strongly connected component has more than one node
     85                if (possibleCycle.size() > 1) {
     86                    errors.add(
     87                        TestError.builder(this, Severity.ERROR, CYCLE_DETECTED)
     88                            .message(trc("graph theory", "Cycle in directional waterway network"))
     89                            .primitives(possibleCycle)
     90                            .highlightWaySegments(createSegments(graphMap, possibleCycle))
     91                            .build()
     92                    );
     93                }
     94            }
     95        }
     96
     97        super.endTest();
     98    }
     99
     100    @Override
     101    public void clear() {
     102        super.clear();
     103        usableWaterways.clear();
     104        visitedWays.clear();
     105    }
     106
     107    /**
     108     * Creates WaySegments from Nodes for the error highlight function.
     109     *
     110     * @param graphMap the complete graph data
     111     * @param nodes    nodes to build the way segments from
     112     * @return WaySegments from the Nodes
     113     */
     114    private static Collection<WaySegment> createSegments(Map<Node, List<Node>> graphMap, Collection<Node> nodes) {
     115        List<Pair<Node, Node>> pairs = new ArrayList<>();
     116
     117        // build new graph exclusively from SCC nodes
     118        for (Node node : nodes) {
     119            for (Node successor : graphMap.get(node)) {
     120                // check for outbound nodes
     121                if (nodes.contains(successor)) {
     122                    pairs.add(new Pair<>(node, successor));
     123                }
     124            }
     125        }
     126
     127        Collection<WaySegment> segments = new ArrayList<>();
     128
     129        for (Pair<Node, Node> pair : pairs) {
     130            final Node n = pair.a;
     131            final Node m = pair.b;
     132
     133            if (n != null && m != null && !n.equals(m)) {
     134                List<Way> intersect = new ArrayList<>(n.getParentWays());
     135                List<Way> mWays = m.getParentWays();
     136                intersect.retainAll(mWays);
     137
     138                for (Way w : intersect) {
     139                    if (w.getNeighbours(n).contains(m) && getNodeIndex(w, n) + 1 == getNodeIndex(w, m)) {
     140                        segments.add(WaySegment.forNodePair(w, n, m));
     141                    }
     142                }
     143            }
     144        }
     145
     146        return segments;
     147    }
     148
     149    /**
     150     * Returns the way index of a node. Only the first occurrence is considered in case it's a closed way.
     151     *
     152     * @param w parent way
     153     * @param n the node to look up
     154     * @return {@code >=0} if the node is found or<br>{@code -1} if node not part of the way
     155     */
     156    private static int getNodeIndex(Way w, Node n) {
     157        for (int i = 0; i < w.getNodesCount(); i++) {
     158            if (w.getNode(i).equals(n)) {
     159                return i;
     160            }
     161        }
     162
     163        return -1;
     164    }
     165
     166    /**
     167     * Returns all directional waterways which connect to at least one other usable way.
     168     *
     169     * @return all directional waterways which connect to at least one other usable way
     170     */
     171    private Collection<Collection<Way>> getGraphs() {
     172        // HashSet doesn't make a difference here
     173        Collection<Collection<Way>> graphs = new ArrayList<>();
     174
     175        for (Way waterway : usableWaterways) {
     176            if (visitedWays.contains(waterway.getUniqueId())) {
     177                continue;
     178            }
     179            Collection<Way> graph = buildGraph(waterway);
     180
     181            if (!graph.isEmpty()) {
     182                graphs.add(graph);
     183            }
     184        }
     185
     186        return graphs;
     187    }
     188
     189    /**
     190     * Returns a collection of ways, which belongs to the same graph.
     191     *
     192     * @param way starting way to extend the graph from
     193     * @return a collection of ways which belongs to the same graph
     194     */
     195    private Collection<Way> buildGraph(Way way) {
     196        final Set<Way> graph = new HashSet<>();
     197        Queue<Way> queue = new ArrayDeque<>();
     198        queue.offer(way);
     199
     200        while (!queue.isEmpty()) {
     201            Way currentWay = queue.poll();
     202            visitedWays.add(currentWay.getUniqueId());
     203
     204            for (Node node : currentWay.getNodes()) {
     205                Collection<Way> referrers = node.referrers(Way.class)
     206                    .filter(this::isPrimitiveUsable)
     207                    .filter(candidate -> candidate != currentWay)
     208                    .collect(Collectors.toList());
     209
     210                if (!referrers.isEmpty()) {
     211                    for (Way referrer : referrers) {
     212                        if (!visitedWays.contains(referrer.getUniqueId())) {
     213                            queue.offer(referrer);
     214                            visitedWays.add(referrer.getUniqueId());
     215                        }
     216                    }
     217                    graph.addAll(referrers);
     218                }
     219            }
     220        }
     221        return graph;
     222    }
     223}
  • test/data/regress/21881/CycleDetector_test_wikipedia.osm

     
     1<?xml version='1.0' encoding='UTF-8'?>
     2<osm version='0.6' upload='never' generator='JOSM'>
     3  <node id='-137726' action='modify' visible='true' lat='47.74161657891' lon='17.37769604149' />
     4  <node id='-137727' action='modify' visible='true' lat='47.74160961975' lon='17.37612305842' />
     5  <node id='-137728' action='modify' visible='true' lat='47.74043350867' lon='17.37611270985' />
     6  <node id='-137731' action='modify' visible='true' lat='47.74043350867' lon='17.37771673864' />
     7  <node id='-137732' action='modify' visible='true' lat='47.74071883984' lon='17.37897926452' />
     8  <node id='-137733' action='modify' visible='true' lat='47.74045438662' lon='17.38021074469' />
     9  <node id='-137734' action='modify' visible='true' lat='47.74011337916' lon='17.37895856738' />
     10  <node id='-137735' action='modify' visible='true' lat='47.74163049723' lon='17.38024179041' />
     11  <node id='-137736' action='modify' visible='true' lat='47.74119902778' lon='17.38124560197' />
     12  <node id='-137737' action='modify' visible='true' lat='47.74161657891' lon='17.38222871639' />
     13  <node id='-137738' action='modify' visible='true' lat='47.7420091937' lon='17.38123761625' />
     14  <node id='-137746' action='modify' visible='true' lat='47.74044046799' lon='17.38222871639' />
     15  <node id='-137759' action='modify' visible='true' lat='47.73993243552' lon='17.38222871639' />
     16  <node id='-137760' action='modify' visible='true' lat='47.73994635429' lon='17.38319113367' />
     17  <node id='-137761' action='modify' visible='true' lat='47.74046134593' lon='17.38319113367' />
     18  <way id='-103300' action='modify' visible='true'>
     19    <nd ref='-137726' />
     20    <nd ref='-137727' />
     21    <tag k='waterway' v='ditch' />
     22  </way>
     23  <way id='-103301' action='modify' visible='true'>
     24    <nd ref='-137727' />
     25    <nd ref='-137728' />
     26    <tag k='waterway' v='ditch' />
     27  </way>
     28  <way id='-103302' action='modify' visible='true'>
     29    <nd ref='-137728' />
     30    <nd ref='-137726' />
     31    <tag k='waterway' v='ditch' />
     32  </way>
     33  <way id='-103305' action='modify' visible='true'>
     34    <nd ref='-137731' />
     35    <nd ref='-137728' />
     36    <tag k='waterway' v='ditch' />
     37  </way>
     38  <way id='-103306' action='modify' visible='true'>
     39    <nd ref='-137731' />
     40    <nd ref='-137726' />
     41    <tag k='waterway' v='ditch' />
     42  </way>
     43  <way id='-103307' action='modify' visible='true'>
     44    <nd ref='-137733' />
     45    <nd ref='-137732' />
     46    <nd ref='-137731' />
     47    <tag k='waterway' v='ditch' />
     48  </way>
     49  <way id='-103309' action='modify' visible='true'>
     50    <nd ref='-137731' />
     51    <nd ref='-137734' />
     52    <nd ref='-137733' />
     53    <tag k='waterway' v='ditch' />
     54  </way>
     55  <way id='-103311' action='modify' visible='true'>
     56    <nd ref='-137733' />
     57    <nd ref='-137735' />
     58    <tag k='waterway' v='ditch' />
     59  </way>
     60  <way id='-103312' action='modify' visible='true'>
     61    <nd ref='-137735' />
     62    <nd ref='-137726' />
     63    <tag k='waterway' v='ditch' />
     64  </way>
     65  <way id='-103313' action='modify' visible='true'>
     66    <nd ref='-137735' />
     67    <nd ref='-137736' />
     68    <nd ref='-137737' />
     69    <tag k='waterway' v='ditch' />
     70  </way>
     71  <way id='-103315' action='modify' visible='true'>
     72    <nd ref='-137737' />
     73    <nd ref='-137738' />
     74    <nd ref='-137735' />
     75    <tag k='waterway' v='ditch' />
     76  </way>
     77  <way id='-103324' action='modify' visible='true'>
     78    <nd ref='-137746' />
     79    <nd ref='-137733' />
     80    <tag k='waterway' v='ditch' />
     81  </way>
     82  <way id='-103325' action='modify' visible='true'>
     83    <nd ref='-137746' />
     84    <nd ref='-137737' />
     85    <tag k='waterway' v='ditch' />
     86  </way>
     87  <way id='-103359' action='modify' visible='true'>
     88    <nd ref='-137746' />
     89    <nd ref='-137759' />
     90    <nd ref='-137760' />
     91    <nd ref='-137761' />
     92    <nd ref='-137746' />
     93    <tag k='waterway' v='ditch' />
     94  </way>
     95</osm>
  • test/unit/org/openstreetmap/josm/data/validation/tests/CycleDetectorTest.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.validation.tests;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5
     6import org.junit.jupiter.api.Test;
     7import org.openstreetmap.josm.TestUtils;
     8import org.openstreetmap.josm.data.osm.DataSet;
     9import org.openstreetmap.josm.io.OsmReader;
     10import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
     11
     12/**
     13 * JUnit test for {@link CycleDetector} validation test.
     14 */
     15@BasicPreferences
     16class CycleDetectorTest {
     17
     18    @Test
     19    void testCycleDetection() throws Exception {
     20        CycleDetector cycleDetector = new CycleDetector();
     21        DataSet ds = OsmReader.parseDataSet(TestUtils.getRegressionDataStream(21881, "CycleDetector_test_wikipedia.osm"), null);
     22        cycleDetector.startTest(null);
     23        cycleDetector.visit(ds.allPrimitives());
     24        cycleDetector.endTest();
     25
     26        // we have 4 cycles in the test file
     27        assertEquals(4, cycleDetector.getErrors().size());
     28    }
     29}