forked from MapComplete/MapComplete
		
	Merge files from develop branch
This commit is contained in:
		
							parent
							
								
									2a03b699d5
								
							
						
					
					
						commit
						3f29632cf4
					
				
					 16 changed files with 484 additions and 135 deletions
				
			
		| 
						 | 
				
			
			@ -185,6 +185,9 @@ export class InitUiElements {
 | 
			
		|||
        // Reset the loading message once things are loaded
 | 
			
		||||
        new CenterMessageBox().AttachTo("centermessage");
 | 
			
		||||
 | 
			
		||||
        // At last, zoom to the needed location if the focus is on an element
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static LoadLayoutFromHash(userLayoutParam: UIEventSource<string>) {
 | 
			
		||||
| 
						 | 
				
			
			@ -391,12 +394,21 @@ export class InitUiElements {
 | 
			
		|||
 | 
			
		||||
        const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap);
 | 
			
		||||
        State.state.layerUpdater = updater;
 | 
			
		||||
        const source = new FeaturePipeline(state.filteredLayers, updater, state.layoutToUse, state.changes, state.locationControl);
 | 
			
		||||
        
 | 
			
		||||
        
 | 
			
		||||
        
 | 
			
		||||
        const source = new FeaturePipeline(state.filteredLayers, 
 | 
			
		||||
            updater, 
 | 
			
		||||
            state.osmApiFeatureSource, 
 | 
			
		||||
            state.layoutToUse, 
 | 
			
		||||
            state.changes,
 | 
			
		||||
            state.locationControl,
 | 
			
		||||
            state.selectedElement);
 | 
			
		||||
 | 
			
		||||
        new ShowDataLayer(source.features, State.state.leafletMap, State.state.layoutToUse);
 | 
			
		||||
 | 
			
		||||
        new SelectedFeatureHandler(Hash.hash, State.state.selectedElement, source);
 | 
			
		||||
 | 
			
		||||
        const selectedFeatureHandler = new SelectedFeatureHandler(Hash.hash, State.state.selectedElement, source, State.state.osmApiFeatureSource);
 | 
			
		||||
        selectedFeatureHandler.zoomToSelectedFeature(State.state.locationControl);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,12 @@
 | 
			
		|||
import {UIEventSource} from "../UIEventSource";
 | 
			
		||||
import FeatureSource from "../FeatureSource/FeatureSource";
 | 
			
		||||
import {OsmObject, OsmObjectMeta} from "../Osm/OsmObject";
 | 
			
		||||
import Loc from "../../Models/Loc";
 | 
			
		||||
import FeaturePipeline from "../FeatureSource/FeaturePipeline";
 | 
			
		||||
import OsmApiFeatureSource from "../FeatureSource/OsmApiFeatureSource";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Makes sure the hash shows the selected element and vice-versa
 | 
			
		||||
 * Makes sure the hash shows the selected element and vice-versa.
 | 
			
		||||
 */
 | 
			
		||||
export default class SelectedFeatureHandler {
 | 
			
		||||
    private readonly _featureSource: FeatureSource;
 | 
			
		||||
| 
						 | 
				
			
			@ -10,13 +14,16 @@ export default class SelectedFeatureHandler {
 | 
			
		|||
    private readonly _selectedFeature: UIEventSource<any>;
 | 
			
		||||
 | 
			
		||||
    private static readonly _no_trigger_on = ["welcome","copyright","layers"]
 | 
			
		||||
    private readonly _osmApiSource: OsmApiFeatureSource;
 | 
			
		||||
    
 | 
			
		||||
    constructor(hash: UIEventSource<string>, 
 | 
			
		||||
                selectedFeature: UIEventSource<any>,
 | 
			
		||||
                featureSource: FeatureSource) {
 | 
			
		||||
                featureSource: FeaturePipeline,
 | 
			
		||||
                osmApiSource: OsmApiFeatureSource) {
 | 
			
		||||
        this._hash = hash;
 | 
			
		||||
        this._selectedFeature = selectedFeature;
 | 
			
		||||
        this._featureSource = featureSource;
 | 
			
		||||
        this._osmApiSource = osmApiSource;
 | 
			
		||||
        const self = this;
 | 
			
		||||
        hash.addCallback(h => {
 | 
			
		||||
            if (h === undefined || h === "") {
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +33,8 @@ export default class SelectedFeatureHandler {
 | 
			
		|||
            }
 | 
			
		||||
        })
 | 
			
		||||
        
 | 
			
		||||
        hash.addCallbackAndRun(h => self.downloadFeature(h))
 | 
			
		||||
        
 | 
			
		||||
        featureSource.features.addCallback(_ => self.selectFeature());
 | 
			
		||||
 | 
			
		||||
        selectedFeature.addCallback(feature => {
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +54,32 @@ export default class SelectedFeatureHandler {
 | 
			
		|||
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // If a feature is selected via the hash, zoom there
 | 
			
		||||
    public zoomToSelectedFeature(location: UIEventSource<Loc>){
 | 
			
		||||
        const hash = this._hash.data;
 | 
			
		||||
        if(hash === undefined || SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0){
 | 
			
		||||
            return; // No valid feature selected
 | 
			
		||||
        }
 | 
			
		||||
        // We should have a valid osm-ID and zoom to it
 | 
			
		||||
        OsmObject.DownloadObject(hash, (element: OsmObject, meta: OsmObjectMeta) => {
 | 
			
		||||
            const centerpoint = element.centerpoint();
 | 
			
		||||
            console.log("Zooming to location for select point: ", centerpoint)
 | 
			
		||||
            location.data.lat = centerpoint[0]
 | 
			
		||||
            location.data.lon = centerpoint[1]
 | 
			
		||||
            location.ping();
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private downloadFeature(hash:  string){
 | 
			
		||||
        if(hash === undefined || hash === ""){
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if(SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0){
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
       this._osmApiSource.load(hash)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private selectFeature(){
 | 
			
		||||
        const features = this._featureSource?.features?.data;
 | 
			
		||||
        if(features === undefined){
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,6 +48,7 @@ export class ElementStorage {
 | 
			
		|||
        const keptKeys = es.data;
 | 
			
		||||
        // The element already exists
 | 
			
		||||
        // We use the new feature to overwrite all the properties in the already existing eventsource
 | 
			
		||||
        console.log("Merging multiple instances of ", elementId)
 | 
			
		||||
        let somethingChanged = false;
 | 
			
		||||
        for (const k in newProperties) {
 | 
			
		||||
            const v = newProperties[k];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,16 +22,21 @@ export default class FeaturePipeline implements FeatureSource {
 | 
			
		|||
 | 
			
		||||
    constructor(flayers: UIEventSource<{ isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[]>,
 | 
			
		||||
                updater: FeatureSource,
 | 
			
		||||
                fromOsmApi: FeatureSource,
 | 
			
		||||
                layout: UIEventSource<LayoutConfig>,
 | 
			
		||||
                newPoints: FeatureSource,
 | 
			
		||||
                locationControl: UIEventSource<Loc>) {
 | 
			
		||||
                locationControl: UIEventSource<Loc>,
 | 
			
		||||
                selectedElement: UIEventSource<any>) {
 | 
			
		||||
 | 
			
		||||
        // first we metatag, then we save to get the metatags into storage too
 | 
			
		||||
        // Note that we need to register before we do metatagging (as it expects the event sources)
 | 
			
		||||
 | 
			
		||||
        const amendedOverpassSource =
 | 
			
		||||
            new RememberingSource(
 | 
			
		||||
                new LocalStorageSaver(
 | 
			
		||||
                    new MetaTaggingFeatureSource( // first we metatag, then we save to get the metatags into storage too
 | 
			
		||||
                        new RegisteringFeatureSource(
 | 
			
		||||
                    new FeatureDuplicatorPerLayer(flayers,
 | 
			
		||||
                        new MetaTaggingFeatureSource(
 | 
			
		||||
                            new RegisteringFeatureSource(
 | 
			
		||||
                                updater)
 | 
			
		||||
                        )), layout));
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -46,10 +51,16 @@ export default class FeaturePipeline implements FeatureSource {
 | 
			
		|||
        newPoints = new MetaTaggingFeatureSource(new FeatureDuplicatorPerLayer(flayers,
 | 
			
		||||
            new RegisteringFeatureSource(newPoints)));
 | 
			
		||||
 | 
			
		||||
        const amendedOsmApiSource = new RememberingSource(
 | 
			
		||||
            new FeatureDuplicatorPerLayer(flayers,
 | 
			
		||||
                new MetaTaggingFeatureSource(
 | 
			
		||||
                    new RegisteringFeatureSource(fromOsmApi))));
 | 
			
		||||
 | 
			
		||||
        const merged =
 | 
			
		||||
 | 
			
		||||
            new FeatureSourceMerger([
 | 
			
		||||
                amendedOverpassSource,
 | 
			
		||||
                amendedOsmApiSource,
 | 
			
		||||
                amendedLocalStorageSource,
 | 
			
		||||
                newPoints,
 | 
			
		||||
                ...geojsonSources
 | 
			
		||||
| 
						 | 
				
			
			@ -60,6 +71,7 @@ export default class FeaturePipeline implements FeatureSource {
 | 
			
		|||
                new FilteringFeatureSource(
 | 
			
		||||
                    flayers,
 | 
			
		||||
                    locationControl,
 | 
			
		||||
                    selectedElement,
 | 
			
		||||
                    merged
 | 
			
		||||
                ));
 | 
			
		||||
        this.features = source.features;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,27 +21,48 @@ export default class FeatureSourceMerger implements FeatureSource {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    private Update() {
 | 
			
		||||
        let all = {}; // Mapping 'id' -> {feature, freshness}
 | 
			
		||||
 | 
			
		||||
        let somethingChanged = false;
 | 
			
		||||
        const all: Map<string, { feature: any, freshness: Date }> = new Map<string, { feature: any; freshness: Date }>();
 | 
			
		||||
        // We seed the dictionary with the previously loaded features
 | 
			
		||||
        const oldValues = this.features.data ?? [];
 | 
			
		||||
        for (const oldValue of oldValues) {
 | 
			
		||||
            all.set(oldValue.feature.id, oldValue)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const source of this._sources) {
 | 
			
		||||
            if (source?.features?.data === undefined) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            for (const f of source.features.data) {
 | 
			
		||||
                const id = f.feature.properties.id;
 | 
			
		||||
                const oldV = all[id];
 | 
			
		||||
                if (oldV === undefined) {
 | 
			
		||||
                    all[id] = f;
 | 
			
		||||
                } else {
 | 
			
		||||
                if (!all.has(id)) {
 | 
			
		||||
                    // This is a new feature
 | 
			
		||||
                    somethingChanged = true;
 | 
			
		||||
                    all.set(id, f);
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // This value has been seen already, either in a previous run or by a previous datasource
 | 
			
		||||
                // Let's figure out if something changed
 | 
			
		||||
                const oldV = all.get(id);
 | 
			
		||||
                if (oldV.freshness < f.freshness) {
 | 
			
		||||
                        all[id] = f;
 | 
			
		||||
                    // Jup, this feature is fresher
 | 
			
		||||
                    all.set(id, f);
 | 
			
		||||
                    somethingChanged = true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if(!somethingChanged){
 | 
			
		||||
            // We don't bother triggering an update
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        const newList = [];
 | 
			
		||||
        for (const id in all) {
 | 
			
		||||
            newList.push(all[id]);
 | 
			
		||||
        }
 | 
			
		||||
        all.forEach((value, key) => {
 | 
			
		||||
            newList.push(value)
 | 
			
		||||
        })
 | 
			
		||||
        this.features.setData(newList);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,12 +5,14 @@ import Loc from "../../Models/Loc";
 | 
			
		|||
 | 
			
		||||
export default class FilteringFeatureSource implements FeatureSource {
 | 
			
		||||
    public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
 | 
			
		||||
public readonly name = "FilteringFeatureSource"
 | 
			
		||||
    public readonly name = "FilteringFeatureSource"
 | 
			
		||||
 | 
			
		||||
    constructor(layers: UIEventSource<{
 | 
			
		||||
                    isDisplayed: UIEventSource<boolean>,
 | 
			
		||||
                    layerDef: LayerConfig
 | 
			
		||||
                }[]>,
 | 
			
		||||
                location: UIEventSource<Loc>,
 | 
			
		||||
                selectedElement: UIEventSource<any>,
 | 
			
		||||
                upstream: FeatureSource) {
 | 
			
		||||
 | 
			
		||||
        const self = this;
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +20,7 @@ public readonly name = "FilteringFeatureSource"
 | 
			
		|||
        function update() {
 | 
			
		||||
 | 
			
		||||
            const layerDict = {};
 | 
			
		||||
            if(layers.data.length == 0){
 | 
			
		||||
            if (layers.data.length == 0) {
 | 
			
		||||
                throw "No layers defined!"
 | 
			
		||||
            }
 | 
			
		||||
            for (const layer of layers.data) {
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +34,11 @@ public readonly name = "FilteringFeatureSource"
 | 
			
		|||
            const newFeatures = features.filter(f => {
 | 
			
		||||
                const layerId = f.feature._matching_layer_id;
 | 
			
		||||
                
 | 
			
		||||
                if(selectedElement.data !== undefined && selectedElement.data?.id === f.feature.id){
 | 
			
		||||
                    // This is the selected object - it gets a free pass even if zoom is not sufficient
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                if (layerId !== undefined) {
 | 
			
		||||
                    const layer: {
 | 
			
		||||
                        isDisplayed: UIEventSource<boolean>,
 | 
			
		||||
| 
						 | 
				
			
			@ -44,9 +51,9 @@ public readonly name = "FilteringFeatureSource"
 | 
			
		|||
 | 
			
		||||
                    const isShown = layer.layerDef.isShown
 | 
			
		||||
                    const tags = f.feature.properties;
 | 
			
		||||
                    if(isShown.IsKnown(tags)){
 | 
			
		||||
                    if (isShown.IsKnown(tags)) {
 | 
			
		||||
                        const result = layer.layerDef.isShown.GetRenderValue(f.feature.properties).txt;
 | 
			
		||||
                        if(result !== "yes"){
 | 
			
		||||
                        if (result !== "yes") {
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +76,7 @@ public readonly name = "FilteringFeatureSource"
 | 
			
		|||
            });
 | 
			
		||||
            console.log("Filtering layer source: input: ", upstream.features.data?.length, "output:", newFeatures.length)
 | 
			
		||||
            self.features.setData(newFeatures);
 | 
			
		||||
            if(missingLayers.size > 0){
 | 
			
		||||
            if (missingLayers.size > 0) {
 | 
			
		||||
                console.error("Some layers were not found: ", Array.from(missingLayers))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -86,7 +93,7 @@ public readonly name = "FilteringFeatureSource"
 | 
			
		|||
                if (l.zoom < layer.layerDef.minzoom) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                if(l.zoom > layer.layerDef.maxzoom){
 | 
			
		||||
                if (l.zoom > layer.layerDef.maxzoom) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                if (!layer.isDisplayed.data) {
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +111,7 @@ public readonly name = "FilteringFeatureSource"
 | 
			
		|||
        const registered = new Set<UIEventSource<boolean>>();
 | 
			
		||||
        layers.addCallbackAndRun(layers => {
 | 
			
		||||
            for (const layer of layers) {
 | 
			
		||||
                if(registered.has(layer.isDisplayed)){
 | 
			
		||||
                if (registered.has(layer.isDisplayed)) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                registered.add(layer.isDisplayed);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,7 +33,7 @@ export default class GeoJsonSource implements FeatureSource {
 | 
			
		|||
        if (zoomLevel === undefined) {
 | 
			
		||||
            // This is a classic, static geojson layer
 | 
			
		||||
            if (onFail === undefined) {
 | 
			
		||||
                onFail = errorMsg => {
 | 
			
		||||
                onFail = _ => {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            this.onFail = onFail;
 | 
			
		||||
| 
						 | 
				
			
			@ -44,57 +44,6 @@ export default class GeoJsonSource implements FeatureSource {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ConfigureDynamicLayer(url: string, zoomLevel: number, locationControl: UIEventSource<Loc>, flayer:  { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }){
 | 
			
		||||
        // This is a dynamic template with a fixed zoom level
 | 
			
		||||
        url = url.replace("{z}", "" + zoomLevel)
 | 
			
		||||
        const loadedTiles = new Set<string>();
 | 
			
		||||
        const self = this;
 | 
			
		||||
        this.onFail = (msg, url) => {
 | 
			
		||||
            console.warn(`Could not load geojson layer from`, url, "due to", msg)
 | 
			
		||||
            loadedTiles.add(url); // We add the url to the 'loadedTiles' in order to not reload it in the future
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const neededTiles = locationControl.map(
 | 
			
		||||
            location => {
 | 
			
		||||
                // Yup, this is cheating to just get the bounds here
 | 
			
		||||
                const bounds = State.state.leafletMap.data.getBounds()
 | 
			
		||||
                const tileRange = Utils.TileRangeBetween(zoomLevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
 | 
			
		||||
                const needed = new Set<string>();
 | 
			
		||||
                for (let x = tileRange.xstart; x <= tileRange.xend; x++) {
 | 
			
		||||
                    for (let y = tileRange.ystart; y <= tileRange.yend; y++) {
 | 
			
		||||
                        let neededUrl = url.replace("{x}", "" + x).replace("{y}", "" + y);
 | 
			
		||||
                        needed.add(neededUrl)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return needed;
 | 
			
		||||
            }
 | 
			
		||||
        , [flayer.isDisplayed]);
 | 
			
		||||
        neededTiles.stabilized(250).addCallback((needed: Set<string>) => {
 | 
			
		||||
            if (needed === undefined) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            if (!flayer.isDisplayed.data) {
 | 
			
		||||
                // No need to download! - the layer is disabled
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
           if(locationControl.data.zoom < flayer.layerDef.minzoom){
 | 
			
		||||
               return;
 | 
			
		||||
           }
 | 
			
		||||
            
 | 
			
		||||
            needed.forEach(neededTile => {
 | 
			
		||||
                if (loadedTiles.has(neededTile)) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                loadedTiles.add(neededTile)
 | 
			
		||||
                self.LoadJSONFrom(neededTile)
 | 
			
		||||
 | 
			
		||||
            })
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Merges together the layers which have the same source
 | 
			
		||||
     * @param flayers
 | 
			
		||||
| 
						 | 
				
			
			@ -152,6 +101,53 @@ export default class GeoJsonSource implements FeatureSource {
 | 
			
		|||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ConfigureDynamicLayer(url: string, zoomLevel: number, locationControl: UIEventSource<Loc>, flayer: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }) {
 | 
			
		||||
        // This is a dynamic template with a fixed zoom level
 | 
			
		||||
        url = url.replace("{z}", "" + zoomLevel)
 | 
			
		||||
        const loadedTiles = new Set<string>();
 | 
			
		||||
        const self = this;
 | 
			
		||||
        this.onFail = (msg, url) => {
 | 
			
		||||
            console.warn(`Could not load geojson layer from`, url, "due to", msg)
 | 
			
		||||
            loadedTiles.add(url); // We add the url to the 'loadedTiles' in order to not reload it in the future
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const neededTiles = locationControl.map(
 | 
			
		||||
            _ => {
 | 
			
		||||
                // Yup, this is cheating to just get the bounds here
 | 
			
		||||
                const bounds = State.state.leafletMap.data.getBounds()
 | 
			
		||||
                const tileRange = Utils.TileRangeBetween(zoomLevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
 | 
			
		||||
                const needed = Utils.MapRange(tileRange, (x, y) => {
 | 
			
		||||
                    return url.replace("{x}", "" + x).replace("{y}", "" + y);
 | 
			
		||||
                })
 | 
			
		||||
                return new Set<string>(needed);
 | 
			
		||||
            }
 | 
			
		||||
            , [flayer.isDisplayed]);
 | 
			
		||||
        neededTiles.stabilized(250).addCallback((needed: Set<string>) => {
 | 
			
		||||
            if (needed === undefined) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            if (!flayer.isDisplayed.data) {
 | 
			
		||||
                // No need to download! - the layer is disabled
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (locationControl.data.zoom < flayer.layerDef.minzoom) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            needed.forEach(neededTile => {
 | 
			
		||||
                if (loadedTiles.has(neededTile)) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                loadedTiles.add(neededTile)
 | 
			
		||||
                self.LoadJSONFrom(neededTile)
 | 
			
		||||
 | 
			
		||||
            })
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private LoadJSONFrom(url: string) {
 | 
			
		||||
        const eventSource = this.features;
 | 
			
		||||
        const self = this;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										91
									
								
								Logic/FeatureSource/OsmApiFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								Logic/FeatureSource/OsmApiFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,91 @@
 | 
			
		|||
import FeatureSource from "./FeatureSource";
 | 
			
		||||
import {UIEventSource} from "../UIEventSource";
 | 
			
		||||
import {OsmObject} from "../Osm/OsmObject";
 | 
			
		||||
import State from "../../State";
 | 
			
		||||
import {Utils} from "../../Utils";
 | 
			
		||||
import Loc from "../../Models/Loc";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default class OsmApiFeatureSource implements FeatureSource {
 | 
			
		||||
    public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
 | 
			
		||||
    public readonly name: string = "OsmApiFeatureSource";
 | 
			
		||||
    private readonly loadedTiles: Set<string> = new Set<string>();
 | 
			
		||||
 | 
			
		||||
    constructor(location: UIEventSource<Loc>) {
 | 
			
		||||
       /* const self = this
 | 
			
		||||
        location.addCallback(_ => {
 | 
			
		||||
            self.loadArea()
 | 
			
		||||
        })
 | 
			
		||||
        */
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public load(id: string) {
 | 
			
		||||
        OsmObject.DownloadObject(id, (element, meta) => {
 | 
			
		||||
            const geojson = element.asGeoJson();
 | 
			
		||||
            geojson.id = geojson.properties.id;
 | 
			
		||||
            this.features.setData([{feature: geojson, freshness: meta["_last_edit:timestamp"]}])
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Loads the current inview-area
 | 
			
		||||
     */
 | 
			
		||||
    public loadArea(z: number = 16): boolean {
 | 
			
		||||
        const layers = State.state.filteredLayers.data;
 | 
			
		||||
 | 
			
		||||
        const disabledLayers = layers.filter(layer => layer.layerDef.source.overpassScript !== undefined || layer.layerDef.source.geojsonSource !== undefined)
 | 
			
		||||
        if (disabledLayers.length > 0) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        const loc = State.state.locationControl.data;
 | 
			
		||||
        if (loc.zoom < 16) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        if (State.state.leafletMap.data === undefined) {
 | 
			
		||||
            return false; // Not yet inited
 | 
			
		||||
        }
 | 
			
		||||
        const bounds = State.state.leafletMap.data.getBounds()
 | 
			
		||||
        const tileRange = Utils.TileRangeBetween(z, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
 | 
			
		||||
        const self = this;
 | 
			
		||||
        Utils.MapRange(tileRange, (x, y) => {
 | 
			
		||||
            const key = x + "/" + y;
 | 
			
		||||
            if (self.loadedTiles.has(key)) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            self.loadedTiles.add(key);
 | 
			
		||||
 | 
			
		||||
            const bounds = Utils.tile_bounds(z, x, y);
 | 
			
		||||
            console.log("Loading OSM data tile", z, x, y, " with bounds", bounds)
 | 
			
		||||
            OsmObject.LoadArea(bounds, objects => {
 | 
			
		||||
                const keptGeoJson: {feature:any, freshness: Date}[] = []
 | 
			
		||||
                // Which layer does the object match?
 | 
			
		||||
                for (const object of objects) {
 | 
			
		||||
 | 
			
		||||
                    for (const flayer of layers) {
 | 
			
		||||
                        const layer = flayer.layerDef;
 | 
			
		||||
                        const tags = object.tags
 | 
			
		||||
                        const doesMatch = layer.source.osmTags.matchesProperties(tags);
 | 
			
		||||
                        if (doesMatch) {
 | 
			
		||||
                            const geoJson = object.asGeoJson();
 | 
			
		||||
                            geoJson._matching_layer_id = layer.id
 | 
			
		||||
                            keptGeoJson.push({feature: geoJson, freshness:  object.timestamp})
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                self.features.setData(keptGeoJson)
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,7 @@
 | 
			
		|||
import LayerConfig from "../Customizations/JSON/LayerConfig";
 | 
			
		||||
import SimpleMetaTagger from "./SimpleMetaTagger";
 | 
			
		||||
import {ExtraFunction} from "./ExtraFunction";
 | 
			
		||||
import State from "../State";
 | 
			
		||||
import {Relation} from "./Osm/ExtractRelations";
 | 
			
		||||
import {meta} from "@turf/turf";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface Params {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,10 +9,14 @@ export abstract class OsmObject {
 | 
			
		|||
    tags: {} = {};
 | 
			
		||||
    version: number;
 | 
			
		||||
    public changed: boolean = false;
 | 
			
		||||
    timestamp: Date;
 | 
			
		||||
 | 
			
		||||
    protected constructor(type: string, id: number) {
 | 
			
		||||
        this.id = id;
 | 
			
		||||
        this.type = type;
 | 
			
		||||
        this.tags = {
 | 
			
		||||
            id: id
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static DownloadObject(id, continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -20,8 +24,7 @@ export abstract class OsmObject {
 | 
			
		|||
        const type = splitted[0];
 | 
			
		||||
        const idN = splitted[1];
 | 
			
		||||
 | 
			
		||||
        const newContinuation = (element: OsmObject, meta :OsmObjectMeta) => {
 | 
			
		||||
            console.log("Received: ", element, "with meta", meta);
 | 
			
		||||
        const newContinuation = (element: OsmObject, meta: OsmObjectMeta) => {
 | 
			
		||||
            continuation(element, meta);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +39,80 @@ export abstract class OsmObject {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static DownloadHistory(id: string, continuation: (versions: OsmObject[]) => void): void {
 | 
			
		||||
        const splitted = id.split("/");
 | 
			
		||||
        const type = splitted[0];
 | 
			
		||||
        const idN = splitted[1];
 | 
			
		||||
        $.getJSON("https://openStreetMap.org/api/0.6/" + type + "/" + idN + "/history", data => {
 | 
			
		||||
            const elements: any[] = data.elements;
 | 
			
		||||
            const osmObjects: OsmObject[] = []
 | 
			
		||||
            for (const element of elements) {
 | 
			
		||||
                let osmObject: OsmObject = null
 | 
			
		||||
                switch (type) {
 | 
			
		||||
                    case("node"):
 | 
			
		||||
                        osmObject = new OsmNode(idN);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case("way"):
 | 
			
		||||
                        osmObject = new OsmWay(idN);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case("relation"):
 | 
			
		||||
                        osmObject = new OsmRelation(idN);
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
                osmObject?.LoadData(element);
 | 
			
		||||
                osmObject?.SaveExtraData(element, []);
 | 
			
		||||
                osmObjects.push(osmObject)
 | 
			
		||||
            }
 | 
			
		||||
            continuation(osmObjects)
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ParseObjects(elements: any[]) : OsmObject[]{
 | 
			
		||||
        const objects: OsmObject[] = [];
 | 
			
		||||
        const allNodes: Map<number, OsmNode> = new Map<number, OsmNode>()
 | 
			
		||||
        for (const element of elements) {
 | 
			
		||||
            const type = element.type;
 | 
			
		||||
            const idN = element.id;
 | 
			
		||||
            let osmObject: OsmObject = null
 | 
			
		||||
            switch (type) {
 | 
			
		||||
                case("node"):
 | 
			
		||||
                    const node = new OsmNode(idN);
 | 
			
		||||
                    allNodes.set(idN, node);
 | 
			
		||||
                    osmObject = node
 | 
			
		||||
                    node.SaveExtraData(element);
 | 
			
		||||
                    break;
 | 
			
		||||
                case("way"):
 | 
			
		||||
                    osmObject = new OsmWay(idN);
 | 
			
		||||
                    const nodes = element.nodes.map(i => allNodes.get(i));
 | 
			
		||||
                    osmObject.SaveExtraData(element, nodes)
 | 
			
		||||
                    break;
 | 
			
		||||
                case("relation"):
 | 
			
		||||
                    osmObject = new OsmRelation(idN);
 | 
			
		||||
                    osmObject.SaveExtraData(element, [])
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
            osmObject.LoadData(element)
 | 
			
		||||
            objects.push(osmObject)
 | 
			
		||||
        }
 | 
			
		||||
        return objects;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    //Loads an area from the OSM-api.
 | 
			
		||||
    // bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds)
 | 
			
		||||
    public static LoadArea(bounds: [[number, number], [number, number]], callback: (objects: OsmObject[]) => void) {
 | 
			
		||||
        const minlon = bounds[0][1]
 | 
			
		||||
        const maxlon = bounds[1][1]
 | 
			
		||||
        const minlat = bounds[1][0]
 | 
			
		||||
        const maxlat = bounds[0][0];
 | 
			
		||||
        const url = `https://www.openstreetmap.org/api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}`
 | 
			
		||||
        $.getJSON(url, data => {
 | 
			
		||||
            const elements: any[] = data.elements;
 | 
			
		||||
            const objects = OsmObject.ParseObjects(elements)
 | 
			
		||||
            callback(objects);
 | 
			
		||||
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static DownloadAll(neededIds, knownElements: any = {}, continuation: ((knownObjects: any) => void)) {
 | 
			
		||||
        // local function which downloads all the objects one by one
 | 
			
		||||
        // this is one big loop, running one download, then rerunning the entire function
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +127,6 @@ export abstract class OsmObject {
 | 
			
		|||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        console.log("Downloading ", neededId);
 | 
			
		||||
        OsmObject.DownloadObject(neededId,
 | 
			
		||||
            function (element) {
 | 
			
		||||
                knownElements[neededId] = element; // assign the element for later, continue downloading the next element
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +135,12 @@ export abstract class OsmObject {
 | 
			
		|||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abstract SaveExtraData(element);
 | 
			
		||||
    // The centerpoint of the feature, as [lat, lon]
 | 
			
		||||
    public abstract centerpoint(): [number, number];
 | 
			
		||||
 | 
			
		||||
    public abstract asGeoJson(): any;
 | 
			
		||||
 | 
			
		||||
    abstract SaveExtraData(element: any, allElements: any[]);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Generates the changeset-XML for tags
 | 
			
		||||
| 
						 | 
				
			
			@ -78,12 +159,20 @@ export abstract class OsmObject {
 | 
			
		|||
 | 
			
		||||
    Download(continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) {
 | 
			
		||||
        const self = this;
 | 
			
		||||
        $.getJSON("https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id,
 | 
			
		||||
            function (data) {
 | 
			
		||||
                const element = data.elements[0];
 | 
			
		||||
                self.tags = element.tags;
 | 
			
		||||
                self.version = element.version;
 | 
			
		||||
                self.SaveExtraData(element);
 | 
			
		||||
        const full = this.type !== "way" ? "" : "/full";
 | 
			
		||||
        const url = "https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id + full;
 | 
			
		||||
        $.getJSON(url, function (data) {
 | 
			
		||||
            
 | 
			
		||||
                const element = data.elements.pop();
 | 
			
		||||
 | 
			
		||||
                let nodes = []
 | 
			
		||||
                if(data.elements.length > 2){
 | 
			
		||||
                    nodes = OsmObject.ParseObjects(data.elements)
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                self.LoadData(element)
 | 
			
		||||
                self.SaveExtraData(element, nodes);
 | 
			
		||||
 | 
			
		||||
                continuation(self, {
 | 
			
		||||
                    "_last_edit:contributor": element.user,
 | 
			
		||||
                    "_last_edit:contributor:uid": element.uid,
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +191,7 @@ export abstract class OsmObject {
 | 
			
		|||
            if (oldV == v) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            console.log("WARNING: overwriting ", oldV, " with ", v, " for key ", k)
 | 
			
		||||
            console.log("Overwriting ", oldV, " with ", v, " for key ", k)
 | 
			
		||||
        }
 | 
			
		||||
        this.tags[k] = v;
 | 
			
		||||
        if (v === undefined || v === "") {
 | 
			
		||||
| 
						 | 
				
			
			@ -119,6 +208,25 @@ export abstract class OsmObject {
 | 
			
		|||
        }
 | 
			
		||||
        return 'version="' + this.version + '"';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private LoadData(element: any): void {
 | 
			
		||||
        this.tags = element.tags ?? this.tags;
 | 
			
		||||
        this.version = element.version;
 | 
			
		||||
        this.timestamp = element.timestamp;
 | 
			
		||||
        const tgs = this.tags;
 | 
			
		||||
        if(element.tags === undefined){
 | 
			
		||||
            // Simple node which is part of a way - not important
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        tgs["_last_edit:contributor"] = element.user
 | 
			
		||||
        tgs["_last_edit:contributor:uid"] = element.uid
 | 
			
		||||
        tgs["_last_edit:changeset"] = element.changeset
 | 
			
		||||
        tgs["_last_edit:timestamp"] = element.timestamp
 | 
			
		||||
        tgs["_version_number"] = element.version
 | 
			
		||||
        tgs["id"] = this.type + "/" + this.id;
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -135,21 +243,36 @@ export class OsmNode extends OsmObject {
 | 
			
		|||
    ChangesetXML(changesetId: string): string {
 | 
			
		||||
        let tags = this.TagsXML();
 | 
			
		||||
 | 
			
		||||
        let change =
 | 
			
		||||
            '        <node id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + ' lat="' + this.lat + '" lon="' + this.lon + '">\n' +
 | 
			
		||||
        return '        <node id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + ' lat="' + this.lat + '" lon="' + this.lon + '">\n' +
 | 
			
		||||
            tags +
 | 
			
		||||
            '        </node>\n';
 | 
			
		||||
 | 
			
		||||
        return change;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    SaveExtraData(element) {
 | 
			
		||||
        this.lat = element.lat;
 | 
			
		||||
        this.lon = element.lon;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    centerpoint(): [number, number] {
 | 
			
		||||
        return [this.lat, this.lon];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    asGeoJson() {
 | 
			
		||||
        return {
 | 
			
		||||
            "type": "Feature",
 | 
			
		||||
            "properties": this.tags,
 | 
			
		||||
            "geometry": {
 | 
			
		||||
                "type": "Point",
 | 
			
		||||
                "coordinates": [
 | 
			
		||||
                    this.lon,
 | 
			
		||||
                    this.lat
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface OsmObjectMeta{
 | 
			
		||||
export interface OsmObjectMeta {
 | 
			
		||||
    "_last_edit:contributor": string,
 | 
			
		||||
    "_last_edit:contributor:uid": number,
 | 
			
		||||
    "_last_edit:changeset": number,
 | 
			
		||||
| 
						 | 
				
			
			@ -161,12 +284,19 @@ export interface OsmObjectMeta{
 | 
			
		|||
export class OsmWay extends OsmObject {
 | 
			
		||||
 | 
			
		||||
    nodes: number[];
 | 
			
		||||
    coordinates: [number, number][] = []
 | 
			
		||||
    lat: number;
 | 
			
		||||
    lon: number;
 | 
			
		||||
 | 
			
		||||
    constructor(id) {
 | 
			
		||||
        super("way", id);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    centerpoint(): [number, number] {
 | 
			
		||||
        return [this.lat, this.lon];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ChangesetXML(changesetId: string): string {
 | 
			
		||||
        let tags = this.TagsXML();
 | 
			
		||||
        let nds = "";
 | 
			
		||||
| 
						 | 
				
			
			@ -174,18 +304,39 @@ export class OsmWay extends OsmObject {
 | 
			
		|||
            nds += '      <nd ref="' + this.nodes[node] + '"/>\n';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let change =
 | 
			
		||||
            '    <way id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' +
 | 
			
		||||
        return '    <way id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' +
 | 
			
		||||
            nds +
 | 
			
		||||
            tags +
 | 
			
		||||
            '        </way>\n';
 | 
			
		||||
 | 
			
		||||
        return change;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    SaveExtraData(element) {
 | 
			
		||||
    SaveExtraData(element, allNodes: OsmNode[]) {
 | 
			
		||||
 | 
			
		||||
        let latSum = 0
 | 
			
		||||
        let lonSum = 0
 | 
			
		||||
 | 
			
		||||
        for (const node of allNodes) {
 | 
			
		||||
            const cp = node.centerpoint();
 | 
			
		||||
            this.coordinates.push(cp);
 | 
			
		||||
            latSum = cp[0]
 | 
			
		||||
            lonSum = cp[1]
 | 
			
		||||
        }
 | 
			
		||||
        let count = this.coordinates.length;
 | 
			
		||||
        this.lat = latSum / count;
 | 
			
		||||
        this.lon = lonSum / count;
 | 
			
		||||
        this.nodes = element.nodes;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    asGeoJson() {
 | 
			
		||||
        return {
 | 
			
		||||
            "type": "Feature",
 | 
			
		||||
            "properties": this.tags,
 | 
			
		||||
            "geometry": {
 | 
			
		||||
                "type": "LineString",
 | 
			
		||||
                "coordinates": this.coordinates.map(c => [c[1], c[0]])
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class OsmRelation extends OsmObject {
 | 
			
		||||
| 
						 | 
				
			
			@ -197,24 +348,29 @@ export class OsmRelation extends OsmObject {
 | 
			
		|||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    centerpoint(): [number, number] {
 | 
			
		||||
        return [0, 0]; // TODO
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ChangesetXML(changesetId: string): string {
 | 
			
		||||
        let members = "";
 | 
			
		||||
        for (const memberI in this.members) {
 | 
			
		||||
            const member = this.members[memberI];
 | 
			
		||||
        for (const member of this.members) {
 | 
			
		||||
            members += '      <member type="' + member.type + '" ref="' + member.ref + '" role="' + member.role + '"/>\n';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let tags = this.TagsXML();
 | 
			
		||||
        let change =
 | 
			
		||||
            '    <relation id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' +
 | 
			
		||||
        return '    <relation id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' +
 | 
			
		||||
            members +
 | 
			
		||||
            tags +
 | 
			
		||||
            '        </relation>\n';
 | 
			
		||||
        return change;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    SaveExtraData(element) {
 | 
			
		||||
        this.members = element.members;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    asGeoJson() {
 | 
			
		||||
        throw "Not Implemented"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -7,7 +7,6 @@ import {Utils} from "../Utils";
 | 
			
		|||
import opening_hours from "opening_hours";
 | 
			
		||||
import {UIElement} from "../UI/UIElement";
 | 
			
		||||
import Combine from "../UI/Base/Combine";
 | 
			
		||||
import UpdateTagsFromOsmAPI from "./Actors/UpdateTagsFromOsmAPI";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const cardinalDirections = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ import { Utils } from "../Utils";
 | 
			
		|||
 | 
			
		||||
export default class Constants {
 | 
			
		||||
    
 | 
			
		||||
    public static vNumber = "0.7.0c";
 | 
			
		||||
    public static vNumber = "0.7.1b";
 | 
			
		||||
 | 
			
		||||
    // The user journey states thresholds when a new feature gets unlocked
 | 
			
		||||
    public static userJourney = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										7
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										7
									
								
								State.ts
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -18,7 +18,7 @@ import LayerConfig from "./Customizations/JSON/LayerConfig";
 | 
			
		|||
import TitleHandler from "./Logic/Actors/TitleHandler";
 | 
			
		||||
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
 | 
			
		||||
import {Relation} from "./Logic/Osm/ExtractRelations";
 | 
			
		||||
import UpdateTagsFromOsmAPI from "./Logic/Actors/UpdateTagsFromOsmAPI";
 | 
			
		||||
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Contains the global state: a bunch of UI-event sources
 | 
			
		||||
| 
						 | 
				
			
			@ -59,6 +59,8 @@ export default class State {
 | 
			
		|||
 | 
			
		||||
    public layerUpdater: UpdateFromOverpass;
 | 
			
		||||
    
 | 
			
		||||
    public osmApiFeatureSource : OsmApiFeatureSource ;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public filteredLayers: UIEventSource<{
 | 
			
		||||
        readonly isDisplayed: UIEventSource<boolean>,
 | 
			
		||||
| 
						 | 
				
			
			@ -218,6 +220,7 @@ export default class State {
 | 
			
		|||
 | 
			
		||||
        this.allElements = new ElementStorage();
 | 
			
		||||
        this.changes = new Changes();
 | 
			
		||||
        this.osmApiFeatureSource = new OsmApiFeatureSource(this.locationControl)
 | 
			
		||||
        
 | 
			
		||||
        new PendingChangesUploader(this.changes, this.selectedElement);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -253,8 +256,6 @@ export default class State {
 | 
			
		|||
 | 
			
		||||
        new TitleHandler(this.layoutToUse, this.selectedElement, this.allElements);
 | 
			
		||||
 | 
			
		||||
        new UpdateTagsFromOsmAPI(this.selectedElement.map(el => el?.properties?.id), this.allElements)
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static asFloat(source: UIEventSource<string>): UIEventSource<number> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,3 @@
 | 
			
		|||
import {VerticalCombine} from "../Base/VerticalCombine";
 | 
			
		||||
import {UIElement} from "../UIElement";
 | 
			
		||||
import {VariableUiElement} from "../Base/VariableUIElement";
 | 
			
		||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
 | 
			
		||||
| 
						 | 
				
			
			@ -53,15 +52,17 @@ export default class MoreScreen extends UIElement {
 | 
			
		|||
        if(path === ""){
 | 
			
		||||
            path = "."
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        const params = `z=${currentLocation.zoom ?? 1}&lat=${currentLocation.lat ?? 0}&lon=${currentLocation.lon ?? 0}`
 | 
			
		||||
        let linkText =
 | 
			
		||||
            `${path}/${layout.id.toLowerCase()}?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
 | 
			
		||||
            `${path}/${layout.id.toLowerCase()}?${params}`
 | 
			
		||||
 | 
			
		||||
        if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
 | 
			
		||||
            linkText = `${path}/index.html?layout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
 | 
			
		||||
            linkText = `${path}/index.html?layout=${layout.id}&${params}`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (customThemeDefinition) {
 | 
			
		||||
            linkText = `${path}/?userlayout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}#${customThemeDefinition}`
 | 
			
		||||
            linkText = `${path}/?userlayout=${layout.id}&${params}#${customThemeDefinition}`
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -164,7 +164,6 @@ export default class ShowDataLayer {
 | 
			
		|||
        });
 | 
			
		||||
        
 | 
			
		||||
        this._popups.set(feature, leafletLayer);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private CreateGeojsonLayer(): L.Layer {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										26
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										26
									
								
								Utils.ts
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -189,7 +189,7 @@ export class Utils {
 | 
			
		|||
     * @param z
 | 
			
		||||
     * @param x
 | 
			
		||||
     * @param y
 | 
			
		||||
     * @returns [[lat, lon], [lat, lon]]
 | 
			
		||||
     * @returns [[maxlat, minlon], [minlat, maxlon]]
 | 
			
		||||
     */
 | 
			
		||||
    static tile_bounds(z: number, x: number, y: number): [[number, number], [number, number]] {
 | 
			
		||||
        return [[Utils.tile2lat(y, z), Utils.tile2long(x, z)], [Utils.tile2lat(y + 1, z), Utils.tile2long(x + 1, z)]]
 | 
			
		||||
| 
						 | 
				
			
			@ -202,7 +202,7 @@ export class Utils {
 | 
			
		|||
        return {x: Utils.lon2tile(lon, z), y: Utils.lat2tile(lat, z), z: z}
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1:number, lon1: number) : TileRange{
 | 
			
		||||
    static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1: number, lon1: number): TileRange {
 | 
			
		||||
        const t0 = Utils.embedded_tile(lat0, lon0, zoomlevel)
 | 
			
		||||
        const t1 = Utils.embedded_tile(lat1, lon1, zoomlevel)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -274,8 +274,27 @@ export class Utils {
 | 
			
		|||
    private static lat2tile(lat, zoom) {
 | 
			
		||||
        return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom)));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    public static MapRange<T> (tileRange: TileRange, f: (x: number, y: number) => T): T[] {
 | 
			
		||||
        const result : T[] = []
 | 
			
		||||
        for (let x = tileRange.xstart; x <= tileRange.xend; x++) {
 | 
			
		||||
            for (let y = tileRange.ystart; y <= tileRange.yend; y++) {
 | 
			
		||||
              const t=  f(x, y);
 | 
			
		||||
              result.push(t)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static downloadTxtFile (contents: string, fileName: string = "download.txt") {
 | 
			
		||||
        const element = document.createElement("a");
 | 
			
		||||
        const file = new Blob([contents], {type: 'text/plain'});
 | 
			
		||||
        element.href = URL.createObjectURL(file);
 | 
			
		||||
        element.download = fileName;
 | 
			
		||||
        document.body.appendChild(element); // Required for this to work in FireFox
 | 
			
		||||
        element.click();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TileRange{
 | 
			
		||||
    xstart: number,
 | 
			
		||||
| 
						 | 
				
			
			@ -284,4 +303,5 @@ export interface TileRange{
 | 
			
		|||
    yend: number,
 | 
			
		||||
    total: number,
 | 
			
		||||
    zoomlevel: number
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue