Skip to content

perf(drawer): composite same cylindrical projections without reprojecting - #47

Merged
nighthawk merged 4 commits into
feat/svg-single-image-base-mapfrom
feat/svg-mercator-tiles-direct
Oct 27, 2026
Merged

nighthawk merged 4 commits into
feat/svg-single-image-base-mapfrom
feat/svg-mercator-tiles-direct

Conversation

@nighthawk

@nighthawk nighthawk commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Stacked on #46. Part of letting Maparoni's street tiles render in any projection (maparoni/lambdaroni#290). Once other projections reproject the tiles, a map in the tiles' own projection should still lay them down as they are.

Since #45, both raster samplers send every output pixel through the drawer's inverse and then the source's forward projection. That applies to Mercator tiles on a Mercator map too.

An equator-aligned cylindrical projection puts x = k·(λ − λ₀), with y depending on latitude alone. Two projections of the same type therefore differ only by a scale and a shift of x.

  • Projection gains an optional longitudeScale, which is k. Mercator, Gall–Peters and Equirectangular have one, and every other projection has nil.
  • cylindricalTransform(to:) returns the scale and shift between two projections of the same type.
  • The tile sampler and the single-image sampler both use it when it applies, with no trigonometry per pixel. Everything else still takes the round trip.

ProjectionMode also becomes CaseIterable, so GeoLogicKit can list the modes without a mirror enum (maparoni/GeoLogicKit#244).

The tests check the transform against project for all three projections, with mixed centre longitudes and phiOne values. They also compare direct and round-trip pixels for both samplers, on zoomed, re-centred maps across the antimeridian. Bilinear sampling gives identical pixels. Nearest sampling can differ on one row, where pixel centres land exactly on a source pixel edge and the round trip's log and atan round the other way.

Testing: swift test. No manual steps. The Lambdaroni PR covers end-to-end renders on dev.

🤖 Generated with Claude Code

…ojecting

The SVG tile compositor sent every output pixel through the drawer's
inverse projection and the tile source's forward projection, even when
both are Mercator. Mercator on Mercator differs only by a longitude
shift, so each pixel now maps to the tiles by a shift and a scale. Any
other projection still takes the round trip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread Sources/GeoDrawer/GeoDrawer+TiledBaseMapBitmap.swift Outdated
nighthawk and others added 2 commits October 28, 2026 09:09
The direct path was Mercator-only. It's really a property of the
projection: an equator-aligned cylindrical projection puts x at
`longitudeScale × (λ − reference.x)` and y on latitude alone, so two of
the same type differ by a scale and a shift of x. `Projection` gains an
optional `longitudeScale` (Mercator, Gall–Peters, Equirectangular) and
`cylindricalTransform(to:)`. Both the tile and the single-image samplers
use it, so an equirectangular map of an equirectangular Blue Marble
skips the round trip too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
So callers that take a projection by name (GeoLogicKit, Lambdaroni's MCP
tools) can list the modes and look one up without a mirror enum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nighthawk nighthawk changed the title perf(drawer): composite Mercator tiles on a Mercator map without reprojecting perf(drawer): composite same cylindrical projections without reprojecting Oct 27, 2026
@nighthawk

nighthawk commented Oct 27, 2026 •

Copy link
Copy Markdown
Member Author

Suggested squash-merge message:

perf(drawer): composite same cylindrical projections without reprojecting

- Projection gains an optional `longitudeScale` (Mercator, Gall–Peters,
  Equirectangular) and `cylindricalTransform(to:)`
- Tile and single-image samplers scale and shift instead of the
  inverse/forward round trip when map and raster share a cylindrical
  projection, e.g. Mercator tiles on a Mercator map
- ProjectionMode is CaseIterable
- Equirectangular's width uses its standard parallel in radians; the
  Cassini example no longer converts it twice

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

init(reference:phiOne:) converted phiOne from degrees for project and
inverse, but computed projectionSize from the degree value, so any
non-zero standard parallel gave the wrong width (negative for 60°). The
Cassini example also converted its value to radians before passing it
in, so it was converted twice; it passes degrees now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nighthawk
nighthawk merged commit b54f350 into feat/svg-single-image-base-map Oct 27, 2026
@nighthawk
nighthawk deleted the feat/svg-mercator-tiles-direct branch October 27, 2026 22:58
@nighthawk nighthawk self-assigned this Oct 27, 2026
@nighthawk nighthawk added the under-the-hood Code refactoring and other under-the-hood tweaks label Oct 27, 2026
nighthawk added a commit that referenced this pull request Oct 28, 2026
…ting (#47)

- Projection gains an optional `longitudeScale` (Mercator, Gall–Peters,
  Equirectangular) and `cylindricalTransform(to:)`
- Tile and single-image samplers scale and shift instead of the
  inverse/forward round trip when map and raster share a cylindrical
  projection, e.g. Mercator tiles on a Mercator map
- ProjectionMode is CaseIterable
- Equirectangular's width uses its standard parallel in radians; the
  Cassini example no longer converts it twice

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

under-the-hood Code refactoring and other under-the-hood tweaks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant