Changeset 19624 in josm
- Timestamp:
- 2026-09-16T10:31:15+02:00 (10 hours ago)
- Location:
- trunk
- Files:
-
- 1 added
- 3 edited
-
src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java (modified) (12 diffs)
-
src/org/openstreetmap/josm/actions/mapmode/ParallelWays.java (modified) (6 diffs)
-
test/unit/org/openstreetmap/josm/actions/mapmode/ParallelWayActionTest.java (modified) (2 diffs)
-
test/unit/org/openstreetmap/josm/actions/mapmode/ParallelWaysTest.java (added)
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java
r19307 r19624 31 31 import org.openstreetmap.josm.data.coor.EastNorth; 32 32 import org.openstreetmap.josm.data.coor.ILatLon; 33 import org.openstreetmap.josm.data.osm.Node;34 33 import org.openstreetmap.josm.data.osm.OsmPrimitive; 35 34 import org.openstreetmap.josm.data.osm.Way; … … 51 50 import org.openstreetmap.josm.gui.util.ModifierExListener; 52 51 import org.openstreetmap.josm.tools.CheckParameterUtil; 53 import org.openstreetmap.josm.tools.Geometry;54 52 import org.openstreetmap.josm.tools.ImageProvider; 55 53 import org.openstreetmap.josm.tools.Logging; … … 102 100 private static final CachingProperty<Double> SNAP_DISTANCE_CHINESE = new DoubleProperty(prefKey("snap-distance-chinese"), 1).cached(); 103 101 private static final CachingProperty<Double> SNAP_DISTANCE_NAUTICAL = new DoubleProperty(prefKey("snap-distance-nautical"), 0.1).cached(); 102 private static final CachingProperty<Double> ARC_STEP_DEGREES 103 = new DoubleProperty(prefKey("arc-step-degrees"), ParallelWays.DEFAULT_ARC_STEP_DEGREES).cached(); 104 private static final CachingProperty<BasicStroke> PREVIEW_STROKE = new StrokeProperty(prefKey("stroke.preview"), "2").cached(); 104 105 private static final CachingProperty<Color> MAIN_COLOR = new NamedColorProperty(marktr("make parallel helper line"), Color.RED).cached(); 105 106 … … 141 142 private EastNorth helperLineStart; 142 143 private EastNorth helperLineEnd; 144 /** the source segment the current offset relates to (the one closest to the mouse) */ 145 private transient ParallelWays.ClosestPoint helperSegment; 143 146 144 147 private final ParallelWayLayer temporaryLayer = new ParallelWayLayer(); … … 192 195 sourceWays = null; 193 196 referenceSegment = null; 197 helperSegment = null; 194 198 } 195 199 … … 332 336 } else if (mode == Mode.DRAGGING) { 333 337 clearSourceWays(); 334 MainApplication.getMap().statusLine.setDist(pWays.getWays()); 338 if (pWays != null) { 339 // The nodes and ways are only created now, since their number depends on the offset 340 pWays.commit(); 341 List<Way> newWays = pWays.getWays(); 342 if (newWays.isEmpty()) { 343 new Notification(tr("Parallel Way:\n" + 344 "The offset is too large, nothing remains of the parallel way(s)")) 345 .setIcon(JOptionPane.INFORMATION_MESSAGE) 346 .show(); 347 pWays = null; 348 } else { 349 getLayerManager().getEditDataSet().setSelected(newWays); 350 MainApplication.getMap().statusLine.setDist(newWays); 351 } 352 } 335 353 } 336 354 … … 383 401 } 384 402 385 // Calculate distance to the reference line 403 // Calculate the distance to the source path: the offset relates to the part of the path closest to the 404 // mouse, so that the helper line stays meaningful when the mouse moves along the way. 386 405 Point p = e.getPoint(); 387 406 EastNorth enp = mv.getEastNorth((int) p.getX(), (int) p.getY()); 388 EastNorth nearestPointOnRefLine = Geometry.closestPointToLine(referenceSegment.getFirstNode().getEastNorth(),389 referenceSegment.getSecondNode().getEastNorth(), enp);407 ParallelWays.ClosestPoint closest = pWays.closestPoint(enp); 408 EastNorth nearestPointOnPath = closest.point; 390 409 391 410 // Note: d is the distance in _projected units_ 392 double d = enp.distance(nearestPointOnRefLine);411 double d = Math.abs(closest.signedDistance); 393 412 double realD = mv.getProjection().eastNorth2latlon(enp).greatCircleDistance( 394 (ILatLon) mv.getProjection().eastNorth2latlon(nearestPointOn RefLine));413 (ILatLon) mv.getProjection().eastNorth2latlon(nearestPointOnPath)); 395 414 double snappedRealD = realD; 396 415 397 boolean toTheRight = Geometry.angleIsClockwise( 398 referenceSegment.getFirstNode(), referenceSegment.getSecondNode(), new Node(enp)); 416 boolean toTheRight = closest.signedDistance < 0; 399 417 400 418 if (snap) { … … 425 443 } 426 444 } 427 d = snappedRealD * (d/realD); // convert back to projected distance. (probably ok on small scales) 428 helperLineStart = nearestPointOnRefLine; 445 if (realD > 0) { 446 d = snappedRealD * (d/realD); // convert back to projected distance. (probably ok on small scales) 447 } 448 helperLineStart = nearestPointOnPath; 429 449 helperLineEnd = enp; 450 helperSegment = closest; 430 451 if (toTheRight) { 431 452 d = -d; … … 507 528 508 529 sourceWays.removeIf(w -> w.isIncomplete() || w.isEmpty()); 530 531 // The selection may have been changed by other means while in this mode (e.g. Selection > Non-branching way 532 // sequences): if it contains the way under the mouse, the selected ways are the source ways. 533 Set<Way> selectedWays = new LinkedHashSet<>(getLayerManager().getEditDataSet().getSelectedWays()); 534 selectedWays.removeIf(w -> w.isIncomplete() || w.isEmpty()); 535 if (selectedWays.contains(referenceSegment.getWay()) && !selectedWays.equals(sourceWays)) { 536 clearSourceWays(); 537 for (Way w : selectedWays) { 538 addSourceWay(w); 539 } 540 } 509 541 510 542 if (!sourceWays.contains(referenceSegment.getWay())) { … … 523 555 i++; 524 556 } 525 pWays = new ParallelWays(sourceWays, copyTags, referenceWayIndex); 526 pWays.commit(); 527 getLayerManager().getEditDataSet().setSelected(pWays.getWays()); 557 pWays = new ParallelWays(sourceWays, copyTags, referenceWayIndex, ARC_STEP_DEGREES.get()); 528 558 return true; 529 559 } catch (IllegalArgumentException e) { 530 560 Logging.debug(e); 531 new Notification(tr("Parallel WayAction\n" +561 new Notification(tr("Parallel Way:\n" + 532 562 "The ways selected must form a simple branchless path")) 533 563 .setIcon(JOptionPane.INFORMATION_MESSAGE) … … 629 659 g.setColor(mainColor); 630 660 MapViewPath line = new MapViewPath(mv); 631 line.moveTo(referenceSegment.getFirstNode()); 632 line.lineTo(referenceSegment.getSecondNode()); 661 if (helperSegment != null) { 662 line.moveTo(helperSegment.segmentStart); 663 line.lineTo(helperSegment.segmentEnd); 664 } else { 665 line.moveTo(referenceSegment.getFirstNode()); 666 line.lineTo(referenceSegment.getSecondNode()); 667 } 633 668 g.draw(line.computeClippedLine(g.getStroke())); 634 669 … … 639 674 line.lineTo(helperLineEnd); 640 675 g.draw(line.computeClippedLine(g.getStroke())); 676 677 // Preview of the parallel way(s) 678 if (pWays != null) { 679 List<EastNorth> pts = pWays.getOffsetPoints(); 680 if (pts.size() > 1) { 681 g.setStroke(PREVIEW_STROKE.get()); 682 line = new MapViewPath(mv); 683 line.moveTo(pts.get(0)); 684 for (int i = 1; i < pts.size(); i++) { 685 line.lineTo(pts.get(i)); 686 } 687 if (pWays.isResultClosed()) { 688 line.lineTo(pts.get(0)); 689 } 690 g.draw(line.computeClippedLine(g.getStroke())); 691 } 692 } 641 693 } 642 694 } -
trunk/src/org/openstreetmap/josm/actions/mapmode/ParallelWays.java
r16438 r19624 3 3 4 4 import java.util.ArrayList; 5 import java.util.Arrays; 5 6 import java.util.Collection; 6 7 import java.util.Collections; 7 import java.util.HashMap;8 8 import java.util.HashSet; 9 9 import java.util.List; 10 import java.util.Map;11 10 import java.util.Set; 11 import java.util.function.IntConsumer; 12 12 import java.util.stream.IntStream; 13 13 … … 22 22 import org.openstreetmap.josm.data.osm.OsmDataManager; 23 23 import org.openstreetmap.josm.data.osm.Way; 24 import org.openstreetmap.josm.tools.Geometry;25 import org.openstreetmap.josm.tools.Utils;26 24 27 25 /** 28 26 * Helper for {@link ParallelWayAction}. 29 * 30 * @author Ole Jørgen Brønner (olejorgenb) 27 * <p> 28 * Computes a one-sided offset ("parallel") of a branchless path made of one or more ways. 29 * <p> 30 * The algorithm is a proper one-sided buffer, which also works when the offset is much larger than 31 * the length of the segments of the source path (e.g. a maritime boundary 22 km off a coastline): 32 * <ol> 33 * <li>Each segment is offset by the requested distance.</li> 34 * <li>At every vertex the two neighbouring offset segments are joined: on the inner side of a turn they are 35 * clipped at their intersection, on the outer side a mitre is used for gentle turns and a circular arc 36 * (approximated by chords) for sharp turns or when the mitre would overshoot too far.</li> 37 * <li>The resulting raw polyline is split at its self-intersections. Every piece is kept only if it lies at 38 * (at least) the offset distance from the source path. Pieces which are closer belong to inverted loops 39 * ("swallowtails") and are dropped. The remaining pieces are chained; the longest chain is the result.</li> 40 * </ol> 41 * All calculations are done in projected coordinates. 42 * <p> 43 * Contrary to earlier versions the nodes and ways are only created (and added to the data set) by 44 * {@link #commit()}, since the number of nodes of the result depends on the offset. Use 45 * {@link #getOffsetPoints()} to draw a preview while the offset is changed. 31 46 */ 32 47 public class ParallelWays { 33 private final List<Way> ways; 48 49 /** Default angular step used to approximate circular arcs, in degrees */ 50 public static final double DEFAULT_ARC_STEP_DEGREES = 10; 51 52 private static final int NO_NODE = -1; 53 /** marker for raw segments of the end caps, which are only used to trim the result and are never part of it */ 54 private static final int CAP = -2; 55 56 private final List<Way> sourceWays; 57 private final boolean copyTags; 58 private final double arcStep; 59 60 /** the source nodes, in path order (duplicates by coordinate removed, oriented like the reference way) */ 34 61 private final List<Node> sortedNodes; 35 62 private final boolean closed; 36 63 private final int nodeCount; 37 64 38 private final EastNorth[] pts; 39 private final EastNorth[] normals; 65 private final double[] px; 66 private final double[] py; 67 /** unit direction of segment i */ 68 private final double[] dirX; 69 private final double[] dirY; 70 private final double[] segLen; 71 /** bounding boxes of the source segments, used to speed up distance computations */ 72 private final double[] segMinX; 73 private final double[] segMinY; 74 private final double[] segMaxX; 75 private final double[] segMaxY; 76 77 /** whether the source way runs in the same direction as sortedNodes (aligned with sourceWays) */ 78 private final boolean[] wayForward; 79 /** source way index for each source segment */ 80 private final int[] segWay; 81 82 // Spatial index of the source segments (rebuilt for every offset, since the cell size depends on it) 83 private double gridCell; 84 private double gridMinX; 85 private double gridMinY; 86 private int gridCols; 87 private int gridRows; 88 private int[][] gridCells; 89 private int[] gridStamp; 90 private int gridQuery; 91 92 // Result of the last changeOffset call 93 private List<EastNorth> resultPts = Collections.emptyList(); 94 private int[] resultPieceSeg = new int[0]; 95 private int[] resultPointNode = new int[0]; 96 private boolean resultClosed; 97 98 private List<Way> ways = Collections.emptyList(); 40 99 41 100 /** … … 44 103 * @param copyTags whether tags should be copied 45 104 * @param refWayIndex Need a reference way to determine the direction of the offset when we manage multiple ways 105 * @throws IllegalArgumentException if the ways do not form a branchless path 46 106 */ 47 107 public ParallelWays(Collection<Way> sourceWays, boolean copyTags, int refWayIndex) { 48 // Possible/sensible to use PrimitiveDeepCopy here? 49 50 // Make a deep copy of the ways, keeping the copied ways connected 51 // TODO: This assumes the first/last nodes of the ways are the only possible shared nodes. 52 Map<Node, Node> splitNodeMap = new HashMap<>(Utils.hashMapInitialCapacity(sourceWays.size())); 53 for (Way w : sourceWays) { 54 copyNodeInMap(splitNodeMap, w.firstNode(), copyTags); 55 copyNodeInMap(splitNodeMap, w.lastNode(), copyTags); 56 } 57 ways = new ArrayList<>(sourceWays.size()); 58 for (Way w : sourceWays) { 59 Way wCopy = new Way(); 60 wCopy.addNode(splitNodeMap.get(w.firstNode())); 61 for (int i = 1; i < w.getNodesCount() - 1; i++) { 62 wCopy.addNode(copyNode(w.getNode(i), copyTags)); 63 } 64 wCopy.addNode(splitNodeMap.get(w.lastNode())); 65 if (copyTags) { 66 wCopy.setKeys(w.getKeys()); 67 } 68 ways.add(wCopy); 69 } 108 this(sourceWays, copyTags, refWayIndex, DEFAULT_ARC_STEP_DEGREES); 109 } 110 111 /** 112 * Constructs a new {@code ParallelWays}. 113 * @param sourceWays source ways 114 * @param copyTags whether tags should be copied 115 * @param refWayIndex Need a reference way to determine the direction of the offset when we manage multiple ways 116 * @param arcStepDegrees angular step (in degrees) of the chords approximating arcs at convex corners 117 * @throws IllegalArgumentException if the ways do not form a branchless path 118 * @since 19624 119 */ 120 public ParallelWays(Collection<Way> sourceWays, boolean copyTags, int refWayIndex, double arcStepDegrees) { 121 this.sourceWays = new ArrayList<>(sourceWays); 122 this.copyTags = copyTags; 123 this.arcStep = Math.toRadians(Math.max(1, Math.min(90, arcStepDegrees))); 70 124 71 125 // Find a linear ordering of the nodes. Fail if there isn't one. 72 NodeGraph nodeGraph = NodeGraph.createUndirectedGraphFromNodeWays( ways);126 NodeGraph nodeGraph = NodeGraph.createUndirectedGraphFromNodeWays(this.sourceWays); 73 127 List<Node> sortedNodesPath = nodeGraph.buildSpanningPath(); 74 if (sortedNodesPath == null) 128 if (sortedNodesPath == null || sortedNodesPath.size() < 2) 75 129 throw new IllegalArgumentException("Ways must have spanning path"); // Create a dedicated exception? 76 130 77 // Fix #8631 - Remove duplicated nodes from graph to be robust with self-intersecting ways 78 Set<Node> removedNodes = new HashSet<>(); 79 sortedNodes = new ArrayList<>(); 80 for (int i = 0; i < sortedNodesPath.size(); i++) { 81 Node n = sortedNodesPath.get(i); 82 if (i < sortedNodesPath.size()-1 && sortedNodesPath.get(i+1).getCoor().equals(n.getCoor())) { 83 removedNodes.add(n); 84 for (Way w : ways) { 85 w.removeNode(n); 86 } 87 continue; 88 } 89 if (!removedNodes.contains(n)) { 90 sortedNodes.add(n); 91 } 92 } 93 94 // Ugly method of ensuring that the offset isn't inverted. I'm sure there is a better and more elegant way 95 Way refWay = ways.get(refWayIndex); 96 boolean refWayReversed = IntStream.range(0, sortedNodes.size() - 1) 97 .noneMatch(i -> sortedNodes.get(i) == refWay.firstNode() && sortedNodes.get(i + 1) == refWay.getNode(1)); 98 if (refWayReversed) { 99 Collections.reverse(sortedNodes); // need to keep the orientation of the reference way. 100 } 101 102 // Initialize the required parameters. (segment normals, etc.) 103 nodeCount = sortedNodes.size(); 104 pts = new EastNorth[nodeCount]; 105 normals = new EastNorth[nodeCount - 1]; 106 int i = 0; 107 for (Node n : sortedNodes) { 108 EastNorth t = n.getEastNorth(); 109 pts[i] = t; 110 i++; 111 } 112 for (i = 0; i < nodeCount - 1; i++) { 113 double dx = pts[i + 1].getX() - pts[i].getX(); 114 double dy = pts[i + 1].getY() - pts[i].getY(); 115 double len = Math.sqrt(dx * dx + dy * dy); 116 normals[i] = new EastNorth(-dy / len, dx / len); 117 } 118 } 119 120 private static void copyNodeInMap(Map<Node, Node> splitNodeMap, Node node, boolean copyTags) { 121 if (!splitNodeMap.containsKey(node)) { 122 splitNodeMap.put(node, copyNode(node, copyTags)); 123 } 131 List<Node> nodes = new ArrayList<>(sortedNodesPath.size()); 132 for (Node n : sortedNodesPath) { 133 if (nodes.isEmpty() || !nodes.get(nodes.size() - 1).getEastNorth().equalsEpsilon(n.getEastNorth(), 1e-9)) { 134 nodes.add(n); 135 } 136 } 137 closed = nodes.size() > 2 && nodes.get(0) == nodes.get(nodes.size() - 1); 138 if (closed) { 139 nodes.remove(nodes.size() - 1); 140 } 141 if (nodes.size() < 2) 142 throw new IllegalArgumentException("Ways must have spanning path"); 143 144 Way refWay = this.sourceWays.get(refWayIndex); 145 if (!isForward(nodes, refWay, closed)) { 146 Collections.reverse(nodes); // need to keep the orientation of the reference way. 147 } 148 if (closed) { 149 // rotate so that the path starts at a way boundary: every way is then a contiguous run of nodes 150 Node start = this.sourceWays.stream().map(Way::firstNode).filter(nodes::contains).findFirst().orElse(nodes.get(0)); 151 Collections.rotate(nodes, -nodes.indexOf(start)); 152 nodes.add(nodes.get(0)); 153 } 154 sortedNodes = nodes; 155 nodeCount = nodes.size(); 156 157 // Initialize the required parameters. (segment directions, etc.) 158 px = new double[nodeCount]; 159 py = new double[nodeCount]; 160 for (int i = 0; i < nodeCount; i++) { 161 EastNorth en = nodes.get(i).getEastNorth(); 162 px[i] = en.getX(); 163 py[i] = en.getY(); 164 } 165 int segCount = nodeCount - 1; 166 dirX = new double[segCount]; 167 dirY = new double[segCount]; 168 segLen = new double[segCount]; 169 segMinX = new double[segCount]; 170 segMinY = new double[segCount]; 171 segMaxX = new double[segCount]; 172 segMaxY = new double[segCount]; 173 for (int i = 0; i < segCount; i++) { 174 double dx = px[i + 1] - px[i]; 175 double dy = py[i + 1] - py[i]; 176 double len = Math.hypot(dx, dy); 177 segLen[i] = len; 178 dirX[i] = dx / len; 179 dirY[i] = dy / len; 180 segMinX[i] = Math.min(px[i], px[i + 1]); 181 segMaxX[i] = Math.max(px[i], px[i + 1]); 182 segMinY[i] = Math.min(py[i], py[i + 1]); 183 segMaxY[i] = Math.max(py[i], py[i + 1]); 184 } 185 186 // Map the source ways onto the path 187 int wayCount = this.sourceWays.size(); 188 wayForward = new boolean[wayCount]; 189 segWay = new int[segCount]; 190 Arrays.fill(segWay, -1); 191 for (int w = 0; w < wayCount; w++) { 192 Way way = this.sourceWays.get(w); 193 Set<Node> wayNodes = new HashSet<>(way.getNodes()); 194 // indices (without the closing duplicate) of the path nodes belonging to this way 195 boolean[] present = new boolean[nodeCount]; 196 int first = -1; 197 int last = -1; 198 for (int i = 0; i < nodeCount - (closed ? 1 : 0); i++) { 199 if (wayNodes.contains(sortedNodes.get(i))) { 200 present[i] = true; 201 if (first < 0) { 202 first = i; 203 } 204 last = i; 205 } 206 } 207 if (first < 0) { 208 first = 0; 209 last = 0; 210 } else if (closed && present[0] && present[nodeCount - 2]) { 211 // the run of this way wraps around the closing node: find where it starts 212 int i = nodeCount - 2; 213 while (i > 0 && present[i - 1]) { 214 i--; 215 } 216 first = i; 217 last = nodeCount - 1; 218 } 219 wayForward[w] = isForward(sortedNodes.subList(first, last + 1), way, false); 220 for (int i = first; i < last; i++) { 221 segWay[i] = w; 222 } 223 } 224 } 225 226 /** 227 * Checks whether a way runs in the same direction as a node list. 228 * @param path the node list 229 * @param way the way 230 * @param cyclic whether the node list is a ring (without repeated closing node) 231 * @return {@code true} if the first segment of the way, found in the path, has the same orientation 232 */ 233 private static boolean isForward(List<Node> path, Way way, boolean cyclic) { 234 int n = path.size(); 235 for (int k = 0; k < way.getNodesCount() - 1; k++) { 236 int a = path.indexOf(way.getNode(k)); 237 int b = path.indexOf(way.getNode(k + 1)); 238 if (a >= 0 && b >= 0 && a != b) { 239 if (cyclic) { 240 return ((b - a) % n + n) % n < n / 2.0; 241 } 242 return b > a; 243 } 244 } 245 return true; 124 246 } 125 247 … … 129 251 */ 130 252 public boolean isClosedPath() { 131 return sortedNodes.get(0) == sortedNodes.get(sortedNodes.size() - 1); 253 return closed; 254 } 255 256 /** 257 * The point of the source path closest to a given point, see {@link #closestPoint(EastNorth)}. 258 * @since 19624 259 */ 260 public static final class ClosestPoint { 261 /** the closest point on the source path */ 262 public final EastNorth point; 263 /** start of the source segment the closest point lies on */ 264 public final EastNorth segmentStart; 265 /** end of the source segment the closest point lies on */ 266 public final EastNorth segmentEnd; 267 /** distance between the given point and the path; positive if the given point lies to the left of the path 268 * (in the direction of the reference way), i.e. the sign matches the offset of {@link #changeOffset(double)} */ 269 public final double signedDistance; 270 271 ClosestPoint(EastNorth point, EastNorth segmentStart, EastNorth segmentEnd, double signedDistance) { 272 this.point = point; 273 this.segmentStart = segmentStart; 274 this.segmentEnd = segmentEnd; 275 this.signedDistance = signedDistance; 276 } 277 } 278 279 /** 280 * Finds the point of the source path closest to the given point. Used to relate the offset to the part of the 281 * path the mouse is currently next to (rather than to the segment where the drag started). 282 * @param p the point (projected coordinates) 283 * @return the closest point, the segment it lies on and the signed distance 284 * @since 19624 285 */ 286 public ClosestPoint closestPoint(EastNorth p) { 287 double x = p.getX(); 288 double y = p.getY(); 289 int bestSeg = 0; 290 double bestSq = Double.POSITIVE_INFINITY; 291 double bestT = 0; 292 for (int i = 0; i < nodeCount - 1; i++) { 293 double rx = x - px[i]; 294 double ry = y - py[i]; 295 double t = Math.max(0, Math.min(segLen[i], rx * dirX[i] + ry * dirY[i])); 296 double ex = rx - t * dirX[i]; 297 double ey = ry - t * dirY[i]; 298 double dSq = ex * ex + ey * ey; 299 if (dSq < bestSq) { 300 bestSq = dSq; 301 bestSeg = i; 302 bestT = t; 303 } 304 } 305 int i = bestSeg; 306 // Side of the path: left of the closest segment is positive. If the closest point is a vertex between two 307 // segments, the point lies in the wedge outside the corner, where the two segments may disagree about the 308 // side (for turns sharper than 90°): the side is then the outside of the turn. 309 double side = dirX[i] * (y - py[i]) - dirY[i] * (x - px[i]); 310 int vertex = -1; 311 if (bestT <= 0) { 312 vertex = i; 313 } else if (bestT >= segLen[i]) { 314 vertex = i + 1; 315 } 316 if (vertex >= 0) { 317 int segCount = nodeCount - 1; 318 int prev = vertex - 1; 319 int next = vertex; 320 if (closed) { 321 prev = (vertex - 1 + segCount) % segCount; 322 next = vertex % segCount; 323 } 324 if (prev >= 0 && next < segCount) { 325 double turn = dirX[prev] * dirY[next] - dirY[prev] * dirX[next]; 326 if (Math.abs(turn) > 1e-12) { 327 side = -turn; 328 } 329 } 330 } 331 return new ClosestPoint(new EastNorth(px[i] + bestT * dirX[i], py[i] + bestT * dirY[i]), 332 new EastNorth(px[i], py[i]), new EastNorth(px[i + 1], py[i + 1]), 333 (side >= 0 ? 1 : -1) * Math.sqrt(bestSq)); 132 334 } 133 335 … … 137 339 */ 138 340 public void changeOffset(double d) { 139 // This is the core algorithm: 140 /* 1. Calculate a parallel line, offset by 'd', to each segment in the path 141 * 2. Find the intersection of lines belonging to neighboring segments. These become the new node positions 142 * 3. Do some special casing for closed paths 143 * 144 * Simple and probably not even close to optimal performance wise 145 */ 146 147 EastNorth[] ppts = new EastNorth[nodeCount]; 148 149 EastNorth prevA = pts[0].add(normals[0].scale(d)); 150 EastNorth prevB = pts[1].add(normals[0].scale(d)); 151 for (int i = 1; i < nodeCount - 1; i++) { 152 EastNorth a = pts[i].add(normals[i].scale(d)); 153 EastNorth b = pts[i + 1].add(normals[i].scale(d)); 154 if (Geometry.segmentsParallel(a, b, prevA, prevB)) { 155 ppts[i] = a; 341 if (d == 0 || Double.isNaN(d)) { 342 // no offset: the result is a copy (for a ring without the repeated closing node) 343 int pointCount = closed ? nodeCount - 1 : nodeCount; 344 resultPts = new ArrayList<>(pointCount); 345 for (int i = 0; i < pointCount; i++) { 346 resultPts.add(new EastNorth(px[i], py[i])); 347 } 348 resultPieceSeg = IntStream.range(0, nodeCount - 1).toArray(); 349 resultPointNode = IntStream.range(0, pointCount).toArray(); 350 resultClosed = closed; 351 return; 352 } 353 buildGrid(Math.abs(d)); 354 RawPolyline raw = buildRawOffset(d); 355 trim(raw, Math.abs(d)); 356 } 357 358 /** 359 * Builds a uniform grid over the source segments, with a cell size of (at least) r, so that all segments 360 * within distance r of a point are found in the 3x3 cells around it. 361 * @param r the (absolute) offset 362 */ 363 private void buildGrid(double r) { 364 double minX = Double.POSITIVE_INFINITY; 365 double minY = Double.POSITIVE_INFINITY; 366 double maxX = Double.NEGATIVE_INFINITY; 367 double maxY = Double.NEGATIVE_INFINITY; 368 for (int i = 0; i < nodeCount; i++) { 369 minX = Math.min(minX, px[i]); 370 maxX = Math.max(maxX, px[i]); 371 minY = Math.min(minY, py[i]); 372 maxY = Math.max(maxY, py[i]); 373 } 374 double extent = Math.max(maxX - minX, maxY - minY); 375 gridCell = Math.max(r, extent / 128); 376 gridMinX = minX; 377 gridMinY = minY; 378 gridCols = (int) ((maxX - minX) / gridCell) + 1; 379 gridRows = (int) ((maxY - minY) / gridCell) + 1; 380 int[] counts = new int[gridCols * gridRows]; 381 int segCount = nodeCount - 1; 382 for (int i = 0; i < segCount; i++) { 383 forEachCell(segMinX[i], segMinY[i], segMaxX[i], segMaxY[i], c -> counts[c]++); 384 } 385 gridCells = new int[counts.length][]; 386 for (int c = 0; c < counts.length; c++) { 387 gridCells[c] = new int[counts[c]]; 388 counts[c] = 0; 389 } 390 for (int i = 0; i < segCount; i++) { 391 final int seg = i; 392 forEachCell(segMinX[i], segMinY[i], segMaxX[i], segMaxY[i], c -> gridCells[c][counts[c]++] = seg); 393 } 394 gridStamp = new int[segCount]; 395 gridQuery = 0; 396 } 397 398 private void forEachCell(double minX, double minY, double maxX, double maxY, IntConsumer consumer) { 399 int c0 = Math.max(0, (int) ((minX - gridMinX) / gridCell)); 400 int c1 = Math.min(gridCols - 1, (int) ((maxX - gridMinX) / gridCell)); 401 int r0 = Math.max(0, (int) ((minY - gridMinY) / gridCell)); 402 int r1 = Math.min(gridRows - 1, (int) ((maxY - gridMinY) / gridCell)); 403 for (int row = r0; row <= r1; row++) { 404 for (int col = c0; col <= c1; col++) { 405 consumer.accept(row * gridCols + col); 406 } 407 } 408 } 409 410 // --------------------------------------------------------------------------------------------------------- 411 // Step 1: raw offset polyline 412 413 /** 414 * The raw (untrimmed) offset polyline. Segment k runs from point k to point k+1 (or to point 0 for the last 415 * segment of a closed polyline). The attributes of a segment are stored at its end point. 416 */ 417 private static final class RawPolyline { 418 double[] x = new double[64]; 419 double[] y = new double[64]; 420 /** index of the source node a point was derived from, or {@link #NO_NODE} */ 421 int[] node = new int[64]; 422 /** source segment index of the segment ending at this point, or {@link #CAP} */ 423 int[] seg = new int[64]; 424 /** if the segment ending at this point is an arc chord: the source node the arc is centered on, else NO_NODE */ 425 int[] arcCenter = new int[64]; 426 int size; 427 428 /** for each source node with an arc: raw index of the first arc point, else -1 */ 429 final int[] arcStart; 430 /** number of chords of the arc at each source node */ 431 final int[] arcChords; 432 /** whether the arc can be replaced by a mitre when it is not affected by the trimming */ 433 final boolean[] arcMitre; 434 final double[] mitreX; 435 final double[] mitreY; 436 437 RawPolyline(int nodeCount) { 438 arcStart = new int[nodeCount]; 439 Arrays.fill(arcStart, -1); 440 arcChords = new int[nodeCount]; 441 arcMitre = new boolean[nodeCount]; 442 mitreX = new double[nodeCount]; 443 mitreY = new double[nodeCount]; 444 } 445 446 void add(double px, double py, int srcNode, int srcSeg, int arc) { 447 if (size == x.length) { 448 int n = size * 2; 449 x = Arrays.copyOf(x, n); 450 y = Arrays.copyOf(y, n); 451 node = Arrays.copyOf(node, n); 452 seg = Arrays.copyOf(seg, n); 453 arcCenter = Arrays.copyOf(arcCenter, n); 454 } 455 x[size] = px; 456 y[size] = py; 457 node[size] = srcNode; 458 seg[size] = srcSeg; 459 arcCenter[size] = arc; 460 size++; 461 } 462 } 463 464 private RawPolyline buildRawOffset(double d) { 465 RawPolyline raw = new RawPolyline(nodeCount); 466 int segCount = nodeCount - 1; 467 if (closed) { 468 addJoin(raw, 0, segCount - 1, 0, d); 469 } else { 470 // Start cap: half circle on the back side of the first node. It is never part of the result, but 471 // trims pieces of the offset which come closer than d to the first node. 472 addCap(raw, 0, Math.atan2(-dirX[0] * d, dirY[0] * d), d > 0 ? -1 : 1, d, true); 473 raw.add(px[0] - dirY[0] * d, py[0] + dirX[0] * d, 0, CAP, NO_NODE); 474 } 475 for (int k = 1; k < segCount; k++) { 476 addJoin(raw, k, k - 1, k, d); 477 } 478 if (!closed) { 479 int s = segCount - 1; 480 raw.add(px[s + 1] - dirY[s] * d, py[s + 1] + dirX[s] * d, s + 1, s, NO_NODE); 481 // End cap: half circle on the front side of the last node 482 addCap(raw, s + 1, Math.atan2(dirX[s] * d, -dirY[s] * d), d > 0 ? -1 : 1, d, false); 483 } else { 484 // the closing segment ends at raw point 0 485 raw.seg[0] = segCount - 1; 486 raw.arcCenter[0] = NO_NODE; 487 } 488 return raw; 489 } 490 491 /** 492 * Adds the chords of a half circle around node {@code k}, starting at the given angle. The chords are marked 493 * as {@link #CAP}: they are only used to trim other pieces. 494 * @param raw the raw polyline 495 * @param k source node index 496 * @param startAngle angle of the first point of the half circle 497 * @param direction rotation direction (+1 counter clockwise) 498 * @param d the offset 499 * @param leading {@code true} if the cap precedes the offset path (the end point of the half circle is then 500 * added by the caller), {@code false} if it follows it (the start point has been added by the caller) 501 */ 502 private void addCap(RawPolyline raw, int k, double startAngle, int direction, double d, boolean leading) { 503 double r = Math.abs(d); 504 int steps = (int) Math.ceil(Math.PI / arcStep - 1e-9); 505 for (int j = leading ? 0 : 1; j < (leading ? steps : steps + 1); j++) { 506 double angle = startAngle + direction * Math.PI * j / steps; 507 raw.add(px[k] + r * Math.cos(angle), py[k] + r * Math.sin(angle), NO_NODE, CAP, NO_NODE); 508 } 509 } 510 511 /** 512 * Adds the offset points at vertex {@code k}, where segment {@code prev} ends and segment {@code next} starts. 513 * @param raw the raw polyline 514 * @param k source node index 515 * @param prev index of the segment ending at k 516 * @param next index of the segment starting at k 517 * @param d the offset 518 */ 519 private void addJoin(RawPolyline raw, int k, int prev, int next, double d) { 520 double r = Math.abs(d); 521 // offset end point of prev, offset start point of next 522 double bx = px[k] - dirY[prev] * d; 523 double by = py[k] + dirX[prev] * d; 524 double ax = px[k] - dirY[next] * d; 525 double ay = py[k] + dirX[next] * d; 526 527 double cross = dirX[prev] * dirY[next] - dirY[prev] * dirX[next]; 528 double dot = dirX[prev] * dirX[next] + dirY[prev] * dirY[next]; 529 double theta = Math.atan2(Math.abs(cross), dot); // turn angle in [0, pi] 530 531 if (theta < 1e-9) { 532 // collinear: the offset points coincide 533 raw.add(bx, by, k, prev, NO_NODE); 534 return; 535 } 536 boolean concave = cross * d > 0; // the turn goes towards the offset side 537 if (concave && theta < Math.PI - 1e-9) { 538 // Clip: intersect the two offset segments. 539 // X = b + t*dir[prev] with -len[prev] <= t <= 0 and X = a + s*dir[next] with 0 <= s <= len[next] 540 double ex = ax - bx; 541 double ey = ay - by; 542 double t = (ex * dirY[next] - ey * dirX[next]) / cross; 543 double s = (ex * dirY[prev] - ey * dirX[prev]) / cross; 544 if (-t <= segLen[prev] && s <= segLen[next] && t <= 1e-9 && s >= -1e-9) { 545 raw.add(bx + t * dirX[prev], by + t * dirY[prev], k, prev, NO_NODE); 156 546 } else { 157 ppts[i] = Geometry.getLineLineIntersection(a, b, prevA, prevB); 158 } 159 prevA = a; 160 prevB = b; 161 } 162 if (isClosedPath()) { 163 EastNorth a = pts[0].add(normals[0].scale(d)); 164 EastNorth b = pts[1].add(normals[0].scale(d)); 165 if (Geometry.segmentsParallel(a, b, prevA, prevB)) { 166 ppts[0] = a; 547 // The offset is larger than the neighbouring segments allow: local inversion. 548 // Emit both points; the inverted part is removed when trimming. 549 raw.add(bx, by, k, prev, NO_NODE); 550 raw.add(ax, ay, NO_NODE, next, k); 551 } 552 return; 553 } 554 // Convex corner (or a hairpin): arc from b to a around the vertex. If the arc survives the trimming 555 // untouched, it may later be replaced by a mitre (for gentle corners, see arcMitre). 556 raw.add(bx, by, k, prev, NO_NODE); 557 int steps = (int) Math.ceil(theta / arcStep - 1e-9); 558 double angle0 = Math.atan2(by - py[k], bx - px[k]); 559 double delta; 560 if (Math.abs(cross) < 1e-12) { 561 // hairpin: rotate away from the segments, i.e. towards dir[prev] 562 delta = theta * (d > 0 ? -1 : 1); 563 } else { 564 delta = theta * (cross >= 0 ? 1 : -1); 565 } 566 raw.arcStart[k] = raw.size - 1; 567 raw.arcChords[k] = steps; 568 for (int j = 1; j < steps; j++) { 569 double angle = angle0 + delta * j / steps; 570 raw.add(px[k] + r * Math.cos(angle), py[k] + r * Math.sin(angle), NO_NODE, next, k); 571 } 572 raw.add(ax, ay, NO_NODE, next, k); 573 574 if (theta < Math.PI - 1e-6) { 575 double mitreDist = r / Math.cos(theta / 2); 576 double overshoot = mitreDist - r; 577 raw.arcMitre[k] = theta <= arcStep || overshoot <= 0.5 * Math.min(segLen[prev], segLen[next]); 578 // the mitre point lies on the bisector of the two normals 579 double mx = (bx - px[k]) + (ax - px[k]); 580 double my = (by - py[k]) + (ay - py[k]); 581 double ml = Math.hypot(mx, my); 582 raw.mitreX[k] = px[k] + mx / ml * mitreDist; 583 raw.mitreY[k] = py[k] + my / ml * mitreDist; 584 } 585 } 586 587 // --------------------------------------------------------------------------------------------------------- 588 // Step 2: trimming 589 590 /** Growable list of pieces (sub segments of the raw polyline) */ 591 private static final class Pieces { 592 int[] start = new int[256]; 593 int[] end = new int[256]; 594 int[] rawSeg = new int[256]; 595 boolean[] valid = new boolean[256]; 596 int size; 597 598 void add(int s, int e, int seg, boolean v) { 599 if (size == start.length) { 600 int n = size * 2; 601 start = Arrays.copyOf(start, n); 602 end = Arrays.copyOf(end, n); 603 rawSeg = Arrays.copyOf(rawSeg, n); 604 valid = Arrays.copyOf(valid, n); 605 } 606 start[size] = s; 607 end[size] = e; 608 rawSeg[size] = seg; 609 valid[size] = v; 610 size++; 611 } 612 } 613 614 /** Point table used while trimming: raw points first, then intersection and transition points */ 615 private static final class Points { 616 final List<double[]> xy = new ArrayList<>(); 617 final List<Integer> node = new ArrayList<>(); 618 619 int add(double x, double y, int srcNode) { 620 xy.add(new double[] {x, y}); 621 node.add(srcNode); 622 return xy.size() - 1; 623 } 624 625 double[] get(int id) { 626 return xy.get(id); 627 } 628 629 double distance(int a, int b) { 630 double[] p = xy.get(a); 631 double[] q = xy.get(b); 632 return Math.hypot(q[0] - p[0], q[1] - p[1]); 633 } 634 } 635 636 private void trim(RawPolyline raw, double r) { 637 int m = raw.size; 638 int rawSegCount = closed ? m : m - 1; 639 if (rawSegCount < 1) { 640 setEmptyResult(); 641 return; 642 } 643 644 Points points = new Points(); 645 for (int i = 0; i < m; i++) { 646 points.add(raw.x[i], raw.y[i], raw.node[i]); 647 } 648 // Raw segments which lie completely inside the offset distance ("deep") can neither be part of the result 649 // nor trim it: skip them when looking for intersections. This is a huge saving for large offsets. 650 boolean[] deep = new boolean[rawSegCount]; 651 double tolerance = r * 1e-7 + 1e-7; 652 double sagitta = r * (1 - Math.cos(arcStep / 2)); 653 for (int i = 0; i < rawSegCount; i++) { 654 int endPoint = (i + 1) % m; 655 double mx = (raw.x[i] + raw.x[endPoint]) / 2; 656 double my = (raw.y[i] + raw.y[endPoint]) / 2; 657 double halfLen = Math.hypot(raw.x[endPoint] - raw.x[i], raw.y[endPoint] - raw.y[i]) / 2; 658 // every point of the segment is within halfLen (+ sagitta for a chord) of the middle 659 double cutoff = r - halfLen - tolerance - (raw.arcCenter[endPoint] != NO_NODE ? sagitta : 0); 660 deep[i] = cutoff > 0 && distanceToSource(mx, my, cutoff) < cutoff; 661 } 662 663 // breakpoints per raw segment: parallel lists of (param, pointId) 664 List<List<double[]>> breaks = new ArrayList<>(Collections.nCopies(rawSegCount, null)); 665 findSelfIntersections(raw, rawSegCount, deep, points, breaks); 666 667 // Build the pieces and determine their validity: a piece must lie at (at least) distance r from the source 668 Pieces pieces = new Pieces(); 669 boolean anyInvalid = false; 670 for (int i = 0; i < rawSegCount; i++) { 671 int endPoint = (i + 1) % m; 672 if (deep[i]) { 673 pieces.add(i, endPoint, i, false); 674 continue; 675 } 676 List<double[]> b = breaks.get(i); 677 int prevId = i; 678 if (b != null) { 679 b.sort((o1, o2) -> Double.compare(o1[0], o2[0])); 680 for (double[] bp : b) { 681 classify(raw, r, points, pieces, i, prevId, (int) bp[1]); 682 prevId = (int) bp[1]; 683 } 684 } 685 classify(raw, r, points, pieces, i, prevId, endPoint); 686 } 687 for (int p = 0; p < pieces.size; p++) { 688 anyInvalid |= !pieces.valid[p]; 689 } 690 691 // Chain the valid pieces. Consecutive valid pieces of the same component meet at a common point 692 // (self-intersection or transition), but pieces of other components (e.g. an excursion of the path, or an 693 // inner loop) may lie between them in raw order: a piece is therefore attached to whichever open chain ends 694 // where it starts. Chord approximations of arcs can leave small gaps - such gaps are bridged; larger gaps 695 // separate different components. 696 double bridgeTolerance = 2 * r * Math.sin(arcStep / 2); 697 double spikeTolerance = 2 * r * (1 - Math.cos(arcStep / 2)) + tolerance; 698 List<Chain> chains = new ArrayList<>(); 699 int pieceCount = pieces.size; 700 int startPiece = 0; 701 if (closed && anyInvalid) { 702 while (pieces.valid[startPiece]) { 703 startPiece++; 704 } 705 startPiece = (startPiece + 1) % pieceCount; 706 } 707 for (int c = 0; c < pieceCount; c++) { 708 int p = (startPiece + c) % pieceCount; 709 if (!pieces.valid[p]) { 710 continue; 711 } 712 int srcSeg = raw.seg[(pieces.rawSeg[p] + 1) % m]; 713 Chain chain = findChainEndingAt(chains, pieces.start[p], points, bridgeTolerance); 714 if (chain == null) { 715 chain = new Chain(pieces.start[p], spikeTolerance); 716 chains.add(chain); 717 } else if (chain.lastId != pieces.start[p]) { 718 chain.append(pieces.start[p], srcSeg, points); 719 } 720 chain.append(pieces.end[p], srcSeg, points); 721 } 722 // Join chains whose end meets the start of another chain (a ring may also close onto itself) 723 boolean merged = true; 724 while (merged && chains.size() > 1) { 725 merged = false; 726 for (int i = 0; i < chains.size() && !merged; i++) { 727 Chain a = chains.get(i); 728 for (int j = 0; j < chains.size(); j++) { 729 Chain b = chains.get(j); 730 int bFirst = b.ids.get(0); 731 if (a != b && (a.lastId == bFirst || points.distance(a.lastId, bFirst) <= bridgeTolerance)) { 732 if (a.lastId != bFirst) { 733 a.append(bFirst, b.segs.get(0), points); 734 } 735 a.appendChain(b); 736 chains.remove(j); 737 merged = true; 738 break; 739 } 740 } 741 } 742 } 743 if (chains.isEmpty()) { 744 setEmptyResult(); 745 return; 746 } 747 Chain best = chains.get(0); 748 for (Chain ch : chains) { 749 if (ch.length > best.length) { 750 best = ch; 751 } 752 } 753 if (closed && best.lastId != best.ids.get(0) && points.distance(best.lastId, best.ids.get(0)) <= bridgeTolerance) { 754 // close a ring which is open by a small gap only 755 best.append(best.ids.get(0), best.segs.get(best.segs.size() - 1), points); 756 } 757 758 // Convert to the result 759 boolean isRing = closed && best.ids.size() > 2 && best.ids.get(0) == best.lastId; 760 List<Integer> ids = new ArrayList<>(best.ids); 761 List<Integer> segs = new ArrayList<>(best.segs); 762 replaceIntactArcsByMitres(raw, points, ids, segs); 763 if (isRing) { 764 ids.remove(ids.size() - 1); 765 // rotate so that the ring starts at a way boundary 766 int n = segs.size(); 767 int rot = 0; 768 for (int i = 0; i < n; i++) { 769 if (segWay[segs.get((i + n - 1) % n)] != segWay[segs.get(i)]) { 770 rot = i; 771 break; 772 } 773 } 774 Collections.rotate(ids, -rot); 775 Collections.rotate(segs, -rot); 776 } 777 resultPts = new ArrayList<>(ids.size()); 778 resultPointNode = new int[ids.size()]; 779 for (int i = 0; i < ids.size(); i++) { 780 int id = ids.get(i); 781 double[] pt = points.get(id); 782 resultPts.add(new EastNorth(pt[0], pt[1])); 783 resultPointNode[i] = points.node.get(id); 784 } 785 resultPieceSeg = segs.stream().mapToInt(Integer::intValue).toArray(); 786 resultClosed = isRing; 787 } 788 789 private void setEmptyResult() { 790 resultPts = Collections.emptyList(); 791 resultPieceSeg = new int[0]; 792 resultPointNode = new int[0]; 793 resultClosed = false; 794 } 795 796 /** 797 * Determines the validity of the piece of raw segment {@code i} between two points, and adds the resulting 798 * piece(s). The validity is sampled at both ends and in the middle; if it changes, the transition point is 799 * located and the piece is split there. 800 */ 801 private void classify(RawPolyline raw, double r, Points points, Pieces pieces, int i, int sId, int eId) { 802 int endPoint = (i + 1) % raw.size; 803 if (raw.seg[endPoint] == CAP) { 804 pieces.add(sId, eId, i, false); // end caps are never part of the result 805 return; 806 } 807 int arc = raw.arcCenter[endPoint]; 808 double[] s = points.get(sId); 809 double[] e = points.get(eId); 810 double tolerance = r * 1e-7 + 1e-7; 811 boolean vs = isValid(s[0], s[1], r, tolerance, arc); 812 boolean ve = isValid(e[0], e[1], r, tolerance, arc); 813 boolean vm = isValid(s[0] + (e[0] - s[0]) / 2, s[1] + (e[1] - s[1]) / 2, r, tolerance, arc); 814 if (vs == vm && vm == ve) { 815 pieces.add(sId, eId, i, vm); 816 return; 817 } 818 // Transitions very close to an end point are artefacts of the chord approximation (a point where two 819 // chords cross lies inside both circles): the validity of the middle is then used for the whole piece. 820 double snapTolerance = 2 * r * (1 - Math.cos(arcStep / 2)) + tolerance; 821 double len = Math.hypot(e[0] - s[0], e[1] - s[1]); 822 int prev = sId; 823 boolean state = vs; 824 if (vs != vm) { 825 double t1 = locateTransition(s, e, 0, 0.5, r, tolerance, arc); 826 if (t1 * len <= snapTolerance) { 827 state = vm; 167 828 } else { 168 ppts[0] = Geometry.getLineLineIntersection(a, b, prevA, prevB); 169 } 170 ppts[nodeCount - 1] = ppts[0]; 171 } else { 172 ppts[0] = pts[0].add(normals[0].scale(d)); 173 ppts[nodeCount - 1] = pts[nodeCount - 1].add(normals[nodeCount - 2].scale(d)); 174 } 175 176 for (int i = 0; i < nodeCount; i++) { 177 sortedNodes.get(i).setEastNorth(ppts[i]); 178 } 179 } 180 181 /** 182 * Performs the action by adding a new sequence command to the undo/redo queue. 829 int id = points.add(s[0] + (e[0] - s[0]) * t1, s[1] + (e[1] - s[1]) * t1, NO_NODE); 830 pieces.add(prev, id, i, state); 831 prev = id; 832 state = vm; 833 } 834 } 835 if (vm != ve) { 836 double t2 = locateTransition(s, e, 0.5, 1, r, tolerance, arc); 837 if ((1 - t2) * len > snapTolerance) { 838 int id = points.add(s[0] + (e[0] - s[0]) * t2, s[1] + (e[1] - s[1]) * t2, NO_NODE); 839 pieces.add(prev, id, i, state); 840 prev = id; 841 state = ve; 842 } 843 } 844 pieces.add(prev, eId, i, state); 845 } 846 847 /** 848 * Locates (by bisection) the parameter between {@code a} and {@code b} where the validity changes. 849 * @return the parameter of the transition 850 */ 851 private double locateTransition(double[] s, double[] e, double a, double b, double r, double tolerance, int arc) { 852 boolean va = isValid(s[0] + (e[0] - s[0]) * a, s[1] + (e[1] - s[1]) * a, r, tolerance, arc); 853 for (int it = 0; it < 40 && b - a > 1e-12; it++) { 854 double mid = (a + b) / 2; 855 boolean vmid = isValid(s[0] + (e[0] - s[0]) * mid, s[1] + (e[1] - s[1]) * mid, r, tolerance, arc); 856 if (vmid == va) { 857 a = mid; 858 } else { 859 b = mid; 860 } 861 } 862 return (a + b) / 2; 863 } 864 865 /** 866 * Checks whether a point of the raw polyline lies at (at least) distance r from the source path. 867 * @param x point 868 * @param y point 869 * @param r offset 870 * @param tolerance allowed deficit 871 * @param arc if the point lies on an arc chord: the center node of the arc; the point is then projected onto 872 * the arc before testing, else {@link #NO_NODE} 873 * @return {@code true} if the point is valid 874 */ 875 private boolean isValid(double x, double y, double r, double tolerance, int arc) { 876 if (arc != NO_NODE) { 877 double vx = x - px[arc]; 878 double vy = y - py[arc]; 879 double vl = Math.hypot(vx, vy); 880 if (vl > 0) { 881 x = px[arc] + vx / vl * r; 882 y = py[arc] + vy / vl * r; 883 } 884 } 885 return distanceToSource(x, y, r - tolerance) >= r - tolerance; 886 } 887 888 /** 889 * Replaces arcs which are completely part of the result by a mitre (where the corner is gentle enough). 890 */ 891 private static void replaceIntactArcsByMitres(RawPolyline raw, Points points, List<Integer> ids, List<Integer> segs) { 892 int[] arcOfRawPoint = new int[raw.size]; 893 Arrays.fill(arcOfRawPoint, NO_NODE); 894 for (int k = 0; k < raw.arcStart.length; k++) { 895 if (raw.arcStart[k] >= 0 && raw.arcMitre[k]) { 896 arcOfRawPoint[raw.arcStart[k]] = k; 897 } 898 } 899 for (int i = 0; i < ids.size(); i++) { 900 int id = ids.get(i); 901 if (id >= raw.size || arcOfRawPoint[id] == NO_NODE) { 902 continue; 903 } 904 int k = arcOfRawPoint[id]; 905 int chords = raw.arcChords[k]; 906 if (i + chords >= ids.size()) { 907 continue; 908 } 909 boolean intact = true; 910 for (int j = 1; j <= chords && intact; j++) { 911 intact = ids.get(i + j) == id + j; 912 } 913 if (!intact) { 914 continue; 915 } 916 int mitreId = points.add(raw.mitreX[k], raw.mitreY[k], k); 917 // the chord pieces i..i+chords-1 collapse into two pieces: (i-1 -> mitre), (mitre -> i+chords) 918 ids.set(i, mitreId); 919 ids.subList(i + 1, i + chords + 1).clear(); 920 // segs.get(j) belongs to the piece ending at ids.get(j+1); keep the segment of the last chord for the 921 // piece leaving the mitre, and drop the others 922 segs.subList(i, i + chords).clear(); 923 } 924 } 925 926 /** 927 * Finds the chain ending at (or within the tolerance of) the given point, preferring an exact match. 928 * @return the chain, or {@code null} 929 */ 930 private static Chain findChainEndingAt(List<Chain> chains, int id, Points points, double tolerance) { 931 Chain near = null; 932 double nearDist = tolerance; 933 for (Chain ch : chains) { 934 if (ch.lastId == id) { 935 return ch; 936 } 937 double dist = points.distance(ch.lastId, id); 938 if (dist <= nearDist) { 939 near = ch; 940 nearDist = dist; 941 } 942 } 943 return near; 944 } 945 946 /** A chain of connected valid pieces */ 947 private static final class Chain { 948 final List<Integer> ids = new ArrayList<>(); 949 final List<Integer> segs = new ArrayList<>(); 950 final double spikeTolerance; 951 int lastId; 952 double length; 953 954 Chain(int startId, double spikeTolerance) { 955 ids.add(startId); 956 lastId = startId; 957 this.spikeTolerance = spikeTolerance; 958 } 959 960 void append(int endId, int srcSeg, Points points) { 961 lastId = endId; 962 if (ids.size() >= 2 && points.distance(ids.get(ids.size() - 2), endId) <= spikeTolerance) { 963 // tiny out-and-back spike (an artefact of the chord approximation at a crossing): drop its tip 964 int tip = ids.remove(ids.size() - 1); 965 segs.remove(segs.size() - 1); 966 length -= points.distance(ids.get(ids.size() - 1), tip); 967 } 968 double len = points.distance(ids.get(ids.size() - 1), endId); 969 if (len < 1e-9) { 970 // zero length piece: keep the connectivity, but don't add a point 971 return; 972 } 973 length += len; 974 ids.add(endId); 975 segs.add(srcSeg); 976 } 977 978 void appendChain(Chain other) { 979 ids.addAll(other.ids.subList(1, other.ids.size())); 980 segs.addAll(other.segs); 981 lastId = other.lastId; 982 length += other.length; 983 } 984 } 985 986 /** 987 * Finds all intersections between non adjacent segments of the raw polyline (sweep on x). 988 * @param raw the raw polyline 989 * @param rawSegCount number of raw segments 990 * @param skip raw segments to ignore 991 * @param points point table, intersection points are appended 992 * @param breaks per raw segment list of (param, pointId), filled 993 */ 994 private void findSelfIntersections(RawPolyline raw, int rawSegCount, boolean[] skip, Points points, 995 List<List<double[]>> breaks) { 996 int m = raw.size; 997 double[] minX = new double[rawSegCount]; 998 double[] maxX = new double[rawSegCount]; 999 double[] minY = new double[rawSegCount]; 1000 double[] maxY = new double[rawSegCount]; 1001 int count = 0; 1002 Integer[] order = new Integer[rawSegCount]; 1003 for (int i = 0; i < rawSegCount; i++) { 1004 if (skip[i]) { 1005 continue; 1006 } 1007 int j = (i + 1) % m; 1008 minX[i] = Math.min(raw.x[i], raw.x[j]); 1009 maxX[i] = Math.max(raw.x[i], raw.x[j]); 1010 minY[i] = Math.min(raw.y[i], raw.y[j]); 1011 maxY[i] = Math.max(raw.y[i], raw.y[j]); 1012 order[count++] = i; 1013 } 1014 Arrays.sort(order, 0, count, (a, b) -> Double.compare(minX[a], minX[b])); 1015 double[] uv = new double[2]; 1016 for (int oi = 0; oi < count; oi++) { 1017 int i = order[oi]; 1018 for (int oj = oi + 1; oj < count; oj++) { 1019 int j = order[oj]; 1020 if (minX[j] > maxX[i]) { 1021 break; 1022 } 1023 if (minY[j] > maxY[i] || maxY[j] < minY[i]) { 1024 continue; 1025 } 1026 int lo = Math.min(i, j); 1027 int hi = Math.max(i, j); 1028 if (hi - lo == 1 || (closed && lo == 0 && hi == rawSegCount - 1)) { 1029 continue; // adjacent 1030 } 1031 int i2 = (i + 1) % m; 1032 int j2 = (j + 1) % m; 1033 if (segmentIntersection(raw.x[i], raw.y[i], raw.x[i2], raw.y[i2], raw.x[j], raw.y[j], raw.x[j2], raw.y[j2], uv)) { 1034 int id = points.add(raw.x[i] + (raw.x[i2] - raw.x[i]) * uv[0], raw.y[i] + (raw.y[i2] - raw.y[i]) * uv[0], NO_NODE); 1035 if (breaks.get(i) == null) { 1036 breaks.set(i, new ArrayList<>(2)); 1037 } 1038 if (breaks.get(j) == null) { 1039 breaks.set(j, new ArrayList<>(2)); 1040 } 1041 breaks.get(i).add(new double[] {uv[0], id}); 1042 breaks.get(j).add(new double[] {uv[1], id}); 1043 } 1044 } 1045 } 1046 } 1047 1048 /** 1049 * Segment/segment intersection. 1050 * @param uv output: parameters along the first and the second segment 1051 * @return true if the segments intersect (touching end points count as intersection) 1052 */ 1053 private static boolean segmentIntersection(double x1, double y1, double x2, double y2, 1054 double x3, double y3, double x4, double y4, double[] uv) { 1055 double a1 = x2 - x1; 1056 double a2 = y2 - y1; 1057 double b1 = x3 - x4; 1058 double b2 = y3 - y4; 1059 double c1 = x3 - x1; 1060 double c2 = y3 - y1; 1061 double det = a1 * b2 - a2 * b1; 1062 double uu = b2 * c1 - b1 * c2; 1063 double vv = a1 * c2 - a2 * c1; 1064 double mag = Math.abs(uu) + Math.abs(vv); 1065 if (det == 0 || Math.abs(det) <= 1e-12 * mag) { 1066 return false; // parallel or collinear 1067 } 1068 double u = uu / det; 1069 double v = vv / det; 1070 if (u < -1e-9 || u > 1 + 1e-9 || v < -1e-9 || v > 1 + 1e-9) { 1071 return false; 1072 } 1073 uv[0] = Math.max(0, Math.min(1, u)); 1074 uv[1] = Math.max(0, Math.min(1, v)); 1075 return true; 1076 } 1077 1078 /** 1079 * Distance from a point to the source path. 1080 * @param x point 1081 * @param y point 1082 * @param cutoff the search can stop as soon as a distance below this value is found 1083 * @return the distance (or any value below cutoff if such a distance exists) 1084 */ 1085 private double distanceToSource(double x, double y, double cutoff) { 1086 // Only segments within the cutoff matter (the result is only compared against it); with a cell size of 1087 // at least the offset they all lie in the 3x3 cells around the point. 1088 int col = (int) Math.floor((x - gridMinX) / gridCell); 1089 int row = (int) Math.floor((y - gridMinY) / gridCell); 1090 if (col < -1 || col > gridCols || row < -1 || row > gridRows) { 1091 return Double.POSITIVE_INFINITY; 1092 } 1093 gridQuery++; 1094 double best = Double.POSITIVE_INFINITY; 1095 double bestSq = Double.POSITIVE_INFINITY; 1096 double cutoffSq = cutoff * cutoff; 1097 for (int rr = Math.max(0, row - 1); rr <= Math.min(gridRows - 1, row + 1); rr++) { 1098 for (int cc = Math.max(0, col - 1); cc <= Math.min(gridCols - 1, col + 1); cc++) { 1099 for (int i : gridCells[rr * gridCols + cc]) { 1100 if (gridStamp[i] == gridQuery) { 1101 continue; 1102 } 1103 gridStamp[i] = gridQuery; 1104 if (x < segMinX[i] - best || x > segMaxX[i] + best || y < segMinY[i] - best || y > segMaxY[i] + best) { 1105 continue; 1106 } 1107 double rx = x - px[i]; 1108 double ry = y - py[i]; 1109 double t = rx * dirX[i] + ry * dirY[i]; 1110 double dSq; 1111 if (t <= 0) { 1112 dSq = rx * rx + ry * ry; 1113 } else if (t >= segLen[i]) { 1114 double ex = x - px[i + 1]; 1115 double ey = y - py[i + 1]; 1116 dSq = ex * ex + ey * ey; 1117 } else { 1118 double c = rx * dirY[i] - ry * dirX[i]; 1119 dSq = c * c; 1120 } 1121 if (dSq < bestSq) { 1122 bestSq = dSq; 1123 best = Math.sqrt(dSq); 1124 if (bestSq < cutoffSq) { 1125 return best; 1126 } 1127 } 1128 } 1129 } 1130 } 1131 return best; 1132 } 1133 1134 // --------------------------------------------------------------------------------------------------------- 1135 // Result access and commit 1136 1137 /** 1138 * Returns the points of the offset path computed by the last call to {@link #changeOffset(double)}. 1139 * For a closed result the first point is not repeated at the end, see {@link #isResultClosed()}. 1140 * @return the offset points (projected coordinates), empty if nothing has been computed or nothing remains 1141 * @since 19624 1142 */ 1143 public List<EastNorth> getOffsetPoints() { 1144 return Collections.unmodifiableList(resultPts); 1145 } 1146 1147 /** 1148 * Determines if the result of the last {@link #changeOffset(double)} call is a closed ring. 1149 * @return {@code true} if the offset path is a closed ring 1150 * @since 19624 1151 */ 1152 public boolean isResultClosed() { 1153 return resultClosed; 1154 } 1155 1156 /** 1157 * Creates the nodes and ways of the offset path (as computed by the last call to {@link #changeOffset(double)}), 1158 * and adds them to the edit data set by adding a new sequence command to the undo/redo queue. 1159 * <p> 1160 * Does nothing if there is no offset path. 183 1161 */ 184 1162 public void commit() { 185 UndoRedoHandler.getInstance().add(new SequenceCommand("Make parallel way(s)", makeAddWayAndNodesCommandList())); 1163 List<Command> commands = makeAddWayAndNodesCommandList(); 1164 if (!commands.isEmpty()) { 1165 UndoRedoHandler.getInstance().add(new SequenceCommand("Make parallel way(s)", commands)); 1166 } 186 1167 } 187 1168 188 1169 private List<Command> makeAddWayAndNodesCommandList() { 189 1170 DataSet ds = OsmDataManager.getInstance().getEditDataSet(); 190 191 List<Command> commands = new ArrayList<>(sortedNodes.size() + ways.size()); 192 Set<Node> dupCheck = new HashSet<>(); 193 for (int i = 0; i < sortedNodes.size() - (isClosedPath() ? 1 : 0); i++) { 194 Node n = sortedNodes.get(i); 195 // don't add the same node twice, see #18386 196 if (dupCheck.add(n)) { 197 commands.add(new AddCommand(ds, n)); 198 } 199 } 200 for (Way w : ways) { 1171 List<Way> newWays = buildWays(); 1172 ways = newWays; 1173 List<Command> commands = new ArrayList<>(); 1174 if (newWays.isEmpty()) { 1175 return commands; 1176 } 1177 List<Node> added = new ArrayList<>(); 1178 for (Way w : newWays) { 1179 for (Node n : w.getNodes()) { 1180 // don't add the same node twice, see #18386 1181 if (!added.contains(n)) { 1182 added.add(n); 1183 commands.add(new AddCommand(ds, n)); 1184 } 1185 } 1186 } 1187 for (Way w : newWays) { 201 1188 commands.add(new AddCommand(ds, w)); 202 1189 } … … 204 1191 } 205 1192 206 private static Node copyNode(Node source, boolean copyTags) { 207 if (copyTags) 208 return new Node(source, true); 209 else { 210 Node n = new Node(); 211 n.setCoor(source.getCoor()); 212 return n; 213 } 214 } 215 216 /** 217 * Returns the resulting parallel ways. 218 * @return the resulting parallel ways 1193 /** 1194 * Builds the (not yet added) ways from the last computed offset path. 1195 * @return the ways, in the order of the source ways; ways swallowed by the offset are omitted 1196 */ 1197 private List<Way> buildWays() { 1198 int pointCount = resultPts.size(); 1199 List<Way> result = new ArrayList<>(sourceWays.size()); 1200 if (pointCount < 2) { 1201 return result; 1202 } 1203 int pieceCount = resultPieceSeg.length; 1204 Node[] nodes = new Node[pointCount]; 1205 for (int w = 0; w < sourceWays.size(); w++) { 1206 int first = -1; 1207 int last = -1; 1208 for (int p = 0; p < pieceCount; p++) { 1209 if (segWay[resultPieceSeg[p]] == w) { 1210 if (first < 0) { 1211 first = p; 1212 } 1213 last = p; 1214 } 1215 } 1216 if (first < 0) { 1217 continue; // way is swallowed by the offset 1218 } 1219 List<Node> wayNodes = new ArrayList<>(last - first + 2); 1220 for (int p = first; p <= last + 1; p++) { 1221 int idx = p % pointCount; 1222 if (nodes[idx] == null) { 1223 nodes[idx] = makeNode(idx); 1224 } 1225 wayNodes.add(nodes[idx]); 1226 } 1227 if (!wayForward[w]) { 1228 Collections.reverse(wayNodes); 1229 } 1230 Way source = sourceWays.get(w); 1231 Way copy = new Way(); 1232 copy.setNodes(wayNodes); 1233 if (copyTags) { 1234 copy.setKeys(source.getKeys()); 1235 } 1236 result.add(copy); 1237 } 1238 return result; 1239 } 1240 1241 private Node makeNode(int idx) { 1242 Node n; 1243 int src = resultPointNode[idx]; 1244 if (copyTags && src != NO_NODE) { 1245 n = new Node(sortedNodes.get(src), true); 1246 } else { 1247 n = new Node(); 1248 } 1249 n.setEastNorth(resultPts.get(idx)); 1250 return n; 1251 } 1252 1253 /** 1254 * Returns the resulting parallel ways, available after {@link #commit()}. 1255 * @return the resulting parallel ways (empty before commit) 219 1256 */ 220 1257 public final List<Way> getWays() { -
trunk/test/unit/org/openstreetmap/josm/actions/mapmode/ParallelWayActionTest.java
r19227 r19624 13 13 import org.openstreetmap.josm.data.coor.LatLon; 14 14 import org.openstreetmap.josm.data.osm.DataSet; 15 import org.openstreetmap.josm.data.osm.Node; 16 import org.openstreetmap.josm.data.osm.Way; 15 17 import org.openstreetmap.josm.gui.MainApplication; 16 18 import org.openstreetmap.josm.gui.MapFrame; … … 71 73 72 74 /** 75 * Several selected ways connected by common nodes are offset together, one result way per source way. 76 */ 77 @Test 78 void testMultipleSelectedWays() { 79 Node a = new Node(LatLon.ZERO); 80 Node b = new Node(new LatLon(0, 0.0001)); 81 Node c = new Node(new LatLon(0, 0.0002)); 82 Way w1 = new Way(); 83 w1.addNode(a); 84 w1.addNode(b); 85 Way w2 = new Way(); 86 w2.addNode(b); 87 w2.addNode(c); 88 for (Node n : new Node[] {a, b, c}) { 89 this.dataSet.addPrimitive(n); 90 } 91 this.dataSet.addPrimitive(w1); 92 this.dataSet.addPrimitive(w2); 93 this.dataSet.setSelected(w1, w2); 94 this.map.selectMapMode(mapMode); 95 MapModeUtils.dragFromTo(new LatLon(0, 0.00005), new LatLon(0.00005, 0.00005)); 96 assertEquals(2, this.dataSet.getWays().size() - 2, "two parallel ways expected: " + this.dataSet.getWays()); 97 assertEquals(3 + 3 + 2 + 2, this.dataSet.allPrimitives().size()); 98 } 99 100 /** 101 * The selection changed while in the mode (e.g. by Selection > Non-branching way sequences, after a first 102 * parallel way has been created) is used as source. 103 */ 104 @Test 105 void testSelectionChangedInMode() { 106 Node a = new Node(LatLon.ZERO); 107 Node b = new Node(new LatLon(0, 0.0001)); 108 Node c = new Node(new LatLon(0, 0.0002)); 109 Way w1 = new Way(); 110 w1.addNode(a); 111 w1.addNode(b); 112 Way w2 = new Way(); 113 w2.addNode(b); 114 w2.addNode(c); 115 for (Node n : new Node[] {a, b, c}) { 116 this.dataSet.addPrimitive(n); 117 } 118 this.dataSet.addPrimitive(w1); 119 this.dataSet.addPrimitive(w2); 120 this.dataSet.setSelected(w1); 121 this.map.selectMapMode(mapMode); 122 // first parallel of the single selected way 123 MapModeUtils.dragFromTo(new LatLon(0, 0.00005), new LatLon(0.00005, 0.00005)); 124 assertEquals(3, this.dataSet.getWays().size()); 125 // now the selection is extended by other means, the next drag must use both ways 126 this.dataSet.setSelected(w1, w2); 127 MapModeUtils.dragFromTo(new LatLon(0, 0.00005), new LatLon(-0.00005, 0.00005)); 128 assertEquals(5, this.dataSet.getWays().size(), this.dataSet.getWays().toString()); 129 } 130 131 /** 73 132 * Unit test of {@link Mode} enum. 74 133 */
Note:
See TracChangeset
for help on using the changeset viewer.
