forked from MapComplete/MapComplete
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:
parent
ca819cf8a6
commit
44748051dd
11 changed files with 193 additions and 293 deletions
|
@ -67,6 +67,7 @@
|
||||||
"#photonEndpoint": "`api/` or `reverse/` will be appended by the code",
|
"#photonEndpoint": "`api/` or `reverse/` will be appended by the code",
|
||||||
"photonEndpoint": "https://photon.komoot.io/",
|
"photonEndpoint": "https://photon.komoot.io/",
|
||||||
"jsonld-proxy": "https://lod.mapcomplete.org/extractgraph?url={url}",
|
"jsonld-proxy": "https://lod.mapcomplete.org/extractgraph?url={url}",
|
||||||
|
"protomaps_archive_server": "https://cache.mapcomplete.org",
|
||||||
"protomaps": {
|
"protomaps": {
|
||||||
"#fork": "Bound to https://mapcomplete.org; get your own at https://protomaps.com/",
|
"#fork": "Bound to https://mapcomplete.org; get your own at https://protomaps.com/",
|
||||||
"api-key": "2af8b969a9e8b692",
|
"api-key": "2af8b969a9e8b692",
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"attribution": "<a href=\"https://github.com/protomaps/basemaps\">Protomaps</a> © <a href=\"https://openstreetmap.org\">OpenStreetMap</a>",
|
"attribution": "<a href=\"https://github.com/protomaps/basemaps\">Protomaps</a> © <a href=\"https://openstreetmap.org\">OpenStreetMap</a>",
|
||||||
"type": "vector",
|
"type": "vector",
|
||||||
"tiles": [
|
"tiles": [
|
||||||
"https://api.protomaps.com/tiles/v4/{z}/{x}/{y}.mvt?key=2af8b969a9e8b692&auto=true"
|
"pmtilesoffl://https://api.protomaps.com/tiles/v4/{z}/{x}/{y}.mvt?key=2af8b969a9e8b692"
|
||||||
],
|
],
|
||||||
"maxzoom": 15,
|
"maxzoom": 15,
|
||||||
"minzoom": 0
|
"minzoom": 0
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { PMTiles, RangeResponse, Source } from "pmtiles"
|
import { PMTiles, RangeResponse, Source } from "pmtiles"
|
||||||
|
import { RequestParameters } from "maplibre-gl"
|
||||||
|
import { IsOnline } from "./Web/IsOnline"
|
||||||
|
import Constants from "../Models/Constants"
|
||||||
|
import { Store, UIEventSource } from "./UIEventSource"
|
||||||
|
|
||||||
|
|
||||||
export interface AreaDescription {
|
export interface AreaDescription {
|
||||||
|
@ -172,8 +176,12 @@ export class OfflineBasemapManager {
|
||||||
|
|
||||||
private readonly blobs: TypedIdb<any>
|
private readonly blobs: TypedIdb<any>
|
||||||
private readonly meta: TypedIdb<AreaDescription>
|
private readonly meta: TypedIdb<AreaDescription>
|
||||||
private metaCached: AreaDescription[] = []
|
public _installedAreas: UIEventSource<AreaDescription[]> = new UIEventSource([])
|
||||||
private readonly installing: Set<string> = new Set()
|
public installedAreas: Store<ReadonlyArray<Readonly<AreaDescription>>> = this._installedAreas
|
||||||
|
|
||||||
|
private readonly _installing: UIEventSource<Map<string, Promise<boolean>>> = new UIEventSource(new Map())
|
||||||
|
public readonly installing: Store<ReadonlyMap<string, object>> = this._installing
|
||||||
|
public static singleton = new OfflineBasemapManager(Constants.pmtiles_host)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 'offline base map manager' is responsible for keeping track of the locally installed 'protomaps' subpyramids.
|
* The 'offline base map manager' is responsible for keeping track of the locally installed 'protomaps' subpyramids.
|
||||||
|
@ -188,7 +196,7 @@ export class OfflineBasemapManager {
|
||||||
* When a user downloads an offline map, they download a 9-* subpyramid, the corresponding 5-8 pyramid and the 'global-basemap'
|
* When a user downloads an offline map, they download a 9-* subpyramid, the corresponding 5-8 pyramid and the 'global-basemap'
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public constructor(host: string) {
|
private constructor(host: string) {
|
||||||
if (!host.endsWith("/")) {
|
if (!host.endsWith("/")) {
|
||||||
host += "/"
|
host += "/"
|
||||||
}
|
}
|
||||||
|
@ -198,13 +206,13 @@ export class OfflineBasemapManager {
|
||||||
this.updateCachedMeta()
|
this.updateCachedMeta()
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateCachedMeta(): Promise<AreaDescription[]> {
|
public async updateCachedMeta(): Promise<ReadonlyArray<Readonly<AreaDescription>>> {
|
||||||
this.metaCached = await this.meta.getAllValues()
|
this._installedAreas.set(await this.meta.getAllValues())
|
||||||
return this.metaCached
|
return this.installedAreas.data
|
||||||
}
|
}
|
||||||
|
|
||||||
public isInstalled(toCompare: { z?: number, minzoom?: number, x: number, y: number }): boolean {
|
public isInstalled(toCompare: { z?: number, minzoom?: number, x: number, y: number }): boolean {
|
||||||
return this.metaCached.some(area => area.x === toCompare.x && area.y === toCompare.y && (toCompare.minzoom ?? toCompare.z) === area.minzoom)
|
return this.installedAreas.data.some(area => area.x === toCompare.x && area.y === toCompare.y && (toCompare.minzoom ?? toCompare.z) === area.minzoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -238,15 +246,20 @@ export class OfflineBasemapManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ! Must be called from a fetch event in the service worker !
|
* Installs the area if not yet installed
|
||||||
* @param areaDescription
|
* @param areaDescription
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
public async installArea(areaDescription: AreaDescription) {
|
public async installArea(areaDescription: AreaDescription) {
|
||||||
const target = this._host + areaDescription.name
|
const target = this._host + areaDescription.name
|
||||||
|
if (this.isInstalled(areaDescription)) {
|
||||||
|
// Already installed
|
||||||
|
return true
|
||||||
|
}
|
||||||
console.log("Installing area from " + target)
|
console.log("Installing area from " + target)
|
||||||
const response = await fetch(target)
|
const response = await fetch(target)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
const blob = await response.blob()
|
const blob = await response.blob()
|
||||||
await this.blobs.set(areaDescription.name, blob)
|
await this.blobs.set(areaDescription.name, blob)
|
||||||
|
@ -254,6 +267,8 @@ export class OfflineBasemapManager {
|
||||||
areaDescription.size = blob.size
|
areaDescription.size = blob.size
|
||||||
await this.meta.set(areaDescription.name, areaDescription)
|
await this.meta.set(areaDescription.name, areaDescription)
|
||||||
await this.updateCachedMeta()
|
await this.updateCachedMeta()
|
||||||
|
console.log("Successfully installed", areaDescription.name)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -275,10 +290,6 @@ export class OfflineBasemapManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getMeta(): AreaDescription[] {
|
|
||||||
return this.metaCached
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Looks through the 'installedMeta' where a tile can be extracted from
|
* Looks through the 'installedMeta' where a tile can be extracted from
|
||||||
* @param z
|
* @param z
|
||||||
|
@ -287,7 +298,7 @@ export class OfflineBasemapManager {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private determineArea(z: number, x: number, y: number): AreaDescription | undefined {
|
private determineArea(z: number, x: number, y: number): AreaDescription | undefined {
|
||||||
for (const areaDescription of this.metaCached) {
|
for (const areaDescription of this.installedAreas.data) {
|
||||||
if (areaDescription.minzoom > z) {
|
if (areaDescription.minzoom > z) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -304,72 +315,119 @@ export class OfflineBasemapManager {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
private async attemptInstall(candidate: AreaDescription) {
|
/**
|
||||||
if (this.installing.has(candidate.name)) {
|
* Attempts to install the given area,
|
||||||
return
|
* returns 'true' if the area was successfully installed OR was already installed previously
|
||||||
|
* @param candidate
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private attemptInstall(candidate: AreaDescription): Promise<boolean> {
|
||||||
|
if (!IsOnline.isOnline.data) {
|
||||||
|
return Promise.resolve(false)
|
||||||
}
|
}
|
||||||
this.installing.add(candidate.name)
|
const alreadyInstalling = this._installing.data.get(candidate.name)
|
||||||
try {
|
if (alreadyInstalling) {
|
||||||
await this.installArea(candidate)
|
return alreadyInstalling
|
||||||
await this.updateCachedMeta()
|
}
|
||||||
} catch (e) {
|
const promise = this.installArea(candidate).catch(e => {
|
||||||
console.error("Could not install basemap archive", candidate.name, "due to", e)
|
console.error("Could not install basemap archive", candidate.name, "due to", e)
|
||||||
} finally {
|
|
||||||
this.installing.delete(candidate.name)
|
return false
|
||||||
|
}).finally(() => {
|
||||||
|
this._installing.data.delete(candidate.name)
|
||||||
|
this._installing.ping()
|
||||||
|
})
|
||||||
|
this._installing.data.set(candidate.name, promise)
|
||||||
|
this._installing.ping()
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to install all required areas for the given location
|
||||||
|
* @param tile
|
||||||
|
*/
|
||||||
|
public async autoInstall(tile: { z: number, x: number, y: number }) {
|
||||||
|
const candidates = this.getInstallCandidates(tile)
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
await this.attemptInstall(candidate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
* Searches for the correct MVT tile locally and returns it as a response.
|
||||||
|
* Returns undefined if not found
|
||||||
* @param z
|
* @param z
|
||||||
* @param x
|
* @param x
|
||||||
* @param y
|
* @param y
|
||||||
* @param fallback: if set and no local tile could be found: ask protomaps instead
|
|
||||||
*/
|
*/
|
||||||
async getTileResponse(z: number, x: number, y: number, options?: {
|
async getTileResponse(z: number, x: number, y: number): Promise<undefined | Response> {
|
||||||
autoInstall?: boolean, fallback?: string
|
if (this._installedAreas.data.length === 0) {
|
||||||
}): Promise<Response> {
|
|
||||||
if (this.metaCached.length === 0) {
|
|
||||||
await this.updateCachedMeta()
|
await this.updateCachedMeta()
|
||||||
}
|
}
|
||||||
let area = this.determineArea(z, x, y)
|
const area = this.determineArea(z, x, y)
|
||||||
|
|
||||||
if (options?.autoInstall && !area) {
|
|
||||||
// We attempt to install the local files ; but we don't wait
|
|
||||||
const candidates = this.getInstallCandidates({ z, x, y })
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
this.attemptInstall(candidate)
|
|
||||||
}
|
|
||||||
area = this.determineArea(z, x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!area) {
|
if (!area) {
|
||||||
if (options?.fallback) {
|
|
||||||
return fetch(options?.fallback)
|
|
||||||
}
|
|
||||||
console.log("No suitable area in the archives (and no fallback):", { z, x, y })
|
console.log("No suitable area in the archives (and no fallback):", { z, x, y })
|
||||||
return new Response("Not found: no suitable area found", { status: 404 })
|
return undefined
|
||||||
}
|
}
|
||||||
const blob = await this.blobs.get(area.name)
|
const blob = await this.blobs.get(area.name)
|
||||||
const pmtiles = new BlobSource(area.name, blob)
|
const pmtiles = new BlobSource(area.name, blob)
|
||||||
const tileData = await pmtiles.pmtiles.getZxy(z, x, y)
|
const tileData = await pmtiles.pmtiles.getZxy(z, x, y)
|
||||||
if (!tileData) {
|
if (!tileData) {
|
||||||
console.log("Not found in the archives:", { z, x, y })
|
console.log("Not found in the archives:", { z, x, y })
|
||||||
return new Response("Not found (not in tile archive, should not happen)", { status: 404 })
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Served tile", { z, x, y }, "from installed archive")
|
console.log("Served tile", { z, x, y }, "from installed archive")
|
||||||
return new Response(
|
return new Response(
|
||||||
tileData.data,
|
tileData.data,
|
||||||
{
|
{
|
||||||
headers: { 'Content-Type': 'application/x.protobuf' }
|
headers: { "Content-Type": "application/x.protobuf" }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteArea(description: AreaDescription): Promise<AreaDescription[]> {
|
deleteArea(description: AreaDescription): Promise<ReadonlyArray<Readonly<AreaDescription>>> {
|
||||||
this.blobs.del(description.name)
|
this.blobs.del(description.name)
|
||||||
this.meta.del(description.name)
|
this.meta.del(description.name)
|
||||||
return this.updateCachedMeta()
|
return this.updateCachedMeta()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async fallback(params: RequestParameters,
|
||||||
|
abortController: AbortController) {
|
||||||
|
params.url = params.url.substr("pmtilesoffl://".length)
|
||||||
|
const response = await fetch(
|
||||||
|
new Request(params.url, params)
|
||||||
|
, abortController)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Could not fetch " + params.url + "; status code is" + response.status)
|
||||||
|
}
|
||||||
|
return { data: await response.arrayBuffer() }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async tilev4(
|
||||||
|
params: RequestParameters,
|
||||||
|
abortController: AbortController
|
||||||
|
): Promise<{ data: unknown } | { data: { tiles: string[], minzoom: number, maxzoom: number, bounds: number[] } } | {
|
||||||
|
data: Uint8Array,
|
||||||
|
cacheControl: string,
|
||||||
|
expires: string
|
||||||
|
} | { data: Uint8Array } | { data: null }> {
|
||||||
|
if (params.type === "arrayBuffer") {
|
||||||
|
const re = new RegExp(/(\d+)\/(\d+)\/(\d+).(mvt|pbf)/)
|
||||||
|
const result = params.url.match(re)
|
||||||
|
if (!result) {
|
||||||
|
return await this.fallback(params, abortController)
|
||||||
|
}
|
||||||
|
const z = Number(result[1])
|
||||||
|
const x = Number(result[2])
|
||||||
|
const y = Number(result[3])
|
||||||
|
const r = await this.getTileResponse(z, x, y)
|
||||||
|
if (r?.ok) {
|
||||||
|
return { data: await r.arrayBuffer() }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return await this.fallback(params, abortController)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -3,40 +3,12 @@ import { Utils } from "../../Utils"
|
||||||
export class ThemeMetaTagging {
|
export class ThemeMetaTagging {
|
||||||
public static readonly themeName = "usersettings"
|
public static readonly themeName = "usersettings"
|
||||||
|
|
||||||
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
|
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
|
||||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
|
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
|
||||||
feat.properties._description
|
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' )
|
||||||
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
|
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
|
||||||
?.at(1)
|
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
|
||||||
)
|
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
|
||||||
Utils.AddLazyProperty(
|
feat.properties['__current_backgroun'] = 'initial_value'
|
||||||
feat.properties,
|
|
||||||
"_d",
|
|
||||||
() => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? ""
|
|
||||||
)
|
|
||||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
|
|
||||||
((feat) => {
|
|
||||||
const e = document.createElement("div")
|
|
||||||
e.innerHTML = feat.properties._d
|
|
||||||
return Array.from(e.getElementsByTagName("a")).filter(
|
|
||||||
(a) => a.href.match(/mastodon|en.osm.town/) !== null
|
|
||||||
)[0]?.href
|
|
||||||
})(feat)
|
|
||||||
)
|
|
||||||
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
|
|
||||||
((feat) => {
|
|
||||||
const e = document.createElement("div")
|
|
||||||
e.innerHTML = feat.properties._d
|
|
||||||
return Array.from(e.getElementsByTagName("a")).filter(
|
|
||||||
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
|
|
||||||
)[0]?.href
|
|
||||||
})(feat)
|
|
||||||
)
|
|
||||||
Utils.AddLazyProperty(
|
|
||||||
feat.properties,
|
|
||||||
"_mastodon_candidate",
|
|
||||||
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
|
|
||||||
)
|
|
||||||
feat.properties["__current_backgroun"] = "initial_value"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -142,6 +142,8 @@ export default class Constants {
|
||||||
public static readonly mapillary_client_token_v4 = Constants.config.api_keys.mapillary_v4
|
public static readonly mapillary_client_token_v4 = Constants.config.api_keys.mapillary_v4
|
||||||
public static defaultOverpassUrls = Constants.config.default_overpass_urls
|
public static defaultOverpassUrls = Constants.config.default_overpass_urls
|
||||||
public static countryCoderEndpoint: string = Constants.config.country_coder_host
|
public static countryCoderEndpoint: string = Constants.config.country_coder_host
|
||||||
|
public static readonly pmtiles_host = Constants.config.protomaps_archive_server
|
||||||
|
|
||||||
public static countryCoderInfo: ServerSourceInfo = {
|
public static countryCoderInfo: ServerSourceInfo = {
|
||||||
url: this.countryCoderEndpoint,
|
url: this.countryCoderEndpoint,
|
||||||
trigger: ["always"],
|
trigger: ["always"],
|
||||||
|
|
|
@ -22,6 +22,10 @@ import { GeolocationControlState } from "../../UI/BigComponents/GeolocationContr
|
||||||
import ShowOverlayRasterLayer from "../../UI/Map/ShowOverlayRasterLayer"
|
import ShowOverlayRasterLayer from "../../UI/Map/ShowOverlayRasterLayer"
|
||||||
import { BBox } from "../../Logic/BBox"
|
import { BBox } from "../../Logic/BBox"
|
||||||
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
||||||
|
import { OfflineBasemapManager } from "../../Logic/OfflineBasemapManager"
|
||||||
|
import { IsOnline } from "../../Logic/Web/IsOnline"
|
||||||
|
import { Tiles } from "../TileRange"
|
||||||
|
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The first core of the state management; everything related to:
|
* The first core of the state management; everything related to:
|
||||||
|
@ -51,6 +55,8 @@ export class UserMapFeatureswitchState extends WithUserRelatedState {
|
||||||
readonly currentView: FeatureSource<Feature<Polygon>>
|
readonly currentView: FeatureSource<Feature<Polygon>>
|
||||||
readonly fullNodeDatabase?: FullNodeDatabaseSource
|
readonly fullNodeDatabase?: FullNodeDatabaseSource
|
||||||
|
|
||||||
|
readonly offlineMapManager = OfflineBasemapManager.singleton
|
||||||
|
public readonly autoDownloadOfflineBasemap = UIEventSource.asBoolean(LocalStorageSource.get("autodownload-offline-basemaps", "true"))
|
||||||
constructor(theme: ThemeConfig, selectedElement: Store<object>) {
|
constructor(theme: ThemeConfig, selectedElement: Store<object>) {
|
||||||
const rasterLayer: UIEventSource<RasterLayerPolygon> =
|
const rasterLayer: UIEventSource<RasterLayerPolygon> =
|
||||||
new UIEventSource<RasterLayerPolygon>(undefined)
|
new UIEventSource<RasterLayerPolygon>(undefined)
|
||||||
|
@ -130,6 +136,7 @@ export class UserMapFeatureswitchState extends WithUserRelatedState {
|
||||||
this.initHotkeys()
|
this.initHotkeys()
|
||||||
this.drawOverlayLayers()
|
this.drawOverlayLayers()
|
||||||
this.drawLock()
|
this.drawLock()
|
||||||
|
this.downloadOfflineBasemaps()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -278,4 +285,18 @@ export class UserMapFeatureswitchState extends WithUserRelatedState {
|
||||||
metaTags: this.userRelatedState.preferencesAsTags,
|
metaTags: this.userRelatedState.preferencesAsTags,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private downloadOfflineBasemaps() {
|
||||||
|
const tile = this.mapProperties.location.mapD(l => {
|
||||||
|
if (!IsOnline.isOnline.data || !this.autoDownloadOfflineBasemap.data) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const z = Math.min(Math.floor(this.mapProperties.zoom.data), 10)
|
||||||
|
return Tiles.embedded_tile(l.lat, l.lon, z)
|
||||||
|
},
|
||||||
|
[IsOnline.isOnline, this.mapProperties.zoom, this.autoDownloadOfflineBasemap])
|
||||||
|
tile.addCallbackAndRunD(tile => {
|
||||||
|
this.offlineMapManager.autoInstall(tile)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,6 @@
|
||||||
import type { MapProperties } from "../../Models/MapProperties"
|
import type { MapProperties } from "../../Models/MapProperties"
|
||||||
import ThemeViewState from "../../Models/ThemeViewState"
|
import ThemeViewState from "../../Models/ThemeViewState"
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
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 Loading from "../Base/Loading.svelte"
|
||||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||||
|
@ -20,12 +18,15 @@
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
import { DownloadIcon, TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
|
import { DownloadIcon, TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||||
import { Accordion, AccordionItem } from "flowbite-svelte"
|
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
|
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 map: UIEventSource<MlMap> = new UIEventSource(undefined)
|
||||||
let mapProperties: MapProperties = new MapLibreAdaptor(map)
|
let mapProperties: MapProperties = new MapLibreAdaptor(map)
|
||||||
state?.showCurrentLocationOn(map)
|
state?.showCurrentLocationOn(map)
|
||||||
|
@ -35,77 +36,52 @@
|
||||||
mapProperties.allowRotating.set(false)
|
mapProperties.allowRotating.set(false)
|
||||||
|
|
||||||
|
|
||||||
const offlineMapManager = new OfflineBasemapManager("https://cache.mapcomplete.org/")
|
const offlineMapManager = OfflineBasemapManager.singleton
|
||||||
let installedMeta: UIEventSource<AreaDescription[]> = new UIEventSource([])
|
let installing: Store<ReadonlyMap<string, object>> = offlineMapManager.installing
|
||||||
|
let installed = offlineMapManager.installedAreas
|
||||||
|
let focusTile: Store<{
|
||||||
function updateMeta() {
|
x: number;
|
||||||
offlineMapManager.updateCachedMeta().then(meta => installedMeta.set(meta))
|
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
|
||||||
let installing = new UIEventSource<string[]>([])
|
await offlineMapManager.autoInstall(tile)
|
||||||
|
|
||||||
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 }) => {
|
||||||
let installed: Store<Feature<Polygon>[]> = installedMeta.map(meta =>
|
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 ?? [])
|
(meta ?? [])
|
||||||
.map(area => {
|
.map(area => {
|
||||||
const f = Tiles.asGeojson(area.minzoom, area.x, area.y)
|
const f = Tiles.asGeojson(area.minzoom, area.x, area.y)
|
||||||
f.properties = {
|
f.properties = {
|
||||||
id: area.minzoom + "-" + area.x + "-" + area.y,
|
id: area.minzoom + "-" + area.x + "-" + area.y,
|
||||||
downloaded: "yes",
|
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
|
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, {
|
new ShowDataLayer(map, {
|
||||||
features: new StaticFeatureSource(installed),
|
features: new StaticFeatureSource(installedFeature),
|
||||||
layer: new LayerConfig({
|
layer: new LayerConfig({
|
||||||
id: "downloaded",
|
id: "downloaded",
|
||||||
source: "special",
|
source: "special",
|
||||||
|
@ -133,21 +109,11 @@
|
||||||
location: ["point", "centroid"],
|
location: ["point", "centroid"],
|
||||||
label: "{text}",
|
label: "{text}",
|
||||||
labelCss: "width: w-min",
|
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, {
|
new ShowDataLayer(map, {
|
||||||
features: new StaticFeatureSource(focusTileFeature),
|
features: new StaticFeatureSource(focusTileFeature),
|
||||||
layer: new LayerConfig({
|
layer: new LayerConfig({
|
||||||
|
@ -170,7 +136,14 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<div class="flex flex-col h-full max-h-leave-room">
|
<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 />
|
<Loading />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="h-full overflow-auto pb-16">
|
<div class="h-full overflow-auto pb-16">
|
||||||
|
@ -210,9 +183,9 @@
|
||||||
<div class="leave-room">
|
<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={() => {
|
<button on:click={() => {
|
||||||
installedMeta?.data?.forEach(area => del(area))
|
installed?.data?.forEach(area => del(area))
|
||||||
}}>
|
}}>
|
||||||
<TrashIcon class="w-6" />
|
<TrashIcon class="w-6" />
|
||||||
Delete all
|
Delete all
|
||||||
|
@ -225,7 +198,7 @@
|
||||||
<th>Zoom ranges</th>
|
<th>Zoom ranges</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
{#each ($installedMeta ?? []) as area }
|
{#each ($installed ?? []) as area }
|
||||||
<tr>
|
<tr>
|
||||||
<td>{area.name}</td>
|
<td>{area.name}</td>
|
||||||
<td>{area.dataVersion}</td>
|
<td>{area.dataVersion}</td>
|
||||||
|
@ -250,14 +223,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem paddingDefault="p-2">
|
|
||||||
<div slot="header">
|
|
||||||
Service worker status
|
|
||||||
</div>
|
|
||||||
<div class="leave-room">
|
|
||||||
<ServiceWorkerStatus />
|
|
||||||
</div>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
|
||||||
import { Utils } from "../../Utils"
|
|
||||||
import Loading from "../Base/Loading.svelte"
|
|
||||||
|
|
||||||
let loadedAssets = new UIEventSource<any>(undefined)
|
|
||||||
|
|
||||||
async function update() {
|
|
||||||
loadedAssets.set(await Utils.downloadJson("./service-worker/status.json"))
|
|
||||||
}
|
|
||||||
|
|
||||||
update()
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $loadedAssets === undefined}
|
|
||||||
<Loading />
|
|
||||||
{:else}
|
|
||||||
<button on:click={() => update()}>Update</button>
|
|
||||||
{JSON.stringify($loadedAssets)}
|
|
||||||
{/if}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import maplibregl, { Map as MLMap, Map as MlMap, ScaleControl, VectorTileSource } from "maplibre-gl"
|
import maplibregl, { Map as MLMap, Map as MlMap, ScaleControl } from "maplibre-gl"
|
||||||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import { BBox } from "../../Logic/BBox"
|
import { BBox } from "../../Logic/BBox"
|
||||||
|
@ -11,7 +11,7 @@ import { Protocol } from "pmtiles"
|
||||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
import { Feature, LineString } from "geojson"
|
import { Feature, LineString } from "geojson"
|
||||||
import RasterLayerHandler from "./RasterLayerHandler"
|
import RasterLayerHandler from "./RasterLayerHandler"
|
||||||
import { IsOnline } from "../../Logic/Web/IsOnline"
|
import { OfflineBasemapManager } from "../../Logic/OfflineBasemapManager"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
||||||
|
@ -76,7 +76,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
if (!MapLibreAdaptor.pmtilesInited) {
|
if (!MapLibreAdaptor.pmtilesInited) {
|
||||||
|
const offlineManager = OfflineBasemapManager.singleton
|
||||||
maplibregl.addProtocol("pmtiles", new Protocol().tile)
|
maplibregl.addProtocol("pmtiles", new Protocol().tile)
|
||||||
|
maplibregl.addProtocol("pmtilesoffl", (request, abort) => offlineManager.tilev4(request, abort))
|
||||||
MapLibreAdaptor.pmtilesInited = true
|
MapLibreAdaptor.pmtilesInited = true
|
||||||
}
|
}
|
||||||
this._maplibreMap = maplibreMap
|
this._maplibreMap = maplibreMap
|
||||||
|
@ -244,15 +246,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.forceOfflineVersion(IsOnline.isOnline.data)
|
|
||||||
map.on("styledata", () => {
|
|
||||||
map.once("sourcedataloading", () => {
|
|
||||||
const isOffline = !IsOnline.isOnline.data
|
|
||||||
if (isOffline) {
|
|
||||||
this.forceOfflineVersion(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.location.addCallbackAndRunD((loc) => {
|
this.location.addCallbackAndRunD((loc) => {
|
||||||
|
@ -271,7 +264,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
this.allowZooming.addCallbackAndRun((allowZooming) => this.setAllowZooming(allowZooming))
|
this.allowZooming.addCallbackAndRun((allowZooming) => this.setAllowZooming(allowZooming))
|
||||||
this.bounds.addCallbackAndRunD((bounds) => this.setBounds(bounds))
|
this.bounds.addCallbackAndRunD((bounds) => this.setBounds(bounds))
|
||||||
this.showScale?.addCallbackAndRun((showScale) => this.setScale(showScale))
|
this.showScale?.addCallbackAndRun((showScale) => this.setScale(showScale))
|
||||||
IsOnline.isOnline.addCallbackAndRun(online => this.forceOfflineVersion(!online))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -764,29 +756,4 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* The service worker will attempt to load tiles from an archive instead of getting them from "api.protomaps.com"
|
|
||||||
* However, when truly offline, the browser might think that that domain is not reachable anymore, so we force
|
|
||||||
* <domain>.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private forceOfflineVersion(enable: boolean) {
|
|
||||||
const src = <VectorTileSource>this._maplibreMap.data?.getSource("protomaps")
|
|
||||||
if (!src) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log("Swapping the map source, forcing offline:", enable, "orig:", src.tiles)
|
|
||||||
if (enable) {
|
|
||||||
this._originalProtomapsSource = Array.from(src.tiles)
|
|
||||||
const l = window.location
|
|
||||||
src.tiles = [l.protocol + "//" + l.host + "/service-worker/offline-basemap/tile/{z}-{x}-{y}.mvt"]
|
|
||||||
} else if (this._originalProtomapsSource !== undefined) {
|
|
||||||
|
|
||||||
src.tiles = this._originalProtomapsSource
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _originalProtomapsSource: string[]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
// The global should be that of a service worker.
|
// The global should be that of a service worker.
|
||||||
|
|
||||||
// This fixes `self`'s type.
|
// This fixes `self`'s type.
|
||||||
import { OfflineBasemapManager } from "./OfflineBasemapManager"
|
|
||||||
import { SWGenerated } from "./SWGenerated"
|
import { SWGenerated } from "./SWGenerated"
|
||||||
|
|
||||||
declare var self: ServiceWorkerGlobalScope
|
declare var self: ServiceWorkerGlobalScope
|
||||||
|
@ -10,8 +9,6 @@ export {}
|
||||||
|
|
||||||
const selfDomain = self.location.hostname
|
const selfDomain = self.location.hostname
|
||||||
|
|
||||||
const offlinemaps = new OfflineBasemapManager("https://cache.mapcomplete.org/")
|
|
||||||
|
|
||||||
function jsonResponse(object: object | []): Response {
|
function jsonResponse(object: object | []): Response {
|
||||||
return new Response(JSON.stringify(object), {
|
return new Response(JSON.stringify(object), {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -72,41 +69,6 @@ class Router {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const basemapRouter = new Router(
|
|
||||||
{
|
|
||||||
"meta.json": (event) => {
|
|
||||||
event.respondWith(
|
|
||||||
offlinemaps.updateCachedMeta().then(meta => jsonResponse(meta)))
|
|
||||||
|
|
||||||
},
|
|
||||||
update: (event) => {
|
|
||||||
event.respondWith(
|
|
||||||
offlinemaps.updateCachedMeta()
|
|
||||||
.then(meta => jsonResponse({
|
|
||||||
"status": "installed",
|
|
||||||
installed: meta
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// No delete or install, this is done directly in the GUI
|
|
||||||
"tile":
|
|
||||||
(event, filename) => {
|
|
||||||
console.log("Got a tile request:", filename)
|
|
||||||
const tileRegex = /(\d+-\d+-\d+).mvt$/
|
|
||||||
const tileMatch = filename.match(tileRegex)
|
|
||||||
if (!tileMatch) {
|
|
||||||
console.log("This is _not_ a tile")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const url = new URL(event.request.url)
|
|
||||||
const autoInstall = url.searchParams.get("auto") === "true"
|
|
||||||
const [z, x, y] = tileMatch[1].split("-").map(s => Number(s))
|
|
||||||
event.respondWith(offlinemaps.getTileResponse(z, x, y, { autoInstall }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const allOffline = new Router({
|
const allOffline = new Router({
|
||||||
"status.json": (event) => {
|
"status.json": (event) => {
|
||||||
|
@ -115,21 +77,13 @@ const allOffline = new Router({
|
||||||
jsonResponse(
|
jsonResponse(
|
||||||
{
|
{
|
||||||
status: "ok", cached,
|
status: "ok", cached,
|
||||||
tiles: offlinemaps.getMeta()
|
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, {
|
}, {})
|
||||||
"offline-basemap": (event, rest) => {
|
|
||||||
basemapRouter.route(event, rest)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(event: FetchEvent, rest: string) => {
|
|
||||||
console.log("Got a request to the service worker for an unknown endpoint:", rest)
|
|
||||||
})
|
|
||||||
|
|
||||||
const matchTiles = /(\d+)\/(\d+)\/(\d+).mvt$/
|
|
||||||
self.addEventListener("fetch", (event) => {
|
self.addEventListener("fetch", (event) => {
|
||||||
const url = event.request.url
|
const url = event.request.url
|
||||||
if (url.endsWith("/service-worker.js")) {
|
if (url.endsWith("/service-worker.js")) {
|
||||||
|
@ -141,23 +95,6 @@ self.addEventListener("fetch", (event) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const urlObj = new URL(url)
|
const urlObj = new URL(url)
|
||||||
if ((urlObj.host === "api.protomaps.com" || urlObj.hostname === selfDomain) && urlObj.pathname.indexOf("tiles/v4") >= 0) {
|
|
||||||
// "https://api.protomaps.com/tiles/v4/${z}/${x}/${y}.mvt?key=2af8b969a9e8b692&auto=true"
|
|
||||||
const auto = urlObj.searchParams.get("auto") === "true"
|
|
||||||
const match = urlObj.pathname.match(matchTiles)
|
|
||||||
if (!match) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const z = Number(match[1])
|
|
||||||
const x = Number(match[2])
|
|
||||||
const y = Number(match[3])
|
|
||||||
console.log("Match is:", match, { x, y, z })
|
|
||||||
|
|
||||||
event.respondWith(offlinemaps.getTileResponse(z, x, y, {
|
|
||||||
autoInstall: auto,
|
|
||||||
fallback: url
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
if (urlObj.hostname === selfDomain && selfDomain !== "localhost" && selfDomain !== "127.0.0.1") {
|
if (urlObj.hostname === selfDomain && selfDomain !== "localhost" && selfDomain !== "127.0.0.1") {
|
||||||
respondFromCache(event)
|
respondFromCache(event)
|
||||||
return
|
return
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"index.ts",
|
"index.ts",
|
||||||
"OfflineBasemapManager.ts",
|
|
||||||
"SWGenerated.ts"
|
"SWGenerated.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue