forked from MapComplete/MapComplete
244 lines
7.2 KiB
Svelte
244 lines
7.2 KiB
Svelte
|
|
<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>
|