Add overlay layer possibility, fix #515

This commit is contained in:
Pieter Vander Vennet 2021-10-14 21:43:14 +02:00
parent 7e053b3ada
commit 891c449058
12 changed files with 263 additions and 56 deletions

View file

@ -39,8 +39,8 @@ import {Tiles} from "./Models/TileRange";
import {TileHierarchyAggregator} from "./UI/ShowDataLayer/TileHierarchyAggregator"; import {TileHierarchyAggregator} from "./UI/ShowDataLayer/TileHierarchyAggregator";
import FilterConfig from "./Models/ThemeConfig/FilterConfig"; import FilterConfig from "./Models/ThemeConfig/FilterConfig";
import FilteredLayer from "./Models/FilteredLayer"; import FilteredLayer from "./Models/FilteredLayer";
import {BBox} from "./Logic/BBox";
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
import ShowOverlayLayer from "./UI/ShowDataLayer/ShowOverlayLayer";
export class InitUiElements { export class InitUiElements {
static InitAll( static InitAll(
@ -176,10 +176,9 @@ export class InitUiElements {
State.state.osmConnection.userDetails State.state.osmConnection.userDetails
.addCallbackAndRunD(_ => addHomeMarker()); .addCallbackAndRunD(_ => addHomeMarker());
State.state.leafletMap.addCallbackAndRunD(_ => addHomeMarker()) State.state.leafletMap.addCallbackAndRunD(_ => addHomeMarker())
InitUiElements.setupAllLayerElements(); InitUiElements.setupAllLayerElements();
State.state.locationControl.ping(); State.state.locationControl.ping();
new SelectedFeatureHandler(Hash.hash, State.state) new SelectedFeatureHandler(Hash.hash, State.state)
@ -506,6 +505,21 @@ export class InitUiElements {
); );
}, state }, 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() { private static setupAllLayerElements() {

View file

@ -68,6 +68,9 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
const self = this; const self = this;
Utils.downloadJson(url) Utils.downloadJson(url)
.then(json => { .then(json => {
if(json.elements === undefined || json.elements === null){
return;
}
if (json.elements === [] && json.remarks.indexOf("runtime error") > 0) { if (json.elements === [] && json.remarks.indexOf("runtime error") > 0) {
self.onFail("Runtime error (timeout)", url) self.onFail("Runtime error (timeout)", url)
return; return;
@ -108,7 +111,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
newFeatures.push({feature: feature, freshness: freshness}) newFeatures.push({feature: feature, freshness: freshness})
} }
if (newFeatures.length == 0) { if ( newFeatures.length == 0) {
return; return;
} }

View file

@ -1,5 +1,7 @@
import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import {LayerConfigJson} from "./LayerConfigJson"; import {LayerConfigJson} from "./LayerConfigJson";
import TilesourceConfig from "../TilesourceConfig";
import TilesourceConfigJson from "./TilesourceConfigJson";
/** /**
* Defines the entire theme. * Defines the entire theme.
@ -155,6 +157,11 @@ export interface LayoutConfigJson {
*/ */
defaultBackgroundId?: string; 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 number of seconds that a feature is allowed to stay in the cache.
* The caching flow is as following: * The caching flow is as following:

View file

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

View file

@ -7,6 +7,7 @@ import {Utils} from "../../Utils";
import LayerConfig from "./LayerConfig"; import LayerConfig from "./LayerConfig";
import {LayerConfigJson} from "./Json/LayerConfigJson"; import {LayerConfigJson} from "./Json/LayerConfigJson";
import Constants from "../Constants"; import Constants from "../Constants";
import TilesourceConfig from "./TilesourceConfig";
export default class LayoutConfig { export default class LayoutConfig {
public readonly id: string; public readonly id: string;
@ -27,6 +28,7 @@ export default class LayoutConfig {
public readonly roamingRenderings: TagRenderingConfig[]; public readonly roamingRenderings: TagRenderingConfig[];
public readonly defaultBackgroundId?: string; public readonly defaultBackgroundId?: string;
public layers: LayerConfig[]; public layers: LayerConfig[];
public tileLayerSources: TilesourceConfig[]
public readonly clustering?: { public readonly clustering?: {
maxZoom: number, maxZoom: number,
minNeededElements: number, minNeededElements: number,
@ -108,6 +110,7 @@ export default class LayoutConfig {
} }
); );
this.defaultBackgroundId = json.defaultBackgroundId; 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); this.layers = LayoutConfig.ExtractLayers(json, official, context);
// ALl the layers are constructed, let them share tagRenderings now! // ALl the layers are constructed, let them share tagRenderings now!

View file

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

View file

@ -19,6 +19,7 @@ import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor";
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
import {BBox} from "./Logic/BBox"; import {BBox} from "./Logic/BBox";
import SelectedElementTagsUpdater from "./Logic/Actors/SelectedElementTagsUpdater"; import SelectedElementTagsUpdater from "./Logic/Actors/SelectedElementTagsUpdater";
import TilesourceConfig from "./Models/ThemeConfig/TilesourceConfig";
/** /**
* Contains the global state: a bunch of UI-event sources * Contains the global state: a bunch of UI-event sources
@ -57,6 +58,8 @@ export default class State {
public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers"); public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers");
public overlayToggles : { config: TilesourceConfig, isDisplayed: UIEventSource<boolean>}[]
/** /**
The latest element that was selected The latest element that was selected
*/ */
@ -420,6 +423,11 @@ export default class State {
.ping(); .ping();
new TitleHandler(this); new TitleHandler(this);
this.overlayToggles = this.layoutToUse.tileLayerSources.filter(c => c.name !== undefined).map(c => ({
config: c,
isDisplayed: new UIEventSource<boolean>(c.defaultState)
}))
} }
private static asFloat(source: UIEventSource<string>): UIEventSource<number> { private static asFloat(source: UIEventSource<string>): UIEventSource<number> {

View file

@ -13,21 +13,71 @@ import State from "../../State";
import FilteredLayer from "../../Models/FilteredLayer"; import FilteredLayer from "../../Models/FilteredLayer";
import BackgroundSelector from "./BackgroundSelector"; import BackgroundSelector from "./BackgroundSelector";
import FilterConfig from "../../Models/ThemeConfig/FilterConfig"; import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
export default class FilterView extends VariableUiElement { export default class FilterView extends VariableUiElement {
constructor(filteredLayer: UIEventSource<FilteredLayer[]>) { constructor(filteredLayer: UIEventSource<FilteredLayer[]>, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]) {
const backgroundSelector = new Toggle( const backgroundSelector = new Toggle(
new BackgroundSelector(), new BackgroundSelector(),
undefined, undefined,
State.state.featureSwitchBackgroundSlection State.state.featureSwitchBackgroundSlection
) )
super( super(
filteredLayer.map((filteredLayers) => filteredLayer.map((filteredLayers) => {
filteredLayers?.map(l => FilterView.createOneFilteredLayerElement(l)).concat(backgroundSelector) 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<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.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) { private static createOneFilteredLayerElement(filteredLayer: FilteredLayer) {
if (filteredLayer.layerDef.name === undefined) { if (filteredLayer.layerDef.name === undefined) {
// Name is not defined: we hide this one // Name is not defined: we hide this one
@ -104,16 +154,16 @@ export default class FilterView extends VariableUiElement {
listFilterElements.forEach((inputElement, i) => listFilterElements.forEach((inputElement, i) =>
inputElement[1].addCallback((changed) => { inputElement[1].addCallback((changed) => {
const oldValue = flayer.appliedFilters.data const oldValue = flayer.appliedFilters.data
if(changed === undefined){ if (changed === undefined) {
// Lets figure out which filter should be removed // Lets figure out which filter should be removed
// We know this inputElement corresponds with layer.filters[i] // We know this inputElement corresponds with layer.filters[i]
// SO, if there is a value in 'oldValue' with this filter, we have to recalculated // 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 // The filter to remove is already gone, we can stop
return; 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 // The changed value is already there
return; return;
} }
@ -126,16 +176,16 @@ export default class FilterView extends VariableUiElement {
); );
flayer.appliedFilters.addCallbackAndRun(appliedFilters => { 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]; const filter = layer.filters[i];
let foundMatch = undefined let foundMatch = undefined
for (const appliedFilter of appliedFilters) { for (const appliedFilter of appliedFilters) {
if(appliedFilter.filter === filter){ if (appliedFilter.filter === filter) {
foundMatch = appliedFilter foundMatch = appliedFilter
break; break;
} }
} }
listFilterElements[i][1].setData(foundMatch) listFilterElements[i][1].setData(foundMatch)
} }
@ -172,7 +222,7 @@ export default class FilterView extends VariableUiElement {
let options = filterConfig.options; let options = filterConfig.options;
const values = options.map((f, i) => ({ const values = options.map((f, i) => ({
filter: filterConfig, selected: i filter: filterConfig, selected: i
})) }))
const radio = new RadioButton( const radio = new RadioButton(
options.map( options.map(
@ -183,13 +233,13 @@ export default class FilterView extends VariableUiElement {
dontStyle: true dontStyle: true
} }
); );
return [radio, return [radio,
radio.GetValue().map( radio.GetValue().map(
i => values[i], i => values[i],
[], [],
selected => { selected => {
return selected?.selected return selected?.selected
} }
)] )]
} }
} }

View file

@ -16,7 +16,7 @@ import {BBox} from "../../Logic/BBox";
export default class LeftControls extends Combine { export default class LeftControls extends Combine {
constructor(state: {featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc>}) { constructor(state: {featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc>, overlayToggles: any}) {
const toggledCopyright = new ScrollableFullScreen( const toggledCopyright = new ScrollableFullScreen(
() => Translations.t.general.attribution.attributionTitle.Clone(), () => Translations.t.general.attribution.attributionTitle.Clone(),
@ -52,12 +52,11 @@ export default class LeftControls extends Combine {
[State.state.featureSwitchExportAsPdf]) [State.state.featureSwitchExportAsPdf])
); );
const toggledFilter = new Toggle( const toggledFilter = new Toggle(
new ScrollableFullScreen( new ScrollableFullScreen(
() => Translations.t.general.layerSelection.title.Clone(), () => 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" "block p-1 rounded-full"
), ),
undefined, undefined,

View file

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

@ -25,6 +25,18 @@
"minNeededFeatures": 25, "minNeededFeatures": 25,
"maxZoom": 16 "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": [ "layers": [
{ {
"id": "to_import", "id": "to_import",

52
test.ts
View file

@ -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 MinimapImplementation from "./UI/Base/MinimapImplementation";
import MoveConfig from "./Models/ThemeConfig/MoveConfig"; import Minimap from "./UI/Base/Minimap";
import {FixedUiElement} from "./UI/Base/FixedUiElement"; import ShowOverlayLayer from "./UI/ShowDataLayer/ShowOverlayLayer";
import Combine from "./UI/Base/Combine"; 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() MinimapImplementation.initialize()
new MoveWizard(
feature,
State.state,
new MoveConfig({
enableRelocation: false,
enableImproveAccuracy: true
}, "test")).AttachTo("maindiv")
*/ const map = Minimap.createMiniMap({
location: new UIEventSource<Loc>({
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)