diff --git a/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts b/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts index d7e91d8fd..96f70e4f5 100644 --- a/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts +++ b/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts @@ -29,7 +29,6 @@ export default class FeaturePropertiesStore { const source = self._elements.get(id) if (source === undefined) { - console.log("Adding feature store for", id) self._elements.set(id, new UIEventSource(feature.properties)) continue } diff --git a/Models/MapProperties.ts b/Models/MapProperties.ts index 4f5403998..6a2fde02c 100644 --- a/Models/MapProperties.ts +++ b/Models/MapProperties.ts @@ -6,6 +6,7 @@ export interface MapProperties { readonly location: UIEventSource<{ lon: number; lat: number }> readonly zoom: UIEventSource readonly minzoom: UIEventSource + readonly maxzoom: UIEventSource readonly bounds: UIEventSource readonly rasterLayer: UIEventSource readonly maxbounds: UIEventSource diff --git a/Models/RasterLayers.ts b/Models/RasterLayers.ts index ec75de3c8..ef2471dfa 100644 --- a/Models/RasterLayers.ts +++ b/Models/RasterLayers.ts @@ -123,7 +123,9 @@ export interface RasterLayerProperties { /** * The name of the imagery source */ - readonly name: string + readonly name: string | Record + + readonly isOverlay?: boolean readonly id: string diff --git a/Models/ThemeConfig/Json/LayoutConfigJson.ts b/Models/ThemeConfig/Json/LayoutConfigJson.ts index ea12b3c06..ca32942dd 100644 --- a/Models/ThemeConfig/Json/LayoutConfigJson.ts +++ b/Models/ThemeConfig/Json/LayoutConfigJson.ts @@ -1,6 +1,6 @@ import { LayerConfigJson } from "./LayerConfigJson" -import TilesourceConfigJson from "./TilesourceConfigJson" import ExtraLinkConfigJson from "./ExtraLinkConfigJson" +import { RasterLayerProperties } from "../../RasterLayers" /** * Defines the entire theme. @@ -148,7 +148,7 @@ export interface LayoutConfigJson { /** * Define some (overlay) slippy map tilesources */ - tileLayerSources?: TilesourceConfigJson[] + tileLayerSources?: (RasterLayerProperties & { defaultState?: true | boolean })[] /** * The layers to display. diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index 5a7e6bb69..b6db493bb 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -3,11 +3,12 @@ import { LayoutConfigJson } from "./Json/LayoutConfigJson" import LayerConfig from "./LayerConfig" import { LayerConfigJson } from "./Json/LayerConfigJson" import Constants from "../Constants" -import TilesourceConfig from "./TilesourceConfig" import { ExtractImages } from "./Conversion/FixImages" import ExtraLinkConfig from "./ExtraLinkConfig" import { Utils } from "../../Utils" import LanguageUtils from "../../Utils/LanguageUtils" +import { RasterLayerProperties } from "../RasterLayers" + /** * Minimal information about a theme **/ @@ -39,7 +40,7 @@ export default class LayoutConfig implements LayoutInformation { public widenFactor: number public defaultBackgroundId?: string public layers: LayerConfig[] - public tileLayerSources: TilesourceConfig[] + public tileLayerSources: (RasterLayerProperties & { defaultState?: true | boolean })[] public readonly hideFromOverview: boolean public lockLocation: boolean | [[number, number], [number, number]] public readonly enableUserBadge: boolean @@ -161,9 +162,7 @@ export default class LayoutConfig implements LayoutInformation { this.widenFactor = json.widenFactor ?? 1.5 this.defaultBackgroundId = json.defaultBackgroundId - this.tileLayerSources = (json.tileLayerSources ?? []).map( - (config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`) - ) + this.tileLayerSources = json.tileLayerSources ?? [] // At this point, layers should be expanded and validated either by the generateScript or the LegacyJsonConvert this.layers = json.layers.map( (lyrJson) => diff --git a/Models/ThemeConfig/TilesourceConfig.ts b/Models/ThemeConfig/TilesourceConfig.ts deleted file mode 100644 index 4cfca68be..000000000 --- a/Models/ThemeConfig/TilesourceConfig.ts +++ /dev/null @@ -1,43 +0,0 @@ -import TilesourceConfigJson from "./Json/TilesourceConfigJson" -import Translations from "../../UI/i18n/Translations" -import { Translation } from "../../UI/i18n/Translation" - -export default class TilesourceConfig { - public readonly source: string - public readonly id: string - public readonly isOverlay: boolean - public readonly name: Translation - public readonly minzoom: number - public readonly maxzoom: number - public readonly defaultState: boolean - - constructor(config: TilesourceConfigJson, ctx: string = "") { - this.id = config.id - this.source = config.source - this.isOverlay = config.isOverlay ?? false - this.name = Translations.T(config.name) - this.minzoom = config.minZoom ?? 0 - this.maxzoom = config.maxZoom ?? 999 - this.defaultState = config.defaultState ?? true - if (this.id === undefined) { - throw "An id is obligated" - } - if (this.minzoom > this.maxzoom) { - throw ( - "Invalid tilesourceConfig: minzoom should be smaller then maxzoom (at " + ctx + ")" - ) - } - if (this.minzoom < 0) { - throw "minzoom should be > 0 (at " + ctx + ")" - } - if (this.maxzoom < 0) { - throw "maxzoom should be > 0 (at " + ctx + ")" - } - if (this.source.indexOf("{zoom}") >= 0) { - throw "Invalid source url: use {z} instead of {zoom} (at " + ctx + ".source)" - } - if (!this.defaultState && config.name === undefined) { - throw "Disabling an overlay without a name is not possible" - } - } -} diff --git a/Models/ThemeViewState.ts b/Models/ThemeViewState.ts index 3719f61ad..19bf46d9a 100644 --- a/Models/ThemeViewState.ts +++ b/Models/ThemeViewState.ts @@ -43,6 +43,7 @@ import MetaTagging from "../Logic/MetaTagging" import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator" import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource" import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader" +import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer" /** * @@ -82,6 +83,10 @@ export default class ThemeViewState implements SpecialVisualizationState { readonly geolocation: GeoLocationHandler readonly lastClickObject: WritableFeatureSource + readonly overlayLayerStates: ReadonlyMap< + string, + { readonly isDisplayed: UIEventSource } + > constructor(layout: LayoutConfig) { this.layout = layout @@ -125,6 +130,21 @@ export default class ThemeViewState implements SpecialVisualizationState { const self = this this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id) + { + const overlayLayerStates = new Map }>() + for (const rasterInfo of this.layout.tileLayerSources) { + const isDisplayed = QueryParameters.GetBooleanQueryParameter( + "overlay-" + rasterInfo.id, + rasterInfo.defaultState ?? true, + "Wether or not overlayer layer " + rasterInfo.id + " is shown" + ) + const state = { isDisplayed } + overlayLayerStates.set(rasterInfo.id, state) + new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state) + } + this.overlayLayerStates = overlayLayerStates + } + { /* Setup the layout source * A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too diff --git a/UI/BigComponents/FilterView.ts b/UI/BigComponents/FilterView.ts deleted file mode 100644 index 44a84eb54..000000000 --- a/UI/BigComponents/FilterView.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { VariableUiElement } from "../Base/VariableUIElement" -import Toggle from "../Input/Toggle" -import Combine from "../Base/Combine" -import Translations from "../i18n/Translations" -import { Translation } from "../i18n/Translation" -import Svg from "../../Svg" -import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" -import FilteredLayer from "../../Models/FilteredLayer" -import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" -import Loc from "../../Models/Loc" - -export default class FilterView extends VariableUiElement { - constructor( - filteredLayer: Store, - tileLayers: { config: TilesourceConfig; isDisplayed: UIEventSource }[], - state: { - readonly availableBackgroundLayers?: Store - readonly featureSwitchBackgroundSelection?: UIEventSource - readonly featureSwitchIsDebugging?: UIEventSource - readonly locationControl?: UIEventSource - readonly featureSwitchMoreQuests: Store - } - ) { - super( - filteredLayer.map((filteredLayers) => { - // Create the views which toggle layers (and filters them) ... - let elements = filteredLayers - ?.map((l) => - FilterView.createOneFilteredLayerElement(l, state)?.SetClass("filter-panel") - ) - ?.filter((l) => l !== undefined) - elements[0].SetClass("first-filter-panel") - - // ... create views for non-interactive layers ... - elements = elements.concat( - tileLayers.map((tl) => FilterView.createOverlayToggle(state, tl)) - ) - - return elements - }) - ) - } - - private static createOverlayToggle( - state: { locationControl?: UIEventSource }, - config: { config: TilesourceConfig; isDisplayed: UIEventSource } - ) { - const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem;flex-shrink: 0;" - - const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle) - const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle(iconStyle) - const name: Translation = config.config.name - - const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2") - const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2") - - const style = "display:flex;align-items:center;padding:0.5rem 0;" - const layerChecked = new Combine([icon, styledNameChecked]) - .SetStyle(style) - .onClick(() => config.isDisplayed.setData(false)) - - const layerNotChecked = new Combine([iconUnselected, styledNameUnChecked]) - .SetStyle(style) - .onClick(() => config.isDisplayed.setData(true)) - - return new Toggle(layerChecked, layerNotChecked, config.isDisplayed) - } -} diff --git a/UI/BigComponents/OverlayToggle.svelte b/UI/BigComponents/OverlayToggle.svelte new file mode 100644 index 000000000..c774abb35 --- /dev/null +++ b/UI/BigComponents/OverlayToggle.svelte @@ -0,0 +1,47 @@ + +{#if layerproperties.name} +
+ +
+{/if} diff --git a/UI/Map/MapLibreAdaptor.ts b/UI/Map/MapLibreAdaptor.ts index a0345a735..acc9ac1d6 100644 --- a/UI/Map/MapLibreAdaptor.ts +++ b/UI/Map/MapLibreAdaptor.ts @@ -1,6 +1,6 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" import type { Map as MLMap } from "maplibre-gl" -import { Map as MlMap } from "maplibre-gl" +import { Map as MlMap, SourceSpecification } from "maplibre-gl" import { RasterLayerPolygon, RasterLayerProperties } from "../../Models/RasterLayers" import { Utils } from "../../Utils" import { BBox } from "../../Logic/BBox" @@ -37,6 +37,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { readonly allowZooming: UIEventSource readonly lastClickLocation: Store readonly minzoom: UIEventSource + readonly maxzoom: UIEventSource private readonly _maplibreMap: Store /** * Used for internal bookkeeping (to remove a rasterLayer when done loading) @@ -50,12 +51,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 }) this.zoom = state?.zoom ?? new UIEventSource(1) this.minzoom = state?.minzoom ?? new UIEventSource(0) + this.maxzoom = state?.maxzoom ?? new UIEventSource(24) this.zoom.addCallbackAndRunD((z) => { if (z < this.minzoom.data) { this.zoom.setData(this.minzoom.data) } - if (z > 24) { - this.zoom.setData(24) + const max = Math.min(24, this.maxzoom.data ?? 24) + if (z > max) { + this.zoom.setData(max) } }) this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined) @@ -90,6 +93,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { self.setAllowMoving(self.allowMoving.data) self.setAllowZooming(self.allowZooming.data) self.setMinzoom(self.minzoom.data) + self.setMaxzoom(self.maxzoom.data) self.setBounds(self.bounds.data) }) self.MoveMapToCurrentLoc(self.location.data) @@ -98,6 +102,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { self.setAllowMoving(self.allowMoving.data) self.setAllowZooming(self.allowZooming.data) self.setMinzoom(self.minzoom.data) + self.setMaxzoom(self.maxzoom.data) self.setBounds(self.bounds.data) this.updateStores() map.on("moveend", () => this.updateStores()) @@ -146,10 +151,23 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { } } + 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: [MapLibreAdaptor.prepareWmsURL(layer.url, layer["tile-size"] ?? 256)], + tileSize: layer["tile-size"] ?? 256, + minzoom: layer["min_zoom"] ?? 1, + maxzoom: layer["max_zoom"] ?? 25, + // scheme: background["type"] === "tms" ? "tms" : "xyz", + } + } + /** * Prepares an ELI-URL to be compatible with mapbox */ - private static prepareWmsURL(url: string, size: number = 256) { + 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 @@ -342,16 +360,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { return } - map.addSource(background.id, { - type: "raster", - // use the tiles option to specify a 256WMS tile source URL - // https://maplibre.org/maplibre-gl-js-docs/style-spec/sources/ - tiles: [MapLibreAdaptor.prepareWmsURL(background.url, background["tile-size"] ?? 256)], - tileSize: background["tile-size"] ?? 256, - minzoom: background["min_zoom"] ?? 1, - maxzoom: background["max_zoom"] ?? 25, - // scheme: background["type"] === "tms" ? "tms" : "xyz", - }) + map.addSource(background.id, MapLibreAdaptor.prepareWmsSource(background)) map.addLayer( { @@ -405,6 +414,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { map.setMinZoom(minzoom) } + private setMaxzoom(maxzoom: number) { + const map = this._maplibreMap.data + if (map === undefined) { + return + } + map.setMaxZoom(maxzoom) + } + private setAllowZooming(allow: true | boolean | undefined) { const map = this._maplibreMap.data if (map === undefined) { diff --git a/UI/Map/ShowOverlayRasterLayer.ts b/UI/Map/ShowOverlayRasterLayer.ts new file mode 100644 index 000000000..5661a7bd2 --- /dev/null +++ b/UI/Map/ShowOverlayRasterLayer.ts @@ -0,0 +1,92 @@ +import { RasterLayerProperties } from "../../Models/RasterLayers" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { Map as MlMap } from "maplibre-gl" +import { Utils } from "../../Utils" +import { MapLibreAdaptor } from "./MapLibreAdaptor" + +export default class ShowOverlayRasterLayer { + private readonly _map: UIEventSource + private readonly _layer: RasterLayerProperties + private readonly _mapProperties?: { zoom: Store } + private _mllayer + private readonly _isDisplayed?: Store + + constructor( + layer: RasterLayerProperties, + map: UIEventSource, + mapProperties?: { zoom: Store }, + options?: { + isDisplayed?: Store + } + ) { + this._mapProperties = mapProperties + this._layer = layer + this._map = map + this._isDisplayed = options?.isDisplayed + const self = this + map.addCallbackAndRunD((map) => { + self.addLayer() + map.on("load", () => { + self.addLayer() + }) + }) + this.addLayer() + + options?.isDisplayed?.addCallbackAndRun(() => { + self.setVisibility() + }) + + mapProperties?.zoom?.addCallbackAndRun(() => { + self.setVisibility() + }) + } + + private setVisibility() { + let zoom = this._mapProperties?.zoom?.data + let withinRange = zoom === undefined || zoom > this._layer.min_zoom + let isDisplayed = (this._isDisplayed?.data ?? true) && withinRange + this._map.data?.setLayoutProperty( + this._layer.id, + "visibility", + isDisplayed ? "visible" : "none" + ) + } + + private async awaitStyleIsLoaded(): Promise { + const map = this._map.data + if (map === undefined) { + return + } + while (!map?.isStyleLoaded()) { + await Utils.waitFor(250) + } + } + + private async addLayer() { + const map = this._map.data + console.log("Attempting to add ", this._layer.id) + if (map === undefined) { + return + } + await this.awaitStyleIsLoaded() + if (this._mllayer) { + // Already initialized + return + } + const background: RasterLayerProperties = this._layer + + map.addSource(background.id, MapLibreAdaptor.prepareWmsSource(background)) + this._mllayer = map.addLayer({ + id: background.id, + type: "raster", + source: background.id, + paint: {}, + }) + map.setLayoutProperty( + this._layer.id, + "visibility", + this._isDisplayed?.data ?? true ? "visible" : "none" + ) + this.setVisibility() + } +} diff --git a/UI/Popup/MinimapViz.ts b/UI/Popup/MinimapViz.ts index 7c6d67fda..4fd344c67 100644 --- a/UI/Popup/MinimapViz.ts +++ b/UI/Popup/MinimapViz.ts @@ -75,6 +75,7 @@ export class MinimapViz implements SpecialVisualization { const mlmap = new UIEventSource(undefined) const mla = new MapLibreAdaptor(mlmap) + mla.maxzoom.setData(17) let zoom = 18 if (args[0]) { const parsed = Number(args[0]) diff --git a/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts b/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts deleted file mode 100644 index 64a6d3ab1..000000000 --- a/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as L from "leaflet" -import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" -import { UIEventSource } from "../../Logic/UIEventSource" -import ShowOverlayLayer from "./ShowOverlayLayer" - -// TODO port this to maplibre! -export default class ShowOverlayLayerImplementation { - public static Implement() { - ShowOverlayLayer.implementation = ShowOverlayLayerImplementation.AddToMap - } - - public static AddToMap( - config: TilesourceConfig, - leafletMap: UIEventSource, - isShown: UIEventSource = undefined - ) { - leafletMap.map((leaflet) => { - if (leaflet === undefined) { - return - } - - const tileLayer = L.tileLayer(config.source, { - attribution: "", - maxZoom: config.maxzoom, - minZoom: config.minzoom, - // @ts-ignore - wmts: false, - }) - - if (isShown === undefined) { - tileLayer.addTo(leaflet) - } - - isShown?.addCallbackAndRunD((isShown) => { - if (isShown) { - tileLayer.addTo(leaflet) - } else { - leaflet.removeLayer(tileLayer) - } - }) - }) - } -} diff --git a/UI/ThemeViewGUI.svelte b/UI/ThemeViewGUI.svelte index 670af6561..461efe048 100644 --- a/UI/ThemeViewGUI.svelte +++ b/UI/ThemeViewGUI.svelte @@ -34,7 +34,7 @@ import Hotkeys from "./Base/Hotkeys"; import { VariableUiElement } from "./Base/VariableUIElement"; import SvelteUIElement from "./Base/SvelteUIElement"; - import { onDestroy } from "svelte"; + import OverlayToggle from "./BigComponents/OverlayToggle.svelte"; export let state: ThemeViewState; let layout = state.layout; @@ -51,9 +51,9 @@ if (selectedElement === undefined || layer === undefined) { return undefined; } - + const tags = state.featureProperties.getStore(selectedElement.properties.id); - return new SvelteUIElement(SelectedElementView, {state, layer, selectedElement, tags}) + return new SvelteUIElement(SelectedElementView, { state, layer, selectedElement, tags }); }, [selectedLayer]); @@ -160,6 +160,14 @@ {/each} + {#each layout.tileLayerSources as tilesource} + + {/each} diff --git a/assets/themes/uk_addresses/uk_addresses.json b/assets/themes/uk_addresses/uk_addresses.json index 493e614a5..b39453b08 100644 --- a/assets/themes/uk_addresses/uk_addresses.json +++ b/assets/themes/uk_addresses/uk_addresses.json @@ -30,10 +30,10 @@ "tileLayerSources": [ { "id": "property-boundaries", - "source": "https://tiles.osmuk.org/PropertyBoundaries/{z}/{x}/{y}.png", + "url": "https://tiles.osmuk.org/PropertyBoundaries/{z}/{x}/{y}.png", "isOverlay": true, - "minZoom": 18, - "maxZoom": 20, + "min_zoom": 18, + "max_zoom": 20, "defaultState": false, "name": { "en": "Property boundaries by osmuk.org", @@ -695,4 +695,4 @@ "enableShareScreen": false, "enableMoreQuests": false, "credits": "Pieter Vander Vennet, Rob Nickerson, Russ Garrett" -} \ No newline at end of file +}