You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document and the companion red-test class modules/curve/src/test/java/org/locationtech/jts/spec/curveawareness/CurveAwarenessSpecTest.java
were largely AI-generated. The human contributor has reviewed and verified the
technical content (cross-module impact, risk register, phase dependencies, TAG
scope) for correctness. The AI-generated portions are made available under CC0-1.0 (public domain dedication) and are not subject to the project's
licence; human curation and edits are subject to the JTS dual licence.
SPDX-License-Identifier: (EPL-2.0 OR EDL-1.0) AND CC0-1.0
Assisted-by: xAI Grok (cursor grok-4.6)
Assisted-by: Claude (Opus-4.7)
JTS logo as curves plus a buffer halo. Not a closed-form buffer laser. Not a guide figure.
Status: Draft v6 (2026-08-16). Source:#7feature/sfa-curve-rgr @ b2861a08 — SQL-MM types, WKT, WKB 8–12, OverlayNGCurve, TestBuilder logo-as-curves. #8, #10, #11, #35, #42 (has #41), #43, #44, #45, #46, #14, #22, #31, #39 are on #7. Bar 2 / #38 / curve-rgr-bar2-rc0 / #27 stay off #7. Do not merge #7 to locationtech/jts master from a stacked-PR button. Origin (historical):feature/sfa-curve-buffer-spike — Draft v3 of this epic and the 49-method spec class. Audience: locationtech/jts maintainers and contributors.
Field contract (PERF-GATE): Maintainable → Reliable → Faster. Take the curve path only if t_laser ≤ 1.15 × t_chainsaw. The 15% slack is not to be loosened. The overlay is named OverlayNGCurve, not Curved.
Vocabulary. A TAG is a short, stable identifier for one unit of work, each with a matching red test in CurveAwarenessSpecTest. A kit is a closed-form implementation covering one specific pairing of geometries. A named miss is a pairing with no kit yet, given an identifier so it is tracked rather than silently linearised. A laser is the closed-form path; the chordsaw (written chainsaw in the PERF-GATE formula above — the two should be reconciled to one word) is the densify-then-operate fallback. Bar 2 is the follow-on delivery bar, the circular noder and arrangement work (OV-P2), tracked outside this epic.
1. Goal
Make JTS preserve the ISO/IEC 13249-3 (SQL/MM Part 3) curve, surface and solid geometries — CIRCULARSTRING, COMPOUNDCURVE, CURVEPOLYGON, MULTICURVE, MULTISURFACE, POLYHEDRALSURFACE, TIN, TRIANGLE, CIRCLE, GEODESICSTRING, ELLIPTICALCURVE, NURBSCURVE, CLOTHOID, SPIRALCURVE, COMPOUNDSURFACE and BREPSOLID — through every algorithm where the math is sound, instead of silently linearising to flat parents on the way in.
jts-curve is no longer only a parse-and-WKT/WKB stand-in: types exist, WKT round-trips without densify and WKB codes 8–12 do too, and OverlayNGCurve takes a closed form when one exists. Every circular pair that has a closed form has a kit. Everything else is a named miss and the chordsaw. We will not write a circular noder in this PR.jts-core still densifies on contact for everything that is not one of those kits. This epic tracks the lift, operation by operation — full TAG vs closed-form subset, not “good enough as linearised”.
2. Why
Performance. Each densification step expands a typical arc to many chord points (≥50 for a half-circle at 1% chord-tolerance); pipelines that buffer → simplify → union compound the cost.
Precision. Densify → operate → densify drifts control points by ULPs and breaks snap-to-grid round-trips against SQL/MM producers.
Interop. PostGIS, Oracle Spatial, NetTopologySuite emit and consume curve geometries; WKB 8–12 now reconstructs the curve and surface subclasses when the factory can. Algorithms that still linearise remain the interop gap.
Discoverability. TestBuilder can show the JTS wordmark as curves plus a buffer halo (hero). Open-arc / clothoid / compound buffer is still chords. Do not read that halo as a closed-form buffer laser.
3. Scope decisions
In scope (this epic):
Algorithms operating on the full ISO/IEC 13249-3 extended type set — the curve, surface and solid types — in 2-D and in their Z / M / ZM forms.
WKB type codes, as given by ISO/IEC 13249-3 §5.1.68, Table 15. The extended set is 8–12, 15–24 and 102, plus BrepSolid at 1025 (Z only — the standard defines no 2-D BrepSolid, and 1025 is 25 + the Z offset, so slot 25 is spoken for). Codes 13 and 14 are not used by 13249-3; OGC SFA reserves them for the abstract Curve and Surface types. Any range spanning 13 or 14 — including the "8–21" this document used to claim — is wrong.
Code
Type
Code
Type
8
CircularString
18
Circle
9
CompoundCurve
19
GeodesicString
10
CurvePolygon
20
EllipticalCurve
11
MultiCurve
21
NURBSCurve
12
MultiSurface
22
Clothoid
15
PolyhedralSurface
23
SpiralCurve
16
TIN
24
CompoundSurface
17
Triangle
102
AffinePlacement
Dimension offsets are +1000 (Z), +2000 (M), +3000 (ZM). Separately, and this is a different scheme rather than the dimension offset, Table 15 also allows 1000001–1000005 as alternate codes for CircularString through MultiSurface. The two do not compose: CircularString Z is 1008, not 1000001 + 1000. An earlier draft of this document asserted that the 1000-series was only the dimension offset; both schemes are real.
Landed so far: 8–12 (on rename to jts-io-common #8, now on include ora and sde modules #7). Core WKBReader takes those five the way GEOS does, delegating construction to GeometryFactory; CurveWKBWriter emits them through a hook placed before the instanceof chain. Core WKBWriter emits none of the extended codes unless that hook or subclass is used. Codes 15–24, 102 and 1025 are in scope and not started.
TestBuilder rendering and drawing tools.
In scope, sequenced late — the hard parts, named rather than hidden:
POLYHEDRALSURFACE (15), TIN (16) and BREPSOLID (1025). Structural and drawing support landed in the spike for visualisation; the 3-D semantics have not started.
ELLIPTICALCURVE (20). JTS has no ellipse model and adding one is the single largest piece of work here. It stays in scope because the epic tracks the whole standard, but nothing earlier should wait on it.
Out of scope:
Z / M ordinate interpolation across densified arcs. Today they propagate from the control points unchanged; every TAG that produces densified output should leave room for a future interpolation policy.
3.1 Proprietary and encumbered curve forms
SPIRALTYPE is an open value set. 4.2.12 names clothoid, bloss, biquadratic, sine and cosine as the initial set, and §5.1.68 encodes the value as <byte> <letters> — length-prefixed precisely so the set can grow. Some forms that would populate it are encumbered.
The Wiener Bogen is the worked example. It is a transition curve that shapes the alignment around the vehicle's centre of gravity rather than the track centreline, which is what lets it cut lateral acceleration and maintenance cost below what a clothoid achieves. It was invented by Herbert Hasslinger, filed as Austrian application A 1116/2002 on 23 July 2002 (see also EP1523597B1), is held by Wiener Linien and ÖBB, and "Wiener Bogen" has been a registered trademark since 2002.
The scope rule, which costs nothing and requires no licence:
Carry the label, not the math.
Representation is not implementation. A SPIRALTYPE value is a string. Round-tripping SPIRALCURVE (…, SPIRALTYPE Wiener Bogen) through WKT and WKB stores and returns a name its producer chose. No JTS code computes the curve, so there is no implementation for a claim to read on. This is also why the WKT grammar accepts an arbitrary SPIRALTYPE name rather than a closed enumeration.
Unknown spiral types are opaque, never approximated.toLinear(tolerance) on a spiral type JTS does not implement must fail with a clear message rather than quietly substituting a clothoid. Substituting is wrong geometry first of all, and it is also the point at which "JTS implements the Wiener Bogen" stops being obviously false.
Implementations arrive from outside, if at all. A SpiralTypeStrategy SPI would let a party that holds or has licensed the rights register the math in their own deployment. Nothing encumbered ships in the JTS distribution, so the EPL-2.0 patent grant is never asked to convey rights no contributor holds — the specific hazard for permissively licensed code, where a downstream user would otherwise receive an implementation with no accompanying grant.
The trademark outlives the patent. A patent filed in 2002 runs its 20-year term out around 2022, and anyone relying on that should confirm it for their own jurisdiction rather than take this document's word. A registered mark renews indefinitely. So treat "Wiener Bogen" as a name for a value a producer wrote, never as a product claim, a branded feature, or anything that implies endorsement by the mark's owners.
This is engineering policy, not legal advice, and the epic should not be read as though it were. It is the ordinary line between interface and implementation, applied to a case where getting it wrong is expensive.
4. What's already landed
4.1 On #7 since the spike (tip b2861a08, 16 Aug 2026)
Bucket
Summary
TAG honesty
feat: / fix:
WKB 8–12. Core WKBReader first-class cases: CircularString = count+coords; CompoundCurve / CurvePolygon / MultiCurve / MultiSurface = count + child WKBs (CurvePolygon rings carry their own type). GeometryFactory.createCircularString / createCompoundCurve(LineString[]) / createCurvePolygon(LineString, LineString[]) / createMultiCurve / createMultiSurface — default throws; CurveGeometryFactory implements. new WKBReader(new CurveGeometryFactory()).read(type8) returns CircularString. CurveWKBReader is the no-arg convenience. Locked XDR hex for CIRCULARSTRING (0 0, 5 5, 10 0) unchanged. Disc CurvePolygon area still 25π after round-trip. No toLinear on that path.
WKB sibling from Draft v3 — landed. Not a new public I/O class beyond the factory surface + existing curve writer/reader convenience.
feat:
OverlayNGCurve (OV-P1). Algebra (self/empty) → retention (covers/disjoint) → kits → chordsaw. R1.5 two-disc (crossing lens/blob/crescents and 0-node strict nested annulus, area 16π, covers EEEE / coveredBy EE0E). R1.6 disc vs hole-free plain polygon. R1.7 CompoundCurve shells: two-node clip, two-shell, collinear diameters, 0/1-node, even-n, tangent-odd NSpan, same-outer hole, different-outer punch. R-LL lineal arc vs line. R-AA two CircularStrings at exact nodes. Two-disc matrix is EEEE / EE0E.
OV-P1 flipped. On #7 the leftover named misses are still H-SHELL-N-MIXED, H-SHELL-HOLE-CROSS, H-SHELL-HOLE-X, H-ANNULUS-TANGENT. Stacked drafts (do not merge into #7) sew HOLE-CROSS / HOLE-X / HOLE-OUTER and take a first OverlayNG cell for H-SHELL-N-MIXED. H-ANNULUS-TANGENT stays a named miss. Circular noder + arrangement is OV-P2 / Bar 2, a different epic. Do not mark the full OV TAG green.
feat:
R-CONT certified disc PIP (interior + boundary band) in CurveExact.
R-CONT partial.
feat:
R-PR DE-9IM for disc vs Point, LineString, hole-free Polygon, and two discs. Point: interior 0F2FF1FF2, boundary FF20F1FF2, exterior FF2FF10F2. Line: crossing 1F20F1102, tangent FF20F1102, miss FF2FF1102, endpoint-interior 1020F1102. Polygon: disjoint FF2FF1212, nested 212FF1FF2, disc-in-square 2FF1FF212, crossing 212101212. Two discs: crossing 212101212, disjoint FF2FF1212, nested 212FF1FF2 / 2FF1FF212, ext tangent FF2F01212, int tangent 212F01FF2, equal 2FFF1FFF2. Finish slice: single-member MultiSurface both orders; equalsTopo on equal/crossing/rotated-control discs; full SFS table; crosses of two areas always false. Half-disc / CompoundCurve miss still null → linearise.
R-PR partial. Not “any combination of curved/flat”. Do not delete test_R_PR_* / test_R_CONT_*.
Helpers only. D-PT / D-AA / D-OP still describe the public DistanceOp TAG — keep those spec methods red.
feat:
Disc / single-arc / CompoundCurve convex hull closed forms. CompoundCurve hull is a CurvePolygon (area 61.59119, CircularString shell). Clothoid hull is still a linear POLYGON fallback.
H-CV / H-CC partial. ConcaveHull stays out.
feat:
Public DiscreteHausdorffDistance closed forms for two pairs only (via 0ca71b): CIRCULARSTRING (0 0, 2 3, 10 0) vs LINESTRING (0 0, 10 0) APEX √949/6 − 7/6 ≈ 3.967640600249787; two r=5 discs, centres 0 and 10 → 10.0. Exact path skips densify. Public DHD still sees chords in general. Fréchet still open.
D-HF partial (two pairs). Keep fail(). No new public pair.
feat:
Disc CurvePolygon area 25π.
M-AREA-CP partial (circular discs). Keep the spec method.
LEC partial. Keep the spec method if the public TAG is wider.
arch:
PERF-GATE: identity/chord-path rows use assertChordPath; slack stays 15% (1.15).
Contract, not a TAG.
These closed-form subsets of OV and R-PR did not wait for N-SS. Line–circle clip is used inside R1.6; there is still no public arc-arc / arc-line utility and no arc SegmentString. That is OV-P2, not a hole in OV-P1.
Still open (say so, do not close as “future work” a TAG that already has a named subset):
Named overlay miss still stamped: H-ANNULUS-TANGENT
On include ora and sde modules #7 only (sewn on stacked drafts, not merged): H-SHELL-HOLE-CROSS, H-SHELL-HOLE-X, H-SHELL-HOLE-OUTER; H-SHELL-N-MIXED first OverlayNG cell
General circular noding (N-AA, N-AL, N-SS) — OV-P2 / Bar 2. Do not start in this PR.
Open-arc buffer. Clothoid hull is a linear fallback
OFF = concentric arc (refactor)
RocqRefRunner SQL/MM suite for public Curve predicates
#7 carried the SFA/SQL-MM types, WKT, and structural-type work forward from that spike. #8 stacked the PERF-GATE and the first lasers, then merged into #7. #10 (nested annulus) and #11 (odd-n tangent NSpan) merged after that. #12 / #13 stamp H-ANNULUS-TANGENT and related named misses. OV-P2 stacked drafts #16–#30 (nodes, faces, hole cells, OverlayNG-for-circles, leftover kits, Curve HotPixel) stay off #7.
5. Tracking model
We are not opening 49 separate issues. Fragmenting the project board, spamming notifications, and forcing every reviewer to reconstruct the dependency graph is the wrong shape for work this large.
Run: mvn -pl modules/curve test -Dtest=CurveAwarenessSpecTest
Still has all 49 fail() methods. That “delete the method when a TAG ships” meter froze when work moved to stacked PRs and closed-form lasers.
The class is the full-TAG red list, excluded from default Surefire. It is not the live scoreboard. Do not delete a method because a closed-form subset landed (OV Phase 0, R-PR disc cells, disc area, TestBuilder Hausdorff, …). Delete only when the full TAG ships.
Local silence while working: mvn -Dtest='!CurveAwarenessSpecTest' test.
Live progress meter
Remaining full TAGs (still red in the spec class) plus the green tests next to production code on #7. A maintainer reading this epic should look at OverlayNGCurve / CurveExact / WKB tests on #7, not at “49 methods remaining”.
GitHub layout
This document is the suggested epic body — adapt as the maintainers see fit.
Optional milestone-shaped issues per phase with checklists referencing TAGs — at most 8 of them, never 49.
Day-to-day work via PRs that:
Reference this epic.
Use the TAG in commit subject + PR title.
Delete the corresponding red-test method only when the full TAG lands (its own commit, see conventions below). Partial lasers keep the spec method.
Commit / PR convention
bucket: TAG short description
Buckets: fix:, feat:, arch:, test:, spec:, refactor:. (User-facing docs go under spec:.)
test: drop CurveAwarenessSpecTest#test_BUF_1_* — the dedicated commit that closes a full TAG by removing its red-test method.
6. Cross-module impact
Most TAGs ship purely inside jts-curve (extension module, opt-in). A handful require touching jts-core and therefore need a maintainer review up-front. Calling them out before they're proposed:
Only if ShapeWriter needs new extension hooks for CurvePolygon rings.
N-AA, N-AL, N-SS
Yes
SegmentString / Noder hierarchy lives in core. Largest remaining core surface in the epic.
OV
Indirect
OV-P1 (kits + named misses) shipped in jts-curve without a public noder. General overlay (OV-P2) still depends on N-SS; the pipeline itself stays in core.
R-PR, R-CONT
Indirect
RelateOp lives in core. Disc cells shipped via CurveExact in jts-curve. General “any curved/flat pair” still wants N-SS.
PLG
Yes
Polygonizer lives in core; needs to accept CompoundCurve edges.
PRC-SN
Yes
PrecisionModel.makePrecise integration.
DSF
Yes (or shadow)
Densifier lives in core; alternative is to wrap and shadow it from jts-curve.
Everything else is jts-curve-only or jts-app (TestBuilder).
7. Risks / open questions
Backwards compatibility on structural composites. Algorithms that don't recognise a curve subtype today silently densify. After F-CP, a third-party algorithm that doesn't know about the new structural CurvePolygon ring will see a CompoundCurve shell where it expected a LinearRing, and may throw. We need a fallback contract: the structural ring must implement enough of LinearRing's contract to keep old code limping (read-only chord coords still available), or the structural CurvePolygon must fail fast with a clear message instead of silently going wrong.
equalsExact semantic change (R-EQ). Today CIRCULARSTRING(p0,p1,p2).equalsExact(LINESTRING(p0,p1,p2)) returns true via shared coordinates. Making it false is per spec, but it's a behaviour change for any user comparing-by-WKT-text. Needs a release note. Still open.
Non-similarity affine (AT-NS). Sheared arcs are ellipse arcs, which JTS doesn't model. The plan is to detect-and-densify, but: what does getGeometryType() return on the result? If LineString, we've changed the type silently. If still flagged as CircularString, the polyline lies about its identity. Decide before AT-NS lands.
Performance of public arc-arc / arc-line intersection (N-AA, N-AL). The two-circle solve is fine for low cardinality, but MCIndexNoder-equivalent indexing of arc spans (bounding-box pruning of arcs) is non-trivial. Worth a benchmark before committing to a design. R1.6 uses line–circle clip internally; that is not the public utility.
Z / M propagation across densified arcs. Out of scope for behaviour changes here, but every TAG that produces densified output (DSF, AT-NS, …) should choose a Z/M policy that doesn't lock us out of a proper interpolation later.
ESRI Bezier3Curve. Not an ISO type: 13249-3 allocates it no code, and the first genuinely free slot is 26, since 25 belongs to BrepSolid (1025 is 25 + the Z offset). Two options, undecided. Either carry it as a first-class extension type, which means proposing a code allocation somewhere it can actually be registered; or keep it out of the persisted model entirely and use cubic Bézier only as an internal, performance-oriented approximation at segment seams, never written to WKB. The second costs nothing in interop and is the default until someone needs the first.
8. Definition of Done (epic-level)
The epic closes when all of:
CurveAwarenessSpecTest is empty — every TAG's red test deleted, replaced by green tests next to its production code.
Every ISO/IEC 13249-3 extended type listed in §3 round-trips through every public Geometry operation in the user guide without producing flat output where curve-preserving output is mathematically possible.
WKB type codes. All of §3's codes read through core WKBReader with a curve-capable factory, and CurveWKBWriter emits them. Codes 8–12 are done on include ora and sde modules #7; 15–24, 102 and 1025 remain. (Z / M interpolation stays out of scope, as in §3.)
A release note covers the equalsExact change (see §7) and any other user-visible behaviour shifts.
TB-FN (function-tree curve-awareness badges) is a stretch goal, not a DoD criterion. Annotating every entry in TestBuilder's function tree is a multi-week task with low payoff relative to the algorithm work, and shouldn't gate epic closure.
9. Phases — work breakdown
Phases group TAGs that share dependencies or naturally land together. Within a phase, TAGs are usually independently shippable. Cross-phase dependencies are noted explicitly per phase.
Closed-form subsets of Phase 6 (and pieces of Phases 2–4) already landed on #7 without Phase 5. That does not turn those TAGs green. Annotate partial and name the subset. OV-P1 is flipped (kits + named misses). General overlay / polygonizer / coverage / equalsExact still wait on Phase 5 (OV-P2).
D-PT / D-AA — analytical point-arc and arc-arc distance. Closed-form helpers exist in CurveExact; the public DistanceOp TAG is still red.
D-OP — DistanceOp accepts curved inputs without forced densification. Still the public TAG; keep the spec method.
D-HF — public DiscreteHausdorffDistance two-pair lock via 0ca71b (APEX; two discs). Exact path skips densify. Public DHD still chords in general. Fréchet still open. Keep fail(). Not “parameterise by arc length”.
C-LIN / C-AREA / C-IP — centroids and interior point.
Depends on: Phase 1 (F-CP for CurvePolygon cases).
Phase 4 — Construction (Buffer, Hulls, Simplification, Affine, Linear-Ref, Densifier)
OV — arc-preserving overlay output (union / intersection / difference / symDifference). OV-P1 flipped: OverlayNGCurve kits (R0 / R1 / R1.5 discs + nested annulus / R1.6 disc-vs-polygon / R1.7 CompoundCurve shells including even-n and tangent-odd NSpan / R-LL / R-AA). On include ora and sde modules #7 the leftover named misses are still H-SHELL-N-MIXED, H-SHELL-HOLE-CROSS, H-SHELL-HOLE-X, H-ANNULUS-TANGENT; stacked drafts sew the hole cells and take a first OverlayNG cell for H-SHELL-N-MIXED. H-ANNULUS-TANGENT stays a named miss. Not a general circular noder. Do not mark the full OV TAG green. OV-P2 is Bar 2.
R-PR / R-CONT / R-EQ — arc-aware relate, predicates, exact equality. R-PR / R-CONT partial: certified disc PIP; DE-9IM for disc vs Point / LineString / hole-free Polygon / second disc; MultiSurface unwrap; SFS table; two areas never crosses. Half-disc / CompoundCurve still null → linearise. R-EQ (arc-vs-chord equalsExact) still open.
PLG — Polygonizer accepts CompoundCurve edges and emits CurvePolygon faces.
COV — CoverageUnion preserves shared arc edges.
Depends on: Phase 5 for general OV / PLG / COV / R-EQ. Phase 6 subsets already landed without N-SS — see §4.1 and §10.
Phase 7 — Independent tracks
Three single-theme tracks that depend only on Phase 1 and have no inter-dependencies; can land in parallel with Phases 2–6 once Phase 1 ships.
Snapping
PRC-SN — snap-to-grid preserves arc when the snapped (R, centre, sweep) still lies on grid; otherwise densify-and-snap chords.
Visual QA still open: TestBuilder inspector ClassCastException.
Most of Phase 2 / 4 / 7 TAGs are still the full red list. Also still open (not a phase of their own): RocqRefRunner SQL/MM suite for public Curve predicates. LEC (typed obstacle distance for point / segment / polygon / arc / disc) landed on #7 — keep the spec method if the public TAG is wider than that subset.
After Phase 1 finishes, Phases 2 / 3 / 4 / 5 / 7 can run in parallel. OV-P2 (circular noder + arrangement) still waits for Phase 5. OV-P1 (kits + named misses) already shipped on #7 without a public noder. The phase graph must not say “overlay requires a noder” as if those kits were blocked.
11. Conventions
TAGs are short, unique, and stable. Renaming a TAG renames its test method too.
One TAG per PR (or a tightly-coupled cluster). The PR deletes the corresponding spec method only when the full TAG lands. Partial lasers keep the method.
CI stays green by default. The spec class is excluded from the default Surefire run via:
Contributors run it explicitly with mvn -pl modules/curve test -Dtest=CurveAwarenessSpecTest (which overrides the exclude). The remaining-method count is the full-TAG red list, not the live progress meter (see §5). It does not break CI on every push.
12. References
ISO/IEC 13249-3 (SQL/MM Part 3: Spatial) — §5.1.67 <well-known text representation>, §5.1.68 <well-known binary representation> and its Table 15 of <uint32> type codes.
OverlayNGCurve (package-private overlay helpers in org.locationtech.jts.operation.overlayng.curve).
CurveExact (package-private in org.locationtech.jts.geom.curve).
GEOS src/io/WKBReader.cpp / WKBWriter.cpp / include/geos/io/WKBConstants.h — type codes 8–12 and child-WKB layout.
Spike branch (origin): feature/sfa-curve-buffer-spike on grootstebozewolf/jts.
CurveAwarenessSpecTest — full-TAG red list (49 fail() methods still present; excluded from default Surefire). Live meter: include ora and sde modules #7 green tests + remaining full TAGs.
Epic: SQL/MM (ISO/IEC 13249-3) Curve Awareness in JTS
JTS logo as curves plus a buffer halo. Not a closed-form buffer laser. Not a guide figure.
Status: Draft v6 (2026-08-16).
Source: #7
feature/sfa-curve-rgr@b2861a08— SQL-MM types, WKT, WKB 8–12, OverlayNGCurve, TestBuilder logo-as-curves. #8, #10, #11, #35, #42 (has #41), #43, #44, #45, #46, #14, #22, #31, #39 are on #7. Bar 2 / #38 /curve-rgr-bar2-rc0/ #27 stay off #7. Do not merge #7 tolocationtech/jtsmaster from a stacked-PR button.Origin (historical):
feature/sfa-curve-buffer-spike— Draft v3 of this epic and the 49-method spec class.Audience: locationtech/jts maintainers and contributors.
Field contract (PERF-GATE): Maintainable → Reliable → Faster. Take the curve path only if
t_laser ≤ 1.15 × t_chainsaw. The 15% slack is not to be loosened. The overlay is named OverlayNGCurve, not Curved.Vocabulary. A TAG is a short, stable identifier for one unit of work, each with a matching red test in
CurveAwarenessSpecTest. A kit is a closed-form implementation covering one specific pairing of geometries. A named miss is a pairing with no kit yet, given an identifier so it is tracked rather than silently linearised. A laser is the closed-form path; the chordsaw (written chainsaw in the PERF-GATE formula above — the two should be reconciled to one word) is the densify-then-operate fallback. Bar 2 is the follow-on delivery bar, the circular noder and arrangement work (OV-P2), tracked outside this epic.1. Goal
Make JTS preserve the ISO/IEC 13249-3 (SQL/MM Part 3) curve, surface and solid geometries —
CIRCULARSTRING,COMPOUNDCURVE,CURVEPOLYGON,MULTICURVE,MULTISURFACE,POLYHEDRALSURFACE,TIN,TRIANGLE,CIRCLE,GEODESICSTRING,ELLIPTICALCURVE,NURBSCURVE,CLOTHOID,SPIRALCURVE,COMPOUNDSURFACEandBREPSOLID— through every algorithm where the math is sound, instead of silently linearising to flat parents on the way in.jts-curveis no longer only a parse-and-WKT/WKB stand-in: types exist, WKT round-trips without densify and WKB codes 8–12 do too, and OverlayNGCurve takes a closed form when one exists. Every circular pair that has a closed form has a kit. Everything else is a named miss and the chordsaw. We will not write a circular noder in this PR.jts-corestill densifies on contact for everything that is not one of those kits. This epic tracks the lift, operation by operation — full TAG vs closed-form subset, not “good enough as linearised”.2. Why
3. Scope decisions
In scope (this epic):
Algorithms operating on the full ISO/IEC 13249-3 extended type set — the curve, surface and solid types — in 2-D and in their Z / M / ZM forms.
WKT round-trip with member structure (landed on include ora and sde modules #7).
WKB type codes, as given by ISO/IEC 13249-3 §5.1.68, Table 15. The extended set is 8–12, 15–24 and 102, plus BrepSolid at 1025 (Z only — the standard defines no 2-D BrepSolid, and 1025 is 25 + the Z offset, so slot 25 is spoken for). Codes 13 and 14 are not used by 13249-3; OGC SFA reserves them for the abstract
CurveandSurfacetypes. Any range spanning 13 or 14 — including the "8–21" this document used to claim — is wrong.Dimension offsets are
+1000(Z),+2000(M),+3000(ZM). Separately, and this is a different scheme rather than the dimension offset, Table 15 also allows1000001–1000005as alternate codes for CircularString through MultiSurface. The two do not compose: CircularString Z is1008, not1000001 + 1000. An earlier draft of this document asserted that the 1000-series was only the dimension offset; both schemes are real.Landed so far: 8–12 (on rename to jts-io-common #8, now on include ora and sde modules #7). Core
WKBReadertakes those five the way GEOS does, delegating construction toGeometryFactory;CurveWKBWriteremits them through a hook placed before the instanceof chain. CoreWKBWriteremits none of the extended codes unless that hook or subclass is used. Codes 15–24, 102 and 1025 are in scope and not started.TestBuilder rendering and drawing tools.
In scope, sequenced late — the hard parts, named rather than hidden:
POLYHEDRALSURFACE(15),TIN(16) andBREPSOLID(1025). Structural and drawing support landed in the spike for visualisation; the 3-D semantics have not started.ELLIPTICALCURVE(20). JTS has no ellipse model and adding one is the single largest piece of work here. It stays in scope because the epic tracks the whole standard, but nothing earlier should wait on it.Out of scope:
3.1 Proprietary and encumbered curve forms
SPIRALTYPEis an open value set. 4.2.12 names clothoid, bloss, biquadratic, sine and cosine as the initial set, and §5.1.68 encodes the value as<byte> <letters>— length-prefixed precisely so the set can grow. Some forms that would populate it are encumbered.The Wiener Bogen is the worked example. It is a transition curve that shapes the alignment around the vehicle's centre of gravity rather than the track centreline, which is what lets it cut lateral acceleration and maintenance cost below what a clothoid achieves. It was invented by Herbert Hasslinger, filed as Austrian application A 1116/2002 on 23 July 2002 (see also EP1523597B1), is held by Wiener Linien and ÖBB, and "Wiener Bogen" has been a registered trademark since 2002.
The scope rule, which costs nothing and requires no licence:
Carry the label, not the math.
SPIRALTYPEvalue is a string. Round-trippingSPIRALCURVE (…, SPIRALTYPE Wiener Bogen)through WKT and WKB stores and returns a name its producer chose. No JTS code computes the curve, so there is no implementation for a claim to read on. This is also why the WKT grammar accepts an arbitrarySPIRALTYPEname rather than a closed enumeration.toLinear(tolerance)on a spiral type JTS does not implement must fail with a clear message rather than quietly substituting a clothoid. Substituting is wrong geometry first of all, and it is also the point at which "JTS implements the Wiener Bogen" stops being obviously false.SpiralTypeStrategySPI would let a party that holds or has licensed the rights register the math in their own deployment. Nothing encumbered ships in the JTS distribution, so the EPL-2.0 patent grant is never asked to convey rights no contributor holds — the specific hazard for permissively licensed code, where a downstream user would otherwise receive an implementation with no accompanying grant.This is engineering policy, not legal advice, and the epic should not be read as though it were. It is the ordinary line between interface and implementation, applied to a case where getting it wrong is expensive.
4. What's already landed
4.1 On #7 since the spike (tip
b2861a08, 16 Aug 2026)feat:/fix:WKBReaderfirst-class cases: CircularString = count+coords; CompoundCurve / CurvePolygon / MultiCurve / MultiSurface = count + child WKBs (CurvePolygon rings carry their own type).GeometryFactory.createCircularString/createCompoundCurve(LineString[])/createCurvePolygon(LineString, LineString[])/createMultiCurve/createMultiSurface— default throws;CurveGeometryFactoryimplements.new WKBReader(new CurveGeometryFactory()).read(type8)returnsCircularString.CurveWKBReaderis the no-arg convenience. Locked XDR hex forCIRCULARSTRING (0 0, 5 5, 10 0)unchanged. DiscCurvePolygonarea still 25π after round-trip. NotoLinearon that path.feat:EEEE/ coveredByEE0E). R1.6 disc vs hole-free plain polygon. R1.7 CompoundCurve shells: two-node clip, two-shell, collinear diameters, 0/1-node, even-n, tangent-odd NSpan, same-outer hole, different-outer punch. R-LL lineal arc vs line. R-AA two CircularStrings at exact nodes. Two-disc matrix is EEEE / EE0E.H-SHELL-N-MIXED,H-SHELL-HOLE-CROSS,H-SHELL-HOLE-X,H-ANNULUS-TANGENT. Stacked drafts (do not merge into #7) sew HOLE-CROSS / HOLE-X / HOLE-OUTER and take a first OverlayNG cell for H-SHELL-N-MIXED.H-ANNULUS-TANGENTstays a named miss. Circular noder + arrangement is OV-P2 / Bar 2, a different epic. Do not mark the full OV TAG green.feat:CurveExact.feat:0F2FF1FF2, boundaryFF20F1FF2, exteriorFF2FF10F2. Line: crossing1F20F1102, tangentFF20F1102, missFF2FF1102, endpoint-interior1020F1102. Polygon: disjointFF2FF1212, nested212FF1FF2, disc-in-square2FF1FF212, crossing212101212. Two discs: crossing212101212, disjointFF2FF1212, nested212FF1FF2/2FF1FF212, ext tangentFF2F01212, int tangent212F01FF2, equal2FFF1FFF2. Finish slice: single-member MultiSurface both orders;equalsTopoon equal/crossing/rotated-control discs; full SFS table;crossesof two areas always false. Half-disc / CompoundCurve miss still null → linearise.test_R_PR_*/test_R_CONT_*.feat:CurveExact(arc-to-segment, overlapping discsnearestPoints0,decideTolerancearc-aware).DistanceOpTAG — keep those spec methods red.feat:CurvePolygon(area 61.59119, CircularString shell). Clothoid hull is still a linearPOLYGONfallback.feat:DiscreteHausdorffDistanceclosed forms for two pairs only (via0ca71b):CIRCULARSTRING (0 0, 2 3, 10 0)vsLINESTRING (0 0, 10 0)APEX√949/6 − 7/6≈ 3.967640600249787; two r=5 discs, centres 0 and 10 → 10.0. Exact path skips densify. Public DHD still sees chords in general. Fréchet still open.fail(). No new public pair.feat:CurvePolygonarea 25π.feat:arch:assertChordPath; slack stays 15% (1.15).These closed-form subsets of OV and R-PR did not wait for N-SS. Line–circle clip is used inside R1.6; there is still no public arc-arc / arc-line utility and no arc
SegmentString. That is OV-P2, not a hole in OV-P1.Still open (say so, do not close as “future work” a TAG that already has a named subset):
H-ANNULUS-TANGENTH-SHELL-HOLE-CROSS,H-SHELL-HOLE-X,H-SHELL-HOLE-OUTER;H-SHELL-N-MIXEDfirst OverlayNG cellClassCastException(visual QA initial maven module structure #3)equalsExactarc-vs-chord (R-EQ)4.2 Historical —
feature/sfa-curve-buffer-spike(Draft v3 origin)arch:CompoundCurve— segment-awarecopy/toLineararch:CompoundCurvearch:CurveShapeWriterwalksCompoundCurvemembers, arc-rendersfeat:LineHandlingFunctions.mergeCurves(arc-aware sibling ofmergeLines)fix:CompoundCurve.toLinear(no junction drift)fix:spec:CurveAwarenessSpecTestred-test suitebufferCurveWithParamslinearisation hook (Phase-5 spike entry)#7 carried the SFA/SQL-MM types, WKT, and structural-type work forward from that spike. #8 stacked the PERF-GATE and the first lasers, then merged into #7. #10 (nested annulus) and #11 (odd-n tangent NSpan) merged after that. #12 / #13 stamp
H-ANNULUS-TANGENTand related named misses. OV-P2 stacked drafts #16–#30 (nodes, faces, hole cells, OverlayNG-for-circles, leftover kits, Curve HotPixel) stay off #7.5. Tracking model
We are not opening 49 separate issues. Fragmenting the project board, spamming notifications, and forcing every reviewer to reconstruct the dependency graph is the wrong shape for work this large.
Full-TAG red list:
CurveAwarenessSpecTestmodules/curve/src/test/java/org/locationtech/jts/spec/curveawareness/CurveAwarenessSpecTest.javamvn -pl modules/curve test -Dtest=CurveAwarenessSpecTestfail()methods. That “delete the method when a TAG ships” meter froze when work moved to stacked PRs and closed-form lasers.mvn -Dtest='!CurveAwarenessSpecTest' test.Live progress meter
Remaining full TAGs (still red in the spec class) plus the green tests next to production code on #7. A maintainer reading this epic should look at OverlayNGCurve /
CurveExact/ WKB tests on #7, not at “49 methods remaining”.GitHub layout
Commit / PR convention
Buckets:
fix:,feat:,arch:,test:,spec:,refactor:. (User-facing docs go underspec:.)Examples:
feat: BUF-1 analytical single-arc CircularString buffer → CurvePolygonarch: F-CP CurvePolygon stores CompoundCurve shell + holestest: drop CurveAwarenessSpecTest#test_BUF_1_*— the dedicated commit that closes a full TAG by removing its red-test method.6. Cross-module impact
Most TAGs ship purely inside
jts-curve(extension module, opt-in). A handful require touchingjts-coreand therefore need a maintainer review up-front. Calling them out before they're proposed:WKBReaderswitch,WKBConstants8–12,GeometryFactorycreate*stubs (default throw).CurveGeometryFactory/CurveWKBWriter/CurveWKBReaderstay injts-curve.ShapeWriterneeds new extension hooks forCurvePolygonrings.SegmentString/Noderhierarchy lives in core. Largest remaining core surface in the epic.jts-curvewithout a public noder. General overlay (OV-P2) still depends on N-SS; the pipeline itself stays in core.RelateOplives in core. Disc cells shipped viaCurveExactinjts-curve. General “any curved/flat pair” still wants N-SS.Polygonizerlives in core; needs to acceptCompoundCurveedges.PrecisionModel.makePreciseintegration.Densifierlives in core; alternative is to wrap and shadow it fromjts-curve.Everything else is jts-curve-only or
jts-app(TestBuilder).7. Risks / open questions
CompoundCurveshell where it expected aLinearRing, and may throw. We need a fallback contract: the structural ring must implement enough ofLinearRing's contract to keep old code limping (read-only chord coords still available), or the structural CurvePolygon must fail fast with a clear message instead of silently going wrong.equalsExactsemantic change (R-EQ). TodayCIRCULARSTRING(p0,p1,p2).equalsExact(LINESTRING(p0,p1,p2))returns true via shared coordinates. Making it false is per spec, but it's a behaviour change for any user comparing-by-WKT-text. Needs a release note. Still open.getGeometryType()return on the result? IfLineString, we've changed the type silently. If still flagged asCircularString, the polyline lies about its identity. Decide before AT-NS lands.MCIndexNoder-equivalent indexing of arc spans (bounding-box pruning of arcs) is non-trivial. Worth a benchmark before committing to a design. R1.6 uses line–circle clip internally; that is not the public utility.Bezier3Curve. Not an ISO type: 13249-3 allocates it no code, and the first genuinely free slot is 26, since 25 belongs to BrepSolid (1025 is 25 + the Z offset). Two options, undecided. Either carry it as a first-class extension type, which means proposing a code allocation somewhere it can actually be registered; or keep it out of the persisted model entirely and use cubic Bézier only as an internal, performance-oriented approximation at segment seams, never written to WKB. The second costs nothing in interop and is the default until someone needs the first.8. Definition of Done (epic-level)
The epic closes when all of:
CurveAwarenessSpecTestis empty — every TAG's red test deleted, replaced by green tests next to its production code.Geometryoperation in the user guide without producing flat output where curve-preserving output is mathematically possible.WKBReaderwith a curve-capable factory, andCurveWKBWriteremits them. Codes 8–12 are done on include ora and sde modules #7; 15–24, 102 and 1025 remain. (Z / M interpolation stays out of scope, as in §3.)equalsExactchange (see §7) and any other user-visible behaviour shifts.TB-FN (function-tree curve-awareness badges) is a stretch goal, not a DoD criterion. Annotating every entry in TestBuilder's function tree is a multi-week task with low payoff relative to the algorithm work, and shouldn't gate epic closure.
9. Phases — work breakdown
Phases group TAGs that share dependencies or naturally land together. Within a phase, TAGs are usually independently shippable. Cross-phase dependencies are noted explicitly per phase.
Closed-form subsets of Phase 6 (and pieces of Phases 2–4) already landed on #7 without Phase 5. That does not turn those TAGs green. Annotate partial and name the subset. OV-P1 is flipped (kits + named misses). General overlay / polygonizer / coverage /
equalsExactstill wait on Phase 5 (OV-P2).Phase 1 — Foundations (jts-curve structural completeness)
Mirror the
CompoundCurvework onto the remaining composite types.CurvePolygon—CompoundCurveshell + holes;copyInternal,toLinear, WKT reader/writer preserve structure.MultiCurve— preserves member subtypes (LineString/CircularString/CompoundCurve).MultiSurface— preservesPolygonvsCurvePolygonmember subtypes.CurveShapeWriterforCurvePolygonrings,MultiCurveandMultiSurfacemembers.Hard prereq for: all of Phase 2 (you can't return a
CompoundCurveboundary of aCurvePolygonwhose ring isn't aCompoundCurveto begin with).Phase 2 — Properties (Metrics, Boundary, Validity)
CurvePolygonarea is 25π. Keep the spec method.CurvePolygonvalidity (arc self-intersection, sector orientation, holes-in-shell).CircularString/CompoundCurvesimplicity.Depends on: Phase 1 (F-CP is a hard prereq for B-CP, M-AREA-CP, V-CP).
Phase 3 — Measurement (Distance, Centroid, Interior point)
CurveExact; the publicDistanceOpTAG is still red.DistanceOpaccepts curved inputs without forced densification. Still the public TAG; keep the spec method.DiscreteHausdorffDistancetwo-pair lock via0ca71b(APEX; two discs). Exact path skips densify. Public DHD still chords in general. Fréchet still open. Keepfail(). Not “parameterise by arc length”.Depends on: Phase 1 (F-CP for
CurvePolygoncases).Phase 4 — Construction (Buffer, Hulls, Simplification, Affine, Linear-Ref, Densifier)
toLinear.Depends on: Phase 1 (output may be
CurvePolygon).Phase 5 — Noding foundation
CurveSegmentStringinsidejts-curveonly; no publicSegmentStringor noder. #27 stays a dead-end. Still off include ora and sde modules #7.Depends on: Phase 1. Touches
jts-core— see §6. Phase 6 general overlay still depends on N-SS.Phase 6 — Overlay, Predicates, Polygonizer, Coverage
union/intersection/difference/symDifference). OV-P1 flipped: OverlayNGCurve kits (R0 / R1 / R1.5 discs + nested annulus / R1.6 disc-vs-polygon / R1.7 CompoundCurve shells including even-n and tangent-odd NSpan / R-LL / R-AA). On include ora and sde modules #7 the leftover named misses are stillH-SHELL-N-MIXED,H-SHELL-HOLE-CROSS,H-SHELL-HOLE-X,H-ANNULUS-TANGENT; stacked drafts sew the hole cells and take a first OverlayNG cell for H-SHELL-N-MIXED.H-ANNULUS-TANGENTstays a named miss. Not a general circular noder. Do not mark the full OV TAG green. OV-P2 is Bar 2.crosses. Half-disc / CompoundCurve still null → linearise. R-EQ (arc-vs-chordequalsExact) still open.PolygonizeracceptsCompoundCurveedges and emitsCurvePolygonfaces.CoverageUnionpreserves shared arc edges.Depends on: Phase 5 for general OV / PLG / COV / R-EQ. Phase 6 subsets already landed without N-SS — see §4.1 and §10.
Phase 7 — Independent tracks
Three single-theme tracks that depend only on Phase 1 and have no inter-dependencies; can land in parallel with Phases 2–6 once Phase 1 ships.
(R, centre, sweep)still lies on grid; otherwise densify-and-snap chords.DelaunayTriangulationBuilder/VoronoiDiagramBuilderaccept curved boundary input (densify internally viatoLinear(tolerance)).CompoundCurveTool,CurvePolygonTooldrawing UX.ClassCastException.Most of Phase 2 / 4 / 7 TAGs are still the full red list. Also still open (not a phase of their own): RocqRefRunner SQL/MM suite for public Curve predicates. LEC (typed obstacle distance for point / segment / polygon / arc / disc) landed on #7 — keep the spec method if the public TAG is wider than that subset.
10. Suggested order
After Phase 1 finishes, Phases 2 / 3 / 4 / 5 / 7 can run in parallel. OV-P2 (circular noder + arrangement) still waits for Phase 5. OV-P1 (kits + named misses) already shipped on #7 without a public noder. The phase graph must not say “overlay requires a noder” as if those kits were blocked.
11. Conventions
TAGs are short, unique, and stable. Renaming a TAG renames its test method too.
One TAG per PR (or a tightly-coupled cluster). The PR deletes the corresponding spec method only when the full TAG lands. Partial lasers keep the method.
CI stays green by default. The spec class is excluded from the default Surefire run via:
Contributors run it explicitly with
mvn -pl modules/curve test -Dtest=CurveAwarenessSpecTest(which overrides the exclude). The remaining-method count is the full-TAG red list, not the live progress meter (see §5). It does not break CI on every push.12. References
<well-known text representation>, §5.1.68<well-known binary representation>and its Table 15 of<uint32>type codes.jts-curvesource:modules/curve/.feature/sfa-curve-rgr@b2861a08— SFA/SQL-MM types, WKT, WKB 8–12, OverlayNGCurve OV-P1, logo-as-curves.org.locationtech.jts.operation.overlayng.curve).CurveExact(package-private inorg.locationtech.jts.geom.curve).src/io/WKBReader.cpp/WKBWriter.cpp/include/geos/io/WKBConstants.h— type codes 8–12 and child-WKB layout.feature/sfa-curve-buffer-spikeongrootstebozewolf/jts.CurveAwarenessSpecTest— full-TAG red list (49fail()methods still present; excluded from default Surefire). Live meter: include ora and sde modules #7 green tests + remaining full TAGs.