2024-02-03 14:33:10 +01:00
|
|
|
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
2023-10-16 14:27:05 +02:00
|
|
|
import type { Map as MLMap } from "maplibre-gl"
|
|
|
|
import { Map as MlMap, SourceSpecification } from "maplibre-gl"
|
2024-02-03 14:33:10 +01:00
|
|
|
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
2023-10-16 14:27:05 +02:00
|
|
|
import { Utils } from "../../Utils"
|
|
|
|
import { BBox } from "../../Logic/BBox"
|
2023-12-15 01:46:01 +01:00
|
|
|
import { ExportableMap, KeyNavigationEvent, MapProperties } from "../../Models/MapProperties"
|
2023-10-16 14:27:05 +02:00
|
|
|
import SvelteUIElement from "../Base/SvelteUIElement"
|
|
|
|
import MaplibreMap from "./MaplibreMap.svelte"
|
|
|
|
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
|
|
|
import * as htmltoimage from "html-to-image"
|
2024-01-28 00:04:24 +01:00
|
|
|
import RasterLayerHandler from "./RasterLayerHandler"
|
2024-02-03 14:33:10 +01:00
|
|
|
import Constants from "../../Models/Constants"
|
2023-03-23 00:58:21 +01:00
|
|
|
|
2023-03-24 19:21:15 +01:00
|
|
|
/**
|
|
|
|
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
|
|
|
*/
|
2023-04-19 03:20:49 +02:00
|
|
|
export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
2023-03-24 19:21:15 +01:00
|
|
|
private static maplibre_control_handlers = [
|
2023-03-25 02:48:24 +01:00
|
|
|
// "scrollZoom",
|
|
|
|
// "boxZoom",
|
|
|
|
// "doubleClickZoom",
|
2023-03-24 19:21:15 +01:00
|
|
|
"dragRotate",
|
|
|
|
"dragPan",
|
|
|
|
"keyboard",
|
|
|
|
"touchZoomRotate",
|
|
|
|
]
|
2023-03-28 05:13:48 +02:00
|
|
|
private static maplibre_zoom_handlers = [
|
|
|
|
"scrollZoom",
|
|
|
|
"boxZoom",
|
|
|
|
"doubleClickZoom",
|
|
|
|
"touchZoomRotate",
|
|
|
|
]
|
2023-03-23 00:58:21 +01:00
|
|
|
readonly location: UIEventSource<{ lon: number; lat: number }>
|
|
|
|
readonly zoom: UIEventSource<number>
|
2023-03-28 05:13:48 +02:00
|
|
|
readonly bounds: UIEventSource<BBox>
|
2023-03-23 00:58:21 +01:00
|
|
|
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
|
2023-03-24 19:21:15 +01:00
|
|
|
readonly maxbounds: UIEventSource<BBox | undefined>
|
|
|
|
readonly allowMoving: UIEventSource<true | boolean | undefined>
|
2023-07-18 01:26:04 +02:00
|
|
|
readonly allowRotating: UIEventSource<true | boolean | undefined>
|
2023-03-28 05:13:48 +02:00
|
|
|
readonly allowZooming: UIEventSource<true | boolean | undefined>
|
|
|
|
readonly lastClickLocation: Store<undefined | { lon: number; lat: number }>
|
2023-04-06 01:33:08 +02:00
|
|
|
readonly minzoom: UIEventSource<number>
|
2023-04-21 01:53:24 +02:00
|
|
|
readonly maxzoom: UIEventSource<number>
|
2023-12-19 22:21:34 +01:00
|
|
|
readonly rotation: UIEventSource<number>
|
2024-02-03 14:33:10 +01:00
|
|
|
readonly pitch: UIEventSource<number>
|
|
|
|
readonly useTerrain: Store<boolean>
|
2023-12-15 01:46:01 +01:00
|
|
|
|
2023-11-16 03:32:04 +01:00
|
|
|
/**
|
2023-12-15 01:46:01 +01:00
|
|
|
* Functions that are called when one of those actions has happened
|
|
|
|
* @private
|
2023-11-16 03:32:04 +01:00
|
|
|
*/
|
2023-12-15 01:46:01 +01:00
|
|
|
private _onKeyNavigation: ((event: KeyNavigationEvent) => void | boolean)[] = []
|
|
|
|
|
2023-03-11 02:37:07 +01:00
|
|
|
private readonly _maplibreMap: Store<MLMap>
|
2023-03-24 19:21:15 +01:00
|
|
|
|
2023-03-29 17:21:20 +02:00
|
|
|
constructor(maplibreMap: Store<MLMap>, state?: Partial<MapProperties>) {
|
2023-03-11 02:37:07 +01:00
|
|
|
this._maplibreMap = maplibreMap
|
|
|
|
|
2023-06-07 22:46:41 +02:00
|
|
|
this.location = state?.location ?? new UIEventSource(undefined)
|
2023-06-04 00:43:32 +02:00
|
|
|
if (this.location.data) {
|
2023-05-19 10:56:30 +02:00
|
|
|
// The MapLibre adaptor updates the element in the location and then pings them
|
|
|
|
// Often, code setting this up doesn't expect the object they pass in to be changed, so we create a copy
|
2023-06-14 20:39:36 +02:00
|
|
|
this.location.setData({ ...this.location.data })
|
2023-05-19 10:56:30 +02:00
|
|
|
}
|
2023-03-23 00:58:21 +01:00
|
|
|
this.zoom = state?.zoom ?? new UIEventSource(1)
|
2023-04-06 01:33:08 +02:00
|
|
|
this.minzoom = state?.minzoom ?? new UIEventSource(0)
|
2023-04-21 01:53:24 +02:00
|
|
|
this.maxzoom = state?.maxzoom ?? new UIEventSource(24)
|
2023-03-24 19:21:15 +01:00
|
|
|
this.zoom.addCallbackAndRunD((z) => {
|
2023-04-06 01:33:08 +02:00
|
|
|
if (z < this.minzoom.data) {
|
|
|
|
this.zoom.setData(this.minzoom.data)
|
2023-03-24 19:21:15 +01:00
|
|
|
}
|
2023-04-21 01:53:24 +02:00
|
|
|
const max = Math.min(24, this.maxzoom.data ?? 24)
|
|
|
|
if (z > max) {
|
|
|
|
this.zoom.setData(max)
|
2023-03-24 19:21:15 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined)
|
|
|
|
this.allowMoving = state?.allowMoving ?? new UIEventSource(true)
|
2023-07-18 01:26:04 +02:00
|
|
|
this.allowRotating = state?.allowRotating ?? new UIEventSource<boolean>(true)
|
2023-03-28 05:13:48 +02:00
|
|
|
this.allowZooming = state?.allowZooming ?? new UIEventSource(true)
|
2023-04-06 01:33:08 +02:00
|
|
|
this.bounds = state?.bounds ?? new UIEventSource(undefined)
|
2023-12-19 22:21:34 +01:00
|
|
|
this.rotation = state?.rotation ?? new UIEventSource<number>(0)
|
2024-02-03 14:33:10 +01:00
|
|
|
this.pitch = state?.pitch ?? new UIEventSource<number>(0)
|
|
|
|
this.useTerrain = state?.useTerrain ?? new ImmutableStore<boolean>(false)
|
2023-03-23 00:58:21 +01:00
|
|
|
this.rasterLayer =
|
|
|
|
state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined)
|
2023-03-11 02:37:07 +01:00
|
|
|
|
2023-03-28 05:13:48 +02:00
|
|
|
const lastClickLocation = new UIEventSource<{ lon: number; lat: number }>(undefined)
|
|
|
|
this.lastClickLocation = lastClickLocation
|
2023-03-23 00:58:21 +01:00
|
|
|
const self = this
|
2023-04-16 03:42:26 +02:00
|
|
|
|
2024-01-28 00:04:24 +01:00
|
|
|
const rasterLayerHandler = new RasterLayerHandler(this._maplibreMap, this.rasterLayer)
|
|
|
|
|
2023-04-16 03:42:26 +02:00
|
|
|
function handleClick(e) {
|
|
|
|
if (e.originalEvent["consumed"]) {
|
|
|
|
// Workaround, 'ShowPointLayer' sets this flag
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const lon = e.lngLat.lng
|
|
|
|
const lat = e.lngLat.lat
|
2023-06-14 20:39:36 +02:00
|
|
|
lastClickLocation.setData({ lon, lat })
|
2023-04-16 03:42:26 +02:00
|
|
|
}
|
|
|
|
|
2023-03-11 02:37:07 +01:00
|
|
|
maplibreMap.addCallbackAndRunD((map) => {
|
|
|
|
map.on("load", () => {
|
2023-03-24 19:21:15 +01:00
|
|
|
self.MoveMapToCurrentLoc(self.location.data)
|
|
|
|
self.SetZoom(self.zoom.data)
|
|
|
|
self.setMaxBounds(self.maxbounds.data)
|
|
|
|
self.setAllowMoving(self.allowMoving.data)
|
2023-07-18 01:26:04 +02:00
|
|
|
self.setAllowRotating(self.allowRotating.data)
|
2023-03-28 05:13:48 +02:00
|
|
|
self.setAllowZooming(self.allowZooming.data)
|
2023-04-06 01:33:08 +02:00
|
|
|
self.setMinzoom(self.minzoom.data)
|
2023-04-21 01:53:24 +02:00
|
|
|
self.setMaxzoom(self.maxzoom.data)
|
2023-04-20 01:52:23 +02:00
|
|
|
self.setBounds(self.bounds.data)
|
2024-02-03 14:33:10 +01:00
|
|
|
self.setTerrain(self.useTerrain.data)
|
2024-01-28 00:04:24 +01:00
|
|
|
rasterLayerHandler.setBackground()
|
2023-05-18 23:42:03 +02:00
|
|
|
this.updateStores(true)
|
2023-03-11 02:37:07 +01:00
|
|
|
})
|
2023-03-24 19:21:15 +01:00
|
|
|
self.MoveMapToCurrentLoc(self.location.data)
|
|
|
|
self.SetZoom(self.zoom.data)
|
|
|
|
self.setMaxBounds(self.maxbounds.data)
|
|
|
|
self.setAllowMoving(self.allowMoving.data)
|
2023-07-18 01:26:04 +02:00
|
|
|
self.setAllowRotating(self.allowRotating.data)
|
2023-03-28 05:13:48 +02:00
|
|
|
self.setAllowZooming(self.allowZooming.data)
|
2023-04-06 01:33:08 +02:00
|
|
|
self.setMinzoom(self.minzoom.data)
|
2023-04-21 01:53:24 +02:00
|
|
|
self.setMaxzoom(self.maxzoom.data)
|
2023-04-20 01:52:23 +02:00
|
|
|
self.setBounds(self.bounds.data)
|
2023-12-19 22:21:34 +01:00
|
|
|
self.SetRotation(self.rotation.data)
|
2024-02-03 14:33:10 +01:00
|
|
|
self.setTerrain(self.useTerrain.data)
|
2024-01-28 00:04:24 +01:00
|
|
|
rasterLayerHandler.setBackground()
|
2023-05-18 23:42:03 +02:00
|
|
|
this.updateStores(true)
|
2023-04-06 01:33:08 +02:00
|
|
|
map.on("moveend", () => this.updateStores())
|
2023-03-28 05:13:48 +02:00
|
|
|
map.on("click", (e) => {
|
2023-04-16 03:42:26 +02:00
|
|
|
handleClick(e)
|
|
|
|
})
|
|
|
|
map.on("contextmenu", (e) => {
|
|
|
|
handleClick(e)
|
|
|
|
})
|
|
|
|
map.on("dblclick", (e) => {
|
|
|
|
handleClick(e)
|
2023-03-28 05:13:48 +02:00
|
|
|
})
|
2024-01-24 23:45:20 +01:00
|
|
|
map.on("rotateend", (_) => {
|
2023-12-19 22:21:34 +01:00
|
|
|
this.updateStores()
|
|
|
|
})
|
2024-02-03 14:33:10 +01:00
|
|
|
map.on("pitchend", () => {
|
|
|
|
this.updateStores()
|
|
|
|
})
|
2023-11-16 03:32:04 +01:00
|
|
|
map.getContainer().addEventListener("keydown", (event) => {
|
2023-12-15 01:46:01 +01:00
|
|
|
let locked: "islocked" = undefined
|
|
|
|
if (!this.allowMoving.data) {
|
|
|
|
locked = "islocked"
|
|
|
|
}
|
|
|
|
switch (event.key) {
|
|
|
|
case "ArrowUp":
|
|
|
|
this.pingKeycodeEvent(locked ?? "north")
|
|
|
|
break
|
|
|
|
case "ArrowRight":
|
|
|
|
this.pingKeycodeEvent(locked ?? "east")
|
|
|
|
break
|
|
|
|
case "ArrowDown":
|
|
|
|
this.pingKeycodeEvent(locked ?? "south")
|
|
|
|
break
|
|
|
|
case "ArrowLeft":
|
|
|
|
this.pingKeycodeEvent(locked ?? "west")
|
|
|
|
break
|
|
|
|
case "+":
|
|
|
|
this.pingKeycodeEvent("in")
|
|
|
|
break
|
|
|
|
case "=":
|
|
|
|
this.pingKeycodeEvent("in")
|
|
|
|
break
|
|
|
|
case "-":
|
|
|
|
this.pingKeycodeEvent("out")
|
|
|
|
break
|
2023-11-16 03:32:04 +01:00
|
|
|
}
|
|
|
|
})
|
2023-03-11 02:37:07 +01:00
|
|
|
})
|
|
|
|
|
2024-01-28 00:04:24 +01:00
|
|
|
this.rasterLayer.addCallbackAndRun((_) => rasterLayerHandler.setBackground())
|
2023-03-23 00:58:21 +01:00
|
|
|
this.location.addCallbackAndRunD((loc) => {
|
2023-03-11 02:37:07 +01:00
|
|
|
self.MoveMapToCurrentLoc(loc)
|
|
|
|
})
|
2023-03-23 00:58:21 +01:00
|
|
|
this.zoom.addCallbackAndRunD((z) => self.SetZoom(z))
|
2023-03-24 19:21:15 +01:00
|
|
|
this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox))
|
2023-12-19 22:21:34 +01:00
|
|
|
this.rotation.addCallbackAndRunD((bearing) => self.SetRotation(bearing))
|
2023-12-15 01:46:01 +01:00
|
|
|
this.allowMoving.addCallbackAndRun((allowMoving) => {
|
|
|
|
self.setAllowMoving(allowMoving)
|
|
|
|
self.pingKeycodeEvent(allowMoving ? "unlocked" : "locked")
|
|
|
|
})
|
2023-07-18 01:26:04 +02:00
|
|
|
this.allowRotating.addCallbackAndRunD((allowRotating) =>
|
|
|
|
self.setAllowRotating(allowRotating)
|
|
|
|
)
|
2023-03-28 05:13:48 +02:00
|
|
|
this.allowZooming.addCallbackAndRun((allowZooming) => self.setAllowZooming(allowZooming))
|
|
|
|
this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds))
|
2024-02-03 14:33:10 +01:00
|
|
|
this.useTerrain?.addCallbackAndRun(useTerrain => self.setTerrain(useTerrain))
|
2023-03-11 02:37:07 +01:00
|
|
|
}
|
2023-03-23 00:58:21 +01:00
|
|
|
|
2023-03-24 19:21:15 +01:00
|
|
|
/**
|
|
|
|
* Convenience constructor
|
|
|
|
*/
|
|
|
|
public static construct(): {
|
|
|
|
map: Store<MLMap>
|
|
|
|
ui: SvelteUIElement
|
|
|
|
mapproperties: MapProperties
|
|
|
|
} {
|
|
|
|
const mlmap = new UIEventSource<MlMap>(undefined)
|
|
|
|
return {
|
|
|
|
map: mlmap,
|
|
|
|
ui: new SvelteUIElement(MaplibreMap, {
|
|
|
|
map: mlmap,
|
|
|
|
}),
|
|
|
|
mapproperties: new MapLibreAdaptor(mlmap),
|
2023-03-11 02:37:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-21 01:53:24 +02:00
|
|
|
public static prepareWmsSource(layer: RasterLayerProperties): SourceSpecification {
|
2024-01-28 00:04:24 +01:00
|
|
|
return RasterLayerHandler.prepareWmsSource(layer)
|
2023-04-21 01:53:24 +02:00
|
|
|
}
|
|
|
|
|
2023-03-11 02:37:07 +01:00
|
|
|
/**
|
|
|
|
* Prepares an ELI-URL to be compatible with mapbox
|
|
|
|
*/
|
|
|
|
|
2023-11-14 16:14:27 +01:00
|
|
|
private static async toBlob(canvas: HTMLCanvasElement): Promise<Blob> {
|
|
|
|
return await new Promise<Blob>((resolve) => canvas.toBlob((blob) => resolve(blob)))
|
|
|
|
}
|
|
|
|
|
|
|
|
private static async createImage(url: string): Promise<HTMLImageElement> {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const img = new Image()
|
|
|
|
img.decode = () => resolve(img) as any
|
|
|
|
img.onload = () => resolve(img)
|
|
|
|
img.onerror = reject
|
|
|
|
img.crossOrigin = "anonymous"
|
|
|
|
img.decoding = "async"
|
|
|
|
img.src = url
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-12-15 01:46:01 +01:00
|
|
|
public onKeyNavigationEvent(f: (event: KeyNavigationEvent) => void | boolean) {
|
|
|
|
this._onKeyNavigation.push(f)
|
|
|
|
return () => {
|
|
|
|
this._onKeyNavigation.splice(this._onKeyNavigation.indexOf(f), 1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-14 16:14:27 +01:00
|
|
|
public async exportAsPng(
|
|
|
|
rescaleIcons: number = 1,
|
|
|
|
progress: UIEventSource<{ current: number; total: number }> = undefined
|
|
|
|
): Promise<Blob> {
|
2023-04-19 03:20:49 +02:00
|
|
|
const map = this._maplibreMap.data
|
2023-05-05 02:03:41 +02:00
|
|
|
if (!map) {
|
2023-04-19 03:20:49 +02:00
|
|
|
return undefined
|
|
|
|
}
|
2023-11-14 16:14:27 +01:00
|
|
|
const drawOn = document.createElement("canvas", {})
|
2023-06-04 00:43:32 +02:00
|
|
|
const ctx = drawOn.getContext("2d")
|
2023-11-14 16:14:27 +01:00
|
|
|
// The width/height has been set in 'mm' on the parent element and converted to pixels by the browser
|
|
|
|
const w = map.getContainer().getBoundingClientRect().width
|
|
|
|
const h = map.getContainer().getBoundingClientRect().height
|
2023-04-19 03:20:49 +02:00
|
|
|
|
2024-02-03 14:33:10 +01:00
|
|
|
const dpi = map.getPixelRatio()
|
2023-11-14 16:14:27 +01:00
|
|
|
// The 'css'-size stays constant...
|
|
|
|
drawOn.style.width = w + "px"
|
|
|
|
drawOn.style.height = h + "px"
|
2023-04-19 03:20:49 +02:00
|
|
|
|
2023-11-14 16:14:27 +01:00
|
|
|
// ...but the number of pixels is increased
|
|
|
|
drawOn.width = Math.ceil(w * dpi)
|
|
|
|
drawOn.height = Math.ceil(h * dpi)
|
2023-11-13 15:14:41 +01:00
|
|
|
|
2023-11-14 16:14:27 +01:00
|
|
|
await this.exportBackgroundOnCanvas(ctx)
|
|
|
|
await this.drawMarkers(ctx, rescaleIcons, progress)
|
|
|
|
return await MapLibreAdaptor.toBlob(drawOn)
|
2023-06-04 00:43:32 +02:00
|
|
|
}
|
2023-04-19 03:20:49 +02:00
|
|
|
|
2023-12-15 01:46:01 +01:00
|
|
|
private pingKeycodeEvent(
|
|
|
|
key: "north" | "east" | "south" | "west" | "in" | "out" | "islocked" | "locked" | "unlocked"
|
|
|
|
) {
|
|
|
|
const event = {
|
|
|
|
date: new Date(),
|
|
|
|
key: key,
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < this._onKeyNavigation.length; i++) {
|
|
|
|
const f = this._onKeyNavigation[i]
|
|
|
|
const unregister = f(event)
|
|
|
|
if (unregister === true) {
|
|
|
|
this._onKeyNavigation.splice(i, 1)
|
|
|
|
i--
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-04 00:43:32 +02:00
|
|
|
/**
|
|
|
|
* Exports the background map and lines to PNG.
|
|
|
|
* Markers are _not_ rendered
|
|
|
|
*/
|
2023-06-04 22:52:13 +02:00
|
|
|
private async exportBackgroundOnCanvas(ctx: CanvasRenderingContext2D): Promise<void> {
|
2023-06-04 00:43:32 +02:00
|
|
|
const map = this._maplibreMap.data
|
|
|
|
// We draw the maplibre-map onto the canvas. This does not export markers
|
|
|
|
// Inspiration by https://github.com/mapbox/mapbox-gl-js/issues/2766
|
2023-04-19 03:20:49 +02:00
|
|
|
|
2023-06-04 00:43:32 +02:00
|
|
|
// Total hack - see https://stackoverflow.com/questions/42483449/mapbox-gl-js-export-map-to-png-or-pdf
|
|
|
|
const promise = new Promise<void>((resolve) => {
|
|
|
|
map.once("render", () => {
|
|
|
|
ctx.drawImage(map.getCanvas(), 0, 0)
|
|
|
|
resolve()
|
2023-04-19 03:20:49 +02:00
|
|
|
})
|
2023-06-04 00:43:32 +02:00
|
|
|
})
|
2023-04-19 03:20:49 +02:00
|
|
|
|
2023-06-04 00:43:32 +02:00
|
|
|
while (!map.isStyleLoaded()) {
|
|
|
|
console.log("Waiting to fully load the style...")
|
|
|
|
await Utils.waitFor(100)
|
2023-04-19 03:20:49 +02:00
|
|
|
}
|
2023-06-04 00:43:32 +02:00
|
|
|
map.triggerRepaint()
|
|
|
|
await promise
|
|
|
|
}
|
2023-04-19 03:20:49 +02:00
|
|
|
|
2023-12-06 04:01:42 +01:00
|
|
|
private async drawElement(
|
|
|
|
drawOn: CanvasRenderingContext2D,
|
|
|
|
element: HTMLElement,
|
|
|
|
rescaleIcons: number,
|
|
|
|
pixelRatio: number
|
|
|
|
) {
|
2023-12-06 17:27:30 +01:00
|
|
|
const style = element.style.transform
|
|
|
|
let x = element.getBoundingClientRect().x
|
|
|
|
let y = element.getBoundingClientRect().y
|
|
|
|
element.style.transform = ""
|
2023-12-06 04:01:42 +01:00
|
|
|
const offset = style.match(/translate\(([-0-9]+)%, ?([-0-9]+)%\)/)
|
|
|
|
|
2023-12-06 17:27:30 +01:00
|
|
|
const w = element.style.width
|
|
|
|
const h = element.style.height
|
|
|
|
|
2023-12-06 04:01:42 +01:00
|
|
|
// Force a wider view for icon badges
|
2023-12-06 17:27:30 +01:00
|
|
|
element.style.width = element.getBoundingClientRect().width * 4 + "px"
|
|
|
|
element.style.height = element.getBoundingClientRect().height + "px"
|
|
|
|
const svgSource = await htmltoimage.toSvg(element)
|
2023-12-06 04:01:42 +01:00
|
|
|
const img = await MapLibreAdaptor.createImage(svgSource)
|
2023-12-06 17:27:30 +01:00
|
|
|
element.style.width = w
|
|
|
|
element.style.height = h
|
|
|
|
|
2023-12-06 04:01:42 +01:00
|
|
|
if (offset && rescaleIcons !== 1) {
|
|
|
|
const [_, __, relYStr] = offset
|
|
|
|
const relY = Number(relYStr)
|
|
|
|
y += img.height * (relY / 100)
|
|
|
|
}
|
|
|
|
|
|
|
|
x *= pixelRatio
|
|
|
|
y *= pixelRatio
|
|
|
|
|
|
|
|
try {
|
|
|
|
drawOn.drawImage(img, x, y, img.width * rescaleIcons, img.height * rescaleIcons)
|
|
|
|
} catch (e) {
|
|
|
|
console.log("Could not draw image because of", e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-14 16:14:27 +01:00
|
|
|
/**
|
|
|
|
* Draws the markers of the current map on the specified canvas.
|
|
|
|
* The DPIfactor is used to calculate the correct position, whereas 'rescaleIcons' can be used to make the icons smaller
|
|
|
|
*/
|
|
|
|
private async drawMarkers(
|
|
|
|
drawOn: CanvasRenderingContext2D,
|
|
|
|
rescaleIcons: number = 1,
|
|
|
|
progress: UIEventSource<{ current: number; total: number }>
|
|
|
|
): Promise<void> {
|
2023-06-04 00:43:32 +02:00
|
|
|
const map = this._maplibreMap.data
|
|
|
|
if (!map) {
|
2023-11-14 16:14:27 +01:00
|
|
|
console.error("There is no map to export from")
|
2023-06-04 00:43:32 +02:00
|
|
|
return undefined
|
2023-04-19 03:20:49 +02:00
|
|
|
}
|
2023-11-13 15:14:41 +01:00
|
|
|
|
|
|
|
const container = map.getContainer()
|
2023-12-06 04:01:42 +01:00
|
|
|
const pixelRatio = map.getPixelRatio()
|
2023-11-14 16:14:27 +01:00
|
|
|
|
2023-11-13 15:14:41 +01:00
|
|
|
function isDisplayed(el: Element) {
|
|
|
|
const r1 = el.getBoundingClientRect()
|
|
|
|
const r2 = container.getBoundingClientRect()
|
|
|
|
return !(
|
|
|
|
r2.left > r1.right ||
|
|
|
|
r2.right < r1.left ||
|
|
|
|
r2.top > r1.bottom ||
|
|
|
|
r2.bottom < r1.top
|
|
|
|
)
|
|
|
|
}
|
2023-11-14 16:14:27 +01:00
|
|
|
|
2023-11-13 15:14:41 +01:00
|
|
|
const markers = Array.from(container.getElementsByClassName("marker"))
|
|
|
|
for (let i = 0; i < markers.length; i++) {
|
2023-11-14 16:14:27 +01:00
|
|
|
const marker = <HTMLElement>markers[i]
|
2023-12-06 04:01:42 +01:00
|
|
|
const labels = Array.from(marker.getElementsByClassName("marker-label"))
|
2023-11-14 16:14:27 +01:00
|
|
|
const style = marker.style.transform
|
2023-12-06 04:01:42 +01:00
|
|
|
|
|
|
|
if (isDisplayed(marker)) {
|
|
|
|
await this.drawElement(drawOn, marker, rescaleIcons, pixelRatio)
|
2023-11-14 16:14:27 +01:00
|
|
|
}
|
|
|
|
|
2023-12-06 04:01:42 +01:00
|
|
|
for (const label of labels) {
|
|
|
|
if (isDisplayed(label)) {
|
|
|
|
await this.drawElement(drawOn, <HTMLElement>label, rescaleIcons, pixelRatio)
|
|
|
|
}
|
|
|
|
}
|
2023-11-14 16:14:27 +01:00
|
|
|
|
|
|
|
if (progress) {
|
|
|
|
progress.setData({ current: i, total: markers.length })
|
|
|
|
}
|
2023-12-06 04:01:42 +01:00
|
|
|
|
2023-11-14 16:14:27 +01:00
|
|
|
marker.style.transform = style
|
2023-11-13 15:14:41 +01:00
|
|
|
}
|
2023-04-19 03:20:49 +02:00
|
|
|
}
|
|
|
|
|
2023-05-18 23:42:03 +02:00
|
|
|
private updateStores(isSetup: boolean = false): void {
|
2023-04-19 03:20:49 +02:00
|
|
|
const map = this._maplibreMap.data
|
2023-04-20 17:42:07 +02:00
|
|
|
if (!map) {
|
2023-04-19 03:20:49 +02:00
|
|
|
return
|
|
|
|
}
|
2023-06-14 20:39:36 +02:00
|
|
|
const { lng, lat } = map.getCenter()
|
2023-06-07 22:46:41 +02:00
|
|
|
if (lng === 0 && lat === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (this.location.data === undefined) {
|
2023-06-14 20:39:36 +02:00
|
|
|
this.location.setData({ lon: lng, lat })
|
2023-06-07 22:46:41 +02:00
|
|
|
} else if (!isSetup) {
|
2023-06-14 23:21:19 +02:00
|
|
|
const lon = map.getCenter().lng
|
|
|
|
const lat = map.getCenter().lat
|
|
|
|
this.location.setData({ lon, lat })
|
2023-05-18 23:42:03 +02:00
|
|
|
}
|
2023-04-19 03:20:49 +02:00
|
|
|
this.zoom.setData(Math.round(map.getZoom() * 10) / 10)
|
|
|
|
const bounds = map.getBounds()
|
|
|
|
const bbox = new BBox([
|
|
|
|
[bounds.getEast(), bounds.getNorth()],
|
|
|
|
[bounds.getWest(), bounds.getSouth()],
|
|
|
|
])
|
2023-05-18 23:42:03 +02:00
|
|
|
if (this.bounds.data === undefined || !isSetup) {
|
|
|
|
this.bounds.setData(bbox)
|
|
|
|
}
|
2023-12-19 22:21:34 +01:00
|
|
|
this.rotation.setData(map.getBearing())
|
2024-02-03 14:33:10 +01:00
|
|
|
this.pitch.setData(map.getPitch())
|
2023-04-19 03:20:49 +02:00
|
|
|
}
|
|
|
|
|
2023-05-18 15:44:54 +02:00
|
|
|
private SetZoom(z: number): void {
|
2023-03-24 19:21:15 +01:00
|
|
|
const map = this._maplibreMap.data
|
|
|
|
if (!map || z === undefined) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (Math.abs(map.getZoom() - z) > 0.01) {
|
|
|
|
map.setZoom(z)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-19 22:21:34 +01:00
|
|
|
private SetRotation(bearing: number): void {
|
|
|
|
const map = this._maplibreMap.data
|
|
|
|
if (!map || bearing === undefined) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
map.rotateTo(bearing, { duration: 0 })
|
|
|
|
}
|
|
|
|
|
2023-05-18 15:44:54 +02:00
|
|
|
private MoveMapToCurrentLoc(loc: { lat: number; lon: number }): void {
|
2023-03-24 19:21:15 +01:00
|
|
|
const map = this._maplibreMap.data
|
|
|
|
if (!map || loc === undefined) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const center = map.getCenter()
|
|
|
|
if (center.lng !== loc.lon || center.lat !== loc.lat) {
|
2023-07-08 01:21:11 +02:00
|
|
|
if (isNaN(loc.lon) || isNaN(loc.lat)) {
|
|
|
|
console.error("Got invalid lat or lon, not setting")
|
|
|
|
} else {
|
|
|
|
map.setCenter({ lng: loc.lon, lat: loc.lat })
|
|
|
|
}
|
2023-03-24 19:21:15 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-01 03:29:57 +01:00
|
|
|
public installCustomKeyboardHandler(viewportStore: UIEventSource<HTMLDivElement>) {
|
|
|
|
viewportStore.mapD(
|
2023-12-22 18:50:22 +01:00
|
|
|
(viewport) => {
|
|
|
|
const map = this._maplibreMap.data
|
|
|
|
if (!map) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const oldKeyboard = map.keyboard
|
2024-01-01 03:29:57 +01:00
|
|
|
const w = viewport.getBoundingClientRect().width
|
|
|
|
if (w < 10) {
|
|
|
|
/// this is weird, but definitively wrong!
|
|
|
|
console.log("Got a very small bound", w, viewport)
|
|
|
|
// We try again later on
|
|
|
|
window.requestAnimationFrame(() => {
|
|
|
|
viewportStore.ping()
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
oldKeyboard._panStep = w
|
2023-12-22 18:50:22 +01:00
|
|
|
},
|
|
|
|
[this._maplibreMap]
|
|
|
|
)
|
|
|
|
}
|
2023-03-24 19:21:15 +01:00
|
|
|
|
|
|
|
private setMaxBounds(bbox: undefined | BBox) {
|
|
|
|
const map = this._maplibreMap.data
|
2023-05-05 02:03:41 +02:00
|
|
|
if (!map) {
|
2023-03-24 19:21:15 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if (bbox) {
|
2023-04-06 01:33:08 +02:00
|
|
|
map?.setMaxBounds(bbox.toLngLat())
|
2023-03-24 19:21:15 +01:00
|
|
|
} else {
|
2023-04-06 01:33:08 +02:00
|
|
|
map?.setMaxBounds(null)
|
2023-03-24 19:21:15 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-18 01:26:04 +02:00
|
|
|
private setAllowRotating(allow: true | boolean | undefined) {
|
|
|
|
const map = this._maplibreMap.data
|
|
|
|
if (!map) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (allow === false) {
|
|
|
|
map.rotateTo(0, { duration: 0 })
|
|
|
|
map.setPitch(0)
|
|
|
|
map.dragRotate.disable()
|
2023-12-15 01:46:01 +01:00
|
|
|
map.keyboard.disableRotation()
|
2023-09-28 23:50:27 +02:00
|
|
|
map.touchZoomRotate.disableRotation()
|
2023-07-18 01:26:04 +02:00
|
|
|
} else {
|
|
|
|
map.dragRotate.enable()
|
2023-12-15 01:46:01 +01:00
|
|
|
map.keyboard.enableRotation()
|
2023-09-28 23:50:27 +02:00
|
|
|
map.touchZoomRotate.enableRotation()
|
2023-07-18 01:26:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-24 19:21:15 +01:00
|
|
|
private setAllowMoving(allow: true | boolean | undefined) {
|
|
|
|
const map = this._maplibreMap.data
|
2023-05-05 02:03:41 +02:00
|
|
|
if (!map) {
|
2023-03-24 19:21:15 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if (allow === false) {
|
|
|
|
for (const id of MapLibreAdaptor.maplibre_control_handlers) {
|
|
|
|
map[id].disable()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for (const id of MapLibreAdaptor.maplibre_control_handlers) {
|
|
|
|
map[id].enable()
|
|
|
|
}
|
|
|
|
}
|
2023-07-18 01:26:04 +02:00
|
|
|
this.setAllowRotating(this.allowRotating.data)
|
2023-03-24 19:21:15 +01:00
|
|
|
}
|
2023-03-28 05:13:48 +02:00
|
|
|
|
2023-04-06 01:33:08 +02:00
|
|
|
private setMinzoom(minzoom: number) {
|
|
|
|
const map = this._maplibreMap.data
|
2023-05-05 02:03:41 +02:00
|
|
|
if (!map) {
|
2023-04-06 01:33:08 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
map.setMinZoom(minzoom)
|
|
|
|
}
|
|
|
|
|
2023-04-21 01:53:24 +02:00
|
|
|
private setMaxzoom(maxzoom: number) {
|
|
|
|
const map = this._maplibreMap.data
|
2023-05-05 02:03:41 +02:00
|
|
|
if (!map) {
|
2023-04-21 01:53:24 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
map.setMaxZoom(maxzoom)
|
|
|
|
}
|
|
|
|
|
2023-03-28 05:13:48 +02:00
|
|
|
private setAllowZooming(allow: true | boolean | undefined) {
|
|
|
|
const map = this._maplibreMap.data
|
2023-05-05 02:03:41 +02:00
|
|
|
if (!map) {
|
2023-03-28 05:13:48 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if (allow === false) {
|
|
|
|
for (const id of MapLibreAdaptor.maplibre_zoom_handlers) {
|
|
|
|
map[id].disable()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for (const id of MapLibreAdaptor.maplibre_zoom_handlers) {
|
|
|
|
map[id].enable()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private setBounds(bounds: BBox) {
|
|
|
|
const map = this._maplibreMap.data
|
2023-05-05 02:03:41 +02:00
|
|
|
if (!map || bounds === undefined) {
|
2023-03-28 05:13:48 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
const oldBounds = map.getBounds()
|
|
|
|
const e = 0.0000001
|
|
|
|
const hasDiff =
|
|
|
|
Math.abs(oldBounds.getWest() - bounds.getWest()) > e &&
|
|
|
|
Math.abs(oldBounds.getEast() - bounds.getEast()) > e &&
|
|
|
|
Math.abs(oldBounds.getNorth() - bounds.getNorth()) > e &&
|
|
|
|
Math.abs(oldBounds.getSouth() - bounds.getSouth()) > e
|
|
|
|
if (!hasDiff) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
map.fitBounds(bounds.toLngLat())
|
|
|
|
}
|
2024-02-03 14:33:10 +01:00
|
|
|
|
|
|
|
private async setTerrain(useTerrain: boolean) {
|
|
|
|
const map = this._maplibreMap.data
|
|
|
|
if (!map) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const id = "maptiler-terrain-data"
|
|
|
|
if (useTerrain) {
|
|
|
|
if(map.getTerrain()){
|
|
|
|
return
|
|
|
|
}
|
|
|
|
map.addSource(id, {
|
|
|
|
"type": "raster-dem",
|
|
|
|
"url": "https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" + Constants.maptilerApiKey
|
|
|
|
})
|
|
|
|
try{
|
|
|
|
while (!map?.isStyleLoaded()) {
|
|
|
|
await Utils.waitFor(250)
|
|
|
|
}
|
|
|
|
map.setTerrain({
|
|
|
|
source: id
|
|
|
|
})
|
|
|
|
}catch (e) {
|
|
|
|
console.error(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2023-03-11 02:37:07 +01:00
|
|
|
}
|