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,51 +392,35 @@ export class InitUiElements { | ||||||
|             return flayers; |             return flayers; | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         const updater = new LoadFromOverpass( |         State.state.featurePipeline = new FeaturePipeline( | ||||||
|             state.locationControl, |             source => { | ||||||
|             state.layoutToUse, |                 new ShowDataLayer( | ||||||
|             state.leafletMap, |                     { | ||||||
|             state.overpassUrl, |                         features: source, | ||||||
|             state.overpassTimeout, |                         leafletMap: State.state.leafletMap, | ||||||
|             Constants.useOsmApiAt |                         layerToShow: source.layer.layerDef | ||||||
|         ); |                     } | ||||||
|         State.state.layerUpdater = updater; |                 ); | ||||||
| 
 |             }, state | ||||||
|         const source = new FeaturePipeline( |  | ||||||
|             state.filteredLayers, |  | ||||||
|             State.state.changes, |  | ||||||
|             updater, |  | ||||||
|             state.osmApiFeatureSource, |  | ||||||
|             state.layoutToUse, |  | ||||||
|             state.locationControl, |  | ||||||
|             state.selectedElement |  | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         State.state.featurePipeline = source; |         /*   const selectedFeatureHandler = new SelectedFeatureHandler( | ||||||
|         new ShowDataLayer( |                Hash.hash, | ||||||
|             source.features, |                State.state.selectedElement, | ||||||
|             State.state.leafletMap, |                source, | ||||||
|             State.state.layoutToUse |                State.state.osmApiFeatureSource | ||||||
|         ); |            ); | ||||||
| 
 |            selectedFeatureHandler.zoomToSelectedFeature( | ||||||
|         const selectedFeatureHandler = new SelectedFeatureHandler( |                State.state.locationControl | ||||||
|             Hash.hash, |            );*/ | ||||||
|             State.state.selectedElement, |  | ||||||
|             source, |  | ||||||
|             State.state.osmApiFeatureSource |  | ||||||
|         ); |  | ||||||
|         selectedFeatureHandler.zoomToSelectedFeature( |  | ||||||
|             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) | ||||||
| 
 | 
 | ||||||
|                 self.features.setData(features); |                 try{ | ||||||
|  |                     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 hist = new Map<string, number>(); |         const self = this; | ||||||
|             for (const feature of features) { |         state.currentBounds.map(bbox => { | ||||||
|                 const contributor = feature.feature.properties["_last_edit:contributor"] |             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>(); | ||||||
|  |         for (const list of featuresList) { | ||||||
|  |             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,15 +65,20 @@ 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; | ||||||
|                     } |                     } | ||||||
|                     result.push(...GeoOperations.calculateOverlap(feat, otherLayer)); |                     for (const otherLayer of otherLayers) { | ||||||
|  |                         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,121 +219,132 @@ 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)) | ||||||
|             if (features === undefined) { |             features = params.getFeaturesWithin(name, new BBox(bbox.geometry.coordinates)) | ||||||
|                 var keys = Utils.NoNull(Array.from(params.featuresPerLayer.keys())); |         }else{ | ||||||
|                 if (keys.length > 0) { |             features = [features] | ||||||
|                     throw `No features defined for ${name}. Defined layers are ${keys.join(", ")}`; |         } | ||||||
|                 } else { |         if (features === undefined) { | ||||||
|                     // This is the first pass over an external dataset
 |             return; | ||||||
|                     // 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) { | ||||||
|             if (otherFeature == feature || otherFeature.id == feature.id) { |             for (const otherFeature of featureList) { | ||||||
|                 continue; // We ignore self
 |                 if (otherFeature == feature || otherFeature.id == feature.id) { | ||||||
|             } |                     continue; // We ignore self
 | ||||||
|             let distance = undefined; |                 } | ||||||
|             if (otherFeature._lon !== undefined && otherFeature._lat !== undefined) { |                 let distance = undefined; | ||||||
|                 distance = GeoOperations.distanceBetween([otherFeature._lon, otherFeature._lat], [feature._lon, feature._lat]); |                 if (otherFeature._lon !== undefined && otherFeature._lat !== undefined) { | ||||||
|             } else { |                     distance = GeoOperations.distanceBetween([otherFeature._lon, otherFeature._lat], [feature._lon, feature._lat]); | ||||||
|                 distance = GeoOperations.distanceBetween( |                 } else { | ||||||
|                     GeoOperations.centerpointCoordinates(otherFeature), |                     distance = GeoOperations.distanceBetween( | ||||||
|                     [feature._lon, feature._lat] |                         GeoOperations.centerpointCoordinates(otherFeature), | ||||||
|                 ) |                         [feature._lon, feature._lat] | ||||||
|             } |                     ) | ||||||
|             if (distance === undefined) { |                 } | ||||||
|                 throw "Undefined distance!" |                 if (distance === undefined) { | ||||||
|             } |                     throw "Undefined distance!" | ||||||
|  |                 } | ||||||
|  |                 if (distance > maxDistance) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|             if (closestFeatures.length === 0) { |                 if (closestFeatures.length === 0) { | ||||||
|                 closestFeatures.push({ |                     closestFeatures.push({ | ||||||
|                     feat: otherFeature, |                         feat: otherFeature, | ||||||
|                     distance: distance |                         distance: distance | ||||||
|                 }) |                     }) | ||||||
|                 continue; |                     continue; | ||||||
|             } |                 } | ||||||
| 
 | 
 | ||||||
|             if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { |                 if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { | ||||||
|                 // The last feature of the list (and thus the furthest away is still closer
 |                     // The last feature of the list (and thus the furthest away is still closer
 | ||||||
|                 // No use for checking, as we already have plenty of features!
 |                     // No use for checking, as we already have plenty of features!
 | ||||||
|                 continue |                     continue | ||||||
|             } |                 } | ||||||
| 
 | 
 | ||||||
|             let targetIndex = closestFeatures.length |                 let targetIndex = closestFeatures.length | ||||||
|             for (let i = 0; i < closestFeatures.length; i++) { |                 for (let i = 0; i < closestFeatures.length; i++) { | ||||||
|                 const closestFeature = closestFeatures[i]; |                     const closestFeature = closestFeatures[i]; | ||||||
| 
 | 
 | ||||||
|                 if (uniqueTag !== undefined) { |                     if (uniqueTag !== undefined) { | ||||||
|                     const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined && |                         const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined && | ||||||
|                         closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag] |                             closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag] | ||||||
|                     if (uniqueTagsMatch) { |                         if (uniqueTagsMatch) { | ||||||
|                         targetIndex = -1 |                             targetIndex = -1 | ||||||
|                         if (closestFeature.distance > distance) { |                             if (closestFeature.distance > distance) { | ||||||
|                             // This is a very special situation:
 |                                 // This is a very special situation:
 | ||||||
|                             // We want to see the tag `uniquetag=some_value` only once in the entire list (e.g. to prevent road segements of identical names to fill up the list of 'names of nearby roads')
 |                                 // We want to see the tag `uniquetag=some_value` only once in the entire list (e.g. to prevent road segements of identical names to fill up the list of 'names of nearby roads')
 | ||||||
|                             // AT this point, we have found a closer segment with the same, identical tag
 |                                 // AT this point, we have found a closer segment with the same, identical tag
 | ||||||
|                             // so we replace directly
 |                                 // so we replace directly
 | ||||||
|                             closestFeatures[i] = {feat: otherFeature, distance: distance} |                                 closestFeatures[i] = {feat: otherFeature, distance: distance} | ||||||
|  |                             } | ||||||
|  |                             break; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     if (closestFeature.distance > distance) { | ||||||
|  |                         targetIndex = i | ||||||
|  | 
 | ||||||
|  |                         if (uniqueTag !== undefined) { | ||||||
|  |                             const uniqueValue = otherFeature.properties[uniqueTag] | ||||||
|  |                             // 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++) { | ||||||
|  |                                 if (closestFeatures[j].feat.properties[uniqueTag] === uniqueValue) { | ||||||
|  |                                     closestFeatures.splice(j, 1) | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|                         } |                         } | ||||||
|                         break; |                         break; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (closestFeature.distance > distance) { |                 if (targetIndex == -1) { | ||||||
|                     targetIndex = i |                     continue; // value is already swapped by the unique tag
 | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                     if (uniqueTag !== undefined) { |                 if (targetIndex < maxFeatures) { | ||||||
|                         const uniqueValue = otherFeature.properties[uniqueTag] |                     // insert and drop one
 | ||||||
|                         // We might still have some other values later one with the same uniquetag that have to be cleaned
 |                     closestFeatures.splice(targetIndex, 0, { | ||||||
|                         for (let j = i; j < closestFeatures.length; j++) { |                         feat: otherFeature, | ||||||
|                                 if(closestFeatures[j].feat.properties[uniqueTag] === uniqueValue){ |                         distance: distance | ||||||
|                                     closestFeatures.splice(j, 1) |                     }) | ||||||
|                                 } |                     if (closestFeatures.length >= maxFeatures) { | ||||||
|                         } |                         closestFeatures.splice(maxFeatures, 1) | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     // Overwrite the last element
 | ||||||
|  |                     closestFeatures[targetIndex] = { | ||||||
|  |                         feat: otherFeature, | ||||||
|  |                         distance: distance | ||||||
|                     } |                     } | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             if (targetIndex == -1) { |  | ||||||
|                 continue; // value is already swapped by the unique tag
 |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (targetIndex < maxFeatures) { |  | ||||||
|                 // insert and drop one
 |  | ||||||
|                 closestFeatures.splice(targetIndex, 0, { |  | ||||||
|                     feat: otherFeature, |  | ||||||
|                     distance: distance |  | ||||||
|                 }) |  | ||||||
|                 if (closestFeatures.length >= maxFeatures) { |  | ||||||
|                     closestFeatures.splice(maxFeatures, 1) |  | ||||||
|                 } |                 } | ||||||
|             } else { |  | ||||||
|                 // Overwrite the last element
 |  | ||||||
|                 closestFeatures[targetIndex] = { |  | ||||||
|                     feat: otherFeature, |  | ||||||
|                     distance: distance |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         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, | ||||||
|                 locationControl: UIEventSource<Loc>, |         state: { | ||||||
|                 selectedElement: UIEventSource<any>) { |             osmApiFeatureSource: FeatureSource, | ||||||
|  |             filteredLayers: UIEventSource<FilteredLayer[]>, | ||||||
|  |             locationControl: UIEventSource<Loc>, | ||||||
|  |             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, | ||||||
|                             // We lump in the new points here
 |                     (src) => { | ||||||
|                             generateNewGeometries: true |                         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 | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|         const merged = |         } | ||||||
|             new FeatureSourceMerger([ |  | ||||||
|                 amendedOverpassSource, |  | ||||||
|                 amendedOsmApiSource, |  | ||||||
|                 amendedLocalStorageSource, |  | ||||||
|                 ...geojsonSources |  | ||||||
|             ]); |  | ||||||
| 
 | 
 | ||||||
|         merged.features.syncWith(allLoadedFeatures) |         // 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() | ||||||
|  |         }) | ||||||
| 
 | 
 | ||||||
|         this.features = new WayHandlingApplyingFeatureSource(flayers, |  | ||||||
|             new FilteringFeatureSource( |  | ||||||
|                 flayers, |  | ||||||
|                 locationControl, |  | ||||||
|                 selectedElement, |  | ||||||
|                 merged |  | ||||||
|             )).features; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private updateAllMetaTagging() { | ||||||
|  |         console.log("Updating the meta tagging") | ||||||
|  |         const self = this; | ||||||
|  |         this.perLayerHierarchy.forEach(hierarchy => { | ||||||
|  |             hierarchy.loadedTiles.forEach(src => { | ||||||
|  |                 MetaTagging.addMetatags( | ||||||
|  |                     src.features.data, | ||||||
|  |                     { | ||||||
|  |                         memberships: this.relationTracker, | ||||||
|  |                         getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox) | ||||||
|  |                     }, | ||||||
|  |                     src.layer.layerDef | ||||||
|  |                 ) | ||||||
|  |             }) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     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) |  * A feature source which only contains features for the defined layer | ||||||
|      * @param featurePipeline The FeaturePipeline you want to export |  */ | ||||||
|      * @param options The options object | export interface FeatureSourceForLayer extends FeatureSource{ | ||||||
|      * @param options.metadata True if you want to include the MapComplete metadata, false otherwise |     readonly layer: FilteredLayer | ||||||
|      */ | } | ||||||
|     public static extractGeoJson(featurePipeline: FeatureSource, options: { metadata?: boolean } = {}) { |  | ||||||
|         let defaults = { |  | ||||||
|             metadata: false, |  | ||||||
|         } |  | ||||||
|         options = Utils.setDefaults(options, defaults); |  | ||||||
| 
 | 
 | ||||||
|         // Select all features, ignore the freshness and other data
 | /** | ||||||
|         let featureList: any[] = featurePipeline.features.data.map((feature) => |  * A feature source which is aware of the indexes it contains | ||||||
|             JSON.parse(JSON.stringify((feature.feature)))); // Make a deep copy!
 |  */ | ||||||
|  | export interface IndexedFeatureSource extends FeatureSource { | ||||||
|  |     readonly containedIds: UIEventSource<Set<string>> | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|         if (!options.metadata) { | /** | ||||||
|             for (let i = 0; i < featureList.length; i++) { |  * A feature source which has some extra data about it's state | ||||||
|                 let feature = featureList[i]; |  */ | ||||||
|                 for (let property in feature.properties) { | export interface FeatureSourceState { | ||||||
|                     if (property[0] == "_" && property !== "_lat" && property !== "_lon") { |     readonly sufficientlyZoomed: UIEventSource<boolean>; | ||||||
|                         delete featureList[i]["properties"][property]; |     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,12 +8,19 @@ 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(); | ||||||
|         this.features = new UIEventSource(features.map(f => ({ |         if(useFeaturesDirectly){ | ||||||
|             feature: f, |             // @ts-ignore
 | ||||||
|             freshness: now |             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 => ({ | ||||||
|  |                 feature: f, | ||||||
|  |                 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,28 +49,30 @@ 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 | ||||||
|             } |             } | ||||||
|             , [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) { |  | ||||||
|             throw "Invalid layer: geojsonZoomLevel expected" |  | ||||||
|         } |  | ||||||
|         if (source.geojsonSource === undefined) { |  | ||||||
|             throw "Invalid layer: geojsonSource expected" |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         super( |         const prefix = LocalStorageSaverActor.storageKey + "-" + layer.layerDef.id + "-" | ||||||
|             layer, |         // @ts-ignore
 | ||||||
|             source.geojsonZoomLevel, |         const indexes: number[] = Object.keys(localStorage) | ||||||
|             (xy) => { |             .filter(key => { | ||||||
|                 const xyz: [number, number, number] = [xy[0], xy[1], source.geojsonZoomLevel] |                 return key.startsWith(prefix) && !key.endsWith("-time"); | ||||||
|                 const src = new GeoJsonSource( |             }) | ||||||
|                     layer, |             .map(key => { | ||||||
|                     xyz |                 return Number(key.substring(prefix.length)); | ||||||
|                 ) |             }) | ||||||
|                 registerLayer(src) | 
 | ||||||
|                 return src |         console.log("Avaible datasets in local storage:", indexes) | ||||||
|             }, | 
 | ||||||
|             state |         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; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (location.zoom < layer.layerDef.minzoom) { | ||||||
|  |                     // No need to download! - the layer is disabled
 | ||||||
|  |                     return undefined; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Yup, this is cheating to just get the bounds here
 | ||||||
|  |                 const bounds = state.leafletMap.data?.getBounds() | ||||||
|  |                 if (bounds === undefined) { | ||||||
|  |                     // We'll retry later
 | ||||||
|  |                     return undefined | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 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 { | ||||||
| 
 | 
 | ||||||
|  | @ -184,6 +185,44 @@ export class GeoOperations { | ||||||
|     static lengthInMeters(feature: any) { |     static lengthInMeters(feature: any) { | ||||||
|         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 | ||||||
|  | @ -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,66 +37,39 @@ 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]; | ||||||
|             if (code === undefined) { |             if (code === undefined) { | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
|              | 
 | ||||||
|             const func = new Function("feat", "return " + code + ";"); |             const func = new Function("feat", "return " + code + ";"); | ||||||
| 
 | 
 | ||||||
|             try { |             try { | ||||||
|  | @ -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,10 +59,9 @@ 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 } |      | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * |      * | ||||||
|      * A list of extra tags to calculate, specified as "keyToAssignTo=javascript-expression". |      * A list of extra tags to calculate, specified as "keyToAssignTo=javascript-expression". | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |     background?: UIEventSource<BaseLayer>, | ||||||
|  |     location?: UIEventSource<Loc>, | ||||||
|  |     bounds?: UIEventSource<BBox>, | ||||||
|  |     allowMoving?: boolean, | ||||||
|  |     leafletOptions?: any, | ||||||
|  |     attribution?: BaseUIElement | boolean, | ||||||
|  |     onFullyLoaded?: (leaflet: L.Map) => void, | ||||||
|  |     leafletMap?: UIEventSource<any>, | ||||||
|  |     lastClickLocation?: UIEventSource<{ lat: number, lon: number }> | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     private static _nextId = 0; | export default class Minimap { | ||||||
|     public readonly leafletMap: UIEventSource<Map> |     /** | ||||||
|     private readonly _id: string; |      * A stub implementation. The actual implementation is injected later on, but only in the browser. | ||||||
|     private readonly _background: UIEventSource<BaseLayer>; |      * importing leaflet crashes node-ts, which is pretty annoying considering the fact that a lot of scripts use it | ||||||
|     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>, |      * Construct a minimap | ||||||
|                     location?: UIEventSource<Loc>, |      */ | ||||||
|                     allowMoving?: boolean, |     public static createMiniMap: (options: MinimapOptions) => BaseUIElement & { readonly leafletMap: UIEventSource<any> } | ||||||
|                     leafletOptions?: any, |  | ||||||
|                     attribution?: BaseUIElement | boolean, |  | ||||||
|                     onFullyLoaded?: (leaflet: L.Map) => void, |  | ||||||
|                     leafletMap?: UIEventSource<Map>, |  | ||||||
|                     lastClickLocation?: UIEventSource<{ lat: number, lon: number }> |  | ||||||
|                 } |  | ||||||
|     ) { |  | ||||||
|         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._id = "minimap" + Minimap._nextId; |  | ||||||
|         this._allowMoving = options.allowMoving ?? true; |  | ||||||
|         this._leafletoptions = options.leafletOptions ?? {} |  | ||||||
|         this._onFullyLoaded = options.onFullyLoaded |  | ||||||
|         this._attribution = options.attribution |  | ||||||
|         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,19 +5,19 @@ 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 | ||||||
|  */ |  */ | ||||||
| export default class Attribution extends Combine { | 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]) |                 new ShowDataLayer( | ||||||
| 
 |                     { | ||||||
|         // Add the features to the minimap
 |                         features: tile, | ||||||
|         new ShowDataLayer( |                         leafletMap: minimap.leafletMap, | ||||||
|             bounded, |                         layerToShow: tile.layer.layerDef, | ||||||
|             minimap.leafletMap, |                         enablePopups: false | ||||||
|             options.layout, |                     } | ||||||
|             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,16 +202,25 @@ 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 |  | ||||||
|                     ) |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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,152 +170,97 @@ 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) { |  | ||||||
| 
 |  | ||||||
|                 for (const layer of theme.layers) { |  | ||||||
|                     if (layer.source.osmTags.matchesProperties(feature.properties)) { |  | ||||||
|                         feature["_matching_layer_id"] = layer.id; |  | ||||||
|                         break; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             const featuresFreshness = geojson.features.map(feature => { |  | ||||||
|                 return ({ |  | ||||||
|                     freshness: osmTime, |  | ||||||
|                     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 |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (layer.wayHandling == LayerConfig.WAYHANDLING_CENTER_ONLY) { |  | ||||||
| 
 |  | ||||||
|                     const centerpoint = GeoOperations.centerpointCoordinates(feature) |  | ||||||
| 
 |  | ||||||
|                     feature.geometry.type = "Point" |  | ||||||
|                     feature.geometry["coordinates"] = centerpoint; |  | ||||||
| 
 |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             for (const feature of geojson.features) { |  | ||||||
|                 // Some cleanup
 |  | ||||||
|                 delete feature["bbox"] |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             const targetPath = geoJsonName(targetdir + ".unfiltered", x, y, r.zoomlevel) |  | ||||||
|             // This is the geojson file containing all features
 |  | ||||||
|             writeFileSync(targetPath, JSON.stringify(geojson, null, " ")) |  | ||||||
| 
 | 
 | ||||||
|  |             allFeatures.push(...geojson.features) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |     return new StaticFeatureSource(allFeatures) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) { | /** | ||||||
|     const z = r.zoomlevel; |  * Load all the tiles into memory from disk | ||||||
|     const generated = {} // layer --> x --> y[]
 |  */ | ||||||
|     for (let x = r.xstart; x <= r.xend; x++) { | function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsTracker: RelationsTracker, targetdir: string) { | ||||||
|         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"; |  | ||||||
| 
 | 
 | ||||||
|                     }) |     function handleLayer(source: FeatureSourceForLayer) { | ||||||
|                 const new_path = geoJsonName(targetdir + "_" + layer.id, x, y, z); |         const layer = source.layer.layerDef; | ||||||
|                 ScriptUtils.erasableLog(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length, ")") |         const layerId = layer.id | ||||||
|                 if (geojson.features.length == 0) { |         if (layer.source.isOsmCacheLayer !== true) { | ||||||
|                     continue; |             return; | ||||||
|  |         } | ||||||
|  |         console.log("Handling layer ", layerId, "which has", source.features.data.length, "features") | ||||||
|  |         if (source.features.data.length === 0) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         MetaTagging.addMetatags(source.features.data, | ||||||
|  |             { | ||||||
|  |                 memberships: relationsTracker, | ||||||
|  |                 getFeaturesWithin: _ => { | ||||||
|  |                     return [allFeatures.features.data.map(f => f.feature)] | ||||||
|                 } |                 } | ||||||
|                 writeFileSync(new_path, JSON.stringify(geojson, null, " ")) |             }, | ||||||
|  |             layer, | ||||||
|  |             false); | ||||||
| 
 | 
 | ||||||
|                 if (generated[layer.id] === undefined) { |         const createdTiles = [] | ||||||
|                     generated[layer.id] = {} |         // 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; | ||||||
|                 } |                 } | ||||||
|                 if (generated[layer.id][x] === undefined) { |                 if (tile.features.data.length === 0) { | ||||||
|                     generated[layer.id][x] = [] |                     return | ||||||
|                 } |                 } | ||||||
|                 generated[layer.id][x].push(y) |                 for (const feature of tile.features.data) { | ||||||
| 
 |                     // Some cleanup
 | ||||||
|  |                     delete feature.feature["bbox"] | ||||||
|  |                 } | ||||||
|  |                 // Lets save this tile!
 | ||||||
|  |                 const [z, x, y] = Utils.tile_from_index(tile.tileIndex) | ||||||
|  |                 console.log("Writing tile ", z, x, y, layerId) | ||||||
|  |                 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, " ")) | ||||||
|             } |             } | ||||||
|         } |         }) | ||||||
|     } | 
 | ||||||
| 
 |         // All the tiles are written at this point
 | ||||||
|     for (const layer of theme.layers) { |         // Only thing left to do is to create the index
 | ||||||
|         const id = layer.id |         const path = targetdir + "_" + layerId + "_overview.json" | ||||||
|         const loaded = generated[id] |         const perX = {} | ||||||
|         if (loaded === undefined) { |         createdTiles.map(i => Utils.tile_from_index(i)).forEach(([z, x, y]) => { | ||||||
|             console.log("No features loaded for layer ", id) |             const key = "" + x | ||||||
|             continue; |             if (perX[key] === undefined) { | ||||||
|         } |                 perX[key] = [] | ||||||
|         writeFileSync(targetdir + "_" + id + "_overview.json", JSON.stringify(loaded)) |             } | ||||||
|  |             perX[key].push(y) | ||||||
|  |         }) | ||||||
|  |         writeFileSync(path, JSON.stringify(perX)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     new PerLayerFeatureSourceSplitter( | ||||||
|  |         new UIEventSource<FilteredLayer[]>(theme.layers.map(l => ({ | ||||||
|  |             layerDef: l, | ||||||
|  |             isDisplayed: new UIEventSource<boolean>(true), | ||||||
|  |             appliedFilters: new UIEventSource(undefined) | ||||||
|  |         }))), | ||||||
|  |         handleLayer, | ||||||
|  |         allFeatures | ||||||
|  |     ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 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) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const featuresDedup = [] |  | ||||||
|     const seen = new Set<string>() |  | ||||||
|     for (const feature of allFeatures) { |  | ||||||
|         const id = feature.properties.id |  | ||||||
|         if (seen.has(id)) { |  | ||||||
|             continue |  | ||||||
|         } |  | ||||||
|         seen.add(id) |  | ||||||
|         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[]) { | ||||||
| 
 | 
 | ||||||
|  | @ -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