forked from MapComplete/MapComplete
		
	refactoring
This commit is contained in:
		
							parent
							
								
									b94a8f5745
								
							
						
					
					
						commit
						5d0fe31c41
					
				
					 114 changed files with 2412 additions and 2958 deletions
				
			
		|  | @ -2,11 +2,12 @@ import { Changes } from "../Osm/Changes" | ||||||
| import Constants from "../../Models/Constants" | import Constants from "../../Models/Constants" | ||||||
| import { UIEventSource } from "../UIEventSource" | import { UIEventSource } from "../UIEventSource" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
|  | import { Feature } from "geojson" | ||||||
| 
 | 
 | ||||||
| export default class PendingChangesUploader { | export default class PendingChangesUploader { | ||||||
|     private lastChange: Date |     private lastChange: Date | ||||||
| 
 | 
 | ||||||
|     constructor(changes: Changes, selectedFeature: UIEventSource<any>) { |     constructor(changes: Changes, selectedFeature: UIEventSource<Feature>) { | ||||||
|         const self = this |         const self = this | ||||||
|         this.lastChange = new Date() |         this.lastChange = new Date() | ||||||
|         changes.pendingChanges.addCallback(() => { |         changes.pendingChanges.addCallback(() => { | ||||||
|  |  | ||||||
|  | @ -2,12 +2,19 @@ import { Store, UIEventSource } from "../UIEventSource" | ||||||
| import Locale from "../../UI/i18n/Locale" | import Locale from "../../UI/i18n/Locale" | ||||||
| import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer" | import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer" | ||||||
| import Combine from "../../UI/Base/Combine" | import Combine from "../../UI/Base/Combine" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" |  | ||||||
| import { ElementStorage } from "../ElementStorage" |  | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
|  | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
|  | import { Feature } from "geojson" | ||||||
|  | import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" | ||||||
|  | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| 
 | 
 | ||||||
| export default class TitleHandler { | export default class TitleHandler { | ||||||
|     constructor(selectedElement: Store<any>, layout: LayoutConfig, allElements: ElementStorage) { |     constructor( | ||||||
|  |         selectedElement: Store<Feature>, | ||||||
|  |         selectedLayer: Store<LayerConfig>, | ||||||
|  |         allElements: FeaturePropertiesStore, | ||||||
|  |         layout: LayoutConfig | ||||||
|  |     ) { | ||||||
|         const currentTitle: Store<string> = selectedElement.map( |         const currentTitle: Store<string> = selectedElement.map( | ||||||
|             (selected) => { |             (selected) => { | ||||||
|                 const defaultTitle = layout?.title?.txt ?? "MapComplete" |                 const defaultTitle = layout?.title?.txt ?? "MapComplete" | ||||||
|  | @ -17,13 +24,14 @@ export default class TitleHandler { | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 const tags = selected.properties |                 const tags = selected.properties | ||||||
|                 for (const layer of layout.layers) { |                 for (const layer of layout?.layers ?? []) { | ||||||
|                     if (layer.title === undefined) { |                     if (layer.title === undefined) { | ||||||
|                         continue |                         continue | ||||||
|                     } |                     } | ||||||
|                     if (layer.source.osmTags.matchesProperties(tags)) { |                     if (layer.source.osmTags.matchesProperties(tags)) { | ||||||
|                         const tagsSource = |                         const tagsSource = | ||||||
|                             allElements.getEventSourceById(tags.id) ?? new UIEventSource<any>(tags) |                             allElements.getStore(tags.id) ?? | ||||||
|  |                             new UIEventSource<Record<string, string>>(tags) | ||||||
|                         const title = new TagRenderingAnswer(tagsSource, layer.title, {}) |                         const title = new TagRenderingAnswer(tagsSource, layer.title, {}) | ||||||
|                         return ( |                         return ( | ||||||
|                             new Combine([defaultTitle, " | ", title]).ConstructElement() |                             new Combine([defaultTitle, " | ", title]).ConstructElement() | ||||||
|  | @ -33,7 +41,7 @@ export default class TitleHandler { | ||||||
|                 } |                 } | ||||||
|                 return defaultTitle |                 return defaultTitle | ||||||
|             }, |             }, | ||||||
|             [Locale.language] |             [Locale.language, selectedLayer] | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         currentTitle.addCallbackAndRunD((title) => { |         currentTitle.addCallbackAndRunD((title) => { | ||||||
|  |  | ||||||
|  | @ -1,39 +1,31 @@ | ||||||
| /// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions
 | /// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions
 | ||||||
| import { Store, UIEventSource } from "./UIEventSource" | import { Store, UIEventSource } from "./UIEventSource" | ||||||
| import FeaturePipeline from "./FeatureSource/FeaturePipeline" |  | ||||||
| import Loc from "../Models/Loc" |  | ||||||
| import { BBox } from "./BBox" | import { BBox } from "./BBox" | ||||||
|  | import GeoIndexedStore from "./FeatureSource/Actors/GeoIndexedStore" | ||||||
| 
 | 
 | ||||||
| export default class ContributorCount { | export default class ContributorCount { | ||||||
|     public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource< |     public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource< | ||||||
|         Map<string, number> |         Map<string, number> | ||||||
|     >(new Map<string, number>()) |     >(new Map<string, number>()) | ||||||
|     private readonly state: { |     private readonly perLayer: ReadonlyMap<string, GeoIndexedStore> | ||||||
|         featurePipeline: FeaturePipeline |  | ||||||
|         currentBounds: Store<BBox> |  | ||||||
|         locationControl: Store<Loc> |  | ||||||
|     } |  | ||||||
|     private lastUpdate: Date = undefined |     private lastUpdate: Date = undefined | ||||||
| 
 | 
 | ||||||
|     constructor(state: { |     constructor(state: { | ||||||
|         featurePipeline: FeaturePipeline |         bounds: Store<BBox> | ||||||
|         currentBounds: Store<BBox> |         dataIsLoading: Store<boolean> | ||||||
|         locationControl: Store<Loc> |         perLayer: ReadonlyMap<string, GeoIndexedStore> | ||||||
|     }) { |     }) { | ||||||
|         this.state = state |         this.perLayer = state.perLayer | ||||||
|         const self = this |         const self = this | ||||||
|         state.currentBounds.map((bbox) => { |         state.bounds.mapD( | ||||||
|             self.update(bbox) |             (bbox) => { | ||||||
|         }) |                 self.update(bbox) | ||||||
|         state.featurePipeline.runningQuery.addCallbackAndRun((_) => |             }, | ||||||
|             self.update(state.currentBounds.data) |             [state.dataIsLoading] | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private update(bbox: BBox) { |     private update(bbox: BBox) { | ||||||
|         if (bbox === undefined) { |  | ||||||
|             return |  | ||||||
|         } |  | ||||||
|         const now = new Date() |         const now = new Date() | ||||||
|         if ( |         if ( | ||||||
|             this.lastUpdate !== undefined && |             this.lastUpdate !== undefined && | ||||||
|  | @ -42,7 +34,9 @@ export default class ContributorCount { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         this.lastUpdate = now |         this.lastUpdate = now | ||||||
|         const featuresList = this.state.featurePipeline.GetAllFeaturesWithin(bbox) |         const featuresList = [].concat( | ||||||
|  |             Array.from(this.perLayer.values()).map((fs) => fs.GetFeaturesWithin(bbox)) | ||||||
|  |         ) | ||||||
|         const hist = new Map<string, number>() |         const hist = new Map<string, number>() | ||||||
|         for (const list of featuresList) { |         for (const list of featuresList) { | ||||||
|             for (const feature of list) { |             for (const feature of list) { | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| import { GeoOperations } from "./GeoOperations" | import { GeoOperations } from "./GeoOperations" | ||||||
| import Combine from "../UI/Base/Combine" | import Combine from "../UI/Base/Combine" | ||||||
| import RelationsTracker from "./Osm/RelationsTracker" |  | ||||||
| import BaseUIElement from "../UI/BaseUIElement" | import BaseUIElement from "../UI/BaseUIElement" | ||||||
| import List from "../UI/Base/List" | import List from "../UI/Base/List" | ||||||
| import Title from "../UI/Base/Title" | import Title from "../UI/Base/Title" | ||||||
|  |  | ||||||
							
								
								
									
										41
									
								
								Logic/FeatureSource/Actors/GeoIndexedStore.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								Logic/FeatureSource/Actors/GeoIndexedStore.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | ||||||
|  | import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource" | ||||||
|  | import { Feature } from "geojson" | ||||||
|  | import { BBox } from "../../BBox" | ||||||
|  | import { GeoOperations } from "../../GeoOperations" | ||||||
|  | import { Store } from "../../UIEventSource" | ||||||
|  | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Allows the retrieval of all features in the requested BBox; useful for one-shot queries; | ||||||
|  |  * | ||||||
|  |  * Use a ClippedFeatureSource for a continuously updating featuresource | ||||||
|  |  */ | ||||||
|  | export default class GeoIndexedStore implements FeatureSource { | ||||||
|  |     public features: Store<Feature[]> | ||||||
|  | 
 | ||||||
|  |     constructor(features: FeatureSource | Store<Feature[]>) { | ||||||
|  |         this.features = features["features"] ?? features | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets the current features within the given bbox. | ||||||
|  |      * | ||||||
|  |      * @param bbox | ||||||
|  |      * @constructor | ||||||
|  |      */ | ||||||
|  |     public GetFeaturesWithin(bbox: BBox): Feature[] { | ||||||
|  |         // TODO optimize
 | ||||||
|  |         const bboxFeature = bbox.asGeoJson({}) | ||||||
|  |         return this.features.data.filter( | ||||||
|  |             (f) => GeoOperations.intersect(f, bboxFeature) !== undefined | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class GeoIndexedStoreForLayer extends GeoIndexedStore implements FeatureSourceForLayer { | ||||||
|  |     readonly layer: FilteredLayer | ||||||
|  |     constructor(features: FeatureSource | Store<Feature[]>, layer: FilteredLayer) { | ||||||
|  |         super(features) | ||||||
|  |         this.layer = layer | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,11 +1,10 @@ | ||||||
| import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||||
| import MetaTagging from "../../MetaTagging" | import MetaTagging from "../../MetaTagging" | ||||||
| import { ExtraFuncParams } from "../../ExtraFunctions" | import { ExtraFuncParams } from "../../ExtraFunctions" | ||||||
| import FeaturePipeline from "../FeaturePipeline" |  | ||||||
| import { BBox } from "../../BBox" | import { BBox } from "../../BBox" | ||||||
| import { UIEventSource } from "../../UIEventSource" | import { UIEventSource } from "../../UIEventSource" | ||||||
| 
 | 
 | ||||||
| /**** | /** | ||||||
|  * Concerned with the logic of updating the right layer at the right time |  * Concerned with the logic of updating the right layer at the right time | ||||||
|  */ |  */ | ||||||
| class MetatagUpdater { | class MetatagUpdater { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,36 @@ | ||||||
|  | import FeatureSource, { Tiled } from "../FeatureSource" | ||||||
|  | import { Tiles } from "../../../Models/TileRange" | ||||||
|  | import { IdbLocalStorage } from "../../Web/IdbLocalStorage" | ||||||
|  | import { UIEventSource } from "../../UIEventSource" | ||||||
|  | import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||||
|  | import { BBox } from "../../BBox" | ||||||
|  | import SimpleFeatureSource from "../Sources/SimpleFeatureSource" | ||||||
|  | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
|  | import Loc from "../../../Models/Loc" | ||||||
|  | import { Feature } from "geojson" | ||||||
|  | import TileLocalStorage from "./TileLocalStorage" | ||||||
|  | import { GeoOperations } from "../../GeoOperations" | ||||||
|  | import { Utils } from "../../../Utils" | ||||||
|  | 
 | ||||||
|  | /*** | ||||||
|  |  * Saves all the features that are passed in to localstorage, so they can be retrieved on the next run | ||||||
|  |  * | ||||||
|  |  * The data is saved in a tiled way on a fixed zoomlevel and is retrievable per layer. | ||||||
|  |  * | ||||||
|  |  * Also see the sibling class | ||||||
|  |  */ | ||||||
|  | export default class SaveFeatureSourceToLocalStorage { | ||||||
|  |     constructor(layername: string, zoomlevel: number, features: FeatureSource) { | ||||||
|  |         const storage = TileLocalStorage.construct<Feature[]>(layername) | ||||||
|  |         features.features.addCallbackAndRunD((features) => { | ||||||
|  |             const sliced = GeoOperations.slice(zoomlevel, features) | ||||||
|  |             sliced.forEach((features, tileIndex) => { | ||||||
|  |                 const src = storage.getTileSource(tileIndex) | ||||||
|  |                 if (Utils.sameList(src.data, features)) { | ||||||
|  |                     return | ||||||
|  |                 } | ||||||
|  |                 src.setData(features) | ||||||
|  |             }) | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,149 +0,0 @@ | ||||||
| import FeatureSource, { Tiled } from "../FeatureSource" |  | ||||||
| import { Tiles } from "../../../Models/TileRange" |  | ||||||
| import { IdbLocalStorage } from "../../Web/IdbLocalStorage" |  | ||||||
| import { UIEventSource } from "../../UIEventSource" |  | ||||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" |  | ||||||
| import { BBox } from "../../BBox" |  | ||||||
| import SimpleFeatureSource from "../Sources/SimpleFeatureSource" |  | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer" |  | ||||||
| import Loc from "../../../Models/Loc" |  | ||||||
| import { Feature } from "geojson" |  | ||||||
| 
 |  | ||||||
| /*** |  | ||||||
|  * 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 way |  | ||||||
|  */ |  | ||||||
| export default class SaveTileToLocalStorageActor { |  | ||||||
|     private readonly visitedTiles: UIEventSource<Map<number, Date>> |  | ||||||
|     private readonly _layer: LayerConfig |  | ||||||
|     private readonly _flayer: FilteredLayer |  | ||||||
|     private readonly initializeTime = new Date() |  | ||||||
| 
 |  | ||||||
|     constructor(layer: FilteredLayer) { |  | ||||||
|         this._flayer = layer |  | ||||||
|         this._layer = layer.layerDef |  | ||||||
|         this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id, { |  | ||||||
|             defaultValue: new Map<number, Date>(), |  | ||||||
|         }) |  | ||||||
|         this.visitedTiles.stabilized(100).addCallbackAndRunD((tiles) => { |  | ||||||
|             for (const key of Array.from(tiles.keys())) { |  | ||||||
|                 const tileFreshness = tiles.get(key) |  | ||||||
| 
 |  | ||||||
|                 const toOld = |  | ||||||
|                     this.initializeTime.getTime() - tileFreshness.getTime() > |  | ||||||
|                     1000 * this._layer.maxAgeOfCache |  | ||||||
|                 if (toOld) { |  | ||||||
|                     // Purge this tile
 |  | ||||||
|                     this.SetIdb(key, undefined) |  | ||||||
|                     console.debug("Purging tile", this._layer.id, key) |  | ||||||
|                     tiles.delete(key) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             this.visitedTiles.ping() |  | ||||||
|             return true |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public LoadTilesFromDisk( |  | ||||||
|         currentBounds: UIEventSource<BBox>, |  | ||||||
|         location: UIEventSource<Loc>, |  | ||||||
|         registerFreshness: (tileId: number, freshness: Date) => void, |  | ||||||
|         registerTile: (src: FeatureSource & Tiled) => void |  | ||||||
|     ) { |  | ||||||
|         const self = this |  | ||||||
|         const loadedTiles = new Set<number>() |  | ||||||
|         this.visitedTiles.addCallbackD((tiles) => { |  | ||||||
|             if (tiles.size === 0) { |  | ||||||
|                 // We don't do anything yet as probably not yet loaded from disk
 |  | ||||||
|                 // We'll unregister later on
 |  | ||||||
|                 return |  | ||||||
|             } |  | ||||||
|             currentBounds.addCallbackAndRunD((bbox) => { |  | ||||||
|                 if (self._layer.minzoomVisible > location.data.zoom) { |  | ||||||
|                     // Not enough zoom
 |  | ||||||
|                     return |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 // Iterate over all available keys in the local storage, check which are needed and fresh enough
 |  | ||||||
|                 for (const key of Array.from(tiles.keys())) { |  | ||||||
|                     const tileFreshness = tiles.get(key) |  | ||||||
|                     if (tileFreshness > self.initializeTime) { |  | ||||||
|                         // This tile is loaded by another source
 |  | ||||||
|                         continue |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     registerFreshness(key, tileFreshness) |  | ||||||
|                     const tileBbox = BBox.fromTileIndex(key) |  | ||||||
|                     if (!bbox.overlapsWith(tileBbox)) { |  | ||||||
|                         continue |  | ||||||
|                     } |  | ||||||
|                     if (loadedTiles.has(key)) { |  | ||||||
|                         // Already loaded earlier
 |  | ||||||
|                         continue |  | ||||||
|                     } |  | ||||||
|                     loadedTiles.add(key) |  | ||||||
|                     this.GetIdb(key).then((features: Feature[]) => { |  | ||||||
|                         if (features === undefined) { |  | ||||||
|                             return |  | ||||||
|                         } |  | ||||||
|                         console.debug("Loaded tile " + self._layer.id + "_" + key + " from disk") |  | ||||||
|                         const src = new SimpleFeatureSource( |  | ||||||
|                             self._flayer, |  | ||||||
|                             key, |  | ||||||
|                             new UIEventSource<Feature[]>(features) |  | ||||||
|                         ) |  | ||||||
|                         registerTile(src) |  | ||||||
|                     }) |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
| 
 |  | ||||||
|             return true // Remove the callback
 |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public addTile(tile: FeatureSource & Tiled) { |  | ||||||
|         const self = this |  | ||||||
|         tile.features.addCallbackAndRunD((features) => { |  | ||||||
|             const now = new Date() |  | ||||||
| 
 |  | ||||||
|             if (features.length > 0) { |  | ||||||
|                 self.SetIdb(tile.tileIndex, features) |  | ||||||
|             } |  | ||||||
|             // We _still_ write the time to know that this tile is empty!
 |  | ||||||
|             this.MarkVisited(tile.tileIndex, now) |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public poison(lon: number, lat: number) { |  | ||||||
|         for (let z = 0; z < 25; z++) { |  | ||||||
|             const { x, y } = Tiles.embedded_tile(lat, lon, z) |  | ||||||
|             const tileId = Tiles.tile_index(z, x, y) |  | ||||||
|             this.visitedTiles.data.delete(tileId) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public MarkVisited(tileId: number, freshness: Date) { |  | ||||||
|         this.visitedTiles.data.set(tileId, freshness) |  | ||||||
|         this.visitedTiles.ping() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private SetIdb(tileIndex, data) { |  | ||||||
|         try { |  | ||||||
|             IdbLocalStorage.SetDirectly(this._layer.id + "_" + tileIndex, data) |  | ||||||
|         } catch (e) { |  | ||||||
|             console.error( |  | ||||||
|                 "Could not save tile to indexed-db: ", |  | ||||||
|                 e, |  | ||||||
|                 "tileIndex is:", |  | ||||||
|                 tileIndex, |  | ||||||
|                 "for layer", |  | ||||||
|                 this._layer.id |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private GetIdb(tileIndex) { |  | ||||||
|         return IdbLocalStorage.GetDirectly(this._layer.id + "_" + tileIndex) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										63
									
								
								Logic/FeatureSource/Actors/TileLocalStorage.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								Logic/FeatureSource/Actors/TileLocalStorage.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | ||||||
|  | import { IdbLocalStorage } from "../../Web/IdbLocalStorage" | ||||||
|  | import { UIEventSource } from "../../UIEventSource" | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A class which allows to read/write a tile to local storage. | ||||||
|  |  * | ||||||
|  |  * Does the heavy lifting for LocalStorageFeatureSource and SaveFeatureToLocalStorage | ||||||
|  |  */ | ||||||
|  | export default class TileLocalStorage<T> { | ||||||
|  |     private static perLayer: Record<string, TileLocalStorage<any>> = {} | ||||||
|  |     private readonly _layername: string | ||||||
|  |     private readonly cachedSources: Record<number, UIEventSource<T>> = {} | ||||||
|  | 
 | ||||||
|  |     private constructor(layername: string) { | ||||||
|  |         this._layername = layername | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static construct<T>(layername: string): TileLocalStorage<T> { | ||||||
|  |         const cached = TileLocalStorage.perLayer[layername] | ||||||
|  |         if (cached) { | ||||||
|  |             return cached | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const tls = new TileLocalStorage<T>(layername) | ||||||
|  |         TileLocalStorage.perLayer[layername] = tls | ||||||
|  |         return tls | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Constructs a UIEventSource element which is synced with localStorage | ||||||
|  |      * @param layername | ||||||
|  |      * @param tileIndex | ||||||
|  |      */ | ||||||
|  |     public getTileSource(tileIndex: number): UIEventSource<T> { | ||||||
|  |         const cached = this.cachedSources[tileIndex] | ||||||
|  |         if (cached) { | ||||||
|  |             return cached | ||||||
|  |         } | ||||||
|  |         const src = UIEventSource.FromPromise(this.GetIdb(tileIndex)) | ||||||
|  |         src.addCallbackD((data) => this.SetIdb(tileIndex, data)) | ||||||
|  |         this.cachedSources[tileIndex] = src | ||||||
|  |         return src | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private SetIdb(tileIndex: number, data): void { | ||||||
|  |         try { | ||||||
|  |             IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data) | ||||||
|  |         } catch (e) { | ||||||
|  |             console.error( | ||||||
|  |                 "Could not save tile to indexed-db: ", | ||||||
|  |                 e, | ||||||
|  |                 "tileIndex is:", | ||||||
|  |                 tileIndex, | ||||||
|  |                 "for layer", | ||||||
|  |                 this._layername | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private GetIdb(tileIndex: number): Promise<any> { | ||||||
|  |         return IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,581 +0,0 @@ | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" |  | ||||||
| import FilteringFeatureSource from "./Sources/FilteringFeatureSource" |  | ||||||
| import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter" |  | ||||||
| import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "./FeatureSource" |  | ||||||
| import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource" |  | ||||||
| import { Store, UIEventSource } from "../UIEventSource" |  | ||||||
| import { TileHierarchyTools } from "./TiledFeatureSource/TileHierarchy" |  | ||||||
| import RememberingSource from "./Sources/RememberingSource" |  | ||||||
| import OverpassFeatureSource from "../Actors/OverpassFeatureSource" |  | ||||||
| import GeoJsonSource from "./Sources/GeoJsonSource" |  | ||||||
| import Loc from "../../Models/Loc" |  | ||||||
| import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor" |  | ||||||
| import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor" |  | ||||||
| import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource" |  | ||||||
| import { TileHierarchyMerger } from "./TiledFeatureSource/TileHierarchyMerger" |  | ||||||
| import { NewGeometryFromChangesFeatureSource } from "./Sources/NewGeometryFromChangesFeatureSource" |  | ||||||
| import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator" |  | ||||||
| /** |  | ||||||
|  * Keeps track of the age of the loaded data. |  | ||||||
|  * Has one freshness-Calculator for every layer |  | ||||||
|  * @private |  | ||||||
|  */ |  | ||||||
| import { BBox } from "../BBox" |  | ||||||
| import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource" |  | ||||||
| import { Tiles } from "../../Models/TileRange" |  | ||||||
| import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource" |  | ||||||
| import MapState from "../State/MapState" |  | ||||||
| import { OsmFeature } from "../../Models/OsmFeature" |  | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" |  | ||||||
| import { FilterState } from "../../Models/FilteredLayer" |  | ||||||
| import { GeoOperations } from "../GeoOperations" |  | ||||||
| import { Utils } from "../../Utils" |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * The features pipeline ties together a myriad of various datasources: |  | ||||||
|  * |  | ||||||
|  * - The Overpass-API |  | ||||||
|  * - The OSM-API |  | ||||||
|  * - Third-party geojson files, either sliced or directly. |  | ||||||
|  * |  | ||||||
|  * In order to truly understand this class, please have a look at the following diagram: https://cdn-images-1.medium.com/fit/c/800/618/1*qTK1iCtyJUr4zOyw4IFD7A.jpeg
 |  | ||||||
|  * |  | ||||||
|  * |  | ||||||
|  */ |  | ||||||
| export default class FeaturePipeline { |  | ||||||
|     public readonly sufficientlyZoomed: Store<boolean> |  | ||||||
|     public readonly runningQuery: Store<boolean> |  | ||||||
|     public readonly timeout: UIEventSource<number> |  | ||||||
|     public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false) |  | ||||||
|     public readonly newDataLoadedSignal: UIEventSource<FeatureSource> = |  | ||||||
|         new UIEventSource<FeatureSource>(undefined) |  | ||||||
|     /** |  | ||||||
|      * Keeps track of all raw OSM-nodes. |  | ||||||
|      * Only initialized if `ReplaceGeometryAction` is needed somewhere |  | ||||||
|      */ |  | ||||||
|     public readonly fullNodeDatabase?: FullNodeDatabaseSource |  | ||||||
|     private readonly overpassUpdater: OverpassFeatureSource |  | ||||||
|     private state: MapState |  | ||||||
|     private readonly perLayerHierarchy: Map<string, TileHierarchyMerger> |  | ||||||
|     private readonly oldestAllowedDate: Date |  | ||||||
|     private readonly osmSourceZoomLevel |  | ||||||
|     private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>() |  | ||||||
| 
 |  | ||||||
|     private readonly newGeometryHandler: NewGeometryFromChangesFeatureSource |  | ||||||
| 
 |  | ||||||
|     constructor( |  | ||||||
|         handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void, |  | ||||||
|         state: MapState, |  | ||||||
|         options?: { |  | ||||||
|             /*Used for metatagging - will receive all the sources with changeGeometry applied but without filtering*/ |  | ||||||
|             handleRawFeatureSource: (source: FeatureSourceForLayer) => void |  | ||||||
|         } |  | ||||||
|     ) { |  | ||||||
|         this.state = state |  | ||||||
| 
 |  | ||||||
|         const self = this |  | ||||||
|         const expiryInSeconds = Math.min( |  | ||||||
|             ...(state.layoutToUse?.layers?.map((l) => l.maxAgeOfCache) ?? []) |  | ||||||
|         ) |  | ||||||
|         this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds) |  | ||||||
|         this.osmSourceZoomLevel = state.osmApiTileSize.data |  | ||||||
|         const useOsmApi = state.locationControl.map( |  | ||||||
|             (l) => l.zoom > (state.overpassMaxZoom.data ?? 12) |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         state.changes.allChanges.addCallbackAndRun((allChanges) => { |  | ||||||
|             allChanges |  | ||||||
|                 .filter((ch) => ch.id < 0 && ch.changes !== undefined) |  | ||||||
|                 .map((ch) => ch.changes) |  | ||||||
|                 .filter((coor) => coor["lat"] !== undefined && coor["lon"] !== undefined) |  | ||||||
|                 .forEach((coor) => { |  | ||||||
|                     state.layoutToUse.layers.forEach((l) => |  | ||||||
|                         self.localStorageSavers.get(l.id)?.poison(coor["lon"], coor["lat"]) |  | ||||||
|                     ) |  | ||||||
|                 }) |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         this.sufficientlyZoomed = state.locationControl.map((location) => { |  | ||||||
|             if (location?.zoom === undefined) { |  | ||||||
|                 return false |  | ||||||
|             } |  | ||||||
|             let minzoom = Math.min( |  | ||||||
|                 ...state.filteredLayers.data.map((layer) => layer.layerDef.minzoom ?? 18) |  | ||||||
|             ) |  | ||||||
|             return location.zoom >= minzoom |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed) |  | ||||||
| 
 |  | ||||||
|         const perLayerHierarchy = new Map<string, TileHierarchyMerger>() |  | ||||||
|         this.perLayerHierarchy = perLayerHierarchy |  | ||||||
| 
 |  | ||||||
|         // Given a tile, wraps it and passes it on to render (handled by 'handleFeatureSource'
 |  | ||||||
|         function patchedHandleFeatureSource( |  | ||||||
|             src: FeatureSourceForLayer & IndexedFeatureSource & Tiled |  | ||||||
|         ) { |  | ||||||
|             // This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
 |  | ||||||
|             const withChanges = new ChangeGeometryApplicator(src, state.changes) |  | ||||||
|             const srcFiltered = new FilteringFeatureSource(state, src.tileIndex, withChanges) |  | ||||||
| 
 |  | ||||||
|             handleFeatureSource(srcFiltered) |  | ||||||
|             if (options?.handleRawFeatureSource) { |  | ||||||
|                 options.handleRawFeatureSource(withChanges) |  | ||||||
|             } |  | ||||||
|             self.somethingLoaded.setData(true) |  | ||||||
|             // We do not mark as visited here, this is the responsability of the code near the actual loader (e.g. overpassLoader and OSMApiFeatureLoader)
 |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (const filteredLayer of state.filteredLayers.data) { |  | ||||||
|             const id = filteredLayer.layerDef.id |  | ||||||
|             const source = filteredLayer.layerDef.source |  | ||||||
| 
 |  | ||||||
|             const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) => |  | ||||||
|                 patchedHandleFeatureSource(tile) |  | ||||||
|             ) |  | ||||||
|             perLayerHierarchy.set(id, hierarchy) |  | ||||||
| 
 |  | ||||||
|             if (id === "type_node") { |  | ||||||
|                 this.fullNodeDatabase = new FullNodeDatabaseSource(filteredLayer, (tile) => { |  | ||||||
|                     perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) |  | ||||||
|                     tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) |  | ||||||
|                 }) |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             const localTileSaver = new SaveTileToLocalStorageActor(filteredLayer) |  | ||||||
|             this.localStorageSavers.set(filteredLayer.layerDef.id, localTileSaver) |  | ||||||
| 
 |  | ||||||
|             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
 |  | ||||||
|                 localTileSaver.LoadTilesFromDisk( |  | ||||||
|                     state.currentBounds, |  | ||||||
|                     state.locationControl, |  | ||||||
|                     (tileIndex, freshness) => |  | ||||||
|                         self.freshnesses.get(id).addTileLoad(tileIndex, freshness), |  | ||||||
|                     (tile) => { |  | ||||||
|                         console.debug("Loaded tile ", id, tile.tileIndex, "from local cache") |  | ||||||
|                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |  | ||||||
|                         hierarchy.registerTile(tile) |  | ||||||
|                         tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) |  | ||||||
|                     } |  | ||||||
|                 ) |  | ||||||
| 
 |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (source.geojsonZoomLevel === undefined) { |  | ||||||
|                 // This is a 'load everything at once' geojson layer
 |  | ||||||
|                 const src = new GeoJsonSource(filteredLayer) |  | ||||||
| 
 |  | ||||||
|                 if (source.isOsmCacheLayer) { |  | ||||||
|                     // We split them up into tiles anyway as it is an OSM source
 |  | ||||||
|                     TiledFeatureSource.createHierarchy(src, { |  | ||||||
|                         layer: src.layer, |  | ||||||
|                         minZoomLevel: this.osmSourceZoomLevel, |  | ||||||
|                         noDuplicates: true, |  | ||||||
|                         registerTile: (tile) => { |  | ||||||
|                             new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |  | ||||||
|                             perLayerHierarchy.get(id).registerTile(tile) |  | ||||||
|                             tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) |  | ||||||
|                         }, |  | ||||||
|                     }) |  | ||||||
|                 } else { |  | ||||||
|                     new RegisteringAllFromFeatureSourceActor(src, state.allElements) |  | ||||||
|                     perLayerHierarchy.get(id).registerTile(src) |  | ||||||
|                     src.features.addCallbackAndRunD((_) => self.onNewDataLoaded(src)) |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 new DynamicGeoJsonTileSource( |  | ||||||
|                     filteredLayer, |  | ||||||
|                     (tile) => { |  | ||||||
|                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |  | ||||||
|                         perLayerHierarchy.get(id).registerTile(tile) |  | ||||||
|                         tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) |  | ||||||
|                     }, |  | ||||||
|                     state |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const osmFeatureSource = new OsmFeatureSource({ |  | ||||||
|             isActive: useOsmApi, |  | ||||||
|             neededTiles: neededTilesFromOsm, |  | ||||||
|             handleTile: (tile) => { |  | ||||||
|                 new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |  | ||||||
|                 if (tile.layer.layerDef.maxAgeOfCache > 0) { |  | ||||||
|                     const saver = self.localStorageSavers.get(tile.layer.layerDef.id) |  | ||||||
|                     if (saver === undefined) { |  | ||||||
|                         console.error( |  | ||||||
|                             "No localStorageSaver found for layer ", |  | ||||||
|                             tile.layer.layerDef.id |  | ||||||
|                         ) |  | ||||||
|                     } |  | ||||||
|                     saver?.addTile(tile) |  | ||||||
|                 } |  | ||||||
|                 perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) |  | ||||||
|                 tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) |  | ||||||
|             }, |  | ||||||
|             state: state, |  | ||||||
|             markTileVisited: (tileId) => |  | ||||||
|                 state.filteredLayers.data.forEach((flayer) => { |  | ||||||
|                     const layer = flayer.layerDef |  | ||||||
|                     if (layer.maxAgeOfCache > 0) { |  | ||||||
|                         const saver = self.localStorageSavers.get(layer.id) |  | ||||||
|                         if (saver === undefined) { |  | ||||||
|                             console.error("No local storage saver found for ", layer.id) |  | ||||||
|                         } else { |  | ||||||
|                             saver.MarkVisited(tileId, new Date()) |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                     self.freshnesses.get(layer.id).addTileLoad(tileId, new Date()) |  | ||||||
|                 }), |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         if (this.fullNodeDatabase !== undefined) { |  | ||||||
|             osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => |  | ||||||
|                 this.fullNodeDatabase.handleOsmJson(osmJson, tileId) |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const updater = this.initOverpassUpdater(state, useOsmApi) |  | ||||||
|         this.overpassUpdater = updater |  | ||||||
|         this.timeout = updater.timeout |  | ||||||
| 
 |  | ||||||
|         // Actually load data from the overpass source
 |  | ||||||
|         new PerLayerFeatureSourceSplitter( |  | ||||||
|             state.filteredLayers, |  | ||||||
|             (source) => |  | ||||||
|                 TiledFeatureSource.createHierarchy(source, { |  | ||||||
|                     layer: source.layer, |  | ||||||
|                     minZoomLevel: source.layer.layerDef.minzoom, |  | ||||||
|                     noDuplicates: true, |  | ||||||
|                     maxFeatureCount: state.layoutToUse.clustering.minNeededElements, |  | ||||||
|                     maxZoomLevel: state.layoutToUse.clustering.maxZoom, |  | ||||||
|                     registerTile: (tile) => { |  | ||||||
|                         // We save the tile data for the given layer to local storage - data sourced from overpass
 |  | ||||||
|                         self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile) |  | ||||||
|                         perLayerHierarchy |  | ||||||
|                             .get(source.layer.layerDef.id) |  | ||||||
|                             .registerTile(new RememberingSource(tile)) |  | ||||||
|                         tile.features.addCallbackAndRunD((f) => { |  | ||||||
|                             if (f.length === 0) { |  | ||||||
|                                 return |  | ||||||
|                             } |  | ||||||
|                             self.onNewDataLoaded(tile) |  | ||||||
|                         }) |  | ||||||
|                     }, |  | ||||||
|                 }), |  | ||||||
|             updater, |  | ||||||
|             { |  | ||||||
|                 handleLeftovers: (leftOvers) => { |  | ||||||
|                     console.warn("Overpass returned a few non-matched features:", leftOvers) |  | ||||||
|                 }, |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         // Also load points/lines that are newly added.
 |  | ||||||
|         const newGeometry = new NewGeometryFromChangesFeatureSource( |  | ||||||
|             state.changes, |  | ||||||
|             state.allElements, |  | ||||||
|             state.osmConnection._oauth_config.url |  | ||||||
|         ) |  | ||||||
|         this.newGeometryHandler = newGeometry |  | ||||||
|         newGeometry.features.addCallbackAndRun((geometries) => { |  | ||||||
|             console.debug("New geometries are:", geometries) |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         new RegisteringAllFromFeatureSourceActor(newGeometry, state.allElements) |  | ||||||
|         // A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next
 |  | ||||||
|         new PerLayerFeatureSourceSplitter( |  | ||||||
|             state.filteredLayers, |  | ||||||
|             (perLayer) => { |  | ||||||
|                 // We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
 |  | ||||||
|                 perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer) |  | ||||||
|                 // AT last, we always apply the metatags whenever possible
 |  | ||||||
|                 perLayer.features.addCallbackAndRunD((_) => { |  | ||||||
|                     self.onNewDataLoaded(perLayer) |  | ||||||
|                 }) |  | ||||||
|             }, |  | ||||||
|             newGeometry, |  | ||||||
|             { |  | ||||||
|                 handleLeftovers: (leftOvers) => { |  | ||||||
|                     console.warn("Got some leftovers from the filteredLayers: ", leftOvers) |  | ||||||
|                 }, |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         this.runningQuery = updater.runningQuery.map( |  | ||||||
|             (overpass) => { |  | ||||||
|                 console.log( |  | ||||||
|                     "FeaturePipeline: runningQuery state changed: Overpass", |  | ||||||
|                     overpass ? "is querying," : "is idle,", |  | ||||||
|                     "osmFeatureSource is", |  | ||||||
|                     osmFeatureSource.isRunning |  | ||||||
|                         ? "is running and needs " + |  | ||||||
|                               neededTilesFromOsm.data?.length + |  | ||||||
|                               " tiles (already got " + |  | ||||||
|                               osmFeatureSource.downloadedTiles.size + |  | ||||||
|                               " tiles )" |  | ||||||
|                         : "is idle" |  | ||||||
|                 ) |  | ||||||
|                 return overpass || osmFeatureSource.isRunning.data |  | ||||||
|             }, |  | ||||||
|             [osmFeatureSource.isRunning] |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public GetAllFeaturesWithin(bbox: BBox): OsmFeature[][] { |  | ||||||
|         const self = this |  | ||||||
|         const tiles: OsmFeature[][] = [] |  | ||||||
|         Array.from(this.perLayerHierarchy.keys()).forEach((key) => { |  | ||||||
|             const fetched: OsmFeature[][] = self.GetFeaturesWithin(key, bbox) |  | ||||||
|             tiles.push(...fetched) |  | ||||||
|         }) |  | ||||||
|         return tiles |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public GetAllFeaturesAndMetaWithin( |  | ||||||
|         bbox: BBox, |  | ||||||
|         layerIdWhitelist?: Set<string> |  | ||||||
|     ): { features: OsmFeature[]; layer: string }[] { |  | ||||||
|         const self = this |  | ||||||
|         const tiles: { features: any[]; layer: string }[] = [] |  | ||||||
|         Array.from(this.perLayerHierarchy.keys()).forEach((key) => { |  | ||||||
|             if (layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)) { |  | ||||||
|                 return |  | ||||||
|             } |  | ||||||
|             return tiles.push({ |  | ||||||
|                 layer: key, |  | ||||||
|                 features: [].concat(...self.GetFeaturesWithin(key, bbox)), |  | ||||||
|             }) |  | ||||||
|         }) |  | ||||||
|         return tiles |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Gets all the tiles which overlap with the given BBOX. |  | ||||||
|      * This might imply that extra features might be shown |  | ||||||
|      */ |  | ||||||
|     public GetFeaturesWithin(layerId: string, bbox: BBox): OsmFeature[][] { |  | ||||||
|         if (layerId === "*") { |  | ||||||
|             return this.GetAllFeaturesWithin(bbox) |  | ||||||
|         } |  | ||||||
|         const requestedHierarchy = this.perLayerHierarchy.get(layerId) |  | ||||||
|         if (requestedHierarchy === undefined) { |  | ||||||
|             console.warn( |  | ||||||
|                 "Layer ", |  | ||||||
|                 layerId, |  | ||||||
|                 "is not defined. Try one of ", |  | ||||||
|                 Array.from(this.perLayerHierarchy.keys()) |  | ||||||
|             ) |  | ||||||
|             return undefined |  | ||||||
|         } |  | ||||||
|         return TileHierarchyTools.getTiles(requestedHierarchy, bbox) |  | ||||||
|             .filter((featureSource) => featureSource.features?.data !== undefined) |  | ||||||
|             .map((featureSource) => <OsmFeature[]>featureSource.features.data) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public GetTilesPerLayerWithin( |  | ||||||
|         bbox: BBox, |  | ||||||
|         handleTile: (tile: FeatureSourceForLayer & Tiled) => void |  | ||||||
|     ) { |  | ||||||
|         Array.from(this.perLayerHierarchy.values()).forEach((hierarchy) => { |  | ||||||
|             TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile) |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private onNewDataLoaded(src: FeatureSource) { |  | ||||||
|         this.newDataLoadedSignal.setData(src) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private freshnessForVisibleLayers(z: number, x: number, y: number): Date { |  | ||||||
|         let oldestDate = undefined |  | ||||||
|         for (const flayer of this.state.filteredLayers.data) { |  | ||||||
|             if (!flayer.isDisplayed.data && !flayer.layerDef.forceLoad) { |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
|             if (this.state.locationControl.data.zoom < flayer.layerDef.minzoom) { |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
|             if (flayer.layerDef.maxAgeOfCache === 0) { |  | ||||||
|                 return undefined |  | ||||||
|             } |  | ||||||
|             const freshnessCalc = this.freshnesses.get(flayer.layerDef.id) |  | ||||||
|             if (freshnessCalc === undefined) { |  | ||||||
|                 console.warn("No freshness tracker found for ", flayer.layerDef.id) |  | ||||||
|                 return undefined |  | ||||||
|             } |  | ||||||
|             const freshness = freshnessCalc.freshnessFor(z, x, y) |  | ||||||
|             if (freshness === undefined) { |  | ||||||
|                 // SOmething is undefined --> we return undefined as we have to download
 |  | ||||||
|                 return undefined |  | ||||||
|             } |  | ||||||
|             if (oldestDate === undefined || oldestDate > freshness) { |  | ||||||
|                 oldestDate = freshness |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return oldestDate |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /* |  | ||||||
|      * Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM |  | ||||||
|      * */ |  | ||||||
|     private getNeededTilesFromOsm(isSufficientlyZoomed: Store<boolean>): Store<number[]> { |  | ||||||
|         const self = this |  | ||||||
|         return this.state.currentBounds.map( |  | ||||||
|             (bbox) => { |  | ||||||
|                 if (bbox === undefined) { |  | ||||||
|                     return [] |  | ||||||
|                 } |  | ||||||
|                 if (!isSufficientlyZoomed.data) { |  | ||||||
|                     return [] |  | ||||||
|                 } |  | ||||||
|                 const osmSourceZoomLevel = self.osmSourceZoomLevel |  | ||||||
|                 const range = bbox.containingTileRange(osmSourceZoomLevel) |  | ||||||
|                 const tileIndexes = [] |  | ||||||
|                 if (range.total >= 100) { |  | ||||||
|                     // Too much tiles!
 |  | ||||||
|                     return undefined |  | ||||||
|                 } |  | ||||||
|                 Tiles.MapRange(range, (x, y) => { |  | ||||||
|                     const i = Tiles.tile_index(osmSourceZoomLevel, x, y) |  | ||||||
|                     const oldestDate = self.freshnessForVisibleLayers(osmSourceZoomLevel, x, y) |  | ||||||
|                     if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) { |  | ||||||
|                         console.debug( |  | ||||||
|                             "Skipping tile", |  | ||||||
|                             osmSourceZoomLevel, |  | ||||||
|                             x, |  | ||||||
|                             y, |  | ||||||
|                             "as a decently fresh one is available" |  | ||||||
|                         ) |  | ||||||
|                         // The cached tiles contain decently fresh data
 |  | ||||||
|                         return undefined |  | ||||||
|                     } |  | ||||||
|                     tileIndexes.push(i) |  | ||||||
|                 }) |  | ||||||
|                 return tileIndexes |  | ||||||
|             }, |  | ||||||
|             [isSufficientlyZoomed] |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private initOverpassUpdater( |  | ||||||
|         state: { |  | ||||||
|             layoutToUse: LayoutConfig |  | ||||||
|             currentBounds: Store<BBox> |  | ||||||
|             locationControl: Store<Loc> |  | ||||||
|             readonly overpassUrl: Store<string[]> |  | ||||||
|             readonly overpassTimeout: Store<number> |  | ||||||
|             readonly overpassMaxZoom: Store<number> |  | ||||||
|         }, |  | ||||||
|         useOsmApi: Store<boolean> |  | ||||||
|     ): OverpassFeatureSource { |  | ||||||
|         const minzoom = Math.min(...state.layoutToUse.layers.map((layer) => layer.minzoom)) |  | ||||||
|         const overpassIsActive = state.currentBounds.map( |  | ||||||
|             (bbox) => { |  | ||||||
|                 if (bbox === undefined) { |  | ||||||
|                     console.debug("Disabling overpass source: no bbox") |  | ||||||
|                     return false |  | ||||||
|                 } |  | ||||||
|                 let zoom = state.locationControl.data.zoom |  | ||||||
|                 if (zoom < minzoom) { |  | ||||||
|                     // We are zoomed out over the zoomlevel of any layer
 |  | ||||||
|                     console.debug("Disabling overpass source: zoom < minzoom") |  | ||||||
|                     return false |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 const range = bbox.containingTileRange(zoom) |  | ||||||
|                 if (range.total >= 5000) { |  | ||||||
|                     // Let's assume we don't have so much data cached
 |  | ||||||
|                     return true |  | ||||||
|                 } |  | ||||||
|                 const self = this |  | ||||||
|                 const allFreshnesses = Tiles.MapRange(range, (x, y) => |  | ||||||
|                     self.freshnessForVisibleLayers(zoom, x, y) |  | ||||||
|                 ) |  | ||||||
|                 return allFreshnesses.some( |  | ||||||
|                     (freshness) => freshness === undefined || freshness < this.oldestAllowedDate |  | ||||||
|                 ) |  | ||||||
|             }, |  | ||||||
|             [state.locationControl] |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         return new OverpassFeatureSource(state, { |  | ||||||
|             padToTiles: state.locationControl.map((l) => Math.min(15, l.zoom + 1)), |  | ||||||
|             isActive: useOsmApi.map((b) => !b && overpassIsActive.data, [overpassIsActive]), |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Builds upon 'GetAllFeaturesAndMetaWithin', but does stricter BBOX-checking and applies the filters |  | ||||||
|      */ |  | ||||||
|     public getAllVisibleElementsWithmeta( |  | ||||||
|         bbox: BBox |  | ||||||
|     ): { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] { |  | ||||||
|         if (bbox === undefined) { |  | ||||||
|             console.warn("No bbox") |  | ||||||
|             return [] |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const layers = Utils.toIdRecord(this.state.layoutToUse.layers) |  | ||||||
|         const elementsWithMeta: { features: OsmFeature[]; layer: string }[] = |  | ||||||
|             this.GetAllFeaturesAndMetaWithin(bbox) |  | ||||||
| 
 |  | ||||||
|         let elements: { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] = [] |  | ||||||
|         let seenElements = new Set<string>() |  | ||||||
|         for (const elementsWithMetaElement of elementsWithMeta) { |  | ||||||
|             const layer = layers[elementsWithMetaElement.layer] |  | ||||||
|             if (layer.title === undefined) { |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
|             const filtered = this.state.filteredLayers.data.find((fl) => fl.layerDef == layer) |  | ||||||
|             for (let i = 0; i < elementsWithMetaElement.features.length; i++) { |  | ||||||
|                 const element = elementsWithMetaElement.features[i] |  | ||||||
|                 if (!filtered.isDisplayed.data) { |  | ||||||
|                     continue |  | ||||||
|                 } |  | ||||||
|                 if (seenElements.has(element.properties.id)) { |  | ||||||
|                     continue |  | ||||||
|                 } |  | ||||||
|                 seenElements.add(element.properties.id) |  | ||||||
|                 if (!bbox.overlapsWith(BBox.get(element))) { |  | ||||||
|                     continue |  | ||||||
|                 } |  | ||||||
|                 if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) { |  | ||||||
|                     continue |  | ||||||
|                 } |  | ||||||
|                 const activeFilters: FilterState[] = Array.from( |  | ||||||
|                     filtered.appliedFilters.data.values() |  | ||||||
|                 ) |  | ||||||
|                 if ( |  | ||||||
|                     !activeFilters.every( |  | ||||||
|                         (filter) => |  | ||||||
|                             filter?.currentFilter === undefined || |  | ||||||
|                             filter?.currentFilter?.matchesProperties(element.properties) |  | ||||||
|                     ) |  | ||||||
|                 ) { |  | ||||||
|                     continue |  | ||||||
|                 } |  | ||||||
|                 const center = GeoOperations.centerpointCoordinates(element) |  | ||||||
|                 elements.push({ |  | ||||||
|                     element, |  | ||||||
|                     center, |  | ||||||
|                     layer: layers[elementsWithMetaElement.layer], |  | ||||||
|                 }) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return elements |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Inject a new point |  | ||||||
|      */ |  | ||||||
|     InjectNewPoint(geojson) { |  | ||||||
|         this.newGeometryHandler.features.data.push(geojson) |  | ||||||
|         this.newGeometryHandler.features.ping() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Store } from "../UIEventSource" | import { Store, UIEventSource } from "../UIEventSource" | ||||||
| import FilteredLayer from "../../Models/FilteredLayer" | import FilteredLayer from "../../Models/FilteredLayer" | ||||||
| import { BBox } from "../BBox" | import { BBox } from "../BBox" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
|  | @ -6,6 +6,9 @@ import { Feature } from "geojson" | ||||||
| export default interface FeatureSource { | export default interface FeatureSource { | ||||||
|     features: Store<Feature[]> |     features: Store<Feature[]> | ||||||
| } | } | ||||||
|  | export interface WritableFeatureSource extends FeatureSource { | ||||||
|  |     features: UIEventSource<Feature[]> | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| export interface Tiled { | export interface Tiled { | ||||||
|     tileIndex: number |     tileIndex: number | ||||||
|  |  | ||||||
|  | @ -1,48 +1,59 @@ | ||||||
| import FeatureSource from "./FeatureSource" | import FeatureSource, { FeatureSourceForLayer } from "./FeatureSource" | ||||||
| import { Store } from "../UIEventSource" |  | ||||||
| import FilteredLayer from "../../Models/FilteredLayer" | import FilteredLayer from "../../Models/FilteredLayer" | ||||||
| import SimpleFeatureSource from "./Sources/SimpleFeatureSource" | import SimpleFeatureSource from "./Sources/SimpleFeatureSource" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
|  | import { Utils } from "../../Utils" | ||||||
|  | import { UIEventSource } from "../UIEventSource" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) |  * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) | ||||||
|  * If this is the case, multiple objects with a different _matching_layer_id are generated. |  * If this is the case, multiple objects with a different _matching_layer_id are generated. | ||||||
|  * In any case, this featureSource marks the objects with _matching_layer_id |  * In any case, this featureSource marks the objects with _matching_layer_id | ||||||
|  */ |  */ | ||||||
| export default class PerLayerFeatureSourceSplitter { | export default class PerLayerFeatureSourceSplitter< | ||||||
|  |     T extends FeatureSourceForLayer = SimpleFeatureSource | ||||||
|  | > { | ||||||
|  |     public readonly perLayer: ReadonlyMap<string, T> | ||||||
|     constructor( |     constructor( | ||||||
|         layers: Store<FilteredLayer[]>, |         layers: FilteredLayer[], | ||||||
|         handleLayerData: (source: FeatureSource, layer: FilteredLayer) => void, |  | ||||||
|         upstream: FeatureSource, |         upstream: FeatureSource, | ||||||
|         options?: { |         options?: { | ||||||
|             tileIndex?: number |             constructStore?: (features: UIEventSource<Feature[]>, layer: FilteredLayer) => T | ||||||
|             handleLeftovers?: (featuresWithoutLayer: any[]) => void |             handleLeftovers?: (featuresWithoutLayer: any[]) => void | ||||||
|         } |         } | ||||||
|     ) { |     ) { | ||||||
|         const knownLayers = new Map<string, SimpleFeatureSource>() |         const knownLayers = new Map<string, T>() | ||||||
|  |         this.perLayer = knownLayers | ||||||
|  |         const layerSources = new Map<string, UIEventSource<Feature[]>>() | ||||||
| 
 | 
 | ||||||
|         function update() { |         const constructStore = | ||||||
|             const features = upstream.features?.data |             options?.constructStore ?? ((store, layer) => new SimpleFeatureSource(layer, store)) | ||||||
|  |         for (const layer of layers) { | ||||||
|  |             const src = new UIEventSource<Feature[]>([]) | ||||||
|  |             layerSources.set(layer.layerDef.id, src) | ||||||
|  |             knownLayers.set(layer.layerDef.id, <T>constructStore(src, layer)) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         upstream.features.addCallbackAndRunD((features) => { | ||||||
|             if (features === undefined) { |             if (features === undefined) { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             if (layers.data === undefined || layers.data.length === 0) { |             if (layers === undefined) { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // We try to figure out (for each feature) in which feature store it should be saved.
 |             // We try to figure out (for each feature) in which feature store it should be saved.
 | ||||||
|             // Note that this splitter is only run when it is invoked by the overpass feature source, so we can't be sure in which layer it should go
 |  | ||||||
| 
 | 
 | ||||||
|             const featuresPerLayer = new Map<string, Feature[]>() |             const featuresPerLayer = new Map<string, Feature[]>() | ||||||
|             const noLayerFound = [] |             const noLayerFound: Feature[] = [] | ||||||
| 
 | 
 | ||||||
|             for (const layer of layers.data) { |             for (const layer of layers) { | ||||||
|                 featuresPerLayer.set(layer.layerDef.id, []) |                 featuresPerLayer.set(layer.layerDef.id, []) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             for (const f of features) { |             for (const f of features) { | ||||||
|                 let foundALayer = false |                 let foundALayer = false | ||||||
|                 for (const layer of layers.data) { |                 for (const layer of layers) { | ||||||
|                     if (layer.layerDef.source.osmTags.matchesProperties(f.properties)) { |                     if (layer.layerDef.source.osmTags.matchesProperties(f.properties)) { | ||||||
|                         // We have found our matching layer!
 |                         // We have found our matching layer!
 | ||||||
|                         featuresPerLayer.get(layer.layerDef.id).push(f) |                         featuresPerLayer.get(layer.layerDef.id).push(f) | ||||||
|  | @ -60,7 +71,7 @@ export default class PerLayerFeatureSourceSplitter { | ||||||
| 
 | 
 | ||||||
|             // At this point, we have our features per layer as a list
 |             // At this point, we have our features per layer as a list
 | ||||||
|             // We assign them to the correct featureSources
 |             // We assign them to the correct featureSources
 | ||||||
|             for (const layer of layers.data) { |             for (const layer of layers) { | ||||||
|                 const id = layer.layerDef.id |                 const id = layer.layerDef.id | ||||||
|                 const features = featuresPerLayer.get(id) |                 const features = featuresPerLayer.get(id) | ||||||
|                 if (features === undefined) { |                 if (features === undefined) { | ||||||
|  | @ -68,25 +79,24 @@ export default class PerLayerFeatureSourceSplitter { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 let featureSource = knownLayers.get(id) |                 const src = layerSources.get(id) | ||||||
|                 if (featureSource === undefined) { | 
 | ||||||
|                     // Not yet initialized - now is a good time
 |                 if (Utils.sameList(src.data, features)) { | ||||||
|                     featureSource = new SimpleFeatureSource(layer) |                     return | ||||||
|                     featureSource.features.setData(features) |  | ||||||
|                     knownLayers.set(id, featureSource) |  | ||||||
|                     handleLayerData(featureSource, layer) |  | ||||||
|                 } else { |  | ||||||
|                     featureSource.features.setData(features) |  | ||||||
|                 } |                 } | ||||||
|  |                 src.setData(features) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // AT last, the leftovers are handled
 |             // AT last, the leftovers are handled
 | ||||||
|             if (options?.handleLeftovers !== undefined && noLayerFound.length > 0) { |             if (options?.handleLeftovers !== undefined && noLayerFound.length > 0) { | ||||||
|                 options.handleLeftovers(noLayerFound) |                 options.handleLeftovers(noLayerFound) | ||||||
|             } |             } | ||||||
|         } |         }) | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|         layers.addCallback((_) => update()) |     public forEach(f: (featureSource: FeatureSourceForLayer) => void) { | ||||||
|         upstream.features.addCallbackAndRunD((_) => update()) |         for (const fs of this.perLayer.values()) { | ||||||
|  |             f(fs) | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								Logic/FeatureSource/Sources/ClippedFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Logic/FeatureSource/Sources/ClippedFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | import FeatureSource from "../FeatureSource" | ||||||
|  | import { Feature, Polygon } from "geojson" | ||||||
|  | import StaticFeatureSource from "./StaticFeatureSource" | ||||||
|  | import { GeoOperations } from "../../GeoOperations" | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Returns a clipped version of the original geojson. Ways which partially intersect the given feature will be split up | ||||||
|  |  */ | ||||||
|  | export default class ClippedFeatureSource extends StaticFeatureSource { | ||||||
|  |     constructor(features: FeatureSource, clipTo: Feature<Polygon>) { | ||||||
|  |         super( | ||||||
|  |             features.features.mapD((features) => { | ||||||
|  |                 return [].concat(features.map((feature) => GeoOperations.clipWith(feature, clipTo))) | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| import { Store, UIEventSource } from "../../UIEventSource" | import { Store, UIEventSource } from "../../UIEventSource" | ||||||
| import FilteredLayer, { FilterState } from "../../../Models/FilteredLayer" | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import FeatureSource from "../FeatureSource" | import FeatureSource from "../FeatureSource" | ||||||
| import { TagsFilter } from "../../Tags/TagsFilter" | import { TagsFilter } from "../../Tags/TagsFilter" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
| import { OsmTags } from "../../../Models/OsmFeature" | import { GlobalFilter } from "../../../Models/GlobalFilter" | ||||||
| 
 | 
 | ||||||
| export default class FilteringFeatureSource implements FeatureSource { | export default class FilteringFeatureSource implements FeatureSource { | ||||||
|     public features: UIEventSource<Feature[]> = new UIEventSource([]) |     public features: UIEventSource<Feature[]> = new UIEventSource([]) | ||||||
|     private readonly upstream: FeatureSource |     private readonly upstream: FeatureSource | ||||||
|     private readonly _fetchStore?: (id: String) => Store<OsmTags> |     private readonly _fetchStore?: (id: string) => Store<Record<string, string>> | ||||||
|     private readonly _globalFilters?: Store<{ filter: FilterState }[]> |     private readonly _globalFilters?: Store<GlobalFilter[]> | ||||||
|     private readonly _alreadyRegistered = new Set<Store<any>>() |     private readonly _alreadyRegistered = new Set<Store<any>>() | ||||||
|     private readonly _is_dirty = new UIEventSource(false) |     private readonly _is_dirty = new UIEventSource(false) | ||||||
|     private readonly _layer: FilteredLayer |     private readonly _layer: FilteredLayer | ||||||
|  | @ -18,8 +18,8 @@ export default class FilteringFeatureSource implements FeatureSource { | ||||||
|     constructor( |     constructor( | ||||||
|         layer: FilteredLayer, |         layer: FilteredLayer, | ||||||
|         upstream: FeatureSource, |         upstream: FeatureSource, | ||||||
|         fetchStore?: (id: String) => Store<OsmTags>, |         fetchStore?: (id: string) => Store<Record<string, string>>, | ||||||
|         globalFilters?: Store<{ filter: FilterState }[]>, |         globalFilters?: Store<GlobalFilter[]>, | ||||||
|         metataggingUpdated?: Store<any> |         metataggingUpdated?: Store<any> | ||||||
|     ) { |     ) { | ||||||
|         this.upstream = upstream |         this.upstream = upstream | ||||||
|  | @ -32,9 +32,11 @@ export default class FilteringFeatureSource implements FeatureSource { | ||||||
|             self.update() |             self.update() | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         layer.appliedFilters.addCallback((_) => { |         layer.appliedFilters.forEach((value) => | ||||||
|             self.update() |             value.addCallback((_) => { | ||||||
|         }) |                 self.update() | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         this._is_dirty.stabilized(1000).addCallbackAndRunD((dirty) => { |         this._is_dirty.stabilized(1000).addCallbackAndRunD((dirty) => { | ||||||
|             if (dirty) { |             if (dirty) { | ||||||
|  | @ -58,7 +60,7 @@ export default class FilteringFeatureSource implements FeatureSource { | ||||||
|         const layer = this._layer |         const layer = this._layer | ||||||
|         const features: Feature[] = this.upstream.features.data ?? [] |         const features: Feature[] = this.upstream.features.data ?? [] | ||||||
|         const includedFeatureIds = new Set<string>() |         const includedFeatureIds = new Set<string>() | ||||||
|         const globalFilters = self._globalFilters?.data?.map((f) => f.filter) |         const globalFilters = self._globalFilters?.data?.map((f) => f) | ||||||
|         const newFeatures = (features ?? []).filter((f) => { |         const newFeatures = (features ?? []).filter((f) => { | ||||||
|             self.registerCallback(f) |             self.registerCallback(f) | ||||||
| 
 | 
 | ||||||
|  | @ -71,19 +73,26 @@ export default class FilteringFeatureSource implements FeatureSource { | ||||||
|                 return false |                 return false | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const tagsFilter = Array.from(layer.appliedFilters?.data?.values() ?? []) |             for (const filter of layer.layerDef.filters) { | ||||||
|             for (const filter of tagsFilter) { |                 const state = layer.appliedFilters.get(filter.id).data | ||||||
|                 const neededTags: TagsFilter = filter?.currentFilter |                 if (state === undefined) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 let neededTags: TagsFilter | ||||||
|  |                 if (typeof state === "string") { | ||||||
|  |                     // This filter uses fields
 | ||||||
|  |                 } else { | ||||||
|  |                     neededTags = filter.options[state].osmTags | ||||||
|  |                 } | ||||||
|                 if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) { |                 if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) { | ||||||
|                     // Hidden by the filter on the layer itself - we want to hide it no matter what
 |                     // Hidden by the filter on the layer itself - we want to hide it no matter what
 | ||||||
|                     return false |                     return false | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             for (const filter of globalFilters ?? []) { |             for (const globalFilter of globalFilters ?? []) { | ||||||
|                 const neededTags: TagsFilter = filter?.currentFilter |                 const neededTags = globalFilter.osmTags | ||||||
|                 if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) { |                 if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) { | ||||||
|                     // Hidden by the filter on the layer itself - we want to hide it no matter what
 |  | ||||||
|                     return false |                     return false | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -58,7 +58,7 @@ export default class GeoJsonSource implements FeatureSource { | ||||||
|                 .replace("{x_max}", "" + bounds.maxLon) |                 .replace("{x_max}", "" + bounds.maxLon) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const eventsource = new UIEventSource<Feature[]>(undefined) |         const eventsource = new UIEventSource<Feature[]>([]) | ||||||
|         if (options?.isActive !== undefined) { |         if (options?.isActive !== undefined) { | ||||||
|             options.isActive.addCallbackAndRunD(async (active) => { |             options.isActive.addCallbackAndRunD(async (active) => { | ||||||
|                 if (!active) { |                 if (!active) { | ||||||
|  |  | ||||||
|  | @ -1,14 +1,15 @@ | ||||||
| import FeatureSource from "./FeatureSource" | import GeoJsonSource from "./GeoJsonSource" | ||||||
| import { Store } from "../UIEventSource" | import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||||
| import FeatureSwitchState from "../State/FeatureSwitchState" | import FeatureSource from "../FeatureSource" | ||||||
| import OverpassFeatureSource from "../Actors/OverpassFeatureSource" | import { Or } from "../../Tags/Or" | ||||||
| import { BBox } from "../BBox" | import FeatureSwitchState from "../../State/FeatureSwitchState" | ||||||
| import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource" | import OverpassFeatureSource from "./OverpassFeatureSource" | ||||||
| import { Or } from "../Tags/Or" | import { Store } from "../../UIEventSource" | ||||||
| import FeatureSourceMerger from "./Sources/FeatureSourceMerger" | import OsmFeatureSource from "./OsmFeatureSource" | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | import FeatureSourceMerger from "./FeatureSourceMerger" | ||||||
| import GeoJsonSource from "./Sources/GeoJsonSource" | import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource" | ||||||
| import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource" | import { BBox } from "../../BBox" | ||||||
|  | import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * This source will fetch the needed data from various sources for the given layout. |  * This source will fetch the needed data from various sources for the given layout. | ||||||
|  | @ -17,22 +18,24 @@ import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSou | ||||||
|  */ |  */ | ||||||
| export default class LayoutSource extends FeatureSourceMerger { | export default class LayoutSource extends FeatureSourceMerger { | ||||||
|     constructor( |     constructor( | ||||||
|         filteredLayers: LayerConfig[], |         layers: LayerConfig[], | ||||||
|         featureSwitches: FeatureSwitchState, |         featureSwitches: FeatureSwitchState, | ||||||
|         newAndChangedElements: FeatureSource, |         newAndChangedElements: FeatureSource, | ||||||
|         mapProperties: { bounds: Store<BBox>; zoom: Store<number> }, |         mapProperties: { bounds: Store<BBox>; zoom: Store<number> }, | ||||||
|         backend: string, |         backend: string, | ||||||
|         isLayerActive: (id: string) => Store<boolean> |         isDisplayed: (id: string) => Store<boolean> | ||||||
|     ) { |     ) { | ||||||
|         const { bounds, zoom } = mapProperties |         const { bounds, zoom } = mapProperties | ||||||
|         // remove all 'special' layers
 |         // remove all 'special' layers
 | ||||||
|         filteredLayers = filteredLayers.filter((flayer) => flayer.source !== null) |         layers = layers.filter((flayer) => flayer.source !== null) | ||||||
| 
 | 
 | ||||||
|         const geojsonlayers = filteredLayers.filter( |         const geojsonlayers = layers.filter((layer) => layer.source.geojsonSource !== undefined) | ||||||
|             (flayer) => flayer.source.geojsonSource !== undefined |         const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined) | ||||||
|         ) |         const fromCache = osmLayers.map( | ||||||
|         const osmLayers = filteredLayers.filter( |             (l) => | ||||||
|             (flayer) => flayer.source.geojsonSource === undefined |                 new LocalStorageFeatureSource(l.id, 15, mapProperties, { | ||||||
|  |                     isActive: isDisplayed(l.id), | ||||||
|  |                 }) | ||||||
|         ) |         ) | ||||||
|         const overpassSource = LayoutSource.setupOverpass(osmLayers, bounds, zoom, featureSwitches) |         const overpassSource = LayoutSource.setupOverpass(osmLayers, bounds, zoom, featureSwitches) | ||||||
|         const osmApiSource = LayoutSource.setupOsmApiSource( |         const osmApiSource = LayoutSource.setupOsmApiSource( | ||||||
|  | @ -43,11 +46,11 @@ export default class LayoutSource extends FeatureSourceMerger { | ||||||
|             featureSwitches |             featureSwitches | ||||||
|         ) |         ) | ||||||
|         const geojsonSources: FeatureSource[] = geojsonlayers.map((l) => |         const geojsonSources: FeatureSource[] = geojsonlayers.map((l) => | ||||||
|             LayoutSource.setupGeojsonSource(l, mapProperties) |             LayoutSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id)) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         const expiryInSeconds = Math.min(...(filteredLayers?.map((l) => l.maxAgeOfCache) ?? [])) |         const expiryInSeconds = Math.min(...(layers?.map((l) => l.maxAgeOfCache) ?? [])) | ||||||
|         super(overpassSource, osmApiSource, newAndChangedElements, ...geojsonSources) |         super(overpassSource, osmApiSource, newAndChangedElements, ...geojsonSources, ...fromCache) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static setupGeojsonSource( |     private static setupGeojsonSource( | ||||||
|  | @ -56,6 +59,10 @@ export default class LayoutSource extends FeatureSourceMerger { | ||||||
|         isActive?: Store<boolean> |         isActive?: Store<boolean> | ||||||
|     ): FeatureSource { |     ): FeatureSource { | ||||||
|         const source = layer.source |         const source = layer.source | ||||||
|  |         isActive = mapProperties.zoom.map( | ||||||
|  |             (z) => (isActive?.data ?? true) && z >= layer.maxzoom, | ||||||
|  |             [isActive] | ||||||
|  |         ) | ||||||
|         if (source.geojsonZoomLevel === undefined) { |         if (source.geojsonZoomLevel === undefined) { | ||||||
|             // This is a 'load everything at once' geojson layer
 |             // This is a 'load everything at once' geojson layer
 | ||||||
|             return new GeoJsonSource(layer, { isActive }) |             return new GeoJsonSource(layer, { isActive }) | ||||||
|  | @ -108,6 +108,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async LoadTile(z, x, y): Promise<void> { |     private async LoadTile(z, x, y): Promise<void> { | ||||||
|  |         console.log("OsmFeatureSource: loading ", z, x, y) | ||||||
|         if (z >= 22) { |         if (z >= 22) { | ||||||
|             throw "This is an absurd high zoom level" |             throw "This is an absurd high zoom level" | ||||||
|         } |         } | ||||||
|  | @ -126,7 +127,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger { | ||||||
| 
 | 
 | ||||||
|         let error = undefined |         let error = undefined | ||||||
|         try { |         try { | ||||||
|             const osmJson = await Utils.downloadJson(url) |             const osmJson = await Utils.downloadJsonCached(url, 2000) | ||||||
|             try { |             try { | ||||||
|                 this.rawDataHandlers.forEach((handler) => |                 this.rawDataHandlers.forEach((handler) => | ||||||
|                     handler(osmJson, Tiles.tile_index(z, x, y)) |                     handler(osmJson, Tiles.tile_index(z, x, y)) | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" |  | ||||||
| import { Or } from "../Tags/Or" |  | ||||||
| import { Overpass } from "../Osm/Overpass" |  | ||||||
| import FeatureSource from "../FeatureSource/FeatureSource" |  | ||||||
| import { Utils } from "../../Utils" |  | ||||||
| import { TagsFilter } from "../Tags/TagsFilter" |  | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" |  | ||||||
| import { BBox } from "../BBox" |  | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" |  | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
|  | import FeatureSource from "../FeatureSource" | ||||||
|  | import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" | ||||||
|  | import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||||
|  | import { Or } from "../../Tags/Or" | ||||||
|  | import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" | ||||||
|  | import { Overpass } from "../../Osm/Overpass" | ||||||
|  | import { Utils } from "../../../Utils" | ||||||
|  | import { TagsFilter } from "../../Tags/TagsFilter" | ||||||
|  | import { BBox } from "../../BBox" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * A wrapper around the 'Overpass'-object. |  * A wrapper around the 'Overpass'-object. | ||||||
|  | @ -99,7 +99,11 @@ export default class OverpassFeatureSource implements FeatureSource { | ||||||
|         ) { |         ) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|         const [bounds, date, updatedLayers] = await this.updateAsync() |         const result = await this.updateAsync() | ||||||
|  |         if (!result) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         const [bounds, date, updatedLayers] = result | ||||||
|         this._lastQueryBBox = bounds |         this._lastQueryBBox = bounds | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -188,6 +192,9 @@ export default class OverpassFeatureSource implements FeatureSource { | ||||||
|             if (data === undefined) { |             if (data === undefined) { | ||||||
|                 return undefined |                 return undefined | ||||||
|             } |             } | ||||||
|  |             // Some metatags are delivered by overpass _without_ underscore-prefix; we fix them below
 | ||||||
|  |             // TODO FIXME re-enable this data.features.forEach((f) => SimpleMetaTaggers.objectMetaInfo.applyMetaTagsOnFeature(f))
 | ||||||
|  | 
 | ||||||
|             self.features.setData(data.features) |             self.features.setData(data.features) | ||||||
|             return [bounds, date, layersToDownload] |             return [bounds, date, layersToDownload] | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|  | @ -1,34 +0,0 @@ | ||||||
| /** |  | ||||||
|  * Every previously added point is remembered, but new points are added. |  | ||||||
|  * Data coming from upstream will always overwrite a previous value |  | ||||||
|  */ |  | ||||||
| import FeatureSource, { Tiled } from "../FeatureSource" |  | ||||||
| import { Store, UIEventSource } from "../../UIEventSource" |  | ||||||
| import { BBox } from "../../BBox" |  | ||||||
| import { Feature } from "geojson" |  | ||||||
| 
 |  | ||||||
| export default class RememberingSource implements FeatureSource, Tiled { |  | ||||||
|     public readonly features: Store<Feature[]> |  | ||||||
|     public readonly tileIndex: number |  | ||||||
|     public readonly bbox: BBox |  | ||||||
| 
 |  | ||||||
|     constructor(source: FeatureSource & Tiled) { |  | ||||||
|         const self = this |  | ||||||
|         this.tileIndex = source.tileIndex |  | ||||||
|         this.bbox = source.bbox |  | ||||||
| 
 |  | ||||||
|         const empty = [] |  | ||||||
|         const featureSource = new UIEventSource<Feature[]>(empty) |  | ||||||
|         this.features = featureSource |  | ||||||
|         source.features.addCallbackAndRunD((features) => { |  | ||||||
|             const oldFeatures = self.features?.data ?? empty |  | ||||||
|             // Then new ids
 |  | ||||||
|             const ids = new Set<string>(features.map((f) => f.properties.id + f.geometry.type)) |  | ||||||
|             // the old data
 |  | ||||||
|             const oldData = oldFeatures.filter( |  | ||||||
|                 (old) => !ids.has(old.feature.properties.id + old.feature.geometry.type) |  | ||||||
|             ) |  | ||||||
|             featureSource.setData([...features, ...oldData]) |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										29
									
								
								Logic/FeatureSource/Sources/TouchesBboxFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								Logic/FeatureSource/Sources/TouchesBboxFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | ||||||
|  | import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource" | ||||||
|  | import StaticFeatureSource from "./StaticFeatureSource" | ||||||
|  | import { GeoOperations } from "../../GeoOperations" | ||||||
|  | import { BBox } from "../../BBox" | ||||||
|  | import exp from "constants" | ||||||
|  | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Results in a feature source which has all the elements that touch the given features | ||||||
|  |  */ | ||||||
|  | export default class BBoxFeatureSource extends StaticFeatureSource { | ||||||
|  |     constructor(features: FeatureSource, mustTouch: BBox) { | ||||||
|  |         const bbox = mustTouch.asGeoJson({}) | ||||||
|  |         super( | ||||||
|  |             features.features.mapD((features) => | ||||||
|  |                 features.filter((feature) => GeoOperations.intersect(feature, bbox) !== undefined) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class BBoxFeatureSourceForLayer extends BBoxFeatureSource implements FeatureSourceForLayer { | ||||||
|  |     constructor(features: FeatureSourceForLayer, mustTouch: BBox) { | ||||||
|  |         super(features, mustTouch) | ||||||
|  |         this.layer = features.layer | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     readonly layer: FilteredLayer | ||||||
|  | } | ||||||
|  | @ -84,7 +84,9 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { | ||||||
|                 }) |                 }) | ||||||
|             }, |             }, | ||||||
|             mapProperties, |             mapProperties, | ||||||
|             { isActive: options.isActive } |             { | ||||||
|  |                 isActive: options?.isActive, | ||||||
|  |             } | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,7 +5,8 @@ import FeatureSource from "../FeatureSource" | ||||||
| import FeatureSourceMerger from "../Sources/FeatureSourceMerger" | import FeatureSourceMerger from "../Sources/FeatureSourceMerger" | ||||||
| 
 | 
 | ||||||
| /*** | /*** | ||||||
|  * A tiled source which dynamically loads the required tiles at a fixed zoom level |  * A tiled source which dynamically loads the required tiles at a fixed zoom level. | ||||||
|  |  * A single featureSource will be initiliased for every tile in view; which will alter be merged into this featureSource | ||||||
|  */ |  */ | ||||||
| export default class DynamicTileSource extends FeatureSourceMerger { | export default class DynamicTileSource extends FeatureSourceMerger { | ||||||
|     constructor( |     constructor( | ||||||
|  |  | ||||||
|  | @ -1,11 +1,13 @@ | ||||||
| import TileHierarchy from "./TileHierarchy" |  | ||||||
| import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource" | import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||||
| import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject" | import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject" | ||||||
| import SimpleFeatureSource from "../Sources/SimpleFeatureSource" | import SimpleFeatureSource from "../Sources/SimpleFeatureSource" | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer" | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import { UIEventSource } from "../../UIEventSource" | import { UIEventSource } from "../../UIEventSource" | ||||||
|  | import { OsmTags } from "../../../Models/OsmFeature"; | ||||||
|  | import { BBox } from "../../BBox"; | ||||||
|  | import { Feature, Point } from "geojson"; | ||||||
| 
 | 
 | ||||||
| export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> { | export default class FullNodeDatabaseSource { | ||||||
|     public readonly loadedTiles = new Map<number, FeatureSource & Tiled>() |     public readonly loadedTiles = new Map<number, FeatureSource & Tiled>() | ||||||
|     private readonly onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void |     private readonly onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void | ||||||
|     private readonly layer: FilteredLayer |     private readonly layer: FilteredLayer | ||||||
|  | @ -81,4 +83,9 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour | ||||||
|     public GetParentWays(nodeId: number): UIEventSource<OsmWay[]> { |     public GetParentWays(nodeId: number): UIEventSource<OsmWay[]> { | ||||||
|         return this.parentWays.get(nodeId) |         return this.parentWays.get(nodeId) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     getNodesWithin(bBox: BBox) : Feature<Point, OsmTags>[]{ | ||||||
|  |         // TODO
 | ||||||
|  |         throw "TODO" | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,28 @@ | ||||||
|  | import DynamicTileSource from "./DynamicTileSource" | ||||||
|  | import { Store } from "../../UIEventSource" | ||||||
|  | import { BBox } from "../../BBox" | ||||||
|  | import TileLocalStorage from "../Actors/TileLocalStorage" | ||||||
|  | import { Feature } from "geojson" | ||||||
|  | import StaticFeatureSource from "../Sources/StaticFeatureSource" | ||||||
|  | 
 | ||||||
|  | export default class LocalStorageFeatureSource extends DynamicTileSource { | ||||||
|  |     constructor( | ||||||
|  |         layername: string, | ||||||
|  |         zoomlevel: number, | ||||||
|  |         mapProperties: { | ||||||
|  |             bounds: Store<BBox> | ||||||
|  |             zoom: Store<number> | ||||||
|  |         }, | ||||||
|  |         options?: { | ||||||
|  |             isActive?: Store<boolean> | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         const storage = TileLocalStorage.construct<Feature[]>(layername) | ||||||
|  |         super( | ||||||
|  |             zoomlevel, | ||||||
|  |             (tileIndex) => new StaticFeatureSource(storage.getTileSource(tileIndex)), | ||||||
|  |             mapProperties, | ||||||
|  |             options | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,24 +0,0 @@ | ||||||
| Data in MapComplete can come from multiple sources. |  | ||||||
| 
 |  | ||||||
| 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 |  | ||||||
|  | @ -1,24 +0,0 @@ | ||||||
| import FeatureSource, { Tiled } from "../FeatureSource" |  | ||||||
| import { BBox } from "../../BBox" |  | ||||||
| 
 |  | ||||||
| export default interface TileHierarchy<T extends FeatureSource> { |  | ||||||
|     /** |  | ||||||
|      * 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: T[] = [] |  | ||||||
|         hierarchy.loadedTiles.forEach((tile) => { |  | ||||||
|             if (tile.bbox.overlapsWith(bbox)) { |  | ||||||
|                 result.push(tile) |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
|         return result |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,58 +0,0 @@ | ||||||
| import TileHierarchy from "./TileHierarchy" |  | ||||||
| import { UIEventSource } from "../../UIEventSource" |  | ||||||
| import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" |  | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer" |  | ||||||
| import FeatureSourceMerger from "../Sources/FeatureSourceMerger" |  | ||||||
| import { Tiles } from "../../../Models/TileRange" |  | ||||||
| import { BBox } from "../../BBox" |  | ||||||
| 
 |  | ||||||
| export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> { |  | ||||||
|     public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map< |  | ||||||
|         number, |  | ||||||
|         FeatureSourceForLayer & Tiled |  | ||||||
|     >() |  | ||||||
|     public readonly layer: FilteredLayer |  | ||||||
|     private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map< |  | ||||||
|         number, |  | ||||||
|         UIEventSource<FeatureSource[]> |  | ||||||
|     >() |  | ||||||
|     private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void |  | ||||||
| 
 |  | ||||||
|     constructor( |  | ||||||
|         layer: FilteredLayer, |  | ||||||
|         handleTile: ( |  | ||||||
|             src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, |  | ||||||
|             index: number |  | ||||||
|         ) => void |  | ||||||
|     ) { |  | ||||||
|         this.layer = layer |  | ||||||
|         this._handleTile = handleTile |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Add another feature source for the given tile. |  | ||||||
|      * Entries for this tile will be merged |  | ||||||
|      * @param src |  | ||||||
|      */ |  | ||||||
|     public registerTile(src: FeatureSource & Tiled) { |  | ||||||
|         const index = src.tileIndex |  | ||||||
|         if (this.sources.has(index)) { |  | ||||||
|             const sources = this.sources.get(index) |  | ||||||
|             sources.data.push(src) |  | ||||||
|             sources.ping() |  | ||||||
|             return |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // We have to setup
 |  | ||||||
|         const sources = new UIEventSource<FeatureSource[]>([src]) |  | ||||||
|         this.sources.set(index, sources) |  | ||||||
|         const merger = new FeatureSourceMerger( |  | ||||||
|             this.layer, |  | ||||||
|             index, |  | ||||||
|             BBox.fromTile(...Tiles.tile_from_index(index)), |  | ||||||
|             sources |  | ||||||
|         ) |  | ||||||
|         this.loadedTiles.set(index, merger) |  | ||||||
|         this._handleTile(merger, index) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,249 +0,0 @@ | ||||||
| import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" |  | ||||||
| import { Store, UIEventSource } from "../../UIEventSource" |  | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer" |  | ||||||
| import TileHierarchy from "./TileHierarchy" |  | ||||||
| import { Tiles } from "../../../Models/TileRange" |  | ||||||
| import { BBox } from "../../BBox" |  | ||||||
| import { Feature } from "geojson"; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * 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[]> |  | ||||||
|     public readonly containedIds: Store<Set<string>> |  | ||||||
| 
 |  | ||||||
|     public readonly bbox: BBox |  | ||||||
|     public readonly tileIndex: number |  | ||||||
|     private upper_left: TiledFeatureSource |  | ||||||
|     private upper_right: TiledFeatureSource |  | ||||||
|     private lower_left: TiledFeatureSource |  | ||||||
|     private lower_right: TiledFeatureSource |  | ||||||
|     private readonly maxzoom: number |  | ||||||
|     private readonly options: TiledFeatureSourceOptions |  | ||||||
| 
 |  | ||||||
|     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 = Tiles.tile_index(z, x, y) |  | ||||||
|         this.name = `TiledFeatureSource(${z},${x},${y})` |  | ||||||
|         this.parent = parent |  | ||||||
|         this.layer = options.layer |  | ||||||
|         options = options ?? {} |  | ||||||
|         this.maxFeatureCount = options?.maxFeatureCount ?? 250 |  | ||||||
|         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 = Tiles.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.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 { |  | ||||||
|         options = { |  | ||||||
|             ...options, |  | ||||||
|             layer: features["layer"] ?? options.layer, |  | ||||||
|         } |  | ||||||
|         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[]) { |  | ||||||
|         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) |  | ||||||
| 
 |  | ||||||
|             // There are a few strategies to deal with features that cross tile boundaries
 |  | ||||||
| 
 |  | ||||||
|             if (this.options.noDuplicates) { |  | ||||||
|                 // Strategy 1: We put the feature into a somewhat matching tile
 |  | ||||||
|                 if (bbox.overlapsWith(this.upper_left.bbox)) { |  | ||||||
|                     ulf.push(feature) |  | ||||||
|                 } else if (bbox.overlapsWith(this.upper_right.bbox)) { |  | ||||||
|                     urf.push(feature) |  | ||||||
|                 } else if (bbox.overlapsWith(this.lower_left.bbox)) { |  | ||||||
|                     llf.push(feature) |  | ||||||
|                 } else if (bbox.overlapsWith(this.lower_right.bbox)) { |  | ||||||
|                     lrf.push(feature) |  | ||||||
|                 } else { |  | ||||||
|                     overlapsboundary.push(feature) |  | ||||||
|                 } |  | ||||||
|             } else if (this.options.minZoomLevel === undefined) { |  | ||||||
|                 // Strategy 2: put it into a strictly matching tile (or in this tile, which is slightly too big)
 |  | ||||||
|                 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 { |  | ||||||
|                 // Strategy 3: 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 |  | ||||||
|     /** |  | ||||||
|      * IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated. |  | ||||||
|      * Setting 'dontEnforceMinZoomLevel' will assign to feature to some matching subtile. |  | ||||||
|      */ |  | ||||||
|     readonly noDuplicates?: boolean |  | ||||||
|     readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void |  | ||||||
|     readonly layer?: FilteredLayer |  | ||||||
| } |  | ||||||
|  | @ -2,19 +2,34 @@ import { BBox } from "./BBox" | ||||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig" | import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||||
| import * as turf from "@turf/turf" | import * as turf from "@turf/turf" | ||||||
| import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf" | import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf" | ||||||
| import { Feature, Geometry, MultiPolygon, Polygon } from "geojson" | import { | ||||||
| import { GeoJSON, LineString, Point, Position } from "geojson" |     Feature, | ||||||
|  |     GeoJSON, | ||||||
|  |     Geometry, | ||||||
|  |     LineString, | ||||||
|  |     MultiPolygon, | ||||||
|  |     Point, | ||||||
|  |     Polygon, | ||||||
|  |     Position, | ||||||
|  | } from "geojson" | ||||||
| import togpx from "togpx" | import togpx from "togpx" | ||||||
| import Constants from "../Models/Constants" | import Constants from "../Models/Constants" | ||||||
|  | import { Tiles } from "../Models/TileRange" | ||||||
| 
 | 
 | ||||||
| export class GeoOperations { | export class GeoOperations { | ||||||
|  |     private static readonly _earthRadius = 6378137 | ||||||
|  |     private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2 | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Create a union between two features |      * Create a union between two features | ||||||
|      */ |      */ | ||||||
|     static union = turf.union |     public static union(f0: Feature, f1: Feature): Feature<Polygon | MultiPolygon> | null { | ||||||
|     static intersect = turf.intersect |         return turf.union(<any>f0, <any>f1) | ||||||
|     private static readonly _earthRadius = 6378137 |     } | ||||||
|     private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2 | 
 | ||||||
|  |     public static intersect(f0: Feature, f1: Feature): Feature<Polygon | MultiPolygon> | null { | ||||||
|  |         return turf.intersect(<any>f0, <any>f1) | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     static surfaceAreaInSqMeters(feature: any) { |     static surfaceAreaInSqMeters(feature: any) { | ||||||
|         return turf.area(feature) |         return turf.area(feature) | ||||||
|  | @ -637,14 +652,14 @@ export class GeoOperations { | ||||||
|      */ |      */ | ||||||
|     static completelyWithin( |     static completelyWithin( | ||||||
|         feature: Feature<Geometry, any>, |         feature: Feature<Geometry, any>, | ||||||
|         possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any> |         possiblyEnclosingFeature: Feature<Polygon | MultiPolygon, any> | ||||||
|     ): boolean { |     ): boolean { | ||||||
|         return booleanWithin(feature, possiblyEncloingFeature) |         return booleanWithin(feature, possiblyEnclosingFeature) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Create an intersection between two features. |      * Create an intersection between two features. | ||||||
|      * A new feature is returned based on 'toSplit', which'll have a geometry that is completely withing boundary |      * One or multiple new feature is returned based on 'toSplit', which'll have a geometry that is completely withing boundary | ||||||
|      */ |      */ | ||||||
|     public static clipWith(toSplit: Feature, boundary: Feature<Polygon>): Feature[] { |     public static clipWith(toSplit: Feature, boundary: Feature<Polygon>): Feature[] { | ||||||
|         if (toSplit.geometry.type === "Point") { |         if (toSplit.geometry.type === "Point") { | ||||||
|  | @ -677,35 +692,6 @@ export class GeoOperations { | ||||||
|         throw "Invalid geometry type with GeoOperations.clipWith: " + toSplit.geometry.type |         throw "Invalid geometry type with GeoOperations.clipWith: " + toSplit.geometry.type | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * Helper function which does the heavy lifting for 'inside' |  | ||||||
|      */ |  | ||||||
|     private static pointInPolygonCoordinates( |  | ||||||
|         x: number, |  | ||||||
|         y: number, |  | ||||||
|         coordinates: [number, number][][] |  | ||||||
|     ): boolean { |  | ||||||
|         const inside = GeoOperations.pointWithinRing( |  | ||||||
|             x, |  | ||||||
|             y, |  | ||||||
|             /*This is the outer ring of the polygon */ coordinates[0] |  | ||||||
|         ) |  | ||||||
|         if (!inside) { |  | ||||||
|             return false |  | ||||||
|         } |  | ||||||
|         for (let i = 1; i < coordinates.length; i++) { |  | ||||||
|             const inHole = GeoOperations.pointWithinRing( |  | ||||||
|                 x, |  | ||||||
|                 y, |  | ||||||
|                 coordinates[i] /* These are inner rings, aka holes*/ |  | ||||||
|             ) |  | ||||||
|             if (inHole) { |  | ||||||
|                 return false |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * |      * | ||||||
|      * |      * | ||||||
|  | @ -763,6 +749,62 @@ export class GeoOperations { | ||||||
|                 throw "Unkown location type: " + location |                 throw "Unkown location type: " + location | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Constructs all tiles where features overlap with and puts those features in them. | ||||||
|  |      * Long features (e.g. lines or polygons) which overlap with multiple tiles are referenced in each tile they overlap with | ||||||
|  |      * @param zoomlevel | ||||||
|  |      * @param features | ||||||
|  |      */ | ||||||
|  |     public static slice(zoomlevel: number, features: Feature[]): Map<number, Feature[]> { | ||||||
|  |         const tiles = new Map<number, Feature[]>() | ||||||
|  | 
 | ||||||
|  |         for (const feature of features) { | ||||||
|  |             const bbox = BBox.get(feature) | ||||||
|  |             Tiles.MapRange(Tiles.tileRangeFrom(bbox, zoomlevel), (x, y) => { | ||||||
|  |                 const i = Tiles.tile_index(zoomlevel, x, y) | ||||||
|  | 
 | ||||||
|  |                 let tiledata = tiles.get(i) | ||||||
|  |                 if (tiledata === undefined) { | ||||||
|  |                     tiledata = [] | ||||||
|  |                     tiles.set(i, tiledata) | ||||||
|  |                 } | ||||||
|  |                 tiledata.push(feature) | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return tiles | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Helper function which does the heavy lifting for 'inside' | ||||||
|  |      */ | ||||||
|  |     private static pointInPolygonCoordinates( | ||||||
|  |         x: number, | ||||||
|  |         y: number, | ||||||
|  |         coordinates: [number, number][][] | ||||||
|  |     ): boolean { | ||||||
|  |         const inside = GeoOperations.pointWithinRing( | ||||||
|  |             x, | ||||||
|  |             y, | ||||||
|  |             /*This is the outer ring of the polygon */ coordinates[0] | ||||||
|  |         ) | ||||||
|  |         if (!inside) { | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |         for (let i = 1; i < coordinates.length; i++) { | ||||||
|  |             const inHole = GeoOperations.pointWithinRing( | ||||||
|  |                 x, | ||||||
|  |                 y, | ||||||
|  |                 coordinates[i] /* These are inner rings, aka holes*/ | ||||||
|  |             ) | ||||||
|  |             if (inHole) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private static pointWithinRing(x: number, y: number, ring: [number, number][]) { |     private static pointWithinRing(x: number, y: number, ring: [number, number][]) { | ||||||
|         let inside = false |         let inside = false | ||||||
|         for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { |         for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { | ||||||
|  |  | ||||||
|  | @ -2,12 +2,12 @@ import { OsmCreateAction } from "./OsmChangeAction" | ||||||
| import { Tag } from "../../Tags/Tag" | import { Tag } from "../../Tags/Tag" | ||||||
| import { Changes } from "../Changes" | import { Changes } from "../Changes" | ||||||
| import { ChangeDescription } from "./ChangeDescription" | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import FeaturePipelineState from "../../State/FeaturePipelineState" |  | ||||||
| import FeatureSource from "../../FeatureSource/FeatureSource" |  | ||||||
| import CreateNewWayAction from "./CreateNewWayAction" | import CreateNewWayAction from "./CreateNewWayAction" | ||||||
| import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWithPointReuseAction" | import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWithPointReuseAction" | ||||||
| import { And } from "../../Tags/And" | import { And } from "../../Tags/And" | ||||||
| import { TagUtils } from "../../Tags/TagUtils" | import { TagUtils } from "../../Tags/TagUtils" | ||||||
|  | import { SpecialVisualizationState } from "../../../UI/SpecialVisualization" | ||||||
|  | import FeatureSource from "../../FeatureSource/FeatureSource" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points |  * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points | ||||||
|  | @ -26,14 +26,14 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct | ||||||
|         tags: Tag[], |         tags: Tag[], | ||||||
|         outerRingCoordinates: [number, number][], |         outerRingCoordinates: [number, number][], | ||||||
|         innerRingsCoordinates: [number, number][][], |         innerRingsCoordinates: [number, number][][], | ||||||
|         state: FeaturePipelineState, |         state: SpecialVisualizationState, | ||||||
|         config: MergePointConfig[], |         config: MergePointConfig[], | ||||||
|         changeType: "import" | "create" | string |         changeType: "import" | "create" | string | ||||||
|     ) { |     ) { | ||||||
|         super(null, true) |         super(null, true) | ||||||
|         this._tags = [...tags, new Tag("type", "multipolygon")] |         this._tags = [...tags, new Tag("type", "multipolygon")] | ||||||
|         this.changeType = changeType |         this.changeType = changeType | ||||||
|         this.theme = state?.layoutToUse?.id ?? "" |         this.theme = state?.layout?.id ?? "" | ||||||
|         this.createOuterWay = new CreateWayWithPointReuseAction( |         this.createOuterWay = new CreateWayWithPointReuseAction( | ||||||
|             [], |             [], | ||||||
|             outerRingCoordinates, |             outerRingCoordinates, | ||||||
|  | @ -45,7 +45,7 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct | ||||||
|                 new CreateNewWayAction( |                 new CreateNewWayAction( | ||||||
|                     [], |                     [], | ||||||
|                     ringCoordinates.map(([lon, lat]) => ({ lat, lon })), |                     ringCoordinates.map(([lon, lat]) => ({ lat, lon })), | ||||||
|                     { theme: state?.layoutToUse?.id } |                     { theme: state?.layout?.id } | ||||||
|                 ) |                 ) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  | @ -59,6 +59,10 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public async getPreview(): Promise<FeatureSource> { | ||||||
|  |         return undefined | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
|         console.log("Running CMPWPRA") |         console.log("Running CMPWPRA") | ||||||
|         const descriptions: ChangeDescription[] = [] |         const descriptions: ChangeDescription[] = [] | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ import { OsmCreateAction } from "./OsmChangeAction" | ||||||
| import { Tag } from "../../Tags/Tag" | import { Tag } from "../../Tags/Tag" | ||||||
| import { Changes } from "../Changes" | import { Changes } from "../Changes" | ||||||
| import { ChangeDescription } from "./ChangeDescription" | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import FeaturePipelineState from "../../State/FeaturePipelineState" |  | ||||||
| import { BBox } from "../../BBox" | import { BBox } from "../../BBox" | ||||||
| import { TagsFilter } from "../../Tags/TagsFilter" | import { TagsFilter } from "../../Tags/TagsFilter" | ||||||
| import { GeoOperations } from "../../GeoOperations" | import { GeoOperations } from "../../GeoOperations" | ||||||
|  | @ -10,6 +9,7 @@ import FeatureSource from "../../FeatureSource/FeatureSource" | ||||||
| import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" | import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" | ||||||
| import CreateNewNodeAction from "./CreateNewNodeAction" | import CreateNewNodeAction from "./CreateNewNodeAction" | ||||||
| import CreateNewWayAction from "./CreateNewWayAction" | import CreateNewWayAction from "./CreateNewWayAction" | ||||||
|  | import { SpecialVisualizationState } from "../../../UI/SpecialVisualization" | ||||||
| 
 | 
 | ||||||
| export interface MergePointConfig { | export interface MergePointConfig { | ||||||
|     withinRangeOfM: number |     withinRangeOfM: number | ||||||
|  | @ -62,14 +62,14 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|      * lngLat-coordinates |      * lngLat-coordinates | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private _coordinateInfo: CoordinateInfo[] |     private readonly _coordinateInfo: CoordinateInfo[] | ||||||
|     private _state: FeaturePipelineState |     private readonly _state: SpecialVisualizationState | ||||||
|     private _config: MergePointConfig[] |     private readonly _config: MergePointConfig[] | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         tags: Tag[], |         tags: Tag[], | ||||||
|         coordinates: [number, number][], |         coordinates: [number, number][], | ||||||
|         state: FeaturePipelineState, |         state: SpecialVisualizationState, | ||||||
|         config: MergePointConfig[] |         config: MergePointConfig[] | ||||||
|     ) { |     ) { | ||||||
|         super(null, true) |         super(null, true) | ||||||
|  | @ -188,7 +188,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
|         const theme = this._state?.layoutToUse?.id |         const theme = this._state?.layout?.id | ||||||
|         const allChanges: ChangeDescription[] = [] |         const allChanges: ChangeDescription[] = [] | ||||||
|         const nodeIdsToUse: { lat: number; lon: number; nodeId?: number }[] = [] |         const nodeIdsToUse: { lat: number; lon: number; nodeId?: number }[] = [] | ||||||
|         for (let i = 0; i < this._coordinateInfo.length; i++) { |         for (let i = 0; i < this._coordinateInfo.length; i++) { | ||||||
|  | @ -252,9 +252,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|     private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] { |     private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] { | ||||||
|         const bbox = new BBox(coordinates) |         const bbox = new BBox(coordinates) | ||||||
|         const state = this._state |         const state = this._state | ||||||
|         const allNodes = [].concat( |         const allNodes =state.fullNodeDatabase?.getNodesWithin(bbox.pad(1.2)) | ||||||
|             ...(state?.featurePipeline?.GetFeaturesWithin("type_node", bbox.pad(1.2)) ?? []) |  | ||||||
|         ) |  | ||||||
|         const maxDistance = Math.max(...this._config.map((c) => c.withinRangeOfM)) |         const maxDistance = Math.max(...this._config.map((c) => c.withinRangeOfM)) | ||||||
| 
 | 
 | ||||||
|         // Init coordianteinfo with undefined but the same length as coordinates
 |         // Init coordianteinfo with undefined but the same length as coordinates
 | ||||||
|  |  | ||||||
|  | @ -12,8 +12,8 @@ import { And } from "../../Tags/And" | ||||||
| import { Utils } from "../../../Utils" | import { Utils } from "../../../Utils" | ||||||
| import { OsmConnection } from "../OsmConnection" | import { OsmConnection } from "../OsmConnection" | ||||||
| import { Feature } from "@turf/turf" | import { Feature } from "@turf/turf" | ||||||
| import FeaturePipeline from "../../FeatureSource/FeaturePipeline" | import { Geometry, LineString, Point } from "geojson" | ||||||
| import { Geometry, LineString, Point, Polygon } from "geojson" | import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" | ||||||
| 
 | 
 | ||||||
| export default class ReplaceGeometryAction extends OsmChangeAction { | export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|     /** |     /** | ||||||
|  | @ -22,7 +22,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|     private readonly feature: any |     private readonly feature: any | ||||||
|     private readonly state: { |     private readonly state: { | ||||||
|         osmConnection: OsmConnection |         osmConnection: OsmConnection | ||||||
|         featurePipeline: FeaturePipeline |         fullNodeDatabase?: FullNodeDatabaseSource | ||||||
|     } |     } | ||||||
|     private readonly wayToReplaceId: string |     private readonly wayToReplaceId: string | ||||||
|     private readonly theme: string |     private readonly theme: string | ||||||
|  | @ -41,7 +41,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|     constructor( |     constructor( | ||||||
|         state: { |         state: { | ||||||
|             osmConnection: OsmConnection |             osmConnection: OsmConnection | ||||||
|             featurePipeline: FeaturePipeline |             fullNodeDatabase?: FullNodeDatabaseSource | ||||||
|         }, |         }, | ||||||
|         feature: any, |         feature: any, | ||||||
|         wayToReplaceId: string, |         wayToReplaceId: string, | ||||||
|  | @ -195,7 +195,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|     }> { |     }> { | ||||||
|         // TODO FIXME: if a new point has to be created, snap to already existing ways
 |         // TODO FIXME: if a new point has to be created, snap to already existing ways
 | ||||||
| 
 | 
 | ||||||
|         const nodeDb = this.state.featurePipeline.fullNodeDatabase |         const nodeDb = this.state.fullNodeDatabase | ||||||
|         if (nodeDb === undefined) { |         if (nodeDb === undefined) { | ||||||
|             throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" |             throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" | ||||||
|         } |         } | ||||||
|  | @ -415,7 +415,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
|         const nodeDb = this.state.featurePipeline.fullNodeDatabase |         const nodeDb = this.state.fullNodeDatabase | ||||||
|         if (nodeDb === undefined) { |         if (nodeDb === undefined) { | ||||||
|             throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" |             throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -5,6 +5,10 @@ export interface GeoCodeResult { | ||||||
|     display_name: string |     display_name: string | ||||||
|     lat: number |     lat: number | ||||||
|     lon: number |     lon: number | ||||||
|  |     /** | ||||||
|  |      * Format: | ||||||
|  |      * [lat, lat, lon, lon] | ||||||
|  |      */ | ||||||
|     boundingbox: number[] |     boundingbox: number[] | ||||||
|     osm_type: "node" | "way" | "relation" |     osm_type: "node" | "way" | "relation" | ||||||
|     osm_id: string |     osm_id: string | ||||||
|  |  | ||||||
|  | @ -15,6 +15,13 @@ import { OsmTags } from "../Models/OsmFeature" | ||||||
| import { UIEventSource } from "./UIEventSource" | import { UIEventSource } from "./UIEventSource" | ||||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * All elements that are needed to perform metatagging | ||||||
|  |  */ | ||||||
|  | export interface MetataggingState { | ||||||
|  |     layout: LayoutConfig | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export abstract class SimpleMetaTagger { | export abstract class SimpleMetaTagger { | ||||||
|     public readonly keys: string[] |     public readonly keys: string[] | ||||||
|     public readonly doc: string |     public readonly doc: string | ||||||
|  | @ -60,7 +67,7 @@ export abstract class SimpleMetaTagger { | ||||||
|         feature: any, |         feature: any, | ||||||
|         layer: LayerConfig, |         layer: LayerConfig, | ||||||
|         tagsStore: UIEventSource<Record<string, string>>, |         tagsStore: UIEventSource<Record<string, string>>, | ||||||
|         state: { layout: LayoutConfig } |         state: MetataggingState | ||||||
|     ): boolean |     ): boolean | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -119,7 +126,7 @@ export class CountryTagger extends SimpleMetaTagger { | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     applyMetaTagsOnFeature(feature, _, state) { |     applyMetaTagsOnFeature(feature, _, tagsSource) { | ||||||
|         let centerPoint: any = GeoOperations.centerpoint(feature) |         let centerPoint: any = GeoOperations.centerpoint(feature) | ||||||
|         const runningTasks = this.runningTasks |         const runningTasks = this.runningTasks | ||||||
|         const lat = centerPoint.geometry.coordinates[1] |         const lat = centerPoint.geometry.coordinates[1] | ||||||
|  | @ -128,28 +135,29 @@ export class CountryTagger extends SimpleMetaTagger { | ||||||
|         CountryTagger.coder |         CountryTagger.coder | ||||||
|             .GetCountryCodeAsync(lon, lat) |             .GetCountryCodeAsync(lon, lat) | ||||||
|             .then((countries) => { |             .then((countries) => { | ||||||
|                 runningTasks.delete(feature) |                 const oldCountry = feature.properties["_country"] | ||||||
|                 try { |                 const newCountry = countries[0].trim().toLowerCase() | ||||||
|                     const oldCountry = feature.properties["_country"] |                 if (oldCountry !== newCountry) { | ||||||
|                     feature.properties["_country"] = countries[0].trim().toLowerCase() |                     tagsSource.data["_country"] = newCountry | ||||||
|                     if (oldCountry !== feature.properties["_country"]) { |                     tagsSource?.ping() | ||||||
|                         const tagsSource = state?.allElements?.getEventSourceById( |  | ||||||
|                             feature.properties.id |  | ||||||
|                         ) |  | ||||||
|                         tagsSource?.ping() |  | ||||||
|                     } |  | ||||||
|                 } catch (e) { |  | ||||||
|                     console.warn(e) |  | ||||||
|                 } |                 } | ||||||
|             }) |             }) | ||||||
|             .catch((_) => { |             .catch((e) => { | ||||||
|                 runningTasks.delete(feature) |                 console.warn(e) | ||||||
|             }) |             }) | ||||||
|  |             .finally(() => runningTasks.delete(feature)) | ||||||
|         return false |         return false | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class InlineMetaTagger extends SimpleMetaTagger { | class InlineMetaTagger extends SimpleMetaTagger { | ||||||
|  |     public readonly applyMetaTagsOnFeature: ( | ||||||
|  |         feature: any, | ||||||
|  |         layer: LayerConfig, | ||||||
|  |         tagsStore: UIEventSource<OsmTags>, | ||||||
|  |         state: MetataggingState | ||||||
|  |     ) => boolean | ||||||
|  | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         docs: { |         docs: { | ||||||
|             keys: string[] |             keys: string[] | ||||||
|  | @ -166,23 +174,17 @@ class InlineMetaTagger extends SimpleMetaTagger { | ||||||
|             feature: any, |             feature: any, | ||||||
|             layer: LayerConfig, |             layer: LayerConfig, | ||||||
|             tagsStore: UIEventSource<OsmTags>, |             tagsStore: UIEventSource<OsmTags>, | ||||||
|             state: { layout: LayoutConfig } |             state: MetataggingState | ||||||
|         ) => boolean |         ) => boolean | ||||||
|     ) { |     ) { | ||||||
|         super(docs) |         super(docs) | ||||||
|         this.applyMetaTagsOnFeature = f |         this.applyMetaTagsOnFeature = f | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     public readonly applyMetaTagsOnFeature: ( |  | ||||||
|         feature: any, |  | ||||||
|         layer: LayerConfig, |  | ||||||
|         tagsStore: UIEventSource<OsmTags>, |  | ||||||
|         state: { layout: LayoutConfig } |  | ||||||
|     ) => boolean |  | ||||||
| } | } | ||||||
| export default class SimpleMetaTaggers { | 
 | ||||||
|     public static readonly objectMetaInfo = new InlineMetaTagger( | export class RewriteMetaInfoTags extends SimpleMetaTagger { | ||||||
|         { |     constructor() { | ||||||
|  |         super({ | ||||||
|             keys: [ |             keys: [ | ||||||
|                 "_last_edit:contributor", |                 "_last_edit:contributor", | ||||||
|                 "_last_edit:contributor:uid", |                 "_last_edit:contributor:uid", | ||||||
|  | @ -192,30 +194,37 @@ export default class SimpleMetaTaggers { | ||||||
|                 "_backend", |                 "_backend", | ||||||
|             ], |             ], | ||||||
|             doc: "Information about the last edit of this object.", |             doc: "Information about the last edit of this object.", | ||||||
|         }, |         }) | ||||||
|         (feature) => { |     } | ||||||
|             /*Note: also called by 'UpdateTagsFromOsmAPI'*/ |  | ||||||
| 
 | 
 | ||||||
|             const tgs = feature.properties |     applyMetaTagsOnFeature(feature: Feature): boolean { | ||||||
|             let movedSomething = false |         /*Note: also called by 'UpdateTagsFromOsmAPI'*/ | ||||||
| 
 | 
 | ||||||
|             function move(src: string, target: string) { |         const tgs = feature.properties | ||||||
|                 if (tgs[src] === undefined) { |         let movedSomething = false | ||||||
|                     return | 
 | ||||||
|                 } |         function move(src: string, target: string) { | ||||||
|                 tgs[target] = tgs[src] |             if (tgs[src] === undefined) { | ||||||
|                 delete tgs[src] |                 return | ||||||
|                 movedSomething = true |  | ||||||
|             } |             } | ||||||
| 
 |             tgs[target] = tgs[src] | ||||||
|             move("user", "_last_edit:contributor") |             delete tgs[src] | ||||||
|             move("uid", "_last_edit:contributor:uid") |             movedSomething = true | ||||||
|             move("changeset", "_last_edit:changeset") |  | ||||||
|             move("timestamp", "_last_edit:timestamp") |  | ||||||
|             move("version", "_version_number") |  | ||||||
|             return movedSomething |  | ||||||
|         } |         } | ||||||
|     ) | 
 | ||||||
|  |         move("user", "_last_edit:contributor") | ||||||
|  |         move("uid", "_last_edit:contributor:uid") | ||||||
|  |         move("changeset", "_last_edit:changeset") | ||||||
|  |         move("timestamp", "_last_edit:timestamp") | ||||||
|  |         move("version", "_version_number") | ||||||
|  |         return movedSomething | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | export default class SimpleMetaTaggers { | ||||||
|  |     /** | ||||||
|  |      * A simple metatagger which rewrites various metatags as needed | ||||||
|  |      */ | ||||||
|  |     public static readonly objectMetaInfo = new RewriteMetaInfoTags() | ||||||
|     public static country = new CountryTagger() |     public static country = new CountryTagger() | ||||||
|     public static geometryType = new InlineMetaTagger( |     public static geometryType = new InlineMetaTagger( | ||||||
|         { |         { | ||||||
|  |  | ||||||
|  | @ -1,10 +1,5 @@ | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" |  | ||||||
| import FeaturePipeline from "../FeatureSource/FeaturePipeline" |  | ||||||
| import { Tiles } from "../../Models/TileRange" |  | ||||||
| import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler" | import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler" | ||||||
| import Hash from "../Web/Hash" | import Hash from "../Web/Hash" | ||||||
| import { BBox } from "../BBox" |  | ||||||
| import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource" |  | ||||||
| import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator" | import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator" | ||||||
| 
 | 
 | ||||||
| export default class FeaturePipelineState { | export default class FeaturePipelineState { | ||||||
|  | @ -14,101 +9,9 @@ export default class FeaturePipelineState { | ||||||
|     public readonly featurePipeline: FeaturePipeline |     public readonly featurePipeline: FeaturePipeline | ||||||
|     private readonly metatagRecalculator: MetaTagRecalculator |     private readonly metatagRecalculator: MetaTagRecalculator | ||||||
| 
 | 
 | ||||||
|     constructor(layoutToUse: LayoutConfig) { |     constructor() { | ||||||
|         const clustering = layoutToUse?.clustering |  | ||||||
|         const clusterCounter = this.featureAggregator |  | ||||||
|         const self = this |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * We are a bit in a bind: |  | ||||||
|          * There is the featurePipeline, which creates some sources during construction |  | ||||||
|          * THere is the metatagger, which needs to have these sources registered AND which takes a FeaturePipeline as argument |  | ||||||
|          * |  | ||||||
|          * This is a bit of a catch-22 (except that it isn't) |  | ||||||
|          * The sources that are registered in the constructor are saved into 'registeredSources' temporary |  | ||||||
|          * |  | ||||||
|          */ |  | ||||||
|         const sourcesToRegister = [] |  | ||||||
| 
 |  | ||||||
|         function registerRaw(source: FeatureSourceForLayer & Tiled) { |  | ||||||
|             if (self.metatagRecalculator === undefined) { |  | ||||||
|                 sourcesToRegister.push(source) |  | ||||||
|             } else { |  | ||||||
|                 self.metatagRecalculator.registerSource(source) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         function registerSource(source: FeatureSourceForLayer & Tiled) { |  | ||||||
|             clusterCounter.addTile(source) |  | ||||||
|             const sourceBBox = source.features.map((allFeatures) => |  | ||||||
|                 BBox.bboxAroundAll(allFeatures.map(BBox.get)) |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|             // Do show features indicates if the respective 'showDataLayer' should be shown. It can be hidden by e.g. clustering
 |  | ||||||
|             source.features.map( |  | ||||||
|                 (f) => { |  | ||||||
|                     const z = self.locationControl.data.zoom |  | ||||||
| 
 |  | ||||||
|                     if (!source.layer.isDisplayed.data) { |  | ||||||
|                         return false |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     const bounds = self.currentBounds.data |  | ||||||
|                     if (bounds === undefined) { |  | ||||||
|                         // Map is not yet displayed
 |  | ||||||
|                         return false |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     if (!sourceBBox.data.overlapsWith(bounds)) { |  | ||||||
|                         // Not within range -> features are hidden
 |  | ||||||
|                         return false |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     if (z < source.layer.layerDef.minzoom) { |  | ||||||
|                         // Layer is always hidden for this zoom level
 |  | ||||||
|                         return false |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     if (z > clustering.maxZoom) { |  | ||||||
|                         return true |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     if (f.length > clustering.minNeededElements) { |  | ||||||
|                         // This tile alone already has too much features
 |  | ||||||
|                         return false |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex) |  | ||||||
|                     if (tileZ >= z) { |  | ||||||
|                         while (tileZ > z) { |  | ||||||
|                             tileZ-- |  | ||||||
|                             tileX = Math.floor(tileX / 2) |  | ||||||
|                             tileY = Math.floor(tileY / 2) |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         if ( |  | ||||||
|                             clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY)) |  | ||||||
|                                 ?.totalValue > clustering.minNeededElements |  | ||||||
|                         ) { |  | ||||||
|                             // To much elements
 |  | ||||||
|                             return false |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     return true |  | ||||||
|                 }, |  | ||||||
|                 [self.currentBounds, source.layer.isDisplayed, sourceBBox] |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         this.featurePipeline = new FeaturePipeline(registerSource, this, { |  | ||||||
|             handleRawFeatureSource: registerRaw, |  | ||||||
|         }) |  | ||||||
|         this.metatagRecalculator = new MetaTagRecalculator(this, this.featurePipeline) |         this.metatagRecalculator = new MetaTagRecalculator(this, this.featurePipeline) | ||||||
|         this.metatagRecalculator.registerSource(this.currentView) |         this.metatagRecalculator.registerSource(this.currentView) | ||||||
| 
 |  | ||||||
|         sourcesToRegister.forEach((source) => self.metatagRecalculator.registerSource(source)) |  | ||||||
| 
 |  | ||||||
|         new SelectedFeatureHandler(Hash.hash, this) |         new SelectedFeatureHandler(Hash.hash, this) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,10 +1,8 @@ | ||||||
| import { UIEventSource } from "../UIEventSource" | import { UIEventSource } from "../UIEventSource" | ||||||
| import { GlobalFilter } from "../../Models/GlobalFilter" | import { GlobalFilter } from "../../Models/GlobalFilter" | ||||||
| import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" | import FilteredLayer from "../../Models/FilteredLayer" | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
| import { OsmConnection } from "../Osm/OsmConnection" | import { OsmConnection } from "../Osm/OsmConnection" | ||||||
| import { LocalStorageSource } from "../Web/LocalStorageSource" |  | ||||||
| import { QueryParameters } from "../Web/QueryParameters" |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The layer state keeps track of: |  * The layer state keeps track of: | ||||||
|  | @ -36,83 +34,14 @@ export default class LayerState { | ||||||
|         this.osmConnection = osmConnection |         this.osmConnection = osmConnection | ||||||
|         this.filteredLayers = new Map() |         this.filteredLayers = new Map() | ||||||
|         for (const layer of layers) { |         for (const layer of layers) { | ||||||
|             this.filteredLayers.set(layer.id, this.initFilteredLayer(layer, context)) |             this.filteredLayers.set( | ||||||
|  |                 layer.id, | ||||||
|  |                 FilteredLayer.initLinkedState(layer, context, this.osmConnection) | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
|         layers.forEach((l) => this.linkFilterStates(l)) |         layers.forEach((l) => this.linkFilterStates(l)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static getPref( |  | ||||||
|         osmConnection: OsmConnection, |  | ||||||
|         key: string, |  | ||||||
|         layer: LayerConfig |  | ||||||
|     ): UIEventSource<boolean> { |  | ||||||
|         return osmConnection.GetPreference(key, layer.shownByDefault + "").sync( |  | ||||||
|             (v) => { |  | ||||||
|                 if (v === undefined) { |  | ||||||
|                     return undefined |  | ||||||
|                 } |  | ||||||
|                 return v === "true" |  | ||||||
|             }, |  | ||||||
|             [], |  | ||||||
|             (b) => { |  | ||||||
|                 if (b === undefined) { |  | ||||||
|                     return undefined |  | ||||||
|                 } |  | ||||||
|                 return "" + b |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
|     /** |  | ||||||
|      * INitializes a filtered layer for the given layer. |  | ||||||
|      * @param layer |  | ||||||
|      * @param context: probably the theme-name. This is used to disambiguate the user settings; e.g. when using the same layer in different contexts |  | ||||||
|      * @private |  | ||||||
|      */ |  | ||||||
|     private initFilteredLayer(layer: LayerConfig, context: string): FilteredLayer | undefined { |  | ||||||
|         let isDisplayed: UIEventSource<boolean> |  | ||||||
|         const osmConnection = this.osmConnection |  | ||||||
|         if (layer.syncSelection === "local") { |  | ||||||
|             isDisplayed = LocalStorageSource.GetParsed( |  | ||||||
|                 context + "-layer-" + layer.id + "-enabled", |  | ||||||
|                 layer.shownByDefault |  | ||||||
|             ) |  | ||||||
|         } else if (layer.syncSelection === "theme-only") { |  | ||||||
|             isDisplayed = LayerState.getPref( |  | ||||||
|                 osmConnection, |  | ||||||
|                 context + "-layer-" + layer.id + "-enabled", |  | ||||||
|                 layer |  | ||||||
|             ) |  | ||||||
|         } else if (layer.syncSelection === "global") { |  | ||||||
|             isDisplayed = LayerState.getPref(osmConnection, "layer-" + layer.id + "-enabled", layer) |  | ||||||
|         } else { |  | ||||||
|             isDisplayed = QueryParameters.GetBooleanQueryParameter( |  | ||||||
|                 "layer-" + layer.id, |  | ||||||
|                 layer.shownByDefault, |  | ||||||
|                 "Wether or not layer " + layer.id + " is shown" |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const flayer: FilteredLayer = { |  | ||||||
|             isDisplayed, |  | ||||||
|             layerDef: layer, |  | ||||||
|             appliedFilters: new UIEventSource<Map<string, FilterState>>( |  | ||||||
|                 new Map<string, FilterState>() |  | ||||||
|             ), |  | ||||||
|         } |  | ||||||
|         layer.filters?.forEach((filterConfig) => { |  | ||||||
|             const stateSrc = filterConfig.initState() |  | ||||||
| 
 |  | ||||||
|             stateSrc.addCallbackAndRun((state) => |  | ||||||
|                 flayer.appliedFilters.data.set(filterConfig.id, state) |  | ||||||
|             ) |  | ||||||
|             flayer.appliedFilters |  | ||||||
|                 .map((dict) => dict.get(filterConfig.id)) |  | ||||||
|                 .addCallback((state) => stateSrc.setData(state)) |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         return flayer |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Some layers copy the filter state of another layer - this is quite often the case for 'sibling'-layers, |      * Some layers copy the filter state of another layer - this is quite often the case for 'sibling'-layers, | ||||||
|      * (where two variations of the same layer are used, e.g. a specific type of shop on all zoom levels and all shops on high zoom). |      * (where two variations of the same layer are used, e.g. a specific type of shop on all zoom levels and all shops on high zoom). | ||||||
|  | @ -136,10 +65,6 @@ export default class LayerState { | ||||||
|         console.warn( |         console.warn( | ||||||
|             "Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs |             "Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs | ||||||
|         ) |         ) | ||||||
|         this.filteredLayers.set(layer.id, { |         this.filteredLayers.set(layer.id, toReuse) | ||||||
|             isDisplayed: toReuse.isDisplayed, |  | ||||||
|             layerDef: layer, |  | ||||||
|             appliedFilters: toReuse.appliedFilters, |  | ||||||
|         }) |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -17,14 +17,10 @@ export default class UserRelatedState { | ||||||
|      The user credentials |      The user credentials | ||||||
|      */ |      */ | ||||||
|     public osmConnection: OsmConnection |     public osmConnection: OsmConnection | ||||||
|     /** |  | ||||||
|      THe change handler |  | ||||||
|      */ |  | ||||||
|     public changes: Changes |  | ||||||
|     /** |     /** | ||||||
|      * The key for mangrove |      * The key for mangrove | ||||||
|      */ |      */ | ||||||
|     public mangroveIdentity: MangroveIdentity |     public readonly mangroveIdentity: MangroveIdentity | ||||||
| 
 | 
 | ||||||
|     public readonly installedUserThemes: Store<string[]> |     public readonly installedUserThemes: Store<string[]> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -63,27 +63,10 @@ export class Stores { | ||||||
|                 stable.setData(undefined) |                 stable.setData(undefined) | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             const oldList = stable.data |             if (Utils.sameList(stable.data, list)) { | ||||||
|             if (oldList === list) { |  | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             if (oldList == list) { |             stable.setData(list) | ||||||
|                 return |  | ||||||
|             } |  | ||||||
|             if (oldList === undefined || oldList.length !== list.length) { |  | ||||||
|                 stable.setData(list) |  | ||||||
|                 return |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             for (let i = 0; i < list.length; i++) { |  | ||||||
|                 if (oldList[i] !== list[i]) { |  | ||||||
|                     stable.setData(list) |  | ||||||
|                     return |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // No actual changes, so we don't do anything
 |  | ||||||
|             return |  | ||||||
|         }) |         }) | ||||||
|         return stable |         return stable | ||||||
|     } |     } | ||||||
|  | @ -93,7 +76,7 @@ export abstract class Store<T> implements Readable<T> { | ||||||
|     abstract readonly data: T |     abstract readonly data: T | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * OPtional value giving a title to the UIEventSource, mainly used for debugging |      * Optional value giving a title to the UIEventSource, mainly used for debugging | ||||||
|      */ |      */ | ||||||
|     public readonly tag: string | undefined |     public readonly tag: string | undefined | ||||||
| 
 | 
 | ||||||
|  | @ -794,4 +777,14 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | ||||||
|     update(f: Updater<T> & ((value: T) => T)): void { |     update(f: Updater<T> & ((value: T) => T)): void { | ||||||
|         this.setData(f(this.data)) |         this.setData(f(this.data)) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a new UIEVentSource. Whenever 'source' changes, the returned UIEventSource will get this value as well. | ||||||
|  |      * However, this value can be overriden without affecting source | ||||||
|  |      */ | ||||||
|  |     static feedFrom<T>(store: Store<T>): UIEventSource<T> { | ||||||
|  |         const src = new UIEventSource(store.data) | ||||||
|  |         store.addCallback((t) => src.setData(t)) | ||||||
|  |         return src | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,10 +1,8 @@ | ||||||
| import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" | import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" | ||||||
| import { MangroveReviews, Review } from "mangrove-reviews-typescript" | import { MangroveReviews, Review } from "mangrove-reviews-typescript" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { Feature, Geometry, Position } from "geojson" | import { Feature, Position } from "geojson" | ||||||
| import { GeoOperations } from "../GeoOperations" | import { GeoOperations } from "../GeoOperations" | ||||||
| import { OsmTags } from "../../Models/OsmFeature" |  | ||||||
| import { ElementStorage } from "../ElementStorage" |  | ||||||
| 
 | 
 | ||||||
| export class MangroveIdentity { | export class MangroveIdentity { | ||||||
|     public readonly keypair: Store<CryptoKeyPair> |     public readonly keypair: Store<CryptoKeyPair> | ||||||
|  | @ -67,11 +65,9 @@ export default class FeatureReviews { | ||||||
|     private readonly _identity: MangroveIdentity |     private readonly _identity: MangroveIdentity | ||||||
| 
 | 
 | ||||||
|     private constructor( |     private constructor( | ||||||
|         feature: Feature<Geometry, OsmTags>, |         feature: Feature, | ||||||
|         state: { |         tagsSource: UIEventSource<Record<string, string>>, | ||||||
|             allElements: ElementStorage |         mangroveIdentity?: MangroveIdentity, | ||||||
|             mangroveIdentity?: MangroveIdentity |  | ||||||
|         }, |  | ||||||
|         options?: { |         options?: { | ||||||
|             nameKey?: "name" | string |             nameKey?: "name" | string | ||||||
|             fallbackName?: string |             fallbackName?: string | ||||||
|  | @ -80,8 +76,7 @@ export default class FeatureReviews { | ||||||
|     ) { |     ) { | ||||||
|         const centerLonLat = GeoOperations.centerpointCoordinates(feature) |         const centerLonLat = GeoOperations.centerpointCoordinates(feature) | ||||||
|         ;[this._lon, this._lat] = centerLonLat |         ;[this._lon, this._lat] = centerLonLat | ||||||
|         this._identity = |         this._identity = mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined)) | ||||||
|             state?.mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined)) |  | ||||||
|         const nameKey = options?.nameKey ?? "name" |         const nameKey = options?.nameKey ?? "name" | ||||||
| 
 | 
 | ||||||
|         if (feature.geometry.type === "Point") { |         if (feature.geometry.type === "Point") { | ||||||
|  | @ -108,9 +103,7 @@ export default class FeatureReviews { | ||||||
| 
 | 
 | ||||||
|             this._uncertainty = options?.uncertaintyRadius ?? maxDistance |             this._uncertainty = options?.uncertaintyRadius ?? maxDistance | ||||||
|         } |         } | ||||||
|         this._name = state.allElements |         this._name = tagsSource            .map((tags) => tags[nameKey] ?? options?.fallbackName) | ||||||
|             .getEventSourceById(feature.properties.id) |  | ||||||
|             .map((tags) => tags[nameKey] ?? options?.fallbackName) |  | ||||||
| 
 | 
 | ||||||
|         this.subjectUri = this.ConstructSubjectUri() |         this.subjectUri = this.ConstructSubjectUri() | ||||||
| 
 | 
 | ||||||
|  | @ -136,11 +129,9 @@ export default class FeatureReviews { | ||||||
|      * Construct a featureReviewsFor or fetches it from the cache |      * Construct a featureReviewsFor or fetches it from the cache | ||||||
|      */ |      */ | ||||||
|     public static construct( |     public static construct( | ||||||
|         feature: Feature<Geometry, OsmTags>, |         feature: Feature, | ||||||
|         state: { |         tagsSource: UIEventSource<Record<string, string>>, | ||||||
|             allElements: ElementStorage |         mangroveIdentity?: MangroveIdentity, | ||||||
|             mangroveIdentity?: MangroveIdentity |  | ||||||
|         }, |  | ||||||
|         options?: { |         options?: { | ||||||
|             nameKey?: "name" | string |             nameKey?: "name" | string | ||||||
|             fallbackName?: string |             fallbackName?: string | ||||||
|  | @ -152,7 +143,7 @@ export default class FeatureReviews { | ||||||
|         if (cached !== undefined) { |         if (cached !== undefined) { | ||||||
|             return cached |             return cached | ||||||
|         } |         } | ||||||
|         const featureReviews = new FeatureReviews(feature, state, options) |         const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options) | ||||||
|         FeatureReviews._featureReviewsCache[key] = featureReviews |         FeatureReviews._featureReviewsCache[key] = featureReviews | ||||||
|         return featureReviews |         return featureReviews | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,14 +1,90 @@ | ||||||
| import { UIEventSource } from "../Logic/UIEventSource" | import { UIEventSource } from "../Logic/UIEventSource" | ||||||
| import LayerConfig from "./ThemeConfig/LayerConfig" | import LayerConfig from "./ThemeConfig/LayerConfig" | ||||||
| import { TagsFilter } from "../Logic/Tags/TagsFilter" | import { OsmConnection } from "../Logic/Osm/OsmConnection" | ||||||
|  | import { LocalStorageSource } from "../Logic/Web/LocalStorageSource" | ||||||
|  | import { QueryParameters } from "../Logic/Web/QueryParameters" | ||||||
| 
 | 
 | ||||||
| export interface FilterState { | export default class FilteredLayer { | ||||||
|     currentFilter: TagsFilter |     /** | ||||||
|     state: string | number |      * Wether or not the specified layer is shown | ||||||
| } |      */ | ||||||
| 
 |  | ||||||
| export default interface FilteredLayer { |  | ||||||
|     readonly isDisplayed: UIEventSource<boolean> |     readonly isDisplayed: UIEventSource<boolean> | ||||||
|     readonly appliedFilters: UIEventSource<Map<string, FilterState>> |     /** | ||||||
|  |      * Maps the filter.option.id onto the actual used state | ||||||
|  |      */ | ||||||
|  |     readonly appliedFilters: Map<string, UIEventSource<undefined | number | string>> | ||||||
|     readonly layerDef: LayerConfig |     readonly layerDef: LayerConfig | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         layer: LayerConfig, | ||||||
|  |         appliedFilters?: Map<string, UIEventSource<undefined | number | string>>, | ||||||
|  |         isDisplayed?: UIEventSource<boolean> | ||||||
|  |     ) { | ||||||
|  |         this.layerDef = layer | ||||||
|  |         this.isDisplayed = isDisplayed ?? new UIEventSource(true) | ||||||
|  |         this.appliedFilters = | ||||||
|  |             appliedFilters ?? new Map<string, UIEventSource<number | string | undefined>>() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Creates a FilteredLayer which is tied into the QueryParameters and/or user preferences | ||||||
|  |      */ | ||||||
|  |     public static initLinkedState( | ||||||
|  |         layer: LayerConfig, | ||||||
|  |         context: string, | ||||||
|  |         osmConnection: OsmConnection | ||||||
|  |     ) { | ||||||
|  |         let isDisplayed: UIEventSource<boolean> | ||||||
|  |         if (layer.syncSelection === "local") { | ||||||
|  |             isDisplayed = LocalStorageSource.GetParsed( | ||||||
|  |                 context + "-layer-" + layer.id + "-enabled", | ||||||
|  |                 layer.shownByDefault | ||||||
|  |             ) | ||||||
|  |         } else if (layer.syncSelection === "theme-only") { | ||||||
|  |             isDisplayed = FilteredLayer.getPref( | ||||||
|  |                 osmConnection, | ||||||
|  |                 context + "-layer-" + layer.id + "-enabled", | ||||||
|  |                 layer | ||||||
|  |             ) | ||||||
|  |         } else if (layer.syncSelection === "global") { | ||||||
|  |             isDisplayed = FilteredLayer.getPref( | ||||||
|  |                 osmConnection, | ||||||
|  |                 "layer-" + layer.id + "-enabled", | ||||||
|  |                 layer | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             isDisplayed = QueryParameters.GetBooleanQueryParameter( | ||||||
|  |                 "layer-" + layer.id, | ||||||
|  |                 layer.shownByDefault, | ||||||
|  |                 "Whether or not layer " + layer.id + " is shown" | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const appliedFilters = new Map<string, UIEventSource<undefined | number | string>>() | ||||||
|  |         for (const subfilter of layer.filters) { | ||||||
|  |             appliedFilters.set(subfilter.id, subfilter.initState()) | ||||||
|  |         } | ||||||
|  |         return new FilteredLayer(layer, appliedFilters, isDisplayed) | ||||||
|  |     } | ||||||
|  |     private static getPref( | ||||||
|  |         osmConnection: OsmConnection, | ||||||
|  |         key: string, | ||||||
|  |         layer: LayerConfig | ||||||
|  |     ): UIEventSource<boolean> { | ||||||
|  |         return osmConnection.GetPreference(key, layer.shownByDefault + "").sync( | ||||||
|  |             (v) => { | ||||||
|  |                 if (v === undefined) { | ||||||
|  |                     return undefined | ||||||
|  |                 } | ||||||
|  |                 return v === "true" | ||||||
|  |             }, | ||||||
|  |             [], | ||||||
|  |             (b) => { | ||||||
|  |                 if (b === undefined) { | ||||||
|  |                     return undefined | ||||||
|  |                 } | ||||||
|  |                 return "" + b | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,9 +1,10 @@ | ||||||
| import { Translation, TypedTranslation } from "../UI/i18n/Translation" | import { Translation, TypedTranslation } from "../UI/i18n/Translation" | ||||||
| import { FilterState } from "./FilteredLayer" |  | ||||||
| import { Tag } from "../Logic/Tags/Tag" | import { Tag } from "../Logic/Tags/Tag" | ||||||
|  | import { TagsFilter } from "../Logic/Tags/TagsFilter" | ||||||
| 
 | 
 | ||||||
| export interface GlobalFilter { | export interface GlobalFilter { | ||||||
|     filter: FilterState |     osmTags: TagsFilter | ||||||
|  |     state: number | string | undefined | ||||||
|     id: string |     id: string | ||||||
|     onNewPoint: { |     onNewPoint: { | ||||||
|         safetyCheck: Translation |         safetyCheck: Translation | ||||||
|  |  | ||||||
|  | @ -5,8 +5,10 @@ import { RasterLayerPolygon } from "./RasterLayers" | ||||||
| export interface MapProperties { | export interface MapProperties { | ||||||
|     readonly location: UIEventSource<{ lon: number; lat: number }> |     readonly location: UIEventSource<{ lon: number; lat: number }> | ||||||
|     readonly zoom: UIEventSource<number> |     readonly zoom: UIEventSource<number> | ||||||
|     readonly bounds: Store<BBox> |     readonly bounds: UIEventSource<BBox> | ||||||
|     readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined> |     readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined> | ||||||
|     readonly maxbounds: UIEventSource<undefined | BBox> |     readonly maxbounds: UIEventSource<undefined | BBox> | ||||||
|     readonly allowMoving: UIEventSource<true | boolean> |     readonly allowMoving: UIEventSource<true | boolean> | ||||||
|  | 
 | ||||||
|  |     readonly allowZooming: UIEventSource<true | boolean> | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -36,6 +36,14 @@ export class AvailableRasterLayers { | ||||||
|         geometry: BBox.global.asGeometry(), |         geometry: BBox.global.asGeometry(), | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static readonly maplibre: RasterLayerPolygon = { | ||||||
|  |         type: "Feature", | ||||||
|  |         properties: <any>{ | ||||||
|  |             name: "MapLibre", | ||||||
|  |             url: null, | ||||||
|  |         }, | ||||||
|  |         geometry: BBox.global.asGeometry(), | ||||||
|  |     } | ||||||
|     public static layersAvailableAt( |     public static layersAvailableAt( | ||||||
|         location: Store<{ lon: number; lat: number }> |         location: Store<{ lon: number; lat: number }> | ||||||
|     ): Store<RasterLayerPolygon[]> { |     ): Store<RasterLayerPolygon[]> { | ||||||
|  | @ -58,6 +66,7 @@ export class AvailableRasterLayers { | ||||||
|                     return GeoOperations.inside(lonlat, eliPolygon) |                     return GeoOperations.inside(lonlat, eliPolygon) | ||||||
|                 }) |                 }) | ||||||
|                 matching.unshift(AvailableRasterLayers.osmCarto) |                 matching.unshift(AvailableRasterLayers.osmCarto) | ||||||
|  |                 matching.unshift(AvailableRasterLayers.maplibre) | ||||||
|                 matching.push(...AvailableRasterLayers.globalLayers) |                 matching.push(...AvailableRasterLayers.globalLayers) | ||||||
|                 return matching |                 return matching | ||||||
|             }) |             }) | ||||||
|  |  | ||||||
|  | @ -5,7 +5,6 @@ import Translations from "../../UI/i18n/Translations" | ||||||
| import { TagUtils } from "../../Logic/Tags/TagUtils" | import { TagUtils } from "../../Logic/Tags/TagUtils" | ||||||
| import { TagConfigJson } from "./Json/TagConfigJson" | import { TagConfigJson } from "./Json/TagConfigJson" | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" | import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import { FilterState } from "../FilteredLayer" |  | ||||||
| import { QueryParameters } from "../../Logic/Web/QueryParameters" | import { QueryParameters } from "../../Logic/Web/QueryParameters" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { RegexTag } from "../../Logic/Tags/RegexTag" | import { RegexTag } from "../../Logic/Tags/RegexTag" | ||||||
|  | @ -144,14 +143,7 @@ export default class FilterConfig { | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public initState(): UIEventSource<FilterState> { |     public initState(): UIEventSource<undefined | number | string> { | ||||||
|         function reset(state: FilterState): string { |  | ||||||
|             if (state === undefined) { |  | ||||||
|                 return "" |  | ||||||
|             } |  | ||||||
|             return "" + state.state |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let defaultValue = "" |         let defaultValue = "" | ||||||
|         if (this.options.length > 1) { |         if (this.options.length > 1) { | ||||||
|             defaultValue = "" + (this.defaultSelection ?? 0) |             defaultValue = "" + (this.defaultSelection ?? 0) | ||||||
|  | @ -159,6 +151,8 @@ export default class FilterConfig { | ||||||
|             // Only a single option
 |             // Only a single option
 | ||||||
|             if (this.defaultSelection === 0) { |             if (this.defaultSelection === 0) { | ||||||
|                 defaultValue = "true" |                 defaultValue = "true" | ||||||
|  |             } else { | ||||||
|  |                 defaultValue = "false" | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         const qp = QueryParameters.GetQueryParameter( |         const qp = QueryParameters.GetQueryParameter( | ||||||
|  | @ -168,12 +162,6 @@ export default class FilterConfig { | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         if (this.options.length > 1) { |         if (this.options.length > 1) { | ||||||
|             // This is a multi-option filter; state should be a number which selects the correct entry
 |  | ||||||
|             const possibleStates: FilterState[] = this.options.map((opt, i) => ({ |  | ||||||
|                 currentFilter: opt.osmTags, |  | ||||||
|                 state: i, |  | ||||||
|             })) |  | ||||||
| 
 |  | ||||||
|             // We map the query parameter for this case
 |             // We map the query parameter for this case
 | ||||||
|             return qp.sync( |             return qp.sync( | ||||||
|                 (str) => { |                 (str) => { | ||||||
|  | @ -182,62 +170,29 @@ export default class FilterConfig { | ||||||
|                         // Nope, not a correct number!
 |                         // Nope, not a correct number!
 | ||||||
|                         return undefined |                         return undefined | ||||||
|                     } |                     } | ||||||
|                     return possibleStates[parsed] |                     return parsed | ||||||
|                 }, |                 }, | ||||||
|                 [], |                 [], | ||||||
|                 reset |                 (n) => "" + n | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const option = this.options[0] |         const option = this.options[0] | ||||||
| 
 | 
 | ||||||
|         if (option.fields.length > 0) { |         if (option.fields.length > 0) { | ||||||
|             return qp.sync( |             return qp | ||||||
|                 (str) => { |  | ||||||
|                     // There are variables in play!
 |  | ||||||
|                     // str should encode a json-hash
 |  | ||||||
|                     try { |  | ||||||
|                         const props = JSON.parse(str) |  | ||||||
| 
 |  | ||||||
|                         const origTags = option.originalTagsSpec |  | ||||||
|                         const rewrittenTags = Utils.WalkJson(origTags, (v) => { |  | ||||||
|                             if (typeof v !== "string") { |  | ||||||
|                                 return v |  | ||||||
|                             } |  | ||||||
|                             for (const key in props) { |  | ||||||
|                                 v = (<string>v).replace("{" + key + "}", props[key]) |  | ||||||
|                             } |  | ||||||
|                             return v |  | ||||||
|                         }) |  | ||||||
|                         const parsed = TagUtils.Tag(rewrittenTags) |  | ||||||
|                         return <FilterState>{ |  | ||||||
|                             currentFilter: parsed, |  | ||||||
|                             state: str, |  | ||||||
|                         } |  | ||||||
|                     } catch (e) { |  | ||||||
|                         return undefined |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 [], |  | ||||||
|                 reset |  | ||||||
|             ) |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // The last case is pretty boring: it is checked or it isn't
 |  | ||||||
|         const filterState: FilterState = { |  | ||||||
|             currentFilter: option.osmTags, |  | ||||||
|             state: "true", |  | ||||||
|         } |  | ||||||
|         return qp.sync( |         return qp.sync( | ||||||
|             (str) => { |             (str) => { | ||||||
|                 // Only a single option exists here
 |                 // Only a single option exists here
 | ||||||
|                 if (str === "true") { |                 if (str === "true") { | ||||||
|                     return filterState |                     return 0 | ||||||
|                 } |                 } | ||||||
|                 return undefined |                 return undefined | ||||||
|             }, |             }, | ||||||
|             [], |             [], | ||||||
|             reset |             (n) => (n === undefined ? "false" : "true") | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -205,25 +205,6 @@ export interface LayoutConfigJson { | ||||||
|           } |           } | ||||||
|     )[] |     )[] | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * If defined, data will be clustered. |  | ||||||
|      * Defaults to {maxZoom: 16, minNeeded: 500} |  | ||||||
|      */ |  | ||||||
|     clustering?: |  | ||||||
|         | { |  | ||||||
|               /** |  | ||||||
|                * All zoom levels above 'maxzoom' are not clustered anymore. |  | ||||||
|                * Defaults to 18 |  | ||||||
|                */ |  | ||||||
|               maxZoom?: number |  | ||||||
|               /** |  | ||||||
|                * The number of elements per tile needed to start clustering |  | ||||||
|                * If clustering is defined, defaults to 250 |  | ||||||
|                */ |  | ||||||
|               minNeededElements?: number |  | ||||||
|           } |  | ||||||
|         | false |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * The URL of a custom CSS stylesheet to modify the layout |      * The URL of a custom CSS stylesheet to modify the layout | ||||||
|      */ |      */ | ||||||
|  |  | ||||||
|  | @ -40,10 +40,6 @@ export default class LayoutConfig implements LayoutInformation { | ||||||
|     public defaultBackgroundId?: string |     public defaultBackgroundId?: string | ||||||
|     public layers: LayerConfig[] |     public layers: LayerConfig[] | ||||||
|     public tileLayerSources: TilesourceConfig[] |     public tileLayerSources: TilesourceConfig[] | ||||||
|     public readonly clustering?: { |  | ||||||
|         maxZoom: number |  | ||||||
|         minNeededElements: number |  | ||||||
|     } |  | ||||||
|     public readonly hideFromOverview: boolean |     public readonly hideFromOverview: boolean | ||||||
|     public lockLocation: boolean | [[number, number], [number, number]] |     public lockLocation: boolean | [[number, number], [number, number]] | ||||||
|     public readonly enableUserBadge: boolean |     public readonly enableUserBadge: boolean | ||||||
|  | @ -188,22 +184,6 @@ export default class LayoutConfig implements LayoutInformation { | ||||||
|             context + ".extraLink" |             context + ".extraLink" | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         this.clustering = { |  | ||||||
|             maxZoom: 16, |  | ||||||
|             minNeededElements: 250, |  | ||||||
|         } |  | ||||||
|         if (json.clustering === false) { |  | ||||||
|             this.clustering = { |  | ||||||
|                 maxZoom: 0, |  | ||||||
|                 minNeededElements: 100000, |  | ||||||
|             } |  | ||||||
|         } else if (json.clustering) { |  | ||||||
|             this.clustering = { |  | ||||||
|                 maxZoom: json.clustering.maxZoom ?? 18, |  | ||||||
|                 minNeededElements: json.clustering.minNeededElements ?? 250, |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         this.hideFromOverview = json.hideFromOverview ?? false |         this.hideFromOverview = json.hideFromOverview ?? false | ||||||
|         this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined |         this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined | ||||||
|         this.enableUserBadge = json.enableUserBadge ?? true |         this.enableUserBadge = json.enableUserBadge ?? true | ||||||
|  |  | ||||||
|  | @ -11,8 +11,6 @@ import { FixedUiElement } from "../../UI/Base/FixedUiElement" | ||||||
| import Img from "../../UI/Base/Img" | import Img from "../../UI/Base/Img" | ||||||
| import Combine from "../../UI/Base/Combine" | import Combine from "../../UI/Base/Combine" | ||||||
| import { VariableUiElement } from "../../UI/Base/VariableUIElement" | import { VariableUiElement } from "../../UI/Base/VariableUIElement" | ||||||
| import { OsmTags } from "../OsmFeature" |  | ||||||
| import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" |  | ||||||
| 
 | 
 | ||||||
| export default class PointRenderingConfig extends WithContextLoader { | export default class PointRenderingConfig extends WithContextLoader { | ||||||
|     private static readonly allowed_location_codes = new Set<string>([ |     private static readonly allowed_location_codes = new Set<string>([ | ||||||
|  | @ -176,7 +174,7 @@ export default class PointRenderingConfig extends WithContextLoader { | ||||||
|         return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin) |         return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GetSimpleIcon(tags: Store<OsmTags>): BaseUIElement { |     public GetSimpleIcon(tags: Store<Record<string, string>>): BaseUIElement { | ||||||
|         const self = this |         const self = this | ||||||
|         if (this.icon === undefined) { |         if (this.icon === undefined) { | ||||||
|             return undefined |             return undefined | ||||||
|  | @ -187,7 +185,7 @@ export default class PointRenderingConfig extends WithContextLoader { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public RenderIcon( |     public RenderIcon( | ||||||
|         tags: Store<OsmTags>, |         tags: Store<Record<string, string>>, | ||||||
|         clickable: boolean, |         clickable: boolean, | ||||||
|         options?: { |         options?: { | ||||||
|             noSize?: false | boolean |             noSize?: false | boolean | ||||||
|  | @ -277,7 +275,7 @@ export default class PointRenderingConfig extends WithContextLoader { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private GetBadges(tags: Store<OsmTags>): BaseUIElement { |     private GetBadges(tags: Store<Record<string, string>>): BaseUIElement { | ||||||
|         if (this.iconBadges.length === 0) { |         if (this.iconBadges.length === 0) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|  | @ -309,7 +307,7 @@ export default class PointRenderingConfig extends WithContextLoader { | ||||||
|         ).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0") |         ).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private GetLabel(tags: Store<OsmTags>): BaseUIElement { |     private GetLabel(tags: Store<Record<string, string>>): BaseUIElement { | ||||||
|         if (this.label === undefined) { |         if (this.label === undefined) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|  |  | ||||||
							
								
								
									
										278
									
								
								Models/ThemeViewState.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								Models/ThemeViewState.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,278 @@ | ||||||
|  | import LayoutConfig from "./ThemeConfig/LayoutConfig" | ||||||
|  | import { SpecialVisualizationState } from "../UI/SpecialVisualization" | ||||||
|  | import { Changes } from "../Logic/Osm/Changes" | ||||||
|  | import { Store, UIEventSource } from "../Logic/UIEventSource" | ||||||
|  | import FeatureSource, { | ||||||
|  |     IndexedFeatureSource, | ||||||
|  |     WritableFeatureSource, | ||||||
|  | } from "../Logic/FeatureSource/FeatureSource" | ||||||
|  | import { OsmConnection } from "../Logic/Osm/OsmConnection" | ||||||
|  | import { DefaultGuiState } from "../UI/DefaultGuiState" | ||||||
|  | import { MapProperties } from "./MapProperties" | ||||||
|  | import LayerState from "../Logic/State/LayerState" | ||||||
|  | import { Feature } from "geojson" | ||||||
|  | import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" | ||||||
|  | import { Map as MlMap } from "maplibre-gl" | ||||||
|  | import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning" | ||||||
|  | import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor" | ||||||
|  | import { GeoLocationState } from "../Logic/State/GeoLocationState" | ||||||
|  | import FeatureSwitchState from "../Logic/State/FeatureSwitchState" | ||||||
|  | import { QueryParameters } from "../Logic/Web/QueryParameters" | ||||||
|  | import UserRelatedState from "../Logic/State/UserRelatedState" | ||||||
|  | import LayerConfig from "./ThemeConfig/LayerConfig" | ||||||
|  | import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" | ||||||
|  | import { AvailableRasterLayers, RasterLayerPolygon } from "./RasterLayers" | ||||||
|  | import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource" | ||||||
|  | import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" | ||||||
|  | import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore" | ||||||
|  | import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter" | ||||||
|  | import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage" | ||||||
|  | import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource" | ||||||
|  | import ShowDataLayer from "../UI/Map/ShowDataLayer" | ||||||
|  | import TitleHandler from "../Logic/Actors/TitleHandler" | ||||||
|  | import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor" | ||||||
|  | import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader" | ||||||
|  | import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater" | ||||||
|  | import { BBox } from "../Logic/BBox" | ||||||
|  | import Constants from "./Constants" | ||||||
|  | import Hotkeys from "../UI/Base/Hotkeys" | ||||||
|  | import Translations from "../UI/i18n/Translations" | ||||||
|  | import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * | ||||||
|  |  * The themeviewState contains all the state needed for the themeViewGUI. | ||||||
|  |  * | ||||||
|  |  * This is pretty much the 'brain' or the HQ of MapComplete | ||||||
|  |  * | ||||||
|  |  * It ties up all the needed elements and starts some actors. | ||||||
|  |  */ | ||||||
|  | export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|  |     readonly layout: LayoutConfig | ||||||
|  |     readonly map: UIEventSource<MlMap> | ||||||
|  |     readonly changes: Changes | ||||||
|  |     readonly featureSwitches: FeatureSwitchState | ||||||
|  |     readonly featureSwitchIsTesting: Store<boolean> | ||||||
|  |     readonly featureSwitchUserbadge: Store<boolean> | ||||||
|  | 
 | ||||||
|  |     readonly featureProperties: FeaturePropertiesStore | ||||||
|  | 
 | ||||||
|  |     readonly osmConnection: OsmConnection | ||||||
|  |     readonly selectedElement: UIEventSource<Feature> | ||||||
|  |     readonly mapProperties: MapProperties | ||||||
|  | 
 | ||||||
|  |     readonly dataIsLoading: Store<boolean> // TODO
 | ||||||
|  |     readonly guistate: DefaultGuiState | ||||||
|  |     readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
 | ||||||
|  | 
 | ||||||
|  |     readonly historicalUserLocations: WritableFeatureSource | ||||||
|  |     readonly indexedFeatures: IndexedFeatureSource | ||||||
|  |     readonly layerState: LayerState | ||||||
|  |     readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | ||||||
|  |     readonly availableLayers: Store<RasterLayerPolygon[]> | ||||||
|  |     readonly selectedLayer: UIEventSource<LayerConfig> | ||||||
|  |     readonly userRelatedState: UserRelatedState | ||||||
|  |     readonly geolocation: GeoLocationHandler | ||||||
|  | 
 | ||||||
|  |     constructor(layout: LayoutConfig) { | ||||||
|  |         this.layout = layout | ||||||
|  |         this.guistate = new DefaultGuiState() | ||||||
|  |         this.map = new UIEventSource<MlMap>(undefined) | ||||||
|  |         const initial = new InitialMapPositioning(layout) | ||||||
|  |         this.mapProperties = new MapLibreAdaptor(this.map, initial) | ||||||
|  |         const geolocationState = new GeoLocationState() | ||||||
|  | 
 | ||||||
|  |         this.featureSwitches = new FeatureSwitchState(layout) | ||||||
|  |         this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting | ||||||
|  |         this.featureSwitchUserbadge = this.featureSwitches.featureSwitchUserbadge | ||||||
|  | 
 | ||||||
|  |         this.osmConnection = new OsmConnection({ | ||||||
|  |             dryRun: this.featureSwitches.featureSwitchIsTesting, | ||||||
|  |             fakeUser: this.featureSwitches.featureSwitchFakeUser.data, | ||||||
|  |             oauth_token: QueryParameters.GetQueryParameter( | ||||||
|  |                 "oauth_token", | ||||||
|  |                 undefined, | ||||||
|  |                 "Used to complete the login" | ||||||
|  |             ), | ||||||
|  |             osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data, | ||||||
|  |         }) | ||||||
|  |         this.userRelatedState = new UserRelatedState(this.osmConnection, layout?.language) | ||||||
|  |         this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element") | ||||||
|  |         this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer") | ||||||
|  |         this.geolocation = new GeoLocationHandler( | ||||||
|  |             geolocationState, | ||||||
|  |             this.selectedElement, | ||||||
|  |             this.mapProperties, | ||||||
|  |             this.userRelatedState.gpsLocationHistoryRetentionTime | ||||||
|  |         ) | ||||||
|  |         this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location) | ||||||
|  | 
 | ||||||
|  |         this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id) | ||||||
|  |         const indexedElements = new LayoutSource( | ||||||
|  |             layout.layers, | ||||||
|  |             this.featureSwitches, | ||||||
|  |             new StaticFeatureSource([]), | ||||||
|  |             this.mapProperties, | ||||||
|  |             this.osmConnection.Backend(), | ||||||
|  |             (id) => this.layerState.filteredLayers.get(id).isDisplayed | ||||||
|  |         ) | ||||||
|  |         this.featureProperties = new FeaturePropertiesStore(indexedElements) | ||||||
|  |         const perLayer = new PerLayerFeatureSourceSplitter( | ||||||
|  |             Array.from(this.layerState.filteredLayers.values()), | ||||||
|  |             indexedElements, | ||||||
|  |             { | ||||||
|  |                 constructStore: (features, layer) => new GeoIndexedStoreForLayer(features, layer), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         this.perLayer = perLayer.perLayer | ||||||
|  | 
 | ||||||
|  |         this.perLayer.forEach((fs) => { | ||||||
|  |             new SaveFeatureSourceToLocalStorage(fs.layer.layerDef.id, 15, fs) | ||||||
|  | 
 | ||||||
|  |             const filtered = new FilteringFeatureSource( | ||||||
|  |                 fs.layer, | ||||||
|  |                 fs, | ||||||
|  |                 (id) => this.featureProperties.getStore(id), | ||||||
|  |                 this.layerState.globalFilters | ||||||
|  |             ) | ||||||
|  |             const doShowLayer = this.mapProperties.zoom.map( | ||||||
|  |                 (z) => | ||||||
|  |                     (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), | ||||||
|  |                 [fs.layer.isDisplayed] | ||||||
|  |             ) | ||||||
|  |             doShowLayer.addCallbackAndRunD((doShow) => | ||||||
|  |                 console.log( | ||||||
|  |                     "Layer", | ||||||
|  |                     fs.layer.layerDef.id, | ||||||
|  |                     "is", | ||||||
|  |                     doShow, | ||||||
|  |                     this.mapProperties.zoom.data, | ||||||
|  |                     fs.layer.layerDef.minzoom | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             new ShowDataLayer(this.map, { | ||||||
|  |                 layer: fs.layer.layerDef, | ||||||
|  |                 features: filtered, | ||||||
|  |                 doShowLayer, | ||||||
|  |                 selectedElement: this.selectedElement, | ||||||
|  |                 selectedLayer: this.selectedLayer, | ||||||
|  |                 fetchStore: (id) => this.featureProperties.getStore(id), | ||||||
|  |             }) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         this.changes = new Changes( | ||||||
|  |             { | ||||||
|  |                 dryRun: this.featureSwitches.featureSwitchIsTesting, | ||||||
|  |                 allElements: indexedElements, | ||||||
|  |                 featurePropertiesStore: this.featureProperties, | ||||||
|  |                 osmConnection: this.osmConnection, | ||||||
|  |                 historicalUserLocations: this.geolocation.historicalUserLocations, | ||||||
|  |             }, | ||||||
|  |             layout?.isLeftRightSensitive() ?? false | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         this.initActors() | ||||||
|  |         this.drawSpecialLayers() | ||||||
|  |         this.initHotkeys() | ||||||
|  |         this.miscSetup() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Various small methods that need to be called | ||||||
|  |      */ | ||||||
|  |     private miscSetup() { | ||||||
|  |         this.userRelatedState.markLayoutAsVisited(this.layout) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private initHotkeys() { | ||||||
|  |         Hotkeys.RegisterHotkey( | ||||||
|  |             { nomod: "Escape", onUp: true }, | ||||||
|  |             Translations.t.hotkeyDocumentation.closeSidebar, | ||||||
|  |             () => { | ||||||
|  |                 this.selectedElement.setData(undefined) | ||||||
|  |                 this.guistate.closeAll() | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add the special layers to the map | ||||||
|  |      * @private | ||||||
|  |      */ | ||||||
|  |     private drawSpecialLayers() { | ||||||
|  |         type AddedByDefaultTypes = typeof Constants.added_by_default[number] | ||||||
|  |         /** | ||||||
|  |          * A listing which maps the layerId onto the featureSource | ||||||
|  |          */ | ||||||
|  |         const empty = [] | ||||||
|  |         const specialLayers: Record<AddedByDefaultTypes | "current_view", FeatureSource> = { | ||||||
|  |             home_location: this.userRelatedState.homeLocation, | ||||||
|  |             gps_location: this.geolocation.currentUserLocation, | ||||||
|  |             gps_location_history: this.geolocation.historicalUserLocations, | ||||||
|  |             gps_track: this.geolocation.historicalUserLocationsTrack, | ||||||
|  |             selected_element: new StaticFeatureSource( | ||||||
|  |                 this.selectedElement.map((f) => (f === undefined ? empty : [f])) | ||||||
|  |             ), | ||||||
|  |             range: new StaticFeatureSource( | ||||||
|  |                 this.mapProperties.maxbounds.map((bbox) => | ||||||
|  |                     bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })] | ||||||
|  |                 ) | ||||||
|  |             ), | ||||||
|  |             current_view: new StaticFeatureSource( | ||||||
|  |                 this.mapProperties.bounds.map((bbox) => | ||||||
|  |                     bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "current_view" })] | ||||||
|  |                 ) | ||||||
|  |             ), | ||||||
|  |         } | ||||||
|  |         if (this.layout?.lockLocation) { | ||||||
|  |             const bbox = new BBox(this.layout.lockLocation) | ||||||
|  |             this.mapProperties.maxbounds.setData(bbox) | ||||||
|  |             ShowDataLayer.showRange( | ||||||
|  |                 this.map, | ||||||
|  |                 new StaticFeatureSource([bbox.asGeoJson({})]), | ||||||
|  |                 this.featureSwitches.featureSwitchIsTesting | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.layerState.filteredLayers | ||||||
|  |             .get("range") | ||||||
|  |             ?.isDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true) | ||||||
|  | 
 | ||||||
|  |         this.layerState.filteredLayers.forEach((flayer) => { | ||||||
|  |             const features = specialLayers[flayer.layerDef.id] | ||||||
|  |             if (features === undefined) { | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |             new ShowDataLayer(this.map, { | ||||||
|  |                 features, | ||||||
|  |                 doShowLayer: flayer.isDisplayed, | ||||||
|  |                 layer: flayer.layerDef, | ||||||
|  |                 selectedElement: this.selectedElement, | ||||||
|  |                 selectedLayer: this.selectedLayer, | ||||||
|  |             }) | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Setup various services for which no reference are needed | ||||||
|  |      * @private | ||||||
|  |      */ | ||||||
|  |     private initActors() { | ||||||
|  |         // Various actors that we don't need to reference
 | ||||||
|  |         new TitleHandler( | ||||||
|  |             this.selectedElement, | ||||||
|  |             this.selectedLayer, | ||||||
|  |             this.featureProperties, | ||||||
|  |             this.layout | ||||||
|  |         ) | ||||||
|  |         new ChangeToElementsActor(this.changes, this.featureProperties) | ||||||
|  |         new PendingChangesUploader(this.changes, this.selectedElement) | ||||||
|  |         new SelectedElementTagsUpdater({ | ||||||
|  |             allElements: this.featureProperties, | ||||||
|  |             changes: this.changes, | ||||||
|  |             selectedElement: this.selectedElement, | ||||||
|  |             layoutToUse: this.layout, | ||||||
|  |             osmConnection: this.osmConnection, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										16
									
								
								State.ts
									
										
									
									
									
								
							|  | @ -1,16 +0,0 @@ | ||||||
| import LayoutConfig from "./Models/ThemeConfig/LayoutConfig" |  | ||||||
| import FeaturePipelineState from "./Logic/State/FeaturePipelineState" |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Contains the global state: a bunch of UI-event sources |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| export default class State extends FeaturePipelineState { |  | ||||||
|     /* The singleton of the global state |  | ||||||
|      */ |  | ||||||
|     public static state: FeaturePipelineState |  | ||||||
| 
 |  | ||||||
|     constructor(layoutToUse: LayoutConfig) { |  | ||||||
|         super(layoutToUse) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										13
									
								
								UI/Base/Checkbox.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								UI/Base/Checkbox.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import { UIEventSource } from "../../Logic/UIEventSource.js"; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * For some stupid reason, it is very hard to bind inputs | ||||||
|  |    */ | ||||||
|  |   export let selected: UIEventSource<boolean>; | ||||||
|  |   let _c: boolean = selected.data ?? true; | ||||||
|  |   $: selected.setData(_c) | ||||||
|  |    | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <input type="checkbox" bind:checked={_c} /> | ||||||
							
								
								
									
										15
									
								
								UI/Base/Dropdown.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								UI/Base/Dropdown.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import { UIEventSource } from "../../Logic/UIEventSource.js"; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * For some stupid reason, it is very hard to bind inputs | ||||||
|  |    */ | ||||||
|  |   export let value: UIEventSource<number>; | ||||||
|  |   let i: number = value.data; | ||||||
|  |   $: value.setData(i) | ||||||
|  |    | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <select bind:value={i} > | ||||||
|  |   <slot></slot> | ||||||
|  | </select> | ||||||
|  | @ -1,14 +1,23 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; |   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||||
|  |   import { onDestroy } from "svelte"; | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here |    * For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here | ||||||
|    */ |    */ | ||||||
|   export let condition: UIEventSource<boolean>; |   export let condition: UIEventSource<boolean>; | ||||||
|   let _c = condition.data; |   let _c = condition.data; | ||||||
|   condition.addCallback(c => _c = c) |   onDestroy(condition.addCallback(c => { | ||||||
|  |     /* Do _not_ abbreviate this as `.addCallback(c => _c = c)`. This is the same as writing `.addCallback(c => {return _c = c})`,  | ||||||
|  |     which will _unregister_ the callback if `c = true`! */ | ||||||
|  |     _c = c; | ||||||
|  |     return false | ||||||
|  |   })) | ||||||
|  |    | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if _c} | {#if _c} | ||||||
|   <slot></slot> |   <slot></slot> | ||||||
|  |   {:else} | ||||||
|  |   <slot name="else"></slot> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
							
								
								
									
										18
									
								
								UI/Base/IfNot.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								UI/Base/IfNot.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||||
|  |   import { onDestroy } from "svelte"; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here | ||||||
|  |    */ | ||||||
|  |   export let condition: UIEventSource<boolean>; | ||||||
|  |   let _c = !condition.data; | ||||||
|  |   onDestroy(condition.addCallback(c => { | ||||||
|  |     _c = !c; | ||||||
|  |     return false | ||||||
|  |   })) | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | {#if _c} | ||||||
|  |   <slot></slot> | ||||||
|  | {/if} | ||||||
							
								
								
									
										13
									
								
								UI/Base/Loading.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								UI/Base/Loading.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | <script> | ||||||
|  |   import ToSvelte from "./ToSvelte.svelte"; | ||||||
|  |   import Svg from "../../Svg"; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="pl-2 p-1 flex"> | ||||||
|  |   <div class="animate-spin self-center w-6 h-6 min-w-6"> | ||||||
|  |   <ToSvelte construct={Svg.loading_ui}></ToSvelte> | ||||||
|  |   </div> | ||||||
|  |   <div class="ml-2"> | ||||||
|  |   <slot></slot> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | @ -8,6 +8,6 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| <div on:click={e => dispatch("click", e)} class="subtle-background block rounded-full min-w-10 h-10 pointer-events-auto m-0.5 md:m-1 p-1"> | <div on:click={e => dispatch("click", e)} class="subtle-background rounded-full min-w-10 w-fit h-10 m-0.5 md:m-1 p-1"> | ||||||
|   <slot class="m-4"></slot> |   <slot class="m-4"></slot> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | @ -1,18 +1,23 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import BaseUIElement from "../BaseUIElement.js" |   import BaseUIElement from "../BaseUIElement.js"; | ||||||
|   import { onMount } from "svelte" |   import { onDestroy, onMount } from "svelte"; | ||||||
| 
 |  | ||||||
|   export let construct: BaseUIElement | (() => BaseUIElement) |  | ||||||
|   let elem: HTMLElement |  | ||||||
| 
 | 
 | ||||||
|  |   export let construct: BaseUIElement | (() => BaseUIElement); | ||||||
|  |   let elem: HTMLElement; | ||||||
|  |   let html: HTMLElement; | ||||||
|   onMount(() => { |   onMount(() => { | ||||||
|     let html = |     const uiElem = typeof construct === "function" | ||||||
|       typeof construct === "function" |       ? construct() : construct; | ||||||
|         ? construct().ConstructElement() |     html =uiElem?.ConstructElement(); | ||||||
|         : construct.ConstructElement() |     if (html !== undefined) { | ||||||
|  |       elem.replaceWith(html); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   onDestroy(() => { | ||||||
|  |     html?.remove(); | ||||||
|  |   }); | ||||||
| 
 | 
 | ||||||
|     elem.replaceWith(html) |  | ||||||
|   }) |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <span bind:this={elem} /> | <span bind:this={elem} /> | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ import { OpenIdEditor, OpenJosm } from "./CopyrightPanel" | ||||||
| import Toggle from "../Input/Toggle" | import Toggle from "../Input/Toggle" | ||||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen" | import ScrollableFullScreen from "../Base/ScrollableFullScreen" | ||||||
| import { DefaultGuiState } from "../DefaultGuiState" | import { DefaultGuiState } from "../DefaultGuiState" | ||||||
|  | import DefaultGUI from "../DefaultGUI" | ||||||
| 
 | 
 | ||||||
| export class BackToThemeOverview extends Toggle { | export class BackToThemeOverview extends Toggle { | ||||||
|     constructor( |     constructor( | ||||||
|  | @ -42,6 +43,7 @@ export class ActionButtons extends Combine { | ||||||
|         readonly locationControl: Store<Loc> |         readonly locationControl: Store<Loc> | ||||||
|         readonly osmConnection: OsmConnection |         readonly osmConnection: OsmConnection | ||||||
|         readonly featureSwitchMoreQuests: Store<boolean> |         readonly featureSwitchMoreQuests: Store<boolean> | ||||||
|  |         readonly defaultGuiState: DefaultGuiState | ||||||
|     }) { |     }) { | ||||||
|         const imgSize = "h-6 w-6" |         const imgSize = "h-6 w-6" | ||||||
|         const iconStyle = "height: 1.5rem; width: 1.5rem" |         const iconStyle = "height: 1.5rem; width: 1.5rem" | ||||||
|  | @ -82,8 +84,8 @@ export class ActionButtons extends Combine { | ||||||
|                 Translations.t.translations.activateButton |                 Translations.t.translations.activateButton | ||||||
|             ).onClick(() => { |             ).onClick(() => { | ||||||
|                 ScrollableFullScreen.collapse() |                 ScrollableFullScreen.collapse() | ||||||
|                 DefaultGuiState.state.userInfoIsOpened.setData(true) |                 state.defaultGuiState.userInfoIsOpened.setData(true) | ||||||
|                 DefaultGuiState.state.userInfoFocusedQuestion.setData("translation-mode") |                 state.defaultGuiState.userInfoFocusedQuestion.setData("translation-mode") | ||||||
|             }), |             }), | ||||||
|         ]) |         ]) | ||||||
|         this.SetClass("block w-full link-no-underline") |         this.SetClass("block w-full link-no-underline") | ||||||
|  |  | ||||||
|  | @ -14,54 +14,53 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import Title from "../Base/Title" | import Title from "../Base/Title" | ||||||
| import { SubtleButton } from "../Base/SubtleButton" | import { SubtleButton } from "../Base/SubtleButton" | ||||||
| import Svg from "../../Svg" | import Svg from "../../Svg" | ||||||
| import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" |  | ||||||
| import { BBox } from "../../Logic/BBox" | import { BBox } from "../../Logic/BBox" | ||||||
| import Loc from "../../Models/Loc" |  | ||||||
| import Toggle from "../Input/Toggle" | import Toggle from "../Input/Toggle" | ||||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" | import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||||
| import Constants from "../../Models/Constants" | import Constants from "../../Models/Constants" | ||||||
| import ContributorCount from "../../Logic/ContributorCount" | import ContributorCount from "../../Logic/ContributorCount" | ||||||
| import Img from "../Base/Img" | import Img from "../Base/Img" | ||||||
| import { TypedTranslation } from "../i18n/Translation" | import { TypedTranslation } from "../i18n/Translation" | ||||||
|  | import GeoIndexedStore from "../../Logic/FeatureSource/Actors/GeoIndexedStore" | ||||||
| 
 | 
 | ||||||
| export class OpenIdEditor extends VariableUiElement { | export class OpenIdEditor extends VariableUiElement { | ||||||
|     constructor( |     constructor( | ||||||
|         state: { readonly locationControl: Store<Loc> }, |         mapProperties: { location: Store<{ lon: number; lat: number }>; zoom: Store<number> }, | ||||||
|         iconStyle?: string, |         iconStyle?: string, | ||||||
|         objectId?: string |         objectId?: string | ||||||
|     ) { |     ) { | ||||||
|         const t = Translations.t.general.attribution |         const t = Translations.t.general.attribution | ||||||
|         super( |         super( | ||||||
|             state.locationControl.map((location) => { |             mapProperties.location.map( | ||||||
|                 let elementSelect = "" |                 (location) => { | ||||||
|                 if (objectId !== undefined) { |                     let elementSelect = "" | ||||||
|                     const parts = objectId.split("/") |                     if (objectId !== undefined) { | ||||||
|                     const tp = parts[0] |                         const parts = objectId.split("/") | ||||||
|                     if ( |                         const tp = parts[0] | ||||||
|                         parts.length === 2 && |                         if ( | ||||||
|                         !isNaN(Number(parts[1])) && |                             parts.length === 2 && | ||||||
|                         (tp === "node" || tp === "way" || tp === "relation") |                             !isNaN(Number(parts[1])) && | ||||||
|                     ) { |                             (tp === "node" || tp === "way" || tp === "relation") | ||||||
|                         elementSelect = "&" + tp + "=" + parts[1] |                         ) { | ||||||
|  |                             elementSelect = "&" + tp + "=" + parts[1] | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                     const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${ | ||||||
|                 const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${ |                         mapProperties.zoom?.data ?? 0 | ||||||
|                     location?.zoom ?? 0 |                     }/${location?.lat ?? 0}/${location?.lon ?? 0}` | ||||||
|                 }/${location?.lat ?? 0}/${location?.lon ?? 0}` |                     return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, { | ||||||
|                 return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, { |                         url: idLink, | ||||||
|                     url: idLink, |                         newTab: true, | ||||||
|                     newTab: true, |                     }) | ||||||
|                 }) |                 }, | ||||||
|             }) |                 [mapProperties.zoom] | ||||||
|  |             ) | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class OpenJosm extends Combine { | export class OpenJosm extends Combine { | ||||||
|     constructor( |     constructor(osmConnection: OsmConnection, bounds: Store<BBox>, iconStyle?: string) { | ||||||
|         state: { osmConnection: OsmConnection; currentBounds: Store<BBox> }, |  | ||||||
|         iconStyle?: string |  | ||||||
|     ) { |  | ||||||
|         const t = Translations.t.general.attribution |         const t = Translations.t.general.attribution | ||||||
| 
 | 
 | ||||||
|         const josmState = new UIEventSource<string>(undefined) |         const josmState = new UIEventSource<string>(undefined) | ||||||
|  | @ -83,21 +82,21 @@ export class OpenJosm extends Combine { | ||||||
| 
 | 
 | ||||||
|         const toggle = new Toggle( |         const toggle = new Toggle( | ||||||
|             new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => { |             new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => { | ||||||
|                 const bounds: any = state.currentBounds.data |                 const bbox = bounds.data | ||||||
|                 if (bounds === undefined) { |                 if (bbox === undefined) { | ||||||
|                     return undefined |                     return | ||||||
|                 } |                 } | ||||||
|                 const top = bounds.getNorth() |                 const top = bbox.getNorth() | ||||||
|                 const bottom = bounds.getSouth() |                 const bottom = bbox.getSouth() | ||||||
|                 const right = bounds.getEast() |                 const right = bbox.getEast() | ||||||
|                 const left = bounds.getWest() |                 const left = bbox.getWest() | ||||||
|                 const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}` |                 const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}` | ||||||
|                 Utils.download(josmLink) |                 Utils.download(josmLink) | ||||||
|                     .then((answer) => josmState.setData(answer.replace(/\n/g, "").trim())) |                     .then((answer) => josmState.setData(answer.replace(/\n/g, "").trim())) | ||||||
|                     .catch((_) => josmState.setData("ERROR")) |                     .catch((_) => josmState.setData("ERROR")) | ||||||
|             }), |             }), | ||||||
|             undefined, |             undefined, | ||||||
|             state.osmConnection.userDetails.map( |             osmConnection.userDetails.map( | ||||||
|                 (ud) => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible |                 (ud) => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  | @ -113,14 +112,14 @@ export default class CopyrightPanel extends Combine { | ||||||
|     private static LicenseObject = CopyrightPanel.GenerateLicenses() |     private static LicenseObject = CopyrightPanel.GenerateLicenses() | ||||||
| 
 | 
 | ||||||
|     constructor(state: { |     constructor(state: { | ||||||
|         layoutToUse: LayoutConfig |         layout: LayoutConfig | ||||||
|         featurePipeline: FeaturePipeline |         bounds: Store<BBox> | ||||||
|         currentBounds: Store<BBox> |  | ||||||
|         locationControl: UIEventSource<Loc> |  | ||||||
|         osmConnection: OsmConnection |         osmConnection: OsmConnection | ||||||
|  |         dataIsLoading: Store<boolean> | ||||||
|  |         perLayer: ReadonlyMap<string, GeoIndexedStore> | ||||||
|     }) { |     }) { | ||||||
|         const t = Translations.t.general.attribution |         const t = Translations.t.general.attribution | ||||||
|         const layoutToUse = state.layoutToUse |         const layoutToUse = state.layout | ||||||
| 
 | 
 | ||||||
|         const iconAttributions: BaseUIElement[] = layoutToUse.usedImages.map( |         const iconAttributions: BaseUIElement[] = layoutToUse.usedImages.map( | ||||||
|             CopyrightPanel.IconAttribution |             CopyrightPanel.IconAttribution | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| import { SubtleButton } from "../Base/SubtleButton" | import { SubtleButton } from "../Base/SubtleButton" | ||||||
| import Svg from "../../Svg" | import Svg from "../../Svg" | ||||||
| import Translations from "../i18n/Translations" | import Translations from "../i18n/Translations" | ||||||
| import State from "../../State" |  | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import Combine from "../Base/Combine" | import Combine from "../Base/Combine" | ||||||
| import CheckBoxes from "../Input/Checkboxes" | import CheckBoxes from "../Input/Checkboxes" | ||||||
|  |  | ||||||
|  | @ -1,16 +1,13 @@ | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { FixedInputElement } from "../Input/FixedInputElement" |  | ||||||
| import { RadioButton } from "../Input/RadioButton" |  | ||||||
| import { VariableUiElement } from "../Base/VariableUIElement" | import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
| import Toggle, { ClickableToggle } from "../Input/Toggle" | import Toggle from "../Input/Toggle" | ||||||
| import Combine from "../Base/Combine" | import Combine from "../Base/Combine" | ||||||
| import Translations from "../i18n/Translations" | import Translations from "../i18n/Translations" | ||||||
| import { Translation } from "../i18n/Translation" | import { Translation } from "../i18n/Translation" | ||||||
| import Svg from "../../Svg" | import Svg from "../../Svg" | ||||||
| import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" | import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import BaseUIElement from "../BaseUIElement" | import BaseUIElement from "../BaseUIElement" | ||||||
| import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" | import FilteredLayer from "../../Models/FilteredLayer" | ||||||
| import BackgroundSelector from "./BackgroundSelector" |  | ||||||
| import FilterConfig from "../../Models/ThemeConfig/FilterConfig" | import FilterConfig from "../../Models/ThemeConfig/FilterConfig" | ||||||
| import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" | import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" | ||||||
| import { SubstitutedTranslation } from "../SubstitutedTranslation" | import { SubstitutedTranslation } from "../SubstitutedTranslation" | ||||||
|  | @ -18,9 +15,7 @@ import ValidatedTextField from "../Input/ValidatedTextField" | ||||||
| import { QueryParameters } from "../../Logic/Web/QueryParameters" | import { QueryParameters } from "../../Logic/Web/QueryParameters" | ||||||
| import { TagUtils } from "../../Logic/Tags/TagUtils" | import { TagUtils } from "../../Logic/Tags/TagUtils" | ||||||
| import { InputElement } from "../Input/InputElement" | import { InputElement } from "../Input/InputElement" | ||||||
| import { DropDown } from "../Input/DropDown" |  | ||||||
| import { FixedUiElement } from "../Base/FixedUiElement" | import { FixedUiElement } from "../Base/FixedUiElement" | ||||||
| import BaseLayer from "../../Models/BaseLayer" |  | ||||||
| import Loc from "../../Models/Loc" | import Loc from "../../Models/Loc" | ||||||
| import { BackToThemeOverview } from "./ActionButtons" | import { BackToThemeOverview } from "./ActionButtons" | ||||||
| 
 | 
 | ||||||
|  | @ -272,102 +267,6 @@ export class LayerFilterPanel extends Combine { | ||||||
|         return [tr, settableFilter] |         return [tr, settableFilter] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static createCheckboxFilter( |  | ||||||
|         filterConfig: FilterConfig |  | ||||||
|     ): [BaseUIElement, UIEventSource<FilterState>] { |  | ||||||
|         let option = filterConfig.options[0] |  | ||||||
| 
 |  | ||||||
|         const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6") |  | ||||||
|         const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2 w-6") |  | ||||||
|         const qp = QueryParameters.GetBooleanQueryParameter( |  | ||||||
|             "filter-" + filterConfig.id, |  | ||||||
|             false, |  | ||||||
|             "Is filter '" + filterConfig.options[0].question.textFor("en") + " enabled?" |  | ||||||
|         ) |  | ||||||
|         const toggle = new ClickableToggle( |  | ||||||
|             new Combine([icon, option.question.Clone().SetClass("block")]).SetClass("flex"), |  | ||||||
|             new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass( |  | ||||||
|                 "flex" |  | ||||||
|             ), |  | ||||||
|             qp |  | ||||||
|         ) |  | ||||||
|             .ToggleOnClick() |  | ||||||
|             .SetClass("block m-1") |  | ||||||
| 
 |  | ||||||
|         return [ |  | ||||||
|             toggle, |  | ||||||
|             toggle.isEnabled.sync( |  | ||||||
|                 (enabled) => |  | ||||||
|                     enabled |  | ||||||
|                         ? { |  | ||||||
|                               currentFilter: option.osmTags, |  | ||||||
|                               state: "true", |  | ||||||
|                           } |  | ||||||
|                         : undefined, |  | ||||||
|                 [], |  | ||||||
|                 (f) => f !== undefined |  | ||||||
|             ), |  | ||||||
|         ] |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static createMultiFilter( |  | ||||||
|         filterConfig: FilterConfig |  | ||||||
|     ): [BaseUIElement, UIEventSource<FilterState>] { |  | ||||||
|         let options = filterConfig.options |  | ||||||
| 
 |  | ||||||
|         const values: FilterState[] = options.map((f, i) => ({ |  | ||||||
|             currentFilter: f.osmTags, |  | ||||||
|             state: i, |  | ||||||
|         })) |  | ||||||
|         let filterPicker: InputElement<number> |  | ||||||
|         const value = QueryParameters.GetQueryParameter( |  | ||||||
|             "filter-" + filterConfig.id, |  | ||||||
|             "0", |  | ||||||
|             "Value for filter " + filterConfig.id |  | ||||||
|         ).sync( |  | ||||||
|             (str) => Number(str), |  | ||||||
|             [], |  | ||||||
|             (n) => "" + n |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         if (options.length <= 6) { |  | ||||||
|             filterPicker = new RadioButton( |  | ||||||
|                 options.map( |  | ||||||
|                     (option, i) => |  | ||||||
|                         new FixedInputElement(option.question.Clone().SetClass("block"), i) |  | ||||||
|                 ), |  | ||||||
|                 { |  | ||||||
|                     value, |  | ||||||
|                     dontStyle: true, |  | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
|         } else { |  | ||||||
|             filterPicker = new DropDown( |  | ||||||
|                 "", |  | ||||||
|                 options.map((option, i) => ({ |  | ||||||
|                     value: i, |  | ||||||
|                     shown: option.question.Clone(), |  | ||||||
|                 })), |  | ||||||
|                 value |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return [ |  | ||||||
|             filterPicker, |  | ||||||
|             filterPicker.GetValue().sync( |  | ||||||
|                 (i) => values[i], |  | ||||||
|                 [], |  | ||||||
|                 (selected) => { |  | ||||||
|                     const v = selected?.state |  | ||||||
|                     if (v === undefined || typeof v === "string") { |  | ||||||
|                         return undefined |  | ||||||
|                     } |  | ||||||
|                     return v |  | ||||||
|                 } |  | ||||||
|             ), |  | ||||||
|         ] |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static createFilter( |     private static createFilter( | ||||||
|         state: {}, |         state: {}, | ||||||
|         filterConfig: FilterConfig |         filterConfig: FilterConfig | ||||||
|  | @ -376,12 +275,6 @@ export class LayerFilterPanel extends Combine { | ||||||
|             return LayerFilterPanel.createFilterWithFields(state, filterConfig) |             return LayerFilterPanel.createFilterWithFields(state, filterConfig) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (filterConfig.options.length === 1) { |         return undefined | ||||||
|             return LayerFilterPanel.createCheckboxFilter(filterConfig) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const filter = LayerFilterPanel.createMultiFilter(filterConfig) |  | ||||||
|         filter[0].SetClass("pl-2") |  | ||||||
|         return filter |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										79
									
								
								UI/BigComponents/Filterview.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								UI/BigComponents/Filterview.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | ||||||
|  | <script lang="ts">/** | ||||||
|  |  * The FilterView shows the various options to enable/disable a single layer. | ||||||
|  |  */ | ||||||
|  | import type FilteredLayer from "../../Models/FilteredLayer"; | ||||||
|  | import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||||
|  | import ToSvelte from "../Base/ToSvelte.svelte"; | ||||||
|  | import Checkbox from "../Base/Checkbox.svelte"; | ||||||
|  | import FilterConfig from "../../Models/ThemeConfig/FilterConfig"; | ||||||
|  | import type { Writable } from "svelte/store"; | ||||||
|  | import If from "../Base/If.svelte"; | ||||||
|  | import Dropdown from "../Base/Dropdown.svelte"; | ||||||
|  | import { onDestroy } from "svelte"; | ||||||
|  | 
 | ||||||
|  | export let filteredLayer: FilteredLayer; | ||||||
|  | export let zoomlevel: number; | ||||||
|  | let layer: LayerConfig = filteredLayer.layerDef; | ||||||
|  | let isDisplayed: boolean = filteredLayer.isDisplayed.data; | ||||||
|  | onDestroy(filteredLayer.isDisplayed.addCallbackAndRunD(d => { | ||||||
|  |   isDisplayed = d; | ||||||
|  |   return false | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Gets a UIEventSource as boolean for the given option, to be used with a checkbox | ||||||
|  |  */ | ||||||
|  | function getBooleanStateFor(option: FilterConfig): Writable<boolean> { | ||||||
|  |   const state = filteredLayer.appliedFilters.get(option.id); | ||||||
|  |   return state.sync(f => f === 0, [], (b) => b ? 0 : undefined); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Gets a UIEventSource as number for the given option, to be used with a dropdown or radiobutton | ||||||
|  |  */ | ||||||
|  | function getStateFor(option: FilterConfig): Writable<number> { | ||||||
|  |   return filteredLayer.appliedFilters.get(option.id); | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | {#if filteredLayer.layerDef.name} | ||||||
|  |   <div> | ||||||
|  |     <label class="flex gap-1"> | ||||||
|  |       <Checkbox selected={filteredLayer.isDisplayed} /> | ||||||
|  |       <If condition={filteredLayer.isDisplayed}> | ||||||
|  |         <ToSvelte construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6")}></ToSvelte> | ||||||
|  |         <ToSvelte slot="else" construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 opacity-50")}></ToSvelte> | ||||||
|  |       </If> | ||||||
|  | 
 | ||||||
|  |       {filteredLayer.layerDef.name} | ||||||
|  |     </label> | ||||||
|  |     <If condition={filteredLayer.isDisplayed}> | ||||||
|  |       <div id="subfilters" class="flex flex-col gap-y-1 mb-4 ml-4"> | ||||||
|  |         {#each filteredLayer.layerDef.filters as filter} | ||||||
|  |           <div> | ||||||
|  | 
 | ||||||
|  |             <!-- There are three (and a half) modes of filters: a single checkbox, a radio button/dropdown or with fields --> | ||||||
|  |             {#if filter.options.length === 1 && filter.options[0].fields.length === 0} | ||||||
|  |               <label> | ||||||
|  |                 <Checkbox selected={getBooleanStateFor(filter)} /> | ||||||
|  |                 {filter.options[0].question} | ||||||
|  |               </label> | ||||||
|  |             {/if} | ||||||
|  | 
 | ||||||
|  |             {#if filter.options.length > 1} | ||||||
|  |               <Dropdown value={getStateFor(filter)}> | ||||||
|  |                 {#each filter.options as option, i} | ||||||
|  |                   <option value={i}> | ||||||
|  |                     { option.question} | ||||||
|  |                   </option> | ||||||
|  |                 {/each} | ||||||
|  |               </Dropdown> | ||||||
|  |             {/if} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |           </div> | ||||||
|  |         {/each} | ||||||
|  |       </div> | ||||||
|  |     </If> | ||||||
|  | 
 | ||||||
|  |   </div> | ||||||
|  | {/if} | ||||||
|  | @ -1,8 +1,7 @@ | ||||||
| import { VariableUiElement } from "../Base/VariableUIElement" | import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
| import Svg from "../../Svg" | import Svg from "../../Svg" | ||||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler" | import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler" | ||||||
| import { BBox } from "../../Logic/BBox" |  | ||||||
| import Hotkeys from "../Base/Hotkeys" | import Hotkeys from "../Base/Hotkeys" | ||||||
| import Translations from "../i18n/Translations" | import Translations from "../i18n/Translations" | ||||||
| import Constants from "../../Models/Constants" | import Constants from "../../Models/Constants" | ||||||
|  | @ -94,14 +93,13 @@ export class GeolocationControl extends VariableUiElement { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (geolocationState.currentGPSLocation.data === undefined) { |             // A location _is_ known! Let's move to this location
 | ||||||
|  |             const currentLocation = geolocationState.currentGPSLocation.data | ||||||
|  |             if (currentLocation === undefined) { | ||||||
|                 // No location is known yet, not much we can do
 |                 // No location is known yet, not much we can do
 | ||||||
|                 lastClick.setData(new Date()) |                 lastClick.setData(new Date()) | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|             // A location _is_ known! Let's move to this location
 |  | ||||||
|             const currentLocation = geolocationState.currentGPSLocation.data |  | ||||||
|             const inBounds = state.bounds.data.contains([ |             const inBounds = state.bounds.data.contains([ | ||||||
|                 currentLocation.longitude, |                 currentLocation.longitude, | ||||||
|                 currentLocation.latitude, |                 currentLocation.latitude, | ||||||
|  |  | ||||||
							
								
								
									
										94
									
								
								UI/BigComponents/Geosearch.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								UI/BigComponents/Geosearch.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,94 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | 
 | ||||||
|  |   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||||
|  |   import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
|  |   import type { Feature } from "geojson"; | ||||||
|  |   import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||||
|  |   import ToSvelte from "../Base/ToSvelte.svelte"; | ||||||
|  |   import Svg from "../../Svg.js"; | ||||||
|  |   import Translations from "../i18n/Translations"; | ||||||
|  |   import Loading from "../Base/Loading.svelte"; | ||||||
|  |   import Hotkeys from "../Base/Hotkeys"; | ||||||
|  |   import { Geocoding } from "../../Logic/Osm/Geocoding"; | ||||||
|  |   import { BBox } from "../../Logic/BBox"; | ||||||
|  |   import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore"; | ||||||
|  | 
 | ||||||
|  |   Translations.t; | ||||||
|  |   export let bounds: UIEventSource<BBox> | ||||||
|  |   export let layout: LayoutConfig; | ||||||
|  |   export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | ||||||
|  |   export let selectedElement: UIEventSource<Feature>; | ||||||
|  |   export let selectedLayer: UIEventSource<LayerConfig>; | ||||||
|  | 
 | ||||||
|  |   let searchContents: string = undefined; | ||||||
|  | 
 | ||||||
|  |   let isRunning: boolean = false; | ||||||
|  | 
 | ||||||
|  |   let inputElement: HTMLInputElement; | ||||||
|  |    | ||||||
|  |   let feedback: string = undefined | ||||||
|  | 
 | ||||||
|  |   Hotkeys.RegisterHotkey( | ||||||
|  |     { ctrl: "F" }, | ||||||
|  |     Translations.t.hotkeyDocumentation.selectSearch, | ||||||
|  |     () => { | ||||||
|  |       inputElement?.focus() | ||||||
|  |       inputElement?.select() | ||||||
|  |     } | ||||||
|  |   ) | ||||||
|  |   async function performSearch() { | ||||||
|  |     try { | ||||||
|  |       isRunning = true; | ||||||
|  |       searchContents = searchContents?.trim() ?? "" | ||||||
|  |       if (searchContents === "") { | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |       const result = await Geocoding.Search(searchContents, bounds.data) | ||||||
|  |       if (result.length == 0) { | ||||||
|  |         feedback = Translations.t.search.nothing.txt | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |       const poi = result[0] | ||||||
|  |       const [lat0, lat1, lon0, lon1] = poi.boundingbox | ||||||
|  |       bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01)) | ||||||
|  |       const id = poi.osm_type + "/" + poi.osm_id | ||||||
|  |       const layers = Array.from(perLayer.values()) | ||||||
|  |       for (const layer of layers) { | ||||||
|  |         const found = layer.features.data.find(f => f.properties.id === id) | ||||||
|  |         selectedElement.setData(found) | ||||||
|  |         selectedLayer.setData(layer.layer.layerDef) | ||||||
|  |          | ||||||
|  |       } | ||||||
|  |     }catch (e) { | ||||||
|  |       console.error(e) | ||||||
|  |       feedback = Translations.t.search.error.txt | ||||||
|  |     } finally { | ||||||
|  |       isRunning = false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="flex normal-background rounded-full pl-2"> | ||||||
|  |   <form> | ||||||
|  | 
 | ||||||
|  |     {#if isRunning} | ||||||
|  |       <Loading>{Translations.t.general.search.searching}</Loading> | ||||||
|  |       {:else if feedback !== undefined} | ||||||
|  |       <div class="alert" on:click={() => feedback = undefined}> | ||||||
|  |         {feedback} | ||||||
|  |       </div> | ||||||
|  |     {:else } | ||||||
|  |       <input | ||||||
|  |         bind:this={inputElement} | ||||||
|  |         on:keypress={keypr => keypr.key === "Enter" ? performSearch() : undefined} | ||||||
|  | 
 | ||||||
|  |         bind:value={searchContents} | ||||||
|  |         placeholder={Translations.t.general.search.search}> | ||||||
|  |     {/if} | ||||||
|  | 
 | ||||||
|  |   </form> | ||||||
|  |   <div class="w-6 h-6" on:click={performSearch}> | ||||||
|  |     <ToSvelte construct={Svg.search_ui}></ToSvelte> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | @ -1,12 +1,6 @@ | ||||||
| import Combine from "../Base/Combine" | import Combine from "../Base/Combine" | ||||||
| import Toggle from "../Input/Toggle" |  | ||||||
| import MapControlButton from "../MapControlButton" |  | ||||||
| import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler" |  | ||||||
| import Svg from "../../Svg" |  | ||||||
| import MapState from "../../Logic/State/MapState" | import MapState from "../../Logic/State/MapState" | ||||||
| import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" |  | ||||||
| import LevelSelector from "./LevelSelector" | import LevelSelector from "./LevelSelector" | ||||||
| import { GeolocationControl } from "./GeolocationControl" |  | ||||||
| 
 | 
 | ||||||
| export default class RightControls extends Combine { | export default class RightControls extends Combine { | ||||||
|     constructor(state: MapState & { featurePipeline: FeaturePipeline }) { |     constructor(state: MapState & { featurePipeline: FeaturePipeline }) { | ||||||
|  |  | ||||||
							
								
								
									
										75
									
								
								UI/BigComponents/SelectedElementView.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								UI/BigComponents/SelectedElementView.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,75 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import type { Feature } from "geojson"; | ||||||
|  |   import { Store, UIEventSource } from "../../Logic/UIEventSource"; | ||||||
|  |   import TagRenderingAnswer from "../Popup/TagRenderingAnswer"; | ||||||
|  |   import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||||
|  |   import ToSvelte from "../Base/ToSvelte.svelte"; | ||||||
|  |   import { VariableUiElement } from "../Base/VariableUIElement.js"; | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||||
|  |   import { onDestroy } from "svelte"; | ||||||
|  | 
 | ||||||
|  |   export let selectedElement: UIEventSource<Feature>; | ||||||
|  |   export let layer: UIEventSource<LayerConfig>; | ||||||
|  |   export let tags: Store<UIEventSource<Record<string, string>>>; | ||||||
|  |   let _tags: UIEventSource<Record<string, string>>; | ||||||
|  |   onDestroy(tags.subscribe(tags => { | ||||||
|  |     _tags = tags; | ||||||
|  |     return false | ||||||
|  |   })); | ||||||
|  | 
 | ||||||
|  |   export let specialVisState: SpecialVisualizationState; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    *        const title = new TagRenderingAnswer( | ||||||
|  |    *             tags, | ||||||
|  |    *             layerConfig.title ?? new TagRenderingConfig("POI"), | ||||||
|  |    *             state | ||||||
|  |    *         ).SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2 text-2xl") | ||||||
|  |    *         const titleIcons = new Combine( | ||||||
|  |    *             layerConfig.titleIcons.map((icon) => { | ||||||
|  |    *                 return new TagRenderingAnswer( | ||||||
|  |    *                     tags, | ||||||
|  |    *                     icon, | ||||||
|  |    *                     state, | ||||||
|  |    *                     "block h-8 max-h-8 align-baseline box-content sm:p-0.5 titleicon" | ||||||
|  |    *                 ) | ||||||
|  |    *             }) | ||||||
|  |    *         ).SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2") | ||||||
|  |    * | ||||||
|  |    *         return new Combine([ | ||||||
|  |    *             new Combine([title, titleIcons]).SetClass( | ||||||
|  |    *                 "flex flex-col sm:flex-row flex-grow justify-between" | ||||||
|  |    *             ), | ||||||
|  |    *         ]) | ||||||
|  |    */ | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div> | ||||||
|  |   <div on:click={() =>selectedElement.setData(undefined)}>close</div> | ||||||
|  |   <div class="flex flex-col sm:flex-row flex-grow justify-between"> | ||||||
|  |     <!-- Title element--> | ||||||
|  |     <ToSvelte | ||||||
|  |       construct={() => new VariableUiElement(tags.mapD(tags =>   new TagRenderingAnswer(tags, layer.data.title, specialVisState), [layer]))}></ToSvelte> | ||||||
|  | 
 | ||||||
|  |     <div class="flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2"> | ||||||
|  | 
 | ||||||
|  |       {#each $layer.titleIcons as titleIconConfig (titleIconConfig.id)} | ||||||
|  |         <div class="w-8 h-8"> | ||||||
|  |           <ToSvelte | ||||||
|  |             construct={() => new VariableUiElement(tags.mapD(tags =>   new TagRenderingAnswer(tags, titleIconConfig, specialVisState)))}></ToSvelte> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |       {/each} | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  |   <ul> | ||||||
|  | 
 | ||||||
|  |     {#each Object.keys($_tags) as key} | ||||||
|  |       <li><b>{key}</b>=<b>{$_tags[key]}</b></li> | ||||||
|  |     {/each} | ||||||
|  |   </ul> | ||||||
|  | </div> | ||||||
|  | @ -4,20 +4,14 @@ import Title from "../Base/Title" | ||||||
| import TagRenderingChart from "./TagRenderingChart" | import TagRenderingChart from "./TagRenderingChart" | ||||||
| import Combine from "../Base/Combine" | import Combine from "../Base/Combine" | ||||||
| import Locale from "../i18n/Locale" | import Locale from "../i18n/Locale" | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" | import { FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource" | ||||||
| import { OsmFeature } from "../../Models/OsmFeature" | import BaseUIElement from "../BaseUIElement" | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" |  | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" |  | ||||||
| 
 | 
 | ||||||
| export default class StatisticsPanel extends VariableUiElement { | export default class StatisticsForLayerPanel extends VariableUiElement { | ||||||
|     constructor( |     constructor(elementsInview: FeatureSourceForLayer) { | ||||||
|         elementsInview: UIEventSource<{ element: OsmFeature; layer: LayerConfig }[]>, |         const layer = elementsInview.layer.layerDef | ||||||
|         state: { |  | ||||||
|             layoutToUse: LayoutConfig |  | ||||||
|         } |  | ||||||
|     ) { |  | ||||||
|         super( |         super( | ||||||
|             elementsInview.stabilized(1000).map( |             elementsInview.features.stabilized(1000).map( | ||||||
|                 (features) => { |                 (features) => { | ||||||
|                     if (features === undefined) { |                     if (features === undefined) { | ||||||
|                         return new Loading("Loading data") |                         return new Loading("Loading data") | ||||||
|  | @ -25,40 +19,33 @@ export default class StatisticsPanel extends VariableUiElement { | ||||||
|                     if (features.length === 0) { |                     if (features.length === 0) { | ||||||
|                         return "No elements in view" |                         return "No elements in view" | ||||||
|                     } |                     } | ||||||
|                     const els = [] |                     const els: BaseUIElement[] = [] | ||||||
|                     for (const layer of state.layoutToUse.layers) { |                     const featuresForLayer = features | ||||||
|                         if (layer.name === undefined) { |                     if (featuresForLayer.length === 0) { | ||||||
|                             continue |                         return | ||||||
|                         } |  | ||||||
|                         const featuresForLayer = features |  | ||||||
|                             .filter((f) => f.layer === layer) |  | ||||||
|                             .map((f) => f.element) |  | ||||||
|                         if (featuresForLayer.length === 0) { |  | ||||||
|                             continue |  | ||||||
|                         } |  | ||||||
|                         els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8")) |  | ||||||
| 
 |  | ||||||
|                         const layerStats = [] |  | ||||||
|                         for (const tagRendering of layer?.tagRenderings ?? []) { |  | ||||||
|                             const chart = new TagRenderingChart(featuresForLayer, tagRendering, { |  | ||||||
|                                 chartclasses: "w-full", |  | ||||||
|                                 chartstyle: "height: 60rem", |  | ||||||
|                                 includeTitle: false, |  | ||||||
|                             }) |  | ||||||
|                             const title = new Title( |  | ||||||
|                                 tagRendering.question?.Clone() ?? tagRendering.id, |  | ||||||
|                                 4 |  | ||||||
|                             ).SetClass("mt-8") |  | ||||||
|                             if (!chart.HasClass("hidden")) { |  | ||||||
|                                 layerStats.push( |  | ||||||
|                                     new Combine([title, chart]).SetClass( |  | ||||||
|                                         "flex flex-col w-full lg:w-1/3" |  | ||||||
|                                     ) |  | ||||||
|                                 ) |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                         els.push(new Combine(layerStats).SetClass("flex flex-wrap")) |  | ||||||
|                     } |                     } | ||||||
|  |                     els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8")) | ||||||
|  | 
 | ||||||
|  |                     const layerStats = [] | ||||||
|  |                     for (const tagRendering of layer?.tagRenderings ?? []) { | ||||||
|  |                         const chart = new TagRenderingChart(featuresForLayer, tagRendering, { | ||||||
|  |                             chartclasses: "w-full", | ||||||
|  |                             chartstyle: "height: 60rem", | ||||||
|  |                             includeTitle: false, | ||||||
|  |                         }) | ||||||
|  |                         const title = new Title( | ||||||
|  |                             tagRendering.question?.Clone() ?? tagRendering.id, | ||||||
|  |                             4 | ||||||
|  |                         ).SetClass("mt-8") | ||||||
|  |                         if (!chart.HasClass("hidden")) { | ||||||
|  |                             layerStats.push( | ||||||
|  |                                 new Combine([title, chart]).SetClass( | ||||||
|  |                                     "flex flex-col w-full lg:w-1/3" | ||||||
|  |                                 ) | ||||||
|  |                             ) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     els.push(new Combine(layerStats).SetClass("flex flex-wrap")) | ||||||
|                     return new Combine(els) |                     return new Combine(els) | ||||||
|                 }, |                 }, | ||||||
|                 [Locale.language] |                 [Locale.language] | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ import LoggedInUserIndicator from "../LoggedInUserIndicator" | ||||||
| import { ActionButtons } from "./ActionButtons" | import { ActionButtons } from "./ActionButtons" | ||||||
| import { BBox } from "../../Logic/BBox" | import { BBox } from "../../Logic/BBox" | ||||||
| import Loc from "../../Models/Loc" | import Loc from "../../Models/Loc" | ||||||
|  | import { DefaultGuiState } from "../DefaultGuiState" | ||||||
| 
 | 
 | ||||||
| export default class ThemeIntroductionPanel extends Combine { | export default class ThemeIntroductionPanel extends Combine { | ||||||
|     constructor( |     constructor( | ||||||
|  | @ -24,6 +25,7 @@ export default class ThemeIntroductionPanel extends Combine { | ||||||
|             osmConnection: OsmConnection |             osmConnection: OsmConnection | ||||||
|             currentBounds: Store<BBox> |             currentBounds: Store<BBox> | ||||||
|             locationControl: UIEventSource<Loc> |             locationControl: UIEventSource<Loc> | ||||||
|  |             defaultGuiState: DefaultGuiState | ||||||
|         }, |         }, | ||||||
|         guistate?: { userInfoIsOpened: UIEventSource<boolean> } |         guistate?: { userInfoIsOpened: UIEventSource<boolean> } | ||||||
|     ) { |     ) { | ||||||
|  |  | ||||||
|  | @ -17,7 +17,7 @@ export default class UploadTraceToOsmUI extends LoginToggle { | ||||||
|     constructor( |     constructor( | ||||||
|         trace: (title: string) => string, |         trace: (title: string) => string, | ||||||
|         state: { |         state: { | ||||||
|             layoutToUse: LayoutConfig |             layout: LayoutConfig | ||||||
|             osmConnection: OsmConnection |             osmConnection: OsmConnection | ||||||
|             readonly featureSwitchUserbadge: Store<boolean> |             readonly featureSwitchUserbadge: Store<boolean> | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| import FeaturePipelineState from "../Logic/State/FeaturePipelineState" | import FeaturePipelineState from "../Logic/State/FeaturePipelineState" | ||||||
| import State from "../State" |  | ||||||
| import { Utils } from "../Utils" | import { Utils } from "../Utils" | ||||||
| import { UIEventSource } from "../Logic/UIEventSource" | import { UIEventSource } from "../Logic/UIEventSource" | ||||||
| import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs" | import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs" | ||||||
|  | @ -11,14 +10,11 @@ import BaseUIElement from "./BaseUIElement" | ||||||
| import LeftControls from "./BigComponents/LeftControls" | import LeftControls from "./BigComponents/LeftControls" | ||||||
| import RightControls from "./BigComponents/RightControls" | import RightControls from "./BigComponents/RightControls" | ||||||
| import CenterMessageBox from "./CenterMessageBox" | import CenterMessageBox from "./CenterMessageBox" | ||||||
| import ShowDataLayer from "./ShowDataLayer/ShowDataLayer" |  | ||||||
| import ScrollableFullScreen from "./Base/ScrollableFullScreen" | import ScrollableFullScreen from "./Base/ScrollableFullScreen" | ||||||
| import Translations from "./i18n/Translations" | import Translations from "./i18n/Translations" | ||||||
| import SimpleAddUI from "./BigComponents/SimpleAddUI" | import SimpleAddUI from "./BigComponents/SimpleAddUI" | ||||||
| import StrayClickHandler from "../Logic/Actors/StrayClickHandler" | import StrayClickHandler from "../Logic/Actors/StrayClickHandler" | ||||||
| import { DefaultGuiState } from "./DefaultGuiState" | import { DefaultGuiState } from "./DefaultGuiState" | ||||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig" |  | ||||||
| import home_location_json from "../assets/layers/home_location/home_location.json" |  | ||||||
| import NewNoteUi from "./Popup/NewNoteUi" | import NewNoteUi from "./Popup/NewNoteUi" | ||||||
| import Combine from "./Base/Combine" | import Combine from "./Base/Combine" | ||||||
| import AddNewMarker from "./BigComponents/AddNewMarker" | import AddNewMarker from "./BigComponents/AddNewMarker" | ||||||
|  | @ -32,7 +28,6 @@ import { FixedUiElement } from "./Base/FixedUiElement" | ||||||
| import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" | import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" | ||||||
| import { GeoLocationState } from "../Logic/State/GeoLocationState" | import { GeoLocationState } from "../Logic/State/GeoLocationState" | ||||||
| import Hotkeys from "./Base/Hotkeys" | import Hotkeys from "./Base/Hotkeys" | ||||||
| import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers" |  | ||||||
| import CopyrightPanel from "./BigComponents/CopyrightPanel" | import CopyrightPanel from "./BigComponents/CopyrightPanel" | ||||||
| import SvelteUIElement from "./Base/SvelteUIElement" | import SvelteUIElement from "./Base/SvelteUIElement" | ||||||
| import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte" | import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte" | ||||||
|  | @ -50,9 +45,6 @@ export default class DefaultGUI { | ||||||
|     constructor(state: FeaturePipelineState, guiState: DefaultGuiState) { |     constructor(state: FeaturePipelineState, guiState: DefaultGuiState) { | ||||||
|         this.state = state |         this.state = state | ||||||
|         this.guiState = guiState |         this.guiState = guiState | ||||||
|         if (this.state.featureSwitchGeolocation.data) { |  | ||||||
|             this.geolocationHandler = new GeoLocationHandler(new GeoLocationState(), state) |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public setup() { |     public setup() { | ||||||
|  | @ -74,10 +66,6 @@ export default class DefaultGUI { | ||||||
|                 this.state.backgroundLayer.setData(AvailableBaseLayers.osmCarto) |                 this.state.backgroundLayer.setData(AvailableBaseLayers.osmCarto) | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
| 
 |  | ||||||
|         Utils.downloadJson("./service-worker-version") |  | ||||||
|             .then((data) => console.log("Service worker", data)) |  | ||||||
|             .catch((_) => console.log("Service worker not active")) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public setupClickDialogOnMap( |     public setupClickDialogOnMap( | ||||||
|  | @ -173,13 +161,6 @@ export default class DefaultGUI { | ||||||
| 
 | 
 | ||||||
|         this.setupClickDialogOnMap(guiState.filterViewIsOpened, state) |         this.setupClickDialogOnMap(guiState.filterViewIsOpened, state) | ||||||
| 
 | 
 | ||||||
|         new ShowDataLayer({ |  | ||||||
|             leafletMap: state.leafletMap, |  | ||||||
|             layerToShow: new LayerConfig(home_location_json, "home_location", true), |  | ||||||
|             features: state.homeLocation, |  | ||||||
|             state, |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         const selectedElement: FilteredLayer = state.filteredLayers.data.filter( |         const selectedElement: FilteredLayer = state.filteredLayers.data.filter( | ||||||
|             (l) => l.layerDef.id === "selected_element" |             (l) => l.layerDef.id === "selected_element" | ||||||
|         )[0] |         )[0] | ||||||
|  | @ -285,23 +266,6 @@ export default class DefaultGUI { | ||||||
|             .SetClass("flex items-center justify-center normal-background h-full") |             .SetClass("flex items-center justify-center normal-background h-full") | ||||||
|             .AttachTo("on-small-screen") |             .AttachTo("on-small-screen") | ||||||
| 
 | 
 | ||||||
|         new Combine([ |  | ||||||
|             Toggle.If(state.featureSwitchSearch, () => { |  | ||||||
|                 const search = new SearchAndGo(state).SetClass( |  | ||||||
|                     "shadow rounded-full h-min w-full overflow-hidden sm:max-w-sm pointer-events-auto" |  | ||||||
|                 ) |  | ||||||
|                 Hotkeys.RegisterHotkey( |  | ||||||
|                     { ctrl: "F" }, |  | ||||||
|                     Translations.t.hotkeyDocumentation.selectSearch, |  | ||||||
|                     () => { |  | ||||||
|                         search.focus() |  | ||||||
|                     } |  | ||||||
|                 ) |  | ||||||
| 
 |  | ||||||
|                 return search |  | ||||||
|             }), |  | ||||||
|         ]).AttachTo("top-right") |  | ||||||
| 
 |  | ||||||
|         new LeftControls(state, guiState).AttachTo("bottom-left") |         new LeftControls(state, guiState).AttachTo("bottom-left") | ||||||
|         new RightControls(state, this.geolocationHandler).AttachTo("bottom-right") |         new RightControls(state, this.geolocationHandler).AttachTo("bottom-right") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| import { UIEventSource } from "../Logic/UIEventSource" | import { UIEventSource } from "../Logic/UIEventSource" | ||||||
| import { QueryParameters } from "../Logic/Web/QueryParameters" |  | ||||||
| import Hash from "../Logic/Web/Hash" | import Hash from "../Logic/Web/Hash" | ||||||
| 
 | 
 | ||||||
| export class DefaultGuiState { | export class DefaultGuiState { | ||||||
|     static state: DefaultGuiState |  | ||||||
| 
 |  | ||||||
|     public readonly welcomeMessageIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>( |     public readonly welcomeMessageIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>( | ||||||
|         false |         false | ||||||
|     ) |     ) | ||||||
|  | 
 | ||||||
|  |     public readonly menuIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||||
|  | 
 | ||||||
|     public readonly downloadControlIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>( |     public readonly downloadControlIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>( | ||||||
|         false |         false | ||||||
|     ) |     ) | ||||||
|  | @ -22,25 +22,17 @@ export class DefaultGuiState { | ||||||
|     public readonly userInfoFocusedQuestion: UIEventSource<string> = new UIEventSource<string>( |     public readonly userInfoFocusedQuestion: UIEventSource<string> = new UIEventSource<string>( | ||||||
|         undefined |         undefined | ||||||
|     ) |     ) | ||||||
|     public readonly welcomeMessageOpenedTab: UIEventSource<number> | 
 | ||||||
|  |     private readonly sources: Record<string, UIEventSource<boolean>> = { | ||||||
|  |         welcome: this.welcomeMessageIsOpened, | ||||||
|  |         download: this.downloadControlIsOpened, | ||||||
|  |         filters: this.filterViewIsOpened, | ||||||
|  |         copyright: this.copyrightViewIsOpened, | ||||||
|  |         currentview: this.currentViewControlIsOpened, | ||||||
|  |         userinfo: this.userInfoIsOpened, | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         this.welcomeMessageOpenedTab = UIEventSource.asFloat( |  | ||||||
|             QueryParameters.GetQueryParameter( |  | ||||||
|                 "tab", |  | ||||||
|                 "0", |  | ||||||
|                 `The tab that is shown in the welcome-message.` |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         const sources = { |  | ||||||
|             welcome: this.welcomeMessageIsOpened, |  | ||||||
|             download: this.downloadControlIsOpened, |  | ||||||
|             filters: this.filterViewIsOpened, |  | ||||||
|             copyright: this.copyrightViewIsOpened, |  | ||||||
|             currentview: this.currentViewControlIsOpened, |  | ||||||
|             userinfo: this.userInfoIsOpened, |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const self = this |         const self = this | ||||||
|         this.userInfoIsOpened.addCallback((isOpen) => { |         this.userInfoIsOpened.addCallback((isOpen) => { | ||||||
|             if (!isOpen) { |             if (!isOpen) { | ||||||
|  | @ -49,10 +41,16 @@ export class DefaultGuiState { | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         sources[Hash.hash.data?.toLowerCase()]?.setData(true) |         this.sources[Hash.hash.data?.toLowerCase()]?.setData(true) | ||||||
| 
 | 
 | ||||||
|         if (Hash.hash.data === "" || Hash.hash.data === undefined) { |         if (Hash.hash.data === "" || Hash.hash.data === undefined) { | ||||||
|             this.welcomeMessageIsOpened.setData(true) |             this.welcomeMessageIsOpened.setData(true) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public closeAll() { | ||||||
|  |         for (const sourceKey in this.sources) { | ||||||
|  |             this.sources[sourceKey].setData(false) | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ export default class DeleteImage extends Toggle { | ||||||
|     constructor( |     constructor( | ||||||
|         key: string, |         key: string, | ||||||
|         tags: Store<any>, |         tags: Store<any>, | ||||||
|         state: { layoutToUse: LayoutConfig; changes?: Changes; osmConnection?: OsmConnection } |         state: { layout: LayoutConfig; changes?: Changes; osmConnection?: OsmConnection } | ||||||
|     ) { |     ) { | ||||||
|         const oldValue = tags.data[key] |         const oldValue = tags.data[key] | ||||||
|         const isDeletedBadge = Translations.t.image.isDeleted |         const isDeletedBadge = Translations.t.image.isDeleted | ||||||
|  | @ -24,7 +24,7 @@ export default class DeleteImage extends Toggle { | ||||||
|                 await state?.changes?.applyAction( |                 await state?.changes?.applyAction( | ||||||
|                     new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, { |                     new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, { | ||||||
|                         changeType: "delete-image", |                         changeType: "delete-image", | ||||||
|                         theme: state.layoutToUse.id, |                         theme: state.layout.id, | ||||||
|                     }) |                     }) | ||||||
|                 ) |                 ) | ||||||
|             }) |             }) | ||||||
|  | @ -39,7 +39,7 @@ export default class DeleteImage extends Toggle { | ||||||
|                 await state?.changes?.applyAction( |                 await state?.changes?.applyAction( | ||||||
|                     new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data, { |                     new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data, { | ||||||
|                         changeType: "answer", |                         changeType: "answer", | ||||||
|                         theme: state.layoutToUse.id, |                         theme: state.layout.id, | ||||||
|                     }) |                     }) | ||||||
|                 ) |                 ) | ||||||
|             }) |             }) | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ export class ImageCarousel extends Toggle { | ||||||
|     constructor( |     constructor( | ||||||
|         images: Store<{ key: string; url: string; provider: ImageProvider }[]>, |         images: Store<{ key: string; url: string; provider: ImageProvider }[]>, | ||||||
|         tags: Store<any>, |         tags: Store<any>, | ||||||
|         state: { osmConnection?: OsmConnection; changes?: Changes; layoutToUse: LayoutConfig } |         state: { osmConnection?: OsmConnection; changes?: Changes; layout: LayoutConfig } | ||||||
|     ) { |     ) { | ||||||
|         const uiElements = images.map( |         const uiElements = images.map( | ||||||
|             (imageURLS: { key: string; url: string; provider: ImageProvider }[]) => { |             (imageURLS: { key: string; url: string; provider: ImageProvider }[]) => { | ||||||
|  |  | ||||||
|  | @ -11,26 +11,19 @@ import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
| import { FixedUiElement } from "../Base/FixedUiElement" | import { FixedUiElement } from "../Base/FixedUiElement" | ||||||
| import { VariableUiElement } from "../Base/VariableUIElement" | import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" |  | ||||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" |  | ||||||
| import { Changes } from "../../Logic/Osm/Changes" |  | ||||||
| import Loading from "../Base/Loading" | import Loading from "../Base/Loading" | ||||||
| import { LoginToggle } from "../Popup/LoginButton" | import { LoginToggle } from "../Popup/LoginButton" | ||||||
| import Constants from "../../Models/Constants" | import Constants from "../../Models/Constants" | ||||||
| import { DefaultGuiState } from "../DefaultGuiState" | import { DefaultGuiState } from "../DefaultGuiState" | ||||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen" | import ScrollableFullScreen from "../Base/ScrollableFullScreen" | ||||||
|  | import { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
| 
 | 
 | ||||||
| export class ImageUploadFlow extends Toggle { | export class ImageUploadFlow extends Toggle { | ||||||
|     private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>() |     private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>() | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         tagsSource: Store<any>, |         tagsSource: Store<any>, | ||||||
|         state: { |         state: SpecialVisualizationState, | ||||||
|             osmConnection: OsmConnection |  | ||||||
|             layoutToUse: LayoutConfig |  | ||||||
|             changes: Changes |  | ||||||
|             featureSwitchUserbadge: Store<boolean> |  | ||||||
|         }, |  | ||||||
|         imagePrefix: string = "image", |         imagePrefix: string = "image", | ||||||
|         text: string = undefined |         text: string = undefined | ||||||
|     ) { |     ) { | ||||||
|  | @ -56,7 +49,7 @@ export class ImageUploadFlow extends Toggle { | ||||||
|             await state.changes.applyAction( |             await state.changes.applyAction( | ||||||
|                 new ChangeTagAction(tags.id, new Tag(key, url), tagsSource.data, { |                 new ChangeTagAction(tags.id, new Tag(key, url), tagsSource.data, { | ||||||
|                     changeType: "add-image", |                     changeType: "add-image", | ||||||
|                     theme: state.layoutToUse.id, |                     theme: state.layout.id, | ||||||
|                 }) |                 }) | ||||||
|             ) |             ) | ||||||
|             console.log("Adding image:" + key, url) |             console.log("Adding image:" + key, url) | ||||||
|  | @ -111,7 +104,7 @@ export class ImageUploadFlow extends Toggle { | ||||||
| 
 | 
 | ||||||
|             const tags = tagsSource.data |             const tags = tagsSource.data | ||||||
| 
 | 
 | ||||||
|             const layout = state?.layoutToUse |             const layout = state?.layout | ||||||
|             let matchingLayer: LayerConfig = undefined |             let matchingLayer: LayerConfig = undefined | ||||||
|             for (const layer of layout?.layers ?? []) { |             for (const layer of layout?.layers ?? []) { | ||||||
|                 if (layer.source.osmTags.matchesProperties(tags)) { |                 if (layer.source.osmTags.matchesProperties(tags)) { | ||||||
|  |  | ||||||
|  | @ -1,31 +1,30 @@ | ||||||
| import { InputElement } from "./InputElement" | import { InputElement } from "./InputElement" | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" | import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import Combine from "../Base/Combine" | import Combine from "../Base/Combine" | ||||||
| import Svg from "../../Svg" | import Svg from "../../Svg" | ||||||
| import { Utils } from "../../Utils" |  | ||||||
| import Loc from "../../Models/Loc" | import Loc from "../../Models/Loc" | ||||||
| import { GeoOperations } from "../../Logic/GeoOperations" | import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
| import Minimap, { MinimapObj } from "../Base/Minimap" |  | ||||||
| import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" | import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" | ||||||
| import BaseUIElement from "../BaseUIElement" | import BaseUIElement from "../BaseUIElement" | ||||||
|  | import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Selects a length after clicking on the minimap, in meters |  * Selects a length after clicking on the minimap, in meters | ||||||
|  */ |  */ | ||||||
| export default class LengthInput extends InputElement<string> { | export default class LengthInput extends InputElement<string> { | ||||||
|     private readonly _location: UIEventSource<Loc> |     private readonly _location: Store<Loc> | ||||||
|     private readonly value: UIEventSource<string> |     private readonly value: UIEventSource<string> | ||||||
|     private readonly background: UIEventSource<any> |     private readonly background: Store<RasterLayerPolygon> | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         mapBackground: UIEventSource<any>, |  | ||||||
|         location: UIEventSource<Loc>, |         location: UIEventSource<Loc>, | ||||||
|  |         mapBackground?: UIEventSource<RasterLayerPolygon>, | ||||||
|         value?: UIEventSource<string> |         value?: UIEventSource<string> | ||||||
|     ) { |     ) { | ||||||
|         super() |         super() | ||||||
|         this._location = location |         this._location = location | ||||||
|         this.value = value ?? new UIEventSource<string>(undefined) |         this.value = value ?? new UIEventSource<string>(undefined) | ||||||
|         this.background = mapBackground |         this.background = mapBackground ?? new ImmutableStore(AvailableRasterLayers.osmCarto) | ||||||
|         this.SetClass("block") |         this.SetClass("block") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -41,28 +40,26 @@ export default class LengthInput extends InputElement<string> { | ||||||
|     protected InnerConstructElement(): HTMLElement { |     protected InnerConstructElement(): HTMLElement { | ||||||
|         let map: BaseUIElement & MinimapObj = undefined |         let map: BaseUIElement & MinimapObj = undefined | ||||||
|         let layerControl: BaseUIElement = undefined |         let layerControl: BaseUIElement = undefined | ||||||
|         if (!Utils.runningFromConsole) { |         map = Minimap.createMiniMap({ | ||||||
|             map = Minimap.createMiniMap({ |             background: this.background, | ||||||
|                 background: this.background, |             allowMoving: false, | ||||||
|                 allowMoving: false, |             location: this._location, | ||||||
|                 location: this._location, |             attribution: true, | ||||||
|                 attribution: true, |             leafletOptions: { | ||||||
|                 leafletOptions: { |                 tap: true, | ||||||
|                     tap: true, |             }, | ||||||
|                 }, |         }) | ||||||
|             }) |  | ||||||
| 
 | 
 | ||||||
|             layerControl = new BackgroundMapSwitch( |         layerControl = new BackgroundMapSwitch( | ||||||
|                 { |             { | ||||||
|                     locationControl: this._location, |                 locationControl: this._location, | ||||||
|                     backgroundLayer: this.background, |                 backgroundLayer: this.background, | ||||||
|                 }, |             }, | ||||||
|                 this.background, |             this.background, | ||||||
|                 { |             { | ||||||
|                     allowedCategories: ["map", "photo"], |                 allowedCategories: ["map", "photo"], | ||||||
|                 } |             } | ||||||
|             ) |         ) | ||||||
|         } |  | ||||||
|         const crosshair = new Combine([ |         const crosshair = new Combine([ | ||||||
|             Svg.length_crosshair_svg().SetStyle( |             Svg.length_crosshair_svg().SetStyle( | ||||||
|                 `position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);` |                 `position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);` | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import * as EmailValidator from "email-validator" | ||||||
| import { parsePhoneNumberFromString } from "libphonenumber-js" | import { parsePhoneNumberFromString } from "libphonenumber-js" | ||||||
| import { InputElement } from "./InputElement" | import { InputElement } from "./InputElement" | ||||||
| import { TextField } from "./TextField" | import { TextField } from "./TextField" | ||||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import CombinedInputElement from "./CombinedInputElement" | import CombinedInputElement from "./CombinedInputElement" | ||||||
| import SimpleDatePicker from "./SimpleDatePicker" | import SimpleDatePicker from "./SimpleDatePicker" | ||||||
| import OpeningHoursInput from "../OpeningHours/OpeningHoursInput" | import OpeningHoursInput from "../OpeningHours/OpeningHoursInput" | ||||||
|  | @ -25,6 +25,7 @@ import InputElementMap from "./InputElementMap" | ||||||
| import Translations from "../i18n/Translations" | import Translations from "../i18n/Translations" | ||||||
| import { Translation } from "../i18n/Translation" | import { Translation } from "../i18n/Translation" | ||||||
| import Locale from "../i18n/Locale" | import Locale from "../i18n/Locale" | ||||||
|  | import { RasterLayerPolygon } from "../../Models/RasterLayers" | ||||||
| 
 | 
 | ||||||
| export class TextFieldDef { | export class TextFieldDef { | ||||||
|     public readonly name: string |     public readonly name: string | ||||||
|  | @ -638,7 +639,7 @@ class LengthTextField extends TextFieldDef { | ||||||
|             location?: [number, number] |             location?: [number, number] | ||||||
|             args?: string[] |             args?: string[] | ||||||
|             feature?: any |             feature?: any | ||||||
|             mapBackgroundLayer?: Store<BaseLayer> |             mapBackgroundLayer?: Store<RasterLayerPolygon> | ||||||
|         } |         } | ||||||
|     ) => { |     ) => { | ||||||
|         options = options ?? {} |         options = options ?? {} | ||||||
|  | @ -674,14 +675,18 @@ class LengthTextField extends TextFieldDef { | ||||||
|             zoom: zoom, |             zoom: zoom, | ||||||
|         }) |         }) | ||||||
|         if (args[1]) { |         if (args[1]) { | ||||||
|             // We have a prefered map!
 |             // The arguments indicate the preferred background type
 | ||||||
|             options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo( |             options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo( | ||||||
|                 location, |                 location, | ||||||
|                 new UIEventSource<string[]>(args[1].split(",")) |                 new ImmutableStore<string[]>(args[1].split(",")) | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|         const background = options?.mapBackgroundLayer |         const background = options?.mapBackgroundLayer | ||||||
|         const li = new LengthInput(new UIEventSource<BaseLayer>(background.data), location, value) |         const li = new LengthInput( | ||||||
|  |             new UIEventSource<RasterLayerPolygon>(background.data), | ||||||
|  |             location, | ||||||
|  |             value | ||||||
|  |         ) | ||||||
|         li.SetStyle("height: 20rem;") |         li.SetStyle("height: 20rem;") | ||||||
|         return li |         return li | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -21,12 +21,20 @@ export class MapLibreAdaptor implements MapProperties { | ||||||
|         "keyboard", |         "keyboard", | ||||||
|         "touchZoomRotate", |         "touchZoomRotate", | ||||||
|     ] |     ] | ||||||
|  |     private static maplibre_zoom_handlers = [ | ||||||
|  |         "scrollZoom", | ||||||
|  |         "boxZoom", | ||||||
|  |         "doubleClickZoom", | ||||||
|  |         "touchZoomRotate", | ||||||
|  |     ] | ||||||
|     readonly location: UIEventSource<{ lon: number; lat: number }> |     readonly location: UIEventSource<{ lon: number; lat: number }> | ||||||
|     readonly zoom: UIEventSource<number> |     readonly zoom: UIEventSource<number> | ||||||
|     readonly bounds: Store<BBox> |     readonly bounds: UIEventSource<BBox> | ||||||
|     readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined> |     readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined> | ||||||
|     readonly maxbounds: UIEventSource<BBox | undefined> |     readonly maxbounds: UIEventSource<BBox | undefined> | ||||||
|     readonly allowMoving: UIEventSource<true | boolean | undefined> |     readonly allowMoving: UIEventSource<true | boolean | undefined> | ||||||
|  |     readonly allowZooming: UIEventSource<true | boolean | undefined> | ||||||
|  |     readonly lastClickLocation: Store<undefined | { lon: number; lat: number }> | ||||||
|     private readonly _maplibreMap: Store<MLMap> |     private readonly _maplibreMap: Store<MLMap> | ||||||
|     private readonly _bounds: UIEventSource<BBox> |     private readonly _bounds: UIEventSource<BBox> | ||||||
|     /** |     /** | ||||||
|  | @ -50,11 +58,14 @@ export class MapLibreAdaptor implements MapProperties { | ||||||
|         }) |         }) | ||||||
|         this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined) |         this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined) | ||||||
|         this.allowMoving = state?.allowMoving ?? new UIEventSource(true) |         this.allowMoving = state?.allowMoving ?? new UIEventSource(true) | ||||||
|  |         this.allowZooming = state?.allowZooming ?? new UIEventSource(true) | ||||||
|         this._bounds = new UIEventSource(undefined) |         this._bounds = new UIEventSource(undefined) | ||||||
|         this.bounds = this._bounds |         this.bounds = this._bounds | ||||||
|         this.rasterLayer = |         this.rasterLayer = | ||||||
|             state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined) |             state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined) | ||||||
| 
 | 
 | ||||||
|  |         const lastClickLocation = new UIEventSource<{ lon: number; lat: number }>(undefined) | ||||||
|  |         this.lastClickLocation = lastClickLocation | ||||||
|         const self = this |         const self = this | ||||||
|         maplibreMap.addCallbackAndRunD((map) => { |         maplibreMap.addCallbackAndRunD((map) => { | ||||||
|             map.on("load", () => { |             map.on("load", () => { | ||||||
|  | @ -63,11 +74,13 @@ export class MapLibreAdaptor implements MapProperties { | ||||||
|                 self.SetZoom(self.zoom.data) |                 self.SetZoom(self.zoom.data) | ||||||
|                 self.setMaxBounds(self.maxbounds.data) |                 self.setMaxBounds(self.maxbounds.data) | ||||||
|                 self.setAllowMoving(self.allowMoving.data) |                 self.setAllowMoving(self.allowMoving.data) | ||||||
|  |                 self.setAllowZooming(self.allowZooming.data) | ||||||
|             }) |             }) | ||||||
|             self.MoveMapToCurrentLoc(self.location.data) |             self.MoveMapToCurrentLoc(self.location.data) | ||||||
|             self.SetZoom(self.zoom.data) |             self.SetZoom(self.zoom.data) | ||||||
|             self.setMaxBounds(self.maxbounds.data) |             self.setMaxBounds(self.maxbounds.data) | ||||||
|             self.setAllowMoving(self.allowMoving.data) |             self.setAllowMoving(self.allowMoving.data) | ||||||
|  |             self.setAllowZooming(self.allowZooming.data) | ||||||
|             map.on("moveend", () => { |             map.on("moveend", () => { | ||||||
|                 const dt = this.location.data |                 const dt = this.location.data | ||||||
|                 dt.lon = map.getCenter().lng |                 dt.lon = map.getCenter().lng | ||||||
|  | @ -81,6 +94,11 @@ export class MapLibreAdaptor implements MapProperties { | ||||||
|                 ]) |                 ]) | ||||||
|                 self._bounds.setData(bbox) |                 self._bounds.setData(bbox) | ||||||
|             }) |             }) | ||||||
|  |             map.on("click", (e) => { | ||||||
|  |                 const lon = e.lngLat.lng | ||||||
|  |                 const lat = e.lngLat.lat | ||||||
|  |                 lastClickLocation.setData({ lon, lat }) | ||||||
|  |             }) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         this.rasterLayer.addCallback((_) => |         this.rasterLayer.addCallback((_) => | ||||||
|  | @ -95,6 +113,8 @@ export class MapLibreAdaptor implements MapProperties { | ||||||
|         this.zoom.addCallbackAndRunD((z) => self.SetZoom(z)) |         this.zoom.addCallbackAndRunD((z) => self.SetZoom(z)) | ||||||
|         this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox)) |         this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox)) | ||||||
|         this.allowMoving.addCallbackAndRun((allowMoving) => self.setAllowMoving(allowMoving)) |         this.allowMoving.addCallbackAndRun((allowMoving) => self.setAllowMoving(allowMoving)) | ||||||
|  |         this.allowZooming.addCallbackAndRun((allowZooming) => self.setAllowZooming(allowZooming)) | ||||||
|  |         this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -205,7 +225,7 @@ export class MapLibreAdaptor implements MapProperties { | ||||||
|             // already the correct background layer, nothing to do
 |             // already the correct background layer, nothing to do
 | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         if (background === undefined) { |         if (!background?.url) { | ||||||
|             // no background to set
 |             // no background to set
 | ||||||
|             this.removeCurrentLayer(map) |             this.removeCurrentLayer(map) | ||||||
|             this._currentRasterLayer = undefined |             this._currentRasterLayer = undefined | ||||||
|  | @ -266,4 +286,38 @@ export class MapLibreAdaptor implements MapProperties { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private setAllowZooming(allow: true | boolean | undefined) { | ||||||
|  |         const map = this._maplibreMap.data | ||||||
|  |         if (map === undefined) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         if (allow === false) { | ||||||
|  |             for (const id of MapLibreAdaptor.maplibre_zoom_handlers) { | ||||||
|  |                 map[id].disable() | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             for (const id of MapLibreAdaptor.maplibre_zoom_handlers) { | ||||||
|  |                 map[id].enable() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private setBounds(bounds: BBox) { | ||||||
|  |         const map = this._maplibreMap.data | ||||||
|  |         if (map === undefined) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         const oldBounds = map.getBounds() | ||||||
|  |         const e = 0.0000001 | ||||||
|  |         const hasDiff = | ||||||
|  |             Math.abs(oldBounds.getWest() - bounds.getWest()) > e && | ||||||
|  |             Math.abs(oldBounds.getEast() - bounds.getEast()) > e && | ||||||
|  |             Math.abs(oldBounds.getNorth() - bounds.getNorth()) > e && | ||||||
|  |             Math.abs(oldBounds.getSouth() - bounds.getSouth()) > e | ||||||
|  |         if (!hasDiff) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         map.fitBounds(bounds.toLngLat()) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { ImmutableStore, Store } from "../../Logic/UIEventSource" | import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import type { Map as MlMap } from "maplibre-gl" | import type { Map as MlMap } from "maplibre-gl" | ||||||
| import { GeoJSONSource, Marker } from "maplibre-gl" | import { GeoJSONSource, Marker } from "maplibre-gl" | ||||||
| import { ShowDataLayerOptions } from "./ShowDataLayerOptions" | import { ShowDataLayerOptions } from "./ShowDataLayerOptions" | ||||||
|  | @ -14,10 +14,13 @@ import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import * as range_layer from "../../assets/layers/range/range.json" | import * as range_layer from "../../assets/layers/range/range.json" | ||||||
| import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" | import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" | ||||||
|  | import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" | ||||||
|  | import FilteredLayer from "../../Models/FilteredLayer" | ||||||
|  | import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" | ||||||
| 
 | 
 | ||||||
| class PointRenderingLayer { | class PointRenderingLayer { | ||||||
|     private readonly _config: PointRenderingConfig |     private readonly _config: PointRenderingConfig | ||||||
|     private readonly _fetchStore?: (id: string) => Store<OsmTags> |     private readonly _fetchStore?: (id: string) => Store<Record<string, string>> | ||||||
|     private readonly _map: MlMap |     private readonly _map: MlMap | ||||||
|     private readonly _onClick: (feature: Feature) => void |     private readonly _onClick: (feature: Feature) => void | ||||||
|     private readonly _allMarkers: Map<string, Marker> = new Map<string, Marker>() |     private readonly _allMarkers: Map<string, Marker> = new Map<string, Marker>() | ||||||
|  | @ -27,7 +30,7 @@ class PointRenderingLayer { | ||||||
|         features: FeatureSource, |         features: FeatureSource, | ||||||
|         config: PointRenderingConfig, |         config: PointRenderingConfig, | ||||||
|         visibility?: Store<boolean>, |         visibility?: Store<boolean>, | ||||||
|         fetchStore?: (id: string) => Store<OsmTags>, |         fetchStore?: (id: string) => Store<Record<string, string>>, | ||||||
|         onClick?: (feature: Feature) => void |         onClick?: (feature: Feature) => void | ||||||
|     ) { |     ) { | ||||||
|         this._config = config |         this._config = config | ||||||
|  | @ -96,7 +99,7 @@ class PointRenderingLayer { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private addPoint(feature: Feature, loc: [number, number]): Marker { |     private addPoint(feature: Feature, loc: [number, number]): Marker { | ||||||
|         let store: Store<OsmTags> |         let store: Store<Record<string, string>> | ||||||
|         if (this._fetchStore) { |         if (this._fetchStore) { | ||||||
|             store = this._fetchStore(feature.properties.id) |             store = this._fetchStore(feature.properties.id) | ||||||
|         } else { |         } else { | ||||||
|  | @ -143,7 +146,7 @@ class LineRenderingLayer { | ||||||
|     private readonly _map: MlMap |     private readonly _map: MlMap | ||||||
|     private readonly _config: LineRenderingConfig |     private readonly _config: LineRenderingConfig | ||||||
|     private readonly _visibility?: Store<boolean> |     private readonly _visibility?: Store<boolean> | ||||||
|     private readonly _fetchStore?: (id: string) => Store<OsmTags> |     private readonly _fetchStore?: (id: string) => Store<Record<string, string>> | ||||||
|     private readonly _onClick?: (feature: Feature) => void |     private readonly _onClick?: (feature: Feature) => void | ||||||
|     private readonly _layername: string |     private readonly _layername: string | ||||||
|     private readonly _listenerInstalledOn: Set<string> = new Set<string>() |     private readonly _listenerInstalledOn: Set<string> = new Set<string>() | ||||||
|  | @ -154,7 +157,7 @@ class LineRenderingLayer { | ||||||
|         layername: string, |         layername: string, | ||||||
|         config: LineRenderingConfig, |         config: LineRenderingConfig, | ||||||
|         visibility?: Store<boolean>, |         visibility?: Store<boolean>, | ||||||
|         fetchStore?: (id: string) => Store<OsmTags>, |         fetchStore?: (id: string) => Store<Record<string, string>>, | ||||||
|         onClick?: (feature: Feature) => void |         onClick?: (feature: Feature) => void | ||||||
|     ) { |     ) { | ||||||
|         this._layername = layername |         this._layername = layername | ||||||
|  | @ -212,9 +215,10 @@ class LineRenderingLayer { | ||||||
|                 promoteId: "id", |                 promoteId: "id", | ||||||
|             }) |             }) | ||||||
|             // @ts-ignore
 |             // @ts-ignore
 | ||||||
|  |             const linelayer = this._layername + "_line" | ||||||
|             map.addLayer({ |             map.addLayer({ | ||||||
|                 source: this._layername, |                 source: this._layername, | ||||||
|                 id: this._layername + "_line", |                 id: linelayer, | ||||||
|                 type: "line", |                 type: "line", | ||||||
|                 paint: { |                 paint: { | ||||||
|                     "line-color": ["feature-state", "color"], |                     "line-color": ["feature-state", "color"], | ||||||
|  | @ -227,9 +231,10 @@ class LineRenderingLayer { | ||||||
|                 }, |                 }, | ||||||
|             }) |             }) | ||||||
| 
 | 
 | ||||||
|  |             const polylayer = this._layername + "_polygon" | ||||||
|             map.addLayer({ |             map.addLayer({ | ||||||
|                 source: this._layername, |                 source: this._layername, | ||||||
|                 id: this._layername + "_polygon", |                 id: polylayer, | ||||||
|                 type: "fill", |                 type: "fill", | ||||||
|                 filter: ["in", ["geometry-type"], ["literal", ["Polygon", "MultiPolygon"]]], |                 filter: ["in", ["geometry-type"], ["literal", ["Polygon", "MultiPolygon"]]], | ||||||
|                 layout: {}, |                 layout: {}, | ||||||
|  | @ -238,6 +243,11 @@ class LineRenderingLayer { | ||||||
|                     "fill-opacity": 0.1, |                     "fill-opacity": 0.1, | ||||||
|                 }, |                 }, | ||||||
|             }) |             }) | ||||||
|  | 
 | ||||||
|  |             this._visibility.addCallbackAndRunD((visible) => { | ||||||
|  |                 map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none") | ||||||
|  |                 map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none") | ||||||
|  |             }) | ||||||
|         } else { |         } else { | ||||||
|             src.setData({ |             src.setData({ | ||||||
|                 type: "FeatureCollection", |                 type: "FeatureCollection", | ||||||
|  | @ -295,6 +305,24 @@ export default class ShowDataLayer { | ||||||
|         map.addCallbackAndRunD((map) => self.initDrawFeatures(map)) |         map.addCallbackAndRunD((map) => self.initDrawFeatures(map)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static showMultipleLayers( | ||||||
|  |         mlmap: UIEventSource<MlMap>, | ||||||
|  |         features: FeatureSource, | ||||||
|  |         layers: LayerConfig[], | ||||||
|  |         options?: Partial<ShowDataLayerOptions> | ||||||
|  |     ) { | ||||||
|  |         const perLayer = new PerLayerFeatureSourceSplitter( | ||||||
|  |             layers.map((l) => new FilteredLayer(l)), | ||||||
|  |             new StaticFeatureSource(features) | ||||||
|  |         ) | ||||||
|  |         perLayer.forEach((fs) => { | ||||||
|  |             new ShowDataLayer(mlmap, { | ||||||
|  |                 layer: fs.layer.layerDef, | ||||||
|  |                 features: fs, | ||||||
|  |                 ...(options ?? {}), | ||||||
|  |             }) | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|     public static showRange( |     public static showRange( | ||||||
|         map: Store<MlMap>, |         map: Store<MlMap>, | ||||||
|         features: FeatureSource, |         features: FeatureSource, | ||||||
|  | @ -318,8 +346,11 @@ export default class ShowDataLayer { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private initDrawFeatures(map: MlMap) { |     private initDrawFeatures(map: MlMap) { | ||||||
|         const { features, doShowLayer, fetchStore, selectedElement } = this._options |         let { features, doShowLayer, fetchStore, selectedElement, selectedLayer } = this._options | ||||||
|         const onClick = (feature: Feature) => selectedElement?.setData(feature) |         const onClick = (feature: Feature) => { | ||||||
|  |             selectedElement?.setData(feature) | ||||||
|  |             selectedLayer?.setData(this._options.layer) | ||||||
|  |         } | ||||||
|         for (let i = 0; i < this._options.layer.lineRendering.length; i++) { |         for (let i = 0; i < this._options.layer.lineRendering.length; i++) { | ||||||
|             const lineRenderingConfig = this._options.layer.lineRendering[i] |             const lineRenderingConfig = this._options.layer.lineRendering[i] | ||||||
|             new LineRenderingLayer( |             new LineRenderingLayer( | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| import FeatureSource from "../../Logic/FeatureSource/FeatureSource" | import FeatureSource from "../../Logic/FeatureSource/FeatureSource" | ||||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import { OsmTags } from "../../Models/OsmFeature" | import { OsmTags } from "../../Models/OsmFeature" | ||||||
|  | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
|  | import { Feature } from "geojson" | ||||||
| 
 | 
 | ||||||
| export interface ShowDataLayerOptions { | export interface ShowDataLayerOptions { | ||||||
|     /** |     /** | ||||||
|  | @ -11,7 +13,12 @@ export interface ShowDataLayerOptions { | ||||||
|      * Indication of the current selected element; overrides some filters. |      * Indication of the current selected element; overrides some filters. | ||||||
|      * When a feature is tapped, the feature will be put in there |      * When a feature is tapped, the feature will be put in there | ||||||
|      */ |      */ | ||||||
|     selectedElement?: UIEventSource<any> |     selectedElement?: UIEventSource<Feature> | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * When a feature of this layer is tapped, the layer will be marked | ||||||
|  |      */ | ||||||
|  |     selectedLayer?: UIEventSource<LayerConfig> | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * If set, zoom to the features when initially loaded and when they are changed |      * If set, zoom to the features when initially loaded and when they are changed | ||||||
|  | @ -26,5 +33,5 @@ export interface ShowDataLayerOptions { | ||||||
|      * Function which fetches the relevant store. |      * Function which fetches the relevant store. | ||||||
|      * If given, the map will update when a property is changed |      * If given, the map will update when a property is changed | ||||||
|      */ |      */ | ||||||
|     fetchStore?: (id: string) => UIEventSource<OsmTags> |     fetchStore?: (id: string) => Store<Record<string, string>> | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,39 +0,0 @@ | ||||||
| /** |  | ||||||
|  * SHows geojson on the given leaflet map, but attempts to figure out the correct layer first |  | ||||||
|  */ |  | ||||||
| import { ImmutableStore, Store } from "../../Logic/UIEventSource" |  | ||||||
| import ShowDataLayer from "./ShowDataLayer" |  | ||||||
| import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" |  | ||||||
| import FilteredLayer from "../../Models/FilteredLayer" |  | ||||||
| import { ShowDataLayerOptions } from "./ShowDataLayerOptions" |  | ||||||
| import { Map as MlMap } from "maplibre-gl" |  | ||||||
| import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource" |  | ||||||
| import { GlobalFilter } from "../../Models/GlobalFilter" |  | ||||||
| 
 |  | ||||||
| export default class ShowDataMultiLayer { |  | ||||||
|     constructor( |  | ||||||
|         map: Store<MlMap>, |  | ||||||
|         options: ShowDataLayerOptions & { |  | ||||||
|             layers: FilteredLayer[] |  | ||||||
|             globalFilters?: Store<GlobalFilter[]> |  | ||||||
|         } |  | ||||||
|     ) { |  | ||||||
|         new PerLayerFeatureSourceSplitter( |  | ||||||
|             new ImmutableStore(options.layers), |  | ||||||
|             (features, layer) => { |  | ||||||
|                 const newOptions = { |  | ||||||
|                     ...options, |  | ||||||
|                     layer: layer.layerDef, |  | ||||||
|                     features: new FilteringFeatureSource( |  | ||||||
|                         layer, |  | ||||||
|                         features, |  | ||||||
|                         options.fetchStore, |  | ||||||
|                         options.globalFilters |  | ||||||
|                     ), |  | ||||||
|                 } |  | ||||||
|                 new ShowDataLayer(map, newOptions) |  | ||||||
|             }, |  | ||||||
|             options.features |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,6 +1,4 @@ | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" | import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" |  | ||||||
| import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" |  | ||||||
| import BaseUIElement from "../BaseUIElement" | import BaseUIElement from "../BaseUIElement" | ||||||
| import LocationInput from "../Input/LocationInput" | import LocationInput from "../Input/LocationInput" | ||||||
| import { BBox } from "../../Logic/BBox" | import { BBox } from "../../Logic/BBox" | ||||||
|  | @ -18,18 +16,13 @@ import { Tag } from "../../Logic/Tags/Tag" | ||||||
| import { WayId } from "../../Models/OsmFeature" | import { WayId } from "../../Models/OsmFeature" | ||||||
| import { Translation } from "../i18n/Translation" | import { Translation } from "../i18n/Translation" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
| import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers" | import { AvailableRasterLayers } from "../../Models/RasterLayers" | ||||||
| import { GlobalFilter } from "../../Logic/State/GlobalFilter" | import { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  | import ClippedFeatureSource from "../../Logic/FeatureSource/Sources/ClippedFeatureSource" | ||||||
| 
 | 
 | ||||||
| export default class ConfirmLocationOfPoint extends Combine { | export default class ConfirmLocationOfPoint extends Combine { | ||||||
|     constructor( |     constructor( | ||||||
|         state: { |         state: SpecialVisualizationState, | ||||||
|             globalFilters: UIEventSource<GlobalFilter[]> |  | ||||||
|             featureSwitchIsTesting: UIEventSource<boolean> |  | ||||||
|             osmConnection: OsmConnection |  | ||||||
|             featurePipeline: FeaturePipeline |  | ||||||
|             backgroundLayer?: UIEventSource<RasterLayerPolygon | undefined> |  | ||||||
|         }, |  | ||||||
|         filterViewIsOpened: UIEventSource<boolean>, |         filterViewIsOpened: UIEventSource<boolean>, | ||||||
|         preset: PresetInfo, |         preset: PresetInfo, | ||||||
|         confirmText: BaseUIElement, |         confirmText: BaseUIElement, | ||||||
|  | @ -55,7 +48,7 @@ export default class ConfirmLocationOfPoint extends Combine { | ||||||
|             const locationSrc = new UIEventSource(zloc) |             const locationSrc = new UIEventSource(zloc) | ||||||
| 
 | 
 | ||||||
|             let backgroundLayer = new UIEventSource( |             let backgroundLayer = new UIEventSource( | ||||||
|                 state?.backgroundLayer?.data ?? AvailableRasterLayers.osmCarto |                 state?.mapProperties.rasterLayer?.data ?? AvailableRasterLayers.osmCarto | ||||||
|             ) |             ) | ||||||
|             if (preset.preciseInput.preferredBackground) { |             if (preset.preciseInput.preferredBackground) { | ||||||
|                 const defaultBackground = AvailableRasterLayers.SelectBestLayerAccordingTo( |                 const defaultBackground = AvailableRasterLayers.SelectBestLayerAccordingTo( | ||||||
|  | @ -105,15 +98,13 @@ export default class ConfirmLocationOfPoint extends Combine { | ||||||
|                         Math.max(preset.boundsFactor ?? 0.25, 2) |                         Math.max(preset.boundsFactor ?? 0.25, 2) | ||||||
|                     ) |                     ) | ||||||
|                     loadedBbox = bbox |                     loadedBbox = bbox | ||||||
|                     const allFeatures: Feature[] = [] |                     const sources = preset.preciseInput.snapToLayers.map( | ||||||
|                     preset.preciseInput.snapToLayers.forEach((layerId) => { |                         (layerId) => | ||||||
|                         console.log("Snapping to", layerId) |                             new ClippedFeatureSource( | ||||||
|                         state.featurePipeline |                                 state.perLayer.get(layerId), | ||||||
|                             .GetFeaturesWithin(layerId, bbox) |                                 bbox.asGeoJson({}) | ||||||
|                             ?.forEach((feats) => allFeatures.push(...(<any[]>feats))) |                             ) | ||||||
|                     }) |                     ) | ||||||
|                     console.log("Snapping to", allFeatures) |  | ||||||
|                     snapToFeatures.setData(allFeatures) |  | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -488,7 +488,7 @@ export class OH { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static CreateOhObject( |     public static CreateOhObject( | ||||||
|         tags: object & { _lat: number; _lon: number; _country?: string }, |         tags: Record<string, string> & { _lat: number; _lon: number; _country?: string }, | ||||||
|         textToParse: string |         textToParse: string | ||||||
|     ) { |     ) { | ||||||
|         // noinspection JSPotentiallyInvalidConstructorUsage
 |         // noinspection JSPotentiallyInvalidConstructorUsage
 | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ export default class OpeningHoursVisualization extends Toggle { | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         tags: UIEventSource<any>, |         tags: UIEventSource<Record<string, string>>, | ||||||
|         state: { osmConnection?: OsmConnection }, |         state: { osmConnection?: OsmConnection }, | ||||||
|         key: string, |         key: string, | ||||||
|         prefix = "", |         prefix = "", | ||||||
|  | @ -49,7 +49,7 @@ export default class OpeningHoursVisualization extends Toggle { | ||||||
|                     } |                     } | ||||||
|                     try { |                     try { | ||||||
|                         return OpeningHoursVisualization.CreateFullVisualisation( |                         return OpeningHoursVisualization.CreateFullVisualisation( | ||||||
|                             OH.CreateOhObject(tags.data, ohtext) |                             OH.CreateOhObject(<any>tags.data, ohtext) | ||||||
|                         ) |                         ) | ||||||
|                     } catch (e) { |                     } catch (e) { | ||||||
|                         console.warn(e, e.stack) |                         console.warn(e, e.stack) | ||||||
|  |  | ||||||
|  | @ -8,7 +8,8 @@ import Toggle from "../Input/Toggle" | ||||||
| import { LoginToggle } from "./LoginButton" | import { LoginToggle } from "./LoginButton" | ||||||
| import Combine from "../Base/Combine" | import Combine from "../Base/Combine" | ||||||
| import Title from "../Base/Title" | import Title from "../Base/Title" | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  | import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
| 
 | 
 | ||||||
| export class AddNoteCommentViz implements SpecialVisualization { | export class AddNoteCommentViz implements SpecialVisualization { | ||||||
|     funcName = "add_note_comment" |     funcName = "add_note_comment" | ||||||
|  | @ -21,7 +22,11 @@ export class AddNoteCommentViz implements SpecialVisualization { | ||||||
|         }, |         }, | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     public constr(state, tags, args) { |     public constr( | ||||||
|  |         state: SpecialVisualizationState, | ||||||
|  |         tags: UIEventSource<Record<string, string>>, | ||||||
|  |         args: string[] | ||||||
|  |     ) { | ||||||
|         const t = Translations.t.notes |         const t = Translations.t.notes | ||||||
|         const textField = new TextField({ |         const textField = new TextField({ | ||||||
|             placeholder: t.addCommentPlaceholder, |             placeholder: t.addCommentPlaceholder, | ||||||
|  | @ -62,12 +67,11 @@ export class AddNoteCommentViz implements SpecialVisualization { | ||||||
|                     return t.addCommentAndClose |                     return t.addCommentAndClose | ||||||
|                 }) |                 }) | ||||||
|             ) |             ) | ||||||
|         ).onClick(() => { |         ).onClick(async () => { | ||||||
|             const id = tags.data[args[1] ?? "id"] |             const id = tags.data[args[1] ?? "id"] | ||||||
|             state.osmConnection.closeNote(id, txt.data).then((_) => { |            await  state.osmConnection.closeNote(id, txt.data) | ||||||
|                 tags.data["closed_at"] = new Date().toISOString() |             tags.data["closed_at"] = new Date().toISOString() | ||||||
|                 tags.ping() |             tags.ping() | ||||||
|             }) |  | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         const reopen = new SubtleButton( |         const reopen = new SubtleButton( | ||||||
|  | @ -80,12 +84,11 @@ export class AddNoteCommentViz implements SpecialVisualization { | ||||||
|                     return t.reopenNoteAndComment |                     return t.reopenNoteAndComment | ||||||
|                 }) |                 }) | ||||||
|             ) |             ) | ||||||
|         ).onClick(() => { |         ).onClick(async () => { | ||||||
|             const id = tags.data[args[1] ?? "id"] |             const id = tags.data[args[1] ?? "id"] | ||||||
|             state.osmConnection.reopenNote(id, txt.data).then((_) => { |             await state.osmConnection.reopenNote(id, txt.data) | ||||||
|                 tags.data["closed_at"] = undefined |             tags.data["closed_at"] = undefined | ||||||
|                 tags.ping() |             tags.ping() | ||||||
|             }) |  | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "") |         const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "") | ||||||
|  |  | ||||||
|  | @ -1,7 +1,5 @@ | ||||||
| import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" |  | ||||||
| import BaseUIElement from "../BaseUIElement" | import BaseUIElement from "../BaseUIElement" | ||||||
| import { Stores, UIEventSource } from "../../Logic/UIEventSource" | import { Stores, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import { DefaultGuiState } from "../DefaultGuiState" |  | ||||||
| import { SubtleButton } from "../Base/SubtleButton" | import { SubtleButton } from "../Base/SubtleButton" | ||||||
| import Img from "../Base/Img" | import Img from "../Base/Img" | ||||||
| import { FixedUiElement } from "../Base/FixedUiElement" | import { FixedUiElement } from "../Base/FixedUiElement" | ||||||
|  | @ -9,8 +7,6 @@ import Combine from "../Base/Combine" | ||||||
| import Link from "../Base/Link" | import Link from "../Base/Link" | ||||||
| import { SubstitutedTranslation } from "../SubstitutedTranslation" | import { SubstitutedTranslation } from "../SubstitutedTranslation" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import Minimap from "../Base/Minimap" |  | ||||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" |  | ||||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" | import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" | ||||||
| import { VariableUiElement } from "../Base/VariableUIElement" | import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
| import Loading from "../Base/Loading" | import Loading from "../Base/Loading" | ||||||
|  | @ -23,15 +19,21 @@ import FilteredLayer from "../../Models/FilteredLayer" | ||||||
| import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" | import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" | ||||||
| import Lazy from "../Base/Lazy" | import Lazy from "../Base/Lazy" | ||||||
| import List from "../Base/List" | import List from "../Base/List" | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  | import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource" | ||||||
|  | import { MapLibreAdaptor } from "../Map/MapLibreAdaptor" | ||||||
|  | import ShowDataLayer from "../Map/ShowDataLayer" | ||||||
|  | import SvelteUIElement from "../Base/SvelteUIElement" | ||||||
|  | import MaplibreMap from "../Map/MaplibreMap.svelte" | ||||||
| 
 | 
 | ||||||
| export interface AutoAction extends SpecialVisualization { | export interface AutoAction extends SpecialVisualization { | ||||||
|     supportsAutoAction: boolean |     supportsAutoAction: boolean | ||||||
| 
 | 
 | ||||||
|     applyActionOn( |     applyActionOn( | ||||||
|         state: { |         state: { | ||||||
|             layoutToUse: LayoutConfig |             layout: LayoutConfig | ||||||
|             changes: Changes |             changes: Changes | ||||||
|  |             indexedFeatures: IndexedFeatureSource | ||||||
|         }, |         }, | ||||||
|         tagSource: UIEventSource<any>, |         tagSource: UIEventSource<any>, | ||||||
|         argument: string[] |         argument: string[] | ||||||
|  | @ -43,7 +45,7 @@ class ApplyButton extends UIElement { | ||||||
|     private readonly text: string |     private readonly text: string | ||||||
|     private readonly targetTagRendering: string |     private readonly targetTagRendering: string | ||||||
|     private readonly target_layer_id: string |     private readonly target_layer_id: string | ||||||
|     private readonly state: FeaturePipelineState |     private readonly state: SpecialVisualizationState | ||||||
|     private readonly target_feature_ids: string[] |     private readonly target_feature_ids: string[] | ||||||
|     private readonly buttonState = new UIEventSource< |     private readonly buttonState = new UIEventSource< | ||||||
|         "idle" | "running" | "done" | { error: string } |         "idle" | "running" | "done" | { error: string } | ||||||
|  | @ -52,7 +54,7 @@ class ApplyButton extends UIElement { | ||||||
|     private readonly tagRenderingConfig: TagRenderingConfig |     private readonly tagRenderingConfig: TagRenderingConfig | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         state: FeaturePipelineState, |         state: SpecialVisualizationState, | ||||||
|         target_feature_ids: string[], |         target_feature_ids: string[], | ||||||
|         options: { |         options: { | ||||||
|             target_layer_id: string |             target_layer_id: string | ||||||
|  | @ -68,9 +70,7 @@ class ApplyButton extends UIElement { | ||||||
|         this.targetTagRendering = options.targetTagRendering |         this.targetTagRendering = options.targetTagRendering | ||||||
|         this.text = options.text |         this.text = options.text | ||||||
|         this.icon = options.icon |         this.icon = options.icon | ||||||
|         this.layer = this.state.filteredLayers.data.find( |         this.layer = this.state.layerState.filteredLayers.get(this.target_layer_id) | ||||||
|             (l) => l.layerDef.id === this.target_layer_id |  | ||||||
|         ) |  | ||||||
|         this.tagRenderingConfig = this.layer.layerDef.tagRenderings.find( |         this.tagRenderingConfig = this.layer.layerDef.tagRenderings.find( | ||||||
|             (tr) => tr.id === this.targetTagRendering |             (tr) => tr.id === this.targetTagRendering | ||||||
|         ) |         ) | ||||||
|  | @ -101,22 +101,23 @@ class ApplyButton extends UIElement { | ||||||
|             ), |             ), | ||||||
|         ]).SetClass("subtle") |         ]).SetClass("subtle") | ||||||
| 
 | 
 | ||||||
|         const previewMap = Minimap.createMiniMap({ |         const mlmap = new UIEventSource(undefined) | ||||||
|             allowMoving: false, |         const mla = new MapLibreAdaptor(mlmap, { | ||||||
|             background: this.state.backgroundLayer, |             rasterLayer: this.state.mapProperties.rasterLayer, | ||||||
|             addLayerControl: true, |         }) | ||||||
|         }).SetClass("h-48") |         mla.allowZooming.setData(false) | ||||||
|  |         mla.allowMoving.setData(false) | ||||||
|  | 
 | ||||||
|  |         const previewMap = new SvelteUIElement(MaplibreMap, { map: mlmap }).SetClass("h-48") | ||||||
| 
 | 
 | ||||||
|         const features = this.target_feature_ids.map((id) => |         const features = this.target_feature_ids.map((id) => | ||||||
|             this.state.allElements.ContainingFeatures.get(id) |             this.state.indexedFeatures.featuresById.data.get(id) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         new ShowDataLayer({ |         new ShowDataLayer(mlmap, { | ||||||
|             leafletMap: previewMap.leafletMap, |  | ||||||
|             zoomToFeatures: true, |  | ||||||
|             features: StaticFeatureSource.fromGeojson(features), |             features: StaticFeatureSource.fromGeojson(features), | ||||||
|             state: this.state, |             zoomToFeatures: true, | ||||||
|             layerToShow: this.layer.layerDef, |             layer: this.layer.layerDef, | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         return new VariableUiElement( |         return new VariableUiElement( | ||||||
|  | @ -144,7 +145,7 @@ class ApplyButton extends UIElement { | ||||||
|             console.log("Applying auto-action on " + this.target_feature_ids.length + " features") |             console.log("Applying auto-action on " + this.target_feature_ids.length + " features") | ||||||
| 
 | 
 | ||||||
|             for (const targetFeatureId of this.target_feature_ids) { |             for (const targetFeatureId of this.target_feature_ids) { | ||||||
|                 const featureTags = this.state.allElements.getEventSourceById(targetFeatureId) |                 const featureTags = this.state.featureProperties.getStore(targetFeatureId) | ||||||
|                 const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt |                 const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt | ||||||
|                 const specialRenderings = Utils.NoNull( |                 const specialRenderings = Utils.NoNull( | ||||||
|                     SubstitutedTranslation.ExtractSpecialComponents(rendering).map((x) => x.special) |                     SubstitutedTranslation.ExtractSpecialComponents(rendering).map((x) => x.special) | ||||||
|  | @ -153,8 +154,8 @@ class ApplyButton extends UIElement { | ||||||
|                 if (specialRenderings.length == 0) { |                 if (specialRenderings.length == 0) { | ||||||
|                     console.warn( |                     console.warn( | ||||||
|                         "AutoApply: feature " + |                         "AutoApply: feature " + | ||||||
|                             targetFeatureId + |                         targetFeatureId + | ||||||
|                             " got a rendering without supported auto actions:", |                         " got a rendering without supported auto actions:", | ||||||
|                         rendering |                         rendering | ||||||
|                     ) |                     ) | ||||||
|                 } |                 } | ||||||
|  | @ -224,7 +225,7 @@ export default class AutoApplyButton implements SpecialVisualization { | ||||||
|             "To effectively use this button, you'll need some ingredients:", |             "To effectively use this button, you'll need some ingredients:", | ||||||
|             new List([ |             new List([ | ||||||
|                 "A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " + |                 "A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " + | ||||||
|                     supportedActions.join(", "), |                 supportedActions.join(", "), | ||||||
|                 "A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ", |                 "A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ", | ||||||
|                 new Link("current_view", "./BuiltinLayers.md#current_view"), |                 new Link("current_view", "./BuiltinLayers.md#current_view"), | ||||||
|                 "Then, use a calculated tag on the host feature to determine the overlapping object ids", |                 "Then, use a calculated tag on the host feature to determine the overlapping object ids", | ||||||
|  | @ -234,18 +235,17 @@ export default class AutoApplyButton implements SpecialVisualization { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     constr( |     constr( | ||||||
|         state: FeaturePipelineState, |         state: SpecialVisualizationState, | ||||||
|         tagSource: UIEventSource<any>, |         tagSource: UIEventSource<Record<string, string>>, | ||||||
|         argument: string[], |         argument: string[] | ||||||
|         guistate: DefaultGuiState |  | ||||||
|     ): BaseUIElement { |     ): BaseUIElement { | ||||||
|         try { |         try { | ||||||
|             if ( |             if ( | ||||||
|                 !state.layoutToUse.official && |                 !state.layout.official && | ||||||
|                 !( |                 !( | ||||||
|                     state.featureSwitchIsTesting.data || |                     state.featureSwitchIsTesting.data || | ||||||
|                     state.osmConnection._oauth_config.url === |                     state.osmConnection._oauth_config.url === | ||||||
|                         OsmConnection.oauth_configs["osm-test"].url |                     OsmConnection.oauth_configs["osm-test"].url | ||||||
|                 ) |                 ) | ||||||
|             ) { |             ) { | ||||||
|                 const t = Translations.t.general.add.import |                 const t = Translations.t.general.add.import | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" |  | ||||||
| import BaseUIElement from "../BaseUIElement" | import BaseUIElement from "../BaseUIElement" | ||||||
| import Translations from "../i18n/Translations" | import Translations from "../i18n/Translations" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
|  | @ -7,7 +6,8 @@ import Img from "../Base/Img" | ||||||
| import { SubtleButton } from "../Base/SubtleButton" | import { SubtleButton } from "../Base/SubtleButton" | ||||||
| import Toggle from "../Input/Toggle" | import Toggle from "../Input/Toggle" | ||||||
| import { LoginToggle } from "./LoginButton" | import { LoginToggle } from "./LoginButton" | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  | import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
| 
 | 
 | ||||||
| export class CloseNoteButton implements SpecialVisualization { | export class CloseNoteButton implements SpecialVisualization { | ||||||
|     public readonly funcName = "close_note" |     public readonly funcName = "close_note" | ||||||
|  | @ -43,7 +43,11 @@ export class CloseNoteButton implements SpecialVisualization { | ||||||
|         }, |         }, | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     public constr(state: FeaturePipelineState, tags, args): BaseUIElement { |     public constr( | ||||||
|  |         state: SpecialVisualizationState, | ||||||
|  |         tags: UIEventSource<Record<string, string>>, | ||||||
|  |         args: string[] | ||||||
|  |     ): BaseUIElement { | ||||||
|         const t = Translations.t.notes |         const t = Translations.t.notes | ||||||
| 
 | 
 | ||||||
|         const params: { |         const params: { | ||||||
|  | @ -78,7 +82,7 @@ export class CloseNoteButton implements SpecialVisualization { | ||||||
|             closeButton = new Toggle( |             closeButton = new Toggle( | ||||||
|                 closeButton, |                 closeButton, | ||||||
|                 params.zoomButton ?? "", |                 params.zoomButton ?? "", | ||||||
|                 state.locationControl.map((l) => l.zoom >= Number(params.minZoom)) |                 state.mapProperties.zoom.map((zoom) => zoom >= Number(params.minZoom)) | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,14 +4,15 @@ import Svg from "../../Svg" | ||||||
| import Combine from "../Base/Combine" | import Combine from "../Base/Combine" | ||||||
| import { GeoOperations } from "../../Logic/GeoOperations" | import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  | import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
| 
 | 
 | ||||||
| export class ExportAsGpxViz implements SpecialVisualization { | export class ExportAsGpxViz implements SpecialVisualization { | ||||||
|     funcName = "export_as_gpx" |     funcName = "export_as_gpx" | ||||||
|     docs = "Exports the selected feature as GPX-file" |     docs = "Exports the selected feature as GPX-file" | ||||||
|     args = [] |     args = [] | ||||||
| 
 | 
 | ||||||
|     constr(state, tagSource) { |     constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>) { | ||||||
|         const t = Translations.t.general.download |         const t = Translations.t.general.download | ||||||
| 
 | 
 | ||||||
|         return new SubtleButton( |         return new SubtleButton( | ||||||
|  | @ -23,10 +24,10 @@ export class ExportAsGpxViz implements SpecialVisualization { | ||||||
|         ).onClick(() => { |         ).onClick(() => { | ||||||
|             console.log("Exporting as GPX!") |             console.log("Exporting as GPX!") | ||||||
|             const tags = tagSource.data |             const tags = tagSource.data | ||||||
|             const feature = state.allElements.ContainingFeatures.get(tags.id) |             const feature = state.indexedFeatures.featuresById.data.get(tags.id) | ||||||
|             const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags) |             const layer = state?.layout?.getMatchingLayer(tags) | ||||||
|             const gpx = GeoOperations.AsGpx(feature, matchingLayer) |             const gpx = GeoOperations.AsGpx(feature, { layer }) | ||||||
|             const title = matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track" |             const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track" | ||||||
|             Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", { |             Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", { | ||||||
|                 mimetype: "{gpx=application/gpx+xml}", |                 mimetype: "{gpx=application/gpx+xml}", | ||||||
|             }) |             }) | ||||||
|  |  | ||||||
|  | @ -1,9 +1,8 @@ | ||||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import { FixedUiElement } from "../Base/FixedUiElement" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
| // import Histogram from "../BigComponents/Histogram";
 | import Histogram from "../BigComponents/Histogram" | ||||||
| // import {SpecialVisualization} from "../SpecialVisualization";
 |  | ||||||
| 
 | 
 | ||||||
| export class HistogramViz { | export class HistogramViz implements SpecialVisualization { | ||||||
|     funcName = "histogram" |     funcName = "histogram" | ||||||
|     docs = "Create a histogram for a list of given values, read from the properties." |     docs = "Create a histogram for a list of given values, read from the properties." | ||||||
|     example = |     example = | ||||||
|  | @ -30,7 +29,11 @@ export class HistogramViz { | ||||||
|         }, |         }, | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     constr(state, tagSource: UIEventSource<any>, args: string[]) { |     constr( | ||||||
|  |         state: SpecialVisualizationState, | ||||||
|  |         tagSource: UIEventSource<Record<string, string>>, | ||||||
|  |         args: string[] | ||||||
|  |     ) { | ||||||
|         let assignColors = undefined |         let assignColors = undefined | ||||||
|         if (args.length >= 3) { |         if (args.length >= 3) { | ||||||
|             const colors = [...args] |             const colors = [...args] | ||||||
|  | @ -63,10 +66,8 @@ export class HistogramViz { | ||||||
|                 return undefined |                 return undefined | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|         return new FixedUiElement("HISTORGRAM") |  | ||||||
|         /* |  | ||||||
|         return new Histogram(listSource, args[1], args[2], { |         return new Histogram(listSource, args[1], args[2], { | ||||||
|             assignColor: assignColors, |             assignColor: assignColors, | ||||||
|         })*/ |         }) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,51 +1,47 @@ | ||||||
| import BaseUIElement from "../BaseUIElement" | import BaseUIElement from "../BaseUIElement"; | ||||||
| import { SubtleButton } from "../Base/SubtleButton" | import { SubtleButton } from "../Base/SubtleButton"; | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" | import { UIEventSource } from "../../Logic/UIEventSource"; | ||||||
| import Combine from "../Base/Combine" | import Combine from "../Base/Combine"; | ||||||
| import { VariableUiElement } from "../Base/VariableUIElement" | import { VariableUiElement } from "../Base/VariableUIElement"; | ||||||
| import Translations from "../i18n/Translations" | import Translations from "../i18n/Translations"; | ||||||
| import Toggle from "../Input/Toggle" | import Toggle from "../Input/Toggle"; | ||||||
| import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction" | import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; | ||||||
| import Loading from "../Base/Loading" | import Loading from "../Base/Loading"; | ||||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" | import { OsmConnection } from "../../Logic/Osm/OsmConnection"; | ||||||
| import Lazy from "../Base/Lazy" | import Lazy from "../Base/Lazy"; | ||||||
| import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint" | import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"; | ||||||
| import Img from "../Base/Img" | import Img from "../Base/Img"; | ||||||
| import FilteredLayer from "../../Models/FilteredLayer" | import FilteredLayer from "../../Models/FilteredLayer"; | ||||||
| import { FixedUiElement } from "../Base/FixedUiElement" | import { FixedUiElement } from "../Base/FixedUiElement"; | ||||||
| import Svg from "../../Svg" | import Svg from "../../Svg"; | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils"; | ||||||
| import Minimap from "../Base/Minimap" | import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" | import CreateWayWithPointReuseAction, { MergePointConfig } from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction"; | ||||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" | import OsmChangeAction, { OsmCreateAction } from "../../Logic/Osm/Actions/OsmChangeAction"; | ||||||
| import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" | import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; | ||||||
| import CreateWayWithPointReuseAction, { | import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"; | ||||||
|     MergePointConfig, | import { PresetInfo } from "../BigComponents/SimpleAddUI"; | ||||||
| } from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction" | import { TagUtils } from "../../Logic/Tags/TagUtils"; | ||||||
| import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction" | import { And } from "../../Logic/Tags/And"; | ||||||
| import FeatureSource from "../../Logic/FeatureSource/FeatureSource" | import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction"; | ||||||
| import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject" | import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction"; | ||||||
| import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" | import { Tag } from "../../Logic/Tags/Tag"; | ||||||
| import { DefaultGuiState } from "../DefaultGuiState" | import TagApplyButton from "./TagApplyButton"; | ||||||
| import { PresetInfo } from "../BigComponents/SimpleAddUI" | import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||||
| import { TagUtils } from "../../Logic/Tags/TagUtils" | import conflation_json from "../../assets/layers/conflation/conflation.json"; | ||||||
| import { And } from "../../Logic/Tags/And" | import { GeoOperations } from "../../Logic/GeoOperations"; | ||||||
| import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction" | import { LoginToggle } from "./LoginButton"; | ||||||
| import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction" | import { AutoAction } from "./AutoApplyButton"; | ||||||
| import { Tag } from "../../Logic/Tags/Tag" | import Hash from "../../Logic/Web/Hash"; | ||||||
| import TagApplyButton from "./TagApplyButton" | import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig"; | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"; | ||||||
| import conflation_json from "../../assets/layers/conflation/conflation.json" |  | ||||||
| import { GeoOperations } from "../../Logic/GeoOperations" |  | ||||||
| import { LoginToggle } from "./LoginButton" |  | ||||||
| import { AutoAction } from "./AutoApplyButton" |  | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" |  | ||||||
| import { Changes } from "../../Logic/Osm/Changes" |  | ||||||
| import { ElementStorage } from "../../Logic/ElementStorage" |  | ||||||
| import Hash from "../../Logic/Web/Hash" |  | ||||||
| import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig" |  | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" |  | ||||||
| import Maproulette from "../../Logic/Maproulette"; | import Maproulette from "../../Logic/Maproulette"; | ||||||
|  | import { Feature, Point } from "geojson"; | ||||||
|  | import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"; | ||||||
|  | import ShowDataLayer from "../Map/ShowDataLayer"; | ||||||
|  | import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"; | ||||||
|  | import SvelteUIElement from "../Base/SvelteUIElement"; | ||||||
|  | import MaplibreMap from "../Map/MaplibreMap.svelte"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * A helper class for the various import-flows. |  * A helper class for the various import-flows. | ||||||
|  | @ -106,7 +102,7 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     abstract constructElement( |     abstract constructElement( | ||||||
|         state: FeaturePipelineState, |         state: SpecialVisualizationState, | ||||||
|         args: { |         args: { | ||||||
|             max_snap_distance: string |             max_snap_distance: string | ||||||
|             snap_onto_layers: string |             snap_onto_layers: string | ||||||
|  | @ -116,13 +112,16 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
|             newTags: UIEventSource<any> |             newTags: UIEventSource<any> | ||||||
|             targetLayer: string |             targetLayer: string | ||||||
|         }, |         }, | ||||||
|         tagSource: UIEventSource<any>, |         tagSource: UIEventSource<Record<string, string>>, | ||||||
|         guiState: DefaultGuiState, |         feature: Feature, | ||||||
|         feature: any, |  | ||||||
|         onCancelClicked: () => void |         onCancelClicked: () => void | ||||||
|     ): BaseUIElement |     ): BaseUIElement | ||||||
| 
 | 
 | ||||||
|     constr(state, tagSource: UIEventSource<any>, argsRaw, guiState) { |     constr( | ||||||
|  |         state: SpecialVisualizationState, | ||||||
|  |         tagSource: UIEventSource<Record<string, string>>, | ||||||
|  |         argsRaw: string[] | ||||||
|  |     ) { | ||||||
|         /** |         /** | ||||||
|          * Some generic import button pre-validation is implemented here: |          * Some generic import button pre-validation is implemented here: | ||||||
|          * - Are we logged in? |          * - Are we logged in? | ||||||
|  | @ -139,7 +138,7 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
|         { |         { | ||||||
|             // Some initial validation
 |             // Some initial validation
 | ||||||
|             if ( |             if ( | ||||||
|                 !state.layoutToUse.official && |                 !state.layout.official && | ||||||
|                 !( |                 !( | ||||||
|                     state.featureSwitchIsTesting.data || |                     state.featureSwitchIsTesting.data || | ||||||
|                     state.osmConnection._oauth_config.url === |                     state.osmConnection._oauth_config.url === | ||||||
|  | @ -148,11 +147,9 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
|             ) { |             ) { | ||||||
|                 return new Combine([t.officialThemesOnly.SetClass("alert"), t.howToTest]) |                 return new Combine([t.officialThemesOnly.SetClass("alert"), t.howToTest]) | ||||||
|             } |             } | ||||||
|             const targetLayer: FilteredLayer = state.filteredLayers.data.filter( |             const targetLayer: FilteredLayer = state.layerState.filteredLayers.get(args.targetLayer) | ||||||
|                 (fl) => fl.layerDef.id === args.targetLayer |  | ||||||
|             )[0] |  | ||||||
|             if (targetLayer === undefined) { |             if (targetLayer === undefined) { | ||||||
|                 const e = `Target layer not defined: error in import button for theme: ${state.layoutToUse.id}: layer ${args.targetLayer} not found` |                 const e = `Target layer not defined: error in import button for theme: ${state.layout.id}: layer ${args.targetLayer} not found` | ||||||
|                 console.error(e) |                 console.error(e) | ||||||
|                 return new FixedUiElement(e).SetClass("alert") |                 return new FixedUiElement(e).SetClass("alert") | ||||||
|             } |             } | ||||||
|  | @ -167,7 +164,7 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
|         const inviteToImportButton = new SubtleButton(img, args.text) |         const inviteToImportButton = new SubtleButton(img, args.text) | ||||||
| 
 | 
 | ||||||
|         const id = tagSource.data.id |         const id = tagSource.data.id | ||||||
|         const feature = state.allElements.ContainingFeatures.get(id) |         const feature = state.indexedFeatures.featuresById.data.get(id) | ||||||
| 
 | 
 | ||||||
|         // Explanation of the tags that will be applied onto the imported/conflated object
 |         // Explanation of the tags that will be applied onto the imported/conflated object
 | ||||||
| 
 | 
 | ||||||
|  | @ -205,22 +202,13 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
|             return tags._imported === "yes" |             return tags._imported === "yes" | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         /**** THe actual panel showing the import guiding map ****/ |         /**** The actual panel showing the import guiding map ****/ | ||||||
|         const importGuidingPanel = this.constructElement( |         const importGuidingPanel = this.constructElement(state, args, tagSource, feature, () => | ||||||
|             state, |             importClicked.setData(false) | ||||||
|             args, |  | ||||||
|             tagSource, |  | ||||||
|             guiState, |  | ||||||
|             feature, |  | ||||||
|             () => importClicked.setData(false) |  | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         const importFlow = new Toggle( |         const importFlow = new Toggle( | ||||||
|             new Toggle( |             new Toggle(new Loading(t0.stillLoading), importGuidingPanel, state.dataIsLoading), | ||||||
|                 new Loading(t0.stillLoading), |  | ||||||
|                 importGuidingPanel, |  | ||||||
|                 state.featurePipeline.runningQuery |  | ||||||
|             ), |  | ||||||
|             inviteToImportButton, |             inviteToImportButton, | ||||||
|             importClicked |             importClicked | ||||||
|         ) |         ) | ||||||
|  | @ -230,7 +218,7 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
|                 new Toggle( |                 new Toggle( | ||||||
|                     new Toggle(t.hasBeenImported, importFlow, isImported), |                     new Toggle(t.hasBeenImported, importFlow, isImported), | ||||||
|                     t.zoomInMore.SetClass("alert block"), |                     t.zoomInMore.SetClass("alert block"), | ||||||
|                     state.locationControl.map((l) => l.zoom >= 18) |                     state.mapProperties.zoom.map((zoom) => zoom >= 18) | ||||||
|                 ), |                 ), | ||||||
|                 pleaseLoginButton, |                 pleaseLoginButton, | ||||||
|                 state |                 state | ||||||
|  | @ -258,8 +246,13 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
| 
 | 
 | ||||||
|     protected abstract canBeImported(feature: any) |     protected abstract canBeImported(feature: any) | ||||||
| 
 | 
 | ||||||
|  |     private static readonly conflationLayer = new LayerConfig( | ||||||
|  |         <LayerConfigJson>conflation_json, | ||||||
|  |         "all_known_layers", | ||||||
|  |         true | ||||||
|  |     ) | ||||||
|     protected createConfirmPanelForWay( |     protected createConfirmPanelForWay( | ||||||
|         state: FeaturePipelineState, |         state: SpecialVisualizationState, | ||||||
|         args: { |         args: { | ||||||
|             max_snap_distance: string |             max_snap_distance: string | ||||||
|             snap_onto_layers: string |             snap_onto_layers: string | ||||||
|  | @ -270,32 +263,32 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
|         }, |         }, | ||||||
|         feature: any, |         feature: any, | ||||||
|         originalFeatureTags: UIEventSource<any>, |         originalFeatureTags: UIEventSource<any>, | ||||||
|         action: OsmChangeAction & { getPreview(): Promise<FeatureSource>; newElementId?: string }, |         action: OsmChangeAction & { getPreview?(): Promise<FeatureSource>; newElementId?: string }, | ||||||
|         onCancel: () => void |         onCancel: () => void | ||||||
|     ): BaseUIElement { |     ): BaseUIElement { | ||||||
|         const self = this |         const self = this | ||||||
|         const confirmationMap = Minimap.createMiniMap({ |         const map = new UIEventSource(undefined) | ||||||
|             allowMoving: state.featureSwitchIsDebugging.data ?? false, |         new MapLibreAdaptor(map, { | ||||||
|             background: state.backgroundLayer, |             allowMoving: UIEventSource.feedFrom(state.featureSwitchIsTesting), | ||||||
|  |             allowZooming: UIEventSource.feedFrom(state.featureSwitchIsTesting), | ||||||
|  |             rasterLayer: state.mapProperties.rasterLayer, | ||||||
|         }) |         }) | ||||||
|  |         const confirmationMap = new SvelteUIElement(MaplibreMap, { map }) | ||||||
|         confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl") |         confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl") | ||||||
| 
 | 
 | ||||||
|         // SHow all relevant data - including (eventually) the way of which the geometry will be replaced
 |         ShowDataLayer.showMultipleLayers( | ||||||
|         new ShowDataMultiLayer({ |             map, | ||||||
|             leafletMap: confirmationMap.leafletMap, |             new StaticFeatureSource([feature]), | ||||||
|             zoomToFeatures: true, |             state.layout.layers, | ||||||
|             features: StaticFeatureSource.fromGeojson([feature]), |             { zoomToFeatures: true } | ||||||
|             state: state, |         ) | ||||||
|             layers: state.filteredLayers, |         // Show all relevant data - including (eventually) the way of which the geometry will be replaced
 | ||||||
|         }) |  | ||||||
| 
 | 
 | ||||||
|         action.getPreview().then((changePreview) => { |         action.getPreview().then((changePreview) => { | ||||||
|             new ShowDataLayer({ |             new ShowDataLayer(map, { | ||||||
|                 leafletMap: confirmationMap.leafletMap, |  | ||||||
|                 zoomToFeatures: false, |                 zoomToFeatures: false, | ||||||
|                 features: changePreview, |                 features: changePreview, | ||||||
|                 state, |                 layer: AbstractImportButton.conflationLayer, | ||||||
|                 layerToShow: new LayerConfig(conflation_json, "all_known_layers", true), |  | ||||||
|             }) |             }) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  | @ -317,9 +310,9 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
|             { |             { | ||||||
|                 originalFeatureTags.data["_imported"] = "yes" |                 originalFeatureTags.data["_imported"] = "yes" | ||||||
|                 originalFeatureTags.ping() // will set isImported as per its definition
 |                 originalFeatureTags.ping() // will set isImported as per its definition
 | ||||||
|                 state.changes.applyAction(action) |                 await state.changes.applyAction(action) | ||||||
|                 const newId = action.newElementId ?? action.mainObjectId |                 const newId = action.newElementId ?? action.mainObjectId | ||||||
|                 state.selectedElement.setData(state.allElements.ContainingFeatures.get(newId)) |                 state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(newId)) | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  | @ -392,7 +385,7 @@ export class ConflateButton extends AbstractImportButton { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     constructElement( |     constructElement( | ||||||
|         state: FeaturePipelineState, |         state: SpecialVisualizationState, | ||||||
|         args: { |         args: { | ||||||
|             max_snap_distance: string |             max_snap_distance: string | ||||||
|             snap_onto_layers: string |             snap_onto_layers: string | ||||||
|  | @ -403,8 +396,7 @@ export class ConflateButton extends AbstractImportButton { | ||||||
|             targetLayer: string |             targetLayer: string | ||||||
|         }, |         }, | ||||||
|         tagSource: UIEventSource<any>, |         tagSource: UIEventSource<any>, | ||||||
|         guiState: DefaultGuiState, |         feature: Feature, | ||||||
|         feature: any, |  | ||||||
|         onCancelClicked: () => void |         onCancelClicked: () => void | ||||||
|     ): BaseUIElement { |     ): BaseUIElement { | ||||||
|         const nodesMustMatch = args.snap_onto_layers |         const nodesMustMatch = args.snap_onto_layers | ||||||
|  | @ -424,10 +416,15 @@ export class ConflateButton extends AbstractImportButton { | ||||||
|         const key = args["way_to_conflate"] |         const key = args["way_to_conflate"] | ||||||
|         const wayToConflate = tagSource.data[key] |         const wayToConflate = tagSource.data[key] | ||||||
|         feature = GeoOperations.removeOvernoding(feature) |         feature = GeoOperations.removeOvernoding(feature) | ||||||
|         const action = new ReplaceGeometryAction(state, feature, wayToConflate, { |         const action: OsmChangeAction & { getPreview(): Promise<any> } = new ReplaceGeometryAction( | ||||||
|             theme: state.layoutToUse.id, |             state, | ||||||
|             newTags: args.newTags.data, |             feature, | ||||||
|         }) |             wayToConflate, | ||||||
|  |             { | ||||||
|  |                 theme: state.layout.id, | ||||||
|  |                 newTags: args.newTags.data, | ||||||
|  |             } | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         return this.createConfirmPanelForWay( |         return this.createConfirmPanelForWay( | ||||||
|             state, |             state, | ||||||
|  | @ -498,9 +495,9 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction | ||||||
|             newTags: UIEventSource<any> |             newTags: UIEventSource<any> | ||||||
|             targetLayer: string |             targetLayer: string | ||||||
|         }, |         }, | ||||||
|         state: FeaturePipelineState, |         state: SpecialVisualizationState, | ||||||
|         mergeConfigs: any[] |         mergeConfigs: any[] | ||||||
|     ) { |     ): OsmCreateAction & { getPreview(): Promise<FeatureSource>; newElementId?: string } { | ||||||
|         const coors = feature.geometry.coordinates |         const coors = feature.geometry.coordinates | ||||||
|         if (feature.geometry.type === "Polygon" && coors.length > 1) { |         if (feature.geometry.type === "Polygon" && coors.length > 1) { | ||||||
|             const outer = coors[0] |             const outer = coors[0] | ||||||
|  | @ -525,8 +522,8 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async applyActionOn( |     async applyActionOn( | ||||||
|         state: { layoutToUse: LayoutConfig; changes: Changes; allElements: ElementStorage }, |         state: SpecialVisualizationState, | ||||||
|         originalFeatureTags: UIEventSource<any>, |         originalFeatureTags: UIEventSource<Record<string, string>>, | ||||||
|         argument: string[] |         argument: string[] | ||||||
|     ): Promise<void> { |     ): Promise<void> { | ||||||
|         const id = originalFeatureTags.data.id |         const id = originalFeatureTags.data.id | ||||||
|  | @ -535,14 +532,9 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction | ||||||
|         } |         } | ||||||
|         AbstractImportButton.importedIds.add(originalFeatureTags.data.id) |         AbstractImportButton.importedIds.add(originalFeatureTags.data.id) | ||||||
|         const args = this.parseArgs(argument, originalFeatureTags) |         const args = this.parseArgs(argument, originalFeatureTags) | ||||||
|         const feature = state.allElements.ContainingFeatures.get(id) |         const feature = state.indexedFeatures.featuresById.data.get(id) | ||||||
|         const mergeConfigs = this.GetMergeConfig(args) |         const mergeConfigs = this.GetMergeConfig(args) | ||||||
|         const action = ImportWayButton.CreateAction( |         const action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs) | ||||||
|             feature, |  | ||||||
|             args, |  | ||||||
|             <FeaturePipelineState>state, |  | ||||||
|             mergeConfigs |  | ||||||
|         ) |  | ||||||
|         await state.changes.applyAction(action) |         await state.changes.applyAction(action) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -557,7 +549,13 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction | ||||||
|         return deps |         return deps | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     constructElement(state, args, originalFeatureTags, guiState, feature, onCancel): BaseUIElement { |     constructElement( | ||||||
|  |         state: SpecialVisualizationState, | ||||||
|  |         args, | ||||||
|  |         originalFeatureTags: UIEventSource<Record<string, string>>, | ||||||
|  |         feature, | ||||||
|  |         onCancel | ||||||
|  |     ): BaseUIElement { | ||||||
|         const geometry = feature.geometry |         const geometry = feature.geometry | ||||||
| 
 | 
 | ||||||
|         if (!(geometry.type == "LineString" || geometry.type === "Polygon")) { |         if (!(geometry.type == "LineString" || geometry.type === "Polygon")) { | ||||||
|  | @ -567,7 +565,12 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction | ||||||
| 
 | 
 | ||||||
|         // Upload the way to OSM
 |         // Upload the way to OSM
 | ||||||
|         const mergeConfigs = this.GetMergeConfig(args) |         const mergeConfigs = this.GetMergeConfig(args) | ||||||
|         let action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs) |         let action: OsmCreateAction & {getPreview?: any} = ImportWayButton.CreateAction( | ||||||
|  |             feature, | ||||||
|  |             args, | ||||||
|  |             state, | ||||||
|  |             mergeConfigs | ||||||
|  |         ) | ||||||
|         return this.createConfirmPanelForWay( |         return this.createConfirmPanelForWay( | ||||||
|             state, |             state, | ||||||
|             args, |             args, | ||||||
|  | @ -663,10 +666,9 @@ export class ImportPointButton extends AbstractImportButton { | ||||||
|             note_id: string |             note_id: string | ||||||
|             maproulette_id: string |             maproulette_id: string | ||||||
|         }, |         }, | ||||||
|         state: FeaturePipelineState, |         state: SpecialVisualizationState, | ||||||
|         guiState: DefaultGuiState, |  | ||||||
|         originalFeatureTags: UIEventSource<any>, |         originalFeatureTags: UIEventSource<any>, | ||||||
|         feature: any, |         feature: Feature<Point>, | ||||||
|         onCancel: () => void, |         onCancel: () => void, | ||||||
|         close: () => void |         close: () => void | ||||||
|     ): BaseUIElement { |     ): BaseUIElement { | ||||||
|  | @ -690,7 +692,7 @@ export class ImportPointButton extends AbstractImportButton { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, { |             const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, { | ||||||
|                 theme: state.layoutToUse.id, |                 theme: state.layout.id, | ||||||
|                 changeType: "import", |                 changeType: "import", | ||||||
|                 snapOnto: <OsmWay>snapOnto, |                 snapOnto: <OsmWay>snapOnto, | ||||||
|                 specialMotivation: specialMotivation, |                 specialMotivation: specialMotivation, | ||||||
|  | @ -698,7 +700,7 @@ export class ImportPointButton extends AbstractImportButton { | ||||||
| 
 | 
 | ||||||
|             await state.changes.applyAction(newElementAction) |             await state.changes.applyAction(newElementAction) | ||||||
|             state.selectedElement.setData( |             state.selectedElement.setData( | ||||||
|                 state.allElements.ContainingFeatures.get(newElementAction.newElementId) |                 state.indexedFeatures.featuresById.data.get(newElementAction.newElementId) | ||||||
|             ) |             ) | ||||||
|             Hash.hash.setData(newElementAction.newElementId) |             Hash.hash.setData(newElementAction.newElementId) | ||||||
| 
 | 
 | ||||||
|  | @ -742,19 +744,17 @@ export class ImportPointButton extends AbstractImportButton { | ||||||
|         const presetInfo = <PresetInfo>{ |         const presetInfo = <PresetInfo>{ | ||||||
|             tags: args.newTags.data, |             tags: args.newTags.data, | ||||||
|             icon: () => new Img(args.icon), |             icon: () => new Img(args.icon), | ||||||
|             layerToAddTo: state.filteredLayers.data.filter( |             layerToAddTo: state.layerState.filteredLayers.get(args.targetLayer), | ||||||
|                 (l) => l.layerDef.id === args.targetLayer |  | ||||||
|             )[0], |  | ||||||
|             name: args.text, |             name: args.text, | ||||||
|             title: Translations.T(args.text), |             title: Translations.T(args.text), | ||||||
|             preciseInput: preciseInputSpec, // must be explicitely assigned, if 'undefined' won't work otherwise
 |             preciseInput: preciseInputSpec, // must be explicitely assigned, if 'undefined' won't work otherwise
 | ||||||
|             boundsFactor: 3, |             boundsFactor: 3, | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const [lon, lat] = feature.geometry.coordinates |         const [lon, lat] = <[number,number]> feature.geometry.coordinates | ||||||
|         return new ConfirmLocationOfPoint( |         return new ConfirmLocationOfPoint( | ||||||
|             state, |             state, | ||||||
|             guiState.filterViewIsOpened, |             state.guistate.filterViewIsOpened, | ||||||
|             presetInfo, |             presetInfo, | ||||||
|             Translations.W(args.text), |             Translations.W(args.text), | ||||||
|             { |             { | ||||||
|  | @ -783,10 +783,9 @@ export class ImportPointButton extends AbstractImportButton { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     constructElement( |     constructElement( | ||||||
|         state, |         state: SpecialVisualizationState, | ||||||
|         args, |         args, | ||||||
|         originalFeatureTags, |         originalFeatureTags, | ||||||
|         guiState, |  | ||||||
|         feature, |         feature, | ||||||
|         onCancel: () => void |         onCancel: () => void | ||||||
|     ): BaseUIElement { |     ): BaseUIElement { | ||||||
|  | @ -797,7 +796,6 @@ export class ImportPointButton extends AbstractImportButton { | ||||||
|                 ImportPointButton.createConfirmPanelForPoint( |                 ImportPointButton.createConfirmPanelForPoint( | ||||||
|                     args, |                     args, | ||||||
|                     state, |                     state, | ||||||
|                     guiState, |  | ||||||
|                     originalFeatureTags, |                     originalFeatureTags, | ||||||
|                     feature, |                     feature, | ||||||
|                     onCancel, |                     onCancel, | ||||||
|  |  | ||||||
|  | @ -1,9 +1,7 @@ | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
| import BaseUIElement from "../BaseUIElement" | import BaseUIElement from "../BaseUIElement" | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" | import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" |  | ||||||
| import { VariableUiElement } from "../Base/VariableUIElement" | import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
| import { OsmTags } from "../../Models/OsmFeature" |  | ||||||
| import all_languages from "../../assets/language_translations.json" | import all_languages from "../../assets/language_translations.json" | ||||||
| import { Translation } from "../i18n/Translation" | import { Translation } from "../i18n/Translation" | ||||||
| import Combine from "../Base/Combine" | import Combine from "../Base/Combine" | ||||||
|  | @ -16,10 +14,9 @@ import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" | ||||||
| import { And } from "../../Logic/Tags/And" | import { And } from "../../Logic/Tags/And" | ||||||
| import { Tag } from "../../Logic/Tags/Tag" | import { Tag } from "../../Logic/Tags/Tag" | ||||||
| import { EditButton, SaveButton } from "./SaveButton" | import { EditButton, SaveButton } from "./SaveButton" | ||||||
| import { FixedUiElement } from "../Base/FixedUiElement" |  | ||||||
| import Translations from "../i18n/Translations" | import Translations from "../i18n/Translations" | ||||||
| import Toggle from "../Input/Toggle" | import Toggle from "../Input/Toggle" | ||||||
| import { On } from "../../Models/ThemeConfig/Conversion/Conversion" | import { Feature } from "geojson" | ||||||
| 
 | 
 | ||||||
| export class LanguageElement implements SpecialVisualization { | export class LanguageElement implements SpecialVisualization { | ||||||
|     funcName: string = "language_chooser" |     funcName: string = "language_chooser" | ||||||
|  | @ -79,9 +76,10 @@ export class LanguageElement implements SpecialVisualization { | ||||||
|     ` |     ` | ||||||
| 
 | 
 | ||||||
|     constr( |     constr( | ||||||
|         state: FeaturePipelineState, |         state: SpecialVisualizationState, | ||||||
|         tagSource: UIEventSource<OsmTags>, |         tagSource: UIEventSource<Record<string, string>>, | ||||||
|         argument: string[] |         argument: string[], | ||||||
|  |         feature: Feature | ||||||
|     ): BaseUIElement { |     ): BaseUIElement { | ||||||
|         let [key, question, item_render, single_render, all_render, on_no_known_languages, mode] = |         let [key, question, item_render, single_render, all_render, on_no_known_languages, mode] = | ||||||
|             argument |             argument | ||||||
|  | @ -172,7 +170,7 @@ export class LanguageElement implements SpecialVisualization { | ||||||
|                                 new And(selection), |                                 new And(selection), | ||||||
|                                 tagSource.data, |                                 tagSource.data, | ||||||
|                                 { |                                 { | ||||||
|                                     theme: state?.layoutToUse?.id ?? "unkown", |                                     theme: state?.layout?.id ?? "unkown", | ||||||
|                                     changeType: "answer", |                                     changeType: "answer", | ||||||
|                                 } |                                 } | ||||||
|                             ) |                             ) | ||||||
|  |  | ||||||
|  | @ -2,7 +2,9 @@ import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
| import { MapillaryLink } from "../BigComponents/MapillaryLink" | import { MapillaryLink } from "../BigComponents/MapillaryLink" | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" | import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import Loc from "../../Models/Loc" | import Loc from "../../Models/Loc" | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  | import { Feature } from "geojson" | ||||||
|  | import BaseUIElement from "../BaseUIElement" | ||||||
| 
 | 
 | ||||||
| export class MapillaryLinkVis implements SpecialVisualization { | export class MapillaryLinkVis implements SpecialVisualization { | ||||||
|     funcName = "mapillary_link" |     funcName = "mapillary_link" | ||||||
|  | @ -15,9 +17,13 @@ export class MapillaryLinkVis implements SpecialVisualization { | ||||||
|         }, |         }, | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     public constr(state, tagsSource, args) { |     public constr( | ||||||
|         const feat = state.allElements.ContainingFeatures.get(tagsSource.data.id) |         state: SpecialVisualizationState, | ||||||
|         const [lon, lat] = GeoOperations.centerpointCoordinates(feat) |         tagsSource: UIEventSource<Record<string, string>>, | ||||||
|  |         args: string[], | ||||||
|  |         feature: Feature | ||||||
|  |     ): BaseUIElement { | ||||||
|  |         const [lon, lat] = GeoOperations.centerpointCoordinates(feature) | ||||||
|         let zoom = Number(args[0]) |         let zoom = Number(args[0]) | ||||||
|         if (isNaN(zoom)) { |         if (isNaN(zoom)) { | ||||||
|             zoom = 18 |             zoom = 18 | ||||||
|  |  | ||||||
|  | @ -1,9 +1,14 @@ | ||||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import Loc from "../../Models/Loc" |  | ||||||
| import Minimap from "../Base/Minimap" |  | ||||||
| import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" |  | ||||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" | import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  | import { Feature } from "geojson" | ||||||
|  | import { MapLibreAdaptor } from "../Map/MapLibreAdaptor" | ||||||
|  | import SvelteUIElement from "../Base/SvelteUIElement" | ||||||
|  | import MaplibreMap from "../Map/MaplibreMap.svelte" | ||||||
|  | import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" | ||||||
|  | import FilteredLayer from "../../Models/FilteredLayer" | ||||||
|  | import ShowDataLayer from "../Map/ShowDataLayer" | ||||||
|  | import { stat } from "fs" | ||||||
| 
 | 
 | ||||||
| export class MinimapViz implements SpecialVisualization { | export class MinimapViz implements SpecialVisualization { | ||||||
|     funcName = "minimap" |     funcName = "minimap" | ||||||
|  | @ -22,16 +27,20 @@ export class MinimapViz implements SpecialVisualization { | ||||||
|     ] |     ] | ||||||
|     example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`" |     example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`" | ||||||
| 
 | 
 | ||||||
|     constr(state, tagSource, args, _) { |     constr( | ||||||
|  |         state: SpecialVisualizationState, | ||||||
|  |         tagSource: UIEventSource<Record<string, string>>, | ||||||
|  |         args: string[] | ||||||
|  |     ) { | ||||||
|         if (state === undefined) { |         if (state === undefined) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|         const keys = [...args] |         const keys = [...args] | ||||||
|         keys.splice(0, 1) |         keys.splice(0, 1) | ||||||
|         const featureStore = state.allElements.ContainingFeatures |         const featuresToShow: Store<Feature[]> = state.indexedFeatures.featuresById.map( | ||||||
|         const featuresToShow: Store<{ freshness: Date; feature: any }[]> = tagSource.map( |             (featuresById) => { | ||||||
|             (properties) => { |                 const properties = tagSource.data | ||||||
|                 const features: { freshness: Date; feature: any }[] = [] |                 const features: Feature[] = [] | ||||||
|                 for (const key of keys) { |                 for (const key of keys) { | ||||||
|                     const value = properties[key] |                     const value = properties[key] | ||||||
|                     if (value === undefined || value === null) { |                     if (value === undefined || value === null) { | ||||||
|  | @ -45,21 +54,22 @@ export class MinimapViz implements SpecialVisualization { | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     for (const id of idList) { |                     for (const id of idList) { | ||||||
|                         const feature = featureStore.get(id) |                         const feature = featuresById.get(id) | ||||||
|                         if (feature === undefined) { |                         if (feature === undefined) { | ||||||
|                             console.warn("No feature found for id ", id) |                             console.warn("No feature found for id ", id) | ||||||
|                             continue |                             continue | ||||||
|                         } |                         } | ||||||
|                         features.push({ |                         features.push(feature) | ||||||
|                             freshness: new Date(), |  | ||||||
|                             feature, |  | ||||||
|                         }) |  | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 return features |                 return features | ||||||
|             } |             }, | ||||||
|  |             [tagSource] | ||||||
|         ) |         ) | ||||||
|         const properties = tagSource.data | 
 | ||||||
|  |         const mlmap = new UIEventSource(undefined) | ||||||
|  |         const mla = new MapLibreAdaptor(mlmap) | ||||||
|  | 
 | ||||||
|         let zoom = 18 |         let zoom = 18 | ||||||
|         if (args[0]) { |         if (args[0]) { | ||||||
|             const parsed = Number(args[0]) |             const parsed = Number(args[0]) | ||||||
|  | @ -67,33 +77,18 @@ export class MinimapViz implements SpecialVisualization { | ||||||
|                 zoom = parsed |                 zoom = parsed | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         const locationSource = new UIEventSource<Loc>({ |         mla.zoom.setData(zoom) | ||||||
|             lat: Number(properties._lat), |         mla.allowMoving.setData(false) | ||||||
|             lon: Number(properties._lon), |         mla.allowZooming.setData(false) | ||||||
|             zoom: zoom, |  | ||||||
|         }) |  | ||||||
|         const minimap = Minimap.createMiniMap({ |  | ||||||
|             background: state.backgroundLayer, |  | ||||||
|             location: locationSource, |  | ||||||
|             allowMoving: false, |  | ||||||
|         }) |  | ||||||
| 
 | 
 | ||||||
|         locationSource.addCallback((loc) => { |         ShowDataLayer.showMultipleLayers( | ||||||
|             if (loc.zoom > zoom) { |             mlmap, | ||||||
|                 // We zoom back
 |             new StaticFeatureSource(featuresToShow), | ||||||
|                 locationSource.data.zoom = zoom |             state.layout.layers | ||||||
|                 locationSource.ping() |         ) | ||||||
|             } |  | ||||||
|         }) |  | ||||||
| 
 | 
 | ||||||
|         new ShowDataMultiLayer({ |         return new SvelteUIElement(MaplibreMap, { map: mlmap }).SetStyle( | ||||||
|             leafletMap: minimap["leafletMap"], |             "overflow: hidden; pointer-events: none;" | ||||||
|             zoomToFeatures: true, |         ) | ||||||
|             layers: state.filteredLayers, |  | ||||||
|             features: new StaticFeatureSource(featuresToShow), |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         minimap.SetStyle("overflow: hidden; pointer-events: none;") |  | ||||||
|         return minimap |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,17 +2,14 @@ import { Store } from "../../Logic/UIEventSource" | ||||||
| import BaseUIElement from "../BaseUIElement" | import BaseUIElement from "../BaseUIElement" | ||||||
| import Combine from "../Base/Combine" | import Combine from "../Base/Combine" | ||||||
| import { SubtleButton } from "../Base/SubtleButton" | import { SubtleButton } from "../Base/SubtleButton" | ||||||
| import { Changes } from "../../Logic/Osm/Changes" |  | ||||||
| import { FixedUiElement } from "../Base/FixedUiElement" | import { FixedUiElement } from "../Base/FixedUiElement" | ||||||
| import Translations from "../i18n/Translations" | import Translations from "../i18n/Translations" | ||||||
| import { VariableUiElement } from "../Base/VariableUIElement" | import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
| import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" | import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" | ||||||
| import { Tag } from "../../Logic/Tags/Tag" | import { Tag } from "../../Logic/Tags/Tag" | ||||||
| import { ElementStorage } from "../../Logic/ElementStorage" |  | ||||||
| import { And } from "../../Logic/Tags/And" | import { And } from "../../Logic/Tags/And" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" |  | ||||||
| import Toggle from "../Input/Toggle" | import Toggle from "../Input/Toggle" | ||||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" | import { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
| 
 | 
 | ||||||
| export interface MultiApplyParams { | export interface MultiApplyParams { | ||||||
|     featureIds: Store<string[]> |     featureIds: Store<string[]> | ||||||
|  | @ -21,12 +18,7 @@ export interface MultiApplyParams { | ||||||
|     autoapply: boolean |     autoapply: boolean | ||||||
|     overwrite: boolean |     overwrite: boolean | ||||||
|     tagsSource: Store<any> |     tagsSource: Store<any> | ||||||
|     state: { |     state: SpecialVisualizationState | ||||||
|         changes: Changes |  | ||||||
|         allElements: ElementStorage |  | ||||||
|         layoutToUse: LayoutConfig |  | ||||||
|         osmConnection: OsmConnection |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class MultiApplyExecutor { | class MultiApplyExecutor { | ||||||
|  | @ -68,14 +60,14 @@ class MultiApplyExecutor { | ||||||
|         console.log("Multi-applying changes...") |         console.log("Multi-applying changes...") | ||||||
|         const featuresToChange = this.params.featureIds.data |         const featuresToChange = this.params.featureIds.data | ||||||
|         const changes = this.params.state.changes |         const changes = this.params.state.changes | ||||||
|         const allElements = this.params.state.allElements |         const allElements = this.params.state.featureProperties | ||||||
|         const keysToChange = this.params.keysToApply |         const keysToChange = this.params.keysToApply | ||||||
|         const overwrite = this.params.overwrite |         const overwrite = this.params.overwrite | ||||||
|         const selfTags = this.params.tagsSource.data |         const selfTags = this.params.tagsSource.data | ||||||
|         const theme = this.params.state.layoutToUse.id |         const theme = this.params.state.layout.id | ||||||
|         for (const id of featuresToChange) { |         for (const id of featuresToChange) { | ||||||
|             const tagsToApply: Tag[] = [] |             const tagsToApply: Tag[] = [] | ||||||
|             const otherFeatureTags = allElements.getEventSourceById(id).data |             const otherFeatureTags = allElements.getStore(id).data | ||||||
|             for (const key of keysToChange) { |             for (const key of keysToChange) { | ||||||
|                 const newValue = selfTags[key] |                 const newValue = selfTags[key] | ||||||
|                 if (newValue === undefined) { |                 if (newValue === undefined) { | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Store } from "../../Logic/UIEventSource" | import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import MultiApply from "./MultiApply" | import MultiApply from "./MultiApply" | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
| 
 | 
 | ||||||
| export class MultiApplyViz implements SpecialVisualization { | export class MultiApplyViz implements SpecialVisualization { | ||||||
|     funcName = "multi_apply" |     funcName = "multi_apply" | ||||||
|  | @ -31,7 +31,11 @@ export class MultiApplyViz implements SpecialVisualization { | ||||||
|     example = |     example = | ||||||
|         "{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}" |         "{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}" | ||||||
| 
 | 
 | ||||||
|     constr(state, tagsSource, args) { |     constr( | ||||||
|  |         state: SpecialVisualizationState, | ||||||
|  |         tagsSource: UIEventSource<Record<string, string>>, | ||||||
|  |         args: string[] | ||||||
|  |     ) { | ||||||
|         const featureIdsKey = args[0] |         const featureIdsKey = args[0] | ||||||
|         const keysToApply = args[1].split(";") |         const keysToApply = args[1].split(";") | ||||||
|         const text = args[2] |         const text = args[2] | ||||||
|  |  | ||||||
|  | @ -1,6 +1,4 @@ | ||||||
| import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" |  | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" | import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import { DefaultGuiState } from "../DefaultGuiState" |  | ||||||
| import BaseUIElement from "../BaseUIElement" | import BaseUIElement from "../BaseUIElement" | ||||||
| import Translations from "../i18n/Translations" | import Translations from "../i18n/Translations" | ||||||
| import { GeoOperations } from "../../Logic/GeoOperations" | import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
|  | @ -19,7 +17,7 @@ import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
| import Toggle from "../Input/Toggle" | import Toggle from "../Input/Toggle" | ||||||
| import Title from "../Base/Title" | import Title from "../Base/Title" | ||||||
| import { MapillaryLinkVis } from "./MapillaryLinkVis" | import { MapillaryLinkVis } from "./MapillaryLinkVis" | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
| 
 | 
 | ||||||
| export class NearbyImageVis implements SpecialVisualization { | export class NearbyImageVis implements SpecialVisualization { | ||||||
|     args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [ |     args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [ | ||||||
|  | @ -39,14 +37,13 @@ export class NearbyImageVis implements SpecialVisualization { | ||||||
|     funcName = "nearby_images" |     funcName = "nearby_images" | ||||||
| 
 | 
 | ||||||
|     constr( |     constr( | ||||||
|         state: FeaturePipelineState, |         state: SpecialVisualizationState, | ||||||
|         tagSource: UIEventSource<any>, |         tagSource: UIEventSource<Record<string, string>>, | ||||||
|         args: string[], |         args: string[] | ||||||
|         guistate: DefaultGuiState |  | ||||||
|     ): BaseUIElement { |     ): BaseUIElement { | ||||||
|         const t = Translations.t.image.nearbyPictures |         const t = Translations.t.image.nearbyPictures | ||||||
|         const mode: "open" | "expandable" | "collapsable" = <any>args[0] |         const mode: "open" | "expandable" | "collapsable" = <any>args[0] | ||||||
|         const feature = state.allElements.ContainingFeatures.get(tagSource.data.id) |         const feature = state.indexedFeatures.featuresById.data.get(tagSource.data.id) | ||||||
|         const [lon, lat] = GeoOperations.centerpointCoordinates(feature) |         const [lon, lat] = GeoOperations.centerpointCoordinates(feature) | ||||||
|         const id: string = tagSource.data["id"] |         const id: string = tagSource.data["id"] | ||||||
|         const canBeEdited: boolean = !!id?.match("(node|way|relation)/-?[0-9]+") |         const canBeEdited: boolean = !!id?.match("(node|way|relation)/-?[0-9]+") | ||||||
|  | @ -69,7 +66,7 @@ export class NearbyImageVis implements SpecialVisualization { | ||||||
|                 } |                 } | ||||||
|                 await state?.changes?.applyAction( |                 await state?.changes?.applyAction( | ||||||
|                     new ChangeTagAction(id, new And(tags), tagSource.data, { |                     new ChangeTagAction(id, new And(tags), tagSource.data, { | ||||||
|                         theme: state?.layoutToUse.id, |                         theme: state?.layout.id, | ||||||
|                         changeType: "link-image", |                         changeType: "link-image", | ||||||
|                     }) |                     }) | ||||||
|                 ) |                 ) | ||||||
|  | @ -116,8 +113,8 @@ export class NearbyImageVis implements SpecialVisualization { | ||||||
|                 maxDaysOld: 365 * 3, |                 maxDaysOld: 365 * 3, | ||||||
|             } |             } | ||||||
|             const slideshow = canBeEdited |             const slideshow = canBeEdited | ||||||
|                 ? new SelectOneNearbyImage(options, state) |                 ? new SelectOneNearbyImage(options, state.indexedFeatures) | ||||||
|                 : new NearbyImages(options, state) |                 : new NearbyImages(options, state.indexedFeatures) | ||||||
|             const controls = new Combine([ |             const controls = new Combine([ | ||||||
|                 towardsCenter, |                 towardsCenter, | ||||||
|                 new Combine([ |                 new Combine([ | ||||||
|  |  | ||||||
|  | @ -13,9 +13,10 @@ import Translations from "../i18n/Translations" | ||||||
| import { Mapillary } from "../../Logic/ImageProviders/Mapillary" | import { Mapillary } from "../../Logic/ImageProviders/Mapillary" | ||||||
| import { SubtleButton } from "../Base/SubtleButton" | import { SubtleButton } from "../Base/SubtleButton" | ||||||
| import { GeoOperations } from "../../Logic/GeoOperations" | import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
| import { ElementStorage } from "../../Logic/ElementStorage" |  | ||||||
| import Lazy from "../Base/Lazy" | import Lazy from "../Base/Lazy" | ||||||
| import P4C from "pic4carto" | import P4C from "pic4carto" | ||||||
|  | import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource" | ||||||
|  | 
 | ||||||
| export interface P4CPicture { | export interface P4CPicture { | ||||||
|     pictureUrl: string |     pictureUrl: string | ||||||
|     date?: number |     date?: number | ||||||
|  | @ -47,15 +48,15 @@ export interface NearbyImageOptions { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class ImagesInLoadedDataFetcher { | class ImagesInLoadedDataFetcher { | ||||||
|     private allElements: ElementStorage |     private indexedFeatures: IndexedFeatureSource | ||||||
| 
 | 
 | ||||||
|     constructor(state: { allElements: ElementStorage }) { |     constructor(indexedFeatures: IndexedFeatureSource) { | ||||||
|         this.allElements = state.allElements |         this.indexedFeatures = indexedFeatures | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public fetchAround(loc: { lon: number; lat: number; searchRadius?: number }): P4CPicture[] { |     public fetchAround(loc: { lon: number; lat: number; searchRadius?: number }): P4CPicture[] { | ||||||
|         const foundImages: P4CPicture[] = [] |         const foundImages: P4CPicture[] = [] | ||||||
|         this.allElements.ContainingFeatures.forEach((feature) => { |         this.indexedFeatures.features.data.forEach((feature) => { | ||||||
|             const props = feature.properties |             const props = feature.properties | ||||||
|             const images = [] |             const images = [] | ||||||
|             if (props.image) { |             if (props.image) { | ||||||
|  | @ -100,7 +101,7 @@ class ImagesInLoadedDataFetcher { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default class NearbyImages extends Lazy { | export default class NearbyImages extends Lazy { | ||||||
|     constructor(options: NearbyImageOptions, state?: { allElements: ElementStorage }) { |     constructor(options: NearbyImageOptions, state?: IndexedFeatureSource) { | ||||||
|         super(() => { |         super(() => { | ||||||
|             const t = Translations.t.image.nearbyPictures |             const t = Translations.t.image.nearbyPictures | ||||||
|             const shownImages = options.shownImagesCount ?? new UIEventSource(25) |             const shownImages = options.shownImagesCount ?? new UIEventSource(25) | ||||||
|  | @ -171,10 +172,7 @@ export default class NearbyImages extends Lazy { | ||||||
|             ) |             ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static buildPictureFetcher( |     private static buildPictureFetcher(options: NearbyImageOptions, state?: IndexedFeatureSource) { | ||||||
|         options: NearbyImageOptions, |  | ||||||
|         state?: { allElements: ElementStorage } |  | ||||||
|     ) { |  | ||||||
|         const picManager = new P4C.PicturesManager({}) |         const picManager = new P4C.PicturesManager({}) | ||||||
|         const searchRadius = options.searchRadius ?? 500 |         const searchRadius = options.searchRadius ?? 500 | ||||||
| 
 | 
 | ||||||
|  | @ -283,7 +281,7 @@ export class SelectOneNearbyImage extends NearbyImages implements InputElement<P | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         options: NearbyImageOptions & { value?: UIEventSource<P4CPicture> }, |         options: NearbyImageOptions & { value?: UIEventSource<P4CPicture> }, | ||||||
|         state?: { allElements: ElementStorage } |         state?: IndexedFeatureSource | ||||||
|     ) { |     ) { | ||||||
|         super(options, state) |         super(options, state) | ||||||
|         this.value = options.value ?? new UIEventSource<P4CPicture>(undefined) |         this.value = options.value ?? new UIEventSource<P4CPicture>(undefined) | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ import Combine from "../Base/Combine" | ||||||
| import Svg from "../../Svg" | import Svg from "../../Svg" | ||||||
| import Translations from "../i18n/Translations" | import Translations from "../i18n/Translations" | ||||||
| import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" | import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"; | ||||||
| 
 | 
 | ||||||
| export class PlantNetDetectionViz implements SpecialVisualization { | export class PlantNetDetectionViz implements SpecialVisualization { | ||||||
|     funcName = "plantnet_detection" |     funcName = "plantnet_detection" | ||||||
|  | @ -27,7 +27,7 @@ export class PlantNetDetectionViz implements SpecialVisualization { | ||||||
|         }, |         }, | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     public constr(state, tags, args) { |     public constr(state: SpecialVisualizationState, tags: UIEventSource<Record<string, string>>, args: string[]) { | ||||||
|         let imagePrefixes: string[] = undefined |         let imagePrefixes: string[] = undefined | ||||||
|         if (args.length > 0) { |         if (args.length > 0) { | ||||||
|             imagePrefixes = [].concat(...args.map((a) => a.split(","))) |             imagePrefixes = [].concat(...args.map((a) => a.split(","))) | ||||||
|  | @ -53,7 +53,7 @@ export class PlantNetDetectionViz implements SpecialVisualization { | ||||||
|                         ]), |                         ]), | ||||||
|                         tags.data, |                         tags.data, | ||||||
|                         { |                         { | ||||||
|                             theme: state.layoutToUse.id, |                             theme: state.layout.id, | ||||||
|                             changeType: "plantnet-ai-detection", |                             changeType: "plantnet-ai-detection", | ||||||
|                         } |                         } | ||||||
|                     ) |                     ) | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
| import ShareButton from "../BigComponents/ShareButton" | import ShareButton from "../BigComponents/ShareButton" | ||||||
| import Svg from "../../Svg" | import Svg from "../../Svg" | ||||||
| import { FixedUiElement } from "../Base/FixedUiElement" | import { FixedUiElement } from "../Base/FixedUiElement" | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"; | ||||||
| 
 | 
 | ||||||
| export class ShareLinkViz implements SpecialVisualization { | export class ShareLinkViz implements SpecialVisualization { | ||||||
|     funcName = "share_link" |     funcName = "share_link" | ||||||
|  | @ -17,12 +17,12 @@ export class ShareLinkViz implements SpecialVisualization { | ||||||
|         }, |         }, | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     public constr(state, tagSource: UIEventSource<any>, args) { |     public constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, args: string[]) { | ||||||
|         if (window.navigator.share) { |         if (window.navigator.share) { | ||||||
|             const generateShareData = () => { |             const generateShareData = () => { | ||||||
|                 const title = state?.layoutToUse?.title?.txt ?? "MapComplete" |                 const title = state?.layout?.title?.txt ?? "MapComplete" | ||||||
| 
 | 
 | ||||||
|                 let matchingLayer: LayerConfig = state?.layoutToUse?.getMatchingLayer( |                 let matchingLayer: LayerConfig = state?.layout?.getMatchingLayer( | ||||||
|                     tagSource?.data |                     tagSource?.data | ||||||
|                 ) |                 ) | ||||||
|                 let name = |                 let name = | ||||||
|  | @ -41,7 +41,7 @@ export class ShareLinkViz implements SpecialVisualization { | ||||||
|                 return { |                 return { | ||||||
|                     title: name, |                     title: name, | ||||||
|                     url: url, |                     url: url, | ||||||
|                     text: state?.layoutToUse?.shortDescription?.txt ?? "MapComplete", |                     text: state?.layout?.shortDescription?.txt ?? "MapComplete", | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
| import BaseUIElement from "../BaseUIElement" | import BaseUIElement from "../BaseUIElement" | ||||||
| import EditableTagRendering from "./EditableTagRendering" | import EditableTagRendering from "./EditableTagRendering" | ||||||
| import Combine from "../Base/Combine" | import Combine from "../Base/Combine" | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
| 
 | 
 | ||||||
| export class StealViz implements SpecialVisualization { | export class StealViz implements SpecialVisualization { | ||||||
|     funcName = "steal" |     funcName = "steal" | ||||||
|  | @ -21,12 +21,12 @@ export class StealViz implements SpecialVisualization { | ||||||
|             required: true, |             required: true, | ||||||
|         }, |         }, | ||||||
|     ] |     ] | ||||||
|     constr(state, featureTags, args) { |     constr(state: SpecialVisualizationState, featureTags, args) { | ||||||
|         const [featureIdKey, layerAndtagRenderingIds] = args |         const [featureIdKey, layerAndtagRenderingIds] = args | ||||||
|         const tagRenderings: [LayerConfig, TagRenderingConfig][] = [] |         const tagRenderings: [LayerConfig, TagRenderingConfig][] = [] | ||||||
|         for (const layerAndTagRenderingId of layerAndtagRenderingIds.split(";")) { |         for (const layerAndTagRenderingId of layerAndtagRenderingIds.split(";")) { | ||||||
|             const [layerId, tagRenderingId] = layerAndTagRenderingId.trim().split(".") |             const [layerId, tagRenderingId] = layerAndTagRenderingId.trim().split(".") | ||||||
|             const layer = state.layoutToUse.layers.find((l) => l.id === layerId) |             const layer = state.layout.layers.find((l) => l.id === layerId) | ||||||
|             const tagRendering = layer.tagRenderings.find((tr) => tr.id === tagRenderingId) |             const tagRendering = layer.tagRenderings.find((tr) => tr.id === tagRenderingId) | ||||||
|             tagRenderings.push([layer, tagRendering]) |             tagRenderings.push([layer, tagRendering]) | ||||||
|         } |         } | ||||||
|  | @ -39,7 +39,7 @@ export class StealViz implements SpecialVisualization { | ||||||
|                 if (featureId === undefined) { |                 if (featureId === undefined) { | ||||||
|                     return undefined |                     return undefined | ||||||
|                 } |                 } | ||||||
|                 const otherTags = state.allElements.getEventSourceById(featureId) |                 const otherTags = state.featureProperties.getStore(featureId) | ||||||
|                 const elements: BaseUIElement[] = [] |                 const elements: BaseUIElement[] = [] | ||||||
|                 for (const [layer, tagRendering] of tagRenderings) { |                 for (const [layer, tagRendering] of tagRenderings) { | ||||||
|                     const el = new EditableTagRendering( |                     const el = new EditableTagRendering( | ||||||
|  |  | ||||||
|  | @ -11,10 +11,9 @@ import { And } from "../../Logic/Tags/And" | ||||||
| import Toggle from "../Input/Toggle" | import Toggle from "../Input/Toggle" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { Tag } from "../../Logic/Tags/Tag" | import { Tag } from "../../Logic/Tags/Tag" | ||||||
| import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" |  | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import { Changes } from "../../Logic/Osm/Changes" | import { Changes } from "../../Logic/Osm/Changes" | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
| 
 | 
 | ||||||
| export default class TagApplyButton implements AutoAction, SpecialVisualization { | export default class TagApplyButton implements AutoAction, SpecialVisualization { | ||||||
|     public readonly funcName = "tag_apply" |     public readonly funcName = "tag_apply" | ||||||
|  | @ -76,7 +75,10 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization | ||||||
|         return tgsSpec |         return tgsSpec | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static generateTagsToApply(spec: string, tagSource: Store<any>): Store<Tag[]> { |     public static generateTagsToApply( | ||||||
|  |         spec: string, | ||||||
|  |         tagSource: Store<Record<string, string>> | ||||||
|  |     ): Store<Tag[]> { | ||||||
|         // Check whether we need to look up a single value
 |         // Check whether we need to look up a single value
 | ||||||
| 
 | 
 | ||||||
|         if (!spec.includes(";") && !spec.includes("=") && spec.includes("$")) { |         if (!spec.includes(";") && !spec.includes("=") && spec.includes("$")) { | ||||||
|  | @ -110,7 +112,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization | ||||||
| 
 | 
 | ||||||
|     async applyActionOn( |     async applyActionOn( | ||||||
|         state: { |         state: { | ||||||
|             layoutToUse: LayoutConfig |             layout: LayoutConfig | ||||||
|             changes: Changes |             changes: Changes | ||||||
|         }, |         }, | ||||||
|         tags: UIEventSource<any>, |         tags: UIEventSource<any>, | ||||||
|  | @ -125,7 +127,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization | ||||||
|             new And(tagsToApply.data), |             new And(tagsToApply.data), | ||||||
|             tags.data, // We pass in the tags of the selected element, not the tags of the target element!
 |             tags.data, // We pass in the tags of the selected element, not the tags of the target element!
 | ||||||
|             { |             { | ||||||
|                 theme: state.layoutToUse.id, |                 theme: state.layout.id, | ||||||
|                 changeType: "answer", |                 changeType: "answer", | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  | @ -133,8 +135,8 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public constr( |     public constr( | ||||||
|         state: FeaturePipelineState, |         state: SpecialVisualizationState, | ||||||
|         tags: UIEventSource<any>, |         tags: UIEventSource<Record<string, string>>, | ||||||
|         args: string[] |         args: string[] | ||||||
|     ): BaseUIElement { |     ): BaseUIElement { | ||||||
|         const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) |         const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) | ||||||
|  | @ -162,9 +164,9 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization | ||||||
|         const applyButton = new SubtleButton( |         const applyButton = new SubtleButton( | ||||||
|             image, |             image, | ||||||
|             new Combine([msg, tagsExplanation]).SetClass("flex flex-col") |             new Combine([msg, tagsExplanation]).SetClass("flex flex-col") | ||||||
|         ).onClick(() => { |         ).onClick(async () => { | ||||||
|             self.applyActionOn(state, tags, args) |  | ||||||
|             applied.setData(true) |             applied.setData(true) | ||||||
|  |             await self.applyActionOn(state, tags, args) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         return new Toggle( |         return new Toggle( | ||||||
|  |  | ||||||
							
								
								
									
										0
									
								
								UI/Popup/TagRenderingAnswer.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								UI/Popup/TagRenderingAnswer.svelte
									
										
									
									
									
										Normal file
									
								
							Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue