Skip to content

Commit

Permalink
feat(mapper): improve offline pmtiles experience, load opfs by default
Browse files Browse the repository at this point in the history
  • Loading branch information
spwoodcock committed Nov 30, 2024
1 parent 43e8b80 commit 8a4d26f
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 55 deletions.
14 changes: 7 additions & 7 deletions src/mapper/src/constants/baseLayers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -124,14 +124,14 @@ export const customStyle = {
},
layers: [
{
id: 'custom',
id: 'pmtiles',
type: 'raster',
source: 'custom',
source: 'pmtiles',
layout: {
visibility: 'visible',
},
},
],
};

export const baseLayers = [stamenStyle, esriStyle, satellite];
export const baseLayers = [osmStyle, stamenStyle, esriStyle, satellite];
61 changes: 45 additions & 16 deletions src/mapper/src/lib/components/map/layer-switcher.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -202,7 +230,8 @@ map = new Map({
>
<img src={style.metadata.thumbnail} alt="Style Thumbnail" class="w-full h-full object-cover" />
<span class="absolute top-0 left-0 bg-white bg-opacity-80 px-1 rounded-br">{style.name}</span>
</div>{/each}
</div>
{/each}
</div>
</div>
</div>
Expand Down
89 changes: 66 additions & 23 deletions src/mapper/src/lib/components/map/main.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -129,6 +153,10 @@
}
});
let taskAreaClicked: boolean = $state(false);
let toggleGeolocationStatus: boolean = $state(false);
let projectSetupStep = $state(null);
$effect(() => {
projectSetupStep = +projectSetupStepStore.projectSetupStep;
});
Expand All @@ -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);
}
}
});
Expand Down Expand Up @@ -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);
}
});
</script>

<!-- Note here we still use Svelte 4 on:click until svelte-maplibre migrates -->
Expand Down Expand Up @@ -213,7 +251,12 @@
</ControlGroup></Control
>
<Control class="flex flex-col gap-y-2" position="bottom-right">
<LayerSwitcher {map} extraStyles={processedBaseLayers} sourcesIdToReAdd={['tasks', 'entities', 'geolocation']} />
<LayerSwitcher
{map}
styles={allBaseLayers}
sourcesIdToReAdd={['tasks', 'entities', 'geolocation']}
switchToNewestStyle={true}
></LayerSwitcher>
<Legend />
</Control>
<!-- Add the Geolocation GeoJSON layer to the map -->
Expand Down
4 changes: 2 additions & 2 deletions src/mapper/src/lib/utils/basemaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 0 additions & 4 deletions src/mapper/src/store/common.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,6 @@ function getProjectBasemapStore() {
},
setProjectPmtilesUrl: (url: string) => {
projectPmtilesUrl = url;
getAlertStore().setAlert({
variant: 'success',
message: 'Success! Check the base layer selector.',
});
},
};
}
Expand Down
3 changes: 2 additions & 1 deletion src/mapper/svelte.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
4 changes: 2 additions & 2 deletions src/mapper/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down

0 comments on commit 8a4d26f

Please sign in to comment.