Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,9 @@ export function BasemapExtractPanel({ open, onClose, mapControllerRef }: Basemap
setBasemapStyleUrl(registerOfflineBasemapStyle(layerId, style));
trackStyledBasemap(layerId, `${layerId}.pmtiles`);
} else {
// One layer, deliberately: an extract is a backdrop to draw over, not the thing being
// inspected, so it stays a single row rather than being split per source layer the way an
// archive added from the PMTiles control or a STAC asset is.
addLayer(
createPMTilesStoreLayer({
id: layerId,
Expand Down
11 changes: 8 additions & 3 deletions packages/map/src/headless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/
import type { GeoLibreLayer } from "@geolibre/core";
import type * as maplibregl from "maplibre-gl";
import { removeLayerFromMap, syncLayer } from "./layer-sync";
import { externalSourceIdsFor, removeLayerFromMap, syncLayer } from "./layer-sync";
import { installMapTransformCompat as _installMapTransformCompat } from "./map-transform-compat";
export { installMapTransformCompat } from "./map-transform-compat";

Expand All @@ -36,8 +36,13 @@ export function createLayerSync(map: maplibregl.Map): LayerSync {
sync(layers) {
const nextIds = new Set(layers.map((layer) => layer.id));
const previousById = new Map(synced.map((layer) => [layer.id, layer]));
// Every layer in `layers` is on the map when this sync ends — including the ones the reorder
// loop below takes off and puts straight back — so a source any of them draws from stays.
const survivingSourceIds = externalSourceIdsFor(layers);
for (const previous of synced) {
if (!nextIds.has(previous.id)) removeLayerFromMap(map, previous.id, previous);
if (!nextIds.has(previous.id)) {
removeLayerFromMap(map, previous.id, previous, survivingSourceIds);
}
}

// Input order is bottom-to-top: each addLayer without an anchor lands on
Expand All @@ -60,7 +65,7 @@ export function createLayerSync(map: maplibregl.Map): LayerSync {
}
for (let index = rebuildFrom; index < layers.length; index += 1) {
const previous = previousById.get(layers[index].id);
if (previous) removeLayerFromMap(map, layers[index].id, previous);
if (previous) removeLayerFromMap(map, layers[index].id, previous, survivingSourceIds);
}

for (const layer of layers) syncLayer(map, layer);
Expand Down
44 changes: 38 additions & 6 deletions packages/map/src/layer-sync.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
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,
pmtilesLayerKinds,
pmtilesVectorLayerId,
} from "./pmtiles-layer";
import { encodeVectorTileLayerPart } from "./vector-tile-layer-ids";
import { addProtocol, config } from "maplibre-gl";
import type { GeoJSON } from "geojson";
Expand Down Expand Up @@ -1127,7 +1132,7 @@ function hasPMTilesNativeSourceLayer(
sourceId: string,
sourceLayer: string,
): boolean {
return ["fill", "line", "circle"].some((kind) =>
return pmtilesLayerKinds.some((kind) =>
nativeLayerIds.includes(pmtilesVectorLayerId(sourceId, sourceLayer, kind)),
);
}
Expand Down Expand Up @@ -3629,10 +3634,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>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

survivingSourceIds is optional and only ever passed by MapController.syncLayers (via externalSourceIdsFor(layers)). The other two call sites that can remove a shared PMTiles-archive layer — headless.ts's createLayerSync().sync()/.dispose() — never pass it, so those paths lean entirely on the stillDrawn map-style scan below.

That scan does protect a straightforward single removal, but createLayerSync's rebuildFrom reorder loop can call removeLayerFromMap for several siblings of the same split archive back-to-back within one sync() call purely to reposition them (they're re-added moments later via syncLayer). 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 once syncLayer re-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.

createPMTilesArchiveLayers and createLayerSync are both public exports (@geolibre/map, @geolibre/map/headless), so an external headless consumer combining them would hit this. Worth threading externalSourceIdsFor(layers) through headless.ts too, mirroring map-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.

): void {
// Drop cached paint-bridge state so a later layer reusing this id never
// skips a fresh opacity/visibility apply against a new bridge.
Expand Down Expand Up @@ -3667,14 +3678,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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

shareable is populated from getExternalSourceIds(layer), which is non-empty for any layer kind that sets metadata.sourceId/sourceIds — MBTiles, vector-tile, Esri Wayback, 3D Tiles, etc. (many plugins set this). For all of those, stillDrawn now does an O(current style layers) walk (getLayersOrder() + getLayer() per id) on every single removal, even though most of these layer kinds don't actually share a source with siblings — only the new PMTiles-archive split case does.

Removing many such layers in one pass (e.g. deleting a project full of external-source layers, or dispose() in headless.ts) turns what was an O(1)-per-removal source cleanup into effectively O(n²) for n external-source layers, since each removal re-derives drawnSources from scratch (the drawnSources cache is scoped per removeLayerFromMap call, not shared across the batch).

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low confidence — minor perf note.

stillDrawn is lazily memoized within one removeLayerFromMap call, but when a whole archive (or any group of layers sharing an external source) is removed together, removeLayerFromMap runs once per sibling in a loop, and each call recomputes map.getLayersOrder() plus a getLayer(...).source lookup for every style layer on the map. For a large map (many layers) and an archive split into many source layers, that's O(siblings × total map layers). The comment above already explains why getStyle() was avoided in favor of this walk, so this is likely an accepted tradeoff — just flagging in case a large archive on a busy map turns out to be noticeably slow to delete.

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.
Expand Down
5 changes: 4 additions & 1 deletion packages/map/src/map-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
} from "./geojson-loader";
import {
mbtilesStyleLayerIds,
externalSourceIdsFor,
removeLayerFromMap,
styleValuesEqual,
syncLayer,
Expand Down Expand Up @@ -1242,9 +1243,11 @@ export class MapController {
const nextIds = layers.map((l) => l.id);
const nextIdSet = new Set(nextIds);
const previousLayers = new Map(this.syncedLayers.map((layer) => [layer.id, layer]));
// Built once for the whole pass: every removal below asks the same question of the same list.
const survivingSourceIds = externalSourceIdsFor(layers);
for (const id of this.layerIds) {
if (!nextIdSet.has(id)) {
removeLayerFromMap(map, id, previousLayers.get(id));
removeLayerFromMap(map, id, previousLayers.get(id), survivingSourceIds);
}
}

Expand Down
65 changes: 60 additions & 5 deletions packages/map/src/pmtiles-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export function normalizePMTilesUrl(url: string): string {
return url.startsWith(`${PMTILES_PROTOCOL}://`) ? url : `${PMTILES_PROTOCOL}://${url}`;
}

/** The MapLibre layers one source layer is drawn with. */
export const pmtilesLayerKinds = ["fill", "line", "circle"] as const;

export function pmtilesVectorLayerId(sourceId: string, sourceLayer: string, kind: string): string {
return `${sourceId}-${encodeVectorTileLayerPart(sourceLayer)}-${kind}`;
}
Expand All @@ -39,7 +42,7 @@ export function pmtilesNativeLayerIds(
return [`${sourceId}-raster`];
}
return sourceLayers.flatMap((sourceLayer) =>
["fill", "line", "circle"].map((kind) => pmtilesVectorLayerId(sourceId, sourceLayer, kind)),
pmtilesLayerKinds.map((kind) => pmtilesVectorLayerId(sourceId, sourceLayer, kind)),
);
}

Expand All @@ -61,6 +64,8 @@ export interface PMTilesStoreLayerOptions {
sourceLayerColors?: Record<string, string>;
/** The MapLibre ids a control created itself; derived from the naming scheme otherwise. */
nativeLayerIds?: readonly string[];
/** The MapLibre source to draw from, when it is not this layer's own — a shared archive. */
sourceId?: string;
}

/**
Expand All @@ -70,6 +75,7 @@ export interface PMTilesStoreLayerOptions {
*/
export function createPMTilesStoreLayer(options: PMTilesStoreLayerOptions): GeoLibreLayer {
const { id, name, tileType } = options;
const sourceId = options.sourceId ?? id;
const sourceLayers = [...options.sourceLayers];
const url = normalizePMTilesUrl(options.url);
const fillColor =
Expand All @@ -81,7 +87,7 @@ export function createPMTilesStoreLayer(options: PMTilesStoreLayerOptions): GeoL
name,
type: "pmtiles",
source: {
sourceId: id,
sourceId,
sourceLayers,
tileType,
type: tileType === "raster" ? "raster" : "vector",
Expand All @@ -104,18 +110,67 @@ export function createPMTilesStoreLayer(options: PMTilesStoreLayerOptions): GeoL
metadata: {
externalNativeLayer: true,
nativeLayerIds: [
...(options.nativeLayerIds ?? pmtilesNativeLayerIds(id, tileType, sourceLayers)),
...(options.nativeLayerIds ?? pmtilesNativeLayerIds(sourceId, tileType, sourceLayers)),
],
pickable: options.pickable ?? true,
sourceId: id,
sourceId,
sourceKind: "pmtiles-url",
...(options.sourceLayerColors ? { sourceLayerColors: options.sourceLayerColors } : {}),
sourceLayers,
tileType,
},
Comment on lines 156 to 166

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low confidence / quality note: this drops sourceLayerColors from the layer's metadata entirely (previously kept via ...(options.sourceLayerColors ? { sourceLayerColors: options.sourceLayerColors } : {})). A repo-wide search shows nothing currently reads metadata.sourceLayerColors (only options.sourceLayerColors at layer-creation time, to seed style.fillColor for the layer's first/only source layer), so this looks like safe dead-metadata cleanup given the new per-source-layer split does the real color work. Flagging only because it changes the persisted GeoLibreLayer.metadata shape for PMTiles layers, and the full archive-assigned color map is no longer recoverable from a single combined layer (STAC/basemap-extract path) after this — e.g. no "restore assigned colors" affordance would be possible without it. Worth a sanity check that no plugin-facing code path expected it.

};
Comment on lines 156 to 167

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quality (medium confidence): this hunk drops the sourceLayerColors field that used to be written into metadata here (removed line: ...(options.sourceLayerColors ? { sourceLayerColors: options.sourceLayerColors } : {})), without replacing its purpose anywhere else.

The PR description says the "archive stays one layer" paths (STAC panel, offline basemap extract) "paint each [source layer] in the colour the archive assigned (assignedSourceLayerColor), which was already computed at add time and previously discarded after the first. A user restyling the layer takes it back," crediting a new test layer-parts-every-path.test.ts.

Neither seems to hold as shipped: fillColor above (line ~81-83) still only ever reads sourceLayerColors[sourceLayers[0]], unchanged from main — so a multi-source-layer archive that stays a single layer (basemap extract, or any STAC asset whose sourceLayers happen to collide/dedupe to one id) still has no way to color source layers past the first, same as before this PR. And layer-parts-every-path.test.ts doesn't exist anywhere in this repo/diff. Worth checking whether the PR body was written against an earlier version of the change — as written it may overstate what this PR actually fixes for per-source-layer coloring.

}

/**
* One layer per source layer in a vector archive, so the Layers panel can show, reorder, style and
* hide them with the machinery it already has. Raster, or a single source layer, stays one layer.
*
* All of them name the archive's one MapLibre source, so removing one must not remove it —
* `removeLayerFromMap` refcounts it against the survivors. That id doubles as the refcount key;
* anything needing the two to differ needs its own field rather than a third reader of this one.
*/
export function createPMTilesArchiveLayers(options: PMTilesStoreLayerOptions): GeoLibreLayer[] {
// Keyed by the id each source layer would take, not by its name: `encodeVectorTileLayerPart` is
// not injective (`a/b` and `a_2Fb` both encode to `a_2Fb`), and an archive's metadata can repeat
// a name outright. Either way a second layer would carry the first one's id.
if (options.tileType === "raster") {
// Raster tiles never split, so the id math below means nothing for them.
return [createPMTilesStoreLayer(options)];
}
const parts = new Map<string, string>();
for (const sourceLayer of options.sourceLayers) {
const id = `${options.id}-${encodeVectorTileLayerPart(sourceLayer)}`;
const taken = parts.get(id);
if (taken === undefined) {
parts.set(id, sourceLayer);
} else if (taken !== sourceLayer) {
// Two different names, one id: the second's features would draw nowhere. Reported rather
// than dropped in silence — the Diagnostics panel is where a user can see it.
console.warn(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor/quality (low confidence): this warns on every call to createPMTilesArchiveLayers. The PMTiles control reports source layers progressively as metadata arrives (per the comments elsewhere in this PR about "a later read" adding source layers), so a genuine encoding collision in an archive will re-log this warning on every progressive layeradd event for that archive, not just once. Not incorrect, just potentially noisy in the console/Diagnostics panel for a long-loading archive.

`PMTiles archive "${options.id}": source layer "${sourceLayer}" collides with "${taken}" and is not drawn.`,
);
}
}
Comment on lines +191 to +214

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the collision-detection loop (and its console.warn) runs before the options.tileType === "raster" check below, so a raster archive that happens to report a non-empty/colliding sourceLayers list (raster tiles never split, and the id math is meaningless for them) would still emit a "collides with... and is not drawn" warning even though nothing is actually dropped — the raster branch ignores sourceLayers for splitting purposes entirely. Low impact in practice since readPMTilesArchiveInfo/readRemotePMTilesInfo always return an empty sourceLayers for raster, but worth guarding (if (options.tileType !== "raster") { ...collision loop... }) so the diagnostic can't misfire for a hybrid/unusual archive.

if (parts.size < 2) {
// Deduped here too, so one layer never carries the same source layer twice.
return [
createPMTilesStoreLayer({ ...options, sourceLayers: [...new Set(options.sourceLayers)] }),
];
Comment on lines +215 to +229

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (medium confidence): when an archive's source layers collide down to a single id (parts.size < 2) because two different names encode to the same id (not just an exact repeat), this fallback keeps both original names via [...new Set(options.sourceLayers)] instead of the collision-resolved parts map. That reproduces exactly the failure the console.warn above describes — a second, differently-named source layer sharing a derived native-layer id with the first — except here it's silently kept in source.sourceLayers and never actually rendered (the second ensureLayer call for the shared id is a no-op), whereas the split path a few lines down correctly drops the loser.

Example: sourceLayers: ["a/b", "a_2Fb"] — both collide to one id, so parts.size === 1, and this branch returns a single layer covering ["a/b", "a_2Fb"] even though only one of them will ever draw.

Using the already-deduped map instead would keep this path consistent with the split path:

Suggested change
if (parts.size < 2) {
// Deduped here too, so one layer never carries the same source layer twice.
return [
createPMTilesStoreLayer({ ...options, sourceLayers: [...new Set(options.sourceLayers)] }),
];
if (parts.size < 2) {
// Deduped here too, so one layer never carries the same source layer twice.
return [createPMTilesStoreLayer({ ...options, sourceLayers: [...parts.values()] })];
}

}
Comment on lines +191 to +230

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor inconsistency: the collision-safe parts map is only used on the multi-layer (parts.size >= 2) return path. When parts.size < 2 this falls back to createPMTilesStoreLayer(options) with the original, non-deduplicated options.sourceLayers — so an archive that reports the same source-layer name twice with nothing else (e.g. sourceLayers: ["units", "units"], parts.size === 1) produces one layer whose source.sourceLayers/derived nativeLayerIds each contain the duplicate.

Today that's harmless (ensureLayer in layer-sync.ts is idempotent, so the duplicate just causes a redundant no-op ensureLayer call), but it's a quiet gap in the invariant this function's own doc comment claims ("Keyed by the id each source layer would take... Either way a second layer would carry the first one's id" — true for the split path, not for the single-layer fallback). Passing sourceLayers: [...parts.values()] on this fallback branch would close it. Confidence: low — cosmetic today, no test exercises this specific single-duplicate case either way.

return [...parts].map(([id, sourceLayer]) =>
createPMTilesStoreLayer({
...options,
id,
name: sourceLayer,
sourceLayers: [sourceLayer],
// The archive's source, and so the archive's ids: a layer deriving its own would name ids
// nothing on the map answers to.
sourceId: options.id,
nativeLayerIds: undefined,
}),
);
}
Comment on lines +178 to +248

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (medium confidence): createPMTilesArchiveLayers doesn't dedupe sourceLayers before mapping each one to a store layer whose id is `${options.id}-${encodeVectorTileLayerPart(sourceLayer)}`. Two entries produce the same store-layer id whenever:

  • the archive's vector_layers metadata literally repeats a source-layer name (malformed/duplicate tileset metadata happens in the wild), or
  • two distinct names collide after encoding — encodeVectorTileLayerPart is documented as "not injective" (a/b and a_2Fb both encode to a_2Fb).

addPMTilesArchive computes its known set once, before iterating, so a colliding second layer isn't recognized as a duplicate: it goes through store.addLayer(layer) again with the same id as the first, silently clobbering/duplicating a layer instead of erroring or merging.

Consider deduping (e.g. new Set(options.sourceLayers)) — or at least logging/dropping repeats — before mapping, since this is now a primary store key rather than just a MapLibre native-layer id suffix (where the pre-existing non-injectivity was lower-stakes).

Suggested change
export function createPMTilesArchiveLayers(options: PMTilesStoreLayerOptions): GeoLibreLayer[] {
const sourceLayers = [...options.sourceLayers];
if (options.tileType === "raster" || sourceLayers.length < 2) {
return [createPMTilesStoreLayer(options)];
}
return sourceLayers.map((sourceLayer) => {
const layer = createPMTilesStoreLayer({
...options,
id: `${options.id}-${encodeVectorTileLayerPart(sourceLayer)}`,
name: sourceLayer,
sourceLayers: [sourceLayer],
nativeLayerIds: ownNativeLayerIds(options.nativeLayerIds, options.id, sourceLayer),
});
// Both fields, because readers are split: `getPMTilesSourceId` prefers the metadata,
// `loadedVectorTileFeatures` reads `source.sourceId` alone and swallows a bad id in a `catch`.
return {
...layer,
source: { ...layer.source, sourceId: options.id },
metadata: { ...layer.metadata, sourceId: options.id },
};
});
}
export function createPMTilesArchiveLayers(options: PMTilesStoreLayerOptions): GeoLibreLayer[] {
const sourceLayers = [...new Set(options.sourceLayers)];
if (options.tileType === "raster" || sourceLayers.length < 2) {
return [createPMTilesStoreLayer(options)];
}
return sourceLayers.map((sourceLayer) => {
const layer = createPMTilesStoreLayer({
...options,
id: `${options.id}-${encodeVectorTileLayerPart(sourceLayer)}`,
name: sourceLayer,
sourceLayers: [sourceLayer],
nativeLayerIds: ownNativeLayerIds(options.nativeLayerIds, options.id, sourceLayer),
});
// Both fields, because readers are split: `getPMTilesSourceId` prefers the metadata,
// `loadedVectorTileFeatures` reads `source.sourceId` alone and swallows a bad id in a `catch`.
return {
...layer,
source: { ...layer.source, sourceId: options.id },
metadata: { ...layer.metadata, sourceId: options.id },
};
});
}

Comment on lines +178 to +248

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quality (low confidence, minor): when two source layer names collide under encodeVectorTileLayerPart (or are literal duplicates), the second one is silently dropped — its features never render under any layer, split or unsplit, and nothing surfaces this to the user (confirmed intentional by the pmtiles-archive-layers.test.ts case "the colliding second name is dropped, the first stands"). That's a reasonable fallback for an edge case, but a real archive with two source layers whose data quietly vanishes with no warning/log could be confusing to debug. Worth at least a one-line log/console warning when this happens, if not already planned as follow-up.


/** Facts about a PMTiles archive needed to build a GeoLibre layer for it. */
export interface PMTilesArchiveInfo {
tileType: "vector" | "raster";
Expand Down
Loading
Loading