forked from MapComplete/MapComplete
Add overlay layer possibility, fix #515
This commit is contained in:
parent
7e053b3ada
commit
891c449058
12 changed files with 263 additions and 56 deletions
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
36
Models/ThemeConfig/Json/TilesourceConfigJson.ts
Normal file
36
Models/ThemeConfig/Json/TilesourceConfigJson.ts
Normal 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;
|
||||
|
||||
}
|
|
@ -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!
|
||||
|
|
38
Models/ThemeConfig/TilesourceConfig.ts
Normal file
38
Models/ThemeConfig/TilesourceConfig.ts
Normal 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"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
8
State.ts
8
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<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> {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
45
UI/ShowDataLayer/ShowOverlayLayer.ts
Normal file
45
UI/ShowDataLayer/ShowOverlayLayer.ts
Normal 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)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
} )
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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
52
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<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)
|
Loading…
Reference in a new issue