Add overlay layer possibility, fix #515

This commit is contained in:
pietervdvn 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 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() {

View file

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

View file

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

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 {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!

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 {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<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers");
public overlayToggles : { config: TilesourceConfig, isDisplayed: UIEventSource<boolean>}[]
/**
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<boolean>(c.defaultState)
}))
}
private static asFloat(source: UIEventSource<string>): UIEventSource<number> {

View file

@ -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<FilteredLayer[]>) {
constructor(filteredLayer: UIEventSource<FilteredLayer[]>, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]) {
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<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) {
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
}
)]
}
}

View file

@ -16,7 +16,7 @@ import {BBox} from "../../Logic/BBox";
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(
() => 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,

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

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