forked from MapComplete/MapComplete
		
	More refactoring of the featurepipeline, introduction of fetching data from the OSM-API directly per tile, personal theme refactoring
This commit is contained in:
		
							parent
							
								
									0a9e7c0b36
								
							
						
					
					
						commit
						41a2a79fe9
					
				
					 48 changed files with 746 additions and 590 deletions
				
			
		|  | @ -1,6 +1,7 @@ | |||
| import AllKnownLayers from "./AllKnownLayers"; | ||||
| import * as known_themes from "../assets/generated/known_layers_and_themes.json" | ||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
| 
 | ||||
| export class AllKnownLayouts { | ||||
| 
 | ||||
|  | @ -8,6 +9,26 @@ export class AllKnownLayouts { | |||
|     public static allKnownLayouts: Map<string, LayoutConfig> = AllKnownLayouts.AllLayouts(); | ||||
|     public static layoutsList: LayoutConfig[] = AllKnownLayouts.GenerateOrderedList(AllKnownLayouts.allKnownLayouts); | ||||
| 
 | ||||
|     public static AllPublicLayers(){ | ||||
|         const allLayers : LayerConfig[] = [] | ||||
|         const seendIds = new Set<string>() | ||||
|         const publicLayouts = AllKnownLayouts.layoutsList.filter(l => !l.hideFromOverview) | ||||
|         for (const layout of publicLayouts) { | ||||
|             if(layout.hideFromOverview){ | ||||
|                 continue | ||||
|             } | ||||
|             for (const layer of layout.layers) { | ||||
|                 if(seendIds.has(layer.id)){ | ||||
|                     continue | ||||
|                 } | ||||
|                 seendIds.add(layer.id) | ||||
|                 allLayers.push(layer) | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
|         return allLayers | ||||
|     } | ||||
|      | ||||
|     private static GenerateOrderedList(allKnownLayouts: Map<string, LayoutConfig>): LayoutConfig[] { | ||||
|         const keys = ["personal", "cyclofix", "hailhydrant", "bookcases", "toilets", "aed"] | ||||
|         const list = [] | ||||
|  |  | |||
|  | @ -27,7 +27,6 @@ import MapControlButton from "./UI/MapControlButton"; | |||
| import LZString from "lz-string"; | ||||
| import AllKnownLayers from "./Customizations/AllKnownLayers"; | ||||
| import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; | ||||
| import {TagsFilter} from "./Logic/Tags/TagsFilter"; | ||||
| import LeftControls from "./UI/BigComponents/LeftControls"; | ||||
| import RightControls from "./UI/BigComponents/RightControls"; | ||||
| import {LayoutConfigJson} from "./Models/ThemeConfig/Json/LayoutConfigJson"; | ||||
|  | @ -40,10 +39,10 @@ import {SubtleButton} from "./UI/Base/SubtleButton"; | |||
| import ShowTileInfo from "./UI/ShowDataLayer/ShowTileInfo"; | ||||
| import {Tiles} from "./Models/TileRange"; | ||||
| import {TileHierarchyAggregator} from "./UI/ShowDataLayer/PerTileCountAggregator"; | ||||
| import {BBox} from "./Logic/GeoOperations"; | ||||
| import StaticFeatureSource from "./Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import FilterConfig from "./Models/ThemeConfig/FilterConfig"; | ||||
| import FilteredLayer from "./Models/FilteredLayer"; | ||||
| import {BBox} from "./Logic/BBox"; | ||||
| import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; | ||||
| 
 | ||||
| export class InitUiElements { | ||||
|     static InitAll( | ||||
|  | @ -71,8 +70,22 @@ export class InitUiElements { | |||
|             layoutFromBase64 | ||||
|         ); | ||||
|          | ||||
|         if(layoutToUse.id === personal.id){ | ||||
|             layoutToUse.layers = AllKnownLayouts.AllPublicLayers() | ||||
|             for (const layer of layoutToUse.layers) { | ||||
|                 layer.minzoomVisible = Math.max(layer.minzoomVisible, layer.minzoom) | ||||
|                 layer.minzoom = Math.max(16, layer.minzoom) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         State.state = new State(layoutToUse); | ||||
| 
 | ||||
|         if(layoutToUse.id === personal.id) { | ||||
|             // Disable overpass all together
 | ||||
|             State.state.overpassMaxZoom.setData(0) | ||||
|              | ||||
|         } | ||||
| 
 | ||||
|             // This 'leaks' the global state via the window object, useful for debugging
 | ||||
|         // @ts-ignore
 | ||||
|         window.mapcomplete_state = State.state; | ||||
|  | @ -102,45 +115,6 @@ export class InitUiElements { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         function updateFavs() { | ||||
|             // This is purely for the personal theme to load the layers there
 | ||||
|             const favs = State.state.favouriteLayers.data ?? []; | ||||
| 
 | ||||
|             const neededLayers = new Set<LayerConfig>(); | ||||
| 
 | ||||
|             console.log("Favourites are: ", favs); | ||||
|             layoutToUse.layers.splice(0, layoutToUse.layers.length); | ||||
|             let somethingChanged = false; | ||||
|             for (const fav of favs) { | ||||
|                 if (AllKnownLayers.sharedLayers.has(fav)) { | ||||
|                     const layer = AllKnownLayers.sharedLayers.get(fav); | ||||
|                     if (!neededLayers.has(layer)) { | ||||
|                         neededLayers.add(layer); | ||||
|                         somethingChanged = true; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 for (const layouts of State.state.installedThemes.data) { | ||||
|                     for (const layer of layouts.layout.layers) { | ||||
|                         if (typeof layer === "string") { | ||||
|                             continue; | ||||
|                         } | ||||
|                         if (layer.id === fav) { | ||||
|                             if (!neededLayers.has(layer)) { | ||||
|                                 neededLayers.add(layer); | ||||
|                                 somethingChanged = true; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             if (somethingChanged) { | ||||
|                 State.state.layoutToUse.data.layers = Array.from(neededLayers); | ||||
|                 State.state.layoutToUse.ping(); | ||||
|                 State.state.featurePipeline?.ForceRefresh(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (layoutToUse.customCss !== undefined) { | ||||
|             Utils.LoadCustomCss(layoutToUse.customCss); | ||||
|         } | ||||
|  | @ -206,18 +180,9 @@ export class InitUiElements { | |||
|             .addCallbackAndRunD(_ => addHomeMarker()); | ||||
|         State.state.leafletMap.addCallbackAndRunD(_ => addHomeMarker()) | ||||
| 
 | ||||
|         if (layoutToUse.id === personal.id) { | ||||
|             updateFavs(); | ||||
|         } | ||||
| 
 | ||||
|         InitUiElements.setupAllLayerElements(); | ||||
| 
 | ||||
|         if (layoutToUse.id === personal.id) { | ||||
|             State.state.favouriteLayers.addCallback(updateFavs); | ||||
|             State.state.installedThemes.addCallback(updateFavs); | ||||
|         } else { | ||||
|             State.state.locationControl.ping(); | ||||
|         } | ||||
| 
 | ||||
|         new SelectedFeatureHandler(Hash.hash, State.state) | ||||
| 
 | ||||
|  | @ -414,15 +379,29 @@ export class InitUiElements { | |||
|             const flayers: FilteredLayer[] = []; | ||||
| 
 | ||||
|             for (const layer of layoutToUse.layers) { | ||||
|                 const isDisplayed = QueryParameters.GetQueryParameter( | ||||
|                 let defaultShown = "true" | ||||
|                 if(layoutToUse.id === personal.id){ | ||||
|                     defaultShown = "false" | ||||
|                 } | ||||
| 
 | ||||
|                 let isDisplayed: UIEventSource<boolean> | ||||
|                 if(layoutToUse.id === personal.id){ | ||||
|                     isDisplayed = State.state.osmConnection.GetPreference("personal-theme-layer-" + layer.id + "-enabled") | ||||
|                         .map(value => value === "yes", [], enabled => { | ||||
|                             return enabled ? "yes" : ""; | ||||
|                         }) | ||||
|                     isDisplayed.addCallbackAndRun(d =>console.log("IsDisplayed for layer", layer.id, "is currently", d) ) | ||||
|                 }else{ | ||||
|                     isDisplayed = QueryParameters.GetQueryParameter( | ||||
|                         "layer-" + layer.id, | ||||
|                     "true", | ||||
|                         defaultShown, | ||||
|                         "Wether or not layer " + layer.id + " is shown" | ||||
|                     ).map<boolean>( | ||||
|                         (str) => str !== "false", | ||||
|                         [], | ||||
|                         (b) => b.toString() | ||||
|                     ); | ||||
|                 } | ||||
|                 const flayer = { | ||||
|                     isDisplayed: isDisplayed, | ||||
|                     layerDef: layer, | ||||
|  | @ -453,8 +432,6 @@ export class InitUiElements { | |||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|         const layers = State.state.layoutToUse.data.layers | ||||
| 
 | ||||
|         const clusterCounter = TileHierarchyAggregator.createHierarchy() | ||||
|         new ShowDataLayer({ | ||||
|             features: clusterCounter.getCountsForZoom(State.state.locationControl, State.state.layoutToUse.data.clustering.minNeededElements), | ||||
|  | @ -472,6 +449,10 @@ export class InitUiElements { | |||
|                     f => { | ||||
|                         const z = State.state.locationControl.data.zoom | ||||
|                          | ||||
|                         if(!source.layer.isDisplayed.data){ | ||||
|                             return false; | ||||
|                         } | ||||
| 
 | ||||
|                         if (z < source.layer.layerDef.minzoom) { | ||||
|                             // Layer is always hidden for this zoom level
 | ||||
|                             return false; | ||||
|  | @ -482,7 +463,7 @@ export class InitUiElements { | |||
|                         } | ||||
| 
 | ||||
|                         if (f.length > clustering.minNeededElements) { | ||||
|                             // This tile alone has too much features
 | ||||
|                             // This tile alone already has too much features
 | ||||
|                             return false | ||||
|                         } | ||||
| 
 | ||||
|  | @ -504,11 +485,12 @@ export class InitUiElements { | |||
|                         const bounds = State.state.currentBounds.data | ||||
|                         const tilebbox = BBox.fromTileIndex(source.tileIndex) | ||||
|                         if (!tilebbox.overlapsWith(bounds)) { | ||||
|                             // Not within range
 | ||||
|                             return false | ||||
|                         } | ||||
| 
 | ||||
|                         return true | ||||
|                     }, [State.state.locationControl, State.state.currentBounds] | ||||
|                     }, [State.state.currentBounds] | ||||
|                 ) | ||||
| 
 | ||||
|                 new ShowDataLayer( | ||||
|  |  | |||
|  | @ -9,9 +9,10 @@ import {TagsFilter} from "../Tags/TagsFilter"; | |||
| import SimpleMetaTagger from "../SimpleMetaTagger"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import RelationsTracker from "../Osm/RelationsTracker"; | ||||
| import {BBox} from "../BBox"; | ||||
| 
 | ||||
| 
 | ||||
| export default class OverpassFeatureSource implements FeatureSource, FeatureSourceState { | ||||
| export default class OverpassFeatureSource implements FeatureSource { | ||||
| 
 | ||||
|     public readonly name = "OverpassFeatureSource" | ||||
| 
 | ||||
|  | @ -21,7 +22,6 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | |||
|     public readonly features: UIEventSource<{ feature: any, freshness: Date }[]> = new UIEventSource<any[]>(undefined); | ||||
| 
 | ||||
| 
 | ||||
|     public readonly sufficientlyZoomed: UIEventSource<boolean>; | ||||
|     public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||
|     public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0); | ||||
| 
 | ||||
|  | @ -40,10 +40,12 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | |||
|     private readonly state: { | ||||
|         readonly locationControl: UIEventSource<Loc>, | ||||
|         readonly layoutToUse: UIEventSource<LayoutConfig>, | ||||
|         readonly leafletMap: any, | ||||
|         readonly overpassUrl: UIEventSource<string>; | ||||
|         readonly overpassTimeout: UIEventSource<number>; | ||||
|         readonly currentBounds :UIEventSource<BBox> | ||||
|     } | ||||
|     private readonly _isActive: UIEventSource<boolean>; | ||||
|     private _onUpdated?: (bbox: BBox, dataFreshness: Date) => void; | ||||
|     /** | ||||
|      * The most important layer should go first, as that one gets first pick for the questions | ||||
|      */ | ||||
|  | @ -51,33 +53,24 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | |||
|         state: { | ||||
|             readonly locationControl: UIEventSource<Loc>, | ||||
|             readonly layoutToUse: UIEventSource<LayoutConfig>, | ||||
|             readonly leafletMap: any, | ||||
|             readonly overpassUrl: UIEventSource<string>; | ||||
|             readonly overpassTimeout: UIEventSource<number>; | ||||
|             readonly overpassMaxZoom: UIEventSource<number> | ||||
|         }) { | ||||
|             readonly overpassMaxZoom: UIEventSource<number>, | ||||
|             readonly currentBounds :UIEventSource<BBox> | ||||
|         },    | ||||
|          | ||||
|        options?: { | ||||
|             isActive?: UIEventSource<boolean>, | ||||
|            onUpdated?:  (bbox: BBox, freshness: Date) => void, | ||||
|        relationTracker: RelationsTracker}) { | ||||
| 
 | ||||
|         this.state = state | ||||
|         this.relationsTracker = new RelationsTracker() | ||||
|         this._isActive = options.isActive; | ||||
|         this._onUpdated =options. onUpdated; | ||||
|         this.relationsTracker = options.relationTracker | ||||
|         const location = state.locationControl | ||||
|         const self = this; | ||||
| 
 | ||||
|         this.sufficientlyZoomed = location.map(location => { | ||||
|                 if (location?.zoom === undefined) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom ?? 18)); | ||||
|                 if (location.zoom < minzoom) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 const maxZoom = state.overpassMaxZoom.data | ||||
|                 if (maxZoom !== undefined && location.zoom > maxZoom) { | ||||
|                     return false; | ||||
|                 } | ||||
| 
 | ||||
|                 return true; | ||||
|             }, [state.layoutToUse] | ||||
|         ); | ||||
|         for (let i = 0; i < 25; i++) { | ||||
|             // This update removes all data on all layers -> erase the map on lower levels too
 | ||||
|             this._previousBounds.set(i, []); | ||||
|  | @ -89,16 +82,11 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | |||
|         location.addCallback(() => { | ||||
|             self.update() | ||||
|         }); | ||||
|         state.leafletMap.addCallbackAndRunD(_ => { | ||||
|             self.update(); | ||||
|         }) | ||||
|     } | ||||
|          | ||||
|     public ForceRefresh() { | ||||
|         for (let i = 0; i < 25; i++) { | ||||
|             this._previousBounds.set(i, []); | ||||
|         } | ||||
|         this.update(); | ||||
|         state.currentBounds.addCallback(_ => { | ||||
|             self.update() | ||||
|         }) | ||||
|         | ||||
|     } | ||||
| 
 | ||||
|     private GetFilter(): Overpass { | ||||
|  | @ -152,24 +140,34 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | |||
|     } | ||||
| 
 | ||||
|     private update() { | ||||
|         this.updateAsync().then(_ => { | ||||
|         if(!this._isActive.data){ | ||||
|             return; | ||||
|         } | ||||
|         const self = this | ||||
|         this.updateAsync().then(bboxAndDate => { | ||||
|             if(bboxAndDate === undefined || self._onUpdated === undefined){ | ||||
|                 return; | ||||
|             } | ||||
|             const [bbox, date] = bboxAndDate | ||||
|             self._onUpdated(bbox, date); | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private async updateAsync(): Promise<void> { | ||||
|     private async updateAsync(): Promise<[BBox, Date]> { | ||||
|         if (this.runningQuery.data) { | ||||
|             console.log("Still running a query, not updating"); | ||||
|             return; | ||||
|             return undefined; | ||||
|         } | ||||
| 
 | ||||
|         if (this.timeout.data > 0) { | ||||
|             console.log("Still in timeout - not updating") | ||||
|             return; | ||||
|             return undefined; | ||||
|         } | ||||
| 
 | ||||
|         const bounds = this.state.leafletMap.data?.getBounds()?.pad(this.state.layoutToUse.data.widenFactor); | ||||
|         const bounds = this.state.currentBounds.data?.pad(this.state.layoutToUse.data.widenFactor)?.expandToTileBounds(14); | ||||
|          | ||||
|         if (bounds === undefined) { | ||||
|             return; | ||||
|             return undefined; | ||||
|         } | ||||
| 
 | ||||
|         const n = Math.min(90, bounds.getNorth()); | ||||
|  | @ -178,13 +176,12 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | |||
|         const w = Math.max(-180, bounds.getWest()); | ||||
|         const queryBounds = {north: n, east: e, south: s, west: w}; | ||||
| 
 | ||||
|         const z = Math.floor(this.state.locationControl.data.zoom ?? 0); | ||||
| 
 | ||||
|         const self = this; | ||||
|         const overpass = this.GetFilter(); | ||||
| 
 | ||||
|         if (overpass === undefined) { | ||||
|             return; | ||||
|             return undefined; | ||||
|         } | ||||
|         this.runningQuery.setData(true); | ||||
| 
 | ||||
|  | @ -195,14 +192,13 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | |||
| 
 | ||||
|             try { | ||||
|                 [data, date] = await overpass.queryGeoJson(queryBounds) | ||||
|                 console.log("Querying overpass is done", data) | ||||
|             } catch (e) { | ||||
|                 console.error(`QUERY FAILED (retrying in ${5 * self.retries.data} sec) due to`, e); | ||||
| 
 | ||||
|                 self.retries.data++; | ||||
|                 self.retries.ping(); | ||||
|                 console.error(`QUERY FAILED (retrying in ${5 * self.retries.data} sec) due to`, e); | ||||
| 
 | ||||
|                 self.timeout.setData(self.retries.data * 5); | ||||
|                 self.runningQuery.setData(false); | ||||
|                  | ||||
|                 while (self.timeout.data > 0) { | ||||
|                     await Utils.waitFor(1000) | ||||
|  | @ -212,16 +208,20 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | |||
|             } | ||||
|         } while (data === undefined); | ||||
| 
 | ||||
|         const z = Math.floor(this.state.locationControl.data.zoom ?? 0); | ||||
|         self._previousBounds.get(z).push(queryBounds); | ||||
|         self.retries.setData(0); | ||||
| 
 | ||||
|         try { | ||||
|             data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date)); | ||||
|             self.features.setData(data.features.map(f => ({feature: f, freshness: date}))); | ||||
|             return [bounds, date]; | ||||
|         } catch (e) { | ||||
|             console.error("Got the overpass response, but could not process it: ", e, e.stack) | ||||
|         } | ||||
|         }finally { | ||||
|             self.runningQuery.setData(false); | ||||
|         } | ||||
|          | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
|  | @ -231,7 +231,7 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | |||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const b = this.state.leafletMap.data.getBounds(); | ||||
|         const b = this.state.currentBounds.data; | ||||
|         return b.getSouth() >= bounds.south && | ||||
|             b.getNorth() <= bounds.north && | ||||
|             b.getEast() <= bounds.east && | ||||
|  |  | |||
							
								
								
									
										158
									
								
								Logic/BBox.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								Logic/BBox.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,158 @@ | |||
| import * as turf from "@turf/turf"; | ||||
| import {TileRange, Tiles} from "../Models/TileRange"; | ||||
| 
 | ||||
| export class BBox { | ||||
| 
 | ||||
|     readonly maxLat: number; | ||||
|     readonly maxLon: number; | ||||
|     readonly minLat: number; | ||||
|     readonly minLon: number; | ||||
|     static global: BBox = new BBox([[-180, -90], [180, 90]]); | ||||
| 
 | ||||
|     constructor(coordinates) { | ||||
|         this.maxLat = -90; | ||||
|         this.maxLon = -180; | ||||
|         this.minLat = 90; | ||||
|         this.minLon = 180; | ||||
| 
 | ||||
| 
 | ||||
|         for (const coordinate of coordinates) { | ||||
|             this.maxLon = Math.max(this.maxLon, coordinate[0]); | ||||
|             this.maxLat = Math.max(this.maxLat, coordinate[1]); | ||||
|             this.minLon = Math.min(this.minLon, coordinate[0]); | ||||
|             this.minLat = Math.min(this.minLat, coordinate[1]); | ||||
|         } | ||||
|         this.check(); | ||||
|     } | ||||
| 
 | ||||
|     static fromLeafletBounds(bounds) { | ||||
|         return new BBox([[bounds.getWest(), bounds.getNorth()], [bounds.getEast(), bounds.getSouth()]]) | ||||
|     } | ||||
| 
 | ||||
|     static get(feature): BBox { | ||||
|         if (feature.bbox?.overlapsWith === undefined) { | ||||
|             const turfBbox: number[] = turf.bbox(feature) | ||||
|             feature.bbox = new BBox([[turfBbox[0], turfBbox[1]], [turfBbox[2], turfBbox[3]]]); | ||||
|         } | ||||
|         return feature.bbox; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Constructs a tilerange which fully contains this bbox (thus might be a bit larger) | ||||
|      * @param zoomlevel | ||||
|      */ | ||||
|     public containingTileRange(zoomlevel): TileRange{ | ||||
|      return   Tiles.TileRangeBetween(zoomlevel, this.minLat, this.minLon, this.maxLat, this.maxLon) | ||||
|     } | ||||
|      | ||||
|     public overlapsWith(other: BBox) { | ||||
|         if (this.maxLon < other.minLon) { | ||||
|             return false; | ||||
|         } | ||||
|         if (this.maxLat < other.minLat) { | ||||
|             return false; | ||||
|         } | ||||
|         if (this.minLon > other.maxLon) { | ||||
|             return false; | ||||
|         } | ||||
|         return this.minLat <= other.maxLat; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public isContainedIn(other: BBox) { | ||||
|         if (this.maxLon > other.maxLon) { | ||||
|             return false; | ||||
|         } | ||||
|         if (this.maxLat > other.maxLat) { | ||||
|             return false; | ||||
|         } | ||||
|         if (this.minLon < other.minLon) { | ||||
|             return false; | ||||
|         } | ||||
|         if (this.minLat < other.minLat) { | ||||
|             return false | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     private check() { | ||||
|         if (isNaN(this.maxLon) || isNaN(this.maxLat) || isNaN(this.minLon) || isNaN(this.minLat)) { | ||||
|             console.log(this); | ||||
|             throw  "BBOX has NAN"; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static fromTile(z: number, x: number, y: number): BBox { | ||||
|         return new BBox(Tiles.tile_bounds_lon_lat(z, x, y)) | ||||
|     } | ||||
| 
 | ||||
|     static fromTileIndex(i: number): BBox { | ||||
|         if (i === 0) { | ||||
|             return BBox.global | ||||
|         } | ||||
|         return BBox.fromTile(...Tiles.tile_from_index(i)) | ||||
|     } | ||||
| 
 | ||||
|     getEast() { | ||||
|         return this.maxLon | ||||
|     } | ||||
| 
 | ||||
|     getNorth() { | ||||
|         return this.maxLat | ||||
|     } | ||||
| 
 | ||||
|     getWest() { | ||||
|         return this.minLon | ||||
|     } | ||||
| 
 | ||||
|     getSouth() { | ||||
|         return this.minLat | ||||
|     } | ||||
| 
 | ||||
|     pad(factor: number): BBox { | ||||
|         const latDiff = this.maxLat - this.minLat | ||||
|         const lat = (this.maxLat + this.minLat) / 2 | ||||
|         const lonDiff = this.maxLon - this.minLon | ||||
|         const lon = (this.maxLon + this.minLon) / 2 | ||||
|         return new BBox([[ | ||||
|             lon - lonDiff * factor, | ||||
|             lat - latDiff * factor | ||||
|         ], [lon + lonDiff * factor, | ||||
|             lat + latDiff * factor]]) | ||||
|     } | ||||
| 
 | ||||
|     toLeaflet() { | ||||
|         return [[this.minLat, this.minLon], [this.maxLat, this.maxLon]] | ||||
|     } | ||||
| 
 | ||||
|     asGeoJson(properties: any): any { | ||||
|         return { | ||||
|             type: "Feature", | ||||
|             properties: properties, | ||||
|             geometry: { | ||||
|                 type: "Polygon", | ||||
|                 coordinates: [[ | ||||
| 
 | ||||
|                     [this.minLon, this.minLat], | ||||
|                     [this.maxLon, this.minLat], | ||||
|                     [this.maxLon, this.maxLat], | ||||
|                     [this.minLon, this.maxLat], | ||||
|                     [this.minLon, this.minLat], | ||||
| 
 | ||||
|                 ]] | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Expands the BBOx so that it contains complete tiles for the given zoomlevel | ||||
|      * @param zoomlevel | ||||
|      */ | ||||
|     expandToTileBounds(zoomlevel: number) : BBox{ | ||||
|         const ul = Tiles.embedded_tile(this.minLat, this.minLon, zoomlevel) | ||||
|         const lr = Tiles.embedded_tile(this.maxLat, this.maxLon, zoomlevel) | ||||
|         const boundsul = Tiles.tile_bounds_lon_lat(ul.z, ul.x, ul.y) | ||||
|         const boundslr = Tiles.tile_bounds_lon_lat(lr.z, lr.x, lr.y) | ||||
|         return new BBox([].concat(boundsul, boundslr)) | ||||
|     } | ||||
| } | ||||
|  | @ -2,7 +2,7 @@ | |||
| import {UIEventSource} from "./UIEventSource"; | ||||
| import FeaturePipeline from "./FeatureSource/FeaturePipeline"; | ||||
| import Loc from "../Models/Loc"; | ||||
| import {BBox} from "./GeoOperations"; | ||||
| import {BBox} from "./BBox"; | ||||
| 
 | ||||
| export default class ContributorCount { | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {BBox, GeoOperations} from "./GeoOperations"; | ||||
| import {GeoOperations} from "./GeoOperations"; | ||||
| import Combine from "../UI/Base/Combine"; | ||||
| import RelationsTracker from "./Osm/RelationsTracker"; | ||||
| import State from "../State"; | ||||
|  | @ -7,6 +7,7 @@ import List from "../UI/Base/List"; | |||
| import Title from "../UI/Base/Title"; | ||||
| import {UIEventSourceTools} from "./UIEventSource"; | ||||
| import AspectedRouting from "./Osm/aspectedRouting"; | ||||
| import {BBox} from "./BBox"; | ||||
| 
 | ||||
| export interface ExtraFuncParams { | ||||
|     /** | ||||
|  |  | |||
|  | @ -17,18 +17,23 @@ import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFea | |||
| import TiledFromLocalStorageSource from "./TiledFeatureSource/TiledFromLocalStorageSource"; | ||||
| import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor"; | ||||
| import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource"; | ||||
| import {BBox} from "../GeoOperations"; | ||||
| import {TileHierarchyMerger} from "./TiledFeatureSource/TileHierarchyMerger"; | ||||
| import RelationsTracker from "../Osm/RelationsTracker"; | ||||
| import {NewGeometryFromChangesFeatureSource} from "./Sources/NewGeometryFromChangesFeatureSource"; | ||||
| import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator"; | ||||
| import {BBox} from "../BBox"; | ||||
| import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource"; | ||||
| import {OsmConnection} from "../Osm/OsmConnection"; | ||||
| import {Tiles} from "../../Models/TileRange"; | ||||
| 
 | ||||
| 
 | ||||
| export default class FeaturePipeline implements FeatureSourceState { | ||||
| export default class FeaturePipeline { | ||||
| 
 | ||||
|     public readonly sufficientlyZoomed: UIEventSource<boolean>; | ||||
|      | ||||
|     public readonly runningQuery: UIEventSource<boolean>; | ||||
|     public readonly timeout: UIEventSource<number>; | ||||
|      | ||||
|     public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
|     public readonly newDataLoadedSignal: UIEventSource<FeatureSource> = new UIEventSource<FeatureSource>(undefined) | ||||
| 
 | ||||
|  | @ -39,24 +44,56 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
|     constructor( | ||||
|         handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void, | ||||
|         state: { | ||||
|             filteredLayers: UIEventSource<FilteredLayer[]>, | ||||
|             locationControl: UIEventSource<Loc>, | ||||
|             selectedElement: UIEventSource<any>, | ||||
|             changes: Changes, | ||||
|             layoutToUse: UIEventSource<LayoutConfig>, | ||||
|             leafletMap: any, | ||||
|             readonly filteredLayers: UIEventSource<FilteredLayer[]>, | ||||
|             readonly locationControl: UIEventSource<Loc>, | ||||
|             readonly selectedElement: UIEventSource<any>, | ||||
|             readonly changes: Changes, | ||||
|             readonly  layoutToUse: UIEventSource<LayoutConfig>, | ||||
|             readonly leafletMap: any, | ||||
|             readonly overpassUrl: UIEventSource<string>; | ||||
|             readonly overpassTimeout: UIEventSource<number>; | ||||
|             readonly overpassMaxZoom: UIEventSource<number>; | ||||
|             readonly osmConnection: OsmConnection | ||||
|             readonly currentBounds: UIEventSource<BBox> | ||||
|         }) { | ||||
| 
 | ||||
|         const self = this | ||||
|         const updater = new OverpassFeatureSource(state); | ||||
| 
 | ||||
|         /** | ||||
|          * Maps tileid onto last download moment | ||||
|          */ | ||||
|         const tileFreshnesses = new Map<number, Date>() | ||||
|         const osmSourceZoomLevel = 14 | ||||
|         const useOsmApi = state.locationControl.map(l => l.zoom > (state.overpassMaxZoom.data ?? 12)) | ||||
|         this.relationTracker = new RelationsTracker() | ||||
| 
 | ||||
|         const updater = new OverpassFeatureSource(state, | ||||
|             { | ||||
|                 relationTracker: this.relationTracker, | ||||
|                 isActive: useOsmApi.map(b => !b), | ||||
|                 onUpdated: (bbox, freshness) => { | ||||
|                     // This callback contains metadata of the overpass call
 | ||||
|                     const range = bbox.containingTileRange(osmSourceZoomLevel) | ||||
|                     Tiles.MapRange(range, (x, y) => { | ||||
|                         tileFreshnesses.set(Tiles.tile_index(osmSourceZoomLevel, x, y), freshness) | ||||
|                     }) | ||||
| 
 | ||||
|                 } | ||||
|             }); | ||||
|          | ||||
|         this.overpassUpdater = updater; | ||||
|         this.sufficientlyZoomed = updater.sufficientlyZoomed | ||||
|         this.runningQuery = updater.runningQuery | ||||
|         this.sufficientlyZoomed = state.locationControl.map(location => { | ||||
|                 if (location?.zoom === undefined) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom ?? 18)); | ||||
|                 return location.zoom >= minzoom; | ||||
|             } | ||||
|         ); | ||||
|          | ||||
|         this.timeout = updater.timeout | ||||
|         this.relationTracker = updater.relationsTracker | ||||
|          | ||||
|          | ||||
|         // Register everything in the state' 'AllElements'
 | ||||
|         new RegisteringAllFromFeatureSourceActor(updater) | ||||
| 
 | ||||
|  | @ -81,6 +118,7 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
|             perLayerHierarchy.get(layerId).registerTile(src) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         for (const filteredLayer of state.filteredLayers.data) { | ||||
|             const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) => patchedHandleFeatureSource(tile)) | ||||
|             const id = filteredLayer.layerDef.id | ||||
|  | @ -91,12 +129,25 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
|                 // This is an OSM layer
 | ||||
|                 // We load the cached values and register them
 | ||||
|                 // Getting data from upstream happens a bit lower
 | ||||
|                 new TiledFromLocalStorageSource(filteredLayer, | ||||
|                 const localStorage = new TiledFromLocalStorageSource(filteredLayer, | ||||
|                     (src) => { | ||||
|                         new RegisteringAllFromFeatureSourceActor(src) | ||||
|                         hierarchy.registerTile(src); | ||||
|                         src.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(src)) | ||||
|                     }, state) | ||||
| 
 | ||||
|                 localStorage.tileFreshness.forEach((value, key) => { | ||||
|                     if (tileFreshnesses.has(key)) { | ||||
|                         const previous = tileFreshnesses.get(key) | ||||
|                         if (value < previous) { | ||||
|                             tileFreshnesses.set(key, value) | ||||
|                         } | ||||
|                     } else { | ||||
|                         tileFreshnesses.set(key, value) | ||||
|                     } | ||||
|                 }) | ||||
| 
 | ||||
| 
 | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|  | @ -106,7 +157,7 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
|                 const src = new GeoJsonSource(filteredLayer) | ||||
|                 TiledFeatureSource.createHierarchy(src, { | ||||
|                     layer: src.layer, | ||||
|                     minZoomLevel:14, | ||||
|                     minZoomLevel: 14, | ||||
|                     dontEnforceMinZoom: true, | ||||
|                     registerTile: (tile) => { | ||||
|                         new RegisteringAllFromFeatureSourceActor(tile) | ||||
|  | @ -125,9 +176,47 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
|                     state | ||||
|                 ) | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         console.log("Tilefreshnesses are", tileFreshnesses) | ||||
|         const oldestAllowedDate = new Date(new Date().getTime() - (60 * 60 * 24 * 30 * 1000)); | ||||
| 
 | ||||
|         const neededTilesFromOsm = state.currentBounds.map(bbox => { | ||||
|             if (bbox === undefined) { | ||||
|                 return | ||||
|             } | ||||
|             const range = bbox.containingTileRange(osmSourceZoomLevel) | ||||
|             const tileIndexes = [] | ||||
|             if (range.total > 100) { | ||||
|                 // Too much tiles!
 | ||||
|                 return [] | ||||
|             } | ||||
|             Tiles.MapRange(range, (x, y) => { | ||||
|                 const i = Tiles.tile_index(osmSourceZoomLevel, x, y); | ||||
|                 if (tileFreshnesses.get(i) > oldestAllowedDate) { | ||||
|                     console.debug("Skipping tile", osmSourceZoomLevel, x, y, "as a decently fresh one is available") | ||||
|                     // The cached tiles contain decently fresh data
 | ||||
|                     return; | ||||
|                 } | ||||
|                 tileIndexes.push(i) | ||||
|             }) | ||||
|             return tileIndexes | ||||
|         }) | ||||
| 
 | ||||
|        const osmFeatureSource = new OsmFeatureSource({ | ||||
|             isActive: useOsmApi, | ||||
|             neededTiles: neededTilesFromOsm, | ||||
|             handleTile: tile => { | ||||
|                 new RegisteringAllFromFeatureSourceActor(tile) | ||||
|                 new SaveTileToLocalStorageActor(tile, tile.tileIndex) | ||||
|                 addToHierarchy(tile, tile.layer.layerDef.id), | ||||
|                     tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) | ||||
| 
 | ||||
|             }, | ||||
|             state: state | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         // Actually load data from the overpass source
 | ||||
|         new PerLayerFeatureSourceSplitter(state.filteredLayers, | ||||
|             (source) => TiledFeatureSource.createHierarchy(source, { | ||||
|  | @ -169,9 +258,15 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
|             self.updateAllMetaTagging() | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         this.runningQuery = updater.runningQuery.map( | ||||
|             overpass => overpass || osmFeatureSource.isRunning.data, [osmFeatureSource.isRunning] | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private applyMetaTags(src: FeatureSourceForLayer){ | ||||
|     private applyMetaTags(src: FeatureSourceForLayer) { | ||||
|         const self = this | ||||
|         console.debug("Applying metatagging onto ", src.name) | ||||
|         window.setTimeout( | ||||
|  | @ -231,7 +326,4 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public ForceRefresh() { | ||||
|         this.overpassUpdater.ForceRefresh() | ||||
|     } | ||||
| } | ||||
|  | @ -1,7 +1,7 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import {BBox} from "../GeoOperations"; | ||||
| import {BBox} from "../BBox"; | ||||
| 
 | ||||
| export default interface FeatureSource { | ||||
|     features: UIEventSource<{ feature: any, freshness: Date }[]>; | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ export default class PerLayerFeatureSourceSplitter { | |||
|                 handleLayerData: (source: FeatureSourceForLayer & Tiled) => void, | ||||
|                 upstream: FeatureSource, | ||||
|                 options?:{ | ||||
|         tileIndex?: number, | ||||
|         handleLeftovers?: (featuresWithoutLayer: any[]) => void | ||||
|                 }) { | ||||
| 
 | ||||
|  | @ -71,7 +72,7 @@ export default class PerLayerFeatureSourceSplitter { | |||
|                 let featureSource = knownLayers.get(id) | ||||
|                 if (featureSource === undefined) { | ||||
|                     // Not yet initialized - now is a good time
 | ||||
|                     featureSource = new SimpleFeatureSource(layer) | ||||
|                     featureSource = new SimpleFeatureSource(layer, options?.tileIndex) | ||||
|                     featureSource.features.setData(features) | ||||
|                     knownLayers.set(id, featureSource) | ||||
|                     handleLayerData(featureSource) | ||||
|  |  | |||
|  | @ -5,9 +5,9 @@ | |||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| import {BBox} from "../../BBox"; | ||||
| 
 | ||||
| export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource { | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | |||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import Hash from "../../Web/Hash"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import {BBox} from "../../BBox"; | ||||
| 
 | ||||
| export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled { | ||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = | ||||
|  |  | |||
|  | @ -5,8 +5,8 @@ import {UIEventSource} from "../../UIEventSource"; | |||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| import {BBox} from "../../BBox"; | ||||
| 
 | ||||
| 
 | ||||
| export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ | |||
|  */ | ||||
| import FeatureSource, {Tiled} from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import {BBox} from "../../BBox"; | ||||
| 
 | ||||
| export default class RememberingSource implements FeatureSource , Tiled{ | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,20 +1,22 @@ | |||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| import {BBox} from "../../BBox"; | ||||
| 
 | ||||
| export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||
|     public readonly name: string = "SimpleFeatureSource"; | ||||
|     public readonly layer: FilteredLayer; | ||||
|     public readonly bbox: BBox = BBox.global; | ||||
|     public readonly tileIndex: number = Tiles.tile_index(0, 0, 0); | ||||
|     public readonly tileIndex: number; | ||||
| 
 | ||||
|     constructor(layer: FilteredLayer) { | ||||
|     constructor(layer: FilteredLayer, tileIndex: number) { | ||||
|         this.name = "SimpleFeatureSource(" + layer.layerDef.id + ")" | ||||
|         this.layer = layer | ||||
|         this.tileIndex = tileIndex ?? 0; | ||||
|         this.bbox = BBox.fromTileIndex(this.tileIndex) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										112
									
								
								Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | |||
| import {Utils} from "../../../Utils"; | ||||
| import * as OsmToGeoJson from "osmtogeojson"; | ||||
| import StaticFeatureSource from "../Sources/StaticFeatureSource"; | ||||
| import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import {OsmConnection} from "../../Osm/OsmConnection"; | ||||
| 
 | ||||
| export default class OsmFeatureSource { | ||||
|     private readonly _backend: string; | ||||
| 
 | ||||
|     public readonly isRunning: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
|     private readonly filteredLayers: UIEventSource<FilteredLayer[]>; | ||||
|     private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void; | ||||
|     private isActive: UIEventSource<boolean>; | ||||
|     private options: { | ||||
|         handleTile: (tile: FeatureSourceForLayer & Tiled) => void; | ||||
|         isActive: UIEventSource<boolean>, | ||||
|         neededTiles: UIEventSource<number[]>, | ||||
|         state: { | ||||
|             readonly osmConnection: OsmConnection; | ||||
|         }; | ||||
|     }; | ||||
|     private readonly downloadedTiles = new Set<number>() | ||||
| 
 | ||||
|     constructor(options: { | ||||
|         handleTile: (tile: FeatureSourceForLayer & Tiled) => void; | ||||
|         isActive: UIEventSource<boolean>, | ||||
|         neededTiles: UIEventSource<number[]>, | ||||
|         state: { | ||||
|             readonly filteredLayers: UIEventSource<FilteredLayer[]>; | ||||
|             readonly osmConnection: OsmConnection; | ||||
|         }; | ||||
|     }) { | ||||
|         this.options = options; | ||||
|         this._backend = options.state.osmConnection._oauth_config.url; | ||||
|         this.filteredLayers = options.state.filteredLayers.map(layers => layers.filter(layer => layer.layerDef.source.geojsonSource === undefined)) | ||||
|         this.handleTile = options.handleTile | ||||
|         this.isActive = options.isActive | ||||
|         const self = this | ||||
|         options.neededTiles.addCallbackAndRunD(neededTiles => { | ||||
|             if (options.isActive?.data === false) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             self.isRunning.setData(true) | ||||
|             try { | ||||
| 
 | ||||
|                 for (const neededTile of neededTiles) { | ||||
|                     if (self.downloadedTiles.has(neededTile)) { | ||||
|                         return; | ||||
|                     } | ||||
|                     self.downloadedTiles.add(neededTile) | ||||
|                     Promise.resolve(self.LoadTile(...Tiles.tile_from_index(neededTile)).then(_ => { | ||||
|                     })) | ||||
|                 } | ||||
|             } catch (e) { | ||||
|                 console.error(e) | ||||
|             } | ||||
|             self.isRunning.setData(false) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private async LoadTile(z, x, y): Promise<void> { | ||||
|         if (z > 18) { | ||||
|             throw "This is an absurd high zoom level" | ||||
|         } | ||||
| 
 | ||||
|         const bbox = BBox.fromTile(z, x, y) | ||||
|         const url = `${this._backend}/api/0.6/map?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` | ||||
|         try { | ||||
| 
 | ||||
|             console.log("Attempting to get tile", z, x, y, "from the osm api") | ||||
|             const osmXml = await Utils.download(url, {"accept": "application/xml"}) | ||||
|             try { | ||||
|                 const parsed = new DOMParser().parseFromString(osmXml, "text/xml"); | ||||
|                 console.log("Got tile", z, x, y, "from the osm api") | ||||
|                 const geojson = OsmToGeoJson.default(parsed, | ||||
|                     // @ts-ignore
 | ||||
|                     { | ||||
|                         flatProperties: true | ||||
|                     }); | ||||
|                 console.log("Tile geojson:", z, x, y, "is", geojson) | ||||
|                 new PerLayerFeatureSourceSplitter(this.filteredLayers, | ||||
|                     this.handleTile, | ||||
|                     new StaticFeatureSource(geojson.features, false), | ||||
|                     { | ||||
|                         tileIndex: Tiles.tile_index(z, x, y) | ||||
|                     } | ||||
|                 ); | ||||
|             } catch (e) { | ||||
|                 console.error("Weird error: ", e) | ||||
|             } | ||||
|         } catch (e) { | ||||
|             console.error("Could not download tile", z, x, y, "due to", e, "; retrying with smaller bounds") | ||||
|             if (e === "rate limited") { | ||||
|                 return; | ||||
|             } | ||||
|             await this.LoadTile(z + 1, x * 2, y * 2) | ||||
|             await this.LoadTile(z + 1, 1 + x * 2, y * 2) | ||||
|             await this.LoadTile(z + 1, x * 2, 1 + y * 2) | ||||
|             await this.LoadTile(z + 1, 1 + x * 2, 1 + y * 2) | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,5 +1,5 @@ | |||
| import FeatureSource, {Tiled} from "../FeatureSource"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import {BBox} from "../../BBox"; | ||||
| 
 | ||||
| export default interface TileHierarchy<T extends FeatureSource & Tiled> { | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,9 +3,9 @@ import {UIEventSource} from "../../UIEventSource"; | |||
| import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| 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>(); | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import TileHierarchy from "./TileHierarchy"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| import {BBox} from "../../BBox"; | ||||
| 
 | ||||
| /** | ||||
|  * Contains all features in a tiled fashion. | ||||
|  | @ -109,7 +109,6 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | |||
|         // To much features - we split
 | ||||
|         return featureCount > this.maxFeatureCount | ||||
|          | ||||
|          | ||||
|     } | ||||
|      | ||||
|     /*** | ||||
|  | @ -143,7 +142,20 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | |||
| 
 | ||||
|         for (const feature of features) { | ||||
|             const bbox = BBox.get(feature.feature) | ||||
|             if (this.options.dontEnforceMinZoom || this.options.minZoomLevel === undefined) { | ||||
| 
 | ||||
|             if (this.options.dontEnforceMinZoom) { | ||||
|                 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) { | ||||
|                 if (bbox.isContainedIn(this.upper_left.bbox)) { | ||||
|                     ulf.push(feature) | ||||
|                 } else if (bbox.isContainedIn(this.upper_right.bbox)) { | ||||
|  |  | |||
|  | @ -5,11 +5,12 @@ import Loc from "../../../Models/Loc"; | |||
| import TileHierarchy from "./TileHierarchy"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import SaveTileToLocalStorageActor from "../Actors/SaveTileToLocalStorageActor"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| import {BBox} from "../../BBox"; | ||||
| 
 | ||||
| export default class TiledFromLocalStorageSource implements TileHierarchy<FeatureSourceForLayer & Tiled> { | ||||
|     public loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>(); | ||||
| public tileFreshness : Map<number, Date> = new Map<number, Date>() | ||||
|      | ||||
|     constructor(layer: FilteredLayer, | ||||
|                 handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void, | ||||
|  | @ -29,7 +30,14 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur | |||
|                 return Number(key.substring(prefix.length)); | ||||
|             }) | ||||
| 
 | ||||
|         console.log("Layer", layer.layerDef.id, "has following tiles in available in localstorage", indexes.map(i => Tiles.tile_from_index(i).join("/")).join(", ")) | ||||
|         console.debug("Layer", layer.layerDef.id, "has following tiles in available in localstorage", indexes.map(i => Tiles.tile_from_index(i).join("/")).join(", ")) | ||||
|         for (const index of indexes) { | ||||
|             const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-" +index+"-time"; | ||||
|             const data = Number(localStorage.getItem(prefix)) | ||||
|             const freshness = new Date() | ||||
|             freshness.setTime(data) | ||||
|             this.tileFreshness.set(index, freshness) | ||||
|         } | ||||
| 
 | ||||
|         const zLevels = indexes.map(i => i % 100) | ||||
|         const indexesSet = new Set(indexes) | ||||
|  | @ -72,7 +80,7 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur | |||
|             } | ||||
|             , [layer.isDisplayed, state.leafletMap]).stabilized(50); | ||||
| 
 | ||||
|         neededTiles.addCallbackAndRun(t => console.log("Tiles to load from localstorage:", t)) | ||||
|         neededTiles.addCallbackAndRun(t => console.debug("Tiles to load from localstorage:", t)) | ||||
| 
 | ||||
|         neededTiles.addCallbackAndRunD(neededIndexes => { | ||||
|             for (const neededIndex of neededIndexes) { | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| import * as turf from '@turf/turf' | ||||
| import {Utils} from "../Utils"; | ||||
| import {Tiles} from "../Models/TileRange"; | ||||
| import {BBox} from "./BBox"; | ||||
| 
 | ||||
| export class GeoOperations { | ||||
| 
 | ||||
|  | @ -379,135 +378,3 @@ export class GeoOperations { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| export class BBox { | ||||
| 
 | ||||
|     readonly maxLat: number; | ||||
|     readonly maxLon: number; | ||||
|     readonly minLat: number; | ||||
|     readonly minLon: number; | ||||
|     static global: BBox = new BBox([[-180, -90], [180, 90]]); | ||||
| 
 | ||||
|     constructor(coordinates) { | ||||
|         this.maxLat = -90; | ||||
|         this.maxLon = -180; | ||||
|         this.minLat = 90; | ||||
|         this.minLon = 180; | ||||
| 
 | ||||
| 
 | ||||
|         for (const coordinate of coordinates) { | ||||
|             this.maxLon = Math.max(this.maxLon, coordinate[0]); | ||||
|             this.maxLat = Math.max(this.maxLat, coordinate[1]); | ||||
|             this.minLon = Math.min(this.minLon, coordinate[0]); | ||||
|             this.minLat = Math.min(this.minLat, coordinate[1]); | ||||
|         } | ||||
|         this.check(); | ||||
|     } | ||||
| 
 | ||||
|     static fromLeafletBounds(bounds) { | ||||
|         return new BBox([[bounds.getWest(), bounds.getNorth()], [bounds.getEast(), bounds.getSouth()]]) | ||||
|     } | ||||
| 
 | ||||
|     static get(feature): BBox { | ||||
|         if (feature.bbox?.overlapsWith === undefined) { | ||||
|             const turfBbox: number[] = turf.bbox(feature) | ||||
|             feature.bbox = new BBox([[turfBbox[0], turfBbox[1]], [turfBbox[2], turfBbox[3]]]); | ||||
|         } | ||||
|         return feature.bbox; | ||||
|     } | ||||
| 
 | ||||
|     public overlapsWith(other: BBox) { | ||||
|         if (this.maxLon < other.minLon) { | ||||
|             return false; | ||||
|         } | ||||
|         if (this.maxLat < other.minLat) { | ||||
|             return false; | ||||
|         } | ||||
|         if (this.minLon > other.maxLon) { | ||||
|             return false; | ||||
|         } | ||||
|         return this.minLat <= other.maxLat; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public isContainedIn(other: BBox) { | ||||
|         if (this.maxLon > other.maxLon) { | ||||
|             return false; | ||||
|         } | ||||
|         if (this.maxLat > other.maxLat) { | ||||
|             return false; | ||||
|         } | ||||
|         if (this.minLon < other.minLon) { | ||||
|             return false; | ||||
|         } | ||||
|         if (this.minLat < other.minLat) { | ||||
|             return false | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     private check() { | ||||
|         if (isNaN(this.maxLon) || isNaN(this.maxLat) || isNaN(this.minLon) || isNaN(this.minLat)) { | ||||
|             console.log(this); | ||||
|             throw  "BBOX has NAN"; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static fromTile(z: number, x: number, y: number): BBox { | ||||
|         return new BBox(Tiles.tile_bounds_lon_lat(z, x, y)) | ||||
|     } | ||||
| 
 | ||||
|     static fromTileIndex(i: number): BBox { | ||||
|         return BBox.fromTile(...Tiles.tile_from_index(i)) | ||||
|     } | ||||
| 
 | ||||
|     getEast() { | ||||
|         return this.maxLon | ||||
|     } | ||||
| 
 | ||||
|     getNorth() { | ||||
|         return this.maxLat | ||||
|     } | ||||
| 
 | ||||
|     getWest() { | ||||
|         return this.minLon | ||||
|     } | ||||
| 
 | ||||
|     getSouth() { | ||||
|         return this.minLat | ||||
|     } | ||||
| 
 | ||||
|     pad(factor: number) : BBox { | ||||
|         const latDiff = this.maxLat - this.minLat | ||||
|         const lat = (this.maxLat + this.minLat) / 2 | ||||
|         const lonDiff = this.maxLon - this.minLon | ||||
|         const lon = (this.maxLon + this.minLon) / 2 | ||||
|         return new BBox([[ | ||||
|             lon - lonDiff * factor, | ||||
|             lat - latDiff * factor | ||||
|         ], [lon + lonDiff * factor, | ||||
|             lat + latDiff * factor]]) | ||||
|     } | ||||
| 
 | ||||
|     toLeaflet() { | ||||
|        return [[this.minLat, this.minLon], [this.maxLat, this.maxLon]] | ||||
|     } | ||||
| 
 | ||||
|     asGeoJson(properties: any) : any{ | ||||
|         return { | ||||
|             type:"Feature", | ||||
|             properties: properties, | ||||
|             geometry:{ | ||||
|                 type:"Polygon", | ||||
|                 coordinates:[[ | ||||
|                      | ||||
|                     [this.minLon, this.minLat], | ||||
|                         [this.maxLon, this.minLat], | ||||
|                         [this.maxLon, this.maxLat], | ||||
|                         [this.minLon, this.maxLat], | ||||
|                         [this.minLon, this.minLat], | ||||
| 
 | ||||
|                 ]] | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -62,26 +62,26 @@ export class OsmConnection { | |||
|     }; | ||||
|     private isChecking = false; | ||||
| 
 | ||||
|     constructor(dryRun: boolean, | ||||
|                 fakeUser: boolean, | ||||
|     constructor(options:{dryRun?: false | boolean, | ||||
|                 fakeUser?: false | boolean, | ||||
|                 allElements: ElementStorage, | ||||
|                 changes: Changes, | ||||
|                 oauth_token: UIEventSource<string>, | ||||
|                 oauth_token?: UIEventSource<string>, | ||||
|                 // Used to keep multiple changesets open and to write to the correct changeset
 | ||||
|                 layoutName: string, | ||||
|                 singlePage: boolean = true, | ||||
|                 osmConfiguration: "osm" | "osm-test" = 'osm' | ||||
|                 singlePage?: boolean, | ||||
|                 osmConfiguration?: "osm" | "osm-test" } | ||||
|     ) { | ||||
|         this.fakeUser = fakeUser; | ||||
|         this._singlePage = singlePage; | ||||
|         this._oauth_config = OsmConnection.oauth_configs[osmConfiguration] ?? OsmConnection.oauth_configs.osm; | ||||
|         this.fakeUser = options.fakeUser ?? false; | ||||
|         this._singlePage = options.singlePage ?? true; | ||||
|         this._oauth_config = OsmConnection.oauth_configs[options.osmConfiguration ?? 'osm'] ?? OsmConnection.oauth_configs.osm; | ||||
|         console.debug("Using backend", this._oauth_config.url) | ||||
|         OsmObject.SetBackendUrl(this._oauth_config.url + "/") | ||||
|         this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; | ||||
| 
 | ||||
|         this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails"); | ||||
|         this.userDetails.data.dryRun = dryRun || fakeUser; | ||||
|         if (fakeUser) { | ||||
|         this.userDetails.data.dryRun = (options.dryRun ?? false) || (options.fakeUser ?? false) ; | ||||
|         if (options.fakeUser) { | ||||
|             const ud = this.userDetails.data; | ||||
|             ud.csCount = 5678 | ||||
|             ud.loggedIn = true; | ||||
|  | @ -98,23 +98,23 @@ export class OsmConnection { | |||
|             } | ||||
|         }); | ||||
|         this.isLoggedIn.addCallbackAndRunD(li => console.log("User is logged in!", li)) | ||||
|         this._dryRun = dryRun; | ||||
|         this._dryRun = options.dryRun; | ||||
| 
 | ||||
|         this.updateAuthObject(); | ||||
| 
 | ||||
|         this.preferencesHandler = new OsmPreferences(this.auth, this); | ||||
| 
 | ||||
|         this.changesetHandler = new ChangesetHandler(layoutName, dryRun, this, allElements, changes, this.auth); | ||||
|         if (oauth_token.data !== undefined) { | ||||
|             console.log(oauth_token.data) | ||||
|         this.changesetHandler = new ChangesetHandler(options.layoutName, options.dryRun, this, options.allElements, options.changes, this.auth); | ||||
|         if (options.oauth_token?.data !== undefined) { | ||||
|             console.log(options.oauth_token.data) | ||||
|             const self = this; | ||||
|             this.auth.bootstrapToken(oauth_token.data, | ||||
|             this.auth.bootstrapToken(options.oauth_token.data, | ||||
|                 (x) => { | ||||
|                     console.log("Called back: ", x) | ||||
|                     self.AttemptLogin(); | ||||
|                 }, this.auth); | ||||
| 
 | ||||
|             oauth_token.setData(undefined); | ||||
|             options.   oauth_token.setData(undefined); | ||||
| 
 | ||||
|         } | ||||
|         if (this.auth.authenticated()) { | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import {Utils} from "../../Utils"; | ||||
| import * as polygon_features from "../../assets/polygon-features.json"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {BBox} from "../GeoOperations"; | ||||
| import {BBox} from "../BBox"; | ||||
| 
 | ||||
| 
 | ||||
| export abstract class OsmObject { | ||||
|  |  | |||
|  | @ -41,10 +41,13 @@ export class Overpass { | |||
|         } | ||||
|         const self = this; | ||||
|         const json = await Utils.downloadJson(query) | ||||
|          | ||||
|         if (json.elements === [] && ((json.remarks ?? json.remark).indexOf("runtime error") >= 0)) { | ||||
|             console.log("Timeout or other runtime error"); | ||||
|             throw("Runtime error (timeout)") | ||||
|         console.log("Got json!", json) | ||||
|         if (json.elements.length === 0 && json.remark !== undefined) { | ||||
|             console.warn("Timeout or other runtime error while querying overpass", json.remark); | ||||
|             throw `Runtime error (timeout or similar)${json.remark}` | ||||
|         } | ||||
|         if(json.elements.length === 0){ | ||||
|          console.warn("No features for" ,json)    | ||||
|         } | ||||
| 
 | ||||
|         self._relationTracker.RegisterRelations(json) | ||||
|  |  | |||
							
								
								
									
										24
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										24
									
								
								State.ts
									
										
									
									
									
								
							|  | @ -17,7 +17,7 @@ import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; | |||
| import FilteredLayer from "./Models/FilteredLayer"; | ||||
| import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor"; | ||||
| import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; | ||||
| import {BBox} from "./Logic/GeoOperations"; | ||||
| import {BBox} from "./Logic/BBox"; | ||||
| 
 | ||||
| /** | ||||
|  * Contains the global state: a bunch of UI-event sources | ||||
|  | @ -83,7 +83,7 @@ export default class State { | |||
|     public readonly featureSwitchExportAsPdf: UIEventSource<boolean>; | ||||
|     public readonly overpassUrl: UIEventSource<string>; | ||||
|     public readonly overpassTimeout: UIEventSource<number>; | ||||
|     public readonly overpassMaxZoom: UIEventSource<number> = new UIEventSource<number>(undefined); | ||||
|     public readonly overpassMaxZoom: UIEventSource<number> = new UIEventSource<number>(20); | ||||
| 
 | ||||
|     public featurePipeline: FeaturePipeline; | ||||
| 
 | ||||
|  | @ -372,21 +372,19 @@ export default class State { | |||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.osmConnection = new OsmConnection( | ||||
|             this.featureSwitchIsTesting.data, | ||||
|             this.featureSwitchFakeUser.data, | ||||
|             this.allElements,  | ||||
|             this.changes, | ||||
|             QueryParameters.GetQueryParameter( | ||||
|         this.osmConnection = new OsmConnection({ | ||||
|             changes: this.changes, | ||||
|             dryRun: this.featureSwitchIsTesting.data, | ||||
|             fakeUser: this.featureSwitchFakeUser.data, | ||||
|             allElements: this.allElements, | ||||
|             oauth_token: QueryParameters.GetQueryParameter( | ||||
|                 "oauth_token", | ||||
|                 undefined, | ||||
|                 "Used to complete the login" | ||||
|             ), | ||||
|             layoutToUse?.id, | ||||
|             true, | ||||
|             // @ts-ignore
 | ||||
|             this.featureSwitchApiURL.data | ||||
|         ); | ||||
|             layoutName: layoutToUse?.id, | ||||
|             osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         new ChangeToElementsActor(this.changes, this.allElements) | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import {BBox} from "../../Logic/GeoOperations"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| 
 | ||||
| export interface MinimapOptions { | ||||
|     background?: UIEventSource<BaseLayer>, | ||||
|  |  | |||
|  | @ -4,10 +4,10 @@ import {UIEventSource} from "../../Logic/UIEventSource"; | |||
| import Loc from "../../Models/Loc"; | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||
| import {BBox} from "../../Logic/GeoOperations"; | ||||
| import * as L from "leaflet"; | ||||
| import {Map} from "leaflet"; | ||||
| import Minimap, {MinimapObj, MinimapOptions} from "./Minimap"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| 
 | ||||
| export default class MinimapImplementation extends BaseUIElement implements MinimapObj { | ||||
|     private static _nextId = 0; | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import Constants from "../../Models/Constants"; | |||
| import Loc from "../../Models/Loc"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {BBox} from "../../Logic/GeoOperations"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| 
 | ||||
| /** | ||||
|  * The bottom right attribution panel in the leaflet map | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ import State from "../../State"; | |||
| import {Utils} from "../../Utils"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import CheckBoxes from "../Input/Checkboxes"; | ||||
| import {BBox, GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import Title from "../Base/Title"; | ||||
| import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; | ||||
|  | @ -13,6 +13,7 @@ import {UIEventSource} from "../../Logic/UIEventSource"; | |||
| import SimpleMetaTagger from "../../Logic/SimpleMetaTagger"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {meta} from "@turf/turf"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| 
 | ||||
| export class DownloadPanel extends Toggle { | ||||
|      | ||||
|  |  | |||
|  | @ -1,7 +1,5 @@ | |||
| import State from "../../State"; | ||||
| import ThemeIntroductionPanel from "./ThemeIntroductionPanel"; | ||||
| import * as personal from "../../assets/themes/personal/personal.json"; | ||||
| import PersonalLayersPanel from "./PersonalLayersPanel"; | ||||
| import Svg from "../../Svg"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import ShareScreen from "./ShareScreen"; | ||||
|  | @ -32,9 +30,7 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | |||
|     private static ConstructBaseTabs(layoutToUse: LayoutConfig, isShown: UIEventSource<boolean>): { header: string | BaseUIElement; content: BaseUIElement }[] { | ||||
| 
 | ||||
|         let welcome: BaseUIElement = new ThemeIntroductionPanel(isShown); | ||||
|         if (layoutToUse.id === personal.id) { | ||||
|             welcome = new PersonalLayersPanel(); | ||||
|         } | ||||
|         | ||||
|         const tabs: { header: string | BaseUIElement, content: BaseUIElement }[] = [ | ||||
|             {header: `<img src='${layoutToUse.icon}'>`, content: welcome}, | ||||
|             { | ||||
|  |  | |||
|  | @ -11,8 +11,8 @@ import AllDownloads from "./AllDownloads"; | |||
| import FilterView from "./FilterView"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; | ||||
| import {BBox} from "../../Logic/GeoOperations"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| 
 | ||||
| export default class LeftControls extends Combine { | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,120 +0,0 @@ | |||
| import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts"; | ||||
| import Svg from "../../Svg"; | ||||
| import State from "../../State"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| 
 | ||||
| export default class PersonalLayersPanel extends VariableUiElement { | ||||
| 
 | ||||
|     constructor() { | ||||
|         super( | ||||
|             State.state.installedThemes.map(installedThemes => { | ||||
|                 const t = Translations.t.favourite; | ||||
| 
 | ||||
|                 // Lets get all the layers
 | ||||
|                 const allThemes = AllKnownLayouts.layoutsList.concat(installedThemes.map(layout => layout.layout)) | ||||
|                     .filter(theme => !theme.hideFromOverview) | ||||
| 
 | ||||
|                 const allLayers = [] | ||||
|                 { | ||||
|                     const seenLayers = new Set<string>() | ||||
|                     for (const layers of allThemes.map(theme => theme.layers)) { | ||||
|                         for (const layer of layers) { | ||||
|                             if (seenLayers.has(layer.id)) { | ||||
|                                 continue | ||||
|                             } | ||||
|                             seenLayers.add(layer.id) | ||||
|                             allLayers.push(layer) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 // Time to create a panel based on them!
 | ||||
|                 const panel: BaseUIElement = new Combine(allLayers.map(PersonalLayersPanel.CreateLayerToggle)); | ||||
| 
 | ||||
| 
 | ||||
|                 return new Toggle( | ||||
|                     new Combine([ | ||||
|                         t.panelIntro.Clone(), | ||||
|                         panel | ||||
|                     ]).SetClass("flex flex-col"), | ||||
|                     new SubtleButton( | ||||
|                         Svg.osm_logo_ui(), | ||||
|                         t.loginNeeded.Clone().SetClass("text-center") | ||||
|                     ).onClick(() => State.state.osmConnection.AttemptLogin()), | ||||
|                     State.state.osmConnection.isLoggedIn | ||||
|                 ) | ||||
|             }) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /*** | ||||
|      * Creates a toggle for the given layer, which'll update State.state.favouriteLayers right away | ||||
|      * @param layer | ||||
|      * @constructor | ||||
|      * @private | ||||
|      */ | ||||
|     private static CreateLayerToggle(layer: LayerConfig): Toggle { | ||||
|         let icon: BaseUIElement = new Combine([layer.GenerateLeafletStyle( | ||||
|             new UIEventSource<any>({id: "node/-1"}), | ||||
|             false | ||||
|         ).icon.html]).SetClass("relative") | ||||
|         let iconUnset = new Combine([layer.GenerateLeafletStyle( | ||||
|             new UIEventSource<any>({id: "node/-1"}), | ||||
|             false | ||||
|         ).icon.html]).SetClass("relative") | ||||
| 
 | ||||
|         iconUnset.SetStyle("opacity:0.1") | ||||
| 
 | ||||
|         let name = layer.name; | ||||
|         if (name === undefined) { | ||||
|             return undefined; | ||||
|         } | ||||
|         const content = new Combine([ | ||||
|             Translations.WT(name).Clone().SetClass("font-bold"), | ||||
|             Translations.WT(layer.description)?.Clone() | ||||
|         ]).SetClass("flex flex-col") | ||||
| 
 | ||||
|         const contentUnselected = new Combine([ | ||||
|             Translations.WT(name).Clone().SetClass("font-bold"), | ||||
|             Translations.WT(layer.description)?.Clone() | ||||
|         ]).SetClass("flex flex-col line-through") | ||||
| 
 | ||||
|         return new Toggle( | ||||
|             new SubtleButton( | ||||
|                 icon, | ||||
|                 content), | ||||
|             new SubtleButton( | ||||
|                 iconUnset, | ||||
|                 contentUnselected | ||||
|             ), | ||||
|             State.state.favouriteLayers.map(favLayers => { | ||||
|                 return favLayers.indexOf(layer.id) >= 0 | ||||
|             }, [], (selected, current) => { | ||||
|                 if (!selected && current.indexOf(layer.id) <= 0) { | ||||
|                     // Not selected and not contained: nothing to change: we return current as is
 | ||||
|                     return current; | ||||
|                 } | ||||
|                 if (selected && current.indexOf(layer.id) >= 0) { | ||||
|                     // Selected and contained: this is fine!
 | ||||
|                     return current; | ||||
|                 } | ||||
|                 const clone = [...current] | ||||
|                 if (selected) { | ||||
|                     clone.push(layer.id) | ||||
|                 } else { | ||||
|                     clone.splice(clone.indexOf(layer.id), 1) | ||||
|                 } | ||||
|                 return clone | ||||
|             }) | ||||
|         ).ToggleOnClick(); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -20,7 +20,7 @@ import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject"; | |||
| import PresetConfig from "../../Models/ThemeConfig/PresetConfig"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import {And} from "../../Logic/Tags/And"; | ||||
| import {BBox} from "../../Logic/GeoOperations"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| 
 | ||||
| /* | ||||
| * The SimpleAddUI is a single panel, which can have multiple states: | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter"; | |||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| import Minimap from "./Base/Minimap"; | ||||
| import Loc from "../Models/Loc"; | ||||
| import {BBox} from "../Logic/GeoOperations"; | ||||
| import BaseLayer from "../Models/BaseLayer"; | ||||
| import {FixedUiElement} from "./Base/FixedUiElement"; | ||||
| import Translations from "./i18n/Translations"; | ||||
|  | @ -14,6 +13,7 @@ import Constants from "../Models/Constants"; | |||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||
| import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"; | ||||
| import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; | ||||
| import {BBox} from "../Logic/BBox"; | ||||
| /** | ||||
|  * Creates screenshoter to take png screenshot | ||||
|  * Creates jspdf and downloads it | ||||
|  |  | |||
|  | @ -7,11 +7,12 @@ import Combine from "../Base/Combine"; | |||
| import Svg from "../../Svg"; | ||||
| import State from "../../State"; | ||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||
| import {BBox, GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; | ||||
| import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; | ||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| 
 | ||||
| export default class LocationInput extends InputElement<Loc> { | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ import {SubtleButton} from "../Base/SubtleButton"; | |||
| import Minimap from "../Base/Minimap"; | ||||
| import State from "../../State"; | ||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; | ||||
| import {BBox, GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import {LeafletMouseEvent} from "leaflet"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {Button} from "../Base/Button"; | ||||
|  | @ -15,6 +15,7 @@ import Title from "../Base/Title"; | |||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| 
 | ||||
| export default class SplitRoadWizard extends Toggle { | ||||
|     private static splitLayerStyling = new LayerConfig({ | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import FeatureSource, {FeatureSourceForLayer, Tiled} from "../../Logic/FeatureSource/FeatureSource"; | ||||
| import {BBox} from "../../Logic/GeoOperations"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Tiles} from "../../Models/TileRange"; | ||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| 
 | ||||
| export class TileHierarchyAggregator implements FeatureSource { | ||||
|     private _parent: TileHierarchyAggregator; | ||||
|  |  | |||
							
								
								
									
										21
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										21
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -192,11 +192,12 @@ export class Utils { | |||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Copies all key-value pairs of the source into the target. | ||||
|      * Copies all key-value pairs of the source into the target. This will change the target | ||||
|      * If the key starts with a '+', the values of the list will be appended to the target instead of overwritten | ||||
|      * @param source | ||||
|      * @param target | ||||
|      * @constructor | ||||
|      * @return the second parameter as is | ||||
|      */ | ||||
|     static Merge(source: any, target: any) { | ||||
|         for (const key in source) { | ||||
|  | @ -288,15 +289,13 @@ export class Utils { | |||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     private static injectedDownloads = {} | ||||
| 
 | ||||
|     public static injectJsonDownloadForTests(url: string, data) { | ||||
|         Utils.injectedDownloads[url] = data | ||||
|     } | ||||
| 
 | ||||
|     public static downloadJson(url: string, headers?: any): Promise<any> { | ||||
| 
 | ||||
|     public static download(url: string, headers?: any): Promise<string> { | ||||
|         const injected = Utils.injectedDownloads[url] | ||||
|         if (injected !== undefined) { | ||||
|             console.log("Using injected resource for test for URL", url) | ||||
|  | @ -311,17 +310,14 @@ export class Utils { | |||
|                 const xhr = new XMLHttpRequest(); | ||||
|                 xhr.onload = () => { | ||||
|                     if (xhr.status == 200) { | ||||
|                         try { | ||||
|                             resolve(JSON.parse(xhr.response)) | ||||
|                         } catch (e) { | ||||
|                             reject("Not a valid json: " + xhr.response) | ||||
|                         } | ||||
|                         resolve(xhr.response) | ||||
|                     } else if (xhr.status === 509 || xhr.status === 429){ | ||||
|                       reject("rate limited")   | ||||
|                     } else { | ||||
|                         reject(xhr.statusText) | ||||
|                     } | ||||
|                 }; | ||||
|                 xhr.open('GET', url); | ||||
|                 xhr.setRequestHeader("accept", "application/json") | ||||
|                 if (headers !== undefined) { | ||||
| 
 | ||||
|                     for (const key in headers) { | ||||
|  | @ -334,6 +330,11 @@ export class Utils { | |||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     public static async downloadJson(url: string, headers?: any): Promise<any> { | ||||
|         const data = await Utils.download(url, Utils.Merge({"accept": "application/json"}, headers ?? {})) | ||||
|         return JSON.parse(data) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Triggers a 'download file' popup which will download the contents | ||||
|      */ | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ | |||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     "#": "if sport is defined and is not bicycle, it is retrackted; if bicycle retail/repair is marked as 'no', it is retracted too.", | ||||
|                     "#": "if sport is defined and is not bicycle, it is not matched; if bicycle retail/repair is marked as 'no', it is not shown to too.", | ||||
|                     "##": "There will be a few false-positives with this. They will get filtered out by people marking both 'not selling bikes' and 'not repairing bikes'. Furthermore, the OSMers will add a sports-subcategory on it", | ||||
|                     "and": [ | ||||
|                         "shop=sports", | ||||
|  | @ -38,13 +38,6 @@ | |||
|                             ] | ||||
|                         } | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     "#": "Any shop with any bicycle service", | ||||
|                     "and": [ | ||||
|                         "shop~*", | ||||
|                         "service:bicycle:.*~~.*" | ||||
|                     ] | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ | |||
|         "mappings": [ | ||||
|             { | ||||
|                 "if": "wheelchair=yes", | ||||
|                 "then": "./assets/layers/toilet/wheelchair.svg" | ||||
|                 "then": "circle:white;./assets/layers/toilet/wheelchair.svg" | ||||
|             }, | ||||
|             { | ||||
|                 "if": { | ||||
|  |  | |||
|  | @ -5,9 +5,10 @@ | |||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||
|    xmlns:cc="http://creativecommons.org/ns#" | ||||
|    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||
|         xmlns="http://www.w3.org/2000/svg" | ||||
|    version="1.1" | ||||
|    id="Layer_1" | ||||
|    width="483.2226563" | ||||
|  | @ -17,12 +18,12 @@ | |||
|    enable-background="new 0 0 483.2226563 551.4306641" | ||||
|    xml:space="preserve" | ||||
|    sodipodi:docname="wheelchair.svg" | ||||
|         inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata | ||||
|    inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"><metadata | ||||
|    id="metadata11"><rdf:RDF><cc:Work | ||||
|        rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type | ||||
|         rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata> | ||||
|          rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata> | ||||
|     <defs | ||||
|             id="defs9"/> | ||||
|    id="defs9" /> | ||||
|     <sodipodi:namedview | ||||
|    pagecolor="#ffffff" | ||||
|    bordercolor="#666666" | ||||
|  | @ -33,16 +34,16 @@ | |||
|    inkscape:pageopacity="0" | ||||
|    inkscape:pageshadow="2" | ||||
|    inkscape:window-width="1920" | ||||
|             inkscape:window-height="1001" | ||||
|    inkscape:window-height="999" | ||||
|    id="namedview7" | ||||
|    showgrid="false" | ||||
|    inkscape:zoom="0.8559553" | ||||
|             inkscape:cx="-66.220714" | ||||
|    inkscape:cx="-16.568588" | ||||
|    inkscape:cy="292.29436" | ||||
|    inkscape:window-x="0" | ||||
|    inkscape:window-y="0" | ||||
|    inkscape:window-maximized="1" | ||||
|             inkscape:current-layer="Layer_1"/> | ||||
|    inkscape:current-layer="layer2" /> | ||||
| 
 | ||||
| 
 | ||||
|     <g | ||||
|  | @ -55,13 +56,7 @@ | |||
|    cy="274.54706" | ||||
|    rx="241.83505" | ||||
|    ry="275.71533" /> | ||||
|         <ellipse | ||||
|                 style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.484;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | ||||
|                 id="path819" | ||||
|                 cx="240.66678" | ||||
|                 cy="275.71533" | ||||
|                 rx="241.83505" | ||||
|                 ry="274.54706"/></g> | ||||
|         </g> | ||||
|     <g | ||||
|    inkscape:groupmode="layer" | ||||
|    id="layer1" | ||||
|  | @ -74,4 +69,4 @@ | |||
|    inkscape:connector-curvature="0" | ||||
|    style="clip-rule:evenodd;fill:#000000;fill-rule:evenodd;stroke-width:0.66635805" | ||||
|    d="m 310.84431,395.24795 c -20.28873,40.10642 -62.75413,66.52908 -108.0502,66.52908 -66.52908,0 -120.790412,-54.26133 -120.790412,-120.79041 0,-46.71209 28.310452,-90.1207 70.555212,-109.36341 l 2.73376,35.67521 c -24.98647,15.74498 -40.3895,44.15435 -40.3895,73.93288 0,48.26215 39.36263,87.62413 87.62413,87.62413 44.15407,0 81.80523,-33.88535 86.93958,-77.35545 z" | ||||
|                 id="path4"/></g></svg> | ||||
|    id="path4" /></g></svg> | ||||
| Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.1 KiB | 
|  | @ -43,6 +43,12 @@ | |||
|   "widenFactor": 1.5, | ||||
|   "roamingRenderings": [], | ||||
|   "layers": [ | ||||
|     "bicycle_library" | ||||
|     { | ||||
|       "builtin": "bicycle_library", | ||||
|       "override": { | ||||
|         "minZoom": 0 | ||||
|       } | ||||
|     }, | ||||
|      | ||||
|   ] | ||||
| } | ||||
|  | @ -12,7 +12,7 @@ | |||
|     "zh_Hant": "個人化主題" | ||||
|   }, | ||||
|   "description": { | ||||
|     "en": "Create a personal theme based on all the available layers of all themes", | ||||
|     "en": "Create a personal theme based on all the available layers of all themes. Open the layer selection to select one or more layers.", | ||||
|     "nl": "Stel je eigen thema samen door lagen te combineren van alle andere themas", | ||||
|     "es": "Crea una interficie basada en todas las capas disponibles de todas las interficies", | ||||
|     "ca": "Crea una interfície basada en totes les capes disponibles de totes les interfícies", | ||||
|  | @ -37,11 +37,14 @@ | |||
|   ], | ||||
|   "maintainer": "MapComplete", | ||||
|   "icon": "./assets/svg/addSmall.svg", | ||||
|   "clustering": { | ||||
|     "maxZoom": 19 | ||||
|   }, | ||||
|   "version": "0", | ||||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 16, | ||||
|   "widenFactor": 3, | ||||
|   "widenFactor": 1.2, | ||||
|   "layers": [], | ||||
|   "roamingRenderings": [] | ||||
| } | ||||
|  | @ -20,7 +20,7 @@ | |||
|   "startZoom": 8, | ||||
|   "startLat": 50.8536, | ||||
|   "startLon": 4.433, | ||||
|   "widenFactor": 2, | ||||
|   "widenFactor": 1.5, | ||||
|   "layers": [ | ||||
|     { | ||||
|       "builtin": [ | ||||
|  |  | |||
|  | @ -10,9 +10,17 @@ import LZString from "lz-string"; | |||
| import BaseUIElement from "./UI/BaseUIElement"; | ||||
| import Table from "./UI/Base/Table"; | ||||
| import {LayoutConfigJson} from "./Models/ThemeConfig/Json/LayoutConfigJson"; | ||||
| import {Changes} from "./Logic/Osm/Changes"; | ||||
| import {ElementStorage} from "./Logic/ElementStorage"; | ||||
| 
 | ||||
| 
 | ||||
| const connection = new OsmConnection(false, false, new UIEventSource<string>(undefined), ""); | ||||
| const connection = new OsmConnection({ | ||||
|     osmConfiguration: 'osm', | ||||
|     changes: new Changes(), | ||||
|     layoutName: '', | ||||
|     allElements: new ElementStorage() | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| let rendered = false; | ||||
| 
 | ||||
|  | @ -20,6 +28,7 @@ function salvageThemes(preferences: any) { | |||
|     const knownThemeNames = new Set<string>(); | ||||
|     const correctThemeNames = [] | ||||
|     for (const key in preferences) { | ||||
|             try{ | ||||
|         if (!(typeof key === "string")) { | ||||
|             continue; | ||||
|         } | ||||
|  | @ -36,7 +45,7 @@ function salvageThemes(preferences: any) { | |||
|         } else { | ||||
|             knownThemeNames.add(theme); | ||||
|         } | ||||
|     } | ||||
|     }catch(e){console.error(e)}} | ||||
| 
 | ||||
|     for (const correctThemeName of correctThemeNames) { | ||||
|         knownThemeNames.delete(correctThemeName); | ||||
|  | @ -65,8 +74,13 @@ function salvageThemes(preferences: any) { | |||
|         try { | ||||
|             jsonObject = JSON.parse(atob(combined)); | ||||
|         } catch (e) { | ||||
|             try{ | ||||
|                  | ||||
|             // We try to decode with lz-string
 | ||||
|             jsonObject = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(combined))) as LayoutConfigJson; | ||||
|             }catch(e0){ | ||||
|                 console.log("Could not salvage theme. Initial parsing failed due to:", e,"\nWith LZ failed due ", e0) | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										56
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										56
									
								
								test.ts
									
										
									
									
									
								
							|  | @ -1,33 +1,35 @@ | |||
| import SplitRoadWizard from "./UI/Popup/SplitRoadWizard"; | ||||
| import State from "./State"; | ||||
| import {Tiles} from "./Models/TileRange"; | ||||
| import OsmFeatureSource from "./Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource"; | ||||
| import {Utils} from "./Utils"; | ||||
| import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; | ||||
| import MinimapImplementation from "./UI/Base/MinimapImplementation"; | ||||
| import {UIEventSource} from "./Logic/UIEventSource"; | ||||
| import FilteredLayer from "./Models/FilteredLayer"; | ||||
| import {And} from "./Logic/Tags/And"; | ||||
| import ShowDataLayer from "./UI/ShowDataLayer/ShowDataLayer"; | ||||
| import ShowTileInfo from "./UI/ShowDataLayer/ShowTileInfo"; | ||||
| import StaticFeatureSource from "./Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import {BBox} from "./Logic/GeoOperations"; | ||||
| import Minimap from "./UI/Base/Minimap"; | ||||
| import LayerConfig from "./Models/ThemeConfig/LayerConfig"; | ||||
| 
 | ||||
| State.state = new State(undefined) | ||||
| const allLayers: LayerConfig[] = [] | ||||
| const seenIds = new Set<string>() | ||||
| for (const layoutConfig of AllKnownLayouts.layoutsList) { | ||||
|     if (layoutConfig.hideFromOverview) { | ||||
|         continue | ||||
|     } | ||||
|     for (const layer of layoutConfig.layers) { | ||||
|         if (seenIds.has(layer.id)) { | ||||
|             continue | ||||
|         } | ||||
|         seenIds.add(layer.id) | ||||
|         allLayers.push(layer) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| const leafletMap = new UIEventSource(undefined) | ||||
| MinimapImplementation.initialize() | ||||
| Minimap.createMiniMap({ | ||||
|     leafletMap: leafletMap, | ||||
| }).SetStyle("height: 600px; width: 600px") | ||||
|     .AttachTo("maindiv") | ||||
| console.log("All layer ids", allLayers.map(l => l.id)) | ||||
| 
 | ||||
| const bbox = BBox.fromTile(16,32754,21785).asGeoJson({ | ||||
|     count: 42, | ||||
|     tileId: 42 | ||||
| const src = new OsmFeatureSource({ | ||||
|     backend: "https://www.openstreetmap.org", | ||||
|     handleTile: tile => console.log("Got tile", tile), | ||||
|     allLayers: allLayers | ||||
| }) | ||||
| 
 | ||||
| console.log(bbox) | ||||
| new ShowDataLayer({ | ||||
|     layerToShow: ShowTileInfo.styling, | ||||
|     leafletMap: leafletMap, | ||||
|     features: new StaticFeatureSource([ bbox], false) | ||||
| src.LoadTile(16, 33354, 21875).then(geojson => { | ||||
|     console.log("Got geojson", geojson); | ||||
|     Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), "test.geojson", { | ||||
|         mimetype: "application/vnd.geo+json" | ||||
|     }) | ||||
| }) | ||||
| //*/
 | ||||
|  | @ -1,8 +1,9 @@ | |||
| import {Utils} from "../Utils"; | ||||
| import * as Assert from "assert"; | ||||
| import T from "./TestHelper"; | ||||
| import {BBox, GeoOperations} from "../Logic/GeoOperations"; | ||||
| import {GeoOperations} from "../Logic/GeoOperations"; | ||||
| import {equal} from "assert"; | ||||
| import {BBox} from "../Logic/BBox"; | ||||
| 
 | ||||
| Utils.runningFromConsole = true; | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,6 +2,9 @@ import T from "./TestHelper"; | |||
| import UserDetails, {OsmConnection} from "../Logic/Osm/OsmConnection"; | ||||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| import ScriptUtils from "../scripts/ScriptUtils"; | ||||
| import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; | ||||
| import {ElementStorage} from "../Logic/ElementStorage"; | ||||
| import {Changes} from "../Logic/Osm/Changes"; | ||||
| 
 | ||||
| 
 | ||||
| export default class OsmConnectionSpec extends T { | ||||
|  | @ -15,12 +18,14 @@ export default class OsmConnectionSpec extends T { | |||
|         super("osmconnection", [ | ||||
|             ["login on dev", | ||||
|                 () => { | ||||
|                     const osmConn = new OsmConnection(false, false, | ||||
|                         new UIEventSource<string>(undefined), | ||||
|                         "Unit test", | ||||
|                         true, | ||||
|                         "osm-test" | ||||
|                     ) | ||||
|                     const osmConn = new OsmConnection({ | ||||
|                             osmConfiguration: "osm-test", | ||||
|                             layoutName: "Unit test", | ||||
|                             allElements: new ElementStorage(), | ||||
|                             changes: new Changes(), | ||||
|                             oauth_token: new UIEventSource<string>(OsmConnectionSpec._osm_token) | ||||
|                         } | ||||
|                     ); | ||||
| 
 | ||||
|                     osmConn.userDetails.map((userdetails: UserDetails) => { | ||||
|                         if (userdetails.loggedIn) { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue