MapComplete/src/UI/BigComponents/OfflineManagement.svelte

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

244 lines
7.2 KiB
Svelte
Raw Normal View History

<script lang="ts">
import type { Map as MlMap } from "maplibre-gl"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Utils } from "../../Utils"
import MaplibreMap from "../Map/MaplibreMap.svelte"
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"
import { Tiles } from "../../Models/TileRange"
import type { Feature, Polygon } from "geojson"
import ShowDataLayer from "../Map/ShowDataLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import { DownloadIcon, TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
export let state: ThemeViewState & SpecialVisualizationState = undefined
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)
mapProperties.maxzoom.set(focusZ - 1)
mapProperties.zoom.set(Math.min(focusZ - 1, state.mapProperties.zoom.data))
mapProperties.location.set(state.mapProperties.location.data)
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))
}
async function pingServiceWorker() {
const l = window.location
const sw = await Utils.downloadJson(l.protocol + "//" + l.host + "/service-worker/offline-basemapM/update")
console.log("Service worker has data:", sw)
}
updateMeta()
pingServiceWorker()
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()
pingServiceWorker()
} catch (e) {
installing.set(installing.data.filter(k => k !== key))
} finally {
installing.set(installing.data.filter(k => k !== key))
}
}
let installed: Store<Feature<Polygon>> = installedMeta.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"
}
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: AreaDescription of areasToInstall) {
console.log("Attempting to install", area)
await install(area)
}
}
new ShowDataLayer(map, {
features: new StaticFeatureSource(installed),
layer: new LayerConfig({
id: "downloaded",
source: "special",
lineRendering: [{
color: "blue",
width: {
mappings: [
{
if: `id!~${focusZ}-.*`,
then: "1"
}
]
},
fillColor: {
mappings: [
{
if: `id!~${focusZ}-.*`,
then: "#00000000"
}
]
}
}],
pointRendering: null
})
})
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({
id: "focustile",
source: "special",
lineRendering: [{
color: "black"
}],
pointRendering: [
{
location: ["point", "centroid"],
label: "{txt}",
labelCss: "width: max-content",
labelCssClasses: "bg-white rounded px-2 flex"
}
]
})
})
</script>
<div class="flex flex-col h-full max-h-leave-room">
{#if $installedMeta === undefined}
<Loading />
{:else}
<div class="relative w-full h-3/4">
<div class="rounded-lg absolute top-0 left-0 h-full w-full">
<MaplibreMap {map} {mapProperties} />
</div>
<div class="absolute top-0 left-0 h-full w-full flex flex-col justify-center items-center pointer-events-none">
<div class="w-16 h-32 mb-16"></div>
{#if $focusTileIsInstalling}
<div class="normal-background rounded-lg">
<Loading>
Data is being downloaded
</Loading>
</div>
{:else}
<button class="primary pointer-events-auto" on:click={() => download()}
class:disabled={$focusTileIsInstalled}>
<DownloadIcon class="w-8 h-8" />
Download
</button>
{/if}
</div>
</div>
<AccordionSingle>
<div slot="header">
Offline tile management
</div>
{Utils.toHumanByteSize(Utils.sum($installedMeta.map(area => area.size)))}
<button on:click={() => {
installedMeta?.data?.forEach(area => del(area))
}}>
<TrashIcon class="w-6" />
Delete all
</button>
<table class="w-full">
<tr>
<th>Name</th>
<th>Map generation date</th>
<th>Size</th>
<th>Zoom ranges</th>
<th>Actions</th>
</tr>
{#each ($installedMeta ?? []) as area }
<tr>
<td>{area.name}</td>
<td>{area.dataVersion}</td>
<td>{Utils.toHumanByteSize(area.size ?? -1)}</td>
<td>{area.minzoom}
{#if area.maxzoom !== undefined}
- {area.maxzoom}
{:else}
and above
{/if}
</td>
<td>
<button on:click={() => del(area)}>
<TrashIcon class="w-6" />
Delete this map
</button>
</td>
</tr>
{/each}
</table>
</AccordionSingle>
{/if}
</div>