Ignore:
Timestamp:
2020-03-21T07:42:00+01:00 (5 years ago)
Author:
gerdp
Message:

fix #josm18847: Circle Arc tool creates unconnected nodes

  • simplified CircleArcMaker to use similar logic as AlignInCirclection
  • ignore preference key circlearc.angle-separation and calculate proper value with the same logic as in CreateCircleAction
Location:
applications/editors/josm/plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/curves
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • applications/editors/josm/plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/curves/CircleArcMaker.java

    r34840 r35384  
    66import java.util.Collection;
    77import java.util.Collections;
    8 import java.util.Comparator;
    98import java.util.HashSet;
    109import java.util.Iterator;
    1110import java.util.LinkedList;
    1211import java.util.List;
    13 import java.util.Objects;
    1412import java.util.Set;
    1513
     
    1715import org.openstreetmap.josm.command.ChangeCommand;
    1816import org.openstreetmap.josm.command.Command;
     17import org.openstreetmap.josm.command.MoveCommand;
     18import org.openstreetmap.josm.command.SequenceCommand;
     19import org.openstreetmap.josm.data.UndoRedoHandler;
    1920import org.openstreetmap.josm.data.coor.EastNorth;
     21import org.openstreetmap.josm.data.coor.LatLon;
     22import org.openstreetmap.josm.data.coor.PolarCoor;
    2023import org.openstreetmap.josm.data.osm.DataSet;
    2124import org.openstreetmap.josm.data.osm.Node;
    2225import org.openstreetmap.josm.data.osm.Way;
     26import org.openstreetmap.josm.data.projection.ProjectionRegistry;
    2327import org.openstreetmap.josm.gui.MainApplication;
     28import org.openstreetmap.josm.tools.Geometry;
    2429
    2530/**
     
    3237    }
    3338
    34     public static Collection<Command> doCircleArc(List<Node> selectedNodes, List<Way> selectedWays, int angleSeparation) {
    35         Collection<Command> cmds = new LinkedList<>();
     39    public static Collection<Command> doCircleArc(List<Node> selectedNodes, List<Way> selectedWays) {
     40        List<Command> cmds = new LinkedList<>();
    3641
    3742        //// Decides which nodes to use as anchors based on selection
     
    4348         * When existing ways are reused for the arc, all ways overlapping these are transformed too.
    4449         *
    45          * 1. Exactly 3 nodes selected:
    46          *      Use these nodes.
     50         * Exactly 3 nodes have to be selected.
    4751         *      - No way selected: create a new way.
    48          * 2. Exactly 1 node selected, node part of exactly 1 way with 3 or more nodes:
    49          *      Node selected used as first node, consequent nodes in the way's direction used as the rest.
    50          *      - Reversed if not enough nodes in forward direction
    51          *      - Selected node used as middle node its the middle node in a 3 node way
    52          *      - Parent way used
    5352         */
    5453
     
    5655        Node n1 = null, n2 = null, n3 = null;
    5756
    58         Set<Way> targetWays = new HashSet<>();
    5957        DataSet ds = MainApplication.getLayerManager().getEditDataSet();
    6058
     59        if (selectedWays.size() > 1)
     60             return cmds;
     61
     62        Way w = null;
    6163        boolean nodesHaveBeenChoosen = false;
    6264        if (selectedNodes.size() == 3) {
     
    6668            n3 = nodeIter.next();
    6769            nodesHaveBeenChoosen = true;
    68             if (selectedWays.isEmpty()) { // Create a brand new way
    69                 Way newWay = new Way();
    70                 targetWays.add(newWay);
    71                 cmds.add(new AddCommand(ds, newWay));
    72                 newWay.addNode(n1);
    73                 newWay.addNode(n2);
    74                 newWay.addNode(n3);
    75             }
    7670        }
    7771        if (!selectedWays.isEmpty()) {
    78             // TODO: use only two nodes inferring the orientation from the parent way.
    79 
     72            w = selectedWays.iterator().next();
    8073            if (!nodesHaveBeenChoosen) {
    8174                // Use the three last nodes in the way as anchors. This is intended to be used with the
    8275                // built in draw mode
    83                 Way w = selectedWays.iterator().next(); //TODO: select last selected way instead
    8476                int nodeCount = w.getNodesCount();
    8577                if (nodeCount < 3)
    86                     return null;
     78                    return cmds;
    8779                n3 = w.getNode(nodeCount - 1);
    8880                n2 = w.getNode(nodeCount - 2);
     
    9082                nodesHaveBeenChoosen = true;
    9183            }
    92             // Fix #7341. Find the first way having all nodes in common to sort them in its nodes order
    93             List<Node> consideredNodes = Arrays.asList(n1, n2, n3);
    94             for (Way w : selectedWays) {
    95                 final List<Node> nodes = w.getNodes();
    96                 if (nodes.containsAll(consideredNodes)) {
    97                     Collections.sort(consideredNodes, new Comparator<Node>() {
    98                         @Override
    99                         public int compare(Node a, Node b) {
    100                             return nodes.indexOf(a) - nodes.indexOf(b);
    101                         }
    102                     });
    103                     n1 = consideredNodes.get(0);
    104                     n2 = consideredNodes.get(1);
    105                     n3 = consideredNodes.get(2);
    106                     break;
    107                 }
    108             }
    109 
    110             for (Node n : consideredNodes) {
    111                 targetWays.addAll(n.getParentWays());
    112             }
    113         }
    114         if (!nodesHaveBeenChoosen) {
    115             return null;
     84        }
     85        List<Node> anchorNodes = Arrays.asList(n1, n2, n3);
     86        if (!nodesHaveBeenChoosen || (w != null && !w.getNodes().containsAll(anchorNodes))) {
     87            return cmds;
    11688        }
    11789
     
    12294        // make sure that points are different
    12395        if (p1.equals(p2) || p1.equals(p3) || p2.equals(p3)) {
    124             return null;
    125         }
    126 
    127         // // Calculate the new points in the arc
    128         ReturnValue<Integer> p2Index = new ReturnValue<>();
    129         List<EastNorth> points = circleArcPoints(p1, p2, p3, angleSeparation, false, p2Index);
    130 
    131         //// Create the new arc nodes. Insert anchor nodes at correct positions.
    132         List<Node> arcNodes = new ArrayList<>(points.size());
    133         arcNodes.add(n1);
    134         int i = 1;
    135         for (EastNorth p : slice(points, 1, -2)) {
    136             if (p2Index.value != null && i == p2Index.value) {
    137                 Node n2new = new Node(n2);
    138                 n2new.setEastNorth(p);
    139                 arcNodes.add(n2); // add the original n2, or else we can't find it in the target ways
    140                 cmds.add(new ChangeCommand(n2, n2new));
    141             } else {
    142                 Node n = new Node(p);
    143                 arcNodes.add(n);
    144                 cmds.add(new AddCommand(ds, n));
     96            return cmds;
     97        }
     98
     99        EastNorth center = Geometry.getCenter(anchorNodes);
     100        if (center == null)
     101            return cmds;
     102        double radius = center.distance(p1);
     103
     104        // see #10777: calculate reasonable number of nodes for full circle (copy from CreateCircleAction)
     105        LatLon ll1 = ProjectionRegistry.getProjection().eastNorth2latlon(p1);
     106        LatLon ll2 = ProjectionRegistry.getProjection().eastNorth2latlon(center);
     107
     108        double radiusInMeters = ll1.greatCircleDistance(ll2);
     109
     110        int numberOfNodesInCircle = (int) Math.ceil(6.0 * Math.pow(radiusInMeters, 0.5));
     111        // an odd number of nodes makes the distribution uneven
     112        if (numberOfNodesInCircle < 6) {
     113            numberOfNodesInCircle = 6;
     114        } else if ((numberOfNodesInCircle % 2) != 0) {
     115            // add 1 to make it even
     116            numberOfNodesInCircle++;
     117        }
     118        double maxAngle = 360.0 / numberOfNodesInCircle;
     119
     120        if (w == null) {
     121            w = new Way();
     122            w.setNodes(anchorNodes);
     123            cmds.add(new AddCommand(ds, w));
     124        }
     125        final List<Node> nodes = new ArrayList<>(w.getNodes());
     126
     127        if (!selectedWays.isEmpty()) {
     128            // Fix #7341. sort nodes in ways nodes order
     129            List<Node> consideredNodes = Arrays.asList(n1, n2, n3);
     130            Collections.sort(consideredNodes, (o1, o2) -> nodes.indexOf(o1) - nodes.indexOf(o2));
     131            n1 = consideredNodes.get(0);
     132            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).forEach(fixNodes::add);
     138        }
     139        boolean needsUndo = false;
     140        if (!cmds.isEmpty()) {
     141            UndoRedoHandler.getInstance().add(new SequenceCommand("add nodes", cmds));
     142            needsUndo = true;
     143        }
     144
     145        int pos1 = nodes.indexOf(n1);
     146        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);
     161        }
     162        return cmds;
     163    }
     164
     165    // code partly taken from AlignInCircleAction
     166    private static List<Command> worker(List<Node> nodes, Set<Node> fixNodes, EastNorth center, double radius, double maxAngle) {
     167        List<Command> cmds = new LinkedList<>();
     168
     169        // 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);
     178        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
     187        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)))
     191                    break;
    145192            }
    146             i++;
    147         }
    148         arcNodes.add(n3);
    149 
    150         Node[] anchorNodes = {n1, n2, n3};
    151         //// "Fuse" the arc with all target ways
    152         fuseArc(ds, anchorNodes, arcNodes, targetWays, cmds);
    153 
     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) {
     202                    delta += 2 * Math.PI;
     203                } else if (clockWise && delta > 0) {
     204                    delta -= 2 * Math.PI;
     205                }
     206                // 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);
     208                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;
     222            }
     223            i = j; // Update start point for next iteration
     224        }
    154225        return cmds;
    155226    }
    156227
    157     private static void fuseArc(DataSet ds, Node[] anchorNodes, List<Node> arcNodes, Set<Way> targetWays, Collection<Command> cmds) {
    158 
    159         for (Way originalTw : targetWays) {
    160             Way tw = new Way(originalTw);
    161             boolean didChangeTw = false;
    162             /// Do one segment at the time (so ways only sharing one segment is fused too)
    163             for (int a = 0; a < 2; a++) {
    164                 int anchorBi = arcNodes.indexOf(anchorNodes[a]); // TODO: optimize away
    165                 int anchorEi = arcNodes.indexOf(anchorNodes[a + 1]);
    166                 /// Find the anchor node indices in current target way
    167                 int bi = -1, ei = -1;
    168                 int i = -1;
    169                 // Caution: nodes might appear multiple times. For now only handle simple closed ways
    170                 for (Node n : tw.getNodes()) {
    171                     i++;
    172                     // We look for the first anchor node. The next should be directly to the left or right.
    173                     // Exception when the way is closed
    174                     if (Objects.equals(n, anchorNodes[a])) {
    175                         bi = i;
    176                         Node otherAnchor = anchorNodes[a + 1];
    177                         if (i > 0 && Objects.equals(tw.getNode(i - 1), otherAnchor)) {
    178                             ei = i - 1;
    179                         } else if (i < (tw.getNodesCount() - 1) && Objects.equals(tw.getNode(i + 1), otherAnchor)) {
    180                             ei = i + 1;
    181                         } else {
    182                             continue; // this can happen with closed ways. Continue searching for the correct index
    183                         }
    184                         break;
    185                     }
    186                 }
    187                 if (bi == -1 || ei == -1) {
    188                     continue; // this segment is not part of the target way
    189                 }
    190                 didChangeTw = true;
    191 
    192                 /// Insert the nodes of this segment
    193                 // Direction of target way relative to the arc node order
    194                 int twDirection = ei > bi ? 1 : 0;
    195                 int anchorI = anchorBi + 1; // don't insert the anchor nodes again
    196                 int twI = bi + (twDirection == 1 ? 1 : 0); // TODO: explain
    197                 while (anchorI < anchorEi) {
    198                     tw.addNode(twI, arcNodes.get(anchorI));
    199                     anchorI++;
    200                     twI += twDirection;
    201                 }
    202             }
    203             if (didChangeTw)
    204                 cmds.add(new ChangeCommand(ds, originalTw, tw));
    205         }
    206     }
    207 
    208     /**
    209      * Return a list of coordinates lying an the circle arc determined by n1, n2 and n3.
    210      * The order of the list and which of the 3 possible arcs to construct are given by the order of n1, n2, n3
    211      * @param p1 n1
    212      * @param p2 n2
    213      * @param p3 n3
    214      * @param angleSeparation maximum angle separation between the arc points
    215      * @param includeAnchors include the anchor points in the list. The original objects will be used, not copies.
    216      *                       If {@code false}, p2 will be replaced by the closest arcpoint.
    217      * @param anchor2Index if non-null, it's value will be set to p2's index in the returned list.
    218      * @return list of coordinates lying an the circle arc determined by n1, n2 and n3
    219      */
    220     private static List<EastNorth> circleArcPoints(EastNorth p1, EastNorth p2, EastNorth p3,
    221             int angleSeparation, boolean includeAnchors, ReturnValue<Integer> anchor2Index) {
    222 
    223         // triangle: three single nodes needed or a way with three nodes
    224 
    225         // let's get some shorter names
    226         double x1 = p1.east();
    227         double y1 = p1.north();
    228         double x2 = p2.east();
    229         double y2 = p2.north();
    230         double x3 = p3.east();
    231         double y3 = p3.north();
    232 
    233         // calculate the center (xc,yc)
    234         double s = 0.5 * ((x2 - x3) * (x1 - x3) - (y2 - y3) * (y3 - y1));
    235         double sUnder = (x1 - x2) * (y3 - y1) - (y2 - y1) * (x1 - x3);
    236 
    237         assert (sUnder != 0);
    238 
    239         s /= sUnder;
    240 
    241         double xc = 0.5 * (x1 + x2) + s * (y2 - y1);
    242         double yc = 0.5 * (y1 + y2) + s * (x1 - x2);
    243 
    244         // calculate the radius (r)
    245         double r = Math.sqrt(Math.pow(xc - x1, 2) + Math.pow(yc - y1, 2));
    246 
    247         // The angles of the anchor points relative to the center
    248         double realA1 = calcang(xc, yc, x1, y1);
    249         double realA2 = calcang(xc, yc, x2, y2);
    250         double realA3 = calcang(xc, yc, x3, y3);
    251 
    252         double startAngle = realA1;
    253         // Transform the angles to get a consistent starting point
    254         double a2 = normalizeAngle(realA2 - startAngle);
    255         double a3 = normalizeAngle(realA3 - startAngle);
    256         int direction = a3 > a2 ? 1 : -1;
    257 
    258         double radialLength = 0;
    259         if (direction == 1) { // counter clockwise
    260             radialLength = a3;
    261         } else { // clockwise
    262             radialLength = Math.PI * 2 - a3;
    263             // make the angles consistent with the direction.
    264             a2 = (Math.PI * 2 - a2);
    265         }
    266         int numberOfNodesInArc = Math.max((int) Math.ceil((radialLength / Math.PI) * 180 / angleSeparation)+1,
    267                 3);
    268         List<EastNorth> points = new ArrayList<>(numberOfNodesInArc);
    269 
    270         // Calculate the circle points in order
    271         double stepLength = radialLength / (numberOfNodesInArc-1);
    272         // Determine closest index to p2
    273 
    274         int indexJustBeforeP2 = (int) Math.floor(a2 / stepLength);
    275         int closestIndexToP2 = indexJustBeforeP2;
    276         if ((a2 - indexJustBeforeP2 * stepLength) > ((indexJustBeforeP2 + 1) * stepLength - a2)) {
    277             closestIndexToP2 = indexJustBeforeP2 + 1;
    278         }
    279         // can't merge with end node
    280         if (closestIndexToP2 == numberOfNodesInArc - 1) {
    281             closestIndexToP2--;
    282         } else if (closestIndexToP2 == 0) {
    283             closestIndexToP2++;
    284         }
    285         assert (closestIndexToP2 != 0);
    286 
    287         double a = direction * stepLength;
    288         points.add(p1);
    289         if (indexJustBeforeP2 == 0 && includeAnchors) {
    290             points.add(p2);
    291         }
    292         // i is ahead of the real index by one, since we need to be ahead in the angle calculation
    293         for (int i = 2; i < numberOfNodesInArc; i++) {
    294             double nextA = direction * (i * stepLength);
    295             double realAngle = a + startAngle;
    296             double x = xc + r * Math.cos(realAngle);
    297             double y = yc + r * Math.sin(realAngle);
    298 
    299             points.add(new EastNorth(x, y));
    300             if (i - 1 == indexJustBeforeP2 && includeAnchors) {
    301                 points.add(p2);
    302             }
    303             a = nextA;
    304         }
    305         points.add(p3);
    306         if (anchor2Index != null) {
    307             anchor2Index.value = closestIndexToP2;
    308         }
    309         return points;
    310     }
    311 
    312     // gah... why can't java support "reverse indices"?
    313     private static <T> List<T> slice(List<T> list, int from, int to) {
    314         if (to < 0)
    315             to += list.size() + 1;
    316         return list.subList(from, to);
    317     }
    318 
    319     /**
    320      * Normalizes {@code angle} so it is between 0 and 2 PI
    321      * @param angle the angle
    322      * @return the normalized angle
    323      */
    324     private static double normalizeAngle(double angle) {
    325         double PI2 = Math.PI * 2;
    326         if (angle < 0) {
    327             angle = angle + (Math.floor(-angle / PI2) + 1) * PI2;
    328         } else if (angle >= PI2) {
    329             angle = angle - Math.floor(angle / PI2) * PI2;
    330         }
    331         return angle;
    332     }
    333 
    334     private static double calcang(double xc, double yc, double x, double y) {
    335         // calculate the angle from xc|yc to x|y
    336         if (xc == x && yc == y)
    337             return 0; // actually invalid, but we won't have this case in this context
    338         double yd = Math.abs(y - yc);
    339         if (yd == 0 && xc < x)
    340             return 0;
    341         if (yd == 0 && xc > x)
    342             return Math.PI;
    343         double xd = Math.abs(x - xc);
    344         double a = Math.atan2(xd, yd);
    345         if (y > yc) {
    346             a = Math.PI - a;
    347         }
    348         if (x < xc) {
    349             a = -a;
    350         }
    351         a = 1.5 * Math.PI + a;
    352         if (a < 0) {
    353             a += 2 * Math.PI;
    354         }
    355         if (a >= 2 * Math.PI) {
    356             a -= 2 * Math.PI;
    357         }
    358         return a;
    359     }
    360 
    361     public static class ReturnValue<T> {
    362         public T value;
    363     }
     228    private static void addMoveCommandIfNeeded(Node n, PolarCoor coor, Collection<Command> cmds) {
     229        EastNorth en = coor.toEastNorth();
     230        double deltaEast = en.east() - n.getEastNorth().east();
     231        double deltaNorth = en.north() - n.getEastNorth().north();
     232        if (Math.abs(deltaEast) > 1e-7 || Math.abs(deltaNorth) > 1e-7) {
     233            cmds.add(new MoveCommand(n, deltaEast, deltaNorth));
     234        }
     235    }
     236
    364237}
  • applications/editors/josm/plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/curves/CurveAction.java

    r34840 r35384  
    2121import org.openstreetmap.josm.data.osm.Way;
    2222import org.openstreetmap.josm.gui.Notification;
    23 import org.openstreetmap.josm.spi.preferences.Config;
    2423import org.openstreetmap.josm.tools.Shortcut;
    2524
     
    3332    private static final long serialVersionUID = 1L;
    3433
    35     private int angleSeparation = -1;
    36 
    3734    public CurveAction() {
    3835        super(tr("Circle arc"), "circlearc", tr("Create a circle arc"),
     
    4037                        Shortcut.SHIFT), true);
    4138        putValue("help", ht("/Action/CreateCircleArc"));
    42         updatePreferences();
    43     }
    44 
    45     private void updatePreferences() {
    46         // @formatter:off
    47         angleSeparation = Config.getPref().getInt(prefKey("circlearc.angle-separation"), 20);
    48         // @formatter:on
    49     }
    50 
    51     private String prefKey(String subKey) {
    52         return "curves." + subKey;
    5339    }
    5440
     
    5844            return;
    5945
    60         updatePreferences();
    61 
    6246        List<Node> selectedNodes = new ArrayList<>(getLayerManager().getEditDataSet().getSelectedNodes());
    6347        List<Way> selectedWays = new ArrayList<>(getLayerManager().getEditDataSet().getSelectedWays());
    6448
    65         Collection<Command> cmds = CircleArcMaker.doCircleArc(selectedNodes, selectedWays, angleSeparation);
     49        Collection<Command> cmds = CircleArcMaker.doCircleArc(selectedNodes, selectedWays);
    6650        if (cmds == null || cmds.isEmpty()) {
    6751            new Notification(tr("Could not use selection to create a curve")).setIcon(JOptionPane.WARNING_MESSAGE).show();
    68 
    6952        } else {
    7053            UndoRedoHandler.getInstance().add(new SequenceCommand("Create a curve", cmds));
Note: See TracChangeset for help on using the changeset viewer.