From 8a4d26f7fc34ce9e7b99ad8e48d46f473418b33d Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Sat, 30 Nov 2024 10:02:50 +0000 Subject: [PATCH] feat(mapper): improve offline pmtiles experience, load opfs by default --- src/mapper/src/constants/baseLayers.ts | 14 +-- .../lib/components/map/layer-switcher.svelte | 61 +++++++++---- src/mapper/src/lib/components/map/main.svelte | 89 ++++++++++++++----- src/mapper/src/lib/utils/basemaps.ts | 4 +- src/mapper/src/store/common.svelte.ts | 4 - src/mapper/svelte.config.js | 3 +- src/mapper/vite.config.ts | 4 +- 7 files changed, 124 insertions(+), 55 deletions(-) diff --git a/src/mapper/src/constants/baseLayers.ts b/src/mapper/src/constants/baseLayers.ts index 1cb3658523..a262414927 100644 --- a/src/mapper/src/constants/baseLayers.ts +++ b/src/mapper/src/constants/baseLayers.ts @@ -107,15 +107,15 @@ let satellite = { ], }; -export const customStyle = { - id: 'Custom', +export const pmtilesStyle = { + id: 'PMTiles', version: 8, - name: 'Custom', + name: 'PMTiles', metadata: { thumbnail: oamLogo, }, sources: { - custom: { + pmtiles: { type: 'raster', url: '', tileSize: 512, @@ -124,9 +124,9 @@ export const customStyle = { }, layers: [ { - id: 'custom', + id: 'pmtiles', type: 'raster', - source: 'custom', + source: 'pmtiles', layout: { visibility: 'visible', }, @@ -134,4 +134,4 @@ export const customStyle = { ], }; -export const baseLayers = [stamenStyle, esriStyle, satellite]; +export const baseLayers = [osmStyle, stamenStyle, esriStyle, satellite]; diff --git a/src/mapper/src/lib/components/map/layer-switcher.svelte b/src/mapper/src/lib/components/map/layer-switcher.svelte index 5b8afe0c4b..f305426fcb 100644 --- a/src/mapper/src/lib/components/map/layer-switcher.svelte +++ b/src/mapper/src/lib/components/map/layer-switcher.svelte @@ -27,20 +27,22 @@ map = new Map({ import { clickOutside } from '$lib/utils/clickOutside'; type Props = { - extraStyles: maplibregl.StyleSpecification[]; + styles: maplibregl.StyleSpecification[]; map: maplibregl.Map | undefined; sourcesIdToReAdd: string[]; + switchToNewestStyle: boolean; }; - const { extraStyles, map, sourcesIdToReAdd }: Props = $props(); + const { styles, map, sourcesIdToReAdd, switchToNewestStyle = false }: Props = $props(); let allStyles: MapLibreStylePlusMetadata[] | [] = $state([]); let selectedStyleUrl: string | undefined = $state(undefined); let isClosed = $state(true); let isOpen = $state(false); + let isFirstLoad = true; $effect(() => { - if (extraStyles.length > 0) { + if (styles.length > 0) { fetchStyleInfo(); } else { allStyles = []; @@ -98,26 +100,52 @@ map = new Map({ * Fetch styles and prepare them with thumbnails. */ async function fetchStyleInfo() { + let processedStyles: MapLibreStylePlusMetadata[] = []; + + // Process the current map style const currentMapStyle = map?.getStyle(); if (currentMapStyle) { const processedStyle = processStyle(currentMapStyle); selectedStyleUrl = processedStyle?.metadata?.thumbnail || undefined; - allStyles = [processedStyle]; + processedStyles.push(processedStyle); } - const extraProcessedStyles = await Promise.all( - extraStyles.map(async (style) => { - if (typeof style === 'string') { - const styleResponse = await fetch(style); - const styleJson = await styleResponse.json(); - return processStyle(styleJson); - } else { - return processStyle(style); - } - }), + // Process additional styles (download first if style is URL) + for (const style of styles) { + if (typeof style === 'string') { + const response = await fetch(style); + const styleJson = await response.json(); + processedStyles.push(processStyle(styleJson)); + } else { + processedStyles.push(processStyle(style)); + } + } + + // Filter out duplicate styles based on `name` field + const deduplicatedStyles = [...processedStyles].filter( + (style, index, self) => self.findIndex((s) => s.name === style.name) === index, ); - allStyles = allStyles.concat(extraProcessedStyles); + // If a new style is added later, we automatically switch + // to that style, if switchToNewestStyle is true + if (switchToNewestStyle && !isFirstLoad) { + // Determine new styles only on subsequent updates + const newStyles = deduplicatedStyles.filter( + style => !allStyles.find(existingStyle => existingStyle.name === style.name) + ); + + // Handle new styles (e.g., auto-switch to the newest style) + if (newStyles.length > 0) { + selectStyle(newStyles[newStyles.length - 1]); + } + } + + // Update allStyles only if there are changes + if (JSON.stringify(allStyles) !== JSON.stringify(deduplicatedStyles)) { + allStyles = deduplicatedStyles; + } + + isFirstLoad = false; } function selectStyle(style: MapLibreStylePlusMetadata) { @@ -202,7 +230,8 @@ map = new Map({ > Style Thumbnail {style.name} - {/each} + + {/each} diff --git a/src/mapper/src/lib/components/map/main.svelte b/src/mapper/src/lib/components/map/main.svelte index 4d67e8c340..5cae8a8d8a 100644 --- a/src/mapper/src/lib/components/map/main.svelte +++ b/src/mapper/src/lib/components/map/main.svelte @@ -2,6 +2,7 @@ import '$styles/page.css'; import '$styles/button.css'; import '@hotosm/ui/dist/hotosm-ui'; + import { onMount, tick } from 'svelte'; import { MapLibre, GeoJSON, @@ -34,8 +35,10 @@ import { getTaskStore } from '$store/tasks.svelte.ts'; import { getProjectSetupStepStore, getProjectBasemapStore } from '$store/common.svelte.ts'; // import { entityFeatcolStore, selectedEntityId } from '$store/entities'; + import { readFileFromOPFS } from '$lib/fs/opfs.ts'; + import { loadOfflinePmtiles } from '$lib/utils/basemaps.ts'; import { projectSetupStep as projectSetupStepEnum } from '$constants/enums.ts'; - import { baseLayers, osmStyle, customStyle } from '$constants/baseLayers.ts'; + import { baseLayers, osmStyle, pmtilesStyle } from '$constants/baseLayers.ts'; import { getEntitiesStatusStore } from '$store/entities.svelte.ts'; import type { GeoJSON as GeoJSONType } from 'geojson'; @@ -61,24 +64,45 @@ let taskAreaClicked: boolean = $state(false); let toggleGeolocationStatus: boolean = $state(false); let projectSetupStep = $state(null); - // If no custom layer URL, omit, else set URL from projectPmtilesUrl - let processedBaseLayers = $derived([ - ...baseLayers, - ...(projectBasemapStore.projectPmtilesUrl - ? [ - { - ...customStyle, - sources: { - ...customStyle.sources, - custom: { - ...customStyle.sources.custom, - url: projectBasemapStore.projectPmtilesUrl, - }, + // Trigger adding the PMTiles layer to baselayers, if PmtilesUrl is set + let allBaseLayers: maplibregl.StyleSpecification[] = $derived( + projectBasemapStore.projectPmtilesUrl ? + [ + ...baseLayers, + { + ...pmtilesStyle, + sources: { + ...pmtilesStyle.sources, + pmtiles: { + ...pmtilesStyle.sources.pmtiles, + url: projectBasemapStore.projectPmtilesUrl, }, }, - ] - : []), - ]); + }, + ] + : baseLayers + ); + // // This does not work! Infinite looping + // // Trigger adding the PMTiles layer to baselayers, if PmtilesUrl is set + // $effect(() => { + // if (projectBasemapStore.projectPmtilesUrl) { + // const layers = allBaseLayers + // .filter((layer) => layer.name !== "PMTiles") + // .push( + // { + // ...pmtilesStyle, + // sources: { + // ...pmtilesStyle.sources, + // pmtiles: { + // ...pmtilesStyle.sources.pmtiles, + // url: projectBasemapStore.projectPmtilesUrl, + // }, + // }, + // }, + // ) + // allBaseLayers = layers; + // } + // }) // using this function since outside click of entity layer couldn't be tracked via FillLayer function handleMapClick(e: maplibregl.MapMouseEvent) { @@ -129,6 +153,10 @@ } }); + let taskAreaClicked: boolean = $state(false); + let toggleGeolocationStatus: boolean = $state(false); + let projectSetupStep = $state(null); + $effect(() => { projectSetupStep = +projectSetupStepStore.projectSetupStep; }); @@ -137,11 +165,6 @@ $effect(() => { if (map) { setMapRef(map); - // Register pmtiles protocol - if (!maplibre.config.REGISTERED_PROTOCOLS.hasOwnProperty('pmtiles')) { - let protocol = new Protocol(); - maplibre.addProtocol('pmtiles', protocol.tile); - } } }); @@ -173,6 +196,21 @@ }), }; } + + onMount(async () => { + // Register pmtiles protocol + if (!maplibre.config.REGISTERED_PROTOCOLS.hasOwnProperty('pmtiles')) { + let protocol = new Protocol(); + maplibre.addProtocol('pmtiles', protocol.tile); + } + + // Attempt loading OPFS PMTiles layers on first load + // note that this sets projectBasemapStore.projectPmtilesUrl + const offlineBasemapFile = await readFileFromOPFS(`${projectId}/basemap.pmtiles`); + if (offlineBasemapFile) { + await loadOfflinePmtiles(projectId); + } + }); @@ -213,7 +251,12 @@ - + diff --git a/src/mapper/src/lib/utils/basemaps.ts b/src/mapper/src/lib/utils/basemaps.ts index fbae9174f5..485ec106da 100644 --- a/src/mapper/src/lib/utils/basemaps.ts +++ b/src/mapper/src/lib/utils/basemaps.ts @@ -46,7 +46,7 @@ export async function loadOnlinePmtiles(url: string | null) { } export async function loadOfflinePmtiles(projectId: number) { - const filePath = `${projectId}/all.pmtiles`; + const filePath = `${projectId}/basemap.pmtiles`; // Read file from OPFS and display on map const opfsPmtilesData = await readFileFromOPFS(filePath); @@ -91,7 +91,7 @@ export async function writeOfflinePmtiles(projectId: number, url: string | null) const data = await downloadBasemap(url); // Copy to OPFS filesystem for offline use - const filePath = `${projectId}/all.pmtiles`; + const filePath = `${projectId}/basemap.pmtiles`; await writeBinaryToOPFS(filePath, data); await loadOfflinePmtiles(projectId); diff --git a/src/mapper/src/store/common.svelte.ts b/src/mapper/src/store/common.svelte.ts index ec52690918..2fe6436a90 100644 --- a/src/mapper/src/store/common.svelte.ts +++ b/src/mapper/src/store/common.svelte.ts @@ -57,10 +57,6 @@ function getProjectBasemapStore() { }, setProjectPmtilesUrl: (url: string) => { projectPmtilesUrl = url; - getAlertStore().setAlert({ - variant: 'success', - message: 'Success! Check the base layer selector.', - }); }, }; } diff --git a/src/mapper/svelte.config.js b/src/mapper/svelte.config.js index 52f5ed58e2..94a12ba292 100644 --- a/src/mapper/svelte.config.js +++ b/src/mapper/svelte.config.js @@ -31,9 +31,10 @@ const config = { alias: { $lib: 'src/lib', $components: 'src/components', - $static: 'static', $store: 'src/store', $routes: 'src/routes', + $constants: 'src/constants', + $static: 'static', $styles: 'styles', $assets: 'assets', }, diff --git a/src/mapper/vite.config.ts b/src/mapper/vite.config.ts index 377aabf934..8accf8d3de 100644 --- a/src/mapper/vite.config.ts +++ b/src/mapper/vite.config.ts @@ -22,12 +22,12 @@ export default defineConfig({ alias: { $lib: path.resolve('./src/lib'), $components: path.resolve('./src/components'), - $static: path.resolve('./static'), $store: path.resolve('./src/store'), $routes: path.resolve('./src/routes'), + $constants: path.resolve('./src/constants'), + $static: path.resolve('./static'), $styles: path.resolve('./src/styles'), $assets: path.resolve('./src/assets'), - $constants: path.resolve('./src/constants'), }, }, test: {