forked from MapComplete/MapComplete
238 lines
7.8 KiB
Svelte
238 lines
7.8 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 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 { DownloadIcon, TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
|
|
import { Accordion, AccordionItem } from "flowbite-svelte"
|
|
import type { AreaDescription } from "../../Logic/OfflineBasemapManager"
|
|
import { OfflineBasemapManager } from "../../Logic/OfflineBasemapManager"
|
|
import Checkbox from "../Base/Checkbox.svelte"
|
|
|
|
|
|
export let state: ThemeViewState & SpecialVisualizationState = undefined
|
|
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)
|
|
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 = 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)
|
|
}
|
|
|
|
async function download() {
|
|
const tile = focusTile.data
|
|
await offlineMapManager.autoInstall(tile)
|
|
}
|
|
|
|
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]
|
|
})
|
|
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 + " " + new Date(area.dataVersion).toLocaleDateString() + " " + Utils.toHumanByteSize(Number(area.size))
|
|
}
|
|
return f
|
|
}
|
|
)
|
|
)
|
|
new ShowDataLayer(map, {
|
|
features: new StaticFeatureSource(installedFeature),
|
|
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: [
|
|
{
|
|
location: ["point", "centroid"],
|
|
label: "{text}",
|
|
labelCss: "width: w-min",
|
|
labelCssClasses: "bg-white rounded px-2 items-center flex flex-col"
|
|
}
|
|
]
|
|
})
|
|
})
|
|
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">
|
|
<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">
|
|
|
|
<Accordion class="" inactiveClass="text-black">
|
|
<AccordionItem paddingDefault="p-2">
|
|
<div slot="header">Map</div>
|
|
<div class="relative leave-room">
|
|
<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>
|
|
</AccordionItem>
|
|
|
|
<AccordionItem paddingDefault="p-2">
|
|
<div slot="header">
|
|
Offline tile management
|
|
</div>
|
|
|
|
<div class="leave-room">
|
|
|
|
|
|
{Utils.toHumanByteSize(Utils.sum($installed.map(area => area.size)))}
|
|
<button on:click={() => {
|
|
installed?.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 ($installed ?? []) 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>
|
|
</div>
|
|
|
|
</AccordionItem>
|
|
</Accordion>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.leave-room {
|
|
height: calc(100vh - 18rem);
|
|
overflow-x: auto;
|
|
width: 100%;
|
|
color: var(--foreground-color);
|
|
}
|
|
</style>
|