-
-
Notifications
You must be signed in to change notification settings - Fork 670
Add remote data through the embed API #1875
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
Changes from 3 commits
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 |
|---|---|---|
|
|
@@ -20,13 +20,107 @@ import type { ProjectUrlLoadState } from "./useProjectUrlLoader"; | |
| */ | ||
| export type DataUrlLoadState = ProjectUrlLoadState & { fitLayerIds?: string[] }; | ||
|
|
||
| export interface DataUrlLoadResult { | ||
| layerIds: string[]; | ||
| fitLayerIds: string[]; | ||
| } | ||
|
|
||
| /** Whether a store layer records the given URL as the data it was loaded from. */ | ||
| function layerPointsAt(layer: GeoLibreLayer, url: string): boolean { | ||
| if (layer.sourcePath === url) return true; | ||
| const source = layer.source as { url?: unknown }; | ||
| return source.url === url; | ||
| } | ||
|
|
||
| /** Load one URL through the same remote-data pipeline used by the `?data=` deep link. */ | ||
| export async function loadDataUrl( | ||
| mapAppAPI: ReturnType<typeof createAppAPI>, | ||
| dataUrl: string, | ||
| options: { styleUrl?: string | null; signal?: AbortSignal; fit?: boolean } = {}, | ||
| ): Promise<DataUrlLoadResult> { | ||
| const fit = options.fit ?? true; | ||
| const [remote, rawStyle] = await Promise.all([ | ||
| fetchRemoteData(dataUrl, { signal: options.signal }), | ||
| options.styleUrl ? fetchRemoteStyle(options.styleUrl, { signal: options.signal }) : null, | ||
| ]); | ||
| if (options.signal?.aborted) throw new DOMException("The operation was aborted", "AbortError"); | ||
| const store = useAppStore.getState(); | ||
| const layerIds: string[] = []; | ||
| const fitLayerIds: string[] = []; | ||
| if (remote.kind === "cog") { | ||
| const rasterStyle = rawStyle === null ? null : parseRasterUrlStyle(rawStyle); | ||
| const id = await addRasterToMap(mapAppAPI, remote.url, { | ||
| name: remote.name, | ||
| defaults: { engine: "maplibre-gl-raster" }, | ||
| zoomTo: fit, | ||
| ...(rasterStyle ? { state: rasterStyle } : {}), | ||
| }); | ||
| if (options.signal?.aborted) { | ||
| store.removeLayer(id); | ||
| throw new DOMException("The operation was aborted", "AbortError"); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| layerIds.push(id); | ||
| } else if (remote.kind === "pmtiles" || remote.kind === "vector") { | ||
| const styleResult = rawStyle === null ? null : parseMapboxStyle(rawStyle); | ||
| if (styleResult && styleResult.matchedLayerCount === 0) { | ||
| throw new Error("The remote style has no supported vector style layers."); | ||
| } | ||
| const previousIds = new Set(store.layers.map((layer) => layer.id)); | ||
| const added = | ||
| remote.kind === "pmtiles" | ||
| ? await addPMTilesLayerFromUrl(mapAppAPI, remote.url, { fit }) | ||
| : await addVectorLayerFromUrl(mapAppAPI, remote.url, { | ||
| name: remote.name, | ||
| fitBounds: fit, | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| if (!added) throw new Error(`Could not add ${remote.name} to the map.`); | ||
| const addedLayers = useAppStore | ||
| .getState() | ||
| .layers.filter((layer) => !previousIds.has(layer.id) && layerPointsAt(layer, remote.url)); | ||
|
Comment on lines
+68
to
+79
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. The Both hooks fire from effects gated on the same |
||
| if (!addedLayers.length) { | ||
| throw new Error( | ||
| `The ${remote.kind === "pmtiles" ? "PMTiles" : "GeoParquet"} loader did not create a layer for ${remote.url}.`, | ||
| ); | ||
| } | ||
| if (options.signal?.aborted) { | ||
| for (const layer of addedLayers) store.removeLayer(layer.id); | ||
| throw new DOMException("The operation was aborted", "AbortError"); | ||
| } | ||
| if (styleResult && addedLayers.some((layer) => layer.metadata.tileType === "raster")) { | ||
| for (const layer of addedLayers) store.removeLayer(layer.id); | ||
| throw new Error("MapLibre vector styles cannot be applied to a raster PMTiles archive."); | ||
| } | ||
| if (styleResult) { | ||
| for (const layer of addedLayers) { | ||
| store.setLayerStyle(layer.id, applyMapboxStyleImport(layer.style, styleResult)); | ||
| } | ||
| } | ||
| layerIds.push(...addedLayers.map((layer) => layer.id)); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } else { | ||
| const imports = remote.layers.map((layer) => { | ||
| if (rawStyle === null) return { layer, styleResult: null }; | ||
| const styleResult = parseMapboxStyle(mapboxStyleForDataLayer(rawStyle, layer.name)); | ||
| if (styleResult.matchedLayerCount === 0) { | ||
| throw new Error( | ||
| `The remote style has no supported layers for "${layer.name}.geojson". ` + | ||
| `Set each style layer's source to the matching filename stem (for example, "${layer.name}").`, | ||
| ); | ||
| } | ||
| return { layer, styleResult }; | ||
| }); | ||
| for (const { layer, styleResult } of imports) { | ||
| const id = store.addGeoJsonLayer(layer.name, layer.data, layer.sourcePath); | ||
| layerIds.push(id); | ||
| fitLayerIds.push(id); | ||
| if (styleResult) { | ||
| const current = useAppStore.getState().layers.find((candidate) => candidate.id === id); | ||
| if (current) store.setLayerStyle(id, applyMapboxStyleImport(current.style, styleResult)); | ||
| } | ||
| } | ||
| } | ||
| return { layerIds, fitLayerIds }; | ||
| } | ||
|
|
||
| export function useDataUrlLoader( | ||
| mapAppAPI: ReturnType<typeof createAppAPI> | null, | ||
| ): DataUrlLoadState { | ||
|
|
@@ -45,104 +139,16 @@ export function useDataUrlLoader( | |
| if (!params || !mapAppAPI) return; | ||
| const controller = new AbortController(); | ||
| setState({ error: null, message: "Loading data from URL...", status: "loading" }); | ||
| void Promise.all([ | ||
| fetchRemoteData(params.dataUrl, { signal: controller.signal }), | ||
| params.styleUrl ? fetchRemoteStyle(params.styleUrl, { signal: controller.signal }) : null, | ||
| ]) | ||
| .then(async ([remote, rawStyle]) => { | ||
| void loadDataUrl(mapAppAPI, params.dataUrl, { | ||
| styleUrl: params.styleUrl, | ||
| signal: controller.signal, | ||
| }) | ||
| .then(({ layerIds, fitLayerIds }) => { | ||
| if (controller.signal.aborted) return; | ||
| const store = useAppStore.getState(); | ||
| const fitLayerIds: string[] = []; | ||
| let count = 0; | ||
| if (remote.kind === "cog") { | ||
| const rasterStyle = rawStyle === null ? null : parseRasterUrlStyle(rawStyle); | ||
| await addRasterToMap(mapAppAPI, remote.url, { | ||
| name: remote.name, | ||
| defaults: { engine: "maplibre-gl-raster" }, | ||
| ...(rasterStyle ? { state: rasterStyle } : {}), | ||
| }); | ||
| count = 1; | ||
| } else if (remote.kind === "pmtiles" || remote.kind === "vector") { | ||
| // Validate the style before invoking a native loader. Those controls | ||
| // assign their own ids, so collect the newly synchronized store | ||
| // layers after the awaited add completes. | ||
| const styleResult = rawStyle === null ? null : parseMapboxStyle(rawStyle); | ||
| if (styleResult && styleResult.matchedLayerCount === 0) { | ||
| throw new Error("The remote style has no supported vector style layers."); | ||
| } | ||
| const previousIds = new Set(store.layers.map((layer) => layer.id)); | ||
| const added = | ||
| remote.kind === "pmtiles" | ||
| ? await addPMTilesLayerFromUrl(mapAppAPI, remote.url) | ||
| : await addVectorLayerFromUrl(mapAppAPI, remote.url, { | ||
| name: remote.name, | ||
| fitBounds: true, | ||
| }); | ||
| if (!added) throw new Error(`Could not add ${remote.name} to the map.`); | ||
| // These loaders assign their own ids, so the added layers have to be | ||
| // recovered from the store. Identify them by the data URL they record | ||
| // and not by an id diff alone: a concurrent `?url=` project load | ||
| // replaces the whole layer array, which would make every project | ||
| // layer look new here and hand this branch someone else's layers to | ||
| // restyle or remove. No match is treated as "could not identify", | ||
| // never as "take whatever is new". | ||
| const addedLayers = useAppStore | ||
| .getState() | ||
| .layers.filter( | ||
| (layer) => !previousIds.has(layer.id) && layerPointsAt(layer, remote.url), | ||
| ); | ||
| if (!addedLayers.length) { | ||
| throw new Error( | ||
| `The ${remote.kind === "pmtiles" ? "PMTiles" : "GeoParquet"} loader did not create a layer for ${remote.url}.`, | ||
| ); | ||
| } | ||
| // Check every added layer before styling any of them: the archive's | ||
| // tile type is only known once the control has read it, so a raster | ||
| // archive paired with a vector style has to be rolled back rather | ||
| // than left behind by a deep link that reports failure. | ||
| if (styleResult && addedLayers.some((layer) => layer.metadata.tileType === "raster")) { | ||
| for (const layer of addedLayers) store.removeLayer(layer.id); | ||
| throw new Error( | ||
| "MapLibre vector styles cannot be applied to a raster PMTiles archive.", | ||
| ); | ||
| } | ||
| if (styleResult) { | ||
| for (const layer of addedLayers) { | ||
| store.setLayerStyle(layer.id, applyMapboxStyleImport(layer.style, styleResult)); | ||
| } | ||
| } | ||
| count = addedLayers.length; | ||
| } else { | ||
| // Resolve and validate every per-file style before mutating the store. | ||
| // This keeps a misspelled source name from producing a partial import. | ||
| const imports = remote.layers.map((layer) => { | ||
| if (rawStyle === null) return { layer, styleResult: null }; | ||
| const styleResult = parseMapboxStyle(mapboxStyleForDataLayer(rawStyle, layer.name)); | ||
| if (styleResult.matchedLayerCount === 0) { | ||
| throw new Error( | ||
| `The remote style has no supported layers for "${layer.name}.geojson". ` + | ||
| `Set each style layer's source to the matching filename stem (for example, "${layer.name}").`, | ||
| ); | ||
| } | ||
| return { layer, styleResult }; | ||
| }); | ||
| for (const { layer, styleResult } of imports) { | ||
| const id = store.addGeoJsonLayer(layer.name, layer.data, layer.sourcePath); | ||
| fitLayerIds.push(id); | ||
| if (styleResult) { | ||
| const current = useAppStore | ||
| .getState() | ||
| .layers.find((candidate) => candidate.id === id); | ||
| if (current) | ||
| store.setLayerStyle(id, applyMapboxStyleImport(current.style, styleResult)); | ||
| } | ||
| count += 1; | ||
| } | ||
| } | ||
| setState({ | ||
| error: null, | ||
| fitLayerIds, | ||
| message: `Loaded ${count} layer${count === 1 ? "" : "s"} from URL`, | ||
| message: `Loaded ${layerIds.length} layer${layerIds.length === 1 ? "" : "s"} from URL`, | ||
| status: "loaded", | ||
| }); | ||
| timeoutRef.current = window.setTimeout(() => { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.