Skip to content

Render single-image base maps (Blue Marble) in SVG output on every platform - #46

Merged
nighthawk merged 3 commits into
mainfrom
feat/svg-single-image-base-map
Oct 28, 2026
Merged

nighthawk merged 3 commits into
mainfrom
feat/svg-single-image-base-map

Conversation

@nighthawk

Copy link
Copy Markdown
Member

Why

GeoProjector#45 taught drawSVG to composite tiled base maps in pure Swift so the Lambda can drape MapTiler tiles under Maparoni's renders. The single-image BaseMap (NASA Blue Marble / Black Marble in equirectangular) still lived only in the CoreGraphics code and was skipped by the SVG writer. World- and continent-scale renders, where street tiles look busy, need the Marble look on Linux too, and that is the blocker for the thumbnails and projected-view-mode directions in Maparoni.

What

  • The inverse-projection sampler moves to a pure-Swift renderBaseMapBitmap, mirroring the tile compositor. The CoreGraphics renderer wraps its bitmap in a CGImage, so there is one sampler for both paths. Works with any target projection (orthographic, Equal Earth, Mercator, ...), since the pixel lookup goes through projection.inverse and the source projection's forward transform.
  • drawSVG embeds .baseMap as a base64 PNG <image> at the drawer's size times pixelDensity, in content order alongside .tiledBaseMap.
  • BaseMapImage.decode(pngData:) and BaseMap(pngData:) decode PNGs through swift-png on every platform. The README documents that Linux has no JPEG decoder (convert NASA's JPEG once) and the memory cost: 4096×2048 is about 32 MB resident.
  • Tests: BaseMapSVGTests renders a synthetic quadrant-coloured equirectangular image under a world-scale orthographic and a Mercator drawer, decodes the embedded PNG and checks that the colours land where the inverse projection says, that corners outside the globe are transparent, that pixelDensity scales the raster, and that the PNG decoder round-trips. The existing CoreGraphics BaseMapTests still pass through the refactored wrapper.

Verified with swift test on macOS and in swift:6.3-amazonlinux2023 (82 tests pass). No consumer pins bumped; GeoLogicKit and Lambdaroni follow up separately.

Manual testing

Render a real Blue Marble on Linux, e.g. in the Lambda or a scratch executable:

let marble = try GeoDrawer.BaseMapImage.decode(pngData: Data(contentsOf: url))!
let drawer = GeoDrawer(size: .init(width: 800, height: 800), projection: Projections.Orthographic())
let svg = drawer.drawSVG([.baseMap(.init(image: marble))], mapBackground: nil, mapOutline: .init(red: 0, green: 0, blue: 0, alpha: 1))

Open the SVG (or rsvg-convert it): the globe should show the Americas facing you, continents in the right place relative to the outline, and the corners transparent.

🤖 Generated with Claude Code

…atform

Move the inverse-projection sampler for `BaseMap` out of the Apple-only
CoreGraphics code into a pure-Swift `renderBaseMapBitmap`, the way the
tile compositor already works, and have `drawSVG` embed the result as a
base64 PNG `<image>` next to tiled base maps. The CoreGraphics renderer
now wraps that bitmap in a `CGImage`, so both paths share one sampler.

`BaseMapImage.decode(pngData:)` and `BaseMap(pngData:)` decode a PNG
via swift-png on any platform; the README notes that Linux has no JPEG
decoder (convert NASA's Blue Marble once) and what a 4096×2048 image
costs in memory.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…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>
@nighthawk nighthawk added enhancement New feature or request under-the-hood Code refactoring and other under-the-hood tweaks labels Oct 27, 2026
@nighthawk nighthawk self-assigned this Oct 27, 2026
…#48)

- MapBounds.crossings(atY:) gives where a polygon boundary crosses a
  row; the tile and single-image samplers test each row's pixels against
  it instead of walking the polygon per pixel
- Danseiji's inverse tests its edge through a precomputed InsideGrid;
  only cells the edge crosses walk the polygon
- Danseiji IV bounds plus inverse over 1.25M points: 1.35 s to 0.07 s
  on an M-series Mac, single-threaded

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@nighthawk
nighthawk merged commit a1a59c4 into main Oct 28, 2026
7 checks passed
@nighthawk
nighthawk deleted the feat/svg-single-image-base-map branch October 28, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request 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