forked from MapComplete/MapComplete
Feature:improve offline basemap retention and fallback behaviour
This commit is contained in:
parent
77ef3a3572
commit
848ec121f1
15 changed files with 312 additions and 167 deletions
|
|
@ -108,7 +108,7 @@
|
||||||
"query:licenses": "vite-node scripts/generateLicenseInfo.ts -- --query && npm run generate:licenses",
|
"query:licenses": "vite-node scripts/generateLicenseInfo.ts -- --query && npm run generate:licenses",
|
||||||
"clean:licenses": "find . -type f -name \"*.license\" -exec rm -f {} +",
|
"clean:licenses": "find . -type f -name \"*.license\" -exec rm -f {} +",
|
||||||
"generate:contributor-list": "vite-node scripts/generateContributors.ts",
|
"generate:contributor-list": "vite-node scripts/generateContributors.ts",
|
||||||
"generate:service-worker": "cd ./src/service-worker/ && rollup -c ",
|
"generate:service-worker": "vite-node scripts/prepareServiceWorker.ts && cd ./src/service-worker/ && rollup -c ",
|
||||||
"generate": "npm run generate:licenses && npm run generate:images && npm run generate:charging-stations && npm run generate:translations && npm run reset:layeroverview && npm run generate:service-worker",
|
"generate": "npm run generate:licenses && npm run generate:images && npm run generate:charging-stations && npm run generate:translations && npm run reset:layeroverview && npm run generate:service-worker",
|
||||||
"generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -",
|
"generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -",
|
||||||
"clean:tests": "find . -type f -name \"*.doctest.ts\" | xargs -r rm",
|
"clean:tests": "find . -type f -name \"*.doctest.ts\" | xargs -r rm",
|
||||||
|
|
|
||||||
|
|
@ -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": [
|
||||||
"./service-worker/offline-basemap/{z}-{x}-{y}.mvt?fallback=true&auto=true"
|
"https://api.protomaps.com/tiles/v4/{z}/{x}/{y}.mvt?key=2af8b969a9e8b692&auto=true"
|
||||||
],
|
],
|
||||||
"maxzoom": 15,
|
"maxzoom": 15,
|
||||||
"minzoom": 0
|
"minzoom": 0
|
||||||
|
|
|
||||||
|
|
@ -1770,10 +1770,6 @@ input[type="range"].range-lg::-moz-range-thumb {
|
||||||
height: 0.875rem;
|
height: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-3\/4 {
|
|
||||||
height: 75%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-32 {
|
.h-32 {
|
||||||
height: 8rem;
|
height: 8rem;
|
||||||
}
|
}
|
||||||
|
|
@ -4147,6 +4143,10 @@ input[type="range"].range-lg::-moz-range-thumb {
|
||||||
padding-bottom: 2.5rem;
|
padding-bottom: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pb-16 {
|
||||||
|
padding-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pb-2 {
|
.pb-2 {
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
import { Store, UIEventSource } from "../UIEventSource"
|
import { Store, UIEventSource } from "../UIEventSource"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import {
|
import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "../../Models/RasterLayers"
|
||||||
AvailableRasterLayers,
|
|
||||||
RasterLayerPolygon,
|
|
||||||
RasterLayerUtils,
|
|
||||||
} from "../../Models/RasterLayers"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When a user pans around on the map, they might pan out of the range of the current background raster layer.
|
* When a user pans around on the map, they might pan out of the range of the current background raster layer.
|
||||||
|
|
@ -13,7 +9,7 @@ import {
|
||||||
export default class BackgroundLayerResetter {
|
export default class BackgroundLayerResetter {
|
||||||
constructor(
|
constructor(
|
||||||
currentBackgroundLayer: UIEventSource<RasterLayerPolygon | undefined>,
|
currentBackgroundLayer: UIEventSource<RasterLayerPolygon | undefined>,
|
||||||
availableLayers: { store: Store<RasterLayerPolygon[]> }
|
availableLayers: Store<RasterLayerPolygon[]>
|
||||||
) {
|
) {
|
||||||
if (Utils.runningFromConsole) {
|
if (Utils.runningFromConsole) {
|
||||||
return
|
return
|
||||||
|
|
@ -28,7 +24,7 @@ export default class BackgroundLayerResetter {
|
||||||
) {
|
) {
|
||||||
BackgroundLayerResetter.installHandler(
|
BackgroundLayerResetter.installHandler(
|
||||||
currentBackgroundLayer,
|
currentBackgroundLayer,
|
||||||
availableLayers.store
|
availableLayers
|
||||||
)
|
)
|
||||||
return true // unregister
|
return true // unregister
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +51,7 @@ export default class BackgroundLayerResetter {
|
||||||
console.log("Current layer properties:", currentBgPolygon)
|
console.log("Current layer properties:", currentBgPolygon)
|
||||||
// Oops, we panned out of range for this layer!
|
// Oops, we panned out of range for this layer!
|
||||||
// What is the 'best' map of the same category which is available?
|
// What is the 'best' map of the same category which is available?
|
||||||
const availableInSameCat = RasterLayerUtils.SelectBestLayerAccordingTo(
|
const availableInSameCat = RasterLayerUtils.selectBestLayerAccordingTo(
|
||||||
availableLayers,
|
availableLayers,
|
||||||
currentBgPolygon?.properties?.category
|
currentBgPolygon?.properties?.category
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { eliCategory } from "../../Models/RasterLayerProperties"
|
||||||
*/
|
*/
|
||||||
export class PreferredRasterLayerSelector {
|
export class PreferredRasterLayerSelector {
|
||||||
private readonly _rasterLayerSetting: UIEventSource<RasterLayerPolygon>
|
private readonly _rasterLayerSetting: UIEventSource<RasterLayerPolygon>
|
||||||
private readonly _availableLayers: { store: Store<RasterLayerPolygon[]> }
|
private readonly _availableLayers: Store<RasterLayerPolygon[]>
|
||||||
private readonly _preferredBackgroundLayer: UIEventSource<
|
private readonly _preferredBackgroundLayer: UIEventSource<
|
||||||
string | "photo" | "map" | "osmbasedmap" | undefined
|
string | "photo" | "map" | "osmbasedmap" | undefined
|
||||||
>
|
>
|
||||||
|
|
@ -17,7 +17,7 @@ export class PreferredRasterLayerSelector {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
rasterLayerSetting: UIEventSource<RasterLayerPolygon>,
|
rasterLayerSetting: UIEventSource<RasterLayerPolygon>,
|
||||||
availableLayers: { store: Store<RasterLayerPolygon[]> },
|
availableLayers: Store<RasterLayerPolygon[]>,
|
||||||
queryParameter: UIEventSource<string>,
|
queryParameter: UIEventSource<string>,
|
||||||
preferredBackgroundLayer: UIEventSource<
|
preferredBackgroundLayer: UIEventSource<
|
||||||
string | "photo" | "map" | "osmbasedmap" | undefined
|
string | "photo" | "map" | "osmbasedmap" | undefined
|
||||||
|
|
@ -47,7 +47,7 @@ export class PreferredRasterLayerSelector {
|
||||||
if (AvailableRasterLayers.globalLayers.find((l) => l.id === layer.properties.id)) {
|
if (AvailableRasterLayers.globalLayers.find((l) => l.id === layer.properties.id)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this._availableLayers.store.addCallbackD(() => this.updateLayer())
|
this._availableLayers.addCallbackD(() => this.updateLayer())
|
||||||
return true // unregister
|
return true // unregister
|
||||||
})
|
})
|
||||||
this.updateLayer()
|
this.updateLayer()
|
||||||
|
|
@ -73,7 +73,7 @@ export class PreferredRasterLayerSelector {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const isCategory = eliCategory.indexOf(<any>targetLayerId) >= 0
|
const isCategory = eliCategory.indexOf(<any>targetLayerId) >= 0
|
||||||
const available = this._availableLayers.store.data
|
const available = this._availableLayers.data
|
||||||
const foundLayer = isCategory
|
const foundLayer = isCategory
|
||||||
? available.find((l) => l.properties.category === targetLayerId)
|
? available.find((l) => l.properties.category === targetLayerId)
|
||||||
: available.find((l) => l.properties.id.toLowerCase() === targetLayerId)
|
: available.find((l) => l.properties.id.toLowerCase() === targetLayerId)
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import { BBox } from "../Logic/BBox"
|
||||||
import { Store, Stores } from "../Logic/UIEventSource"
|
import { Store, Stores } from "../Logic/UIEventSource"
|
||||||
import { GeoOperations } from "../Logic/GeoOperations"
|
import { GeoOperations } from "../Logic/GeoOperations"
|
||||||
import { EliCategory, RasterLayerProperties } from "./RasterLayerProperties"
|
import { EliCategory, RasterLayerProperties } from "./RasterLayerProperties"
|
||||||
import { Utils } from "../Utils"
|
|
||||||
import { default as ELI } from "../../public/assets/data/editor-layer-index.json"
|
import { default as ELI } from "../../public/assets/data/editor-layer-index.json"
|
||||||
|
import { IsOnline } from "../Logic/Web/IsOnline"
|
||||||
|
|
||||||
export type EditorLayerIndex = (Feature<Polygon, EditorLayerIndexProperties> & RasterLayerPolygon)[]
|
export type EditorLayerIndex = (Feature<Polygon, EditorLayerIndexProperties> & RasterLayerPolygon)[]
|
||||||
|
|
||||||
|
|
@ -20,8 +20,6 @@ export class AvailableRasterLayers {
|
||||||
return AvailableRasterLayers._editorLayerIndex
|
return AvailableRasterLayers._editorLayerIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly globalLayers: ReadonlyArray<RasterLayerPolygon> =
|
|
||||||
AvailableRasterLayers.initGlobalLayers()
|
|
||||||
public static bing = <RasterLayerPolygon>bingJson
|
public static bing = <RasterLayerPolygon>bingJson
|
||||||
public static readonly osmCartoProperties: RasterLayerProperties = {
|
public static readonly osmCartoProperties: RasterLayerProperties = {
|
||||||
id: "osm",
|
id: "osm",
|
||||||
|
|
@ -29,19 +27,42 @@ export class AvailableRasterLayers {
|
||||||
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
attribution: {
|
attribution: {
|
||||||
text: "OpenStreetMap",
|
text: "OpenStreetMap",
|
||||||
url: "https://openStreetMap.org/copyright",
|
url: "https://openStreetMap.org/copyright"
|
||||||
},
|
},
|
||||||
best: true,
|
best: true,
|
||||||
max_zoom: 19,
|
max_zoom: 19,
|
||||||
min_zoom: 0,
|
min_zoom: 0,
|
||||||
category: "osmbasedmap",
|
category: "osmbasedmap"
|
||||||
}
|
}
|
||||||
public static readonly osmCarto: RasterLayerPolygon = {
|
public static readonly osmCarto: RasterLayerPolygon = {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties: AvailableRasterLayers.osmCartoProperties,
|
properties: AvailableRasterLayers.osmCartoProperties,
|
||||||
geometry: BBox.global.asGeometry(),
|
geometry: BBox.global.asGeometry()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static readonly sunnyOfflineProperties: RasterLayerProperties = {
|
||||||
|
"style": "./assets/sunny.json",
|
||||||
|
"best": true,
|
||||||
|
"id": "protomaps.sunny-self",
|
||||||
|
"name": "Protomaps Sunny",
|
||||||
|
"type": "vector",
|
||||||
|
"category": "osmbasedmap",
|
||||||
|
"attribution": {
|
||||||
|
"text": "Protomaps",
|
||||||
|
"url": "https://protomaps.com/"
|
||||||
|
},
|
||||||
|
url: "https://mapcomplete.org/"
|
||||||
|
}
|
||||||
|
public static readonly sunnyOffline: RasterLayerPolygon = {
|
||||||
|
type: "Feature",
|
||||||
|
properties: AvailableRasterLayers.sunnyOfflineProperties,
|
||||||
|
geometry: BBox.global.asGeometry()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static readonly globalLayers: ReadonlyArray<RasterLayerPolygon> =
|
||||||
|
AvailableRasterLayers.initGlobalLayers()
|
||||||
|
|
||||||
public static allAvailableGlobalLayers = new Set([
|
public static allAvailableGlobalLayers = new Set([
|
||||||
...AvailableRasterLayers.globalLayers,
|
...AvailableRasterLayers.globalLayers,
|
||||||
AvailableRasterLayers.osmCarto,
|
AvailableRasterLayers.osmCarto,
|
||||||
|
|
@ -53,6 +74,7 @@ export class AvailableRasterLayers {
|
||||||
(properties) =>
|
(properties) =>
|
||||||
properties.id !== "osm.carto" && properties.id !== "Bing" /*Added separately*/
|
properties.id !== "osm.carto" && properties.id !== "Bing" /*Added separately*/
|
||||||
)
|
)
|
||||||
|
gl.unshift(AvailableRasterLayers.sunnyOfflineProperties)
|
||||||
const glEli: RasterLayerProperties[] = globallayersEli["default"] ?? globallayersEli
|
const glEli: RasterLayerProperties[] = globallayersEli["default"] ?? globallayersEli
|
||||||
const joined = gl.concat(glEli)
|
const joined = gl.concat(glEli)
|
||||||
if (joined.some((j) => !j.id)) {
|
if (joined.some((j) => !j.id)) {
|
||||||
|
|
@ -71,20 +93,13 @@ export class AvailableRasterLayers {
|
||||||
/**
|
/**
|
||||||
* The default background layer that any theme uses which does not explicitly define a background
|
* The default background layer that any theme uses which does not explicitly define a background
|
||||||
*/
|
*/
|
||||||
public static readonly defaultBackgroundLayer: RasterLayerPolygon =
|
public static readonly defaultBackgroundLayer: RasterLayerPolygon = AvailableRasterLayers.sunnyOffline
|
||||||
AvailableRasterLayers.globalLayers.find((l) => {
|
|
||||||
return l.properties.id === "protomaps.sunny"
|
|
||||||
})
|
|
||||||
|
|
||||||
public static layersAvailableAt(
|
public static layersAvailableAt(
|
||||||
location: Store<{ lon: number; lat: number }>,
|
location: Store<{ lon: number; lat: number }>,
|
||||||
enableBing?: Store<boolean>
|
enableBing?: Store<boolean>
|
||||||
): { store: Store<RasterLayerPolygon[]> } {
|
): Store<RasterLayerPolygon[]> {
|
||||||
const store = { store: undefined }
|
return AvailableRasterLayers._layersAvailableAt(location, enableBing)
|
||||||
Utils.AddLazyProperty(store, "store", () =>
|
|
||||||
AvailableRasterLayers._layersAvailableAt(location, enableBing)
|
|
||||||
)
|
|
||||||
return store
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static _layersAvailableAt(
|
private static _layersAvailableAt(
|
||||||
|
|
@ -101,6 +116,9 @@ export class AvailableRasterLayers {
|
||||||
return Stores.ListStabilized(
|
return Stores.ListStabilized(
|
||||||
availableLayersBboxes.mapD(
|
availableLayersBboxes.mapD(
|
||||||
(eliPolygons) => {
|
(eliPolygons) => {
|
||||||
|
if (!IsOnline.isOnline.data) {
|
||||||
|
return [this.sunnyOffline]
|
||||||
|
}
|
||||||
const loc = location.data
|
const loc = location.data
|
||||||
const lonlat: [number, number] = [loc?.lon ?? 0, loc?.lat ?? 0]
|
const lonlat: [number, number] = [loc?.lon ?? 0, loc?.lat ?? 0]
|
||||||
const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
|
const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
|
||||||
|
|
@ -118,14 +136,14 @@ export class AvailableRasterLayers {
|
||||||
if (
|
if (
|
||||||
!matching.some(
|
!matching.some(
|
||||||
(l) =>
|
(l) =>
|
||||||
l.id === AvailableRasterLayers.defaultBackgroundLayer.properties.id
|
l.id === AvailableRasterLayers.defaultBackgroundLayer?.properties?.id
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
matching.push(AvailableRasterLayers.defaultBackgroundLayer)
|
matching.push(AvailableRasterLayers.defaultBackgroundLayer)
|
||||||
}
|
}
|
||||||
return matching
|
return matching
|
||||||
},
|
},
|
||||||
[enableBing]
|
[enableBing, IsOnline.isOnline]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +159,7 @@ export class RasterLayerUtils {
|
||||||
* @param ignoreLayer
|
* @param ignoreLayer
|
||||||
* @param skipLayers Skip the first N layers
|
* @param skipLayers Skip the first N layers
|
||||||
*/
|
*/
|
||||||
public static SelectBestLayerAccordingTo(
|
public static selectBestLayerAccordingTo(
|
||||||
available: RasterLayerPolygon[],
|
available: RasterLayerPolygon[],
|
||||||
preferredCategory: string,
|
preferredCategory: string,
|
||||||
ignoreLayer?: RasterLayerPolygon,
|
ignoreLayer?: RasterLayerPolygon,
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export class UserMapFeatureswitchState extends WithUserRelatedState {
|
||||||
Feature<Point, GeoLocationPointProperties>
|
Feature<Point, GeoLocationPointProperties>
|
||||||
>
|
>
|
||||||
|
|
||||||
readonly availableLayers: { store: Store<RasterLayerPolygon[]> }
|
readonly availableLayers: Store<RasterLayerPolygon[]>
|
||||||
readonly currentView: FeatureSource<Feature<Polygon>>
|
readonly currentView: FeatureSource<Feature<Polygon>>
|
||||||
readonly fullNodeDatabase?: FullNodeDatabaseSource
|
readonly fullNodeDatabase?: FullNodeDatabaseSource
|
||||||
|
|
||||||
|
|
@ -180,14 +180,14 @@ export class UserMapFeatureswitchState extends WithUserRelatedState {
|
||||||
|
|
||||||
const setLayerCategory = (category: EliCategory, skipLayers: number = 0) => {
|
const setLayerCategory = (category: EliCategory, skipLayers: number = 0) => {
|
||||||
const timeOfCall = new Date()
|
const timeOfCall = new Date()
|
||||||
this.availableLayers.store.addCallbackAndRunD((available) => {
|
this.availableLayers.addCallbackAndRunD((available) => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const timeDiff = (now.getTime() - timeOfCall.getTime()) / 1000
|
const timeDiff = (now.getTime() - timeOfCall.getTime()) / 1000
|
||||||
if (timeDiff > 3) {
|
if (timeDiff > 3) {
|
||||||
return true // unregister
|
return true // unregister
|
||||||
}
|
}
|
||||||
const current = this.mapProperties.rasterLayer
|
const current = this.mapProperties.rasterLayer
|
||||||
const best = RasterLayerUtils.SelectBestLayerAccordingTo(
|
const best = RasterLayerUtils.selectBestLayerAccordingTo(
|
||||||
available,
|
available,
|
||||||
category,
|
category,
|
||||||
current.data,
|
current.data,
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@
|
||||||
let rasterLayer = state?.mapProperties.rasterLayer
|
let rasterLayer = state?.mapProperties.rasterLayer
|
||||||
if (bg !== undefined) {
|
if (bg !== undefined) {
|
||||||
if (eliCategory.indexOf(bg) >= 0) {
|
if (eliCategory.indexOf(bg) >= 0) {
|
||||||
const availableLayers = state.availableLayers.store.data
|
const availableLayers = state.availableLayers.data
|
||||||
const startLayer: RasterLayerPolygon = RasterLayerUtils.SelectBestLayerAccordingTo(
|
const startLayer: RasterLayerPolygon = RasterLayerUtils.selectBestLayerAccordingTo(
|
||||||
availableLayers,
|
availableLayers,
|
||||||
bg
|
bg
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import maplibregl, { Map as MLMap, Map as MlMap, ScaleControl } from "maplibre-gl"
|
import maplibregl, { Map as MLMap, Map as MlMap, ScaleControl, VectorTileSource } 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"
|
||||||
|
|
@ -7,11 +7,11 @@ import { ExportableMap, KeyNavigationEvent, MapProperties } from "../../Models/M
|
||||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||||
import MaplibreMap from "./MaplibreMap.svelte"
|
import MaplibreMap from "./MaplibreMap.svelte"
|
||||||
import * as htmltoimage from "html-to-image"
|
import * as htmltoimage from "html-to-image"
|
||||||
import Constants from "../../Models/Constants"
|
|
||||||
import { Protocol } from "pmtiles"
|
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"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
||||||
|
|
@ -169,8 +169,8 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncStores = () => {
|
const syncStores = () => {
|
||||||
this.MoveMapToCurrentLoc(this.location.data)
|
this.moveMapToCurrentLoc(this.location.data)
|
||||||
this.SetZoom(this.zoom.data)
|
this.setZoom(this.zoom.data)
|
||||||
this.setMaxBounds(this.maxbounds.data)
|
this.setMaxBounds(this.maxbounds.data)
|
||||||
this.setAllowMoving(this.allowMoving.data)
|
this.setAllowMoving(this.allowMoving.data)
|
||||||
this.setAllowRotating(this.allowRotating.data)
|
this.setAllowRotating(this.allowRotating.data)
|
||||||
|
|
@ -178,7 +178,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
this.setMinzoom(this.minzoom.data)
|
this.setMinzoom(this.minzoom.data)
|
||||||
this.setMaxzoom(this.maxzoom.data)
|
this.setMaxzoom(this.maxzoom.data)
|
||||||
this.setBounds(this.bounds.data)
|
this.setBounds(this.bounds.data)
|
||||||
this.SetRotation(this.rotation.data)
|
this.setRotation(this.rotation.data)
|
||||||
this.setScale(this.showScale.data)
|
this.setScale(this.showScale.data)
|
||||||
this.updateStores(true)
|
this.updateStores(true)
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +201,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
})
|
})
|
||||||
|
|
||||||
// map.on("contextmenu", ...) only works on desktop, hence we listen to the container:
|
// map.on("contextmenu", ...) only works on desktop, hence we listen to the container:
|
||||||
|
|
||||||
map._container.addEventListener("contextmenu", (e) => {
|
map._container.addEventListener("contextmenu", (e) => {
|
||||||
const lngLat = map.unproject([e.x, e.y])
|
const lngLat = map.unproject([e.x, e.y])
|
||||||
lastClickLocation.setData({ lon: lngLat.lng, lat: lngLat.lat, mode: "right" })
|
lastClickLocation.setData({ lon: lngLat.lng, lat: lngLat.lat, mode: "right" })
|
||||||
|
|
@ -210,7 +209,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
handleClick(e, "left")
|
handleClick(e, "left")
|
||||||
})
|
})
|
||||||
|
|
||||||
map.on("rotateend", (_) => {
|
map.on("rotateend", () => {
|
||||||
this.updateStores()
|
this.updateStores()
|
||||||
})
|
})
|
||||||
map.on("pitchend", () => {
|
map.on("pitchend", () => {
|
||||||
|
|
@ -245,14 +244,23 @@ 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) => {
|
||||||
this.MoveMapToCurrentLoc(loc)
|
this.moveMapToCurrentLoc(loc)
|
||||||
})
|
})
|
||||||
this.zoom.addCallbackAndRunD((z) => this.SetZoom(z))
|
this.zoom.addCallbackAndRunD((z) => this.setZoom(z))
|
||||||
this.maxbounds.addCallbackAndRun((bbox) => this.setMaxBounds(bbox))
|
this.maxbounds.addCallbackAndRun((bbox) => this.setMaxBounds(bbox))
|
||||||
this.rotation.addCallbackAndRunD((bearing) => this.SetRotation(bearing))
|
this.rotation.addCallbackAndRunD((bearing) => this.setRotation(bearing))
|
||||||
this.allowMoving.addCallbackAndRun((allowMoving) => {
|
this.allowMoving.addCallbackAndRun((allowMoving) => {
|
||||||
this.setAllowMoving(allowMoving)
|
this.setAllowMoving(allowMoving)
|
||||||
this.pingKeycodeEvent(allowMoving ? "unlocked" : "locked")
|
this.pingKeycodeEvent(allowMoving ? "unlocked" : "locked")
|
||||||
|
|
@ -263,6 +271,7 @@ 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -449,7 +458,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
element.style.height = "" + h
|
element.style.height = "" + h
|
||||||
|
|
||||||
if (offset && rescaleIcons !== 1) {
|
if (offset && rescaleIcons !== 1) {
|
||||||
const [_, __, relYStr] = offset
|
const relYStr = offset[2]
|
||||||
const relY = Number(relYStr)
|
const relY = Number(relYStr)
|
||||||
y += img.height * (relY / 100)
|
y += img.height * (relY / 100)
|
||||||
}
|
}
|
||||||
|
|
@ -541,7 +550,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
this.pitch.setData(map.getPitch())
|
this.pitch.setData(map.getPitch())
|
||||||
}
|
}
|
||||||
|
|
||||||
private SetZoom(z: number): void {
|
private setZoom(z: number): void {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (!map || z === undefined) {
|
if (!map || z === undefined) {
|
||||||
return
|
return
|
||||||
|
|
@ -554,7 +563,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SetRotation(bearing: number): void {
|
private setRotation(bearing: number): void {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (!map || bearing === undefined) {
|
if (!map || bearing === undefined) {
|
||||||
return
|
return
|
||||||
|
|
@ -562,7 +571,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
map.rotateTo(bearing, { duration: 0 })
|
map.rotateTo(bearing, { duration: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
private MoveMapToCurrentLoc(loc: { lat: number; lon: number }): void {
|
private moveMapToCurrentLoc(loc: { lat: number; lon: number }): void {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (!map || loc === undefined) {
|
if (!map || loc === undefined) {
|
||||||
return
|
return
|
||||||
|
|
@ -754,4 +763,30 @@ 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[]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,10 @@
|
||||||
import Loading from "../Base/Loading.svelte"
|
import Loading from "../Base/Loading.svelte"
|
||||||
import Page from "../Base/Page.svelte"
|
import Page from "../Base/Page.svelte"
|
||||||
import ThemeViewState from "../../Models/ThemeViewState"
|
import ThemeViewState from "../../Models/ThemeViewState"
|
||||||
import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid"
|
import { Square3Stack3dIcon, WifiIcon } from "@babeard/svelte-heroicons/solid"
|
||||||
|
import { IsOnline } from "../../Logic/Web/IsOnline"
|
||||||
|
import Cross_bottom_right from "../../assets/svg/Cross_bottom_right.svelte"
|
||||||
|
import CrossedOut from "../Base/CrossedOut.svelte"
|
||||||
|
|
||||||
export let state: ThemeViewState
|
export let state: ThemeViewState
|
||||||
|
|
||||||
|
|
@ -19,8 +22,7 @@
|
||||||
let mapproperties = state.mapProperties
|
let mapproperties = state.mapProperties
|
||||||
let userstate = state.userRelatedState
|
let userstate = state.userRelatedState
|
||||||
let shown = state.guistate.pageStates.background
|
let shown = state.guistate.pageStates.background
|
||||||
let availableLayers: { store: Store<RasterLayerPolygon[]> } = state.availableLayers
|
let availableLayers: Store<RasterLayerPolygon[]> = state.availableLayers
|
||||||
let _availableLayers = availableLayers.store
|
|
||||||
|
|
||||||
type CategoryType = "photo" | "map" | "other" | "osmbasedmap"
|
type CategoryType = "photo" | "map" | "other" | "osmbasedmap"
|
||||||
const categories: Record<CategoryType, EliCategory[]> = {
|
const categories: Record<CategoryType, EliCategory[]> = {
|
||||||
|
|
@ -32,7 +34,7 @@
|
||||||
|
|
||||||
function availableForCategory(type: CategoryType): Store<RasterLayerPolygon[]> {
|
function availableForCategory(type: CategoryType): Store<RasterLayerPolygon[]> {
|
||||||
const keywords = categories[type]
|
const keywords = categories[type]
|
||||||
return _availableLayers.mapD((available) =>
|
return availableLayers.mapD((available) =>
|
||||||
available.filter((layer) => keywords.indexOf(<EliCategory>layer.properties.category) >= 0)
|
available.filter((layer) => keywords.indexOf(<EliCategory>layer.properties.category) >= 0)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -51,6 +53,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
export let onlyLink: boolean
|
export let onlyLink: boolean
|
||||||
|
let isOnline = IsOnline.isOnline
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page {onlyLink} {shown} fullscreen={true}>
|
<Page {onlyLink} {shown} fullscreen={true}>
|
||||||
|
|
@ -59,7 +62,17 @@
|
||||||
|
|
||||||
<Tr t={Translations.t.general.backgroundMap} />
|
<Tr t={Translations.t.general.backgroundMap} />
|
||||||
</div>
|
</div>
|
||||||
{#if $_availableLayers?.length < 1}
|
|
||||||
|
{#if !$isOnline}
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<div class="alert flex items-center gap-x-4">
|
||||||
|
<CrossedOut>
|
||||||
|
<WifiIcon />
|
||||||
|
</CrossedOut>
|
||||||
|
Your device is offline. Aerial imagery is not available
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if $availableLayers?.length < 1}
|
||||||
<Loading />
|
<Loading />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col gap-x-2 gap-y-2 sm:flex-row" style="height: calc( 100% - 5rem)">
|
<div class="flex flex-col gap-x-2 gap-y-2 sm:flex-row" style="height: calc( 100% - 5rem)">
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@
|
||||||
import WelcomeBack from "./BigComponents/WelcomeBack.svelte"
|
import WelcomeBack from "./BigComponents/WelcomeBack.svelte"
|
||||||
import InsetSpacer from "./Base/InsetSpacer.svelte"
|
import InsetSpacer from "./Base/InsetSpacer.svelte"
|
||||||
import { AndroidPolyfill } from "../Logic/Web/AndroidPolyfill"
|
import { AndroidPolyfill } from "../Logic/Web/AndroidPolyfill"
|
||||||
|
import { IsOnline } from "../Logic/Web/IsOnline"
|
||||||
export let state: WithSearchState
|
export let state: WithSearchState
|
||||||
new TitleHandler(state.selectedElement, state)
|
new TitleHandler(state.selectedElement, state)
|
||||||
|
|
||||||
|
|
@ -183,6 +184,8 @@
|
||||||
function onMapDragged() {
|
function onMapDragged() {
|
||||||
mapIsDragged.ping()
|
mapIsDragged.ping()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isOnline = IsOnline.isOnline
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|
@ -258,9 +261,11 @@
|
||||||
<Filter class="h-6 w-6" />
|
<Filter class="h-6 w-6" />
|
||||||
</MapControlButton>
|
</MapControlButton>
|
||||||
</If>
|
</If>
|
||||||
|
{#if $isOnline}
|
||||||
<If condition={state.featureSwitches.featureSwitchBackgroundSelection}>
|
<If condition={state.featureSwitches.featureSwitchBackgroundSelection}>
|
||||||
<OpenBackgroundSelectorButton hideTooltip={true} {state} />
|
<OpenBackgroundSelectorButton hideTooltip={true} {state} />
|
||||||
</If>
|
</If>
|
||||||
|
{/if}
|
||||||
<button
|
<button
|
||||||
class="unstyled bg-black-transparent pointer-events-auto ml-1 h-fit max-h-12 cursor-pointer overflow-hidden rounded-2xl px-1 text-white opacity-50 hover:opacity-100"
|
class="unstyled bg-black-transparent pointer-events-auto ml-1 h-fit max-h-12 cursor-pointer overflow-hidden rounded-2xl px-1 text-white opacity-50 hover:opacity-100"
|
||||||
style="background: #00000088; padding: 0.25rem; border-radius: 2rem;"
|
style="background: #00000088; padding: 0.25rem; border-radius: 2rem;"
|
||||||
|
|
@ -441,7 +446,9 @@
|
||||||
<If condition={state.featureSwitches.featureSwitchFakeUser}>
|
<If condition={state.featureSwitches.featureSwitchFakeUser}>
|
||||||
<div class="alert w-fit">Faking a user (Testmode)</div>
|
<div class="alert w-fit">Faking a user (Testmode)</div>
|
||||||
</If>
|
</If>
|
||||||
{#if $apiState === "unknown"}
|
{#if !$isOnline}
|
||||||
|
<div class="alert">Offline mode</div>
|
||||||
|
{:else if $apiState === "unknown"}
|
||||||
<Loading />
|
<Loading />
|
||||||
{:else if $apiState !== "online"}
|
{:else if $apiState !== "online"}
|
||||||
<div class="alert w-fit">API is {$apiState}</div>
|
<div class="alert w-fit">API is {$apiState}</div>
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,5 @@
|
||||||
{
|
{
|
||||||
"layers": [
|
"layers": [
|
||||||
{
|
|
||||||
"style": "assets/sunny-hosted.json",
|
|
||||||
"connect-src": [
|
|
||||||
"https://protomaps.github.io"
|
|
||||||
],
|
|
||||||
"best": true,
|
|
||||||
"id": "protomaps.sunny",
|
|
||||||
"name": "Protomaps Sunny (Hosted by Protomaps)",
|
|
||||||
"type": "vector",
|
|
||||||
"category": "osmbasedmap",
|
|
||||||
"attribution": {
|
|
||||||
"text": "Protomaps",
|
|
||||||
"url": "https://protomaps.com/"
|
|
||||||
},
|
|
||||||
"url": "pmtiles://https://api.protomaps.com/tiles/v4.json?key=2af8b969a9e8b692"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"style": "assets/sunny.json",
|
|
||||||
"connect-src": [
|
|
||||||
"https://protomaps.github.io"
|
|
||||||
],
|
|
||||||
"best": true,
|
|
||||||
"id": "protomaps.sunny-self",
|
|
||||||
"name": "Protomaps Sunny (Offline)",
|
|
||||||
"type": "vector",
|
|
||||||
"category": "osmbasedmap",
|
|
||||||
"attribution": {
|
|
||||||
"text": "Protomaps",
|
|
||||||
"url": "https://protomaps.com/"
|
|
||||||
},
|
|
||||||
"url": "https://127.0.0.1/service-worker/offline-basemap/{z}-{x}-{y}.mvt"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "OpenStreetMap Carto",
|
"name": "OpenStreetMap Carto",
|
||||||
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
|
|
@ -134,7 +102,6 @@
|
||||||
"url": "https://protomaps.com/"
|
"url": "https://protomaps.com/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Americana",
|
"name": "Americana",
|
||||||
"url": "https://americanamap.org/style.json",
|
"url": "https://americanamap.org/style.json",
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,7 @@ 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[] = []
|
private metaCached: AreaDescription[] = []
|
||||||
|
private readonly installing: Set<string> = new Set()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
|
|
@ -194,6 +195,7 @@ export class OfflineBasemapManager {
|
||||||
this._host = host
|
this._host = host
|
||||||
this.blobs = new TypedIdb("OfflineBasemap")
|
this.blobs = new TypedIdb("OfflineBasemap")
|
||||||
this.meta = new TypedIdb<AreaDescription>("OfflineBasemapMeta")
|
this.meta = new TypedIdb<AreaDescription>("OfflineBasemapMeta")
|
||||||
|
this.updateCachedMeta()
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateCachedMeta(): Promise<AreaDescription[]> {
|
public async updateCachedMeta(): Promise<AreaDescription[]> {
|
||||||
|
|
@ -223,7 +225,6 @@ export class OfflineBasemapManager {
|
||||||
const x = tile.x >> zDiff
|
const x = tile.x >> zDiff
|
||||||
const y = tile.y >> zDiff
|
const y = tile.y >> zDiff
|
||||||
if (!this.isInstalled({ z, x, y })) {
|
if (!this.isInstalled({ z, x, y })) {
|
||||||
console.log("Installing level " + z + " archive")
|
|
||||||
yield <AreaDescription>{
|
yield <AreaDescription>{
|
||||||
name: `${z}-${x}-${y}.pmtiles`,
|
name: `${z}-${x}-${y}.pmtiles`,
|
||||||
minzoom: z,
|
minzoom: z,
|
||||||
|
|
@ -303,9 +304,19 @@ export class OfflineBasemapManager {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFallback(z: number, x: number, y: number): Promise<Response> {
|
private async attemptInstall(candidate: AreaDescription) {
|
||||||
const url = `https://api.protomaps.com/tiles/v4/${z}/${x}/${y}.mvt?key=2af8b969a9e8b692`
|
if (this.installing.has(candidate.name)) {
|
||||||
return fetch(url)
|
return
|
||||||
|
}
|
||||||
|
this.installing.add(candidate.name)
|
||||||
|
try {
|
||||||
|
await this.installArea(candidate)
|
||||||
|
await this.updateCachedMeta()
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not install basemap archive", candidate.name, "due to", e)
|
||||||
|
} finally {
|
||||||
|
this.installing.delete(candidate.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -316,7 +327,7 @@ export class OfflineBasemapManager {
|
||||||
* @param fallback: if set and no local tile could be found: ask protomaps instead
|
* @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, options?: {
|
||||||
autoInstall?: boolean, fallback?: boolean
|
autoInstall?: boolean, fallback?: string
|
||||||
}): Promise<Response> {
|
}): Promise<Response> {
|
||||||
if (this.metaCached.length === 0) {
|
if (this.metaCached.length === 0) {
|
||||||
await this.updateCachedMeta()
|
await this.updateCachedMeta()
|
||||||
|
|
@ -324,28 +335,30 @@ export class OfflineBasemapManager {
|
||||||
let area = this.determineArea(z, x, y)
|
let area = this.determineArea(z, x, y)
|
||||||
|
|
||||||
if (options?.autoInstall && !area) {
|
if (options?.autoInstall && !area) {
|
||||||
// We attempt to install the local files
|
// We attempt to install the local files ; but we don't wait
|
||||||
const candidates = this.getInstallCandidates({ z, x, y })
|
const candidates = this.getInstallCandidates({ z, x, y })
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
await this.installArea(candidate)
|
this.attemptInstall(candidate)
|
||||||
}
|
}
|
||||||
await this.updateCachedMeta()
|
|
||||||
area = this.determineArea(z, x, y)
|
area = this.determineArea(z, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!area) {
|
if (!area) {
|
||||||
if (options?.fallback) {
|
if (options?.fallback) {
|
||||||
return this.getFallback(z, x, y)
|
return fetch(options?.fallback)
|
||||||
}
|
}
|
||||||
|
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 new Response("Not found: no suitable area found", { status: 404 })
|
||||||
}
|
}
|
||||||
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 })
|
||||||
return new Response("Not found (not in tile archive, should not happen)", { status: 404 })
|
return new Response("Not found (not in tile archive, should not happen)", { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Served tile", { z, x, y }, "from installed archive")
|
||||||
return new Response(
|
return new Response(
|
||||||
tileData.data,
|
tileData.data,
|
||||||
{
|
{
|
||||||
|
|
@ -357,7 +370,6 @@ export class OfflineBasemapManager {
|
||||||
deleteArea(description: AreaDescription): Promise<AreaDescription[]> {
|
deleteArea(description: AreaDescription): Promise<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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,13 @@
|
||||||
// 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"
|
||||||
|
|
||||||
declare var self: ServiceWorkerGlobalScope
|
declare var self: ServiceWorkerGlobalScope
|
||||||
export {}
|
export {}
|
||||||
import { OfflineBasemapManager } from "./OfflineBasemapManager"
|
|
||||||
|
|
||||||
|
const selfDomain = self.location.hostname
|
||||||
|
|
||||||
const offlinemaps = new OfflineBasemapManager("https://cache.mapcomplete.org/")
|
const offlinemaps = new OfflineBasemapManager("https://cache.mapcomplete.org/")
|
||||||
|
|
||||||
|
|
@ -17,74 +20,164 @@ function jsonResponse(object: object | []): Response {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function routeOffline(event: FetchEvent) {
|
function respondFromCache(event: FetchEvent) {
|
||||||
const url = new URL(event.request.url)
|
event.respondWith(
|
||||||
const rest = url.pathname.split("/service-worker/offline-basemap/")[1]
|
caches.open(SWGenerated.vNumber).then(async cache => {
|
||||||
|
const cached = await cache.match(event.request)
|
||||||
|
if (!cached) {
|
||||||
|
const response = await fetch(event.request)
|
||||||
|
cache.put(event.request, response.clone())
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
return cached
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (rest === "meta.json") {
|
async function listCachedRequests(): Promise<string[]> {
|
||||||
event.respondWith(
|
const cache = await caches.open(SWGenerated.vNumber)
|
||||||
offlinemaps.updateCachedMeta().then(meta => jsonResponse(meta)))
|
const requests = await cache.keys()
|
||||||
return
|
return requests.map(req => req.url)
|
||||||
}
|
}
|
||||||
if (rest.indexOf("delete") >= 0) {
|
|
||||||
const filename = rest.split("/").pop()
|
|
||||||
if (!filename) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const description = OfflineBasemapManager.getAreaDescriptionForMapcomplete(filename)
|
|
||||||
event.respondWith(
|
|
||||||
offlinemaps.deleteArea(description)
|
|
||||||
.then(() =>
|
|
||||||
offlinemaps.updateCachedMeta())
|
|
||||||
.then(meta => jsonResponse({
|
|
||||||
"status": "installed",
|
|
||||||
installed: meta
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
if (rest.indexOf("update") >= 0) {
|
|
||||||
event.respondWith(
|
|
||||||
offlinemaps.updateCachedMeta()
|
|
||||||
.then(meta => jsonResponse({
|
|
||||||
"status": "installed",
|
|
||||||
installed: meta
|
|
||||||
})))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (rest.indexOf("install") >= 0) {
|
|
||||||
const filename = rest.split("/").pop()
|
|
||||||
if (!filename) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const description = OfflineBasemapManager.getAreaDescriptionForMapcomplete(filename)
|
|
||||||
event.respondWith(
|
|
||||||
offlinemaps.installArea(description)
|
|
||||||
.then(() =>
|
|
||||||
offlinemaps.updateCachedMeta())
|
|
||||||
.then(meta => jsonResponse({
|
|
||||||
"status": "installed",
|
|
||||||
installed: meta
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const tileRegex =/(\d+-\d+-\d+).mvt$/
|
|
||||||
const tileMatch = rest.match(tileRegex)
|
|
||||||
if (tileMatch) {
|
|
||||||
const fallback = url.searchParams.get("fallback") === "true"
|
|
||||||
const autoInstall = url.searchParams.get("auto") === "true"
|
|
||||||
|
|
||||||
const [z, x, y] = tileMatch[1].split("-").map(Number)
|
class Router {
|
||||||
event.respondWith(offlinemaps.getTileResponse(z, x, y, { fallback, autoInstall }))
|
private readonly _endpoints: Record<string, (event: FetchEvent) => void>
|
||||||
|
private readonly _subpaths: Record<string, (event: FetchEvent, rest: string) => void>
|
||||||
|
private readonly _fallback: undefined | ((event: FetchEvent, rest: string) => void)
|
||||||
|
|
||||||
|
constructor(endpoints: Record<string, (event: FetchEvent) => void>,
|
||||||
|
subpaths: Record<string, (event: FetchEvent, rest: string) => void>,
|
||||||
|
fallback?: undefined | ((event: FetchEvent, rest: string) => void)
|
||||||
|
) {
|
||||||
|
this._endpoints = endpoints
|
||||||
|
this._subpaths = subpaths
|
||||||
|
this._fallback = fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
public route(event: FetchEvent, rest?: string) {
|
||||||
|
const url = new URL(event.request.url)
|
||||||
|
rest ??= url.pathname.split("/service-worker/")[1]
|
||||||
|
console.log(">>> routing", rest)
|
||||||
|
if (rest.indexOf("/") > 0) {
|
||||||
|
const nextSegment = rest.split("/")[0]
|
||||||
|
if (this._subpaths[nextSegment]) {
|
||||||
|
return this._subpaths[nextSegment](event, rest.substring(nextSegment.length + 1))
|
||||||
|
}
|
||||||
|
} else if (this._endpoints[rest] !== undefined) {
|
||||||
|
return this._endpoints[rest](event)
|
||||||
|
}
|
||||||
|
if (this._fallback) {
|
||||||
|
return this._fallback(event, rest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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({
|
||||||
|
"status.json": (event) => {
|
||||||
|
event.respondWith(
|
||||||
|
listCachedRequests().then(cached =>
|
||||||
|
jsonResponse(
|
||||||
|
{
|
||||||
|
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.indexOf("/service-worker/offline-basemap/") >= 0) {
|
if (url.endsWith("/service-worker.js")) {
|
||||||
routeOffline(event)
|
return // Installation of a new version, we don't interfere
|
||||||
|
}
|
||||||
|
console.log("Intercepting event", event.request.url)
|
||||||
|
if (url.indexOf("/service-worker/") >= 0) {
|
||||||
|
allOffline.route(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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") {
|
||||||
|
respondFromCache(event)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
self.addEventListener("install", () => self.skipWaiting())
|
self.addEventListener("install", () => self.skipWaiting())
|
||||||
self.addEventListener("activate", event => event.waitUntil(self.clients.claim()))
|
self.addEventListener("activate", event => {
|
||||||
|
event.waitUntil(self.clients.claim())
|
||||||
|
|
||||||
|
// Delete the old caches (of an older version number
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((keys) =>
|
||||||
|
Promise.all(
|
||||||
|
keys.map(async (key) => {
|
||||||
|
if (key !== SWGenerated.vNumber) {
|
||||||
|
await caches.delete(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,9 @@
|
||||||
],
|
],
|
||||||
"types": ["pmtiles"]
|
"types": ["pmtiles"]
|
||||||
},
|
},
|
||||||
"include": ["index.ts", "OfflineBasemapManager.ts"]
|
"include": [
|
||||||
|
"index.ts",
|
||||||
|
"OfflineBasemapManager.ts",
|
||||||
|
"SWGenerated.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue