|
| 1 | +/* |
| 2 | + * Copyright (c) 2026 grootstebozewolf |
| 3 | + * |
| 4 | + * All rights reserved. This program and the accompanying materials |
| 5 | + * are made available under the terms of the Eclipse Public License 2.0 |
| 6 | + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. |
| 7 | + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html |
| 8 | + * and the Eclipse Distribution License is available at |
| 9 | + * |
| 10 | + * http://www.eclipse.org/org/documents/edl-v10.php. |
| 11 | + */ |
| 12 | +package org.locationtech.jts.geom.curved; |
| 13 | + |
| 14 | +import java.util.ArrayList; |
| 15 | +import java.util.Comparator; |
| 16 | +import java.util.HashMap; |
| 17 | +import java.util.List; |
| 18 | +import java.util.Map; |
| 19 | + |
| 20 | +import org.locationtech.jts.geom.Coordinate; |
| 21 | +import org.locationtech.jts.geom.CoordinateSequence; |
| 22 | +import org.locationtech.jts.geom.impl.CoordinateArraySequence; |
| 23 | + |
| 24 | +/** |
| 25 | + * An arc-aware analogue of {@code NodedSegmentString} for arc-aware noding (N-SS, |
| 26 | + * JTS #1195). It carries a control-point sequence read as consecutive arc pieces |
| 27 | + * {@code (p[2i], p[2i+1], p[2i+2])} (a collinear triple is a straight chord) plus |
| 28 | + * an opaque {@code data} context, records intersection nodes per arc piece, and |
| 29 | + * splits itself at those nodes into noded sub-strings. |
| 30 | + * <p> |
| 31 | + * Splitting is arc-aware: an arc cut at a crossing yields sub-arcs on the same |
| 32 | + * circle (each mid point recomputed at the sub-arc's angular midpoint), and a |
| 33 | + * sub-string spans the original arc joints (which are not nodes), breaking only |
| 34 | + * at the recorded crossings — the same rule as linear {@code NodedSegmentString}. |
| 35 | + * The crossings come from the oracle-pinned {@link CircularArcs} primitives. |
| 36 | + */ |
| 37 | +public final class ArcSegmentString { |
| 38 | + |
| 39 | + private static final double EPS = 1e-9; |
| 40 | + |
| 41 | + private final CoordinateSequence pts; |
| 42 | + private final Object data; |
| 43 | + private final Map<Integer, List<double[]>> nodes = new HashMap<Integer, List<double[]>>(); |
| 44 | + |
| 45 | + public ArcSegmentString(CoordinateSequence pts, Object data) { |
| 46 | + this.pts = pts; |
| 47 | + this.data = data; |
| 48 | + } |
| 49 | + |
| 50 | + public CoordinateSequence getCoordinateSequence() { return pts; } |
| 51 | + public Object getData() { return data; } |
| 52 | + |
| 53 | + /** Number of arc pieces (consecutive control-point triples). */ |
| 54 | + public int numArcs() { return Math.max(0, (pts.size() - 1) / 2); } |
| 55 | + |
| 56 | + /** The {@code i}-th arc piece as {sx,sy,mx,my,ex,ey}. */ |
| 57 | + double[] arc(int i) { |
| 58 | + int b = 2 * i; |
| 59 | + return new double[]{ pts.getX(b), pts.getY(b), pts.getX(b+1), pts.getY(b+1), pts.getX(b+2), pts.getY(b+2) }; |
| 60 | + } |
| 61 | + |
| 62 | + /** |
| 63 | + * Records an intersection node on arc piece {@code arcIndex}. A point coincident |
| 64 | + * with the arc's endpoints (a joint, not an interior crossing) or with an already |
| 65 | + * recorded node is ignored, so no zero-length sub-arc is produced. |
| 66 | + */ |
| 67 | + void addNode(int arcIndex, double x, double y) { |
| 68 | + double[] a = arc(arcIndex); |
| 69 | + if (near(x, y, a[0], a[1]) || near(x, y, a[4], a[5])) return; // at an endpoint joint |
| 70 | + List<double[]> list = nodes.get(arcIndex); |
| 71 | + if (list == null) { list = new ArrayList<double[]>(); nodes.put(arcIndex, list); } |
| 72 | + for (double[] q : list) if (near(x, y, q[0], q[1])) return; // duplicate |
| 73 | + list.add(new double[]{ x, y }); |
| 74 | + } |
| 75 | + |
| 76 | + /** True iff any node has been recorded on any arc. */ |
| 77 | + boolean hasNodes() { return !nodes.isEmpty(); } |
| 78 | + |
| 79 | + /** |
| 80 | + * Splits this string at its recorded nodes into noded sub-strings, each carrying |
| 81 | + * the same {@code data}. With no nodes, returns this string unchanged. |
| 82 | + */ |
| 83 | + public List<ArcSegmentString> getNodedSubstrings() { |
| 84 | + List<ArcSegmentString> out = new ArrayList<ArcSegmentString>(); |
| 85 | + if (!hasNodes()) { out.add(this); return out; } |
| 86 | + List<Coordinate> cur = new ArrayList<Coordinate>(); |
| 87 | + int nA = numArcs(); |
| 88 | + for (int i = 0; i < nA; i++) { |
| 89 | + double[] a = arc(i); |
| 90 | + double[] c = circle(a); // null if collinear (chord) |
| 91 | + List<double[]> breaks = breakPoints(a, c, nodes.get(i)); |
| 92 | + for (int j = 0; j + 1 < breaks.size(); j++) { |
| 93 | + double[] p = breaks.get(j), q = breaks.get(j + 1); |
| 94 | + double mx, my; |
| 95 | + if (c != null) { double[] m = arcMid(c, p, q); mx = m[0]; my = m[1]; } |
| 96 | + else { mx = 0.5 * (p[0] + q[0]); my = 0.5 * (p[1] + q[1]); } |
| 97 | + if (cur.isEmpty()) cur.add(new Coordinate(p[0], p[1])); |
| 98 | + cur.add(new Coordinate(mx, my)); |
| 99 | + cur.add(new Coordinate(q[0], q[1])); |
| 100 | + boolean endIsNode = (j + 1 < breaks.size() - 1); // q is an interior crossing |
| 101 | + if (endIsNode) { out.add(build(cur)); cur = new ArrayList<Coordinate>(); } |
| 102 | + } |
| 103 | + } |
| 104 | + if (!cur.isEmpty()) out.add(build(cur)); |
| 105 | + return out; |
| 106 | + } |
| 107 | + |
| 108 | + /** Ordered break points along one arc: start, interior nodes (by sweep), end. */ |
| 109 | + private static List<double[]> breakPoints(double[] a, double[] c, List<double[]> ns) { |
| 110 | + List<double[]> bp = new ArrayList<double[]>(); |
| 111 | + bp.add(new double[]{ a[0], a[1] }); |
| 112 | + if (ns != null && !ns.isEmpty()) { |
| 113 | + List<double[]> sorted = new ArrayList<double[]>(ns); |
| 114 | + if (c != null) { |
| 115 | + final double[] cc = c; |
| 116 | + sorted.sort(new Comparator<double[]>() { |
| 117 | + public int compare(double[] p, double[] q) { |
| 118 | + return Double.compare(sweepFrac(cc, p), sweepFrac(cc, q)); |
| 119 | + } |
| 120 | + }); |
| 121 | + } else { |
| 122 | + final double[] aa = a; |
| 123 | + sorted.sort(new Comparator<double[]>() { |
| 124 | + public int compare(double[] p, double[] q) { |
| 125 | + return Double.compare(d2(aa[0],aa[1],p[0],p[1]), d2(aa[0],aa[1],q[0],q[1])); |
| 126 | + } |
| 127 | + }); |
| 128 | + } |
| 129 | + bp.addAll(sorted); |
| 130 | + } |
| 131 | + bp.add(new double[]{ a[4], a[5] }); |
| 132 | + return bp; |
| 133 | + } |
| 134 | + |
| 135 | + private ArcSegmentString build(List<Coordinate> coords) { |
| 136 | + return new ArcSegmentString(new CoordinateArraySequence(coords.toArray(new Coordinate[0])), data); |
| 137 | + } |
| 138 | + |
| 139 | + // ---- intersection of two arc/chord pieces (delegates to the oracle-pinned primitives) ---- |
| 140 | + |
| 141 | + static double[][] intersectPieces(double[] a, double[] b) { |
| 142 | + boolean aArc = isArc(a), bArc = isArc(b); |
| 143 | + if (aArc && bArc) { |
| 144 | + return CircularArcs.intersectArc(a[0],a[1],a[2],a[3],a[4],a[5], b[0],b[1],b[2],b[3],b[4],b[5]); |
| 145 | + } |
| 146 | + if (aArc) return CircularArcs.intersectSegment(a[0],a[1],a[2],a[3],a[4],a[5], b[0],b[1], b[4],b[5]); |
| 147 | + if (bArc) return CircularArcs.intersectSegment(b[0],b[1],b[2],b[3],b[4],b[5], a[0],a[1], a[4],a[5]); |
| 148 | + double[] p = segSeg(a[0],a[1],a[4],a[5], b[0],b[1],b[4],b[5]); |
| 149 | + return (p == null) ? new double[0][] : new double[][]{ p }; |
| 150 | + } |
| 151 | + |
| 152 | + // ---- geometry helpers ---- |
| 153 | + |
| 154 | + static boolean isArc(double[] p) { |
| 155 | + return 2 * (p[0]*(p[3]-p[5]) + p[2]*(p[5]-p[1]) + p[4]*(p[1]-p[3])) != 0.0; |
| 156 | + } |
| 157 | + |
| 158 | + /** |
| 159 | + * Mid control point for the sub-arc of {@code arc} from {@code (px,py)} to |
| 160 | + * {@code (qx,qy)} (both on the arc), recomputed on the arc's circle at the |
| 161 | + * sub-arc's angular midpoint. Collinear (chord) pieces return the chord midpoint. |
| 162 | + */ |
| 163 | + static double[] midOnArc(double[] arc, double px, double py, double qx, double qy) { |
| 164 | + double[] c = circle(arc); |
| 165 | + if (c == null) return new double[]{ 0.5*(px+qx), 0.5*(py+qy) }; |
| 166 | + return arcMid(c, new double[]{ px, py }, new double[]{ qx, qy }); |
| 167 | + } |
| 168 | + |
| 169 | + /** {cx, cy, r, a0, signedSweep} of the arc, or null if collinear/degenerate. */ |
| 170 | + private static double[] circle(double[] p) { |
| 171 | + double sx=p[0],sy=p[1],mx=p[2],my=p[3],ex=p[4],ey=p[5]; |
| 172 | + double d = 2 * (sx*(my-ey) + mx*(ey-sy) + ex*(sy-my)); |
| 173 | + if (d == 0.0) return null; |
| 174 | + double s2=sx*sx+sy*sy, m2=mx*mx+my*my, e2=ex*ex+ey*ey; |
| 175 | + double cx=(s2*(my-ey)+m2*(ey-sy)+e2*(sy-my))/d; |
| 176 | + double cy=(s2*(ex-mx)+m2*(sx-ex)+e2*(mx-sx))/d; |
| 177 | + double r=Math.hypot(sx-cx,sy-cy); |
| 178 | + if (!Double.isFinite(r) || r==0.0) return null; |
| 179 | + double a0=Math.atan2(sy-cy,sx-cx), am=Math.atan2(my-cy,mx-cx), ae=Math.atan2(ey-cy,ex-cx); |
| 180 | + boolean ccw = d > 0; |
| 181 | + double theta = directedSweep(a0,am,ccw) + directedSweep(am,ae,ccw); |
| 182 | + return new double[]{ cx, cy, r, a0, ccw ? theta : -theta }; |
| 183 | + } |
| 184 | + |
| 185 | + private static double sweepFrac(double[] c, double[] p) { |
| 186 | + boolean ccw = c[4] >= 0; |
| 187 | + return directedSweep(c[3], Math.atan2(p[1]-c[1], p[0]-c[0]), ccw); |
| 188 | + } |
| 189 | + |
| 190 | + /** Point on the circle at the angular midpoint of the sub-arc from p to q (in the arc's direction). */ |
| 191 | + private static double[] arcMid(double[] c, double[] p, double[] q) { |
| 192 | + boolean ccw = c[4] >= 0; |
| 193 | + double ap = Math.atan2(p[1]-c[1], p[0]-c[0]); |
| 194 | + double sw = directedSweep(ap, Math.atan2(q[1]-c[1], q[0]-c[0]), ccw); |
| 195 | + double mid = ap + (ccw ? 1 : -1) * sw / 2; |
| 196 | + return new double[]{ c[0] + c[2]*Math.cos(mid), c[1] + c[2]*Math.sin(mid) }; |
| 197 | + } |
| 198 | + |
| 199 | + private static double directedSweep(double from, double to, boolean ccw) { |
| 200 | + double t = ccw ? (to - from) : (from - to); |
| 201 | + t %= 2 * Math.PI; |
| 202 | + if (t < 0) t += 2 * Math.PI; |
| 203 | + return t; |
| 204 | + } |
| 205 | + |
| 206 | + private static double[] segSeg(double x1,double y1,double x2,double y2, double x3,double y3,double x4,double y4) { |
| 207 | + double d = (x2-x1)*(y4-y3) - (y2-y1)*(x4-x3); |
| 208 | + if (Math.abs(d) < 1e-12) return null; |
| 209 | + double t = ((x3-x1)*(y4-y3) - (y3-y1)*(x4-x3)) / d; |
| 210 | + double u = ((x3-x1)*(y2-y1) - (y3-y1)*(x2-x1)) / d; |
| 211 | + if (t < -1e-9 || t > 1+1e-9 || u < -1e-9 || u > 1+1e-9) return null; |
| 212 | + return new double[]{ x1 + t*(x2-x1), y1 + t*(y2-y1) }; |
| 213 | + } |
| 214 | + |
| 215 | + private static boolean near(double x1,double y1,double x2,double y2) { return Math.hypot(x1-x2,y1-y2) <= EPS; } |
| 216 | + private static double d2(double x1,double y1,double x2,double y2) { double dx=x1-x2,dy=y1-y2; return dx*dx+dy*dy; } |
| 217 | +} |
0 commit comments