Changeset 15574 in josm for trunk/src/org


Ignore:
Timestamp:
2019-12-09T09:47:20+01:00 (5 years ago)
Author:
GerdP
Message:

fix #18367 and #18385: CombineWayAction (C) refuses to combine ways or silently reverses ways
Changes:

  • try first to combine the ways with the method Multipolygon.joinWays() If that method returns a single line string we can use it, else use the result of NodeGraph.buildSpanningPathNoRemove(). Both methods will not add or remove segments
  • if ways are combined execute checks for overlapping segments or self-intersection and show a notification popup right after the command was added to the UndoRedoHandler
  • The code which handles reversed ways needed changes. In the unpatched version it sometimes claims wrongly that ways were reversed, in special cases it sometimes silently reverted ways. The old code did not handle the case properly that a node can appear more than once. I really hope that I got it right now.
  • Fix some sonarlint issues
  • let NodeGraph routines return an ArrayList instead of a LinkedList (improves performance a bit)
  • Add unit tests
Location:
trunk/src/org/openstreetmap/josm
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/actions/CombineWayAction.java

    r15555 r15574  
    3232import org.openstreetmap.josm.data.osm.TagCollection;
    3333import org.openstreetmap.josm.data.osm.Way;
     34import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
     35import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay;
    3436import org.openstreetmap.josm.data.preferences.BooleanProperty;
     37import org.openstreetmap.josm.data.validation.Test;
     38import org.openstreetmap.josm.data.validation.tests.OverlappingWays;
     39import org.openstreetmap.josm.data.validation.tests.SelfIntersectingWay;
    3540import org.openstreetmap.josm.gui.ExtendedDialog;
    3641import org.openstreetmap.josm.gui.MainApplication;
     
    123128
    124129        // try to build a new way which includes all the combined ways
    125         NodeGraph graph = NodeGraph.createNearlyUndirectedGraphFromNodeWays(ways);
    126         List<Node> path = graph.buildSpanningPathNoRemove();
    127         if (path == null) {
     130        List<Node> path = tryJoin(ways);
     131        if (path.isEmpty()) {
    128132            warnCombiningImpossible();
    129133            return null;
     
    137141        List<Way> reversedWays = new LinkedList<>();
    138142        List<Way> unreversedWays = new LinkedList<>();
    139         for (Way w: ways) {
    140             // Treat zero or one-node ways as unreversed as Combine action action is a good way to fix them (see #8971)
    141             if (w.getNodesCount() < 2 || (path.indexOf(w.getNode(0)) + 1) == path.lastIndexOf(w.getNode(1))) {
    142                 unreversedWays.add(w);
    143             } else {
    144                 reversedWays.add(w);
    145             }
    146         }
     143        detectReversedWays(ways, path, reversedWays, unreversedWays);
    147144        // reverse path if all ways have been reversed
    148145        if (unreversedWays.isEmpty()) {
     
    164161            }
    165162            // if there are still reversed ways with direction-dependent tags, reverse their tags
    166             if (!reversedWays.isEmpty() && PROP_REVERSE_WAY.get()) {
     163            if (!reversedWays.isEmpty() && Boolean.TRUE.equals(PROP_REVERSE_WAY.get())) {
    167164                List<Way> unreversedTagWays = new ArrayList<>(ways);
    168165                unreversedTagWays.removeAll(reversedWays);
     
    211208
    212209        return new Pair<>(targetWay, sequenceCommand);
     210    }
     211
     212    protected static void detectReversedWays(Collection<Way> ways, List<Node> path, List<Way> reversedWays,
     213            List<Way> unreversedWays) {
     214        for (Way w: ways) {
     215            // Treat zero or one-node ways as unreversed as Combine action action is a good way to fix them (see #8971)
     216            if (w.getNodesCount() < 2) {
     217                unreversedWays.add(w);
     218            } else {
     219                boolean foundStartSegment = false;
     220                int last = path.lastIndexOf(w.getNode(0));
     221
     222                for (int i = path.indexOf(w.getNode(0)); i <= last; i++) {
     223                    if (path.get(i) == w.getNode(0) && i + 1 < path.size() && w.getNode(1) == path.get(i + 1)) {
     224                        foundStartSegment = true;
     225                        break;
     226                    }
     227                }
     228                if (foundStartSegment) {
     229                    unreversedWays.add(w);
     230                } else {
     231                    reversedWays.add(w);
     232                }
     233            }
     234        }
     235    }
     236
     237    protected static List<Node> tryJoin(Collection<Way> ways) {
     238        List<Node> path = joinWithMultipolygonCode(ways);
     239        if (path.isEmpty()) {
     240            NodeGraph graph = NodeGraph.createNearlyUndirectedGraphFromNodeWays(ways);
     241            path = graph.buildSpanningPathNoRemove();
     242        }
     243        return path;
     244    }
     245
     246    /**
     247     * Use {@link Multipolygon#joinWays(Collection)} to join ways.
     248     * @param ways the ways
     249     * @return List of nodes of the combined ways or null if ways could not be combined to a single way.
     250     * Result may contain overlapping segments.
     251     */
     252    private static List<Node> joinWithMultipolygonCode(Collection<Way> ways) {
     253        // sort so that old unclosed ways appear first
     254        LinkedList<Way> toJoin = new LinkedList<>(ways);
     255        toJoin.sort((o1, o2) -> {
     256            int d = Boolean.compare(o1.isNew(), o2.isNew());
     257            if (d == 0)
     258                d = Boolean.compare(o1.isClosed(), o2.isClosed());
     259            return d;
     260        });
     261        Collection<JoinedWay> list = Multipolygon.joinWays(toJoin);
     262        if (list.size() == 1) {
     263            // ways form a single line string
     264            return new ArrayList<>(list.iterator().next().getNodes());
     265        }
     266        return Collections.emptyList();
    213267    }
    214268
     
    238292        if (combineResult == null)
    239293            return;
     294
    240295        final Way selectedWay = combineResult.a;
    241296        UndoRedoHandler.getInstance().add(combineResult.b);
     297        Test test = new OverlappingWays();
     298        test.startTest(null);
     299        test.visit(combineResult.a);
     300        test.endTest();
     301        if (test.getErrors().isEmpty()) {
     302            test = new SelfIntersectingWay();
     303            test.startTest(null);
     304            test.visit(combineResult.a);
     305            test.endTest();
     306        }
     307        if (!test.getErrors().isEmpty()) {
     308            new Notification(test.getErrors().get(0).getMessage())
     309            .setIcon(JOptionPane.WARNING_MESSAGE)
     310            .setDuration(Notification.TIME_SHORT)
     311            .show();
     312        }
    242313        if (selectedWay != null) {
    243314            GuiHelper.runInEDT(() -> ds.setSelected(selectedWay));
     
    262333        setEnabled(numWays >= 2);
    263334    }
     335
    264336}
  • trunk/src/org/openstreetmap/josm/data/osm/NodeGraph.java

    r15559 r15574  
    1212import java.util.LinkedHashMap;
    1313import java.util.LinkedHashSet;
    14 import java.util.LinkedList;
    1514import java.util.List;
    1615import java.util.Map;
     
    1817import java.util.Optional;
    1918import java.util.Set;
    20 import java.util.Stack;
    2119import java.util.TreeMap;
    2220
     
    249247    }
    250248
    251     protected List<Node> buildPathFromNodePairs(Stack<NodePair> path) {
    252         List<Node> ret = new LinkedList<>();
    253         for (NodePair pair: path) {
     249    protected List<Node> buildPathFromNodePairs(Deque<NodePair> path) {
     250        List<Node> ret = new ArrayList<>(path.size() + 1);
     251        for (NodePair pair : path) {
    254252            ret.add(pair.getA());
    255253        }
    256         ret.add(path.peek().getB());
     254        ret.add(path.peekLast().getB());
    257255        return ret;
    258256    }
     
    264262     *
    265263     * @param startNode the start node
    266      * @return the spanning path; null, if no path is found
     264     * @return the spanning path; empty list if no path is found
    267265     */
    268266    protected List<Node> buildSpanningPath(Node startNode) {
    269267        if (startNode != null) {
    270             // do not simply replace `Stack` by `ArrayDeque` because of different iteration behaviour, see #11957
    271             Stack<NodePair> path = new Stack<>();
     268            Deque<NodePair> path = new ArrayDeque<>();
    272269            Set<NodePair> dupCheck = new HashSet<>();
    273             Stack<NodePair> nextPairs = new Stack<>();
     270            Deque<NodePair> nextPairs = new ArrayDeque<>();
    274271            nextPairs.addAll(getOutboundPairs(startNode));
    275272            while (!nextPairs.isEmpty()) {
    276                 NodePair cur = nextPairs.pop();
     273                NodePair cur = nextPairs.removeLast();
    277274                if (!dupCheck.contains(cur) && !dupCheck.contains(cur.swap())) {
    278                     while (!path.isEmpty() && !path.peek().isPredecessorOf(cur)) {
    279                         dupCheck.remove(path.pop());
     275                    while (!path.isEmpty() && !path.peekLast().isPredecessorOf(cur)) {
     276                        dupCheck.remove(path.removeLast());
    280277                    }
    281                     path.push(cur);
     278                    path.addLast(cur);
    282279                    dupCheck.add(cur);
    283                     if (isSpanningWay(path)) return buildPathFromNodePairs(path);
    284                     nextPairs.addAll(getOutboundPairs(path.peek()));
     280                    if (isSpanningWay(path))
     281                        return buildPathFromNodePairs(path);
     282                    nextPairs.addAll(getOutboundPairs(path.peekLast()));
    285283                }
    286284            }
     
    324322     * any duplicated edge was removed.
    325323     *
    326      * @return the path; null, if no path was found or duplicated edges were found
    327      * @since 15555
     324     * @return List of nodes that build the path; an empty list if no path or duplicated edges were found
     325     * @since 15573 (return value not null)
    328326     */
    329327    public List<Node> buildSpanningPathNoRemove() {
    330         if (edges.size() != addedEdges)
    331             return null;
    332         return buildSpanningPath();
     328        List<Node> path = null;
     329        if (edges.size() == addedEdges)
     330            path = buildSpanningPath();
     331        return path == null ? Collections.emptyList() : path;
    333332    }
    334333
Note: See TracChangeset for help on using the changeset viewer.