From 891c449058731e17ac1d5bf54c9e3819c9e2cf2b Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Thu, 14 Oct 2021 21:43:14 +0200 Subject: [PATCH] Add overlay layer possibility, fix #515 --- InitUiElements.ts | 22 ++++- Logic/FeatureSource/Sources/GeoJsonSource.ts | 5 +- Models/ThemeConfig/Json/LayoutConfigJson.ts | 7 ++ .../ThemeConfig/Json/TilesourceConfigJson.ts | 36 ++++++++ Models/ThemeConfig/LayoutConfig.ts | 3 + Models/ThemeConfig/TilesourceConfig.ts | 38 ++++++++ State.ts | 8 ++ UI/BigComponents/FilterView.ts | 86 +++++++++++++++---- UI/BigComponents/LeftControls.ts | 5 +- UI/ShowDataLayer/ShowOverlayLayer.ts | 45 ++++++++++ assets/themes/uk_addresses/uk_addresses.json | 12 +++ test.ts | 52 +++++------ 12 files changed, 263 insertions(+), 56 deletions(-) create mode 100644 Models/ThemeConfig/Json/TilesourceConfigJson.ts create mode 100644 Models/ThemeConfig/TilesourceConfig.ts create mode 100644 UI/ShowDataLayer/ShowOverlayLayer.ts diff --git a/InitUiElements.ts b/InitUiElements.ts index 59f56b849f..7e63c59725 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -39,8 +39,8 @@ import {Tiles} from "./Models/TileRange"; import {TileHierarchyAggregator} from "./UI/ShowDataLayer/TileHierarchyAggregator"; import FilterConfig from "./Models/ThemeConfig/FilterConfig"; import FilteredLayer from "./Models/FilteredLayer"; -import {BBox} from "./Logic/BBox"; import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; +import ShowOverlayLayer from "./UI/ShowDataLayer/ShowOverlayLayer"; export class InitUiElements { static InitAll( @@ -176,10 +176,9 @@ export class InitUiElements { State.state.osmConnection.userDetails .addCallbackAndRunD(_ => addHomeMarker()); State.state.leafletMap.addCallbackAndRunD(_ => addHomeMarker()) - - + InitUiElements.setupAllLayerElements(); - State.state.locationControl.ping(); + State.state.locationControl.ping(); new SelectedFeatureHandler(Hash.hash, State.state) @@ -506,6 +505,21 @@ export class InitUiElements { ); }, state ); + + + const initialized =new Set() + + for (const overlayToggle of State.state.overlayToggles) { + new ShowOverlayLayer(overlayToggle.config, state.leafletMap, overlayToggle.isDisplayed) + initialized.add(overlayToggle.config) + } + + for (const tileLayerSource of state.layoutToUse.tileLayerSources) { + if (initialized.has(tileLayerSource)) { + continue + } + new ShowOverlayLayer(tileLayerSource, state.leafletMap) + } } private static setupAllLayerElements() { diff --git a/Logic/FeatureSource/Sources/GeoJsonSource.ts b/Logic/FeatureSource/Sources/GeoJsonSource.ts index b341a0dd3b..81785c14b1 100644 --- a/Logic/FeatureSource/Sources/GeoJsonSource.ts +++ b/Logic/FeatureSource/Sources/GeoJsonSource.ts @@ -68,6 +68,9 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { const self = this; Utils.downloadJson(url) .then(json => { + if(json.elements === undefined || json.elements === null){ + return; + } if (json.elements === [] && json.remarks.indexOf("runtime error") > 0) { self.onFail("Runtime error (timeout)", url) return; @@ -108,7 +111,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { newFeatures.push({feature: feature, freshness: freshness}) } - if (newFeatures.length == 0) { + if ( newFeatures.length == 0) { return; } diff --git a/Models/ThemeConfig/Json/LayoutConfigJson.ts b/Models/ThemeConfig/Json/LayoutConfigJson.ts index c56f6c1147..401cd5f689 100644 --- a/Models/ThemeConfig/Json/LayoutConfigJson.ts +++ b/Models/ThemeConfig/Json/LayoutConfigJson.ts @@ -1,5 +1,7 @@ import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; import {LayerConfigJson} from "./LayerConfigJson"; +import TilesourceConfig from "../TilesourceConfig"; +import TilesourceConfigJson from "./TilesourceConfigJson"; /** * Defines the entire theme. @@ -155,6 +157,11 @@ export interface LayoutConfigJson { */ defaultBackgroundId?: string; + /** + * Define some (overlay) slippy map tilesources + */ + tileLayerSources?: TilesourceConfigJson[] + /** * The number of seconds that a feature is allowed to stay in the cache. * The caching flow is as following: diff --git a/Models/ThemeConfig/Json/TilesourceConfigJson.ts b/Models/ThemeConfig/Json/TilesourceConfigJson.ts new file mode 100644 index 0000000000..43e6f2d139 --- /dev/null +++ b/Models/ThemeConfig/Json/TilesourceConfigJson.ts @@ -0,0 +1,36 @@ +/** + * Configuration for a tilesource config + */ +export default interface TilesourceConfigJson { + + + /** + * The path, where {x}, {y} and {z} will be substituted + */ + source: string, + + isOverlay?: boolean, + + /** + * How this will be shown in the selection menu. + * Make undefined if this may not be toggled + */ + name?: any | string + + /** + * Only visible at this or a higher zoom level + */ + minZoom?: number + + /** + * Only visible at this or a lower zoom level + */ + maxZoom?: number + + + /** + * The default state, set to false to hide by default + */ + defaultState: boolean; + +} \ No newline at end of file diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index bfdf00e14a..f56a561b01 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -7,6 +7,7 @@ import {Utils} from "../../Utils"; import LayerConfig from "./LayerConfig"; import {LayerConfigJson} from "./Json/LayerConfigJson"; import Constants from "../Constants"; +import TilesourceConfig from "./TilesourceConfig"; export default class LayoutConfig { public readonly id: string; @@ -27,6 +28,7 @@ export default class LayoutConfig { public readonly roamingRenderings: TagRenderingConfig[]; public readonly defaultBackgroundId?: string; public layers: LayerConfig[]; + public tileLayerSources: TilesourceConfig[] public readonly clustering?: { maxZoom: number, minNeededElements: number, @@ -108,6 +110,7 @@ export default class LayoutConfig { } ); this.defaultBackgroundId = json.defaultBackgroundId; + this.tileLayerSources = (json.tileLayerSources??[]).map((config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`)) this.layers = LayoutConfig.ExtractLayers(json, official, context); // ALl the layers are constructed, let them share tagRenderings now! diff --git a/Models/ThemeConfig/TilesourceConfig.ts b/Models/ThemeConfig/TilesourceConfig.ts new file mode 100644 index 0000000000..2cb01e40e4 --- /dev/null +++ b/Models/ThemeConfig/TilesourceConfig.ts @@ -0,0 +1,38 @@ +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 isOverlay: boolean + public readonly name: Translation + public readonly minzoom: number + public readonly maxzoom: number + public readonly defaultState: boolean; + + constructor(config: TilesourceConfigJson, ctx: string = "") { + + 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.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" + } + } + +} \ No newline at end of file diff --git a/State.ts b/State.ts index cdc61c57e7..30485d6085 100644 --- a/State.ts +++ b/State.ts @@ -19,6 +19,7 @@ import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor"; import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; import {BBox} from "./Logic/BBox"; import SelectedElementTagsUpdater from "./Logic/Actors/SelectedElementTagsUpdater"; +import TilesourceConfig from "./Models/ThemeConfig/TilesourceConfig"; /** * Contains the global state: a bunch of UI-event sources @@ -57,6 +58,8 @@ export default class State { public filteredLayers: UIEventSource = new UIEventSource([], "filteredLayers"); + public overlayToggles : { config: TilesourceConfig, isDisplayed: UIEventSource}[] + /** The latest element that was selected */ @@ -420,6 +423,11 @@ export default class State { .ping(); new TitleHandler(this); + + this.overlayToggles = this.layoutToUse.tileLayerSources.filter(c => c.name !== undefined).map(c => ({ + config: c, + isDisplayed: new UIEventSource(c.defaultState) + })) } private static asFloat(source: UIEventSource): UIEventSource { diff --git a/UI/BigComponents/FilterView.ts b/UI/BigComponents/FilterView.ts index f5e48161fd..d8a46d517a 100644 --- a/UI/BigComponents/FilterView.ts +++ b/UI/BigComponents/FilterView.ts @@ -13,21 +13,71 @@ import State from "../../State"; import FilteredLayer from "../../Models/FilteredLayer"; import BackgroundSelector from "./BackgroundSelector"; import FilterConfig from "../../Models/ThemeConfig/FilterConfig"; +import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; export default class FilterView extends VariableUiElement { - constructor(filteredLayer: UIEventSource) { + constructor(filteredLayer: UIEventSource, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource }[]) { const backgroundSelector = new Toggle( new BackgroundSelector(), undefined, State.state.featureSwitchBackgroundSlection ) super( - filteredLayer.map((filteredLayers) => - filteredLayers?.map(l => FilterView.createOneFilteredLayerElement(l)).concat(backgroundSelector) + filteredLayer.map((filteredLayers) => { + let elements = filteredLayers?.map(l => FilterView.createOneFilteredLayerElement(l)) + elements = elements.concat(tileLayers.map(tl => FilterView.createOverlayToggle(tl))) + return elements.concat(backgroundSelector); + } ) ); } + private static createOverlayToggle(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.Clone(); + + const styledNameChecked = name + .Clone() + .SetStyle("font-size:large;padding-left:1.25rem"); + + const styledNameUnChecked = name + .Clone() + .SetStyle("font-size:large;padding-left:1.25rem"); + + const zoomStatus = + new Toggle( + undefined, + Translations.t.general.layerSelection.zoomInToSeeThisLayer.Clone() + .SetClass("alert") + .SetStyle("display: block ruby;width:min-content;"), + State.state.locationControl.map(location => location.zoom >= config.config.minzoom) + ) + + + const style = + "display:flex;align-items:center;padding:0.5rem 0;"; + const layerChecked = new Combine([icon, styledNameChecked, zoomStatus]) + .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 + ); + } + private static createOneFilteredLayerElement(filteredLayer: FilteredLayer) { if (filteredLayer.layerDef.name === undefined) { // Name is not defined: we hide this one @@ -104,16 +154,16 @@ export default class FilterView extends VariableUiElement { listFilterElements.forEach((inputElement, i) => inputElement[1].addCallback((changed) => { const oldValue = flayer.appliedFilters.data - - if(changed === undefined){ + + if (changed === undefined) { // Lets figure out which filter should be removed // We know this inputElement corresponds with layer.filters[i] // SO, if there is a value in 'oldValue' with this filter, we have to recalculated - if(!oldValue.some(f => f.filter === layer.filters[i])){ + if (!oldValue.some(f => f.filter === layer.filters[i])) { // The filter to remove is already gone, we can stop return; } - }else if(oldValue.some(f => f.filter === changed.filter && f.selected === changed.selected)){ + } else if (oldValue.some(f => f.filter === changed.filter && f.selected === changed.selected)) { // The changed value is already there return; } @@ -126,16 +176,16 @@ export default class FilterView extends VariableUiElement { ); flayer.appliedFilters.addCallbackAndRun(appliedFilters => { - for (let i = 0; i < layer.filters.length; i++){ + for (let i = 0; i < layer.filters.length; i++) { const filter = layer.filters[i]; let foundMatch = undefined for (const appliedFilter of appliedFilters) { - if(appliedFilter.filter === filter){ + if (appliedFilter.filter === filter) { foundMatch = appliedFilter - break; + break; } } - + listFilterElements[i][1].setData(foundMatch) } @@ -172,7 +222,7 @@ export default class FilterView extends VariableUiElement { let options = filterConfig.options; const values = options.map((f, i) => ({ - filter: filterConfig, selected: i + filter: filterConfig, selected: i })) const radio = new RadioButton( options.map( @@ -183,13 +233,13 @@ export default class FilterView extends VariableUiElement { dontStyle: true } ); - return [radio, + return [radio, radio.GetValue().map( - i => values[i], + i => values[i], [], - selected => { - return selected?.selected - } - )] + selected => { + return selected?.selected + } + )] } } diff --git a/UI/BigComponents/LeftControls.ts b/UI/BigComponents/LeftControls.ts index 7ee2a74e64..fef132b4e7 100644 --- a/UI/BigComponents/LeftControls.ts +++ b/UI/BigComponents/LeftControls.ts @@ -16,7 +16,7 @@ import {BBox} from "../../Logic/BBox"; export default class LeftControls extends Combine { - constructor(state: {featurePipeline: FeaturePipeline, currentBounds: UIEventSource, locationControl: UIEventSource}) { + constructor(state: {featurePipeline: FeaturePipeline, currentBounds: UIEventSource, locationControl: UIEventSource, overlayToggles: any}) { const toggledCopyright = new ScrollableFullScreen( () => Translations.t.general.attribution.attributionTitle.Clone(), @@ -52,12 +52,11 @@ export default class LeftControls extends Combine { [State.state.featureSwitchExportAsPdf]) ); - const toggledFilter = new Toggle( new ScrollableFullScreen( () => Translations.t.general.layerSelection.title.Clone(), () => - new FilterView(State.state.filteredLayers).SetClass( + new FilterView(State.state.filteredLayers, state.overlayToggles).SetClass( "block p-1 rounded-full" ), undefined, diff --git a/UI/ShowDataLayer/ShowOverlayLayer.ts b/UI/ShowDataLayer/ShowOverlayLayer.ts new file mode 100644 index 0000000000..0756c5e023 --- /dev/null +++ b/UI/ShowDataLayer/ShowOverlayLayer.ts @@ -0,0 +1,45 @@ +import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import * as L from "leaflet"; + +export default class ShowOverlayLayer { + + constructor(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) + } + + }) + + } ) + + + + } + + +} \ No newline at end of file diff --git a/assets/themes/uk_addresses/uk_addresses.json b/assets/themes/uk_addresses/uk_addresses.json index 273c5f8d9e..d134c69dc8 100644 --- a/assets/themes/uk_addresses/uk_addresses.json +++ b/assets/themes/uk_addresses/uk_addresses.json @@ -25,6 +25,18 @@ "minNeededFeatures": 25, "maxZoom": 16 }, + "tileLayerSources": [ + { + "source": "https://tiles.osmuk.org/PropertyBoundaries/{z}/{x}/{y}.png", + "isOverlay": true, + "minZoom": 18, + "maxZoom": 20, + "defaultState": false, + "name": { + "en": "Parcel boundaries" + } + } + ], "layers": [ { "id": "to_import", diff --git a/test.ts b/test.ts index 0442ef0910..8dc3bf0344 100644 --- a/test.ts +++ b/test.ts @@ -1,34 +1,26 @@ -import MoveWizard from "./UI/Popup/MoveWizard"; -import State from "./State"; -import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; import MinimapImplementation from "./UI/Base/MinimapImplementation"; -import MoveConfig from "./Models/ThemeConfig/MoveConfig"; -import {FixedUiElement} from "./UI/Base/FixedUiElement"; -import Combine from "./UI/Base/Combine"; +import Minimap from "./UI/Base/Minimap"; +import ShowOverlayLayer from "./UI/ShowDataLayer/ShowOverlayLayer"; +import TilesourceConfig from "./Models/ThemeConfig/TilesourceConfig"; +import Loc from "./Models/Loc"; +import {UIEventSource} from "./Logic/UIEventSource"; - -State.state = new State(AllKnownLayouts.allKnownLayouts.get("bookcases")) -const feature = { - "type": "Feature", - "properties": { - id: "node/14925464" - }, - "geometry": { - "type": "Point", - "coordinates": [ - 4.21875, - 50.958426723359935 - ] - } -} -/* MinimapImplementation.initialize() -new MoveWizard( - feature, - State.state, - new MoveConfig({ - enableRelocation: false, - enableImproveAccuracy: true - }, "test")).AttachTo("maindiv") -*/ +const map = Minimap.createMiniMap({ + location: new UIEventSource({ + zoom: 19, + lat: 51.51896, + lon: -0.11267 + }) + +}) +map.SetStyle("height: 50rem") +map.AttachTo("maindiv") + +new ShowOverlayLayer(new TilesourceConfig({ + "source": "https://tiles.osmuk.org/PropertyBoundaries/{z}/{x}/{y}.png", + "isOverlay": true, + minZoom: 18, + maxZoom: 20 +}), map) \ No newline at end of file