Add initial clustering per tile, very broken

This commit is contained in:
pietervdvn 2021-09-26 17:36:39 +02:00
parent 2b78c4b53f
commit c5e9448720
88 changed files with 1080 additions and 651 deletions

View file

@ -75,9 +75,7 @@ class StatsDownloader {
while (url) { while (url) {
ScriptUtils.erasableLog(`Downloading stats for ${year}-${month}, page ${page} ${url}`) ScriptUtils.erasableLog(`Downloading stats for ${year}-${month}, page ${page} ${url}`)
const result = await ScriptUtils.DownloadJSON(url, { const result = await ScriptUtils.DownloadJSON(url, headers)
headers: headers
})
page++; page++;
allFeatures.push(...result.features) allFeatures.push(...result.features)
if (result.features === undefined) { if (result.features === undefined) {

View file

@ -15,7 +15,6 @@ import Link from "./UI/Base/Link";
import * as personal from "./assets/themes/personal/personal.json"; import * as personal from "./assets/themes/personal/personal.json";
import * as L from "leaflet"; import * as L from "leaflet";
import Img from "./UI/Base/Img"; import Img from "./UI/Base/Img";
import UserDetails from "./Logic/Osm/OsmConnection";
import Attribution from "./UI/BigComponents/Attribution"; import Attribution from "./UI/BigComponents/Attribution";
import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter"; import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter";
import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs"; import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs";
@ -38,6 +37,9 @@ import Minimap from "./UI/Base/Minimap";
import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler"; import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler";
import Combine from "./UI/Base/Combine"; import Combine from "./UI/Base/Combine";
import {SubtleButton} from "./UI/Base/SubtleButton"; import {SubtleButton} from "./UI/Base/SubtleButton";
import ShowTileInfo from "./UI/ShowDataLayer/ShowTileInfo";
import {Tiles} from "./Models/TileRange";
import PerTileCountAggregator from "./UI/ShowDataLayer/PerTileCountAggregator";
export class InitUiElements { export class InitUiElements {
static InitAll( static InitAll(
@ -167,9 +169,20 @@ export class InitUiElements {
).AttachTo("messagesbox"); ).AttachTo("messagesbox");
} }
State.state.osmConnection.userDetails function addHomeMarker() {
.map((userDetails: UserDetails) => userDetails?.home) const userDetails = State.state.osmConnection.userDetails.data;
.addCallbackAndRunD((home) => { if (userDetails === undefined) {
return false;
}
console.log("Adding home location of ", userDetails)
const home = userDetails.home;
if (home === undefined) {
return userDetails.loggedIn; // If logged in, the home is not set and we unregister. If not logged in, we stay registered if a login still comes
}
const leaflet = State.state.leafletMap.data;
if (leaflet === undefined) {
return false;
}
const color = getComputedStyle(document.body).getPropertyValue( const color = getComputedStyle(document.body).getPropertyValue(
"--subtle-detail-color" "--subtle-detail-color"
); );
@ -181,8 +194,13 @@ export class InitUiElements {
iconAnchor: [15, 15], iconAnchor: [15, 15],
}); });
const marker = L.marker([home.lat, home.lon], {icon: icon}); const marker = L.marker([home.lat, home.lon], {icon: icon});
marker.addTo(State.state.leafletMap.data); marker.addTo(leaflet);
}); return true;
}
State.state.osmConnection.userDetails
.addCallbackAndRunD(_ => addHomeMarker());
State.state.leafletMap.addCallbackAndRunD(_ => addHomeMarker())
if (layoutToUse.id === personal.id) { if (layoutToUse.id === personal.id) {
updateFavs(); updateFavs();
@ -250,16 +268,16 @@ export class InitUiElements {
return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))]; return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))];
} catch (e) { } catch (e) {
if(hash === undefined || hash.length < 10){ if (hash === undefined || hash.length < 10) {
e = "Did you effectively add a theme? It seems no data could be found." e = "Did you effectively add a theme? It seems no data could be found."
} }
new Combine([ new Combine([
"Error: could not parse the custom layout:", "Error: could not parse the custom layout:",
new FixedUiElement(""+e).SetClass("alert"), new FixedUiElement("" + e).SetClass("alert"),
new SubtleButton("./assets/svg/mapcomplete_logo.svg", new SubtleButton("./assets/svg/mapcomplete_logo.svg",
"Go back to the theme overview", "Go back to the theme overview",
{url: window.location.protocol+"//"+ window.location.hostname+"/index.html", newTab: false}) {url: window.location.protocol + "//" + window.location.hostname + "/index.html", newTab: false})
]) ])
.SetClass("flex flex-col") .SetClass("flex flex-col")
@ -361,12 +379,12 @@ export class InitUiElements {
const layout = State.state.layoutToUse.data; const layout = State.state.layoutToUse.data;
if (layout.lockLocation) { if (layout.lockLocation) {
if (layout.lockLocation === true) { if (layout.lockLocation === true) {
const tile = Utils.embedded_tile( const tile = Tiles.embedded_tile(
layout.startLat, layout.startLat,
layout.startLon, layout.startLon,
layout.startZoom - 1 layout.startZoom - 1
); );
const bounds = Utils.tile_bounds(tile.z, tile.x, tile.y); const bounds = Tiles.tile_bounds(tile.z, tile.x, tile.y);
// We use the bounds to get a sense of distance for this zoom level // We use the bounds to get a sense of distance for this zoom level
const latDiff = bounds[0][0] - bounds[1][0]; const latDiff = bounds[0][0] - bounds[1][0];
const lonDiff = bounds[0][1] - bounds[1][1]; const lonDiff = bounds[0][1] - bounds[1][1];
@ -402,6 +420,9 @@ export class InitUiElements {
const flayer = { const flayer = {
isDisplayed: isDisplayed, isDisplayed: isDisplayed,
layerDef: layer, layerDef: layer,
isSufficientlyZoomed: state.locationControl.map(l => {
return l.zoom >= (layer.minzoomVisible ?? layer.minzoom)
}),
appliedFilters: new UIEventSource<TagsFilter>(undefined), appliedFilters: new UIEventSource<TagsFilter>(undefined),
}; };
flayers.push(flayer); flayers.push(flayer);
@ -409,13 +430,54 @@ export class InitUiElements {
return flayers; return flayers;
}); });
const clusterCounter = new PerTileCountAggregator(State.state.locationControl.map(l => {
const z = l.zoom + 1
if(z < 7){
return 7
}
return z
}))
const clusterShow = Math.min(...State.state.layoutToUse.data.layers.map(layer => layer.minzoomVisible ?? layer.minzoom))
new ShowDataLayer({
features: clusterCounter,
leafletMap: State.state.leafletMap,
layerToShow: ShowTileInfo.styling,
doShowLayer: State.state.locationControl.map(l => l.zoom < clusterShow)
})
State.state.featurePipeline = new FeaturePipeline( State.state.featurePipeline = new FeaturePipeline(
source => { source => {
const clustering = State.state.layoutToUse.data.clustering
const doShowFeatures = source.features.map(
f => {
const z = State.state.locationControl.data.zoom
if(z >= clustering.maxZoom){
return true
}
if(z < source.layer.layerDef.minzoom){
return false;
}
if(f.length > clustering.minNeededElements){
console.log("Activating clustering for tile ", Tiles.tile_from_index(source.tileIndex)," as it has ", f.length, "features (clustering starts at)", clustering.minNeededElements)
return false
}
return true
}, [State.state.locationControl]
)
clusterCounter.addTile(source, doShowFeatures.map(b => !b))
/*
new ShowTileInfo({source: source,
leafletMap: State.state.leafletMap,
layer: source.layer.layerDef,
doShowLayer: doShowFeatures.map(b => !b)
})*/
new ShowDataLayer( new ShowDataLayer(
{ {
features: source, features: source,
leafletMap: State.state.leafletMap, leafletMap: State.state.leafletMap,
layerToShow: source.layer.layerDef layerToShow: source.layer.layerDef,
doShowLayer: doShowFeatures
} }
); );
}, state }, state

View file

@ -44,7 +44,6 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour
readonly overpassUrl: UIEventSource<string>; readonly overpassUrl: UIEventSource<string>;
readonly overpassTimeout: UIEventSource<number>; readonly overpassTimeout: UIEventSource<number>;
} }
/** /**
* The most important layer should go first, as that one gets first pick for the questions * The most important layer should go first, as that one gets first pick for the questions
*/ */
@ -57,6 +56,7 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour
readonly overpassTimeout: UIEventSource<number>; readonly overpassTimeout: UIEventSource<number>;
readonly overpassMaxZoom: UIEventSource<number> readonly overpassMaxZoom: UIEventSource<number>
}) { }) {
console.trace("Initializing an overpass FS")
this.state = state this.state = state
@ -153,7 +153,12 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour
return new Overpass(new Or(filters), extraScripts, this.state.overpassUrl, this.state.overpassTimeout, this.relationsTracker); return new Overpass(new Or(filters), extraScripts, this.state.overpassUrl, this.state.overpassTimeout, this.relationsTracker);
} }
private update(): void { private update() {
this.updateAsync().then(_ => {
})
}
private async updateAsync(): Promise<void> {
if (this.runningQuery.data) { if (this.runningQuery.data) {
console.log("Still running a query, not updating"); console.log("Still running a query, not updating");
return; return;
@ -184,49 +189,41 @@ export default class OverpassFeatureSource implements FeatureSource, FeatureSour
return; return;
} }
this.runningQuery.setData(true); this.runningQuery.setData(true);
overpass.queryGeoJson(queryBounds).
then(([data, date]) => { let data: any = undefined
let date: Date = undefined
do {
try {
[data, date] = await overpass.queryGeoJson(queryBounds)
} catch (e) {
console.error(`QUERY FAILED (retrying in ${5 * self.retries.data} sec) due to`, e);
self.retries.data++;
self.retries.ping();
self.timeout.setData(self.retries.data * 5);
self.runningQuery.setData(false);
while (self.timeout.data > 0) {
await Utils.waitFor(1000)
self.timeout.data--
self.timeout.ping();
}
}
} while (data === undefined);
self._previousBounds.get(z).push(queryBounds); self._previousBounds.get(z).push(queryBounds);
self.retries.setData(0); self.retries.setData(0);
const features = data.features.map(f => ({feature: f, freshness: date}));
SimpleMetaTagger.objectMetaInfo.addMetaTags(features)
try{ try {
self.features.setData(features); data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date));
}catch(e){ self.features.setData(data.features.map(f => ({feature: f, freshness: date})));
} catch (e) {
console.error("Got the overpass response, but could not process it: ", e, e.stack) console.error("Got the overpass response, but could not process it: ", e, e.stack)
} }
self.runningQuery.setData(false); self.runningQuery.setData(false);
})
.catch((reason) => {
self.retries.data++;
self.ForceRefresh();
self.timeout.setData(self.retries.data * 5);
console.error(`QUERY FAILED (retrying in ${5 * self.retries.data} sec) due to`, reason);
self.retries.ping();
self.runningQuery.setData(false);
function countDown() {
window?.setTimeout(
function () {
if (self.timeout.data > 1) {
self.timeout.setData(self.timeout.data - 1);
window.setTimeout(
countDown,
1000
)
} else {
self.timeout.setData(0);
self.update()
}
}, 1000
)
}
countDown();
}
);
} }

View file

@ -256,7 +256,7 @@ export class ExtraFunction {
let closestFeatures: { feat: any, distance: number }[] = []; let closestFeatures: { feat: any, distance: number }[] = [];
for(const featureList of features) { for(const featureList of features) {
for (const otherFeature of featureList) { for (const otherFeature of featureList) {
if (otherFeature == feature || otherFeature.id == feature.id) { if (otherFeature === feature || otherFeature.id === feature.id) {
continue; // We ignore self continue; // We ignore self
} }
let distance = undefined; let distance = undefined;
@ -268,7 +268,8 @@ export class ExtraFunction {
[feature._lon, feature._lat] [feature._lon, feature._lat]
) )
} }
if (distance === undefined) { if (distance === undefined || distance === null) {
console.error("Could not calculate the distance between", feature, "and", otherFeature)
throw "Undefined distance!" throw "Undefined distance!"
} }
if (distance > maxDistance) { if (distance > maxDistance) {

View file

@ -37,7 +37,7 @@ export default class FeaturePipeline implements FeatureSourceState {
private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>; private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>;
constructor( constructor(
handleFeatureSource: (source: FeatureSourceForLayer) => void, handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void,
state: { state: {
filteredLayers: UIEventSource<FilteredLayer[]>, filteredLayers: UIEventSource<FilteredLayer[]>,
locationControl: UIEventSource<Loc>, locationControl: UIEventSource<Loc>,
@ -52,7 +52,6 @@ export default class FeaturePipeline implements FeatureSourceState {
const self = this const self = this
const updater = new OverpassFeatureSource(state); const updater = new OverpassFeatureSource(state);
updater.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(updater))
this.overpassUpdater = updater; this.overpassUpdater = updater;
this.sufficientlyZoomed = updater.sufficientlyZoomed this.sufficientlyZoomed = updater.sufficientlyZoomed
this.runningQuery = updater.runningQuery this.runningQuery = updater.runningQuery
@ -65,14 +64,15 @@ export default class FeaturePipeline implements FeatureSourceState {
const perLayerHierarchy = new Map<string, TileHierarchyMerger>() const perLayerHierarchy = new Map<string, TileHierarchyMerger>()
this.perLayerHierarchy = perLayerHierarchy this.perLayerHierarchy = perLayerHierarchy
const patchedHandleFeatureSource = function (src: FeatureSourceForLayer & IndexedFeatureSource) { const patchedHandleFeatureSource = function (src: FeatureSourceForLayer & IndexedFeatureSource & Tiled) {
// This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile // This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
const srcFiltered = const srcFiltered =
new FilteringFeatureSource(state, new FilteringFeatureSource(state, src.tileIndex,
new WayHandlingApplyingFeatureSource( new WayHandlingApplyingFeatureSource(
new ChangeGeometryApplicator(src, state.changes) new ChangeGeometryApplicator(src, state.changes)
) )
) )
handleFeatureSource(srcFiltered) handleFeatureSource(srcFiltered)
self.somethingLoaded.setData(true) self.somethingLoaded.setData(true)
}; };
@ -102,10 +102,12 @@ export default class FeaturePipeline implements FeatureSourceState {
if (source.geojsonZoomLevel === undefined) { if (source.geojsonZoomLevel === undefined) {
// This is a 'load everything at once' geojson layer // This is a 'load everything at once' geojson layer
// We split them up into tiles // We split them up into tiles anyway
const src = new GeoJsonSource(filteredLayer) const src = new GeoJsonSource(filteredLayer)
TiledFeatureSource.createHierarchy(src, { TiledFeatureSource.createHierarchy(src, {
layer: src.layer, layer: src.layer,
minZoomLevel:14,
dontEnforceMinZoom: true,
registerTile: (tile) => { registerTile: (tile) => {
new RegisteringAllFromFeatureSourceActor(tile) new RegisteringAllFromFeatureSourceActor(tile)
addToHierarchy(tile, id) addToHierarchy(tile, id)
@ -115,14 +117,11 @@ export default class FeaturePipeline implements FeatureSourceState {
} else { } else {
new DynamicGeoJsonTileSource( new DynamicGeoJsonTileSource(
filteredLayer, filteredLayer,
src => TiledFeatureSource.createHierarchy(src, { tile => {
layer: src.layer,
registerTile: (tile) => {
new RegisteringAllFromFeatureSourceActor(tile) new RegisteringAllFromFeatureSourceActor(tile)
addToHierarchy(tile, id) addToHierarchy(tile, id)
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
} },
}),
state state
) )
} }
@ -133,13 +132,17 @@ export default class FeaturePipeline implements FeatureSourceState {
new PerLayerFeatureSourceSplitter(state.filteredLayers, new PerLayerFeatureSourceSplitter(state.filteredLayers,
(source) => TiledFeatureSource.createHierarchy(source, { (source) => TiledFeatureSource.createHierarchy(source, {
layer: source.layer, layer: source.layer,
minZoomLevel: 14,
dontEnforceMinZoom: true,
registerTile: (tile) => { registerTile: (tile) => {
// We save the tile data for the given layer to local storage // We save the tile data for the given layer to local storage
new SaveTileToLocalStorageActor(tile, tile.tileIndex) new SaveTileToLocalStorageActor(tile, tile.tileIndex)
addToHierarchy(tile, source.layer.layerDef.id); addToHierarchy(new RememberingSource(tile), source.layer.layerDef.id);
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
} }
}), }),
new RememberingSource(updater)) updater)
// Also load points/lines that are newly added. // Also load points/lines that are newly added.
@ -152,6 +155,8 @@ export default class FeaturePipeline implements FeatureSourceState {
addToHierarchy(perLayer, perLayer.layer.layerDef.id) addToHierarchy(perLayer, perLayer.layer.layerDef.id)
// AT last, we always apply the metatags whenever possible // AT last, we always apply the metatags whenever possible
perLayer.features.addCallbackAndRunD(_ => self.applyMetaTags(perLayer)) perLayer.features.addCallbackAndRunD(_ => self.applyMetaTags(perLayer))
perLayer.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(perLayer))
}, },
newGeometry newGeometry
) )
@ -166,6 +171,7 @@ export default class FeaturePipeline implements FeatureSourceState {
private applyMetaTags(src: FeatureSourceForLayer){ private applyMetaTags(src: FeatureSourceForLayer){
const self = this const self = this
console.log("Applying metatagging onto ", src.name)
MetaTagging.addMetatags( MetaTagging.addMetatags(
src.features.data, src.features.data,
{ {
@ -183,6 +189,7 @@ export default class FeaturePipeline implements FeatureSourceState {
private updateAllMetaTagging() { private updateAllMetaTagging() {
const self = this; const self = this;
console.log("Reupdating all metatagging")
this.perLayerHierarchy.forEach(hierarchy => { this.perLayerHierarchy.forEach(hierarchy => {
hierarchy.loadedTiles.forEach(src => { hierarchy.loadedTiles.forEach(src => {
self.applyMetaTags(src) self.applyMetaTags(src)

View file

@ -7,6 +7,7 @@ import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from
import FilteredLayer from "../../../Models/FilteredLayer"; import FilteredLayer from "../../../Models/FilteredLayer";
import {BBox} from "../../GeoOperations"; import {BBox} from "../../GeoOperations";
import {Utils} from "../../../Utils"; import {Utils} from "../../../Utils";
import {Tiles} from "../../../Models/TileRange";
export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource { export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource {
@ -23,7 +24,7 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled
this.bbox = bbox; this.bbox = bbox;
this._sources = sources; this._sources = sources;
this.layer = layer; this.layer = layer;
this.name = "FeatureSourceMerger("+layer.layerDef.id+", "+Utils.tile_from_index(tileIndex).join(",")+")" this.name = "FeatureSourceMerger("+layer.layerDef.id+", "+Tiles.tile_from_index(tileIndex).join(",")+")"
const self = this; const self = this;
const handledSources = new Set<FeatureSource>(); const handledSources = new Set<FeatureSource>();

View file

@ -1,24 +1,29 @@
import {UIEventSource} from "../../UIEventSource"; import {UIEventSource} from "../../UIEventSource";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import FilteredLayer from "../../../Models/FilteredLayer"; import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer} from "../FeatureSource"; import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import Hash from "../../Web/Hash"; import Hash from "../../Web/Hash";
import {BBox} from "../../GeoOperations";
export default class FilteringFeatureSource implements FeatureSourceForLayer { export default class FilteringFeatureSource implements FeatureSourceForLayer , Tiled {
public features: UIEventSource<{ feature: any; freshness: Date }[]> = public features: UIEventSource<{ feature: any; freshness: Date }[]> =
new UIEventSource<{ feature: any; freshness: Date }[]>([]); new UIEventSource<{ feature: any; freshness: Date }[]>([]);
public readonly name; public readonly name;
public readonly layer: FilteredLayer; public readonly layer: FilteredLayer;
public readonly tileIndex : number
public readonly bbox : BBox
constructor( constructor(
state: { state: {
locationControl: UIEventSource<{ zoom: number }>, locationControl: UIEventSource<{ zoom: number }>,
selectedElement: UIEventSource<any>, selectedElement: UIEventSource<any>,
}, },
tileIndex,
upstream: FeatureSourceForLayer upstream: FeatureSourceForLayer
) { ) {
const self = this; const self = this;
this.name = "FilteringFeatureSource("+upstream.name+")" this.name = "FilteringFeatureSource("+upstream.name+")"
this.tileIndex = tileIndex
this.bbox = BBox.fromTileIndex(tileIndex)
this.layer = upstream.layer; this.layer = upstream.layer;
const layer = upstream.layer; const layer = upstream.layer;
@ -51,7 +56,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer {
return false; return false;
} }
} }
if (!FilteringFeatureSource.showLayer(layer, state.locationControl.data)) { if (!layer.isDisplayed) {
// The layer itself is either disabled or hidden due to zoom constraints // The layer itself is either disabled or hidden due to zoom constraints
// We should return true, but it might still match some other layer // We should return true, but it might still match some other layer
return false; return false;
@ -66,10 +71,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer {
update(); update();
}); });
let isShown = state.locationControl.map((l) => FilteringFeatureSource.showLayer(layer, l), layer.isDisplayed.addCallback(isShown => {
[layer.isDisplayed])
isShown.addCallback(isShown => {
if (isShown) { if (isShown) {
update(); update();
} else { } else {
@ -78,7 +80,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer {
}); });
layer.appliedFilters.addCallback(_ => { layer.appliedFilters.addCallback(_ => {
if(!isShown.data){ if(!layer.isDisplayed.data){
// Currently not shown. // Currently not shown.
// Note that a change in 'isSHown' will trigger an update as well, so we don't have to watch it another time // Note that a change in 'isSHown' will trigger an update as well, so we don't have to watch it another time
return; return;
@ -93,10 +95,8 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer {
layer: { layer: {
isDisplayed: UIEventSource<boolean>; isDisplayed: UIEventSource<boolean>;
layerDef: LayerConfig; layerDef: LayerConfig;
}, }) {
location: { zoom: number }) { return layer.isDisplayed.data;
return layer.isDisplayed.data &&
layer.layerDef.minzoomVisible <= location.zoom;
} }
} }

View file

@ -6,6 +6,7 @@ import FilteredLayer from "../../../Models/FilteredLayer";
import {Utils} from "../../../Utils"; import {Utils} from "../../../Utils";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {BBox} from "../../GeoOperations"; import {BBox} from "../../GeoOperations";
import {Tiles} from "../../../Models/TileRange";
export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
@ -35,10 +36,10 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
.replace('{z}', "" + z) .replace('{z}', "" + z)
.replace('{x}', "" + x) .replace('{x}', "" + x)
.replace('{y}', "" + y) .replace('{y}', "" + y)
this.tileIndex = Utils.tile_index(z, x, y) this.tileIndex = Tiles.tile_index(z, x, y)
this.bbox = BBox.fromTile(z, x, y) this.bbox = BBox.fromTile(z, x, y)
} else { } else {
this.tileIndex = Utils.tile_index(0, 0, 0) this.tileIndex = Tiles.tile_index(0, 0, 0)
this.bbox = BBox.global; this.bbox = BBox.global;
} }
@ -89,7 +90,6 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
newFeatures.push({feature: feature, freshness: freshness}) newFeatures.push({feature: feature, freshness: freshness})
} }
console.debug("Downloaded " + newFeatures.length + " new features and " + skipped + " already seen features from " + url);
if (newFeatures.length == 0) { if (newFeatures.length == 0) {
return; return;

View file

@ -2,17 +2,23 @@
* Every previously added point is remembered, but new points are added. * Every previously added point is remembered, but new points are added.
* Data coming from upstream will always overwrite a previous value * Data coming from upstream will always overwrite a previous value
*/ */
import FeatureSource from "../FeatureSource"; import FeatureSource, {Tiled} from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource"; import {UIEventSource} from "../../UIEventSource";
import {BBox} from "../../GeoOperations";
export default class RememberingSource implements FeatureSource { export default class RememberingSource implements FeatureSource , Tiled{
public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>; public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>;
public readonly name; public readonly name;
public readonly tileIndex : number
public readonly bbox : BBox
constructor(source: FeatureSource) { constructor(source: FeatureSource & Tiled) {
const self = this; const self = this;
this.name = "RememberingSource of " + source.name; this.name = "RememberingSource of " + source.name;
this.tileIndex= source.tileIndex
this.bbox = source.bbox;
const empty = []; const empty = [];
this.features = source.features.map(features => { this.features = source.features.map(features => {
const oldFeatures = self.features?.data ?? empty; const oldFeatures = self.features?.data ?? empty;

View file

@ -3,13 +3,14 @@ import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {BBox} from "../../GeoOperations"; import {BBox} from "../../GeoOperations";
import {Utils} from "../../../Utils"; import {Utils} from "../../../Utils";
import {Tiles} from "../../../Models/TileRange";
export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled { export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
public readonly name: string = "SimpleFeatureSource"; public readonly name: string = "SimpleFeatureSource";
public readonly layer: FilteredLayer; public readonly layer: FilteredLayer;
public readonly bbox: BBox = BBox.global; public readonly bbox: BBox = BBox.global;
public readonly tileIndex: number = Utils.tile_index(0, 0, 0); public readonly tileIndex: number = Tiles.tile_index(0, 0, 0);
constructor(layer: FilteredLayer) { constructor(layer: FilteredLayer) {
this.name = "SimpleFeatureSource(" + layer.layerDef.id + ")" this.name = "SimpleFeatureSource(" + layer.layerDef.id + ")"

View file

@ -8,12 +8,13 @@ export default class StaticFeatureSource implements FeatureSource {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name: string = "StaticFeatureSource" public readonly name: string = "StaticFeatureSource"
constructor(features: any[] | UIEventSource<any[]>, useFeaturesDirectly = false) { constructor(features: any[] | UIEventSource<any[] | UIEventSource<{ feature: any, freshness: Date }>>, useFeaturesDirectly) {
const now = new Date(); const now = new Date();
if(useFeaturesDirectly){ if (useFeaturesDirectly) {
// @ts-ignore // @ts-ignore
this.features = features this.features = features
}else if (features instanceof UIEventSource) { } else if (features instanceof UIEventSource) {
// @ts-ignore
this.features = features.map(features => features.map(f => ({feature: f, freshness: now}))) this.features = features.map(features => features.map(f => ({feature: f, freshness: now})))
} else { } else {
this.features = new UIEventSource(features.map(f => ({ this.features = new UIEventSource(features.map(f => ({

View file

@ -12,7 +12,8 @@ export default class WayHandlingApplyingFeatureSource implements FeatureSourceFo
public readonly layer; public readonly layer;
constructor(upstream: FeatureSourceForLayer) { constructor(upstream: FeatureSourceForLayer) {
this.name = "Wayhandling(" + upstream.name+")";
this.name = "Wayhandling(" + upstream.name + ")";
this.layer = upstream.layer this.layer = upstream.layer
const layer = upstream.layer.layerDef; const layer = upstream.layer.layerDef;

View file

@ -1,5 +1,5 @@
import FilteredLayer from "../../../Models/FilteredLayer"; import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer} from "../FeatureSource"; import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource"; import {UIEventSource} from "../../UIEventSource";
import Loc from "../../../Models/Loc"; import Loc from "../../../Models/Loc";
import DynamicTileSource from "./DynamicTileSource"; import DynamicTileSource from "./DynamicTileSource";
@ -8,7 +8,7 @@ import GeoJsonSource from "../Sources/GeoJsonSource";
export default class DynamicGeoJsonTileSource extends DynamicTileSource { export default class DynamicGeoJsonTileSource extends DynamicTileSource {
constructor(layer: FilteredLayer, constructor(layer: FilteredLayer,
registerLayer: (layer: FeatureSourceForLayer) => void, registerLayer: (layer: FeatureSourceForLayer & Tiled) => void,
state: { state: {
locationControl: UIEventSource<Loc> locationControl: UIEventSource<Loc>
leafletMap: any leafletMap: any

View file

@ -6,6 +6,7 @@ import {Utils} from "../../../Utils";
import {UIEventSource} from "../../UIEventSource"; import {UIEventSource} from "../../UIEventSource";
import Loc from "../../../Models/Loc"; import Loc from "../../../Models/Loc";
import TileHierarchy from "./TileHierarchy"; import TileHierarchy from "./TileHierarchy";
import {Tiles} from "../../../Models/TileRange";
/*** /***
* A tiled source which dynamically loads the required tiles at a fixed zoom level * A tiled source which dynamically loads the required tiles at a fixed zoom level
@ -46,9 +47,9 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor
// We'll retry later // We'll retry later
return undefined return undefined
} }
const tileRange = Utils.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) const tileRange = Tiles.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
const needed = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i)) const needed = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i))
if (needed.length === 0) { if (needed.length === 0) {
return undefined return undefined
} }
@ -63,7 +64,7 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor
} }
for (const neededIndex of neededIndexes) { for (const neededIndex of neededIndexes) {
self._loadedTiles.add(neededIndex) self._loadedTiles.add(neededIndex)
const src = constructTile( Utils.tile_from_index(neededIndex)) const src = constructTile(Tiles.tile_from_index(neededIndex))
if(src !== undefined){ if(src !== undefined){
self.loadedTiles.set(neededIndex, src) self.loadedTiles.set(neededIndex, src)
} }

View file

@ -5,6 +5,7 @@ import FilteredLayer from "../../../Models/FilteredLayer";
import {Utils} from "../../../Utils"; import {Utils} from "../../../Utils";
import {BBox} from "../../GeoOperations"; import {BBox} from "../../GeoOperations";
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"; import FeatureSourceMerger from "../Sources/FeatureSourceMerger";
import {Tiles} from "../../../Models/TileRange";
export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> { export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> {
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>(); public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
@ -13,7 +14,7 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer
public readonly layer: FilteredLayer; public readonly layer: FilteredLayer;
private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void; private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void;
constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void) { constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, index: number) => void) {
this.layer = layer; this.layer = layer;
this._handleTile = handleTile; this._handleTile = handleTile;
} }
@ -37,7 +38,7 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer
// We have to setup // We have to setup
const sources = new UIEventSource<FeatureSource[]>([src]) const sources = new UIEventSource<FeatureSource[]>([src])
this.sources.set(index, sources) this.sources.set(index, sources)
const merger = new FeatureSourceMerger(this.layer, index, BBox.fromTile(...Utils.tile_from_index(index)), sources) const merger = new FeatureSourceMerger(this.layer, index, BBox.fromTile(...Tiles.tile_from_index(index)), sources)
this.loadedTiles.set(index, merger) this.loadedTiles.set(index, merger)
this._handleTile(merger, index) this._handleTile(merger, index)
} }

View file

@ -4,7 +4,7 @@ import {Utils} from "../../../Utils";
import {BBox} from "../../GeoOperations"; import {BBox} from "../../GeoOperations";
import FilteredLayer from "../../../Models/FilteredLayer"; import FilteredLayer from "../../../Models/FilteredLayer";
import TileHierarchy from "./TileHierarchy"; import TileHierarchy from "./TileHierarchy";
import {feature} from "@turf/turf"; import {Tiles} from "../../../Models/TileRange";
/** /**
* Contains all features in a tiled fashion. * Contains all features in a tiled fashion.
@ -41,12 +41,12 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
this.x = x; this.x = x;
this.y = y; this.y = y;
this.bbox = BBox.fromTile(z, x, y) this.bbox = BBox.fromTile(z, x, y)
this.tileIndex = Utils.tile_index(z, x, y) this.tileIndex = Tiles.tile_index(z, x, y)
this.name = `TiledFeatureSource(${z},${x},${y})` this.name = `TiledFeatureSource(${z},${x},${y})`
this.parent = parent; this.parent = parent;
this.layer = options.layer this.layer = options.layer
options = options ?? {} options = options ?? {}
this.maxFeatureCount = options?.maxFeatureCount ?? 500; this.maxFeatureCount = options?.maxFeatureCount ?? 250;
this.maxzoom = options.maxZoomLevel ?? 18 this.maxzoom = options.maxZoomLevel ?? 18
this.options = options; this.options = options;
if (parent === undefined) { if (parent === undefined) {
@ -61,7 +61,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
} else { } else {
this.root = this.parent.root; this.root = this.parent.root;
this.loadedTiles = this.root.loadedTiles; this.loadedTiles = this.root.loadedTiles;
const i = Utils.tile_index(z, x, y) const i = Tiles.tile_index(z, x, y)
this.root.loadedTiles.set(i, this) this.root.loadedTiles.set(i, this)
} }
this.features = new UIEventSource<any[]>([]) this.features = new UIEventSource<any[]>([])
@ -143,9 +143,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
for (const feature of features) { for (const feature of features) {
const bbox = BBox.get(feature.feature) const bbox = BBox.get(feature.feature)
if (this.options.minZoomLevel === undefined) { if (this.options.dontEnforceMinZoom || this.options.minZoomLevel === undefined) {
if (bbox.isContainedIn(this.upper_left.bbox)) { if (bbox.isContainedIn(this.upper_left.bbox)) {
ulf.push(feature) ulf.push(feature)
} else if (bbox.isContainedIn(this.upper_right.bbox)) { } else if (bbox.isContainedIn(this.upper_right.bbox)) {
@ -186,6 +184,11 @@ export interface TiledFeatureSourceOptions {
readonly maxFeatureCount?: number, readonly maxFeatureCount?: number,
readonly maxZoomLevel?: number, readonly maxZoomLevel?: number,
readonly minZoomLevel?: number, readonly minZoomLevel?: number,
/**
* IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated.
* Setting 'dontEnforceMinZoomLevel' will still allow bigger zoom levels for those features
*/
readonly dontEnforceMinZoom?: boolean,
readonly registerTile?: (tile: TiledFeatureSource & Tiled) => void, readonly registerTile?: (tile: TiledFeatureSource & Tiled) => void,
readonly layer?: FilteredLayer readonly layer?: FilteredLayer
} }

View file

@ -6,6 +6,7 @@ import TileHierarchy from "./TileHierarchy";
import {Utils} from "../../../Utils"; import {Utils} from "../../../Utils";
import SaveTileToLocalStorageActor from "../Actors/SaveTileToLocalStorageActor"; import SaveTileToLocalStorageActor from "../Actors/SaveTileToLocalStorageActor";
import {BBox} from "../../GeoOperations"; import {BBox} from "../../GeoOperations";
import {Tiles} from "../../../Models/TileRange";
export default class TiledFromLocalStorageSource implements TileHierarchy<FeatureSourceForLayer & Tiled> { export default class TiledFromLocalStorageSource implements TileHierarchy<FeatureSourceForLayer & Tiled> {
public loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>(); public loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
@ -17,6 +18,7 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
leafletMap: any leafletMap: any
}) { }) {
const undefinedTiles = new Set<number>()
const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-" const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-"
// @ts-ignore // @ts-ignore
const indexes: number[] = Object.keys(localStorage) const indexes: number[] = Object.keys(localStorage)
@ -27,7 +29,7 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
return Number(key.substring(prefix.length)); return Number(key.substring(prefix.length));
}) })
console.log("Layer", layer.layerDef.id, "has following tiles in available in localstorage", indexes.map(i => Utils.tile_from_index(i).join("/")).join(", ")) console.log("Layer", layer.layerDef.id, "has following tiles in available in localstorage", indexes.map(i => Tiles.tile_from_index(i).join("/")).join(", "))
const zLevels = indexes.map(i => i % 100) const zLevels = indexes.map(i => i % 100)
const indexesSet = new Set(indexes) const indexesSet = new Set(indexes)
@ -57,9 +59,9 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
const needed = [] const needed = []
for (let z = minZoom; z <= maxZoom; z++) { for (let z = minZoom; z <= maxZoom; z++) {
const tileRange = Utils.TileRangeBetween(z, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) const tileRange = Tiles.TileRangeBetween(z, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
const neededZ = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(z, x, y)) const neededZ = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(z, x, y))
.filter(i => !self.loadedTiles.has(i) && indexesSet.has(i)) .filter(i => !self.loadedTiles.has(i) && !undefinedTiles.has(i) && indexesSet.has(i))
needed.push(...neededZ) needed.push(...neededZ)
} }
@ -84,12 +86,13 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
features: new UIEventSource<{ feature: any; freshness: Date }[]>(features), features: new UIEventSource<{ feature: any; freshness: Date }[]>(features),
name: "FromLocalStorage(" + key + ")", name: "FromLocalStorage(" + key + ")",
tileIndex: neededIndex, tileIndex: neededIndex,
bbox: BBox.fromTile(...Utils.tile_from_index(neededIndex)) bbox: BBox.fromTileIndex(neededIndex)
} }
handleFeatureSource(src, neededIndex) handleFeatureSource(src, neededIndex)
self.loadedTiles.set(neededIndex, src) self.loadedTiles.set(neededIndex, src)
} catch (e) { } catch (e) {
console.error("Could not load data tile from local storage due to", e) console.error("Could not load data tile from local storage due to", e)
undefinedTiles.add(neededIndex)
} }
} }

View file

@ -1,5 +1,6 @@
import * as turf from '@turf/turf' import * as turf from '@turf/turf'
import {Utils} from "../Utils"; import {Utils} from "../Utils";
import {Tiles} from "../Models/TileRange";
export class GeoOperations { export class GeoOperations {
@ -8,7 +9,7 @@ export class GeoOperations {
} }
/** /**
* Converts a GeoJSon feature to a point feature * Converts a GeoJson feature to a point GeoJson feature
* @param feature * @param feature
*/ */
static centerpoint(feature: any) { static centerpoint(feature: any) {
@ -451,8 +452,12 @@ export class BBox {
} }
} }
static fromTile(z: number, x: number, y: number) { static fromTile(z: number, x: number, y: number): BBox {
return new BBox(Utils.tile_bounds_lon_lat(z, x, y)) return new BBox(Tiles.tile_bounds_lon_lat(z, x, y))
}
static fromTileIndex(i: number): BBox {
return BBox.fromTile(...Tiles.tile_from_index(i))
} }
getEast() { getEast() {

View file

@ -12,8 +12,11 @@ export default abstract class ImageAttributionSource {
if (cached !== undefined) { if (cached !== undefined) {
return cached; return cached;
} }
const src = this.DownloadAttribution(url) const src = new UIEventSource(undefined)
this._cache.set(url, src) this._cache.set(url, src)
this.DownloadAttribution(url).then(license =>
src.setData(license))
.catch(e => console.error("Could not download license information for ", url, " due to", e))
return src; return src;
} }
@ -21,10 +24,10 @@ export default abstract class ImageAttributionSource {
public abstract SourceIcon(backlinkSource?: string): BaseUIElement; public abstract SourceIcon(backlinkSource?: string): BaseUIElement;
/*Converts a value to a URL. Can return null if not applicable*/ /*Converts a value to a URL. Can return null if not applicable*/
public PrepareUrl(value: string): string | UIEventSource<string>{ public PrepareUrl(value: string): string | UIEventSource<string> {
return value; return value;
} }
protected abstract DownloadAttribution(url: string): UIEventSource<LicenseInfo>; protected abstract DownloadAttribution(url: string): Promise<LicenseInfo>;
} }

View file

@ -2,8 +2,9 @@
import $ from "jquery" import $ from "jquery"
import {LicenseInfo} from "./Wikimedia"; import {LicenseInfo} from "./Wikimedia";
import ImageAttributionSource from "./ImageAttributionSource"; import ImageAttributionSource from "./ImageAttributionSource";
import {UIEventSource} from "../UIEventSource";
import BaseUIElement from "../../UI/BaseUIElement"; import BaseUIElement from "../../UI/BaseUIElement";
import {Utils} from "../../Utils";
import Constants from "../../Models/Constants";
export class Imgur extends ImageAttributionSource { export class Imgur extends ImageAttributionSource {
@ -86,35 +87,18 @@ export class Imgur extends ImageAttributionSource {
return undefined; return undefined;
} }
protected DownloadAttribution(url: string): UIEventSource<LicenseInfo> { protected async DownloadAttribution(url: string): Promise<LicenseInfo> {
const src = new UIEventSource<LicenseInfo>(undefined)
const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0]; const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0];
const apiUrl = 'https://api.imgur.com/3/image/' + hash; const apiUrl = 'https://api.imgur.com/3/image/' + hash;
const apiKey = '7070e7167f0a25a'; const response = await Utils.downloadJson(apiUrl, {Authorization: 'Client-ID ' + Constants.ImgurApiKey})
const settings = {
async: true,
crossDomain: true,
processData: false,
contentType: false,
type: 'GET',
url: apiUrl,
headers: {
Authorization: 'Client-ID ' + apiKey,
Accept: 'application/json',
},
};
// @ts-ignore
$.ajax(settings).done(function (response) {
const descr: string = response.data.description ?? ""; const descr: string = response.data.description ?? "";
const data: any = {}; const data: any = {};
for (const tag of descr.split("\n")) { for (const tag of descr.split("\n")) {
const kv = tag.split(":"); const kv = tag.split(":");
const k = kv[0]; const k = kv[0];
data[k] = kv[1].replace("\r", ""); data[k] = kv[1]?.replace("\r", "");
} }
@ -123,13 +107,7 @@ export class Imgur extends ImageAttributionSource {
licenseInfo.licenseShortName = data.license; licenseInfo.licenseShortName = data.license;
licenseInfo.artist = data.author; licenseInfo.artist = data.author;
src.setData(licenseInfo) return licenseInfo
}).fail((reason) => {
console.log("Getting metadata from to IMGUR failed", reason)
});
return src;
} }

View file

@ -24,7 +24,7 @@ export class Mapillary extends ImageAttributionSource {
} { } {
if (value.startsWith("https://a.mapillary.com")) { if (value.startsWith("https://a.mapillary.com")) {
const key = value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1) const key = value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1)
return {key:key, isApiv4: !isNaN(Number(key))}; return {key: key, isApiv4: !isNaN(Number(key))};
} }
const newApiFormat = value.match(/https?:\/\/www.mapillary.com\/app\/\?pKey=([0-9]*)/) const newApiFormat = value.match(/https?:\/\/www.mapillary.com\/app\/\?pKey=([0-9]*)/)
if (newApiFormat !== null) { if (newApiFormat !== null) {
@ -32,9 +32,9 @@ export class Mapillary extends ImageAttributionSource {
} }
const mapview = value.match(/https?:\/\/www.mapillary.com\/map\/im\/(.*)/) const mapview = value.match(/https?:\/\/www.mapillary.com\/map\/im\/(.*)/)
if(mapview !== null){ if (mapview !== null) {
const key = mapview[1] const key = mapview[1]
return {key:key, isApiv4: !isNaN(Number(key))}; return {key: key, isApiv4: !isNaN(Number(key))};
} }
@ -62,11 +62,11 @@ export class Mapillary extends ImageAttributionSource {
return `https://images.mapillary.com/${keyV.key}/thumb-640.jpg?client_id=${Mapillary.client_token_v3}` return `https://images.mapillary.com/${keyV.key}/thumb-640.jpg?client_id=${Mapillary.client_token_v3}`
} else { } else {
const key = keyV.key; const key = keyV.key;
if(Mapillary.v4_cached_urls.has(key)){ if (Mapillary.v4_cached_urls.has(key)) {
return Mapillary.v4_cached_urls.get(key) return Mapillary.v4_cached_urls.get(key)
} }
const metadataUrl ='https://graph.mapillary.com/' + key + '?fields=thumb_1024_url&&access_token=' + Mapillary.client_token_v4; const metadataUrl = 'https://graph.mapillary.com/' + key + '?fields=thumb_1024_url&&access_token=' + Mapillary.client_token_v4;
const source = new UIEventSource<string>(undefined) const source = new UIEventSource<string>(undefined)
Mapillary.v4_cached_urls.set(key, source) Mapillary.v4_cached_urls.set(key, source)
Utils.downloadJson(metadataUrl).then( Utils.downloadJson(metadataUrl).then(
@ -79,31 +79,28 @@ export class Mapillary extends ImageAttributionSource {
} }
} }
protected DownloadAttribution(url: string): UIEventSource<LicenseInfo> { protected async DownloadAttribution(url: string): Promise<LicenseInfo> {
const keyV = Mapillary.ExtractKeyFromURL(url) const keyV = Mapillary.ExtractKeyFromURL(url)
if(keyV.isApiv4){ if (keyV.isApiv4) {
const license = new LicenseInfo() const license = new LicenseInfo()
license.artist = "Contributor name unavailable"; license.artist = "Contributor name unavailable";
license.license = "CC BY-SA 4.0"; license.license = "CC BY-SA 4.0";
// license.license = "Creative Commons Attribution-ShareAlike 4.0 International License"; // license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
license.attributionRequired = true; license.attributionRequired = true;
return new UIEventSource<LicenseInfo>(license) return license
} }
const key = keyV.key const key = keyV.key
const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2` const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`
const source = new UIEventSource<LicenseInfo>(undefined) const data = await Utils.downloadJson(metadataURL)
Utils.downloadJson(metadataURL).then(data => {
const license = new LicenseInfo(); const license = new LicenseInfo();
license.artist = data.properties?.username; license.artist = data.properties?.username;
license.licenseShortName = "CC BY-SA 4.0"; license.licenseShortName = "CC BY-SA 4.0";
license.license = "Creative Commons Attribution-ShareAlike 4.0 International License"; license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
license.attributionRequired = true; license.attributionRequired = true;
source.setData(license);
})
return source return license
} }
} }

View file

@ -1,7 +1,6 @@
import ImageAttributionSource from "./ImageAttributionSource"; import ImageAttributionSource from "./ImageAttributionSource";
import BaseUIElement from "../../UI/BaseUIElement"; import BaseUIElement from "../../UI/BaseUIElement";
import Svg from "../../Svg"; import Svg from "../../Svg";
import {UIEventSource} from "../UIEventSource";
import Link from "../../UI/Base/Link"; import Link from "../../UI/Base/Link";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
@ -124,28 +123,23 @@ export class Wikimedia extends ImageAttributionSource {
.replace(/'/g, '%27'); .replace(/'/g, '%27');
} }
protected DownloadAttribution(filename: string): UIEventSource<LicenseInfo> { protected async DownloadAttribution(filename: string): Promise<LicenseInfo> {
const source = new UIEventSource<LicenseInfo>(undefined);
filename = Wikimedia.ExtractFileName(filename) filename = Wikimedia.ExtractFileName(filename)
if (filename === "") { if (filename === "") {
return source; return undefined;
} }
const url = "https://en.wikipedia.org/w/" + const url = "https://en.wikipedia.org/w/" +
"api.php?action=query&prop=imageinfo&iiprop=extmetadata&" + "api.php?action=query&prop=imageinfo&iiprop=extmetadata&" +
"titles=" + filename + "titles=" + filename +
"&format=json&origin=*"; "&format=json&origin=*";
Utils.downloadJson(url).then( const data = await Utils.downloadJson(url)
data => {
const licenseInfo = new LicenseInfo(); const licenseInfo = new LicenseInfo();
const license = (data.query.pages[-1].imageinfo ?? [])[0]?.extmetadata; const license = (data.query.pages[-1].imageinfo ?? [])[0]?.extmetadata;
if (license === undefined) { if (license === undefined) {
console.error("This file has no usable metedata or license attached... Please fix the license info file yourself!") console.error("This file has no usable metedata or license attached... Please fix the license info file yourself!")
source.setData(null) return undefined;
return;
} }
licenseInfo.artist = license.Artist?.value; licenseInfo.artist = license.Artist?.value;
@ -156,11 +150,7 @@ export class Wikimedia extends ImageAttributionSource {
licenseInfo.licenseShortName = license.LicenseShortName?.value; licenseInfo.licenseShortName = license.LicenseShortName?.value;
licenseInfo.credit = license.Credit?.value; licenseInfo.credit = license.Credit?.value;
licenseInfo.description = license.ImageDescription?.value; licenseInfo.description = license.ImageDescription?.value;
source.setData(licenseInfo); return licenseInfo;
}
)
return source;
} }

View file

@ -2,6 +2,7 @@ import SimpleMetaTagger from "./SimpleMetaTagger";
import {ExtraFuncParams, ExtraFunction} from "./ExtraFunction"; import {ExtraFuncParams, ExtraFunction} from "./ExtraFunction";
import {UIEventSource} from "./UIEventSource"; import {UIEventSource} from "./UIEventSource";
import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import State from "../State";
/** /**
@ -31,39 +32,57 @@ export default class MetaTagging {
return; return;
} }
for (const metatag of SimpleMetaTagger.metatags) {
try { const metatagsToApply: SimpleMetaTagger [] = []
for (const metatag of SimpleMetaTagger.metatags) {
if (metatag.includesDates) { if (metatag.includesDates) {
if (options.includeDates ?? true) { if (options.includeDates ?? true) {
metatag.addMetaTags(features); metatagsToApply.push(metatag)
} }
} else { } else {
if (options.includeNonDates ?? true) { if (options.includeNonDates ?? true) {
metatag.addMetaTags(features); metatagsToApply.push(metatag)
}
} }
} }
// The calculated functions - per layer - which add the new keys
const layerFuncs = this.createRetaggingFunc(layer)
for (let i = 0; i < features.length; i++) {
const ff = features[i];
const feature = ff.feature
const freshness = ff.freshness
let somethingChanged = false
for (const metatag of metatagsToApply) {
try {
if(!metatag.keys.some(key => feature.properties[key] === undefined)){
// All keys are already defined, we probably already ran this one
continue
}
somethingChanged = somethingChanged || metatag.applyMetaTagsOnFeature(feature, freshness)
} catch (e) { } catch (e) {
console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e) console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e)
} }
} }
// The functions - per layer - which add the new keys if(layerFuncs !== undefined){
const layerFuncs = this.createRetaggingFunc(layer)
if (layerFuncs !== undefined) {
for (const feature of features) {
try { try {
layerFuncs(params, feature.feature) layerFuncs(params, feature)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
somethingChanged = true
}
if(somethingChanged){
State.state.allElements.getEventSourceById(feature.properties.id).ping()
} }
} }
} }
private static createRetaggingFunc(layer: LayerConfig): private static createRetaggingFunc(layer: LayerConfig):
((params: ExtraFuncParams, feature: any) => void) { ((params: ExtraFuncParams, feature: any) => void) {
const calculatedTags: [string, string][] = layer.calculatedTags; const calculatedTags: [string, string][] = layer.calculatedTags;
@ -92,11 +111,13 @@ export default class MetaTagging {
d = JSON.stringify(d); d = JSON.stringify(d);
} }
feature.properties[key] = d; feature.properties[key] = d;
console.log("Written a delayed calculated tag onto ", feature.properties.id, ": ", key, ":==", d)
}) })
result = result.data result = result.data
} }
if (result === undefined || result === "") { if (result === undefined || result === "") {
console.log("Calculated tag for", key, "gave undefined", feature.properties.id)
return; return;
} }
if (typeof result !== "string") { if (typeof result !== "string") {
@ -104,6 +125,7 @@ export default class MetaTagging {
result = JSON.stringify(result); result = JSON.stringify(result);
} }
feature.properties[key] = result; feature.properties[key] = result;
console.log("Written a calculated tag onto ", feature.properties.id, ": ", key, ":==", result)
} catch (e) { } catch (e) {
if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) {
console.warn("Could not calculate a calculated tag defined by " + code + " due to " + e + ". This is code defined in the theme. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e) console.warn("Could not calculate a calculated tag defined by " + code + " due to " + e + ". This is code defined in the theme. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e)

View file

@ -94,6 +94,7 @@ export class OsmConnection {
self.AttemptLogin() self.AttemptLogin()
} }
}); });
this.isLoggedIn.addCallbackAndRunD(li => console.log("User is logged in!", li))
this._dryRun = dryRun; this._dryRun = dryRun;
this.updateAuthObject(); this.updateAuthObject();

View file

@ -31,7 +31,7 @@ export default class SimpleMetaTagger {
"_version_number"], "_version_number"],
doc: "Information about the last edit of this object." doc: "Information about the last edit of this object."
}, },
(feature) => {/*Note: also handled by 'UpdateTagsFromOsmAPI'*/ (feature) => {/*Note: also called by 'UpdateTagsFromOsmAPI'*/
const tgs = feature.properties; const tgs = feature.properties;
@ -48,6 +48,7 @@ export default class SimpleMetaTagger {
move("changeset", "_last_edit:changeset") move("changeset", "_last_edit:changeset")
move("timestamp", "_last_edit:timestamp") move("timestamp", "_last_edit:timestamp")
move("version", "_version_number") move("version", "_version_number")
return true;
} }
) )
private static latlon = new SimpleMetaTagger({ private static latlon = new SimpleMetaTagger({
@ -62,6 +63,7 @@ export default class SimpleMetaTagger {
feature.properties["_lon"] = "" + lon; feature.properties["_lon"] = "" + lon;
feature._lon = lon; // This is dirty, I know feature._lon = lon; // This is dirty, I know
feature._lat = lat; feature._lat = lat;
return true;
}) })
); );
private static surfaceArea = new SimpleMetaTagger( private static surfaceArea = new SimpleMetaTagger(
@ -74,6 +76,7 @@ export default class SimpleMetaTagger {
feature.properties["_surface"] = "" + sqMeters; feature.properties["_surface"] = "" + sqMeters;
feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10; feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10;
feature.area = sqMeters; feature.area = sqMeters;
return true;
}) })
); );
@ -118,9 +121,7 @@ export default class SimpleMetaTagger {
} }
} }
if (rewritten) { return rewritten
State.state.allElements.getEventSourceById(feature.id).ping();
}
}) })
) )
@ -135,6 +136,7 @@ export default class SimpleMetaTagger {
const km = Math.floor(l / 1000) const km = Math.floor(l / 1000)
const kmRest = Math.round((l - km * 1000) / 100) const kmRest = Math.round((l - km * 1000) / 100)
feature.properties["_length:km"] = "" + km + "." + kmRest feature.properties["_length:km"] = "" + km + "." + kmRest
return true;
}) })
) )
private static country = new SimpleMetaTagger( private static country = new SimpleMetaTagger(
@ -144,7 +146,6 @@ export default class SimpleMetaTagger {
}, },
feature => { feature => {
let centerPoint: any = GeoOperations.centerpoint(feature); let centerPoint: any = GeoOperations.centerpoint(feature);
const lat = centerPoint.geometry.coordinates[1]; const lat = centerPoint.geometry.coordinates[1];
const lon = centerPoint.geometry.coordinates[0]; const lon = centerPoint.geometry.coordinates[0];
@ -157,11 +158,11 @@ export default class SimpleMetaTagger {
const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id);
tagsSource.ping(); tagsSource.ping();
} }
} catch (e) { } catch (e) {
console.warn(e) console.warn(e)
} }
}) })
return false;
} }
) )
private static isOpen = new SimpleMetaTagger( private static isOpen = new SimpleMetaTagger(
@ -174,7 +175,7 @@ export default class SimpleMetaTagger {
if (Utils.runningFromConsole) { if (Utils.runningFromConsole) {
// We are running from console, thus probably creating a cache // We are running from console, thus probably creating a cache
// isOpen is irrelevant // isOpen is irrelevant
return return false
} }
const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id);
@ -199,7 +200,7 @@ export default class SimpleMetaTagger {
if (oldNextChange > (new Date()).getTime() && if (oldNextChange > (new Date()).getTime() &&
tags["_isOpen:oldvalue"] === tags["opening_hours"]) { tags["_isOpen:oldvalue"] === tags["opening_hours"]) {
// Already calculated and should not yet be triggered // Already calculated and should not yet be triggered
return; return false;
} }
tags["_isOpen"] = oh.getState() ? "yes" : "no"; tags["_isOpen"] = oh.getState() ? "yes" : "no";
@ -227,6 +228,7 @@ export default class SimpleMetaTagger {
} }
} }
updateTags(); updateTags();
return true;
} catch (e) { } catch (e) {
console.warn("Error while parsing opening hours of ", tags.id, e); console.warn("Error while parsing opening hours of ", tags.id, e);
tags["_isOpen"] = "parse_error"; tags["_isOpen"] = "parse_error";
@ -244,11 +246,11 @@ export default class SimpleMetaTagger {
const tags = feature.properties; const tags = feature.properties;
const direction = tags["camera:direction"] ?? tags["direction"]; const direction = tags["camera:direction"] ?? tags["direction"];
if (direction === undefined) { if (direction === undefined) {
return; return false;
} }
const n = cardinalDirections[direction] ?? Number(direction); const n = cardinalDirections[direction] ?? Number(direction);
if (isNaN(n)) { if (isNaN(n)) {
return; return false;
} }
// The % operator has range (-360, 360). We apply a trick to get [0, 360). // The % operator has range (-360, 360). We apply a trick to get [0, 360).
@ -256,7 +258,7 @@ export default class SimpleMetaTagger {
tags["_direction:numerical"] = normalized; tags["_direction:numerical"] = normalized;
tags["_direction:leftright"] = normalized <= 180 ? "right" : "left"; tags["_direction:leftright"] = normalized <= 180 ? "right" : "left";
return true;
}) })
) )
private static carriageWayWidth = new SimpleMetaTagger( private static carriageWayWidth = new SimpleMetaTagger(
@ -268,7 +270,7 @@ export default class SimpleMetaTagger {
const properties = feature.properties; const properties = feature.properties;
if (properties["width:carriageway"] === undefined) { if (properties["width:carriageway"] === undefined) {
return; return false;
} }
const carWidth = 2; const carWidth = 2;
@ -366,7 +368,7 @@ export default class SimpleMetaTagger {
properties["_width:difference"] = Utils.Round(targetWidth - width); properties["_width:difference"] = Utils.Round(targetWidth - width);
properties["_width:difference:no_pedestrians"] = Utils.Round(targetWidthIgnoringPedestrians - width); properties["_width:difference:no_pedestrians"] = Utils.Round(targetWidthIgnoringPedestrians - width);
return true;
} }
); );
private static currentTime = new SimpleMetaTagger( private static currentTime = new SimpleMetaTagger(
@ -375,7 +377,7 @@ export default class SimpleMetaTagger {
doc: "Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely", doc: "Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely",
includesDates: true includesDates: true
}, },
(feature, _, freshness) => { (feature, freshness) => {
const now = new Date(); const now = new Date();
if (typeof freshness === "string") { if (typeof freshness === "string") {
@ -394,7 +396,7 @@ export default class SimpleMetaTagger {
feature.properties["_now:datetime"] = datetime(now); feature.properties["_now:datetime"] = datetime(now);
feature.properties["_loaded:date"] = date(freshness); feature.properties["_loaded:date"] = date(freshness);
feature.properties["_loaded:datetime"] = datetime(freshness); feature.properties["_loaded:datetime"] = datetime(freshness);
return true;
} }
) )
public static metatags = [ public static metatags = [
@ -413,12 +415,18 @@ export default class SimpleMetaTagger {
public readonly keys: string[]; public readonly keys: string[];
public readonly doc: string; public readonly doc: string;
public readonly includesDates: boolean public readonly includesDates: boolean
private readonly _f: (feature: any, index: number, freshness: Date) => void; public readonly applyMetaTagsOnFeature: (feature: any, freshness: Date) => boolean;
constructor(docs: { keys: string[], doc: string, includesDates?: boolean }, f: ((feature: any, index: number, freshness: Date) => void)) { /***
* A function that adds some extra data to a feature
* @param docs: what does this extra data do?
* @param f: apply the changes. Returns true if something changed
*/
constructor(docs: { keys: string[], doc: string, includesDates?: boolean },
f: ((feature: any, freshness: Date) => boolean)) {
this.keys = docs.keys; this.keys = docs.keys;
this.doc = docs.doc; this.doc = docs.doc;
this._f = f; this.applyMetaTagsOnFeature = f;
this.includesDates = docs.includesDates ?? false; this.includesDates = docs.includesDates ?? false;
for (const key of docs.keys) { for (const key of docs.keys) {
if (!key.startsWith('_') && key.toLowerCase().indexOf("theme") < 0) { if (!key.startsWith('_') && key.toLowerCase().indexOf("theme") < 0) {
@ -450,12 +458,4 @@ export default class SimpleMetaTagger {
return new Combine(subElements).SetClass("flex-col") return new Combine(subElements).SetClass("flex-col")
} }
public addMetaTags(features: { feature: any, freshness: Date }[]) {
for (let i = 0; i < features.length; i++) {
let feature = features[i];
this._f(feature.feature, i, feature.freshness);
}
}
} }

View file

@ -3,6 +3,7 @@ import {Utils} from "../Utils";
export default class Constants { export default class Constants {
public static vNumber = "0.10.0-alpha-1"; public static vNumber = "0.10.0-alpha-1";
public static ImgurApiKey = '7070e7167f0a25a'
// The user journey states thresholds when a new feature gets unlocked // The user journey states thresholds when a new feature gets unlocked
public static userJourney = { public static userJourney = {

View file

@ -18,6 +18,7 @@ import FilterConfig from "./FilterConfig";
import {Unit} from "../Unit"; import {Unit} from "../Unit";
import DeleteConfig from "./DeleteConfig"; import DeleteConfig from "./DeleteConfig";
import Svg from "../../Svg"; import Svg from "../../Svg";
import Img from "../../UI/Base/Img";
export default class LayerConfig { export default class LayerConfig {
static WAYHANDLING_DEFAULT = 0; static WAYHANDLING_DEFAULT = 0;
@ -495,19 +496,20 @@ export default class LayerConfig {
const iconUrlStatic = render(this.icon); const iconUrlStatic = render(this.icon);
const self = this; const self = this;
function genHtmlFromString(sourcePart: string, rotation: string, style?: string): BaseUIElement { function genHtmlFromString(sourcePart: string, rotation: string): BaseUIElement {
style = style ?? `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
let html: BaseUIElement = new FixedUiElement( let html: BaseUIElement = new FixedUiElement(
`<img src="${sourcePart}" style="${style}" />` `<img src="${sourcePart}" style="${style}" />`
); );
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/); const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/);
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) { if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
html = new Combine([ html = new Img(
(Svg.All[match[1] + ".svg"] as string).replace( (Svg.All[match[1] + ".svg"] as string).replace(
/#000000/g, /#000000/g,
match[2] match[2]
), ),
]).SetStyle(style); true
).SetStyle(style);
} }
return html; return html;
} }
@ -540,7 +542,7 @@ export default class LayerConfig {
.filter((prt) => prt != ""); .filter((prt) => prt != "");
for (const badgePartStr of partDefs) { for (const badgePartStr of partDefs) {
badgeParts.push(genHtmlFromString(badgePartStr, "0", `width:unset;height:100%;display:block;`)); badgeParts.push(genHtmlFromString(badgePartStr, "0"));
} }
const badgeCompound = new Combine(badgeParts).SetStyle( const badgeCompound = new Combine(badgeParts).SetStyle(

View file

@ -5,7 +5,6 @@ import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
import AllKnownLayers from "../../Customizations/AllKnownLayers"; import AllKnownLayers from "../../Customizations/AllKnownLayers";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import LayerConfig from "./LayerConfig"; import LayerConfig from "./LayerConfig";
import {Unit} from "../Unit";
import {LayerConfigJson} from "./Json/LayerConfigJson"; import {LayerConfigJson} from "./Json/LayerConfigJson";
export default class LayoutConfig { export default class LayoutConfig {
@ -87,6 +86,9 @@ export default class LayoutConfig {
this.startZoom = json.startZoom; this.startZoom = json.startZoom;
this.startLat = json.startLat; this.startLat = json.startLat;
this.startLon = json.startLon; this.startLon = json.startLon;
if(json.widenFactor < 1){
throw "Widenfactor too small"
}
this.widenFactor = json.widenFactor ?? 1.5; this.widenFactor = json.widenFactor ?? 1.5;
this.roamingRenderings = (json.roamingRenderings ?? []).map((tr, i) => { this.roamingRenderings = (json.roamingRenderings ?? []).map((tr, i) => {
if (typeof tr === "string") { if (typeof tr === "string") {

View file

@ -6,3 +6,105 @@ export interface TileRange {
total: number, total: number,
zoomlevel: number zoomlevel: number
} }
export class Tiles {
public static MapRange<T>(tileRange: TileRange, f: (x: number, y: number) => T): T[] {
const result: T[] = []
for (let x = tileRange.xstart; x <= tileRange.xend; x++) {
for (let y = tileRange.ystart; y <= tileRange.yend; y++) {
const t = f(x, y);
result.push(t)
}
}
return result;
}
private static tile2long(x, z) {
return (x / Math.pow(2, z) * 360 - 180);
}
private static tile2lat(y, z) {
const n = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
return (180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))));
}
private static lon2tile(lon, zoom) {
return (Math.floor((lon + 180) / 360 * Math.pow(2, zoom)));
}
private static lat2tile(lat, zoom) {
return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom)));
}
/**
* Calculates the tile bounds of the
* @param z
* @param x
* @param y
* @returns [[maxlat, minlon], [minlat, maxlon]]
*/
static tile_bounds(z: number, x: number, y: number): [[number, number], [number, number]] {
return [[Tiles.tile2lat(y, z), Tiles.tile2long(x, z)], [Tiles.tile2lat(y + 1, z), Tiles.tile2long(x + 1, z)]]
}
static tile_bounds_lon_lat(z: number, x: number, y: number): [[number, number], [number, number]] {
return [[Tiles.tile2long(x, z), Tiles.tile2lat(y, z)], [Tiles.tile2long(x + 1, z), Tiles.tile2lat(y + 1, z)]]
}
/**
* Returns the centerpoint [lon, lat] of the specified tile
* @param z
* @param x
* @param y
*/
static centerPointOf(z: number, x: number, y: number): [number, number]{
return [(Tiles.tile2long(x, z) + Tiles.tile2long(x+1, z)) / 2, (Tiles.tile2lat(y, z) + Tiles.tile2lat(y+1, z)) / 2]
}
static tile_index(z: number, x: number, y: number): number {
return ((x * (2 << z)) + y) * 100 + z
}
/**
* Given a tile index number, returns [z, x, y]
* @param index
* @returns 'zxy'
*/
static tile_from_index(index: number): [number, number, number] {
const z = index % 100;
const factor = 2 << z
index = Math.floor(index / 100)
const x = Math.floor(index / factor)
return [z, x, index % factor]
}
/**
* Return x, y of the tile containing (lat, lon) on the given zoom level
*/
static embedded_tile(lat: number, lon: number, z: number): { x: number, y: number, z: number } {
return {x: Tiles.lon2tile(lon, z), y: Tiles.lat2tile(lat, z), z: z}
}
static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1: number, lon1: number): TileRange {
const t0 = Tiles.embedded_tile(lat0, lon0, zoomlevel)
const t1 = Tiles.embedded_tile(lat1, lon1, zoomlevel)
const xstart = Math.min(t0.x, t1.x)
const xend = Math.max(t0.x, t1.x)
const ystart = Math.min(t0.y, t1.y)
const yend = Math.max(t0.y, t1.y)
const total = (1 + xend - xstart) * (1 + yend - ystart)
return {
xstart: xstart,
xend: xend,
ystart: ystart,
yend: yend,
total: total,
zoomlevel: zoomlevel
}
}
}

View file

@ -36,6 +36,8 @@ export default class ScrollableFullScreen extends UIElement {
this._component = this.BuildComponent(title("desktop"), content("desktop"), isShown) this._component = this.BuildComponent(title("desktop"), content("desktop"), isShown)
.SetClass("hidden md:block"); .SetClass("hidden md:block");
this._fullscreencomponent = this.BuildComponent(title("mobile"), content("mobile"), isShown); this._fullscreencomponent = this.BuildComponent(title("mobile"), content("mobile"), isShown);
const self = this; const self = this;
isShown.addCallback(isShown => { isShown.addCallback(isShown => {
if (isShown) { if (isShown) {

View file

@ -2,22 +2,23 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
export class VariableUiElement extends BaseUIElement { export class VariableUiElement extends BaseUIElement {
private _element: HTMLElement; private readonly _contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>;
constructor( constructor(contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>) {
contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>
) {
super(); super();
this._contents = contents;
this._element = document.createElement("span"); }
const el = this._element;
contents.addCallbackAndRun((contents) => { protected InnerConstructElement(): HTMLElement {
const el = document.createElement("span");
this._contents.addCallbackAndRun((contents) => {
while (el.firstChild) { while (el.firstChild) {
el.removeChild(el.lastChild); el.removeChild(el.lastChild);
} }
if (contents === undefined) { if (contents === undefined) {
return el; return;
} }
if (typeof contents === "string") { if (typeof contents === "string") {
el.innerHTML = contents; el.innerHTML = contents;
@ -35,9 +36,6 @@ export class VariableUiElement extends BaseUIElement {
} }
} }
}); });
} return el;
protected InnerConstructElement(): HTMLElement {
return this._element;
} }
} }

View file

@ -100,8 +100,6 @@ export default abstract class BaseUIElement {
throw "ERROR! This is not a correct baseUIElement: " + this.constructor.name throw "ERROR! This is not a correct baseUIElement: " + this.constructor.name
} }
try { try {
const el = this.InnerConstructElement(); const el = this.InnerConstructElement();
if (el === undefined) { if (el === undefined) {

View file

@ -13,17 +13,16 @@ export default class Attribution extends VariableUiElement {
} }
super( super(
license.map((license: LicenseInfo) => { license.map((license: LicenseInfo) => {
if(license === undefined){
if (license?.artist === undefined) { return undefined
return undefined;
} }
return new Combine([ return new Combine([
icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"), icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"),
new Combine([ new Combine([
Translations.W(license.artist).SetClass("block font-bold"), Translations.W(license?.artist ?? ".").SetClass("block font-bold"),
Translations.W((license.license ?? "") === "" ? "CC0" : (license.license ?? "")) Translations.W((license?.license ?? "") === "" ? "CC0" : (license?.license ?? ""))
]).SetClass("flex flex-col") ]).SetClass("flex flex-col")
]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg") ]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg")

View file

@ -48,7 +48,7 @@ export default class DeleteImage extends Toggle {
tags.map(tags => (tags[key] ?? "") !== "") tags.map(tags => (tags[key] ?? "") !== "")
), ),
undefined /*Login (and thus editing) is disabled*/, undefined /*Login (and thus editing) is disabled*/,
State.state?.featureSwitchUserbadge ?? new UIEventSource<boolean>(true) State.state.osmConnection.isLoggedIn
) )
this.SetClass("cursor-pointer") this.SetClass("cursor-pointer")
} }

View file

@ -9,7 +9,6 @@ import State from "../../State";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import {BBox, GeoOperations} from "../../Logic/GeoOperations"; import {BBox, GeoOperations} from "../../Logic/GeoOperations";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import * as L from "leaflet";
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../../Models/ThemeConfig/LayerConfig";

View file

@ -31,8 +31,10 @@ export default class EditableTagRendering extends Toggle {
const answerWithEditButton = new Combine([answer, const answerWithEditButton = new Combine([answer,
new Toggle(editButton, undefined, State.state.osmConnection.isLoggedIn)]) new Toggle(editButton,
.SetClass("flex justify-between w-full") undefined,
State.state.osmConnection.isLoggedIn)
]).SetClass("flex justify-between w-full")
const cancelbutton = const cancelbutton =

View file

@ -71,7 +71,7 @@ export default class SplitRoadWizard extends Toggle {
}) })
new ShowDataMultiLayer({ new ShowDataMultiLayer({
features: new StaticFeatureSource([roadElement]), features: new StaticFeatureSource([roadElement], false),
layers: State.state.filteredLayers, layers: State.state.filteredLayers,
leafletMap: miniMap.leafletMap, leafletMap: miniMap.leafletMap,
enablePopups: false, enablePopups: false,

View file

@ -0,0 +1,156 @@
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../../Logic/FeatureSource/FeatureSource";
import {BBox} from "../../Logic/GeoOperations";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {UIEventSource} from "../../Logic/UIEventSource";
import {Tiles} from "../../Models/TileRange";
/**
* A feature source containing meta features.
* It will contain exactly one point for every tile of the specified (dynamic) zoom level
*/
export default class PerTileCountAggregator implements FeatureSource {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
public readonly name: string = "PerTileCountAggregator"
private readonly perTile: Map<number, SingleTileCounter> = new Map<number, SingleTileCounter>()
private readonly _requestedZoomLevel: UIEventSource<number>;
constructor(requestedZoomLevel: UIEventSource<number>) {
this._requestedZoomLevel = requestedZoomLevel;
const self = this;
this._requestedZoomLevel.addCallbackAndRun(_ => self.update())
}
private update() {
const now = new Date()
const allCountsAsFeatures : {feature: any, freshness: Date}[] = []
const aggregate = this.calculatePerTileCount()
aggregate.forEach((totalsPerLayer, tileIndex) => {
const totals = {}
let totalCount = 0
totalsPerLayer.forEach((total, layerId) => {
totals[layerId] = total
totalCount += total
})
totals["tileId"] = tileIndex
totals["count"] = totalCount
const feature = {
"type": "Feature",
"properties": totals,
"geometry": {
"type": "Point",
"coordinates": Tiles.centerPointOf(...Tiles.tile_from_index(tileIndex))
}
}
allCountsAsFeatures.push({feature: feature, freshness: now})
const bbox= BBox.fromTileIndex(tileIndex)
const box = {
"type": "Feature",
"properties":totals,
"geometry": {
"type": "Polygon",
"coordinates": [
[
[bbox.minLon, bbox.minLat],
[bbox.minLon, bbox.maxLat],
[bbox.maxLon, bbox.maxLat],
[bbox.maxLon, bbox.minLat],
[bbox.minLon, bbox.minLat]
]
]
}
}
allCountsAsFeatures.push({feature:box, freshness: now})
})
this.features.setData(allCountsAsFeatures)
}
/**
* Calculates an aggregate count per tile and per subtile
* @private
*/
private calculatePerTileCount() {
const perTileCount = new Map<number, Map<string, number>>()
const targetZoom = this._requestedZoomLevel.data;
// We only search for tiles of the same zoomlevel or a higher zoomlevel, which is embedded
for (const singleTileCounter of Array.from(this.perTile.values())) {
let tileZ = singleTileCounter.z
let tileX = singleTileCounter.x
let tileY = singleTileCounter.y
if (tileZ < targetZoom) {
continue;
}
while (tileZ > targetZoom) {
tileX = Math.floor(tileX / 2)
tileY = Math.floor(tileY / 2)
tileZ--
}
const tileI = Tiles.tile_index(tileZ, tileX, tileY)
let counts = perTileCount.get(tileI)
if (counts === undefined) {
counts = new Map<string, number>()
perTileCount.set(tileI, counts)
}
singleTileCounter.countsPerLayer.data.forEach((count, layerId) => {
if (counts.has(layerId)) {
counts.set(layerId, count + counts.get(layerId))
} else {
counts.set(layerId, count)
}
})
}
return perTileCount;
}
public addTile(tile: FeatureSourceForLayer & Tiled, shouldBeCounted: UIEventSource<boolean>) {
let counter = this.perTile.get(tile.tileIndex)
if (counter === undefined) {
counter = new SingleTileCounter(tile.tileIndex)
this.perTile.set(tile.tileIndex, counter)
// We do **NOT** add a callback on the perTile index, even though we could! It'll update just fine without it
}
counter.addTileCount(tile, shouldBeCounted)
}
}
/**
* Keeps track of a single tile
*/
class SingleTileCounter implements Tiled {
public readonly bbox: BBox;
public readonly tileIndex: number;
public readonly countsPerLayer: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>())
private readonly registeredLayers: Map<string, LayerConfig> = new Map<string, LayerConfig>();
public readonly z: number
public readonly x: number
public readonly y: number
constructor(tileIndex: number) {
this.tileIndex = tileIndex
this.bbox = BBox.fromTileIndex(tileIndex)
const [z, x, y] = Tiles.tile_from_index(tileIndex)
this.z = z;
this.x = x;
this.y = y
}
public addTileCount(source: FeatureSourceForLayer, shouldBeCounted: UIEventSource<boolean>) {
const layer = source.layer.layerDef
this.registeredLayers.set(layer.id, layer)
const self = this
source.features.map(f => {
/*if (!shouldBeCounted.data) {
return;
}*/
self.countsPerLayer.data.set(layer.id, f.length)
self.countsPerLayer.ping()
}, [shouldBeCounted])
}
}

View file

@ -41,13 +41,14 @@ export default class ShowDataLayer {
options.leafletMap.addCallback(_ => self.update(options)); options.leafletMap.addCallback(_ => self.update(options));
this.update(options); this.update(options);
State.state.selectedElement.addCallbackAndRunD(selected => { State.state.selectedElement.addCallbackAndRunD(selected => {
if (self._leafletMap.data === undefined) { if (self._leafletMap.data === undefined) {
return; return;
} }
const v = self.leafletLayersPerId.get(selected.properties.id) const v = self.leafletLayersPerId.get(selected.properties.id)
if(v === undefined){return;} if (v === undefined) {
return;
}
const leafletLayer = v.leafletlayer const leafletLayer = v.leafletlayer
const feature = v.feature const feature = v.feature
if (leafletLayer.getPopup().isOpen()) { if (leafletLayer.getPopup().isOpen()) {
@ -66,6 +67,21 @@ export default class ShowDataLayer {
} }
}) })
options.doShowLayer?.addCallbackAndRun(doShow => {
const mp = options.leafletMap.data;
if (this.geoLayer == undefined || mp == undefined) {
return;
}
if (doShow) {
mp.addLayer(this.geoLayer)
} else {
mp.removeLayer(this.geoLayer)
}
})
} }
private update(options) { private update(options) {
@ -83,21 +99,19 @@ export default class ShowDataLayer {
mp.removeLayer(this.geoLayer); mp.removeLayer(this.geoLayer);
} }
this.geoLayer= this.CreateGeojsonLayer() this.geoLayer = this.CreateGeojsonLayer()
const allFeats = this._features.data; const allFeats = this._features.data;
for (const feat of allFeats) { for (const feat of allFeats) {
if (feat === undefined) { if (feat === undefined) {
continue continue
} }
try{ try {
this.geoLayer.addData(feat); this.geoLayer.addData(feat);
}catch(e){ } catch (e) {
console.error("Could not add ", feat, "to the geojson layer in leaflet") console.error("Could not add ", feat, "to the geojson layer in leaflet")
} }
} }
mp.addLayer(this.geoLayer)
if (options.zoomToFeatures ?? false) { if (options.zoomToFeatures ?? false) {
try { try {
mp.fitBounds(this.geoLayer.getBounds(), {animate: false}) mp.fitBounds(this.geoLayer.getBounds(), {animate: false})
@ -105,6 +119,10 @@ export default class ShowDataLayer {
console.error(e) console.error(e)
} }
} }
if (options.doShowLayer?.data ?? true) {
mp.addLayer(this.geoLayer)
}
} }
@ -125,7 +143,8 @@ export default class ShowDataLayer {
return; return;
} }
const tagSource = feature.properties.id === undefined ? new UIEventSource<any>(feature.properties) : State.state.allElements.getEventSourceById(feature.properties.id) const tagSource = feature.properties.id === undefined ? new UIEventSource<any>(feature.properties) :
State.state.allElements.getEventSourceById(feature.properties.id)
const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0) const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0)
const style = layer.GenerateLeafletStyle(tagSource, clickable); const style = layer.GenerateLeafletStyle(tagSource, clickable);
const baseElement = style.icon.html; const baseElement = style.icon.html;
@ -193,8 +212,10 @@ export default class ShowDataLayer {
infobox.Activate(); infobox.Activate();
}); });
// Add the feature to the index to open the popup when needed // Add the feature to the index to open the popup when needed
this.leafletLayersPerId.set(feature.properties.id, {feature: feature, leafletlayer: leafletLayer}) this.leafletLayersPerId.set(feature.properties.id, {feature: feature, leafletlayer: leafletLayer})
} }
private CreateGeojsonLayer(): L.Layer { private CreateGeojsonLayer(): L.Layer {

View file

@ -6,4 +6,5 @@ export interface ShowDataLayerOptions {
leafletMap: UIEventSource<L.Map>, leafletMap: UIEventSource<L.Map>,
enablePopups?: true | boolean, enablePopups?: true | boolean,
zoomToFeatures?: false | boolean, zoomToFeatures?: false | boolean,
doShowLayer?: UIEventSource<boolean>
} }

View file

@ -0,0 +1,79 @@
import FeatureSource, {Tiled} from "../../Logic/FeatureSource/FeatureSource";
import {UIEventSource} from "../../Logic/UIEventSource";
import {Utils} from "../../Utils";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import ShowDataLayer from "./ShowDataLayer";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import {GeoOperations} from "../../Logic/GeoOperations";
import {Tiles} from "../../Models/TileRange";
export default class ShowTileInfo {
public static readonly styling = new LayerConfig({
id: "tileinfo_styling",
title: {
render: "Tile {z}/{x}/{y}"
},
tagRenderings: [
"all_tags"
],
source: {
osmTags: "tileId~*"
},
color: {"render": "#3c3"},
width: {
"render": "1"
},
label: {
render: "<div class='rounded-full text-xl font-bold' style='width: 2rem; height: 2rem; background: white'>{count}</div>"
}
}, "tileinfo", true)
constructor(options: {
source: FeatureSource & Tiled, leafletMap: UIEventSource<any>, layer?: LayerConfig,
doShowLayer?: UIEventSource<boolean>
}) {
const source = options.source
const metaFeature: UIEventSource<any[]> =
source.features.map(features => {
const bbox = source.bbox
const [z, x, y] = Tiles.tile_from_index(source.tileIndex)
const box = {
"type": "Feature",
"properties": {
"z": z,
"x": x,
"y": y,
"tileIndex": source.tileIndex,
"source": source.name,
"count": features.length,
tileId: source.name + "/" + source.tileIndex
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[bbox.minLon, bbox.minLat],
[bbox.minLon, bbox.maxLat],
[bbox.maxLon, bbox.maxLat],
[bbox.maxLon, bbox.minLat],
[bbox.minLon, bbox.minLat]
]
]
}
}
const center = GeoOperations.centerpoint(box)
return [box, center]
})
new ShowDataLayer({
layerToShow: ShowTileInfo.styling,
features: new StaticFeatureSource(metaFeature, false),
leafletMap: options.leafletMap,
doShowLayer: options.doShowLayer
})
}
}

105
Utils.ts
View file

@ -10,7 +10,7 @@ export class Utils {
*/ */
public static runningFromConsole = typeof window === "undefined"; public static runningFromConsole = typeof window === "undefined";
public static readonly assets_path = "./assets/svg/"; public static readonly assets_path = "./assets/svg/";
public static externalDownloadFunction: (url: string) => Promise<any>; public static externalDownloadFunction: (url: string, headers?: any) => Promise<any>;
private static knownKeys = ["addExtraTags", "and", "calculatedTags", "changesetmessage", "clustering", "color", "condition", "customCss", "dashArray", "defaultBackgroundId", "description", "descriptionTail", "doNotDownload", "enableAddNewPoints", "enableBackgroundLayerSelection", "enableGeolocation", "enableLayers", "enableMoreQuests", "enableSearch", "enableShareScreen", "enableUserBadge", "freeform", "hideFromOverview", "hideInAnswer", "icon", "iconOverlays", "iconSize", "id", "if", "ifnot", "isShown", "key", "language", "layers", "lockLocation", "maintainer", "mappings", "maxzoom", "maxZoom", "minNeededElements", "minzoom", "multiAnswer", "name", "or", "osmTags", "passAllFeatures", "presets", "question", "render", "roaming", "roamingRenderings", "rotation", "shortDescription", "socialImage", "source", "startLat", "startLon", "startZoom", "tagRenderings", "tags", "then", "title", "titleIcons", "type", "version", "wayHandling", "widenFactor", "width"] private static knownKeys = ["addExtraTags", "and", "calculatedTags", "changesetmessage", "clustering", "color", "condition", "customCss", "dashArray", "defaultBackgroundId", "description", "descriptionTail", "doNotDownload", "enableAddNewPoints", "enableBackgroundLayerSelection", "enableGeolocation", "enableLayers", "enableMoreQuests", "enableSearch", "enableShareScreen", "enableUserBadge", "freeform", "hideFromOverview", "hideInAnswer", "icon", "iconOverlays", "iconSize", "id", "if", "ifnot", "isShown", "key", "language", "layers", "lockLocation", "maintainer", "mappings", "maxzoom", "maxZoom", "minNeededElements", "minzoom", "multiAnswer", "name", "or", "osmTags", "passAllFeatures", "presets", "question", "render", "roaming", "roamingRenderings", "rotation", "shortDescription", "socialImage", "source", "startLat", "startLon", "startZoom", "tagRenderings", "tags", "then", "title", "titleIcons", "type", "version", "wayHandling", "widenFactor", "width"]
private static extraKeys = ["nl", "en", "fr", "de", "pt", "es", "name", "phone", "email", "amenity", "leisure", "highway", "building", "yes", "no", "true", "false"] private static extraKeys = ["nl", "en", "fr", "de", "pt", "es", "name", "phone", "email", "amenity", "leisure", "highway", "building", "yes", "no", "true", "false"]
@ -247,64 +247,6 @@ export class Utils {
return dict.get(k); return dict.get(k);
} }
/**
* Calculates the tile bounds of the
* @param z
* @param x
* @param y
* @returns [[maxlat, minlon], [minlat, maxlon]]
*/
static tile_bounds(z: number, x: number, y: number): [[number, number], [number, number]] {
return [[Utils.tile2lat(y, z), Utils.tile2long(x, z)], [Utils.tile2lat(y + 1, z), Utils.tile2long(x + 1, z)]]
}
static tile_bounds_lon_lat(z: number, x: number, y: number): [[number, number], [number, number]] {
return [[Utils.tile2long(x, z), Utils.tile2lat(y, z)], [Utils.tile2long(x + 1, z), Utils.tile2lat(y + 1, z)]]
}
static tile_index(z: number, x: number, y: number): number {
return ((x * (2 << z)) + y) * 100 + z
}
/**
* Given a tile index number, returns [z, x, y]
* @param index
* @returns 'zxy'
*/
static tile_from_index(index: number): [number, number, number] {
const z = index % 100;
const factor = 2 << z
index = Math.floor(index / 100)
return [z, Math.floor(index / factor), index % factor]
}
/**
* Return x, y of the tile containing (lat, lon) on the given zoom level
*/
static embedded_tile(lat: number, lon: number, z: number): { x: number, y: number, z: number } {
return {x: Utils.lon2tile(lon, z), y: Utils.lat2tile(lat, z), z: z}
}
static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1: number, lon1: number): TileRange {
const t0 = Utils.embedded_tile(lat0, lon0, zoomlevel)
const t1 = Utils.embedded_tile(lat1, lon1, zoomlevel)
const xstart = Math.min(t0.x, t1.x)
const xend = Math.max(t0.x, t1.x)
const ystart = Math.min(t0.y, t1.y)
const yend = Math.max(t0.y, t1.y)
const total = (1 + xend - xstart) * (1 + yend - ystart)
return {
xstart: xstart,
xend: xend,
ystart: ystart,
yend: yend,
total: total,
zoomlevel: zoomlevel
}
}
public static MinifyJSON(stringified: string): string { public static MinifyJSON(stringified: string): string {
stringified = stringified.replace(/\|/g, "||"); stringified = stringified.replace(/\|/g, "||");
@ -345,16 +287,7 @@ export class Utils {
return result; return result;
} }
public static MapRange<T>(tileRange: TileRange, f: (x: number, y: number) => T): T[] {
const result: T[] = []
for (let x = tileRange.xstart; x <= tileRange.xend; x++) {
for (let y = tileRange.ystart; y <= tileRange.yend; y++) {
const t = f(x, y);
result.push(t)
}
}
return result;
}
private static injectedDownloads = {} private static injectedDownloads = {}
@ -362,7 +295,7 @@ export class Utils {
Utils.injectedDownloads[url] = data Utils.injectedDownloads[url] = data
} }
public static downloadJson(url: string): Promise<any> { public static downloadJson(url: string, headers?: any): Promise<any> {
const injected = Utils.injectedDownloads[url] const injected = Utils.injectedDownloads[url]
if (injected !== undefined) { if (injected !== undefined) {
@ -371,7 +304,7 @@ export class Utils {
} }
if (this.externalDownloadFunction !== undefined) { if (this.externalDownloadFunction !== undefined) {
return this.externalDownloadFunction(url) return this.externalDownloadFunction(url, headers)
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -379,7 +312,6 @@ export class Utils {
xhr.onload = () => { xhr.onload = () => {
if (xhr.status == 200) { if (xhr.status == 200) {
try { try {
console.log("Got a response! Parsing now...")
resolve(JSON.parse(xhr.response)) resolve(JSON.parse(xhr.response))
} catch (e) { } catch (e) {
reject("Not a valid json: " + xhr.response) reject("Not a valid json: " + xhr.response)
@ -390,6 +322,13 @@ export class Utils {
}; };
xhr.open('GET', url); xhr.open('GET', url);
xhr.setRequestHeader("accept", "application/json") xhr.setRequestHeader("accept", "application/json")
if (headers !== undefined) {
for (const key in headers) {
xhr.setRequestHeader(key, headers[key])
}
}
xhr.send(); xhr.send();
} }
) )
@ -449,22 +388,6 @@ export class Utils {
return bestColor ?? hex; return bestColor ?? hex;
} }
private static tile2long(x, z) {
return (x / Math.pow(2, z) * 360 - 180);
}
private static tile2lat(y, z) {
const n = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
return (180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))));
}
private static lon2tile(lon, zoom) {
return (Math.floor((lon + 180) / 360 * Math.pow(2, zoom)));
}
private static lat2tile(lat, zoom) {
return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom)));
}
private static colorDiff(c0: { r: number, g: number, b: number }, c1: { r: number, g: number, b: number }) { private static colorDiff(c0: { r: number, g: number, b: number }, c1: { r: number, g: number, b: number }) {
return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b); return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b);
@ -506,5 +429,11 @@ export class Utils {
} }
return copy return copy
} }
public static async waitFor(timeMillis: number): Promise<void> {
return new Promise((resolve) => {
window.setTimeout(resolve, timeMillis);
})
}
} }

View file

@ -1592,8 +1592,13 @@
{ {
"#": "plugs-9", "#": "plugs-9",
"question": { "question": {
"en": "How much plugs of type <b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> are available here?", "en": "What kind of authentication is available at the charging station?",
"nl": "Hoeveel stekkers van type <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft dit oplaadpunt?" "nl": "Hoeveel stekkers van type <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft dit oplaadpunt?",
"it": "Quali sono gli orari di apertura di questa stazione di ricarica?",
"ja": "この充電ステーションはいつオープンしますか?",
"nb_NO": "Når åpnet denne ladestasjonen?",
"ru": "В какое время работает эта зарядная станция?",
"zh_Hant": "何時是充電站開放使用的時間?"
}, },
"render": { "render": {
"en": "There are <b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> plugs of type <b>Type 2 with cable</b> (mennekes) available here", "en": "There are <b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> plugs of type <b>Type 2 with cable</b> (mennekes) available here",
@ -1608,17 +1613,52 @@
"socket:type2_cable~*", "socket:type2_cable~*",
"socket:type2_cable!=0" "socket:type2_cable!=0"
] ]
},
"en": {
"0": {
"then": "Authentication by a membership card"
},
"1": {
"then": "Authentication by an app"
},
"2": {
"then": "Authentication via phone call is available"
},
"3": {
"then": "Authentication via phone call is available"
},
"4": {
"then": "Authentication via NFC is available"
},
"5": {
"then": "Authentication via Money Card is available"
},
"6": {
"then": "Authentication via debit card is available"
},
"7": {
"then": "No authentication is needed"
}
} }
}, },
{ {
"#": "voltage-9", "#": "voltage-9",
"question": { "question": {
"en": "What voltage do the plugs with <b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> offer?", "en": "What's the phone number for authentication call or SMS?",
"nl": "Welke spanning levert de stekker van type <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/>" "nl": "Welke spanning levert de stekker van type <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/>",
"it": "A quale rete appartiene questa stazione di ricarica?",
"ja": "この充電ステーションの運営チェーンはどこですか?",
"ru": "К какой сети относится эта станция?",
"zh_Hant": "充電站所屬的網路是?"
}, },
"render": { "render": {
"en": "<b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs {socket:type2_cable:voltage} volt", "en": "Authenticate by calling or SMS'ing to <a href='tel:{authentication:phone_call:number}'>{authentication:phone_call:number}</a>",
"nl": "<b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft een spanning van {socket:type2_cable:voltage} volt" "nl": "<b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft een spanning van {socket:type2_cable:voltage} volt",
"it": "{network}",
"ja": "{network}",
"nb_NO": "{network}",
"ru": "{network}",
"zh_Hant": "{network}"
}, },
"freeform": { "freeform": {
"key": "socket:type2_cable:voltage", "key": "socket:type2_cable:voltage",
@ -1650,7 +1690,7 @@
{ {
"#": "current-9", "#": "current-9",
"question": { "question": {
"en": "What current do the plugs with <b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> offer?", "en": "When is this charging station opened?",
"nl": "Welke stroom levert de stekker van type <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/>?" "nl": "Welke stroom levert de stekker van type <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/>?"
}, },
"render": { "render": {
@ -1665,7 +1705,7 @@
{ {
"if": "socket:socket:type2_cable:current=16 A", "if": "socket:socket:type2_cable:current=16 A",
"then": { "then": {
"en": "<b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs at most 16 A", "en": "24/7 opened (including holidays)",
"nl": "<b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> levert een stroom van maximaal 16 A" "nl": "<b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> levert een stroom van maximaal 16 A"
} }
}, },
@ -1687,12 +1727,12 @@
{ {
"#": "power-output-9", "#": "power-output-9",
"question": { "question": {
"en": "What power output does a single plug of type <b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> offer?", "en": "How much does one have to pay to use this charging station?",
"nl": "Welk vermogen levert een enkele stekker van type <b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/>?" "nl": "Hoeveel kost het gebruik van dit oplaadpunt?"
}, },
"render": { "render": {
"en": "<b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs at most {socket:type2_cable:output}", "en": "Using this charging station costs <b>{charge}</b>",
"nl": "<b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> levert een vermogen van maximaal {socket:type2_cable:output}" "nl": "Dit oplaadpunt gebruiken kost <b>{charge}</b>"
}, },
"freeform": { "freeform": {
"key": "socket:type2_cable:output", "key": "socket:type2_cable:output",
@ -1702,8 +1742,8 @@
{ {
"if": "socket:socket:type2_cable:output=11 kw", "if": "socket:socket:type2_cable:output=11 kw",
"then": { "then": {
"en": "<b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs at most 11 kw", "en": "Free to use",
"nl": "<b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> levert een vermogen van maximaal 11 kw" "nl": "Gratis te gebruiken"
} }
}, },
{ {
@ -1740,17 +1780,31 @@
"socket:tesla_supercharger_ccs~*", "socket:tesla_supercharger_ccs~*",
"socket:tesla_supercharger_ccs!=0" "socket:tesla_supercharger_ccs!=0"
] ]
},
"en": {
"mappings+": {
"0": {
"then": "Payment is done using a dedicated app"
}
}
},
"nl": {
"mappings+": {
"0": {
"then": "Betalen via een app van het netwerk"
}
}
} }
}, },
{ {
"#": "voltage-10", "#": "voltage-10",
"question": { "question": {
"en": "What voltage do the plugs with <b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> offer?", "en": "What is the maximum amount of time one is allowed to stay here?",
"nl": "Welke spanning levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/>" "nl": "Hoelang mag een voertuig hier blijven staan?"
}, },
"render": { "render": {
"en": "<b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs {socket:tesla_supercharger_ccs:voltage} volt", "en": "One can stay at most <b>{canonical(maxstay)}</b>",
"nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> heeft een spanning van {socket:tesla_supercharger_ccs:voltage} volt" "nl": "De maximale parkeertijd hier is <b>{canonical(maxstay)}</b>"
}, },
"freeform": { "freeform": {
"key": "socket:tesla_supercharger_ccs:voltage", "key": "socket:tesla_supercharger_ccs:voltage",
@ -1760,8 +1814,8 @@
{ {
"if": "socket:socket:tesla_supercharger_ccs:voltage=500 V", "if": "socket:socket:tesla_supercharger_ccs:voltage=500 V",
"then": { "then": {
"en": "<b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs 500 volt", "en": "No timelimit on leaving your vehicle here",
"nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> heeft een spanning van 500 volt" "nl": "Geen maximum parkeertijd"
} }
}, },
{ {
@ -1782,11 +1836,11 @@
{ {
"#": "current-10", "#": "current-10",
"question": { "question": {
"en": "What current do the plugs with <b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> offer?", "en": "Is this charging station part of a network?",
"nl": "Welke stroom levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/>?" "nl": "Welke stroom levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/>?"
}, },
"render": { "render": {
"en": "<b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs at most {socket:tesla_supercharger_ccs:current}A", "en": "Part of the network <b>{network}</b>",
"nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> levert een stroom van maximaal {socket:tesla_supercharger_ccs:current}A" "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> levert een stroom van maximaal {socket:tesla_supercharger_ccs:current}A"
}, },
"freeform": { "freeform": {
@ -1797,14 +1851,14 @@
{ {
"if": "socket:socket:tesla_supercharger_ccs:current=125 A", "if": "socket:socket:tesla_supercharger_ccs:current=125 A",
"then": { "then": {
"en": "<b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs at most 125 A", "en": "Not part of a bigger network",
"nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> levert een stroom van maximaal 125 A" "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> levert een stroom van maximaal 125 A"
} }
}, },
{ {
"if": "socket:socket:tesla_supercharger_ccs:current=350 A", "if": "socket:socket:tesla_supercharger_ccs:current=350 A",
"then": { "then": {
"en": "<b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs at most 350 A", "en": "Not part of a bigger network",
"nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> levert een stroom van maximaal 350 A" "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> levert een stroom van maximaal 350 A"
} }
} }
@ -1849,11 +1903,11 @@
{ {
"#": "plugs-11", "#": "plugs-11",
"question": { "question": {
"en": "How much plugs of type <b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> are available here?", "en": "What number can one call if there is a problem with this charging station?",
"nl": "Hoeveel stekkers van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> heeft dit oplaadpunt?" "nl": "Hoeveel stekkers van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> heeft dit oplaadpunt?"
}, },
"render": { "render": {
"en": "There are <b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> plugs of type <b>Tesla Supercharger (destination)</b> available here", "en": "In case of problems, call <a href='tel:{phone}'>{phone}</a>",
"nl": "Hier zijn <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> stekkers van het type " "nl": "Hier zijn <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> stekkers van het type "
}, },
"freeform": { "freeform": {
@ -1870,11 +1924,11 @@
{ {
"#": "voltage-11", "#": "voltage-11",
"question": { "question": {
"en": "What voltage do the plugs with <b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> offer?", "en": "What is the email address of the operator?",
"nl": "Welke spanning levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/>" "nl": "Welke spanning levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/>"
}, },
"render": { "render": {
"en": "<b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> outputs {socket:tesla_destination:voltage} volt", "en": "In case of problems, send an email to <a href='mailto:{email}'>{email}</a>",
"nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> heeft een spanning van {socket:tesla_destination:voltage} volt" "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> heeft een spanning van {socket:tesla_destination:voltage} volt"
}, },
"freeform": { "freeform": {
@ -1900,11 +1954,11 @@
{ {
"#": "current-11", "#": "current-11",
"question": { "question": {
"en": "What current do the plugs with <b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> offer?", "en": "What is the website of the operator?",
"nl": "Welke stroom levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/>?" "nl": "Welke stroom levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/>?"
}, },
"render": { "render": {
"en": "<b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> outputs at most {socket:tesla_destination:current}A", "en": "More info on <a href='{website}'>{website}</a>",
"nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> levert een stroom van maximaal {socket:tesla_destination:current}A" "nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> levert een stroom van maximaal {socket:tesla_destination:current}A"
}, },
"freeform": { "freeform": {
@ -1981,7 +2035,7 @@
{ {
"#": "plugs-12", "#": "plugs-12",
"question": { "question": {
"en": "How much plugs of type <b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> are available here?", "en": "What is the reference number of this charging station?",
"nl": "Hoeveel stekkers van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft dit oplaadpunt?" "nl": "Hoeveel stekkers van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft dit oplaadpunt?"
}, },
"render": { "render": {
@ -2002,8 +2056,8 @@
{ {
"#": "voltage-12", "#": "voltage-12",
"question": { "question": {
"en": "What voltage do the plugs with <b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> offer?", "en": "Is this charging point in use?",
"nl": "Welke spanning levert de stekker van type <b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/>" "nl": "Is dit oplaadpunt operationeel?"
}, },
"render": { "render": {
"en": "<b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs {socket:tesla_destination:voltage} volt", "en": "<b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs {socket:tesla_destination:voltage} volt",
@ -2017,15 +2071,15 @@
{ {
"if": "socket:socket:tesla_destination:voltage=230 V", "if": "socket:socket:tesla_destination:voltage=230 V",
"then": { "then": {
"en": "<b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs 230 volt", "en": "This charging station is broken",
"nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft een spanning van 230 volt" "nl": "Dit oplaadpunt is kapot"
} }
}, },
{ {
"if": "socket:socket:tesla_destination:voltage=400 V", "if": "socket:socket:tesla_destination:voltage=400 V",
"then": { "then": {
"en": "<b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> outputs 400 volt", "en": "A charging station is planned here",
"nl": "<b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_tethered.svg'/> heeft een spanning van 400 volt" "nl": "Hier zal binnenkort een oplaadpunt gebouwd worden"
} }
} }
], ],
@ -2296,6 +2350,14 @@
"en": "Payment is done using a dedicated app", "en": "Payment is done using a dedicated app",
"nl": "Betalen via een app van het netwerk" "nl": "Betalen via een app van het netwerk"
} }
},
{
"if": "payment:membership_card=yes",
"ifnot": "payment:membership_card=no",
"then": {
"en": "Payment is done using a membership card",
"nl": "Betalen via een lidkaart van het netwerk"
}
} }
], ],
"mappings": [ "mappings": [

View file

@ -48,8 +48,9 @@
} }
}, },
"calculatedTags": [ "calculatedTags": [
"_closest_other_drinking_water_id=feat.closest('drinking_water')?.id", "_closest_other_drinking_water=feat.closestn('drinking_water', 1, 500).map(f => ({id: f.feat.id, distance: f.distance}))[0]",
"_closest_other_drinking_water_distance=Math.floor(feat.distanceTo(feat.closest('drinking_water')).distance * 1000)" "_closest_other_drinking_water_id=JSON.parse(feat.properties._closest_other_drinking_water)?.id",
"_closest_other_drinking_water_distance=Math.floor(JSON.parse(feat.properties._closest_other_drinking_water)?.distance * 1000)"
], ],
"minzoom": 13, "minzoom": 13,
"wayHandling": 1, "wayHandling": 1,

View file

@ -12,6 +12,7 @@
] ]
} }
}, },
"minzoom": 12,
"wayHandling": 1, "wayHandling": 1,
"icon": { "icon": {
"render": "circle:white;./assets/layers/food/restaurant.svg", "render": "circle:white;./assets/layers/food/restaurant.svg",

View file

@ -19,7 +19,7 @@
"source": { "source": {
"osmTags": "amenity=public_bookcase" "osmTags": "amenity=public_bookcase"
}, },
"minzoom": 12, "minzoom": 10,
"wayHandling": 2, "wayHandling": 2,
"title": { "title": {
"render": { "render": {

View file

@ -52,7 +52,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 1.5,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
"bench", "bench",

View file

@ -40,7 +40,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 1.5,
"roamingRenderings": [], "roamingRenderings": [],
"layers": [ "layers": [
"bicycle_library" "bicycle_library"

View file

@ -47,7 +47,7 @@
"startLat": 50.8435, "startLat": 50.8435,
"startLon": 4.3688, "startLon": 4.3688,
"startZoom": 14, "startZoom": 14,
"widenFactor": 0.05, "widenFactor": 1.5,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
"bike_monitoring_station" "bike_monitoring_station"

View file

@ -22,7 +22,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 1.5,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
"binocular" "binocular"

View file

@ -25,7 +25,7 @@
"startLat": 50.8435, "startLat": 50.8435,
"startLon": 4.3688, "startLon": 4.3688,
"startZoom": 16, "startZoom": 16,
"widenFactor": 0.01, "widenFactor": 1.2,
"socialImage": "./assets/themes/buurtnatuur/social_image.jpg", "socialImage": "./assets/themes/buurtnatuur/social_image.jpg",
"layers": [ "layers": [
{ {

View file

@ -18,7 +18,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 1.5,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
"cafe_pub" "cafe_pub"

View file

@ -47,7 +47,7 @@
"startLat": 43.14, "startLat": 43.14,
"startLon": 3.14, "startLon": 3.14,
"startZoom": 14, "startZoom": 14,
"widenFactor": 0.05, "widenFactor": 1.5,
"socialImage": "./assets/themes/campersite/Bar%C3%9Fel_Wohnmobilstellplatz.jpg", "socialImage": "./assets/themes/campersite/Bar%C3%9Fel_Wohnmobilstellplatz.jpg",
"layers": [ "layers": [
{ {

View file

@ -39,7 +39,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 1.5,
"socialImage": "", "socialImage": "",
"defaultBackgroundId": "CartoDB.Voyager", "defaultBackgroundId": "CartoDB.Voyager",
"layers": [ "layers": [

View file

@ -48,7 +48,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 1.5,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
{ {

View file

@ -21,7 +21,7 @@
"clustering": { "clustering": {
"maxZoom": 1 "maxZoom": 1
}, },
"widenFactor": 0.005, "widenFactor": 1.1,
"enableDownload": true, "enableDownload": true,
"enablePdfDownload": true, "enablePdfDownload": true,
"layers": [ "layers": [

View file

@ -24,7 +24,7 @@
"startLat": 51, "startLat": 51,
"startLon": 3.75, "startLon": 3.75,
"startZoom": 11, "startZoom": 11,
"widenFactor": 1, "widenFactor": 1.5,
"socialImage": "./assets/themes/cycle_infra/cycle-infra.svg", "socialImage": "./assets/themes/cycle_infra/cycle-infra.svg",
"enableDownload": true, "enableDownload": true,
"layers": [ "layers": [

View file

@ -40,7 +40,7 @@
"defaultBackgroundId": "CartoDB.Voyager", "defaultBackgroundId": "CartoDB.Voyager",
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 2,
"socialImage": "assets/themes/cyclofix/logo.svg", "socialImage": "assets/themes/cyclofix/logo.svg",
"layers": [ "layers": [
"bike_cafe", "bike_cafe",

View file

@ -38,7 +38,7 @@
"startLat": 51.02768, "startLat": 51.02768,
"startLon": 4.480705, "startLon": 4.480705,
"startZoom": 15, "startZoom": 15,
"widenFactor": 0.05, "widenFactor": 1.5,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
{ {

View file

@ -18,7 +18,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 3,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
"food" "food"

View file

@ -24,7 +24,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 3,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
{ {

View file

@ -18,7 +18,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.001, "widenFactor": 2,
"socialImage": "", "socialImage": "",
"hideFromOverview": true, "hideFromOverview": true,
"layers": [ "layers": [

View file

@ -52,7 +52,7 @@
"startZoom": 1, "startZoom": 1,
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"widenFactor": 0.1, "widenFactor": 5,
"layers": [ "layers": [
"ghost_bike" "ghost_bike"
], ],

View file

@ -18,7 +18,7 @@
"startLat": 51.2132, "startLat": 51.2132,
"startLon": 3.231, "startLon": 3.231,
"startZoom": 14, "startZoom": 14,
"widenFactor": 0.05, "widenFactor": 2,
"cacheTimeout": 3600, "cacheTimeout": 3600,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [

View file

@ -18,7 +18,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 5,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
{ {

View file

@ -36,7 +36,7 @@
"startLat": 13.67801, "startLat": 13.67801,
"startLon": 121.6625, "startLon": 121.6625,
"startZoom": 6, "startZoom": 6,
"widenFactor": 0.05, "widenFactor": 3,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
{ {

View file

@ -36,7 +36,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 5,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
"map" "map"

View file

@ -18,7 +18,7 @@
"startLat": 51.20875, "startLat": 51.20875,
"startLon": 3.22435, "startLon": 3.22435,
"startZoom": 12, "startZoom": 12,
"widenFactor": 0.05, "widenFactor": 2,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
"drinking_water", "drinking_water",

View file

@ -23,7 +23,7 @@
"startLat": 51.20875, "startLat": 51.20875,
"startLon": 3.22435, "startLon": 3.22435,
"startZoom": 15, "startZoom": 15,
"widenFactor": 0.05, "widenFactor": 2,
"socialImage": "", "socialImage": "",
"defaultBackgroundId": "CartoDB.Positron", "defaultBackgroundId": "CartoDB.Positron",
"enablePdfDownload": true, "enablePdfDownload": true,

View file

@ -22,7 +22,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 5,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
"observation_tower" "observation_tower"

View file

@ -22,7 +22,7 @@
"startLat": 51.20875, "startLat": 51.20875,
"startLon": 3.22435, "startLon": 3.22435,
"startZoom": 12, "startZoom": 12,
"widenFactor": 0.05, "widenFactor": 1.2,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
"parking" "parking"

View file

@ -41,7 +41,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 16, "startZoom": 16,
"widenFactor": 0.05, "widenFactor": 3,
"layers": [], "layers": [],
"roamingRenderings": [] "roamingRenderings": []
} }

View file

@ -19,7 +19,7 @@
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"hideFromOverview": true, "hideFromOverview": true,
"widenFactor": 0.05, "widenFactor": 3,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
"play_forest" "play_forest"

View file

@ -38,7 +38,7 @@
"startLat": 50.535, "startLat": 50.535,
"startLon": 4.399, "startLon": 4.399,
"startZoom": 13, "startZoom": 13,
"widenFactor": 0.05, "widenFactor": 5,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
"playground" "playground"

View file

@ -34,7 +34,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 3,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
{ {

View file

@ -22,7 +22,7 @@
"startLat": 51.17174, "startLat": 51.17174,
"startLon": 4.449462, "startLon": 4.449462,
"startZoom": 12, "startZoom": 12,
"widenFactor": 0.05, "widenFactor": 1.2,
"socialImage": "./assets/themes/speelplekken/social_image.jpg", "socialImage": "./assets/themes/speelplekken/social_image.jpg",
"defaultBackgroundId": "CartoDB.Positron", "defaultBackgroundId": "CartoDB.Positron",
"layers": [ "layers": [

View file

@ -20,7 +20,7 @@
"startLat": 51.17174, "startLat": 51.17174,
"startLon": 4.449462, "startLon": 4.449462,
"startZoom": 12, "startZoom": 12,
"widenFactor": 0.05, "widenFactor": 2,
"socialImage": "", "socialImage": "",
"defaultBackgroundId": "CartoDB.Positron", "defaultBackgroundId": "CartoDB.Positron",
"layers": [ "layers": [

View file

@ -37,7 +37,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 2,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
"sport_pitch" "sport_pitch"

View file

@ -37,7 +37,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 2,
"socialImage": "", "socialImage": "",
"defaultBackgroundId": "osm", "defaultBackgroundId": "osm",
"layers": [ "layers": [

View file

@ -20,7 +20,7 @@
"startZoom": 8, "startZoom": 8,
"startLat": 50.8536, "startLat": 50.8536,
"startLon": 4.433, "startLon": 4.433,
"widenFactor": 0.2, "widenFactor": 2,
"layers": [ "layers": [
{ {
"builtin": [ "builtin": [

View file

@ -35,7 +35,7 @@
"startZoom": 12, "startZoom": 12,
"startLat": 51.2095, "startLat": 51.2095,
"startLon": 3.2222, "startLon": 3.2222,
"widenFactor": 0.05, "widenFactor": 3,
"icon": "./assets/themes/toilets/toilets.svg", "icon": "./assets/themes/toilets/toilets.svg",
"layers": [ "layers": [
"toilet" "toilet"

View file

@ -45,7 +45,7 @@
"startLat": 50.642, "startLat": 50.642,
"startLon": 4.482, "startLon": 4.482,
"startZoom": 8, "startZoom": 8,
"widenFactor": 0.01, "widenFactor": 1.5,
"socialImage": "./assets/themes/trees/logo.svg", "socialImage": "./assets/themes/trees/logo.svg",
"clustering": { "clustering": {
"maxZoom": 18 "maxZoom": 18

View file

@ -18,7 +18,7 @@
"startLat": -0.08528530407, "startLat": -0.08528530407,
"startLon": 51.52103754846, "startLon": 51.52103754846,
"startZoom": 18, "startZoom": 18,
"widenFactor": 0.5, "widenFactor": 1.5,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
{ {

View file

@ -22,7 +22,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 2,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
{ {

View file

@ -25,7 +25,7 @@
"startLat": 51.20875, "startLat": 51.20875,
"startLon": 3.22435, "startLon": 3.22435,
"startZoom": 14, "startZoom": 14,
"widenFactor": 0.05, "widenFactor": 2,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
{ {

View file

@ -48,13 +48,10 @@ export default class ScriptUtils {
}) })
} }
public static DownloadJSON(url, options?: { public static DownloadJSON(url, headers?: any): Promise<any> {
headers: any
}): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
headers = headers ?? {}
const headers = options?.headers ?? {}
headers.accept = "application/json" headers.accept = "application/json"
console.log("Fetching", url) console.log("Fetching", url)
const urlObj = new URL(url) const urlObj = new URL(url)

View file

@ -14,7 +14,7 @@ import RelationsTracker from "../Logic/Osm/RelationsTracker";
import * as OsmToGeoJson from "osmtogeojson"; import * as OsmToGeoJson from "osmtogeojson";
import MetaTagging from "../Logic/MetaTagging"; import MetaTagging from "../Logic/MetaTagging";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import {TileRange} from "../Models/TileRange"; import {TileRange, Tiles} from "../Models/TileRange";
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
import ScriptUtils from "./ScriptUtils"; import ScriptUtils from "./ScriptUtils";
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"; import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
@ -86,7 +86,7 @@ async function downloadRaw(targetdir: string, r: TileRange, overpass: Overpass)/
} }
console.log("x:", (x - r.xstart), "/", (r.xend - r.xstart), "; y:", (y - r.ystart), "/", (r.yend - r.ystart), "; total: ", downloaded, "/", r.total, "failed: ", failed, "skipped: ", skipped) console.log("x:", (x - r.xstart), "/", (r.xend - r.xstart), "; y:", (y - r.ystart), "/", (r.yend - r.ystart), "; total: ", downloaded, "/", r.total, "failed: ", failed, "skipped: ", skipped)
const boundsArr = Utils.tile_bounds(r.zoomlevel, x, y) const boundsArr = Tiles.tile_bounds(r.zoomlevel, x, y)
const bounds = { const bounds = {
north: Math.max(boundsArr[0][0], boundsArr[1][0]), north: Math.max(boundsArr[0][0], boundsArr[1][0]),
south: Math.min(boundsArr[0][0], boundsArr[1][0]), south: Math.min(boundsArr[0][0], boundsArr[1][0]),
@ -174,7 +174,7 @@ function loadAllTiles(targetdir: string, r: TileRange, theme: LayoutConfig, extr
allFeatures.push(...geojson.features) allFeatures.push(...geojson.features)
} }
} }
return new StaticFeatureSource(allFeatures) return new StaticFeatureSource(allFeatures, false)
} }
/** /**
@ -225,7 +225,7 @@ function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsT
delete feature.feature["bbox"] delete feature.feature["bbox"]
} }
// Lets save this tile! // Lets save this tile!
const [z, x, y] = Utils.tile_from_index(tile.tileIndex) const [z, x, y] = Tiles.tile_from_index(tile.tileIndex)
console.log("Writing tile ", z, x, y, layerId) console.log("Writing tile ", z, x, y, layerId)
const targetPath = geoJsonName(targetdir + "_" + layerId, x, y, z) const targetPath = geoJsonName(targetdir + "_" + layerId, x, y, z)
createdTiles.push(tile.tileIndex) createdTiles.push(tile.tileIndex)
@ -241,7 +241,7 @@ function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsT
// Only thing left to do is to create the index // Only thing left to do is to create the index
const path = targetdir + "_" + layerId + "_overview.json" const path = targetdir + "_" + layerId + "_overview.json"
const perX = {} const perX = {}
createdTiles.map(i => Utils.tile_from_index(i)).forEach(([z, x, y]) => { createdTiles.map(i => Tiles.tile_from_index(i)).forEach(([z, x, y]) => {
const key = "" + x const key = "" + x
if (perX[key] === undefined) { if (perX[key] === undefined) {
perX[key] = [] perX[key] = []
@ -279,7 +279,7 @@ async function main(args: string[]) {
const lat1 = Number(args[5]) const lat1 = Number(args[5])
const lon1 = Number(args[6]) const lon1 = Number(args[6])
const tileRange = Utils.TileRangeBetween(zoomlevel, lat0, lon0, lat1, lon1) const tileRange = Tiles.TileRangeBetween(zoomlevel, lat0, lon0, lat1, lon1)
const theme = AllKnownLayouts.allKnownLayouts.get(themeName) const theme = AllKnownLayouts.allKnownLayouts.get(themeName)
if (theme === undefined) { if (theme === undefined) {

View file

@ -33,7 +33,7 @@ class TranslationPart {
} }
const v = translations[translationsKey] const v = translations[translationsKey]
if (typeof (v) != "string") { if (typeof (v) != "string") {
console.error("Non-string object in translation: ", translations[translationsKey]) console.error("Non-string object in translation while trying to add more translations to '", translationsKey ,"': ", v)
throw "Error in an object depicting a translation: a non-string object was found. (" + context + ")\n You probably put some other section accidentally in the translation" throw "Error in an object depicting a translation: a non-string object was found. (" + context + ")\n You probably put some other section accidentally in the translation"
} }
this.contents.set(translationsKey, v) this.contents.set(translationsKey, v)
@ -41,9 +41,7 @@ class TranslationPart {
} }
recursiveAdd(object: any, context: string) { recursiveAdd(object: any, context: string) {
const isProbablyTranslationObject = knownLanguages.some(l => object.hasOwnProperty(l));
const isProbablyTranslationObject = knownLanguages.map(l => object.hasOwnProperty(l)).filter(x => x).length > 0;
if (isProbablyTranslationObject) { if (isProbablyTranslationObject) {
this.addTranslationObject(object, context) this.addTranslationObject(object, context)
return; return;