Skip to content

Struct-based Projection with an add-only custom registration API (#242) - #247

Closed
trasch wants to merge 1 commit into
projection-api-polishfrom
add-struct-projection
Closed

trasch wants to merge 1 commit into
projection-api-polishfrom
add-struct-projection

Conversation

@trasch

@trasch trasch commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements #242 in the agreed design: a struct-based Projection with SRID identity, plus an add-only custom registration API. Stacks on #245.

While a major (breaking) change by nature (public enum → struct), the source impact is deliberately minimized: all 125 built-in projections keep their constant names, so dot-shorthand usage (x == .epsg4326, switch on .kind, etc.) keeps compiling unchanged — the whole in-repo codebase needed only 3 tiny rawValue → srid renames.

Phase A — struct-based Projection

  • public struct Projection: Hashable, Codable, Sendable, CustomStringConvertible with SRID identity (custom ==/hash); Codable encodes the raw SRID identically to the enum's wire format (decode throws for unregistered SRIDs)
  • All built-in projections expose the same static let constants; init?(srid:) (with the historical EPSG:3857 aliases), init?(wkt:) (registry-driven + UTM token short-circuit), init?(utmZone:hemisphere:), description, kind/flags, wraparoundExtent, crsLength(fromMeters:), utmZone, utmHemisphere all preserved in shape

Phase B — add-only registration

  • public struct CustomProjection: value-type descriptor with @Sendable forward/inverse closures relative to the EPSG:4326 (WGS84) pivot — explicitly documented as the seam where datum-capable definitions (e.g. NAD27, issue Support CRSs on other datums (NAD27, OSGB, CH1903+, ...) #243) would perform datum transformations. Also carries kind, wraparoundExtent, validExtent (new public struct ProjectionExtent), worldBoundingBox, WKT fragment sets
  • Projection.register(CustomProjection) -> Bool — add-only by design: rejected for SRID ≤ 0, duplicates and built-in shadowing; no unregister/deactivate path (per review decision — enabling/disabling of built-ins was rejected earlier as performance-neutral and correctness-degrading)
  • The registry keeps a Mutex-guarded add-only snapshot store; nothing is ever removed or replaced

Performance — the big win of the definition-capture design

This branch's parallel test run exposed catastrophic contention from per-access registry reads (~924 s full-suite wall time; a graph perf test at 370×). The fix is architectural: Projection values capture their definition at construction — hot paths (projected(to:), kind, clamped(), isValid, wraparoundExtent) read the stored definition with zero locking, and the registry is only used for cold-path lookups (init?(srid:), Codable), WKT matching and registration.

Release-build benchmarks (per coordinate, before → after):

Path Before (enum, registry) After (definition capture)
full projected(to:) 4326↔3857 ~178 ns (35× baseline) ~43 ns (~1.7× baseline)
4326→UTM→4326 ~300 ns ~164 ns
4326→4978→4326 ~531 ns ~443 ns

Testing (+6)

CustomProjectionTests: registration rules (duplicate/<= 0/shadowed SRIDs rejected), end-to-end round trips through a custom definition, registry-driven capabilities (validity/clamp/normalize/kind-dispatch on a custom), WKT matching, Codable semantics (register-before-decode), and an 8-task concurrent registration storm.

Note for test authors (surfaced by the suite, documented in the tests): Swift Testing runs tests in parallel against the process-global registry — reserve SRID blocks and use per-registration WKT fragments.

Also adds a Projections section with a CustomProjection example to the README and full DocC throughout.

Full suite: 2488 tests in 180 suites, zero warnings.

Implements issue #242 in two steps on this branch.

Phase A - struct-based Projection:
- Projection becomes a public struct with SRID identity: custom
  ==/hash and a Codable implementation that encodes the raw SRID
  identically to the previous enum wire format. All 125 built-in
  projections keep their constant names, so dot-shorthand usage
  (x == .epsg4326) keeps compiling unchanged.
- The generated case lists are gone: kind, capability flags,
  wraparoundExtent and crsLength resolve through the registry.
- init?(srid:) resolves through an SRID alias table (the historical
  EPSG:3857 aliases) plus a registry check; init?(wkt:) and the UTM
  token short-circuit are unchanged; descriptions unchanged.

Phase B - add-only custom registration:
- public CustomProjection struct: a value-type descriptor with
  @sendable forward/inverse closures relative to the EPSG:4326
  (WGS84) pivot, plus kind, wraparound extent, valid extent
  (public ProjectionExtent struct), world bounding box and WKT
  fragment sets. The pivot contract makes datum-capable custom
  definitions possible.
- public Projection.register(CustomProjection) is add-only:
  rejected for SRID <= 0, duplicates and built-in shadowing; there
  is no unregister/deactivate path by design.
- ProjectionExtent becomes the public extent type used by all
  definition metadata.
- The registry keeps a Mutex-guarded add-only snapshot: built-ins
  seeded once, custom definitions appended, never removed or
  replaced. Hot paths NEVER read the registry - Projection values
  capture their definition at construction - so projected(to:),
  kind, clamped(), isValid and wraparoundExtent resolve via the
  stored definition with zero locking. The registry is only used
  for lookups (init?(srid:), Codable), WKT matching and
  registration.

Performance (release benchmarks, before -> after):
- full projected(to:) path: ~178 ns/coordinate (35x baseline)
  -> ~1.7x of the direct-math baseline
- No hot-path registry access remains; a parallel 2488-test run
  previously melted down (924s wall) on per-access Mutex reads,
  which motivated the definition capture.

Tests (+6): custom registration/rejection rules, end-to-end
round trips, registry-driven capabilities on a custom definition,
WKT matching, Codable semantics (register-before-decode) and a
concurrent registration storm. Note for test authors: Swift
Testing runs tests in parallel against the process-global
registry - reserve SRID blocks and use unique WKT fragments.

Also adds a Projections section with a custom-projection example
to the README.
@trasch

trasch commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #250, which consolidates the full projection-genericity chain into a single reviewable PR. All commits are unchanged in #250.

@trasch trasch closed this Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant