Changeset 35384 in osm for applications/editors/josm/plugins/utilsplugin2/src/org
- Timestamp:
- 2020-03-21T07:42:00+01:00 (5 years ago)
- 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 6 6 import java.util.Collection; 7 7 import java.util.Collections; 8 import java.util.Comparator;9 8 import java.util.HashSet; 10 9 import java.util.Iterator; 11 10 import java.util.LinkedList; 12 11 import java.util.List; 13 import java.util.Objects;14 12 import java.util.Set; 15 13 … … 17 15 import org.openstreetmap.josm.command.ChangeCommand; 18 16 import org.openstreetmap.josm.command.Command; 17 import org.openstreetmap.josm.command.MoveCommand; 18 import org.openstreetmap.josm.command.SequenceCommand; 19 import org.openstreetmap.josm.data.UndoRedoHandler; 19 20 import org.openstreetmap.josm.data.coor.EastNorth; 21 import org.openstreetmap.josm.data.coor.LatLon; 22 import org.openstreetmap.josm.data.coor.PolarCoor; 20 23 import org.openstreetmap.josm.data.osm.DataSet; 21 24 import org.openstreetmap.josm.data.osm.Node; 22 25 import org.openstreetmap.josm.data.osm.Way; 26 import org.openstreetmap.josm.data.projection.ProjectionRegistry; 23 27 import org.openstreetmap.josm.gui.MainApplication; 28 import org.openstreetmap.josm.tools.Geometry; 24 29 25 30 /** … … 32 37 } 33 38 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<>(); 36 41 37 42 //// Decides which nodes to use as anchors based on selection … … 43 48 * When existing ways are reused for the arc, all ways overlapping these are transformed too. 44 49 * 45 * 1. Exactly 3 nodes selected: 46 * Use these nodes. 50 * Exactly 3 nodes have to be selected. 47 51 * - 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 direction51 * - Selected node used as middle node its the middle node in a 3 node way52 * - Parent way used53 52 */ 54 53 … … 56 55 Node n1 = null, n2 = null, n3 = null; 57 56 58 Set<Way> targetWays = new HashSet<>();59 57 DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 60 58 59 if (selectedWays.size() > 1) 60 return cmds; 61 62 Way w = null; 61 63 boolean nodesHaveBeenChoosen = false; 62 64 if (selectedNodes.size() == 3) { … … 66 68 n3 = nodeIter.next(); 67 69 nodesHaveBeenChoosen = true; 68 if (selectedWays.isEmpty()) { // Create a brand new way69 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 }76 70 } 77 71 if (!selectedWays.isEmpty()) { 78 // TODO: use only two nodes inferring the orientation from the parent way. 79 72 w = selectedWays.iterator().next(); 80 73 if (!nodesHaveBeenChoosen) { 81 74 // Use the three last nodes in the way as anchors. This is intended to be used with the 82 75 // built in draw mode 83 Way w = selectedWays.iterator().next(); //TODO: select last selected way instead84 76 int nodeCount = w.getNodesCount(); 85 77 if (nodeCount < 3) 86 return null;78 return cmds; 87 79 n3 = w.getNode(nodeCount - 1); 88 80 n2 = w.getNode(nodeCount - 2); … … 90 82 nodesHaveBeenChoosen = true; 91 83 } 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; 116 88 } 117 89 … … 122 94 // make sure that points are different 123 95 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; 145 192 } 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 } 154 225 return cmds; 155 226 } 156 227 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 364 237 } -
applications/editors/josm/plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/curves/CurveAction.java
r34840 r35384 21 21 import org.openstreetmap.josm.data.osm.Way; 22 22 import org.openstreetmap.josm.gui.Notification; 23 import org.openstreetmap.josm.spi.preferences.Config;24 23 import org.openstreetmap.josm.tools.Shortcut; 25 24 … … 33 32 private static final long serialVersionUID = 1L; 34 33 35 private int angleSeparation = -1;36 37 34 public CurveAction() { 38 35 super(tr("Circle arc"), "circlearc", tr("Create a circle arc"), … … 40 37 Shortcut.SHIFT), true); 41 38 putValue("help", ht("/Action/CreateCircleArc")); 42 updatePreferences();43 }44 45 private void updatePreferences() {46 // @formatter:off47 angleSeparation = Config.getPref().getInt(prefKey("circlearc.angle-separation"), 20);48 // @formatter:on49 }50 51 private String prefKey(String subKey) {52 return "curves." + subKey;53 39 } 54 40 … … 58 44 return; 59 45 60 updatePreferences();61 62 46 List<Node> selectedNodes = new ArrayList<>(getLayerManager().getEditDataSet().getSelectedNodes()); 63 47 List<Way> selectedWays = new ArrayList<>(getLayerManager().getEditDataSet().getSelectedWays()); 64 48 65 Collection<Command> cmds = CircleArcMaker.doCircleArc(selectedNodes, selectedWays , angleSeparation);49 Collection<Command> cmds = CircleArcMaker.doCircleArc(selectedNodes, selectedWays); 66 50 if (cmds == null || cmds.isEmpty()) { 67 51 new Notification(tr("Could not use selection to create a curve")).setIcon(JOptionPane.WARNING_MESSAGE).show(); 68 69 52 } else { 70 53 UndoRedoHandler.getInstance().add(new SequenceCommand("Create a curve", cmds));
Note:
See TracChangeset
for help on using the changeset viewer.