From c5e9448720f5b1afd0ea8b380492be3300dc2a1d Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 26 Sep 2021 17:36:39 +0200 Subject: [PATCH] Add initial clustering per tile, very broken --- Docs/Tools/GenerateSeries.ts | 4 +- InitUiElements.ts | 118 +++++-- Logic/Actors/OverpassFeatureSource.ts | 79 +++-- Logic/ExtraFunction.ts | 5 +- Logic/FeatureSource/FeaturePipeline.ts | 31 +- .../Sources/FeatureSourceMerger.ts | 3 +- .../Sources/FilteringFeatureSource.ts | 26 +- Logic/FeatureSource/Sources/GeoJsonSource.ts | 6 +- .../Sources/RememberingSource.ts | 14 +- .../Sources/SimpleFeatureSource.ts | 3 +- .../Sources/StaticFeatureSource.ts | 7 +- .../WayHandlingApplyingFeatureSource.ts | 5 +- .../DynamicGeoJsonTileSource.ts | 4 +- .../TiledFeatureSource/DynamicTileSource.ts | 7 +- .../TiledFeatureSource/TileHierarchyMerger.ts | 5 +- .../TiledFeatureSource/TiledFeatureSource.ts | 17 +- .../TiledFromLocalStorageSource.ts | 13 +- Logic/GeoOperations.ts | 11 +- .../ImageProviders/ImageAttributionSource.ts | 9 +- Logic/ImageProviders/Imgur.ts | 52 +-- Logic/ImageProviders/Mapillary.ts | 53 ++- Logic/ImageProviders/Wikimedia.ts | 46 +-- Logic/MetaTagging.ts | 66 ++-- Logic/Osm/OsmConnection.ts | 1 + Logic/SimpleMetaTagger.ts | 52 +-- Models/Constants.ts | 1 + Models/ThemeConfig/LayerConfig.ts | 12 +- Models/ThemeConfig/LayoutConfig.ts | 4 +- Models/TileRange.ts | 102 ++++++ UI/Base/ScrollableFullScreen.ts | 2 + UI/Base/VariableUIElement.ts | 22 +- UI/BaseUIElement.ts | 2 - UI/Image/Attribution.ts | 11 +- UI/Image/DeleteImage.ts | 2 +- UI/Input/LocationInput.ts | 1 - UI/Popup/EditableTagRendering.ts | 6 +- UI/Popup/SplitRoadWizard.ts | 2 +- UI/ShowDataLayer/PerTileCountAggregator.ts | 156 +++++++++ UI/ShowDataLayer/ShowDataLayer.ts | 37 ++- UI/ShowDataLayer/ShowDataLayerOptions.ts | 1 + UI/ShowDataLayer/ShowTileInfo.ts | 79 +++++ Utils.ts | 105 +----- .../charging_station/charging_station.json | 136 +++++--- .../layers/drinking_water/drinking_water.json | 307 +++++++++--------- assets/layers/food/food.json | 1 + .../public_bookcase/public_bookcase.json | 2 +- assets/themes/benches/benches.json | 2 +- assets/themes/bicyclelib/bicyclelib.json | 2 +- .../bike_monitoring_stations.json | 2 +- assets/themes/binoculars/binoculars.json | 2 +- assets/themes/buurtnatuur/buurtnatuur.json | 2 +- .../themes/cafes_and_pubs/cafes_and_pubs.json | 2 +- assets/themes/campersite/campersite.json | 2 +- .../charging_stations/charging_stations.json | 2 +- assets/themes/climbing/climbing.json | 2 +- .../themes/cycle_highways/cycle_highways.json | 2 +- assets/themes/cycle_infra/cycle_infra.json | 2 +- assets/themes/cyclofix/cyclofix.json | 2 +- .../themes/facadegardens/facadegardens.json | 2 +- assets/themes/food/food.json | 2 +- assets/themes/fritures/fritures.json | 2 +- assets/themes/fruit_trees/fruit_trees.json | 2 +- assets/themes/ghostbikes/ghostbikes.json | 2 +- assets/themes/grb.json | 2 +- assets/themes/hackerspaces/hackerspaces.json | 2 +- assets/themes/hailhydrant/hailhydrant.json | 2 +- assets/themes/maps/maps.json | 2 +- assets/themes/nature/nature.json | 2 +- assets/themes/natuurpunt/natuurpunt.json | 2 +- .../observation_towers.json | 2 +- assets/themes/parkings/parkings.json | 2 +- assets/themes/personal/personal.json | 2 +- assets/themes/play_forests/play_forests.json | 2 +- assets/themes/playgrounds/playgrounds.json | 2 +- assets/themes/shops/shops.json | 2 +- assets/themes/speelplekken/speelplekken.json | 2 +- .../speelplekken/speelplekken_temp.json | 2 +- .../themes/sport_pitches/sport_pitches.json | 2 +- assets/themes/surveillance/surveillance.json | 2 +- .../toerisme_vlaanderen.json | 2 +- assets/themes/toilets/toilets.json | 2 +- assets/themes/trees/trees.json | 2 +- assets/themes/uk_addresses/uk_addresses.json | 2 +- assets/themes/waste_basket/waste_basket.json | 2 +- assets/themes/widths/width.json | 2 +- scripts/ScriptUtils.ts | 7 +- scripts/generateCache.ts | 12 +- scripts/generateTranslations.ts | 6 +- 88 files changed, 1080 insertions(+), 651 deletions(-) create mode 100644 UI/ShowDataLayer/PerTileCountAggregator.ts create mode 100644 UI/ShowDataLayer/ShowTileInfo.ts diff --git a/Docs/Tools/GenerateSeries.ts b/Docs/Tools/GenerateSeries.ts index 5cced5a6b..73e97e9a4 100644 --- a/Docs/Tools/GenerateSeries.ts +++ b/Docs/Tools/GenerateSeries.ts @@ -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) { diff --git a/InitUiElements.ts b/InitUiElements.ts index c36b22b77..0bc6506d1 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -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,22 +169,38 @@ export class InitUiElements { ).AttachTo("messagesbox"); } - State.state.osmConnection.userDetails - .map((userDetails: UserDetails) => userDetails?.home) - .addCallbackAndRunD((home) => { - const color = getComputedStyle(document.body).getPropertyValue( - "--subtle-detail-color" - ); - const icon = L.icon({ - iconUrl: Img.AsData( - Svg.home_white_bg.replace(/#ffffff/g, color) - ), - iconSize: [30, 30], - iconAnchor: [15, 15], - }); - const marker = L.marker([home.lat, home.lon], {icon: icon}); - marker.addTo(State.state.leafletMap.data); + 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" + ); + const icon = L.icon({ + iconUrl: Img.AsData( + Svg.home_white_bg.replace(/#ffffff/g, color) + ), + iconSize: [30, 30], + iconAnchor: [15, 15], }); + const marker = L.marker([home.lat, home.lon], {icon: icon}); + marker.addTo(leaflet); + return true; + } + + State.state.osmConnection.userDetails + .addCallbackAndRunD(_ => addHomeMarker()); + State.state.leafletMap.addCallbackAndRunD(_ => addHomeMarker()) if (layoutToUse.id === personal.id) { updateFavs(); @@ -209,7 +227,7 @@ export class InitUiElements { static LoadLayoutFromHash( userLayoutParam: UIEventSource ): [LayoutConfig, string] { - let hash = location.hash.substr(1); + let hash = location.hash.substr(1); try { const layoutFromBase64 = userLayoutParam.data; // layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter @@ -249,18 +267,18 @@ export class InitUiElements { userLayoutParam.setData(layoutToUse.id); 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 SubtleButton("./assets/svg/mapcomplete_logo.svg", - "Go back to the theme overview", - {url: window.location.protocol+"//"+ window.location.hostname+"/index.html", newTab: false}) - + 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}) + ]) .SetClass("flex flex-col") .AttachTo("centermessage"); @@ -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(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 diff --git a/Logic/Actors/OverpassFeatureSource.ts b/Logic/Actors/OverpassFeatureSource.ts index 1e7ace7cd..7ddafacd8 100644 --- a/Logic/Actors/OverpassFeatureSource.ts +++ b/Logic/Actors/OverpassFeatureSource.ts @@ -24,9 +24,9 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour public readonly sufficientlyZoomed: UIEventSource; public readonly runningQuery: UIEventSource = new UIEventSource(false); public readonly timeout: UIEventSource = new UIEventSource(0); - + public readonly relationsTracker: RelationsTracker; - + private readonly retries: UIEventSource = new UIEventSource(0); /** @@ -44,7 +44,6 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour readonly overpassUrl: UIEventSource; readonly overpassTimeout: UIEventSource; } - /** * 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; readonly overpassMaxZoom: UIEventSource }) { + 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 { if (this.runningQuery.data) { console.log("Still running a query, not updating"); return; @@ -179,54 +184,46 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour const self = this; const overpass = this.GetFilter(); - + if (overpass === undefined) { return; } this.runningQuery.setData(true); - overpass.queryGeoJson(queryBounds). - then(([data, date]) => { - 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){ - console.error("Got the overpass response, but could not process it: ", e, e.stack) - } - self.runningQuery.setData(false); - }) - .catch((reason) => { + 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.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.timeout.setData(self.retries.data * 5); 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 - ) + while (self.timeout.data > 0) { + await Utils.waitFor(1000) + self.timeout.data-- + self.timeout.ping(); } - - countDown(); - } - ); + } while (data === undefined); + + self._previousBounds.get(z).push(queryBounds); + self.retries.setData(0); + + try { + data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date)); + self.features.setData(data.features.map(f => ({feature: f, freshness: date}))); + } catch (e) { + console.error("Got the overpass response, but could not process it: ", e, e.stack) + } + self.runningQuery.setData(false); } diff --git a/Logic/ExtraFunction.ts b/Logic/ExtraFunction.ts index 04f7a64f6..5839fca08 100644 --- a/Logic/ExtraFunction.ts +++ b/Logic/ExtraFunction.ts @@ -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) { diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index c6206eac4..1e033dad7 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -37,7 +37,7 @@ export default class FeaturePipeline implements FeatureSourceState { private readonly perLayerHierarchy: Map; constructor( - handleFeatureSource: (source: FeatureSourceForLayer) => void, + handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void, state: { filteredLayers: UIEventSource, locationControl: UIEventSource, @@ -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() 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) diff --git a/Logic/FeatureSource/Sources/FeatureSourceMerger.ts b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts index fb349ae5d..44ae28543 100644 --- a/Logic/FeatureSource/Sources/FeatureSourceMerger.ts +++ b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts @@ -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(); diff --git a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts index 6848f9f26..70d5a566c 100644 --- a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts @@ -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, }, + 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; layerDef: LayerConfig; - }, - location: { zoom: number }) { - return layer.isDisplayed.data && - layer.layerDef.minzoomVisible <= location.zoom; + }) { + return layer.isDisplayed.data; } } diff --git a/Logic/FeatureSource/Sources/GeoJsonSource.ts b/Logic/FeatureSource/Sources/GeoJsonSource.ts index e6d24fbca..6222fc729 100644 --- a/Logic/FeatureSource/Sources/GeoJsonSource.ts +++ b/Logic/FeatureSource/Sources/GeoJsonSource.ts @@ -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; diff --git a/Logic/FeatureSource/Sources/RememberingSource.ts b/Logic/FeatureSource/Sources/RememberingSource.ts index 99f422478..b31097061 100644 --- a/Logic/FeatureSource/Sources/RememberingSource.ts +++ b/Logic/FeatureSource/Sources/RememberingSource.ts @@ -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; - - constructor(source: FeatureSource) { + public readonly tileIndex : number + public readonly bbox : BBox + + 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; diff --git a/Logic/FeatureSource/Sources/SimpleFeatureSource.ts b/Logic/FeatureSource/Sources/SimpleFeatureSource.ts index ad8d7be5d..b3a476f5f 100644 --- a/Logic/FeatureSource/Sources/SimpleFeatureSource.ts +++ b/Logic/FeatureSource/Sources/SimpleFeatureSource.ts @@ -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 + ")" diff --git a/Logic/FeatureSource/Sources/StaticFeatureSource.ts b/Logic/FeatureSource/Sources/StaticFeatureSource.ts index 0cc58d656..d5795a1d5 100644 --- a/Logic/FeatureSource/Sources/StaticFeatureSource.ts +++ b/Logic/FeatureSource/Sources/StaticFeatureSource.ts @@ -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, useFeaturesDirectly = false) { + constructor(features: any[] | UIEventSource>, 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 => ({ diff --git a/Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts b/Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts index 37ee94b8a..cb36c4b21 100644 --- a/Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts +++ b/Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts @@ -12,10 +12,11 @@ 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; - + if (layer.wayHandling === LayerConfig.WAYHANDLING_DEFAULT) { // We don't have to do anything fancy // lets just wire up the upstream diff --git a/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts b/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts index 357db85d4..8843b182a 100644 --- a/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts @@ -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 leafletMap: any diff --git a/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts b/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts index 43021587c..a6ef04458 100644 --- a/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts @@ -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 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 { public readonly loadedTiles: Map = new Map(); @@ -13,7 +14,7 @@ export class TileHierarchyMerger implements TileHierarchy 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([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) } diff --git a/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts index 62a7c9e5e..2dcf3e6d2 100644 --- a/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts @@ -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([]) @@ -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 } \ No newline at end of file diff --git a/Logic/FeatureSource/TiledFeatureSource/TiledFromLocalStorageSource.ts b/Logic/FeatureSource/TiledFeatureSource/TiledFromLocalStorageSource.ts index e88a1d82d..3e879bd4a 100644 --- a/Logic/FeatureSource/TiledFeatureSource/TiledFromLocalStorageSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/TiledFromLocalStorageSource.ts @@ -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 { public loadedTiles: Map = new Map(); @@ -17,6 +18,7 @@ export default class TiledFromLocalStorageSource implements TileHierarchy() 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 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 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(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) } } diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 66fd90053..dd806ded4 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -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() { diff --git a/Logic/ImageProviders/ImageAttributionSource.ts b/Logic/ImageProviders/ImageAttributionSource.ts index 7841c899b..1f0f097b7 100644 --- a/Logic/ImageProviders/ImageAttributionSource.ts +++ b/Logic/ImageProviders/ImageAttributionSource.ts @@ -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{ + public PrepareUrl(value: string): string | UIEventSource { return value; } - protected abstract DownloadAttribution(url: string): UIEventSource; + protected abstract DownloadAttribution(url: string): Promise; } \ No newline at end of file diff --git a/Logic/ImageProviders/Imgur.ts b/Logic/ImageProviders/Imgur.ts index 4325d3e37..f85f3228a 100644 --- a/Logic/ImageProviders/Imgur.ts +++ b/Logic/ImageProviders/Imgur.ts @@ -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,50 +87,27 @@ export class Imgur extends ImageAttributionSource { return undefined; } - protected DownloadAttribution(url: string): UIEventSource { - const src = new UIEventSource(undefined) - - + protected async DownloadAttribution(url: string): Promise { 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", ""); - } + 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", ""); + } - const licenseInfo = new LicenseInfo(); + const licenseInfo = new LicenseInfo(); - licenseInfo.licenseShortName = data.license; - licenseInfo.artist = data.author; + 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 } diff --git a/Logic/ImageProviders/Mapillary.ts b/Logic/ImageProviders/Mapillary.ts index 3f992dbce..ae5808a01 100644 --- a/Logic/ImageProviders/Mapillary.ts +++ b/Logic/ImageProviders/Mapillary.ts @@ -8,7 +8,7 @@ import {Utils} from "../../Utils"; export class Mapillary extends ImageAttributionSource { public static readonly singleton = new Mapillary(); - + private static readonly v4_cached_urls = new Map>(); private static readonly client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2' @@ -23,8 +23,8 @@ export class Mapillary extends ImageAttributionSource { isApiv4?: boolean } { 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))}; + const key = value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1) + return {key: key, isApiv4: !isNaN(Number(key))}; } const newApiFormat = value.match(/https?:\/\/www.mapillary.com\/app\/\?pKey=([0-9]*)/) if (newApiFormat !== null) { @@ -32,11 +32,11 @@ 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))}; } - + if (value.toLowerCase().startsWith("https://www.mapillary.com/map/im/")) { // Extract the key of the image @@ -62,48 +62,45 @@ 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(undefined) Mapillary.v4_cached_urls.set(key, source) Utils.downloadJson(metadataUrl).then( - json => { - console.warn("Got response on mapillary image", json, json["thumb_1024_url"]) - return source.setData(json["thumb_1024_url"]); - } + json => { + console.warn("Got response on mapillary image", json, json["thumb_1024_url"]) + return source.setData(json["thumb_1024_url"]); + } ) return source } } - protected DownloadAttribution(url: string): UIEventSource { + protected async DownloadAttribution(url: string): Promise { 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(license) - + return license + } const key = keyV.key - - const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2` - const source = new UIEventSource(undefined) - Utils.downloadJson(metadataURL).then(data => { - 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 + const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2` + 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; + + return license } } \ No newline at end of file diff --git a/Logic/ImageProviders/Wikimedia.ts b/Logic/ImageProviders/Wikimedia.ts index 32c762523..cd5aea5d5 100644 --- a/Logic/ImageProviders/Wikimedia.ts +++ b/Logic/ImageProviders/Wikimedia.ts @@ -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,43 +123,34 @@ export class Wikimedia extends ImageAttributionSource { .replace(/'/g, '%27'); } - protected DownloadAttribution(filename: string): UIEventSource { - - const source = new UIEventSource(undefined); - + protected async DownloadAttribution(filename: string): Promise { 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 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; - } + 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!") + return undefined; + } - licenseInfo.artist = license.Artist?.value; - licenseInfo.license = license.License?.value; - licenseInfo.copyrighted = license.Copyrighted?.value; - licenseInfo.attributionRequired = license.AttributionRequired?.value; - licenseInfo.usageTerms = license.UsageTerms?.value; - licenseInfo.licenseShortName = license.LicenseShortName?.value; - licenseInfo.credit = license.Credit?.value; - licenseInfo.description = license.ImageDescription?.value; - source.setData(licenseInfo); - } - ) - - return source; + licenseInfo.artist = license.Artist?.value; + licenseInfo.license = license.License?.value; + licenseInfo.copyrighted = license.Copyrighted?.value; + licenseInfo.attributionRequired = license.AttributionRequired?.value; + licenseInfo.usageTerms = license.UsageTerms?.value; + licenseInfo.licenseShortName = license.LicenseShortName?.value; + licenseInfo.credit = license.Credit?.value; + licenseInfo.description = license.ImageDescription?.value; + return licenseInfo; } diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index 5745fa4ad..c7eda184b 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -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"; /** @@ -20,50 +21,68 @@ export default class MetaTagging { * The given features should be part of the given layer */ public static addMetatags(features: { feature: any; freshness: Date }[], - params: ExtraFuncParams, - layer: LayerConfig, - options?: { - includeDates?: true | boolean, - includeNonDates?: true | boolean - }) { + params: ExtraFuncParams, + layer: LayerConfig, + options?: { + includeDates?: true | boolean, + includeNonDates?: true | boolean + }) { if (features === undefined || features.length === 0) { 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) } } - - } catch (e) { - console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e) } - } - // The functions - per layer - which add the new keys + // The calculated functions - per layer - which add the new keys const layerFuncs = this.createRetaggingFunc(layer) - if (layerFuncs !== undefined) { - for (const feature of features) { - try { - layerFuncs(params, feature.feature) - } catch (e) { - console.error(e) + 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) + } + } + + if(layerFuncs !== undefined){ + try { + 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) diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index 01d18f8cd..5a5e82612 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -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(); diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index 6e8f3e0fc..4a2dac23b 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -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); - } - } - - } diff --git a/Models/Constants.ts b/Models/Constants.ts index d6f212ac6..66ca7227b 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -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 = { diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts index 18c9fe3b4..2ed02ae6d 100644 --- a/Models/ThemeConfig/LayerConfig.ts +++ b/Models/ThemeConfig/LayerConfig.ts @@ -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( `` ); 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( diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index dd0d472ed..b632d8a29 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -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") { diff --git a/Models/TileRange.ts b/Models/TileRange.ts index e1dba5532..215d5a76f 100644 --- a/Models/TileRange.ts +++ b/Models/TileRange.ts @@ -5,4 +5,106 @@ export interface TileRange { yend: number, total: number, zoomlevel: number +} + +export class Tiles { + + public static MapRange(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 + } + } + } \ No newline at end of file diff --git a/UI/Base/ScrollableFullScreen.ts b/UI/Base/ScrollableFullScreen.ts index 4a158ec0a..da8114f8c 100644 --- a/UI/Base/ScrollableFullScreen.ts +++ b/UI/Base/ScrollableFullScreen.ts @@ -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) { diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index fbc3bb564..cf53daf10 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -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; - constructor( - contents: UIEventSource - ) { + constructor(contents: UIEventSource) { 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; } } diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index 99462acbd..a11db9319 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -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) { diff --git a/UI/Image/Attribution.ts b/UI/Image/Attribution.ts index 30040482f..9afd9b815 100644 --- a/UI/Image/Attribution.ts +++ b/UI/Image/Attribution.ts @@ -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") diff --git a/UI/Image/DeleteImage.ts b/UI/Image/DeleteImage.ts index a5ee19266..fe3f2cf09 100644 --- a/UI/Image/DeleteImage.ts +++ b/UI/Image/DeleteImage.ts @@ -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(true) + State.state.osmConnection.isLoggedIn ) this.SetClass("cursor-pointer") } diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index a350e38d3..0621eccc8 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -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"; diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index 4e5c313f5..4c5a13ea5 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -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 = diff --git a/UI/Popup/SplitRoadWizard.ts b/UI/Popup/SplitRoadWizard.ts index 01fb00d05..b1df0a5fa 100644 --- a/UI/Popup/SplitRoadWizard.ts +++ b/UI/Popup/SplitRoadWizard.ts @@ -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, diff --git a/UI/ShowDataLayer/PerTileCountAggregator.ts b/UI/ShowDataLayer/PerTileCountAggregator.ts new file mode 100644 index 000000000..82e247f74 --- /dev/null +++ b/UI/ShowDataLayer/PerTileCountAggregator.ts @@ -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 = new Map() + private readonly _requestedZoomLevel: UIEventSource; + + constructor(requestedZoomLevel: UIEventSource) { + 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>() + 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() + 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) { + 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> = new UIEventSource>(new Map()) + private readonly registeredLayers: Map = new Map(); + 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) { + 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]) + } + +} \ No newline at end of file diff --git a/UI/ShowDataLayer/ShowDataLayer.ts b/UI/ShowDataLayer/ShowDataLayer.ts index cdda7889b..278ea3597 100644 --- a/UI/ShowDataLayer/ShowDataLayer.ts +++ b/UI/ShowDataLayer/ShowDataLayer.ts @@ -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(feature.properties) : State.state.allElements.getEventSourceById(feature.properties.id) + const tagSource = feature.properties.id === undefined ? new UIEventSource(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 { diff --git a/UI/ShowDataLayer/ShowDataLayerOptions.ts b/UI/ShowDataLayer/ShowDataLayerOptions.ts index 349472351..2323ce067 100644 --- a/UI/ShowDataLayer/ShowDataLayerOptions.ts +++ b/UI/ShowDataLayer/ShowDataLayerOptions.ts @@ -6,4 +6,5 @@ export interface ShowDataLayerOptions { leafletMap: UIEventSource, enablePopups?: true | boolean, zoomToFeatures?: false | boolean, + doShowLayer?: UIEventSource } \ No newline at end of file diff --git a/UI/ShowDataLayer/ShowTileInfo.ts b/UI/ShowDataLayer/ShowTileInfo.ts new file mode 100644 index 000000000..a2fa322b6 --- /dev/null +++ b/UI/ShowDataLayer/ShowTileInfo.ts @@ -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: "
{count}
" + } + }, "tileinfo", true) + + constructor(options: { + source: FeatureSource & Tiled, leafletMap: UIEventSource, layer?: LayerConfig, + doShowLayer?: UIEventSource + }) { + + + const source = options.source + const metaFeature: UIEventSource = + 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 + }) + + } + +} \ No newline at end of file diff --git a/Utils.ts b/Utils.ts index 9f1373498..477598f79 100644 --- a/Utils.ts +++ b/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; + public static externalDownloadFunction: (url: string, headers?: any) => Promise; 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(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 { + public static downloadJson(url: string, headers?: any): Promise { 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 { + return new Promise((resolve) => { + window.setTimeout(resolve, timeMillis); + }) + } } diff --git a/assets/layers/charging_station/charging_station.json b/assets/layers/charging_station/charging_station.json index c726b52dd..03dc010ee 100644 --- a/assets/layers/charging_station/charging_station.json +++ b/assets/layers/charging_station/charging_station.json @@ -1592,8 +1592,13 @@ { "#": "plugs-9", "question": { - "en": "How much plugs of type Type 2 with cable (mennekes) are available here?", - "nl": "Hoeveel stekkers van type Type 2 met kabel (J1772) heeft dit oplaadpunt?" + "en": "What kind of authentication is available at the charging station?", + "nl": "Hoeveel stekkers van type Type 2 met kabel (J1772) 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 Type 2 with cable (mennekes) plugs of type Type 2 with cable (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 Type 2 with cable (mennekes) offer?", - "nl": "Welke spanning levert de stekker van type Type 2 met kabel (J1772) " + "en": "What's the phone number for authentication call or SMS?", + "nl": "Welke spanning levert de stekker van type Type 2 met kabel (J1772) ", + "it": "A quale rete appartiene questa stazione di ricarica?", + "ja": "この充電ステーションの運営チェーンはどこですか?", + "ru": "К какой сети относится эта станция?", + "zh_Hant": "充電站所屬的網路是?" }, "render": { - "en": "Type 2 with cable (mennekes) outputs {socket:type2_cable:voltage} volt", - "nl": "Type 2 met kabel (J1772) heeft een spanning van {socket:type2_cable:voltage} volt" + "en": "Authenticate by calling or SMS'ing to {authentication:phone_call:number}", + "nl": "Type 2 met kabel (J1772) 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 Type 2 with cable (mennekes) offer?", + "en": "When is this charging station opened?", "nl": "Welke stroom levert de stekker van type Type 2 met kabel (J1772) ?" }, "render": { @@ -1665,7 +1705,7 @@ { "if": "socket:socket:type2_cable:current=16 A", "then": { - "en": "Type 2 with cable (mennekes) outputs at most 16 A", + "en": "24/7 opened (including holidays)", "nl": "Type 2 met kabel (J1772) 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 Type 2 with cable (mennekes) offer?", - "nl": "Welk vermogen levert een enkele stekker van type Type 2 met kabel (J1772) ?" + "en": "How much does one have to pay to use this charging station?", + "nl": "Hoeveel kost het gebruik van dit oplaadpunt?" }, "render": { - "en": "Type 2 with cable (mennekes) outputs at most {socket:type2_cable:output}", - "nl": "Type 2 met kabel (J1772) levert een vermogen van maximaal {socket:type2_cable:output}" + "en": "Using this charging station costs {charge}", + "nl": "Dit oplaadpunt gebruiken kost {charge}" }, "freeform": { "key": "socket:type2_cable:output", @@ -1702,8 +1742,8 @@ { "if": "socket:socket:type2_cable:output=11 kw", "then": { - "en": "Type 2 with cable (mennekes) outputs at most 11 kw", - "nl": "Type 2 met kabel (J1772) 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 Tesla Supercharger CCS (a branded type2_css) offer?", - "nl": "Welke spanning levert de stekker van type " + "en": "What is the maximum amount of time one is allowed to stay here?", + "nl": "Hoelang mag een voertuig hier blijven staan?" }, "render": { - "en": "Tesla Supercharger CCS (a branded type2_css) outputs {socket:tesla_supercharger_ccs:voltage} volt", - "nl": " heeft een spanning van {socket:tesla_supercharger_ccs:voltage} volt" + "en": "One can stay at most {canonical(maxstay)}", + "nl": "De maximale parkeertijd hier is {canonical(maxstay)}" }, "freeform": { "key": "socket:tesla_supercharger_ccs:voltage", @@ -1760,8 +1814,8 @@ { "if": "socket:socket:tesla_supercharger_ccs:voltage=500 V", "then": { - "en": "Tesla Supercharger CCS (a branded type2_css) outputs 500 volt", - "nl": " 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 Tesla Supercharger CCS (a branded type2_css) offer?", + "en": "Is this charging station part of a network?", "nl": "Welke stroom levert de stekker van type ?" }, "render": { - "en": "Tesla Supercharger CCS (a branded type2_css) outputs at most {socket:tesla_supercharger_ccs:current}A", + "en": "Part of the network {network}", "nl": " 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": "Tesla Supercharger CCS (a branded type2_css) outputs at most 125 A", + "en": "Not part of a bigger network", "nl": " levert een stroom van maximaal 125 A" } }, { "if": "socket:socket:tesla_supercharger_ccs:current=350 A", "then": { - "en": "Tesla Supercharger CCS (a branded type2_css) outputs at most 350 A", + "en": "Not part of a bigger network", "nl": " levert een stroom van maximaal 350 A" } } @@ -1849,11 +1903,11 @@ { "#": "plugs-11", "question": { - "en": "How much plugs of type Tesla Supercharger (destination) are available here?", + "en": "What number can one call if there is a problem with this charging station?", "nl": "Hoeveel stekkers van type heeft dit oplaadpunt?" }, "render": { - "en": "There are Tesla Supercharger (destination) plugs of type Tesla Supercharger (destination) available here", + "en": "In case of problems, call {phone}", "nl": "Hier zijn stekkers van het type " }, "freeform": { @@ -1870,11 +1924,11 @@ { "#": "voltage-11", "question": { - "en": "What voltage do the plugs with Tesla Supercharger (destination) offer?", + "en": "What is the email address of the operator?", "nl": "Welke spanning levert de stekker van type " }, "render": { - "en": "Tesla Supercharger (destination) outputs {socket:tesla_destination:voltage} volt", + "en": "In case of problems, send an email to {email}", "nl": " heeft een spanning van {socket:tesla_destination:voltage} volt" }, "freeform": { @@ -1900,11 +1954,11 @@ { "#": "current-11", "question": { - "en": "What current do the plugs with Tesla Supercharger (destination) offer?", + "en": "What is the website of the operator?", "nl": "Welke stroom levert de stekker van type ?" }, "render": { - "en": "Tesla Supercharger (destination) outputs at most {socket:tesla_destination:current}A", + "en": "More info on {website}", "nl": " levert een stroom van maximaal {socket:tesla_destination:current}A" }, "freeform": { @@ -1981,7 +2035,7 @@ { "#": "plugs-12", "question": { - "en": "How much plugs of type Tesla supercharger (destination (A Type 2 with cable branded as tesla) are available here?", + "en": "What is the reference number of this charging station?", "nl": "Hoeveel stekkers van type heeft dit oplaadpunt?" }, "render": { @@ -2002,8 +2056,8 @@ { "#": "voltage-12", "question": { - "en": "What voltage do the plugs with Tesla supercharger (destination (A Type 2 with cable branded as tesla) offer?", - "nl": "Welke spanning levert de stekker van type " + "en": "Is this charging point in use?", + "nl": "Is dit oplaadpunt operationeel?" }, "render": { "en": "Tesla supercharger (destination (A Type 2 with cable branded as tesla) outputs {socket:tesla_destination:voltage} volt", @@ -2017,15 +2071,15 @@ { "if": "socket:socket:tesla_destination:voltage=230 V", "then": { - "en": "Tesla supercharger (destination (A Type 2 with cable branded as tesla) outputs 230 volt", - "nl": " 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": "Tesla supercharger (destination (A Type 2 with cable branded as tesla) outputs 400 volt", - "nl": " 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": [ diff --git a/assets/layers/drinking_water/drinking_water.json b/assets/layers/drinking_water/drinking_water.json index 3645da855..8b0651933 100644 --- a/assets/layers/drinking_water/drinking_water.json +++ b/assets/layers/drinking_water/drinking_water.json @@ -1,61 +1,6 @@ { - "id": "drinking_water", - "name": { - "en": "Drinking water", - "nl": "Drinkbaar water", - "fr": "Eau potable", - "gl": "Auga potábel", - "de": "Trinkwasser", - "it": "Acqua potabile", - "ru": "Питьевая вода", - "id": "Air minum" - }, - "title": { - "render": { - "en": "Drinking water", - "nl": "Drinkbaar water", - "fr": "Eau potable", - "gl": "Auga potábel", - "de": "Trinkwasser", - "it": "Acqua potabile", - "ru": "Питьевая вода", - "id": "Air minum" - } - }, - "icon": { - "render": "pin:#6BC4F7;./assets/layers/drinking_water/drips.svg" - }, - "iconOverlays": [ - { - "if": { - "or": [ - "operational_status=broken", - "operational_status=closed" - ] - }, - "then": "close:#c33", - "badge": true - } - ], - "iconSize": "40,40,bottom", - "source": { - "osmTags": { - "and": [ - "amenity=drinking_water", - "access!=permissive", - "access!=private" - ] - } - }, - "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)" - ], - "minzoom": 13, - "wayHandling": 1, - "presets": [ - { - "title": { + "id": "drinking_water", + "name": { "en": "Drinking water", "nl": "Drinkbaar water", "fr": "Eau potable", @@ -64,105 +9,161 @@ "it": "Acqua potabile", "ru": "Питьевая вода", "id": "Air minum" - }, - "tags": [ - "amenity=drinking_water" - ] - } - ], - "color": "#6bc4f7", - "tagRenderings": [ - "images", - { - "#": "Still in use?", - "question": { - "en": "Is this drinking water spot still operational?", - "nl": "Is deze drinkwaterkraan nog steeds werkende?", - "it": "Questo punto di acqua potabile è sempre funzionante?", - "fr": "Ce point d'eau potable est-il toujours opérationnel ?", - "de": "Ist diese Trinkwasserstelle noch in Betrieb?" - }, - "render": { - "en": "The operational status is {operational_status", - "nl": "Deze waterkraan-status is {operational_status}", - "it": "Lo stato operativo è {operational_status}", - "fr": "L'état opérationnel est {operational_status", - "de": "Der Betriebsstatus ist {operational_status" - }, - "freeform": { - "key": "operational_status" - }, - "mappings": [ - { - "if": "operational_status=", - "then": { - "en": "This drinking water works", - "nl": "Deze drinkwaterfontein werkt", - "it": "La fontanella funziona", - "fr": "Cette fontaine fonctionne" - } - }, - { - "if": "operational_status=broken", - "then": { - "en": "This drinking water is broken", - "nl": "Deze drinkwaterfontein is kapot", - "it": "La fontanella è guasta", - "fr": "Cette fontaine est cassée" - } - }, - { - "if": "operational_status=closed", - "then": { - "en": "This drinking water is closed", - "nl": "Deze drinkwaterfontein is afgesloten", - "it": "La fontanella è chiusa", - "fr": "Cette fontaine est fermée" - } - } - ] }, - { - "#": "Bottle refill", - "question": { - "en": "How easy is it to fill water bottles?", - "nl": "Hoe gemakkelijk is het om drinkbussen bij te vullen?", - "de": "Wie einfach ist es, Wasserflaschen zu füllen?", - "it": "Quanto è facile riempire d’acqua le bottiglie?", - "fr": "Est-il facile de remplir des bouteilles d'eau ?" - }, - "mappings": [ + "title": { + "render": { + "en": "Drinking water", + "nl": "Drinkbaar water", + "fr": "Eau potable", + "gl": "Auga potábel", + "de": "Trinkwasser", + "it": "Acqua potabile", + "ru": "Питьевая вода", + "id": "Air minum" + } + }, + "icon": { + "render": "pin:#6BC4F7;./assets/layers/drinking_water/drips.svg" + }, + "iconOverlays": [ { - "if": "bottle=yes", - "then": { - "en": "It is easy to refill water bottles", - "nl": "Een drinkbus bijvullen gaat makkelijk", - "de": "Es ist einfach, Wasserflaschen nachzufüllen", - "it": "È facile riempire d’acqua le bottiglie", - "fr": "Il est facile de remplir les bouteilles d'eau" - } + "if": { + "or": [ + "operational_status=broken", + "operational_status=closed" + ] + }, + "then": "close:#c33", + "badge": true + } + ], + "iconSize": "40,40,bottom", + "source": { + "osmTags": { + "and": [ + "amenity=drinking_water", + "access!=permissive", + "access!=private" + ] + } + }, + "calculatedTags": [ + "_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, + "presets": [ + { + "title": { + "en": "Drinking water", + "nl": "Drinkbaar water", + "fr": "Eau potable", + "gl": "Auga potábel", + "de": "Trinkwasser", + "it": "Acqua potabile", + "ru": "Питьевая вода", + "id": "Air minum" + }, + "tags": [ + "amenity=drinking_water" + ] + } + ], + "color": "#6bc4f7", + "tagRenderings": [ + "images", + { + "#": "Still in use?", + "question": { + "en": "Is this drinking water spot still operational?", + "nl": "Is deze drinkwaterkraan nog steeds werkende?", + "it": "Questo punto di acqua potabile è sempre funzionante?", + "fr": "Ce point d'eau potable est-il toujours opérationnel ?", + "de": "Ist diese Trinkwasserstelle noch in Betrieb?" + }, + "render": { + "en": "The operational status is {operational_status", + "nl": "Deze waterkraan-status is {operational_status}", + "it": "Lo stato operativo è {operational_status}", + "fr": "L'état opérationnel est {operational_status", + "de": "Der Betriebsstatus ist {operational_status" + }, + "freeform": { + "key": "operational_status" + }, + "mappings": [ + { + "if": "operational_status=", + "then": { + "en": "This drinking water works", + "nl": "Deze drinkwaterfontein werkt", + "it": "La fontanella funziona", + "fr": "Cette fontaine fonctionne" + } + }, + { + "if": "operational_status=broken", + "then": { + "en": "This drinking water is broken", + "nl": "Deze drinkwaterfontein is kapot", + "it": "La fontanella è guasta", + "fr": "Cette fontaine est cassée" + } + }, + { + "if": "operational_status=closed", + "then": { + "en": "This drinking water is closed", + "nl": "Deze drinkwaterfontein is afgesloten", + "it": "La fontanella è chiusa", + "fr": "Cette fontaine est fermée" + } + } + ] }, { - "if": "bottle=no", - "then": { - "en": "Water bottles may not fit", - "nl": "Een drinkbus past moeilijk", - "de": "Wasserflaschen passen möglicherweise nicht", - "it": "Le bottiglie d’acqua potrebbero non entrare", - "fr": "Les bouteilles d'eau peuvent ne pas passer" - } + "#": "Bottle refill", + "question": { + "en": "How easy is it to fill water bottles?", + "nl": "Hoe gemakkelijk is het om drinkbussen bij te vullen?", + "de": "Wie einfach ist es, Wasserflaschen zu füllen?", + "it": "Quanto è facile riempire d’acqua le bottiglie?", + "fr": "Est-il facile de remplir des bouteilles d'eau ?" + }, + "mappings": [ + { + "if": "bottle=yes", + "then": { + "en": "It is easy to refill water bottles", + "nl": "Een drinkbus bijvullen gaat makkelijk", + "de": "Es ist einfach, Wasserflaschen nachzufüllen", + "it": "È facile riempire d’acqua le bottiglie", + "fr": "Il est facile de remplir les bouteilles d'eau" + } + }, + { + "if": "bottle=no", + "then": { + "en": "Water bottles may not fit", + "nl": "Een drinkbus past moeilijk", + "de": "Wasserflaschen passen möglicherweise nicht", + "it": "Le bottiglie d’acqua potrebbero non entrare", + "fr": "Les bouteilles d'eau peuvent ne pas passer" + } + } + ] + }, + { + "render": { + "en": "There is another drinking water fountain at {_closest_other_drinking_water_distance} meter", + "nl": "Er bevindt zich een ander drinkwaterpunt op {_closest_other_drinking_water_distance} meter", + "it": "C’è un’altra fontanella a {_closest_other_drinking_water_distance} metri", + "de": "Ein weiterer Trinkwasserbrunnen befindet sich in {_closest_other_drinking_water_distance} Meter", + "fr": "Une autre source d’eau potable est à {_closest_other_drinking_water_distance} mètres a>" + }, + "condition": "_closest_other_drinking_water_id~*" } - ] - }, - { - "render": { - "en": "There is another drinking water fountain at {_closest_other_drinking_water_distance} meter", - "nl": "Er bevindt zich een ander drinkwaterpunt op {_closest_other_drinking_water_distance} meter", - "it": "C’è un’altra fontanella a {_closest_other_drinking_water_distance} metri", - "de": "Ein weiterer Trinkwasserbrunnen befindet sich in {_closest_other_drinking_water_distance} Meter", - "fr": "Une autre source d’eau potable est à {_closest_other_drinking_water_distance} mètres a>" - }, - "condition": "_closest_other_drinking_water_id~*" - } - ] + ] } \ No newline at end of file diff --git a/assets/layers/food/food.json b/assets/layers/food/food.json index 26dbcc65a..64b18a306 100644 --- a/assets/layers/food/food.json +++ b/assets/layers/food/food.json @@ -12,6 +12,7 @@ ] } }, + "minzoom": 12, "wayHandling": 1, "icon": { "render": "circle:white;./assets/layers/food/restaurant.svg", diff --git a/assets/layers/public_bookcase/public_bookcase.json b/assets/layers/public_bookcase/public_bookcase.json index 786d7e220..a4715ce5a 100644 --- a/assets/layers/public_bookcase/public_bookcase.json +++ b/assets/layers/public_bookcase/public_bookcase.json @@ -19,7 +19,7 @@ "source": { "osmTags": "amenity=public_bookcase" }, - "minzoom": 12, + "minzoom": 10, "wayHandling": 2, "title": { "render": { diff --git a/assets/themes/benches/benches.json b/assets/themes/benches/benches.json index a43292dbf..d94f88597 100644 --- a/assets/themes/benches/benches.json +++ b/assets/themes/benches/benches.json @@ -52,7 +52,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 1.5, "socialImage": "", "layers": [ "bench", diff --git a/assets/themes/bicyclelib/bicyclelib.json b/assets/themes/bicyclelib/bicyclelib.json index fe40e2d34..3a8fb3e8e 100644 --- a/assets/themes/bicyclelib/bicyclelib.json +++ b/assets/themes/bicyclelib/bicyclelib.json @@ -40,7 +40,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 1.5, "roamingRenderings": [], "layers": [ "bicycle_library" diff --git a/assets/themes/bike_monitoring_station/bike_monitoring_stations.json b/assets/themes/bike_monitoring_station/bike_monitoring_stations.json index 5703e2ed5..f64339938 100644 --- a/assets/themes/bike_monitoring_station/bike_monitoring_stations.json +++ b/assets/themes/bike_monitoring_station/bike_monitoring_stations.json @@ -47,7 +47,7 @@ "startLat": 50.8435, "startLon": 4.3688, "startZoom": 14, - "widenFactor": 0.05, + "widenFactor": 1.5, "socialImage": "", "layers": [ "bike_monitoring_station" diff --git a/assets/themes/binoculars/binoculars.json b/assets/themes/binoculars/binoculars.json index 0f9817e46..75622d3a5 100644 --- a/assets/themes/binoculars/binoculars.json +++ b/assets/themes/binoculars/binoculars.json @@ -22,7 +22,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 1.5, "socialImage": "", "layers": [ "binocular" diff --git a/assets/themes/buurtnatuur/buurtnatuur.json b/assets/themes/buurtnatuur/buurtnatuur.json index 9c82e4d44..9c4f2a189 100644 --- a/assets/themes/buurtnatuur/buurtnatuur.json +++ b/assets/themes/buurtnatuur/buurtnatuur.json @@ -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": [ { diff --git a/assets/themes/cafes_and_pubs/cafes_and_pubs.json b/assets/themes/cafes_and_pubs/cafes_and_pubs.json index 1f9b704f8..c7ec682d1 100644 --- a/assets/themes/cafes_and_pubs/cafes_and_pubs.json +++ b/assets/themes/cafes_and_pubs/cafes_and_pubs.json @@ -18,7 +18,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 1.5, "socialImage": "", "layers": [ "cafe_pub" diff --git a/assets/themes/campersite/campersite.json b/assets/themes/campersite/campersite.json index 7b24124e5..592b9f544 100644 --- a/assets/themes/campersite/campersite.json +++ b/assets/themes/campersite/campersite.json @@ -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": [ { diff --git a/assets/themes/charging_stations/charging_stations.json b/assets/themes/charging_stations/charging_stations.json index b193b4daf..16745dc60 100644 --- a/assets/themes/charging_stations/charging_stations.json +++ b/assets/themes/charging_stations/charging_stations.json @@ -39,7 +39,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 1.5, "socialImage": "", "defaultBackgroundId": "CartoDB.Voyager", "layers": [ diff --git a/assets/themes/climbing/climbing.json b/assets/themes/climbing/climbing.json index 44a2520db..143fcddc3 100644 --- a/assets/themes/climbing/climbing.json +++ b/assets/themes/climbing/climbing.json @@ -48,7 +48,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 1.5, "socialImage": "", "layers": [ { diff --git a/assets/themes/cycle_highways/cycle_highways.json b/assets/themes/cycle_highways/cycle_highways.json index 85a91aa7d..09312a9ea 100644 --- a/assets/themes/cycle_highways/cycle_highways.json +++ b/assets/themes/cycle_highways/cycle_highways.json @@ -21,7 +21,7 @@ "clustering": { "maxZoom": 1 }, - "widenFactor": 0.005, + "widenFactor": 1.1, "enableDownload": true, "enablePdfDownload": true, "layers": [ diff --git a/assets/themes/cycle_infra/cycle_infra.json b/assets/themes/cycle_infra/cycle_infra.json index 7446095d9..8af56b886 100644 --- a/assets/themes/cycle_infra/cycle_infra.json +++ b/assets/themes/cycle_infra/cycle_infra.json @@ -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": [ diff --git a/assets/themes/cyclofix/cyclofix.json b/assets/themes/cyclofix/cyclofix.json index 2e1016d77..47a626736 100644 --- a/assets/themes/cyclofix/cyclofix.json +++ b/assets/themes/cyclofix/cyclofix.json @@ -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", diff --git a/assets/themes/facadegardens/facadegardens.json b/assets/themes/facadegardens/facadegardens.json index f3e28a1ef..08a87063c 100644 --- a/assets/themes/facadegardens/facadegardens.json +++ b/assets/themes/facadegardens/facadegardens.json @@ -38,7 +38,7 @@ "startLat": 51.02768, "startLon": 4.480705, "startZoom": 15, - "widenFactor": 0.05, + "widenFactor": 1.5, "socialImage": "", "layers": [ { diff --git a/assets/themes/food/food.json b/assets/themes/food/food.json index 416f9d161..fed26b560 100644 --- a/assets/themes/food/food.json +++ b/assets/themes/food/food.json @@ -18,7 +18,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 3, "socialImage": "", "layers": [ "food" diff --git a/assets/themes/fritures/fritures.json b/assets/themes/fritures/fritures.json index 33d83c0f5..fec9038cc 100644 --- a/assets/themes/fritures/fritures.json +++ b/assets/themes/fritures/fritures.json @@ -24,7 +24,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 3, "socialImage": "", "layers": [ { diff --git a/assets/themes/fruit_trees/fruit_trees.json b/assets/themes/fruit_trees/fruit_trees.json index 60d6e8a23..58b350f89 100644 --- a/assets/themes/fruit_trees/fruit_trees.json +++ b/assets/themes/fruit_trees/fruit_trees.json @@ -18,7 +18,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.001, + "widenFactor": 2, "socialImage": "", "hideFromOverview": true, "layers": [ diff --git a/assets/themes/ghostbikes/ghostbikes.json b/assets/themes/ghostbikes/ghostbikes.json index 229a32751..7c9aec38b 100644 --- a/assets/themes/ghostbikes/ghostbikes.json +++ b/assets/themes/ghostbikes/ghostbikes.json @@ -52,7 +52,7 @@ "startZoom": 1, "startLat": 0, "startLon": 0, - "widenFactor": 0.1, + "widenFactor": 5, "layers": [ "ghost_bike" ], diff --git a/assets/themes/grb.json b/assets/themes/grb.json index 37efacae0..dee3ca22f 100644 --- a/assets/themes/grb.json +++ b/assets/themes/grb.json @@ -18,7 +18,7 @@ "startLat": 51.2132, "startLon": 3.231, "startZoom": 14, - "widenFactor": 0.05, + "widenFactor": 2, "cacheTimeout": 3600, "socialImage": "", "layers": [ diff --git a/assets/themes/hackerspaces/hackerspaces.json b/assets/themes/hackerspaces/hackerspaces.json index 538bc0414..98bf59574 100644 --- a/assets/themes/hackerspaces/hackerspaces.json +++ b/assets/themes/hackerspaces/hackerspaces.json @@ -18,7 +18,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 5, "socialImage": "", "layers": [ { diff --git a/assets/themes/hailhydrant/hailhydrant.json b/assets/themes/hailhydrant/hailhydrant.json index c5cdd3795..c6e7a1f84 100644 --- a/assets/themes/hailhydrant/hailhydrant.json +++ b/assets/themes/hailhydrant/hailhydrant.json @@ -36,7 +36,7 @@ "startLat": 13.67801, "startLon": 121.6625, "startZoom": 6, - "widenFactor": 0.05, + "widenFactor": 3, "socialImage": "", "layers": [ { diff --git a/assets/themes/maps/maps.json b/assets/themes/maps/maps.json index e34cdce30..52d757a9d 100644 --- a/assets/themes/maps/maps.json +++ b/assets/themes/maps/maps.json @@ -36,7 +36,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 5, "socialImage": "", "layers": [ "map" diff --git a/assets/themes/nature/nature.json b/assets/themes/nature/nature.json index 9aeb9e498..0f8004e2c 100644 --- a/assets/themes/nature/nature.json +++ b/assets/themes/nature/nature.json @@ -18,7 +18,7 @@ "startLat": 51.20875, "startLon": 3.22435, "startZoom": 12, - "widenFactor": 0.05, + "widenFactor": 2, "socialImage": "", "layers": [ "drinking_water", diff --git a/assets/themes/natuurpunt/natuurpunt.json b/assets/themes/natuurpunt/natuurpunt.json index 4616fae02..f0e921d38 100644 --- a/assets/themes/natuurpunt/natuurpunt.json +++ b/assets/themes/natuurpunt/natuurpunt.json @@ -23,7 +23,7 @@ "startLat": 51.20875, "startLon": 3.22435, "startZoom": 15, - "widenFactor": 0.05, + "widenFactor": 2, "socialImage": "", "defaultBackgroundId": "CartoDB.Positron", "enablePdfDownload": true, diff --git a/assets/themes/observation_towers/observation_towers.json b/assets/themes/observation_towers/observation_towers.json index 2bdd0cbad..3ab5a6b58 100644 --- a/assets/themes/observation_towers/observation_towers.json +++ b/assets/themes/observation_towers/observation_towers.json @@ -22,7 +22,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 5, "socialImage": "", "layers": [ "observation_tower" diff --git a/assets/themes/parkings/parkings.json b/assets/themes/parkings/parkings.json index bb25e8bfd..ca322052c 100644 --- a/assets/themes/parkings/parkings.json +++ b/assets/themes/parkings/parkings.json @@ -22,7 +22,7 @@ "startLat": 51.20875, "startLon": 3.22435, "startZoom": 12, - "widenFactor": 0.05, + "widenFactor": 1.2, "socialImage": "", "layers": [ "parking" diff --git a/assets/themes/personal/personal.json b/assets/themes/personal/personal.json index 02e26cdbd..d9df14a7b 100644 --- a/assets/themes/personal/personal.json +++ b/assets/themes/personal/personal.json @@ -41,7 +41,7 @@ "startLat": 0, "startLon": 0, "startZoom": 16, - "widenFactor": 0.05, + "widenFactor": 3, "layers": [], "roamingRenderings": [] } \ No newline at end of file diff --git a/assets/themes/play_forests/play_forests.json b/assets/themes/play_forests/play_forests.json index bd824ee67..5c65a702a 100644 --- a/assets/themes/play_forests/play_forests.json +++ b/assets/themes/play_forests/play_forests.json @@ -19,7 +19,7 @@ "startLon": 0, "startZoom": 1, "hideFromOverview": true, - "widenFactor": 0.05, + "widenFactor": 3, "socialImage": "", "layers": [ "play_forest" diff --git a/assets/themes/playgrounds/playgrounds.json b/assets/themes/playgrounds/playgrounds.json index 418aa36bc..2908a88b4 100644 --- a/assets/themes/playgrounds/playgrounds.json +++ b/assets/themes/playgrounds/playgrounds.json @@ -38,7 +38,7 @@ "startLat": 50.535, "startLon": 4.399, "startZoom": 13, - "widenFactor": 0.05, + "widenFactor": 5, "socialImage": "", "layers": [ "playground" diff --git a/assets/themes/shops/shops.json b/assets/themes/shops/shops.json index 09209f3f4..706268e3d 100644 --- a/assets/themes/shops/shops.json +++ b/assets/themes/shops/shops.json @@ -34,7 +34,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 3, "socialImage": "", "layers": [ { diff --git a/assets/themes/speelplekken/speelplekken.json b/assets/themes/speelplekken/speelplekken.json index 21cb17212..a87466742 100644 --- a/assets/themes/speelplekken/speelplekken.json +++ b/assets/themes/speelplekken/speelplekken.json @@ -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": [ diff --git a/assets/themes/speelplekken/speelplekken_temp.json b/assets/themes/speelplekken/speelplekken_temp.json index 867298215..752d793b3 100644 --- a/assets/themes/speelplekken/speelplekken_temp.json +++ b/assets/themes/speelplekken/speelplekken_temp.json @@ -20,7 +20,7 @@ "startLat": 51.17174, "startLon": 4.449462, "startZoom": 12, - "widenFactor": 0.05, + "widenFactor": 2, "socialImage": "", "defaultBackgroundId": "CartoDB.Positron", "layers": [ diff --git a/assets/themes/sport_pitches/sport_pitches.json b/assets/themes/sport_pitches/sport_pitches.json index 7f1fdeebd..4db24cdd6 100644 --- a/assets/themes/sport_pitches/sport_pitches.json +++ b/assets/themes/sport_pitches/sport_pitches.json @@ -37,7 +37,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 2, "socialImage": "", "layers": [ "sport_pitch" diff --git a/assets/themes/surveillance/surveillance.json b/assets/themes/surveillance/surveillance.json index 001045fd1..a61633b23 100644 --- a/assets/themes/surveillance/surveillance.json +++ b/assets/themes/surveillance/surveillance.json @@ -37,7 +37,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 2, "socialImage": "", "defaultBackgroundId": "osm", "layers": [ diff --git a/assets/themes/toerisme_vlaanderen/toerisme_vlaanderen.json b/assets/themes/toerisme_vlaanderen/toerisme_vlaanderen.json index be5567d5d..c09b548e8 100644 --- a/assets/themes/toerisme_vlaanderen/toerisme_vlaanderen.json +++ b/assets/themes/toerisme_vlaanderen/toerisme_vlaanderen.json @@ -20,7 +20,7 @@ "startZoom": 8, "startLat": 50.8536, "startLon": 4.433, - "widenFactor": 0.2, + "widenFactor": 2, "layers": [ { "builtin": [ diff --git a/assets/themes/toilets/toilets.json b/assets/themes/toilets/toilets.json index e343d9ea2..1d1567de4 100644 --- a/assets/themes/toilets/toilets.json +++ b/assets/themes/toilets/toilets.json @@ -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" diff --git a/assets/themes/trees/trees.json b/assets/themes/trees/trees.json index ea26fdeca..52961293a 100644 --- a/assets/themes/trees/trees.json +++ b/assets/themes/trees/trees.json @@ -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 diff --git a/assets/themes/uk_addresses/uk_addresses.json b/assets/themes/uk_addresses/uk_addresses.json index b89d25e60..7de4c3a36 100644 --- a/assets/themes/uk_addresses/uk_addresses.json +++ b/assets/themes/uk_addresses/uk_addresses.json @@ -18,7 +18,7 @@ "startLat": -0.08528530407, "startLon": 51.52103754846, "startZoom": 18, - "widenFactor": 0.5, + "widenFactor": 1.5, "socialImage": "", "layers": [ { diff --git a/assets/themes/waste_basket/waste_basket.json b/assets/themes/waste_basket/waste_basket.json index 2a1ecef84..eb95d85eb 100644 --- a/assets/themes/waste_basket/waste_basket.json +++ b/assets/themes/waste_basket/waste_basket.json @@ -22,7 +22,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 2, "socialImage": "", "layers": [ { diff --git a/assets/themes/widths/width.json b/assets/themes/widths/width.json index 5ee75eb7e..6e56d5f21 100644 --- a/assets/themes/widths/width.json +++ b/assets/themes/widths/width.json @@ -25,7 +25,7 @@ "startLat": 51.20875, "startLon": 3.22435, "startZoom": 14, - "widenFactor": 0.05, + "widenFactor": 2, "socialImage": "", "layers": [ { diff --git a/scripts/ScriptUtils.ts b/scripts/ScriptUtils.ts index 3551ba5f8..22cdd0d93 100644 --- a/scripts/ScriptUtils.ts +++ b/scripts/ScriptUtils.ts @@ -48,13 +48,10 @@ export default class ScriptUtils { }) } - public static DownloadJSON(url, options?: { - headers: any - }): Promise { + public static DownloadJSON(url, headers?: any): Promise { 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) diff --git a/scripts/generateCache.ts b/scripts/generateCache.ts index 5a8377125..a5a915bde 100644 --- a/scripts/generateCache.ts +++ b/scripts/generateCache.ts @@ -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) { diff --git a/scripts/generateTranslations.ts b/scripts/generateTranslations.ts index ce05cf32b..6fda20b0b 100644 --- a/scripts/generateTranslations.ts +++ b/scripts/generateTranslations.ts @@ -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;