Ticket #19188: 19188.patch
File 19188.patch, 15.0 KB (added by , 5 years ago) |
---|
-
src/org/openstreetmap/josm/plugins/utilsplugin2/curves/CircleArcMaker.java
1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.plugins.utilsplugin2.curves; 3 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 4 6 import java.util.ArrayList; 5 7 import java.util.Arrays; 6 8 import java.util.Collection; … … 10 12 import java.util.LinkedList; 11 13 import java.util.List; 12 14 import java.util.Set; 15 import java.util.stream.Collectors; 13 16 14 17 import org.openstreetmap.josm.command.AddCommand; 15 18 import org.openstreetmap.josm.command.ChangeCommand; … … 27 30 import org.openstreetmap.josm.data.projection.ProjectionRegistry; 28 31 import org.openstreetmap.josm.gui.MainApplication; 29 32 import org.openstreetmap.josm.tools.Geometry; 33 import org.openstreetmap.josm.tools.JosmRuntimeException; 30 34 31 35 /** 32 36 * Create a circle arc … … 107 111 LatLon ll2 = ProjectionRegistry.getProjection().eastNorth2latlon(center); 108 112 109 113 double radiusInMeters = ll1.greatCircleDistance(ll2); 114 if (radiusInMeters < 0.01) 115 throw new JosmRuntimeException(tr("Radius too small")); 110 116 111 117 int numberOfNodesInCircle = (int) Math.ceil(6.0 * Math.pow(radiusInMeters, 0.5)); 112 118 // an odd number of nodes makes the distribution uneven … … 126 132 final List<Node> nodes = new ArrayList<>(w.getNodes()); 127 133 128 134 if (!selectedWays.isEmpty()) { 135 if (w.isClosed()) { 136 // see #19188 137 nodes.clear(); 138 nodes.addAll(findShortestPart(w, anchorNodes)); 139 } 129 140 // Fix #7341. sort nodes in ways nodes order 130 141 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)); 132 143 n1 = consideredNodes.get(0); 144 n2 = consideredNodes.get(1); 133 145 n3 = consideredNodes.get(2); 146 anchorNodes = Arrays.asList(n1, n2, n3); 134 147 } 135 148 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)); 141 153 } 154 boolean clockWise = Geometry.isClockwise(cwTest); 155 142 156 boolean needsUndo = false; 143 157 if (!cmds.isEmpty()) { 144 158 UndoRedoHandler.getInstance().add(new SequenceCommand("add nodes", cmds)); 145 159 needsUndo = true; 146 160 } 161 Set<Way> targetWays = new HashSet<>(); 162 anchorNodes.forEach(n -> targetWays.addAll(n.getParentWays())); 147 163 148 164 int pos1 = nodes.indexOf(n1); 149 165 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 } 160 173 } 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 } 164 187 } 188 if (cmds.isEmpty()) { 189 throw new JosmRuntimeException(tr("Nothing to do")); 190 } 165 191 return cmds; 166 192 } 167 193 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 168 235 // 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) { 170 238 List<Command> cmds = new LinkedList<>(); 171 239 172 240 // 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. 175 242 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); 181 244 double maxStep = Math.PI * 2 / (360.0 / maxAngle); 182 245 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 190 248 int j; // End position for current arc 191 while (i < node Count) {192 for (j = i + 1; j < node Count; 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))) { 194 252 break; 253 } 195 254 } 196 255 Node first = nodes.get(i); 256 PolarCoor pcFirst = new PolarCoor(radius, PolarCoor.computeAngle(first.getEastNorth(), center), center); 197 257 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 } 204 271 if (!clockWise && delta < 0) { 205 272 delta += 2 * Math.PI; 206 273 } else if (clockWise && delta > 0) { 207 274 delta -= 2 * Math.PI; 208 275 } 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 209 282 // do we have enough nodes to produce a nice circle? 210 283 int numToAdd = Math.max((int) Math.ceil(Math.abs(delta / maxStep)), j - i) - (j-i); 211 284 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++) { 213 288 PolarCoor p = new PolarCoor(radius, pcFirst.angle + (k - i) * step, center); 214 289 addMoveCommandIfNeeded(nodes.get(k), p, cmds); 215 290 } … … 221 296 cmds.add(new AddCommand(nodes.get(0).getDataSet(), nNew)); 222 297 } 223 298 j += numToAdd; 224 nodeCount += numToAdd;225 299 } 226 300 i = j; // Update start point for next iteration 227 301 } … … 237 311 } 238 312 } 239 313 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 240 364 } -
src/org/openstreetmap/josm/plugins/utilsplugin2/curves/CurveAction.java
20 20 import org.openstreetmap.josm.data.osm.OsmPrimitive; 21 21 import org.openstreetmap.josm.data.osm.Way; 22 22 import org.openstreetmap.josm.gui.Notification; 23 import org.openstreetmap.josm.tools.JosmRuntimeException; 23 24 import org.openstreetmap.josm.tools.Shortcut; 24 25 25 26 // TODO: investigate splines … … 46 47 List<Node> selectedNodes = new ArrayList<>(getLayerManager().getEditDataSet().getSelectedNodes()); 47 48 List<Way> selectedWays = new ArrayList<>(getLayerManager().getEditDataSet().getSelectedWays()); 48 49 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()); 54 60 } 61 if (msg != null) { 62 new Notification(msg).setIcon(JOptionPane.WARNING_MESSAGE).show(); 63 } 55 64 } 56 65 57 66 @Override