forked from MapComplete/MapComplete
		
	More refactoring, move minimap behind facade
This commit is contained in:
		
							parent
							
								
									c11ff652b8
								
							
						
					
					
						commit
						d5c1ba4cd1
					
				
					 79 changed files with 1848 additions and 1118 deletions
				
			
		| 
						 | 
					@ -1,7 +1,6 @@
 | 
				
			||||||
import {FixedUiElement} from "./UI/Base/FixedUiElement";
 | 
					import {FixedUiElement} from "./UI/Base/FixedUiElement";
 | 
				
			||||||
import Toggle from "./UI/Input/Toggle";
 | 
					import Toggle from "./UI/Input/Toggle";
 | 
				
			||||||
import State from "./State";
 | 
					import State from "./State";
 | 
				
			||||||
import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource";
 | 
					 | 
				
			||||||
import {UIEventSource} from "./Logic/UIEventSource";
 | 
					import {UIEventSource} from "./Logic/UIEventSource";
 | 
				
			||||||
import {QueryParameters} from "./Logic/Web/QueryParameters";
 | 
					import {QueryParameters} from "./Logic/Web/QueryParameters";
 | 
				
			||||||
import StrayClickHandler from "./Logic/Actors/StrayClickHandler";
 | 
					import StrayClickHandler from "./Logic/Actors/StrayClickHandler";
 | 
				
			||||||
| 
						 | 
					@ -18,17 +17,15 @@ import * as L from "leaflet";
 | 
				
			||||||
import Img from "./UI/Base/Img";
 | 
					import Img from "./UI/Base/Img";
 | 
				
			||||||
import UserDetails from "./Logic/Osm/OsmConnection";
 | 
					import UserDetails from "./Logic/Osm/OsmConnection";
 | 
				
			||||||
import Attribution from "./UI/BigComponents/Attribution";
 | 
					import Attribution from "./UI/BigComponents/Attribution";
 | 
				
			||||||
import LayerResetter from "./Logic/Actors/LayerResetter";
 | 
					import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter";
 | 
				
			||||||
import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs";
 | 
					import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs";
 | 
				
			||||||
import ShowDataLayer from "./UI/ShowDataLayer";
 | 
					import ShowDataLayer from "./UI/ShowDataLayer/ShowDataLayer";
 | 
				
			||||||
import Hash from "./Logic/Web/Hash";
 | 
					import Hash from "./Logic/Web/Hash";
 | 
				
			||||||
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
 | 
					import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
 | 
				
			||||||
import ScrollableFullScreen from "./UI/Base/ScrollableFullScreen";
 | 
					import ScrollableFullScreen from "./UI/Base/ScrollableFullScreen";
 | 
				
			||||||
import Translations from "./UI/i18n/Translations";
 | 
					import Translations from "./UI/i18n/Translations";
 | 
				
			||||||
import MapControlButton from "./UI/MapControlButton";
 | 
					import MapControlButton from "./UI/MapControlButton";
 | 
				
			||||||
import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler";
 | 
					 | 
				
			||||||
import LZString from "lz-string";
 | 
					import LZString from "lz-string";
 | 
				
			||||||
import FeatureSource from "./Logic/FeatureSource/FeatureSource";
 | 
					 | 
				
			||||||
import AllKnownLayers from "./Customizations/AllKnownLayers";
 | 
					import AllKnownLayers from "./Customizations/AllKnownLayers";
 | 
				
			||||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
 | 
					import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
 | 
				
			||||||
import {TagsFilter} from "./Logic/Tags/TagsFilter";
 | 
					import {TagsFilter} from "./Logic/Tags/TagsFilter";
 | 
				
			||||||
| 
						 | 
					@ -38,7 +35,6 @@ import {LayoutConfigJson} from "./Models/ThemeConfig/Json/LayoutConfigJson";
 | 
				
			||||||
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
 | 
					import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
 | 
				
			||||||
import LayerConfig from "./Models/ThemeConfig/LayerConfig";
 | 
					import LayerConfig from "./Models/ThemeConfig/LayerConfig";
 | 
				
			||||||
import Minimap from "./UI/Base/Minimap";
 | 
					import Minimap from "./UI/Base/Minimap";
 | 
				
			||||||
import Constants from "./Models/Constants";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class InitUiElements {
 | 
					export class InitUiElements {
 | 
				
			||||||
    static InitAll(
 | 
					    static InitAll(
 | 
				
			||||||
| 
						 | 
					@ -130,10 +126,9 @@ export class InitUiElements {
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if (somethingChanged) {
 | 
					            if (somethingChanged) {
 | 
				
			||||||
                console.log("layoutToUse.layers:", layoutToUse.layers);
 | 
					 | 
				
			||||||
                State.state.layoutToUse.data.layers = Array.from(neededLayers);
 | 
					                State.state.layoutToUse.data.layers = Array.from(neededLayers);
 | 
				
			||||||
                State.state.layoutToUse.ping();
 | 
					                State.state.layoutToUse.ping();
 | 
				
			||||||
                State.state.layerUpdater?.ForceRefresh();
 | 
					                State.state.featurePipeline?.ForceRefresh();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -320,7 +315,7 @@ export class InitUiElements {
 | 
				
			||||||
            (layer) => layer.id
 | 
					            (layer) => layer.id
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        new LayerResetter(
 | 
					        new BackgroundLayerResetter(
 | 
				
			||||||
            State.state.backgroundLayer,
 | 
					            State.state.backgroundLayer,
 | 
				
			||||||
            State.state.locationControl,
 | 
					            State.state.locationControl,
 | 
				
			||||||
            State.state.availableBackgroundLayers,
 | 
					            State.state.availableBackgroundLayers,
 | 
				
			||||||
| 
						 | 
					@ -333,13 +328,14 @@ export class InitUiElements {
 | 
				
			||||||
            State.state.locationControl,
 | 
					            State.state.locationControl,
 | 
				
			||||||
            State.state.osmConnection.userDetails,
 | 
					            State.state.osmConnection.userDetails,
 | 
				
			||||||
            State.state.layoutToUse,
 | 
					            State.state.layoutToUse,
 | 
				
			||||||
            State.state.leafletMap
 | 
					            State.state.currentBounds
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        new Minimap({
 | 
					        Minimap.createMiniMap({
 | 
				
			||||||
            background: State.state.backgroundLayer,
 | 
					            background: State.state.backgroundLayer,
 | 
				
			||||||
            location: State.state.locationControl,
 | 
					            location: State.state.locationControl,
 | 
				
			||||||
            leafletMap: State.state.leafletMap,
 | 
					            leafletMap: State.state.leafletMap,
 | 
				
			||||||
 | 
					            bounds: State.state.currentBounds,
 | 
				
			||||||
            attribution: attr,
 | 
					            attribution: attr,
 | 
				
			||||||
            lastClickLocation: State.state.LastClickLocation
 | 
					            lastClickLocation: State.state.LastClickLocation
 | 
				
			||||||
        }).SetClass("w-full h-full")
 | 
					        }).SetClass("w-full h-full")
 | 
				
			||||||
| 
						 | 
					@ -371,7 +367,7 @@ export class InitUiElements {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static InitLayers(): FeatureSource {
 | 
					    private static InitLayers(): void {
 | 
				
			||||||
        const state = State.state;
 | 
					        const state = State.state;
 | 
				
			||||||
        state.filteredLayers = state.layoutToUse.map((layoutToUse) => {
 | 
					        state.filteredLayers = state.layoutToUse.map((layoutToUse) => {
 | 
				
			||||||
            const flayers = [];
 | 
					            const flayers = [];
 | 
				
			||||||
| 
						 | 
					@ -396,34 +392,19 @@ export class InitUiElements {
 | 
				
			||||||
            return flayers;
 | 
					            return flayers;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const updater = new LoadFromOverpass(
 | 
					        State.state.featurePipeline = new FeaturePipeline(
 | 
				
			||||||
            state.locationControl,
 | 
					            source => {
 | 
				
			||||||
            state.layoutToUse,
 | 
					 | 
				
			||||||
            state.leafletMap,
 | 
					 | 
				
			||||||
            state.overpassUrl,
 | 
					 | 
				
			||||||
            state.overpassTimeout,
 | 
					 | 
				
			||||||
            Constants.useOsmApiAt
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        State.state.layerUpdater = updater;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const source = new FeaturePipeline(
 | 
					 | 
				
			||||||
            state.filteredLayers,
 | 
					 | 
				
			||||||
            State.state.changes,
 | 
					 | 
				
			||||||
            updater,
 | 
					 | 
				
			||||||
            state.osmApiFeatureSource,
 | 
					 | 
				
			||||||
            state.layoutToUse,
 | 
					 | 
				
			||||||
            state.locationControl,
 | 
					 | 
				
			||||||
            state.selectedElement
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        State.state.featurePipeline = source;
 | 
					 | 
				
			||||||
                new ShowDataLayer(
 | 
					                new ShowDataLayer(
 | 
				
			||||||
            source.features,
 | 
					                    {
 | 
				
			||||||
            State.state.leafletMap,
 | 
					                        features: source,
 | 
				
			||||||
            State.state.layoutToUse
 | 
					                        leafletMap: State.state.leafletMap,
 | 
				
			||||||
 | 
					                        layerToShow: source.layer.layerDef
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }, state
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const selectedFeatureHandler = new SelectedFeatureHandler(
 | 
					        /*   const selectedFeatureHandler = new SelectedFeatureHandler(
 | 
				
			||||||
               Hash.hash,
 | 
					               Hash.hash,
 | 
				
			||||||
               State.state.selectedElement,
 | 
					               State.state.selectedElement,
 | 
				
			||||||
               source,
 | 
					               source,
 | 
				
			||||||
| 
						 | 
					@ -431,16 +412,15 @@ export class InitUiElements {
 | 
				
			||||||
           );
 | 
					           );
 | 
				
			||||||
           selectedFeatureHandler.zoomToSelectedFeature(
 | 
					           selectedFeatureHandler.zoomToSelectedFeature(
 | 
				
			||||||
               State.state.locationControl
 | 
					               State.state.locationControl
 | 
				
			||||||
        );
 | 
					           );*/
 | 
				
			||||||
        return source;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static setupAllLayerElements() {
 | 
					    private static setupAllLayerElements() {
 | 
				
			||||||
        // ------------- Setup the layers -------------------------------
 | 
					        // ------------- Setup the layers -------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const source = InitUiElements.InitLayers();
 | 
					        InitUiElements.InitLayers();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        new LeftControls(source).AttachTo("bottom-left");
 | 
					        new LeftControls(State.state).AttachTo("bottom-left");
 | 
				
			||||||
        new RightControls().AttachTo("bottom-right");
 | 
					        new RightControls().AttachTo("bottom-right");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // ------------------ Setup various other UI elements ------------
 | 
					        // ------------------ Setup various other UI elements ------------
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ import Loc from "../../Models/Loc";
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Sets the current background layer to a layer that is actually available
 | 
					 * Sets the current background layer to a layer that is actually available
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export default class LayerResetter {
 | 
					export default class BackgroundLayerResetter {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(currentBackgroundLayer: UIEventSource<BaseLayer>,
 | 
					    constructor(currentBackgroundLayer: UIEventSource<BaseLayer>,
 | 
				
			||||||
                location: UIEventSource<Loc>,
 | 
					                location: UIEventSource<Loc>,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,14 +3,15 @@ import Loc from "../../Models/Loc";
 | 
				
			||||||
import {Or} from "../Tags/Or";
 | 
					import {Or} from "../Tags/Or";
 | 
				
			||||||
import {Overpass} from "../Osm/Overpass";
 | 
					import {Overpass} from "../Osm/Overpass";
 | 
				
			||||||
import Bounds from "../../Models/Bounds";
 | 
					import Bounds from "../../Models/Bounds";
 | 
				
			||||||
import FeatureSource from "../FeatureSource/FeatureSource";
 | 
					import FeatureSource, {FeatureSourceState} from "../FeatureSource/FeatureSource";
 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					import {Utils} from "../../Utils";
 | 
				
			||||||
import {TagsFilter} from "../Tags/TagsFilter";
 | 
					import {TagsFilter} from "../Tags/TagsFilter";
 | 
				
			||||||
import SimpleMetaTagger from "../SimpleMetaTagger";
 | 
					import SimpleMetaTagger from "../SimpleMetaTagger";
 | 
				
			||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
 | 
					import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
 | 
				
			||||||
 | 
					import RelationsTracker from "../Osm/RelationsTracker";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class OverpassFeatureSource implements FeatureSource {
 | 
					export default class OverpassFeatureSource implements FeatureSource, FeatureSourceState {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public readonly name = "OverpassFeatureSource"
 | 
					    public readonly name = "OverpassFeatureSource"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,6 +25,9 @@ export default class OverpassFeatureSource implements FeatureSource {
 | 
				
			||||||
    public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false);
 | 
					    public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false);
 | 
				
			||||||
    public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0);
 | 
					    public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    public readonly relationsTracker: RelationsTracker;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private readonly retries: UIEventSource<number> = new UIEventSource<number>(0);
 | 
					    private readonly retries: UIEventSource<number> = new UIEventSource<number>(0);
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The previous bounds for which the query has been run at the given zoom level
 | 
					     * The previous bounds for which the query has been run at the given zoom level
 | 
				
			||||||
| 
						 | 
					@ -33,56 +37,61 @@ export default class OverpassFeatureSource implements FeatureSource {
 | 
				
			||||||
     * we start checking the bounds at the first zoom level the layer might operate. If in bounds - no reload needed, otherwise we continue walking down
 | 
					     * we start checking the bounds at the first zoom level the layer might operate. If in bounds - no reload needed, otherwise we continue walking down
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private readonly _previousBounds: Map<number, Bounds[]> = new Map<number, Bounds[]>();
 | 
					    private readonly _previousBounds: Map<number, Bounds[]> = new Map<number, Bounds[]>();
 | 
				
			||||||
    private readonly _location: UIEventSource<Loc>;
 | 
					    private readonly state: {
 | 
				
			||||||
    private readonly _layoutToUse: UIEventSource<LayoutConfig>;
 | 
					        readonly locationControl: UIEventSource<Loc>,
 | 
				
			||||||
    private readonly _leafletMap: UIEventSource<L.Map>;
 | 
					        readonly layoutToUse: UIEventSource<LayoutConfig>,
 | 
				
			||||||
    private readonly _interpreterUrl: UIEventSource<string>;
 | 
					        readonly leafletMap: any,
 | 
				
			||||||
    private readonly _timeout: UIEventSource<number>;
 | 
					        readonly overpassUrl: UIEventSource<string>;
 | 
				
			||||||
 | 
					        readonly overpassTimeout: UIEventSource<number>;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The most important layer should go first, as that one gets first pick for the questions
 | 
					     * The most important layer should go first, as that one gets first pick for the questions
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(
 | 
					    constructor(
 | 
				
			||||||
        location: UIEventSource<Loc>,
 | 
					        state: {
 | 
				
			||||||
        layoutToUse: UIEventSource<LayoutConfig>,
 | 
					            readonly locationControl: UIEventSource<Loc>,
 | 
				
			||||||
        leafletMap: UIEventSource<L.Map>,
 | 
					            readonly layoutToUse: UIEventSource<LayoutConfig>,
 | 
				
			||||||
        interpreterUrl: UIEventSource<string>,
 | 
					            readonly leafletMap: any,
 | 
				
			||||||
        timeout: UIEventSource<number>,
 | 
					            readonly overpassUrl: UIEventSource<string>;
 | 
				
			||||||
        maxZoom = undefined) {
 | 
					            readonly overpassTimeout: UIEventSource<number>;
 | 
				
			||||||
        this._location = location;
 | 
					            readonly overpassMaxZoom: UIEventSource<number>
 | 
				
			||||||
        this._layoutToUse = layoutToUse;
 | 
					        }) {
 | 
				
			||||||
        this._leafletMap = leafletMap;
 | 
					
 | 
				
			||||||
        this._interpreterUrl = interpreterUrl;
 | 
					
 | 
				
			||||||
        this._timeout = timeout;
 | 
					        this.state = state
 | 
				
			||||||
 | 
					        this.relationsTracker = new RelationsTracker()
 | 
				
			||||||
 | 
					        const location = state.locationControl
 | 
				
			||||||
        const self = this;
 | 
					        const self = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.sufficientlyZoomed = location.map(location => {
 | 
					        this.sufficientlyZoomed = location.map(location => {
 | 
				
			||||||
                if (location?.zoom === undefined) {
 | 
					                if (location?.zoom === undefined) {
 | 
				
			||||||
                    return false;
 | 
					                    return false;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                let minzoom = Math.min(...layoutToUse.data.layers.map(layer => layer.minzoom ?? 18));
 | 
					                let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom ?? 18));
 | 
				
			||||||
                if(location.zoom < minzoom){
 | 
					                if (location.zoom < minzoom) {
 | 
				
			||||||
                    return false;
 | 
					                    return false;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                if(maxZoom !== undefined && location.zoom > maxZoom){
 | 
					                const maxZoom = state.overpassMaxZoom.data
 | 
				
			||||||
 | 
					                if (maxZoom !== undefined && location.zoom > maxZoom) {
 | 
				
			||||||
                    return false;
 | 
					                    return false;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return true;
 | 
					                return true;
 | 
				
			||||||
            }, [layoutToUse]
 | 
					            }, [state.layoutToUse]
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        for (let i = 0; i < 25; i++) {
 | 
					        for (let i = 0; i < 25; i++) {
 | 
				
			||||||
            // This update removes all data on all layers -> erase the map on lower levels too
 | 
					            // This update removes all data on all layers -> erase the map on lower levels too
 | 
				
			||||||
            this._previousBounds.set(i, []);
 | 
					            this._previousBounds.set(i, []);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        layoutToUse.addCallback(() => {
 | 
					        state.layoutToUse.addCallback(() => {
 | 
				
			||||||
            self.update()
 | 
					            self.update()
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        location.addCallback(() => {
 | 
					        location.addCallback(() => {
 | 
				
			||||||
            self.update()
 | 
					            self.update()
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        leafletMap.addCallbackAndRunD(_ => {
 | 
					        state.leafletMap.addCallbackAndRunD(_ => {
 | 
				
			||||||
            self.update();
 | 
					            self.update();
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -97,11 +106,11 @@ export default class OverpassFeatureSource implements FeatureSource {
 | 
				
			||||||
    private GetFilter(): Overpass {
 | 
					    private GetFilter(): Overpass {
 | 
				
			||||||
        let filters: TagsFilter[] = [];
 | 
					        let filters: TagsFilter[] = [];
 | 
				
			||||||
        let extraScripts: string[] = [];
 | 
					        let extraScripts: string[] = [];
 | 
				
			||||||
        for (const layer of this._layoutToUse.data.layers) {
 | 
					        for (const layer of this.state.layoutToUse.data.layers) {
 | 
				
			||||||
            if (typeof (layer) === "string") {
 | 
					            if (typeof (layer) === "string") {
 | 
				
			||||||
                throw "A layer was not expanded!"
 | 
					                throw "A layer was not expanded!"
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if (this._location.data.zoom < layer.minzoom) {
 | 
					            if (this.state.locationControl.data.zoom < layer.minzoom) {
 | 
				
			||||||
                continue;
 | 
					                continue;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if (layer.doNotDownload) {
 | 
					            if (layer.doNotDownload) {
 | 
				
			||||||
| 
						 | 
					@ -141,7 +150,7 @@ export default class OverpassFeatureSource implements FeatureSource {
 | 
				
			||||||
        if (filters.length + extraScripts.length === 0) {
 | 
					        if (filters.length + extraScripts.length === 0) {
 | 
				
			||||||
            return undefined;
 | 
					            return undefined;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return new Overpass(new Or(filters), extraScripts, this._interpreterUrl, this._timeout);
 | 
					        return new Overpass(new Or(filters), extraScripts, this.state.overpassUrl, this.state.overpassTimeout, this.relationsTracker);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private update(): void {
 | 
					    private update(): void {
 | 
				
			||||||
| 
						 | 
					@ -155,21 +164,22 @@ export default class OverpassFeatureSource implements FeatureSource {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const bounds = this._leafletMap.data?.getBounds()?.pad( this._layoutToUse.data.widenFactor);
 | 
					        const bounds = this.state.leafletMap.data?.getBounds()?.pad(this.state.layoutToUse.data.widenFactor);
 | 
				
			||||||
        if (bounds === undefined) {
 | 
					        if (bounds === undefined) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const n = Math.min(90, bounds.getNorth() );
 | 
					        const n = Math.min(90, bounds.getNorth());
 | 
				
			||||||
        const e = Math.min(180, bounds.getEast() );
 | 
					        const e = Math.min(180, bounds.getEast());
 | 
				
			||||||
        const s = Math.max(-90, bounds.getSouth());
 | 
					        const s = Math.max(-90, bounds.getSouth());
 | 
				
			||||||
        const w = Math.max(-180, bounds.getWest());
 | 
					        const w = Math.max(-180, bounds.getWest());
 | 
				
			||||||
        const queryBounds = {north: n, east: e, south: s, west: w};
 | 
					        const queryBounds = {north: n, east: e, south: s, west: w};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const z = Math.floor(this._location.data.zoom ?? 0);
 | 
					        const z = Math.floor(this.state.locationControl.data.zoom ?? 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const self = this;
 | 
					        const self = this;
 | 
				
			||||||
        const overpass = this.GetFilter();
 | 
					        const overpass = this.GetFilter();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        if (overpass === undefined) {
 | 
					        if (overpass === undefined) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -181,14 +191,18 @@ export default class OverpassFeatureSource implements FeatureSource {
 | 
				
			||||||
                const features = data.features.map(f => ({feature: f, freshness: date}));
 | 
					                const features = data.features.map(f => ({feature: f, freshness: date}));
 | 
				
			||||||
                SimpleMetaTagger.objectMetaInfo.addMetaTags(features)
 | 
					                SimpleMetaTagger.objectMetaInfo.addMetaTags(features)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                try{
 | 
				
			||||||
                    self.features.setData(features);
 | 
					                    self.features.setData(features);
 | 
				
			||||||
 | 
					                }catch(e){
 | 
				
			||||||
 | 
					                    console.error("Got the overpass response, but could not process it: ", e, e.stack)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
                self.runningQuery.setData(false);
 | 
					                self.runningQuery.setData(false);
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            function (reason) {
 | 
					            function (reason) {
 | 
				
			||||||
                self.retries.data++;
 | 
					                self.retries.data++;
 | 
				
			||||||
                self.ForceRefresh();
 | 
					                self.ForceRefresh();
 | 
				
			||||||
                self.timeout.setData(self.retries.data * 5);
 | 
					                self.timeout.setData(self.retries.data * 5);
 | 
				
			||||||
                console.log(`QUERY FAILED (retrying in ${5 * self.retries.data} sec) due to ${reason}`);
 | 
					                console.error(`QUERY FAILED (retrying in ${5 * self.retries.data} sec) due to ${reason}`);
 | 
				
			||||||
                self.retries.ping();
 | 
					                self.retries.ping();
 | 
				
			||||||
                self.runningQuery.setData(false);
 | 
					                self.runningQuery.setData(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -222,7 +236,7 @@ export default class OverpassFeatureSource implements FeatureSource {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const b = this._leafletMap.data.getBounds();
 | 
					        const b = this.state.leafletMap.data.getBounds();
 | 
				
			||||||
        return b.getSouth() >= bounds.south &&
 | 
					        return b.getSouth() >= bounds.south &&
 | 
				
			||||||
            b.getNorth() <= bounds.north &&
 | 
					            b.getNorth() <= bounds.north &&
 | 
				
			||||||
            b.getEast() <= bounds.east &&
 | 
					            b.getEast() <= bounds.east &&
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@ import FeatureSource from "../FeatureSource/FeatureSource";
 | 
				
			||||||
import {OsmObject} from "../Osm/OsmObject";
 | 
					import {OsmObject} from "../Osm/OsmObject";
 | 
				
			||||||
import Loc from "../../Models/Loc";
 | 
					import Loc from "../../Models/Loc";
 | 
				
			||||||
import FeaturePipeline from "../FeatureSource/FeaturePipeline";
 | 
					import FeaturePipeline from "../FeatureSource/FeaturePipeline";
 | 
				
			||||||
import OsmApiFeatureSource from "../FeatureSource/OsmApiFeatureSource";
 | 
					import OsmApiFeatureSource from "../FeatureSource/Sources/OsmApiFeatureSource";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Makes sure the hash shows the selected element and vice-versa.
 | 
					 * Makes sure the hash shows the selected element and vice-versa.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,21 +1,49 @@
 | 
				
			||||||
/// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions
 | 
					/// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions
 | 
				
			||||||
import FeatureSource from "./FeatureSource/FeatureSource";
 | 
					import FeatureSource from "./FeatureSource/FeatureSource";
 | 
				
			||||||
import {UIEventSource} from "./UIEventSource";
 | 
					import {UIEventSource} from "./UIEventSource";
 | 
				
			||||||
 | 
					import FeaturePipeline from "./FeatureSource/FeaturePipeline";
 | 
				
			||||||
 | 
					import Loc from "../Models/Loc";
 | 
				
			||||||
 | 
					import State from "../State";
 | 
				
			||||||
 | 
					import {BBox} from "./GeoOperations";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class ContributorCount {
 | 
					export default class ContributorCount {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public readonly Contributors: UIEventSource<Map<string, number>>;
 | 
					    public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>());
 | 
				
			||||||
 | 
					    private readonly state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(featureSource: FeatureSource) {
 | 
					    constructor(state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> }) {
 | 
				
			||||||
        this.Contributors = featureSource.features.map(features => {
 | 
					        this.state = state;
 | 
				
			||||||
 | 
					        const self = this;
 | 
				
			||||||
 | 
					        state.currentBounds.map(bbox => {
 | 
				
			||||||
 | 
					            self.update(bbox)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        state.featurePipeline.runningQuery.addCallbackAndRun(
 | 
				
			||||||
 | 
					            _ =>   self.update(state.currentBounds.data)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private lastUpdate: Date = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private update(bbox: BBox) {
 | 
				
			||||||
 | 
					        if(bbox === undefined){
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const now = new Date();
 | 
				
			||||||
 | 
					        if (this.lastUpdate !== undefined && ((now.getTime() - this.lastUpdate.getTime()) < 1000 * 60)) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        console.log("Calculating contributors")
 | 
				
			||||||
 | 
					        const featuresList = this.state.featurePipeline.GetAllFeaturesWithin(bbox)
 | 
				
			||||||
        const hist = new Map<string, number>();
 | 
					        const hist = new Map<string, number>();
 | 
				
			||||||
            for (const feature of features) {
 | 
					        for (const list of featuresList) {
 | 
				
			||||||
                const contributor = feature.feature.properties["_last_edit:contributor"]
 | 
					            for (const feature of list) {
 | 
				
			||||||
 | 
					                const contributor = feature.properties["_last_edit:contributor"]
 | 
				
			||||||
                const count = hist.get(contributor) ?? 0;
 | 
					                const count = hist.get(contributor) ?? 0;
 | 
				
			||||||
                hist.set(contributor, count + 1)
 | 
					                hist.set(contributor, count + 1)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return hist;
 | 
					        }
 | 
				
			||||||
        })
 | 
					        this.Contributors.setData(hist)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,24 @@
 | 
				
			||||||
import {GeoOperations} from "./GeoOperations";
 | 
					import {BBox, GeoOperations} from "./GeoOperations";
 | 
				
			||||||
import Combine from "../UI/Base/Combine";
 | 
					import Combine from "../UI/Base/Combine";
 | 
				
			||||||
import {Relation} from "./Osm/ExtractRelations";
 | 
					import RelationsTracker from "./Osm/RelationsTracker";
 | 
				
			||||||
import State from "../State";
 | 
					import State from "../State";
 | 
				
			||||||
import {Utils} from "../Utils";
 | 
					 | 
				
			||||||
import BaseUIElement from "../UI/BaseUIElement";
 | 
					import BaseUIElement from "../UI/BaseUIElement";
 | 
				
			||||||
import List from "../UI/Base/List";
 | 
					import List from "../UI/Base/List";
 | 
				
			||||||
import Title from "../UI/Base/Title";
 | 
					import Title from "../UI/Base/Title";
 | 
				
			||||||
import {UIEventSourceTools} from "./UIEventSource";
 | 
					import {UIEventSourceTools} from "./UIEventSource";
 | 
				
			||||||
import AspectedRouting from "./Osm/aspectedRouting";
 | 
					import AspectedRouting from "./Osm/aspectedRouting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ExtraFuncParams {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Gets all the features from the given layer within the given BBOX.
 | 
				
			||||||
 | 
					     * Note that more features then requested can be given back.
 | 
				
			||||||
 | 
					     * Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...]
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    getFeaturesWithin: (layerId: string, bbox: BBox) => any[][],
 | 
				
			||||||
 | 
					    memberships: RelationsTracker
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ExtraFunction {
 | 
					export class ExtraFunction {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,16 +65,21 @@ export class ExtraFunction {
 | 
				
			||||||
        (params, feat) => {
 | 
					        (params, feat) => {
 | 
				
			||||||
            return (...layerIds: string[]) => {
 | 
					            return (...layerIds: string[]) => {
 | 
				
			||||||
                const result = []
 | 
					                const result = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const bbox = BBox.get(feat)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                for (const layerId of layerIds) {
 | 
					                for (const layerId of layerIds) {
 | 
				
			||||||
                    const otherLayer = params.featuresPerLayer.get(layerId);
 | 
					                    const otherLayers = params.getFeaturesWithin(layerId, bbox)
 | 
				
			||||||
                    if (otherLayer === undefined) {
 | 
					                    if (otherLayers === undefined) {
 | 
				
			||||||
                        continue;
 | 
					                        continue;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    if (otherLayer.length === 0) {
 | 
					                    if (otherLayers.length === 0) {
 | 
				
			||||||
                        continue;
 | 
					                        continue;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                    for (const otherLayer of otherLayers) {
 | 
				
			||||||
                        result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
 | 
					                        result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
                return result;
 | 
					                return result;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -77,6 +92,9 @@ export class ExtraFunction {
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        (featuresPerLayer, feature) => {
 | 
					        (featuresPerLayer, feature) => {
 | 
				
			||||||
            return (arg0, lat) => {
 | 
					            return (arg0, lat) => {
 | 
				
			||||||
 | 
					                if (arg0 === undefined) {
 | 
				
			||||||
 | 
					                    return undefined;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
                if (typeof arg0 === "number") {
 | 
					                if (typeof arg0 === "number") {
 | 
				
			||||||
                    // Feature._lon and ._lat is conveniently place by one of the other metatags
 | 
					                    // Feature._lon and ._lat is conveniently place by one of the other metatags
 | 
				
			||||||
                    return GeoOperations.distanceBetween([arg0, lat], [feature._lon, feature._lat]);
 | 
					                    return GeoOperations.distanceBetween([arg0, lat], [feature._lon, feature._lat]);
 | 
				
			||||||
| 
						 | 
					@ -103,7 +121,7 @@ export class ExtraFunction {
 | 
				
			||||||
            args: ["list of features"]
 | 
					            args: ["list of features"]
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        (params, feature) => {
 | 
					        (params, feature) => {
 | 
				
			||||||
            return (features) => ExtraFunction.GetClosestNFeatures(params, feature, features)[0].feat
 | 
					            return (features) => ExtraFunction.GetClosestNFeatures(params, feature, features)?.[0]?.feat
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -113,12 +131,13 @@ export class ExtraFunction {
 | 
				
			||||||
            doc: "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. " +
 | 
					            doc: "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. " +
 | 
				
			||||||
                "Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet laoded)\n\n" +
 | 
					                "Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet laoded)\n\n" +
 | 
				
			||||||
                "If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)",
 | 
					                "If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)",
 | 
				
			||||||
            args: ["list of features", "amount of features", "unique tag key (optional)"]
 | 
					            args: ["list of features", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"]
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        (params, feature) => {
 | 
					        (params, feature) => {
 | 
				
			||||||
            return (features, amount, uniqueTag) => ExtraFunction.GetClosestNFeatures(params, feature, features, {
 | 
					            return (features, amount, uniqueTag, maxDistanceInMeters) => ExtraFunction.GetClosestNFeatures(params, feature, features, {
 | 
				
			||||||
                maxFeatures: Number(amount),
 | 
					                maxFeatures: Number(amount),
 | 
				
			||||||
                uniqueTag: uniqueTag
 | 
					                uniqueTag: uniqueTag,
 | 
				
			||||||
 | 
					                maxDistance: Number(maxDistanceInMeters)
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
| 
						 | 
					@ -131,8 +150,10 @@ export class ExtraFunction {
 | 
				
			||||||
                "For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`",
 | 
					                "For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`",
 | 
				
			||||||
            args: []
 | 
					            args: []
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        (params, _) => {
 | 
					        (params, feat) => {
 | 
				
			||||||
            return () => params.relations ?? [];
 | 
					            return () =>
 | 
				
			||||||
 | 
					                params.memberships.knownRelations.data.get(feat.properties.id) ?? []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    private static readonly AspectedRouting = new ExtraFunction(
 | 
					    private static readonly AspectedRouting = new ExtraFunction(
 | 
				
			||||||
| 
						 | 
					@ -165,19 +186,19 @@ export class ExtraFunction {
 | 
				
			||||||
    private readonly _name: string;
 | 
					    private readonly _name: string;
 | 
				
			||||||
    private readonly _args: string[];
 | 
					    private readonly _args: string[];
 | 
				
			||||||
    private readonly _doc: string;
 | 
					    private readonly _doc: string;
 | 
				
			||||||
    private readonly _f: (params: { featuresPerLayer: Map<string, any[]>, relations: { role: string, relation: Relation }[] }, feat: any) => any;
 | 
					    private readonly _f: (params: ExtraFuncParams, feat: any) => any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(options: { name: string, doc: string, args: string[] },
 | 
					    constructor(options: { name: string, doc: string, args: string[] },
 | 
				
			||||||
                f: ((params: { featuresPerLayer: Map<string, any[]>, relations: { role: string, relation: Relation }[] }, feat: any) => any)) {
 | 
					                f: ((params: ExtraFuncParams, feat: any) => any)) {
 | 
				
			||||||
        this._name = options.name;
 | 
					        this._name = options.name;
 | 
				
			||||||
        this._doc = options.doc;
 | 
					        this._doc = options.doc;
 | 
				
			||||||
        this._args = options.args;
 | 
					        this._args = options.args;
 | 
				
			||||||
        this._f = f;
 | 
					        this._f = f;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static FullPatchFeature(featuresPerLayer: Map<string, any[]>, relations: { role: string, relation: Relation }[], feature) {
 | 
					    public static FullPatchFeature(params: ExtraFuncParams, feature) {
 | 
				
			||||||
        for (const func of ExtraFunction.allFuncs) {
 | 
					        for (const func of ExtraFunction.allFuncs) {
 | 
				
			||||||
            func.PatchFeature(featuresPerLayer, relations, feature);
 | 
					            func.PatchFeature(params, feature);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -198,29 +219,36 @@ export class ExtraFunction {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Gets the closes N features, sorted by ascending distance
 | 
					     * Gets the closes N features, sorted by ascending distance.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param params: The link to mapcomplete state
 | 
				
			||||||
 | 
					     * @param feature: The central feature under consideration
 | 
				
			||||||
 | 
					     * @param features: The other features
 | 
				
			||||||
 | 
					     * @param options: maxFeatures: The maximum amount of features to be returned. Default: 1; uniqueTag: returned features are not allowed to have the same value for this key; maxDistance: stop searching if it is too far away (in meter). Default: 500m
 | 
				
			||||||
 | 
					     * @constructor
 | 
				
			||||||
 | 
					     * @private
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private static GetClosestNFeatures(params, feature, features, options?: { maxFeatures?: number, uniqueTag?: string | undefined }): { feat: any, distance: number }[] {
 | 
					    private static GetClosestNFeatures(params: ExtraFuncParams,
 | 
				
			||||||
 | 
					                                       feature: any,
 | 
				
			||||||
 | 
					                                       features: string | any[],
 | 
				
			||||||
 | 
					                                       options?: { maxFeatures?: number, uniqueTag?: string | undefined, maxDistance?: number }): { feat: any, distance: number }[] {
 | 
				
			||||||
        const maxFeatures = options?.maxFeatures ?? 1
 | 
					        const maxFeatures = options?.maxFeatures ?? 1
 | 
				
			||||||
        const uniqueTag : string | undefined = options?.uniqueTag
 | 
					        const maxDistance = options?.maxDistance ?? 500
 | 
				
			||||||
 | 
					        const uniqueTag: string | undefined = options?.uniqueTag
 | 
				
			||||||
        if (typeof features === "string") {
 | 
					        if (typeof features === "string") {
 | 
				
			||||||
            const name = features
 | 
					            const name = features
 | 
				
			||||||
            features = params.featuresPerLayer.get(features)
 | 
					            const bbox = GeoOperations.bbox(GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance))
 | 
				
			||||||
 | 
					            features = params.getFeaturesWithin(name, new BBox(bbox.geometry.coordinates))
 | 
				
			||||||
 | 
					        }else{
 | 
				
			||||||
 | 
					            features = [features]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        if (features === undefined) {
 | 
					        if (features === undefined) {
 | 
				
			||||||
                var keys = Utils.NoNull(Array.from(params.featuresPerLayer.keys()));
 | 
					            return;
 | 
				
			||||||
                if (keys.length > 0) {
 | 
					 | 
				
			||||||
                    throw `No features defined for ${name}. Defined layers are ${keys.join(", ")}`;
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    // This is the first pass over an external dataset
 | 
					 | 
				
			||||||
                    // Other data probably still has to load!
 | 
					 | 
				
			||||||
                    return undefined;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let closestFeatures: { feat: any, distance: number }[] = [];
 | 
					        let closestFeatures: { feat: any, distance: number }[] = [];
 | 
				
			||||||
        for (const otherFeature of features) {
 | 
					        for(const featureList of features) {
 | 
				
			||||||
 | 
					            for (const otherFeature of featureList) {
 | 
				
			||||||
                if (otherFeature == feature || otherFeature.id == feature.id) {
 | 
					                if (otherFeature == feature || otherFeature.id == feature.id) {
 | 
				
			||||||
                    continue; // We ignore self
 | 
					                    continue; // We ignore self
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
| 
						 | 
					@ -236,6 +264,9 @@ export class ExtraFunction {
 | 
				
			||||||
                if (distance === undefined) {
 | 
					                if (distance === undefined) {
 | 
				
			||||||
                    throw "Undefined distance!"
 | 
					                    throw "Undefined distance!"
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                if (distance > maxDistance) {
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (closestFeatures.length === 0) {
 | 
					                if (closestFeatures.length === 0) {
 | 
				
			||||||
                    closestFeatures.push({
 | 
					                    closestFeatures.push({
 | 
				
			||||||
| 
						 | 
					@ -278,7 +309,7 @@ export class ExtraFunction {
 | 
				
			||||||
                            const uniqueValue = otherFeature.properties[uniqueTag]
 | 
					                            const uniqueValue = otherFeature.properties[uniqueTag]
 | 
				
			||||||
                            // We might still have some other values later one with the same uniquetag that have to be cleaned
 | 
					                            // We might still have some other values later one with the same uniquetag that have to be cleaned
 | 
				
			||||||
                            for (let j = i; j < closestFeatures.length; j++) {
 | 
					                            for (let j = i; j < closestFeatures.length; j++) {
 | 
				
			||||||
                                if(closestFeatures[j].feat.properties[uniqueTag] === uniqueValue){
 | 
					                                if (closestFeatures[j].feat.properties[uniqueTag] === uniqueValue) {
 | 
				
			||||||
                                    closestFeatures.splice(j, 1)
 | 
					                                    closestFeatures.splice(j, 1)
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
| 
						 | 
					@ -309,10 +340,11 @@ export class ExtraFunction {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        return closestFeatures;
 | 
					        return closestFeatures;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public PatchFeature(featuresPerLayer: Map<string, any[]>, relations: { role: string, relation: Relation }[], feature: any) {
 | 
					    public PatchFeature(params: ExtraFuncParams, feature: any) {
 | 
				
			||||||
        feature[this._name] = this._f({featuresPerLayer: featuresPerLayer, relations: relations}, feature)
 | 
					        feature[this._name] = this._f(params, feature)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,11 @@
 | 
				
			||||||
import {FeatureSourceForLayer} from "./FeatureSource";
 | 
					 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/***
 | 
					/***
 | 
				
			||||||
 * Saves all the features that are passed in to localstorage, so they can be retrieved on the next run
 | 
					 * Saves all the features that are passed in to localstorage, so they can be retrieved on the next run
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * Technically, more an Actor then a featuresource, but it fits more neatly this ay
 | 
					 * Technically, more an Actor then a featuresource, but it fits more neatly this ay
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					import {FeatureSourceForLayer} from "../FeatureSource";
 | 
				
			||||||
 | 
					import {Utils} from "../../../Utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class LocalStorageSaverActor {
 | 
					export default class LocalStorageSaverActor {
 | 
				
			||||||
    public static readonly storageKey: string = "cached-features";
 | 
					    public static readonly storageKey: string = "cached-features";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,6 @@ export default class LocalStorageSaverActor {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                localStorage.setItem(key, JSON.stringify(features));
 | 
					                localStorage.setItem(key, JSON.stringify(features));
 | 
				
			||||||
                console.log("Saved ", features.length, "elements to", key)
 | 
					 | 
				
			||||||
                localStorage.setItem(key + "-time", JSON.stringify(now))
 | 
					                localStorage.setItem(key + "-time", JSON.stringify(now))
 | 
				
			||||||
            } catch (e) {
 | 
					            } catch (e) {
 | 
				
			||||||
                console.warn("Could not save the features to local storage:", e)
 | 
					                console.warn("Could not save the features to local storage:", e)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
import FeatureSource from "./FeatureSource";
 | 
					import FeatureSource from "../FeatureSource";
 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					import {UIEventSource} from "../../UIEventSource";
 | 
				
			||||||
import State from "../../State";
 | 
					import State from "../../../State";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class RegisteringAllFromFeatureSourceActor {
 | 
					export default class RegisteringAllFromFeatureSourceActor {
 | 
				
			||||||
    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
 | 
					    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,35 @@
 | 
				
			||||||
import FeatureSource from "./FeatureSource";
 | 
					import FeatureSource, {IndexedFeatureSource} from "./FeatureSource";
 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					import {UIEventSource} from "../UIEventSource";
 | 
				
			||||||
import {Changes} from "../Osm/Changes";
 | 
					import {Changes} from "../Osm/Changes";
 | 
				
			||||||
import {ChangeDescription} from "../Osm/Actions/ChangeDescription";
 | 
					import {ChangeDescription} from "../Osm/Actions/ChangeDescription";
 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					import {Utils} from "../../Utils";
 | 
				
			||||||
import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject";
 | 
					import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * A feature source containing exclusively new elements
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class NewGeometryChangeApplicatorFeatureSource implements FeatureSource{
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
 | 
				
			||||||
 | 
					    public readonly name: string = "newFeatures";
 | 
				
			||||||
 | 
					    constructor(changes: Changes) {
 | 
				
			||||||
 | 
					        const seenChanges = new Set<ChangeDescription>();
 | 
				
			||||||
 | 
					        changes.pendingChanges.addCallbackAndRunD(changes => {
 | 
				
			||||||
 | 
					            for (const change of changes) {
 | 
				
			||||||
 | 
					                if(seenChanges.has(change)){
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                seenChanges.add(change)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if(change.id < 0){
 | 
				
			||||||
 | 
					                    // This is a new object!
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Applies changes from 'Changes' onto a featureSource
 | 
					 * Applies changes from 'Changes' onto a featureSource
 | 
				
			||||||
| 
						 | 
					@ -12,10 +37,18 @@ import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject";
 | 
				
			||||||
export default class ChangeApplicator implements FeatureSource {
 | 
					export default class ChangeApplicator implements FeatureSource {
 | 
				
			||||||
    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
 | 
					    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
 | 
				
			||||||
    public readonly name: string;
 | 
					    public readonly name: string;
 | 
				
			||||||
 | 
					    private readonly source: IndexedFeatureSource;
 | 
				
			||||||
 | 
					    private readonly changes: Changes;
 | 
				
			||||||
 | 
					    private readonly mode?: {
 | 
				
			||||||
 | 
					        generateNewGeometries: boolean
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(source: FeatureSource, changes: Changes, mode?: {
 | 
					    constructor(source: IndexedFeatureSource, changes: Changes, mode?: {
 | 
				
			||||||
        generateNewGeometries: boolean
 | 
					        generateNewGeometries: boolean
 | 
				
			||||||
    }) {
 | 
					    }) {
 | 
				
			||||||
 | 
					        this.source = source;
 | 
				
			||||||
 | 
					        this.changes = changes;
 | 
				
			||||||
 | 
					        this.mode = mode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.name = "ChangesApplied(" + source.name + ")"
 | 
					        this.name = "ChangesApplied(" + source.name + ")"
 | 
				
			||||||
        this.features = source.features
 | 
					        this.features = source.features
 | 
				
			||||||
| 
						 | 
					@ -26,7 +59,7 @@ export default class ChangeApplicator implements FeatureSource {
 | 
				
			||||||
            if (runningUpdate) {
 | 
					            if (runningUpdate) {
 | 
				
			||||||
                return; // No need to ping again
 | 
					                return; // No need to ping again
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ChangeApplicator.ApplyChanges(features, changes.pendingChanges.data, mode)
 | 
					            self.ApplyChanges()
 | 
				
			||||||
            seenChanges.clear()
 | 
					            seenChanges.clear()
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,19 +67,20 @@ export default class ChangeApplicator implements FeatureSource {
 | 
				
			||||||
            runningUpdate = true;
 | 
					            runningUpdate = true;
 | 
				
			||||||
            changes = changes.filter(ch => !seenChanges.has(ch))
 | 
					            changes = changes.filter(ch => !seenChanges.has(ch))
 | 
				
			||||||
            changes.forEach(c => seenChanges.add(c))
 | 
					            changes.forEach(c => seenChanges.add(c))
 | 
				
			||||||
            ChangeApplicator.ApplyChanges(self.features.data, changes, mode)
 | 
					            self.ApplyChanges()
 | 
				
			||||||
            source.features.ping()
 | 
					            source.features.ping()
 | 
				
			||||||
            runningUpdate = false;
 | 
					            runningUpdate = false;
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Returns true if the geometry is changed and the source should be pinged
 | 
					     * Returns true if the geometry is changed and the source should be pinged
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private static ApplyChanges(features: { feature: any; freshness: Date }[], cs: ChangeDescription[], mode: { generateNewGeometries: boolean }): boolean {
 | 
					    private ApplyChanges(): boolean {
 | 
				
			||||||
 | 
					        const cs = this.changes.pendingChanges.data
 | 
				
			||||||
 | 
					        const features = this.source.features.data
 | 
				
			||||||
 | 
					        const loadedIds = this.source.containedIds
 | 
				
			||||||
        if (cs.length === 0 || features === undefined) {
 | 
					        if (cs.length === 0 || features === undefined) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -56,12 +90,18 @@ export default class ChangeApplicator implements FeatureSource {
 | 
				
			||||||
        const changesPerId: Map<string, ChangeDescription[]> = new Map<string, ChangeDescription[]>()
 | 
					        const changesPerId: Map<string, ChangeDescription[]> = new Map<string, ChangeDescription[]>()
 | 
				
			||||||
        for (const c of cs) {
 | 
					        for (const c of cs) {
 | 
				
			||||||
            const id = c.type + "/" + c.id
 | 
					            const id = c.type + "/" + c.id
 | 
				
			||||||
 | 
					            if (!loadedIds.has(id)) {
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            if (!changesPerId.has(id)) {
 | 
					            if (!changesPerId.has(id)) {
 | 
				
			||||||
                changesPerId.set(id, [])
 | 
					                changesPerId.set(id, [])
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            changesPerId.get(id).push(c)
 | 
					            changesPerId.get(id).push(c)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        if (changesPerId.size === 0) {
 | 
				
			||||||
 | 
					            // The current feature source set doesn't contain any changed feature, so we can safely skip
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const now = new Date()
 | 
					        const now = new Date()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -77,7 +117,7 @@ export default class ChangeApplicator implements FeatureSource {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // First, create the new features - they have a negative ID
 | 
					        // First, create the new features - they have a negative ID
 | 
				
			||||||
        // We don't set the properties yet though
 | 
					        // We don't set the properties yet though
 | 
				
			||||||
        if (mode?.generateNewGeometries) {
 | 
					        if (this.mode?.generateNewGeometries) {
 | 
				
			||||||
            changesPerId.forEach(cs => {
 | 
					            changesPerId.forEach(cs => {
 | 
				
			||||||
                cs
 | 
					                cs
 | 
				
			||||||
                    .forEach(change => {
 | 
					                    .forEach(change => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,95 +1,191 @@
 | 
				
			||||||
import FilteringFeatureSource from "../FeatureSource/FilteringFeatureSource";
 | 
					 | 
				
			||||||
import FeatureSourceMerger from "../FeatureSource/FeatureSourceMerger";
 | 
					 | 
				
			||||||
import RememberingSource from "../FeatureSource/RememberingSource";
 | 
					 | 
				
			||||||
import WayHandlingApplyingFeatureSource from "../FeatureSource/WayHandlingApplyingFeatureSource";
 | 
					 | 
				
			||||||
import FeatureDuplicatorPerLayer from "../FeatureSource/FeatureDuplicatorPerLayer";
 | 
					 | 
				
			||||||
import FeatureSource from "../FeatureSource/FeatureSource";
 | 
					 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					 | 
				
			||||||
import LocalStorageSaver from "./LocalStorageSaver";
 | 
					 | 
				
			||||||
import LocalStorageSource from "./LocalStorageSource";
 | 
					 | 
				
			||||||
import Loc from "../../Models/Loc";
 | 
					 | 
				
			||||||
import GeoJsonSource from "./GeoJsonSource";
 | 
					 | 
				
			||||||
import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource";
 | 
					 | 
				
			||||||
import RegisteringFeatureSource from "./RegisteringFeatureSource";
 | 
					 | 
				
			||||||
import FilteredLayer from "../../Models/FilteredLayer";
 | 
					 | 
				
			||||||
import {Changes} from "../Osm/Changes";
 | 
					 | 
				
			||||||
import ChangeApplicator from "./ChangeApplicator";
 | 
					 | 
				
			||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
 | 
					import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
 | 
				
			||||||
 | 
					import FilteringFeatureSource from "./Sources/FilteringFeatureSource";
 | 
				
			||||||
 | 
					import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter";
 | 
				
			||||||
 | 
					import FeatureSource, {FeatureSourceForLayer, FeatureSourceState, Tiled} from "./FeatureSource";
 | 
				
			||||||
 | 
					import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource";
 | 
				
			||||||
 | 
					import {UIEventSource} from "../UIEventSource";
 | 
				
			||||||
 | 
					import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy";
 | 
				
			||||||
 | 
					import FilteredLayer from "../../Models/FilteredLayer";
 | 
				
			||||||
 | 
					import MetaTagging from "../MetaTagging";
 | 
				
			||||||
 | 
					import RememberingSource from "./Sources/RememberingSource";
 | 
				
			||||||
 | 
					import OverpassFeatureSource from "../Actors/OverpassFeatureSource";
 | 
				
			||||||
 | 
					import {Changes} from "../Osm/Changes";
 | 
				
			||||||
 | 
					import GeoJsonSource from "./Sources/GeoJsonSource";
 | 
				
			||||||
 | 
					import Loc from "../../Models/Loc";
 | 
				
			||||||
 | 
					import WayHandlingApplyingFeatureSource from "./Sources/WayHandlingApplyingFeatureSource";
 | 
				
			||||||
 | 
					import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor";
 | 
				
			||||||
 | 
					import {Utils} from "../../Utils";
 | 
				
			||||||
 | 
					import TiledFromLocalStorageSource from "./TiledFeatureSource/TiledFromLocalStorageSource";
 | 
				
			||||||
 | 
					import LocalStorageSaverActor from "./Actors/LocalStorageSaverActor";
 | 
				
			||||||
 | 
					import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource";
 | 
				
			||||||
 | 
					import {BBox} from "../GeoOperations";
 | 
				
			||||||
 | 
					import {TileHierarchyMerger} from "./TiledFeatureSource/TileHierarchyMerger";
 | 
				
			||||||
 | 
					import RelationsTracker from "../Osm/RelationsTracker";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class FeaturePipeline implements FeatureSource {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public features: UIEventSource<{ feature: any; freshness: Date }[]>;
 | 
					export default class FeaturePipeline implements FeatureSourceState {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public readonly name = "FeaturePipeline"
 | 
					    public readonly sufficientlyZoomed: UIEventSource<boolean>;
 | 
				
			||||||
 | 
					    public readonly runningQuery: UIEventSource<boolean>;
 | 
				
			||||||
 | 
					    public readonly timeout: UIEventSource<number>;
 | 
				
			||||||
 | 
					    public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(flayers: UIEventSource<FilteredLayer[]>,
 | 
					    private readonly overpassUpdater: OverpassFeatureSource
 | 
				
			||||||
                changes: Changes,
 | 
					    private readonly relationTracker: RelationsTracker
 | 
				
			||||||
                updater: FeatureSource,
 | 
					    private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>;
 | 
				
			||||||
                fromOsmApi: FeatureSource,
 | 
					    constructor(
 | 
				
			||||||
                layout: UIEventSource<LayoutConfig>,
 | 
					        handleFeatureSource: (source: FeatureSourceForLayer) => void,
 | 
				
			||||||
 | 
					        state: {
 | 
				
			||||||
 | 
					            osmApiFeatureSource: FeatureSource,
 | 
				
			||||||
 | 
					            filteredLayers: UIEventSource<FilteredLayer[]>,
 | 
				
			||||||
            locationControl: UIEventSource<Loc>,
 | 
					            locationControl: UIEventSource<Loc>,
 | 
				
			||||||
                selectedElement: UIEventSource<any>) {
 | 
					            selectedElement: UIEventSource<any>,
 | 
				
			||||||
 | 
					            changes: Changes,
 | 
				
			||||||
 | 
					            layoutToUse: UIEventSource<LayoutConfig>,
 | 
				
			||||||
 | 
					            leafletMap: any,
 | 
				
			||||||
 | 
					            readonly overpassUrl: UIEventSource<string>;
 | 
				
			||||||
 | 
					            readonly overpassTimeout: UIEventSource<number>;
 | 
				
			||||||
 | 
					            readonly overpassMaxZoom: UIEventSource<number>;
 | 
				
			||||||
 | 
					        }) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const allLoadedFeatures = new UIEventSource<{ feature: any; freshness: Date }[]>([])
 | 
					        const self = this
 | 
				
			||||||
 | 
					        const updater = new OverpassFeatureSource(state);
 | 
				
			||||||
 | 
					        this.overpassUpdater = updater;
 | 
				
			||||||
 | 
					        this.sufficientlyZoomed = updater.sufficientlyZoomed
 | 
				
			||||||
 | 
					        this.runningQuery = updater.runningQuery
 | 
				
			||||||
 | 
					        this.timeout = updater.timeout
 | 
				
			||||||
 | 
					        this.relationTracker = updater.relationsTracker
 | 
				
			||||||
 | 
					        // Register everything in the state' 'AllElements'
 | 
				
			||||||
 | 
					        new RegisteringAllFromFeatureSourceActor(updater)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // first we metatag, then we save to get the metatags into storage too
 | 
					        const perLayerHierarchy = new Map<string, TileHierarchyMerger>()
 | 
				
			||||||
        // Note that we need to register before we do metatagging (as it expects the event sources)
 | 
					        this.perLayerHierarchy = perLayerHierarchy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // AT last, the metaTagging also needs to be run _after_ the duplicatorPerLayer
 | 
					        const patchedHandleFeatureSource = function (src: FeatureSourceForLayer) {
 | 
				
			||||||
        const amendedOverpassSource =
 | 
					            // This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
 | 
				
			||||||
            new RememberingSource(
 | 
					            const srcFiltered =
 | 
				
			||||||
                new LocalStorageSaver(
 | 
					                new FilteringFeatureSource(state,
 | 
				
			||||||
                    new MetaTaggingFeatureSource(allLoadedFeatures,
 | 
					                    new WayHandlingApplyingFeatureSource(
 | 
				
			||||||
                        new FeatureDuplicatorPerLayer(flayers,
 | 
					                        src,
 | 
				
			||||||
                            new RegisteringFeatureSource(
 | 
					                    )
 | 
				
			||||||
                                new ChangeApplicator(
 | 
					                )
 | 
				
			||||||
                                    updater, changes
 | 
					            handleFeatureSource(srcFiltered)
 | 
				
			||||||
                                ))
 | 
					            self.somethingLoaded.setData(true)
 | 
				
			||||||
                        )), layout));
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const geojsonSources: FeatureSource [] = GeoJsonSource
 | 
					        function addToHierarchy(src: FeatureSource & Tiled, layerId: string) {
 | 
				
			||||||
            .ConstructMultiSource(flayers.data, locationControl)
 | 
					            perLayerHierarchy.get(layerId).registerTile(src)
 | 
				
			||||||
            .map(geojsonSource => {
 | 
					 | 
				
			||||||
                let source = new RegisteringFeatureSource(
 | 
					 | 
				
			||||||
                    new FeatureDuplicatorPerLayer(flayers,
 | 
					 | 
				
			||||||
                        new ChangeApplicator(geojsonSource, changes)));
 | 
					 | 
				
			||||||
                if (!geojsonSource.isOsmCache) {
 | 
					 | 
				
			||||||
                    source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
                return source
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const amendedLocalStorageSource =
 | 
					        for (const filteredLayer of state.filteredLayers.data) {
 | 
				
			||||||
            new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new ChangeApplicator(new LocalStorageSource(layout), changes))
 | 
					            const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) => patchedHandleFeatureSource(tile))
 | 
				
			||||||
            ));
 | 
					            const id = filteredLayer.layerDef.id
 | 
				
			||||||
 | 
					            perLayerHierarchy.set(id, hierarchy)
 | 
				
			||||||
 | 
					            const source = filteredLayer.layerDef.source
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const amendedOsmApiSource = new RememberingSource(
 | 
					            if (source.geojsonSource === undefined) {
 | 
				
			||||||
            new MetaTaggingFeatureSource(allLoadedFeatures,
 | 
					                // This is an OSM layer
 | 
				
			||||||
                new FeatureDuplicatorPerLayer(flayers,
 | 
					                // We load the cached values and register them
 | 
				
			||||||
                    new RegisteringFeatureSource(new ChangeApplicator(fromOsmApi, changes,
 | 
					                // Getting data from upstream happens a bit lower
 | 
				
			||||||
 | 
					                new TiledFromLocalStorageSource(filteredLayer,
 | 
				
			||||||
 | 
					                    (src) => {
 | 
				
			||||||
 | 
					                        new RegisteringAllFromFeatureSourceActor(src)
 | 
				
			||||||
 | 
					                        hierarchy.registerTile(src);
 | 
				
			||||||
 | 
					                    }, state)
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (source.geojsonZoomLevel === undefined) {
 | 
				
			||||||
 | 
					                // This is a 'load everything at once' geojson layer
 | 
				
			||||||
 | 
					                // We split them up into tiles
 | 
				
			||||||
 | 
					                const src = new GeoJsonSource(filteredLayer)
 | 
				
			||||||
 | 
					                TiledFeatureSource.createHierarchy(src, {
 | 
				
			||||||
 | 
					                    layer: src.layer,
 | 
				
			||||||
 | 
					                    registerTile: (tile) => {
 | 
				
			||||||
 | 
					                        new RegisteringAllFromFeatureSourceActor(tile)
 | 
				
			||||||
 | 
					                        addToHierarchy(tile, id)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                new DynamicGeoJsonTileSource(
 | 
				
			||||||
 | 
					                    filteredLayer,
 | 
				
			||||||
 | 
					                    src => TiledFeatureSource.createHierarchy(src, {
 | 
				
			||||||
 | 
					                        layer: src.layer,
 | 
				
			||||||
 | 
					                        registerTile: (tile) => {
 | 
				
			||||||
 | 
					                            new RegisteringAllFromFeatureSourceActor(tile)
 | 
				
			||||||
 | 
					                            addToHierarchy(tile, id)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }),
 | 
				
			||||||
 | 
					                    state
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Actually load data from the overpass source
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        new PerLayerFeatureSourceSplitter(state.filteredLayers,
 | 
				
			||||||
 | 
					            (source) => TiledFeatureSource.createHierarchy(source, {
 | 
				
			||||||
 | 
					                layer: source.layer,
 | 
				
			||||||
 | 
					                registerTile: (tile) => {
 | 
				
			||||||
 | 
					                    // We save the tile data for the given layer to local storage
 | 
				
			||||||
 | 
					                    const [z, x, y] = Utils.tile_from_index(tile.tileIndex)
 | 
				
			||||||
 | 
					                    new LocalStorageSaverActor(tile, x, y, z)
 | 
				
			||||||
 | 
					                    addToHierarchy(tile, source.layer.layerDef.id);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }), new RememberingSource(updater))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Whenever fresh data comes in, we need to update the metatagging
 | 
				
			||||||
 | 
					        updater.features.addCallback(_ => {
 | 
				
			||||||
 | 
					            self.updateAllMetaTagging()
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private updateAllMetaTagging() {
 | 
				
			||||||
 | 
					        console.log("Updating the meta tagging")
 | 
				
			||||||
 | 
					        const self = this;
 | 
				
			||||||
 | 
					        this.perLayerHierarchy.forEach(hierarchy => {
 | 
				
			||||||
 | 
					            hierarchy.loadedTiles.forEach(src => {
 | 
				
			||||||
 | 
					                MetaTagging.addMetatags(
 | 
				
			||||||
 | 
					                    src.features.data,
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                            // We lump in the new points here
 | 
					                        memberships: this.relationTracker,
 | 
				
			||||||
                            generateNewGeometries: true
 | 
					                        getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox)
 | 
				
			||||||
                        }
 | 
					                    },
 | 
				
			||||||
                    )))));
 | 
					                    src.layer.layerDef
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const merged =
 | 
					 | 
				
			||||||
            new FeatureSourceMerger([
 | 
					 | 
				
			||||||
                amendedOverpassSource,
 | 
					 | 
				
			||||||
                amendedOsmApiSource,
 | 
					 | 
				
			||||||
                amendedLocalStorageSource,
 | 
					 | 
				
			||||||
                ...geojsonSources
 | 
					 | 
				
			||||||
            ]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        merged.features.syncWith(allLoadedFeatures)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.features = new WayHandlingApplyingFeatureSource(flayers,
 | 
					 | 
				
			||||||
            new FilteringFeatureSource(
 | 
					 | 
				
			||||||
                flayers,
 | 
					 | 
				
			||||||
                locationControl,
 | 
					 | 
				
			||||||
                selectedElement,
 | 
					 | 
				
			||||||
                merged
 | 
					 | 
				
			||||||
            )).features;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    public GetAllFeaturesWithin(bbox: BBox): any[][]{
 | 
				
			||||||
 | 
					        const self = this
 | 
				
			||||||
 | 
					        const tiles = []
 | 
				
			||||||
 | 
					        Array.from(this.perLayerHierarchy.keys())
 | 
				
			||||||
 | 
					            .forEach(key => tiles.push(...self.GetFeaturesWithin(key, bbox)))
 | 
				
			||||||
 | 
					        return tiles;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public GetFeaturesWithin(layerId: string, bbox: BBox): any[][]{
 | 
				
			||||||
 | 
					        const requestedHierarchy = this.perLayerHierarchy.get(layerId)
 | 
				
			||||||
 | 
					        if (requestedHierarchy === undefined) {
 | 
				
			||||||
 | 
					            return undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return TileHierarchyTools.getTiles(requestedHierarchy, bbox)
 | 
				
			||||||
 | 
					            .filter(featureSource => featureSource.features?.data !== undefined)
 | 
				
			||||||
 | 
					            .map(featureSource => featureSource.features.data.map(fs => fs.feature))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public GetTilesPerLayerWithin(bbox: BBox, handleTile: (tile:  FeatureSourceForLayer & Tiled) => void){
 | 
				
			||||||
 | 
					       Array.from(this.perLayerHierarchy.values()).forEach(hierarchy => {
 | 
				
			||||||
 | 
					            TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ForceRefresh() {
 | 
				
			||||||
 | 
					        this.overpassUpdater.ForceRefresh()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,7 @@
 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					import {UIEventSource} from "../UIEventSource";
 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					import {Utils} from "../../Utils";
 | 
				
			||||||
 | 
					import FilteredLayer from "../../Models/FilteredLayer";
 | 
				
			||||||
 | 
					import {BBox} from "../GeoOperations";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default interface FeatureSource {
 | 
					export default interface FeatureSource {
 | 
				
			||||||
    features: UIEventSource<{ feature: any, freshness: Date }[]>;
 | 
					    features: UIEventSource<{ feature: any, freshness: Date }[]>;
 | 
				
			||||||
| 
						 | 
					@ -9,38 +11,30 @@ export default interface FeatureSource {
 | 
				
			||||||
    name: string;
 | 
					    name: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class FeatureSourceUtils {
 | 
					export interface Tiled {
 | 
				
			||||||
 | 
					    tileIndex: number,
 | 
				
			||||||
    /**
 | 
					    bbox: BBox
 | 
				
			||||||
     * Exports given featurePipeline as a geojson FeatureLists (downloads as a json)
 | 
					}
 | 
				
			||||||
     * @param featurePipeline The FeaturePipeline you want to export
 | 
					
 | 
				
			||||||
     * @param options The options object
 | 
					/**
 | 
				
			||||||
     * @param options.metadata True if you want to include the MapComplete metadata, false otherwise
 | 
					 * A feature source which only contains features for the defined layer
 | 
				
			||||||
     */
 | 
					 */
 | 
				
			||||||
    public static extractGeoJson(featurePipeline: FeatureSource, options: { metadata?: boolean } = {}) {
 | 
					export interface FeatureSourceForLayer extends FeatureSource{
 | 
				
			||||||
        let defaults = {
 | 
					    readonly layer: FilteredLayer
 | 
				
			||||||
            metadata: false,
 | 
					}
 | 
				
			||||||
        }
 | 
					
 | 
				
			||||||
        options = Utils.setDefaults(options, defaults);
 | 
					/**
 | 
				
			||||||
 | 
					 * A feature source which is aware of the indexes it contains
 | 
				
			||||||
        // Select all features, ignore the freshness and other data
 | 
					 */
 | 
				
			||||||
        let featureList: any[] = featurePipeline.features.data.map((feature) =>
 | 
					export interface IndexedFeatureSource extends FeatureSource {
 | 
				
			||||||
            JSON.parse(JSON.stringify((feature.feature)))); // Make a deep copy!
 | 
					    readonly containedIds: UIEventSource<Set<string>>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
        if (!options.metadata) {
 | 
					
 | 
				
			||||||
            for (let i = 0; i < featureList.length; i++) {
 | 
					/**
 | 
				
			||||||
                let feature = featureList[i];
 | 
					 * A feature source which has some extra data about it's state
 | 
				
			||||||
                for (let property in feature.properties) {
 | 
					 */
 | 
				
			||||||
                    if (property[0] == "_" && property !== "_lat" && property !== "_lon") {
 | 
					export interface FeatureSourceState {
 | 
				
			||||||
                        delete featureList[i]["properties"][property];
 | 
					    readonly sufficientlyZoomed: UIEventSource<boolean>;
 | 
				
			||||||
                    }
 | 
					    readonly runningQuery: UIEventSource<boolean>;
 | 
				
			||||||
                }
 | 
					    readonly timeout: UIEventSource<number>;
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return {type: "FeatureCollection", features: featureList}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,7 @@
 | 
				
			||||||
import FeatureSource from "./FeatureSource";
 | 
					import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource";
 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					import {UIEventSource} from "../UIEventSource";
 | 
				
			||||||
import FilteredLayer from "../../Models/FilteredLayer";
 | 
					import FilteredLayer from "../../Models/FilteredLayer";
 | 
				
			||||||
import OverpassFeatureSource from "../Actors/OverpassFeatureSource";
 | 
					import SimpleFeatureSource from "./Sources/SimpleFeatureSource";
 | 
				
			||||||
import SimpleFeatureSource from "./SimpleFeatureSource";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					@ -13,17 +12,17 @@ import SimpleFeatureSource from "./SimpleFeatureSource";
 | 
				
			||||||
export default class PerLayerFeatureSourceSplitter {
 | 
					export default class PerLayerFeatureSourceSplitter {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(layers: UIEventSource<FilteredLayer[]>,
 | 
					    constructor(layers: UIEventSource<FilteredLayer[]>,
 | 
				
			||||||
                handleLayerData: (source: FeatureSource) => void,
 | 
					                handleLayerData: (source: FeatureSourceForLayer) => void,
 | 
				
			||||||
                upstream: OverpassFeatureSource) {
 | 
					                upstream: FeatureSource) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const knownLayers = new Map<string, FeatureSource>()
 | 
					        const knownLayers = new Map<string, FeatureSourceForLayer>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        function update() {
 | 
					        function update() {
 | 
				
			||||||
            const features = upstream.features.data;
 | 
					            const features = upstream.features.data;
 | 
				
			||||||
            if (features === undefined) {
 | 
					            if (features === undefined) {
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if(layers.data === undefined){
 | 
					            if (layers.data === undefined) {
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,19 +68,16 @@ export default class PerLayerFeatureSourceSplitter {
 | 
				
			||||||
                if (featureSource === undefined) {
 | 
					                if (featureSource === undefined) {
 | 
				
			||||||
                    // Not yet initialized - now is a good time
 | 
					                    // Not yet initialized - now is a good time
 | 
				
			||||||
                    featureSource = new SimpleFeatureSource(layer)
 | 
					                    featureSource = new SimpleFeatureSource(layer)
 | 
				
			||||||
 | 
					                    featureSource.features.setData(features)
 | 
				
			||||||
                    knownLayers.set(id, featureSource)
 | 
					                    knownLayers.set(id, featureSource)
 | 
				
			||||||
                    handleLayerData(featureSource)
 | 
					                    handleLayerData(featureSource)
 | 
				
			||||||
                }
 | 
					                } else {
 | 
				
			||||||
                    featureSource.features.setData(features)
 | 
					                    featureSource.features.setData(features)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            upstream.features.addCallbackAndRunD(_ => update())
 | 
					 | 
				
			||||||
            layers.addCallbackAndRunD(_ => update())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        layers.addCallbackAndRunD(_ => update())
 | 
					        layers.addCallback(_ => update())
 | 
				
			||||||
        upstream.features.addCallbackAndRunD(_ => update())
 | 
					        upstream.features.addCallbackAndRunD(_ => update())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,22 +1,28 @@
 | 
				
			||||||
import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource";
 | 
					 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					 | 
				
			||||||
import FilteredLayer from "../../Models/FilteredLayer";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Merges features from different featureSources for a single layer
 | 
					 * Merges features from different featureSources for a single layer
 | 
				
			||||||
 * Uses the freshest feature available in the case multiple sources offer data with the same identifier
 | 
					 * Uses the freshest feature available in the case multiple sources offer data with the same identifier
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export default class FeatureSourceMerger implements FeatureSourceForLayer {
 | 
					import {UIEventSource} from "../../UIEventSource";
 | 
				
			||||||
 | 
					import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
 | 
				
			||||||
 | 
					import FilteredLayer from "../../../Models/FilteredLayer";
 | 
				
			||||||
 | 
					import {BBox} from "../../GeoOperations";
 | 
				
			||||||
 | 
					import {Utils} from "../../../Utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
 | 
					    public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
 | 
				
			||||||
    public readonly name;
 | 
					    public readonly name;
 | 
				
			||||||
    public readonly layer: FilteredLayer
 | 
					    public readonly layer: FilteredLayer
 | 
				
			||||||
    private readonly _sources: UIEventSource<FeatureSource[]>;
 | 
					    private readonly _sources: UIEventSource<FeatureSource[]>;
 | 
				
			||||||
 | 
					    public readonly tileIndex: number;
 | 
				
			||||||
 | 
					    public readonly bbox: BBox;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(layer: FilteredLayer ,sources: UIEventSource<FeatureSource[]>) {
 | 
					    constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource<FeatureSource[]>) {
 | 
				
			||||||
 | 
					        this.tileIndex = tileIndex;
 | 
				
			||||||
 | 
					        this.bbox = bbox;
 | 
				
			||||||
        this._sources = sources;
 | 
					        this._sources = sources;
 | 
				
			||||||
        this.layer = layer;
 | 
					        this.layer = layer;
 | 
				
			||||||
        this.name = "SourceMerger"
 | 
					        this.name = "FeatureSourceMerger("+layer.layerDef.id+", "+Utils.tile_from_index(tileIndex).join(",")+")"
 | 
				
			||||||
        const self = this;
 | 
					        const self = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const handledSources = new Set<FeatureSource>();
 | 
					        const handledSources = new Set<FeatureSource>();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,13 @@
 | 
				
			||||||
import {FeatureSourceForLayer} from "./FeatureSource";
 | 
					import {UIEventSource} from "../../UIEventSource";
 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
 | 
				
			||||||
import Hash from "../Web/Hash";
 | 
					import FilteredLayer from "../../../Models/FilteredLayer";
 | 
				
			||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
 | 
					import {FeatureSourceForLayer} from "../FeatureSource";
 | 
				
			||||||
import FilteredLayer from "../../Models/FilteredLayer";
 | 
					import Hash from "../../Web/Hash";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class FilteringFeatureSource implements FeatureSourceForLayer {
 | 
					export default class FilteringFeatureSource implements FeatureSourceForLayer {
 | 
				
			||||||
    public features: UIEventSource<{ feature: any; freshness: Date }[]> =
 | 
					    public features: UIEventSource<{ feature: any; freshness: Date }[]> =
 | 
				
			||||||
        new UIEventSource<{ feature: any; freshness: Date }[]>([]);
 | 
					        new UIEventSource<{ feature: any; freshness: Date }[]>([]);
 | 
				
			||||||
    public readonly name = "FilteringFeatureSource";
 | 
					    public readonly name;
 | 
				
			||||||
    public readonly layer: FilteredLayer;
 | 
					    public readonly layer: FilteredLayer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(
 | 
					    constructor(
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer {
 | 
				
			||||||
        upstream: FeatureSourceForLayer
 | 
					        upstream: FeatureSourceForLayer
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        const self = this;
 | 
					        const self = this;
 | 
				
			||||||
 | 
					        this.name = "FilteringFeatureSource("+upstream.name+")"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.layer = upstream.layer;
 | 
					        this.layer = upstream.layer;
 | 
				
			||||||
        const layer = upstream.layer;
 | 
					        const layer = upstream.layer;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,14 @@
 | 
				
			||||||
import {FeatureSourceForLayer} from "./FeatureSource";
 | 
					 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					 | 
				
			||||||
import FilteredLayer from "../../Models/FilteredLayer";
 | 
					 | 
				
			||||||
import {control} from "leaflet";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Fetches a geojson file somewhere and passes it along
 | 
					 * Fetches a geojson file somewhere and passes it along
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export default class GeoJsonSource implements FeatureSourceForLayer {
 | 
					import {UIEventSource} from "../../UIEventSource";
 | 
				
			||||||
 | 
					import FilteredLayer from "../../../Models/FilteredLayer";
 | 
				
			||||||
 | 
					import {Utils} from "../../../Utils";
 | 
				
			||||||
 | 
					import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
 | 
				
			||||||
 | 
					import {BBox} from "../../GeoOperations";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
 | 
					    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
 | 
				
			||||||
    public readonly name;
 | 
					    public readonly name;
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,8 @@ export default class GeoJsonSource implements FeatureSourceForLayer {
 | 
				
			||||||
    private readonly seenids: Set<string> = new Set<string>()
 | 
					    private readonly seenids: Set<string> = new Set<string>()
 | 
				
			||||||
    public readonly layer: FilteredLayer;
 | 
					    public readonly layer: FilteredLayer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public readonly tileIndex
 | 
				
			||||||
 | 
					    public readonly bbox;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public constructor(flayer: FilteredLayer,
 | 
					    public constructor(flayer: FilteredLayer,
 | 
				
			||||||
                       zxy?: [number, number, number]) {
 | 
					                       zxy?: [number, number, number]) {
 | 
				
			||||||
| 
						 | 
					@ -28,10 +30,16 @@ export default class GeoJsonSource implements FeatureSourceForLayer {
 | 
				
			||||||
        this.layer = flayer;
 | 
					        this.layer = flayer;
 | 
				
			||||||
        let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id);
 | 
					        let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id);
 | 
				
			||||||
        if (zxy !== undefined) {
 | 
					        if (zxy !== undefined) {
 | 
				
			||||||
 | 
					            const [z, x, y] = zxy;
 | 
				
			||||||
            url = url
 | 
					            url = url
 | 
				
			||||||
                .replace('{z}', "" + zxy[0])
 | 
					                .replace('{z}', "" + z)
 | 
				
			||||||
                .replace('{x}', "" + zxy[1])
 | 
					                .replace('{x}', "" + x)
 | 
				
			||||||
                .replace('{y}', "" + zxy[2])
 | 
					                .replace('{y}', "" + y)
 | 
				
			||||||
 | 
					            this.tileIndex = Utils.tile_index(z, x, y)
 | 
				
			||||||
 | 
					            this.bbox = BBox.fromTile(z, x, y)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.tileIndex = Utils.tile_index(0, 0, 0)
 | 
				
			||||||
 | 
					            this.bbox = BBox.global;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.name = "GeoJsonSource of " + url;
 | 
					        this.name = "GeoJsonSource of " + url;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
import FeatureSource from "./FeatureSource";
 | 
					import FeatureSource from "../FeatureSource";
 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					import {UIEventSource} from "../../UIEventSource";
 | 
				
			||||||
import {OsmObject} from "../Osm/OsmObject";
 | 
					import Loc from "../../../Models/Loc";
 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					import FilteredLayer from "../../../Models/FilteredLayer";
 | 
				
			||||||
import Loc from "../../Models/Loc";
 | 
					import {Utils} from "../../../Utils";
 | 
				
			||||||
import FilteredLayer from "../../Models/FilteredLayer";
 | 
					import {OsmObject} from "../../Osm/OsmObject";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class OsmApiFeatureSource implements FeatureSource {
 | 
					export default class OsmApiFeatureSource implements FeatureSource {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,10 @@
 | 
				
			||||||
 | 
					 | 
				
			||||||
import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource";
 | 
					 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					 | 
				
			||||||
import FilteredLayer from "../../Models/FilteredLayer";
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Every previously added point is remembered, but new points are added.
 | 
					 * Every previously added point is remembered, but new points are added.
 | 
				
			||||||
 * Data coming from upstream will always overwrite a previous value
 | 
					 * Data coming from upstream will always overwrite a previous value
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					import FeatureSource from "../FeatureSource";
 | 
				
			||||||
 | 
					import {UIEventSource} from "../../UIEventSource";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class RememberingSource implements FeatureSource {
 | 
					export default class RememberingSource implements FeatureSource {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>;
 | 
					    public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
import {FeatureSourceForLayer} from "./FeatureSource";
 | 
					import {UIEventSource} from "../../UIEventSource";
 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					import FilteredLayer from "../../../Models/FilteredLayer";
 | 
				
			||||||
import FilteredLayer from "../../Models/FilteredLayer";
 | 
					import {FeatureSourceForLayer} from "../FeatureSource";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class SimpleFeatureSource implements FeatureSourceForLayer {
 | 
					export default class SimpleFeatureSource implements FeatureSourceForLayer {
 | 
				
			||||||
    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
 | 
					    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,13 +8,20 @@ export default class StaticFeatureSource implements FeatureSource {
 | 
				
			||||||
    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
 | 
					    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
 | 
				
			||||||
    public readonly name: string = "StaticFeatureSource"
 | 
					    public readonly name: string = "StaticFeatureSource"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(features: any[]) {
 | 
					    constructor(features: any[] | UIEventSource<any[]>, useFeaturesDirectly = false) {
 | 
				
			||||||
        const now = new Date();
 | 
					        const now = new Date();
 | 
				
			||||||
 | 
					        if(useFeaturesDirectly){
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            this.features = features
 | 
				
			||||||
 | 
					        }else         if (features instanceof UIEventSource) {
 | 
				
			||||||
 | 
					            this.features = features.map(features => features.map(f => ({feature: f, freshness: now})))
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
            this.features = new UIEventSource(features.map(f => ({
 | 
					            this.features = new UIEventSource(features.map(f => ({
 | 
				
			||||||
                feature: f,
 | 
					                feature: f,
 | 
				
			||||||
                freshness: now
 | 
					                freshness: now
 | 
				
			||||||
            })))
 | 
					            })))
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,18 +1,18 @@
 | 
				
			||||||
import {FeatureSourceForLayer} from "./FeatureSource";
 | 
					 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					 | 
				
			||||||
import {GeoOperations} from "../GeoOperations";
 | 
					 | 
				
			||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * This is the part of the pipeline which introduces extra points at the center of an area (but only if this is demanded by the wayhandling)
 | 
					 * This is the part of the pipeline which introduces extra points at the center of an area (but only if this is demanded by the wayhandling)
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					import {UIEventSource} from "../../UIEventSource";
 | 
				
			||||||
 | 
					import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
 | 
				
			||||||
 | 
					import {GeoOperations} from "../../GeoOperations";
 | 
				
			||||||
 | 
					import {FeatureSourceForLayer} from "../FeatureSource";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class WayHandlingApplyingFeatureSource implements FeatureSourceForLayer {
 | 
					export default class WayHandlingApplyingFeatureSource implements FeatureSourceForLayer {
 | 
				
			||||||
    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
 | 
					    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
 | 
				
			||||||
    public readonly name;
 | 
					    public readonly name;
 | 
				
			||||||
    public readonly layer;
 | 
					    public readonly layer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(upstream: FeatureSourceForLayer) {
 | 
					    constructor(upstream: FeatureSourceForLayer) {
 | 
				
			||||||
        this.name = "Wayhandling of " + upstream.name;
 | 
					        this.name = "Wayhandling(" + upstream.name+")";
 | 
				
			||||||
        this.layer = upstream.layer
 | 
					        this.layer = upstream.layer
 | 
				
			||||||
        const layer = upstream.layer.layerDef;
 | 
					        const layer = upstream.layer.layerDef;
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,63 @@
 | 
				
			||||||
 | 
					import FilteredLayer from "../../../Models/FilteredLayer";
 | 
				
			||||||
 | 
					import {FeatureSourceForLayer} from "../FeatureSource";
 | 
				
			||||||
 | 
					import {UIEventSource} from "../../UIEventSource";
 | 
				
			||||||
 | 
					import Loc from "../../../Models/Loc";
 | 
				
			||||||
 | 
					import DynamicTileSource from "./DynamicTileSource";
 | 
				
			||||||
 | 
					import {Utils} from "../../../Utils";
 | 
				
			||||||
 | 
					import GeoJsonSource from "../Sources/GeoJsonSource";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class DynamicGeoJsonTileSource extends DynamicTileSource {
 | 
				
			||||||
 | 
					    constructor(layer: FilteredLayer,
 | 
				
			||||||
 | 
					                registerLayer: (layer: FeatureSourceForLayer) => void,
 | 
				
			||||||
 | 
					                state: {
 | 
				
			||||||
 | 
					                    locationControl: UIEventSource<Loc>
 | 
				
			||||||
 | 
					                    leafletMap: any
 | 
				
			||||||
 | 
					                }) {
 | 
				
			||||||
 | 
					        const source = layer.layerDef.source
 | 
				
			||||||
 | 
					        if (source.geojsonZoomLevel === undefined) {
 | 
				
			||||||
 | 
					            throw "Invalid layer: geojsonZoomLevel expected"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (source.geojsonSource === undefined) {
 | 
				
			||||||
 | 
					            throw "Invalid layer: geojsonSource expected"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const whitelistUrl = source.geojsonSource.replace("{z}_{x}_{y}.geojson", "overview.json")
 | 
				
			||||||
 | 
					            .replace("{layer}",layer.layerDef.id)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let whitelist = undefined
 | 
				
			||||||
 | 
					        Utils.downloadJson(whitelistUrl).then(
 | 
				
			||||||
 | 
					            json => {
 | 
				
			||||||
 | 
					                const data = new Map<number, Set<number>>();
 | 
				
			||||||
 | 
					                for (const x in json) {
 | 
				
			||||||
 | 
					                    data.set(Number(x), new Set(json[x]))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                whitelist = data
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ).catch(err => {
 | 
				
			||||||
 | 
					            console.warn("No whitelist found for ", layer.layerDef.id, err)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        super(
 | 
				
			||||||
 | 
					            layer,
 | 
				
			||||||
 | 
					            source.geojsonZoomLevel,
 | 
				
			||||||
 | 
					            (zxy) => {
 | 
				
			||||||
 | 
					                if(whitelist !== undefined){
 | 
				
			||||||
 | 
					                    const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2])
 | 
				
			||||||
 | 
					                    if(!isWhiteListed){
 | 
				
			||||||
 | 
					                        return undefined;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                const src = new GeoJsonSource(
 | 
				
			||||||
 | 
					                    layer,
 | 
				
			||||||
 | 
					                    zxy
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                registerLayer(src)
 | 
				
			||||||
 | 
					                return src
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            state
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,22 +1,24 @@
 | 
				
			||||||
/***
 | 
					
 | 
				
			||||||
 * A tiled source which dynamically loads the required tiles
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
import State from "../../../State";
 | 
					import State from "../../../State";
 | 
				
			||||||
import FilteredLayer from "../../../Models/FilteredLayer";
 | 
					import FilteredLayer from "../../../Models/FilteredLayer";
 | 
				
			||||||
import {FeatureSourceForLayer} from "../FeatureSource";
 | 
					import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
 | 
				
			||||||
import {Utils} from "../../../Utils";
 | 
					import {Utils} from "../../../Utils";
 | 
				
			||||||
import {UIEventSource} from "../../UIEventSource";
 | 
					import {UIEventSource} from "../../UIEventSource";
 | 
				
			||||||
import Loc from "../../../Models/Loc";
 | 
					import Loc from "../../../Models/Loc";
 | 
				
			||||||
 | 
					import TileHierarchy from "./TileHierarchy";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class DynamicTileSource {
 | 
					/***
 | 
				
			||||||
 | 
					 * A tiled source which dynamically loads the required tiles at a fixed zoom level
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export default class DynamicTileSource implements TileHierarchy<FeatureSourceForLayer & Tiled> {
 | 
				
			||||||
    private readonly _loadedTiles = new Set<number>();
 | 
					    private readonly _loadedTiles = new Set<number>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public readonly existingTiles: Map<number, Map<number, FeatureSourceForLayer>> = new Map<number, Map<number, FeatureSourceForLayer>>()
 | 
					    public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(
 | 
					    constructor(
 | 
				
			||||||
        layer: FilteredLayer,
 | 
					        layer: FilteredLayer,
 | 
				
			||||||
        zoomlevel: number,
 | 
					        zoomlevel: number,
 | 
				
			||||||
        constructTile: (xy: [number, number]) => FeatureSourceForLayer,
 | 
					        constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled),
 | 
				
			||||||
        state: {
 | 
					        state: {
 | 
				
			||||||
            locationControl: UIEventSource<Loc>
 | 
					            locationControl: UIEventSource<Loc>
 | 
				
			||||||
            leafletMap: any
 | 
					            leafletMap: any
 | 
				
			||||||
| 
						 | 
					@ -24,6 +26,8 @@ export default class DynamicTileSource {
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        state = State.state
 | 
					        state = State.state
 | 
				
			||||||
        const self = this;
 | 
					        const self = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.loadedTiles = new Map<number,FeatureSourceForLayer & Tiled>()
 | 
				
			||||||
        const neededTiles = state.locationControl.map(
 | 
					        const neededTiles = state.locationControl.map(
 | 
				
			||||||
            location => {
 | 
					            location => {
 | 
				
			||||||
                if (!layer.isDisplayed.data) {
 | 
					                if (!layer.isDisplayed.data) {
 | 
				
			||||||
| 
						 | 
					@ -45,7 +49,7 @@ export default class DynamicTileSource {
 | 
				
			||||||
                const tileRange = Utils.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
 | 
					                const tileRange = Utils.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                const needed = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i))
 | 
					                const needed = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i))
 | 
				
			||||||
                if(needed.length === 0){
 | 
					                if (needed.length === 0) {
 | 
				
			||||||
                    return undefined
 | 
					                    return undefined
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                return needed
 | 
					                return needed
 | 
				
			||||||
| 
						 | 
					@ -53,20 +57,22 @@ export default class DynamicTileSource {
 | 
				
			||||||
            , [layer.isDisplayed, state.leafletMap]).stabilized(250);
 | 
					            , [layer.isDisplayed, state.leafletMap]).stabilized(250);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        neededTiles.addCallbackAndRunD(neededIndexes => {
 | 
					        neededTiles.addCallbackAndRunD(neededIndexes => {
 | 
				
			||||||
 | 
					            console.log("Tiled geojson source ",layer.layerDef.id," needs", neededIndexes)
 | 
				
			||||||
 | 
					            if (neededIndexes === undefined) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            for (const neededIndex of neededIndexes) {
 | 
					            for (const neededIndex of neededIndexes) {
 | 
				
			||||||
                self._loadedTiles.add(neededIndex)
 | 
					                self._loadedTiles.add(neededIndex)
 | 
				
			||||||
                const xy = Utils.tile_from_index(zoomlevel, neededIndex)
 | 
					                const src = constructTile( Utils.tile_from_index(neededIndex))
 | 
				
			||||||
                const src = constructTile(xy)
 | 
					                if(src !== undefined){
 | 
				
			||||||
                let xmap = self.existingTiles.get(xy[0])
 | 
					                    self.loadedTiles.set(neededIndex, src)
 | 
				
			||||||
                if(xmap === undefined){
 | 
					 | 
				
			||||||
                   xmap =  new Map<number, FeatureSourceForLayer>()
 | 
					 | 
				
			||||||
                   self.existingTiles.set(xy[0], xmap) 
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            xmap.set(xy[1], src)
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,27 @@
 | 
				
			||||||
Data in MapComplete can come from multiple sources.
 | 
					Data in MapComplete can come from multiple sources.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
In order to keep thins snappy, they are distributed over a tiled database
 | 
					Currently, they are:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- The Overpass-API
 | 
				
			||||||
 | 
					- The OSM-API
 | 
				
			||||||
 | 
					- One or more GeoJSON files. This can be a single file or a set of tiled geojson files
 | 
				
			||||||
 | 
					- LocalStorage, containing features from a previous visit
 | 
				
			||||||
 | 
					- Changes made by the user introducing new features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When the data enters from Overpass or from the OSM-API, they are first distributed per layer:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OVERPASS | ---PerLayerFeatureSource---> FeatureSourceForLayer[]
 | 
				
			||||||
 | 
					OSM      |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The GeoJSon files (not tiled) are then added to this list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A single FeatureSourcePerLayer is then further handled by splitting it into a tile hierarchy.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					In order to keep thins snappy, they are distributed over a tiled database per layer.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Notes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`cached-featuresbookcases` is the old key used `cahced-features{themeid}` and should be cleaned up
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					import FeatureSource, {Tiled} from "../FeatureSource";
 | 
				
			||||||
 | 
					import {BBox} from "../../GeoOperations";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default interface TileHierarchy<T extends FeatureSource & Tiled> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * A mapping from 'tile_index' to the actual tile featrues
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    loadedTiles: Map<number, T>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class TileHierarchyTools {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static getTiles<T extends FeatureSource & Tiled>(hierarchy: TileHierarchy<T>, bbox: BBox): T[] {
 | 
				
			||||||
 | 
					        const result = []
 | 
				
			||||||
 | 
					        hierarchy.loadedTiles.forEach((tile) => {
 | 
				
			||||||
 | 
					            if (tile.bbox.overlapsWith(bbox)) {
 | 
				
			||||||
 | 
					                result.push(tile)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        return result;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,10 @@
 | 
				
			||||||
import TileHierarchy from "./TiledFeatureSource/TileHierarchy";
 | 
					import TileHierarchy from "./TileHierarchy";
 | 
				
			||||||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource";
 | 
					import {UIEventSource} from "../../UIEventSource";
 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
 | 
				
			||||||
import FilteredLayer from "../../Models/FilteredLayer";
 | 
					import FilteredLayer from "../../../Models/FilteredLayer";
 | 
				
			||||||
import FeatureSourceMerger from "./Sources/FeatureSourceMerger";
 | 
					import {Utils} from "../../../Utils";
 | 
				
			||||||
import {BBox} from "../GeoOperations";
 | 
					import {BBox} from "../../GeoOperations";
 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					import FeatureSourceMerger from "../Sources/FeatureSourceMerger";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> {
 | 
					export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> {
 | 
				
			||||||
    public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
 | 
					    public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
 | 
				
			||||||
| 
						 | 
					@ -24,8 +24,9 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer
 | 
				
			||||||
     * @param src
 | 
					     * @param src
 | 
				
			||||||
     * @param index
 | 
					     * @param index
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public registerTile(src: FeatureSource, index: number) {
 | 
					    public registerTile(src: FeatureSource  & Tiled) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const index = src.tileIndex
 | 
				
			||||||
        if (this.sources.has(index)) {
 | 
					        if (this.sources.has(index)) {
 | 
				
			||||||
            const sources = this.sources.get(index)
 | 
					            const sources = this.sources.get(index)
 | 
				
			||||||
            sources.data.push(src)
 | 
					            sources.data.push(src)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,191 @@
 | 
				
			||||||
 | 
					import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
 | 
				
			||||||
 | 
					import {UIEventSource} from "../../UIEventSource";
 | 
				
			||||||
 | 
					import {Utils} from "../../../Utils";
 | 
				
			||||||
 | 
					import {BBox} from "../../GeoOperations";
 | 
				
			||||||
 | 
					import FilteredLayer from "../../../Models/FilteredLayer";
 | 
				
			||||||
 | 
					import TileHierarchy from "./TileHierarchy";
 | 
				
			||||||
 | 
					import {feature} from "@turf/turf";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Contains all features in a tiled fashion.
 | 
				
			||||||
 | 
					 * The data will be automatically broken down into subtiles when there are too much features in a single tile or if the zoomlevel is too high
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, FeatureSourceForLayer, TileHierarchy<IndexedFeatureSource & FeatureSourceForLayer & Tiled> {
 | 
				
			||||||
 | 
					    public readonly z: number;
 | 
				
			||||||
 | 
					    public readonly x: number;
 | 
				
			||||||
 | 
					    public readonly y: number;
 | 
				
			||||||
 | 
					    public readonly parent: TiledFeatureSource;
 | 
				
			||||||
 | 
					    public readonly root: TiledFeatureSource
 | 
				
			||||||
 | 
					    public readonly layer: FilteredLayer;
 | 
				
			||||||
 | 
					    /* An index of all known tiles. allTiles[z][x][y].get('layerid') will yield the corresponding tile.
 | 
				
			||||||
 | 
					    * Only defined on the root element!
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public readonly loadedTiles: Map<number, TiledFeatureSource & FeatureSourceForLayer> = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public readonly maxFeatureCount: number;
 | 
				
			||||||
 | 
					    public readonly name;
 | 
				
			||||||
 | 
					    public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>
 | 
				
			||||||
 | 
					    public readonly containedIds: UIEventSource<Set<string>>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public readonly bbox: BBox;
 | 
				
			||||||
 | 
					    private upper_left: TiledFeatureSource
 | 
				
			||||||
 | 
					    private upper_right: TiledFeatureSource
 | 
				
			||||||
 | 
					    private lower_left: TiledFeatureSource
 | 
				
			||||||
 | 
					    private lower_right: TiledFeatureSource
 | 
				
			||||||
 | 
					    private readonly maxzoom: number;
 | 
				
			||||||
 | 
					    private readonly options: TiledFeatureSourceOptions
 | 
				
			||||||
 | 
					    public readonly tileIndex: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private constructor(z: number, x: number, y: number, parent: TiledFeatureSource, options?: TiledFeatureSourceOptions) {
 | 
				
			||||||
 | 
					        this.z = z;
 | 
				
			||||||
 | 
					        this.x = x;
 | 
				
			||||||
 | 
					        this.y = y;
 | 
				
			||||||
 | 
					        this.bbox = BBox.fromTile(z, x, y)
 | 
				
			||||||
 | 
					        this.tileIndex = Utils.tile_index(z, x, y)
 | 
				
			||||||
 | 
					        this.name = `TiledFeatureSource(${z},${x},${y})`
 | 
				
			||||||
 | 
					        this.parent = parent;
 | 
				
			||||||
 | 
					        this.layer = options.layer
 | 
				
			||||||
 | 
					        options = options ?? {}
 | 
				
			||||||
 | 
					        this.maxFeatureCount = options?.maxFeatureCount ?? 500;
 | 
				
			||||||
 | 
					        this.maxzoom = options.maxZoomLevel ?? 18
 | 
				
			||||||
 | 
					        this.options = options;
 | 
				
			||||||
 | 
					        if (parent === undefined) {
 | 
				
			||||||
 | 
					            throw "Parent is not allowed to be undefined. Use null instead"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (parent === null && z !== 0 && x !== 0 && y !== 0) {
 | 
				
			||||||
 | 
					            throw "Invalid root tile: z, x and y should all be null"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (parent === null) {
 | 
				
			||||||
 | 
					            this.root = this;
 | 
				
			||||||
 | 
					            this.loadedTiles = new Map()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.root = this.parent.root;
 | 
				
			||||||
 | 
					            this.loadedTiles = this.root.loadedTiles;
 | 
				
			||||||
 | 
					            const i = Utils.tile_index(z, x, y)
 | 
				
			||||||
 | 
					            this.root.loadedTiles.set(i, this)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.features = new UIEventSource<any[]>([])
 | 
				
			||||||
 | 
					        this.containedIds = this.features.map(features => {
 | 
				
			||||||
 | 
					            if (features === undefined) {
 | 
				
			||||||
 | 
					                return undefined;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return new Set(features.map(f => f.feature.properties.id))
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // We register this tile, but only when there is some data in it
 | 
				
			||||||
 | 
					        if (this.options.registerTile !== undefined) {
 | 
				
			||||||
 | 
					            this.features.addCallbackAndRunD(features => {
 | 
				
			||||||
 | 
					                if (features.length === 0) {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                this.options.registerTile(this)
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static createHierarchy(features: FeatureSource, options?: TiledFeatureSourceOptions): TiledFeatureSource {
 | 
				
			||||||
 | 
					        const root = new TiledFeatureSource(0, 0, 0, null, options)
 | 
				
			||||||
 | 
					        features.features?.addCallbackAndRunD(feats => root.addFeatures(feats))
 | 
				
			||||||
 | 
					        return root;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private isSplitNeeded(featureCount: number){
 | 
				
			||||||
 | 
					        if(this.upper_left !== undefined){
 | 
				
			||||||
 | 
					            // This tile has been split previously, so we keep on splitting
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if(this.z >= this.maxzoom){
 | 
				
			||||||
 | 
					            // We are not allowed to split any further
 | 
				
			||||||
 | 
					            return false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if(this.options.minZoomLevel !== undefined && this.z < this.options.minZoomLevel){
 | 
				
			||||||
 | 
					            // We must have at least this zoom level before we are allowed to start splitting
 | 
				
			||||||
 | 
					            return true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // To much features - we split
 | 
				
			||||||
 | 
					        return featureCount > this.maxFeatureCount
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /***
 | 
				
			||||||
 | 
					     * Adds the list of features to this hierarchy.
 | 
				
			||||||
 | 
					     * If there are too much features, the list will be broken down and distributed over the subtiles (only retaining features that don't fit a subtile on this level)
 | 
				
			||||||
 | 
					     * @param features
 | 
				
			||||||
 | 
					     * @private
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private addFeatures(features: { feature: any, freshness: Date }[]) {
 | 
				
			||||||
 | 
					        if (features === undefined || features.length === 0) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (!this.isSplitNeeded(features.length)) {
 | 
				
			||||||
 | 
					            this.features.setData(features)
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.upper_left === undefined) {
 | 
				
			||||||
 | 
					            this.upper_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2, this, this.options)
 | 
				
			||||||
 | 
					            this.upper_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2, this, this.options)
 | 
				
			||||||
 | 
					            this.lower_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2 + 1, this, this.options)
 | 
				
			||||||
 | 
					            this.lower_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2 + 1, this, this.options)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const ulf = []
 | 
				
			||||||
 | 
					        const urf = []
 | 
				
			||||||
 | 
					        const llf = []
 | 
				
			||||||
 | 
					        const lrf = []
 | 
				
			||||||
 | 
					        const overlapsboundary = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (const feature of features) {
 | 
				
			||||||
 | 
					            const bbox = BBox.get(feature.feature)
 | 
				
			||||||
 | 
					            if (this.options.minZoomLevel === undefined) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (bbox.isContainedIn(this.upper_left.bbox)) {
 | 
				
			||||||
 | 
					                    ulf.push(feature)
 | 
				
			||||||
 | 
					                } else if (bbox.isContainedIn(this.upper_right.bbox)) {
 | 
				
			||||||
 | 
					                    urf.push(feature)
 | 
				
			||||||
 | 
					                } else if (bbox.isContainedIn(this.lower_left.bbox)) {
 | 
				
			||||||
 | 
					                    llf.push(feature)
 | 
				
			||||||
 | 
					                } else if (bbox.isContainedIn(this.lower_right.bbox)) {
 | 
				
			||||||
 | 
					                    lrf.push(feature)
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    overlapsboundary.push(feature)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // We duplicate a feature on a boundary into every tile as we need to get to the minZoomLevel
 | 
				
			||||||
 | 
					                if (bbox.overlapsWith(this.upper_left.bbox)) {
 | 
				
			||||||
 | 
					                    ulf.push(feature)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (bbox.overlapsWith(this.upper_right.bbox)) {
 | 
				
			||||||
 | 
					                    urf.push(feature)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (bbox.overlapsWith(this.lower_left.bbox)) {
 | 
				
			||||||
 | 
					                    llf.push(feature)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (bbox.overlapsWith(this.lower_right.bbox)) {
 | 
				
			||||||
 | 
					                    lrf.push(feature)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.upper_left.addFeatures(ulf)
 | 
				
			||||||
 | 
					        this.upper_right.addFeatures(urf)
 | 
				
			||||||
 | 
					        this.lower_left.addFeatures(llf)
 | 
				
			||||||
 | 
					        this.lower_right.addFeatures(lrf)
 | 
				
			||||||
 | 
					        this.features.setData(overlapsboundary)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface TiledFeatureSourceOptions {
 | 
				
			||||||
 | 
					    readonly maxFeatureCount?: number,
 | 
				
			||||||
 | 
					    readonly maxZoomLevel?: number,
 | 
				
			||||||
 | 
					    readonly minZoomLevel?: number,
 | 
				
			||||||
 | 
					    readonly registerTile?: (tile: TiledFeatureSource & Tiled) => void,
 | 
				
			||||||
 | 
					    readonly layer?: FilteredLayer
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,40 +1,102 @@
 | 
				
			||||||
import FilteredLayer from "../../../Models/FilteredLayer";
 | 
					import FilteredLayer from "../../../Models/FilteredLayer";
 | 
				
			||||||
import {FeatureSourceForLayer} from "../FeatureSource";
 | 
					import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
 | 
				
			||||||
import {UIEventSource} from "../../UIEventSource";
 | 
					import {UIEventSource} from "../../UIEventSource";
 | 
				
			||||||
import Loc from "../../../Models/Loc";
 | 
					import Loc from "../../../Models/Loc";
 | 
				
			||||||
import GeoJsonSource from "../GeoJsonSource";
 | 
					import TileHierarchy from "./TileHierarchy";
 | 
				
			||||||
import DynamicTileSource from "./DynamicTileSource";
 | 
					import {Utils} from "../../../Utils";
 | 
				
			||||||
 | 
					import LocalStorageSaverActor from "../Actors/LocalStorageSaverActor";
 | 
				
			||||||
 | 
					import {BBox} from "../../GeoOperations";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class TiledFromLocalStorageSource implements TileHierarchy<FeatureSourceForLayer & Tiled> {
 | 
				
			||||||
 | 
					    public loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class DynamicGeoJsonTileSource extends DynamicTileSource {
 | 
					 | 
				
			||||||
    constructor(layer: FilteredLayer,
 | 
					    constructor(layer: FilteredLayer,
 | 
				
			||||||
                registerLayer: (layer: FeatureSourceForLayer) => void,
 | 
					                handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void,
 | 
				
			||||||
                state: {
 | 
					                state: {
 | 
				
			||||||
                    locationControl: UIEventSource<Loc>
 | 
					                    locationControl: UIEventSource<Loc>
 | 
				
			||||||
                    leafletMap: any
 | 
					                    leafletMap: any
 | 
				
			||||||
                }) {
 | 
					                }) {
 | 
				
			||||||
        const source = layer.layerDef.source
 | 
					
 | 
				
			||||||
        if (source.geojsonZoomLevel === undefined) {
 | 
					        const prefix = LocalStorageSaverActor.storageKey + "-" + layer.layerDef.id + "-"
 | 
				
			||||||
            throw "Invalid layer: geojsonZoomLevel expected"
 | 
					        // @ts-ignore
 | 
				
			||||||
        }
 | 
					        const indexes: number[] = Object.keys(localStorage)
 | 
				
			||||||
        if (source.geojsonSource === undefined) {
 | 
					            .filter(key => {
 | 
				
			||||||
            throw "Invalid layer: geojsonSource expected"
 | 
					                return key.startsWith(prefix) && !key.endsWith("-time");
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .map(key => {
 | 
				
			||||||
 | 
					                return Number(key.substring(prefix.length));
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log("Avaible datasets in local storage:", indexes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const zLevels = indexes.map(i => i % 100)
 | 
				
			||||||
 | 
					        const indexesSet = new Set(indexes)
 | 
				
			||||||
 | 
					        const maxZoom = Math.max(...zLevels)
 | 
				
			||||||
 | 
					        const minZoom = Math.min(...zLevels)
 | 
				
			||||||
 | 
					        const self = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const neededTiles = state.locationControl.map(
 | 
				
			||||||
 | 
					            location => {
 | 
				
			||||||
 | 
					                if (!layer.isDisplayed.data) {
 | 
				
			||||||
 | 
					                    // No need to download! - the layer is disabled
 | 
				
			||||||
 | 
					                    return undefined;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        super(
 | 
					                if (location.zoom < layer.layerDef.minzoom) {
 | 
				
			||||||
            layer,
 | 
					                    // No need to download! - the layer is disabled
 | 
				
			||||||
            source.geojsonZoomLevel,
 | 
					                    return undefined;
 | 
				
			||||||
            (xy) => {
 | 
					                }
 | 
				
			||||||
                const xyz: [number, number, number] = [xy[0], xy[1], source.geojsonZoomLevel]
 | 
					
 | 
				
			||||||
                const src = new GeoJsonSource(
 | 
					                // Yup, this is cheating to just get the bounds here
 | 
				
			||||||
                    layer,
 | 
					                const bounds = state.leafletMap.data?.getBounds()
 | 
				
			||||||
                    xyz
 | 
					                if (bounds === undefined) {
 | 
				
			||||||
                )
 | 
					                    // We'll retry later
 | 
				
			||||||
                registerLayer(src)
 | 
					                    return undefined
 | 
				
			||||||
                return src
 | 
					                }
 | 
				
			||||||
            },
 | 
					
 | 
				
			||||||
            state
 | 
					                const needed = []
 | 
				
			||||||
        );
 | 
					                for (let z = minZoom; z <= maxZoom; z++) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    const tileRange = Utils.TileRangeBetween(z, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
 | 
				
			||||||
 | 
					                    const neededZ = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(z, x, y))
 | 
				
			||||||
 | 
					                        .filter(i => !self.loadedTiles.has(i) && indexesSet.has(i))
 | 
				
			||||||
 | 
					                    needed.push(...neededZ)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (needed.length === 0) {
 | 
				
			||||||
 | 
					                    return undefined
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return needed
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            , [layer.isDisplayed, state.leafletMap]).stabilized(50);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        neededTiles.addCallbackAndRun(t => console.log("Tiles to load from localstorage:", t))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        neededTiles.addCallbackAndRunD(neededIndexes => {
 | 
				
			||||||
 | 
					            for (const neededIndex of neededIndexes) {
 | 
				
			||||||
 | 
					                // We load the features from localStorage
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    const key = LocalStorageSaverActor.storageKey + "-" + layer.layerDef.id + "-" + neededIndex
 | 
				
			||||||
 | 
					                    const data = localStorage.getItem(key)
 | 
				
			||||||
 | 
					                    const features = JSON.parse(data)
 | 
				
			||||||
 | 
					                    const src = {
 | 
				
			||||||
 | 
					                        layer: layer,
 | 
				
			||||||
 | 
					                        features: new UIEventSource<{ feature: any; freshness: Date }[]>(features),
 | 
				
			||||||
 | 
					                        name: "FromLocalStorage(" + key + ")",
 | 
				
			||||||
 | 
					                        tileIndex: neededIndex,
 | 
				
			||||||
 | 
					                        bbox: BBox.fromTile(...Utils.tile_from_index(neededIndex))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    handleFeatureSource(src, neededIndex)
 | 
				
			||||||
 | 
					                    self.loadedTiles.set(neededIndex, src)
 | 
				
			||||||
 | 
					                } catch (e) {
 | 
				
			||||||
 | 
					                    console.error("Could not load data tile from local storage due to", e)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import * as turf from '@turf/turf'
 | 
					import * as turf from '@turf/turf'
 | 
				
			||||||
 | 
					import {Utils} from "../Utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class GeoOperations {
 | 
					export class GeoOperations {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -185,6 +186,44 @@ export class GeoOperations {
 | 
				
			||||||
        return turf.length(feature) * 1000
 | 
					        return turf.length(feature) * 1000
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    static buffer(feature: any, bufferSizeInMeter: number){
 | 
				
			||||||
 | 
					        return turf.buffer(feature, bufferSizeInMeter/1000, {
 | 
				
			||||||
 | 
					            units: 'kilometers'
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    static bbox(feature: any){
 | 
				
			||||||
 | 
					        const [lon, lat, lon0, lat0] = turf.bbox(feature)
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            "type": "Feature",
 | 
				
			||||||
 | 
					            "geometry": {
 | 
				
			||||||
 | 
					                "type": "LineString",
 | 
				
			||||||
 | 
					                "coordinates": [
 | 
				
			||||||
 | 
					                    [
 | 
				
			||||||
 | 
					                        lon,
 | 
				
			||||||
 | 
					                        lat
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    [
 | 
				
			||||||
 | 
					                        lon0,
 | 
				
			||||||
 | 
					                        lat
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    [
 | 
				
			||||||
 | 
					                        lon0,
 | 
				
			||||||
 | 
					                        lat0
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    [
 | 
				
			||||||
 | 
					                        lon,
 | 
				
			||||||
 | 
					                        lat0
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    [
 | 
				
			||||||
 | 
					                        lon,
 | 
				
			||||||
 | 
					                        lat
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Generates the closest point on a way from a given point
 | 
					     * Generates the closest point on a way from a given point
 | 
				
			||||||
     * @param way The road on which you want to find a point
 | 
					     * @param way The road on which you want to find a point
 | 
				
			||||||
| 
						 | 
					@ -340,6 +379,7 @@ export class BBox {
 | 
				
			||||||
    readonly maxLon: number;
 | 
					    readonly maxLon: number;
 | 
				
			||||||
    readonly minLat: number;
 | 
					    readonly minLat: number;
 | 
				
			||||||
    readonly minLon: number;
 | 
					    readonly minLon: number;
 | 
				
			||||||
 | 
					    static global: BBox = new BBox([[-180,-90],[180,90]]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(coordinates) {
 | 
					    constructor(coordinates) {
 | 
				
			||||||
        this.maxLat = Number.MIN_VALUE;
 | 
					        this.maxLat = Number.MIN_VALUE;
 | 
				
			||||||
| 
						 | 
					@ -361,12 +401,11 @@ export class BBox {
 | 
				
			||||||
        return new BBox([[bounds.getWest(), bounds.getNorth()], [bounds.getEast(), bounds.getSouth()]])
 | 
					        return new BBox([[bounds.getWest(), bounds.getNorth()], [bounds.getEast(), bounds.getSouth()]])
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    static get(feature) {
 | 
					    static get(feature): BBox {
 | 
				
			||||||
        if (feature.bbox?.overlapsWith === undefined) {
 | 
					        if (feature.bbox?.overlapsWith === undefined) {
 | 
				
			||||||
            const turfBbox: number[] = turf.bbox(feature)
 | 
					            const turfBbox: number[] = turf.bbox(feature)
 | 
				
			||||||
            feature.bbox = new BBox([[turfBbox[0], turfBbox[1]], [turfBbox[2], turfBbox[3]]]);
 | 
					            feature.bbox = new BBox([[turfBbox[0], turfBbox[1]], [turfBbox[2], turfBbox[3]]]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return feature.bbox;
 | 
					        return feature.bbox;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -407,4 +446,23 @@ export class BBox {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static fromTile(z: number, x: number, y: number) {
 | 
				
			||||||
 | 
					      return new BBox( Utils.tile_bounds_lon_lat(z, x, y))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getEast() {
 | 
				
			||||||
 | 
					        return this.maxLon
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getNorth() {
 | 
				
			||||||
 | 
					        return this.maxLat
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getWest() {
 | 
				
			||||||
 | 
					        return this.minLon
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getSouth() {
 | 
				
			||||||
 | 
					        return this.minLat
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -32,7 +32,6 @@ export class Mapillary extends ImageAttributionSource {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const mapview = value.match(/https?:\/\/www.mapillary.com\/map\/im\/(.*)/)
 | 
					        const mapview = value.match(/https?:\/\/www.mapillary.com\/map\/im\/(.*)/)
 | 
				
			||||||
        console.log("Mapview matched ", value, mapview)
 | 
					 | 
				
			||||||
        if(mapview !== null){
 | 
					        if(mapview !== null){
 | 
				
			||||||
            const key = mapview[1]
 | 
					            const key = mapview[1]
 | 
				
			||||||
            return {key:key, isApiv4: !isNaN(Number(key))};
 | 
					            return {key:key, isApiv4: !isNaN(Number(key))};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,9 @@
 | 
				
			||||||
import SimpleMetaTagger from "./SimpleMetaTagger";
 | 
					import SimpleMetaTagger from "./SimpleMetaTagger";
 | 
				
			||||||
import {ExtraFunction} from "./ExtraFunction";
 | 
					import {ExtraFuncParams, ExtraFunction} from "./ExtraFunction";
 | 
				
			||||||
import {Relation} from "./Osm/ExtractRelations";
 | 
					 | 
				
			||||||
import {UIEventSource} from "./UIEventSource";
 | 
					import {UIEventSource} from "./UIEventSource";
 | 
				
			||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
 | 
					import LayerConfig from "../Models/ThemeConfig/LayerConfig";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Params {
 | 
					 | 
				
			||||||
    featuresPerLayer: Map<string, any[]>,
 | 
					 | 
				
			||||||
    memberships: Map<string, { role: string, relation: Relation }[]>
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
 | 
					 * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
| 
						 | 
					@ -22,13 +16,12 @@ export default class MetaTagging {
 | 
				
			||||||
    private static readonly stopErrorOutputAt = 10;
 | 
					    private static readonly stopErrorOutputAt = 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * An actor which adds metatags on every feature in the given object
 | 
					     * This method (re)calculates all metatags and calculated tags on every given object.
 | 
				
			||||||
     * The features are a list of geojson-features, with a "properties"-field and geometry
 | 
					     * The given features should be part of the given layer
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    static addMetatags(features: { feature: any; freshness: Date }[],
 | 
					    static addMetatags(features: { feature: any; freshness: Date }[],
 | 
				
			||||||
                       allKnownFeatures: UIEventSource<{ feature: any; freshness: Date }[]>,
 | 
					                       params: ExtraFuncParams,
 | 
				
			||||||
                       relations: Map<string, { role: string, relation: Relation }[]>,
 | 
					                       layer: LayerConfig,
 | 
				
			||||||
                       layers: LayerConfig[],
 | 
					 | 
				
			||||||
                       includeDates = true) {
 | 
					                       includeDates = true) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (features === undefined || features.length === 0) {
 | 
					        if (features === undefined || features.length === 0) {
 | 
				
			||||||
| 
						 | 
					@ -44,59 +37,32 @@ export default class MetaTagging {
 | 
				
			||||||
                metatag.addMetaTags(features);
 | 
					                metatag.addMetaTags(features);
 | 
				
			||||||
            } catch (e) {
 | 
					            } catch (e) {
 | 
				
			||||||
                console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e)
 | 
					                console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e)
 | 
				
			||||||
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // The functions - per layer - which add the new keys
 | 
					        // The functions - per layer - which add the new keys
 | 
				
			||||||
        const layerFuncs = new Map<string, ((params: Params, feature: any) => void)>();
 | 
					        const layerFuncs = this.createRetaggingFunc(layer)
 | 
				
			||||||
        for (const layer of layers) {
 | 
					 | 
				
			||||||
            layerFuncs.set(layer.id, this.createRetaggingFunc(layer));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        allKnownFeatures.addCallbackAndRunD(newFeatures => {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const featuresPerLayer = new Map<string, any[]>();
 | 
					 | 
				
			||||||
            const allFeatures = Array.from(new Set(features.concat(newFeatures)))
 | 
					 | 
				
			||||||
            for (const feature of allFeatures) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                const key = feature.feature._matching_layer_id;
 | 
					 | 
				
			||||||
                if (!featuresPerLayer.has(key)) {
 | 
					 | 
				
			||||||
                    featuresPerLayer.set(key, [])
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                featuresPerLayer.get(key).push(feature.feature)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (layerFuncs !== undefined) {
 | 
				
			||||||
            for (const feature of features) {
 | 
					            for (const feature of features) {
 | 
				
			||||||
                // @ts-ignore
 | 
					 | 
				
			||||||
                const key = feature.feature._matching_layer_id;
 | 
					 | 
				
			||||||
                const f = layerFuncs.get(key);
 | 
					 | 
				
			||||||
                if (f === undefined) {
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                try {
 | 
					                try {
 | 
				
			||||||
                    f({featuresPerLayer: featuresPerLayer, memberships: relations}, feature.feature)
 | 
					                    layerFuncs(params, feature.feature)
 | 
				
			||||||
                } catch (e) {
 | 
					                } catch (e) {
 | 
				
			||||||
                    console.error(e)
 | 
					                    console.error(e)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static createRetaggingFunc(layer: LayerConfig):
 | 
					    private static createRetaggingFunc(layer: LayerConfig):
 | 
				
			||||||
        ((params: Params, feature: any) => void) {
 | 
					        ((params: ExtraFuncParams, feature: any) => void) {
 | 
				
			||||||
        const calculatedTags: [string, string][] = layer.calculatedTags;
 | 
					        const calculatedTags: [string, string][] = layer.calculatedTags;
 | 
				
			||||||
        if (calculatedTags === undefined) {
 | 
					        if (calculatedTags === undefined) {
 | 
				
			||||||
            return undefined;
 | 
					            return undefined;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const functions: ((params: Params, feature: any) => void)[] = [];
 | 
					        const functions: ((params: ExtraFuncParams, feature: any) => void)[] = [];
 | 
				
			||||||
        for (const entry of calculatedTags) {
 | 
					        for (const entry of calculatedTags) {
 | 
				
			||||||
            const key = entry[0]
 | 
					            const key = entry[0]
 | 
				
			||||||
            const code = entry[1];
 | 
					            const code = entry[1];
 | 
				
			||||||
| 
						 | 
					@ -145,14 +111,13 @@ export default class MetaTagging {
 | 
				
			||||||
                console.error("Could not create a dynamic function: ", e)
 | 
					                console.error("Could not create a dynamic function: ", e)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return (params: Params, feature) => {
 | 
					        return (params: ExtraFuncParams, feature) => {
 | 
				
			||||||
            const tags = feature.properties
 | 
					            const tags = feature.properties
 | 
				
			||||||
            if (tags === undefined) {
 | 
					            if (tags === undefined) {
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const relations = params.memberships?.get(feature.properties.id) ?? []
 | 
					            ExtraFunction.FullPatchFeature(params, feature);
 | 
				
			||||||
            ExtraFunction.FullPatchFeature(params.featuresPerLayer, relations, feature);
 | 
					 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                for (const f of functions) {
 | 
					                for (const f of functions) {
 | 
				
			||||||
                    f(params, feature);
 | 
					                    f(params, feature);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,30 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Represents a single change to an object
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
export interface ChangeDescription {
 | 
					export interface ChangeDescription {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Identifier of the object
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    type: "node" | "way" | "relation",
 | 
					    type: "node" | "way" | "relation",
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Negative for a new objects
 | 
					     * Identifier of the object
 | 
				
			||||||
 | 
					     * Negative for new objects
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    id: number,
 | 
					    id: number,
 | 
				
			||||||
    /*
 | 
					    
 | 
				
			||||||
 v = "" or v = undefined to erase this tag
 | 
					    /**
 | 
				
			||||||
 | 
					     * All changes to tags
 | 
				
			||||||
 | 
					     * v = "" or v = undefined to erase this tag
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    tags?: { k: string, v: string }[],
 | 
					    tags?: { k: string, v: string }[],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * A change to the geometry:
 | 
				
			||||||
 | 
					     * 1) Change of node location
 | 
				
			||||||
 | 
					     * 2) Change of way geometry
 | 
				
			||||||
 | 
					     * 3) Change of relation members (untested)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    changes?: {
 | 
					    changes?: {
 | 
				
			||||||
        lat: number,
 | 
					        lat: number,
 | 
				
			||||||
        lon: number
 | 
					        lon: number
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@ export class Geocoding {
 | 
				
			||||||
                      osm_type: string, osm_id: string
 | 
					                      osm_type: string, osm_id: string
 | 
				
			||||||
                  }[]) => void),
 | 
					                  }[]) => void),
 | 
				
			||||||
                  onFail: (() => void)) {
 | 
					                  onFail: (() => void)) {
 | 
				
			||||||
        const b = State.state.leafletMap.data.getBounds();
 | 
					        const b = State.state.currentBounds.data;
 | 
				
			||||||
        const url = Geocoding.host + "format=json&limit=1&viewbox=" +
 | 
					        const url = Geocoding.host + "format=json&limit=1&viewbox=" +
 | 
				
			||||||
            `${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}` +
 | 
					            `${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}` +
 | 
				
			||||||
            "&accept-language=nl&q=" + query;
 | 
					            "&accept-language=nl&q=" + query;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import * as OsmToGeoJson from "osmtogeojson";
 | 
					import * as OsmToGeoJson from "osmtogeojson";
 | 
				
			||||||
import Bounds from "../../Models/Bounds";
 | 
					import Bounds from "../../Models/Bounds";
 | 
				
			||||||
import {TagsFilter} from "../Tags/TagsFilter";
 | 
					import {TagsFilter} from "../Tags/TagsFilter";
 | 
				
			||||||
import ExtractRelations from "./ExtractRelations";
 | 
					import RelationsTracker from "./RelationsTracker";
 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					import {Utils} from "../../Utils";
 | 
				
			||||||
import {UIEventSource} from "../UIEventSource";
 | 
					import {UIEventSource} from "../UIEventSource";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,16 +15,20 @@ export class Overpass {
 | 
				
			||||||
    private readonly _timeout: UIEventSource<number>;
 | 
					    private readonly _timeout: UIEventSource<number>;
 | 
				
			||||||
    private readonly _extraScripts: string[];
 | 
					    private readonly _extraScripts: string[];
 | 
				
			||||||
    private _includeMeta: boolean;
 | 
					    private _includeMeta: boolean;
 | 
				
			||||||
 | 
					    private _relationTracker: RelationsTracker;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
   
 | 
					   
 | 
				
			||||||
    constructor(filter: TagsFilter, extraScripts: string[],
 | 
					    constructor(filter: TagsFilter, extraScripts: string[],
 | 
				
			||||||
                interpreterUrl: UIEventSource<string>,
 | 
					                interpreterUrl: UIEventSource<string>,
 | 
				
			||||||
                timeout: UIEventSource<number>,
 | 
					                timeout: UIEventSource<number>,
 | 
				
			||||||
 | 
					                relationTracker: RelationsTracker,
 | 
				
			||||||
                includeMeta = true) {
 | 
					                includeMeta = true) {
 | 
				
			||||||
        this._timeout = timeout;
 | 
					        this._timeout = timeout;
 | 
				
			||||||
        this._interpreterUrl = interpreterUrl;
 | 
					        this._interpreterUrl = interpreterUrl;
 | 
				
			||||||
        this._filter = filter
 | 
					        this._filter = filter
 | 
				
			||||||
        this._extraScripts = extraScripts;
 | 
					        this._extraScripts = extraScripts;
 | 
				
			||||||
        this._includeMeta = includeMeta;
 | 
					        this._includeMeta = includeMeta;
 | 
				
			||||||
 | 
					        this._relationTracker = relationTracker
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryGeoJson(bounds: Bounds, continuation: ((any, date: Date) => void), onFail: ((reason) => void)): void {
 | 
					    queryGeoJson(bounds: Bounds, continuation: ((any, date: Date) => void), onFail: ((reason) => void)): void {
 | 
				
			||||||
| 
						 | 
					@ -35,6 +39,7 @@ export class Overpass {
 | 
				
			||||||
            console.log("Using testing URL")
 | 
					            console.log("Using testing URL")
 | 
				
			||||||
            query = Overpass.testUrl;
 | 
					            query = Overpass.testUrl;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        const self = this;
 | 
				
			||||||
        Utils.downloadJson(query)
 | 
					        Utils.downloadJson(query)
 | 
				
			||||||
            .then(json => {
 | 
					            .then(json => {
 | 
				
			||||||
                if (json.elements === [] && ((json.remarks ?? json.remark).indexOf("runtime error") >= 0)) {
 | 
					                if (json.elements === [] && ((json.remarks ?? json.remark).indexOf("runtime error") >= 0)) {
 | 
				
			||||||
| 
						 | 
					@ -44,13 +49,15 @@ export class Overpass {
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                ExtractRelations.RegisterRelations(json)
 | 
					                self._relationTracker.RegisterRelations(json)
 | 
				
			||||||
                // @ts-ignore
 | 
					                // @ts-ignore
 | 
				
			||||||
                const geojson = OsmToGeoJson.default(json);
 | 
					                const geojson = OsmToGeoJson.default(json);
 | 
				
			||||||
                const osmTime = new Date(json.osm3s.timestamp_osm_base);
 | 
					                const osmTime = new Date(json.osm3s.timestamp_osm_base);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                continuation(geojson, osmTime);
 | 
					                continuation(geojson, osmTime);
 | 
				
			||||||
            }).catch(onFail)
 | 
					            }).catch(e => {
 | 
				
			||||||
 | 
					            onFail(e);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    buildQuery(bbox: string): string {
 | 
					    buildQuery(bbox: string): string {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import State from "../../State";
 | 
					import State from "../../State";
 | 
				
			||||||
 | 
					import {UIEventSource} from "../UIEventSource";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Relation {
 | 
					export interface Relation {
 | 
				
			||||||
    id: number,
 | 
					    id: number,
 | 
				
			||||||
| 
						 | 
					@ -13,11 +14,15 @@ export interface Relation {
 | 
				
			||||||
    properties: any
 | 
					    properties: any
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class ExtractRelations {
 | 
					export default class RelationsTracker {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static RegisterRelations(overpassJson: any): void {
 | 
					    public knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>(new Map(), "Relation memberships");
 | 
				
			||||||
        const memberships = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(overpassJson))
 | 
					
 | 
				
			||||||
        State.state.knownRelations.setData(memberships)
 | 
					    constructor() {
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public RegisterRelations(overpassJson: any): void {
 | 
				
			||||||
 | 
					        this.UpdateMembershipTable(RelationsTracker.GetRelationElements(overpassJson))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					@ -25,7 +30,7 @@ export default class ExtractRelations {
 | 
				
			||||||
     * @param overpassJson
 | 
					     * @param overpassJson
 | 
				
			||||||
     * @constructor
 | 
					     * @constructor
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public static GetRelationElements(overpassJson: any): Relation[] {
 | 
					    private static GetRelationElements(overpassJson: any): Relation[] {
 | 
				
			||||||
        const relations = overpassJson.elements
 | 
					        const relations = overpassJson.elements
 | 
				
			||||||
            .filter(element => element.type === "relation" && element.tags.type !== "multipolygon")
 | 
					            .filter(element => element.type === "relation" && element.tags.type !== "multipolygon")
 | 
				
			||||||
        for (const relation of relations) {
 | 
					        for (const relation of relations) {
 | 
				
			||||||
| 
						 | 
					@ -39,12 +44,11 @@ export default class ExtractRelations {
 | 
				
			||||||
     * @param relations
 | 
					     * @param relations
 | 
				
			||||||
     * @constructor
 | 
					     * @constructor
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public static BuildMembershipTable(relations: Relation[]): Map<string, { role: string, relation: Relation }[]> {
 | 
					    private UpdateMembershipTable(relations: Relation[]): void {
 | 
				
			||||||
        const memberships = new Map<string, { role: string, relation: Relation }[]>()
 | 
					        const memberships = this.knownRelations.data
 | 
				
			||||||
 | 
					        let changed = false;
 | 
				
			||||||
        for (const relation of relations) {
 | 
					        for (const relation of relations) {
 | 
				
			||||||
            for (const member of relation.members) {
 | 
					            for (const member of relation.members) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
                const role = {
 | 
					                const role = {
 | 
				
			||||||
                    role: member.role,
 | 
					                    role: member.role,
 | 
				
			||||||
                    relation: relation
 | 
					                    relation: relation
 | 
				
			||||||
| 
						 | 
					@ -53,11 +57,21 @@ export default class ExtractRelations {
 | 
				
			||||||
                if (!memberships.has(key)) {
 | 
					                if (!memberships.has(key)) {
 | 
				
			||||||
                    memberships.set(key, [])
 | 
					                    memberships.set(key, [])
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                memberships.get(key).push(role)
 | 
					                const knownRelations = memberships.get(key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const alreadyExists = knownRelations.some(knownRole => {
 | 
				
			||||||
 | 
					                    return knownRole.role === role.role && knownRole.relation === role.relation
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                if (!alreadyExists) {
 | 
				
			||||||
 | 
					                    knownRelations.push(role)
 | 
				
			||||||
 | 
					                    changed = true;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (changed) {
 | 
				
			||||||
 | 
					            this.knownRelations.ping()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return memberships
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@ import Combine from "../UI/Base/Combine";
 | 
				
			||||||
import BaseUIElement from "../UI/BaseUIElement";
 | 
					import BaseUIElement from "../UI/BaseUIElement";
 | 
				
			||||||
import Title from "../UI/Base/Title";
 | 
					import Title from "../UI/Base/Title";
 | 
				
			||||||
import {FixedUiElement} from "../UI/Base/FixedUiElement";
 | 
					import {FixedUiElement} from "../UI/Base/FixedUiElement";
 | 
				
			||||||
 | 
					import CountryCoder from "latlon2country/index";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const cardinalDirections = {
 | 
					const cardinalDirections = {
 | 
				
			||||||
| 
						 | 
					@ -20,7 +21,7 @@ const cardinalDirections = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class SimpleMetaTagger {
 | 
					export default class SimpleMetaTagger {
 | 
				
			||||||
    static coder: any;
 | 
					    private static coder: CountryCoder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
 | 
				
			||||||
    public static readonly objectMetaInfo = new SimpleMetaTagger(
 | 
					    public static readonly objectMetaInfo = new SimpleMetaTagger(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            keys: ["_last_edit:contributor",
 | 
					            keys: ["_last_edit:contributor",
 | 
				
			||||||
| 
						 | 
					@ -84,7 +85,7 @@ export default class SimpleMetaTagger {
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        (feature => {
 | 
					        (feature => {
 | 
				
			||||||
            const units = Utils.NoNull([].concat(...State.state?.layoutToUse?.data?.layers?.map(layer => layer.units ?? [])));
 | 
					            const units = Utils.NoNull([].concat(...State.state?.layoutToUse?.data?.layers?.map(layer => layer.units ?? [])));
 | 
				
			||||||
            if(units.length == 0){
 | 
					            if (units.length == 0) {
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            let rewritten = false;
 | 
					            let rewritten = false;
 | 
				
			||||||
| 
						 | 
					@ -93,7 +94,7 @@ export default class SimpleMetaTagger {
 | 
				
			||||||
                    continue;
 | 
					                    continue;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                for (const unit of units) {
 | 
					                for (const unit of units) {
 | 
				
			||||||
                    if(unit.appliesToKeys === undefined){
 | 
					                    if (unit.appliesToKeys === undefined) {
 | 
				
			||||||
                        console.error("The unit ", unit, "has no appliesToKey defined")
 | 
					                        console.error("The unit ", unit, "has no appliesToKey defined")
 | 
				
			||||||
                        continue
 | 
					                        continue
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
| 
						 | 
					@ -148,7 +149,7 @@ export default class SimpleMetaTagger {
 | 
				
			||||||
            const lat = centerPoint.geometry.coordinates[1];
 | 
					            const lat = centerPoint.geometry.coordinates[1];
 | 
				
			||||||
            const lon = centerPoint.geometry.coordinates[0];
 | 
					            const lon = centerPoint.geometry.coordinates[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => {
 | 
					            SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, (countries: string[]) => {
 | 
				
			||||||
                try {
 | 
					                try {
 | 
				
			||||||
                    const oldCountry = feature.properties["_country"];
 | 
					                    const oldCountry = feature.properties["_country"];
 | 
				
			||||||
                    feature.properties["_country"] = countries[0].trim().toLowerCase();
 | 
					                    feature.properties["_country"] = countries[0].trim().toLowerCase();
 | 
				
			||||||
| 
						 | 
					@ -160,7 +161,7 @@ export default class SimpleMetaTagger {
 | 
				
			||||||
                } catch (e) {
 | 
					                } catch (e) {
 | 
				
			||||||
                    console.warn(e)
 | 
					                    console.warn(e)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            })
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    private static isOpen = new SimpleMetaTagger(
 | 
					    private static isOpen = new SimpleMetaTagger(
 | 
				
			||||||
| 
						 | 
					@ -426,11 +427,7 @@ export default class SimpleMetaTagger {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    static GetCountryCodeFor(lon: number, lat: number, callback: (country: string) => void) {
 | 
					    public static HelpText(): BaseUIElement {
 | 
				
			||||||
        SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, callback)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    static HelpText(): BaseUIElement {
 | 
					 | 
				
			||||||
        const subElements: (string | BaseUIElement)[] = [
 | 
					        const subElements: (string | BaseUIElement)[] = [
 | 
				
			||||||
            new Combine([
 | 
					            new Combine([
 | 
				
			||||||
                new Title("Metatags", 1),
 | 
					                new Title("Metatags", 1),
 | 
				
			||||||
| 
						 | 
					@ -453,7 +450,7 @@ export default class SimpleMetaTagger {
 | 
				
			||||||
        return new Combine(subElements).SetClass("flex-col")
 | 
					        return new Combine(subElements).SetClass("flex-col")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    addMetaTags(features: { feature: any, freshness: Date }[]) {
 | 
					    public addMetaTags(features: { feature: any, freshness: Date }[]) {
 | 
				
			||||||
        for (let i = 0; i < features.length; i++) {
 | 
					        for (let i = 0; i < features.length; i++) {
 | 
				
			||||||
            let feature = features[i];
 | 
					            let feature = features[i];
 | 
				
			||||||
            this._f(feature.feature, i, feature.freshness);
 | 
					            this._f(feature.feature, i, feature.freshness);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -81,9 +81,12 @@ export class UIEventSource<T> {
 | 
				
			||||||
        return this;
 | 
					        return this;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public addCallbackAndRun(callback: ((latestData: T) => void)): UIEventSource<T> {
 | 
					    public addCallbackAndRun(callback: ((latestData: T) => (boolean | void | any))): UIEventSource<T> {
 | 
				
			||||||
        callback(this.data);
 | 
					        const doDeleteCallback = callback(this.data);
 | 
				
			||||||
        return this.addCallback(callback);
 | 
					        if (!doDeleteCallback) {
 | 
				
			||||||
 | 
					            this.addCallback(callback);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return this;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public setData(t: T): UIEventSource<T> {
 | 
					    public setData(t: T): UIEventSource<T> {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@ import {Utils} from "../Utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class Constants {
 | 
					export default class Constants {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static vNumber = "0.9.12";
 | 
					    public static vNumber = "0.10.0";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // The user journey states thresholds when a new feature gets unlocked
 | 
					    // The user journey states thresholds when a new feature gets unlocked
 | 
				
			||||||
    public static userJourney = {
 | 
					    public static userJourney = {
 | 
				
			||||||
| 
						 | 
					@ -26,12 +26,6 @@ export default class Constants {
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    static updateTimeoutSec: number = 30;
 | 
					    static updateTimeoutSec: number = 30;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * If zoom >= useOsmApiAt, then the OSM api will be used directly.
 | 
					 | 
				
			||||||
     * If undefined, use overpass exclusively
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    static useOsmApiAt = undefined;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private static isRetina(): boolean {
 | 
					    private static isRetina(): boolean {
 | 
				
			||||||
        if (Utils.runningFromConsole) {
 | 
					        if (Utils.runningFromConsole) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,9 +59,8 @@ export interface LayerConfigJson {
 | 
				
			||||||
     * NOTE: the previous format was 'overpassTags: AndOrTagConfigJson | string', which is interpreted as a shorthand for source: {osmTags: "key=value"}
 | 
					     * NOTE: the previous format was 'overpassTags: AndOrTagConfigJson | string', which is interpreted as a shorthand for source: {osmTags: "key=value"}
 | 
				
			||||||
     *  While still supported, this is considered deprecated
 | 
					     *  While still supported, this is considered deprecated
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    source: { osmTags: AndOrTagConfigJson | string } |
 | 
					    source: { osmTags: AndOrTagConfigJson | string, overpassScript?: string  } |
 | 
				
			||||||
        { osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number, isOsmCache?: boolean } |
 | 
					        { osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number, isOsmCache?: boolean }
 | 
				
			||||||
        { osmTags: AndOrTagConfigJson | string, overpassScript: string }
 | 
					 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -246,14 +246,6 @@ export default class LayoutConfig {
 | 
				
			||||||
        return icons
 | 
					        return icons
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public LayerIndex(): Map<string, LayerConfig> {
 | 
					 | 
				
			||||||
        const index = new Map<string, LayerConfig>();
 | 
					 | 
				
			||||||
        for (const layer of this.layers) {
 | 
					 | 
				
			||||||
            index.set(layer.id, layer)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return index;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Replaces all the relative image-urls with a fixed image url
 | 
					     * Replaces all the relative image-urls with a fixed image url
 | 
				
			||||||
     * This is to fix loading from external sources
 | 
					     * This is to fix loading from external sources
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,18 +2,18 @@ import {TagsFilter} from "../../Logic/Tags/TagsFilter";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class SourceConfig {
 | 
					export default class SourceConfig {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    osmTags?: TagsFilter;
 | 
					    public readonly osmTags?: TagsFilter;
 | 
				
			||||||
    overpassScript?: string;
 | 
					    public readonly overpassScript?: string;
 | 
				
			||||||
    geojsonSource?: string;
 | 
					    public readonly geojsonSource?: string;
 | 
				
			||||||
    geojsonZoomLevel?: number;
 | 
					    public readonly geojsonZoomLevel?: number;
 | 
				
			||||||
    isOsmCacheLayer: boolean;
 | 
					    public readonly isOsmCacheLayer: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(params: {
 | 
					    constructor(params: {
 | 
				
			||||||
        osmTags?: TagsFilter,
 | 
					        osmTags?: TagsFilter,
 | 
				
			||||||
        overpassScript?: string,
 | 
					        overpassScript?: string,
 | 
				
			||||||
        geojsonSource?: string,
 | 
					        geojsonSource?: string,
 | 
				
			||||||
        isOsmCache?: boolean,
 | 
					        isOsmCache?: boolean,
 | 
				
			||||||
        geojsonSourceLevel?: number
 | 
					        geojsonSourceLevel?: number,
 | 
				
			||||||
    }, context?: string) {
 | 
					    }, context?: string) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let defined = 0;
 | 
					        let defined = 0;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										23
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										23
									
								
								State.ts
									
										
									
									
									
								
							| 
						 | 
					@ -11,16 +11,14 @@ import InstalledThemes from "./Logic/Actors/InstalledThemes";
 | 
				
			||||||
import BaseLayer from "./Models/BaseLayer";
 | 
					import BaseLayer from "./Models/BaseLayer";
 | 
				
			||||||
import Loc from "./Models/Loc";
 | 
					import Loc from "./Models/Loc";
 | 
				
			||||||
import Constants from "./Models/Constants";
 | 
					import Constants from "./Models/Constants";
 | 
				
			||||||
 | 
					 | 
				
			||||||
import OverpassFeatureSource from "./Logic/Actors/OverpassFeatureSource";
 | 
					 | 
				
			||||||
import TitleHandler from "./Logic/Actors/TitleHandler";
 | 
					import TitleHandler from "./Logic/Actors/TitleHandler";
 | 
				
			||||||
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
 | 
					import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
 | 
				
			||||||
import {Relation} from "./Logic/Osm/ExtractRelations";
 | 
					import OsmApiFeatureSource from "./Logic/FeatureSource/Sources/OsmApiFeatureSource";
 | 
				
			||||||
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
 | 
					 | 
				
			||||||
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
 | 
					import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
 | 
				
			||||||
import FilteredLayer from "./Models/FilteredLayer";
 | 
					import FilteredLayer from "./Models/FilteredLayer";
 | 
				
			||||||
import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor";
 | 
					import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor";
 | 
				
			||||||
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
 | 
					import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
 | 
				
			||||||
 | 
					import {BBox} from "./Logic/GeoOperations";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Contains the global state: a bunch of UI-event sources
 | 
					 * Contains the global state: a bunch of UI-event sources
 | 
				
			||||||
| 
						 | 
					@ -57,8 +55,6 @@ export default class State {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public favouriteLayers: UIEventSource<string[]>;
 | 
					    public favouriteLayers: UIEventSource<string[]>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public layerUpdater: OverpassFeatureSource;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public osmApiFeatureSource: OsmApiFeatureSource;
 | 
					    public osmApiFeatureSource: OsmApiFeatureSource;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers");
 | 
					    public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers");
 | 
				
			||||||
| 
						 | 
					@ -71,12 +67,6 @@ export default class State {
 | 
				
			||||||
        "Selected element"
 | 
					        "Selected element"
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Keeps track of relations: which way is part of which other way?
 | 
					 | 
				
			||||||
     * Set by the overpass-updater; used in the metatagging
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public readonly knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>(undefined, "Relation memberships");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public readonly featureSwitchUserbadge: UIEventSource<boolean>;
 | 
					    public readonly featureSwitchUserbadge: UIEventSource<boolean>;
 | 
				
			||||||
    public readonly featureSwitchSearch: UIEventSource<boolean>;
 | 
					    public readonly featureSwitchSearch: UIEventSource<boolean>;
 | 
				
			||||||
    public readonly featureSwitchBackgroundSlection: UIEventSource<boolean>;
 | 
					    public readonly featureSwitchBackgroundSlection: UIEventSource<boolean>;
 | 
				
			||||||
| 
						 | 
					@ -96,6 +86,7 @@ export default class State {
 | 
				
			||||||
    public readonly featureSwitchExportAsPdf: UIEventSource<boolean>;
 | 
					    public readonly featureSwitchExportAsPdf: UIEventSource<boolean>;
 | 
				
			||||||
    public readonly overpassUrl: UIEventSource<string>;
 | 
					    public readonly overpassUrl: UIEventSource<string>;
 | 
				
			||||||
    public readonly overpassTimeout: UIEventSource<number>;
 | 
					    public readonly overpassTimeout: UIEventSource<number>;
 | 
				
			||||||
 | 
					    public readonly overpassMaxZoom: UIEventSource<number> = new UIEventSource<number>(undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public featurePipeline: FeaturePipeline;
 | 
					    public featurePipeline: FeaturePipeline;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -104,6 +95,12 @@ export default class State {
 | 
				
			||||||
     * The map location: currently centered lat, lon and zoom
 | 
					     * The map location: currently centered lat, lon and zoom
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl");
 | 
					    public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The current visible extent of the screen
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public readonly currentBounds = new UIEventSource<BBox>(undefined)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    public backgroundLayer;
 | 
					    public backgroundLayer;
 | 
				
			||||||
    public readonly backgroundLayerId: UIEventSource<string>;
 | 
					    public readonly backgroundLayerId: UIEventSource<string>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -398,7 +395,7 @@ export default class State {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        new ChangeToElementsActor(this.changes, this.allElements)
 | 
					        new ChangeToElementsActor(this.changes, this.allElements)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.osmApiFeatureSource = new OsmApiFeatureSource(Constants.useOsmApiAt, this)
 | 
					        this.osmApiFeatureSource = new OsmApiFeatureSource(this)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        new PendingChangesUploader(this.changes, this.selectedElement);
 | 
					        new PendingChangesUploader(this.changes, this.selectedElement);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,9 @@ export default class Img extends BaseUIElement {
 | 
				
			||||||
        fallbackImage?: string
 | 
					        fallbackImage?: string
 | 
				
			||||||
    }) {
 | 
					    }) {
 | 
				
			||||||
        super();
 | 
					        super();
 | 
				
			||||||
 | 
					        if(src === undefined || src === "undefined"){
 | 
				
			||||||
 | 
					            throw "Undefined src for image"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        this._src = src;
 | 
					        this._src = src;
 | 
				
			||||||
        this._rawSvg = rawSvg;
 | 
					        this._rawSvg = rawSvg;
 | 
				
			||||||
        this._options = options;
 | 
					        this._options = options;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,208 +1,30 @@
 | 
				
			||||||
import BaseUIElement from "../BaseUIElement";
 | 
					import BaseUIElement from "../BaseUIElement";
 | 
				
			||||||
import * as L from "leaflet";
 | 
					 | 
				
			||||||
import {Map} from "leaflet";
 | 
					 | 
				
			||||||
import {UIEventSource} from "../../Logic/UIEventSource";
 | 
					 | 
				
			||||||
import Loc from "../../Models/Loc";
 | 
					import Loc from "../../Models/Loc";
 | 
				
			||||||
import BaseLayer from "../../Models/BaseLayer";
 | 
					import BaseLayer from "../../Models/BaseLayer";
 | 
				
			||||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
 | 
					import {BBox} from "../../Logic/GeoOperations";
 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					import {UIEventSource} from "../../Logic/UIEventSource";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class Minimap extends BaseUIElement {
 | 
					export interface MinimapOptions {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    private static _nextId = 0;
 | 
					 | 
				
			||||||
    public readonly leafletMap: UIEventSource<Map>
 | 
					 | 
				
			||||||
    private readonly _id: string;
 | 
					 | 
				
			||||||
    private readonly _background: UIEventSource<BaseLayer>;
 | 
					 | 
				
			||||||
    private readonly _location: UIEventSource<Loc>;
 | 
					 | 
				
			||||||
    private _isInited = false;
 | 
					 | 
				
			||||||
    private _allowMoving: boolean;
 | 
					 | 
				
			||||||
    private readonly _leafletoptions: any;
 | 
					 | 
				
			||||||
    private readonly _onFullyLoaded: (leaflet: L.Map) => void
 | 
					 | 
				
			||||||
    private readonly _attribution: BaseUIElement | boolean;
 | 
					 | 
				
			||||||
    private readonly _lastClickLocation: UIEventSource<{ lat: number; lon: number }>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    constructor(options?: {
 | 
					 | 
				
			||||||
    background?: UIEventSource<BaseLayer>,
 | 
					    background?: UIEventSource<BaseLayer>,
 | 
				
			||||||
    location?: UIEventSource<Loc>,
 | 
					    location?: UIEventSource<Loc>,
 | 
				
			||||||
 | 
					    bounds?: UIEventSource<BBox>,
 | 
				
			||||||
    allowMoving?: boolean,
 | 
					    allowMoving?: boolean,
 | 
				
			||||||
    leafletOptions?: any,
 | 
					    leafletOptions?: any,
 | 
				
			||||||
    attribution?: BaseUIElement | boolean,
 | 
					    attribution?: BaseUIElement | boolean,
 | 
				
			||||||
    onFullyLoaded?: (leaflet: L.Map) => void,
 | 
					    onFullyLoaded?: (leaflet: L.Map) => void,
 | 
				
			||||||
                    leafletMap?: UIEventSource<Map>,
 | 
					    leafletMap?: UIEventSource<any>,
 | 
				
			||||||
    lastClickLocation?: UIEventSource<{ lat: number, lon: number }>
 | 
					    lastClickLocation?: UIEventSource<{ lat: number, lon: number }>
 | 
				
			||||||
                }
 | 
					}
 | 
				
			||||||
    ) {
 | 
					
 | 
				
			||||||
        super()
 | 
					export default class Minimap {
 | 
				
			||||||
        options = options ?? {}
 | 
					    /**
 | 
				
			||||||
        this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined)
 | 
					     * A stub implementation. The actual implementation is injected later on, but only in the browser.
 | 
				
			||||||
        this._background = options?.background ?? new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
 | 
					     * importing leaflet crashes node-ts, which is pretty annoying considering the fact that a lot of scripts use it
 | 
				
			||||||
        this._location = options?.location ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
 | 
					     */
 | 
				
			||||||
        this._id = "minimap" + Minimap._nextId;
 | 
					
 | 
				
			||||||
        this._allowMoving = options.allowMoving ?? true;
 | 
					    /**
 | 
				
			||||||
        this._leafletoptions = options.leafletOptions ?? {}
 | 
					     * Construct a minimap
 | 
				
			||||||
        this._onFullyLoaded = options.onFullyLoaded
 | 
					     */
 | 
				
			||||||
        this._attribution = options.attribution
 | 
					    public static createMiniMap: (options: MinimapOptions) => BaseUIElement & { readonly leafletMap: UIEventSource<any> }
 | 
				
			||||||
        this._lastClickLocation = options.lastClickLocation;
 | 
					
 | 
				
			||||||
        Minimap._nextId++
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    protected InnerConstructElement(): HTMLElement {
 | 
					 | 
				
			||||||
        const div = document.createElement("div")
 | 
					 | 
				
			||||||
        div.id = this._id;
 | 
					 | 
				
			||||||
        div.style.height = "100%"
 | 
					 | 
				
			||||||
        div.style.width = "100%"
 | 
					 | 
				
			||||||
        div.style.minWidth = "40px"
 | 
					 | 
				
			||||||
        div.style.minHeight = "40px"
 | 
					 | 
				
			||||||
        div.style.position = "relative"
 | 
					 | 
				
			||||||
        const wrapper = document.createElement("div")
 | 
					 | 
				
			||||||
        wrapper.appendChild(div)
 | 
					 | 
				
			||||||
        const self = this;
 | 
					 | 
				
			||||||
        // @ts-ignore
 | 
					 | 
				
			||||||
        const resizeObserver = new ResizeObserver(_ => {
 | 
					 | 
				
			||||||
            self.InitMap();
 | 
					 | 
				
			||||||
            self.leafletMap?.data?.invalidateSize()
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        resizeObserver.observe(div);
 | 
					 | 
				
			||||||
        return wrapper;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private InitMap() {
 | 
					 | 
				
			||||||
        if (this._constructedHtmlElement === undefined) {
 | 
					 | 
				
			||||||
            // This element isn't initialized yet
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (document.getElementById(this._id) === null) {
 | 
					 | 
				
			||||||
            // not yet attached, we probably got some other event
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (this._isInited) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this._isInited = true;
 | 
					 | 
				
			||||||
        const location = this._location;
 | 
					 | 
				
			||||||
        const self = this;
 | 
					 | 
				
			||||||
        let currentLayer = this._background.data.layer()
 | 
					 | 
				
			||||||
        const options = {
 | 
					 | 
				
			||||||
            center: <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0],
 | 
					 | 
				
			||||||
            zoom: location.data?.zoom ?? 2,
 | 
					 | 
				
			||||||
            layers: [currentLayer],
 | 
					 | 
				
			||||||
            zoomControl: false,
 | 
					 | 
				
			||||||
            attributionControl: this._attribution !== undefined,
 | 
					 | 
				
			||||||
            dragging: this._allowMoving,
 | 
					 | 
				
			||||||
            scrollWheelZoom: this._allowMoving,
 | 
					 | 
				
			||||||
            doubleClickZoom: this._allowMoving,
 | 
					 | 
				
			||||||
            keyboard: this._allowMoving,
 | 
					 | 
				
			||||||
            touchZoom: this._allowMoving,
 | 
					 | 
				
			||||||
            // Disabling this breaks the geojson layer - don't ask me why!  zoomAnimation: this._allowMoving,
 | 
					 | 
				
			||||||
            fadeAnimation: this._allowMoving,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Utils.Merge(this._leafletoptions, options)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const map = L.map(this._id, options);
 | 
					 | 
				
			||||||
        if (self._onFullyLoaded !== undefined) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            currentLayer.on("load", () => {
 | 
					 | 
				
			||||||
                console.log("Fully loaded all tiles!")
 | 
					 | 
				
			||||||
                self._onFullyLoaded(map)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Users are not allowed to zoom to the 'copies' on the left and the right, stuff goes wrong then
 | 
					 | 
				
			||||||
        // We give a bit of leeway for people on the edges
 | 
					 | 
				
			||||||
        // Also see: https://www.reddit.com/r/openstreetmap/comments/ih4zzc/mapcomplete_a_new_easytouse_editor/g31ubyv/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        map.setMaxBounds(
 | 
					 | 
				
			||||||
            [[-100, -200], [100, 200]]
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (this._attribution !== undefined) {
 | 
					 | 
				
			||||||
            if (this._attribution === true) {
 | 
					 | 
				
			||||||
                map.attributionControl.setPrefix(false)
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                map.attributionControl.setPrefix(
 | 
					 | 
				
			||||||
                    "<span id='leaflet-attribution'></span>");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this._background.addCallbackAndRun(layer => {
 | 
					 | 
				
			||||||
            const newLayer = layer.layer()
 | 
					 | 
				
			||||||
            if (currentLayer !== undefined) {
 | 
					 | 
				
			||||||
                map.removeLayer(currentLayer);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            currentLayer = newLayer;
 | 
					 | 
				
			||||||
            if (self._onFullyLoaded !== undefined) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                currentLayer.on("load", () => {
 | 
					 | 
				
			||||||
                    console.log("Fully loaded all tiles!")
 | 
					 | 
				
			||||||
                    self._onFullyLoaded(map)
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            map.addLayer(newLayer);
 | 
					 | 
				
			||||||
            map.setMaxZoom(layer.max_zoom ?? map.getMaxZoom())
 | 
					 | 
				
			||||||
            if (self._attribution !== true && self._attribution !== false) {
 | 
					 | 
				
			||||||
                self._attribution?.AttachTo('leaflet-attribution')
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let isRecursing = false;
 | 
					 | 
				
			||||||
        map.on("moveend", function () {
 | 
					 | 
				
			||||||
            if (isRecursing) {
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (map.getZoom() === location.data.zoom &&
 | 
					 | 
				
			||||||
                map.getCenter().lat === location.data.lat &&
 | 
					 | 
				
			||||||
                map.getCenter().lng === location.data.lon) {
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            location.data.zoom = map.getZoom();
 | 
					 | 
				
			||||||
            location.data.lat = map.getCenter().lat;
 | 
					 | 
				
			||||||
            location.data.lon = map.getCenter().lng;
 | 
					 | 
				
			||||||
            isRecursing = true;
 | 
					 | 
				
			||||||
            location.ping();
 | 
					 | 
				
			||||||
            isRecursing = false; // This is ugly, I know
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        location.addCallback(loc => {
 | 
					 | 
				
			||||||
            const mapLoc = map.getCenter()
 | 
					 | 
				
			||||||
            const dlat = Math.abs(loc.lat - mapLoc[0])
 | 
					 | 
				
			||||||
            const dlon = Math.abs(loc.lon - mapLoc[1])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (dlat < 0.000001 && dlon < 0.000001 && map.getZoom() === loc.zoom) {
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            map.setView([loc.lat, loc.lon], loc.zoom)
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        location.map(loc => loc.zoom)
 | 
					 | 
				
			||||||
            .addCallback(zoom => {
 | 
					 | 
				
			||||||
                if (Math.abs(map.getZoom() - zoom) > 0.1) {
 | 
					 | 
				
			||||||
                    map.setZoom(zoom, {});
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (this._lastClickLocation) {
 | 
					 | 
				
			||||||
            const lastClickLocation = this._lastClickLocation
 | 
					 | 
				
			||||||
            map.on("click", function (e) {
 | 
					 | 
				
			||||||
                // @ts-ignore
 | 
					 | 
				
			||||||
                lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng})
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            map.on("contextmenu", function (e) {
 | 
					 | 
				
			||||||
                // @ts-ignore
 | 
					 | 
				
			||||||
                lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng});
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.leafletMap.setData(map)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,215 @@
 | 
				
			||||||
 | 
					import {Utils} from "../../Utils";
 | 
				
			||||||
 | 
					import BaseUIElement from "../BaseUIElement";
 | 
				
			||||||
 | 
					import {UIEventSource} from "../../Logic/UIEventSource";
 | 
				
			||||||
 | 
					import Loc from "../../Models/Loc";
 | 
				
			||||||
 | 
					import BaseLayer from "../../Models/BaseLayer";
 | 
				
			||||||
 | 
					import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
 | 
				
			||||||
 | 
					import {BBox} from "../../Logic/GeoOperations";
 | 
				
			||||||
 | 
					import * as L from "leaflet";
 | 
				
			||||||
 | 
					import {Map} from "leaflet";
 | 
				
			||||||
 | 
					import Minimap, {MinimapOptions} from "./Minimap";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class MinimapImplementation extends BaseUIElement {
 | 
				
			||||||
 | 
					    private static _nextId = 0;
 | 
				
			||||||
 | 
					    public readonly leafletMap: UIEventSource<Map>
 | 
				
			||||||
 | 
					    private readonly _id: string;
 | 
				
			||||||
 | 
					    private readonly _background: UIEventSource<BaseLayer>;
 | 
				
			||||||
 | 
					    private readonly _location: UIEventSource<Loc>;
 | 
				
			||||||
 | 
					    private _isInited = false;
 | 
				
			||||||
 | 
					    private _allowMoving: boolean;
 | 
				
			||||||
 | 
					    private readonly _leafletoptions: any;
 | 
				
			||||||
 | 
					    private readonly _onFullyLoaded: (leaflet: L.Map) => void
 | 
				
			||||||
 | 
					    private readonly _attribution: BaseUIElement | boolean;
 | 
				
			||||||
 | 
					    private readonly _lastClickLocation: UIEventSource<{ lat: number; lon: number }>;
 | 
				
			||||||
 | 
					    private readonly _bounds: UIEventSource<BBox> | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private constructor(options: MinimapOptions) {
 | 
				
			||||||
 | 
					        super()
 | 
				
			||||||
 | 
					        options = options ?? {}
 | 
				
			||||||
 | 
					        this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined)
 | 
				
			||||||
 | 
					        this._background = options?.background ?? new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
 | 
				
			||||||
 | 
					        this._location = options?.location ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
 | 
				
			||||||
 | 
					        this._bounds = options?.bounds;
 | 
				
			||||||
 | 
					        this._id = "minimap" + MinimapImplementation._nextId;
 | 
				
			||||||
 | 
					        this._allowMoving = options.allowMoving ?? true;
 | 
				
			||||||
 | 
					        this._leafletoptions = options.leafletOptions ?? {}
 | 
				
			||||||
 | 
					        this._onFullyLoaded = options.onFullyLoaded
 | 
				
			||||||
 | 
					        this._attribution = options.attribution
 | 
				
			||||||
 | 
					        this._lastClickLocation = options.lastClickLocation;
 | 
				
			||||||
 | 
					        MinimapImplementation._nextId++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static initialize() {
 | 
				
			||||||
 | 
					        Minimap.createMiniMap = options => new MinimapImplementation(options)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected InnerConstructElement(): HTMLElement {
 | 
				
			||||||
 | 
					        const div = document.createElement("div")
 | 
				
			||||||
 | 
					        div.id = this._id;
 | 
				
			||||||
 | 
					        div.style.height = "100%"
 | 
				
			||||||
 | 
					        div.style.width = "100%"
 | 
				
			||||||
 | 
					        div.style.minWidth = "40px"
 | 
				
			||||||
 | 
					        div.style.minHeight = "40px"
 | 
				
			||||||
 | 
					        div.style.position = "relative"
 | 
				
			||||||
 | 
					        const wrapper = document.createElement("div")
 | 
				
			||||||
 | 
					        wrapper.appendChild(div)
 | 
				
			||||||
 | 
					        const self = this;
 | 
				
			||||||
 | 
					        // @ts-ignore
 | 
				
			||||||
 | 
					        const resizeObserver = new ResizeObserver(_ => {
 | 
				
			||||||
 | 
					            self.InitMap();
 | 
				
			||||||
 | 
					            self.leafletMap?.data?.invalidateSize()
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        resizeObserver.observe(div);
 | 
				
			||||||
 | 
					        return wrapper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private InitMap() {
 | 
				
			||||||
 | 
					        if (this._constructedHtmlElement === undefined) {
 | 
				
			||||||
 | 
					            // This element isn't initialized yet
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (document.getElementById(this._id) === null) {
 | 
				
			||||||
 | 
					            // not yet attached, we probably got some other event
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this._isInited) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this._isInited = true;
 | 
				
			||||||
 | 
					        const location = this._location;
 | 
				
			||||||
 | 
					        const self = this;
 | 
				
			||||||
 | 
					        let currentLayer = this._background.data.layer()
 | 
				
			||||||
 | 
					        const options = {
 | 
				
			||||||
 | 
					            center: <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0],
 | 
				
			||||||
 | 
					            zoom: location.data?.zoom ?? 2,
 | 
				
			||||||
 | 
					            layers: [currentLayer],
 | 
				
			||||||
 | 
					            zoomControl: false,
 | 
				
			||||||
 | 
					            attributionControl: this._attribution !== undefined,
 | 
				
			||||||
 | 
					            dragging: this._allowMoving,
 | 
				
			||||||
 | 
					            scrollWheelZoom: this._allowMoving,
 | 
				
			||||||
 | 
					            doubleClickZoom: this._allowMoving,
 | 
				
			||||||
 | 
					            keyboard: this._allowMoving,
 | 
				
			||||||
 | 
					            touchZoom: this._allowMoving,
 | 
				
			||||||
 | 
					            // Disabling this breaks the geojson layer - don't ask me why!  zoomAnimation: this._allowMoving,
 | 
				
			||||||
 | 
					            fadeAnimation: this._allowMoving,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Utils.Merge(this._leafletoptions, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const map = L.map(this._id, options);
 | 
				
			||||||
 | 
					        if (self._onFullyLoaded !== undefined) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            currentLayer.on("load", () => {
 | 
				
			||||||
 | 
					                console.log("Fully loaded all tiles!")
 | 
				
			||||||
 | 
					                self._onFullyLoaded(map)
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Users are not allowed to zoom to the 'copies' on the left and the right, stuff goes wrong then
 | 
				
			||||||
 | 
					        // We give a bit of leeway for people on the edges
 | 
				
			||||||
 | 
					        // Also see: https://www.reddit.com/r/openstreetmap/comments/ih4zzc/mapcomplete_a_new_easytouse_editor/g31ubyv/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        map.setMaxBounds(
 | 
				
			||||||
 | 
					            [[-100, -200], [100, 200]]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this._attribution !== undefined) {
 | 
				
			||||||
 | 
					            if (this._attribution === true) {
 | 
				
			||||||
 | 
					                map.attributionControl.setPrefix(false)
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                map.attributionControl.setPrefix(
 | 
				
			||||||
 | 
					                    "<span id='leaflet-attribution'></span>");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this._background.addCallbackAndRun(layer => {
 | 
				
			||||||
 | 
					            const newLayer = layer.layer()
 | 
				
			||||||
 | 
					            if (currentLayer !== undefined) {
 | 
				
			||||||
 | 
					                map.removeLayer(currentLayer);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            currentLayer = newLayer;
 | 
				
			||||||
 | 
					            if (self._onFullyLoaded !== undefined) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                currentLayer.on("load", () => {
 | 
				
			||||||
 | 
					                    console.log("Fully loaded all tiles!")
 | 
				
			||||||
 | 
					                    self._onFullyLoaded(map)
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            map.addLayer(newLayer);
 | 
				
			||||||
 | 
					            map.setMaxZoom(layer.max_zoom ?? map.getMaxZoom())
 | 
				
			||||||
 | 
					            if (self._attribution !== true && self._attribution !== false) {
 | 
				
			||||||
 | 
					                self._attribution?.AttachTo('leaflet-attribution')
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let isRecursing = false;
 | 
				
			||||||
 | 
					        map.on("moveend", function () {
 | 
				
			||||||
 | 
					            if (isRecursing) {
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (map.getZoom() === location.data.zoom &&
 | 
				
			||||||
 | 
					                map.getCenter().lat === location.data.lat &&
 | 
				
			||||||
 | 
					                map.getCenter().lng === location.data.lon) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            location.data.zoom = map.getZoom();
 | 
				
			||||||
 | 
					            location.data.lat = map.getCenter().lat;
 | 
				
			||||||
 | 
					            location.data.lon = map.getCenter().lng;
 | 
				
			||||||
 | 
					            isRecursing = true;
 | 
				
			||||||
 | 
					            location.ping();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (self._bounds !== undefined) {
 | 
				
			||||||
 | 
					                self._bounds.setData(BBox.fromLeafletBounds(map.getBounds()))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            isRecursing = false; // This is ugly, I know
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        location.addCallback(loc => {
 | 
				
			||||||
 | 
					            const mapLoc = map.getCenter()
 | 
				
			||||||
 | 
					            const dlat = Math.abs(loc.lat - mapLoc[0])
 | 
				
			||||||
 | 
					            const dlon = Math.abs(loc.lon - mapLoc[1])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (dlat < 0.000001 && dlon < 0.000001 && map.getZoom() === loc.zoom) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            map.setView([loc.lat, loc.lon], loc.zoom)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        location.map(loc => loc.zoom)
 | 
				
			||||||
 | 
					            .addCallback(zoom => {
 | 
				
			||||||
 | 
					                if (Math.abs(map.getZoom() - zoom) > 0.1) {
 | 
				
			||||||
 | 
					                    map.setZoom(zoom, {});
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (self._bounds !== undefined) {
 | 
				
			||||||
 | 
					            self._bounds.setData(BBox.fromLeafletBounds(map.getBounds()))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this._lastClickLocation) {
 | 
				
			||||||
 | 
					            const lastClickLocation = this._lastClickLocation
 | 
				
			||||||
 | 
					            map.on("click", function (e) {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng})
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            map.on("contextmenu", function (e) {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng});
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.leafletMap.setData(map)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -32,7 +32,7 @@ export default class AllDownloads extends ScrollableFullScreen {
 | 
				
			||||||
                    freeDivId: "belowmap",
 | 
					                    freeDivId: "belowmap",
 | 
				
			||||||
                    background: State.state.backgroundLayer,
 | 
					                    background: State.state.backgroundLayer,
 | 
				
			||||||
                    location: State.state.locationControl,
 | 
					                    location: State.state.locationControl,
 | 
				
			||||||
                    features: State.state.featurePipeline.features,
 | 
					                    features: State.state.featurePipeline,
 | 
				
			||||||
                    layout: State.state.layoutToUse,
 | 
					                    layout: State.state.layoutToUse,
 | 
				
			||||||
                }).isRunning.addCallbackAndRun(isRunning => isExporting.setData(isRunning))
 | 
					                }).isRunning.addCallbackAndRun(isRunning => isExporting.setData(isRunning))
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,9 +5,9 @@ import {UIEventSource} from "../../Logic/UIEventSource";
 | 
				
			||||||
import UserDetails from "../../Logic/Osm/OsmConnection";
 | 
					import UserDetails from "../../Logic/Osm/OsmConnection";
 | 
				
			||||||
import Constants from "../../Models/Constants";
 | 
					import Constants from "../../Models/Constants";
 | 
				
			||||||
import Loc from "../../Models/Loc";
 | 
					import Loc from "../../Models/Loc";
 | 
				
			||||||
import * as L from "leaflet"
 | 
					 | 
				
			||||||
import {VariableUiElement} from "../Base/VariableUIElement";
 | 
					import {VariableUiElement} from "../Base/VariableUIElement";
 | 
				
			||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
 | 
					import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
 | 
				
			||||||
 | 
					import {BBox} from "../../Logic/GeoOperations";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The bottom right attribution panel in the leaflet map
 | 
					 * The bottom right attribution panel in the leaflet map
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,7 @@ export default class Attribution extends Combine {
 | 
				
			||||||
     constructor(location: UIEventSource<Loc>,
 | 
					     constructor(location: UIEventSource<Loc>,
 | 
				
			||||||
                userDetails: UIEventSource<UserDetails>,
 | 
					                userDetails: UIEventSource<UserDetails>,
 | 
				
			||||||
                layoutToUse: UIEventSource<LayoutConfig>,
 | 
					                layoutToUse: UIEventSource<LayoutConfig>,
 | 
				
			||||||
                leafletMap: UIEventSource<L.Map>) {
 | 
					                currentBounds: UIEventSource<BBox>) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const mapComplete = new Link(`Mapcomplete ${Constants.vNumber}`, 'https://github.com/pietervdvn/MapComplete', true);
 | 
					        const mapComplete = new Link(`Mapcomplete ${Constants.vNumber}`, 'https://github.com/pietervdvn/MapComplete', true);
 | 
				
			||||||
        const reportBug = new Link(Svg.bug_ui().SetClass("small-image"), "https://github.com/pietervdvn/MapComplete/issues", true);
 | 
					        const reportBug = new Link(Svg.bug_ui().SetClass("small-image"), "https://github.com/pietervdvn/MapComplete/issues", true);
 | 
				
			||||||
| 
						 | 
					@ -43,7 +43,7 @@ export default class Attribution extends Combine {
 | 
				
			||||||
                    if (userDetails.csCount < Constants.userJourney.tagsVisibleAndWikiLinked) {
 | 
					                    if (userDetails.csCount < Constants.userJourney.tagsVisibleAndWikiLinked) {
 | 
				
			||||||
                        return undefined;
 | 
					                        return undefined;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    const bounds: any = leafletMap?.data?.getBounds();
 | 
					                    const bounds: any = currentBounds.data;
 | 
				
			||||||
                    if (bounds === undefined) {
 | 
					                    if (bounds === undefined) {
 | 
				
			||||||
                        return undefined
 | 
					                        return undefined
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
| 
						 | 
					@ -55,7 +55,7 @@ export default class Attribution extends Combine {
 | 
				
			||||||
                    const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
 | 
					                    const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
 | 
				
			||||||
                    return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true);
 | 
					                    return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true);
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                [location, leafletMap]
 | 
					                [location, currentBounds]
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]);
 | 
					        super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,10 +26,13 @@ export default class AttributionPanel extends Combine {
 | 
				
			||||||
            ((layoutToUse.data.maintainer ?? "") == "") ? "" : Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.data.maintainer}),
 | 
					            ((layoutToUse.data.maintainer ?? "") == "") ? "" : Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.data.maintainer}),
 | 
				
			||||||
            layoutToUse.data.credits,
 | 
					            layoutToUse.data.credits,
 | 
				
			||||||
            "<br/>",
 | 
					            "<br/>",
 | 
				
			||||||
            new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, State.state.leafletMap),
 | 
					            new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, State.state.currentBounds),
 | 
				
			||||||
            "<br/>",
 | 
					            "<br/>",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            new VariableUiElement(contributions.map(contributions => {
 | 
					            new VariableUiElement(contributions.map(contributions => {
 | 
				
			||||||
 | 
					                if(contributions === undefined){
 | 
				
			||||||
 | 
					                    return ""
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
                const sorted = Array.from(contributions, ([name, value]) => ({
 | 
					                const sorted = Array.from(contributions, ([name, value]) => ({
 | 
				
			||||||
                    name,
 | 
					                    name,
 | 
				
			||||||
                    value
 | 
					                    value
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,54 +2,113 @@ import {SubtleButton} from "../Base/SubtleButton";
 | 
				
			||||||
import Svg from "../../Svg";
 | 
					import Svg from "../../Svg";
 | 
				
			||||||
import Translations from "../i18n/Translations";
 | 
					import Translations from "../i18n/Translations";
 | 
				
			||||||
import State from "../../State";
 | 
					import State from "../../State";
 | 
				
			||||||
import {FeatureSourceUtils} from "../../Logic/FeatureSource/FeatureSource";
 | 
					 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					import {Utils} from "../../Utils";
 | 
				
			||||||
import Combine from "../Base/Combine";
 | 
					import Combine from "../Base/Combine";
 | 
				
			||||||
import CheckBoxes from "../Input/Checkboxes";
 | 
					import CheckBoxes from "../Input/Checkboxes";
 | 
				
			||||||
import {GeoOperations} from "../../Logic/GeoOperations";
 | 
					import {BBox, GeoOperations} from "../../Logic/GeoOperations";
 | 
				
			||||||
import Toggle from "../Input/Toggle";
 | 
					import Toggle from "../Input/Toggle";
 | 
				
			||||||
import Title from "../Base/Title";
 | 
					import Title from "../Base/Title";
 | 
				
			||||||
 | 
					import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
 | 
				
			||||||
 | 
					import {UIEventSource} from "../../Logic/UIEventSource";
 | 
				
			||||||
 | 
					import SimpleMetaTagger from "../../Logic/SimpleMetaTagger";
 | 
				
			||||||
 | 
					import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
 | 
				
			||||||
 | 
					import {meta} from "@turf/turf";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class DownloadPanel extends Toggle {
 | 
					export class DownloadPanel extends Toggle {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        const state: {
 | 
				
			||||||
 | 
					            featurePipeline: FeaturePipeline,
 | 
				
			||||||
 | 
					            layoutToUse: UIEventSource<LayoutConfig>,
 | 
				
			||||||
 | 
					            currentBounds: UIEventSource<BBox>
 | 
				
			||||||
 | 
					        } = State.state
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const t = Translations.t.general.download
 | 
					        const t = Translations.t.general.download
 | 
				
			||||||
        const somethingLoaded = State.state.featurePipeline.features.map(features => features.length > 0);
 | 
					        const name = State.state.layoutToUse.data.id;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        const includeMetaToggle = new CheckBoxes([t.includeMetaData.Clone()])
 | 
					        const includeMetaToggle = new CheckBoxes([t.includeMetaData.Clone()])
 | 
				
			||||||
        const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0)
 | 
					        const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        const buttonGeoJson = new SubtleButton(Svg.floppy_ui(),
 | 
					        const buttonGeoJson = new SubtleButton(Svg.floppy_ui(),
 | 
				
			||||||
            new Combine([t.downloadGeojson.Clone().SetClass("font-bold"),
 | 
					            new Combine([t.downloadGeojson.Clone().SetClass("font-bold"),
 | 
				
			||||||
                t.downloadGeoJsonHelper.Clone()]).SetClass("flex flex-col"))
 | 
					                t.downloadGeoJsonHelper.Clone()]).SetClass("flex flex-col"))
 | 
				
			||||||
            .onClick(() => {
 | 
					            .onClick(() => {
 | 
				
			||||||
                const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline, {metadata: metaisIncluded.data})
 | 
					                const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data)
 | 
				
			||||||
                const name = State.state.layoutToUse.data.id;
 | 
					                Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, "  "),
 | 
				
			||||||
                Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson),
 | 
					 | 
				
			||||||
                    `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, {
 | 
					                    `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, {
 | 
				
			||||||
                        mimetype: "application/vnd.geo+json"
 | 
					                        mimetype: "application/vnd.geo+json"
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const buttonCSV = new SubtleButton(Svg.floppy_ui(), new Combine(
 | 
					        const buttonCSV = new SubtleButton(Svg.floppy_ui(), new Combine(
 | 
				
			||||||
            [t.downloadCSV.Clone().SetClass("font-bold"),
 | 
					            [t.downloadCSV.Clone().SetClass("font-bold"),
 | 
				
			||||||
                t.downloadCSVHelper.Clone()]).SetClass("flex flex-col"))
 | 
					                t.downloadCSVHelper.Clone()]).SetClass("flex flex-col"))
 | 
				
			||||||
            .onClick(() => {
 | 
					            .onClick(() => {
 | 
				
			||||||
                const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline, {metadata: metaisIncluded.data})
 | 
					                const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data)
 | 
				
			||||||
                const csv = GeoOperations.toCSV(geojson.features)
 | 
					                const csv = GeoOperations.toCSV(geojson.features)
 | 
				
			||||||
                const name = State.state.layoutToUse.data.id;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Utils.offerContentsAsDownloadableFile(csv,
 | 
					                Utils.offerContentsAsDownloadableFile(csv,
 | 
				
			||||||
                    `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`, {
 | 
					                    `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`, {
 | 
				
			||||||
                        mimetype: "text/csv"
 | 
					                        mimetype: "text/csv"
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const downloadButtons = new Combine(
 | 
					        const downloadButtons = new Combine(
 | 
				
			||||||
            [new Title(t.title), buttonGeoJson, buttonCSV, includeMetaToggle, t.licenseInfo.Clone().SetClass("link-underline")])
 | 
					            [new Title(t.title),
 | 
				
			||||||
 | 
					                buttonGeoJson, 
 | 
				
			||||||
 | 
					                buttonCSV,
 | 
				
			||||||
 | 
					                includeMetaToggle, 
 | 
				
			||||||
 | 
					                t.licenseInfo.Clone().SetClass("link-underline")])
 | 
				
			||||||
            .SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4")
 | 
					            .SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        super(
 | 
					        super(
 | 
				
			||||||
            downloadButtons,
 | 
					            downloadButtons,
 | 
				
			||||||
            t.noDataLoaded.Clone(),
 | 
					            t.noDataLoaded.Clone(),
 | 
				
			||||||
            somethingLoaded)
 | 
					            state.featurePipeline.somethingLoaded)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static getCleanGeoJson(state: {
 | 
				
			||||||
 | 
					        featurePipeline: FeaturePipeline,
 | 
				
			||||||
 | 
					        currentBounds: UIEventSource<BBox>
 | 
				
			||||||
 | 
					    }, includeMetaData: boolean) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resultFeatures = []
 | 
				
			||||||
 | 
					        const featureList = state.featurePipeline.GetAllFeaturesWithin(state.currentBounds.data);
 | 
				
			||||||
 | 
					        for (const tile of featureList) {
 | 
				
			||||||
 | 
					            for (const feature of tile) {
 | 
				
			||||||
 | 
					                const cleaned = {
 | 
				
			||||||
 | 
					                    type: feature.type,
 | 
				
			||||||
 | 
					                    geometry: feature.geometry,
 | 
				
			||||||
 | 
					                    properties: {...feature.properties}
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (!includeMetaData) {
 | 
				
			||||||
 | 
					                    for (const key in cleaned.properties) {
 | 
				
			||||||
 | 
					                        if (key === "_lon" || key === "_lat") {
 | 
				
			||||||
 | 
					                            continue;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        if (key.startsWith("_")) {
 | 
				
			||||||
 | 
					                            delete feature.properties[key]
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const datedKeys = [].concat(SimpleMetaTagger.metatags.filter(tagging => tagging.includesDates).map(tagging => tagging.keys))
 | 
				
			||||||
 | 
					                for (const key of datedKeys) {
 | 
				
			||||||
 | 
					                    delete feature.properties[key]
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                resultFeatures.push(feature)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            type:"FeatureCollection",
 | 
				
			||||||
 | 
					            features: featureList
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,7 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen";
 | 
				
			||||||
import BaseUIElement from "../BaseUIElement";
 | 
					import BaseUIElement from "../BaseUIElement";
 | 
				
			||||||
import Toggle from "../Input/Toggle";
 | 
					import Toggle from "../Input/Toggle";
 | 
				
			||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
 | 
					import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
 | 
				
			||||||
 | 
					import {Utils} from "../../Utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
 | 
					export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -62,9 +63,15 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)
 | 
					        const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)
 | 
				
			||||||
        const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)]
 | 
					        const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const now = new Date()
 | 
				
			||||||
 | 
					        const date = now.getFullYear()+"-"+Utils.TwoDigits(now.getMonth()+1)+"-"+Utils.TwoDigits(now.getDate())
 | 
				
			||||||
 | 
					        const osmcha_link = `https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%22${date}%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D`
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        tabsWithAboutMc.push({
 | 
					        tabsWithAboutMc.push({
 | 
				
			||||||
                header: Svg.help,
 | 
					                header: Svg.help,
 | 
				
			||||||
                content: new Combine([Translations.t.general.aboutMapcomplete.Clone(), "<br/>Version " + Constants.vNumber])
 | 
					                content: new Combine([Translations.t.general.aboutMapcomplete.Clone()
 | 
				
			||||||
 | 
					                    .Subs({"osmcha_link": osmcha_link}), "<br/>Version " + Constants.vNumber])
 | 
				
			||||||
                    .SetClass("link-underline")
 | 
					                    .SetClass("link-underline")
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,7 +52,7 @@ export default class ImportButton extends Toggle {
 | 
				
			||||||
        const withLoadingCheck = new Toggle(
 | 
					        const withLoadingCheck = new Toggle(
 | 
				
			||||||
            t.stillLoading,
 | 
					            t.stillLoading,
 | 
				
			||||||
            new Combine([button, appliedTags]).SetClass("flex flex-col"),
 | 
					            new Combine([button, appliedTags]).SetClass("flex flex-col"),
 | 
				
			||||||
            State.state.layerUpdater.runningQuery
 | 
					            State.state.featurePipeline.runningQuery
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        super(t.hasBeenImported, withLoadingCheck, isImported)
 | 
					        super(t.hasBeenImported, withLoadingCheck, isImported)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,18 +9,21 @@ import MapControlButton from "../MapControlButton";
 | 
				
			||||||
import Svg from "../../Svg";
 | 
					import Svg from "../../Svg";
 | 
				
			||||||
import AllDownloads from "./AllDownloads";
 | 
					import AllDownloads from "./AllDownloads";
 | 
				
			||||||
import FilterView from "./FilterView";
 | 
					import FilterView from "./FilterView";
 | 
				
			||||||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
 | 
					import {UIEventSource} from "../../Logic/UIEventSource";
 | 
				
			||||||
 | 
					import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
 | 
				
			||||||
 | 
					import {BBox} from "../../Logic/GeoOperations";
 | 
				
			||||||
 | 
					import Loc from "../../Models/Loc";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class LeftControls extends Combine {
 | 
					export default class LeftControls extends Combine {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(featureSource: FeatureSource) {
 | 
					    constructor(state: {featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc>}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const toggledCopyright = new ScrollableFullScreen(
 | 
					        const toggledCopyright = new ScrollableFullScreen(
 | 
				
			||||||
            () => Translations.t.general.attribution.attributionTitle.Clone(),
 | 
					            () => Translations.t.general.attribution.attributionTitle.Clone(),
 | 
				
			||||||
            () =>
 | 
					            () =>
 | 
				
			||||||
                new AttributionPanel(
 | 
					                new AttributionPanel(
 | 
				
			||||||
                    State.state.layoutToUse,
 | 
					                    State.state.layoutToUse,
 | 
				
			||||||
                    new ContributorCount(featureSource).Contributors
 | 
					                    new ContributorCount(state).Contributors
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
            undefined
 | 
					            undefined
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,10 +65,6 @@ export default class SimpleAddUI extends Toggle {
 | 
				
			||||||
            State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
 | 
					            State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
 | 
				
			||||||
                newElementAction.newElementId
 | 
					                newElementAction.newElementId
 | 
				
			||||||
            ))
 | 
					            ))
 | 
				
			||||||
            console.log("Did set selected element to", State.state.allElements.ContainingFeatures.get(
 | 
					 | 
				
			||||||
                newElementAction.newElementId
 | 
					 | 
				
			||||||
            ))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const addUi = new VariableUiElement(
 | 
					        const addUi = new VariableUiElement(
 | 
				
			||||||
| 
						 | 
					@ -104,7 +100,7 @@ export default class SimpleAddUI extends Toggle {
 | 
				
			||||||
                    new Toggle(
 | 
					                    new Toggle(
 | 
				
			||||||
                        Translations.t.general.add.stillLoading.Clone().SetClass("alert"),
 | 
					                        Translations.t.general.add.stillLoading.Clone().SetClass("alert"),
 | 
				
			||||||
                        addUi,
 | 
					                        addUi,
 | 
				
			||||||
                        State.state.layerUpdater.runningQuery
 | 
					                        State.state.featurePipeline.runningQuery
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"),
 | 
					                    Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"),
 | 
				
			||||||
                    State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
 | 
					                    State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
 | 
				
			||||||
| 
						 | 
					@ -150,7 +146,6 @@ export default class SimpleAddUI extends Toggle {
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const tags = TagUtils.KVtoProperties(preset.tags ?? []);
 | 
					            const tags = TagUtils.KVtoProperties(preset.tags ?? []);
 | 
				
			||||||
            console.log("Opening precise input ", preset.preciseInput, "with tags", tags)
 | 
					 | 
				
			||||||
            preciseInput = new LocationInput({
 | 
					            preciseInput = new LocationInput({
 | 
				
			||||||
                mapBackground: backgroundLayer,
 | 
					                mapBackground: backgroundLayer,
 | 
				
			||||||
                centerLocation: locationSrc,
 | 
					                centerLocation: locationSrc,
 | 
				
			||||||
| 
						 | 
					@ -215,10 +210,7 @@ export default class SimpleAddUI extends Toggle {
 | 
				
			||||||
        const disableFiltersOrConfirm = new Toggle(
 | 
					        const disableFiltersOrConfirm = new Toggle(
 | 
				
			||||||
            openLayerOrConfirm,
 | 
					            openLayerOrConfirm,
 | 
				
			||||||
            disableFilter,
 | 
					            disableFilter,
 | 
				
			||||||
            preset.layerToAddTo.appliedFilters.map(filters => {
 | 
					            preset.layerToAddTo.appliedFilters.map(filters => filters === undefined || filters.normalize().and.length === 0)
 | 
				
			||||||
                console.log("Current filters are ", filters)
 | 
					 | 
				
			||||||
                return filters === undefined || filters.normalize().and.length === 0;
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ export default class CenterMessageBox extends VariableUiElement {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
        const state = State.state;
 | 
					        const state = State.state;
 | 
				
			||||||
        const updater = State.state.layerUpdater;
 | 
					        const updater = State.state.featurePipeline;
 | 
				
			||||||
        const t = Translations.t.centerMessage;
 | 
					        const t = Translations.t.centerMessage;
 | 
				
			||||||
        const message = updater.runningQuery.map(
 | 
					        const message = updater.runningQuery.map(
 | 
				
			||||||
            isRunning => {
 | 
					            isRunning => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,19 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import jsPDF from "jspdf";
 | 
				
			||||||
 | 
					import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter";
 | 
				
			||||||
 | 
					import {UIEventSource} from "../Logic/UIEventSource";
 | 
				
			||||||
 | 
					import Minimap from "./Base/Minimap";
 | 
				
			||||||
 | 
					import Loc from "../Models/Loc";
 | 
				
			||||||
 | 
					import {BBox} from "../Logic/GeoOperations";
 | 
				
			||||||
 | 
					import BaseLayer from "../Models/BaseLayer";
 | 
				
			||||||
 | 
					import {FixedUiElement} from "./Base/FixedUiElement";
 | 
				
			||||||
 | 
					import Translations from "./i18n/Translations";
 | 
				
			||||||
 | 
					import State from "../State";
 | 
				
			||||||
 | 
					import Constants from "../Models/Constants";
 | 
				
			||||||
 | 
					import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
 | 
				
			||||||
 | 
					import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline";
 | 
				
			||||||
 | 
					import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Creates screenshoter to take png screenshot
 | 
					 * Creates screenshoter to take png screenshot
 | 
				
			||||||
 * Creates jspdf and downloads it
 | 
					 * Creates jspdf and downloads it
 | 
				
			||||||
| 
						 | 
					@ -8,21 +24,6 @@
 | 
				
			||||||
 *        -    add new layout in "PDFLayout"
 | 
					 *        -    add new layout in "PDFLayout"
 | 
				
			||||||
 *                -> in there are more instructions
 | 
					 *                -> in there are more instructions
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					 | 
				
			||||||
import jsPDF from "jspdf";
 | 
					 | 
				
			||||||
import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter";
 | 
					 | 
				
			||||||
import {UIEventSource} from "../Logic/UIEventSource";
 | 
					 | 
				
			||||||
import Minimap from "./Base/Minimap";
 | 
					 | 
				
			||||||
import Loc from "../Models/Loc";
 | 
					 | 
				
			||||||
import {BBox} from "../Logic/GeoOperations";
 | 
					 | 
				
			||||||
import ShowDataLayer from "./ShowDataLayer";
 | 
					 | 
				
			||||||
import BaseLayer from "../Models/BaseLayer";
 | 
					 | 
				
			||||||
import {FixedUiElement} from "./Base/FixedUiElement";
 | 
					 | 
				
			||||||
import Translations from "./i18n/Translations";
 | 
					 | 
				
			||||||
import State from "../State";
 | 
					 | 
				
			||||||
import Constants from "../Models/Constants";
 | 
					 | 
				
			||||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default class ExportPDF {
 | 
					export default class ExportPDF {
 | 
				
			||||||
    // dimensions of the map in milimeter
 | 
					    // dimensions of the map in milimeter
 | 
				
			||||||
    public isRunning = new UIEventSource(true)
 | 
					    public isRunning = new UIEventSource(true)
 | 
				
			||||||
| 
						 | 
					@ -39,7 +40,7 @@ export default class ExportPDF {
 | 
				
			||||||
            freeDivId: string,
 | 
					            freeDivId: string,
 | 
				
			||||||
            location: UIEventSource<Loc>,
 | 
					            location: UIEventSource<Loc>,
 | 
				
			||||||
            background?: UIEventSource<BaseLayer>
 | 
					            background?: UIEventSource<BaseLayer>
 | 
				
			||||||
            features: UIEventSource<{ feature: any }[]>,
 | 
					            features: FeaturePipeline,
 | 
				
			||||||
            layout: UIEventSource<LayoutConfig>
 | 
					            layout: UIEventSource<LayoutConfig>
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
| 
						 | 
					@ -57,7 +58,7 @@ export default class ExportPDF {
 | 
				
			||||||
            zoom: l.zoom + 1
 | 
					            zoom: l.zoom + 1
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const minimap = new Minimap({
 | 
					        const minimap = Minimap.createMiniMap({
 | 
				
			||||||
            location: new UIEventSource<Loc>(loc), // We remove the link between the old and the new UI-event source as moving the map while the export is running fucks up the screenshot
 | 
					            location: new UIEventSource<Loc>(loc), // We remove the link between the old and the new UI-event source as moving the map while the export is running fucks up the screenshot
 | 
				
			||||||
            background: options.background,
 | 
					            background: options.background,
 | 
				
			||||||
            allowMoving: false,
 | 
					            allowMoving: false,
 | 
				
			||||||
| 
						 | 
					@ -83,24 +84,21 @@ export default class ExportPDF {
 | 
				
			||||||
        minimap.AttachTo(options.freeDivId)
 | 
					        minimap.AttachTo(options.freeDivId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Next: we prepare the features. Only fully contained features are shown
 | 
					        // Next: we prepare the features. Only fully contained features are shown
 | 
				
			||||||
        const bounded = options.features.map(feats => {
 | 
					        minimap.leafletMap .addCallbackAndRunD(leaflet => {
 | 
				
			||||||
 | 
					 | 
				
			||||||
            const leaflet = minimap.leafletMap.data;
 | 
					 | 
				
			||||||
            if (leaflet === undefined) {
 | 
					 | 
				
			||||||
                return feats
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const bounds = BBox.fromLeafletBounds(leaflet.getBounds().pad(0.2))
 | 
					            const bounds = BBox.fromLeafletBounds(leaflet.getBounds().pad(0.2))
 | 
				
			||||||
            return feats.filter(f => BBox.get(f.feature).isContainedIn(bounds))
 | 
					            options.features.GetTilesPerLayerWithin(bounds, tile => {
 | 
				
			||||||
 | 
					                console.log("REndering", tile.name)
 | 
				
			||||||
        }, [minimap.leafletMap])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Add the features to the minimap
 | 
					 | 
				
			||||||
                new ShowDataLayer(
 | 
					                new ShowDataLayer(
 | 
				
			||||||
            bounded,
 | 
					                    {
 | 
				
			||||||
            minimap.leafletMap,
 | 
					                        features: tile,
 | 
				
			||||||
            options.layout,
 | 
					                        leafletMap: minimap.leafletMap,
 | 
				
			||||||
            false
 | 
					                        layerToShow: tile.layer.layerDef,
 | 
				
			||||||
 | 
					                        enablePopups: false
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import Img from "../Base/Img";
 | 
				
			||||||
import ImageAttributionSource from "../../Logic/ImageProviders/ImageAttributionSource";
 | 
					import ImageAttributionSource from "../../Logic/ImageProviders/ImageAttributionSource";
 | 
				
			||||||
import BaseUIElement from "../BaseUIElement";
 | 
					import BaseUIElement from "../BaseUIElement";
 | 
				
			||||||
import {VariableUiElement} from "../Base/VariableUIElement";
 | 
					import {VariableUiElement} from "../Base/VariableUIElement";
 | 
				
			||||||
 | 
					import Loading from "../Base/Loading";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class AttributedImage extends Combine {
 | 
					export class AttributedImage extends Combine {
 | 
				
			||||||
| 
						 | 
					@ -16,8 +17,13 @@ export class AttributedImage extends Combine {
 | 
				
			||||||
            img = new Img(urlSource);
 | 
					            img = new Img(urlSource);
 | 
				
			||||||
            attr = new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())
 | 
					            attr = new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            img = new VariableUiElement(preparedUrl.map(url => new Img(url, false, {fallbackImage: './assets/svg/blocked.svg'})))
 | 
					            img = new VariableUiElement(preparedUrl.map(url => {
 | 
				
			||||||
            attr = new VariableUiElement(preparedUrl.map(url => new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())))
 | 
					                if(url === undefined){
 | 
				
			||||||
 | 
					                    return new Loading()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return new Img(url, false, {fallbackImage: './assets/svg/blocked.svg'});
 | 
				
			||||||
 | 
					            }))
 | 
				
			||||||
 | 
					            attr = new VariableUiElement(preparedUrl.map(_ => new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())))
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,13 +6,13 @@ import BaseUIElement from "../BaseUIElement";
 | 
				
			||||||
import {FixedUiElement} from "../Base/FixedUiElement";
 | 
					import {FixedUiElement} from "../Base/FixedUiElement";
 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					import {Utils} from "../../Utils";
 | 
				
			||||||
import Loc from "../../Models/Loc";
 | 
					import Loc from "../../Models/Loc";
 | 
				
			||||||
 | 
					import Minimap from "../Base/Minimap";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Selects a direction in degrees
 | 
					 * Selects a direction in degrees
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export default class DirectionInput extends InputElement<string> {
 | 
					export default class DirectionInput extends InputElement<string> {
 | 
				
			||||||
    public static constructMinimap: ((any) => BaseUIElement);
 | 
					 | 
				
			||||||
    public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
 | 
					    public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
 | 
				
			||||||
    private readonly _location: UIEventSource<Loc>;
 | 
					    private readonly _location: UIEventSource<Loc>;
 | 
				
			||||||
    private readonly value: UIEventSource<string>;
 | 
					    private readonly value: UIEventSource<string>;
 | 
				
			||||||
| 
						 | 
					@ -40,7 +40,7 @@ export default class DirectionInput extends InputElement<string> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let map: BaseUIElement = new FixedUiElement("")
 | 
					        let map: BaseUIElement = new FixedUiElement("")
 | 
				
			||||||
        if (!Utils.runningFromConsole) {
 | 
					        if (!Utils.runningFromConsole) {
 | 
				
			||||||
            map = DirectionInput.constructMinimap({
 | 
					            map = Minimap.createMiniMap({
 | 
				
			||||||
                background: this.background,
 | 
					                background: this.background,
 | 
				
			||||||
                allowMoving: false,
 | 
					                allowMoving: false,
 | 
				
			||||||
                location: this._location
 | 
					                location: this._location
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ import Svg from "../../Svg";
 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					import {Utils} from "../../Utils";
 | 
				
			||||||
import Loc from "../../Models/Loc";
 | 
					import Loc from "../../Models/Loc";
 | 
				
			||||||
import {GeoOperations} from "../../Logic/GeoOperations";
 | 
					import {GeoOperations} from "../../Logic/GeoOperations";
 | 
				
			||||||
import DirectionInput from "./DirectionInput";
 | 
					import Minimap from "../Base/Minimap";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					@ -41,7 +41,7 @@ export default class LengthInput extends InputElement<string> {
 | 
				
			||||||
        // @ts-ignore
 | 
					        // @ts-ignore
 | 
				
			||||||
        let map = undefined
 | 
					        let map = undefined
 | 
				
			||||||
        if (!Utils.runningFromConsole) {
 | 
					        if (!Utils.runningFromConsole) {
 | 
				
			||||||
            map = DirectionInput.constructMinimap({
 | 
					            map = Minimap.createMiniMap({
 | 
				
			||||||
                background: this.background,
 | 
					                background: this.background,
 | 
				
			||||||
                allowMoving: false,
 | 
					                allowMoving: false,
 | 
				
			||||||
                location: this._location,
 | 
					                location: this._location,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,35 +8,31 @@ import Svg from "../../Svg";
 | 
				
			||||||
import State from "../../State";
 | 
					import State from "../../State";
 | 
				
			||||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
 | 
					import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
 | 
				
			||||||
import {GeoOperations} from "../../Logic/GeoOperations";
 | 
					import {GeoOperations} from "../../Logic/GeoOperations";
 | 
				
			||||||
import ShowDataLayer from "../ShowDataLayer";
 | 
					import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
 | 
				
			||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
 | 
					 | 
				
			||||||
import * as L from "leaflet";
 | 
					import * as L from "leaflet";
 | 
				
			||||||
 | 
					import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
 | 
				
			||||||
 | 
					import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
 | 
				
			||||||
 | 
					import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class LocationInput extends InputElement<Loc> {
 | 
					export default class LocationInput extends InputElement<Loc> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static readonly matchLayout = new UIEventSource(new LayoutConfig({
 | 
					    private static readonly matchLayer = new LayerConfig(
 | 
				
			||||||
        description: "Matchpoint style",
 | 
					        {
 | 
				
			||||||
        icon: "./assets/svg/crosshair-empty.svg",
 | 
					 | 
				
			||||||
        id: "matchpoint",
 | 
					 | 
				
			||||||
        language: ["en"],
 | 
					 | 
				
			||||||
        layers: [{
 | 
					 | 
				
			||||||
            id: "matchpoint", source: {
 | 
					            id: "matchpoint", source: {
 | 
				
			||||||
                osmTags: {and: []}
 | 
					                osmTags: {and: []}
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            icon: "./assets/svg/crosshair-empty.svg"
 | 
					            icon: "./assets/svg/crosshair-empty.svg"
 | 
				
			||||||
        }],
 | 
					        }, "matchpoint icon", true
 | 
				
			||||||
        maintainer: "MapComplete",
 | 
					    )
 | 
				
			||||||
        startLat: 0,
 | 
					 | 
				
			||||||
        startLon: 0,
 | 
					 | 
				
			||||||
        startZoom: 0,
 | 
					 | 
				
			||||||
        title: "Location input",
 | 
					 | 
				
			||||||
        version: "0"
 | 
					 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
    IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
 | 
					    IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
 | 
				
			||||||
    public readonly snappedOnto: UIEventSource<any> = new UIEventSource<any>(undefined)
 | 
					    public readonly snappedOnto: UIEventSource<any> = new UIEventSource<any>(undefined)
 | 
				
			||||||
    private _centerLocation: UIEventSource<Loc>;
 | 
					    private _centerLocation: UIEventSource<Loc>;
 | 
				
			||||||
    private readonly mapBackground: UIEventSource<BaseLayer>;
 | 
					    private readonly mapBackground: UIEventSource<BaseLayer>;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The features to which the input should be snapped
 | 
				
			||||||
 | 
					     * @private
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    private readonly _snapTo: UIEventSource<{ feature: any }[]>
 | 
					    private readonly _snapTo: UIEventSource<{ feature: any }[]>
 | 
				
			||||||
    private readonly _value: UIEventSource<Loc>
 | 
					    private readonly _value: UIEventSource<Loc>
 | 
				
			||||||
    private readonly _snappedPoint: UIEventSource<any>
 | 
					    private readonly _snappedPoint: UIEventSource<any>
 | 
				
			||||||
| 
						 | 
					@ -143,7 +139,7 @@ export default class LocationInput extends InputElement<Loc> {
 | 
				
			||||||
    protected InnerConstructElement(): HTMLElement {
 | 
					    protected InnerConstructElement(): HTMLElement {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            const clickLocation = new UIEventSource<Loc>(undefined);
 | 
					            const clickLocation = new UIEventSource<Loc>(undefined);
 | 
				
			||||||
            const map = new Minimap(
 | 
					            const map = Minimap.createMiniMap(
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    location: this._centerLocation,
 | 
					                    location: this._centerLocation,
 | 
				
			||||||
                    background: this.mapBackground,
 | 
					                    background: this.mapBackground,
 | 
				
			||||||
| 
						 | 
					@ -198,7 +194,6 @@ export default class LocationInput extends InputElement<Loc> {
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (this._snapTo !== undefined) {
 | 
					            if (this._snapTo !== undefined) {
 | 
				
			||||||
                new ShowDataLayer(this._snapTo, map.leafletMap, State.state.layoutToUse, false, false)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                const matchPoint = this._snappedPoint.map(loc => {
 | 
					                const matchPoint = this._snappedPoint.map(loc => {
 | 
				
			||||||
                    if (loc === undefined) {
 | 
					                    if (loc === undefined) {
 | 
				
			||||||
| 
						 | 
					@ -207,18 +202,27 @@ export default class LocationInput extends InputElement<Loc> {
 | 
				
			||||||
                    return [{feature: loc}];
 | 
					                    return [{feature: loc}];
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
                if (this._snapTo) {
 | 
					                if (this._snapTo) {
 | 
				
			||||||
                    let layout = LocationInput.matchLayout
 | 
					                    if (this._snappedPointTags === undefined) {
 | 
				
			||||||
                    if (this._snappedPointTags !== undefined) {
 | 
					                        // No special tags - we show a default crosshair
 | 
				
			||||||
                        layout = State.state.layoutToUse
 | 
					                        new ShowDataLayer({
 | 
				
			||||||
 | 
					                            features: new StaticFeatureSource(matchPoint),
 | 
				
			||||||
 | 
					                            enablePopups: false,
 | 
				
			||||||
 | 
					                            zoomToFeatures: false,
 | 
				
			||||||
 | 
					                            leafletMap: map.leafletMap,
 | 
				
			||||||
 | 
					                            layerToShow: LocationInput.matchLayer
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                    }else{
 | 
				
			||||||
 | 
					                        new ShowDataMultiLayer({
 | 
				
			||||||
 | 
					                                features: new StaticFeatureSource(matchPoint),
 | 
				
			||||||
 | 
					                                enablePopups: false,
 | 
				
			||||||
 | 
					                                zoomToFeatures: false,
 | 
				
			||||||
 | 
					                                leafletMap: map.leafletMap,
 | 
				
			||||||
 | 
					                                layers: State.state.filteredLayers
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                    new ShowDataLayer(
 | 
					 | 
				
			||||||
                        matchPoint,
 | 
					 | 
				
			||||||
                        map.leafletMap,
 | 
					 | 
				
			||||||
                        layout,
 | 
					 | 
				
			||||||
                        false, false
 | 
					 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.mapBackground.map(layer => {
 | 
					            this.mapBackground.map(layer => {
 | 
				
			||||||
                const leaflet = map.leafletMap.data
 | 
					                const leaflet = map.leafletMap.data
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
 | 
				
			||||||
import {SubtleButton} from "../Base/SubtleButton";
 | 
					import {SubtleButton} from "../Base/SubtleButton";
 | 
				
			||||||
import Minimap from "../Base/Minimap";
 | 
					import Minimap from "../Base/Minimap";
 | 
				
			||||||
import State from "../../State";
 | 
					import State from "../../State";
 | 
				
			||||||
import ShowDataLayer from "../ShowDataLayer";
 | 
					import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
 | 
				
			||||||
import {GeoOperations} from "../../Logic/GeoOperations";
 | 
					import {GeoOperations} from "../../Logic/GeoOperations";
 | 
				
			||||||
import {LeafletMouseEvent} from "leaflet";
 | 
					import {LeafletMouseEvent} from "leaflet";
 | 
				
			||||||
import Combine from "../Base/Combine";
 | 
					import Combine from "../Base/Combine";
 | 
				
			||||||
| 
						 | 
					@ -13,10 +13,16 @@ import Translations from "../i18n/Translations";
 | 
				
			||||||
import SplitAction from "../../Logic/Osm/Actions/SplitAction";
 | 
					import SplitAction from "../../Logic/Osm/Actions/SplitAction";
 | 
				
			||||||
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
 | 
					import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
 | 
				
			||||||
import Title from "../Base/Title";
 | 
					import Title from "../Base/Title";
 | 
				
			||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
 | 
					import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
 | 
				
			||||||
 | 
					import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
 | 
				
			||||||
 | 
					import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class SplitRoadWizard extends Toggle {
 | 
					export default class SplitRoadWizard extends Toggle {
 | 
				
			||||||
    private static splitLayout = new UIEventSource(SplitRoadWizard.GetSplitLayout())
 | 
					    private static splitLayerStyling = new LayerConfig({
 | 
				
			||||||
 | 
					        id: "splitpositions",
 | 
				
			||||||
 | 
					        source: {osmTags: "_cutposition=yes"},
 | 
				
			||||||
 | 
					        icon: "./assets/svg/plus.svg"
 | 
				
			||||||
 | 
					    }, "(BUILTIN) SplitRoadWizard.ts", true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * A UI Element used for splitting roads
 | 
					     * A UI Element used for splitting roads
 | 
				
			||||||
| 
						 | 
					@ -36,7 +42,7 @@ export default class SplitRoadWizard extends Toggle {
 | 
				
			||||||
        const splitClicked = new UIEventSource<boolean>(false);
 | 
					        const splitClicked = new UIEventSource<boolean>(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Minimap on which you can select the points to be splitted
 | 
					        // Minimap on which you can select the points to be splitted
 | 
				
			||||||
        const miniMap = new Minimap({background: State.state.backgroundLayer, allowMoving: false});
 | 
					        const miniMap = Minimap.createMiniMap({background: State.state.backgroundLayer, allowMoving: false});
 | 
				
			||||||
        miniMap.SetStyle("width: 100%; height: 24rem;");
 | 
					        miniMap.SetStyle("width: 100%; height: 24rem;");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Define how a cut is displayed on the map
 | 
					        // Define how a cut is displayed on the map
 | 
				
			||||||
| 
						 | 
					@ -45,8 +51,20 @@ export default class SplitRoadWizard extends Toggle {
 | 
				
			||||||
        const roadElement = State.state.allElements.ContainingFeatures.get(id)
 | 
					        const roadElement = State.state.allElements.ContainingFeatures.get(id)
 | 
				
			||||||
        const roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]);
 | 
					        const roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]);
 | 
				
			||||||
        // Datalayer displaying the road and the cut points (if any)
 | 
					        // Datalayer displaying the road and the cut points (if any)
 | 
				
			||||||
        new ShowDataLayer(roadEventSource, miniMap.leafletMap, State.state.layoutToUse, false, true);
 | 
					        new ShowDataMultiLayer({
 | 
				
			||||||
        new ShowDataLayer(splitPoints, miniMap.leafletMap, SplitRoadWizard.splitLayout, false, false)
 | 
					            features: new StaticFeatureSource(roadEventSource, true),
 | 
				
			||||||
 | 
					            layers: State.state.filteredLayers,
 | 
				
			||||||
 | 
					            leafletMap: miniMap.leafletMap,
 | 
				
			||||||
 | 
					            enablePopups: false,
 | 
				
			||||||
 | 
					            zoomToFeatures: true
 | 
				
			||||||
 | 
					        }) 
 | 
				
			||||||
 | 
					        new ShowDataLayer({
 | 
				
			||||||
 | 
					            features: new StaticFeatureSource(splitPoints, true),
 | 
				
			||||||
 | 
					            leafletMap: miniMap.leafletMap,
 | 
				
			||||||
 | 
					            zoomToFeatures: false,
 | 
				
			||||||
 | 
					            enablePopups: false,
 | 
				
			||||||
 | 
					            layerToShow:  SplitRoadWizard.splitLayerStyling
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Handles a click on the overleaf map.
 | 
					         * Handles a click on the overleaf map.
 | 
				
			||||||
| 
						 | 
					@ -135,21 +153,4 @@ export default class SplitRoadWizard extends Toggle {
 | 
				
			||||||
        const confirm = new Toggle(mapView, splitToggle, splitClicked);
 | 
					        const confirm = new Toggle(mapView, splitToggle, splitClicked);
 | 
				
			||||||
        super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit)
 | 
					        super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    private static GetSplitLayout(): LayoutConfig {
 | 
					 | 
				
			||||||
        return new LayoutConfig({
 | 
					 | 
				
			||||||
            maintainer: "mapcomplete",
 | 
					 | 
				
			||||||
            language: ["en"],
 | 
					 | 
				
			||||||
            startLon: 0,
 | 
					 | 
				
			||||||
            startLat: 0,
 | 
					 | 
				
			||||||
            description: "Split points visualisations - built in at SplitRoadWizard.ts",
 | 
					 | 
				
			||||||
            icon: "", startZoom: 0,
 | 
					 | 
				
			||||||
            title: "Split locations",
 | 
					 | 
				
			||||||
            version: "",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            id: "splitpositions",
 | 
					 | 
				
			||||||
            layers: [{id: "splitpositions", source: {osmTags: "_cutposition=yes"}, icon: "./assets/svg/plus.svg"}]
 | 
					 | 
				
			||||||
        }, true, "(BUILTIN) SplitRoadWizard.ts")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,19 +1,11 @@
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The data layer shows all the given geojson elements with the appropriate icon etc
 | 
					 * The data layer shows all the given geojson elements with the appropriate icon etc
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
import {UIEventSource} from "../Logic/UIEventSource";
 | 
					import {UIEventSource} from "../../Logic/UIEventSource";
 | 
				
			||||||
import * as L from "leaflet"
 | 
					import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
 | 
				
			||||||
import State from "../State";
 | 
					import FeatureInfoBox from "../Popup/FeatureInfoBox";
 | 
				
			||||||
import FeatureInfoBox from "./Popup/FeatureInfoBox";
 | 
					import State from "../../State";
 | 
				
			||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
 | 
					import {ShowDataLayerOptions} from "./ShowDataLayerOptions";
 | 
				
			||||||
import FeatureSource from "../Logic/FeatureSource/FeatureSource";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface ShowDataLayerOptions {
 | 
					 | 
				
			||||||
    features: FeatureSource,
 | 
					 | 
				
			||||||
    leafletMap: UIEventSource<L.Map>,
 | 
					 | 
				
			||||||
    enablePopups?: true | boolean,
 | 
					 | 
				
			||||||
    zoomToFeatures? : false | boolean,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class ShowDataLayer {
 | 
					export default class ShowDataLayer {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
 | 
				
			||||||
 | 
					import {UIEventSource} from "../../Logic/UIEventSource";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ShowDataLayerOptions {
 | 
				
			||||||
 | 
					    features: FeatureSource,
 | 
				
			||||||
 | 
					    leafletMap: UIEventSource<L.Map>,
 | 
				
			||||||
 | 
					    enablePopups?: true | boolean,
 | 
				
			||||||
 | 
					    zoomToFeatures?: false | boolean,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,12 @@
 | 
				
			||||||
import {UIEventSource} from "../Logic/UIEventSource";
 | 
					 | 
				
			||||||
import FilteredLayer from "../Models/FilteredLayer";
 | 
					 | 
				
			||||||
import ShowDataLayer, {ShowDataLayerOptions} from "./ShowDataLayer/ShowDataLayer";
 | 
					 | 
				
			||||||
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * SHows geojson on the given leaflet map, but attempts to figure out the correct layer first
 | 
					 * SHows geojson on the given leaflet map, but attempts to figure out the correct layer first
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					import {UIEventSource} from "../../Logic/UIEventSource";
 | 
				
			||||||
 | 
					import ShowDataLayer from "./ShowDataLayer";
 | 
				
			||||||
 | 
					import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
 | 
				
			||||||
 | 
					import FilteredLayer from "../../Models/FilteredLayer";
 | 
				
			||||||
 | 
					import {ShowDataLayerOptions} from "./ShowDataLayerOptions";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class ShowDataMultiLayer {
 | 
					export default class ShowDataMultiLayer {
 | 
				
			||||||
    constructor(options: ShowDataLayerOptions & { layers: UIEventSource<FilteredLayer[]> }) {
 | 
					    constructor(options: ShowDataLayerOptions & { layers: UIEventSource<FilteredLayer[]> }) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,6 @@ import {ImageCarousel} from "./Image/ImageCarousel";
 | 
				
			||||||
import Combine from "./Base/Combine";
 | 
					import Combine from "./Base/Combine";
 | 
				
			||||||
import {FixedUiElement} from "./Base/FixedUiElement";
 | 
					import {FixedUiElement} from "./Base/FixedUiElement";
 | 
				
			||||||
import {ImageUploadFlow} from "./Image/ImageUploadFlow";
 | 
					import {ImageUploadFlow} from "./Image/ImageUploadFlow";
 | 
				
			||||||
 | 
					 | 
				
			||||||
import ShareButton from "./BigComponents/ShareButton";
 | 
					import ShareButton from "./BigComponents/ShareButton";
 | 
				
			||||||
import Svg from "../Svg";
 | 
					import Svg from "../Svg";
 | 
				
			||||||
import ReviewElement from "./Reviews/ReviewElement";
 | 
					import ReviewElement from "./Reviews/ReviewElement";
 | 
				
			||||||
| 
						 | 
					@ -13,7 +12,6 @@ import MangroveReviews from "../Logic/Web/MangroveReviews";
 | 
				
			||||||
import Translations from "./i18n/Translations";
 | 
					import Translations from "./i18n/Translations";
 | 
				
			||||||
import ReviewForm from "./Reviews/ReviewForm";
 | 
					import ReviewForm from "./Reviews/ReviewForm";
 | 
				
			||||||
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization";
 | 
					import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization";
 | 
				
			||||||
 | 
					 | 
				
			||||||
import State from "../State";
 | 
					import State from "../State";
 | 
				
			||||||
import {ImageSearcher} from "../Logic/Actors/ImageSearcher";
 | 
					import {ImageSearcher} from "../Logic/Actors/ImageSearcher";
 | 
				
			||||||
import BaseUIElement from "./BaseUIElement";
 | 
					import BaseUIElement from "./BaseUIElement";
 | 
				
			||||||
| 
						 | 
					@ -26,6 +24,9 @@ import BaseLayer from "../Models/BaseLayer";
 | 
				
			||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
 | 
					import LayerConfig from "../Models/ThemeConfig/LayerConfig";
 | 
				
			||||||
import ImportButton from "./BigComponents/ImportButton";
 | 
					import ImportButton from "./BigComponents/ImportButton";
 | 
				
			||||||
import {Tag} from "../Logic/Tags/Tag";
 | 
					import {Tag} from "../Logic/Tags/Tag";
 | 
				
			||||||
 | 
					import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
 | 
				
			||||||
 | 
					import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer";
 | 
				
			||||||
 | 
					import Minimap from "./Base/Minimap";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface SpecialVisualization {
 | 
					export interface SpecialVisualization {
 | 
				
			||||||
    funcName: string,
 | 
					    funcName: string,
 | 
				
			||||||
| 
						 | 
					@ -37,14 +38,6 @@ export interface SpecialVisualization {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class SpecialVisualizations {
 | 
					export default class SpecialVisualizations {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    static constructMiniMap: (options?: {
 | 
					 | 
				
			||||||
        background?: UIEventSource<BaseLayer>,
 | 
					 | 
				
			||||||
        location?: UIEventSource<Loc>,
 | 
					 | 
				
			||||||
        allowMoving?: boolean,
 | 
					 | 
				
			||||||
        leafletOptions?: any
 | 
					 | 
				
			||||||
    }) => BaseUIElement;
 | 
					 | 
				
			||||||
    static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any;
 | 
					 | 
				
			||||||
    public static specialVisualizations: SpecialVisualization[] =
 | 
					    public static specialVisualizations: SpecialVisualization[] =
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
| 
						 | 
					@ -153,7 +146,7 @@ export default class SpecialVisualizations {
 | 
				
			||||||
                        lon: Number(properties._lon),
 | 
					                        lon: Number(properties._lon),
 | 
				
			||||||
                        zoom: zoom
 | 
					                        zoom: zoom
 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
                    const minimap = SpecialVisualizations.constructMiniMap(
 | 
					                    const minimap = Minimap.createMiniMap(
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            background: state.backgroundLayer,
 | 
					                            background: state.backgroundLayer,
 | 
				
			||||||
                            location: locationSource,
 | 
					                            location: locationSource,
 | 
				
			||||||
| 
						 | 
					@ -169,12 +162,14 @@ export default class SpecialVisualizations {
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    SpecialVisualizations.constructShowDataLayer(
 | 
					                   new ShowDataMultiLayer(
 | 
				
			||||||
                        featuresToShow,
 | 
					                        {
 | 
				
			||||||
                        minimap["leafletMap"],
 | 
					                            leafletMap: minimap["leafletMap"],
 | 
				
			||||||
                        State.state.layoutToUse,
 | 
					                            enablePopups : false,
 | 
				
			||||||
                        false,
 | 
					                            zoomToFeatures: true,
 | 
				
			||||||
                        true
 | 
					                            layers: State.state.filteredLayers,
 | 
				
			||||||
 | 
					                            features: new StaticFeatureSource(featuresToShow, true)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										28
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										28
									
								
								Utils.ts
									
										
									
									
									
								
							| 
						 | 
					@ -245,7 +245,6 @@ export class Utils {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        dict.set(k, v());
 | 
					        dict.set(k, v());
 | 
				
			||||||
        return dict.get(k);
 | 
					        return dict.get(k);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					@ -259,6 +258,26 @@ export class Utils {
 | 
				
			||||||
        return [[Utils.tile2lat(y, z), Utils.tile2long(x, z)], [Utils.tile2lat(y + 1, z), Utils.tile2long(x + 1, z)]]
 | 
					        return [[Utils.tile2lat(y, z), Utils.tile2long(x, z)], [Utils.tile2lat(y + 1, z), Utils.tile2long(x + 1, z)]]
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static tile_bounds_lon_lat(z: number, x: number, y: number): [[number, number], [number, number]] {
 | 
				
			||||||
 | 
					        return [[Utils.tile2long(x, z),Utils.tile2lat(y, z)], [Utils.tile2long(x + 1, z), Utils.tile2lat(y + 1, z)]]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    static tile_index(z: number, x: number, y: number):number{
 | 
				
			||||||
 | 
					        return ((x * (2 << z)) + y) * 100 + z
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Given a tile index number, returns [z, x, y]
 | 
				
			||||||
 | 
					     * @param index
 | 
				
			||||||
 | 
					     * @returns 'zxy'
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    static tile_from_index(index: number) : [number, number, number]{
 | 
				
			||||||
 | 
					        const z = index % 100;
 | 
				
			||||||
 | 
					        const factor = 2 << z
 | 
				
			||||||
 | 
					        index = Math.floor(index / 100)
 | 
				
			||||||
 | 
					        return [z, Math.floor(index / factor), index % factor]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Return x, y of the tile containing (lat, lon) on the given zoom level
 | 
					     * Return x, y of the tile containing (lat, lon) on the given zoom level
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
| 
						 | 
					@ -422,13 +441,6 @@ export class Utils {
 | 
				
			||||||
        return bestColor ?? hex;
 | 
					        return bestColor ?? hex;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static setDefaults(options, defaults) {
 | 
					 | 
				
			||||||
        for (let key in defaults) {
 | 
					 | 
				
			||||||
            if (!(key in options)) options[key] = defaults[key];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return options;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private static tile2long(x, z) {
 | 
					    private static tile2long(x, z) {
 | 
				
			||||||
        return (x / Math.pow(2, z) * 360 - 180);
 | 
					        return (x / Math.pow(2, z) * 360 - 180);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,7 +48,7 @@
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "calculatedTags": [
 | 
					  "calculatedTags": [
 | 
				
			||||||
    "_closest_other_drinking_water_id=feat.closest('drinking_water').id",
 | 
					    "_closest_other_drinking_water_id=feat.closest('drinking_water')?.id",
 | 
				
			||||||
    "_closest_other_drinking_water_distance=Math.floor(feat.distanceTo(feat.closest('drinking_water')) * 1000)"
 | 
					    "_closest_other_drinking_water_distance=Math.floor(feat.distanceTo(feat.closest('drinking_water')) * 1000)"
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  "minzoom": 13,
 | 
					  "minzoom": 13,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -401,5 +401,43 @@
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "filter": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "options": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "question": {
 | 
				
			||||||
 | 
					            "en": "Wheelchair accessible"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "osmTags": "wheelchair=yes"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "options": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "question": {
 | 
				
			||||||
 | 
					            "en": "Has a changing table"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "osmTags": "changing_table=yes"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "options": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "question": {
 | 
				
			||||||
 | 
					            "en": "Free to use"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "osmTags": {
 | 
				
			||||||
 | 
					            "or": [
 | 
				
			||||||
 | 
					              "fee=no",
 | 
				
			||||||
 | 
					              "fee=0",
 | 
				
			||||||
 | 
					              "charge=0"
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -39,7 +39,7 @@
 | 
				
			||||||
  "startLat": 0,
 | 
					  "startLat": 0,
 | 
				
			||||||
  "startLon": 0,
 | 
					  "startLon": 0,
 | 
				
			||||||
  "startZoom": 1,
 | 
					  "startZoom": 1,
 | 
				
			||||||
  "widenFactor": 0.05,
 | 
					  "widenFactor": 1,
 | 
				
			||||||
  "roamingRenderings": [],
 | 
					  "roamingRenderings": [],
 | 
				
			||||||
  "layers": [
 | 
					  "layers": [
 | 
				
			||||||
    "public_bookcase"
 | 
					    "public_bookcase"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,7 @@
 | 
				
			||||||
  "defaultBackgroundId": "CartoDB.Voyager",
 | 
					  "defaultBackgroundId": "CartoDB.Voyager",
 | 
				
			||||||
  "startLon": 4.351697,
 | 
					  "startLon": 4.351697,
 | 
				
			||||||
  "startZoom": 16,
 | 
					  "startZoom": 16,
 | 
				
			||||||
  "widenFactor": 0.05,
 | 
					  "widenFactor": 2,
 | 
				
			||||||
  "layers": [
 | 
					  "layers": [
 | 
				
			||||||
    "drinking_water"
 | 
					    "drinking_water"
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,6 +55,7 @@
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      "#": "Nature reserve overview from cache, points only, z < 13",
 | 
					      "#": "Nature reserve overview from cache, points only, z < 13",
 | 
				
			||||||
      "builtin": "nature_reserve",
 | 
					      "builtin": "nature_reserve",
 | 
				
			||||||
 | 
					      "wayHandling": 1,
 | 
				
			||||||
      "override": {
 | 
					      "override": {
 | 
				
			||||||
        "source": {
 | 
					        "source": {
 | 
				
			||||||
          "osmTags": {
 | 
					          "osmTags": {
 | 
				
			||||||
| 
						 | 
					@ -63,6 +64,7 @@
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_nature_reserve_points.geojson",
 | 
					          "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_nature_reserve_points.geojson",
 | 
				
			||||||
 | 
					          "geoJsonZoomLevel": 0,
 | 
				
			||||||
          "isOsmCache": "duplicate"
 | 
					          "isOsmCache": "duplicate"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "minzoom": 1,
 | 
					        "minzoom": 1,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -152,7 +152,7 @@
 | 
				
			||||||
          ]
 | 
					          ]
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson",
 | 
					        "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson",
 | 
				
			||||||
        "geoJsonZoomLevel": 14,
 | 
					        "geoJsonZoomLevel": 11,
 | 
				
			||||||
        "isOsmCache": true
 | 
					        "isOsmCache": true
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "title": {
 | 
					      "title": {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -131,7 +131,7 @@
 | 
				
			||||||
                ]
 | 
					                ]
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
              "then": {
 | 
					              "then": {
 | 
				
			||||||
                "en": "This object has no house number"
 | 
					                "en": "This building has no house number"
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          ]
 | 
					          ]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										21
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										21
									
								
								index.ts
									
										
									
									
									
								
							| 
						 | 
					@ -8,31 +8,16 @@ import MoreScreen from "./UI/BigComponents/MoreScreen";
 | 
				
			||||||
import State from "./State";
 | 
					import State from "./State";
 | 
				
			||||||
import Combine from "./UI/Base/Combine";
 | 
					import Combine from "./UI/Base/Combine";
 | 
				
			||||||
import Translations from "./UI/i18n/Translations";
 | 
					import Translations from "./UI/i18n/Translations";
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import CountryCoder from "latlon2country"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import SimpleMetaTagger from "./Logic/SimpleMetaTagger";
 | 
					 | 
				
			||||||
import Minimap from "./UI/Base/Minimap";
 | 
					 | 
				
			||||||
import DirectionInput from "./UI/Input/DirectionInput";
 | 
					 | 
				
			||||||
import SpecialVisualizations from "./UI/SpecialVisualizations";
 | 
					 | 
				
			||||||
import ShowDataLayer from "./UI/ShowDataLayer";
 | 
					 | 
				
			||||||
import * as L from "leaflet";
 | 
					 | 
				
			||||||
import ValidatedTextField from "./UI/Input/ValidatedTextField";
 | 
					import ValidatedTextField from "./UI/Input/ValidatedTextField";
 | 
				
			||||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
 | 
					import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
 | 
				
			||||||
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
 | 
					import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
 | 
				
			||||||
import Constants from "./Models/Constants";
 | 
					import Constants from "./Models/Constants";
 | 
				
			||||||
 | 
					import MinimapImplementation from "./UI/Base/MinimapImplementation";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MinimapImplementation.initialize()
 | 
				
			||||||
// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts
 | 
					// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts
 | 
				
			||||||
SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
 | 
					 | 
				
			||||||
DirectionInput.constructMinimap = options => new Minimap(options)
 | 
					 | 
				
			||||||
ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref)
 | 
					ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref)
 | 
				
			||||||
SpecialVisualizations.constructMiniMap = options => new Minimap(options)
 | 
					
 | 
				
			||||||
SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>,
 | 
					 | 
				
			||||||
                                                leafletMap: UIEventSource<L.Map>,
 | 
					 | 
				
			||||||
                                                layoutToUse: UIEventSource<LayoutConfig>,
 | 
					 | 
				
			||||||
                                                enablePopups = true,
 | 
					 | 
				
			||||||
                                                zoomToFeatures = false) => new ShowDataLayer(features, leafletMap, layoutToUse, enablePopups, zoomToFeatures)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
let defaultLayout = ""
 | 
					let defaultLayout = ""
 | 
				
			||||||
// --------------------- Special actions based on the parameters -----------------
 | 
					// --------------------- Special actions based on the parameters -----------------
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -159,7 +159,7 @@
 | 
				
			||||||
    "noTagsSelected": "No tags selected",
 | 
					    "noTagsSelected": "No tags selected",
 | 
				
			||||||
    "testing": "Testing - changes won't be saved",
 | 
					    "testing": "Testing - changes won't be saved",
 | 
				
			||||||
    "customThemeIntro": "<h3>Custom themes</h3>These are previously visited user-generated themes.",
 | 
					    "customThemeIntro": "<h3>Custom themes</h3>These are previously visited user-generated themes.",
 | 
				
			||||||
    "aboutMapcomplete": "<h3>About MapComplete</h3><p>With MapComplete you can enrich OpenStreetMap with information on a <b>single theme.</b> Answer a few questions, and within minutes your contributions will be available around the globe! The <b>theme maintainer</b> defines elements, questions and languages for the theme.</p><h3>Find out more</h3><p>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>When embedded in a website, the iframe links to a full-screen MapComplete</li><li>The full-screen version offers information about OpenStreetMap</li><li>Viewing works without login, but editing requires an OSM login.</li><li>If you are not logged in, you are asked to log in</li><li>Once you answered a single question, you can add new points to the map</li><li>After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >OsmCha</a>.</p>",
 | 
					    "aboutMapcomplete": "<h3>About MapComplete</h3><p>With MapComplete you can enrich OpenStreetMap with information on a <b>single theme.</b> Answer a few questions, and within minutes your contributions will be available around the globe! The <b>theme maintainer</b> defines elements, questions and languages for the theme.</p><h3>Find out more</h3><p>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>When embedded in a website, the iframe links to a full-screen MapComplete</li><li>The full-screen version offers information about OpenStreetMap</li><li>Viewing works without login, but editing requires an OSM login.</li><li>If you are not logged in, you are asked to log in</li><li>Once you answered a single question, you can add new points to the map</li><li>After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='{osmcha_link}' target='_blank' >OsmCha</a>.</p>",
 | 
				
			||||||
    "backgroundMap": "Background map",
 | 
					    "backgroundMap": "Background map",
 | 
				
			||||||
    "openTheMap": "Open the map",
 | 
					    "openTheMap": "Open the map",
 | 
				
			||||||
    "loginOnlyNeededToEdit": "if you want to edit the map",
 | 
					    "loginOnlyNeededToEdit": "if you want to edit the map",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3021,6 +3021,29 @@
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "toilet": {
 | 
					    "toilet": {
 | 
				
			||||||
 | 
					        "filter": {
 | 
				
			||||||
 | 
					            "0": {
 | 
				
			||||||
 | 
					                "options": {
 | 
				
			||||||
 | 
					                    "0": {
 | 
				
			||||||
 | 
					                        "question": "Wheelchair accessible"
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "1": {
 | 
				
			||||||
 | 
					                "options": {
 | 
				
			||||||
 | 
					                    "0": {
 | 
				
			||||||
 | 
					                        "question": "Has a changing table"
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "2": {
 | 
				
			||||||
 | 
					                "options": {
 | 
				
			||||||
 | 
					                    "0": {
 | 
				
			||||||
 | 
					                        "question": "Free to use"
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "name": "Toilets",
 | 
					        "name": "Toilets",
 | 
				
			||||||
        "presets": {
 | 
					        "presets": {
 | 
				
			||||||
            "0": {
 | 
					            "0": {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -192,7 +192,7 @@
 | 
				
			||||||
      "getStartedNewAccount": " of <a href='https://www.openstreetmap.org/user/new' target='_blank'>maak een nieuwe account aan</a>",
 | 
					      "getStartedNewAccount": " of <a href='https://www.openstreetmap.org/user/new' target='_blank'>maak een nieuwe account aan</a>",
 | 
				
			||||||
      "noTagsSelected": "Geen tags geselecteerd",
 | 
					      "noTagsSelected": "Geen tags geselecteerd",
 | 
				
			||||||
      "customThemeIntro": "<h3>Onofficiële thema's</h3>De onderstaande thema's heb je eerder bezocht en zijn gemaakt door andere OpenStreetMappers.",
 | 
					      "customThemeIntro": "<h3>Onofficiële thema's</h3>De onderstaande thema's heb je eerder bezocht en zijn gemaakt door andere OpenStreetMappers.",
 | 
				
			||||||
      "aboutMapcomplete": "<h3>Over MapComplete</h3><p>Met MapComplete kun je OpenStreetMap verrijken met informatie over een bepaald thema. Beantwoord enkele vragen, en binnen een paar minuten is jouw bijdrage wereldwijd beschikbaar! De <b>maker van het thema</b> bepaalt de elementen, vragen en taalversies voor het thema.</p><h3>Ontdek meer</h3><p>MapComplete <b>biedt altijd de volgende stap</b> naar meer OpenStreetMap:<ul><li>Indien ingebed in een website linkt het iframe naar de volledige MapComplete</li><li>De volledige versie heeft uitleg over OpenStreetMap</li><li>Bekijken kan altijd, maar wijzigen vereist een OSM-account</li><li>Als je niet aangemeld bent, wordt je gevraagd dit te doen</li><li>Als je minstens één vraag hebt beantwoord, kan je ook elementen toevoegen</li><li>Heb je genoeg changesets, dan verschijnen de OSM-tags, nog later links naar de wiki</li></ul></p><p>Merk je <b>een bug</b> of wil je een <b>extra feature</b>? Wil je <b>helpen vertalen</b>? Bezoek dan de <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>broncode</a> en <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker</a>. <p></p>Wil je <b>je vorderingen</b> zien? Volg de edits <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >op OsmCha</a>.</p>",
 | 
					      "aboutMapcomplete": "<h3>Over MapComplete</h3><p>Met MapComplete kun je OpenStreetMap verrijken met informatie over een bepaald thema. Beantwoord enkele vragen, en binnen een paar minuten is jouw bijdrage wereldwijd beschikbaar! De <b>maker van het thema</b> bepaalt de elementen, vragen en taalversies voor het thema.</p><h3>Ontdek meer</h3><p>MapComplete <b>biedt altijd de volgende stap</b> naar meer OpenStreetMap:<ul><li>Indien ingebed in een website linkt het iframe naar de volledige MapComplete</li><li>De volledige versie heeft uitleg over OpenStreetMap</li><li>Bekijken kan altijd, maar wijzigen vereist een OSM-account</li><li>Als je niet aangemeld bent, wordt je gevraagd dit te doen</li><li>Als je minstens één vraag hebt beantwoord, kan je ook elementen toevoegen</li><li>Heb je genoeg changesets, dan verschijnen de OSM-tags, nog later links naar de wiki</li></ul></p><p>Merk je <b>een bug</b> of wil je een <b>extra feature</b>? Wil je <b>helpen vertalen</b>? Bezoek dan de <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>broncode</a> en <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker</a>. <p></p>Wil je <b>je vorderingen</b> zien? Volg de edits <a href='{osmcha_link}' target='_blank' >op OsmCha</a>.</p>",
 | 
				
			||||||
      "backgroundMap": "Achtergrondkaart",
 | 
					      "backgroundMap": "Achtergrondkaart",
 | 
				
			||||||
      "layerSelection": {
 | 
					      "layerSelection": {
 | 
				
			||||||
        "zoomInToSeeThisLayer": "Vergroot de kaart om deze laag te zien",
 | 
					        "zoomInToSeeThisLayer": "Vergroot de kaart om deze laag te zien",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1351,6 +1351,11 @@
 | 
				
			||||||
                "title": {
 | 
					                "title": {
 | 
				
			||||||
                    "render": "Known address"
 | 
					                    "render": "Known address"
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "2": {
 | 
				
			||||||
 | 
					                "title": {
 | 
				
			||||||
 | 
					                    "render": "{name}"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "shortDescription": "Help to build an open dataset of UK addresses",
 | 
					        "shortDescription": "Help to build an open dataset of UK addresses",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,10 +20,10 @@
 | 
				
			||||||
    "reset:translations": "ts-node scripts/generateTranslations.ts --ignore-weblate",
 | 
					    "reset:translations": "ts-node scripts/generateTranslations.ts --ignore-weblate",
 | 
				
			||||||
    "generate:layouts": "ts-node scripts/generateLayouts.ts",
 | 
					    "generate:layouts": "ts-node scripts/generateLayouts.ts",
 | 
				
			||||||
    "generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts",
 | 
					    "generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts",
 | 
				
			||||||
    "generate:cache:speelplekken:mini": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.181710380278176 4.423413276672363 51.193007664772495 4.444141387939452",
 | 
					    "generate:cache:speelplekken:mini": "ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache_mini/ 51.181710380278176 4.423413276672363 51.193007664772495 4.444141387939452",
 | 
				
			||||||
    "generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.20 4.35 51.09 4.56",
 | 
					    "generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.20 4.35 51.09 4.56",
 | 
				
			||||||
    "generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../pietervdvn.github.io/natuurpunt_cache/ 50.40 2.1 51.54 6.4 --generate-point-overview nature_reserve,visitor_information_centre",
 | 
					    "generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../pietervdvn.github.io/natuurpunt_cache/ 50.40 2.1 51.54 6.4 --generate-point-overview nature_reserve,visitor_information_centre",
 | 
				
			||||||
    "generate:layeroverview": "npm run generate:licenses && echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && ts-node scripts/generateLayerOverview.ts --no-fail",
 | 
					    "generate:layeroverview": "echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && ts-node scripts/generateLayerOverview.ts --no-fail",
 | 
				
			||||||
    "generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail",
 | 
					    "generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail",
 | 
				
			||||||
    "query:licenses": "ts-node scripts/generateLicenseInfo.ts --query",
 | 
					    "query:licenses": "ts-node scripts/generateLicenseInfo.ts --query",
 | 
				
			||||||
    "generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push",
 | 
					    "generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,6 @@ import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Utils.runningFromConsole = true
 | 
					Utils.runningFromConsole = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
export default class ScriptUtils {
 | 
					export default class ScriptUtils {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,27 +2,32 @@
 | 
				
			||||||
 * Generates a collection of geojson files based on an overpass query for a given theme
 | 
					 * Generates a collection of geojson files based on an overpass query for a given theme
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
import {Utils} from "../Utils";
 | 
					import {Utils} from "../Utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Utils.runningFromConsole = true
 | 
					Utils.runningFromConsole = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {Overpass} from "../Logic/Osm/Overpass";
 | 
					import {Overpass} from "../Logic/Osm/Overpass";
 | 
				
			||||||
import * as fs from "fs";
 | 
					 | 
				
			||||||
import {existsSync, readFileSync, writeFileSync} from "fs";
 | 
					import {existsSync, readFileSync, writeFileSync} from "fs";
 | 
				
			||||||
import {TagsFilter} from "../Logic/Tags/TagsFilter";
 | 
					import {TagsFilter} from "../Logic/Tags/TagsFilter";
 | 
				
			||||||
import {Or} from "../Logic/Tags/Or";
 | 
					import {Or} from "../Logic/Tags/Or";
 | 
				
			||||||
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
 | 
					import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
 | 
				
			||||||
import ExtractRelations from "../Logic/Osm/ExtractRelations";
 | 
					import RelationsTracker from "../Logic/Osm/RelationsTracker";
 | 
				
			||||||
import * as OsmToGeoJson from "osmtogeojson";
 | 
					import * as OsmToGeoJson from "osmtogeojson";
 | 
				
			||||||
import MetaTagging from "../Logic/MetaTagging";
 | 
					import MetaTagging from "../Logic/MetaTagging";
 | 
				
			||||||
import {GeoOperations} from "../Logic/GeoOperations";
 | 
					 | 
				
			||||||
import {UIEventSource} from "../Logic/UIEventSource";
 | 
					import {UIEventSource} from "../Logic/UIEventSource";
 | 
				
			||||||
import {TileRange} from "../Models/TileRange";
 | 
					import {TileRange} from "../Models/TileRange";
 | 
				
			||||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
 | 
					import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
 | 
				
			||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
 | 
					 | 
				
			||||||
import ScriptUtils from "./ScriptUtils";
 | 
					import ScriptUtils from "./ScriptUtils";
 | 
				
			||||||
 | 
					import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
 | 
				
			||||||
 | 
					import FilteredLayer from "../Models/FilteredLayer";
 | 
				
			||||||
 | 
					import FeatureSource, {FeatureSourceForLayer} from "../Logic/FeatureSource/FeatureSource";
 | 
				
			||||||
 | 
					import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
 | 
				
			||||||
 | 
					import TiledFeatureSource from "../Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ScriptUtils.fixUtils()
 | 
					ScriptUtils.fixUtils()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createOverpassObject(theme: LayoutConfig) {
 | 
					function createOverpassObject(theme: LayoutConfig, relationTracker: RelationsTracker) {
 | 
				
			||||||
    let filters: TagsFilter[] = [];
 | 
					    let filters: TagsFilter[] = [];
 | 
				
			||||||
    let extraScripts: string[] = [];
 | 
					    let extraScripts: string[] = [];
 | 
				
			||||||
    for (const layer of theme.layers) {
 | 
					    for (const layer of theme.layers) {
 | 
				
			||||||
| 
						 | 
					@ -54,7 +59,7 @@ function createOverpassObject(theme: LayoutConfig) {
 | 
				
			||||||
        throw "Nothing to download! The theme doesn't declare anything to download"
 | 
					        throw "Nothing to download! The theme doesn't declare anything to download"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return new Overpass(new Or(filters), extraScripts, new UIEventSource<string>("https://overpass.kumi.systems/api/interpreter"), //https://overpass-api.de/api/interpreter"),
 | 
					    return new Overpass(new Or(filters), extraScripts, new UIEventSource<string>("https://overpass.kumi.systems/api/interpreter"), //https://overpass-api.de/api/interpreter"),
 | 
				
			||||||
        new UIEventSource<number>(60));
 | 
					        new UIEventSource<number>(60), relationTracker);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function rawJsonName(targetDir: string, x: number, y: number, z: number): string {
 | 
					function rawJsonName(targetDir: string, x: number, y: number, z: number): string {
 | 
				
			||||||
| 
						 | 
					@ -75,7 +80,7 @@ async function downloadRaw(targetdir: string, r: TileRange, overpass: Overpass)/
 | 
				
			||||||
            downloaded++;
 | 
					            downloaded++;
 | 
				
			||||||
            const filename = rawJsonName(targetdir, x, y, r.zoomlevel)
 | 
					            const filename = rawJsonName(targetdir, x, y, r.zoomlevel)
 | 
				
			||||||
            if (existsSync(filename)) {
 | 
					            if (existsSync(filename)) {
 | 
				
			||||||
                console.log("Already exists: ", filename)
 | 
					                console.log("Already exists (not downloading again): ", filename)
 | 
				
			||||||
                skipped++
 | 
					                skipped++
 | 
				
			||||||
                continue;
 | 
					                continue;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					@ -145,14 +150,16 @@ async function downloadExtraData(theme: LayoutConfig)/* : any[] */ {
 | 
				
			||||||
    return allFeatures;
 | 
					    return allFeatures;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]) {
 | 
					
 | 
				
			||||||
 | 
					function loadAllTiles(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]): FeatureSource {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let allFeatures = [...extraFeatures]
 | 
				
			||||||
    let processed = 0;
 | 
					    let processed = 0;
 | 
				
			||||||
    const layerIndex = theme.LayerIndex();
 | 
					 | 
				
			||||||
    for (let x = r.xstart; x <= r.xend; x++) {
 | 
					    for (let x = r.xstart; x <= r.xend; x++) {
 | 
				
			||||||
        for (let y = r.ystart; y <= r.yend; y++) {
 | 
					        for (let y = r.ystart; y <= r.yend; y++) {
 | 
				
			||||||
            processed++;
 | 
					            processed++;
 | 
				
			||||||
            const filename = rawJsonName(targetdir, x, y, r.zoomlevel)
 | 
					            const filename = rawJsonName(targetdir, x, y, r.zoomlevel)
 | 
				
			||||||
            ScriptUtils.erasableLog(" Post processing", processed, "/", r.total, filename)
 | 
					            console.log(" Loading and processing", processed, "/", r.total, filename)
 | 
				
			||||||
            if (!existsSync(filename)) {
 | 
					            if (!existsSync(filename)) {
 | 
				
			||||||
                console.error("Not found - and not downloaded. Run this script again!: " + filename)
 | 
					                console.error("Not found - and not downloaded. Run this script again!: " + filename)
 | 
				
			||||||
                continue;
 | 
					                continue;
 | 
				
			||||||
| 
						 | 
					@ -163,153 +170,98 @@ function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extra
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Create and save the geojson file - which is the main chunk of the data
 | 
					            // Create and save the geojson file - which is the main chunk of the data
 | 
				
			||||||
            const geojson = OsmToGeoJson.default(rawOsm);
 | 
					            const geojson = OsmToGeoJson.default(rawOsm);
 | 
				
			||||||
            const osmTime = new Date(rawOsm.osm3s.timestamp_osm_base);
 | 
					 | 
				
			||||||
            // And merge in the extra features - needed for the metatagging
 | 
					 | 
				
			||||||
            geojson.features.push(...extraFeatures);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for (const feature of geojson.features) {
 | 
					            allFeatures.push(...geojson.features)
 | 
				
			||||||
 | 
					 | 
				
			||||||
                for (const layer of theme.layers) {
 | 
					 | 
				
			||||||
                    if (layer.source.osmTags.matchesProperties(feature.properties)) {
 | 
					 | 
				
			||||||
                        feature["_matching_layer_id"] = layer.id;
 | 
					 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    return new StaticFeatureSource(allFeatures)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Load all the tiles into memory from disk
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsTracker: RelationsTracker, targetdir: string) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function handleLayer(source: FeatureSourceForLayer) {
 | 
				
			||||||
 | 
					        const layer = source.layer.layerDef;
 | 
				
			||||||
 | 
					        const layerId = layer.id
 | 
				
			||||||
 | 
					        if (layer.source.isOsmCacheLayer !== true) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
            const featuresFreshness = geojson.features.map(feature => {
 | 
					        console.log("Handling layer ", layerId, "which has", source.features.data.length, "features")
 | 
				
			||||||
                return ({
 | 
					        if (source.features.data.length === 0) {
 | 
				
			||||||
                    freshness: osmTime,
 | 
					            return;
 | 
				
			||||||
                    feature: feature
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            // Extract the relationship information
 | 
					 | 
				
			||||||
            const relations = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(rawOsm))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            MetaTagging.addMetatags(featuresFreshness, new UIEventSource<{ feature: any; freshness: Date }[]>(featuresFreshness), relations, theme.layers, false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for (const feature of geojson.features) {
 | 
					 | 
				
			||||||
                const layer = layerIndex.get(feature["_matching_layer_id"])
 | 
					 | 
				
			||||||
                if (layer === undefined) {
 | 
					 | 
				
			||||||
                    // Probably some extra, unneeded data, e.g. a point of a way
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        MetaTagging.addMetatags(source.features.data,
 | 
				
			||||||
                if (layer.wayHandling == LayerConfig.WAYHANDLING_CENTER_ONLY) {
 | 
					            {
 | 
				
			||||||
 | 
					                memberships: relationsTracker,
 | 
				
			||||||
                    const centerpoint = GeoOperations.centerpointCoordinates(feature)
 | 
					                getFeaturesWithin: _ => {
 | 
				
			||||||
 | 
					                    return [allFeatures.features.data.map(f => f.feature)]
 | 
				
			||||||
                    feature.geometry.type = "Point"
 | 
					 | 
				
			||||||
                    feature.geometry["coordinates"] = centerpoint;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            layer,
 | 
				
			||||||
 | 
					            false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const createdTiles = []
 | 
				
			||||||
 | 
					        // At this point, we have all the features of the entire area.
 | 
				
			||||||
 | 
					        // However, we want to export them per tile of a fixed size, so we use a dynamicTileSOurce to split it up
 | 
				
			||||||
 | 
					        TiledFeatureSource.createHierarchy(source, {
 | 
				
			||||||
 | 
					            minZoomLevel: 14,
 | 
				
			||||||
 | 
					            maxZoomLevel: 14,
 | 
				
			||||||
 | 
					            maxFeatureCount: undefined,
 | 
				
			||||||
 | 
					            registerTile: tile => {
 | 
				
			||||||
 | 
					                if (tile.z < 12) {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            for (const feature of geojson.features) {
 | 
					                if (tile.features.data.length === 0) {
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                for (const feature of tile.features.data) {
 | 
				
			||||||
                    // Some cleanup
 | 
					                    // Some cleanup
 | 
				
			||||||
                delete feature["bbox"]
 | 
					                    delete feature.feature["bbox"]
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                // Lets save this tile!
 | 
				
			||||||
            const targetPath = geoJsonName(targetdir + ".unfiltered", x, y, r.zoomlevel)
 | 
					                const [z, x, y] = Utils.tile_from_index(tile.tileIndex)
 | 
				
			||||||
            // This is the geojson file containing all features
 | 
					                console.log("Writing tile ", z, x, y, layerId)
 | 
				
			||||||
            writeFileSync(targetPath, JSON.stringify(geojson, null, " "))
 | 
					                const targetPath = geoJsonName(targetdir + "_" + layerId, x, y, z)
 | 
				
			||||||
 | 
					                createdTiles.push(tile.tileIndex)
 | 
				
			||||||
 | 
					                // This is the geojson file containing all features for this tile
 | 
				
			||||||
 | 
					                writeFileSync(targetPath, JSON.stringify({
 | 
				
			||||||
 | 
					                    type: "FeatureCollection",
 | 
				
			||||||
 | 
					                    features: tile.features.data.map(f => f.feature)
 | 
				
			||||||
 | 
					                }, null, " "))
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) {
 | 
					 | 
				
			||||||
    const z = r.zoomlevel;
 | 
					 | 
				
			||||||
    const generated = {} // layer --> x --> y[]
 | 
					 | 
				
			||||||
    for (let x = r.xstart; x <= r.xend; x++) {
 | 
					 | 
				
			||||||
        for (let y = r.ystart; y <= r.yend; y++) {
 | 
					 | 
				
			||||||
            const file = readFileSync(geoJsonName(targetdir + ".unfiltered", x, y, z), "UTF8")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for (const layer of theme.layers) {
 | 
					 | 
				
			||||||
                if (!layer.source.isOsmCacheLayer) {
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                const geojson = JSON.parse(file)
 | 
					 | 
				
			||||||
                const oldLength = geojson.features.length;
 | 
					 | 
				
			||||||
                geojson.features = geojson.features
 | 
					 | 
				
			||||||
                    .filter(f => f._matching_layer_id === layer.id)
 | 
					 | 
				
			||||||
                    .filter(f => {
 | 
					 | 
				
			||||||
                        const isShown = layer.isShown.GetRenderValue(f.properties).txt
 | 
					 | 
				
			||||||
                        return isShown !== "no";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
                const new_path = geoJsonName(targetdir + "_" + layer.id, x, y, z);
 | 
					 | 
				
			||||||
                ScriptUtils.erasableLog(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length, ")")
 | 
					 | 
				
			||||||
                if (geojson.features.length == 0) {
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                writeFileSync(new_path, JSON.stringify(geojson, null, " "))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (generated[layer.id] === undefined) {
 | 
					        // All the tiles are written at this point
 | 
				
			||||||
                    generated[layer.id] = {}
 | 
					        // Only thing left to do is to create the index
 | 
				
			||||||
 | 
					        const path = targetdir + "_" + layerId + "_overview.json"
 | 
				
			||||||
 | 
					        const perX = {}
 | 
				
			||||||
 | 
					        createdTiles.map(i => Utils.tile_from_index(i)).forEach(([z, x, y]) => {
 | 
				
			||||||
 | 
					            const key = "" + x
 | 
				
			||||||
 | 
					            if (perX[key] === undefined) {
 | 
				
			||||||
 | 
					                perX[key] = []
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
                if (generated[layer.id][x] === undefined) {
 | 
					            perX[key].push(y)
 | 
				
			||||||
                    generated[layer.id][x] = []
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                generated[layer.id][x].push(y)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const layer of theme.layers) {
 | 
					 | 
				
			||||||
        const id = layer.id
 | 
					 | 
				
			||||||
        const loaded = generated[id]
 | 
					 | 
				
			||||||
        if (loaded === undefined) {
 | 
					 | 
				
			||||||
            console.log("No features loaded for layer ", id)
 | 
					 | 
				
			||||||
            continue;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        writeFileSync(targetdir + "_" + id + "_overview.json", JSON.stringify(loaded))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function createOverview(targetdir: string, r: TileRange, z: number, layername: string) {
 | 
					 | 
				
			||||||
    const allFeatures = []
 | 
					 | 
				
			||||||
    for (let x = r.xstart; x <= r.xend; x++) {
 | 
					 | 
				
			||||||
        for (let y = r.ystart; y <= r.yend; y++) {
 | 
					 | 
				
			||||||
            const read_path = geoJsonName(targetdir + "_" + layername, x, y, z);
 | 
					 | 
				
			||||||
            if (!fs.existsSync(read_path)) {
 | 
					 | 
				
			||||||
                continue;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const features = JSON.parse(fs.readFileSync(read_path, "UTF-8")).features
 | 
					 | 
				
			||||||
            const pointsOnly = features.map(f => {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                f.properties["_last_edit:timestamp"] = "1970-01-01"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (f.geometry.type === "Point") {
 | 
					 | 
				
			||||||
                    return f
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    return GeoOperations.centerpoint(f)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
            allFeatures.push(...pointsOnly)
 | 
					        writeFileSync(path, JSON.stringify(perX))
 | 
				
			||||||
        }
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const featuresDedup = []
 | 
					    new PerLayerFeatureSourceSplitter(
 | 
				
			||||||
    const seen = new Set<string>()
 | 
					        new UIEventSource<FilteredLayer[]>(theme.layers.map(l => ({
 | 
				
			||||||
    for (const feature of allFeatures) {
 | 
					            layerDef: l,
 | 
				
			||||||
        const id = feature.properties.id
 | 
					            isDisplayed: new UIEventSource<boolean>(true),
 | 
				
			||||||
        if (seen.has(id)) {
 | 
					            appliedFilters: new UIEventSource(undefined)
 | 
				
			||||||
            continue
 | 
					        }))),
 | 
				
			||||||
        }
 | 
					        handleLayer,
 | 
				
			||||||
        seen.add(id)
 | 
					        allFeatures
 | 
				
			||||||
        featuresDedup.push(feature)
 | 
					    )
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const geojson = {
 | 
					 | 
				
			||||||
        "type": "FeatureCollection",
 | 
					 | 
				
			||||||
        "features": featuresDedup
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    writeFileSync(targetdir + "_" + layername + "_points.geojson", JSON.stringify(geojson, null, " "))
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function main(args: string[]) {
 | 
					async function main(args: string[]) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (args.length == 0) {
 | 
					    if (args.length == 0) {
 | 
				
			||||||
| 
						 | 
					@ -335,8 +287,8 @@ async function main(args: string[]) {
 | 
				
			||||||
        console.error("The theme " + theme + " was not found; try one of ", keys);
 | 
					        console.error("The theme " + theme + " was not found; try one of ", keys);
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    const relationTracker = new RelationsTracker()
 | 
				
			||||||
    const overpass = createOverpassObject(theme)
 | 
					    const overpass = createOverpassObject(theme, relationTracker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let failed = 0;
 | 
					    let failed = 0;
 | 
				
			||||||
    do {
 | 
					    do {
 | 
				
			||||||
| 
						 | 
					@ -348,21 +300,13 @@ async function main(args: string[]) {
 | 
				
			||||||
    } while (failed > 0)
 | 
					    } while (failed > 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const extraFeatures = await downloadExtraData(theme);
 | 
					    const extraFeatures = await downloadExtraData(theme);
 | 
				
			||||||
    postProcess(targetdir, tileRange, theme, extraFeatures)
 | 
					    const allFeaturesSource = loadAllTiles(targetdir, tileRange, theme, extraFeatures)
 | 
				
			||||||
    splitPerLayer(targetdir, tileRange, theme)
 | 
					    postProcess(allFeaturesSource, theme, relationTracker, targetdir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (args[7] === "--generate-point-overview") {
 | 
					 | 
				
			||||||
        const targetLayers = args[8].split(",")
 | 
					 | 
				
			||||||
        for (const targetLayer of targetLayers) {
 | 
					 | 
				
			||||||
            if (!theme.layers.some(l => l.id === targetLayer)) {
 | 
					 | 
				
			||||||
                throw "Target layer " + targetLayer + " not found, did you mistype the name? Found layers are: " + theme.layers.map(l => l.id).join(",")
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            createOverview(targetdir, tileRange, zoomlevel, targetLayer)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let args = [...process.argv]
 | 
					let args = [...process.argv]
 | 
				
			||||||
args.splice(0, 2)
 | 
					args.splice(0, 2)
 | 
				
			||||||
main(args);
 | 
					main(args);
 | 
				
			||||||
 | 
					console.log("All done!")
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue