Ignore:
Timestamp:
2020-05-13T11:34:47+02:00 (5 years ago)
Author:
gerdp
Message:

see #josm19188: circle arc: Issues with three selected nodes and a selected closed way

  • fix calculation of clockwise flag
  • improve positioning of added nodes
  • ignore fixed nodes when they would produce loops
  • add more plausibility checks
  • correct handling of other ways connected to the modified part so that new nodes are also added to those ways which share segements with the selected nodes
Location:
applications/editors/josm/plugins/utilsplugin2
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • applications/editors/josm/plugins/utilsplugin2/build.xml

    r34768 r35440  
    55    <property name="commit.message" value="[josm_utilsplugin2]: select boundary by double-click; multitagger table highlights"/>
    66    <!-- enter the *lowest* JOSM version this plugin is currently compatible with -->
    7     <property name="plugin.main.version" value="14518"/>
     7    <property name="plugin.main.version" value="14654"/>
    88
    99    <property name="plugin.author" value="Kalle Lampila, Upliner, Zverik, akks, joshdoe and others"/>
  • applications/editors/josm/plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/curves/CircleArcMaker.java

    r35438 r35440  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.plugins.utilsplugin2.curves;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
    35
    46import java.util.ArrayList;
     
    1113import java.util.List;
    1214import java.util.Set;
     15import java.util.stream.Collectors;
    1316
    1417import org.openstreetmap.josm.command.AddCommand;
     
    2326import org.openstreetmap.josm.data.osm.DataSet;
    2427import org.openstreetmap.josm.data.osm.Node;
     28import org.openstreetmap.josm.data.osm.Relation;
    2529import org.openstreetmap.josm.data.osm.Way;
    2630import org.openstreetmap.josm.data.projection.ProjectionRegistry;
    2731import org.openstreetmap.josm.gui.MainApplication;
    2832import org.openstreetmap.josm.tools.Geometry;
     33import org.openstreetmap.josm.tools.JosmRuntimeException;
    2934
    3035/**
     
    107112
    108113        double radiusInMeters = ll1.greatCircleDistance(ll2);
     114        if (radiusInMeters < 0.01)
     115            throw new JosmRuntimeException(tr("Radius too small"));
    109116
    110117        int numberOfNodesInCircle = (int) Math.ceil(6.0 * Math.pow(radiusInMeters, 0.5));
     
    126133
    127134        if (!selectedWays.isEmpty()) {
     135            if (w.isClosed()) {
     136                // see #19188
     137                nodes.clear();
     138                nodes.addAll(findShortestPart(w, anchorNodes));
     139            }
    128140            // Fix #7341. sort nodes in ways nodes order
    129141            List<Node> consideredNodes = Arrays.asList(n1, n2, n3);
    130             Collections.sort(consideredNodes, (o1, o2) -> nodes.indexOf(o1) - nodes.indexOf(o2));
     142            consideredNodes.sort((o1, o2) -> nodes.indexOf(o1) - nodes.indexOf(o2));
    131143            n1 = consideredNodes.get(0);
     144            n2 = consideredNodes.get(1);
    132145            n3 = consideredNodes.get(2);
    133         }
    134 
    135         Set<Node> fixNodes = new HashSet<>(anchorNodes);
    136         if (!selectedWays.isEmpty()) {
    137             nodes.stream().filter(n -> n.getParentWays().size() > 1 || n.isTagged()).forEach(fixNodes::add);
    138         }
     146            anchorNodes = Arrays.asList(n1, n2, n3);
     147        }
     148
     149
     150        List<Node> cwTest = new ArrayList<>(Arrays.asList(n1, n2, n3));
     151        if (cwTest.get(0) != cwTest.get(cwTest.size() - 1)) {
     152            cwTest.add(cwTest.get(0));
     153        }
     154        boolean clockwise = Geometry.isClockwise(cwTest);
     155
    139156        boolean needsUndo = false;
    140157        if (!cmds.isEmpty()) {
     
    145162        int pos1 = nodes.indexOf(n1);
    146163        int pos3 = nodes.indexOf(n3);
    147         List<Node> toModify = new ArrayList<>(nodes.subList(pos1, pos3 + 1));
    148         cmds.addAll(worker(toModify, fixNodes, center, radius, maxAngle));
    149         if (toModify.size() > pos3 + 1 - pos1) {
    150             List<Node> changed = new ArrayList<>();
    151             changed.addAll(nodes.subList(0, pos1));
    152             changed.addAll(toModify);
    153             changed.addAll(nodes.subList(pos3 + 1, nodes.size()));
    154             Way wNew = new Way(w);
    155             wNew.setNodes(changed);
    156             cmds.add(new ChangeCommand(w, wNew));
    157         }
    158         if (needsUndo) {
    159             // make sure we don't add the new nodes twice
    160             UndoRedoHandler.getInstance().undo(1);
     164        Set<Node> fixNodes = new HashSet<>(anchorNodes);
     165        if (!selectedWays.isEmpty()) {
     166            for (int i = pos1 + 1; i < pos3; i++) {
     167                Node n = nodes.get(i);
     168                if (n.isTagged() || n.isReferredByWays(2) || n.referrers(Relation.class).count() > 0) {
     169                    fixNodes.add(n);
     170                }
     171            }
     172        }
     173
     174        List<Node> orig = nodes.subList(pos1, pos3 + 1);
     175        List<Node> arcNodes = new ArrayList<>(orig);
     176
     177        Set<Way> targetWays = new HashSet<>();
     178        if (!selectedWays.isEmpty()) {
     179            orig.forEach(n -> targetWays.addAll(n.getParentWays()));
     180        } else {
     181            targetWays.add(w);
     182        }
     183        try {
     184            List<Command> modCmds = worker(arcNodes, fixNodes, center, radius, maxAngle, clockwise);
     185            cmds.addAll(modCmds);
     186            if (!arcNodes.equals(orig)) {
     187                fuseArc(ds, arcNodes, targetWays, cmds);
     188            }
     189        } finally {
     190            if (needsUndo) {
     191                // make sure we don't add the new nodes twice
     192                UndoRedoHandler.getInstance().undo(1);
     193            }
     194        }
     195        if (cmds.isEmpty()) {
     196            throw new JosmRuntimeException(tr("Nothing to do"));
    161197        }
    162198        return cmds;
    163199    }
    164200
     201    /**
     202     * Try to find out which nodes should be moved when a closed way is modified. The positions of the anchor
     203     * nodes might not give the right order. Rotate the nodes so that each of the selected nodes is first
     204     * and check which variant produces the shortest part of the way.
     205     * @param w the closed way to modify
     206     * @param anchorNodes the (selected) anchor nodes
     207     * @return the way nodes, possibly rotated
     208     * @throws JosmRuntimeException if no usable rotation was found
     209     */
     210    private static List<Node> findShortestPart(Way w, List<Node> anchorNodes) {
     211        int bestRotate = 0;
     212        double shortest = Double.MAX_VALUE;
     213        final double wayLength = w.getLength();
     214        for (int i = 0; i < w.getNodesCount() - 1; i++) {
     215            List<Node> nodes = rotate(w, i);
     216            List<Integer> positions = anchorNodes.stream().map(nodes::indexOf).sorted().collect(Collectors.toList());
     217            double lenghth = getLength(nodes, positions.get(0), positions.get(2));
     218            if (lenghth < shortest) {
     219                bestRotate = i;
     220                shortest = lenghth;
     221            }
     222        }
     223        if (shortest >= wayLength / 2)
     224            throw new JosmRuntimeException(tr("Could not detect which part of the closed way should be changed"));
     225        return rotate(w, bestRotate);
     226    }
     227
     228    private static List<Node> rotate(Way w, int distance) {
     229        List<Node> nodes = new ArrayList<>(w.getNodes());
     230        nodes.remove(nodes.size() - 1); // remove closing node
     231        Collections.rotate(nodes, distance);
     232        nodes.add(nodes.get(0));
     233        return nodes;
     234    }
     235
     236    private static double getLength(List<Node> nodes, int pos1, int pos3) {
     237        Way tmp = new Way();
     238        tmp.setNodes(nodes.subList(pos1, pos3+1));
     239        return tmp.getLength();
     240    }
     241
    165242    // code partly taken from AlignInCircleAction
    166     private static List<Command> worker(List<Node> nodes, Set<Node> fixNodes, EastNorth center, double radius, double maxAngle) {
     243    private static List<Command> worker(List<Node> origNodes, Set<Node> origFixNodes, EastNorth center, double radius,
     244            double maxAngle, boolean clockwise) {
    167245        List<Command> cmds = new LinkedList<>();
    168 
    169246        // Move each node to that distance from the center.
    170         // Nodes that are not "fix" will be adjust making regular arcs.
    171         int nodeCount = nodes.size();
    172 
    173         List<Node> cwTest = new ArrayList<>(nodes);
    174         if (cwTest.get(0) != cwTest.get(cwTest.size() - 1)) {
    175             cwTest.add(cwTest.get(0));
    176         }
    177         boolean clockWise = Geometry.isClockwise(cwTest);
     247        // Nodes that are not "fix" will be adjusted making regular arcs.
     248        List<Node> nodes = new ArrayList<>(origNodes);
     249        Set<Node> fixNodes = new HashSet<>(origFixNodes);
    178250        double maxStep = Math.PI * 2 / (360.0 / maxAngle);
    179 
    180         // Search first fixed node
    181         int startPosition;
    182         for (startPosition = 0; startPosition < nodeCount; startPosition++) {
    183             if (fixNodes.contains(nodes.get(startPosition)))
    184                 break;
    185         }
    186         int i = startPosition; // Start position for current arc
     251        double sumAbsDelta = 0;
     252        int i = 0; // Start position for current arc
    187253        int j; // End position for current arc
    188         while (i < nodeCount) {
    189             for (j = i + 1; j < nodeCount; j++) {
    190                 if (fixNodes.contains(nodes.get(j)))
     254        while (i < nodes.size()) {
     255            Node first = nodes.get(i);
     256            PolarCoor pcFirst = new PolarCoor(radius, PolarCoor.computeAngle(first.getEastNorth(), center), center);
     257            for (j = i + 1; j < nodes.size(); j++) {
     258                Node test = nodes.get(j);
     259                if (fixNodes.contains(test)) {
    191260                    break;
    192             }
    193             Node first = nodes.get(i);
    194 
    195             PolarCoor pcFirst = new PolarCoor(radius, PolarCoor.computeAngle(first.getEastNorth(), center), center);
    196             addMoveCommandIfNeeded(first, pcFirst, cmds);
    197             if (j < nodeCount) {
    198                 double delta;
    199                 PolarCoor pcLast = new PolarCoor(nodes.get(j).getEastNorth(), center);
    200                 delta = pcLast.angle - pcFirst.angle;
    201                 if (!clockWise && delta < 0) {
     261                }
     262            }
     263
     264            if (j < nodes.size()) {
     265                Node last = nodes.get(j);
     266                PolarCoor pcLast = new PolarCoor(last.getEastNorth(), center);
     267                double delta = pcLast.angle - pcFirst.angle;
     268                if ((!clockwise && delta < 0 || clockwise && delta > 0)
     269                        && Math.signum(pcFirst.angle) == Math.signum(pcLast.angle)) {
     270                    // cannot project node onto circle arc, ignore that it is fixed
     271                    if (!last.isSelected() && fixNodes.remove(last)) {
     272                        continue;
     273                    }
     274                    if (!first.isSelected() && fixNodes.remove(first)) {
     275                        // try again with fewer fixed nodes
     276                        return worker(origNodes, fixNodes, center, radius, maxAngle, clockwise);
     277                    }
     278                }
     279                if (!clockwise && delta < 0) {
    202280                    delta += 2 * Math.PI;
    203                 } else if (clockWise && delta > 0) {
     281                } else if (clockwise && delta > 0) {
    204282                    delta -= 2 * Math.PI;
    205283                }
     284                sumAbsDelta += Math.abs(delta);
     285                if (sumAbsDelta > 2 * Math.PI) {
     286                    // something went really wrong, we would add more than a full circle
     287                    throw new JosmRuntimeException("Would create a loop");
     288                }
     289
    206290                // do we have enough nodes to produce a nice circle?
    207                 int numToAdd = Math.max((int) Math.ceil(Math.abs(delta / maxStep)), j - i) - (j-i);
     291                int numToAdd = Math.max((int) Math.ceil(Math.abs(delta / maxStep)), j - i) - (j - i);
     292                int added = 0;
    208293                double step = delta / (numToAdd + j - i);
    209                 for (int k = i + 1; k < j; k++) {
    210                     PolarCoor p = new PolarCoor(radius, pcFirst.angle + (k - i) * step, center);
    211                     addMoveCommandIfNeeded(nodes.get(k), p, cmds);
    212                 }
    213                 // add needed nodes
    214                 for (int k = j; k < j + numToAdd; k++) {
    215                     PolarCoor p = new PolarCoor(radius, pcFirst.angle + (k - i) * step, center);
    216                     Node nNew = new Node(p.toEastNorth());
    217                     nodes.add(k, nNew);
    218                     cmds.add(new AddCommand(nodes.get(0).getDataSet(), nNew));
    219                 }
    220                 j += numToAdd;
    221                 nodeCount += numToAdd;
     294
     295                // move existing nodes or add new nodes
     296                List<Node> oldNodes = new ArrayList<>(nodes.subList(i, j));
     297                PolarCoor ref = pcFirst;
     298                for (int k = i; k < j + numToAdd; k++) {
     299                    PolarCoor pc1 = new PolarCoor(radius, pcFirst.angle + (k - i) * step, center);
     300                    if (!oldNodes.isEmpty()) {
     301                        PolarCoor pc2 = new PolarCoor(oldNodes.get(0).getEastNorth(), center);
     302                        if (pc2.angle < ref.angle && ref.angle < 0 && step > 0
     303                                || pc2.angle > ref.angle && ref.angle > 0 && step < 0) {
     304                            // projected node would produce a loop
     305                            pc2 = ref; //
     306                        }
     307
     308                        double delta2 = ref.angle - pc2.angle;
     309                        if (ref.angle < 0 && pc2.angle > 0) {
     310                            delta2 += 2 * Math.PI;
     311                        } else if (ref.angle > 0 && pc2.angle < 0) {
     312                            delta2 -= 2 * Math.PI;
     313                        }
     314                        if (added >= numToAdd || Math.abs(delta2) < Math.abs(step * 1.5)) {
     315                            // existing node is close enough
     316                            addMoveCommandIfNeeded(oldNodes.remove(0), pc1, cmds);
     317                            ref = pc1;
     318                        }
     319                    }
     320                    if (ref != pc1) {
     321                        ref = pc1;
     322                        Node nNew = new Node(pc1.toEastNorth());
     323                        nodes.add(k, nNew);
     324                        cmds.add(new AddCommand(nodes.get(0).getDataSet(), nNew));
     325                        ++added;
     326                    }
     327                }
     328
     329                j += added;
    222330            }
    223331            i = j; // Update start point for next iteration
    224332        }
     333        origNodes.clear();
     334        origNodes.addAll(nodes);
    225335        return cmds;
    226336    }
     
    235345    }
    236346
     347    private static void fuseArc(DataSet ds, List<Node> arcNodes, Set<Way> targetWays, Collection<Command> cmds) {
     348        // replace each segment of the target ways with the corresponding nodes of the new arc
     349        for (Way originalTw : targetWays) {
     350            Way tw = new Way(originalTw);
     351            boolean twWasChanged = false;
     352            for (int i = 0; i < arcNodes.size(); i++) {
     353                Node arcNode1 = arcNodes.get(i);
     354                // we don't want to match nodes which where added by worker
     355                if (arcNode1.getDataSet() != ds || !arcNode1.getParentWays().contains(originalTw))
     356                    continue;
     357
     358                boolean changed = false;
     359                for (int j = i + 1; j < arcNodes.size() && !changed; j++) {
     360                    Node arcNode2 = arcNodes.get(j);
     361                    if (arcNode2.getDataSet() != ds || !arcNode2.getParentWays().contains(originalTw))
     362                        continue;
     363                    changed = tryAddArc(tw, i, j, arcNodes);
     364                    twWasChanged |= changed;
     365                }
     366            }
     367            if (twWasChanged) {
     368                cmds.add(new ChangeCommand(ds, originalTw, tw));
     369            }
     370        }
     371    }
     372
     373    private static boolean tryAddArc(Way tw, int i, int j, List<Node> arcNodes) {
     374        int pos1 = tw.getNodes().indexOf(arcNodes.get(i));
     375        int pos2 = tw.getNodes().indexOf(arcNodes.get(j));
     376        if (tw.isClosed()) {
     377            if (pos1 - pos2 > 1 && pos2 == 0) {
     378                pos2 = tw.getNodesCount() - 1;
     379            } else if (pos2 - pos1 > 1 && pos1 == 0) {
     380                pos1 = tw.getNodesCount() - 1;
     381            }
     382        }
     383        if (pos2 + 1 == pos1) {
     384            for (int k = i + 1; k < j; k++) {
     385                tw.addNode(pos1, arcNodes.get(k));
     386            }
     387            return true;
     388        } else if (pos2 - 1 == pos1) {
     389            for (int k = j - 1; k > i; k--) {
     390                tw.addNode(pos2, arcNodes.get(k));
     391            }
     392            return true;
     393        }
     394        return false;
     395    }
    237396}
  • applications/editors/josm/plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/curves/CurveAction.java

    r35384 r35440  
    2121import org.openstreetmap.josm.data.osm.Way;
    2222import org.openstreetmap.josm.gui.Notification;
     23import org.openstreetmap.josm.tools.JosmRuntimeException;
    2324import org.openstreetmap.josm.tools.Shortcut;
    2425
     
    4748        List<Way> selectedWays = new ArrayList<>(getLayerManager().getEditDataSet().getSelectedWays());
    4849
    49         Collection<Command> cmds = CircleArcMaker.doCircleArc(selectedNodes, selectedWays);
    50         if (cmds == null || cmds.isEmpty()) {
    51             new Notification(tr("Could not use selection to create a curve")).setIcon(JOptionPane.WARNING_MESSAGE).show();
    52         } else {
    53             UndoRedoHandler.getInstance().add(new SequenceCommand("Create a curve", cmds));
     50        String msg = null;
     51        try {
     52            Collection<Command> cmds = CircleArcMaker.doCircleArc(selectedNodes, selectedWays);
     53            if (cmds == null || cmds.isEmpty()) {
     54                msg = tr("Could not use selection to create a curve");
     55            } else {
     56                UndoRedoHandler.getInstance().add(new SequenceCommand("Create a curve", cmds));
     57            }
     58        } catch (JosmRuntimeException ex) {
     59            msg = tr("Could not use selection to create a curve: {0}", ex.getMessage());
     60        }
     61        if (msg != null) {
     62            new Notification(msg).setIcon(JOptionPane.WARNING_MESSAGE).show();
    5463        }
    5564    }
Note: See TracChangeset for help on using the changeset viewer.