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