Skip to content

Vector styles, tile work and the street-tree sample, with every module on its develop - #12

Merged
wf9a5m75 merged 57 commits into
developfrom
feature/bearing-clockwise
Nov 5, 2026
Merged

wf9a5m75 merged 57 commits into
developfrom
feature/bearing-clockwise

Conversation

@wf9a5m75

@wf9a5m75 wf9a5m75 commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

What

This merges feature/bearing-clockwise into develop.

The branch holds the work since #11:

  • vector style handoff and offline packages
  • tile size and transparent-tile handling across backends
  • the street-tree sample and its labels
  • sample pages and tests

Submodules

Every module now points at the tip of its own develop:

  • Before: the modules pointed at their own feature/bearing-clockwise branches, and ios-for-template at checkpoint/vector-style-20261010.
  • Check: for each module, everything those branches held has reached that module's develop in newer form. ios-for-template's vector-style entry point went in through ios-for-template#5.
  • Result: develop pins no unmerged branch.

This also brings in today's merged fixes:

  • Tile redraw: tiles are redrawn when a tiled marker changes (Google Maps, MapKit, ArcGIS, Mapbox, TomTom), and the tile URL carries its version (core).
  • TomTom: markers are drawn as tiles.
  • Camera stops: reported only after a move on Mapbox, and reported at all on ArcGIS 2D.
  • Panning: lighter on Longdo and OpenMobileMaps. OpenMobileMaps marker tiles are drawn at their size.
  • Labels: drawn into tiles as text, with vertical writing (labelWritingMode).

ios-sdk-core points at core develop (a899663), which contains release 1.3.2 in newer form.

Sample project

  • ios-for-googlemaps on develop requires ios-maps-sdk 11.2.0, so the sample moves to it.
  • Package.resolved keeps the resolution against the current modules.
  • In project.pbxproj, the conflicts with develop were only this branch's added test files. develop's Marker Label Position page (Add a marker label position sample page #10) merged in automatically.

Checked

  • Built the sample from this branch's state, with every submodule at the commit it points to.
  • On an iPad, the street-tree page works with MapLibre, including vertical labels.

masashi and others added 30 commits September 1, 2026 11:43
bearing を「値を増やすと地図が時計回りに回る」向きに統一し、ネイティブ SDK との変換をコアの CameraBearing に集約する。
An independent SwiftPM package depending on ios-sdk-core, not a core
change — it implements the core's TileProvider and serves through the
existing LocalTileServer, the same way ios-heatmap does, so backends
that cannot render a vector style see ordinary raster tiles.

The renderer and its XCFramework come from the mapconductor-vectortile
repo via scripts/sync-sdk.sh; only the integration layer lives here.

Verified on the iPhone 17 Pro simulator: provider -> tile server route
-> URL template -> HTTP -> PNG, 5 tests.
RenderMode.gpu / .cpu / .auto, mirroring Android. Asking for .gpu explicitly and
quietly getting the CPU is the failure that hides worst, so that case throws
rather than falling back; .auto falls back silently by design.

gpuRenders and gpuFallbacks are public for the same reason the Android ones
are: a suite once passed green while every single tile fell back.
Reports rather than asserts a ratio: it measures the platform, and pinning a
number would only fail the suite on a faster machine. It exists so the claim
behind the whole-pixel rounding in ios-sdk-core is checkable on Apple hardware
instead of inherited from the Android measurement.
android-sdk reached this from the opposite direction. There, one snapshot
observer read every state to find in-place changes, and that cost the size of
the collection on every commit — 350 ms a frame while dragging 144k markers.
iOS already pushed: each state published its changes and the collector
subscribed. The bill arrives somewhere else.

Subscribing is one `map`, one `eraseToAnyPublisher`, one `receive(on:)` and one
`sink` per state, plus the `AnyCancellable` holding them, and it does not stay
linear:

  states     sync    now
   1,000     47ms    1ms
  10,000    236ms   15ms
  50,000  2,175ms   52ms
 144,183       —   134ms

on the main actor, on a simulator. The last row is Tokyo's street trees, which
is what raised the question. Whatever the old curve was doing between 10k and
50k, it was not going to make that a number anyone would wait for.

Replacing the fingerprint chain with `objectWillChange` was measured first,
since the states are all ObservableObject and that publisher already exists. It
came out worse — 2,898 ms at 50,000. The cost is the subscription machinery,
not the chain fed into it, so the fix is to have none: `didSet` on each
`@Published` property calls a closure the collector installed.

`asFlow()` stays. Several provider controllers subscribe to it directly rather
than through the collector, and they still pay per state — around a second to
subscribe 50,000 on the same machine. That is a separate path and a separate
change.

`overlayChangePublisher()` had exactly one caller, the collector, so the
protocol requirement goes with it and the six one-line conformances become
empty.

The web SDK was measured too and needs nothing: it also pushes, one closure per
state, and 144,183 markers land in 58 ms.
Tokyo's 144,183 street trees, the sample android-sdk has had since the marker
tiling work. A harder shape than the post office set: six times as many, inside
one metropolitan area rather than spread over a country, and each species draws
its own colour, so the renderer cannot collapse two trees of different species
that land on the same pixel.

All three read the same 2.8 MB asset — a magic, three string tables, then a
fixed 20-byte record per tree, little-endian. The source is a 12 MB Shift-JIS
CSV; parsing that on a phone or in a browser would measure CSV parsing rather
than the map. The web decode is 10 ms in node for the whole file.

Species colours come out of the same golden-angle hue rotation on each
platform, with the last index — the bucket holding every species under a
hundred trees — grey rather than competing with a named species for a hue. A
species is the same colour on all three.

The basemap is the stripped-down style the Android sample uses: land, water,
the major roads and the place names, with the finer streets from zoom 13. It is
MapLibre's own design type, so it applies there and the page runs on whatever
basemap the other providers bring.

Verified by rendering, not by compiling. iOS has a screenshot test that opens
the page and waits for the trees, because the failures here — asset missing
from the bundle, style URL unresolved — all look like "the map draws and the
trees do not" and none of them throw. It caught the first cut passing
declutterPx scaled by the content scale, which thinned a retina screen to a
third of the density Android shows.
The module had neither a README nor a licence, unlike ios-heatmap and
the rest.

There is no SwiftUI wrapper here as there is on Android, so the usage
section is the two-route wiring itself — register groundTiles and
labelTiles, hand the label overlay over on onGlyphsLoaded with a fresh
cacheKey, close the provider — taken from the sample app rather than
invented.

Records why tileSize is limited to 512 and 1024 (MapKit's grid is built
from 256 and anything else draws a blank map; 768 was tried) and that
raising it does not make labels larger relative to the map.
CI here was api-surface and deploy only. Nothing ran the tests, so a
change that kept every signature and broke what was behind it went
through green.

Not `swift test`: it does not build. ios-sdk-core depends on
TilePng.xcframework, which carries ios-arm64 and ios-arm64-simulator
only, so building for macOS fails on the missing slice. Nobody noticed
because nothing was running it. The job targets a simulator instead.

The simulator is chosen at runtime rather than named. Pinning "iPhone 16"
means a runner image change fails the build for a reason that has nothing
to do with the code.

Xcode is pinned, from api/TOOLCHAIN. The api baseline records what the
Swift compiler emits, so a different Xcode produces thousands of lines of
diff with the sources untouched; failing with "wrong toolchain" beats
failing with a 3,000-line diff.

Also picks up the Web Mercator conformance test, which holds this
platform to the same answers as android-sdk and react-sdk.
SPM test targets cannot run tool-hosted on a device:

    Cannot test target "MapConductorCoreTests" on "iPad":
    Tool-hosted testing is unavailable on device destinations.

So the numbers behind "iOS is slower than Android" were coming from a
simulator running on an M-series Mac, which lies in the direction that
matters. The Benchmarks target is hosted by the sample app and runs the
same fixtures on the hardware.

What it found: 144,183 markers at zoom 9 took 281 ms on an iPad Pro 11
and 109 ms on a Pixel 5a. The core fix this pins brings it to 64 ms.

The street tree icons drop from 14 to 10, and the parameter is `sizePt`
rather than `sizePx` because that is what it always was — a
`UIGraphicsImageRenderer` makes device-scale pixels from a point size, so
passing points is what makes the icon density-independent. android-sdk
was the odd one out, building its bitmap at raw pixels, and has been
brought here rather than the other way round.
Carries the in-flight vectortile work alongside the marker fixes:
renderTile(request:isCancelled:) so an abandoned map stops paying for
doomed tiles, a disk cache keyed by style/generation, bundled glyphs
and the split ground/labels layer tests.
Benchmarks (a test-host target, because SPM tests cannot run on device)
measure what the simulator lies about: tile cost per zoom, GPU vs CPU
tile parity (DeviceSeamReproBench is the regression net for the Metal
rasterizer's misdraw), seam screenshots, phase breakdowns.

FrameGapProbe logs dropped display-link frames per second and
MainThreadHangSampler walks the main thread's frame-pointer chain when
it stalls over 300ms -- xctrace cannot attach to this device, so the
app carries its own profiler. Debug builds only.

The street tree page reads MAPCONDUCTOR_SAMPLE_INIT_CAMERA for
deterministic repro at a reported spot, and MAPCONDUCTOR_SAMPLE_TILE_DEBUG
bakes a gray tile grid with x/y/z labels into marker tiles so a seam
report can name the exact tile pair.
The probe wraps another provider, and its job is to observe, not to
decide. Swallowing the throw would turn a failed tile into an empty
one -- which is exactly the confusion the core change removes.
The page gets an "As basemap" toggle covering MapLibre (bundled blank style), Google, ArcGIS, Mapbox, TomTom, MapTiler, Longdo and Open Mobile Maps; MapKit, HERE and Mappls have no design that draws nothing. Also syncs the vector tile binding (output version 17, close() waits for in-flight native calls, fetch slots only for real transfers) and bumps the provider modules for their None designs.
…or glyph handovers

The provider keeps the straight-RGBA ground of every tile the GPU drew (64 MB, NSCache) and, for a full tile whose glyphs arrived later, composites the labels onto it instead of drawing the geometry again. The sample's status line now also reports the summed serve time and the GPU render count, which is what told the merged-layer experiment apart from the two-layer one (measured: one merged layer waits for nine neighbours and glyphs before showing anything, first paint three times slower; the split stays).
… layer over instead of replacing it

The sample replaced its label layer on every glyph generation (up to a dozen in one load), which blanked the labels each time; the replacement now stacks above the old one, which goes 0.9 s later, as android-vectortile and react-vectortile already did.
SourceTileDiskCache, 128 MB by default, under the asset cache directory; the in-memory source cache is filled from it. Bumps core (camera semantics), arcgis (podspec) and tomtom (0.74.0).
…kages

VectorStyleHandoff serves a style to a map that registers VectorStyleSupport,
optionally from an OfflinePackage through the tile server's file route.
OfflinePackage and OfflinePackageDownloader mirror android-vectortile's
layout, so a package made on one platform serves the other. The provider's
fetch closure may throw: an offline refusal or a transport failure is not
remembered as "no tile", and a tile drawn short of a source is not kept.

Sample: an Offline Map page with a select-area mode, Download, an
airplane-mode toggle and fetch counters; the vector tile page takes the
direct path when the backend offers it.
VectorTileProvider and VectorStyleHandoff now take the same rules
VectorStyle does, so a style can be adjusted on the vector-tile path too:
a rule that hides a role leaves those layers out of the document the
renderer is given. The handoff reports through a new diagnostics — rules
cannot reach a style served from an offline package, and saying so beats
a silently unadjusted map.

fix: key rendered tiles by the document actually drawn

The disk cache hashed the style as the caller wrote it, so a provider
built with rules wrote its adjusted tiles under the author's key and
going back to no rules served them forever: no roads, gpu=0, and a trace
that read 'mounted'. Nothing about it looked wrong.

examples: draw/skip switches and a ColorPicker per category on the vector
tile page, built from those rules; labels can be switched off but not
tinted. The page's tile URL carries a style generation, without which a
restyle at the same tile size never reaches the map. The palette and the
hidden set are settable at launch, because a UI test that drove the
system colour sheet would be a test of the sheet.

chore: ios-icons baseline re-dumped (separator spelling only).
api-surface.sh now dumps and checks ios-for-openmobilemaps. maps-core 4.0.0 does not
build with library evolution until openmobilemaps/maps-core#931 lands, so the
check needs the local maps-core with that change.

ios-vectorstyle and ios-vectortile require the published core from 1.3.2, like
the other modules.
Labels each tree with its species once the camera stops at zoom 16 or more,
to see how fast the SDK relabels 144k markers.

- Labels go on the sides of a north-south street and above or below an
  east-west one, the same side along a whole street, and only where they
  cover neither another label nor another tree's dot.
- Labels above or below a tree are vertical (labelWritingMode vertical-rl),
  so a row of trees along a horizontal street can each have a name.
- Labelling rewrites the icon of the trees that changed in place; the marker
  list is not replaced.

Needs ios-sdk-core with labelled ImageIcon and labelWritingMode
(ios-sdk-core#23).
The modules pointed at their own feature/bearing-clockwise branches (the
template at checkpoint/vector-style-20261010). Everything those branches
held has reached each module's develop, in newer form, so this branch now
points at develop throughout and can itself go to develop without pinning
unmerged branches. That also brings in today's fixes: tiles redrawn when a
tiled marker changes, camera stops on Mapbox and ArcGIS 2D, TomTom marker
tiles, lighter panning on Longdo and OpenMobileMaps, and labels drawn into
tiles with vertical writing.

ios-for-googlemaps on develop requires ios-maps-sdk 11.2.0, so the sample
moves to it; Package.resolved is the resolution that follows.
# Conflicts:
#	ios-sdk-core
#	samples/MapConductorSampleApp/MapConductorSampleApp.xcodeproj/project.pbxproj
#	samples/MapConductorSampleApp/MapConductorSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@wf9a5m75
wf9a5m75 merged commit 4de9406 into develop Nov 5, 2026
1 check passed
@wf9a5m75
wf9a5m75 deleted the feature/bearing-clockwise branch November 5, 2026 11:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant