Feature(offline): rework to use a protocol handler instead of a service worker to intercept as service workers don't always work, simplify code, add option to auto-download

This commit is contained in:
Pieter Vander Vennet 2025-08-01 14:54:30 +02:00
parent ca819cf8a6
commit 44748051dd
11 changed files with 193 additions and 293 deletions

View file

@ -8,8 +8,6 @@
import type { MapProperties } from "../../Models/MapProperties"
import ThemeViewState from "../../Models/ThemeViewState"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { AreaDescription } from "../../service-worker/OfflineBasemapManager"
import { OfflineBasemapManager } from "../../service-worker/OfflineBasemapManager"
import Loading from "../Base/Loading.svelte"
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
@ -20,12 +18,15 @@
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { DownloadIcon, TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Accordion, AccordionItem } from "flowbite-svelte"
import ServiceWorkerStatus from "./ServiceWorkerStatus.svelte"
import type { AreaDescription } from "../../Logic/OfflineBasemapManager"
import { OfflineBasemapManager } from "../../Logic/OfflineBasemapManager"
import Checkbox from "../Base/Checkbox.svelte"
export let state: ThemeViewState & SpecialVisualizationState = undefined
let focusZ = Math.max(...Object.keys(OfflineBasemapManager.zoomelevels).map(Number))
export let autoDownload = state.autoDownloadOfflineBasemap
let focusZ = Math.max(...Object.keys(OfflineBasemapManager.zoomelevels).map(Number))
let map: UIEventSource<MlMap> = new UIEventSource(undefined)
let mapProperties: MapProperties = new MapLibreAdaptor(map)
state?.showCurrentLocationOn(map)
@ -35,77 +36,52 @@
mapProperties.allowRotating.set(false)
const offlineMapManager = new OfflineBasemapManager("https://cache.mapcomplete.org/")
let installedMeta: UIEventSource<AreaDescription[]> = new UIEventSource([])
function updateMeta() {
offlineMapManager.updateCachedMeta().then(meta => installedMeta.set(meta))
const offlineMapManager = OfflineBasemapManager.singleton
let installing: Store<ReadonlyMap<string, object>> = offlineMapManager.installing
let installed = offlineMapManager.installedAreas
let focusTile: Store<{
x: number;
y: number;
z: number
} | undefined> = mapProperties.location.mapD(location => Tiles.embedded_tile(location.lat, location.lon, focusZ))
let focusTileIsInstalled = focusTile.mapD(tile => offlineMapManager.isInstalled(tile), [installed])
let focusTileIsInstalling = focusTile.mapD(tile => {
const { x, y, z } = tile
return installing.data?.has(`${z}-${x}-${y}.pmtiles`)
}, [installing])
async function del(areaDescr: AreaDescription) {
await offlineMapManager.deleteArea(areaDescr)
}
updateMeta()
async function download() {
const tile = focusTile.data
await offlineMapManager.autoInstall(tile)
}
let installing = new UIEventSource<string[]>([])
async function install(tile: AreaDescription) {
const key = `${tile.minzoom}-${tile.x}-${tile.y}`
installing.set([...installing.data ?? [], key])
try {
const descr = OfflineBasemapManager.getAreaDescriptionForMapcomplete(key + ".pmtiles")
await offlineMapManager.installArea(descr)
updateMeta()
} catch (e) {
installing.set(installing.data.filter(k => k !== key))
} finally {
installing.set(installing.data.filter(k => k !== key))
let focusTileFeature = focusTile.mapD(({ x, y, z }) => {
const f = Tiles.asGeojson(z, x, y)
f.properties = {
id: "center_point_" + z + "_" + x + "_" + y,
txt: "Tile " + x + " " + y
}
}
let installed: Store<Feature<Polygon>[]> = installedMeta.map(meta =>
return [f]
})
let installedFeature: Store<Feature<Polygon>[]> = installed.map(meta =>
(meta ?? [])
.map(area => {
const f = Tiles.asGeojson(area.minzoom, area.x, area.y)
f.properties = {
id: area.minzoom + "-" + area.x + "-" + area.y,
downloaded: "yes",
text: area.name + " " + area.dataVersion + " " + Utils.toHumanByteSize(Number(area.size))
text: area.name + " " + new Date(area.dataVersion).toLocaleDateString() + " " + Utils.toHumanByteSize(Number(area.size))
}
return f
}
)
)
let focusTile: Store<{
x: number;
y: number;
z: number
} | undefined> = mapProperties.location.mapD(location => Tiles.embedded_tile(location.lat, location.lon, focusZ))
let focusTileIsInstalled = focusTile.mapD(tile => offlineMapManager.isInstalled(tile), [installedMeta])
let focusTileIsInstalling = focusTile.mapD(tile => {
const { x, y, z } = tile
return installing.data?.some(area => area === `${z}-${x}-${y}.pmtiles`)
}, [installing])
async function del(areaDescr: AreaDescription) {
await offlineMapManager.deleteArea(areaDescr)
updateMeta()
}
async function download() {
const areasToInstall = Array.from(offlineMapManager.getInstallCandidates(focusTile.data))
for (const area of areasToInstall) {
console.log("Attempting to install", area)
await install(area)
}
}
new ShowDataLayer(map, {
features: new StaticFeatureSource(installed),
features: new StaticFeatureSource(installedFeature),
layer: new LayerConfig({
id: "downloaded",
source: "special",
@ -133,21 +109,11 @@
location: ["point", "centroid"],
label: "{text}",
labelCss: "width: w-min",
labelCssClasses: "bg-white rounded px-2"
labelCssClasses: "bg-white rounded px-2 items-center flex flex-col"
}
]
})
})
let focusTileFeature = focusTile.mapD(({ x, y, z }) => {
const f = Tiles.asGeojson(z, x, y)
f.properties = {
id: "center_point_" + z + "_" + x + "_" + y,
txt: "Tile " + x + " " + y
}
return [f]
})
new ShowDataLayer(map, {
features: new StaticFeatureSource(focusTileFeature),
layer: new LayerConfig({
@ -170,7 +136,14 @@
</script>
<div class="flex flex-col h-full max-h-leave-room">
{#if $installedMeta === undefined}
<Checkbox selected={autoDownload}>Automatically download the basemap when browsing around</Checkbox>
<div>
If checked, MapComplete will automatically download the basemap to the cache for the area.
This results in bigger initial data loads, but requires less internet over the long run.
If you plan to visit a region with less connectivity, you can also select the area you want to download below.
</div>
{#if $installed === undefined}
<Loading />
{:else}
<div class="h-full overflow-auto pb-16">
@ -210,9 +183,9 @@
<div class="leave-room">
{Utils.toHumanByteSize(Utils.sum($installedMeta.map(area => area.size)))}
{Utils.toHumanByteSize(Utils.sum($installed.map(area => area.size)))}
<button on:click={() => {
installedMeta?.data?.forEach(area => del(area))
installed?.data?.forEach(area => del(area))
}}>
<TrashIcon class="w-6" />
Delete all
@ -225,7 +198,7 @@
<th>Zoom ranges</th>
<th>Actions</th>
</tr>
{#each ($installedMeta ?? []) as area }
{#each ($installed ?? []) as area }
<tr>
<td>{area.name}</td>
<td>{area.dataVersion}</td>
@ -250,14 +223,6 @@
</div>
</AccordionItem>
<AccordionItem paddingDefault="p-2">
<div slot="header">
Service worker status
</div>
<div class="leave-room">
<ServiceWorkerStatus />
</div>
</AccordionItem>
</Accordion>
</div>
{/if}