Repository navigation
Vector styles, tile work and the street-tree sample, with every module on its develop - #12
Merged
Merged
Conversation
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
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.
What
This merges
feature/bearing-clockwiseinto develop.The branch holds the work since #11:
Submodules
Every module now points at the tip of its own develop:
feature/bearing-clockwisebranches, and ios-for-template atcheckpoint/vector-style-20261010.This also brings in today's merged fixes:
labelWritingMode).ios-sdk-core points at core develop (
a899663), which contains release 1.3.2 in newer form.Sample project
Package.resolvedkeeps the resolution against the current modules.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