Ticket #24983: parallel-way-24983.patch
| File parallel-way-24983.patch, 41.6 KB (added by , 13 days ago) |
|---|
-
src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java
diff --git src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java index 3715aca227..8b315edbfa 100644
public class ParallelWayAction extends MapMode implements ModifierExListener { 677 677 678 678 // Preview of the parallel way(s) 679 679 if (pWays != null) { 680 List<EastNorth> pts = pWays.getOffsetPoints(); 681 if (pts.size() > 1) { 682 g.setStroke(PREVIEW_STROKE.get()); 683 line = new MapViewPath(mv); 684 line.moveTo(pts.get(0)); 685 for (int i = 1; i < pts.size(); i++) { 686 line.lineTo(pts.get(i)); 680 g.setStroke(PREVIEW_STROKE.get()); 681 for (List<EastNorth> pts : pWays.getOffsetPaths()) { 682 if (pts.size() > 1) { 683 line = new MapViewPath(mv); 684 line.moveTo(pts.get(0)); 685 for (int i = 1; i < pts.size(); i++) { 686 line.lineTo(pts.get(i)); 687 } 688 g.draw(line.computeClippedLine(g.getStroke())); 687 689 } 688 if (pWays.isResultClosed()) {689 line.lineTo(pts.get(0));690 }691 g.draw(line.computeClippedLine(g.getStroke()));692 690 } 693 691 } 694 692 } -
src/org/openstreetmap/josm/actions/mapmode/ParallelWays.java
diff --git src/org/openstreetmap/josm/actions/mapmode/ParallelWays.java src/org/openstreetmap/josm/actions/mapmode/ParallelWays.java index 8cee3b00d4..7e585810a7 100644
import java.util.ArrayList; 5 5 import java.util.Arrays; 6 6 import java.util.Collection; 7 7 import java.util.Collections; 8 import java.util.HashMap; 8 9 import java.util.HashSet; 9 10 import java.util.List; 11 import java.util.Map; 10 12 import java.util.Set; 11 13 import java.util.function.IntConsumer; 12 14 import java.util.stream.IntStream; … … import org.openstreetmap.josm.data.osm.Way; 36 38 * (approximated by chords) for sharp turns or when the mitre would overshoot too far.</li> 37 39 * <li>The resulting raw polyline is split at its self-intersections. Every piece is kept only if it lies at 38 40 * (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>41 * ("swallowtails") and are dropped. The remaining pieces are chained.</li> 40 42 * </ol> 43 * Where the path crosses itself, or comes back within twice the offset of itself, the offset can consist of 44 * several separate chains (e.g. around a loop which passes under its own approach). All of them are kept, and each 45 * becomes its own way(s). The exception are chains produced only by retraced segments (a path which goes out and 46 * back along the same nodes): seen from such a segment, both sides are "its" side, so they would draw a loop 47 * around the excursion on the far side of the path. 41 48 * All calculations are done in projected coordinates. 42 49 * <p> 43 50 * Contrary to earlier versions the nodes and ways are only created (and added to the data set) by 44 51 * {@link #commit()}, since the number of nodes of the result depends on the offset. Use 45 * {@link #getOffsetP oints()} to draw a preview while the offset is changed.52 * {@link #getOffsetPaths()} to draw a preview while the offset is changed. 46 53 */ 47 54 public class ParallelWays { 48 55 … … public class ParallelWays { 52 59 private static final int NO_NODE = -1; 53 60 /** marker for raw segments of the end caps, which are only used to trim the result and are never part of it */ 54 61 private static final int CAP = -2; 62 /** maximal distance between the samples of the validity of a piece, as a fraction of the offset */ 63 private static final double SAMPLE_SPACING = 0.125; 55 64 56 65 private final List<Way> sourceWays; 57 66 private final boolean copyTags; … … public class ParallelWays { 78 87 private final boolean[] wayForward; 79 88 /** source way index for each source segment */ 80 89 private final int[] segWay; 90 /** whether the path also runs along each source segment in the opposite direction (out and back) */ 91 private final boolean[] retraced; 81 92 82 93 // Spatial index of the source segments (rebuilt for every offset, since the cell size depends on it) 83 94 private double gridCell; … … public class ParallelWays { 89 100 private int[] gridStamp; 90 101 private int gridQuery; 91 102 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; 103 // Result of the last changeOffset call, longest path first 104 private List<OffsetPath> results = Collections.emptyList(); 97 105 98 106 private List<Way> ways = Collections.emptyList(); 99 107 108 /** One connected path of the result */ 109 private static final class OffsetPath { 110 /** the points; for a ring the first point is not repeated at the end */ 111 final List<EastNorth> pts; 112 /** for each piece (from point i to point i + 1, wrapping around for a ring): the source segment */ 113 final int[] pieceSeg; 114 /** for each point: the source node it was derived from, or {@link #NO_NODE} */ 115 final int[] pointNode; 116 /** for each point: an identifier shared by the points of different paths which are the same point */ 117 final int[] pointId; 118 final boolean closed; 119 120 OffsetPath(List<EastNorth> pts, int[] pieceSeg, int[] pointNode, int[] pointId, boolean closed) { 121 this.pts = pts; 122 this.pieceSeg = pieceSeg; 123 this.pointNode = pointNode; 124 this.pointId = pointId; 125 this.closed = closed; 126 } 127 } 128 100 129 /** 101 130 * Constructs a new {@code ParallelWays}. 102 131 * @param sourceWays source ways … … public class ParallelWays { 221 250 segWay[i] = w; 222 251 } 223 252 } 253 retraced = findRetracedSegments(); 254 } 255 256 /** 257 * Finds the segments which the path also runs along in the opposite direction, as in a - b - c - b - d. 258 * @return for each segment, whether it is retraced 259 */ 260 private boolean[] findRetracedSegments() { 261 int segCount = nodeCount - 1; 262 Set<List<Double>> segments = new HashSet<>(segCount * 2); 263 for (int i = 0; i < segCount; i++) { 264 segments.add(Arrays.asList(px[i], py[i], px[i + 1], py[i + 1])); 265 } 266 boolean[] result = new boolean[segCount]; 267 for (int i = 0; i < segCount; i++) { 268 result[i] = segments.contains(Arrays.asList(px[i + 1], py[i + 1], px[i], py[i])); 269 } 270 return result; 224 271 } 225 272 226 273 /** … … public class ParallelWays { 341 388 if (d == 0 || Double.isNaN(d)) { 342 389 // no offset: the result is a copy (for a ring without the repeated closing node) 343 390 int pointCount = closed ? nodeCount - 1 : nodeCount; 344 resultPts = new ArrayList<>(pointCount);391 List<EastNorth> pts = new ArrayList<>(pointCount); 345 392 for (int i = 0; i < pointCount; i++) { 346 resultPts.add(new EastNorth(px[i], py[i]));393 pts.add(new EastNorth(px[i], py[i])); 347 394 } 348 resultPieceSeg = IntStream.range(0, nodeCount - 1).toArray();349 result PointNode = IntStream.range(0, pointCount).toArray();350 resultClosed = closed;395 int[] indices = IntStream.range(0, pointCount).toArray(); 396 results = Collections.singletonList( 397 new OffsetPath(pts, IntStream.range(0, nodeCount - 1).toArray(), indices, indices, closed)); 351 398 return; 352 399 } 353 400 buildGrid(Math.abs(d)); … … public class ParallelWays { 720 767 chain.append(pieces.end[p], srcSeg, points); 721 768 } 722 769 // 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); 770 // (each chain is extended as far as it goes before moving on: starts never reappear, so there is no need to 771 // look at a chain again) 772 for (int i = 0; i < chains.size(); i++) { 773 Chain a = chains.get(i); 774 boolean merged = true; 775 while (merged) { 776 merged = false; 728 777 for (int j = 0; j < chains.size(); j++) { 729 778 Chain b = chains.get(j); 730 779 int bFirst = b.ids.get(0); … … public class ParallelWays { 734 783 } 735 784 a.appendChain(b); 736 785 chains.remove(j); 786 if (j < i) { 787 i--; 788 } 737 789 merged = true; 738 790 break; 739 791 } 740 792 } 741 793 } 742 794 } 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; 795 // A chain made only of retraced segments goes around an out-and-back excursion of the path: drop it, unless 796 // nothing else is left (the whole path goes out and back) 797 if (chains.stream().anyMatch(ch -> !isRetracedOnly(ch))) { 798 chains.removeIf(this::isRetracedOnly); 799 } 800 chains.removeIf(ch -> ch.ids.size() < 2); 801 chains.sort((a, b) -> Double.compare(b.length, a.length)); 802 List<OffsetPath> paths = new ArrayList<>(chains.size()); 803 for (Chain chain : chains) { 804 if (chain.ids.size() > 2 && chain.lastId != chain.ids.get(0) 805 && points.distance(chain.lastId, chain.ids.get(0)) <= bridgeTolerance) { 806 // close a ring which is open by a small gap only (also for an open path, around e.g. a pocket of sea 807 // which is further than the offset from the coast); a piece which is merely short is not a ring 808 chain.append(chain.ids.get(0), chain.segs.get(chain.segs.size() - 1), points); 751 809 } 810 paths.add(toOffsetPath(chain, raw, points, r)); 752 811 } 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 } 812 results = paths; 813 } 814 815 /** 816 * Determines if a chain is made only of retraced segments, see {@link #findRetracedSegments()}. 817 * @param chain the chain 818 * @return {@code true} if every piece of the chain is the offset of a retraced segment 819 */ 820 private boolean isRetracedOnly(Chain chain) { 821 return chain.segs.stream().allMatch(seg -> seg >= 0 && retraced[seg]); 822 } 757 823 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); 824 /** 825 * Converts a chain into a path of the result. 826 * @param chain the chain 827 * @param raw the raw polyline 828 * @param points the point table 829 * @param r the offset 830 * @return the path 831 */ 832 private OffsetPath toOffsetPath(Chain chain, RawPolyline raw, Points points, double r) { 833 boolean isRing = chain.ids.size() > 2 && chain.ids.get(0) == chain.lastId; 834 List<Integer> ids = new ArrayList<>(chain.ids); 835 List<Integer> segs = new ArrayList<>(chain.segs); 836 replaceIntactArcsByMitres(raw, points, ids, segs, r); 763 837 if (isRing) { 764 838 ids.remove(ids.size() - 1); 765 839 // rotate so that the ring starts at a way boundary … … public class ParallelWays { 774 848 Collections.rotate(ids, -rot); 775 849 Collections.rotate(segs, -rot); 776 850 } 777 resultPts = new ArrayList<>(ids.size()); 778 resultPointNode = new int[ids.size()]; 851 List<EastNorth> pts = new ArrayList<>(ids.size()); 852 int[] pointNode = new int[ids.size()]; 853 int[] pointId = new int[ids.size()]; 779 854 for (int i = 0; i < ids.size(); i++) { 780 855 int id = ids.get(i); 781 856 double[] pt = points.get(id); 782 resultPts.add(new EastNorth(pt[0], pt[1])); 783 resultPointNode[i] = points.node.get(id); 857 pts.add(new EastNorth(pt[0], pt[1])); 858 pointNode[i] = points.node.get(id); 859 pointId[i] = id; 784 860 } 785 resultPieceSeg = segs.stream().mapToInt(Integer::intValue).toArray(); 786 resultClosed = isRing; 861 return new OffsetPath(pts, segs.stream().mapToInt(Integer::intValue).toArray(), pointNode, pointId, isRing); 787 862 } 788 863 789 864 private void setEmptyResult() { 790 resultPts = Collections.emptyList(); 791 resultPieceSeg = new int[0]; 792 resultPointNode = new int[0]; 793 resultClosed = false; 865 results = Collections.emptyList(); 794 866 } 795 867 796 868 /** 797 869 * 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 is799 * located and the piece is split there.870 * piece(s). The validity is sampled at both ends and in between, at most {@link #SAMPLE_SPACING} times the offset 871 * apart; wherever it changes, the transition point is located and the piece is split there. 800 872 */ 801 873 private void classify(RawPolyline raw, double r, Points points, Pieces pieces, int i, int sId, int eId) { 802 874 int endPoint = (i + 1) % raw.size; … … public class ParallelWays { 808 880 double[] s = points.get(sId); 809 881 double[] e = points.get(eId); 810 882 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 } 883 double len = Math.hypot(e[0] - s[0], e[1] - s[1]); 884 // A piece may pass within r of another part of the path somewhere in its middle (e.g. where the path crosses 885 // itself), so sampling its ends and middle is not enough for a long piece. With samples r / 8 apart, only a 886 // stretch which grazes the r-circle by less than about 0.2 % of r can be missed. 887 int intervals = Math.max(2, (int) Math.ceil(len / (r * SAMPLE_SPACING))); 888 // For many samples, gather the source segments near the piece once instead of searching for each sample 889 int[] nearby = intervals == 2 ? null : nearbySegments(Math.min(s[0], e[0]), Math.min(s[1], e[1]), 890 Math.max(s[0], e[0]), Math.max(s[1], e[1]), r); 818 891 // 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.892 // chords cross lies inside both circles): the validity of the interior is then used up to the end point. 820 893 double snapTolerance = 2 * r * (1 - Math.cos(arcStep / 2)) + tolerance; 821 double len = Math.hypot(e[0] - s[0], e[1] - s[1]);822 894 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; 828 } else { 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; 895 boolean state = isValid(s[0], s[1], r, tolerance, arc, nearby); 896 boolean sampled = state; 897 double tPrev = 0; 898 for (int k = 1; k <= intervals; k++) { 899 double t = (double) k / intervals; 900 boolean v = isValid(s[0] + (e[0] - s[0]) * t, s[1] + (e[1] - s[1]) * t, r, tolerance, arc, nearby); 901 if (v != sampled) { 902 double tt = locateTransition(s, e, tPrev, t, r, tolerance, arc, nearby); 903 if (tt * len <= snapTolerance) { 904 state = v; 905 } else if ((1 - tt) * len > snapTolerance) { 906 int id = points.add(s[0] + (e[0] - s[0]) * tt, s[1] + (e[1] - s[1]) * tt, NO_NODE); 907 pieces.add(prev, id, i, state); 908 prev = id; 909 state = v; 910 } 911 sampled = v; 842 912 } 913 tPrev = t; 843 914 } 844 915 pieces.add(prev, eId, i, state); 845 916 } … … public class ParallelWays { 848 919 * Locates (by bisection) the parameter between {@code a} and {@code b} where the validity changes. 849 920 * @return the parameter of the transition 850 921 */ 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); 922 private double locateTransition(double[] s, double[] e, double a, double b, double r, double tolerance, int arc, 923 int[] nearby) { 924 boolean va = isValid(s[0] + (e[0] - s[0]) * a, s[1] + (e[1] - s[1]) * a, r, tolerance, arc, nearby); 853 925 for (int it = 0; it < 40 && b - a > 1e-12; it++) { 854 926 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 );927 boolean vmid = isValid(s[0] + (e[0] - s[0]) * mid, s[1] + (e[1] - s[1]) * mid, r, tolerance, arc, nearby); 856 928 if (vmid == va) { 857 929 a = mid; 858 930 } else { … … public class ParallelWays { 870 942 * @param tolerance allowed deficit 871 943 * @param arc if the point lies on an arc chord: the center node of the arc; the point is then projected onto 872 944 * the arc before testing, else {@link #NO_NODE} 945 * @param nearby the source segments which can come within r of the point, or {@code null} to search them all 873 946 * @return {@code true} if the point is valid 874 947 */ 875 private boolean isValid(double x, double y, double r, double tolerance, int arc ) {948 private boolean isValid(double x, double y, double r, double tolerance, int arc, int[] nearby) { 876 949 if (arc != NO_NODE) { 877 950 double vx = x - px[arc]; 878 951 double vy = y - py[arc]; … … public class ParallelWays { 882 955 y = py[arc] + vy / vl * r; 883 956 } 884 957 } 885 return distanceToSource(x, y, r - tolerance) >= r - tolerance; 958 double dist = nearby == null ? distanceToSource(x, y, r - tolerance) : distanceToSegments(nearby, x, y, r - tolerance); 959 return dist >= r - tolerance; 886 960 } 887 961 888 962 /** 889 * Replaces arcs which are completely part of the result by a mitre (where the corner is gentle enough). 963 * Replaces arcs which are completely part of the result by a mitre (where the corner is gentle enough, and the 964 * mitre keeps the distance to the rest of the path). 890 965 */ 891 private static void replaceIntactArcsByMitres(RawPolyline raw, Points points, List<Integer> ids, List<Integer> segs) {966 private void replaceIntactArcsByMitres(RawPolyline raw, Points points, List<Integer> ids, List<Integer> segs, double r) { 892 967 int[] arcOfRawPoint = new int[raw.size]; 893 968 Arrays.fill(arcOfRawPoint, NO_NODE); 894 969 for (int k = 0; k < raw.arcStart.length; k++) { … … public class ParallelWays { 910 985 for (int j = 1; j <= chords && intact; j++) { 911 986 intact = ids.get(i + j) == id + j; 912 987 } 913 if (!intact ) {988 if (!intact || !isValidMitre(raw, k, r)) { 914 989 continue; 915 990 } 916 991 int mitreId = points.add(raw.mitreX[k], raw.mitreY[k], k); … … public class ParallelWays { 923 998 } 924 999 } 925 1000 1001 /** 1002 * Checks whether the mitre at node {@code k} keeps the offset distance to the source path. It sticks out beyond 1003 * the arc it replaces, so it may come too close to another part of the path even though the arc does not. 1004 * @param raw the raw polyline 1005 * @param k the source node of the arc 1006 * @param r the offset 1007 * @return {@code true} if both legs of the mitre (from the ends of the arc to the mitre point) are valid 1008 */ 1009 private boolean isValidMitre(RawPolyline raw, int k, double r) { 1010 double tolerance = r * 1e-7 + 1e-7; 1011 for (int end : new int[] {raw.arcStart[k], raw.arcStart[k] + raw.arcChords[k]}) { 1012 double dx = raw.mitreX[k] - raw.x[end]; 1013 double dy = raw.mitreY[k] - raw.y[end]; 1014 int intervals = Math.max(1, (int) Math.ceil(Math.hypot(dx, dy) / (r * SAMPLE_SPACING))); 1015 for (int j = 1; j <= intervals; j++) { 1016 double t = (double) j / intervals; 1017 if (!isValid(raw.x[end] + dx * t, raw.y[end] + dy * t, r, tolerance, NO_NODE, null)) { 1018 return false; 1019 } 1020 } 1021 } 1022 return true; 1023 } 1024 926 1025 /** 927 1026 * Finds the chain ending at (or within the tolerance of) the given point, preferring an exact match. 928 1027 * @return the chain, or {@code null} … … public class ParallelWays { 1104 1203 if (x < segMinX[i] - best || x > segMaxX[i] + best || y < segMinY[i] - best || y > segMaxY[i] + best) { 1105 1204 continue; 1106 1205 } 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 } 1206 double dSq = segmentDistanceSq(i, x, y); 1121 1207 if (dSq < bestSq) { 1122 1208 bestSq = dSq; 1123 1209 best = Math.sqrt(dSq); … … public class ParallelWays { 1131 1217 return best; 1132 1218 } 1133 1219 1220 /** 1221 * Distance from a point to some of the source segments. 1222 * @param segments indices of the segments 1223 * @param x point 1224 * @param y point 1225 * @param cutoff the search can stop as soon as a distance below this value is found 1226 * @return the distance (or any value below cutoff if such a distance exists) 1227 */ 1228 private double distanceToSegments(int[] segments, double x, double y, double cutoff) { 1229 double bestSq = Double.POSITIVE_INFINITY; 1230 double cutoffSq = cutoff * cutoff; 1231 for (int i : segments) { 1232 bestSq = Math.min(bestSq, segmentDistanceSq(i, x, y)); 1233 if (bestSq < cutoffSq) { 1234 break; 1235 } 1236 } 1237 return Math.sqrt(bestSq); 1238 } 1239 1240 /** 1241 * Squared distance from a point to a source segment. 1242 * @param i segment index 1243 * @param x point 1244 * @param y point 1245 * @return the squared distance 1246 */ 1247 private double segmentDistanceSq(int i, double x, double y) { 1248 double rx = x - px[i]; 1249 double ry = y - py[i]; 1250 double t = rx * dirX[i] + ry * dirY[i]; 1251 if (t <= 0) { 1252 return rx * rx + ry * ry; 1253 } else if (t >= segLen[i]) { 1254 double ex = x - px[i + 1]; 1255 double ey = y - py[i + 1]; 1256 return ex * ex + ey * ey; 1257 } 1258 double c = rx * dirY[i] - ry * dirX[i]; 1259 return c * c; 1260 } 1261 1262 /** 1263 * Finds the source segments which may come within the given distance of an area. 1264 * @param minX area 1265 * @param minY area 1266 * @param maxX area 1267 * @param maxY area 1268 * @param r the distance 1269 * @return the indices of the segments whose bounding box comes within r of the area 1270 */ 1271 private int[] nearbySegments(double minX, double minY, double maxX, double maxY, double r) { 1272 gridQuery++; 1273 List<Integer> found = new ArrayList<>(); 1274 forEachCell(minX - r, minY - r, maxX + r, maxY + r, c -> { 1275 for (int i : gridCells[c]) { 1276 if (gridStamp[i] != gridQuery) { 1277 gridStamp[i] = gridQuery; 1278 if (segMaxX[i] >= minX - r && segMinX[i] <= maxX + r && segMaxY[i] >= minY - r && segMinY[i] <= maxY + r) { 1279 found.add(i); 1280 } 1281 } 1282 } 1283 }); 1284 return found.stream().mapToInt(Integer::intValue).toArray(); 1285 } 1286 1134 1287 // --------------------------------------------------------------------------------------------------------- 1135 1288 // Result access and commit 1136 1289 1137 1290 /** 1138 * Returns the points of the offset path computed by the last call to {@link #changeOffset(double)}.1291 * Returns the points of the longest offset path computed by the last call to {@link #changeOffset(double)}. 1139 1292 * For a closed result the first point is not repeated at the end, see {@link #isResultClosed()}. 1293 * Use {@link #getOffsetPaths()} for all of them. 1140 1294 * @return the offset points (projected coordinates), empty if nothing has been computed or nothing remains 1141 1295 * @since 19624 1142 1296 */ 1143 1297 public List<EastNorth> getOffsetPoints() { 1144 return Collections.unmodifiableList(resultPts);1298 return results.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(results.get(0).pts); 1145 1299 } 1146 1300 1147 1301 /** 1148 * Determines if the resultof the last {@link #changeOffset(double)} call is a closed ring.1302 * Determines if the longest offset path of the last {@link #changeOffset(double)} call is a closed ring. 1149 1303 * @return {@code true} if the offset path is a closed ring 1150 1304 * @since 19624 1151 1305 */ 1152 1306 public boolean isResultClosed() { 1153 return resultClosed; 1307 return !results.isEmpty() && results.get(0).closed; 1308 } 1309 1310 /** 1311 * Returns all offset paths computed by the last call to {@link #changeOffset(double)}, longest first. There is 1312 * more than one where the source path crosses itself or comes back within twice the offset of itself. A closed 1313 * path (ring) repeats its first point at the end. 1314 * @return the offset paths (projected coordinates), empty if nothing has been computed or nothing remains 1315 * @since xxx 1316 */ 1317 public List<List<EastNorth>> getOffsetPaths() { 1318 List<List<EastNorth>> paths = new ArrayList<>(results.size()); 1319 for (OffsetPath path : results) { 1320 List<EastNorth> pts = new ArrayList<>(path.pts); 1321 if (path.closed && !pts.isEmpty()) { 1322 pts.add(pts.get(0)); 1323 } 1324 paths.add(Collections.unmodifiableList(pts)); 1325 } 1326 return Collections.unmodifiableList(paths); 1154 1327 } 1155 1328 1156 1329 /** … … public class ParallelWays { 1191 1364 } 1192 1365 1193 1366 /** 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 omitted1367 * Builds the (not yet added) ways from the last computed offset paths. 1368 * @return the ways, per offset path in the order of the source ways; ways swallowed by the offset are omitted 1196 1369 */ 1197 1370 private List<Way> buildWays() { 1198 int pointCount = resultPts.size();1199 1371 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; 1372 // paths which meet at a common point share its node 1373 Map<Integer, Node> nodes = new HashMap<>(); 1374 for (OffsetPath path : results) { 1375 int pointCount = path.pts.size(); 1376 if (pointCount < 2) { 1377 continue; 1378 } 1379 int pieceCount = path.pieceSeg.length; 1380 for (int w = 0; w < sourceWays.size(); w++) { 1381 int first = -1; 1382 int last = -1; 1383 for (int p = 0; p < pieceCount; p++) { 1384 if (segWay[path.pieceSeg[p]] == w) { 1385 if (first < 0) { 1386 first = p; 1387 } 1388 last = p; 1212 1389 } 1213 last = p;1214 1390 } 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); 1391 if (first < 0) { 1392 continue; // way is swallowed by the offset 1224 1393 } 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()); 1394 List<Node> wayNodes = new ArrayList<>(last - first + 2); 1395 for (int p = first; p <= last + 1; p++) { 1396 int idx = p % pointCount; 1397 wayNodes.add(nodes.computeIfAbsent(path.pointId[idx], id -> makeNode(path, idx))); 1398 } 1399 if (!wayForward[w]) { 1400 Collections.reverse(wayNodes); 1401 } 1402 Way source = sourceWays.get(w); 1403 Way copy = new Way(); 1404 copy.setNodes(wayNodes); 1405 if (copyTags) { 1406 copy.setKeys(source.getKeys()); 1407 } 1408 result.add(copy); 1235 1409 } 1236 result.add(copy);1237 1410 } 1238 1411 return result; 1239 1412 } 1240 1413 1241 private Node makeNode( int idx) {1414 private Node makeNode(OffsetPath path, int idx) { 1242 1415 Node n; 1243 int src = resultPointNode[idx];1416 int src = path.pointNode[idx]; 1244 1417 if (copyTags && src != NO_NODE) { 1245 1418 n = new Node(sortedNodes.get(src), true); 1246 1419 } else { 1247 1420 n = new Node(); 1248 1421 } 1249 n.setEastNorth( resultPts.get(idx));1422 n.setEastNorth(path.pts.get(idx)); 1250 1423 return n; 1251 1424 } 1252 1425 -
test/unit/org/openstreetmap/josm/actions/mapmode/ParallelWaysTest.java
diff --git test/unit/org/openstreetmap/josm/actions/mapmode/ParallelWaysTest.java test/unit/org/openstreetmap/josm/actions/mapmode/ParallelWaysTest.java index de8a663142..11e66b1a13 100644
import static org.junit.jupiter.api.Assertions.assertSame; 8 8 import static org.junit.jupiter.api.Assertions.assertTrue; 9 9 10 10 import java.util.Arrays; 11 import java.util.Collection; 12 import java.util.Collections; 11 13 import java.util.List; 12 14 import java.util.Random; 13 15 … … class ParallelWaysTest { 70 72 return best; 71 73 } 72 74 75 private static double distanceToWays(Collection<Way> ways, EastNorth p) { 76 return ways.stream().mapToDouble(w -> distanceToWay(w, p)).min().orElse(Double.POSITIVE_INFINITY); 77 } 78 73 79 private static double maxSegmentLength(Way w) { 74 80 double max = 0; 75 81 for (int i = 0; i < w.getNodesCount() - 1; i++) { … … class ParallelWaysTest { 115 121 } 116 122 } 117 123 124 /** 125 * Checks that no point of the parallel comes closer to the source than the offset (less the tolerance of the 126 * chord approximation of arcs). Unlike {@link #assertVertexDistances} this also samples the segments, which may 127 * pass another part of the path between two vertices. 128 * @param source the source ways 129 * @param pts the parallel 130 * @param r the (absolute) offset 131 * @param spacing the maximal distance between two samples 132 */ 133 private static void assertSegmentDistances(Collection<Way> source, List<EastNorth> pts, double r, double spacing) { 134 double minDist = r * (1 - 2 * (1 - Math.cos(Math.toRadians(ParallelWays.DEFAULT_ARC_STEP_DEGREES / 2)))) * (1 - 1e-6); 135 for (int i = 0; i < pts.size() - 1; i++) { 136 EastNorth a = pts.get(i); 137 EastNorth b = pts.get(i + 1); 138 int n = Math.max(1, (int) Math.ceil(a.distance(b) / spacing)); 139 for (int k = 0; k <= n; k++) { 140 EastNorth p = a.interpolate(b, (double) k / n); 141 double dist = distanceToWays(source, p); 142 assertTrue(dist >= minDist, "point " + p + " of segment " + i + " too close to source: " + dist + " < " + r); 143 } 144 } 145 } 146 147 private static void assertPoints(List<EastNorth> actual, double... xy) { 148 assertEquals(xy.length / 2, actual.size(), actual.toString()); 149 for (int i = 0; i < actual.size(); i++) { 150 // points where the validity changes are located to within the tolerance of the validity test 151 assertTrue(actual.get(i).equalsEpsilon(new EastNorth(xy[2 * i], xy[2 * i + 1]), 1e-4), i + ": " + actual); 152 } 153 } 154 118 155 /** 119 156 * A small offset of a simple way must keep the node count 120 157 */ … … class ParallelWaysTest { 234 271 long millis = (System.nanoTime() - start) / 1_000_000; 235 272 List<EastNorth> pts = pw.getOffsetPoints(); 236 273 assertTrue(pts.size() >= 2, "d=" + d); 237 assertVertexDistances(coast, pts, Math.abs(d)); 274 for (List<EastNorth> path : pw.getOffsetPaths()) { 275 assertTrue(path.size() >= 2, "d=" + d + ": degenerate path " + path); 276 assertVertexDistances(coast, path, Math.abs(d)); 277 assertSegmentDistances(Collections.singletonList(coast), path, Math.abs(d), Math.abs(d) / 2); 278 } 238 279 assertTrue(millis < 5000, "offset took " + millis + " ms"); 239 280 System.out.println("ParallelWays: " + count + " nodes, d=" + d + " -> " + pts.size() + " points in " + millis + " ms"); 240 281 } … … class ParallelWaysTest { 254 295 List<EastNorth> pts = pw.getOffsetPoints(); 255 296 double r = Math.abs(d); 256 297 assertVertexDistances(w, pts, r); 298 // the offset of the excursion itself on the far side (a loop around it) is not wanted 299 assertEquals(1, pw.getOffsetPaths().size(), "d=" + d + ": " + pw.getOffsetPaths()); 257 300 assertEquals(new EastNorth(0, d), pts.get(0), "d=" + d); 258 301 assertEquals(new EastNorth(2000, d), pts.get(pts.size() - 1), "d=" + d); 259 302 // every vertex of the main path has the parallel within reach … … class ParallelWaysTest { 276 319 } 277 320 } 278 321 322 /** 323 * A path which only goes out and back along the same nodes still has a parallel, although all of it is the 324 * offset of retraced segments. 325 */ 326 @Test 327 void testRetracedOnly() { 328 Way w = way(0, 0, 500, 0, 1000, 0, 500, 0, 0, 0); 329 ParallelWays pw = new ParallelWays(Arrays.asList(w), false, 0); 330 pw.changeOffset(50); 331 assertFalse(pw.getOffsetPaths().isEmpty()); 332 assertVertexDistances(w, pw.getOffsetPoints(), 50); 333 } 334 335 /** 336 * A path which crosses itself, like a road looping round and passing under its own approach: on both sides the 337 * parallel consists of separate paths, and all of them are kept, longest first. None of them may cross the road. 338 * Non-regression test for #24983, where only the longest one was kept. 339 */ 340 @Test 341 void testSelfCrossingLoop() { 342 // north along x = 0, a clockwise loop to the east, back west along y = 100 under the approach, then north 343 // along x = -300 344 Way w = way(0, -1000, 0, 300, 300, 300, 300, 100, -300, 100, -300, 1300); 345 List<Way> source = Collections.singletonList(w); 346 ParallelWays pw = new ParallelWays(source, false, 0); 347 348 pw.changeOffset(-50); // right: on the inside of the loop 349 List<List<EastNorth>> paths = pw.getOffsetPaths(); 350 assertEquals(3, paths.size(), paths.toString()); 351 // the westward leg and the road north, starting where the leg comes clear of the approach 352 assertPoints(paths.get(0), -50, 150, -250, 150, -250, 1300); 353 // the approach, up to where it passes under the westward leg 354 assertPoints(paths.get(1), 50, -1000, 50, 50); 355 // the ring inside the loop 356 assertPoints(paths.get(2), 50, 150, 50, 250, 250, 250, 250, 150, 50, 150); 357 assertEquals(paths.get(0), pw.getOffsetPoints()); 358 assertFalse(pw.isResultClosed()); 359 paths.forEach(path -> assertSegmentDistances(source, path, 50, 5)); 360 361 pw.changeOffset(50); // left: on the outside of the loop 362 paths = pw.getOffsetPaths(); 363 assertEquals(2, paths.size(), paths.toString()); 364 // the approach, then under the loop along the westward leg, and north 365 assertPoints(paths.get(0), -50, -1000, -50, 50, -350, 50, -350, 1300); 366 // around the outside of the loop, from where it comes clear of the approach until it comes back to it 367 assertPoints(paths.get(1), -50, 150, -50, 350, 350, 350, 350, 50, 50, 50); 368 paths.forEach(path -> assertSegmentDistances(source, path, 50, 5)); 369 } 370 371 /** 372 * A bay with a mouth narrower than twice the offset: the water further than the offset from the shore is an 373 * enclosed pocket, so its parallel is a ring, although the source path is open. At the mouth the ring passes the 374 * two ends of the path, whose round caps are never part of the result: the small gap they leave is closed. 375 */ 376 @Test 377 void testBayPocketRing() { 378 // counter clockwise around the bay (water on the left) with wavy shores, leaving a mouth at angle 0 379 int steps = 120; 380 double mouth = 0.15; 381 double[] xy = new double[2 * (steps + 1)]; 382 for (int i = 0; i <= steps; i++) { 383 double a = mouth + (2 * Math.PI - 2 * mouth) * i / steps; 384 double rad = 1000 + 150 * Math.cos(5 * a); 385 xy[2 * i] = rad * Math.cos(a); 386 xy[2 * i + 1] = rad * Math.sin(a); 387 } 388 Way w = way(xy); 389 ParallelWays pw = new ParallelWays(Arrays.asList(w), false, 0); 390 double r = 250; 391 pw.changeOffset(r); 392 List<List<EastNorth>> paths = pw.getOffsetPaths(); 393 assertFalse(paths.isEmpty()); 394 for (List<EastNorth> path : paths) { 395 // a closed path repeats its first point; an open one must not end just short of its start 396 double gap = path.get(0).distance(path.get(path.size() - 1)); 397 assertTrue(gap == 0 || gap > r, "path open by a gap of " + gap + ": " + path); 398 assertSegmentDistances(Collections.singletonList(w), path, r, r / 8); 399 } 400 assertTrue(paths.stream().anyMatch(path -> path.size() > 2 && path.get(0).equals(path.get(path.size() - 1))), paths.toString()); 401 } 402 403 /** 404 * The separate paths of a self-crossing path become separate ways; a ring becomes a closed way. 405 */ 406 @Test 407 void testCommitSelfCrossingLoop() { 408 DataSet ds = new DataSet(); 409 OsmDataLayer layer = new OsmDataLayer(ds, "ParallelWaysTest", null); 410 MainApplication.getLayerManager().addLayer(layer); 411 try { 412 Way w = way(0, -1000, 0, 300, 300, 300, 300, 100, -300, 100, -300, 1300); 413 w.put("highway", "service"); 414 List<Way> result = commitWays(ds, Collections.singletonList(w), 0, -50); 415 assertEquals(3, result.size(), result.toString()); 416 assertTrue(result.stream().allMatch(r -> "service".equals(r.get("highway")))); 417 assertFalse(result.get(0).isClosed()); 418 assertFalse(result.get(1).isClosed()); 419 assertTrue(result.get(2).isClosed()); 420 assertEquals(5, result.get(2).getNodesCount()); 421 } finally { 422 MainApplication.getLayerManager().removeLayer(layer); 423 } 424 } 425 279 426 /** 280 427 * The closest point of the path relates the offset to the segment next to the mouse 281 428 */
