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 Toggle from "./UI/Input/Toggle"; | ||||
| import State from "./State"; | ||||
| import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource"; | ||||
| import {UIEventSource} from "./Logic/UIEventSource"; | ||||
| import {QueryParameters} from "./Logic/Web/QueryParameters"; | ||||
| import StrayClickHandler from "./Logic/Actors/StrayClickHandler"; | ||||
|  | @ -18,17 +17,15 @@ import * as L from "leaflet"; | |||
| import Img from "./UI/Base/Img"; | ||||
| import UserDetails from "./Logic/Osm/OsmConnection"; | ||||
| 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 ShowDataLayer from "./UI/ShowDataLayer"; | ||||
| import ShowDataLayer from "./UI/ShowDataLayer/ShowDataLayer"; | ||||
| import Hash from "./Logic/Web/Hash"; | ||||
| import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; | ||||
| import ScrollableFullScreen from "./UI/Base/ScrollableFullScreen"; | ||||
| import Translations from "./UI/i18n/Translations"; | ||||
| import MapControlButton from "./UI/MapControlButton"; | ||||
| import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler"; | ||||
| import LZString from "lz-string"; | ||||
| import FeatureSource from "./Logic/FeatureSource/FeatureSource"; | ||||
| import AllKnownLayers from "./Customizations/AllKnownLayers"; | ||||
| import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; | ||||
| import {TagsFilter} from "./Logic/Tags/TagsFilter"; | ||||
|  | @ -38,7 +35,6 @@ import {LayoutConfigJson} from "./Models/ThemeConfig/Json/LayoutConfigJson"; | |||
| import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; | ||||
| import LayerConfig from "./Models/ThemeConfig/LayerConfig"; | ||||
| import Minimap from "./UI/Base/Minimap"; | ||||
| import Constants from "./Models/Constants"; | ||||
| 
 | ||||
| export class InitUiElements { | ||||
|     static InitAll( | ||||
|  | @ -130,10 +126,9 @@ export class InitUiElements { | |||
|                 } | ||||
|             } | ||||
|             if (somethingChanged) { | ||||
|                 console.log("layoutToUse.layers:", layoutToUse.layers); | ||||
|                 State.state.layoutToUse.data.layers = Array.from(neededLayers); | ||||
|                 State.state.layoutToUse.ping(); | ||||
|                 State.state.layerUpdater?.ForceRefresh(); | ||||
|                 State.state.featurePipeline?.ForceRefresh(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -320,7 +315,7 @@ export class InitUiElements { | |||
|             (layer) => layer.id | ||||
|         ); | ||||
| 
 | ||||
|         new LayerResetter( | ||||
|         new BackgroundLayerResetter( | ||||
|             State.state.backgroundLayer, | ||||
|             State.state.locationControl, | ||||
|             State.state.availableBackgroundLayers, | ||||
|  | @ -333,13 +328,14 @@ export class InitUiElements { | |||
|             State.state.locationControl, | ||||
|             State.state.osmConnection.userDetails, | ||||
|             State.state.layoutToUse, | ||||
|             State.state.leafletMap | ||||
|             State.state.currentBounds | ||||
|         ); | ||||
| 
 | ||||
|         new Minimap({ | ||||
|         Minimap.createMiniMap({ | ||||
|             background: State.state.backgroundLayer, | ||||
|             location: State.state.locationControl, | ||||
|             leafletMap: State.state.leafletMap, | ||||
|             bounds: State.state.currentBounds, | ||||
|             attribution: attr, | ||||
|             lastClickLocation: State.state.LastClickLocation | ||||
|         }).SetClass("w-full h-full") | ||||
|  | @ -371,7 +367,7 @@ export class InitUiElements { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static InitLayers(): FeatureSource { | ||||
|     private static InitLayers(): void { | ||||
|         const state = State.state; | ||||
|         state.filteredLayers = state.layoutToUse.map((layoutToUse) => { | ||||
|             const flayers = []; | ||||
|  | @ -396,51 +392,35 @@ export class InitUiElements { | |||
|             return flayers; | ||||
|         }); | ||||
| 
 | ||||
|         const updater = new LoadFromOverpass( | ||||
|             state.locationControl, | ||||
|             state.layoutToUse, | ||||
|             state.leafletMap, | ||||
|             state.overpassUrl, | ||||
|             state.overpassTimeout, | ||||
|             Constants.useOsmApiAt | ||||
|         ); | ||||
|         State.state.layerUpdater = updater; | ||||
| 
 | ||||
|         const source = new FeaturePipeline( | ||||
|             state.filteredLayers, | ||||
|             State.state.changes, | ||||
|             updater, | ||||
|             state.osmApiFeatureSource, | ||||
|             state.layoutToUse, | ||||
|             state.locationControl, | ||||
|             state.selectedElement | ||||
|         State.state.featurePipeline = new FeaturePipeline( | ||||
|             source => { | ||||
|                 new ShowDataLayer( | ||||
|                     { | ||||
|                         features: source, | ||||
|                         leafletMap: State.state.leafletMap, | ||||
|                         layerToShow: source.layer.layerDef | ||||
|                     } | ||||
|                 ); | ||||
|             }, state | ||||
|         ); | ||||
| 
 | ||||
|         State.state.featurePipeline = source; | ||||
|         new ShowDataLayer( | ||||
|             source.features, | ||||
|             State.state.leafletMap, | ||||
|             State.state.layoutToUse | ||||
|         ); | ||||
| 
 | ||||
|         const selectedFeatureHandler = new SelectedFeatureHandler( | ||||
|             Hash.hash, | ||||
|             State.state.selectedElement, | ||||
|             source, | ||||
|             State.state.osmApiFeatureSource | ||||
|         ); | ||||
|         selectedFeatureHandler.zoomToSelectedFeature( | ||||
|             State.state.locationControl | ||||
|         ); | ||||
|         return source; | ||||
|         /*   const selectedFeatureHandler = new SelectedFeatureHandler( | ||||
|                Hash.hash, | ||||
|                State.state.selectedElement, | ||||
|                source, | ||||
|                State.state.osmApiFeatureSource | ||||
|            ); | ||||
|            selectedFeatureHandler.zoomToSelectedFeature( | ||||
|                State.state.locationControl | ||||
|            );*/ | ||||
|     } | ||||
| 
 | ||||
|     private static setupAllLayerElements() { | ||||
|         // ------------- 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"); | ||||
| 
 | ||||
|         // ------------------ 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 | ||||
|  */ | ||||
| export default class LayerResetter { | ||||
| export default class BackgroundLayerResetter { | ||||
| 
 | ||||
|     constructor(currentBackgroundLayer: UIEventSource<BaseLayer>, | ||||
|                 location: UIEventSource<Loc>, | ||||
|  |  | |||
|  | @ -3,14 +3,15 @@ import Loc from "../../Models/Loc"; | |||
| import {Or} from "../Tags/Or"; | ||||
| import {Overpass} from "../Osm/Overpass"; | ||||
| import Bounds from "../../Models/Bounds"; | ||||
| import FeatureSource from "../FeatureSource/FeatureSource"; | ||||
| import FeatureSource, {FeatureSourceState} from "../FeatureSource/FeatureSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {TagsFilter} from "../Tags/TagsFilter"; | ||||
| import SimpleMetaTagger from "../SimpleMetaTagger"; | ||||
| 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" | ||||
| 
 | ||||
|  | @ -24,6 +25,9 @@ export default class OverpassFeatureSource implements FeatureSource { | |||
|     public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||
|     public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0); | ||||
|      | ||||
|     public readonly relationsTracker: RelationsTracker; | ||||
|      | ||||
| 
 | ||||
|     private readonly retries: UIEventSource<number> = new UIEventSource<number>(0); | ||||
|     /** | ||||
|      * 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 | ||||
|      */ | ||||
|     private readonly _previousBounds: Map<number, Bounds[]> = new Map<number, Bounds[]>(); | ||||
|     private readonly _location: UIEventSource<Loc>; | ||||
|     private readonly _layoutToUse: UIEventSource<LayoutConfig>; | ||||
|     private readonly _leafletMap: UIEventSource<L.Map>; | ||||
|     private readonly _interpreterUrl: UIEventSource<string>; | ||||
|     private readonly _timeout: UIEventSource<number>; | ||||
|     private readonly state: { | ||||
|         readonly locationControl: UIEventSource<Loc>, | ||||
|         readonly layoutToUse: UIEventSource<LayoutConfig>, | ||||
|         readonly leafletMap: any, | ||||
|         readonly overpassUrl: UIEventSource<string>; | ||||
|         readonly overpassTimeout: UIEventSource<number>; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The most important layer should go first, as that one gets first pick for the questions | ||||
|      */ | ||||
|     constructor( | ||||
|         location: UIEventSource<Loc>, | ||||
|         layoutToUse: UIEventSource<LayoutConfig>, | ||||
|         leafletMap: UIEventSource<L.Map>, | ||||
|         interpreterUrl: UIEventSource<string>, | ||||
|         timeout: UIEventSource<number>, | ||||
|         maxZoom = undefined) { | ||||
|         this._location = location; | ||||
|         this._layoutToUse = layoutToUse; | ||||
|         this._leafletMap = leafletMap; | ||||
|         this._interpreterUrl = interpreterUrl; | ||||
|         this._timeout = timeout; | ||||
|         state: { | ||||
|             readonly locationControl: UIEventSource<Loc>, | ||||
|             readonly layoutToUse: UIEventSource<LayoutConfig>, | ||||
|             readonly leafletMap: any, | ||||
|             readonly overpassUrl: UIEventSource<string>; | ||||
|             readonly overpassTimeout: UIEventSource<number>; | ||||
|             readonly overpassMaxZoom: UIEventSource<number> | ||||
|         }) { | ||||
| 
 | ||||
| 
 | ||||
|         this.state = state | ||||
|         this.relationsTracker = new RelationsTracker() | ||||
|         const location = state.locationControl | ||||
|         const self = this; | ||||
| 
 | ||||
|         this.sufficientlyZoomed = location.map(location => { | ||||
|                 if (location?.zoom === undefined) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 let minzoom = Math.min(...layoutToUse.data.layers.map(layer => layer.minzoom ?? 18)); | ||||
|                 if(location.zoom < minzoom){ | ||||
|                 let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom ?? 18)); | ||||
|                 if (location.zoom < minzoom) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 if(maxZoom !== undefined && location.zoom > maxZoom){ | ||||
|                 const maxZoom = state.overpassMaxZoom.data | ||||
|                 if (maxZoom !== undefined && location.zoom > maxZoom) { | ||||
|                     return false; | ||||
|                 } | ||||
| 
 | ||||
|                 return true; | ||||
|             }, [layoutToUse] | ||||
|             }, [state.layoutToUse] | ||||
|         ); | ||||
|         for (let i = 0; i < 25; i++) { | ||||
|             // This update removes all data on all layers -> erase the map on lower levels too
 | ||||
|             this._previousBounds.set(i, []); | ||||
|         } | ||||
| 
 | ||||
|         layoutToUse.addCallback(() => { | ||||
|         state.layoutToUse.addCallback(() => { | ||||
|             self.update() | ||||
|         }); | ||||
|         location.addCallback(() => { | ||||
|             self.update() | ||||
|         }); | ||||
|         leafletMap.addCallbackAndRunD(_ => { | ||||
|         state.leafletMap.addCallbackAndRunD(_ => { | ||||
|             self.update(); | ||||
|         }) | ||||
|     } | ||||
|  | @ -97,11 +106,11 @@ export default class OverpassFeatureSource implements FeatureSource { | |||
|     private GetFilter(): Overpass { | ||||
|         let filters: TagsFilter[] = []; | ||||
|         let extraScripts: string[] = []; | ||||
|         for (const layer of this._layoutToUse.data.layers) { | ||||
|         for (const layer of this.state.layoutToUse.data.layers) { | ||||
|             if (typeof (layer) === "string") { | ||||
|                 throw "A layer was not expanded!" | ||||
|             } | ||||
|             if (this._location.data.zoom < layer.minzoom) { | ||||
|             if (this.state.locationControl.data.zoom < layer.minzoom) { | ||||
|                 continue; | ||||
|             } | ||||
|             if (layer.doNotDownload) { | ||||
|  | @ -141,7 +150,7 @@ export default class OverpassFeatureSource implements FeatureSource { | |||
|         if (filters.length + extraScripts.length === 0) { | ||||
|             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 { | ||||
|  | @ -155,21 +164,22 @@ export default class OverpassFeatureSource implements FeatureSource { | |||
|             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) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const n = Math.min(90, bounds.getNorth() ); | ||||
|         const e = Math.min(180, bounds.getEast() ); | ||||
|         const n = Math.min(90, bounds.getNorth()); | ||||
|         const e = Math.min(180, bounds.getEast()); | ||||
|         const s = Math.max(-90, bounds.getSouth()); | ||||
|         const w = Math.max(-180, bounds.getWest()); | ||||
|         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 overpass = this.GetFilter(); | ||||
|          | ||||
|         if (overpass === undefined) { | ||||
|             return; | ||||
|         } | ||||
|  | @ -181,14 +191,18 @@ export default class OverpassFeatureSource implements FeatureSource { | |||
|                 const features = data.features.map(f => ({feature: f, freshness: date})); | ||||
|                 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); | ||||
|             }, | ||||
|             function (reason) { | ||||
|                 self.retries.data++; | ||||
|                 self.ForceRefresh(); | ||||
|                 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.runningQuery.setData(false); | ||||
| 
 | ||||
|  | @ -222,7 +236,7 @@ export default class OverpassFeatureSource implements FeatureSource { | |||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const b = this._leafletMap.data.getBounds(); | ||||
|         const b = this.state.leafletMap.data.getBounds(); | ||||
|         return b.getSouth() >= bounds.south && | ||||
|             b.getNorth() <= bounds.north && | ||||
|             b.getEast() <= bounds.east && | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import FeatureSource from "../FeatureSource/FeatureSource"; | |||
| import {OsmObject} from "../Osm/OsmObject"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| 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. | ||||
|  |  | |||
|  | @ -1,21 +1,49 @@ | |||
| /// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions
 | ||||
| import FeatureSource from "./FeatureSource/FeatureSource"; | ||||
| 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 { | ||||
| 
 | ||||
|     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) { | ||||
|         this.Contributors = featureSource.features.map(features => { | ||||
|             const hist = new Map<string, number>(); | ||||
|             for (const feature of features) { | ||||
|                 const contributor = feature.feature.properties["_last_edit:contributor"] | ||||
|     constructor(state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> }) { | ||||
|         this.state = state; | ||||
|         const self = this; | ||||
|         state.currentBounds.map(bbox => { | ||||
|             self.update(bbox) | ||||
|         }) | ||||
|         state.featurePipeline.runningQuery.addCallbackAndRun( | ||||
|             _ =>   self.update(state.currentBounds.data) | ||||
|         ) | ||||
|        | ||||
|     } | ||||
| 
 | ||||
|     private lastUpdate: Date = undefined; | ||||
| 
 | ||||
|     private update(bbox: BBox) { | ||||
|         if(bbox === undefined){ | ||||
|             return; | ||||
|         } | ||||
|         const now = new Date(); | ||||
|         if (this.lastUpdate !== undefined && ((now.getTime() - this.lastUpdate.getTime()) < 1000 * 60)) { | ||||
|             return; | ||||
|         } | ||||
|         console.log("Calculating contributors") | ||||
|         const featuresList = this.state.featurePipeline.GetAllFeaturesWithin(bbox) | ||||
|         const hist = new Map<string, number>(); | ||||
|         for (const list of featuresList) { | ||||
|             for (const feature of list) { | ||||
|                 const contributor = feature.properties["_last_edit:contributor"] | ||||
|                 const count = hist.get(contributor) ?? 0; | ||||
|                 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 {Relation} from "./Osm/ExtractRelations"; | ||||
| import RelationsTracker from "./Osm/RelationsTracker"; | ||||
| import State from "../State"; | ||||
| import {Utils} from "../Utils"; | ||||
| import BaseUIElement from "../UI/BaseUIElement"; | ||||
| import List from "../UI/Base/List"; | ||||
| import Title from "../UI/Base/Title"; | ||||
| import {UIEventSourceTools} from "./UIEventSource"; | ||||
| 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 { | ||||
| 
 | ||||
| 
 | ||||
|  | @ -55,15 +65,20 @@ export class ExtraFunction { | |||
|         (params, feat) => { | ||||
|             return (...layerIds: string[]) => { | ||||
|                 const result = [] | ||||
| 
 | ||||
|                 const bbox = BBox.get(feat) | ||||
| 
 | ||||
|                 for (const layerId of layerIds) { | ||||
|                     const otherLayer = params.featuresPerLayer.get(layerId); | ||||
|                     if (otherLayer === undefined) { | ||||
|                     const otherLayers = params.getFeaturesWithin(layerId, bbox) | ||||
|                     if (otherLayers === undefined) { | ||||
|                         continue; | ||||
|                     } | ||||
|                     if (otherLayer.length === 0) { | ||||
|                     if (otherLayers.length === 0) { | ||||
|                         continue; | ||||
|                     } | ||||
|                     result.push(...GeoOperations.calculateOverlap(feat, otherLayer)); | ||||
|                     for (const otherLayer of otherLayers) { | ||||
|                         result.push(...GeoOperations.calculateOverlap(feat, otherLayer)); | ||||
|                     } | ||||
|                 } | ||||
|                 return result; | ||||
|             } | ||||
|  | @ -77,6 +92,9 @@ export class ExtraFunction { | |||
|         }, | ||||
|         (featuresPerLayer, feature) => { | ||||
|             return (arg0, lat) => { | ||||
|                 if (arg0 === undefined) { | ||||
|                     return undefined; | ||||
|                 } | ||||
|                 if (typeof arg0 === "number") { | ||||
|                     // Feature._lon and ._lat is conveniently place by one of the other metatags
 | ||||
|                     return GeoOperations.distanceBetween([arg0, lat], [feature._lon, feature._lat]); | ||||
|  | @ -103,7 +121,7 @@ export class ExtraFunction { | |||
|             args: ["list of features"] | ||||
|         }, | ||||
|         (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. " + | ||||
|                 "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)", | ||||
|             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) => { | ||||
|             return (features, amount, uniqueTag) => ExtraFunction.GetClosestNFeatures(params, feature, features, { | ||||
|             return (features, amount, uniqueTag, maxDistanceInMeters) => ExtraFunction.GetClosestNFeatures(params, feature, features, { | ||||
|                 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(';')`", | ||||
|             args: [] | ||||
|         }, | ||||
|         (params, _) => { | ||||
|             return () => params.relations ?? []; | ||||
|         (params, feat) => { | ||||
|             return () => | ||||
|                 params.memberships.knownRelations.data.get(feat.properties.id) ?? [] | ||||
| 
 | ||||
|         } | ||||
|     ) | ||||
|     private static readonly AspectedRouting = new ExtraFunction( | ||||
|  | @ -165,19 +186,19 @@ export class ExtraFunction { | |||
|     private readonly _name: string; | ||||
|     private readonly _args: 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[] }, | ||||
|                 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._doc = options.doc; | ||||
|         this._args = options.args; | ||||
|         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) { | ||||
|             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 uniqueTag : string | undefined = options?.uniqueTag | ||||
|         const maxDistance = options?.maxDistance ?? 500 | ||||
|         const uniqueTag: string | undefined = options?.uniqueTag | ||||
|         if (typeof features === "string") { | ||||
|             const name = features | ||||
|             features = params.featuresPerLayer.get(features) | ||||
|             if (features === undefined) { | ||||
|                 var keys = Utils.NoNull(Array.from(params.featuresPerLayer.keys())); | ||||
|                 if (keys.length > 0) { | ||||
|                     throw `No features defined for ${name}. Defined layers are ${keys.join(", ")}`; | ||||
|                 } else { | ||||
|                     // This is the first pass over an external dataset
 | ||||
|                     // Other data probably still has to load!
 | ||||
|                     return undefined; | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|             const bbox = GeoOperations.bbox(GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance)) | ||||
|             features = params.getFeaturesWithin(name, new BBox(bbox.geometry.coordinates)) | ||||
|         }else{ | ||||
|             features = [features] | ||||
|         } | ||||
|         if (features === undefined) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let closestFeatures: { feat: any, distance: number }[] = []; | ||||
|         for (const otherFeature of features) { | ||||
|             if (otherFeature == feature || otherFeature.id == feature.id) { | ||||
|                 continue; // We ignore self
 | ||||
|             } | ||||
|             let distance = undefined; | ||||
|             if (otherFeature._lon !== undefined && otherFeature._lat !== undefined) { | ||||
|                 distance = GeoOperations.distanceBetween([otherFeature._lon, otherFeature._lat], [feature._lon, feature._lat]); | ||||
|             } else { | ||||
|                 distance = GeoOperations.distanceBetween( | ||||
|                     GeoOperations.centerpointCoordinates(otherFeature), | ||||
|                     [feature._lon, feature._lat] | ||||
|                 ) | ||||
|             } | ||||
|             if (distance === undefined) { | ||||
|                 throw "Undefined distance!" | ||||
|             } | ||||
|         for(const featureList of features) { | ||||
|             for (const otherFeature of featureList) { | ||||
|                 if (otherFeature == feature || otherFeature.id == feature.id) { | ||||
|                     continue; // We ignore self
 | ||||
|                 } | ||||
|                 let distance = undefined; | ||||
|                 if (otherFeature._lon !== undefined && otherFeature._lat !== undefined) { | ||||
|                     distance = GeoOperations.distanceBetween([otherFeature._lon, otherFeature._lat], [feature._lon, feature._lat]); | ||||
|                 } else { | ||||
|                     distance = GeoOperations.distanceBetween( | ||||
|                         GeoOperations.centerpointCoordinates(otherFeature), | ||||
|                         [feature._lon, feature._lat] | ||||
|                     ) | ||||
|                 } | ||||
|                 if (distance === undefined) { | ||||
|                     throw "Undefined distance!" | ||||
|                 } | ||||
|                 if (distance > maxDistance) { | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|             if (closestFeatures.length === 0) { | ||||
|                 closestFeatures.push({ | ||||
|                     feat: otherFeature, | ||||
|                     distance: distance | ||||
|                 }) | ||||
|                 continue; | ||||
|             } | ||||
|                 if (closestFeatures.length === 0) { | ||||
|                     closestFeatures.push({ | ||||
|                         feat: otherFeature, | ||||
|                         distance: distance | ||||
|                     }) | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|             if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { | ||||
|                 // 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!
 | ||||
|                 continue | ||||
|             } | ||||
|                 if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { | ||||
|                     // 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!
 | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|             let targetIndex = closestFeatures.length | ||||
|             for (let i = 0; i < closestFeatures.length; i++) { | ||||
|                 const closestFeature = closestFeatures[i]; | ||||
|                 let targetIndex = closestFeatures.length | ||||
|                 for (let i = 0; i < closestFeatures.length; i++) { | ||||
|                     const closestFeature = closestFeatures[i]; | ||||
| 
 | ||||
|                 if (uniqueTag !== undefined) { | ||||
|                     const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined && | ||||
|                         closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag] | ||||
|                     if (uniqueTagsMatch) { | ||||
|                         targetIndex = -1 | ||||
|                         if (closestFeature.distance > distance) { | ||||
|                             // 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')
 | ||||
|                             // AT this point, we have found a closer segment with the same, identical tag
 | ||||
|                             // so we replace directly
 | ||||
|                             closestFeatures[i] = {feat: otherFeature, distance: distance} | ||||
|                     if (uniqueTag !== undefined) { | ||||
|                         const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined && | ||||
|                             closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag] | ||||
|                         if (uniqueTagsMatch) { | ||||
|                             targetIndex = -1 | ||||
|                             if (closestFeature.distance > distance) { | ||||
|                                 // 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')
 | ||||
|                                 // AT this point, we have found a closer segment with the same, identical tag
 | ||||
|                                 // so we replace directly
 | ||||
|                                 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; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 if (closestFeature.distance > distance) { | ||||
|                     targetIndex = i | ||||
|                 if (targetIndex == -1) { | ||||
|                     continue; // value is already swapped by the unique tag
 | ||||
|                 } | ||||
| 
 | ||||
|                     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) | ||||
|                                 } | ||||
|                         } | ||||
|                 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 | ||||
|                     } | ||||
|                     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; | ||||
|     } | ||||
| 
 | ||||
|     public PatchFeature(featuresPerLayer: Map<string, any[]>, relations: { role: string, relation: Relation }[], feature: any) { | ||||
|         feature[this._name] = this._f({featuresPerLayer: featuresPerLayer, relations: relations}, feature) | ||||
|     public PatchFeature(params: ExtraFuncParams, feature: any) { | ||||
|         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 | ||||
|  * | ||||
|  * 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 { | ||||
|     public static readonly storageKey: string = "cached-features"; | ||||
| 
 | ||||
|  | @ -21,7 +21,6 @@ export default class LocalStorageSaverActor { | |||
| 
 | ||||
|             try { | ||||
|                 localStorage.setItem(key, JSON.stringify(features)); | ||||
|                 console.log("Saved ", features.length, "elements to", key) | ||||
|                 localStorage.setItem(key + "-time", JSON.stringify(now)) | ||||
|             } catch (e) { | ||||
|                 console.warn("Could not save the features to local storage:", e) | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import FeatureSource from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import State from "../../State"; | ||||
| import FeatureSource from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import State from "../../../State"; | ||||
| 
 | ||||
| export default class RegisteringAllFromFeatureSourceActor { | ||||
|     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 {Changes} from "../Osm/Changes"; | ||||
| import {ChangeDescription} from "../Osm/Actions/ChangeDescription"; | ||||
| import {Utils} from "../../Utils"; | ||||
| 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 | ||||
|  | @ -12,10 +37,18 @@ import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject"; | |||
| export default class ChangeApplicator implements FeatureSource { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
|     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 | ||||
|     }) { | ||||
|         this.source = source; | ||||
|         this.changes = changes; | ||||
|         this.mode = mode; | ||||
| 
 | ||||
|         this.name = "ChangesApplied(" + source.name + ")" | ||||
|         this.features = source.features | ||||
|  | @ -26,7 +59,7 @@ export default class ChangeApplicator implements FeatureSource { | |||
|             if (runningUpdate) { | ||||
|                 return; // No need to ping again
 | ||||
|             } | ||||
|             ChangeApplicator.ApplyChanges(features, changes.pendingChanges.data, mode) | ||||
|             self.ApplyChanges() | ||||
|             seenChanges.clear() | ||||
|         }) | ||||
| 
 | ||||
|  | @ -34,19 +67,20 @@ export default class ChangeApplicator implements FeatureSource { | |||
|             runningUpdate = true; | ||||
|             changes = changes.filter(ch => !seenChanges.has(ch)) | ||||
|             changes.forEach(c => seenChanges.add(c)) | ||||
|             ChangeApplicator.ApplyChanges(self.features.data, changes, mode) | ||||
|             self.ApplyChanges() | ||||
|             source.features.ping() | ||||
|             runningUpdate = false; | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * 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) { | ||||
|             return; | ||||
|         } | ||||
|  | @ -56,12 +90,18 @@ export default class ChangeApplicator implements FeatureSource { | |||
|         const changesPerId: Map<string, ChangeDescription[]> = new Map<string, ChangeDescription[]>() | ||||
|         for (const c of cs) { | ||||
|             const id = c.type + "/" + c.id | ||||
|             if (!loadedIds.has(id)) { | ||||
|                 continue | ||||
|             } | ||||
|             if (!changesPerId.has(id)) { | ||||
|                 changesPerId.set(id, []) | ||||
|             } | ||||
|             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() | ||||
| 
 | ||||
|  | @ -77,7 +117,7 @@ export default class ChangeApplicator implements FeatureSource { | |||
| 
 | ||||
|         // First, create the new features - they have a negative ID
 | ||||
|         // We don't set the properties yet though
 | ||||
|         if (mode?.generateNewGeometries) { | ||||
|         if (this.mode?.generateNewGeometries) { | ||||
|             changesPerId.forEach(cs => { | ||||
|                 cs | ||||
|                     .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 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[]>, | ||||
|                 changes: Changes, | ||||
|                 updater: FeatureSource, | ||||
|                 fromOsmApi: FeatureSource, | ||||
|                 layout: UIEventSource<LayoutConfig>, | ||||
|                 locationControl: UIEventSource<Loc>, | ||||
|                 selectedElement: UIEventSource<any>) { | ||||
|     private readonly overpassUpdater: OverpassFeatureSource | ||||
|     private readonly relationTracker: RelationsTracker | ||||
|     private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>; | ||||
|     constructor( | ||||
|         handleFeatureSource: (source: FeatureSourceForLayer) => void, | ||||
|         state: { | ||||
|             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
 | ||||
|         // Note that we need to register before we do metatagging (as it expects the event sources)
 | ||||
|         const perLayerHierarchy = new Map<string, TileHierarchyMerger>() | ||||
|         this.perLayerHierarchy = perLayerHierarchy | ||||
| 
 | ||||
|         // AT last, the metaTagging also needs to be run _after_ the duplicatorPerLayer
 | ||||
|         const amendedOverpassSource = | ||||
|             new RememberingSource( | ||||
|                 new LocalStorageSaver( | ||||
|                     new MetaTaggingFeatureSource(allLoadedFeatures, | ||||
|                         new FeatureDuplicatorPerLayer(flayers, | ||||
|                             new RegisteringFeatureSource( | ||||
|                                 new ChangeApplicator( | ||||
|                                     updater, changes | ||||
|                                 )) | ||||
|                         )), layout)); | ||||
|         const patchedHandleFeatureSource = function (src: FeatureSourceForLayer) { | ||||
|             // This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
 | ||||
|             const srcFiltered = | ||||
|                 new FilteringFeatureSource(state, | ||||
|                     new WayHandlingApplyingFeatureSource( | ||||
|                         src, | ||||
|                     ) | ||||
|                 ) | ||||
|             handleFeatureSource(srcFiltered) | ||||
|             self.somethingLoaded.setData(true) | ||||
|         }; | ||||
| 
 | ||||
|         const geojsonSources: FeatureSource [] = GeoJsonSource | ||||
|             .ConstructMultiSource(flayers.data, locationControl) | ||||
|             .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 | ||||
|             }); | ||||
|         function addToHierarchy(src: FeatureSource & Tiled, layerId: string) { | ||||
|             perLayerHierarchy.get(layerId).registerTile(src) | ||||
|         } | ||||
| 
 | ||||
|         const amendedLocalStorageSource = | ||||
|             new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new ChangeApplicator(new LocalStorageSource(layout), changes)) | ||||
|             )); | ||||
|         for (const filteredLayer of state.filteredLayers.data) { | ||||
|             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( | ||||
|             new MetaTaggingFeatureSource(allLoadedFeatures, | ||||
|                 new FeatureDuplicatorPerLayer(flayers, | ||||
|                     new RegisteringFeatureSource(new ChangeApplicator(fromOsmApi, changes, | ||||
|                         { | ||||
|                             // We lump in the new points here
 | ||||
|                             generateNewGeometries: true | ||||
|             if (source.geojsonSource === undefined) { | ||||
|                 // This is an OSM layer
 | ||||
|                 // We load the cached values and register them
 | ||||
|                 // Getting data from upstream happens a bit lower
 | ||||
|                 new TiledFromLocalStorageSource(filteredLayer, | ||||
|                     (src) => { | ||||
|                         new RegisteringAllFromFeatureSourceActor(src) | ||||
|                         hierarchy.registerTile(src); | ||||
|                     }, state) | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (source.geojsonZoomLevel === undefined) { | ||||
|                 // This is a 'load everything at once' geojson layer
 | ||||
|                 // We split them up into tiles
 | ||||
|                 const src = new GeoJsonSource(filteredLayer) | ||||
|                 TiledFeatureSource.createHierarchy(src, { | ||||
|                     layer: src.layer, | ||||
|                     registerTile: (tile) => { | ||||
|                         new RegisteringAllFromFeatureSourceActor(tile) | ||||
|                         addToHierarchy(tile, id) | ||||
|                     } | ||||
|                 }) | ||||
|             } else { | ||||
|                 new DynamicGeoJsonTileSource( | ||||
|                     filteredLayer, | ||||
|                     src => TiledFeatureSource.createHierarchy(src, { | ||||
|                         layer: src.layer, | ||||
|                         registerTile: (tile) => { | ||||
|                             new RegisteringAllFromFeatureSourceActor(tile) | ||||
|                             addToHierarchy(tile, id) | ||||
|                         } | ||||
|                     ))))); | ||||
|                     }), | ||||
|                     state | ||||
|                 ) | ||||
|             } | ||||
| 
 | ||||
|         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 {Utils} from "../../Utils"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import {BBox} from "../GeoOperations"; | ||||
| 
 | ||||
| export default interface FeatureSource { | ||||
|     features: UIEventSource<{ feature: any, freshness: Date }[]>; | ||||
|  | @ -9,38 +11,30 @@ export default interface FeatureSource { | |||
|     name: string; | ||||
| } | ||||
| 
 | ||||
| export class FeatureSourceUtils { | ||||
| 
 | ||||
|     /** | ||||
|      * Exports given featurePipeline as a geojson FeatureLists (downloads as a json) | ||||
|      * @param featurePipeline The FeaturePipeline you want to export | ||||
|      * @param options The options object | ||||
|      * @param options.metadata True if you want to include the MapComplete metadata, false otherwise | ||||
|      */ | ||||
|     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) => | ||||
|             JSON.parse(JSON.stringify((feature.feature)))); // Make a deep copy!
 | ||||
| 
 | ||||
|         if (!options.metadata) { | ||||
|             for (let i = 0; i < featureList.length; i++) { | ||||
|                 let feature = featureList[i]; | ||||
|                 for (let property in feature.properties) { | ||||
|                     if (property[0] == "_" && property !== "_lat" && property !== "_lon") { | ||||
|                         delete featureList[i]["properties"][property]; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return {type: "FeatureCollection", features: featureList} | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| export interface Tiled { | ||||
|     tileIndex: number, | ||||
|     bbox: BBox | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * A feature source which only contains features for the defined layer | ||||
|  */ | ||||
| export interface FeatureSourceForLayer extends FeatureSource{ | ||||
|     readonly layer: FilteredLayer | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * A feature source which is aware of the indexes it contains | ||||
|  */ | ||||
| export interface IndexedFeatureSource extends FeatureSource { | ||||
|     readonly containedIds: UIEventSource<Set<string>> | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * A feature source which has some extra data about it's state | ||||
|  */ | ||||
| export interface FeatureSourceState { | ||||
|     readonly sufficientlyZoomed: UIEventSource<boolean>; | ||||
|     readonly runningQuery: UIEventSource<boolean>; | ||||
|     readonly timeout: UIEventSource<number>; | ||||
| } | ||||
|  | @ -1,8 +1,7 @@ | |||
| import FeatureSource from "./FeatureSource"; | ||||
| import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import OverpassFeatureSource from "../Actors/OverpassFeatureSource"; | ||||
| import SimpleFeatureSource from "./SimpleFeatureSource"; | ||||
| import SimpleFeatureSource from "./Sources/SimpleFeatureSource"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  | @ -13,17 +12,17 @@ import SimpleFeatureSource from "./SimpleFeatureSource"; | |||
| export default class PerLayerFeatureSourceSplitter { | ||||
| 
 | ||||
|     constructor(layers: UIEventSource<FilteredLayer[]>, | ||||
|                 handleLayerData: (source: FeatureSource) => void, | ||||
|                 upstream: OverpassFeatureSource) { | ||||
|                 handleLayerData: (source: FeatureSourceForLayer) => void, | ||||
|                 upstream: FeatureSource) { | ||||
| 
 | ||||
|         const knownLayers = new Map<string, FeatureSource>() | ||||
|         const knownLayers = new Map<string, FeatureSourceForLayer>() | ||||
| 
 | ||||
|         function update() { | ||||
|             const features = upstream.features.data; | ||||
|             if (features === undefined) { | ||||
|                 return; | ||||
|             } | ||||
|             if(layers.data === undefined){ | ||||
|             if (layers.data === undefined) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|  | @ -69,19 +68,16 @@ export default class PerLayerFeatureSourceSplitter { | |||
|                 if (featureSource === undefined) { | ||||
|                     // Not yet initialized - now is a good time
 | ||||
|                     featureSource = new SimpleFeatureSource(layer) | ||||
|                     featureSource.features.setData(features) | ||||
|                     knownLayers.set(id, 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()) | ||||
|     } | ||||
| } | ||||
|  | @ -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 | ||||
|  * 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 readonly name; | ||||
|     public readonly layer: FilteredLayer | ||||
|     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.layer = layer; | ||||
|         this.name = "SourceMerger" | ||||
|         this.name = "FeatureSourceMerger("+layer.layerDef.id+", "+Utils.tile_from_index(tileIndex).join(",")+")" | ||||
|         const self = this; | ||||
| 
 | ||||
|         const handledSources = new Set<FeatureSource>(); | ||||
|  |  | |||
|  | @ -1,13 +1,13 @@ | |||
| import {FeatureSourceForLayer} from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Hash from "../Web/Hash"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer} from "../FeatureSource"; | ||||
| import Hash from "../../Web/Hash"; | ||||
| 
 | ||||
| export default class FilteringFeatureSource implements FeatureSourceForLayer { | ||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = | ||||
|         new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||
|     public readonly name = "FilteringFeatureSource"; | ||||
|     public readonly name; | ||||
|     public readonly layer: FilteredLayer; | ||||
| 
 | ||||
|     constructor( | ||||
|  | @ -18,6 +18,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer { | |||
|         upstream: FeatureSourceForLayer | ||||
|     ) { | ||||
|         const self = this; | ||||
|         this.name = "FilteringFeatureSource("+upstream.name+")" | ||||
| 
 | ||||
|         this.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 | ||||
|  */ | ||||
| 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 name; | ||||
|  | @ -17,6 +17,8 @@ export default class GeoJsonSource implements FeatureSourceForLayer { | |||
|     private readonly seenids: Set<string> = new Set<string>() | ||||
|     public readonly layer: FilteredLayer; | ||||
| 
 | ||||
|     public readonly tileIndex | ||||
|     public readonly bbox; | ||||
| 
 | ||||
|     public constructor(flayer: FilteredLayer, | ||||
|                        zxy?: [number, number, number]) { | ||||
|  | @ -28,10 +30,16 @@ export default class GeoJsonSource implements FeatureSourceForLayer { | |||
|         this.layer = flayer; | ||||
|         let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); | ||||
|         if (zxy !== undefined) { | ||||
|             const [z, x, y] = zxy; | ||||
|             url = url | ||||
|                 .replace('{z}', "" + zxy[0]) | ||||
|                 .replace('{x}', "" + zxy[1]) | ||||
|                 .replace('{y}', "" + zxy[2]) | ||||
|                 .replace('{z}', "" + z) | ||||
|                 .replace('{x}', "" + x) | ||||
|                 .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; | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import FeatureSource from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {OsmObject} from "../Osm/OsmObject"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import FeatureSource from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import Loc from "../../../Models/Loc"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {OsmObject} from "../../Osm/OsmObject"; | ||||
| 
 | ||||
| 
 | ||||
| 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. | ||||
|  * Data coming from upstream will always overwrite a previous value | ||||
|  */ | ||||
| import FeatureSource from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| 
 | ||||
| export default class RememberingSource implements FeatureSource { | ||||
| 
 | ||||
|     public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>; | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {FeatureSourceForLayer} from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer} from "../FeatureSource"; | ||||
| 
 | ||||
| export default class SimpleFeatureSource implements FeatureSourceForLayer { | ||||
|     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 name: string = "StaticFeatureSource" | ||||
| 
 | ||||
|     constructor(features: any[]) { | ||||
|     constructor(features: any[] | UIEventSource<any[]>, useFeaturesDirectly = false) { | ||||
|         const now = new Date(); | ||||
|         this.features = new UIEventSource(features.map(f => ({ | ||||
|             feature: f, | ||||
|             freshness: now | ||||
|         }))) | ||||
|         if(useFeaturesDirectly){ | ||||
|             // @ts-ignore
 | ||||
|             this.features = features | ||||
|         }else         if (features instanceof UIEventSource) { | ||||
|             this.features = features.map(features => features.map(f => ({feature: f, freshness: now}))) | ||||
|         } else { | ||||
|             this.features = new UIEventSource(features.map(f => ({ | ||||
|                 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) | ||||
|  */ | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||
| import {GeoOperations} from "../../GeoOperations"; | ||||
| import {FeatureSourceForLayer} from "../FeatureSource"; | ||||
| 
 | ||||
| export default class WayHandlingApplyingFeatureSource implements FeatureSourceForLayer { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly name; | ||||
|     public readonly layer; | ||||
| 
 | ||||
|     constructor(upstream: FeatureSourceForLayer) { | ||||
|         this.name = "Wayhandling of " + upstream.name; | ||||
|         this.name = "Wayhandling(" + upstream.name+")"; | ||||
|         this.layer = upstream.layer | ||||
|         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 FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer} from "../FeatureSource"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| 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>(); | ||||
| 
 | ||||
|     public readonly existingTiles: Map<number, Map<number, FeatureSourceForLayer>> = new Map<number, Map<number, FeatureSourceForLayer>>() | ||||
|     public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>; | ||||
| 
 | ||||
|     constructor( | ||||
|         layer: FilteredLayer, | ||||
|         zoomlevel: number, | ||||
|         constructTile: (xy: [number, number]) => FeatureSourceForLayer, | ||||
|         constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled), | ||||
|         state: { | ||||
|             locationControl: UIEventSource<Loc> | ||||
|             leafletMap: any | ||||
|  | @ -24,6 +26,8 @@ export default class DynamicTileSource { | |||
|     ) { | ||||
|         state = State.state | ||||
|         const self = this; | ||||
| 
 | ||||
|         this.loadedTiles = new Map<number,FeatureSourceForLayer & Tiled>() | ||||
|         const neededTiles = state.locationControl.map( | ||||
|             location => { | ||||
|                 if (!layer.isDisplayed.data) { | ||||
|  | @ -45,7 +49,7 @@ export default class DynamicTileSource { | |||
|                 const tileRange = Utils.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) | ||||
| 
 | ||||
|                 const 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 needed | ||||
|  | @ -53,20 +57,22 @@ export default class DynamicTileSource { | |||
|             , [layer.isDisplayed, state.leafletMap]).stabilized(250); | ||||
| 
 | ||||
|         neededTiles.addCallbackAndRunD(neededIndexes => { | ||||
|             console.log("Tiled geojson source ",layer.layerDef.id," needs", neededIndexes) | ||||
|             if (neededIndexes === undefined) { | ||||
|                 return; | ||||
|             } | ||||
|             for (const neededIndex of neededIndexes) { | ||||
|                 self._loadedTiles.add(neededIndex) | ||||
|                 const xy = Utils.tile_from_index(zoomlevel, neededIndex) | ||||
|                 const src = constructTile(xy) | ||||
|                 let xmap = self.existingTiles.get(xy[0]) | ||||
|                 if(xmap === undefined){ | ||||
|                    xmap =  new Map<number, FeatureSourceForLayer>() | ||||
|                    self.existingTiles.set(xy[0], xmap)  | ||||
|                 const src = constructTile( Utils.tile_from_index(neededIndex)) | ||||
|                 if(src !== undefined){ | ||||
|                     self.loadedTiles.set(neededIndex, src) | ||||
|                 } | ||||
|             xmap.set(xy[1], src) | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,3 +1,27 @@ | |||
| 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 FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import FeatureSourceMerger from "./Sources/FeatureSourceMerger"; | ||||
| import {BBox} from "../GeoOperations"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import TileHierarchy from "./TileHierarchy"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import FeatureSourceMerger from "../Sources/FeatureSourceMerger"; | ||||
| 
 | ||||
| export class TileHierarchyMerger implements TileHierarchy<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 index | ||||
|      */ | ||||
|     public registerTile(src: FeatureSource, index: number) { | ||||
|     public registerTile(src: FeatureSource  & Tiled) { | ||||
| 
 | ||||
|         const index = src.tileIndex | ||||
|         if (this.sources.has(index)) { | ||||
|             const sources = this.sources.get(index) | ||||
|             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 {FeatureSourceForLayer} from "../FeatureSource"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import Loc from "../../../Models/Loc"; | ||||
| import GeoJsonSource from "../GeoJsonSource"; | ||||
| import DynamicTileSource from "./DynamicTileSource"; | ||||
| import TileHierarchy from "./TileHierarchy"; | ||||
| 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, | ||||
|                 registerLayer: (layer: FeatureSourceForLayer) => void, | ||||
|                 handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => 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" | ||||
|         } | ||||
| 
 | ||||
|         super( | ||||
|             layer, | ||||
|             source.geojsonZoomLevel, | ||||
|             (xy) => { | ||||
|                 const xyz: [number, number, number] = [xy[0], xy[1], source.geojsonZoomLevel] | ||||
|                 const src = new GeoJsonSource( | ||||
|                     layer, | ||||
|                     xyz | ||||
|                 ) | ||||
|                 registerLayer(src) | ||||
|                 return src | ||||
|             }, | ||||
|             state | ||||
|         ); | ||||
|         const prefix = LocalStorageSaverActor.storageKey + "-" + layer.layerDef.id + "-" | ||||
|         // @ts-ignore
 | ||||
|         const indexes: number[] = Object.keys(localStorage) | ||||
|             .filter(key => { | ||||
|                 return key.startsWith(prefix) && !key.endsWith("-time"); | ||||
|             }) | ||||
|             .map(key => { | ||||
|                 return Number(key.substring(prefix.length)); | ||||
|             }) | ||||
| 
 | ||||
|         console.log("Avaible datasets in local storage:", indexes) | ||||
| 
 | ||||
|         const zLevels = indexes.map(i => i % 100) | ||||
|         const indexesSet = new Set(indexes) | ||||
|         const maxZoom = Math.max(...zLevels) | ||||
|         const minZoom = Math.min(...zLevels) | ||||
|         const self = this; | ||||
| 
 | ||||
|         const neededTiles = state.locationControl.map( | ||||
|             location => { | ||||
|                 if (!layer.isDisplayed.data) { | ||||
|                     // No need to download! - the layer is disabled
 | ||||
|                     return undefined; | ||||
|                 } | ||||
| 
 | ||||
|                 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 {Utils} from "../Utils"; | ||||
| 
 | ||||
| export class GeoOperations { | ||||
| 
 | ||||
|  | @ -185,6 +186,44 @@ export class GeoOperations { | |||
|         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 | ||||
|      * @param way The road on which you want to find a point | ||||
|  | @ -340,6 +379,7 @@ export class BBox { | |||
|     readonly maxLon: number; | ||||
|     readonly minLat: number; | ||||
|     readonly minLon: number; | ||||
|     static global: BBox = new BBox([[-180,-90],[180,90]]); | ||||
| 
 | ||||
|     constructor(coordinates) { | ||||
|         this.maxLat = Number.MIN_VALUE; | ||||
|  | @ -361,12 +401,11 @@ export class BBox { | |||
|         return new BBox([[bounds.getWest(), bounds.getNorth()], [bounds.getEast(), bounds.getSouth()]]) | ||||
|     } | ||||
| 
 | ||||
|     static get(feature) { | ||||
|     static get(feature): BBox { | ||||
|         if (feature.bbox?.overlapsWith === undefined) { | ||||
|             const turfBbox: number[] = turf.bbox(feature) | ||||
|             feature.bbox = new BBox([[turfBbox[0], turfBbox[1]], [turfBbox[2], turfBbox[3]]]); | ||||
|         } | ||||
| 
 | ||||
|         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\/(.*)/) | ||||
|         console.log("Mapview matched ", value, mapview) | ||||
|         if(mapview !== null){ | ||||
|             const key = mapview[1] | ||||
|             return {key:key, isApiv4: !isNaN(Number(key))}; | ||||
|  |  | |||
|  | @ -1,15 +1,9 @@ | |||
| import SimpleMetaTagger from "./SimpleMetaTagger"; | ||||
| import {ExtraFunction} from "./ExtraFunction"; | ||||
| import {Relation} from "./Osm/ExtractRelations"; | ||||
| import {ExtraFuncParams, ExtraFunction} from "./ExtraFunction"; | ||||
| import {UIEventSource} from "./UIEventSource"; | ||||
| 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, ... | ||||
|  * | ||||
|  | @ -22,13 +16,12 @@ export default class MetaTagging { | |||
|     private static readonly stopErrorOutputAt = 10; | ||||
| 
 | ||||
|     /** | ||||
|      * An actor which adds metatags on every feature in the given object | ||||
|      * The features are a list of geojson-features, with a "properties"-field and geometry | ||||
|      * This method (re)calculates all metatags and calculated tags on every given object. | ||||
|      * The given features should be part of the given layer | ||||
|      */ | ||||
|     static addMetatags(features: { feature: any; freshness: Date }[], | ||||
|                        allKnownFeatures: UIEventSource<{ feature: any; freshness: Date }[]>, | ||||
|                        relations: Map<string, { role: string, relation: Relation }[]>, | ||||
|                        layers: LayerConfig[], | ||||
|                        params: ExtraFuncParams, | ||||
|                        layer: LayerConfig, | ||||
|                        includeDates = true) { | ||||
| 
 | ||||
|         if (features === undefined || features.length === 0) { | ||||
|  | @ -44,59 +37,32 @@ export default class MetaTagging { | |||
|                 metatag.addMetaTags(features); | ||||
|             } catch (e) { | ||||
|                 console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e) | ||||
| 
 | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // The functions - per layer - which add the new keys
 | ||||
|         const layerFuncs = new Map<string, ((params: Params, feature: any) => void)>(); | ||||
|         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) | ||||
|             } | ||||
|         const layerFuncs = this.createRetaggingFunc(layer) | ||||
| 
 | ||||
|         if (layerFuncs !== undefined) { | ||||
|             for (const feature of features) { | ||||
|                 // @ts-ignore
 | ||||
|                 const key = feature.feature._matching_layer_id; | ||||
|                 const f = layerFuncs.get(key); | ||||
|                 if (f === undefined) { | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 try { | ||||
|                     f({featuresPerLayer: featuresPerLayer, memberships: relations}, feature.feature) | ||||
|                     layerFuncs(params, feature.feature) | ||||
|                 } catch (e) { | ||||
|                     console.error(e) | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static createRetaggingFunc(layer: LayerConfig): | ||||
|         ((params: Params, feature: any) => void) { | ||||
|         ((params: ExtraFuncParams, feature: any) => void) { | ||||
|         const calculatedTags: [string, string][] = layer.calculatedTags; | ||||
|         if (calculatedTags === undefined) { | ||||
|             return undefined; | ||||
|         } | ||||
| 
 | ||||
|         const functions: ((params: Params, feature: any) => void)[] = []; | ||||
|         const functions: ((params: ExtraFuncParams, feature: any) => void)[] = []; | ||||
|         for (const entry of calculatedTags) { | ||||
|             const key = entry[0] | ||||
|             const code = entry[1]; | ||||
|  | @ -145,14 +111,13 @@ export default class MetaTagging { | |||
|                 console.error("Could not create a dynamic function: ", e) | ||||
|             } | ||||
|         } | ||||
|         return (params: Params, feature) => { | ||||
|         return (params: ExtraFuncParams, feature) => { | ||||
|             const tags = feature.properties | ||||
|             if (tags === undefined) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const relations = params.memberships?.get(feature.properties.id) ?? [] | ||||
|             ExtraFunction.FullPatchFeature(params.featuresPerLayer, relations, feature); | ||||
|             ExtraFunction.FullPatchFeature(params, feature); | ||||
|             try { | ||||
|                 for (const f of functions) { | ||||
|                     f(params, feature); | ||||
|  |  | |||
|  | @ -1,15 +1,30 @@ | |||
| /** | ||||
|  * Represents a single change to an object | ||||
|  */ | ||||
| export interface ChangeDescription { | ||||
| 
 | ||||
|     /** | ||||
|      * Identifier of the object | ||||
|      */ | ||||
|     type: "node" | "way" | "relation", | ||||
|     /** | ||||
|      * Negative for a new objects | ||||
|      * Identifier of the object | ||||
|      * Negative for new objects | ||||
|      */ | ||||
|     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 }[], | ||||
| 
 | ||||
|     /** | ||||
|      * A change to the geometry: | ||||
|      * 1) Change of node location | ||||
|      * 2) Change of way geometry | ||||
|      * 3) Change of relation members (untested) | ||||
|      */ | ||||
|     changes?: { | ||||
|         lat: number, | ||||
|         lon: number | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ export class Geocoding { | |||
|                       osm_type: string, osm_id: string | ||||
|                   }[]) => 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=" + | ||||
|             `${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}` + | ||||
|             "&accept-language=nl&q=" + query; | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import * as OsmToGeoJson from "osmtogeojson"; | ||||
| import Bounds from "../../Models/Bounds"; | ||||
| import {TagsFilter} from "../Tags/TagsFilter"; | ||||
| import ExtractRelations from "./ExtractRelations"; | ||||
| import RelationsTracker from "./RelationsTracker"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| 
 | ||||
|  | @ -15,16 +15,20 @@ export class Overpass { | |||
|     private readonly _timeout: UIEventSource<number>; | ||||
|     private readonly _extraScripts: string[]; | ||||
|     private _includeMeta: boolean; | ||||
|     private _relationTracker: RelationsTracker; | ||||
|      | ||||
|     | ||||
|     constructor(filter: TagsFilter, extraScripts: string[], | ||||
|                 interpreterUrl: UIEventSource<string>, | ||||
|                 timeout: UIEventSource<number>, | ||||
|                 relationTracker: RelationsTracker, | ||||
|                 includeMeta = true) { | ||||
|         this._timeout = timeout; | ||||
|         this._interpreterUrl = interpreterUrl; | ||||
|         this._filter = filter | ||||
|         this._extraScripts = extraScripts; | ||||
|         this._includeMeta = includeMeta; | ||||
|         this._relationTracker = relationTracker | ||||
|     } | ||||
| 
 | ||||
|     queryGeoJson(bounds: Bounds, continuation: ((any, date: Date) => void), onFail: ((reason) => void)): void { | ||||
|  | @ -35,6 +39,7 @@ export class Overpass { | |||
|             console.log("Using testing URL") | ||||
|             query = Overpass.testUrl; | ||||
|         } | ||||
|         const self = this; | ||||
|         Utils.downloadJson(query) | ||||
|             .then(json => { | ||||
|                 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
 | ||||
|                 const geojson = OsmToGeoJson.default(json); | ||||
|                 const osmTime = new Date(json.osm3s.timestamp_osm_base); | ||||
| 
 | ||||
|                 continuation(geojson, osmTime); | ||||
|             }).catch(onFail) | ||||
|             }).catch(e => { | ||||
|             onFail(e); | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     buildQuery(bbox: string): string { | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| import State from "../../State"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| 
 | ||||
| export interface Relation { | ||||
|     id: number, | ||||
|  | @ -13,11 +14,15 @@ export interface Relation { | |||
|     properties: any | ||||
| } | ||||
| 
 | ||||
| export default class ExtractRelations { | ||||
| export default class RelationsTracker { | ||||
| 
 | ||||
|     public static RegisterRelations(overpassJson: any): void { | ||||
|         const memberships = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(overpassJson)) | ||||
|         State.state.knownRelations.setData(memberships) | ||||
|     public knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>(new Map(), "Relation memberships"); | ||||
| 
 | ||||
|     constructor() { | ||||
|     } | ||||
| 
 | ||||
|     public RegisterRelations(overpassJson: any): void { | ||||
|         this.UpdateMembershipTable(RelationsTracker.GetRelationElements(overpassJson)) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -25,7 +30,7 @@ export default class ExtractRelations { | |||
|      * @param overpassJson | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static GetRelationElements(overpassJson: any): Relation[] { | ||||
|     private static GetRelationElements(overpassJson: any): Relation[] { | ||||
|         const relations = overpassJson.elements | ||||
|             .filter(element => element.type === "relation" && element.tags.type !== "multipolygon") | ||||
|         for (const relation of relations) { | ||||
|  | @ -39,12 +44,11 @@ export default class ExtractRelations { | |||
|      * @param relations | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static BuildMembershipTable(relations: Relation[]): Map<string, { role: string, relation: Relation }[]> { | ||||
|         const memberships = new Map<string, { role: string, relation: Relation }[]>() | ||||
| 
 | ||||
|     private UpdateMembershipTable(relations: Relation[]): void { | ||||
|         const memberships = this.knownRelations.data | ||||
|         let changed = false; | ||||
|         for (const relation of relations) { | ||||
|             for (const member of relation.members) { | ||||
| 
 | ||||
|                 const role = { | ||||
|                     role: member.role, | ||||
|                     relation: relation | ||||
|  | @ -53,11 +57,21 @@ export default class ExtractRelations { | |||
|                 if (!memberships.has(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 Title from "../UI/Base/Title"; | ||||
| import {FixedUiElement} from "../UI/Base/FixedUiElement"; | ||||
| import CountryCoder from "latlon2country/index"; | ||||
| 
 | ||||
| 
 | ||||
| const cardinalDirections = { | ||||
|  | @ -20,7 +21,7 @@ const cardinalDirections = { | |||
| 
 | ||||
| 
 | ||||
| export default class SimpleMetaTagger { | ||||
|     static coder: any; | ||||
|     private static coder: CountryCoder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); | ||||
|     public static readonly objectMetaInfo = new SimpleMetaTagger( | ||||
|         { | ||||
|             keys: ["_last_edit:contributor", | ||||
|  | @ -84,7 +85,7 @@ export default class SimpleMetaTagger { | |||
|         }, | ||||
|         (feature => { | ||||
|             const units = Utils.NoNull([].concat(...State.state?.layoutToUse?.data?.layers?.map(layer => layer.units ?? []))); | ||||
|             if(units.length == 0){ | ||||
|             if (units.length == 0) { | ||||
|                 return; | ||||
|             } | ||||
|             let rewritten = false; | ||||
|  | @ -93,7 +94,7 @@ export default class SimpleMetaTagger { | |||
|                     continue; | ||||
|                 } | ||||
|                 for (const unit of units) { | ||||
|                     if(unit.appliesToKeys === undefined){ | ||||
|                     if (unit.appliesToKeys === undefined) { | ||||
|                         console.error("The unit ", unit, "has no appliesToKey defined") | ||||
|                         continue | ||||
|                     } | ||||
|  | @ -148,7 +149,7 @@ export default class SimpleMetaTagger { | |||
|             const lat = centerPoint.geometry.coordinates[1]; | ||||
|             const lon = centerPoint.geometry.coordinates[0]; | ||||
| 
 | ||||
|             SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => { | ||||
|             SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, (countries: string[]) => { | ||||
|                 try { | ||||
|                     const oldCountry = feature.properties["_country"]; | ||||
|                     feature.properties["_country"] = countries[0].trim().toLowerCase(); | ||||
|  | @ -160,7 +161,7 @@ export default class SimpleMetaTagger { | |||
|                 } catch (e) { | ||||
|                     console.warn(e) | ||||
|                 } | ||||
|             }); | ||||
|             }) | ||||
|         } | ||||
|     ) | ||||
|     private static isOpen = new SimpleMetaTagger( | ||||
|  | @ -426,11 +427,7 @@ export default class SimpleMetaTagger { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static GetCountryCodeFor(lon: number, lat: number, callback: (country: string) => void) { | ||||
|         SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, callback) | ||||
|     } | ||||
| 
 | ||||
|     static HelpText(): BaseUIElement { | ||||
|     public static HelpText(): BaseUIElement { | ||||
|         const subElements: (string | BaseUIElement)[] = [ | ||||
|             new Combine([ | ||||
|                 new Title("Metatags", 1), | ||||
|  | @ -453,7 +450,7 @@ export default class SimpleMetaTagger { | |||
|         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++) { | ||||
|             let feature = features[i]; | ||||
|             this._f(feature.feature, i, feature.freshness); | ||||
|  |  | |||
|  | @ -81,9 +81,12 @@ export class UIEventSource<T> { | |||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     public addCallbackAndRun(callback: ((latestData: T) => void)): UIEventSource<T> { | ||||
|         callback(this.data); | ||||
|         return this.addCallback(callback); | ||||
|     public addCallbackAndRun(callback: ((latestData: T) => (boolean | void | any))): UIEventSource<T> { | ||||
|         const doDeleteCallback = callback(this.data); | ||||
|         if (!doDeleteCallback) { | ||||
|             this.addCallback(callback); | ||||
|         } | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     public setData(t: T): UIEventSource<T> { | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import {Utils} from "../Utils"; | |||
| 
 | ||||
| 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
 | ||||
|     public static userJourney = { | ||||
|  | @ -26,12 +26,6 @@ export default class Constants { | |||
|      */ | ||||
|     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 { | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return; | ||||
|  |  | |||
|  | @ -59,9 +59,8 @@ export interface LayerConfigJson { | |||
|      * NOTE: the previous format was 'overpassTags: AndOrTagConfigJson | string', which is interpreted as a shorthand for source: {osmTags: "key=value"} | ||||
|      *  While still supported, this is considered deprecated | ||||
|      */ | ||||
|     source: { osmTags: AndOrTagConfigJson | string } | | ||||
|         { osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number, isOsmCache?: boolean } | | ||||
|         { osmTags: AndOrTagConfigJson | string, overpassScript: string } | ||||
|     source: { osmTags: AndOrTagConfigJson | string, overpassScript?: string  } | | ||||
|         { osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number, isOsmCache?: boolean } | ||||
|      | ||||
|     /** | ||||
|      * | ||||
|  |  | |||
|  | @ -246,14 +246,6 @@ export default class LayoutConfig { | |||
|         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 | ||||
|      * This is to fix loading from external sources | ||||
|  |  | |||
|  | @ -2,18 +2,18 @@ import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | |||
| 
 | ||||
| export default class SourceConfig { | ||||
| 
 | ||||
|     osmTags?: TagsFilter; | ||||
|     overpassScript?: string; | ||||
|     geojsonSource?: string; | ||||
|     geojsonZoomLevel?: number; | ||||
|     isOsmCacheLayer: boolean; | ||||
|     public readonly osmTags?: TagsFilter; | ||||
|     public readonly overpassScript?: string; | ||||
|     public readonly geojsonSource?: string; | ||||
|     public readonly geojsonZoomLevel?: number; | ||||
|     public readonly isOsmCacheLayer: boolean; | ||||
| 
 | ||||
|     constructor(params: { | ||||
|         osmTags?: TagsFilter, | ||||
|         overpassScript?: string, | ||||
|         geojsonSource?: string, | ||||
|         isOsmCache?: boolean, | ||||
|         geojsonSourceLevel?: number | ||||
|         geojsonSourceLevel?: number, | ||||
|     }, context?: string) { | ||||
| 
 | ||||
|         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 Loc from "./Models/Loc"; | ||||
| import Constants from "./Models/Constants"; | ||||
| 
 | ||||
| import OverpassFeatureSource from "./Logic/Actors/OverpassFeatureSource"; | ||||
| import TitleHandler from "./Logic/Actors/TitleHandler"; | ||||
| import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; | ||||
| import {Relation} from "./Logic/Osm/ExtractRelations"; | ||||
| import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; | ||||
| import OsmApiFeatureSource from "./Logic/FeatureSource/Sources/OsmApiFeatureSource"; | ||||
| import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; | ||||
| import FilteredLayer from "./Models/FilteredLayer"; | ||||
| import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor"; | ||||
| import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; | ||||
| import {BBox} from "./Logic/GeoOperations"; | ||||
| 
 | ||||
| /** | ||||
|  * Contains the global state: a bunch of UI-event sources | ||||
|  | @ -57,8 +55,6 @@ export default class State { | |||
| 
 | ||||
|     public favouriteLayers: UIEventSource<string[]>; | ||||
| 
 | ||||
|     public layerUpdater: OverpassFeatureSource; | ||||
| 
 | ||||
|     public osmApiFeatureSource: OsmApiFeatureSource; | ||||
| 
 | ||||
|     public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers"); | ||||
|  | @ -71,12 +67,6 @@ export default class State { | |||
|         "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 featureSwitchSearch: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchBackgroundSlection: UIEventSource<boolean>; | ||||
|  | @ -96,6 +86,7 @@ export default class State { | |||
|     public readonly featureSwitchExportAsPdf: UIEventSource<boolean>; | ||||
|     public readonly overpassUrl: UIEventSource<string>; | ||||
|     public readonly overpassTimeout: UIEventSource<number>; | ||||
|     public readonly overpassMaxZoom: UIEventSource<number> = new UIEventSource<number>(undefined); | ||||
| 
 | ||||
|     public featurePipeline: FeaturePipeline; | ||||
| 
 | ||||
|  | @ -104,6 +95,12 @@ export default class State { | |||
|      * The map location: currently centered lat, lon and zoom | ||||
|      */ | ||||
|     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 readonly backgroundLayerId: UIEventSource<string>; | ||||
| 
 | ||||
|  | @ -398,7 +395,7 @@ export default class State { | |||
| 
 | ||||
|         new ChangeToElementsActor(this.changes, this.allElements) | ||||
| 
 | ||||
|         this.osmApiFeatureSource = new OsmApiFeatureSource(Constants.useOsmApiAt, this) | ||||
|         this.osmApiFeatureSource = new OsmApiFeatureSource(this) | ||||
| 
 | ||||
|         new PendingChangesUploader(this.changes, this.selectedElement); | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,6 +10,9 @@ export default class Img extends BaseUIElement { | |||
|         fallbackImage?: string | ||||
|     }) { | ||||
|         super(); | ||||
|         if(src === undefined || src === "undefined"){ | ||||
|             throw "Undefined src for image" | ||||
|         } | ||||
|         this._src = src; | ||||
|         this._rawSvg = rawSvg; | ||||
|         this._options = options; | ||||
|  |  | |||
|  | @ -1,208 +1,30 @@ | |||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import * as L from "leaflet"; | ||||
| import {Map} from "leaflet"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {BBox} from "../../Logic/GeoOperations"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| 
 | ||||
| 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 }> | ||||
| } | ||||
| 
 | ||||
| export default class Minimap { | ||||
|     /** | ||||
|      * A stub implementation. The actual implementation is injected later on, but only in the browser. | ||||
|      * importing leaflet crashes node-ts, which is pretty annoying considering the fact that a lot of scripts use it | ||||
|      */ | ||||
| 
 | ||||
|     /** | ||||
|      * Construct a minimap | ||||
|      */ | ||||
|     public static createMiniMap: (options: MinimapOptions) => BaseUIElement & { readonly leafletMap: UIEventSource<any> } | ||||
| 
 | ||||
| export default class Minimap 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 }>; | ||||
| 
 | ||||
|     constructor(options?: { | ||||
|                     background?: UIEventSource<BaseLayer>, | ||||
|                     location?: UIEventSource<Loc>, | ||||
|                     allowMoving?: boolean, | ||||
|                     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", | ||||
|                     background: State.state.backgroundLayer, | ||||
|                     location: State.state.locationControl, | ||||
|                     features: State.state.featurePipeline.features, | ||||
|                     features: State.state.featurePipeline, | ||||
|                     layout: State.state.layoutToUse, | ||||
|                 }).isRunning.addCallbackAndRun(isRunning => isExporting.setData(isRunning)) | ||||
|         } | ||||
|  |  | |||
|  | @ -5,19 +5,19 @@ import {UIEventSource} from "../../Logic/UIEventSource"; | |||
| import UserDetails from "../../Logic/Osm/OsmConnection"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import * as L from "leaflet" | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {BBox} from "../../Logic/GeoOperations"; | ||||
| 
 | ||||
| /** | ||||
|  * The bottom right attribution panel in the leaflet map | ||||
|  */ | ||||
| export default class Attribution extends Combine { | ||||
| 
 | ||||
|     constructor(location: UIEventSource<Loc>, | ||||
|      constructor(location: UIEventSource<Loc>, | ||||
|                 userDetails: UIEventSource<UserDetails>, | ||||
|                 layoutToUse: UIEventSource<LayoutConfig>, | ||||
|                 leafletMap: UIEventSource<L.Map>) { | ||||
|                 currentBounds: UIEventSource<BBox>) { | ||||
| 
 | ||||
|         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); | ||||
|  | @ -43,7 +43,7 @@ export default class Attribution extends Combine { | |||
|                     if (userDetails.csCount < Constants.userJourney.tagsVisibleAndWikiLinked) { | ||||
|                         return undefined; | ||||
|                     } | ||||
|                     const bounds: any = leafletMap?.data?.getBounds(); | ||||
|                     const bounds: any = currentBounds.data; | ||||
|                     if (bounds === 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}` | ||||
|                     return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true); | ||||
|                 }, | ||||
|                 [location, leafletMap] | ||||
|                 [location, currentBounds] | ||||
|             ) | ||||
|         ) | ||||
|         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.credits, | ||||
|             "<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/>", | ||||
| 
 | ||||
|             new VariableUiElement(contributions.map(contributions => { | ||||
|                 if(contributions === undefined){ | ||||
|                     return "" | ||||
|                 } | ||||
|                 const sorted = Array.from(contributions, ([name, value]) => ({ | ||||
|                     name, | ||||
|                     value | ||||
|  |  | |||
|  | @ -2,54 +2,113 @@ import {SubtleButton} from "../Base/SubtleButton"; | |||
| import Svg from "../../Svg"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import State from "../../State"; | ||||
| import {FeatureSourceUtils} from "../../Logic/FeatureSource/FeatureSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import CheckBoxes from "../Input/Checkboxes"; | ||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import {BBox, GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| 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 { | ||||
|      | ||||
|     constructor() { | ||||
|         const state: { | ||||
|             featurePipeline: FeaturePipeline, | ||||
|             layoutToUse: UIEventSource<LayoutConfig>, | ||||
|             currentBounds: UIEventSource<BBox> | ||||
|         } = State.state | ||||
|          | ||||
| 
 | ||||
|         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 metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0) | ||||
| 
 | ||||
|          | ||||
|         const buttonGeoJson = new SubtleButton(Svg.floppy_ui(), | ||||
|             new Combine([t.downloadGeojson.Clone().SetClass("font-bold"), | ||||
|                 t.downloadGeoJsonHelper.Clone()]).SetClass("flex flex-col")) | ||||
|             .onClick(() => { | ||||
|                 const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline, {metadata: metaisIncluded.data}) | ||||
|                 const name = State.state.layoutToUse.data.id; | ||||
|                 Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), | ||||
|                 const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data) | ||||
|                 Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, "  "), | ||||
|                     `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, { | ||||
|                         mimetype: "application/vnd.geo+json" | ||||
|                     }); | ||||
|             }) | ||||
|          | ||||
| 
 | ||||
|         const buttonCSV = new SubtleButton(Svg.floppy_ui(), new Combine( | ||||
|             [t.downloadCSV.Clone().SetClass("font-bold"), | ||||
|                 t.downloadCSVHelper.Clone()]).SetClass("flex flex-col")) | ||||
|             .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 name = State.state.layoutToUse.data.id; | ||||
| 
 | ||||
|                 Utils.offerContentsAsDownloadableFile(csv, | ||||
|                     `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`, { | ||||
|                         mimetype: "text/csv" | ||||
|                     }); | ||||
| 
 | ||||
| 
 | ||||
|             }) | ||||
| 
 | ||||
|         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") | ||||
| 
 | ||||
|         super( | ||||
|             downloadButtons, | ||||
|             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 Toggle from "../Input/Toggle"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {Utils} from "../../Utils"; | ||||
| 
 | ||||
| export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | ||||
| 
 | ||||
|  | @ -62,9 +63,15 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | |||
| 
 | ||||
|         const tabs = 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({ | ||||
|                 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") | ||||
|             } | ||||
|         ); | ||||
|  |  | |||
|  | @ -52,7 +52,7 @@ export default class ImportButton extends Toggle { | |||
|         const withLoadingCheck = new Toggle( | ||||
|             t.stillLoading, | ||||
|             new Combine([button, appliedTags]).SetClass("flex flex-col"), | ||||
|             State.state.layerUpdater.runningQuery | ||||
|             State.state.featurePipeline.runningQuery | ||||
|         ) | ||||
|         super(t.hasBeenImported, withLoadingCheck, isImported) | ||||
|     } | ||||
|  |  | |||
|  | @ -9,18 +9,21 @@ import MapControlButton from "../MapControlButton"; | |||
| import Svg from "../../Svg"; | ||||
| import AllDownloads from "./AllDownloads"; | ||||
| 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 { | ||||
| 
 | ||||
|     constructor(featureSource: FeatureSource) { | ||||
|     constructor(state: {featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc>}) { | ||||
| 
 | ||||
|         const toggledCopyright = new ScrollableFullScreen( | ||||
|             () => Translations.t.general.attribution.attributionTitle.Clone(), | ||||
|             () => | ||||
|                 new AttributionPanel( | ||||
|                     State.state.layoutToUse, | ||||
|                     new ContributorCount(featureSource).Contributors | ||||
|                     new ContributorCount(state).Contributors | ||||
|                 ), | ||||
|             undefined | ||||
|         ); | ||||
|  |  | |||
|  | @ -65,10 +65,6 @@ export default class SimpleAddUI extends Toggle { | |||
|             State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get( | ||||
|                 newElementAction.newElementId | ||||
|             )) | ||||
|             console.log("Did set selected element to", State.state.allElements.ContainingFeatures.get( | ||||
|                 newElementAction.newElementId | ||||
|             )) | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         const addUi = new VariableUiElement( | ||||
|  | @ -104,7 +100,7 @@ export default class SimpleAddUI extends Toggle { | |||
|                     new Toggle( | ||||
|                         Translations.t.general.add.stillLoading.Clone().SetClass("alert"), | ||||
|                         addUi, | ||||
|                         State.state.layerUpdater.runningQuery | ||||
|                         State.state.featurePipeline.runningQuery | ||||
|                     ), | ||||
|                     Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"), | ||||
|                     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 ?? []); | ||||
|             console.log("Opening precise input ", preset.preciseInput, "with tags", tags) | ||||
|             preciseInput = new LocationInput({ | ||||
|                 mapBackground: backgroundLayer, | ||||
|                 centerLocation: locationSrc, | ||||
|  | @ -215,10 +210,7 @@ export default class SimpleAddUI extends Toggle { | |||
|         const disableFiltersOrConfirm = new Toggle( | ||||
|             openLayerOrConfirm, | ||||
|             disableFilter, | ||||
|             preset.layerToAddTo.appliedFilters.map(filters => { | ||||
|                 console.log("Current filters are ", filters) | ||||
|                 return filters === undefined || filters.normalize().and.length === 0; | ||||
|             }) | ||||
|             preset.layerToAddTo.appliedFilters.map(filters => filters === undefined || filters.normalize().and.length === 0) | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ export default class CenterMessageBox extends VariableUiElement { | |||
| 
 | ||||
|     constructor() { | ||||
|         const state = State.state; | ||||
|         const updater = State.state.layerUpdater; | ||||
|         const updater = State.state.featurePipeline; | ||||
|         const t = Translations.t.centerMessage; | ||||
|         const message = updater.runningQuery.map( | ||||
|             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 jspdf and downloads it | ||||
|  | @ -8,21 +24,6 @@ | |||
|  *        -    add new layout in "PDFLayout" | ||||
|  *                -> 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 { | ||||
|     // dimensions of the map in milimeter
 | ||||
|     public isRunning = new UIEventSource(true) | ||||
|  | @ -39,7 +40,7 @@ export default class ExportPDF { | |||
|             freeDivId: string, | ||||
|             location: UIEventSource<Loc>, | ||||
|             background?: UIEventSource<BaseLayer> | ||||
|             features: UIEventSource<{ feature: any }[]>, | ||||
|             features: FeaturePipeline, | ||||
|             layout: UIEventSource<LayoutConfig> | ||||
|         } | ||||
|     ) { | ||||
|  | @ -57,7 +58,7 @@ export default class ExportPDF { | |||
|             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
 | ||||
|             background: options.background, | ||||
|             allowMoving: false, | ||||
|  | @ -83,24 +84,21 @@ export default class ExportPDF { | |||
|         minimap.AttachTo(options.freeDivId) | ||||
| 
 | ||||
|         // Next: we prepare the features. Only fully contained features are shown
 | ||||
|         const bounded = options.features.map(feats => { | ||||
| 
 | ||||
|             const leaflet = minimap.leafletMap.data; | ||||
|             if (leaflet === undefined) { | ||||
|                 return feats | ||||
|             } | ||||
|         minimap.leafletMap .addCallbackAndRunD(leaflet => { | ||||
|             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) | ||||
|                 new ShowDataLayer( | ||||
|                     { | ||||
|                         features: tile, | ||||
|                         leafletMap: minimap.leafletMap, | ||||
|                         layerToShow: tile.layer.layerDef, | ||||
|                         enablePopups: false | ||||
|                     } | ||||
|                 ) | ||||
|             }) | ||||
|              | ||||
|         }, [minimap.leafletMap]) | ||||
| 
 | ||||
|         // Add the features to the minimap
 | ||||
|         new ShowDataLayer( | ||||
|             bounded, | ||||
|             minimap.leafletMap, | ||||
|             options.layout, | ||||
|             false | ||||
|         ) | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import Img from "../Base/Img"; | |||
| import ImageAttributionSource from "../../Logic/ImageProviders/ImageAttributionSource"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import Loading from "../Base/Loading"; | ||||
| 
 | ||||
| 
 | ||||
| export class AttributedImage extends Combine { | ||||
|  | @ -16,8 +17,13 @@ export class AttributedImage extends Combine { | |||
|             img = new Img(urlSource); | ||||
|             attr = new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon()) | ||||
|         } else { | ||||
|             img = new VariableUiElement(preparedUrl.map(url => new Img(url, false, {fallbackImage: './assets/svg/blocked.svg'}))) | ||||
|             attr = new VariableUiElement(preparedUrl.map(url => new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon()))) | ||||
|             img = new VariableUiElement(preparedUrl.map(url => { | ||||
|                 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 {Utils} from "../../Utils"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import Minimap from "../Base/Minimap"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Selects a direction in degrees | ||||
|  */ | ||||
| export default class DirectionInput extends InputElement<string> { | ||||
|     public static constructMinimap: ((any) => BaseUIElement); | ||||
|     public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||
|     private readonly _location: UIEventSource<Loc>; | ||||
|     private readonly value: UIEventSource<string>; | ||||
|  | @ -40,7 +40,7 @@ export default class DirectionInput extends InputElement<string> { | |||
| 
 | ||||
|         let map: BaseUIElement = new FixedUiElement("") | ||||
|         if (!Utils.runningFromConsole) { | ||||
|             map = DirectionInput.constructMinimap({ | ||||
|             map = Minimap.createMiniMap({ | ||||
|                 background: this.background, | ||||
|                 allowMoving: false, | ||||
|                 location: this._location | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ import Svg from "../../Svg"; | |||
| import {Utils} from "../../Utils"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| 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
 | ||||
|         let map = undefined | ||||
|         if (!Utils.runningFromConsole) { | ||||
|             map = DirectionInput.constructMinimap({ | ||||
|             map = Minimap.createMiniMap({ | ||||
|                 background: this.background, | ||||
|                 allowMoving: false, | ||||
|                 location: this._location, | ||||
|  |  | |||
|  | @ -8,35 +8,31 @@ import Svg from "../../Svg"; | |||
| import State from "../../State"; | ||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import ShowDataLayer from "../ShowDataLayer"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; | ||||
| 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> { | ||||
| 
 | ||||
|     private static readonly matchLayout = new UIEventSource(new LayoutConfig({ | ||||
|         description: "Matchpoint style", | ||||
|         icon: "./assets/svg/crosshair-empty.svg", | ||||
|         id: "matchpoint", | ||||
|         language: ["en"], | ||||
|         layers: [{ | ||||
|     private static readonly matchLayer = new LayerConfig( | ||||
|         { | ||||
|             id: "matchpoint", source: { | ||||
|                 osmTags: {and: []} | ||||
|             }, | ||||
|             icon: "./assets/svg/crosshair-empty.svg" | ||||
|         }], | ||||
|         maintainer: "MapComplete", | ||||
|         startLat: 0, | ||||
|         startLon: 0, | ||||
|         startZoom: 0, | ||||
|         title: "Location input", | ||||
|         version: "0" | ||||
|         }, "matchpoint icon", true | ||||
|     ) | ||||
|      | ||||
|     })); | ||||
|     IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||
|     public readonly snappedOnto: UIEventSource<any> = new UIEventSource<any>(undefined) | ||||
|     private _centerLocation: UIEventSource<Loc>; | ||||
|     private readonly mapBackground: UIEventSource<BaseLayer>; | ||||
|     /** | ||||
|      * The features to which the input should be snapped | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _snapTo: UIEventSource<{ feature: any }[]> | ||||
|     private readonly _value: UIEventSource<Loc> | ||||
|     private readonly _snappedPoint: UIEventSource<any> | ||||
|  | @ -143,7 +139,7 @@ export default class LocationInput extends InputElement<Loc> { | |||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         try { | ||||
|             const clickLocation = new UIEventSource<Loc>(undefined); | ||||
|             const map = new Minimap( | ||||
|             const map = Minimap.createMiniMap( | ||||
|                 { | ||||
|                     location: this._centerLocation, | ||||
|                     background: this.mapBackground, | ||||
|  | @ -198,7 +194,6 @@ export default class LocationInput extends InputElement<Loc> { | |||
|             }) | ||||
| 
 | ||||
|             if (this._snapTo !== undefined) { | ||||
|                 new ShowDataLayer(this._snapTo, map.leafletMap, State.state.layoutToUse, false, false) | ||||
| 
 | ||||
|                 const matchPoint = this._snappedPoint.map(loc => { | ||||
|                     if (loc === undefined) { | ||||
|  | @ -207,16 +202,25 @@ export default class LocationInput extends InputElement<Loc> { | |||
|                     return [{feature: loc}]; | ||||
|                 }) | ||||
|                 if (this._snapTo) { | ||||
|                     let layout = LocationInput.matchLayout | ||||
|                     if (this._snappedPointTags !== undefined) { | ||||
|                         layout = State.state.layoutToUse | ||||
|                     if (this._snappedPointTags === undefined) { | ||||
|                         // No special tags - we show a default crosshair
 | ||||
|                         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 Minimap from "../Base/Minimap"; | ||||
| import State from "../../State"; | ||||
| import ShowDataLayer from "../ShowDataLayer"; | ||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; | ||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import {LeafletMouseEvent} from "leaflet"; | ||||
| import Combine from "../Base/Combine"; | ||||
|  | @ -13,10 +13,16 @@ import Translations from "../i18n/Translations"; | |||
| import SplitAction from "../../Logic/Osm/Actions/SplitAction"; | ||||
| import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject"; | ||||
| 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 { | ||||
|     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 | ||||
|  | @ -36,7 +42,7 @@ export default class SplitRoadWizard extends Toggle { | |||
|         const splitClicked = new UIEventSource<boolean>(false); | ||||
| 
 | ||||
|         // 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;"); | ||||
| 
 | ||||
|         // 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 roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]); | ||||
|         // Datalayer displaying the road and the cut points (if any)
 | ||||
|         new ShowDataLayer(roadEventSource, miniMap.leafletMap, State.state.layoutToUse, false, true); | ||||
|         new ShowDataLayer(splitPoints, miniMap.leafletMap, SplitRoadWizard.splitLayout, false, false) | ||||
|         new ShowDataMultiLayer({ | ||||
|             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. | ||||
|  | @ -135,21 +153,4 @@ export default class SplitRoadWizard extends Toggle { | |||
|         const confirm = new Toggle(mapView, splitToggle, splitClicked); | ||||
|         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 | ||||
|  */ | ||||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| import * as L from "leaflet" | ||||
| import State from "../State"; | ||||
| import FeatureInfoBox from "./Popup/FeatureInfoBox"; | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
| import FeatureSource from "../Logic/FeatureSource/FeatureSource"; | ||||
| 
 | ||||
| export interface ShowDataLayerOptions { | ||||
|     features: FeatureSource, | ||||
|     leafletMap: UIEventSource<L.Map>, | ||||
|     enablePopups?: true | boolean, | ||||
|     zoomToFeatures? : false | boolean, | ||||
| } | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import FeatureInfoBox from "../Popup/FeatureInfoBox"; | ||||
| import State from "../../State"; | ||||
| import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; | ||||
| 
 | ||||
| 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 | ||||
|  */ | ||||
| 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 { | ||||
|     constructor(options: ShowDataLayerOptions & { layers: UIEventSource<FilteredLayer[]> }) { | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ import {ImageCarousel} from "./Image/ImageCarousel"; | |||
| import Combine from "./Base/Combine"; | ||||
| import {FixedUiElement} from "./Base/FixedUiElement"; | ||||
| import {ImageUploadFlow} from "./Image/ImageUploadFlow"; | ||||
| 
 | ||||
| import ShareButton from "./BigComponents/ShareButton"; | ||||
| import Svg from "../Svg"; | ||||
| import ReviewElement from "./Reviews/ReviewElement"; | ||||
|  | @ -13,7 +12,6 @@ import MangroveReviews from "../Logic/Web/MangroveReviews"; | |||
| import Translations from "./i18n/Translations"; | ||||
| import ReviewForm from "./Reviews/ReviewForm"; | ||||
| import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"; | ||||
| 
 | ||||
| import State from "../State"; | ||||
| import {ImageSearcher} from "../Logic/Actors/ImageSearcher"; | ||||
| import BaseUIElement from "./BaseUIElement"; | ||||
|  | @ -26,6 +24,9 @@ import BaseLayer from "../Models/BaseLayer"; | |||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
| import ImportButton from "./BigComponents/ImportButton"; | ||||
| 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 { | ||||
|     funcName: string, | ||||
|  | @ -37,14 +38,6 @@ export interface SpecialVisualization { | |||
| 
 | ||||
| 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[] = | ||||
|         [ | ||||
|             { | ||||
|  | @ -153,7 +146,7 @@ export default class SpecialVisualizations { | |||
|                         lon: Number(properties._lon), | ||||
|                         zoom: zoom | ||||
|                     }) | ||||
|                     const minimap = SpecialVisualizations.constructMiniMap( | ||||
|                     const minimap = Minimap.createMiniMap( | ||||
|                         { | ||||
|                             background: state.backgroundLayer, | ||||
|                             location: locationSource, | ||||
|  | @ -169,12 +162,14 @@ export default class SpecialVisualizations { | |||
|                         } | ||||
|                     }) | ||||
| 
 | ||||
|                     SpecialVisualizations.constructShowDataLayer( | ||||
|                         featuresToShow, | ||||
|                         minimap["leafletMap"], | ||||
|                         State.state.layoutToUse, | ||||
|                         false, | ||||
|                         true | ||||
|                    new ShowDataMultiLayer( | ||||
|                         { | ||||
|                             leafletMap: minimap["leafletMap"], | ||||
|                             enablePopups : false, | ||||
|                             zoomToFeatures: 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()); | ||||
|         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)]] | ||||
|     } | ||||
| 
 | ||||
|     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 | ||||
|      */ | ||||
|  | @ -422,13 +441,6 @@ export class Utils { | |||
|         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) { | ||||
|         return (x / Math.pow(2, z) * 360 - 180); | ||||
|     } | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ | |||
|     } | ||||
|   }, | ||||
|   "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)" | ||||
|   ], | ||||
|   "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, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 1, | ||||
|   "roamingRenderings": [], | ||||
|   "layers": [ | ||||
|     "public_bookcase" | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ | |||
|   "defaultBackgroundId": "CartoDB.Voyager", | ||||
|   "startLon": 4.351697, | ||||
|   "startZoom": 16, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 2, | ||||
|   "layers": [ | ||||
|     "drinking_water" | ||||
|   ], | ||||
|  |  | |||
|  | @ -55,6 +55,7 @@ | |||
|     { | ||||
|       "#": "Nature reserve overview from cache, points only, z < 13", | ||||
|       "builtin": "nature_reserve", | ||||
|       "wayHandling": 1, | ||||
|       "override": { | ||||
|         "source": { | ||||
|           "osmTags": { | ||||
|  | @ -63,6 +64,7 @@ | |||
|             ] | ||||
|           }, | ||||
|           "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_nature_reserve_points.geojson", | ||||
|           "geoJsonZoomLevel": 0, | ||||
|           "isOsmCache": "duplicate" | ||||
|         }, | ||||
|         "minzoom": 1, | ||||
|  |  | |||
|  | @ -152,7 +152,7 @@ | |||
|           ] | ||||
|         }, | ||||
|         "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson", | ||||
|         "geoJsonZoomLevel": 14, | ||||
|         "geoJsonZoomLevel": 11, | ||||
|         "isOsmCache": true | ||||
|       }, | ||||
|       "title": { | ||||
|  |  | |||
|  | @ -131,7 +131,7 @@ | |||
|                 ] | ||||
|               }, | ||||
|               "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 Combine from "./UI/Base/Combine"; | ||||
| 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 AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; | ||||
| import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; | ||||
| 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
 | ||||
| SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); | ||||
| DirectionInput.constructMinimap = options => new Minimap(options) | ||||
| 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 = "" | ||||
| // --------------------- Special actions based on the parameters -----------------
 | ||||
|  |  | |||
|  | @ -159,7 +159,7 @@ | |||
|     "noTagsSelected": "No tags selected", | ||||
|     "testing": "Testing - changes won't be saved", | ||||
|     "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", | ||||
|     "openTheMap": "Open the map", | ||||
|     "loginOnlyNeededToEdit": "if you want to edit the map", | ||||
|  |  | |||
|  | @ -3021,6 +3021,29 @@ | |||
|         } | ||||
|     }, | ||||
|     "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", | ||||
|         "presets": { | ||||
|             "0": { | ||||
|  |  | |||
|  | @ -192,7 +192,7 @@ | |||
|       "getStartedNewAccount": " of <a href='https://www.openstreetmap.org/user/new' target='_blank'>maak een nieuwe account aan</a>", | ||||
|       "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.", | ||||
|       "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", | ||||
|       "layerSelection": { | ||||
|         "zoomInToSeeThisLayer": "Vergroot de kaart om deze laag te zien", | ||||
|  |  | |||
|  | @ -1351,6 +1351,11 @@ | |||
|                 "title": { | ||||
|                     "render": "Known address" | ||||
|                 } | ||||
|             }, | ||||
|             "2": { | ||||
|                 "title": { | ||||
|                     "render": "{name}" | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "shortDescription": "Help to build an open dataset of UK addresses", | ||||
|  |  | |||
|  | @ -20,10 +20,10 @@ | |||
|     "reset:translations": "ts-node scripts/generateTranslations.ts --ignore-weblate", | ||||
|     "generate:layouts": "ts-node scripts/generateLayouts.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: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", | ||||
|     "query:licenses": "ts-node scripts/generateLicenseInfo.ts --query", | ||||
|     "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 | ||||
| 
 | ||||
| 
 | ||||
| export default class ScriptUtils { | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,27 +2,32 @@ | |||
|  * Generates a collection of geojson files based on an overpass query for a given theme | ||||
|  */ | ||||
| import {Utils} from "../Utils"; | ||||
| 
 | ||||
| Utils.runningFromConsole = true | ||||
| 
 | ||||
| import {Overpass} from "../Logic/Osm/Overpass"; | ||||
| import * as fs from "fs"; | ||||
| import {existsSync, readFileSync, writeFileSync} from "fs"; | ||||
| import {TagsFilter} from "../Logic/Tags/TagsFilter"; | ||||
| import {Or} from "../Logic/Tags/Or"; | ||||
| import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; | ||||
| import ExtractRelations from "../Logic/Osm/ExtractRelations"; | ||||
| import RelationsTracker from "../Logic/Osm/RelationsTracker"; | ||||
| import * as OsmToGeoJson from "osmtogeojson"; | ||||
| import MetaTagging from "../Logic/MetaTagging"; | ||||
| import {GeoOperations} from "../Logic/GeoOperations"; | ||||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| import {TileRange} from "../Models/TileRange"; | ||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
| 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() | ||||
| 
 | ||||
| 
 | ||||
| function createOverpassObject(theme: LayoutConfig) { | ||||
| function createOverpassObject(theme: LayoutConfig, relationTracker: RelationsTracker) { | ||||
|     let filters: TagsFilter[] = []; | ||||
|     let extraScripts: string[] = []; | ||||
|     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" | ||||
|     } | ||||
|     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 { | ||||
|  | @ -75,7 +80,7 @@ async function downloadRaw(targetdir: string, r: TileRange, overpass: Overpass)/ | |||
|             downloaded++; | ||||
|             const filename = rawJsonName(targetdir, x, y, r.zoomlevel) | ||||
|             if (existsSync(filename)) { | ||||
|                 console.log("Already exists: ", filename) | ||||
|                 console.log("Already exists (not downloading again): ", filename) | ||||
|                 skipped++ | ||||
|                 continue; | ||||
|             } | ||||
|  | @ -145,14 +150,16 @@ async function downloadExtraData(theme: LayoutConfig)/* : any[] */ { | |||
|     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; | ||||
|     const layerIndex = theme.LayerIndex(); | ||||
|     for (let x = r.xstart; x <= r.xend; x++) { | ||||
|         for (let y = r.ystart; y <= r.yend; y++) { | ||||
|             processed++; | ||||
|             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)) { | ||||
|                 console.error("Not found - and not downloaded. Run this script again!: " + filename) | ||||
|                 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
 | ||||
|             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; | ||||
|     const generated = {} // layer --> x --> y[]
 | ||||
|     for (let x = r.xstart; x <= r.xend; x++) { | ||||
|         for (let y = r.ystart; y <= r.yend; y++) { | ||||
|             const file = readFileSync(geoJsonName(targetdir + ".unfiltered", x, y, z), "UTF8") | ||||
| /** | ||||
|  * Load all the tiles into memory from disk | ||||
|  */ | ||||
| function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsTracker: RelationsTracker, targetdir: string) { | ||||
| 
 | ||||
|             for (const layer of theme.layers) { | ||||
|                 if (!layer.source.isOsmCacheLayer) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 const geojson = JSON.parse(file) | ||||
|                 const oldLength = geojson.features.length; | ||||
|                 geojson.features = geojson.features | ||||
|                     .filter(f => f._matching_layer_id === layer.id) | ||||
|                     .filter(f => { | ||||
|                         const isShown = layer.isShown.GetRenderValue(f.properties).txt | ||||
|                         return isShown !== "no"; | ||||
| 
 | ||||
|                     }) | ||||
|                 const new_path = geoJsonName(targetdir + "_" + layer.id, x, y, z); | ||||
|                 ScriptUtils.erasableLog(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length, ")") | ||||
|                 if (geojson.features.length == 0) { | ||||
|                     continue; | ||||
|     function handleLayer(source: FeatureSourceForLayer) { | ||||
|         const layer = source.layer.layerDef; | ||||
|         const layerId = layer.id | ||||
|         if (layer.source.isOsmCacheLayer !== true) { | ||||
|             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) { | ||||
|                     generated[layer.id] = {} | ||||
|         const createdTiles = [] | ||||
|         // At this point, we have all the features of the entire area.
 | ||||
|         // However, we want to export them per tile of a fixed size, so we use a dynamicTileSOurce to split it up
 | ||||
|         TiledFeatureSource.createHierarchy(source, { | ||||
|             minZoomLevel: 14, | ||||
|             maxZoomLevel: 14, | ||||
|             maxFeatureCount: undefined, | ||||
|             registerTile: tile => { | ||||
|                 if (tile.z < 12) { | ||||
|                     return; | ||||
|                 } | ||||
|                 if (generated[layer.id][x] === undefined) { | ||||
|                     generated[layer.id][x] = [] | ||||
|                 if (tile.features.data.length === 0) { | ||||
|                     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, " ")) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     for (const layer of theme.layers) { | ||||
|         const id = layer.id | ||||
|         const loaded = generated[id] | ||||
|         if (loaded === undefined) { | ||||
|             console.log("No features loaded for layer ", id) | ||||
|             continue; | ||||
|         } | ||||
|         writeFileSync(targetdir + "_" + id + "_overview.json", JSON.stringify(loaded)) | ||||
|         }) | ||||
| 
 | ||||
|         // All the tiles are written at this point
 | ||||
|         // Only thing left to do is to create the index
 | ||||
|         const path = targetdir + "_" + layerId + "_overview.json" | ||||
|         const perX = {} | ||||
|         createdTiles.map(i => Utils.tile_from_index(i)).forEach(([z, x, y]) => { | ||||
|             const key = "" + x | ||||
|             if (perX[key] === undefined) { | ||||
|                 perX[key] = [] | ||||
|             } | ||||
|             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[]) { | ||||
| 
 | ||||
|  | @ -335,8 +287,8 @@ async function main(args: string[]) { | |||
|         console.error("The theme " + theme + " was not found; try one of ", keys); | ||||
|         return | ||||
|     } | ||||
| 
 | ||||
|     const overpass = createOverpassObject(theme) | ||||
|     const relationTracker = new RelationsTracker() | ||||
|     const overpass = createOverpassObject(theme, relationTracker) | ||||
| 
 | ||||
|     let failed = 0; | ||||
|     do { | ||||
|  | @ -348,21 +300,13 @@ async function main(args: string[]) { | |||
|     } while (failed > 0) | ||||
| 
 | ||||
|     const extraFeatures = await downloadExtraData(theme); | ||||
|     postProcess(targetdir, tileRange, theme, extraFeatures) | ||||
|     splitPerLayer(targetdir, tileRange, theme) | ||||
|     const allFeaturesSource = loadAllTiles(targetdir, tileRange, theme, extraFeatures) | ||||
|     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] | ||||
| args.splice(0, 2) | ||||
| main(args); | ||||
| console.log("All done!") | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue