Ticket #19188: 19188.patch

File 19188.patch, 15.0 KB (added by GerdP, 4 years ago)
  • src/org/openstreetmap/josm/plugins/utilsplugin2/curves/CircleArcMaker.java

     
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.plugins.utilsplugin2.curves;
    33
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
    46import java.util.ArrayList;
    57import java.util.Arrays;
    68import java.util.Collection;
     
    1012import java.util.LinkedList;
    1113import java.util.List;
    1214import java.util.Set;
     15import java.util.stream.Collectors;
    1316
    1417import org.openstreetmap.josm.command.AddCommand;
    1518import org.openstreetmap.josm.command.ChangeCommand;
     
    2730import org.openstreetmap.josm.data.projection.ProjectionRegistry;
    2831import org.openstreetmap.josm.gui.MainApplication;
    2932import org.openstreetmap.josm.tools.Geometry;
     33import org.openstreetmap.josm.tools.JosmRuntimeException;
    3034
    3135/**
    3236 * Create a circle arc
     
    107111        LatLon ll2 = ProjectionRegistry.getProjection().eastNorth2latlon(center);
    108112
    109113        double radiusInMeters = ll1.greatCircleDistance(ll2);
     114        if (radiusInMeters < 0.01)
     115            throw new JosmRuntimeException(tr("Radius too small"));
    110116
    111117        int numberOfNodesInCircle = (int) Math.ceil(6.0 * Math.pow(radiusInMeters, 0.5));
    112118        // an odd number of nodes makes the distribution uneven
     
    126132        final List<Node> nodes = new ArrayList<>(w.getNodes());
    127133
    128134        if (!selectedWays.isEmpty()) {
     135            if (w.isClosed()) {
     136                // see #19188
     137                nodes.clear();
     138                nodes.addAll(findShortestPart(w, anchorNodes));
     139            }
    129140            // Fix #7341. sort nodes in ways nodes order
    130141            List<Node> consideredNodes = Arrays.asList(n1, n2, n3);
    131             Collections.sort(consideredNodes, (o1, o2) -> nodes.indexOf(o1) - nodes.indexOf(o2));
     142            consideredNodes.sort((o1, o2) -> nodes.indexOf(o1) - nodes.indexOf(o2));
    132143            n1 = consideredNodes.get(0);
     144            n2 = consideredNodes.get(1);
    133145            n3 = consideredNodes.get(2);
     146            anchorNodes = Arrays.asList(n1, n2, n3);
    134147        }
    135148
    136         Set<Node> fixNodes = new HashSet<>(anchorNodes);
    137         if (!selectedWays.isEmpty()) {
    138             nodes.stream().filter(
    139                     n -> n.isTagged() || n.getParentWays().size() > 1 || n.referrers(Relation.class).count() > 0)
    140                     .forEach(fixNodes::add);
     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));
    141153        }
     154        boolean clockWise = Geometry.isClockwise(cwTest);
     155
    142156        boolean needsUndo = false;
    143157        if (!cmds.isEmpty()) {
    144158            UndoRedoHandler.getInstance().add(new SequenceCommand("add nodes", cmds));
    145159            needsUndo = true;
    146160        }
     161        Set<Way> targetWays = new HashSet<>();
     162        anchorNodes.forEach(n -> targetWays.addAll(n.getParentWays()));
    147163
    148164        int pos1 = nodes.indexOf(n1);
    149165        int pos3 = nodes.indexOf(n3);
    150         List<Node> toModify = new ArrayList<>(nodes.subList(pos1, pos3 + 1));
    151         cmds.addAll(worker(toModify, fixNodes, center, radius, maxAngle));
    152         if (toModify.size() > pos3 + 1 - pos1) {
    153             List<Node> changed = new ArrayList<>();
    154             changed.addAll(nodes.subList(0, pos1));
    155             changed.addAll(toModify);
    156             changed.addAll(nodes.subList(pos3 + 1, nodes.size()));
    157             Way wNew = new Way(w);
    158             wNew.setNodes(changed);
    159             cmds.add(new ChangeCommand(w, wNew));
     166        Set<Node> fixNodes = new HashSet<>(anchorNodes);
     167        if (!selectedWays.isEmpty()) {
     168            for (int i = pos1 + 1; i < pos3; i++) {
     169                Node n = nodes.get(i);
     170                if (n.isTagged() || n.getParentWays().size() > 1 || n.referrers(Relation.class).count() > 0)
     171                    fixNodes.add(n);
     172            }
    160173        }
    161         if (needsUndo) {
    162             // make sure we don't add the new nodes twice
    163             UndoRedoHandler.getInstance().undo(1);
     174
     175        List<Node> orig = nodes.subList(pos1, pos3 + 1);
     176        List<Node> arcNodes = new ArrayList<>(orig);
     177        try {
     178            cmds.addAll(worker(arcNodes, fixNodes, center, radius, maxAngle, clockWise));
     179            if (!arcNodes.equals(orig)) {
     180                fuseArc(ds, arcNodes, targetWays, cmds);
     181            }
     182        } finally {
     183            if (needsUndo) {
     184                // make sure we don't add the new nodes twice
     185                UndoRedoHandler.getInstance().undo(1);
     186            }
    164187        }
     188        if (cmds.isEmpty()) {
     189            throw new JosmRuntimeException(tr("Nothing to do"));
     190        }
    165191        return cmds;
    166192    }
    167193
     194    /**
     195     * Try to find out which nodes should be moved when a closed way is modified. The positions of the anchor
     196     * nodes might not give the right order. Rotate the nodes so that each of the selected nodes is first
     197     * and check which variant produces the shortest part of the way.
     198     * @param w the closed way to modify
     199     * @param anchorNodes the (selected) anchor nodes
     200     * @return the way nodes, possibly rotated
     201     * @throws JosmRuntimeException if no usable rotation was found
     202     */
     203    private static List<Node> findShortestPart(Way w, List<Node> anchorNodes) {
     204        int bestRotate = 0;
     205        double shortest = Double.MAX_VALUE;
     206        final double wayLength = w.getLength();
     207        for (int i = 0; i < w.getNodesCount() - 1; i++) {
     208            List<Node> nodes = rotate(w, i);
     209            List<Integer> positions = anchorNodes.stream().map(nodes::indexOf).sorted().collect(Collectors.toList());
     210            double lenghth = getLength(nodes, positions.get(0), positions.get(2));
     211            if (lenghth < shortest) {
     212                bestRotate = i;
     213                shortest = lenghth;
     214            }
     215        }
     216        if (shortest >= wayLength / 2)
     217            throw new JosmRuntimeException(tr("Don't know which part of closed way should be changed"));
     218        return rotate(w, bestRotate);
     219    }
     220
     221    private static List<Node> rotate(Way w, int distance) {
     222        List<Node> nodes = new ArrayList<>(w.getNodes());
     223        nodes.remove(nodes.size() - 1); // remove closing node
     224        Collections.rotate(nodes, distance);
     225        nodes.add(nodes.get(0));
     226        return nodes;
     227    }
     228
     229    private static double getLength(List<Node> nodes, int pos1, int pos3) {
     230        Way tmp = new Way();
     231        tmp.setNodes(nodes.subList(pos1, pos3+1));
     232        return tmp.getLength();
     233    }
     234
    168235    // code partly taken from AlignInCircleAction
    169     private static List<Command> worker(List<Node> nodes, Set<Node> fixNodes, EastNorth center, double radius, double maxAngle) {
     236    private static List<Command> worker(List<Node> nodes, Set<Node> origFixNodes, EastNorth center, double radius,
     237            double maxAngle, boolean clockWise) {
    170238        List<Command> cmds = new LinkedList<>();
    171239
    172240        // Move each node to that distance from the center.
    173         // Nodes that are not "fix" will be adjust making regular arcs.
    174         int nodeCount = nodes.size();
     241        // Nodes that are not "fix" will be adjusted making regular arcs.
    175242
    176         List<Node> cwTest = new ArrayList<>(nodes);
    177         if (cwTest.get(0) != cwTest.get(cwTest.size() - 1)) {
    178             cwTest.add(cwTest.get(0));
    179         }
    180         boolean clockWise = Geometry.isClockwise(cwTest);
     243        HashSet<Node> fixNodes = new HashSet<>(origFixNodes);
    181244        double maxStep = Math.PI * 2 / (360.0 / maxAngle);
    182245
    183         // Search first fixed node
    184         int startPosition;
    185         for (startPosition = 0; startPosition < nodeCount; startPosition++) {
    186             if (fixNodes.contains(nodes.get(startPosition)))
    187                 break;
    188         }
    189         int i = startPosition; // Start position for current arc
     246        double sumAbsDelta = 0;
     247        int i = 0; // Start position for current arc
    190248        int j; // End position for current arc
    191         while (i < nodeCount) {
    192             for (j = i + 1; j < nodeCount; j++) {
    193                 if (fixNodes.contains(nodes.get(j)))
     249        while (i < nodes.size()) {
     250            for (j = i + 1; j < nodes.size(); j++) {
     251                if (fixNodes.contains(nodes.get(j))) {
    194252                    break;
     253                }
    195254            }
    196255            Node first = nodes.get(i);
     256            PolarCoor pcFirst = new PolarCoor(radius, PolarCoor.computeAngle(first.getEastNorth(), center), center);
    197257
    198             PolarCoor pcFirst = new PolarCoor(radius, PolarCoor.computeAngle(first.getEastNorth(), center), center);
    199             addMoveCommandIfNeeded(first, pcFirst, cmds);
    200             if (j < nodeCount) {
    201                 double delta;
    202                 PolarCoor pcLast = new PolarCoor(nodes.get(j).getEastNorth(), center);
    203                 delta = pcLast.angle - pcFirst.angle;
     258            if (j < nodes.size()) {
     259                Node last = nodes.get(j);
     260                PolarCoor pcLast = new PolarCoor(last.getEastNorth(), center);
     261                double delta = pcLast.angle - pcFirst.angle;
     262                if ((!clockWise && delta < 0 || clockWise && delta > 0)
     263                        && Math.signum(pcFirst.angle) == Math.signum(pcLast.angle)) {
     264                    // cannot project node onto circle arc, ignore that it is fixed
     265                    if (!last.isSelected() && fixNodes.remove(last)) {
     266                        continue;
     267                    } else {
     268                        throw new JosmRuntimeException(tr("Too many fixed nodes"));
     269                    }
     270                }
    204271                if (!clockWise && delta < 0) {
    205272                    delta += 2 * Math.PI;
    206273                } else if (clockWise && delta > 0) {
    207274                    delta -= 2 * Math.PI;
    208275                }
     276                sumAbsDelta += Math.abs(delta);
     277                if (sumAbsDelta > 2 * Math.PI) {
     278                    // something went wrong, we would add more than a full circle
     279                    throw new JosmRuntimeException(tr("Would produce a loop"));
     280                }
     281
    209282                // do we have enough nodes to produce a nice circle?
    210283                int numToAdd = Math.max((int) Math.ceil(Math.abs(delta / maxStep)), j - i) - (j-i);
    211284                double step = delta / (numToAdd + j - i);
    212                 for (int k = i + 1; k < j; k++) {
     285
     286                // TODO: better mix new nodes and old nodes to reduce move distance
     287                for (int k = i; k < j; k++) {
    213288                    PolarCoor p = new PolarCoor(radius, pcFirst.angle + (k - i) * step, center);
    214289                    addMoveCommandIfNeeded(nodes.get(k), p, cmds);
    215290                }
     
    221296                    cmds.add(new AddCommand(nodes.get(0).getDataSet(), nNew));
    222297                }
    223298                j += numToAdd;
    224                 nodeCount += numToAdd;
    225299            }
    226300            i = j; // Update start point for next iteration
    227301        }
     
    237311        }
    238312    }
    239313
     314    private static void fuseArc(DataSet ds, List<Node> arcNodes, Set<Way> targetWays, Collection<Command> cmds) {
     315        // replace each segment of the target ways with the corresponding nodes of the new arc
     316        for (Way originalTw : targetWays) {
     317            Way tw = new Way(originalTw);
     318            boolean twWasChanged = false;
     319            for (int i = 0; i < arcNodes.size(); i++) {
     320                Node arcNode1 = arcNodes.get(i);
     321                // we don't want to match nodes which where added by worker
     322                if (arcNode1.getDataSet() != ds || !arcNode1.getParentWays().contains(originalTw))
     323                    continue;
     324
     325                boolean changed = false;
     326                for (int j = i + 1; j < arcNodes.size() && !changed; j++) {
     327                    Node arcNode2 = arcNodes.get(j);
     328                    if (arcNode2.getDataSet() != ds || !arcNode2.getParentWays().contains(originalTw))
     329                        continue;
     330                    changed = tryAddArc(tw, i, j, arcNodes);
     331                    twWasChanged |= changed;
     332                }
     333            }
     334            if (twWasChanged) {
     335                cmds.add(new ChangeCommand(ds, originalTw, tw));
     336            }
     337        }
     338    }
     339
     340    private static boolean tryAddArc(Way tw, int i, int j, List<Node> arcNodes) {
     341        int pos1 = tw.getNodes().indexOf(arcNodes.get(i));
     342        int pos2 = tw.getNodes().indexOf(arcNodes.get(j));
     343        if (tw.isClosed()) {
     344            if (pos1 - pos2 > 1 && pos2 == 0) {
     345                pos2 = tw.getNodesCount() - 1;
     346            } else if (pos2 - pos1 > 1 && pos1 == 0) {
     347                pos1 = tw.getNodesCount() - 1;
     348            }
     349        }
     350        if (pos2 + 1 == pos1) {
     351            for (int k = i + 1; k < j; k++) {
     352                tw.addNode(pos1, arcNodes.get(k));
     353            }
     354            return true;
     355        } else if (pos2 - 1 == pos1) {
     356            for (int k = j - 1; k > i; k--) {
     357                tw.addNode(pos2, arcNodes.get(k));
     358            }
     359            return true;
     360        }
     361        return false;
     362    }
     363
    240364}
  • src/org/openstreetmap/josm/plugins/utilsplugin2/curves/CurveAction.java

     
    2020import org.openstreetmap.josm.data.osm.OsmPrimitive;
    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
    2526// TODO: investigate splines
     
    4647        List<Node> selectedNodes = new ArrayList<>(getLayerManager().getEditDataSet().getSelectedNodes());
    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());
    5460        }
     61        if (msg != null) {
     62            new Notification(msg).setIcon(JOptionPane.WARNING_MESSAGE).show();
     63        }
    5564    }
    5665
    5766    @Override