Danseiji plus better splits#40
Merged
Merged
Conversation
…/lon Adds a required `inverse(_:)` method to the `Projection` protocol and implements it for all 8 in-tree projections, plus a high-level `coordinate(at:size:zoomTo:insets:coordinateSystem:)` API that turns a screen-pixel `Point` into a `GeoJSON.Position?`. EqualEarth and NaturalEarth use Newton-Raphson; the rest are closed-form. Out-of-image clicks (e.g. off the globe of an Orthographic map) return nil via a new `MapBounds.contains` helper. https://claude.ai/code/session_01CsF6py8qHa7t2ZQpmsaHh7
Adds a bottom status bar to the macOS Cassini sample that shows the geographic coordinate under the cursor, computed via the new `Projection.coordinate(at:size:zoomTo:insets:coordinateSystem:)` API. Clicking the map locks the coordinate; the status bar then offers Copy (to clipboard, returns to live mode) and Discard (returns to live mode without copying), with ⌘C and Esc shortcuts. Exposes `GeoDrawer.zoomTo` so the example can mirror the drawer's projected zoom rect when computing inverses. https://claude.ai/code/session_01CsF6py8qHa7t2ZQpmsaHh7
Adds all six Danseiji map projections (I–VI) by Justin Kunimune as a new optional SPM target, GeoProjectorDanseiji. Kept separate from the core GeoProjector library so consumers that don't want the ~6.9 MB of mesh data don't pay the cost. - Vendor the six danseijiX.csv mesh files under Sources/GeoProjectorDanseiji/Resources/, unchanged from upstream (https://github.com/jkunimune/Map-Projections, MIT licensed). - Port the forward-projection algorithm from upstream Danseiji.java (mesh-cell barycentric interpolation). Inverse projection is intentionally not exposed — the Projection protocol only requires forward. - Expose Projections.DanseijiI … Projections.DanseijiVI, matching the one-struct-per-projection pattern used by NaturalEarth, EqualEarth, etc. - Add DanseijiVariant — a sibling of ProjectionMode for clients that want Codable, enum-driven selection across the six variants. - Cache parsed mesh data per variant under an NSLock so initialisers stay cheap. - Wire the Cassini example app to expose the six new variants in its picker. Closes part of #3.
…i0gH' into claude/add-danseiji-projection-c9Abs
The merged inverse-projection feature added a required `inverse(_:)` method to the `Projection` protocol, which Danseiji structs need to implement. - Extend the CSV parser to read the `pixels` section (phi, lam samples on a rectangular grid) plus the edge polygon's bounding box, both of which were previously skipped. - Port `DanseijiProjection.inverse` from upstream `Danseiji.java`: ray-cast point-in-polygon test against the edge, then bilinear interpolation of the pixel grid in 3-D Cartesian space (so meridians don't blow up at the poles) before converting back to (lon, lat). - Apply the reference longitude shift on the way out. - Add inverse round-trip tests over a coarse lat/lon grid for all six variants (loose tolerance — ~3° — given the bilinear interpolation step).
Replace the willWrap-based splitter with a generic boundary-crossing detector that consults each projection's mapBounds in projected radian space. Polygons whose exterior crosses the projection edge are now split at the crossing and closed along the boundary, instead of drawn as long chords across (or beyond) the visible map. Fixes the "huge sweeping triangles / horizontal stripes" rendering glitches on Danseiji III & IV (whose bezier outlines are interrupted), the orthographic limb chord, and the antimeridian-crossing chord on cylindrical projections (Antarctica's exterior wraps -180° to +180°). Removes Projection.willWrap entirely — the boundary-aware split subsumes its job in the renderer, and the per-projection overrides existed only to feed it. Projection.point(for:size:…) drops its tupled Bool return in the same change (callers were only the renderer). - Sources/GeoProjector/MapBounds+Contains.swift: new public helpers firstIntersection(from:to:projectionSize:) and boundaryArc(from:to:projectionSize:) covering rectangle, ellipse, and bezier outlines. contains(_:projectionSize:) made public. - Sources/GeoDrawer/GeoDrawer+BoundarySplit.swift: new helper file with boundarySplit and boundarySplitClosed. - Sources/GeoDrawer/GeoDrawer.swift: convertLine rewritten on top of the new helpers; new convertPolygon variant closes each split piece along the projection boundary so polygon fill follows the edge. - Tests/GeoDrawerTests/BoundarySplitTests.swift: covers the geometric primitives plus end-to-end splits through Equirectangular, Orthographic, and a synthetic interrupted bezier. Antarctica's residual unfilled gap near the south pole (the GeoJSON stops at lat -85.6° and never reaches -90°) is left as a follow-up.
…ctions Three independent visual bugs all shared the same root cause family — the boundary splitter not handling antimeridian/antipode wrap cleanly, plus floating-point edge cases. - `GeoDrawer+BoundarySplit`: detect wrap-length jumps in every prev/curr case (inside↔inside, inside↔outside, outside↔inside) and shift the far-side point onto the near-side before computing the boundary intersection. Also detect paired exit/entry crossings for concave bezier notches, and merge first/last raw pieces when the input ring is closed and starts/ends inside the bounds. - `MapBounds.allIntersections`: generalises `firstIntersection` to return every crossing with a de-dup pass for segment-through-vertex cases. Powers the concave-notch split. - `AzimuthalEquidistant.project`: clamp `cos(c)` to `[-1, 1]` before `acos` and reject points within `1e-6` of the antipode where `k = c / sin(c)` blows up. Tests cover the regressions: - equirectangular and bezier antimeridian splits (closed polygon and bare line) - bezier concave-notch split - Equal Earth + Natural Earth real-continents at non-zero longitude references - Azimuthal antipode rejection and grid-wide finiteness Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The slider-drag and projection-switch lag in the example app was driven by re-projecting all continents per slider tick. A few hot paths were doing significantly more work than they needed to: - `Interpolator.interpolate` projected `a` and `b` at every recursion step, even though both are already projected by the caller (and re-projected again by the parent recursion). Refactor to a tail-recursive helper that threads `aProj`/`bProj` through, so each unprojected midpoint is projected exactly once. Also drops the `[c_proj].compactMap.map` allocation and the `lefty + middy + righty` array concatenations in favour of a single in-place output buffer. - `GeoDrawer.projectLine` was projecting every interior polygon vertex twice (once at the end of pair (i-1, i), once at the start of pair (i, i+1)). Project the whole vertex list once and feed those projections into the new `Interpolator.interpolateInto`. As a side-effect, the SVG output no longer contains duplicated vertices back-to-back, so the SVG snapshot tests are updated. - `Point.distanceSquared` and `Point.halfway` now do `dx * dx` instead of `pow(_, 2)` and direct multiplication instead of division. Tagged `@inline(__always)` since they sit in the deepest hot path. EqualEarth's `poly9`/`poly8` likewise pull `pow` out in favour of explicit products. - `MapBounds.pointInPolygon` swaps array subscripts for `withUnsafeBufferPointer`, which removes the per-element bounds checks that dominate Debug builds. Hoists the loop-invariant `p.x`/`p.y` to locals. - `boundarySplit` no longer pays for the `mapBounds.allIntersections` search on every (true, true) pair when the boundary is convex (`.rectangle`, `.ellipse`, or convex-bezier projections like Equal Earth and Natural Earth). A convex shape can't be exited and re-entered by a single straight segment with both endpoints inside, so the multi-crossing detector is only useful for the genuinely concave Danseiji notches. Convexity is detected once per `boundarySplit` call via a new `MapBounds.isConvex` helper. Bench (98 polygons × 5 reps, world continents): - Equal Earth 706ms → 136ms Debug | 17.5ms → 4.6ms Release - Orthographic 129ms → 35ms Debug | 14.0ms → 4.8ms Release Tests: 55 pass (3 SVG snapshots updated for de-duplicated vertices, 2 new `PerfBench` cases). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings in three commits from `main`: - c043d34 Add `inverse` function (#38) - 1fe35c6 Add tests for SVG zoom, plus add a bunch to .gitignore (#37) - 1a7b4f3 Disable unused reference sliders in Cassini example (#39) Conflicts resolved by keeping this branch's versions, since the `inverse` work and `MapBounds.contains` helper that landed on `main` were already present here (via the earlier Danseiji and boundary-split commits) — the branch versions go further: - `MapBounds+Contains.swift`: keep this branch's superset, which adds `firstIntersection`, `allIntersections`, `boundaryArc`, and `isConvex`. `contains` stays `public` so cross-module callers in `GeoDrawer` can use it. - `Projection.swift`: drop the `willWrap` requirement that came back via `main`; `boundary-aware split` (commit 34c3140 + the wrap detection in b535f8c) replaces it. - `Projections+Orthographic.swift`: drop the equivalent `willWrap` override. - `InverseTests.swift`: drop the trailing `?.0` tuple unwrap in the new click-round-trip test — `point(for:size:…)` returns `Point?` now. - `Examples/Cassini/ContentView+Model.swift`: keep the Danseiji enum cases AND extend the new `usesReferenceLatitude` / `usesReferenceLongitude` switches to cover them. Danseiji's mesh data is computed for a fixed orientation, so latitude is unused; longitude rotates the map east/west, so it's used. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
V's mesh emphasises continents over oceans; VI weighs population alongside area. Both are hand-tuned, asymmetric meshes — rotating the reference longitude shears the deformations relative to the underlying geography in a way that stops being meaningful. (IV is similarly hand-tuned but its distortions are unique enough that playing with the reference is fun, so keep that one.) V and VI now ignore the constructor's `reference` argument and pin `reference` at `(0, 0)`. The Cassini example marks them as not using the reference longitude, so that slider is disabled when V or VI is selected. New `vAndVIIgnoreReference` test in `DanseijiTests` confirms a non-zero reference produces the same projected coordinates as the centred default. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Danseiji III–VI have edge polygons that aren't symmetric around the geographic origin (the mesh data was hand-tuned that way: III's centre sits at (+0.26, +0.56), IV's at (-0.49, +1.45), V's at (+0.34, +0.25), VI's at (+0.59, +0.72)). The canvas-fitting code in `simpleTranslate` assumed the projection rectangle was `[-w/2, +w/2] × [-h/2, +h/2]` and fit `projectionSize` (computed as 2 × max(|xMin|, |xMax|) etc.) into the canvas. With asymmetric edges that meant the actual map sat off-centre and partway off-canvas, and edge insets weren't respected. Add a `visibleBounds: Rect` to `Projection` describing the rectangle in projection-radians that should be fit to the canvas. Default is `projectionSize` centred at the origin, so all existing projections are unaffected. `simpleTranslate` and `simpleUntranslate` now normalise against `visibleBounds` rather than assuming centred-at-origin. Danseiji projections override `visibleBounds` to return the `DanseijiData.edgeBounds` rectangle that the mesh parser already computed, so canvas fitting hugs the actual edge polygon for every variant. I and II are unchanged because their edges happen to be centred at the origin already. New `edgePolygonIsCentredInCanvas` and `edgeInsetsShrinkProportionally` tests run all six variants and assert opposing canvas margins match, every edge vertex is inside the canvas, and edge insets shrink the map on every side. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- The `PerfBench` struct ended up *outside* the `#if canImport(Testing)`
guard, so Linux builds (which lack swift-testing) failed compiling
with "unknown attribute 'Test'" on every benchmark method. Move the
struct back inside the guard.
- Switch the Danseiji resource declaration from `.copy("Resources")`
to `.process("Resources")`. SPM's `.copy` produces a bare directory
bundle that fails iOS CodeSign with "bundle format unrecognized,
invalid, or unsuitable" — `.process` emits a properly-structured
bundle (with Info.plist) that signs cleanly. The CSV files are still
loaded via `Bundle.module.url(forResource:withExtension:)` and the
Danseiji unit tests continue to pass.
Verified locally with `swift test` (62/62 pass), plus
`xcodebuild -sdk iphoneos` and Cassini on `iphonesimulator` both build.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Note that we now tag releases (the package is on `0.x` so far). Replace the "no tagged versions yet" branch dependency with a `from: "0.1.0"` example. - Add `GeoProjectorDanseiji` to the library list, with a short note that it's a separate product so apps that don't use it don't pull in the ~1 MB of mesh data. - Mention `inverse` and `coordinate(at:...)` as a first-class capability of the projection API (added in #38, but the README still spoke of forward projection only). Add a short example that maps a click back to lat/lon. - Drop the dangling `?.0` tuple in the projection example — the API returns `Point?` since the boundary-aware split rewrite landed. - Tidy the platform-conventions sentence (the previous version had swapped lat/lon and only mentioned macOS); call out that `GeoMapView` is `NSView` on macOS and `UIView` elsewhere. - Mention that `GeoDrawer` can also draw to a `CGContext` or to SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses part of #3, closes #10 and #21