Fix overlay layers

This commit is contained in:
Pieter Vander Vennet 2023-04-21 01:53:24 +02:00
parent 3aeedf22c8
commit 24f7610d0a
15 changed files with 216 additions and 184 deletions

View file

@ -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<any>(feature.properties))
continue
}

View file

@ -6,6 +6,7 @@ export interface MapProperties {
readonly location: UIEventSource<{ lon: number; lat: number }>
readonly zoom: UIEventSource<number>
readonly minzoom: UIEventSource<number>
readonly maxzoom: UIEventSource<number>
readonly bounds: UIEventSource<BBox>
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
readonly maxbounds: UIEventSource<undefined | BBox>

View file

@ -123,7 +123,9 @@ export interface RasterLayerProperties {
/**
* The name of the imagery source
*/
readonly name: string
readonly name: string | Record<string, string>
readonly isOverlay?: boolean
readonly id: string

View file

@ -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.

View file

@ -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) =>

View file

@ -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"
}
}
}

View file

@ -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<boolean> }
>
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<string, { isDisplayed: UIEventSource<boolean> }>()
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

View file

@ -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<FilteredLayer[]>,
tileLayers: { config: TilesourceConfig; isDisplayed: UIEventSource<boolean> }[],
state: {
readonly availableBackgroundLayers?: Store<BaseLayer[]>
readonly featureSwitchBackgroundSelection?: UIEventSource<boolean>
readonly featureSwitchIsDebugging?: UIEventSource<boolean>
readonly locationControl?: UIEventSource<Loc>
readonly featureSwitchMoreQuests: Store<boolean>
}
) {
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<Loc> },
config: { config: TilesourceConfig; isDisplayed: UIEventSource<boolean> }
) {
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)
}
}

View file

@ -0,0 +1,47 @@
<script lang="ts">/**
* The OverlayToggle shows a single toggle to enable or disable an overlay
*/
import Checkbox from "../Base/Checkbox.svelte";
import { onDestroy } from "svelte";
import { UIEventSource } from "../../Logic/UIEventSource";
import Tr from "../Base/Tr.svelte";
import Translations from "../i18n/Translations";
import type { RasterLayerProperties } from "../../Models/RasterLayers";
import { Translation } from "../i18n/Translation";
export let layerproperties : RasterLayerProperties
export let state: {isDisplayed: UIEventSource<boolean>};
export let zoomlevel: UIEventSource<number>;
export let highlightedLayer: UIEventSource<string> | undefined;
let isDisplayed: boolean = state.isDisplayed.data;
onDestroy(state.isDisplayed.addCallbackAndRunD(d => {
isDisplayed = d;
return false;
}));
let mainElem: HTMLElement;
$: onDestroy(
highlightedLayer.addCallbackAndRun(highlightedLayer => {
if (highlightedLayer === layerproperties.id) {
mainElem?.classList?.add("glowing-shadow");
} else {
mainElem?.classList?.remove("glowing-shadow");
}
})
);
</script>
{#if layerproperties.name}
<div bind:this={mainElem}>
<label class="flex gap-1">
<Checkbox selected={state.isDisplayed} />
<Tr t={new Translation(layerproperties.name)}/>
{#if $zoomlevel < layerproperties.min_zoom}
<span class="alert">
<Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer} />
</span>
{/if}
</label>
</div>
{/if}

View file

@ -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<true | boolean | undefined>
readonly lastClickLocation: Store<undefined | { lon: number; lat: number }>
readonly minzoom: UIEventSource<number>
readonly maxzoom: UIEventSource<number>
private readonly _maplibreMap: Store<MLMap>
/**
* 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) {

View file

@ -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<MlMap>
private readonly _layer: RasterLayerProperties
private readonly _mapProperties?: { zoom: Store<number> }
private _mllayer
private readonly _isDisplayed?: Store<boolean>
constructor(
layer: RasterLayerProperties,
map: UIEventSource<MlMap>,
mapProperties?: { zoom: Store<number> },
options?: {
isDisplayed?: Store<boolean>
}
) {
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<void> {
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()
}
}

View file

@ -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])

View file

@ -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<any>,
isShown: UIEventSource<boolean> = 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)
}
})
})
}
}

View file

@ -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;
@ -53,7 +53,7 @@
}
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 @@
<Filterview zoomlevel={state.mapProperties.zoom} filteredLayer={state.layerState.filteredLayers.get(layer.id)}
highlightedLayer={state.guistate.highlightedLayerInFilters}></Filterview>
{/each}
{#each layout.tileLayerSources as tilesource}
<OverlayToggle
layerproperties={tilesource}
state={state.overlayLayerStates.get(tilesource.id)}
highlightedLayer={state.guistate.highlightedLayerInFilters}
zoomlevel={state.mapProperties.zoom}
/>
{/each}
<If condition={state.featureSwitches.featureSwitchBackgroundSelection}>
<RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker>
</If>

View file

@ -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",