forked from MapComplete/MapComplete
Add initial clustering per tile, very broken
This commit is contained in:
parent
2b78c4b53f
commit
c5e9448720
88 changed files with 1080 additions and 651 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue