import { Map as MLMap, SourceSpecification } from "maplibre-gl" import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource" import { RasterLayerPolygon } from "../../Models/RasterLayers" import { RasterLayerProperties } from "../../Models/RasterLayerProperties" import { Utils } from "../../Utils" class SingleBackgroundHandler { // Value between 0 and 1.0 public opacity = new UIEventSource(0.0) private _map: Store private _background: UIEventSource private readonly _targetLayer: RasterLayerPolygon private _deactivationTime: Date = undefined /** * Deactivate a layer after 60 seconds */ public static readonly DEACTIVATE_AFTER = 60 private fadeStep = 0.1 constructor( map: Store, targetLayer: RasterLayerPolygon, background: UIEventSource ) { this._targetLayer = targetLayer this._map = map this._background = background background.addCallback(async () => { await this.update() }) map.addCallbackAndRunD(async (map) => { map.on("load", async () => { await this.update() }) await this.update() map.on("moveend", () => this.onMove(map)) map.on("zoomend", () => this.onMove(map)) }) } private onMove(map: MLMap) { if (!this._deactivationTime) { return } // in seconds const timeSinceDeactivation = (new Date().getTime() - this._deactivationTime.getTime()) / 1000 if (timeSinceDeactivation < SingleBackgroundHandler.DEACTIVATE_AFTER) { return } console.debug( "Removing raster layer", this._targetLayer.properties.id, "map moved and not been used for", SingleBackgroundHandler.DEACTIVATE_AFTER ) if (map.getLayer(this._targetLayer.properties.id)) { map.removeLayer(this._targetLayer.properties.id) } } private async update() { const newTarget: RasterLayerPolygon | undefined = this._background.data const targetLayer = this._targetLayer if (newTarget?.properties?.id !== targetLayer.properties.id) { this._deactivationTime = new Date() await this.awaitStyleIsLoaded() this.fadeOut() } else { this._deactivationTime = undefined await this.enable() this.fadeIn() } } private async awaitStyleIsLoaded(): Promise { const map = this._map.data if (!map) { return } while (!map?.isStyleLoaded()) { await Utils.waitFor(250) } } private async enable(){ let ttl = 15 await this.awaitStyleIsLoaded() while(!this.tryEnable() && ttl > 0){ ttl --; await Utils.waitFor(250) } } /** * Returns 'false' if should be attempted again * @private */ private tryEnable(): boolean { const map: MLMap = this._map.data if (!map) { return true } const background = this._targetLayer.properties console.debug("Enabling", background.id) let addLayerBeforeId = "aeroway_fill" // this is the first non-landuse item in the stylesheet, we add the raster layer before the roads but above the landuse if (background.category === "osmbasedmap" || background.category === "map") { // The background layer is already an OSM-based map or another map, so we don't want anything from the baselayer addLayerBeforeId = undefined } if (!map.getSource(background.id)) { try { map.addSource(background.id, RasterLayerHandler.prepareWmsSource(background)) } catch (e) { return false } } if (!map.getLayer(background.id)) { addLayerBeforeId ??= map .getStyle() .layers.find((l) => l.id.startsWith("mapcomplete_"))?.id map.addLayer( { id: background.id, type: "raster", source: background.id, paint: { "raster-opacity": 0, }, }, addLayerBeforeId ) this.opacity.addCallbackAndRun((o) => { map.setPaintProperty(background.id, "raster-opacity", o) }) } return true } private fadeOut() { Stores.Chronic( 8, () => this.opacity.data > 0 && this._deactivationTime !== undefined ).addCallback((_) => this.opacity.setData(Math.max(0, this.opacity.data - this.fadeStep))) } private fadeIn() { Stores.Chronic( 8, () => this.opacity.data < 1.0 && this._deactivationTime === undefined ).addCallback((_) => this.opacity.setData(Math.min(1.0, this.opacity.data + this.fadeStep))) } } export default class RasterLayerHandler { private _singleLayerHandlers: Record = {} constructor(map: Store, background: UIEventSource) { background.addCallbackAndRunD((l) => { const key = l.properties.id if (!this._singleLayerHandlers[key]) { this._singleLayerHandlers[key] = new SingleBackgroundHandler(map, l, background) } }) } public static prepareWmsSource(layer: RasterLayerProperties): SourceSpecification { return { type: "raster", // use the tiles option to specify a 256WMS tile source URL // https://maplibre.org/maplibre-gl-js-docs/style-spec/sources/ tiles: [RasterLayerHandler.prepareWmsURL(layer.url, layer["tile-size"] ?? 256)], tileSize: layer["tile-size"] ?? 256, minzoom: layer["min_zoom"] ?? 1, maxzoom: layer["max_zoom"] ?? 25, // Bit of a hack, but seems to work scheme: layer.url.includes("{-y}") ? "tms" : "xyz", } } private static prepareWmsURL(url: string, size: number = 256): string { // ELI: LAYERS=OGWRGB13_15VL&STYLES=&FORMAT=image/jpeg&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap // PROD: SERVICE=WMS&REQUEST=GetMap&LAYERS=OGWRGB13_15VL&STYLES=&FORMAT=image/jpeg&TRANSPARENT=false&VERSION=1.3.0&WIDTH=256&HEIGHT=256&CRS=EPSG:3857&BBOX=488585.4847988467,6590094.830634755,489196.9810251281,6590706.32686104 const toReplace = { "{bbox}": "{bbox-epsg-3857}", "{proj}": "EPSG:3857", "{width}": "" + size, "{height}": "" + size, "{zoom}": "{z}", "{-y}": "{y}", } for (const key in toReplace) { url = url.replace(new RegExp(key), toReplace[key]) } const subdomains = url.match(/\{switch:([a-zA-Z0-9,]*)}/) if (subdomains !== null) { const options = subdomains[1].split(",") const option = options[Math.floor(Math.random() * options.length)] url = url.replace(subdomains[0], option) } return url } }