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) {
ScriptUtils.erasableLog(`Downloading stats for ${year}-${month}, page ${page} ${url}`)
const result = await ScriptUtils.DownloadJSON(url, {
headers: headers
})
const result = await ScriptUtils.DownloadJSON(url, headers)
page++;
allFeatures.push(...result.features)
if (result.features === undefined) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,10 +12,11 @@ export default class WayHandlingApplyingFeatureSource implements FeatureSourceFo
public readonly layer;
constructor(upstream: FeatureSourceForLayer) {
this.name = "Wayhandling(" + upstream.name+")";
this.name = "Wayhandling(" + upstream.name + ")";
this.layer = upstream.layer
const layer = upstream.layer.layerDef;
if (layer.wayHandling === LayerConfig.WAYHANDLING_DEFAULT) {
// We don't have to do anything fancy
// lets just wire up the upstream

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -1,61 +1,6 @@
{
"id": "drinking_water",
"name": {
"en": "Drinking water",
"nl": "Drinkbaar water",
"fr": "Eau potable",
"gl": "Auga potábel",
"de": "Trinkwasser",
"it": "Acqua potabile",
"ru": "Питьевая вода",
"id": "Air minum"
},
"title": {
"render": {
"en": "Drinking water",
"nl": "Drinkbaar water",
"fr": "Eau potable",
"gl": "Auga potábel",
"de": "Trinkwasser",
"it": "Acqua potabile",
"ru": "Питьевая вода",
"id": "Air minum"
}
},
"icon": {
"render": "pin:#6BC4F7;./assets/layers/drinking_water/drips.svg"
},
"iconOverlays": [
{
"if": {
"or": [
"operational_status=broken",
"operational_status=closed"
]
},
"then": "close:#c33",
"badge": true
}
],
"iconSize": "40,40,bottom",
"source": {
"osmTags": {
"and": [
"amenity=drinking_water",
"access!=permissive",
"access!=private"
]
}
},
"calculatedTags": [
"_closest_other_drinking_water_id=feat.closest('drinking_water')?.id",
"_closest_other_drinking_water_distance=Math.floor(feat.distanceTo(feat.closest('drinking_water')).distance * 1000)"
],
"minzoom": 13,
"wayHandling": 1,
"presets": [
{
"title": {
"id": "drinking_water",
"name": {
"en": "Drinking water",
"nl": "Drinkbaar water",
"fr": "Eau potable",
@ -64,105 +9,161 @@
"it": "Acqua potabile",
"ru": "Питьевая вода",
"id": "Air minum"
},
"tags": [
"amenity=drinking_water"
]
}
],
"color": "#6bc4f7",
"tagRenderings": [
"images",
{
"#": "Still in use?",
"question": {
"en": "Is this drinking water spot still operational?",
"nl": "Is deze drinkwaterkraan nog steeds werkende?",
"it": "Questo punto di acqua potabile è sempre funzionante?",
"fr": "Ce point d'eau potable est-il toujours opérationnel ?",
"de": "Ist diese Trinkwasserstelle noch in Betrieb?"
},
"render": {
"en": "The operational status is <i>{operational_status</i>",
"nl": "Deze waterkraan-status is <i>{operational_status}</i>",
"it": "Lo stato operativo è <i>{operational_status}</i>",
"fr": "L'état opérationnel est <i>{operational_status</i>",
"de": "Der Betriebsstatus ist <i>{operational_status</i>"
},
"freeform": {
"key": "operational_status"
},
"mappings": [
{
"if": "operational_status=",
"then": {
"en": "This drinking water works",
"nl": "Deze drinkwaterfontein werkt",
"it": "La fontanella funziona",
"fr": "Cette fontaine fonctionne"
}
},
{
"if": "operational_status=broken",
"then": {
"en": "This drinking water is broken",
"nl": "Deze drinkwaterfontein is kapot",
"it": "La fontanella è guasta",
"fr": "Cette fontaine est cassée"
}
},
{
"if": "operational_status=closed",
"then": {
"en": "This drinking water is closed",
"nl": "Deze drinkwaterfontein is afgesloten",
"it": "La fontanella è chiusa",
"fr": "Cette fontaine est fermée"
}
}
]
},
{
"#": "Bottle refill",
"question": {
"en": "How easy is it to fill water bottles?",
"nl": "Hoe gemakkelijk is het om drinkbussen bij te vullen?",
"de": "Wie einfach ist es, Wasserflaschen zu füllen?",
"it": "Quanto è facile riempire dacqua le bottiglie?",
"fr": "Est-il facile de remplir des bouteilles d'eau ?"
},
"mappings": [
"title": {
"render": {
"en": "Drinking water",
"nl": "Drinkbaar water",
"fr": "Eau potable",
"gl": "Auga potábel",
"de": "Trinkwasser",
"it": "Acqua potabile",
"ru": "Питьевая вода",
"id": "Air minum"
}
},
"icon": {
"render": "pin:#6BC4F7;./assets/layers/drinking_water/drips.svg"
},
"iconOverlays": [
{
"if": "bottle=yes",
"then": {
"en": "It is easy to refill water bottles",
"nl": "Een drinkbus bijvullen gaat makkelijk",
"de": "Es ist einfach, Wasserflaschen nachzufüllen",
"it": "È facile riempire dacqua le bottiglie",
"fr": "Il est facile de remplir les bouteilles d'eau"
}
"if": {
"or": [
"operational_status=broken",
"operational_status=closed"
]
},
"then": "close:#c33",
"badge": true
}
],
"iconSize": "40,40,bottom",
"source": {
"osmTags": {
"and": [
"amenity=drinking_water",
"access!=permissive",
"access!=private"
]
}
},
"calculatedTags": [
"_closest_other_drinking_water=feat.closestn('drinking_water', 1, 500).map(f => ({id: f.feat.id, distance: f.distance}))[0]",
"_closest_other_drinking_water_id=JSON.parse(feat.properties._closest_other_drinking_water)?.id",
"_closest_other_drinking_water_distance=Math.floor(JSON.parse(feat.properties._closest_other_drinking_water)?.distance * 1000)"
],
"minzoom": 13,
"wayHandling": 1,
"presets": [
{
"title": {
"en": "Drinking water",
"nl": "Drinkbaar water",
"fr": "Eau potable",
"gl": "Auga potábel",
"de": "Trinkwasser",
"it": "Acqua potabile",
"ru": "Питьевая вода",
"id": "Air minum"
},
"tags": [
"amenity=drinking_water"
]
}
],
"color": "#6bc4f7",
"tagRenderings": [
"images",
{
"#": "Still in use?",
"question": {
"en": "Is this drinking water spot still operational?",
"nl": "Is deze drinkwaterkraan nog steeds werkende?",
"it": "Questo punto di acqua potabile è sempre funzionante?",
"fr": "Ce point d'eau potable est-il toujours opérationnel ?",
"de": "Ist diese Trinkwasserstelle noch in Betrieb?"
},
"render": {
"en": "The operational status is <i>{operational_status</i>",
"nl": "Deze waterkraan-status is <i>{operational_status}</i>",
"it": "Lo stato operativo è <i>{operational_status}</i>",
"fr": "L'état opérationnel est <i>{operational_status</i>",
"de": "Der Betriebsstatus ist <i>{operational_status</i>"
},
"freeform": {
"key": "operational_status"
},
"mappings": [
{
"if": "operational_status=",
"then": {
"en": "This drinking water works",
"nl": "Deze drinkwaterfontein werkt",
"it": "La fontanella funziona",
"fr": "Cette fontaine fonctionne"
}
},
{
"if": "operational_status=broken",
"then": {
"en": "This drinking water is broken",
"nl": "Deze drinkwaterfontein is kapot",
"it": "La fontanella è guasta",
"fr": "Cette fontaine est cassée"
}
},
{
"if": "operational_status=closed",
"then": {
"en": "This drinking water is closed",
"nl": "Deze drinkwaterfontein is afgesloten",
"it": "La fontanella è chiusa",
"fr": "Cette fontaine est fermée"
}
}
]
},
{
"if": "bottle=no",
"then": {
"en": "Water bottles may not fit",
"nl": "Een drinkbus past moeilijk",
"de": "Wasserflaschen passen möglicherweise nicht",
"it": "Le bottiglie dacqua potrebbero non entrare",
"fr": "Les bouteilles d'eau peuvent ne pas passer"
}
"#": "Bottle refill",
"question": {
"en": "How easy is it to fill water bottles?",
"nl": "Hoe gemakkelijk is het om drinkbussen bij te vullen?",
"de": "Wie einfach ist es, Wasserflaschen zu füllen?",
"it": "Quanto è facile riempire dacqua le bottiglie?",
"fr": "Est-il facile de remplir des bouteilles d'eau ?"
},
"mappings": [
{
"if": "bottle=yes",
"then": {
"en": "It is easy to refill water bottles",
"nl": "Een drinkbus bijvullen gaat makkelijk",
"de": "Es ist einfach, Wasserflaschen nachzufüllen",
"it": "È facile riempire dacqua le bottiglie",
"fr": "Il est facile de remplir les bouteilles d'eau"
}
},
{
"if": "bottle=no",
"then": {
"en": "Water bottles may not fit",
"nl": "Een drinkbus past moeilijk",
"de": "Wasserflaschen passen möglicherweise nicht",
"it": "Le bottiglie dacqua potrebbero non entrare",
"fr": "Les bouteilles d'eau peuvent ne pas passer"
}
}
]
},
{
"render": {
"en": "<a href='#{_closest_other_drinking_water_id}'>There is another drinking water fountain at {_closest_other_drinking_water_distance} meter</a>",
"nl": "<a href='#{_closest_other_drinking_water_id}'>Er bevindt zich een ander drinkwaterpunt op {_closest_other_drinking_water_distance} meter</a>",
"it": "<a href='#{_closest_other_drinking_water_id}'>Cè unaltra fontanella a {_closest_other_drinking_water_distance} metri</a>",
"de": "<a href='#{_closest_other_drinking_water_id}'>Ein weiterer Trinkwasserbrunnen befindet sich in {_closest_other_drinking_water_distance} Meter</a>",
"fr": "<a href='#{_closest_other_drinking_water_id}'>Une autre source deau potable est à {_closest_other_drinking_water_distance} mètres a>"
},
"condition": "_closest_other_drinking_water_id~*"
}
]
},
{
"render": {
"en": "<a href='#{_closest_other_drinking_water_id}'>There is another drinking water fountain at {_closest_other_drinking_water_distance} meter</a>",
"nl": "<a href='#{_closest_other_drinking_water_id}'>Er bevindt zich een ander drinkwaterpunt op {_closest_other_drinking_water_distance} meter</a>",
"it": "<a href='#{_closest_other_drinking_water_id}'>Cè unaltra fontanella a {_closest_other_drinking_water_distance} metri</a>",
"de": "<a href='#{_closest_other_drinking_water_id}'>Ein weiterer Trinkwasserbrunnen befindet sich in {_closest_other_drinking_water_distance} Meter</a>",
"fr": "<a href='#{_closest_other_drinking_water_id}'>Une autre source deau potable est à {_closest_other_drinking_water_distance} mètres a>"
},
"condition": "_closest_other_drinking_water_id~*"
}
]
]
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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