forked from MapComplete/MapComplete
Feature(offline): include fonts locally
This commit is contained in:
parent
d2500d6db2
commit
31eb9b5587
1028 changed files with 2499 additions and 2 deletions
243
src/UI/BigComponents/OfflineManagement.svelte
Normal file
243
src/UI/BigComponents/OfflineManagement.svelte
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
<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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue