forked from MapComplete/MapComplete
		
	Add initial clustering per tile, very broken
This commit is contained in:
		
							parent
							
								
									2b78c4b53f
								
							
						
					
					
						commit
						c5e9448720
					
				
					 88 changed files with 1080 additions and 651 deletions
				
			
		|  | @ -75,9 +75,7 @@ class StatsDownloader { | ||||||
| 
 | 
 | ||||||
|         while (url) { |         while (url) { | ||||||
|             ScriptUtils.erasableLog(`Downloading stats for ${year}-${month}, page ${page} ${url}`) |             ScriptUtils.erasableLog(`Downloading stats for ${year}-${month}, page ${page} ${url}`) | ||||||
|             const result = await ScriptUtils.DownloadJSON(url, { |             const result = await ScriptUtils.DownloadJSON(url, headers) | ||||||
|                 headers: headers |  | ||||||
|             }) |  | ||||||
|             page++; |             page++; | ||||||
|             allFeatures.push(...result.features) |             allFeatures.push(...result.features) | ||||||
|             if (result.features === undefined) { |             if (result.features === undefined) { | ||||||
|  |  | ||||||
|  | @ -15,7 +15,6 @@ import Link from "./UI/Base/Link"; | ||||||
| import * as personal from "./assets/themes/personal/personal.json"; | import * as personal from "./assets/themes/personal/personal.json"; | ||||||
| import * as L from "leaflet"; | import * as L from "leaflet"; | ||||||
| import Img from "./UI/Base/Img"; | import Img from "./UI/Base/Img"; | ||||||
| import UserDetails from "./Logic/Osm/OsmConnection"; |  | ||||||
| import Attribution from "./UI/BigComponents/Attribution"; | import Attribution from "./UI/BigComponents/Attribution"; | ||||||
| import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter"; | import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter"; | ||||||
| import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs"; | import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs"; | ||||||
|  | @ -38,6 +37,9 @@ import Minimap from "./UI/Base/Minimap"; | ||||||
| import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler"; | import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler"; | ||||||
| import Combine from "./UI/Base/Combine"; | import Combine from "./UI/Base/Combine"; | ||||||
| import {SubtleButton} from "./UI/Base/SubtleButton"; | import {SubtleButton} from "./UI/Base/SubtleButton"; | ||||||
|  | import ShowTileInfo from "./UI/ShowDataLayer/ShowTileInfo"; | ||||||
|  | import {Tiles} from "./Models/TileRange"; | ||||||
|  | import PerTileCountAggregator from "./UI/ShowDataLayer/PerTileCountAggregator"; | ||||||
| 
 | 
 | ||||||
| export class InitUiElements { | export class InitUiElements { | ||||||
|     static InitAll( |     static InitAll( | ||||||
|  | @ -167,9 +169,20 @@ export class InitUiElements { | ||||||
|             ).AttachTo("messagesbox"); |             ).AttachTo("messagesbox"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         State.state.osmConnection.userDetails |         function addHomeMarker() { | ||||||
|             .map((userDetails: UserDetails) => userDetails?.home) |             const userDetails = State.state.osmConnection.userDetails.data; | ||||||
|             .addCallbackAndRunD((home) => { |             if (userDetails === undefined) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             console.log("Adding home location of ", userDetails) | ||||||
|  |             const home = userDetails.home; | ||||||
|  |             if (home === undefined) { | ||||||
|  |                 return userDetails.loggedIn; // If logged in, the home is not set and we unregister. If not logged in, we stay registered if a login still comes
 | ||||||
|  |             } | ||||||
|  |             const leaflet = State.state.leafletMap.data; | ||||||
|  |             if (leaflet === undefined) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|             const color = getComputedStyle(document.body).getPropertyValue( |             const color = getComputedStyle(document.body).getPropertyValue( | ||||||
|                 "--subtle-detail-color" |                 "--subtle-detail-color" | ||||||
|             ); |             ); | ||||||
|  | @ -181,8 +194,13 @@ export class InitUiElements { | ||||||
|                 iconAnchor: [15, 15], |                 iconAnchor: [15, 15], | ||||||
|             }); |             }); | ||||||
|             const marker = L.marker([home.lat, home.lon], {icon: icon}); |             const marker = L.marker([home.lat, home.lon], {icon: icon}); | ||||||
|                 marker.addTo(State.state.leafletMap.data); |             marker.addTo(leaflet); | ||||||
|             }); |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         State.state.osmConnection.userDetails | ||||||
|  |             .addCallbackAndRunD(_ => addHomeMarker()); | ||||||
|  |         State.state.leafletMap.addCallbackAndRunD(_ => addHomeMarker()) | ||||||
| 
 | 
 | ||||||
|         if (layoutToUse.id === personal.id) { |         if (layoutToUse.id === personal.id) { | ||||||
|             updateFavs(); |             updateFavs(); | ||||||
|  | @ -250,16 +268,16 @@ export class InitUiElements { | ||||||
|             return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))]; |             return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))]; | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
| 
 | 
 | ||||||
|             if(hash === undefined || hash.length < 10){ |             if (hash === undefined || hash.length < 10) { | ||||||
|                 e = "Did you effectively add a theme? It seems no data could be found." |                 e = "Did you effectively add a theme? It seems no data could be found." | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             new Combine([ |             new Combine([ | ||||||
|                 "Error: could not parse the custom layout:", |                 "Error: could not parse the custom layout:", | ||||||
|                 new FixedUiElement(""+e).SetClass("alert"), |                 new FixedUiElement("" + e).SetClass("alert"), | ||||||
|                 new SubtleButton("./assets/svg/mapcomplete_logo.svg", |                 new SubtleButton("./assets/svg/mapcomplete_logo.svg", | ||||||
|                     "Go back to the theme overview", |                     "Go back to the theme overview", | ||||||
|                     {url: window.location.protocol+"//"+ window.location.hostname+"/index.html", newTab: false}) |                     {url: window.location.protocol + "//" + window.location.hostname + "/index.html", newTab: false}) | ||||||
| 
 | 
 | ||||||
|             ]) |             ]) | ||||||
|                 .SetClass("flex flex-col") |                 .SetClass("flex flex-col") | ||||||
|  | @ -361,12 +379,12 @@ export class InitUiElements { | ||||||
|         const layout = State.state.layoutToUse.data; |         const layout = State.state.layoutToUse.data; | ||||||
|         if (layout.lockLocation) { |         if (layout.lockLocation) { | ||||||
|             if (layout.lockLocation === true) { |             if (layout.lockLocation === true) { | ||||||
|                 const tile = Utils.embedded_tile( |                 const tile = Tiles.embedded_tile( | ||||||
|                     layout.startLat, |                     layout.startLat, | ||||||
|                     layout.startLon, |                     layout.startLon, | ||||||
|                     layout.startZoom - 1 |                     layout.startZoom - 1 | ||||||
|                 ); |                 ); | ||||||
|                 const bounds = Utils.tile_bounds(tile.z, tile.x, tile.y); |                 const bounds = Tiles.tile_bounds(tile.z, tile.x, tile.y); | ||||||
|                 // We use the bounds to get a sense of distance for this zoom level
 |                 // We use the bounds to get a sense of distance for this zoom level
 | ||||||
|                 const latDiff = bounds[0][0] - bounds[1][0]; |                 const latDiff = bounds[0][0] - bounds[1][0]; | ||||||
|                 const lonDiff = bounds[0][1] - bounds[1][1]; |                 const lonDiff = bounds[0][1] - bounds[1][1]; | ||||||
|  | @ -402,6 +420,9 @@ export class InitUiElements { | ||||||
|                 const flayer = { |                 const flayer = { | ||||||
|                     isDisplayed: isDisplayed, |                     isDisplayed: isDisplayed, | ||||||
|                     layerDef: layer, |                     layerDef: layer, | ||||||
|  |                     isSufficientlyZoomed: state.locationControl.map(l => { | ||||||
|  |                         return l.zoom >= (layer.minzoomVisible ?? layer.minzoom) | ||||||
|  |                     }), | ||||||
|                     appliedFilters: new UIEventSource<TagsFilter>(undefined), |                     appliedFilters: new UIEventSource<TagsFilter>(undefined), | ||||||
|                 }; |                 }; | ||||||
|                 flayers.push(flayer); |                 flayers.push(flayer); | ||||||
|  | @ -409,13 +430,54 @@ export class InitUiElements { | ||||||
|             return flayers; |             return flayers; | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  |         const clusterCounter = new PerTileCountAggregator(State.state.locationControl.map(l => { | ||||||
|  |             const z = l.zoom + 1 | ||||||
|  |             if(z < 7){ | ||||||
|  |                 return 7 | ||||||
|  |             } | ||||||
|  |             return z | ||||||
|  |         })) | ||||||
|  |         const clusterShow = Math.min(...State.state.layoutToUse.data.layers.map(layer => layer.minzoomVisible ?? layer.minzoom)) | ||||||
|  |         new ShowDataLayer({ | ||||||
|  |             features: clusterCounter, | ||||||
|  |             leafletMap: State.state.leafletMap, | ||||||
|  |             layerToShow: ShowTileInfo.styling, | ||||||
|  |             doShowLayer: State.state.locationControl.map(l => l.zoom < clusterShow) | ||||||
|  |         }) | ||||||
|         State.state.featurePipeline = new FeaturePipeline( |         State.state.featurePipeline = new FeaturePipeline( | ||||||
|             source => { |             source => { | ||||||
|  |                 const clustering = State.state.layoutToUse.data.clustering | ||||||
|  |                 const doShowFeatures = source.features.map( | ||||||
|  |                     f => { | ||||||
|  |                         const z = State.state.locationControl.data.zoom | ||||||
|  |                         if(z >= clustering.maxZoom){ | ||||||
|  |                             return true | ||||||
|  |                         } | ||||||
|  |                         if(z < source.layer.layerDef.minzoom){ | ||||||
|  |                             return false; | ||||||
|  |                         } | ||||||
|  |                         if(f.length > clustering.minNeededElements){ | ||||||
|  |                             console.log("Activating clustering for tile ", Tiles.tile_from_index(source.tileIndex)," as it has ", f.length, "features (clustering starts at)", clustering.minNeededElements) | ||||||
|  |                             return false | ||||||
|  |                         } | ||||||
|  |                          | ||||||
|  |                         return true | ||||||
|  |                     }, [State.state.locationControl] | ||||||
|  |                 ) | ||||||
|  |                 clusterCounter.addTile(source, doShowFeatures.map(b => !b)) | ||||||
|  |                  | ||||||
|  |                 /* | ||||||
|  |                 new ShowTileInfo({source: source,  | ||||||
|  |                     leafletMap: State.state.leafletMap,  | ||||||
|  |                     layer: source.layer.layerDef, | ||||||
|  |                     doShowLayer: doShowFeatures.map(b => !b) | ||||||
|  |                 })*/ | ||||||
|                 new ShowDataLayer( |                 new ShowDataLayer( | ||||||
|                     { |                     { | ||||||
|                         features: source, |                         features: source, | ||||||
|                         leafletMap: State.state.leafletMap, |                         leafletMap: State.state.leafletMap, | ||||||
|                         layerToShow: source.layer.layerDef |                         layerToShow: source.layer.layerDef, | ||||||
|  |                         doShowLayer: doShowFeatures | ||||||
|                     } |                     } | ||||||
|                 ); |                 ); | ||||||
|             }, state |             }, state | ||||||
|  |  | ||||||
|  | @ -44,7 +44,6 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | ||||||
|         readonly overpassUrl: UIEventSource<string>; |         readonly overpassUrl: UIEventSource<string>; | ||||||
|         readonly overpassTimeout: UIEventSource<number>; |         readonly overpassTimeout: UIEventSource<number>; | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * The most important layer should go first, as that one gets first pick for the questions |      * The most important layer should go first, as that one gets first pick for the questions | ||||||
|      */ |      */ | ||||||
|  | @ -57,6 +56,7 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | ||||||
|             readonly overpassTimeout: UIEventSource<number>; |             readonly overpassTimeout: UIEventSource<number>; | ||||||
|             readonly overpassMaxZoom: UIEventSource<number> |             readonly overpassMaxZoom: UIEventSource<number> | ||||||
|         }) { |         }) { | ||||||
|  |         console.trace("Initializing an overpass FS") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         this.state = state |         this.state = state | ||||||
|  | @ -153,7 +153,12 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | ||||||
|         return new Overpass(new Or(filters), extraScripts, this.state.overpassUrl, this.state.overpassTimeout, this.relationsTracker); |         return new Overpass(new Or(filters), extraScripts, this.state.overpassUrl, this.state.overpassTimeout, this.relationsTracker); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private update(): void { |     private update() { | ||||||
|  |         this.updateAsync().then(_ => { | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private async updateAsync(): Promise<void> { | ||||||
|         if (this.runningQuery.data) { |         if (this.runningQuery.data) { | ||||||
|             console.log("Still running a query, not updating"); |             console.log("Still running a query, not updating"); | ||||||
|             return; |             return; | ||||||
|  | @ -184,49 +189,41 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         this.runningQuery.setData(true); |         this.runningQuery.setData(true); | ||||||
|         overpass.queryGeoJson(queryBounds). | 
 | ||||||
|             then(([data, date]) => { |         let data: any = undefined | ||||||
|  |         let date: Date = undefined | ||||||
|  | 
 | ||||||
|  |         do { | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  |                 [data, date] = await overpass.queryGeoJson(queryBounds) | ||||||
|  |             } catch (e) { | ||||||
|  |                 console.error(`QUERY FAILED (retrying in ${5 * self.retries.data} sec) due to`, e); | ||||||
|  | 
 | ||||||
|  |                 self.retries.data++; | ||||||
|  |                 self.retries.ping(); | ||||||
|  | 
 | ||||||
|  |                 self.timeout.setData(self.retries.data * 5); | ||||||
|  |                 self.runningQuery.setData(false); | ||||||
|  | 
 | ||||||
|  |                 while (self.timeout.data > 0) { | ||||||
|  |                     await Utils.waitFor(1000) | ||||||
|  |                     self.timeout.data-- | ||||||
|  |                     self.timeout.ping(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } while (data === undefined); | ||||||
|  | 
 | ||||||
|         self._previousBounds.get(z).push(queryBounds); |         self._previousBounds.get(z).push(queryBounds); | ||||||
|         self.retries.setData(0); |         self.retries.setData(0); | ||||||
|                 const features = data.features.map(f => ({feature: f, freshness: date})); |  | ||||||
|                 SimpleMetaTagger.objectMetaInfo.addMetaTags(features) |  | ||||||
| 
 | 
 | ||||||
|                 try{ |         try { | ||||||
|                     self.features.setData(features); |             data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date)); | ||||||
|                 }catch(e){ |             self.features.setData(data.features.map(f => ({feature: f, freshness: date}))); | ||||||
|  |         } catch (e) { | ||||||
|             console.error("Got the overpass response, but could not process it: ", e, e.stack) |             console.error("Got the overpass response, but could not process it: ", e, e.stack) | ||||||
|         } |         } | ||||||
|         self.runningQuery.setData(false); |         self.runningQuery.setData(false); | ||||||
|             }) |  | ||||||
|             .catch((reason) => { |  | ||||||
|                 self.retries.data++; |  | ||||||
|                 self.ForceRefresh(); |  | ||||||
|                 self.timeout.setData(self.retries.data * 5); |  | ||||||
|                 console.error(`QUERY FAILED (retrying in ${5 * self.retries.data} sec) due to`, reason); |  | ||||||
|                 self.retries.ping(); |  | ||||||
|                 self.runningQuery.setData(false); |  | ||||||
| 
 |  | ||||||
|                 function countDown() { |  | ||||||
|                     window?.setTimeout( |  | ||||||
|                         function () { |  | ||||||
|                             if (self.timeout.data > 1) { |  | ||||||
|                                 self.timeout.setData(self.timeout.data - 1); |  | ||||||
|                                 window.setTimeout( |  | ||||||
|                                     countDown, |  | ||||||
|                                     1000 |  | ||||||
|                                 ) |  | ||||||
|                             } else { |  | ||||||
|                                 self.timeout.setData(0); |  | ||||||
|                                 self.update() |  | ||||||
|                             } |  | ||||||
|                         }, 1000 |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 countDown(); |  | ||||||
| 
 |  | ||||||
|             } |  | ||||||
|         ); |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -256,7 +256,7 @@ export class ExtraFunction { | ||||||
|         let closestFeatures: { feat: any, distance: number }[] = []; |         let closestFeatures: { feat: any, distance: number }[] = []; | ||||||
|         for(const featureList of features) { |         for(const featureList of features) { | ||||||
|             for (const otherFeature of featureList) { |             for (const otherFeature of featureList) { | ||||||
|                 if (otherFeature == feature || otherFeature.id == feature.id) { |                 if (otherFeature === feature || otherFeature.id === feature.id) { | ||||||
|                     continue; // We ignore self
 |                     continue; // We ignore self
 | ||||||
|                 } |                 } | ||||||
|                 let distance = undefined; |                 let distance = undefined; | ||||||
|  | @ -268,7 +268,8 @@ export class ExtraFunction { | ||||||
|                         [feature._lon, feature._lat] |                         [feature._lon, feature._lat] | ||||||
|                     ) |                     ) | ||||||
|                 } |                 } | ||||||
|                 if (distance === undefined) { |                 if (distance === undefined || distance === null) { | ||||||
|  |                     console.error("Could not calculate the distance between", feature, "and", otherFeature) | ||||||
|                     throw "Undefined distance!" |                     throw "Undefined distance!" | ||||||
|                 } |                 } | ||||||
|                 if (distance > maxDistance) { |                 if (distance > maxDistance) { | ||||||
|  |  | ||||||
|  | @ -37,7 +37,7 @@ export default class FeaturePipeline implements FeatureSourceState { | ||||||
|     private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>; |     private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>; | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         handleFeatureSource: (source: FeatureSourceForLayer) => void, |         handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void, | ||||||
|         state: { |         state: { | ||||||
|             filteredLayers: UIEventSource<FilteredLayer[]>, |             filteredLayers: UIEventSource<FilteredLayer[]>, | ||||||
|             locationControl: UIEventSource<Loc>, |             locationControl: UIEventSource<Loc>, | ||||||
|  | @ -52,7 +52,6 @@ export default class FeaturePipeline implements FeatureSourceState { | ||||||
| 
 | 
 | ||||||
|         const self = this |         const self = this | ||||||
|         const updater = new OverpassFeatureSource(state); |         const updater = new OverpassFeatureSource(state); | ||||||
|         updater.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(updater)) |  | ||||||
|         this.overpassUpdater = updater; |         this.overpassUpdater = updater; | ||||||
|         this.sufficientlyZoomed = updater.sufficientlyZoomed |         this.sufficientlyZoomed = updater.sufficientlyZoomed | ||||||
|         this.runningQuery = updater.runningQuery |         this.runningQuery = updater.runningQuery | ||||||
|  | @ -65,14 +64,15 @@ export default class FeaturePipeline implements FeatureSourceState { | ||||||
|         const perLayerHierarchy = new Map<string, TileHierarchyMerger>() |         const perLayerHierarchy = new Map<string, TileHierarchyMerger>() | ||||||
|         this.perLayerHierarchy = perLayerHierarchy |         this.perLayerHierarchy = perLayerHierarchy | ||||||
| 
 | 
 | ||||||
|         const patchedHandleFeatureSource = function (src: FeatureSourceForLayer & IndexedFeatureSource) { |         const patchedHandleFeatureSource = function (src: FeatureSourceForLayer & IndexedFeatureSource & Tiled) { | ||||||
|             // This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
 |             // This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
 | ||||||
|             const srcFiltered = |             const srcFiltered = | ||||||
|                 new FilteringFeatureSource(state, |                 new FilteringFeatureSource(state, src.tileIndex, | ||||||
|                     new WayHandlingApplyingFeatureSource( |                     new WayHandlingApplyingFeatureSource( | ||||||
|                         new ChangeGeometryApplicator(src, state.changes) |                         new ChangeGeometryApplicator(src, state.changes) | ||||||
|                     ) |                     ) | ||||||
|                 ) |                 ) | ||||||
|  |              | ||||||
|             handleFeatureSource(srcFiltered) |             handleFeatureSource(srcFiltered) | ||||||
|             self.somethingLoaded.setData(true) |             self.somethingLoaded.setData(true) | ||||||
|         }; |         }; | ||||||
|  | @ -102,10 +102,12 @@ export default class FeaturePipeline implements FeatureSourceState { | ||||||
| 
 | 
 | ||||||
|             if (source.geojsonZoomLevel === undefined) { |             if (source.geojsonZoomLevel === undefined) { | ||||||
|                 // This is a 'load everything at once' geojson layer
 |                 // This is a 'load everything at once' geojson layer
 | ||||||
|                 // We split them up into tiles
 |                 // We split them up into tiles anyway
 | ||||||
|                 const src = new GeoJsonSource(filteredLayer) |                 const src = new GeoJsonSource(filteredLayer) | ||||||
|                 TiledFeatureSource.createHierarchy(src, { |                 TiledFeatureSource.createHierarchy(src, { | ||||||
|                     layer: src.layer, |                     layer: src.layer, | ||||||
|  |                     minZoomLevel:14, | ||||||
|  |                     dontEnforceMinZoom: true, | ||||||
|                     registerTile: (tile) => { |                     registerTile: (tile) => { | ||||||
|                         new RegisteringAllFromFeatureSourceActor(tile) |                         new RegisteringAllFromFeatureSourceActor(tile) | ||||||
|                         addToHierarchy(tile, id) |                         addToHierarchy(tile, id) | ||||||
|  | @ -115,14 +117,11 @@ export default class FeaturePipeline implements FeatureSourceState { | ||||||
|             } else { |             } else { | ||||||
|                 new DynamicGeoJsonTileSource( |                 new DynamicGeoJsonTileSource( | ||||||
|                     filteredLayer, |                     filteredLayer, | ||||||
|                     src => TiledFeatureSource.createHierarchy(src, { |                     tile => { | ||||||
|                         layer: src.layer, |  | ||||||
|                         registerTile: (tile) => { |  | ||||||
|                             new RegisteringAllFromFeatureSourceActor(tile) |                             new RegisteringAllFromFeatureSourceActor(tile) | ||||||
|                             addToHierarchy(tile, id) |                             addToHierarchy(tile, id) | ||||||
|                             tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) |                             tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) | ||||||
|                         } |                         }, | ||||||
|                     }), |  | ||||||
|                     state |                     state | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|  | @ -133,13 +132,17 @@ export default class FeaturePipeline implements FeatureSourceState { | ||||||
|         new PerLayerFeatureSourceSplitter(state.filteredLayers, |         new PerLayerFeatureSourceSplitter(state.filteredLayers, | ||||||
|             (source) => TiledFeatureSource.createHierarchy(source, { |             (source) => TiledFeatureSource.createHierarchy(source, { | ||||||
|                 layer: source.layer, |                 layer: source.layer, | ||||||
|  |                 minZoomLevel: 14, | ||||||
|  |                 dontEnforceMinZoom: true, | ||||||
|                 registerTile: (tile) => { |                 registerTile: (tile) => { | ||||||
|                     // We save the tile data for the given layer to local storage
 |                     // We save the tile data for the given layer to local storage
 | ||||||
|                     new SaveTileToLocalStorageActor(tile, tile.tileIndex) |                     new SaveTileToLocalStorageActor(tile, tile.tileIndex) | ||||||
|                     addToHierarchy(tile, source.layer.layerDef.id); |                     addToHierarchy(new RememberingSource(tile), source.layer.layerDef.id); | ||||||
|  |                     tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) | ||||||
|  | 
 | ||||||
|                 } |                 } | ||||||
|             }), |             }), | ||||||
|             new RememberingSource(updater)) |             updater) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         // Also load points/lines that are newly added. 
 |         // Also load points/lines that are newly added. 
 | ||||||
|  | @ -152,6 +155,8 @@ export default class FeaturePipeline implements FeatureSourceState { | ||||||
|                 addToHierarchy(perLayer, perLayer.layer.layerDef.id) |                 addToHierarchy(perLayer, perLayer.layer.layerDef.id) | ||||||
|                 // AT last, we always apply the metatags whenever possible
 |                 // AT last, we always apply the metatags whenever possible
 | ||||||
|                 perLayer.features.addCallbackAndRunD(_ => self.applyMetaTags(perLayer)) |                 perLayer.features.addCallbackAndRunD(_ => self.applyMetaTags(perLayer)) | ||||||
|  |                 perLayer.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(perLayer)) | ||||||
|  | 
 | ||||||
|             }, |             }, | ||||||
|             newGeometry |             newGeometry | ||||||
|         ) |         ) | ||||||
|  | @ -166,6 +171,7 @@ export default class FeaturePipeline implements FeatureSourceState { | ||||||
|      |      | ||||||
|     private applyMetaTags(src: FeatureSourceForLayer){ |     private applyMetaTags(src: FeatureSourceForLayer){ | ||||||
|         const self = this |         const self = this | ||||||
|  |         console.log("Applying metatagging onto ", src.name) | ||||||
|         MetaTagging.addMetatags( |         MetaTagging.addMetatags( | ||||||
|             src.features.data, |             src.features.data, | ||||||
|             { |             { | ||||||
|  | @ -183,6 +189,7 @@ export default class FeaturePipeline implements FeatureSourceState { | ||||||
| 
 | 
 | ||||||
|     private updateAllMetaTagging() { |     private updateAllMetaTagging() { | ||||||
|         const self = this; |         const self = this; | ||||||
|  |         console.log("Reupdating all metatagging") | ||||||
|         this.perLayerHierarchy.forEach(hierarchy => { |         this.perLayerHierarchy.forEach(hierarchy => { | ||||||
|             hierarchy.loadedTiles.forEach(src => { |             hierarchy.loadedTiles.forEach(src => { | ||||||
|                 self.applyMetaTags(src) |                 self.applyMetaTags(src) | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer"; | ||||||
| import {BBox} from "../../GeoOperations"; | import {BBox} from "../../GeoOperations"; | ||||||
| import {Utils} from "../../../Utils"; | import {Utils} from "../../../Utils"; | ||||||
|  | import {Tiles} from "../../../Models/TileRange"; | ||||||
| 
 | 
 | ||||||
| export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource { | export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource { | ||||||
| 
 | 
 | ||||||
|  | @ -23,7 +24,7 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled | ||||||
|         this.bbox = bbox; |         this.bbox = bbox; | ||||||
|         this._sources = sources; |         this._sources = sources; | ||||||
|         this.layer = layer; |         this.layer = layer; | ||||||
|         this.name = "FeatureSourceMerger("+layer.layerDef.id+", "+Utils.tile_from_index(tileIndex).join(",")+")" |         this.name = "FeatureSourceMerger("+layer.layerDef.id+", "+Tiles.tile_from_index(tileIndex).join(",")+")" | ||||||
|         const self = this; |         const self = this; | ||||||
| 
 | 
 | ||||||
|         const handledSources = new Set<FeatureSource>(); |         const handledSources = new Set<FeatureSource>(); | ||||||
|  |  | ||||||
|  | @ -1,24 +1,29 @@ | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import {UIEventSource} from "../../UIEventSource"; | ||||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer"; | ||||||
| import {FeatureSourceForLayer} from "../FeatureSource"; | import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||||
| import Hash from "../../Web/Hash"; | import Hash from "../../Web/Hash"; | ||||||
|  | import {BBox} from "../../GeoOperations"; | ||||||
| 
 | 
 | ||||||
| export default class FilteringFeatureSource implements FeatureSourceForLayer { | export default class FilteringFeatureSource implements FeatureSourceForLayer , Tiled { | ||||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = |     public features: UIEventSource<{ feature: any; freshness: Date }[]> = | ||||||
|         new UIEventSource<{ feature: any; freshness: Date }[]>([]); |         new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||||
|     public readonly name; |     public readonly name; | ||||||
|     public readonly layer: FilteredLayer; |     public readonly layer: FilteredLayer; | ||||||
| 
 | public readonly tileIndex : number | ||||||
|  |     public readonly bbox : BBox | ||||||
|     constructor( |     constructor( | ||||||
|         state: { |         state: { | ||||||
|             locationControl: UIEventSource<{ zoom: number }>, |             locationControl: UIEventSource<{ zoom: number }>, | ||||||
|             selectedElement: UIEventSource<any>, |             selectedElement: UIEventSource<any>, | ||||||
|         }, |         }, | ||||||
|  |         tileIndex, | ||||||
|         upstream: FeatureSourceForLayer |         upstream: FeatureSourceForLayer | ||||||
|     ) { |     ) { | ||||||
|         const self = this; |         const self = this; | ||||||
|         this.name = "FilteringFeatureSource("+upstream.name+")" |         this.name = "FilteringFeatureSource("+upstream.name+")" | ||||||
|  |         this.tileIndex = tileIndex | ||||||
|  |         this.bbox = BBox.fromTileIndex(tileIndex) | ||||||
| 
 | 
 | ||||||
|         this.layer = upstream.layer; |         this.layer = upstream.layer; | ||||||
|         const layer = upstream.layer; |         const layer = upstream.layer; | ||||||
|  | @ -51,7 +56,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer { | ||||||
|                         return false; |                         return false; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 if (!FilteringFeatureSource.showLayer(layer, state.locationControl.data)) { |                 if (!layer.isDisplayed) { | ||||||
|                     // The layer itself is either disabled or hidden due to zoom constraints
 |                     // The layer itself is either disabled or hidden due to zoom constraints
 | ||||||
|                     // We should return true, but it might still match some other layer
 |                     // We should return true, but it might still match some other layer
 | ||||||
|                     return false; |                     return false; | ||||||
|  | @ -66,10 +71,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer { | ||||||
|             update(); |             update(); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         let isShown = state.locationControl.map((l) => FilteringFeatureSource.showLayer(layer, l), |         layer.isDisplayed.addCallback(isShown => { | ||||||
|             [layer.isDisplayed]) |  | ||||||
|              |  | ||||||
|         isShown.addCallback(isShown => { |  | ||||||
|             if (isShown) { |             if (isShown) { | ||||||
|                 update(); |                 update(); | ||||||
|             } else { |             } else { | ||||||
|  | @ -78,7 +80,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer { | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         layer.appliedFilters.addCallback(_ => { |         layer.appliedFilters.addCallback(_ => { | ||||||
|             if(!isShown.data){ |             if(!layer.isDisplayed.data){ | ||||||
|                 // Currently not shown.
 |                 // Currently not shown.
 | ||||||
|                 // Note that a change in 'isSHown' will trigger an update as well, so we don't have to watch it another time
 |                 // Note that a change in 'isSHown' will trigger an update as well, so we don't have to watch it another time
 | ||||||
|                 return; |                 return; | ||||||
|  | @ -93,10 +95,8 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer { | ||||||
|         layer: { |         layer: { | ||||||
|             isDisplayed: UIEventSource<boolean>; |             isDisplayed: UIEventSource<boolean>; | ||||||
|             layerDef: LayerConfig; |             layerDef: LayerConfig; | ||||||
|         }, |         }) { | ||||||
|         location: { zoom: number }) { |         return layer.isDisplayed.data; | ||||||
|         return layer.isDisplayed.data && |  | ||||||
|             layer.layerDef.minzoomVisible <= location.zoom; |  | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import FilteredLayer from "../../../Models/FilteredLayer"; | ||||||
| import {Utils} from "../../../Utils"; | import {Utils} from "../../../Utils"; | ||||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||||
| import {BBox} from "../../GeoOperations"; | import {BBox} from "../../GeoOperations"; | ||||||
|  | import {Tiles} from "../../../Models/TileRange"; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | ||||||
|  | @ -35,10 +36,10 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | ||||||
|                 .replace('{z}', "" + z) |                 .replace('{z}', "" + z) | ||||||
|                 .replace('{x}', "" + x) |                 .replace('{x}', "" + x) | ||||||
|                 .replace('{y}', "" + y) |                 .replace('{y}', "" + y) | ||||||
|             this.tileIndex = Utils.tile_index(z, x, y) |             this.tileIndex = Tiles.tile_index(z, x, y) | ||||||
|             this.bbox = BBox.fromTile(z, x, y) |             this.bbox = BBox.fromTile(z, x, y) | ||||||
|         } else { |         } else { | ||||||
|             this.tileIndex = Utils.tile_index(0, 0, 0) |             this.tileIndex = Tiles.tile_index(0, 0, 0) | ||||||
|             this.bbox = BBox.global; |             this.bbox = BBox.global; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -89,7 +90,6 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | ||||||
| 
 | 
 | ||||||
|                     newFeatures.push({feature: feature, freshness: freshness}) |                     newFeatures.push({feature: feature, freshness: freshness}) | ||||||
|                 } |                 } | ||||||
|                 console.debug("Downloaded " + newFeatures.length + " new features and " + skipped + " already seen features from " + url); |  | ||||||
| 
 | 
 | ||||||
|                 if (newFeatures.length == 0) { |                 if (newFeatures.length == 0) { | ||||||
|                     return; |                     return; | ||||||
|  |  | ||||||
|  | @ -2,17 +2,23 @@ | ||||||
|  * Every previously added point is remembered, but new points are added. |  * Every previously added point is remembered, but new points are added. | ||||||
|  * Data coming from upstream will always overwrite a previous value |  * Data coming from upstream will always overwrite a previous value | ||||||
|  */ |  */ | ||||||
| import FeatureSource from "../FeatureSource"; | import FeatureSource, {Tiled} from "../FeatureSource"; | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import {UIEventSource} from "../../UIEventSource"; | ||||||
|  | import {BBox} from "../../GeoOperations"; | ||||||
| 
 | 
 | ||||||
| export default class RememberingSource implements FeatureSource { | export default class RememberingSource implements FeatureSource , Tiled{ | ||||||
| 
 | 
 | ||||||
|     public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>; |     public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>; | ||||||
|     public readonly name; |     public readonly name; | ||||||
|  |     public readonly  tileIndex : number | ||||||
|  |     public  readonly  bbox : BBox | ||||||
|      |      | ||||||
|     constructor(source: FeatureSource) { |     constructor(source: FeatureSource & Tiled) { | ||||||
|         const self = this; |         const self = this; | ||||||
|         this.name = "RememberingSource of " + source.name; |         this.name = "RememberingSource of " + source.name; | ||||||
|  |         this.tileIndex=  source.tileIndex | ||||||
|  |         this.bbox = source.bbox; | ||||||
|  |          | ||||||
|         const empty = []; |         const empty = []; | ||||||
|         this.features = source.features.map(features => { |         this.features = source.features.map(features => { | ||||||
|             const oldFeatures = self.features?.data ?? empty; |             const oldFeatures = self.features?.data ?? empty; | ||||||
|  |  | ||||||
|  | @ -3,13 +3,14 @@ import FilteredLayer from "../../../Models/FilteredLayer"; | ||||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||||
| import {BBox} from "../../GeoOperations"; | import {BBox} from "../../GeoOperations"; | ||||||
| import {Utils} from "../../../Utils"; | import {Utils} from "../../../Utils"; | ||||||
|  | import {Tiles} from "../../../Models/TileRange"; | ||||||
| 
 | 
 | ||||||
| export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled { | export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled { | ||||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); |     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||||
|     public readonly name: string = "SimpleFeatureSource"; |     public readonly name: string = "SimpleFeatureSource"; | ||||||
|     public readonly layer: FilteredLayer; |     public readonly layer: FilteredLayer; | ||||||
|     public readonly bbox: BBox = BBox.global; |     public readonly bbox: BBox = BBox.global; | ||||||
|     public readonly tileIndex: number = Utils.tile_index(0, 0, 0); |     public readonly tileIndex: number = Tiles.tile_index(0, 0, 0); | ||||||
| 
 | 
 | ||||||
|     constructor(layer: FilteredLayer) { |     constructor(layer: FilteredLayer) { | ||||||
|         this.name = "SimpleFeatureSource(" + layer.layerDef.id + ")" |         this.name = "SimpleFeatureSource(" + layer.layerDef.id + ")" | ||||||
|  |  | ||||||
|  | @ -8,12 +8,13 @@ export default class StaticFeatureSource implements FeatureSource { | ||||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; |     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||||
|     public readonly name: string = "StaticFeatureSource" |     public readonly name: string = "StaticFeatureSource" | ||||||
| 
 | 
 | ||||||
|     constructor(features: any[] | UIEventSource<any[]>, useFeaturesDirectly = false) { |     constructor(features: any[] | UIEventSource<any[] | UIEventSource<{ feature: any, freshness: Date }>>, useFeaturesDirectly) { | ||||||
|         const now = new Date(); |         const now = new Date(); | ||||||
|         if(useFeaturesDirectly){ |         if (useFeaturesDirectly) { | ||||||
|             // @ts-ignore
 |             // @ts-ignore
 | ||||||
|             this.features = features |             this.features = features | ||||||
|         }else         if (features instanceof UIEventSource) { |         } else if (features instanceof UIEventSource) { | ||||||
|  |             // @ts-ignore
 | ||||||
|             this.features = features.map(features => features.map(f => ({feature: f, freshness: now}))) |             this.features = features.map(features => features.map(f => ({feature: f, freshness: now}))) | ||||||
|         } else { |         } else { | ||||||
|             this.features = new UIEventSource(features.map(f => ({ |             this.features = new UIEventSource(features.map(f => ({ | ||||||
|  |  | ||||||
|  | @ -12,7 +12,8 @@ export default class WayHandlingApplyingFeatureSource implements FeatureSourceFo | ||||||
|     public readonly layer; |     public readonly layer; | ||||||
| 
 | 
 | ||||||
|     constructor(upstream: FeatureSourceForLayer) { |     constructor(upstream: FeatureSourceForLayer) { | ||||||
|         this.name = "Wayhandling(" + upstream.name+")"; |          | ||||||
|  |         this.name = "Wayhandling(" + upstream.name + ")"; | ||||||
|         this.layer = upstream.layer |         this.layer = upstream.layer | ||||||
|         const layer = upstream.layer.layerDef; |         const layer = upstream.layer.layerDef; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer"; | ||||||
| import {FeatureSourceForLayer} from "../FeatureSource"; | import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import {UIEventSource} from "../../UIEventSource"; | ||||||
| import Loc from "../../../Models/Loc"; | import Loc from "../../../Models/Loc"; | ||||||
| import DynamicTileSource from "./DynamicTileSource"; | import DynamicTileSource from "./DynamicTileSource"; | ||||||
|  | @ -8,7 +8,7 @@ import GeoJsonSource from "../Sources/GeoJsonSource"; | ||||||
| 
 | 
 | ||||||
| export default class DynamicGeoJsonTileSource extends DynamicTileSource { | export default class DynamicGeoJsonTileSource extends DynamicTileSource { | ||||||
|     constructor(layer: FilteredLayer, |     constructor(layer: FilteredLayer, | ||||||
|                 registerLayer: (layer: FeatureSourceForLayer) => void, |                 registerLayer: (layer: FeatureSourceForLayer & Tiled) => void, | ||||||
|                 state: { |                 state: { | ||||||
|                     locationControl: UIEventSource<Loc> |                     locationControl: UIEventSource<Loc> | ||||||
|                     leafletMap: any |                     leafletMap: any | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import {Utils} from "../../../Utils"; | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import {UIEventSource} from "../../UIEventSource"; | ||||||
| import Loc from "../../../Models/Loc"; | import Loc from "../../../Models/Loc"; | ||||||
| import TileHierarchy from "./TileHierarchy"; | import TileHierarchy from "./TileHierarchy"; | ||||||
|  | import {Tiles} from "../../../Models/TileRange"; | ||||||
| 
 | 
 | ||||||
| /*** | /*** | ||||||
|  * A tiled source which dynamically loads the required tiles at a fixed zoom level |  * A tiled source which dynamically loads the required tiles at a fixed zoom level | ||||||
|  | @ -46,9 +47,9 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor | ||||||
|                     // We'll retry later
 |                     // We'll retry later
 | ||||||
|                     return undefined |                     return undefined | ||||||
|                 } |                 } | ||||||
|                 const tileRange = Utils.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) |                 const tileRange = Tiles.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) | ||||||
| 
 | 
 | ||||||
|                 const needed = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i)) |                 const needed = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i)) | ||||||
|                 if (needed.length === 0) { |                 if (needed.length === 0) { | ||||||
|                     return undefined |                     return undefined | ||||||
|                 } |                 } | ||||||
|  | @ -63,7 +64,7 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor | ||||||
|             } |             } | ||||||
|             for (const neededIndex of neededIndexes) { |             for (const neededIndex of neededIndexes) { | ||||||
|                 self._loadedTiles.add(neededIndex) |                 self._loadedTiles.add(neededIndex) | ||||||
|                 const src = constructTile( Utils.tile_from_index(neededIndex)) |                 const src = constructTile(Tiles.tile_from_index(neededIndex)) | ||||||
|                 if(src !== undefined){ |                 if(src !== undefined){ | ||||||
|                     self.loadedTiles.set(neededIndex, src) |                     self.loadedTiles.set(neededIndex, src) | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import FilteredLayer from "../../../Models/FilteredLayer"; | ||||||
| import {Utils} from "../../../Utils"; | import {Utils} from "../../../Utils"; | ||||||
| import {BBox} from "../../GeoOperations"; | import {BBox} from "../../GeoOperations"; | ||||||
| import FeatureSourceMerger from "../Sources/FeatureSourceMerger"; | import FeatureSourceMerger from "../Sources/FeatureSourceMerger"; | ||||||
|  | import {Tiles} from "../../../Models/TileRange"; | ||||||
| 
 | 
 | ||||||
| export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> { | export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> { | ||||||
|     public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>(); |     public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>(); | ||||||
|  | @ -13,7 +14,7 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer | ||||||
|     public readonly layer: FilteredLayer; |     public readonly layer: FilteredLayer; | ||||||
|     private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void; |     private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void; | ||||||
| 
 | 
 | ||||||
|     constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void) { |     constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, index: number) => void) { | ||||||
|         this.layer = layer; |         this.layer = layer; | ||||||
|         this._handleTile = handleTile; |         this._handleTile = handleTile; | ||||||
|     } |     } | ||||||
|  | @ -37,7 +38,7 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer | ||||||
|         // We have to setup
 |         // We have to setup
 | ||||||
|         const sources = new UIEventSource<FeatureSource[]>([src]) |         const sources = new UIEventSource<FeatureSource[]>([src]) | ||||||
|         this.sources.set(index, sources) |         this.sources.set(index, sources) | ||||||
|         const merger = new FeatureSourceMerger(this.layer, index, BBox.fromTile(...Utils.tile_from_index(index)), sources) |         const merger = new FeatureSourceMerger(this.layer, index, BBox.fromTile(...Tiles.tile_from_index(index)), sources) | ||||||
|         this.loadedTiles.set(index, merger) |         this.loadedTiles.set(index, merger) | ||||||
|         this._handleTile(merger, index) |         this._handleTile(merger, index) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import {Utils} from "../../../Utils"; | ||||||
| import {BBox} from "../../GeoOperations"; | import {BBox} from "../../GeoOperations"; | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer"; | ||||||
| import TileHierarchy from "./TileHierarchy"; | import TileHierarchy from "./TileHierarchy"; | ||||||
| import {feature} from "@turf/turf"; | import {Tiles} from "../../../Models/TileRange"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Contains all features in a tiled fashion. |  * Contains all features in a tiled fashion. | ||||||
|  | @ -41,12 +41,12 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | ||||||
|         this.x = x; |         this.x = x; | ||||||
|         this.y = y; |         this.y = y; | ||||||
|         this.bbox = BBox.fromTile(z, x, y) |         this.bbox = BBox.fromTile(z, x, y) | ||||||
|         this.tileIndex = Utils.tile_index(z, x, y) |         this.tileIndex = Tiles.tile_index(z, x, y) | ||||||
|         this.name = `TiledFeatureSource(${z},${x},${y})` |         this.name = `TiledFeatureSource(${z},${x},${y})` | ||||||
|         this.parent = parent; |         this.parent = parent; | ||||||
|         this.layer = options.layer |         this.layer = options.layer | ||||||
|         options = options ?? {} |         options = options ?? {} | ||||||
|         this.maxFeatureCount = options?.maxFeatureCount ?? 500; |         this.maxFeatureCount = options?.maxFeatureCount ?? 250; | ||||||
|         this.maxzoom = options.maxZoomLevel ?? 18 |         this.maxzoom = options.maxZoomLevel ?? 18 | ||||||
|         this.options = options; |         this.options = options; | ||||||
|         if (parent === undefined) { |         if (parent === undefined) { | ||||||
|  | @ -61,7 +61,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | ||||||
|         } else { |         } else { | ||||||
|             this.root = this.parent.root; |             this.root = this.parent.root; | ||||||
|             this.loadedTiles = this.root.loadedTiles; |             this.loadedTiles = this.root.loadedTiles; | ||||||
|             const i = Utils.tile_index(z, x, y) |             const i = Tiles.tile_index(z, x, y) | ||||||
|             this.root.loadedTiles.set(i, this) |             this.root.loadedTiles.set(i, this) | ||||||
|         } |         } | ||||||
|         this.features = new UIEventSource<any[]>([]) |         this.features = new UIEventSource<any[]>([]) | ||||||
|  | @ -143,9 +143,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | ||||||
| 
 | 
 | ||||||
|         for (const feature of features) { |         for (const feature of features) { | ||||||
|             const bbox = BBox.get(feature.feature) |             const bbox = BBox.get(feature.feature) | ||||||
|             if (this.options.minZoomLevel === undefined) { |             if (this.options.dontEnforceMinZoom || this.options.minZoomLevel === undefined) { | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                 if (bbox.isContainedIn(this.upper_left.bbox)) { |                 if (bbox.isContainedIn(this.upper_left.bbox)) { | ||||||
|                     ulf.push(feature) |                     ulf.push(feature) | ||||||
|                 } else if (bbox.isContainedIn(this.upper_right.bbox)) { |                 } else if (bbox.isContainedIn(this.upper_right.bbox)) { | ||||||
|  | @ -186,6 +184,11 @@ export interface TiledFeatureSourceOptions { | ||||||
|     readonly maxFeatureCount?: number, |     readonly maxFeatureCount?: number, | ||||||
|     readonly maxZoomLevel?: number, |     readonly maxZoomLevel?: number, | ||||||
|     readonly minZoomLevel?: number, |     readonly minZoomLevel?: number, | ||||||
|  |     /** | ||||||
|  |      * IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated. | ||||||
|  |      * Setting 'dontEnforceMinZoomLevel' will still allow bigger zoom levels for those features | ||||||
|  |      */ | ||||||
|  |     readonly dontEnforceMinZoom?: boolean, | ||||||
|     readonly registerTile?: (tile: TiledFeatureSource & Tiled) => void, |     readonly registerTile?: (tile: TiledFeatureSource & Tiled) => void, | ||||||
|     readonly layer?: FilteredLayer |     readonly layer?: FilteredLayer | ||||||
| } | } | ||||||
|  | @ -6,6 +6,7 @@ import TileHierarchy from "./TileHierarchy"; | ||||||
| import {Utils} from "../../../Utils"; | import {Utils} from "../../../Utils"; | ||||||
| import SaveTileToLocalStorageActor from "../Actors/SaveTileToLocalStorageActor"; | import SaveTileToLocalStorageActor from "../Actors/SaveTileToLocalStorageActor"; | ||||||
| import {BBox} from "../../GeoOperations"; | import {BBox} from "../../GeoOperations"; | ||||||
|  | import {Tiles} from "../../../Models/TileRange"; | ||||||
| 
 | 
 | ||||||
| export default class TiledFromLocalStorageSource implements TileHierarchy<FeatureSourceForLayer & Tiled> { | export default class TiledFromLocalStorageSource implements TileHierarchy<FeatureSourceForLayer & Tiled> { | ||||||
|     public loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>(); |     public loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>(); | ||||||
|  | @ -17,6 +18,7 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur | ||||||
|                     leafletMap: any |                     leafletMap: any | ||||||
|                 }) { |                 }) { | ||||||
| 
 | 
 | ||||||
|  |         const undefinedTiles = new Set<number>() | ||||||
|         const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-" |         const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-" | ||||||
|         // @ts-ignore
 |         // @ts-ignore
 | ||||||
|         const indexes: number[] = Object.keys(localStorage) |         const indexes: number[] = Object.keys(localStorage) | ||||||
|  | @ -27,7 +29,7 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur | ||||||
|                 return Number(key.substring(prefix.length)); |                 return Number(key.substring(prefix.length)); | ||||||
|             }) |             }) | ||||||
| 
 | 
 | ||||||
|         console.log("Layer", layer.layerDef.id, "has following tiles in available in localstorage", indexes.map(i => Utils.tile_from_index(i).join("/")).join(", ")) |         console.log("Layer", layer.layerDef.id, "has following tiles in available in localstorage", indexes.map(i => Tiles.tile_from_index(i).join("/")).join(", ")) | ||||||
| 
 | 
 | ||||||
|         const zLevels = indexes.map(i => i % 100) |         const zLevels = indexes.map(i => i % 100) | ||||||
|         const indexesSet = new Set(indexes) |         const indexesSet = new Set(indexes) | ||||||
|  | @ -57,9 +59,9 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur | ||||||
|                 const needed = [] |                 const needed = [] | ||||||
|                 for (let z = minZoom; z <= maxZoom; z++) { |                 for (let z = minZoom; z <= maxZoom; z++) { | ||||||
| 
 | 
 | ||||||
|                     const tileRange = Utils.TileRangeBetween(z, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) |                     const tileRange = Tiles.TileRangeBetween(z, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) | ||||||
|                     const neededZ = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(z, x, y)) |                     const neededZ = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(z, x, y)) | ||||||
|                         .filter(i => !self.loadedTiles.has(i) && indexesSet.has(i)) |                         .filter(i => !self.loadedTiles.has(i) && !undefinedTiles.has(i) && indexesSet.has(i)) | ||||||
|                     needed.push(...neededZ) |                     needed.push(...neededZ) | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|  | @ -84,12 +86,13 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur | ||||||
|                         features: new UIEventSource<{ feature: any; freshness: Date }[]>(features), |                         features: new UIEventSource<{ feature: any; freshness: Date }[]>(features), | ||||||
|                         name: "FromLocalStorage(" + key + ")", |                         name: "FromLocalStorage(" + key + ")", | ||||||
|                         tileIndex: neededIndex, |                         tileIndex: neededIndex, | ||||||
|                         bbox: BBox.fromTile(...Utils.tile_from_index(neededIndex)) |                         bbox: BBox.fromTileIndex(neededIndex) | ||||||
|                     } |                     } | ||||||
|                     handleFeatureSource(src, neededIndex) |                     handleFeatureSource(src, neededIndex) | ||||||
|                     self.loadedTiles.set(neededIndex, src) |                     self.loadedTiles.set(neededIndex, src) | ||||||
|                 } catch (e) { |                 } catch (e) { | ||||||
|                     console.error("Could not load data tile from local storage due to", e) |                     console.error("Could not load data tile from local storage due to", e) | ||||||
|  |                     undefinedTiles.add(neededIndex) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import * as turf from '@turf/turf' | import * as turf from '@turf/turf' | ||||||
| import {Utils} from "../Utils"; | import {Utils} from "../Utils"; | ||||||
|  | import {Tiles} from "../Models/TileRange"; | ||||||
| 
 | 
 | ||||||
| export class GeoOperations { | export class GeoOperations { | ||||||
| 
 | 
 | ||||||
|  | @ -8,7 +9,7 @@ export class GeoOperations { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Converts a GeoJSon feature to a point feature |      * Converts a GeoJson feature to a point GeoJson feature | ||||||
|      * @param feature |      * @param feature | ||||||
|      */ |      */ | ||||||
|     static centerpoint(feature: any) { |     static centerpoint(feature: any) { | ||||||
|  | @ -451,8 +452,12 @@ export class BBox { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static fromTile(z: number, x: number, y: number) { |     static fromTile(z: number, x: number, y: number): BBox { | ||||||
|         return new BBox(Utils.tile_bounds_lon_lat(z, x, y)) |         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() { |     getEast() { | ||||||
|  |  | ||||||
|  | @ -12,8 +12,11 @@ export default abstract class ImageAttributionSource { | ||||||
|         if (cached !== undefined) { |         if (cached !== undefined) { | ||||||
|             return cached; |             return cached; | ||||||
|         } |         } | ||||||
|         const src = this.DownloadAttribution(url) |         const src = new UIEventSource(undefined) | ||||||
|         this._cache.set(url, src) |         this._cache.set(url, src) | ||||||
|  |         this.DownloadAttribution(url).then(license => | ||||||
|  |             src.setData(license)) | ||||||
|  |             .catch(e => console.error("Could not download license information for ", url, " due to", e)) | ||||||
|         return src; |         return src; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -21,10 +24,10 @@ export default abstract class ImageAttributionSource { | ||||||
|     public abstract SourceIcon(backlinkSource?: string): BaseUIElement; |     public abstract SourceIcon(backlinkSource?: string): BaseUIElement; | ||||||
| 
 | 
 | ||||||
|     /*Converts a value to a URL. Can return null if not applicable*/ |     /*Converts a value to a URL. Can return null if not applicable*/ | ||||||
|     public PrepareUrl(value: string): string | UIEventSource<string>{ |     public PrepareUrl(value: string): string | UIEventSource<string> { | ||||||
|         return value; |         return value; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected abstract DownloadAttribution(url: string): UIEventSource<LicenseInfo>; |     protected abstract DownloadAttribution(url: string): Promise<LicenseInfo>; | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -2,8 +2,9 @@ | ||||||
| import $ from "jquery" | import $ from "jquery" | ||||||
| import {LicenseInfo} from "./Wikimedia"; | import {LicenseInfo} from "./Wikimedia"; | ||||||
| import ImageAttributionSource from "./ImageAttributionSource"; | import ImageAttributionSource from "./ImageAttributionSource"; | ||||||
| import {UIEventSource} from "../UIEventSource"; |  | ||||||
| import BaseUIElement from "../../UI/BaseUIElement"; | import BaseUIElement from "../../UI/BaseUIElement"; | ||||||
|  | import {Utils} from "../../Utils"; | ||||||
|  | import Constants from "../../Models/Constants"; | ||||||
| 
 | 
 | ||||||
| export class Imgur extends ImageAttributionSource { | export class Imgur extends ImageAttributionSource { | ||||||
| 
 | 
 | ||||||
|  | @ -86,35 +87,18 @@ export class Imgur extends ImageAttributionSource { | ||||||
|         return undefined; |         return undefined; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected DownloadAttribution(url: string): UIEventSource<LicenseInfo> { |     protected async DownloadAttribution(url: string): Promise<LicenseInfo> { | ||||||
|         const src = new UIEventSource<LicenseInfo>(undefined) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0]; |         const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0]; | ||||||
| 
 | 
 | ||||||
|         const apiUrl = 'https://api.imgur.com/3/image/' + hash; |         const apiUrl = 'https://api.imgur.com/3/image/' + hash; | ||||||
|         const apiKey = '7070e7167f0a25a'; |         const response = await Utils.downloadJson(apiUrl, {Authorization: 'Client-ID ' + Constants.ImgurApiKey}) | ||||||
| 
 | 
 | ||||||
|         const settings = { |  | ||||||
|             async: true, |  | ||||||
|             crossDomain: true, |  | ||||||
|             processData: false, |  | ||||||
|             contentType: false, |  | ||||||
|             type: 'GET', |  | ||||||
|             url: apiUrl, |  | ||||||
|             headers: { |  | ||||||
|                 Authorization: 'Client-ID ' + apiKey, |  | ||||||
|                 Accept: 'application/json', |  | ||||||
|             }, |  | ||||||
|         }; |  | ||||||
|         // @ts-ignore
 |  | ||||||
|         $.ajax(settings).done(function (response) { |  | ||||||
|         const descr: string = response.data.description ?? ""; |         const descr: string = response.data.description ?? ""; | ||||||
|         const data: any = {}; |         const data: any = {}; | ||||||
|         for (const tag of descr.split("\n")) { |         for (const tag of descr.split("\n")) { | ||||||
|             const kv = tag.split(":"); |             const kv = tag.split(":"); | ||||||
|             const k = kv[0]; |             const k = kv[0]; | ||||||
|                 data[k] = kv[1].replace("\r", ""); |             data[k] = kv[1]?.replace("\r", ""); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -123,13 +107,7 @@ export class Imgur extends ImageAttributionSource { | ||||||
|         licenseInfo.licenseShortName = data.license; |         licenseInfo.licenseShortName = data.license; | ||||||
|         licenseInfo.artist = data.author; |         licenseInfo.artist = data.author; | ||||||
| 
 | 
 | ||||||
|             src.setData(licenseInfo) |         return licenseInfo | ||||||
| 
 |  | ||||||
|         }).fail((reason) => { |  | ||||||
|             console.log("Getting metadata from to IMGUR failed", reason) |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         return src; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -24,7 +24,7 @@ export class Mapillary extends ImageAttributionSource { | ||||||
|     } { |     } { | ||||||
|         if (value.startsWith("https://a.mapillary.com")) { |         if (value.startsWith("https://a.mapillary.com")) { | ||||||
|             const key = value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1) |             const key = value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1) | ||||||
|             return {key:key, isApiv4: !isNaN(Number(key))}; |             return {key: key, isApiv4: !isNaN(Number(key))}; | ||||||
|         } |         } | ||||||
|         const newApiFormat = value.match(/https?:\/\/www.mapillary.com\/app\/\?pKey=([0-9]*)/) |         const newApiFormat = value.match(/https?:\/\/www.mapillary.com\/app\/\?pKey=([0-9]*)/) | ||||||
|         if (newApiFormat !== null) { |         if (newApiFormat !== null) { | ||||||
|  | @ -32,9 +32,9 @@ export class Mapillary extends ImageAttributionSource { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const mapview = value.match(/https?:\/\/www.mapillary.com\/map\/im\/(.*)/) |         const mapview = value.match(/https?:\/\/www.mapillary.com\/map\/im\/(.*)/) | ||||||
|         if(mapview !== null){ |         if (mapview !== null) { | ||||||
|             const key = mapview[1] |             const key = mapview[1] | ||||||
|             return {key:key, isApiv4: !isNaN(Number(key))}; |             return {key: key, isApiv4: !isNaN(Number(key))}; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -62,11 +62,11 @@ export class Mapillary extends ImageAttributionSource { | ||||||
|             return `https://images.mapillary.com/${keyV.key}/thumb-640.jpg?client_id=${Mapillary.client_token_v3}` |             return `https://images.mapillary.com/${keyV.key}/thumb-640.jpg?client_id=${Mapillary.client_token_v3}` | ||||||
|         } else { |         } else { | ||||||
|             const key = keyV.key; |             const key = keyV.key; | ||||||
|             if(Mapillary.v4_cached_urls.has(key)){ |             if (Mapillary.v4_cached_urls.has(key)) { | ||||||
|                 return Mapillary.v4_cached_urls.get(key) |                 return Mapillary.v4_cached_urls.get(key) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const metadataUrl ='https://graph.mapillary.com/' + key + '?fields=thumb_1024_url&&access_token=' + Mapillary.client_token_v4; |             const metadataUrl = 'https://graph.mapillary.com/' + key + '?fields=thumb_1024_url&&access_token=' + Mapillary.client_token_v4; | ||||||
|             const source = new UIEventSource<string>(undefined) |             const source = new UIEventSource<string>(undefined) | ||||||
|             Mapillary.v4_cached_urls.set(key, source) |             Mapillary.v4_cached_urls.set(key, source) | ||||||
|             Utils.downloadJson(metadataUrl).then( |             Utils.downloadJson(metadataUrl).then( | ||||||
|  | @ -79,31 +79,28 @@ export class Mapillary extends ImageAttributionSource { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected DownloadAttribution(url: string): UIEventSource<LicenseInfo> { |     protected async DownloadAttribution(url: string): Promise<LicenseInfo> { | ||||||
| 
 | 
 | ||||||
|         const keyV = Mapillary.ExtractKeyFromURL(url) |         const keyV = Mapillary.ExtractKeyFromURL(url) | ||||||
|         if(keyV.isApiv4){ |         if (keyV.isApiv4) { | ||||||
|             const license = new LicenseInfo() |             const license = new LicenseInfo() | ||||||
|             license.artist = "Contributor name unavailable"; |             license.artist = "Contributor name unavailable"; | ||||||
|             license.license = "CC BY-SA 4.0"; |             license.license = "CC BY-SA 4.0"; | ||||||
|             // license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
 |             // license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
 | ||||||
|             license.attributionRequired = true; |             license.attributionRequired = true; | ||||||
|             return new UIEventSource<LicenseInfo>(license) |             return license | ||||||
| 
 | 
 | ||||||
|         } |         } | ||||||
|         const key = keyV.key |         const key = keyV.key | ||||||
| 
 | 
 | ||||||
|         const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2` |         const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2` | ||||||
|         const source = new UIEventSource<LicenseInfo>(undefined) |         const data = await Utils.downloadJson(metadataURL) | ||||||
|         Utils.downloadJson(metadataURL).then(data => { |  | ||||||
|         const license = new LicenseInfo(); |         const license = new LicenseInfo(); | ||||||
|         license.artist = data.properties?.username; |         license.artist = data.properties?.username; | ||||||
|         license.licenseShortName = "CC BY-SA 4.0"; |         license.licenseShortName = "CC BY-SA 4.0"; | ||||||
|         license.license = "Creative Commons Attribution-ShareAlike 4.0 International License"; |         license.license = "Creative Commons Attribution-ShareAlike 4.0 International License"; | ||||||
|         license.attributionRequired = true; |         license.attributionRequired = true; | ||||||
|             source.setData(license); |  | ||||||
|         }) |  | ||||||
| 
 | 
 | ||||||
|         return source |         return license | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| import ImageAttributionSource from "./ImageAttributionSource"; | import ImageAttributionSource from "./ImageAttributionSource"; | ||||||
| import BaseUIElement from "../../UI/BaseUIElement"; | import BaseUIElement from "../../UI/BaseUIElement"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| import {UIEventSource} from "../UIEventSource"; |  | ||||||
| import Link from "../../UI/Base/Link"; | import Link from "../../UI/Base/Link"; | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
| 
 | 
 | ||||||
|  | @ -124,28 +123,23 @@ export class Wikimedia extends ImageAttributionSource { | ||||||
|             .replace(/'/g, '%27'); |             .replace(/'/g, '%27'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected DownloadAttribution(filename: string): UIEventSource<LicenseInfo> { |     protected async DownloadAttribution(filename: string): Promise<LicenseInfo> { | ||||||
| 
 |  | ||||||
|         const source = new UIEventSource<LicenseInfo>(undefined); |  | ||||||
| 
 |  | ||||||
|         filename = Wikimedia.ExtractFileName(filename) |         filename = Wikimedia.ExtractFileName(filename) | ||||||
| 
 | 
 | ||||||
|         if (filename === "") { |         if (filename === "") { | ||||||
|             return source; |             return undefined; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const url = "https://en.wikipedia.org/w/" + |         const url = "https://en.wikipedia.org/w/" + | ||||||
|             "api.php?action=query&prop=imageinfo&iiprop=extmetadata&" + |             "api.php?action=query&prop=imageinfo&iiprop=extmetadata&" + | ||||||
|             "titles=" + filename + |             "titles=" + filename + | ||||||
|             "&format=json&origin=*"; |             "&format=json&origin=*"; | ||||||
|         Utils.downloadJson(url).then( |         const data = await Utils.downloadJson(url) | ||||||
|             data => { |  | ||||||
|         const licenseInfo = new LicenseInfo(); |         const licenseInfo = new LicenseInfo(); | ||||||
|         const license = (data.query.pages[-1].imageinfo ?? [])[0]?.extmetadata; |         const license = (data.query.pages[-1].imageinfo ?? [])[0]?.extmetadata; | ||||||
|         if (license === undefined) { |         if (license === undefined) { | ||||||
|             console.error("This file has no usable metedata or license attached... Please fix the license info file yourself!") |             console.error("This file has no usable metedata or license attached... Please fix the license info file yourself!") | ||||||
|                     source.setData(null) |             return undefined; | ||||||
|                     return; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         licenseInfo.artist = license.Artist?.value; |         licenseInfo.artist = license.Artist?.value; | ||||||
|  | @ -156,11 +150,7 @@ export class Wikimedia extends ImageAttributionSource { | ||||||
|         licenseInfo.licenseShortName = license.LicenseShortName?.value; |         licenseInfo.licenseShortName = license.LicenseShortName?.value; | ||||||
|         licenseInfo.credit = license.Credit?.value; |         licenseInfo.credit = license.Credit?.value; | ||||||
|         licenseInfo.description = license.ImageDescription?.value; |         licenseInfo.description = license.ImageDescription?.value; | ||||||
|                 source.setData(licenseInfo); |         return licenseInfo; | ||||||
|             } |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         return source; |  | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ import SimpleMetaTagger from "./SimpleMetaTagger"; | ||||||
| import {ExtraFuncParams, ExtraFunction} from "./ExtraFunction"; | import {ExtraFuncParams, ExtraFunction} from "./ExtraFunction"; | ||||||
| import {UIEventSource} from "./UIEventSource"; | import {UIEventSource} from "./UIEventSource"; | ||||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||||
|  | import State from "../State"; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -31,39 +32,57 @@ export default class MetaTagging { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         for (const metatag of SimpleMetaTagger.metatags) { |  | ||||||
| 
 | 
 | ||||||
|             try { |             const metatagsToApply: SimpleMetaTagger [] = [] | ||||||
|  |             for (const metatag of SimpleMetaTagger.metatags) { | ||||||
|                 if (metatag.includesDates) { |                 if (metatag.includesDates) { | ||||||
|                     if (options.includeDates ?? true) { |                     if (options.includeDates ?? true) { | ||||||
|                         metatag.addMetaTags(features); |                         metatagsToApply.push(metatag) | ||||||
|                     } |                     } | ||||||
|                 } else { |                 } else { | ||||||
|                     if (options.includeNonDates ?? true) { |                     if (options.includeNonDates ?? true) { | ||||||
|                         metatag.addMetaTags(features); |                         metatagsToApply.push(metatag) | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |         // The calculated functions - per layer - which add the new keys
 | ||||||
|  |         const layerFuncs = this.createRetaggingFunc(layer) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         for (let i = 0; i < features.length; i++) { | ||||||
|  |                 const ff = features[i]; | ||||||
|  |                 const feature = ff.feature | ||||||
|  |                 const freshness = ff.freshness | ||||||
|  |                 let somethingChanged = false | ||||||
|  |                 for (const metatag of metatagsToApply) { | ||||||
|  |                     try { | ||||||
|  |                         if(!metatag.keys.some(key => feature.properties[key] === undefined)){ | ||||||
|  |                             // All keys are already defined, we probably already ran this one
 | ||||||
|  |                             continue | ||||||
|  |                         } | ||||||
|  |                         somethingChanged = somethingChanged || metatag.applyMetaTagsOnFeature(feature, freshness) | ||||||
|                     } catch (e) { |                     } catch (e) { | ||||||
|                         console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e) |                         console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e) | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                  |                  | ||||||
|         // The functions - per layer - which add the new keys
 |                 if(layerFuncs !== undefined){ | ||||||
|         const layerFuncs = this.createRetaggingFunc(layer) |  | ||||||
| 
 |  | ||||||
|         if (layerFuncs !== undefined) { |  | ||||||
|             for (const feature of features) { |  | ||||||
| 
 |  | ||||||
|                     try { |                     try { | ||||||
|                     layerFuncs(params, feature.feature) |                         layerFuncs(params, feature) | ||||||
|                     } catch (e) { |                     } catch (e) { | ||||||
|                         console.error(e) |                         console.error(e) | ||||||
|                     } |                     } | ||||||
|  |                     somethingChanged = true | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 if(somethingChanged){ | ||||||
|  |                     State.state.allElements.getEventSourceById(feature.properties.id).ping() | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|     private static createRetaggingFunc(layer: LayerConfig): |     private static createRetaggingFunc(layer: LayerConfig): | ||||||
|         ((params: ExtraFuncParams, feature: any) => void) { |         ((params: ExtraFuncParams, feature: any) => void) { | ||||||
|         const calculatedTags: [string, string][] = layer.calculatedTags; |         const calculatedTags: [string, string][] = layer.calculatedTags; | ||||||
|  | @ -92,11 +111,13 @@ export default class MetaTagging { | ||||||
|                                     d = JSON.stringify(d); |                                     d = JSON.stringify(d); | ||||||
|                                 } |                                 } | ||||||
|                                 feature.properties[key] = d; |                                 feature.properties[key] = d; | ||||||
|  |                                 console.log("Written a delayed calculated tag onto ", feature.properties.id, ": ", key, ":==", d) | ||||||
|                             }) |                             }) | ||||||
|                             result = result.data |                             result = result.data | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
|                         if (result === undefined || result === "") { |                         if (result === undefined || result === "") { | ||||||
|  |                             console.log("Calculated tag for", key, "gave undefined", feature.properties.id) | ||||||
|                             return; |                             return; | ||||||
|                         } |                         } | ||||||
|                         if (typeof result !== "string") { |                         if (typeof result !== "string") { | ||||||
|  | @ -104,6 +125,7 @@ export default class MetaTagging { | ||||||
|                             result = JSON.stringify(result); |                             result = JSON.stringify(result); | ||||||
|                         } |                         } | ||||||
|                         feature.properties[key] = result; |                         feature.properties[key] = result; | ||||||
|  |                         console.log("Written a calculated tag onto ", feature.properties.id, ": ", key, ":==", result) | ||||||
|                     } catch (e) { |                     } catch (e) { | ||||||
|                         if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { |                         if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { | ||||||
|                             console.warn("Could not calculate a calculated tag defined by " + code + " due to " + e + ". This is code defined in the theme. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e) |                             console.warn("Could not calculate a calculated tag defined by " + code + " due to " + e + ". This is code defined in the theme. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e) | ||||||
|  |  | ||||||
|  | @ -94,6 +94,7 @@ export class OsmConnection { | ||||||
|                 self.AttemptLogin() |                 self.AttemptLogin() | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  |         this.isLoggedIn.addCallbackAndRunD(li => console.log("User is logged in!", li)) | ||||||
|         this._dryRun = dryRun; |         this._dryRun = dryRun; | ||||||
| 
 | 
 | ||||||
|         this.updateAuthObject(); |         this.updateAuthObject(); | ||||||
|  |  | ||||||
|  | @ -31,7 +31,7 @@ export default class SimpleMetaTagger { | ||||||
|                 "_version_number"], |                 "_version_number"], | ||||||
|             doc: "Information about the last edit of this object." |             doc: "Information about the last edit of this object." | ||||||
|         }, |         }, | ||||||
|         (feature) => {/*Note: also handled by 'UpdateTagsFromOsmAPI'*/ |         (feature) => {/*Note: also called by 'UpdateTagsFromOsmAPI'*/ | ||||||
| 
 | 
 | ||||||
|             const tgs = feature.properties; |             const tgs = feature.properties; | ||||||
| 
 | 
 | ||||||
|  | @ -48,6 +48,7 @@ export default class SimpleMetaTagger { | ||||||
|             move("changeset", "_last_edit:changeset") |             move("changeset", "_last_edit:changeset") | ||||||
|             move("timestamp", "_last_edit:timestamp") |             move("timestamp", "_last_edit:timestamp") | ||||||
|             move("version", "_version_number") |             move("version", "_version_number") | ||||||
|  |             return true; | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     private static latlon = new SimpleMetaTagger({ |     private static latlon = new SimpleMetaTagger({ | ||||||
|  | @ -62,6 +63,7 @@ export default class SimpleMetaTagger { | ||||||
|             feature.properties["_lon"] = "" + lon; |             feature.properties["_lon"] = "" + lon; | ||||||
|             feature._lon = lon; // This is dirty, I know
 |             feature._lon = lon; // This is dirty, I know
 | ||||||
|             feature._lat = lat; |             feature._lat = lat; | ||||||
|  |             return true; | ||||||
|         }) |         }) | ||||||
|     ); |     ); | ||||||
|     private static surfaceArea = new SimpleMetaTagger( |     private static surfaceArea = new SimpleMetaTagger( | ||||||
|  | @ -74,6 +76,7 @@ export default class SimpleMetaTagger { | ||||||
|             feature.properties["_surface"] = "" + sqMeters; |             feature.properties["_surface"] = "" + sqMeters; | ||||||
|             feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10; |             feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10; | ||||||
|             feature.area = sqMeters; |             feature.area = sqMeters; | ||||||
|  |             return true; | ||||||
|         }) |         }) | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|  | @ -118,9 +121,7 @@ export default class SimpleMetaTagger { | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|             } |             } | ||||||
|             if (rewritten) { |             return rewritten | ||||||
|                 State.state.allElements.getEventSourceById(feature.id).ping(); |  | ||||||
|             } |  | ||||||
|         }) |         }) | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  | @ -135,6 +136,7 @@ export default class SimpleMetaTagger { | ||||||
|             const km = Math.floor(l / 1000) |             const km = Math.floor(l / 1000) | ||||||
|             const kmRest = Math.round((l - km * 1000) / 100) |             const kmRest = Math.round((l - km * 1000) / 100) | ||||||
|             feature.properties["_length:km"] = "" + km + "." + kmRest |             feature.properties["_length:km"] = "" + km + "." + kmRest | ||||||
|  |             return true; | ||||||
|         }) |         }) | ||||||
|     ) |     ) | ||||||
|     private static country = new SimpleMetaTagger( |     private static country = new SimpleMetaTagger( | ||||||
|  | @ -144,7 +146,6 @@ export default class SimpleMetaTagger { | ||||||
|         }, |         }, | ||||||
|         feature => { |         feature => { | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|             let centerPoint: any = GeoOperations.centerpoint(feature); |             let centerPoint: any = GeoOperations.centerpoint(feature); | ||||||
|             const lat = centerPoint.geometry.coordinates[1]; |             const lat = centerPoint.geometry.coordinates[1]; | ||||||
|             const lon = centerPoint.geometry.coordinates[0]; |             const lon = centerPoint.geometry.coordinates[0]; | ||||||
|  | @ -157,11 +158,11 @@ export default class SimpleMetaTagger { | ||||||
|                         const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); |                         const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); | ||||||
|                         tagsSource.ping(); |                         tagsSource.ping(); | ||||||
|                     } |                     } | ||||||
| 
 |  | ||||||
|                 } catch (e) { |                 } catch (e) { | ||||||
|                     console.warn(e) |                     console.warn(e) | ||||||
|                 } |                 } | ||||||
|             }) |             }) | ||||||
|  |             return false; | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     private static isOpen = new SimpleMetaTagger( |     private static isOpen = new SimpleMetaTagger( | ||||||
|  | @ -174,7 +175,7 @@ export default class SimpleMetaTagger { | ||||||
|             if (Utils.runningFromConsole) { |             if (Utils.runningFromConsole) { | ||||||
|                 // We are running from console, thus probably creating a cache
 |                 // We are running from console, thus probably creating a cache
 | ||||||
|                 // isOpen is irrelevant
 |                 // isOpen is irrelevant
 | ||||||
|                 return |                 return false | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); |             const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); | ||||||
|  | @ -199,7 +200,7 @@ export default class SimpleMetaTagger { | ||||||
|                         if (oldNextChange > (new Date()).getTime() && |                         if (oldNextChange > (new Date()).getTime() && | ||||||
|                             tags["_isOpen:oldvalue"] === tags["opening_hours"]) { |                             tags["_isOpen:oldvalue"] === tags["opening_hours"]) { | ||||||
|                             // Already calculated and should not yet be triggered
 |                             // Already calculated and should not yet be triggered
 | ||||||
|                             return; |                             return false; | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
|                         tags["_isOpen"] = oh.getState() ? "yes" : "no"; |                         tags["_isOpen"] = oh.getState() ? "yes" : "no"; | ||||||
|  | @ -227,6 +228,7 @@ export default class SimpleMetaTagger { | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                     updateTags(); |                     updateTags(); | ||||||
|  |                     return true; | ||||||
|                 } catch (e) { |                 } catch (e) { | ||||||
|                     console.warn("Error while parsing opening hours of ", tags.id, e); |                     console.warn("Error while parsing opening hours of ", tags.id, e); | ||||||
|                     tags["_isOpen"] = "parse_error"; |                     tags["_isOpen"] = "parse_error"; | ||||||
|  | @ -244,11 +246,11 @@ export default class SimpleMetaTagger { | ||||||
|             const tags = feature.properties; |             const tags = feature.properties; | ||||||
|             const direction = tags["camera:direction"] ?? tags["direction"]; |             const direction = tags["camera:direction"] ?? tags["direction"]; | ||||||
|             if (direction === undefined) { |             if (direction === undefined) { | ||||||
|                 return; |                 return false; | ||||||
|             } |             } | ||||||
|             const n = cardinalDirections[direction] ?? Number(direction); |             const n = cardinalDirections[direction] ?? Number(direction); | ||||||
|             if (isNaN(n)) { |             if (isNaN(n)) { | ||||||
|                 return; |                 return false; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // The % operator has range (-360, 360). We apply a trick to get [0, 360).
 |             // The % operator has range (-360, 360). We apply a trick to get [0, 360).
 | ||||||
|  | @ -256,7 +258,7 @@ export default class SimpleMetaTagger { | ||||||
| 
 | 
 | ||||||
|             tags["_direction:numerical"] = normalized; |             tags["_direction:numerical"] = normalized; | ||||||
|             tags["_direction:leftright"] = normalized <= 180 ? "right" : "left"; |             tags["_direction:leftright"] = normalized <= 180 ? "right" : "left"; | ||||||
| 
 |             return true; | ||||||
|         }) |         }) | ||||||
|     ) |     ) | ||||||
|     private static carriageWayWidth = new SimpleMetaTagger( |     private static carriageWayWidth = new SimpleMetaTagger( | ||||||
|  | @ -268,7 +270,7 @@ export default class SimpleMetaTagger { | ||||||
| 
 | 
 | ||||||
|             const properties = feature.properties; |             const properties = feature.properties; | ||||||
|             if (properties["width:carriageway"] === undefined) { |             if (properties["width:carriageway"] === undefined) { | ||||||
|                 return; |                 return false; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const carWidth = 2; |             const carWidth = 2; | ||||||
|  | @ -366,7 +368,7 @@ export default class SimpleMetaTagger { | ||||||
| 
 | 
 | ||||||
|             properties["_width:difference"] = Utils.Round(targetWidth - width); |             properties["_width:difference"] = Utils.Round(targetWidth - width); | ||||||
|             properties["_width:difference:no_pedestrians"] = Utils.Round(targetWidthIgnoringPedestrians - width); |             properties["_width:difference:no_pedestrians"] = Utils.Round(targetWidthIgnoringPedestrians - width); | ||||||
| 
 |             return true; | ||||||
|         } |         } | ||||||
|     ); |     ); | ||||||
|     private static currentTime = new SimpleMetaTagger( |     private static currentTime = new SimpleMetaTagger( | ||||||
|  | @ -375,7 +377,7 @@ export default class SimpleMetaTagger { | ||||||
|             doc: "Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely", |             doc: "Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely", | ||||||
|             includesDates: true |             includesDates: true | ||||||
|         }, |         }, | ||||||
|         (feature, _, freshness) => { |         (feature, freshness) => { | ||||||
|             const now = new Date(); |             const now = new Date(); | ||||||
| 
 | 
 | ||||||
|             if (typeof freshness === "string") { |             if (typeof freshness === "string") { | ||||||
|  | @ -394,7 +396,7 @@ export default class SimpleMetaTagger { | ||||||
|             feature.properties["_now:datetime"] = datetime(now); |             feature.properties["_now:datetime"] = datetime(now); | ||||||
|             feature.properties["_loaded:date"] = date(freshness); |             feature.properties["_loaded:date"] = date(freshness); | ||||||
|             feature.properties["_loaded:datetime"] = datetime(freshness); |             feature.properties["_loaded:datetime"] = datetime(freshness); | ||||||
| 
 |             return true; | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     public static metatags = [ |     public static metatags = [ | ||||||
|  | @ -413,12 +415,18 @@ export default class SimpleMetaTagger { | ||||||
|     public readonly keys: string[]; |     public readonly keys: string[]; | ||||||
|     public readonly doc: string; |     public readonly doc: string; | ||||||
|     public readonly includesDates: boolean |     public readonly includesDates: boolean | ||||||
|     private readonly _f: (feature: any, index: number, freshness: Date) => void; |     public readonly applyMetaTagsOnFeature: (feature: any, freshness: Date) => boolean; | ||||||
| 
 | 
 | ||||||
|     constructor(docs: { keys: string[], doc: string, includesDates?: boolean }, f: ((feature: any, index: number, freshness: Date) => void)) { |     /*** | ||||||
|  |      * A function that adds some extra data to a feature | ||||||
|  |      * @param docs: what does this extra data do? | ||||||
|  |      * @param f: apply the changes. Returns true if something changed | ||||||
|  |      */ | ||||||
|  |     constructor(docs: { keys: string[], doc: string, includesDates?: boolean }, | ||||||
|  |                 f: ((feature: any, freshness: Date) => boolean)) { | ||||||
|         this.keys = docs.keys; |         this.keys = docs.keys; | ||||||
|         this.doc = docs.doc; |         this.doc = docs.doc; | ||||||
|         this._f = f; |         this.applyMetaTagsOnFeature = f; | ||||||
|         this.includesDates = docs.includesDates ?? false; |         this.includesDates = docs.includesDates ?? false; | ||||||
|         for (const key of docs.keys) { |         for (const key of docs.keys) { | ||||||
|             if (!key.startsWith('_') && key.toLowerCase().indexOf("theme") < 0) { |             if (!key.startsWith('_') && key.toLowerCase().indexOf("theme") < 0) { | ||||||
|  | @ -450,12 +458,4 @@ export default class SimpleMetaTagger { | ||||||
|         return new Combine(subElements).SetClass("flex-col") |         return new Combine(subElements).SetClass("flex-col") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public addMetaTags(features: { feature: any, freshness: Date }[]) { |  | ||||||
|         for (let i = 0; i < features.length; i++) { |  | ||||||
|             let feature = features[i]; |  | ||||||
|             this._f(feature.feature, i, feature.freshness); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ import {Utils} from "../Utils"; | ||||||
| export default class Constants { | export default class Constants { | ||||||
| 
 | 
 | ||||||
|     public static vNumber = "0.10.0-alpha-1"; |     public static vNumber = "0.10.0-alpha-1"; | ||||||
|  |     public static ImgurApiKey = '7070e7167f0a25a' | ||||||
| 
 | 
 | ||||||
|     // The user journey states thresholds when a new feature gets unlocked
 |     // The user journey states thresholds when a new feature gets unlocked
 | ||||||
|     public static userJourney = { |     public static userJourney = { | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ import FilterConfig from "./FilterConfig"; | ||||||
| import {Unit} from "../Unit"; | import {Unit} from "../Unit"; | ||||||
| import DeleteConfig from "./DeleteConfig"; | import DeleteConfig from "./DeleteConfig"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
|  | import Img from "../../UI/Base/Img"; | ||||||
| 
 | 
 | ||||||
| export default class LayerConfig { | export default class LayerConfig { | ||||||
|     static WAYHANDLING_DEFAULT = 0; |     static WAYHANDLING_DEFAULT = 0; | ||||||
|  | @ -495,19 +496,20 @@ export default class LayerConfig { | ||||||
|         const iconUrlStatic = render(this.icon); |         const iconUrlStatic = render(this.icon); | ||||||
|         const self = this; |         const self = this; | ||||||
| 
 | 
 | ||||||
|         function genHtmlFromString(sourcePart: string, rotation: string, style?: string): BaseUIElement { |         function genHtmlFromString(sourcePart: string, rotation: string): BaseUIElement { | ||||||
|             style = style ?? `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; |             const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; | ||||||
|             let html: BaseUIElement = new FixedUiElement( |             let html: BaseUIElement = new FixedUiElement( | ||||||
|                 `<img src="${sourcePart}" style="${style}" />` |                 `<img src="${sourcePart}" style="${style}" />` | ||||||
|             ); |             ); | ||||||
|             const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/); |             const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/); | ||||||
|             if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) { |             if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) { | ||||||
|                 html = new Combine([ |                 html = new Img( | ||||||
|                     (Svg.All[match[1] + ".svg"] as string).replace( |                     (Svg.All[match[1] + ".svg"] as string).replace( | ||||||
|                         /#000000/g, |                         /#000000/g, | ||||||
|                         match[2] |                         match[2] | ||||||
|                     ), |                     ), | ||||||
|                 ]).SetStyle(style); |                     true | ||||||
|  |                 ).SetStyle(style); | ||||||
|             } |             } | ||||||
|             return html; |             return html; | ||||||
|         } |         } | ||||||
|  | @ -540,7 +542,7 @@ export default class LayerConfig { | ||||||
|                         .filter((prt) => prt != ""); |                         .filter((prt) => prt != ""); | ||||||
| 
 | 
 | ||||||
|                     for (const badgePartStr of partDefs) { |                     for (const badgePartStr of partDefs) { | ||||||
|                         badgeParts.push(genHtmlFromString(badgePartStr, "0", `width:unset;height:100%;display:block;`)); |                         badgeParts.push(genHtmlFromString(badgePartStr, "0")); | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     const badgeCompound = new Combine(badgeParts).SetStyle( |                     const badgeCompound = new Combine(badgeParts).SetStyle( | ||||||
|  |  | ||||||
|  | @ -5,7 +5,6 @@ import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; | ||||||
| import AllKnownLayers from "../../Customizations/AllKnownLayers"; | import AllKnownLayers from "../../Customizations/AllKnownLayers"; | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
| import LayerConfig from "./LayerConfig"; | import LayerConfig from "./LayerConfig"; | ||||||
| import {Unit} from "../Unit"; |  | ||||||
| import {LayerConfigJson} from "./Json/LayerConfigJson"; | import {LayerConfigJson} from "./Json/LayerConfigJson"; | ||||||
| 
 | 
 | ||||||
| export default class LayoutConfig { | export default class LayoutConfig { | ||||||
|  | @ -87,6 +86,9 @@ export default class LayoutConfig { | ||||||
|         this.startZoom = json.startZoom; |         this.startZoom = json.startZoom; | ||||||
|         this.startLat = json.startLat; |         this.startLat = json.startLat; | ||||||
|         this.startLon = json.startLon; |         this.startLon = json.startLon; | ||||||
|  |         if(json.widenFactor < 1){ | ||||||
|  |             throw "Widenfactor too small" | ||||||
|  |         } | ||||||
|         this.widenFactor = json.widenFactor ?? 1.5; |         this.widenFactor = json.widenFactor ?? 1.5; | ||||||
|         this.roamingRenderings = (json.roamingRenderings ?? []).map((tr, i) => { |         this.roamingRenderings = (json.roamingRenderings ?? []).map((tr, i) => { | ||||||
|                 if (typeof tr === "string") { |                 if (typeof tr === "string") { | ||||||
|  |  | ||||||
|  | @ -6,3 +6,105 @@ export interface TileRange { | ||||||
|     total: number, |     total: number, | ||||||
|     zoomlevel: number |     zoomlevel: number | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export class Tiles { | ||||||
|  | 
 | ||||||
|  |     public static MapRange<T>(tileRange: TileRange, f: (x: number, y: number) => T): T[] { | ||||||
|  |         const result: T[] = [] | ||||||
|  |         for (let x = tileRange.xstart; x <= tileRange.xend; x++) { | ||||||
|  |             for (let y = tileRange.ystart; y <= tileRange.yend; y++) { | ||||||
|  |                 const t = f(x, y); | ||||||
|  |                 result.push(t) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     private static tile2long(x, z) { | ||||||
|  |         return (x / Math.pow(2, z) * 360 - 180); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static tile2lat(y, z) { | ||||||
|  |         const n = Math.PI - 2 * Math.PI * y / Math.pow(2, z); | ||||||
|  |         return (180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static lon2tile(lon, zoom) { | ||||||
|  |         return (Math.floor((lon + 180) / 360 * Math.pow(2, zoom))); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static lat2tile(lat, zoom) { | ||||||
|  |         return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom))); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Calculates the tile bounds of the | ||||||
|  |      * @param z | ||||||
|  |      * @param x | ||||||
|  |      * @param y | ||||||
|  |      * @returns [[maxlat, minlon], [minlat, maxlon]] | ||||||
|  |      */ | ||||||
|  |     static tile_bounds(z: number, x: number, y: number): [[number, number], [number, number]] { | ||||||
|  |         return [[Tiles.tile2lat(y, z), Tiles.tile2long(x, z)], [Tiles.tile2lat(y + 1, z), Tiles.tile2long(x + 1, z)]] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     static tile_bounds_lon_lat(z: number, x: number, y: number): [[number, number], [number, number]] { | ||||||
|  |         return [[Tiles.tile2long(x, z), Tiles.tile2lat(y, z)], [Tiles.tile2long(x + 1, z), Tiles.tile2lat(y + 1, z)]] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns the centerpoint [lon, lat] of the specified tile | ||||||
|  |      * @param z | ||||||
|  |      * @param x | ||||||
|  |      * @param y | ||||||
|  |      */ | ||||||
|  |     static centerPointOf(z: number, x: number, y: number): [number, number]{ | ||||||
|  |         return [(Tiles.tile2long(x, z) + Tiles.tile2long(x+1, z)) / 2, (Tiles.tile2lat(y, z) + Tiles.tile2lat(y+1, z)) / 2] | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     static tile_index(z: number, x: number, y: number): number { | ||||||
|  |         return ((x * (2 << z)) + y) * 100 + z | ||||||
|  |     } | ||||||
|  |     /** | ||||||
|  |      * Given a tile index number, returns [z, x, y] | ||||||
|  |      * @param index | ||||||
|  |      * @returns 'zxy' | ||||||
|  |      */ | ||||||
|  |     static tile_from_index(index: number): [number, number, number] { | ||||||
|  |         const z = index % 100; | ||||||
|  |         const factor = 2 << z | ||||||
|  |         index = Math.floor(index / 100) | ||||||
|  |         const x = Math.floor(index / factor) | ||||||
|  |         return [z, x, index % factor] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return x, y of the tile containing (lat, lon) on the given zoom level | ||||||
|  |      */ | ||||||
|  |     static embedded_tile(lat: number, lon: number, z: number): { x: number, y: number, z: number } { | ||||||
|  |         return {x: Tiles.lon2tile(lon, z), y: Tiles.lat2tile(lat, z), z: z} | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1: number, lon1: number): TileRange { | ||||||
|  |         const t0 = Tiles.embedded_tile(lat0, lon0, zoomlevel) | ||||||
|  |         const t1 = Tiles.embedded_tile(lat1, lon1, zoomlevel) | ||||||
|  | 
 | ||||||
|  |         const xstart = Math.min(t0.x, t1.x) | ||||||
|  |         const xend = Math.max(t0.x, t1.x) | ||||||
|  |         const ystart = Math.min(t0.y, t1.y) | ||||||
|  |         const yend = Math.max(t0.y, t1.y) | ||||||
|  |         const total = (1 + xend - xstart) * (1 + yend - ystart) | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             xstart: xstart, | ||||||
|  |             xend: xend, | ||||||
|  |             ystart: ystart, | ||||||
|  |             yend: yend, | ||||||
|  |             total: total, | ||||||
|  |             zoomlevel: zoomlevel | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -36,6 +36,8 @@ export default class ScrollableFullScreen extends UIElement { | ||||||
|         this._component = this.BuildComponent(title("desktop"), content("desktop"), isShown) |         this._component = this.BuildComponent(title("desktop"), content("desktop"), isShown) | ||||||
|             .SetClass("hidden md:block"); |             .SetClass("hidden md:block"); | ||||||
|         this._fullscreencomponent = this.BuildComponent(title("mobile"), content("mobile"), isShown); |         this._fullscreencomponent = this.BuildComponent(title("mobile"), content("mobile"), isShown); | ||||||
|  | 
 | ||||||
|  |          | ||||||
|         const self = this; |         const self = this; | ||||||
|         isShown.addCallback(isShown => { |         isShown.addCallback(isShown => { | ||||||
|             if (isShown) { |             if (isShown) { | ||||||
|  |  | ||||||
|  | @ -2,22 +2,23 @@ import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| 
 | 
 | ||||||
| export class VariableUiElement extends BaseUIElement { | export class VariableUiElement extends BaseUIElement { | ||||||
|     private _element: HTMLElement; |     private readonly _contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>; | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor(contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>) { | ||||||
|         contents: UIEventSource<string | BaseUIElement | BaseUIElement[]> |  | ||||||
|     ) { |  | ||||||
|         super(); |         super(); | ||||||
|  |         this._contents = contents; | ||||||
| 
 | 
 | ||||||
|         this._element = document.createElement("span"); |     } | ||||||
|         const el = this._element; | 
 | ||||||
|         contents.addCallbackAndRun((contents) => { |     protected InnerConstructElement(): HTMLElement { | ||||||
|  |         const el = document.createElement("span"); | ||||||
|  |         this._contents.addCallbackAndRun((contents) => { | ||||||
|             while (el.firstChild) { |             while (el.firstChild) { | ||||||
|                 el.removeChild(el.lastChild); |                 el.removeChild(el.lastChild); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (contents === undefined) { |             if (contents === undefined) { | ||||||
|                 return el; |                 return; | ||||||
|             } |             } | ||||||
|             if (typeof contents === "string") { |             if (typeof contents === "string") { | ||||||
|                 el.innerHTML = contents; |                 el.innerHTML = contents; | ||||||
|  | @ -35,9 +36,6 @@ export class VariableUiElement extends BaseUIElement { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |         return el; | ||||||
| 
 |  | ||||||
|     protected InnerConstructElement(): HTMLElement { |  | ||||||
|         return this._element; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -100,8 +100,6 @@ export default abstract class BaseUIElement { | ||||||
|             throw "ERROR! This is not a correct baseUIElement: " + this.constructor.name |             throw "ERROR! This is not a correct baseUIElement: " + this.constructor.name | ||||||
|         } |         } | ||||||
|         try { |         try { | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             const el = this.InnerConstructElement(); |             const el = this.InnerConstructElement(); | ||||||
| 
 | 
 | ||||||
|             if (el === undefined) { |             if (el === undefined) { | ||||||
|  |  | ||||||
|  | @ -13,17 +13,16 @@ export default class Attribution extends VariableUiElement { | ||||||
|         } |         } | ||||||
|         super( |         super( | ||||||
|             license.map((license: LicenseInfo) => { |             license.map((license: LicenseInfo) => { | ||||||
| 
 |                 if(license === undefined){ | ||||||
|                 if (license?.artist === undefined) { |                     return undefined | ||||||
|                     return undefined; |  | ||||||
|                 } |                 } | ||||||
|                  |                  | ||||||
|                 return new Combine([ |                 return new Combine([ | ||||||
|                     icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"), |                     icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"), | ||||||
| 
 | 
 | ||||||
|                     new Combine([ |                     new Combine([ | ||||||
|                         Translations.W(license.artist).SetClass("block font-bold"), |                         Translations.W(license?.artist ?? ".").SetClass("block font-bold"), | ||||||
|                         Translations.W((license.license ?? "") === "" ? "CC0" : (license.license ?? "")) |                         Translations.W((license?.license ?? "") === "" ? "CC0" : (license?.license ?? "")) | ||||||
|                     ]).SetClass("flex flex-col") |                     ]).SetClass("flex flex-col") | ||||||
|                 ]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg") |                 ]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -48,7 +48,7 @@ export default class DeleteImage extends Toggle { | ||||||
|                 tags.map(tags => (tags[key] ?? "") !== "") |                 tags.map(tags => (tags[key] ?? "") !== "") | ||||||
|             ), |             ), | ||||||
|             undefined /*Login (and thus editing) is disabled*/, |             undefined /*Login (and thus editing) is disabled*/, | ||||||
|             State.state?.featureSwitchUserbadge ?? new UIEventSource<boolean>(true) |             State.state.osmConnection.isLoggedIn | ||||||
|         ) |         ) | ||||||
|         this.SetClass("cursor-pointer") |         this.SetClass("cursor-pointer") | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -9,7 +9,6 @@ import State from "../../State"; | ||||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||||
| import {BBox, GeoOperations} from "../../Logic/GeoOperations"; | import {BBox, GeoOperations} from "../../Logic/GeoOperations"; | ||||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; | import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; | ||||||
| import * as L from "leaflet"; |  | ||||||
| import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; | import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; | ||||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||||
|  |  | ||||||
|  | @ -31,8 +31,10 @@ export default class EditableTagRendering extends Toggle { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|             const answerWithEditButton = new Combine([answer, |             const answerWithEditButton = new Combine([answer, | ||||||
|                 new Toggle(editButton, undefined, State.state.osmConnection.isLoggedIn)]) |                 new Toggle(editButton,  | ||||||
|                 .SetClass("flex justify-between w-full") |                     undefined, | ||||||
|  |                     State.state.osmConnection.isLoggedIn) | ||||||
|  |             ]).SetClass("flex justify-between w-full") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|             const cancelbutton = |             const cancelbutton = | ||||||
|  |  | ||||||
|  | @ -71,7 +71,7 @@ export default class SplitRoadWizard extends Toggle { | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         new ShowDataMultiLayer({ |         new ShowDataMultiLayer({ | ||||||
|             features: new StaticFeatureSource([roadElement]), |             features: new StaticFeatureSource([roadElement], false), | ||||||
|             layers: State.state.filteredLayers, |             layers: State.state.filteredLayers, | ||||||
|             leafletMap: miniMap.leafletMap, |             leafletMap: miniMap.leafletMap, | ||||||
|             enablePopups: false, |             enablePopups: false, | ||||||
|  |  | ||||||
							
								
								
									
										156
									
								
								UI/ShowDataLayer/PerTileCountAggregator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								UI/ShowDataLayer/PerTileCountAggregator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,156 @@ | ||||||
|  | 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"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A feature source containing meta features. | ||||||
|  |  * It will contain exactly one point for every tile of the specified (dynamic) zoom level | ||||||
|  |  */ | ||||||
|  | export default class PerTileCountAggregator implements FeatureSource { | ||||||
|  |     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||||
|  |     public readonly name: string = "PerTileCountAggregator" | ||||||
|  | 
 | ||||||
|  |     private readonly perTile: Map<number, SingleTileCounter> = new Map<number, SingleTileCounter>() | ||||||
|  |     private readonly _requestedZoomLevel: UIEventSource<number>; | ||||||
|  | 
 | ||||||
|  |     constructor(requestedZoomLevel: UIEventSource<number>) { | ||||||
|  |         this._requestedZoomLevel = requestedZoomLevel; | ||||||
|  |         const self = this; | ||||||
|  |         this._requestedZoomLevel.addCallbackAndRun(_ => self.update()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private update() { | ||||||
|  |         const now = new Date() | ||||||
|  |         const allCountsAsFeatures : {feature: any, freshness: Date}[] = [] | ||||||
|  |         const aggregate = this.calculatePerTileCount() | ||||||
|  |         aggregate.forEach((totalsPerLayer, tileIndex) => { | ||||||
|  |             const totals = {} | ||||||
|  |             let totalCount = 0 | ||||||
|  |             totalsPerLayer.forEach((total, layerId) => { | ||||||
|  |                 totals[layerId] = total | ||||||
|  |                 totalCount += total | ||||||
|  |             }) | ||||||
|  |             totals["tileId"] = tileIndex | ||||||
|  |             totals["count"] = totalCount | ||||||
|  |             const feature = { | ||||||
|  |                 "type": "Feature", | ||||||
|  |                 "properties": totals, | ||||||
|  |                 "geometry": { | ||||||
|  |                     "type": "Point", | ||||||
|  |                     "coordinates": Tiles.centerPointOf(...Tiles.tile_from_index(tileIndex)) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             allCountsAsFeatures.push({feature: feature, freshness: now}) | ||||||
|  | 
 | ||||||
|  |             const bbox=  BBox.fromTileIndex(tileIndex) | ||||||
|  |             const box = { | ||||||
|  |                 "type": "Feature", | ||||||
|  |                 "properties":totals, | ||||||
|  |                 "geometry": { | ||||||
|  |                     "type": "Polygon", | ||||||
|  |                     "coordinates": [ | ||||||
|  |                         [ | ||||||
|  |                             [bbox.minLon, bbox.minLat], | ||||||
|  |                             [bbox.minLon, bbox.maxLat], | ||||||
|  |                             [bbox.maxLon, bbox.maxLat], | ||||||
|  |                             [bbox.maxLon, bbox.minLat], | ||||||
|  |                             [bbox.minLon, bbox.minLat] | ||||||
|  |                         ] | ||||||
|  |                     ] | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             allCountsAsFeatures.push({feature:box, freshness: now}) | ||||||
|  |         }) | ||||||
|  |         this.features.setData(allCountsAsFeatures) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Calculates an aggregate count per tile and per subtile | ||||||
|  |      * @private | ||||||
|  |      */ | ||||||
|  |     private calculatePerTileCount() { | ||||||
|  |         const perTileCount = new Map<number, Map<string, number>>() | ||||||
|  |         const targetZoom = this._requestedZoomLevel.data; | ||||||
|  |         // We only search for tiles of the same zoomlevel or a higher zoomlevel, which is embedded
 | ||||||
|  |         for (const singleTileCounter of Array.from(this.perTile.values())) { | ||||||
|  | 
 | ||||||
|  |             let tileZ = singleTileCounter.z | ||||||
|  |             let tileX = singleTileCounter.x | ||||||
|  |             let tileY = singleTileCounter.y | ||||||
|  |             if (tileZ < targetZoom) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             while (tileZ > targetZoom) { | ||||||
|  |                 tileX = Math.floor(tileX / 2) | ||||||
|  |                 tileY = Math.floor(tileY / 2) | ||||||
|  |                 tileZ-- | ||||||
|  |             } | ||||||
|  |             const tileI = Tiles.tile_index(tileZ, tileX, tileY) | ||||||
|  |             let counts = perTileCount.get(tileI) | ||||||
|  |             if (counts === undefined) { | ||||||
|  |                 counts = new Map<string, number>() | ||||||
|  |                 perTileCount.set(tileI, counts) | ||||||
|  |             } | ||||||
|  |             singleTileCounter.countsPerLayer.data.forEach((count, layerId) => { | ||||||
|  |                 if (counts.has(layerId)) { | ||||||
|  |                     counts.set(layerId, count + counts.get(layerId)) | ||||||
|  |                 } else { | ||||||
|  |                     counts.set(layerId, count) | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |         return perTileCount; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public addTile(tile: FeatureSourceForLayer & Tiled, shouldBeCounted: UIEventSource<boolean>) { | ||||||
|  |         let counter = this.perTile.get(tile.tileIndex) | ||||||
|  |         if (counter === undefined) { | ||||||
|  |             counter = new SingleTileCounter(tile.tileIndex) | ||||||
|  |             this.perTile.set(tile.tileIndex, counter) | ||||||
|  |             // We do **NOT** add a callback on the perTile index, even though we could! It'll update just fine without it
 | ||||||
|  |         } | ||||||
|  |         counter.addTileCount(tile, shouldBeCounted) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Keeps track of a single tile | ||||||
|  |  */ | ||||||
|  | class SingleTileCounter implements Tiled { | ||||||
|  |     public readonly bbox: BBox; | ||||||
|  |     public readonly tileIndex: number; | ||||||
|  |     public readonly countsPerLayer: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>()) | ||||||
|  |     private readonly registeredLayers: Map<string, LayerConfig> = new Map<string, LayerConfig>(); | ||||||
|  |     public readonly z: number | ||||||
|  |     public readonly x: number | ||||||
|  |     public readonly y: number | ||||||
|  | 
 | ||||||
|  |     constructor(tileIndex: number) { | ||||||
|  |         this.tileIndex = tileIndex | ||||||
|  |         this.bbox = BBox.fromTileIndex(tileIndex) | ||||||
|  |         const [z, x, y] = Tiles.tile_from_index(tileIndex) | ||||||
|  |         this.z = z; | ||||||
|  |         this.x = x; | ||||||
|  |         this.y = y | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public addTileCount(source: FeatureSourceForLayer, shouldBeCounted: UIEventSource<boolean>) { | ||||||
|  |         const layer = source.layer.layerDef | ||||||
|  |         this.registeredLayers.set(layer.id, layer) | ||||||
|  |         const self = this | ||||||
|  |         source.features.map(f => { | ||||||
|  |             /*if (!shouldBeCounted.data) { | ||||||
|  |                 return; | ||||||
|  |             }*/ | ||||||
|  |             self.countsPerLayer.data.set(layer.id, f.length) | ||||||
|  |             self.countsPerLayer.ping() | ||||||
|  |         }, [shouldBeCounted]) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -41,13 +41,14 @@ export default class ShowDataLayer { | ||||||
|         options.leafletMap.addCallback(_ => self.update(options)); |         options.leafletMap.addCallback(_ => self.update(options)); | ||||||
|         this.update(options); |         this.update(options); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         State.state.selectedElement.addCallbackAndRunD(selected => { |         State.state.selectedElement.addCallbackAndRunD(selected => { | ||||||
|             if (self._leafletMap.data === undefined) { |             if (self._leafletMap.data === undefined) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             const v = self.leafletLayersPerId.get(selected.properties.id) |             const v = self.leafletLayersPerId.get(selected.properties.id) | ||||||
|             if(v === undefined){return;} |             if (v === undefined) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|             const leafletLayer = v.leafletlayer |             const leafletLayer = v.leafletlayer | ||||||
|             const feature = v.feature |             const feature = v.feature | ||||||
|             if (leafletLayer.getPopup().isOpen()) { |             if (leafletLayer.getPopup().isOpen()) { | ||||||
|  | @ -66,6 +67,21 @@ export default class ShowDataLayer { | ||||||
| 
 | 
 | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|  | 
 | ||||||
|  |         options.doShowLayer?.addCallbackAndRun(doShow => { | ||||||
|  |             const mp = options.leafletMap.data; | ||||||
|  |             if (this.geoLayer == undefined || mp == undefined) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             if (doShow) { | ||||||
|  |                 mp.addLayer(this.geoLayer) | ||||||
|  |             } else { | ||||||
|  |                 mp.removeLayer(this.geoLayer) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private update(options) { |     private update(options) { | ||||||
|  | @ -83,21 +99,19 @@ export default class ShowDataLayer { | ||||||
|             mp.removeLayer(this.geoLayer); |             mp.removeLayer(this.geoLayer); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.geoLayer= this.CreateGeojsonLayer() |         this.geoLayer = this.CreateGeojsonLayer() | ||||||
|         const allFeats = this._features.data; |         const allFeats = this._features.data; | ||||||
|         for (const feat of allFeats) { |         for (const feat of allFeats) { | ||||||
|             if (feat === undefined) { |             if (feat === undefined) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             try{ |             try { | ||||||
|                 this.geoLayer.addData(feat); |                 this.geoLayer.addData(feat); | ||||||
|             }catch(e){ |             } catch (e) { | ||||||
|                 console.error("Could not add ", feat, "to the geojson layer in leaflet") |                 console.error("Could not add ", feat, "to the geojson layer in leaflet") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         mp.addLayer(this.geoLayer) |  | ||||||
| 
 |  | ||||||
|         if (options.zoomToFeatures ?? false) { |         if (options.zoomToFeatures ?? false) { | ||||||
|             try { |             try { | ||||||
|                 mp.fitBounds(this.geoLayer.getBounds(), {animate: false}) |                 mp.fitBounds(this.geoLayer.getBounds(), {animate: false}) | ||||||
|  | @ -105,6 +119,10 @@ export default class ShowDataLayer { | ||||||
|                 console.error(e) |                 console.error(e) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         if (options.doShowLayer?.data ?? true) { | ||||||
|  |             mp.addLayer(this.geoLayer) | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -125,7 +143,8 @@ export default class ShowDataLayer { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const tagSource = feature.properties.id === undefined ? new UIEventSource<any>(feature.properties) : State.state.allElements.getEventSourceById(feature.properties.id) |         const tagSource = feature.properties.id === undefined ? new UIEventSource<any>(feature.properties) :  | ||||||
|  |             State.state.allElements.getEventSourceById(feature.properties.id) | ||||||
|         const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0) |         const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0) | ||||||
|         const style = layer.GenerateLeafletStyle(tagSource, clickable); |         const style = layer.GenerateLeafletStyle(tagSource, clickable); | ||||||
|         const baseElement = style.icon.html; |         const baseElement = style.icon.html; | ||||||
|  | @ -193,8 +212,10 @@ export default class ShowDataLayer { | ||||||
|             infobox.Activate(); |             infobox.Activate(); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         // Add the feature to the index to open the popup when needed
 |         // Add the feature to the index to open the popup when needed
 | ||||||
|         this.leafletLayersPerId.set(feature.properties.id, {feature: feature, leafletlayer: leafletLayer}) |         this.leafletLayersPerId.set(feature.properties.id, {feature: feature, leafletlayer: leafletLayer}) | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private CreateGeojsonLayer(): L.Layer { |     private CreateGeojsonLayer(): L.Layer { | ||||||
|  |  | ||||||
|  | @ -6,4 +6,5 @@ export interface ShowDataLayerOptions { | ||||||
|     leafletMap: UIEventSource<L.Map>, |     leafletMap: UIEventSource<L.Map>, | ||||||
|     enablePopups?: true | boolean, |     enablePopups?: true | boolean, | ||||||
|     zoomToFeatures?: false | boolean, |     zoomToFeatures?: false | boolean, | ||||||
|  |     doShowLayer?: UIEventSource<boolean> | ||||||
| } | } | ||||||
							
								
								
									
										79
									
								
								UI/ShowDataLayer/ShowTileInfo.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								UI/ShowDataLayer/ShowTileInfo.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | ||||||
|  | import FeatureSource, {Tiled} from "../../Logic/FeatureSource/FeatureSource"; | ||||||
|  | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
|  | import {Utils} from "../../Utils"; | ||||||
|  | import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||||
|  | import ShowDataLayer from "./ShowDataLayer"; | ||||||
|  | import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||||
|  | import {GeoOperations} from "../../Logic/GeoOperations"; | ||||||
|  | import {Tiles} from "../../Models/TileRange"; | ||||||
|  | 
 | ||||||
|  | export default class ShowTileInfo { | ||||||
|  |     public static readonly styling = new LayerConfig({ | ||||||
|  |         id: "tileinfo_styling", | ||||||
|  |         title: { | ||||||
|  |             render: "Tile {z}/{x}/{y}" | ||||||
|  |         }, | ||||||
|  |         tagRenderings: [ | ||||||
|  |             "all_tags" | ||||||
|  |         ], | ||||||
|  |         source: { | ||||||
|  |             osmTags: "tileId~*" | ||||||
|  |         }, | ||||||
|  |         color: {"render": "#3c3"}, | ||||||
|  |         width: { | ||||||
|  |             "render": "1" | ||||||
|  |         }, | ||||||
|  |         label: { | ||||||
|  |             render: "<div class='rounded-full text-xl font-bold' style='width: 2rem; height: 2rem; background: white'>{count}</div>" | ||||||
|  |         } | ||||||
|  |     }, "tileinfo", true) | ||||||
|  | 
 | ||||||
|  |     constructor(options: { | ||||||
|  |         source: FeatureSource & Tiled, leafletMap: UIEventSource<any>, layer?: LayerConfig, | ||||||
|  |         doShowLayer?: UIEventSource<boolean> | ||||||
|  |     }) { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const source = options.source | ||||||
|  |         const metaFeature: UIEventSource<any[]> = | ||||||
|  |             source.features.map(features => { | ||||||
|  |                 const bbox = source.bbox | ||||||
|  |                 const [z, x, y] = Tiles.tile_from_index(source.tileIndex) | ||||||
|  |                 const box = { | ||||||
|  |                     "type": "Feature", | ||||||
|  |                     "properties": { | ||||||
|  |                         "z": z, | ||||||
|  |                         "x": x, | ||||||
|  |                         "y": y, | ||||||
|  |                         "tileIndex": source.tileIndex, | ||||||
|  |                         "source": source.name, | ||||||
|  |                         "count": features.length, | ||||||
|  |                         tileId: source.name + "/" + source.tileIndex | ||||||
|  |                     }, | ||||||
|  |                     "geometry": { | ||||||
|  |                         "type": "Polygon", | ||||||
|  |                         "coordinates": [ | ||||||
|  |                             [ | ||||||
|  |                                 [bbox.minLon, bbox.minLat], | ||||||
|  |                                 [bbox.minLon, bbox.maxLat], | ||||||
|  |                                 [bbox.maxLon, bbox.maxLat], | ||||||
|  |                                 [bbox.maxLon, bbox.minLat], | ||||||
|  |                                 [bbox.minLon, bbox.minLat] | ||||||
|  |                             ] | ||||||
|  |                         ] | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 const center = GeoOperations.centerpoint(box) | ||||||
|  |                 return [box, center] | ||||||
|  |             }) | ||||||
|  | 
 | ||||||
|  |         new ShowDataLayer({ | ||||||
|  |             layerToShow: ShowTileInfo.styling, | ||||||
|  |             features: new StaticFeatureSource(metaFeature, false), | ||||||
|  |             leafletMap: options.leafletMap, | ||||||
|  |             doShowLayer: options.doShowLayer | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										105
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										105
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -10,7 +10,7 @@ export class Utils { | ||||||
|      */ |      */ | ||||||
|     public static runningFromConsole = typeof window === "undefined"; |     public static runningFromConsole = typeof window === "undefined"; | ||||||
|     public static readonly assets_path = "./assets/svg/"; |     public static readonly assets_path = "./assets/svg/"; | ||||||
|     public static externalDownloadFunction: (url: string) => Promise<any>; |     public static externalDownloadFunction: (url: string, headers?: any) => Promise<any>; | ||||||
|     private static knownKeys = ["addExtraTags", "and", "calculatedTags", "changesetmessage", "clustering", "color", "condition", "customCss", "dashArray", "defaultBackgroundId", "description", "descriptionTail", "doNotDownload", "enableAddNewPoints", "enableBackgroundLayerSelection", "enableGeolocation", "enableLayers", "enableMoreQuests", "enableSearch", "enableShareScreen", "enableUserBadge", "freeform", "hideFromOverview", "hideInAnswer", "icon", "iconOverlays", "iconSize", "id", "if", "ifnot", "isShown", "key", "language", "layers", "lockLocation", "maintainer", "mappings", "maxzoom", "maxZoom", "minNeededElements", "minzoom", "multiAnswer", "name", "or", "osmTags", "passAllFeatures", "presets", "question", "render", "roaming", "roamingRenderings", "rotation", "shortDescription", "socialImage", "source", "startLat", "startLon", "startZoom", "tagRenderings", "tags", "then", "title", "titleIcons", "type", "version", "wayHandling", "widenFactor", "width"] |     private static knownKeys = ["addExtraTags", "and", "calculatedTags", "changesetmessage", "clustering", "color", "condition", "customCss", "dashArray", "defaultBackgroundId", "description", "descriptionTail", "doNotDownload", "enableAddNewPoints", "enableBackgroundLayerSelection", "enableGeolocation", "enableLayers", "enableMoreQuests", "enableSearch", "enableShareScreen", "enableUserBadge", "freeform", "hideFromOverview", "hideInAnswer", "icon", "iconOverlays", "iconSize", "id", "if", "ifnot", "isShown", "key", "language", "layers", "lockLocation", "maintainer", "mappings", "maxzoom", "maxZoom", "minNeededElements", "minzoom", "multiAnswer", "name", "or", "osmTags", "passAllFeatures", "presets", "question", "render", "roaming", "roamingRenderings", "rotation", "shortDescription", "socialImage", "source", "startLat", "startLon", "startZoom", "tagRenderings", "tags", "then", "title", "titleIcons", "type", "version", "wayHandling", "widenFactor", "width"] | ||||||
|     private static extraKeys = ["nl", "en", "fr", "de", "pt", "es", "name", "phone", "email", "amenity", "leisure", "highway", "building", "yes", "no", "true", "false"] |     private static extraKeys = ["nl", "en", "fr", "de", "pt", "es", "name", "phone", "email", "amenity", "leisure", "highway", "building", "yes", "no", "true", "false"] | ||||||
| 
 | 
 | ||||||
|  | @ -247,64 +247,6 @@ export class Utils { | ||||||
|         return dict.get(k); |         return dict.get(k); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * Calculates the tile bounds of the |  | ||||||
|      * @param z |  | ||||||
|      * @param x |  | ||||||
|      * @param y |  | ||||||
|      * @returns [[maxlat, minlon], [minlat, maxlon]] |  | ||||||
|      */ |  | ||||||
|     static tile_bounds(z: number, x: number, y: number): [[number, number], [number, number]] { |  | ||||||
|         return [[Utils.tile2lat(y, z), Utils.tile2long(x, z)], [Utils.tile2lat(y + 1, z), Utils.tile2long(x + 1, z)]] |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static tile_bounds_lon_lat(z: number, x: number, y: number): [[number, number], [number, number]] { |  | ||||||
|         return [[Utils.tile2long(x, z), Utils.tile2lat(y, z)], [Utils.tile2long(x + 1, z), Utils.tile2lat(y + 1, z)]] |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static tile_index(z: number, x: number, y: number): number { |  | ||||||
|         return ((x * (2 << z)) + y) * 100 + z |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Given a tile index number, returns [z, x, y] |  | ||||||
|      * @param index |  | ||||||
|      * @returns 'zxy' |  | ||||||
|      */ |  | ||||||
|     static tile_from_index(index: number): [number, number, number] { |  | ||||||
|         const z = index % 100; |  | ||||||
|         const factor = 2 << z |  | ||||||
|         index = Math.floor(index / 100) |  | ||||||
|         return [z, Math.floor(index / factor), index % factor] |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Return x, y of the tile containing (lat, lon) on the given zoom level |  | ||||||
|      */ |  | ||||||
|     static embedded_tile(lat: number, lon: number, z: number): { x: number, y: number, z: number } { |  | ||||||
|         return {x: Utils.lon2tile(lon, z), y: Utils.lat2tile(lat, z), z: z} |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1: number, lon1: number): TileRange { |  | ||||||
|         const t0 = Utils.embedded_tile(lat0, lon0, zoomlevel) |  | ||||||
|         const t1 = Utils.embedded_tile(lat1, lon1, zoomlevel) |  | ||||||
| 
 |  | ||||||
|         const xstart = Math.min(t0.x, t1.x) |  | ||||||
|         const xend = Math.max(t0.x, t1.x) |  | ||||||
|         const ystart = Math.min(t0.y, t1.y) |  | ||||||
|         const yend = Math.max(t0.y, t1.y) |  | ||||||
|         const total = (1 + xend - xstart) * (1 + yend - ystart) |  | ||||||
| 
 |  | ||||||
|         return { |  | ||||||
|             xstart: xstart, |  | ||||||
|             xend: xend, |  | ||||||
|             ystart: ystart, |  | ||||||
|             yend: yend, |  | ||||||
|             total: total, |  | ||||||
|             zoomlevel: zoomlevel |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static MinifyJSON(stringified: string): string { |     public static MinifyJSON(stringified: string): string { | ||||||
|         stringified = stringified.replace(/\|/g, "||"); |         stringified = stringified.replace(/\|/g, "||"); | ||||||
| 
 | 
 | ||||||
|  | @ -345,16 +287,7 @@ export class Utils { | ||||||
|         return result; |         return result; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static MapRange<T>(tileRange: TileRange, f: (x: number, y: number) => T): T[] { | 
 | ||||||
|         const result: T[] = [] |  | ||||||
|         for (let x = tileRange.xstart; x <= tileRange.xend; x++) { |  | ||||||
|             for (let y = tileRange.ystart; y <= tileRange.yend; y++) { |  | ||||||
|                 const t = f(x, y); |  | ||||||
|                 result.push(t) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return result; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     private static injectedDownloads = {} |     private static injectedDownloads = {} | ||||||
| 
 | 
 | ||||||
|  | @ -362,7 +295,7 @@ export class Utils { | ||||||
|         Utils.injectedDownloads[url] = data |         Utils.injectedDownloads[url] = data | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static downloadJson(url: string): Promise<any> { |     public static downloadJson(url: string, headers?: any): Promise<any> { | ||||||
| 
 | 
 | ||||||
|         const injected = Utils.injectedDownloads[url] |         const injected = Utils.injectedDownloads[url] | ||||||
|         if (injected !== undefined) { |         if (injected !== undefined) { | ||||||
|  | @ -371,7 +304,7 @@ export class Utils { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (this.externalDownloadFunction !== undefined) { |         if (this.externalDownloadFunction !== undefined) { | ||||||
|             return this.externalDownloadFunction(url) |             return this.externalDownloadFunction(url, headers) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return new Promise((resolve, reject) => { |         return new Promise((resolve, reject) => { | ||||||
|  | @ -379,7 +312,6 @@ export class Utils { | ||||||
|                 xhr.onload = () => { |                 xhr.onload = () => { | ||||||
|                     if (xhr.status == 200) { |                     if (xhr.status == 200) { | ||||||
|                         try { |                         try { | ||||||
|                             console.log("Got a response! Parsing now...") |  | ||||||
|                             resolve(JSON.parse(xhr.response)) |                             resolve(JSON.parse(xhr.response)) | ||||||
|                         } catch (e) { |                         } catch (e) { | ||||||
|                             reject("Not a valid json: " + xhr.response) |                             reject("Not a valid json: " + xhr.response) | ||||||
|  | @ -390,6 +322,13 @@ export class Utils { | ||||||
|                 }; |                 }; | ||||||
|                 xhr.open('GET', url); |                 xhr.open('GET', url); | ||||||
|                 xhr.setRequestHeader("accept", "application/json") |                 xhr.setRequestHeader("accept", "application/json") | ||||||
|  |                 if (headers !== undefined) { | ||||||
|  | 
 | ||||||
|  |                     for (const key in headers) { | ||||||
|  |                         xhr.setRequestHeader(key, headers[key]) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|                 xhr.send(); |                 xhr.send(); | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  | @ -449,22 +388,6 @@ export class Utils { | ||||||
|         return bestColor ?? hex; |         return bestColor ?? hex; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static tile2long(x, z) { |  | ||||||
|         return (x / Math.pow(2, z) * 360 - 180); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static tile2lat(y, z) { |  | ||||||
|         const n = Math.PI - 2 * Math.PI * y / Math.pow(2, z); |  | ||||||
|         return (180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static lon2tile(lon, zoom) { |  | ||||||
|         return (Math.floor((lon + 180) / 360 * Math.pow(2, zoom))); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static lat2tile(lat, zoom) { |  | ||||||
|         return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom))); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     private static colorDiff(c0: { r: number, g: number, b: number }, c1: { r: number, g: number, b: number }) { |     private static colorDiff(c0: { r: number, g: number, b: number }, c1: { r: number, g: number, b: number }) { | ||||||
|         return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b); |         return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b); | ||||||
|  | @ -506,5 +429,11 @@ export class Utils { | ||||||
|         } |         } | ||||||
|         return copy |         return copy | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public static async waitFor(timeMillis: number): Promise<void> { | ||||||
|  |         return new Promise((resolve) => { | ||||||
|  |             window.setTimeout(resolve, timeMillis); | ||||||
|  |         }) | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1592,8 +1592,13 @@ | ||||||
|         { |         { | ||||||
|             "#": "plugs-9", |             "#": "plugs-9", | ||||||
|             "question": { |             "question": { | ||||||
|                 "en": "How much plugs of type <b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> are available here?", |                 "en": "What kind of authentication is available at the charging station?", | ||||||
|                 "nl": "Hoeveel stekkers van type  <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft dit oplaadpunt?" |                 "nl": "Hoeveel stekkers van type  <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft dit oplaadpunt?", | ||||||
|  |                 "it": "Quali sono gli orari di apertura di questa stazione di ricarica?", | ||||||
|  |                 "ja": "この充電ステーションはいつオープンしますか?", | ||||||
|  |                 "nb_NO": "Når åpnet denne ladestasjonen?", | ||||||
|  |                 "ru": "В какое время работает эта зарядная станция?", | ||||||
|  |                 "zh_Hant": "何時是充電站開放使用的時間?" | ||||||
|             }, |             }, | ||||||
|             "render": { |             "render": { | ||||||
|                 "en": "There are <b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> plugs of type <b>Type 2 with cable</b> (mennekes) available here", |                 "en": "There are <b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> plugs of type <b>Type 2 with cable</b> (mennekes) available here", | ||||||
|  | @ -1608,17 +1613,52 @@ | ||||||
|                     "socket:type2_cable~*", |                     "socket:type2_cable~*", | ||||||
|                     "socket:type2_cable!=0" |                     "socket:type2_cable!=0" | ||||||
|                 ] |                 ] | ||||||
|  |             }, | ||||||
|  |             "en": { | ||||||
|  |                 "0": { | ||||||
|  |                     "then": "Authentication by a membership card" | ||||||
|  |                 }, | ||||||
|  |                 "1": { | ||||||
|  |                     "then": "Authentication by an app" | ||||||
|  |                 }, | ||||||
|  |                 "2": { | ||||||
|  |                     "then": "Authentication via phone call is available" | ||||||
|  |                 }, | ||||||
|  |                 "3": { | ||||||
|  |                     "then": "Authentication via phone call is available" | ||||||
|  |                 }, | ||||||
|  |                 "4": { | ||||||
|  |                     "then": "Authentication via NFC is available" | ||||||
|  |                 }, | ||||||
|  |                 "5": { | ||||||
|  |                     "then": "Authentication via Money Card is available" | ||||||
|  |                 }, | ||||||
|  |                 "6": { | ||||||
|  |                     "then": "Authentication via debit card is available" | ||||||
|  |                 }, | ||||||
|  |                 "7": { | ||||||
|  |                     "then": "No authentication is needed" | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "#": "voltage-9", |             "#": "voltage-9", | ||||||
|             "question": { |             "question": { | ||||||
|                 "en": "What voltage do the plugs with <b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> offer?", |                 "en": "What's the phone number for authentication call or SMS?", | ||||||
|                 "nl": "Welke spanning levert de stekker van type <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/>" |                 "nl": "Welke spanning levert de stekker van type <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/>", | ||||||
|  |                 "it": "A quale rete appartiene questa stazione di ricarica?", | ||||||
|  |                 "ja": "この充電ステーションの運営チェーンはどこですか?", | ||||||
|  |                 "ru": "К какой сети относится эта станция?", | ||||||
|  |                 "zh_Hant": "充電站所屬的網路是?" | ||||||
|             }, |             }, | ||||||
|             "render": { |             "render": { | ||||||
|                 "en": "<b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs {socket:type2_cable:voltage} volt", |                 "en": "Authenticate by calling or SMS'ing to <a href='tel:{authentication:phone_call:number}'>{authentication:phone_call:number}</a>", | ||||||
|                 "nl": "<b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft een spanning van {socket:type2_cable:voltage} volt" |                 "nl": "<b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft een spanning van {socket:type2_cable:voltage} volt", | ||||||
|  |                 "it": "{network}", | ||||||
|  |                 "ja": "{network}", | ||||||
|  |                 "nb_NO": "{network}", | ||||||
|  |                 "ru": "{network}", | ||||||
|  |                 "zh_Hant": "{network}" | ||||||
|             }, |             }, | ||||||
|             "freeform": { |             "freeform": { | ||||||
|                 "key": "socket:type2_cable:voltage", |                 "key": "socket:type2_cable:voltage", | ||||||
|  | @ -1650,7 +1690,7 @@ | ||||||
|         { |         { | ||||||
|             "#": "current-9", |             "#": "current-9", | ||||||
|             "question": { |             "question": { | ||||||
|                 "en": "What current do the plugs with <b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> offer?", |                 "en": "When is this charging station opened?", | ||||||
|                 "nl": "Welke stroom levert de stekker van type <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/>?" |                 "nl": "Welke stroom levert de stekker van type <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/>?" | ||||||
|             }, |             }, | ||||||
|             "render": { |             "render": { | ||||||
|  | @ -1665,7 +1705,7 @@ | ||||||
|                 { |                 { | ||||||
|                     "if": "socket:socket:type2_cable:current=16 A", |                     "if": "socket:socket:type2_cable:current=16 A", | ||||||
|                     "then": { |                     "then": { | ||||||
|                         "en": "<b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs at most 16 A", |                         "en": "24/7 opened (including holidays)", | ||||||
|                         "nl": "<b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> levert een stroom van maximaal 16 A" |                         "nl": "<b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> levert een stroom van maximaal 16 A" | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|  | @ -1687,12 +1727,12 @@ | ||||||
|         { |         { | ||||||
|             "#": "power-output-9", |             "#": "power-output-9", | ||||||
|             "question": { |             "question": { | ||||||
|                 "en": "What power output does a single plug of type <b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> offer?", |                 "en": "How much does one have to pay to use this charging station?", | ||||||
|                 "nl": "Welk vermogen levert een enkele stekker van type <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/>?" |                 "nl": "Hoeveel kost het gebruik van dit oplaadpunt?" | ||||||
|             }, |             }, | ||||||
|             "render": { |             "render": { | ||||||
|                 "en": "<b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs at most {socket:type2_cable:output}", |                 "en": "Using this charging station costs <b>{charge}</b>", | ||||||
|                 "nl": "<b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> levert een vermogen van maximaal {socket:type2_cable:output}" |                 "nl": "Dit oplaadpunt gebruiken kost <b>{charge}</b>" | ||||||
|             }, |             }, | ||||||
|             "freeform": { |             "freeform": { | ||||||
|                 "key": "socket:type2_cable:output", |                 "key": "socket:type2_cable:output", | ||||||
|  | @ -1702,8 +1742,8 @@ | ||||||
|                 { |                 { | ||||||
|                     "if": "socket:socket:type2_cable:output=11 kw", |                     "if": "socket:socket:type2_cable:output=11 kw", | ||||||
|                     "then": { |                     "then": { | ||||||
|                         "en": "<b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs at most 11 kw", |                         "en": "Free to use", | ||||||
|                         "nl": "<b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> levert een vermogen van maximaal 11 kw" |                         "nl": "Gratis te gebruiken" | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|  | @ -1740,17 +1780,31 @@ | ||||||
|                     "socket:tesla_supercharger_ccs~*", |                     "socket:tesla_supercharger_ccs~*", | ||||||
|                     "socket:tesla_supercharger_ccs!=0" |                     "socket:tesla_supercharger_ccs!=0" | ||||||
|                 ] |                 ] | ||||||
|  |             }, | ||||||
|  |             "en": { | ||||||
|  |                 "mappings+": { | ||||||
|  |                     "0": { | ||||||
|  |                         "then": "Payment is done using a dedicated app" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             "nl": { | ||||||
|  |                 "mappings+": { | ||||||
|  |                     "0": { | ||||||
|  |                         "then": "Betalen via een app van het netwerk" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "#": "voltage-10", |             "#": "voltage-10", | ||||||
|             "question": { |             "question": { | ||||||
|                 "en": "What voltage do the plugs with <b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> offer?", |                 "en": "What is the maximum amount of time one is allowed to stay here?", | ||||||
|                 "nl": "Welke spanning levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/>" |                 "nl": "Hoelang mag een voertuig hier blijven staan?" | ||||||
|             }, |             }, | ||||||
|             "render": { |             "render": { | ||||||
|                 "en": "<b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs {socket:tesla_supercharger_ccs:voltage} volt", |                 "en": "One can stay at most <b>{canonical(maxstay)}</b>", | ||||||
|                 "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> heeft een spanning van {socket:tesla_supercharger_ccs:voltage} volt" |                 "nl": "De maximale parkeertijd hier is <b>{canonical(maxstay)}</b>" | ||||||
|             }, |             }, | ||||||
|             "freeform": { |             "freeform": { | ||||||
|                 "key": "socket:tesla_supercharger_ccs:voltage", |                 "key": "socket:tesla_supercharger_ccs:voltage", | ||||||
|  | @ -1760,8 +1814,8 @@ | ||||||
|                 { |                 { | ||||||
|                     "if": "socket:socket:tesla_supercharger_ccs:voltage=500 V", |                     "if": "socket:socket:tesla_supercharger_ccs:voltage=500 V", | ||||||
|                     "then": { |                     "then": { | ||||||
|                         "en": "<b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs 500 volt", |                         "en": "No timelimit on leaving your vehicle here", | ||||||
|                         "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> heeft een spanning van 500 volt" |                         "nl": "Geen maximum parkeertijd" | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|  | @ -1782,11 +1836,11 @@ | ||||||
|         { |         { | ||||||
|             "#": "current-10", |             "#": "current-10", | ||||||
|             "question": { |             "question": { | ||||||
|                 "en": "What current do the plugs with <b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> offer?", |                 "en": "Is this charging station part of a network?", | ||||||
|                 "nl": "Welke stroom levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/>?" |                 "nl": "Welke stroom levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/>?" | ||||||
|             }, |             }, | ||||||
|             "render": { |             "render": { | ||||||
|                 "en": "<b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs at most {socket:tesla_supercharger_ccs:current}A", |                 "en": "Part of the network <b>{network}</b>", | ||||||
|                 "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> levert een stroom van maximaal {socket:tesla_supercharger_ccs:current}A" |                 "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> levert een stroom van maximaal {socket:tesla_supercharger_ccs:current}A" | ||||||
|             }, |             }, | ||||||
|             "freeform": { |             "freeform": { | ||||||
|  | @ -1797,14 +1851,14 @@ | ||||||
|                 { |                 { | ||||||
|                     "if": "socket:socket:tesla_supercharger_ccs:current=125 A", |                     "if": "socket:socket:tesla_supercharger_ccs:current=125 A", | ||||||
|                     "then": { |                     "then": { | ||||||
|                         "en": "<b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs at most 125 A", |                         "en": "Not part of a bigger network", | ||||||
|                         "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> levert een stroom van maximaal 125 A" |                         "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> levert een stroom van maximaal 125 A" | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "if": "socket:socket:tesla_supercharger_ccs:current=350 A", |                     "if": "socket:socket:tesla_supercharger_ccs:current=350 A", | ||||||
|                     "then": { |                     "then": { | ||||||
|                         "en": "<b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs at most 350 A", |                         "en": "Not part of a bigger network", | ||||||
|                         "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> levert een stroom van maximaal 350 A" |                         "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> levert een stroom van maximaal 350 A" | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  | @ -1849,11 +1903,11 @@ | ||||||
|         { |         { | ||||||
|             "#": "plugs-11", |             "#": "plugs-11", | ||||||
|             "question": { |             "question": { | ||||||
|                 "en": "How much plugs of type <b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> are available here?", |                 "en": "What number can one call if there is a problem with this charging station?", | ||||||
|                 "nl": "Hoeveel stekkers van type  <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> heeft dit oplaadpunt?" |                 "nl": "Hoeveel stekkers van type  <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> heeft dit oplaadpunt?" | ||||||
|             }, |             }, | ||||||
|             "render": { |             "render": { | ||||||
|                 "en": "There are <b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> plugs of type <b>Tesla Supercharger (destination)</b> available here", |                 "en": "In case of problems, call <a href='tel:{phone}'>{phone}</a>", | ||||||
|                 "nl": "Hier zijn <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> stekkers van het type " |                 "nl": "Hier zijn <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> stekkers van het type " | ||||||
|             }, |             }, | ||||||
|             "freeform": { |             "freeform": { | ||||||
|  | @ -1870,11 +1924,11 @@ | ||||||
|         { |         { | ||||||
|             "#": "voltage-11", |             "#": "voltage-11", | ||||||
|             "question": { |             "question": { | ||||||
|                 "en": "What voltage do the plugs with <b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> offer?", |                 "en": "What is the email address of the operator?", | ||||||
|                 "nl": "Welke spanning levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/>" |                 "nl": "Welke spanning levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/>" | ||||||
|             }, |             }, | ||||||
|             "render": { |             "render": { | ||||||
|                 "en": "<b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> outputs {socket:tesla_destination:voltage} volt", |                 "en": "In case of problems, send an email to <a href='mailto:{email}'>{email}</a>", | ||||||
|                 "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> heeft een spanning van {socket:tesla_destination:voltage} volt" |                 "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> heeft een spanning van {socket:tesla_destination:voltage} volt" | ||||||
|             }, |             }, | ||||||
|             "freeform": { |             "freeform": { | ||||||
|  | @ -1900,11 +1954,11 @@ | ||||||
|         { |         { | ||||||
|             "#": "current-11", |             "#": "current-11", | ||||||
|             "question": { |             "question": { | ||||||
|                 "en": "What current do the plugs with <b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> offer?", |                 "en": "What is the website of the operator?", | ||||||
|                 "nl": "Welke stroom levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/>?" |                 "nl": "Welke stroom levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/>?" | ||||||
|             }, |             }, | ||||||
|             "render": { |             "render": { | ||||||
|                 "en": "<b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> outputs at most {socket:tesla_destination:current}A", |                 "en": "More info on <a href='{website}'>{website}</a>", | ||||||
|                 "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> levert een stroom van maximaal {socket:tesla_destination:current}A" |                 "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> levert een stroom van maximaal {socket:tesla_destination:current}A" | ||||||
|             }, |             }, | ||||||
|             "freeform": { |             "freeform": { | ||||||
|  | @ -1981,7 +2035,7 @@ | ||||||
|         { |         { | ||||||
|             "#": "plugs-12", |             "#": "plugs-12", | ||||||
|             "question": { |             "question": { | ||||||
|                 "en": "How much plugs of type <b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> are available here?", |                 "en": "What is the reference number of this charging station?", | ||||||
|                 "nl": "Hoeveel stekkers van type  <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft dit oplaadpunt?" |                 "nl": "Hoeveel stekkers van type  <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft dit oplaadpunt?" | ||||||
|             }, |             }, | ||||||
|             "render": { |             "render": { | ||||||
|  | @ -2002,8 +2056,8 @@ | ||||||
|         { |         { | ||||||
|             "#": "voltage-12", |             "#": "voltage-12", | ||||||
|             "question": { |             "question": { | ||||||
|                 "en": "What voltage do the plugs with <b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> offer?", |                 "en": "Is this charging point in use?", | ||||||
|                 "nl": "Welke spanning levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/>" |                 "nl": "Is dit oplaadpunt operationeel?" | ||||||
|             }, |             }, | ||||||
|             "render": { |             "render": { | ||||||
|                 "en": "<b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs {socket:tesla_destination:voltage} volt", |                 "en": "<b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs {socket:tesla_destination:voltage} volt", | ||||||
|  | @ -2017,15 +2071,15 @@ | ||||||
|                 { |                 { | ||||||
|                     "if": "socket:socket:tesla_destination:voltage=230 V", |                     "if": "socket:socket:tesla_destination:voltage=230 V", | ||||||
|                     "then": { |                     "then": { | ||||||
|                         "en": "<b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs 230 volt", |                         "en": "This charging station is broken", | ||||||
|                         "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft een spanning van 230 volt" |                         "nl": "Dit oplaadpunt is kapot" | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "if": "socket:socket:tesla_destination:voltage=400 V", |                     "if": "socket:socket:tesla_destination:voltage=400 V", | ||||||
|                     "then": { |                     "then": { | ||||||
|                         "en": "<b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs 400 volt", |                         "en": "A charging station is planned here", | ||||||
|                         "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft een spanning van 400 volt" |                         "nl": "Hier zal binnenkort een oplaadpunt gebouwd worden" | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             ], |             ], | ||||||
|  | @ -2296,6 +2350,14 @@ | ||||||
|                             "en": "Payment is done using a dedicated app", |                             "en": "Payment is done using a dedicated app", | ||||||
|                             "nl": "Betalen via een app van het netwerk" |                             "nl": "Betalen via een app van het netwerk" | ||||||
|                         } |                         } | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "if": "payment:membership_card=yes", | ||||||
|  |                         "ifnot": "payment:membership_card=no", | ||||||
|  |                         "then": { | ||||||
|  |                             "en": "Payment is done using a membership card", | ||||||
|  |                             "nl": "Betalen via een lidkaart van het netwerk" | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 ], |                 ], | ||||||
|                 "mappings": [ |                 "mappings": [ | ||||||
|  |  | ||||||
|  | @ -48,8 +48,9 @@ | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "calculatedTags": [ |     "calculatedTags": [ | ||||||
|     "_closest_other_drinking_water_id=feat.closest('drinking_water')?.id", |         "_closest_other_drinking_water=feat.closestn('drinking_water', 1, 500).map(f => ({id: f.feat.id, distance: f.distance}))[0]", | ||||||
|     "_closest_other_drinking_water_distance=Math.floor(feat.distanceTo(feat.closest('drinking_water')).distance * 1000)" |         "_closest_other_drinking_water_id=JSON.parse(feat.properties._closest_other_drinking_water)?.id", | ||||||
|  |         "_closest_other_drinking_water_distance=Math.floor(JSON.parse(feat.properties._closest_other_drinking_water)?.distance * 1000)" | ||||||
|     ], |     ], | ||||||
|     "minzoom": 13, |     "minzoom": 13, | ||||||
|     "wayHandling": 1, |     "wayHandling": 1, | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|  |     "minzoom": 12, | ||||||
|     "wayHandling": 1, |     "wayHandling": 1, | ||||||
|     "icon": { |     "icon": { | ||||||
|         "render": "circle:white;./assets/layers/food/restaurant.svg", |         "render": "circle:white;./assets/layers/food/restaurant.svg", | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ | ||||||
|     "source": { |     "source": { | ||||||
|         "osmTags": "amenity=public_bookcase" |         "osmTags": "amenity=public_bookcase" | ||||||
|     }, |     }, | ||||||
|     "minzoom": 12, |     "minzoom": 10, | ||||||
|     "wayHandling": 2, |     "wayHandling": 2, | ||||||
|     "title": { |     "title": { | ||||||
|         "render": { |         "render": { | ||||||
|  |  | ||||||
|  | @ -52,7 +52,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 1.5, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "bench", |     "bench", | ||||||
|  |  | ||||||
|  | @ -40,7 +40,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 1.5, | ||||||
|   "roamingRenderings": [], |   "roamingRenderings": [], | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "bicycle_library" |     "bicycle_library" | ||||||
|  |  | ||||||
|  | @ -47,7 +47,7 @@ | ||||||
|   "startLat": 50.8435, |   "startLat": 50.8435, | ||||||
|   "startLon": 4.3688, |   "startLon": 4.3688, | ||||||
|   "startZoom": 14, |   "startZoom": 14, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 1.5, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "bike_monitoring_station" |     "bike_monitoring_station" | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 1.5, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "binocular" |     "binocular" | ||||||
|  |  | ||||||
|  | @ -25,7 +25,7 @@ | ||||||
|   "startLat": 50.8435, |   "startLat": 50.8435, | ||||||
|   "startLon": 4.3688, |   "startLon": 4.3688, | ||||||
|   "startZoom": 16, |   "startZoom": 16, | ||||||
|   "widenFactor": 0.01, |   "widenFactor": 1.2, | ||||||
|   "socialImage": "./assets/themes/buurtnatuur/social_image.jpg", |   "socialImage": "./assets/themes/buurtnatuur/social_image.jpg", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 1.5, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "cafe_pub" |     "cafe_pub" | ||||||
|  |  | ||||||
|  | @ -47,7 +47,7 @@ | ||||||
|   "startLat": 43.14, |   "startLat": 43.14, | ||||||
|   "startLon": 3.14, |   "startLon": 3.14, | ||||||
|   "startZoom": 14, |   "startZoom": 14, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 1.5, | ||||||
|   "socialImage": "./assets/themes/campersite/Bar%C3%9Fel_Wohnmobilstellplatz.jpg", |   "socialImage": "./assets/themes/campersite/Bar%C3%9Fel_Wohnmobilstellplatz.jpg", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -39,7 +39,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 1.5, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "defaultBackgroundId": "CartoDB.Voyager", |   "defaultBackgroundId": "CartoDB.Voyager", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|  |  | ||||||
|  | @ -48,7 +48,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 1.5, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -21,7 +21,7 @@ | ||||||
|   "clustering": { |   "clustering": { | ||||||
|     "maxZoom": 1 |     "maxZoom": 1 | ||||||
|   }, |   }, | ||||||
|   "widenFactor": 0.005, |   "widenFactor": 1.1, | ||||||
|   "enableDownload": true, |   "enableDownload": true, | ||||||
|   "enablePdfDownload": true, |   "enablePdfDownload": true, | ||||||
|   "layers": [ |   "layers": [ | ||||||
|  |  | ||||||
|  | @ -24,7 +24,7 @@ | ||||||
|   "startLat": 51, |   "startLat": 51, | ||||||
|   "startLon": 3.75, |   "startLon": 3.75, | ||||||
|   "startZoom": 11, |   "startZoom": 11, | ||||||
|   "widenFactor": 1, |   "widenFactor": 1.5, | ||||||
|   "socialImage": "./assets/themes/cycle_infra/cycle-infra.svg", |   "socialImage": "./assets/themes/cycle_infra/cycle-infra.svg", | ||||||
|   "enableDownload": true, |   "enableDownload": true, | ||||||
|   "layers": [ |   "layers": [ | ||||||
|  |  | ||||||
|  | @ -40,7 +40,7 @@ | ||||||
|   "defaultBackgroundId": "CartoDB.Voyager", |   "defaultBackgroundId": "CartoDB.Voyager", | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 2, | ||||||
|   "socialImage": "assets/themes/cyclofix/logo.svg", |   "socialImage": "assets/themes/cyclofix/logo.svg", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "bike_cafe", |     "bike_cafe", | ||||||
|  |  | ||||||
|  | @ -38,7 +38,7 @@ | ||||||
|   "startLat": 51.02768, |   "startLat": 51.02768, | ||||||
|   "startLon": 4.480705, |   "startLon": 4.480705, | ||||||
|   "startZoom": 15, |   "startZoom": 15, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 1.5, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 3, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "food" |     "food" | ||||||
|  |  | ||||||
|  | @ -24,7 +24,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 3, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.001, |   "widenFactor": 2, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "hideFromOverview": true, |   "hideFromOverview": true, | ||||||
|   "layers": [ |   "layers": [ | ||||||
|  |  | ||||||
|  | @ -52,7 +52,7 @@ | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "widenFactor": 0.1, |   "widenFactor": 5, | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "ghost_bike" |     "ghost_bike" | ||||||
|   ], |   ], | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ | ||||||
|   "startLat": 51.2132, |   "startLat": 51.2132, | ||||||
|   "startLon": 3.231, |   "startLon": 3.231, | ||||||
|   "startZoom": 14, |   "startZoom": 14, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 2, | ||||||
|   "cacheTimeout": 3600, |   "cacheTimeout": 3600, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 5, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ | ||||||
|   "startLat": 13.67801, |   "startLat": 13.67801, | ||||||
|   "startLon": 121.6625, |   "startLon": 121.6625, | ||||||
|   "startZoom": 6, |   "startZoom": 6, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 3, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 5, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "map" |     "map" | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ | ||||||
|   "startLat": 51.20875, |   "startLat": 51.20875, | ||||||
|   "startLon": 3.22435, |   "startLon": 3.22435, | ||||||
|   "startZoom": 12, |   "startZoom": 12, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 2, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "drinking_water", |     "drinking_water", | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ | ||||||
|   "startLat": 51.20875, |   "startLat": 51.20875, | ||||||
|   "startLon": 3.22435, |   "startLon": 3.22435, | ||||||
|   "startZoom": 15, |   "startZoom": 15, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 2, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "defaultBackgroundId": "CartoDB.Positron", |   "defaultBackgroundId": "CartoDB.Positron", | ||||||
|   "enablePdfDownload": true, |   "enablePdfDownload": true, | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 5, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "observation_tower" |     "observation_tower" | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ | ||||||
|   "startLat": 51.20875, |   "startLat": 51.20875, | ||||||
|   "startLon": 3.22435, |   "startLon": 3.22435, | ||||||
|   "startZoom": 12, |   "startZoom": 12, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 1.2, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "parking" |     "parking" | ||||||
|  |  | ||||||
|  | @ -41,7 +41,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 16, |   "startZoom": 16, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 3, | ||||||
|   "layers": [], |   "layers": [], | ||||||
|   "roamingRenderings": [] |   "roamingRenderings": [] | ||||||
| } | } | ||||||
|  | @ -19,7 +19,7 @@ | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "hideFromOverview": true, |   "hideFromOverview": true, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 3, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "play_forest" |     "play_forest" | ||||||
|  |  | ||||||
|  | @ -38,7 +38,7 @@ | ||||||
|   "startLat": 50.535, |   "startLat": 50.535, | ||||||
|   "startLon": 4.399, |   "startLon": 4.399, | ||||||
|   "startZoom": 13, |   "startZoom": 13, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 5, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "playground" |     "playground" | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 3, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ | ||||||
|   "startLat": 51.17174, |   "startLat": 51.17174, | ||||||
|   "startLon": 4.449462, |   "startLon": 4.449462, | ||||||
|   "startZoom": 12, |   "startZoom": 12, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 1.2, | ||||||
|   "socialImage": "./assets/themes/speelplekken/social_image.jpg", |   "socialImage": "./assets/themes/speelplekken/social_image.jpg", | ||||||
|   "defaultBackgroundId": "CartoDB.Positron", |   "defaultBackgroundId": "CartoDB.Positron", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ | ||||||
|   "startLat": 51.17174, |   "startLat": 51.17174, | ||||||
|   "startLon": 4.449462, |   "startLon": 4.449462, | ||||||
|   "startZoom": 12, |   "startZoom": 12, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 2, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "defaultBackgroundId": "CartoDB.Positron", |   "defaultBackgroundId": "CartoDB.Positron", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|  |  | ||||||
|  | @ -37,7 +37,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 2, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "sport_pitch" |     "sport_pitch" | ||||||
|  |  | ||||||
|  | @ -37,7 +37,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 2, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "defaultBackgroundId": "osm", |   "defaultBackgroundId": "osm", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ | ||||||
|   "startZoom": 8, |   "startZoom": 8, | ||||||
|   "startLat": 50.8536, |   "startLat": 50.8536, | ||||||
|   "startLon": 4.433, |   "startLon": 4.433, | ||||||
|   "widenFactor": 0.2, |   "widenFactor": 2, | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|       "builtin": [ |       "builtin": [ | ||||||
|  |  | ||||||
|  | @ -35,7 +35,7 @@ | ||||||
|   "startZoom": 12, |   "startZoom": 12, | ||||||
|   "startLat": 51.2095, |   "startLat": 51.2095, | ||||||
|   "startLon": 3.2222, |   "startLon": 3.2222, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 3, | ||||||
|   "icon": "./assets/themes/toilets/toilets.svg", |   "icon": "./assets/themes/toilets/toilets.svg", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "toilet" |     "toilet" | ||||||
|  |  | ||||||
|  | @ -45,7 +45,7 @@ | ||||||
|   "startLat": 50.642, |   "startLat": 50.642, | ||||||
|   "startLon": 4.482, |   "startLon": 4.482, | ||||||
|   "startZoom": 8, |   "startZoom": 8, | ||||||
|   "widenFactor": 0.01, |   "widenFactor": 1.5, | ||||||
|   "socialImage": "./assets/themes/trees/logo.svg", |   "socialImage": "./assets/themes/trees/logo.svg", | ||||||
|   "clustering": { |   "clustering": { | ||||||
|     "maxZoom": 18 |     "maxZoom": 18 | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ | ||||||
|   "startLat": -0.08528530407, |   "startLat": -0.08528530407, | ||||||
|   "startLon": 51.52103754846, |   "startLon": 51.52103754846, | ||||||
|   "startZoom": 18, |   "startZoom": 18, | ||||||
|   "widenFactor": 0.5, |   "widenFactor": 1.5, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 1, |   "startZoom": 1, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 2, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -25,7 +25,7 @@ | ||||||
|   "startLat": 51.20875, |   "startLat": 51.20875, | ||||||
|   "startLon": 3.22435, |   "startLon": 3.22435, | ||||||
|   "startZoom": 14, |   "startZoom": 14, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 2, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -48,13 +48,10 @@ export default class ScriptUtils { | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static DownloadJSON(url, options?: { |     public static DownloadJSON(url, headers?: any): Promise<any> { | ||||||
|         headers: any |  | ||||||
|     }): Promise<any> { |  | ||||||
|         return new Promise((resolve, reject) => { |         return new Promise((resolve, reject) => { | ||||||
|             try { |             try { | ||||||
| 
 |                 headers = headers ?? {} | ||||||
|                 const headers = options?.headers ?? {} |  | ||||||
|                 headers.accept = "application/json" |                 headers.accept = "application/json" | ||||||
|                 console.log("Fetching", url) |                 console.log("Fetching", url) | ||||||
|                 const urlObj = new URL(url) |                 const urlObj = new URL(url) | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ import RelationsTracker from "../Logic/Osm/RelationsTracker"; | ||||||
| import * as OsmToGeoJson from "osmtogeojson"; | import * as OsmToGeoJson from "osmtogeojson"; | ||||||
| import MetaTagging from "../Logic/MetaTagging"; | import MetaTagging from "../Logic/MetaTagging"; | ||||||
| import {UIEventSource} from "../Logic/UIEventSource"; | import {UIEventSource} from "../Logic/UIEventSource"; | ||||||
| import {TileRange} from "../Models/TileRange"; | import {TileRange, Tiles} from "../Models/TileRange"; | ||||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||||
| import ScriptUtils from "./ScriptUtils"; | import ScriptUtils from "./ScriptUtils"; | ||||||
| import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"; | import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"; | ||||||
|  | @ -86,7 +86,7 @@ async function downloadRaw(targetdir: string, r: TileRange, overpass: Overpass)/ | ||||||
|             } |             } | ||||||
|             console.log("x:", (x - r.xstart), "/", (r.xend - r.xstart), "; y:", (y - r.ystart), "/", (r.yend - r.ystart), "; total: ", downloaded, "/", r.total, "failed: ", failed, "skipped: ", skipped) |             console.log("x:", (x - r.xstart), "/", (r.xend - r.xstart), "; y:", (y - r.ystart), "/", (r.yend - r.ystart), "; total: ", downloaded, "/", r.total, "failed: ", failed, "skipped: ", skipped) | ||||||
| 
 | 
 | ||||||
|             const boundsArr = Utils.tile_bounds(r.zoomlevel, x, y) |             const boundsArr = Tiles.tile_bounds(r.zoomlevel, x, y) | ||||||
|             const bounds = { |             const bounds = { | ||||||
|                 north: Math.max(boundsArr[0][0], boundsArr[1][0]), |                 north: Math.max(boundsArr[0][0], boundsArr[1][0]), | ||||||
|                 south: Math.min(boundsArr[0][0], boundsArr[1][0]), |                 south: Math.min(boundsArr[0][0], boundsArr[1][0]), | ||||||
|  | @ -174,7 +174,7 @@ function loadAllTiles(targetdir: string, r: TileRange, theme: LayoutConfig, extr | ||||||
|             allFeatures.push(...geojson.features) |             allFeatures.push(...geojson.features) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     return new StaticFeatureSource(allFeatures) |     return new StaticFeatureSource(allFeatures, false) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -225,7 +225,7 @@ function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsT | ||||||
|                     delete feature.feature["bbox"] |                     delete feature.feature["bbox"] | ||||||
|                 } |                 } | ||||||
|                 // Lets save this tile!
 |                 // Lets save this tile!
 | ||||||
|                 const [z, x, y] = Utils.tile_from_index(tile.tileIndex) |                 const [z, x, y] = Tiles.tile_from_index(tile.tileIndex) | ||||||
|                 console.log("Writing tile ", z, x, y, layerId) |                 console.log("Writing tile ", z, x, y, layerId) | ||||||
|                 const targetPath = geoJsonName(targetdir + "_" + layerId, x, y, z) |                 const targetPath = geoJsonName(targetdir + "_" + layerId, x, y, z) | ||||||
|                 createdTiles.push(tile.tileIndex) |                 createdTiles.push(tile.tileIndex) | ||||||
|  | @ -241,7 +241,7 @@ function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsT | ||||||
|         // Only thing left to do is to create the index
 |         // Only thing left to do is to create the index
 | ||||||
|         const path = targetdir + "_" + layerId + "_overview.json" |         const path = targetdir + "_" + layerId + "_overview.json" | ||||||
|         const perX = {} |         const perX = {} | ||||||
|         createdTiles.map(i => Utils.tile_from_index(i)).forEach(([z, x, y]) => { |         createdTiles.map(i => Tiles.tile_from_index(i)).forEach(([z, x, y]) => { | ||||||
|             const key = "" + x |             const key = "" + x | ||||||
|             if (perX[key] === undefined) { |             if (perX[key] === undefined) { | ||||||
|                 perX[key] = [] |                 perX[key] = [] | ||||||
|  | @ -279,7 +279,7 @@ async function main(args: string[]) { | ||||||
|     const lat1 = Number(args[5]) |     const lat1 = Number(args[5]) | ||||||
|     const lon1 = Number(args[6]) |     const lon1 = Number(args[6]) | ||||||
| 
 | 
 | ||||||
|     const tileRange = Utils.TileRangeBetween(zoomlevel, lat0, lon0, lat1, lon1) |     const tileRange = Tiles.TileRangeBetween(zoomlevel, lat0, lon0, lat1, lon1) | ||||||
| 
 | 
 | ||||||
|     const theme = AllKnownLayouts.allKnownLayouts.get(themeName) |     const theme = AllKnownLayouts.allKnownLayouts.get(themeName) | ||||||
|     if (theme === undefined) { |     if (theme === undefined) { | ||||||
|  |  | ||||||
|  | @ -33,7 +33,7 @@ class TranslationPart { | ||||||
|             } |             } | ||||||
|             const v = translations[translationsKey] |             const v = translations[translationsKey] | ||||||
|             if (typeof (v) != "string") { |             if (typeof (v) != "string") { | ||||||
|                 console.error("Non-string object in translation: ", translations[translationsKey]) |                 console.error("Non-string object in translation while trying to add more translations to '", translationsKey ,"': ", v) | ||||||
|                 throw "Error in an object depicting a translation: a non-string object was found. (" + context + ")\n    You probably put some other section accidentally in the translation" |                 throw "Error in an object depicting a translation: a non-string object was found. (" + context + ")\n    You probably put some other section accidentally in the translation" | ||||||
|             } |             } | ||||||
|             this.contents.set(translationsKey, v) |             this.contents.set(translationsKey, v) | ||||||
|  | @ -41,9 +41,7 @@ class TranslationPart { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     recursiveAdd(object: any, context: string) { |     recursiveAdd(object: any, context: string) { | ||||||
| 
 |         const isProbablyTranslationObject = knownLanguages.some(l => object.hasOwnProperty(l)); | ||||||
| 
 |  | ||||||
|         const isProbablyTranslationObject = knownLanguages.map(l => object.hasOwnProperty(l)).filter(x => x).length > 0; |  | ||||||
|         if (isProbablyTranslationObject) { |         if (isProbablyTranslationObject) { | ||||||
|             this.addTranslationObject(object, context) |             this.addTranslationObject(object, context) | ||||||
|             return; |             return; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue