-
-
Notifications
You must be signed in to change notification settings - Fork 671
feat(layers): show a tiled archive as a group of its source layers #2065
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
3ac6ec9
c60e19c
5cf921a
87ff209
1fc142e
8278159
ee7dee9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,27 @@ | ||
| import { | ||
| controlRendersLayer, | ||
| DEFAULT_LAYER_STYLE, | ||
| type GeoLibreLayer, | ||
| type ExternalNativePaintBridge, | ||
| generatorCircleRadiusValue, | ||
| geojsonHasZCoordinates, | ||
| getExternalNativePaintBridge, | ||
| type LayerStyle, | ||
| pluginOwnsPaint, | ||
| proportionalRadiusExpression, | ||
| ruleBasedVisibilityFilter, | ||
| shouldUseTiledRendering, | ||
| styleValue, | ||
| type ExternalNativePaintBridge, | ||
| type GeoLibreLayer, | ||
| type LayerStyle, | ||
| validateMapExpression, | ||
| } from "@geolibre/core"; | ||
| import { normalizePMTilesUrl, PMTILES_PROTOCOL, pmtilesVectorLayerId } from "./pmtiles-layer"; | ||
| import { | ||
| normalizePMTilesUrl, | ||
| PMTILES_PROTOCOL, | ||
| pmtilesControlLayerId, | ||
| pmtilesIdNamesSourceLayer, | ||
| pmtilesLayerKinds, | ||
| pmtilesVectorLayerId, | ||
| } from "./pmtiles-layer"; | ||
| import { encodeVectorTileLayerPart } from "./vector-tile-layer-ids"; | ||
| import { addProtocol, config } from "maplibre-gl"; | ||
| import type { GeoJSON } from "geojson"; | ||
|
|
@@ -912,18 +919,9 @@ function ensurePMTilesExternalLayer( | |
| } | ||
|
|
||
| for (const sourceLayer of sourceLayers) { | ||
| const fillId = getPMTilesNativeLayerId( | ||
| nativeLayerIds, | ||
| pmtilesVectorLayerId(sourceId, sourceLayer, "fill"), | ||
| ); | ||
| const lineId = getPMTilesNativeLayerId( | ||
| nativeLayerIds, | ||
| pmtilesVectorLayerId(sourceId, sourceLayer, "line"), | ||
| ); | ||
| const circleId = getPMTilesNativeLayerId( | ||
| nativeLayerIds, | ||
| pmtilesVectorLayerId(sourceId, sourceLayer, "circle"), | ||
| ); | ||
| const fillId = getPMTilesNativeLayerId(nativeLayerIds, sourceId, sourceLayer, "fill"); | ||
| const lineId = getPMTilesNativeLayerId(nativeLayerIds, sourceId, sourceLayer, "line"); | ||
| const circleId = getPMTilesNativeLayerId(nativeLayerIds, sourceId, sourceLayer, "circle"); | ||
|
|
||
| ensureLayer( | ||
| map, | ||
|
|
@@ -1116,22 +1114,12 @@ function getPMTilesRenderableSourceLayers( | |
| ): string[] { | ||
| const sourceLayers = getPMTilesSourceLayers(layer); | ||
| const savedSourceLayers = sourceLayers.filter((sourceLayer) => | ||
| hasPMTilesNativeSourceLayer(nativeLayerIds, sourceId, sourceLayer), | ||
| pmtilesIdNamesSourceLayer(nativeLayerIds, sourceId, sourceLayer), | ||
| ); | ||
|
|
||
| return savedSourceLayers.length > 0 ? savedSourceLayers : sourceLayers; | ||
| } | ||
|
|
||
| function hasPMTilesNativeSourceLayer( | ||
| nativeLayerIds: string[], | ||
| sourceId: string, | ||
| sourceLayer: string, | ||
| ): boolean { | ||
| return ["fill", "line", "circle"].some((kind) => | ||
| nativeLayerIds.includes(pmtilesVectorLayerId(sourceId, sourceLayer, kind)), | ||
| ); | ||
| } | ||
|
|
||
| function getPMTilesSourceLayers(layer: GeoLibreLayer): string[] { | ||
| const sourceLayers = layer.source.sourceLayers ?? layer.metadata.sourceLayers; | ||
| return Array.isArray(sourceLayers) | ||
|
|
@@ -1142,8 +1130,20 @@ function getPMTilesSourceLayers(layer: GeoLibreLayer): string[] { | |
| : []; | ||
| } | ||
|
|
||
| function getPMTilesNativeLayerId(nativeLayerIds: string[], fallbackId: string): string { | ||
| return nativeLayerIds.find((nativeLayerId) => nativeLayerId === fallbackId) ?? fallbackId; | ||
| /** | ||
| * The id this source layer is already drawn under, or the one to draw it under. Both schemes are | ||
| * consulted — see `pmtilesControlLayerId` — or a control-added layer gets a second set over it. | ||
| */ | ||
| function getPMTilesNativeLayerId( | ||
| nativeLayerIds: string[], | ||
| sourceId: string, | ||
| sourceLayer: string, | ||
| kind: (typeof pmtilesLayerKinds)[number], | ||
| ): string { | ||
| const encoded = pmtilesVectorLayerId(sourceId, sourceLayer, kind); | ||
| if (nativeLayerIds.includes(encoded)) return encoded; | ||
| const raw = pmtilesControlLayerId(sourceId, sourceLayer, kind); | ||
| return nativeLayerIds.includes(raw) ? raw : encoded; | ||
| } | ||
|
|
||
| function isWaybackExternalRasterLayer(layer: GeoLibreLayer): boolean { | ||
|
|
@@ -3629,10 +3629,16 @@ function moveLayer(map: maplibregl.Map, id: string, beforeId?: string): void { | |
| } | ||
| } | ||
|
|
||
| /** The external sources a set of layers draws from — what a removal must not pull out from under. */ | ||
| export function externalSourceIdsFor(layers: readonly GeoLibreLayer[]): Set<string> { | ||
| return new Set(layers.flatMap((layer) => getExternalSourceIds(layer))); | ||
| } | ||
|
|
||
| export function removeLayerFromMap( | ||
| map: maplibregl.Map, | ||
| layerId: string, | ||
| layer?: GeoLibreLayer, | ||
| survivingSourceIds?: ReadonlySet<string>, | ||
| ): void { | ||
| // Drop cached paint-bridge state so a later layer reusing this id never | ||
| // skips a fresh opacity/visibility apply against a new bridge. | ||
|
|
@@ -3667,14 +3673,35 @@ export function removeLayerFromMap( | |
| ]) { | ||
| if (map.getLayer(id)) map.removeLayer(id); | ||
| } | ||
| // An archive's source layers share one source, so it goes only once nothing draws from it. The | ||
| // store half covers a layer that survives this sync; the map half covers its siblings inside one | ||
| // — deleting a folder removes its children in a single pass, and MapLibre reports removing a | ||
| // source still under a style layer as an error the user can do nothing about. | ||
| const stillInUse = survivingSourceIds ?? new Set<string>(); | ||
| // Only an external source can be shared — the derived ids below are this layer's alone — so the | ||
| // map is asked at most once, and only when a shareable source is actually up for removal. Walked | ||
| // layer by layer rather than read from `getStyle()`, which serializes the whole document. | ||
| const shareable = new Set(getExternalSourceIds(layer)); | ||
| let drawnSources: Set<string> | undefined; | ||
| const stillDrawn = (src: string): boolean => { | ||
| drawnSources ??= new Set( | ||
| map | ||
| .getLayersOrder() | ||
| .map((styleLayerId) => map.getLayer(styleLayerId)?.source) | ||
| .filter((source): source is string => typeof source === "string"), | ||
| ); | ||
| return drawnSources.has(src); | ||
| }; | ||
|
Comment on lines
+3681
to
+3694
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Performance: this scan now runs for every removal of any layer with an external source, not just shared PMTiles archives.
Removing many such layers in one pass (e.g. deleting a project full of external-source layers, or Confidence: medium — likely fine in practice given typical layer counts, but worth confirming this doesn't show up as a regression on large projects/imports with many external layers.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Low confidence — minor perf note.
|
||
| for (const src of [ | ||
| ...getExternalSourceIds(layer), | ||
| sourceId(layerId), | ||
| labelSourceId(layerId), | ||
| invertedSourceId(layerId), | ||
| generatorSourceId(layerId), | ||
| ]) { | ||
| if (src && map.getSource(src)) map.removeSource(src); | ||
| if (!src || stillInUse.has(src) || !map.getSource(src)) continue; | ||
| if (shareable.has(src) && stillDrawn(src)) continue; | ||
| map.removeSource(src); | ||
| } | ||
| // Drop radius-override tracking for the removed layer's native ids so a | ||
| // later layer reusing an id never inherits a stale restore. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
survivingSourceIdsis optional and only ever passed byMapController.syncLayers(viaexternalSourceIdsFor(layers)). The other two call sites that can remove a shared PMTiles-archive layer —headless.ts'screateLayerSync().sync()/.dispose()— never pass it, so those paths lean entirely on thestillDrawnmap-style scan below.That scan does protect a straightforward single removal, but
createLayerSync'srebuildFromreorder loop can callremoveLayerFromMapfor several siblings of the same split archive back-to-back within onesync()call purely to reposition them (they're re-added moments later viasyncLayer). If the last sibling processed in that inner loop is also the last one still drawing from the shared source at that instant, the source gets removed and then immediately recreated oncesyncLayerre-adds it — a same-tick self-heal, but it can needlessly evict and refetch a shared archive's tiles on a reorder that touches more than one of its layers, which couldn't happen before this PR since every PMTiles layer had its own unshared source.createPMTilesArchiveLayersandcreateLayerSyncare both public exports (@geolibre/map,@geolibre/map/headless), so an external headless consumer combining them would hit this. Worth threadingexternalSourceIdsFor(layers)throughheadless.tstoo, mirroringmap-controller.ts. Confidence: medium-low — this is a same-tick self-heal (not a stuck/broken state) and isn't covered by tests either way.