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) { | ||||
|             ScriptUtils.erasableLog(`Downloading stats for ${year}-${month}, page ${page} ${url}`) | ||||
|             const result = await ScriptUtils.DownloadJSON(url, { | ||||
|                 headers: headers | ||||
|             }) | ||||
|             const result = await ScriptUtils.DownloadJSON(url, headers) | ||||
|             page++; | ||||
|             allFeatures.push(...result.features) | ||||
|             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 L from "leaflet"; | ||||
| import Img from "./UI/Base/Img"; | ||||
| import UserDetails from "./Logic/Osm/OsmConnection"; | ||||
| import Attribution from "./UI/BigComponents/Attribution"; | ||||
| import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter"; | ||||
| import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs"; | ||||
|  | @ -38,6 +37,9 @@ import Minimap from "./UI/Base/Minimap"; | |||
| import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler"; | ||||
| import Combine from "./UI/Base/Combine"; | ||||
| 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 { | ||||
|     static InitAll( | ||||
|  | @ -167,9 +169,20 @@ export class InitUiElements { | |||
|             ).AttachTo("messagesbox"); | ||||
|         } | ||||
| 
 | ||||
|         State.state.osmConnection.userDetails | ||||
|             .map((userDetails: UserDetails) => userDetails?.home) | ||||
|             .addCallbackAndRunD((home) => { | ||||
|         function addHomeMarker() { | ||||
|             const userDetails = State.state.osmConnection.userDetails.data; | ||||
|             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( | ||||
|                 "--subtle-detail-color" | ||||
|             ); | ||||
|  | @ -181,8 +194,13 @@ export class InitUiElements { | |||
|                 iconAnchor: [15, 15], | ||||
|             }); | ||||
|             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) { | ||||
|             updateFavs(); | ||||
|  | @ -250,16 +268,16 @@ export class InitUiElements { | |||
|             return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))]; | ||||
|         } 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." | ||||
|             } | ||||
| 
 | ||||
|             new Combine([ | ||||
|                 "Error: could not parse the custom layout:", | ||||
|                 new FixedUiElement(""+e).SetClass("alert"), | ||||
|                 new FixedUiElement("" + e).SetClass("alert"), | ||||
|                 new SubtleButton("./assets/svg/mapcomplete_logo.svg", | ||||
|                     "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") | ||||
|  | @ -361,12 +379,12 @@ export class InitUiElements { | |||
|         const layout = State.state.layoutToUse.data; | ||||
|         if (layout.lockLocation) { | ||||
|             if (layout.lockLocation === true) { | ||||
|                 const tile = Utils.embedded_tile( | ||||
|                 const tile = Tiles.embedded_tile( | ||||
|                     layout.startLat, | ||||
|                     layout.startLon, | ||||
|                     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
 | ||||
|                 const latDiff = bounds[0][0] - bounds[1][0]; | ||||
|                 const lonDiff = bounds[0][1] - bounds[1][1]; | ||||
|  | @ -402,6 +420,9 @@ export class InitUiElements { | |||
|                 const flayer = { | ||||
|                     isDisplayed: isDisplayed, | ||||
|                     layerDef: layer, | ||||
|                     isSufficientlyZoomed: state.locationControl.map(l => { | ||||
|                         return l.zoom >= (layer.minzoomVisible ?? layer.minzoom) | ||||
|                     }), | ||||
|                     appliedFilters: new UIEventSource<TagsFilter>(undefined), | ||||
|                 }; | ||||
|                 flayers.push(flayer); | ||||
|  | @ -409,13 +430,54 @@ export class InitUiElements { | |||
|             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( | ||||
|             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( | ||||
|                     { | ||||
|                         features: source, | ||||
|                         leafletMap: State.state.leafletMap, | ||||
|                         layerToShow: source.layer.layerDef | ||||
|                         layerToShow: source.layer.layerDef, | ||||
|                         doShowLayer: doShowFeatures | ||||
|                     } | ||||
|                 ); | ||||
|             }, state | ||||
|  |  | |||
|  | @ -44,7 +44,6 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | |||
|         readonly overpassUrl: UIEventSource<string>; | ||||
|         readonly overpassTimeout: UIEventSource<number>; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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 overpassMaxZoom: UIEventSource<number> | ||||
|         }) { | ||||
|         console.trace("Initializing an overpass FS") | ||||
| 
 | ||||
| 
 | ||||
|         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); | ||||
|     } | ||||
| 
 | ||||
|     private update(): void { | ||||
|     private update() { | ||||
|         this.updateAsync().then(_ => { | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private async updateAsync(): Promise<void> { | ||||
|         if (this.runningQuery.data) { | ||||
|             console.log("Still running a query, not updating"); | ||||
|             return; | ||||
|  | @ -184,49 +189,41 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour | |||
|             return; | ||||
|         } | ||||
|         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.retries.setData(0); | ||||
|                 const features = data.features.map(f => ({feature: f, freshness: date})); | ||||
|                 SimpleMetaTagger.objectMetaInfo.addMetaTags(features) | ||||
| 
 | ||||
|                 try{ | ||||
|                     self.features.setData(features); | ||||
|                 }catch(e){ | ||||
|         try { | ||||
|             data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date)); | ||||
|             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) | ||||
|         } | ||||
|         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 }[] = []; | ||||
|         for(const featureList of features) { | ||||
|             for (const otherFeature of featureList) { | ||||
|                 if (otherFeature == feature || otherFeature.id == feature.id) { | ||||
|                 if (otherFeature === feature || otherFeature.id === feature.id) { | ||||
|                     continue; // We ignore self
 | ||||
|                 } | ||||
|                 let distance = undefined; | ||||
|  | @ -268,7 +268,8 @@ export class ExtraFunction { | |||
|                         [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!" | ||||
|                 } | ||||
|                 if (distance > maxDistance) { | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
|     private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>; | ||||
| 
 | ||||
|     constructor( | ||||
|         handleFeatureSource: (source: FeatureSourceForLayer) => void, | ||||
|         handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void, | ||||
|         state: { | ||||
|             filteredLayers: UIEventSource<FilteredLayer[]>, | ||||
|             locationControl: UIEventSource<Loc>, | ||||
|  | @ -52,7 +52,6 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
| 
 | ||||
|         const self = this | ||||
|         const updater = new OverpassFeatureSource(state); | ||||
|         updater.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(updater)) | ||||
|         this.overpassUpdater = updater; | ||||
|         this.sufficientlyZoomed = updater.sufficientlyZoomed | ||||
|         this.runningQuery = updater.runningQuery | ||||
|  | @ -65,14 +64,15 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
|         const perLayerHierarchy = new Map<string, TileHierarchyMerger>() | ||||
|         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
 | ||||
|             const srcFiltered = | ||||
|                 new FilteringFeatureSource(state, | ||||
|                 new FilteringFeatureSource(state, src.tileIndex, | ||||
|                     new WayHandlingApplyingFeatureSource( | ||||
|                         new ChangeGeometryApplicator(src, state.changes) | ||||
|                     ) | ||||
|                 ) | ||||
|              | ||||
|             handleFeatureSource(srcFiltered) | ||||
|             self.somethingLoaded.setData(true) | ||||
|         }; | ||||
|  | @ -102,10 +102,12 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
| 
 | ||||
|             if (source.geojsonZoomLevel === undefined) { | ||||
|                 // 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) | ||||
|                 TiledFeatureSource.createHierarchy(src, { | ||||
|                     layer: src.layer, | ||||
|                     minZoomLevel:14, | ||||
|                     dontEnforceMinZoom: true, | ||||
|                     registerTile: (tile) => { | ||||
|                         new RegisteringAllFromFeatureSourceActor(tile) | ||||
|                         addToHierarchy(tile, id) | ||||
|  | @ -115,14 +117,11 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
|             } else { | ||||
|                 new DynamicGeoJsonTileSource( | ||||
|                     filteredLayer, | ||||
|                     src => TiledFeatureSource.createHierarchy(src, { | ||||
|                         layer: src.layer, | ||||
|                         registerTile: (tile) => { | ||||
|                     tile => { | ||||
|                             new RegisteringAllFromFeatureSourceActor(tile) | ||||
|                             addToHierarchy(tile, id) | ||||
|                             tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) | ||||
|                         } | ||||
|                     }), | ||||
|                         }, | ||||
|                     state | ||||
|                 ) | ||||
|             } | ||||
|  | @ -133,13 +132,17 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
|         new PerLayerFeatureSourceSplitter(state.filteredLayers, | ||||
|             (source) => TiledFeatureSource.createHierarchy(source, { | ||||
|                 layer: source.layer, | ||||
|                 minZoomLevel: 14, | ||||
|                 dontEnforceMinZoom: true, | ||||
|                 registerTile: (tile) => { | ||||
|                     // We save the tile data for the given layer to local storage
 | ||||
|                     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. 
 | ||||
|  | @ -152,6 +155,8 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
|                 addToHierarchy(perLayer, perLayer.layer.layerDef.id) | ||||
|                 // AT last, we always apply the metatags whenever possible
 | ||||
|                 perLayer.features.addCallbackAndRunD(_ => self.applyMetaTags(perLayer)) | ||||
|                 perLayer.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(perLayer)) | ||||
| 
 | ||||
|             }, | ||||
|             newGeometry | ||||
|         ) | ||||
|  | @ -166,6 +171,7 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
|      | ||||
|     private applyMetaTags(src: FeatureSourceForLayer){ | ||||
|         const self = this | ||||
|         console.log("Applying metatagging onto ", src.name) | ||||
|         MetaTagging.addMetatags( | ||||
|             src.features.data, | ||||
|             { | ||||
|  | @ -183,6 +189,7 @@ export default class FeaturePipeline implements FeatureSourceState { | |||
| 
 | ||||
|     private updateAllMetaTagging() { | ||||
|         const self = this; | ||||
|         console.log("Reupdating all metatagging") | ||||
|         this.perLayerHierarchy.forEach(hierarchy => { | ||||
|             hierarchy.loadedTiles.forEach(src => { | ||||
|                 self.applyMetaTags(src) | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from | |||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| 
 | ||||
| export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource { | ||||
| 
 | ||||
|  | @ -23,7 +24,7 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled | |||
|         this.bbox = bbox; | ||||
|         this._sources = sources; | ||||
|         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 handledSources = new Set<FeatureSource>(); | ||||
|  |  | |||
|  | @ -1,24 +1,29 @@ | |||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer} from "../FeatureSource"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| 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 }[]> = | ||||
|         new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||
|     public readonly name; | ||||
|     public readonly layer: FilteredLayer; | ||||
| 
 | ||||
| public readonly tileIndex : number | ||||
|     public readonly bbox : BBox | ||||
|     constructor( | ||||
|         state: { | ||||
|             locationControl: UIEventSource<{ zoom: number }>, | ||||
|             selectedElement: UIEventSource<any>, | ||||
|         }, | ||||
|         tileIndex, | ||||
|         upstream: FeatureSourceForLayer | ||||
|     ) { | ||||
|         const self = this; | ||||
|         this.name = "FilteringFeatureSource("+upstream.name+")" | ||||
|         this.tileIndex = tileIndex | ||||
|         this.bbox = BBox.fromTileIndex(tileIndex) | ||||
| 
 | ||||
|         this.layer = upstream.layer; | ||||
|         const layer = upstream.layer; | ||||
|  | @ -51,7 +56,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer { | |||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
|                 if (!FilteringFeatureSource.showLayer(layer, state.locationControl.data)) { | ||||
|                 if (!layer.isDisplayed) { | ||||
|                     // The layer itself is either disabled or hidden due to zoom constraints
 | ||||
|                     // We should return true, but it might still match some other layer
 | ||||
|                     return false; | ||||
|  | @ -66,10 +71,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer { | |||
|             update(); | ||||
|         }); | ||||
| 
 | ||||
|         let isShown = state.locationControl.map((l) => FilteringFeatureSource.showLayer(layer, l), | ||||
|             [layer.isDisplayed]) | ||||
|              | ||||
|         isShown.addCallback(isShown => { | ||||
|         layer.isDisplayed.addCallback(isShown => { | ||||
|             if (isShown) { | ||||
|                 update(); | ||||
|             } else { | ||||
|  | @ -78,7 +80,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer { | |||
|         }); | ||||
| 
 | ||||
|         layer.appliedFilters.addCallback(_ => { | ||||
|             if(!isShown.data){ | ||||
|             if(!layer.isDisplayed.data){ | ||||
|                 // 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
 | ||||
|                 return; | ||||
|  | @ -93,10 +95,8 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer { | |||
|         layer: { | ||||
|             isDisplayed: UIEventSource<boolean>; | ||||
|             layerDef: LayerConfig; | ||||
|         }, | ||||
|         location: { zoom: number }) { | ||||
|         return layer.isDisplayed.data && | ||||
|             layer.layerDef.minzoomVisible <= location.zoom; | ||||
|         }) { | ||||
|         return layer.isDisplayed.data; | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import FilteredLayer from "../../../Models/FilteredLayer"; | |||
| import {Utils} from "../../../Utils"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| 
 | ||||
| 
 | ||||
| export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | ||||
|  | @ -35,10 +36,10 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | |||
|                 .replace('{z}', "" + z) | ||||
|                 .replace('{x}', "" + x) | ||||
|                 .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) | ||||
|         } else { | ||||
|             this.tileIndex = Utils.tile_index(0, 0, 0) | ||||
|             this.tileIndex = Tiles.tile_index(0, 0, 0) | ||||
|             this.bbox = BBox.global; | ||||
|         } | ||||
| 
 | ||||
|  | @ -89,7 +90,6 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | |||
| 
 | ||||
|                     newFeatures.push({feature: feature, freshness: freshness}) | ||||
|                 } | ||||
|                 console.debug("Downloaded " + newFeatures.length + " new features and " + skipped + " already seen features from " + url); | ||||
| 
 | ||||
|                 if (newFeatures.length == 0) { | ||||
|                     return; | ||||
|  |  | |||
|  | @ -2,17 +2,23 @@ | |||
|  * Every previously added point is remembered, but new points are added. | ||||
|  * Data coming from upstream will always overwrite a previous value | ||||
|  */ | ||||
| import FeatureSource from "../FeatureSource"; | ||||
| import FeatureSource, {Tiled} from "../FeatureSource"; | ||||
| 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 name; | ||||
|     public readonly  tileIndex : number | ||||
|     public  readonly  bbox : BBox | ||||
|      | ||||
|     constructor(source: FeatureSource) { | ||||
|     constructor(source: FeatureSource & Tiled) { | ||||
|         const self = this; | ||||
|         this.name = "RememberingSource of " + source.name; | ||||
|         this.tileIndex=  source.tileIndex | ||||
|         this.bbox = source.bbox; | ||||
|          | ||||
|         const empty = []; | ||||
|         this.features = source.features.map(features => { | ||||
|             const oldFeatures = self.features?.data ?? empty; | ||||
|  |  | |||
|  | @ -3,13 +3,14 @@ import FilteredLayer from "../../../Models/FilteredLayer"; | |||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| 
 | ||||
| export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||
|     public readonly name: string = "SimpleFeatureSource"; | ||||
|     public readonly layer: FilteredLayer; | ||||
|     public readonly bbox: BBox = BBox.global; | ||||
|     public readonly tileIndex: number = Utils.tile_index(0, 0, 0); | ||||
|     public readonly tileIndex: number = Tiles.tile_index(0, 0, 0); | ||||
| 
 | ||||
|     constructor(layer: FilteredLayer) { | ||||
|         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 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(); | ||||
|         if(useFeaturesDirectly){ | ||||
|         if (useFeaturesDirectly) { | ||||
|             // @ts-ignore
 | ||||
|             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}))) | ||||
|         } else { | ||||
|             this.features = new UIEventSource(features.map(f => ({ | ||||
|  |  | |||
|  | @ -12,7 +12,8 @@ export default class WayHandlingApplyingFeatureSource implements FeatureSourceFo | |||
|     public readonly layer; | ||||
| 
 | ||||
|     constructor(upstream: FeatureSourceForLayer) { | ||||
|         this.name = "Wayhandling(" + upstream.name+")"; | ||||
|          | ||||
|         this.name = "Wayhandling(" + upstream.name + ")"; | ||||
|         this.layer = upstream.layer | ||||
|         const layer = upstream.layer.layerDef; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer} from "../FeatureSource"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import Loc from "../../../Models/Loc"; | ||||
| import DynamicTileSource from "./DynamicTileSource"; | ||||
|  | @ -8,7 +8,7 @@ import GeoJsonSource from "../Sources/GeoJsonSource"; | |||
| 
 | ||||
| export default class DynamicGeoJsonTileSource extends DynamicTileSource { | ||||
|     constructor(layer: FilteredLayer, | ||||
|                 registerLayer: (layer: FeatureSourceForLayer) => void, | ||||
|                 registerLayer: (layer: FeatureSourceForLayer & Tiled) => void, | ||||
|                 state: { | ||||
|                     locationControl: UIEventSource<Loc> | ||||
|                     leafletMap: any | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import {Utils} from "../../../Utils"; | |||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import Loc from "../../../Models/Loc"; | ||||
| import TileHierarchy from "./TileHierarchy"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| 
 | ||||
| /*** | ||||
|  * 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
 | ||||
|                     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) { | ||||
|                     return undefined | ||||
|                 } | ||||
|  | @ -63,7 +64,7 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor | |||
|             } | ||||
|             for (const neededIndex of neededIndexes) { | ||||
|                 self._loadedTiles.add(neededIndex) | ||||
|                 const src = constructTile( Utils.tile_from_index(neededIndex)) | ||||
|                 const src = constructTile(Tiles.tile_from_index(neededIndex)) | ||||
|                 if(src !== undefined){ | ||||
|                     self.loadedTiles.set(neededIndex, src) | ||||
|                 } | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import FilteredLayer from "../../../Models/FilteredLayer"; | |||
| import {Utils} from "../../../Utils"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import FeatureSourceMerger from "../Sources/FeatureSourceMerger"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| 
 | ||||
| export class TileHierarchyMerger implements TileHierarchy<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; | ||||
|     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._handleTile = handleTile; | ||||
|     } | ||||
|  | @ -37,7 +38,7 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer | |||
|         // We have to setup
 | ||||
|         const sources = new UIEventSource<FeatureSource[]>([src]) | ||||
|         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._handleTile(merger, index) | ||||
|     } | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ import {Utils} from "../../../Utils"; | |||
| import {BBox} from "../../GeoOperations"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import TileHierarchy from "./TileHierarchy"; | ||||
| import {feature} from "@turf/turf"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| 
 | ||||
| /** | ||||
|  * Contains all features in a tiled fashion. | ||||
|  | @ -41,12 +41,12 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | |||
|         this.x = x; | ||||
|         this.y = 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.parent = parent; | ||||
|         this.layer = options.layer | ||||
|         options = options ?? {} | ||||
|         this.maxFeatureCount = options?.maxFeatureCount ?? 500; | ||||
|         this.maxFeatureCount = options?.maxFeatureCount ?? 250; | ||||
|         this.maxzoom = options.maxZoomLevel ?? 18 | ||||
|         this.options = options; | ||||
|         if (parent === undefined) { | ||||
|  | @ -61,7 +61,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | |||
|         } else { | ||||
|             this.root = this.parent.root; | ||||
|             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.features = new UIEventSource<any[]>([]) | ||||
|  | @ -143,9 +143,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | |||
| 
 | ||||
|         for (const feature of features) { | ||||
|             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)) { | ||||
|                     ulf.push(feature) | ||||
|                 } else if (bbox.isContainedIn(this.upper_right.bbox)) { | ||||
|  | @ -186,6 +184,11 @@ export interface TiledFeatureSourceOptions { | |||
|     readonly maxFeatureCount?: number, | ||||
|     readonly maxZoomLevel?: number, | ||||
|     readonly minZoomLevel?: number, | ||||
|     /** | ||||
|      * IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated. | ||||
|      * Setting 'dontEnforceMinZoomLevel' will still allow bigger zoom levels for those features | ||||
|      */ | ||||
|     readonly dontEnforceMinZoom?: boolean, | ||||
|     readonly registerTile?: (tile: TiledFeatureSource & Tiled) => void, | ||||
|     readonly layer?: FilteredLayer | ||||
| } | ||||
|  | @ -6,6 +6,7 @@ import TileHierarchy from "./TileHierarchy"; | |||
| import {Utils} from "../../../Utils"; | ||||
| import SaveTileToLocalStorageActor from "../Actors/SaveTileToLocalStorageActor"; | ||||
| import {BBox} from "../../GeoOperations"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| 
 | ||||
| export default class TiledFromLocalStorageSource implements TileHierarchy<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 | ||||
|                 }) { | ||||
| 
 | ||||
|         const undefinedTiles = new Set<number>() | ||||
|         const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-" | ||||
|         // @ts-ignore
 | ||||
|         const indexes: number[] = Object.keys(localStorage) | ||||
|  | @ -27,7 +29,7 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur | |||
|                 return Number(key.substring(prefix.length)); | ||||
|             }) | ||||
| 
 | ||||
|         console.log("Layer", layer.layerDef.id, "has following tiles in available in localstorage", indexes.map(i => 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 indexesSet = new Set(indexes) | ||||
|  | @ -57,9 +59,9 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur | |||
|                 const needed = [] | ||||
|                 for (let z = minZoom; z <= maxZoom; z++) { | ||||
| 
 | ||||
|                     const tileRange = Utils.TileRangeBetween(z, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) | ||||
|                     const neededZ = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(z, x, y)) | ||||
|                         .filter(i => !self.loadedTiles.has(i) && indexesSet.has(i)) | ||||
|                     const tileRange = Tiles.TileRangeBetween(z, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) | ||||
|                     const neededZ = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(z, x, y)) | ||||
|                         .filter(i => !self.loadedTiles.has(i) && !undefinedTiles.has(i) && indexesSet.has(i)) | ||||
|                     needed.push(...neededZ) | ||||
|                 } | ||||
| 
 | ||||
|  | @ -84,12 +86,13 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur | |||
|                         features: new UIEventSource<{ feature: any; freshness: Date }[]>(features), | ||||
|                         name: "FromLocalStorage(" + key + ")", | ||||
|                         tileIndex: neededIndex, | ||||
|                         bbox: BBox.fromTile(...Utils.tile_from_index(neededIndex)) | ||||
|                         bbox: BBox.fromTileIndex(neededIndex) | ||||
|                     } | ||||
|                     handleFeatureSource(src, neededIndex) | ||||
|                     self.loadedTiles.set(neededIndex, src) | ||||
|                 } catch (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 {Utils} from "../Utils"; | ||||
| import {Tiles} from "../Models/TileRange"; | ||||
| 
 | ||||
| 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 | ||||
|      */ | ||||
|     static centerpoint(feature: any) { | ||||
|  | @ -451,8 +452,12 @@ export class BBox { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static fromTile(z: number, x: number, y: number) { | ||||
|         return new BBox(Utils.tile_bounds_lon_lat(z, x, y)) | ||||
|     static fromTile(z: number, x: number, y: number): BBox { | ||||
|         return new BBox(Tiles.tile_bounds_lon_lat(z, x, y)) | ||||
|     } | ||||
| 
 | ||||
|     static fromTileIndex(i: number): BBox { | ||||
|         return BBox.fromTile(...Tiles.tile_from_index(i)) | ||||
|     } | ||||
| 
 | ||||
|     getEast() { | ||||
|  |  | |||
|  | @ -12,8 +12,11 @@ export default abstract class ImageAttributionSource { | |||
|         if (cached !== undefined) { | ||||
|             return cached; | ||||
|         } | ||||
|         const src = this.DownloadAttribution(url) | ||||
|         const src = new UIEventSource(undefined) | ||||
|         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; | ||||
|     } | ||||
| 
 | ||||
|  | @ -21,10 +24,10 @@ export default abstract class ImageAttributionSource { | |||
|     public abstract SourceIcon(backlinkSource?: string): BaseUIElement; | ||||
| 
 | ||||
|     /*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; | ||||
|     } | ||||
| 
 | ||||
|     protected abstract DownloadAttribution(url: string): UIEventSource<LicenseInfo>; | ||||
|     protected abstract DownloadAttribution(url: string): Promise<LicenseInfo>; | ||||
| 
 | ||||
| } | ||||
|  | @ -2,8 +2,9 @@ | |||
| import $ from "jquery" | ||||
| import {LicenseInfo} from "./Wikimedia"; | ||||
| import ImageAttributionSource from "./ImageAttributionSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| 
 | ||||
| export class Imgur extends ImageAttributionSource { | ||||
| 
 | ||||
|  | @ -86,35 +87,18 @@ export class Imgur extends ImageAttributionSource { | |||
|         return undefined; | ||||
|     } | ||||
| 
 | ||||
|     protected DownloadAttribution(url: string): UIEventSource<LicenseInfo> { | ||||
|         const src = new UIEventSource<LicenseInfo>(undefined) | ||||
| 
 | ||||
| 
 | ||||
|     protected async DownloadAttribution(url: string): Promise<LicenseInfo> { | ||||
|         const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0]; | ||||
| 
 | ||||
|         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 data: any = {}; | ||||
|         for (const tag of descr.split("\n")) { | ||||
|             const kv = tag.split(":"); | ||||
|             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.artist = data.author; | ||||
| 
 | ||||
|             src.setData(licenseInfo) | ||||
| 
 | ||||
|         }).fail((reason) => { | ||||
|             console.log("Getting metadata from to IMGUR failed", reason) | ||||
|         }); | ||||
| 
 | ||||
|         return src; | ||||
|         return licenseInfo | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ export class Mapillary extends ImageAttributionSource { | |||
|     } { | ||||
|         if (value.startsWith("https://a.mapillary.com")) { | ||||
|             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]*)/) | ||||
|         if (newApiFormat !== null) { | ||||
|  | @ -32,9 +32,9 @@ export class Mapillary extends ImageAttributionSource { | |||
|         } | ||||
| 
 | ||||
|         const mapview = value.match(/https?:\/\/www.mapillary.com\/map\/im\/(.*)/) | ||||
|         if(mapview !== null){ | ||||
|         if (mapview !== null) { | ||||
|             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}` | ||||
|         } else { | ||||
|             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) | ||||
|             } | ||||
| 
 | ||||
|             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) | ||||
|             Mapillary.v4_cached_urls.set(key, source) | ||||
|             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) | ||||
|         if(keyV.isApiv4){ | ||||
|         if (keyV.isApiv4) { | ||||
|             const license = new LicenseInfo() | ||||
|             license.artist = "Contributor name unavailable"; | ||||
|             license.license = "CC BY-SA 4.0"; | ||||
|             // license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
 | ||||
|             license.attributionRequired = true; | ||||
|             return new UIEventSource<LicenseInfo>(license) | ||||
|             return license | ||||
| 
 | ||||
|         } | ||||
|         const key = keyV.key | ||||
| 
 | ||||
|         const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2` | ||||
|         const source = new UIEventSource<LicenseInfo>(undefined) | ||||
|         Utils.downloadJson(metadataURL).then(data => { | ||||
|         const data = await Utils.downloadJson(metadataURL) | ||||
|         const license = new LicenseInfo(); | ||||
|         license.artist = data.properties?.username; | ||||
|         license.licenseShortName = "CC BY-SA 4.0"; | ||||
|         license.license = "Creative Commons Attribution-ShareAlike 4.0 International License"; | ||||
|         license.attributionRequired = true; | ||||
|             source.setData(license); | ||||
|         }) | ||||
| 
 | ||||
|         return source | ||||
|         return license | ||||
|     } | ||||
| } | ||||
|  | @ -1,7 +1,6 @@ | |||
| import ImageAttributionSource from "./ImageAttributionSource"; | ||||
| import BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import Svg from "../../Svg"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Link from "../../UI/Base/Link"; | ||||
| import {Utils} from "../../Utils"; | ||||
| 
 | ||||
|  | @ -124,28 +123,23 @@ export class Wikimedia extends ImageAttributionSource { | |||
|             .replace(/'/g, '%27'); | ||||
|     } | ||||
| 
 | ||||
|     protected DownloadAttribution(filename: string): UIEventSource<LicenseInfo> { | ||||
| 
 | ||||
|         const source = new UIEventSource<LicenseInfo>(undefined); | ||||
| 
 | ||||
|     protected async DownloadAttribution(filename: string): Promise<LicenseInfo> { | ||||
|         filename = Wikimedia.ExtractFileName(filename) | ||||
| 
 | ||||
|         if (filename === "") { | ||||
|             return source; | ||||
|             return undefined; | ||||
|         } | ||||
| 
 | ||||
|         const url = "https://en.wikipedia.org/w/" + | ||||
|             "api.php?action=query&prop=imageinfo&iiprop=extmetadata&" + | ||||
|             "titles=" + filename + | ||||
|             "&format=json&origin=*"; | ||||
|         Utils.downloadJson(url).then( | ||||
|             data => { | ||||
|         const data = await Utils.downloadJson(url) | ||||
|         const licenseInfo = new LicenseInfo(); | ||||
|         const license = (data.query.pages[-1].imageinfo ?? [])[0]?.extmetadata; | ||||
|         if (license === undefined) { | ||||
|             console.error("This file has no usable metedata or license attached... Please fix the license info file yourself!") | ||||
|                     source.setData(null) | ||||
|                     return; | ||||
|             return undefined; | ||||
|         } | ||||
| 
 | ||||
|         licenseInfo.artist = license.Artist?.value; | ||||
|  | @ -156,11 +150,7 @@ export class Wikimedia extends ImageAttributionSource { | |||
|         licenseInfo.licenseShortName = license.LicenseShortName?.value; | ||||
|         licenseInfo.credit = license.Credit?.value; | ||||
|         licenseInfo.description = license.ImageDescription?.value; | ||||
|                 source.setData(licenseInfo); | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
|         return source; | ||||
|         return licenseInfo; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import SimpleMetaTagger from "./SimpleMetaTagger"; | |||
| import {ExtraFuncParams, ExtraFunction} from "./ExtraFunction"; | ||||
| import {UIEventSource} from "./UIEventSource"; | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
| import State from "../State"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  | @ -31,39 +32,57 @@ export default class MetaTagging { | |||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         for (const metatag of SimpleMetaTagger.metatags) { | ||||
| 
 | ||||
|             try { | ||||
|             const metatagsToApply: SimpleMetaTagger [] = [] | ||||
|             for (const metatag of SimpleMetaTagger.metatags) { | ||||
|                 if (metatag.includesDates) { | ||||
|                     if (options.includeDates ?? true) { | ||||
|                         metatag.addMetaTags(features); | ||||
|                         metatagsToApply.push(metatag) | ||||
|                     } | ||||
|                 } else { | ||||
|                     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) { | ||||
|                         console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e) | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|         // The functions - per layer - which add the new keys
 | ||||
|         const layerFuncs = this.createRetaggingFunc(layer) | ||||
| 
 | ||||
|         if (layerFuncs !== undefined) { | ||||
|             for (const feature of features) { | ||||
| 
 | ||||
|                 if(layerFuncs !== undefined){ | ||||
|                     try { | ||||
|                     layerFuncs(params, feature.feature) | ||||
|                         layerFuncs(params, feature) | ||||
|                     } catch (e) { | ||||
|                         console.error(e) | ||||
|                     } | ||||
|                     somethingChanged = true | ||||
|                 } | ||||
|                  | ||||
|                 if(somethingChanged){ | ||||
|                     State.state.allElements.getEventSourceById(feature.properties.id).ping() | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private static createRetaggingFunc(layer: LayerConfig): | ||||
|         ((params: ExtraFuncParams, feature: any) => void) { | ||||
|         const calculatedTags: [string, string][] = layer.calculatedTags; | ||||
|  | @ -92,11 +111,13 @@ export default class MetaTagging { | |||
|                                     d = JSON.stringify(d); | ||||
|                                 } | ||||
|                                 feature.properties[key] = d; | ||||
|                                 console.log("Written a delayed calculated tag onto ", feature.properties.id, ": ", key, ":==", d) | ||||
|                             }) | ||||
|                             result = result.data | ||||
|                         } | ||||
| 
 | ||||
|                         if (result === undefined || result === "") { | ||||
|                             console.log("Calculated tag for", key, "gave undefined", feature.properties.id) | ||||
|                             return; | ||||
|                         } | ||||
|                         if (typeof result !== "string") { | ||||
|  | @ -104,6 +125,7 @@ export default class MetaTagging { | |||
|                             result = JSON.stringify(result); | ||||
|                         } | ||||
|                         feature.properties[key] = result; | ||||
|                         console.log("Written a calculated tag onto ", feature.properties.id, ": ", key, ":==", result) | ||||
|                     } catch (e) { | ||||
|                         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) | ||||
|  |  | |||
|  | @ -94,6 +94,7 @@ export class OsmConnection { | |||
|                 self.AttemptLogin() | ||||
|             } | ||||
|         }); | ||||
|         this.isLoggedIn.addCallbackAndRunD(li => console.log("User is logged in!", li)) | ||||
|         this._dryRun = dryRun; | ||||
| 
 | ||||
|         this.updateAuthObject(); | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ export default class SimpleMetaTagger { | |||
|                 "_version_number"], | ||||
|             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; | ||||
| 
 | ||||
|  | @ -48,6 +48,7 @@ export default class SimpleMetaTagger { | |||
|             move("changeset", "_last_edit:changeset") | ||||
|             move("timestamp", "_last_edit:timestamp") | ||||
|             move("version", "_version_number") | ||||
|             return true; | ||||
|         } | ||||
|     ) | ||||
|     private static latlon = new SimpleMetaTagger({ | ||||
|  | @ -62,6 +63,7 @@ export default class SimpleMetaTagger { | |||
|             feature.properties["_lon"] = "" + lon; | ||||
|             feature._lon = lon; // This is dirty, I know
 | ||||
|             feature._lat = lat; | ||||
|             return true; | ||||
|         }) | ||||
|     ); | ||||
|     private static surfaceArea = new SimpleMetaTagger( | ||||
|  | @ -74,6 +76,7 @@ export default class SimpleMetaTagger { | |||
|             feature.properties["_surface"] = "" + sqMeters; | ||||
|             feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10; | ||||
|             feature.area = sqMeters; | ||||
|             return true; | ||||
|         }) | ||||
|     ); | ||||
| 
 | ||||
|  | @ -118,9 +121,7 @@ export default class SimpleMetaTagger { | |||
|                 } | ||||
| 
 | ||||
|             } | ||||
|             if (rewritten) { | ||||
|                 State.state.allElements.getEventSourceById(feature.id).ping(); | ||||
|             } | ||||
|             return rewritten | ||||
|         }) | ||||
|     ) | ||||
| 
 | ||||
|  | @ -135,6 +136,7 @@ export default class SimpleMetaTagger { | |||
|             const km = Math.floor(l / 1000) | ||||
|             const kmRest = Math.round((l - km * 1000) / 100) | ||||
|             feature.properties["_length:km"] = "" + km + "." + kmRest | ||||
|             return true; | ||||
|         }) | ||||
|     ) | ||||
|     private static country = new SimpleMetaTagger( | ||||
|  | @ -144,7 +146,6 @@ export default class SimpleMetaTagger { | |||
|         }, | ||||
|         feature => { | ||||
| 
 | ||||
| 
 | ||||
|             let centerPoint: any = GeoOperations.centerpoint(feature); | ||||
|             const lat = centerPoint.geometry.coordinates[1]; | ||||
|             const lon = centerPoint.geometry.coordinates[0]; | ||||
|  | @ -157,11 +158,11 @@ export default class SimpleMetaTagger { | |||
|                         const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); | ||||
|                         tagsSource.ping(); | ||||
|                     } | ||||
| 
 | ||||
|                 } catch (e) { | ||||
|                     console.warn(e) | ||||
|                 } | ||||
|             }) | ||||
|             return false; | ||||
|         } | ||||
|     ) | ||||
|     private static isOpen = new SimpleMetaTagger( | ||||
|  | @ -174,7 +175,7 @@ export default class SimpleMetaTagger { | |||
|             if (Utils.runningFromConsole) { | ||||
|                 // We are running from console, thus probably creating a cache
 | ||||
|                 // isOpen is irrelevant
 | ||||
|                 return | ||||
|                 return false | ||||
|             } | ||||
| 
 | ||||
|             const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); | ||||
|  | @ -199,7 +200,7 @@ export default class SimpleMetaTagger { | |||
|                         if (oldNextChange > (new Date()).getTime() && | ||||
|                             tags["_isOpen:oldvalue"] === tags["opening_hours"]) { | ||||
|                             // Already calculated and should not yet be triggered
 | ||||
|                             return; | ||||
|                             return false; | ||||
|                         } | ||||
| 
 | ||||
|                         tags["_isOpen"] = oh.getState() ? "yes" : "no"; | ||||
|  | @ -227,6 +228,7 @@ export default class SimpleMetaTagger { | |||
|                         } | ||||
|                     } | ||||
|                     updateTags(); | ||||
|                     return true; | ||||
|                 } catch (e) { | ||||
|                     console.warn("Error while parsing opening hours of ", tags.id, e); | ||||
|                     tags["_isOpen"] = "parse_error"; | ||||
|  | @ -244,11 +246,11 @@ export default class SimpleMetaTagger { | |||
|             const tags = feature.properties; | ||||
|             const direction = tags["camera:direction"] ?? tags["direction"]; | ||||
|             if (direction === undefined) { | ||||
|                 return; | ||||
|                 return false; | ||||
|             } | ||||
|             const n = cardinalDirections[direction] ?? Number(direction); | ||||
|             if (isNaN(n)) { | ||||
|                 return; | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             // 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:leftright"] = normalized <= 180 ? "right" : "left"; | ||||
| 
 | ||||
|             return true; | ||||
|         }) | ||||
|     ) | ||||
|     private static carriageWayWidth = new SimpleMetaTagger( | ||||
|  | @ -268,7 +270,7 @@ export default class SimpleMetaTagger { | |||
| 
 | ||||
|             const properties = feature.properties; | ||||
|             if (properties["width:carriageway"] === undefined) { | ||||
|                 return; | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             const carWidth = 2; | ||||
|  | @ -366,7 +368,7 @@ export default class SimpleMetaTagger { | |||
| 
 | ||||
|             properties["_width:difference"] = Utils.Round(targetWidth - width); | ||||
|             properties["_width:difference:no_pedestrians"] = Utils.Round(targetWidthIgnoringPedestrians - width); | ||||
| 
 | ||||
|             return true; | ||||
|         } | ||||
|     ); | ||||
|     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", | ||||
|             includesDates: true | ||||
|         }, | ||||
|         (feature, _, freshness) => { | ||||
|         (feature, freshness) => { | ||||
|             const now = new Date(); | ||||
| 
 | ||||
|             if (typeof freshness === "string") { | ||||
|  | @ -394,7 +396,7 @@ export default class SimpleMetaTagger { | |||
|             feature.properties["_now:datetime"] = datetime(now); | ||||
|             feature.properties["_loaded:date"] = date(freshness); | ||||
|             feature.properties["_loaded:datetime"] = datetime(freshness); | ||||
| 
 | ||||
|             return true; | ||||
|         } | ||||
|     ) | ||||
|     public static metatags = [ | ||||
|  | @ -413,12 +415,18 @@ export default class SimpleMetaTagger { | |||
|     public readonly keys: string[]; | ||||
|     public readonly doc: string; | ||||
|     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.doc = docs.doc; | ||||
|         this._f = f; | ||||
|         this.applyMetaTagsOnFeature = f; | ||||
|         this.includesDates = docs.includesDates ?? false; | ||||
|         for (const key of docs.keys) { | ||||
|             if (!key.startsWith('_') && key.toLowerCase().indexOf("theme") < 0) { | ||||
|  | @ -450,12 +458,4 @@ export default class SimpleMetaTagger { | |||
|         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 { | ||||
| 
 | ||||
|     public static vNumber = "0.10.0-alpha-1"; | ||||
|     public static ImgurApiKey = '7070e7167f0a25a' | ||||
| 
 | ||||
|     // The user journey states thresholds when a new feature gets unlocked
 | ||||
|     public static userJourney = { | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ import FilterConfig from "./FilterConfig"; | |||
| import {Unit} from "../Unit"; | ||||
| import DeleteConfig from "./DeleteConfig"; | ||||
| import Svg from "../../Svg"; | ||||
| import Img from "../../UI/Base/Img"; | ||||
| 
 | ||||
| export default class LayerConfig { | ||||
|     static WAYHANDLING_DEFAULT = 0; | ||||
|  | @ -495,19 +496,20 @@ export default class LayerConfig { | |||
|         const iconUrlStatic = render(this.icon); | ||||
|         const self = this; | ||||
| 
 | ||||
|         function genHtmlFromString(sourcePart: string, rotation: string, style?: string): BaseUIElement { | ||||
|             style = style ?? `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; | ||||
|         function genHtmlFromString(sourcePart: string, rotation: string): BaseUIElement { | ||||
|             const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; | ||||
|             let html: BaseUIElement = new FixedUiElement( | ||||
|                 `<img src="${sourcePart}" style="${style}" />` | ||||
|             ); | ||||
|             const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/); | ||||
|             if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) { | ||||
|                 html = new Combine([ | ||||
|                 html = new Img( | ||||
|                     (Svg.All[match[1] + ".svg"] as string).replace( | ||||
|                         /#000000/g, | ||||
|                         match[2] | ||||
|                     ), | ||||
|                 ]).SetStyle(style); | ||||
|                     true | ||||
|                 ).SetStyle(style); | ||||
|             } | ||||
|             return html; | ||||
|         } | ||||
|  | @ -540,7 +542,7 @@ export default class LayerConfig { | |||
|                         .filter((prt) => prt != ""); | ||||
| 
 | ||||
|                     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( | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; | |||
| import AllKnownLayers from "../../Customizations/AllKnownLayers"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import LayerConfig from "./LayerConfig"; | ||||
| import {Unit} from "../Unit"; | ||||
| import {LayerConfigJson} from "./Json/LayerConfigJson"; | ||||
| 
 | ||||
| export default class LayoutConfig { | ||||
|  | @ -87,6 +86,9 @@ export default class LayoutConfig { | |||
|         this.startZoom = json.startZoom; | ||||
|         this.startLat = json.startLat; | ||||
|         this.startLon = json.startLon; | ||||
|         if(json.widenFactor < 1){ | ||||
|             throw "Widenfactor too small" | ||||
|         } | ||||
|         this.widenFactor = json.widenFactor ?? 1.5; | ||||
|         this.roamingRenderings = (json.roamingRenderings ?? []).map((tr, i) => { | ||||
|                 if (typeof tr === "string") { | ||||
|  |  | |||
|  | @ -6,3 +6,105 @@ export interface TileRange { | |||
|     total: 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) | ||||
|             .SetClass("hidden md:block"); | ||||
|         this._fullscreencomponent = this.BuildComponent(title("mobile"), content("mobile"), isShown); | ||||
| 
 | ||||
|          | ||||
|         const self = this; | ||||
|         isShown.addCallback(isShown => { | ||||
|             if (isShown) { | ||||
|  |  | |||
|  | @ -2,22 +2,23 @@ import {UIEventSource} from "../../Logic/UIEventSource"; | |||
| import BaseUIElement from "../BaseUIElement"; | ||||
| 
 | ||||
| export class VariableUiElement extends BaseUIElement { | ||||
|     private _element: HTMLElement; | ||||
|     private readonly _contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>; | ||||
| 
 | ||||
|     constructor( | ||||
|         contents: UIEventSource<string | BaseUIElement | BaseUIElement[]> | ||||
|     ) { | ||||
|     constructor(contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>) { | ||||
|         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) { | ||||
|                 el.removeChild(el.lastChild); | ||||
|             } | ||||
| 
 | ||||
|             if (contents === undefined) { | ||||
|                 return el; | ||||
|                 return; | ||||
|             } | ||||
|             if (typeof contents === "string") { | ||||
|                 el.innerHTML = contents; | ||||
|  | @ -35,9 +36,6 @@ export class VariableUiElement extends BaseUIElement { | |||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         return this._element; | ||||
|         return el; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -100,8 +100,6 @@ export default abstract class BaseUIElement { | |||
|             throw "ERROR! This is not a correct baseUIElement: " + this.constructor.name | ||||
|         } | ||||
|         try { | ||||
| 
 | ||||
| 
 | ||||
|             const el = this.InnerConstructElement(); | ||||
| 
 | ||||
|             if (el === undefined) { | ||||
|  |  | |||
|  | @ -13,17 +13,16 @@ export default class Attribution extends VariableUiElement { | |||
|         } | ||||
|         super( | ||||
|             license.map((license: LicenseInfo) => { | ||||
| 
 | ||||
|                 if (license?.artist === undefined) { | ||||
|                     return undefined; | ||||
|                 if(license === undefined){ | ||||
|                     return undefined | ||||
|                 } | ||||
|                  | ||||
|                 return new Combine([ | ||||
|                     icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"), | ||||
| 
 | ||||
|                     new Combine([ | ||||
|                         Translations.W(license.artist).SetClass("block font-bold"), | ||||
|                         Translations.W((license.license ?? "") === "" ? "CC0" : (license.license ?? "")) | ||||
|                         Translations.W(license?.artist ?? ".").SetClass("block font-bold"), | ||||
|                         Translations.W((license?.license ?? "") === "" ? "CC0" : (license?.license ?? "")) | ||||
|                     ]).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") | ||||
| 
 | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ export default class DeleteImage extends Toggle { | |||
|                 tags.map(tags => (tags[key] ?? "") !== "") | ||||
|             ), | ||||
|             undefined /*Login (and thus editing) is disabled*/, | ||||
|             State.state?.featureSwitchUserbadge ?? new UIEventSource<boolean>(true) | ||||
|             State.state.osmConnection.isLoggedIn | ||||
|         ) | ||||
|         this.SetClass("cursor-pointer") | ||||
|     } | ||||
|  |  | |||
|  | @ -9,7 +9,6 @@ import State from "../../State"; | |||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||
| import {BBox, GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; | ||||
| import * as L from "leaflet"; | ||||
| import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; | ||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
|  |  | |||
|  | @ -31,8 +31,10 @@ export default class EditableTagRendering extends Toggle { | |||
| 
 | ||||
| 
 | ||||
|             const answerWithEditButton = new Combine([answer, | ||||
|                 new Toggle(editButton, undefined, State.state.osmConnection.isLoggedIn)]) | ||||
|                 .SetClass("flex justify-between w-full") | ||||
|                 new Toggle(editButton,  | ||||
|                     undefined, | ||||
|                     State.state.osmConnection.isLoggedIn) | ||||
|             ]).SetClass("flex justify-between w-full") | ||||
| 
 | ||||
| 
 | ||||
|             const cancelbutton = | ||||
|  |  | |||
|  | @ -71,7 +71,7 @@ export default class SplitRoadWizard extends Toggle { | |||
|         }) | ||||
| 
 | ||||
|         new ShowDataMultiLayer({ | ||||
|             features: new StaticFeatureSource([roadElement]), | ||||
|             features: new StaticFeatureSource([roadElement], false), | ||||
|             layers: State.state.filteredLayers, | ||||
|             leafletMap: miniMap.leafletMap, | ||||
|             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)); | ||||
|         this.update(options); | ||||
| 
 | ||||
| 
 | ||||
|         State.state.selectedElement.addCallbackAndRunD(selected => { | ||||
|             if (self._leafletMap.data === undefined) { | ||||
|                 return; | ||||
|             } | ||||
|             const v = self.leafletLayersPerId.get(selected.properties.id) | ||||
|             if(v === undefined){return;} | ||||
|             if (v === undefined) { | ||||
|                 return; | ||||
|             } | ||||
|             const leafletLayer = v.leafletlayer | ||||
|             const feature = v.feature | ||||
|             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) { | ||||
|  | @ -83,21 +99,19 @@ export default class ShowDataLayer { | |||
|             mp.removeLayer(this.geoLayer); | ||||
|         } | ||||
| 
 | ||||
|         this.geoLayer= this.CreateGeojsonLayer() | ||||
|         this.geoLayer = this.CreateGeojsonLayer() | ||||
|         const allFeats = this._features.data; | ||||
|         for (const feat of allFeats) { | ||||
|             if (feat === undefined) { | ||||
|                 continue | ||||
|             } | ||||
|             try{ | ||||
|             try { | ||||
|                 this.geoLayer.addData(feat); | ||||
|             }catch(e){ | ||||
|             } catch (e) { | ||||
|                 console.error("Could not add ", feat, "to the geojson layer in leaflet") | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         mp.addLayer(this.geoLayer) | ||||
| 
 | ||||
|         if (options.zoomToFeatures ?? false) { | ||||
|             try { | ||||
|                 mp.fitBounds(this.geoLayer.getBounds(), {animate: false}) | ||||
|  | @ -105,6 +119,10 @@ export default class ShowDataLayer { | |||
|                 console.error(e) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (options.doShowLayer?.data ?? true) { | ||||
|             mp.addLayer(this.geoLayer) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -125,7 +143,8 @@ export default class ShowDataLayer { | |||
|             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 style = layer.GenerateLeafletStyle(tagSource, clickable); | ||||
|         const baseElement = style.icon.html; | ||||
|  | @ -193,8 +212,10 @@ export default class ShowDataLayer { | |||
|             infobox.Activate(); | ||||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|         // Add the feature to the index to open the popup when needed
 | ||||
|         this.leafletLayersPerId.set(feature.properties.id, {feature: feature, leafletlayer: leafletLayer}) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private CreateGeojsonLayer(): L.Layer { | ||||
|  |  | |||
|  | @ -6,4 +6,5 @@ export interface ShowDataLayerOptions { | |||
|     leafletMap: UIEventSource<L.Map>, | ||||
|     enablePopups?: true | 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 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 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); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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 { | ||||
|         stringified = stringified.replace(/\|/g, "||"); | ||||
| 
 | ||||
|  | @ -345,16 +287,7 @@ export class Utils { | |||
|         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 = {} | ||||
| 
 | ||||
|  | @ -362,7 +295,7 @@ export class Utils { | |||
|         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] | ||||
|         if (injected !== undefined) { | ||||
|  | @ -371,7 +304,7 @@ export class Utils { | |||
|         } | ||||
| 
 | ||||
|         if (this.externalDownloadFunction !== undefined) { | ||||
|             return this.externalDownloadFunction(url) | ||||
|             return this.externalDownloadFunction(url, headers) | ||||
|         } | ||||
| 
 | ||||
|         return new Promise((resolve, reject) => { | ||||
|  | @ -379,7 +312,6 @@ export class Utils { | |||
|                 xhr.onload = () => { | ||||
|                     if (xhr.status == 200) { | ||||
|                         try { | ||||
|                             console.log("Got a response! Parsing now...") | ||||
|                             resolve(JSON.parse(xhr.response)) | ||||
|                         } catch (e) { | ||||
|                             reject("Not a valid json: " + xhr.response) | ||||
|  | @ -390,6 +322,13 @@ export class Utils { | |||
|                 }; | ||||
|                 xhr.open('GET', url); | ||||
|                 xhr.setRequestHeader("accept", "application/json") | ||||
|                 if (headers !== undefined) { | ||||
| 
 | ||||
|                     for (const key in headers) { | ||||
|                         xhr.setRequestHeader(key, headers[key]) | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 xhr.send(); | ||||
|             } | ||||
|         ) | ||||
|  | @ -449,22 +388,6 @@ export class Utils { | |||
|         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 }) { | ||||
|         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 | ||||
|     } | ||||
| 
 | ||||
|     public static async waitFor(timeMillis: number): Promise<void> { | ||||
|         return new Promise((resolve) => { | ||||
|             window.setTimeout(resolve, timeMillis); | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1592,8 +1592,13 @@ | |||
|         { | ||||
|             "#": "plugs-9", | ||||
|             "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?", | ||||
|                 "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?" | ||||
|                 "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?", | ||||
|                 "it": "Quali sono gli orari di apertura di questa stazione di ricarica?", | ||||
|                 "ja": "この充電ステーションはいつオープンしますか?", | ||||
|                 "nb_NO": "Når åpnet denne ladestasjonen?", | ||||
|                 "ru": "В какое время работает эта зарядная станция?", | ||||
|                 "zh_Hant": "何時是充電站開放使用的時間?" | ||||
|             }, | ||||
|             "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", | ||||
|  | @ -1608,17 +1613,52 @@ | |||
|                     "socket:type2_cable~*", | ||||
|                     "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", | ||||
|             "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?", | ||||
|                 "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'/>" | ||||
|                 "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'/>", | ||||
|                 "it": "A quale rete appartiene questa stazione di ricarica?", | ||||
|                 "ja": "この充電ステーションの運営チェーンはどこですか?", | ||||
|                 "ru": "К какой сети относится эта станция?", | ||||
|                 "zh_Hant": "充電站所屬的網路是?" | ||||
|             }, | ||||
|             "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", | ||||
|                 "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" | ||||
|                 "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", | ||||
|                 "it": "{network}", | ||||
|                 "ja": "{network}", | ||||
|                 "nb_NO": "{network}", | ||||
|                 "ru": "{network}", | ||||
|                 "zh_Hant": "{network}" | ||||
|             }, | ||||
|             "freeform": { | ||||
|                 "key": "socket:type2_cable:voltage", | ||||
|  | @ -1650,7 +1690,7 @@ | |||
|         { | ||||
|             "#": "current-9", | ||||
|             "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'/>?" | ||||
|             }, | ||||
|             "render": { | ||||
|  | @ -1665,7 +1705,7 @@ | |||
|                 { | ||||
|                     "if": "socket:socket:type2_cable:current=16 A", | ||||
|                     "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" | ||||
|                     } | ||||
|                 }, | ||||
|  | @ -1687,12 +1727,12 @@ | |||
|         { | ||||
|             "#": "power-output-9", | ||||
|             "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?", | ||||
|                 "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'/>?" | ||||
|                 "en": "How much does one have to pay to use this charging station?", | ||||
|                 "nl": "Hoeveel kost het gebruik van dit oplaadpunt?" | ||||
|             }, | ||||
|             "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}", | ||||
|                 "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}" | ||||
|                 "en": "Using this charging station costs <b>{charge}</b>", | ||||
|                 "nl": "Dit oplaadpunt gebruiken kost <b>{charge}</b>" | ||||
|             }, | ||||
|             "freeform": { | ||||
|                 "key": "socket:type2_cable:output", | ||||
|  | @ -1702,8 +1742,8 @@ | |||
|                 { | ||||
|                     "if": "socket:socket:type2_cable:output=11 kw", | ||||
|                     "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", | ||||
|                         "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" | ||||
|                         "en": "Free to use", | ||||
|                         "nl": "Gratis te gebruiken" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|  | @ -1740,17 +1780,31 @@ | |||
|                     "socket:tesla_supercharger_ccs~*", | ||||
|                     "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", | ||||
|             "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?", | ||||
|                 "nl": "Welke spanning levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/>" | ||||
|                 "en": "What is the maximum amount of time one is allowed to stay here?", | ||||
|                 "nl": "Hoelang mag een voertuig hier blijven staan?" | ||||
|             }, | ||||
|             "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", | ||||
|                 "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" | ||||
|                 "en": "One can stay at most <b>{canonical(maxstay)}</b>", | ||||
|                 "nl": "De maximale parkeertijd hier is <b>{canonical(maxstay)}</b>" | ||||
|             }, | ||||
|             "freeform": { | ||||
|                 "key": "socket:tesla_supercharger_ccs:voltage", | ||||
|  | @ -1760,8 +1814,8 @@ | |||
|                 { | ||||
|                     "if": "socket:socket:tesla_supercharger_ccs:voltage=500 V", | ||||
|                     "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", | ||||
|                         "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> heeft een spanning van 500 volt" | ||||
|                         "en": "No timelimit on leaving your vehicle here", | ||||
|                         "nl": "Geen maximum parkeertijd" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|  | @ -1782,11 +1836,11 @@ | |||
|         { | ||||
|             "#": "current-10", | ||||
|             "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'/>?" | ||||
|             }, | ||||
|             "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" | ||||
|             }, | ||||
|             "freeform": { | ||||
|  | @ -1797,14 +1851,14 @@ | |||
|                 { | ||||
|                     "if": "socket:socket:tesla_supercharger_ccs:current=125 A", | ||||
|                     "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" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "if": "socket:socket:tesla_supercharger_ccs:current=350 A", | ||||
|                     "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" | ||||
|                     } | ||||
|                 } | ||||
|  | @ -1849,11 +1903,11 @@ | |||
|         { | ||||
|             "#": "plugs-11", | ||||
|             "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?" | ||||
|             }, | ||||
|             "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 " | ||||
|             }, | ||||
|             "freeform": { | ||||
|  | @ -1870,11 +1924,11 @@ | |||
|         { | ||||
|             "#": "voltage-11", | ||||
|             "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'/>" | ||||
|             }, | ||||
|             "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" | ||||
|             }, | ||||
|             "freeform": { | ||||
|  | @ -1900,11 +1954,11 @@ | |||
|         { | ||||
|             "#": "current-11", | ||||
|             "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'/>?" | ||||
|             }, | ||||
|             "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" | ||||
|             }, | ||||
|             "freeform": { | ||||
|  | @ -1981,7 +2035,7 @@ | |||
|         { | ||||
|             "#": "plugs-12", | ||||
|             "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?" | ||||
|             }, | ||||
|             "render": { | ||||
|  | @ -2002,8 +2056,8 @@ | |||
|         { | ||||
|             "#": "voltage-12", | ||||
|             "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?", | ||||
|                 "nl": "Welke spanning levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/>" | ||||
|                 "en": "Is this charging point in use?", | ||||
|                 "nl": "Is dit oplaadpunt operationeel?" | ||||
|             }, | ||||
|             "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", | ||||
|  | @ -2017,15 +2071,15 @@ | |||
|                 { | ||||
|                     "if": "socket:socket:tesla_destination:voltage=230 V", | ||||
|                     "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", | ||||
|                         "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft een spanning van 230 volt" | ||||
|                         "en": "This charging station is broken", | ||||
|                         "nl": "Dit oplaadpunt is kapot" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "if": "socket:socket:tesla_destination:voltage=400 V", | ||||
|                     "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", | ||||
|                         "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft een spanning van 400 volt" | ||||
|                         "en": "A charging station is planned here", | ||||
|                         "nl": "Hier zal binnenkort een oplaadpunt gebouwd worden" | ||||
|                     } | ||||
|                 } | ||||
|             ], | ||||
|  | @ -2296,6 +2350,14 @@ | |||
|                             "en": "Payment is done using a dedicated app", | ||||
|                             "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": [ | ||||
|  |  | |||
|  | @ -48,8 +48,9 @@ | |||
|         } | ||||
|     }, | ||||
|     "calculatedTags": [ | ||||
|     "_closest_other_drinking_water_id=feat.closest('drinking_water')?.id", | ||||
|     "_closest_other_drinking_water_distance=Math.floor(feat.distanceTo(feat.closest('drinking_water')).distance * 1000)" | ||||
|         "_closest_other_drinking_water=feat.closestn('drinking_water', 1, 500).map(f => ({id: f.feat.id, distance: f.distance}))[0]", | ||||
|         "_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, | ||||
|     "wayHandling": 1, | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ | |||
|             ] | ||||
|         } | ||||
|     }, | ||||
|     "minzoom": 12, | ||||
|     "wayHandling": 1, | ||||
|     "icon": { | ||||
|         "render": "circle:white;./assets/layers/food/restaurant.svg", | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ | |||
|     "source": { | ||||
|         "osmTags": "amenity=public_bookcase" | ||||
|     }, | ||||
|     "minzoom": 12, | ||||
|     "minzoom": 10, | ||||
|     "wayHandling": 2, | ||||
|     "title": { | ||||
|         "render": { | ||||
|  |  | |||
|  | @ -52,7 +52,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 1.5, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     "bench", | ||||
|  |  | |||
|  | @ -40,7 +40,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 1.5, | ||||
|   "roamingRenderings": [], | ||||
|   "layers": [ | ||||
|     "bicycle_library" | ||||
|  |  | |||
|  | @ -47,7 +47,7 @@ | |||
|   "startLat": 50.8435, | ||||
|   "startLon": 4.3688, | ||||
|   "startZoom": 14, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 1.5, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     "bike_monitoring_station" | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 1.5, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     "binocular" | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ | |||
|   "startLat": 50.8435, | ||||
|   "startLon": 4.3688, | ||||
|   "startZoom": 16, | ||||
|   "widenFactor": 0.01, | ||||
|   "widenFactor": 1.2, | ||||
|   "socialImage": "./assets/themes/buurtnatuur/social_image.jpg", | ||||
|   "layers": [ | ||||
|     { | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 1.5, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     "cafe_pub" | ||||
|  |  | |||
|  | @ -47,7 +47,7 @@ | |||
|   "startLat": 43.14, | ||||
|   "startLon": 3.14, | ||||
|   "startZoom": 14, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 1.5, | ||||
|   "socialImage": "./assets/themes/campersite/Bar%C3%9Fel_Wohnmobilstellplatz.jpg", | ||||
|   "layers": [ | ||||
|     { | ||||
|  |  | |||
|  | @ -39,7 +39,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 1.5, | ||||
|   "socialImage": "", | ||||
|   "defaultBackgroundId": "CartoDB.Voyager", | ||||
|   "layers": [ | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 1.5, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     { | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ | |||
|   "clustering": { | ||||
|     "maxZoom": 1 | ||||
|   }, | ||||
|   "widenFactor": 0.005, | ||||
|   "widenFactor": 1.1, | ||||
|   "enableDownload": true, | ||||
|   "enablePdfDownload": true, | ||||
|   "layers": [ | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ | |||
|   "startLat": 51, | ||||
|   "startLon": 3.75, | ||||
|   "startZoom": 11, | ||||
|   "widenFactor": 1, | ||||
|   "widenFactor": 1.5, | ||||
|   "socialImage": "./assets/themes/cycle_infra/cycle-infra.svg", | ||||
|   "enableDownload": true, | ||||
|   "layers": [ | ||||
|  |  | |||
|  | @ -40,7 +40,7 @@ | |||
|   "defaultBackgroundId": "CartoDB.Voyager", | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 2, | ||||
|   "socialImage": "assets/themes/cyclofix/logo.svg", | ||||
|   "layers": [ | ||||
|     "bike_cafe", | ||||
|  |  | |||
|  | @ -38,7 +38,7 @@ | |||
|   "startLat": 51.02768, | ||||
|   "startLon": 4.480705, | ||||
|   "startZoom": 15, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 1.5, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     { | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 3, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     "food" | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 3, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     { | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.001, | ||||
|   "widenFactor": 2, | ||||
|   "socialImage": "", | ||||
|   "hideFromOverview": true, | ||||
|   "layers": [ | ||||
|  |  | |||
|  | @ -52,7 +52,7 @@ | |||
|   "startZoom": 1, | ||||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "widenFactor": 0.1, | ||||
|   "widenFactor": 5, | ||||
|   "layers": [ | ||||
|     "ghost_bike" | ||||
|   ], | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ | |||
|   "startLat": 51.2132, | ||||
|   "startLon": 3.231, | ||||
|   "startZoom": 14, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 2, | ||||
|   "cacheTimeout": 3600, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 5, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     { | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ | |||
|   "startLat": 13.67801, | ||||
|   "startLon": 121.6625, | ||||
|   "startZoom": 6, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 3, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     { | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 5, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     "map" | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ | |||
|   "startLat": 51.20875, | ||||
|   "startLon": 3.22435, | ||||
|   "startZoom": 12, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 2, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     "drinking_water", | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ | |||
|   "startLat": 51.20875, | ||||
|   "startLon": 3.22435, | ||||
|   "startZoom": 15, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 2, | ||||
|   "socialImage": "", | ||||
|   "defaultBackgroundId": "CartoDB.Positron", | ||||
|   "enablePdfDownload": true, | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 5, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     "observation_tower" | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ | |||
|   "startLat": 51.20875, | ||||
|   "startLon": 3.22435, | ||||
|   "startZoom": 12, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 1.2, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     "parking" | ||||
|  |  | |||
|  | @ -41,7 +41,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 16, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 3, | ||||
|   "layers": [], | ||||
|   "roamingRenderings": [] | ||||
| } | ||||
|  | @ -19,7 +19,7 @@ | |||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "hideFromOverview": true, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 3, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     "play_forest" | ||||
|  |  | |||
|  | @ -38,7 +38,7 @@ | |||
|   "startLat": 50.535, | ||||
|   "startLon": 4.399, | ||||
|   "startZoom": 13, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 5, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     "playground" | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 3, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     { | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ | |||
|   "startLat": 51.17174, | ||||
|   "startLon": 4.449462, | ||||
|   "startZoom": 12, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 1.2, | ||||
|   "socialImage": "./assets/themes/speelplekken/social_image.jpg", | ||||
|   "defaultBackgroundId": "CartoDB.Positron", | ||||
|   "layers": [ | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ | |||
|   "startLat": 51.17174, | ||||
|   "startLon": 4.449462, | ||||
|   "startZoom": 12, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 2, | ||||
|   "socialImage": "", | ||||
|   "defaultBackgroundId": "CartoDB.Positron", | ||||
|   "layers": [ | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 2, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     "sport_pitch" | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 2, | ||||
|   "socialImage": "", | ||||
|   "defaultBackgroundId": "osm", | ||||
|   "layers": [ | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ | |||
|   "startZoom": 8, | ||||
|   "startLat": 50.8536, | ||||
|   "startLon": 4.433, | ||||
|   "widenFactor": 0.2, | ||||
|   "widenFactor": 2, | ||||
|   "layers": [ | ||||
|     { | ||||
|       "builtin": [ | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ | |||
|   "startZoom": 12, | ||||
|   "startLat": 51.2095, | ||||
|   "startLon": 3.2222, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 3, | ||||
|   "icon": "./assets/themes/toilets/toilets.svg", | ||||
|   "layers": [ | ||||
|     "toilet" | ||||
|  |  | |||
|  | @ -45,7 +45,7 @@ | |||
|   "startLat": 50.642, | ||||
|   "startLon": 4.482, | ||||
|   "startZoom": 8, | ||||
|   "widenFactor": 0.01, | ||||
|   "widenFactor": 1.5, | ||||
|   "socialImage": "./assets/themes/trees/logo.svg", | ||||
|   "clustering": { | ||||
|     "maxZoom": 18 | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ | |||
|   "startLat": -0.08528530407, | ||||
|   "startLon": 51.52103754846, | ||||
|   "startZoom": 18, | ||||
|   "widenFactor": 0.5, | ||||
|   "widenFactor": 1.5, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     { | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ | |||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|   "startZoom": 1, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 2, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     { | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ | |||
|   "startLat": 51.20875, | ||||
|   "startLon": 3.22435, | ||||
|   "startZoom": 14, | ||||
|   "widenFactor": 0.05, | ||||
|   "widenFactor": 2, | ||||
|   "socialImage": "", | ||||
|   "layers": [ | ||||
|     { | ||||
|  |  | |||
|  | @ -48,13 +48,10 @@ export default class ScriptUtils { | |||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public static DownloadJSON(url, options?: { | ||||
|         headers: any | ||||
|     }): Promise<any> { | ||||
|     public static DownloadJSON(url, headers?: any): Promise<any> { | ||||
|         return new Promise((resolve, reject) => { | ||||
|             try { | ||||
| 
 | ||||
|                 const headers = options?.headers ?? {} | ||||
|                 headers = headers ?? {} | ||||
|                 headers.accept = "application/json" | ||||
|                 console.log("Fetching", url) | ||||
|                 const urlObj = new URL(url) | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ import RelationsTracker from "../Logic/Osm/RelationsTracker"; | |||
| import * as OsmToGeoJson from "osmtogeojson"; | ||||
| import MetaTagging from "../Logic/MetaTagging"; | ||||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| import {TileRange} from "../Models/TileRange"; | ||||
| import {TileRange, Tiles} from "../Models/TileRange"; | ||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||
| import ScriptUtils from "./ScriptUtils"; | ||||
| 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) | ||||
| 
 | ||||
|             const boundsArr = Utils.tile_bounds(r.zoomlevel, x, y) | ||||
|             const boundsArr = Tiles.tile_bounds(r.zoomlevel, x, y) | ||||
|             const bounds = { | ||||
|                 north: Math.max(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) | ||||
|         } | ||||
|     } | ||||
|     return new StaticFeatureSource(allFeatures) | ||||
|     return new StaticFeatureSource(allFeatures, false) | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -225,7 +225,7 @@ function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsT | |||
|                     delete feature.feature["bbox"] | ||||
|                 } | ||||
|                 // 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) | ||||
|                 const targetPath = geoJsonName(targetdir + "_" + layerId, x, y, z) | ||||
|                 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
 | ||||
|         const path = targetdir + "_" + layerId + "_overview.json" | ||||
|         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 | ||||
|             if (perX[key] === undefined) { | ||||
|                 perX[key] = [] | ||||
|  | @ -279,7 +279,7 @@ async function main(args: string[]) { | |||
|     const lat1 = Number(args[5]) | ||||
|     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) | ||||
|     if (theme === undefined) { | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ class TranslationPart { | |||
|             } | ||||
|             const v = translations[translationsKey] | ||||
|             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" | ||||
|             } | ||||
|             this.contents.set(translationsKey, v) | ||||
|  | @ -41,9 +41,7 @@ class TranslationPart { | |||
|     } | ||||
| 
 | ||||
|     recursiveAdd(object: any, context: string) { | ||||
| 
 | ||||
| 
 | ||||
|         const isProbablyTranslationObject = knownLanguages.map(l => object.hasOwnProperty(l)).filter(x => x).length > 0; | ||||
|         const isProbablyTranslationObject = knownLanguages.some(l => object.hasOwnProperty(l)); | ||||
|         if (isProbablyTranslationObject) { | ||||
|             this.addTranslationObject(object, context) | ||||
|             return; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue