Performance optimazations

This commit is contained in:
pietervdvn 2022-01-26 20:47:08 +01:00
parent 632e7e9f9a
commit d2b245ab54
15 changed files with 321 additions and 214 deletions

View file

@ -117,7 +117,6 @@ export default class SelectedElementTagsUpdater {
const localValue = currentTags[key] const localValue = currentTags[key]
if (localValue !== osmValue) { if (localValue !== osmValue) {
console.log("Local value for ", key, ":", localValue, "upstream", osmValue)
somethingChanged = true; somethingChanged = true;
currentTags[key] = osmValue currentTags[key] = osmValue
} }

View file

@ -48,12 +48,12 @@ export class BBox {
} }
return feature.bbox; return feature.bbox;
} }
static bboxAroundAll(bboxes: BBox[]): BBox{ static bboxAroundAll(bboxes: BBox[]): BBox {
let maxLat: number = -90; let maxLat: number = -90;
let maxLon: number= -180; let maxLon: number = -180;
let minLat: number= 80; let minLat: number = 80;
let minLon: number= 180; let minLon: number = 180;
for (const bbox of bboxes) { for (const bbox of bboxes) {
maxLat = Math.max(maxLat, bbox.maxLat) maxLat = Math.max(maxLat, bbox.maxLat)
@ -61,7 +61,7 @@ export class BBox {
minLat = Math.min(minLat, bbox.minLat) minLat = Math.min(minLat, bbox.minLat)
minLon = Math.min(minLon, bbox.minLon) minLon = Math.min(minLon, bbox.minLon)
} }
return new BBox([[maxLon, maxLat],[minLon,minLat]]) return new BBox([[maxLon, maxLat], [minLon, minLat]])
} }
static fromTile(z: number, x: number, y: number): BBox { static fromTile(z: number, x: number, y: number): BBox {
@ -75,6 +75,14 @@ export class BBox {
return BBox.fromTile(...Tiles.tile_from_index(i)) return BBox.fromTile(...Tiles.tile_from_index(i))
} }
public unionWith(other: BBox) {
return new BBox([[
Math.max(this.maxLon, other.maxLon),
Math.max(this.maxLat, other.maxLat)],
[Math.min(this.minLon, other.minLon),
Math.min(this.minLat, other.minLat)]])
}
/** /**
* Constructs a tilerange which fully contains this bbox (thus might be a bit larger) * Constructs a tilerange which fully contains this bbox (thus might be a bit larger)
* @param zoomlevel * @param zoomlevel

View file

@ -0,0 +1,112 @@
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import MetaTagging from "../../MetaTagging";
import {ElementStorage} from "../../ElementStorage";
import {ExtraFuncParams} from "../../ExtraFunctions";
import FeaturePipeline from "../FeaturePipeline";
import {BBox} from "../../BBox";
import {UIEventSource} from "../../UIEventSource";
/****
* Concerned with the logic of updating the right layer at the right time
*/
class MetatagUpdater {
public readonly neededLayerBboxes = new Map<string /*layerId*/, BBox>()
private source: FeatureSourceForLayer & Tiled;
private readonly params: ExtraFuncParams
private state: { allElements?: ElementStorage };
private readonly isDirty = new UIEventSource(false)
constructor(source: FeatureSourceForLayer & Tiled, state: { allElements?: ElementStorage }, featurePipeline: FeaturePipeline) {
this.state = state;
this.source = source;
const self = this;
this.params = {
getFeatureById(id) {
return state.allElements.ContainingFeatures.get(id)
},
getFeaturesWithin(layerId, bbox) {
// We keep track of the BBOX that this source needs
let oldBbox: BBox = self.neededLayerBboxes.get(layerId)
if (oldBbox === undefined) {
self.neededLayerBboxes.set(layerId, bbox);
} else if (!bbox.isContainedIn(oldBbox)) {
self.neededLayerBboxes.set(layerId,oldBbox.unionWith(bbox))
}
return featurePipeline.GetFeaturesWithin(layerId, bbox)
},
memberships: featurePipeline.relationTracker
}
this.isDirty.stabilized(100).addCallback(dirty => {
if(dirty){
self.updateMetaTags()
}
})
this.source.features.addCallbackAndRunD(_ => self.isDirty.setData(true))
}
public requestUpdate(){
this.isDirty.setData(true)
}
private updateMetaTags() {
const features = this.source.features.data
if (features.length === 0) {
this.isDirty.setData(false)
return
}
MetaTagging.addMetatags(
features,
this.params,
this.source.layer.layerDef,
this.state)
this.isDirty.setData(false)
}
}
export default class MetaTagRecalculator {
private _state: {
allElements?: ElementStorage
};
private _featurePipeline: FeaturePipeline;
private readonly _alreadyRegistered: Set<FeatureSourceForLayer & Tiled> = new Set<FeatureSourceForLayer & Tiled>()
private readonly _notifiers : MetatagUpdater[] = []
/**
* The meta tag recalculator receives tiles of layers.
* It keeps track of which sources have had their share calculated, and which should be re-updated if some other data is loaded
*/
constructor(state: { allElements?: ElementStorage }, featurePipeline: FeaturePipeline) {
this._featurePipeline = featurePipeline;
this._state = state;
}
public registerSource(source: FeatureSourceForLayer & Tiled, recalculateOnEveryChange = false) {
if (source === undefined) {
return;
}
if (this._alreadyRegistered.has(source)) {
return;
}
this._alreadyRegistered.add(source)
this._notifiers.push(new MetatagUpdater(source,this._state,this._featurePipeline))
const self = this;
source.features.addCallbackAndRunD(_ => {
const layerName = source.layer.layerDef.id
for (const updater of self._notifiers ) {
const neededBbox = updater.neededLayerBboxes.get(layerName)
if(neededBbox == undefined){
continue
}
if(source.bbox === undefined || neededBbox.overlapsWith(source.bbox)){
updater.requestUpdate()
}
}
})
}
}

View file

@ -5,7 +5,6 @@ import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from
import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource"; import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource";
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy"; import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy";
import MetaTagging from "../MetaTagging";
import RememberingSource from "./Sources/RememberingSource"; import RememberingSource from "./Sources/RememberingSource";
import OverpassFeatureSource from "../Actors/OverpassFeatureSource"; import OverpassFeatureSource from "../Actors/OverpassFeatureSource";
import GeoJsonSource from "./Sources/GeoJsonSource"; import GeoJsonSource from "./Sources/GeoJsonSource";
@ -49,7 +48,7 @@ export default class FeaturePipeline {
private readonly overpassUpdater: OverpassFeatureSource private readonly overpassUpdater: OverpassFeatureSource
private state: MapState; private state: MapState;
private readonly relationTracker: RelationsTracker public readonly relationTracker: RelationsTracker
private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>; private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>;
/** /**
@ -63,9 +62,7 @@ export default class FeaturePipeline {
private readonly osmSourceZoomLevel private readonly osmSourceZoomLevel
private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>() private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>()
private readonly metataggingRecalculated = new UIEventSource<void>(undefined)
private readonly requestMetataggingRecalculation = new UIEventSource<Date>(undefined)
/** /**
* Keeps track of all raw OSM-nodes. * Keeps track of all raw OSM-nodes.
* Only initialized if 'type_node' is defined as layer * Only initialized if 'type_node' is defined as layer
@ -74,7 +71,12 @@ export default class FeaturePipeline {
constructor( constructor(
handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void, handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void,
state: MapState) { state: MapState,
options? : {
/*Used for metatagging - will receive all the sources with changeGeometry applied but without filtering*/
handleRawFeatureSource: (source: FeatureSourceForLayer) => void
}
) {
this.state = state; this.state = state;
const self = this const self = this
@ -102,10 +104,6 @@ export default class FeaturePipeline {
return location.zoom >= minzoom; return location.zoom >= minzoom;
} }
); );
this.requestMetataggingRecalculation.stabilized(500).addCallbackAndRunD(_ => {
self.updateAllMetaTagging("Request stabilized")
})
const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed) const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed)
@ -115,13 +113,13 @@ export default class FeaturePipeline {
// Given a tile, wraps it and passes it on to render (handled by 'handleFeatureSource' // Given a tile, wraps it and passes it on to render (handled by 'handleFeatureSource'
function patchedHandleFeatureSource(src: FeatureSourceForLayer & IndexedFeatureSource & Tiled) { function patchedHandleFeatureSource(src: FeatureSourceForLayer & IndexedFeatureSource & Tiled) {
// This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile // This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
const srcFiltered = const withChanges = new ChangeGeometryApplicator(src, state.changes);
new FilteringFeatureSource(state, src.tileIndex, const srcFiltered = new FilteringFeatureSource(state, src.tileIndex,withChanges)
new ChangeGeometryApplicator(src, state.changes),
self.metataggingRecalculated
)
handleFeatureSource(srcFiltered) handleFeatureSource(srcFiltered)
if(options?.handleRawFeatureSource){
options.handleRawFeatureSource(withChanges)
}
self.somethingLoaded.setData(true) self.somethingLoaded.setData(true)
// We do not mark as visited here, this is the responsability of the code near the actual loader (e.g. overpassLoader and OSMApiFeatureLoader) // We do not mark as visited here, this is the responsability of the code near the actual loader (e.g. overpassLoader and OSMApiFeatureLoader)
} }
@ -178,11 +176,6 @@ export default class FeaturePipeline {
if (id === "current_view") { if (id === "current_view") {
handlePriviligedFeatureSource(state.currentView) handlePriviligedFeatureSource(state.currentView)
state.currentView.features.map(ffs => ffs[0]?.feature?.properties?.id).withEqualityStabilized((x,y) => x === y)
.addCallbackAndRunD(_ => {
self.applyMetaTags(state.currentView, <any>this.state, `currentview changed`)
}
)
continue continue
} }
@ -324,7 +317,6 @@ export default class FeaturePipeline {
perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer) perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer)
// AT last, we always apply the metatags whenever possible // AT last, we always apply the metatags whenever possible
perLayer.features.addCallbackAndRunD(feats => { perLayer.features.addCallbackAndRunD(feats => {
console.log("New feature for layer ", perLayer.layer.layerDef.id, ":", feats)
self.onNewDataLoaded(perLayer); self.onNewDataLoaded(perLayer);
}) })
@ -335,13 +327,6 @@ export default class FeaturePipeline {
}} }}
) )
// Whenever fresh data comes in, we need to update the metatagging
self.newDataLoadedSignal.stabilized(250).addCallback(src => {
self.updateAllMetaTagging(`New data loaded by ${src.name} (and stabilized)`)
})
this.runningQuery = updater.runningQuery.map( this.runningQuery = updater.runningQuery.map(
overpass => { overpass => {
console.log("FeaturePipeline: runningQuery state changed: Overpass", overpass ? "is querying," : "is idle,", console.log("FeaturePipeline: runningQuery state changed: Overpass", overpass ? "is querying," : "is idle,",
@ -354,7 +339,6 @@ export default class FeaturePipeline {
private onNewDataLoaded(src: FeatureSource){ private onNewDataLoaded(src: FeatureSource){
this.newDataLoadedSignal.setData(src) this.newDataLoadedSignal.setData(src)
this.requestMetataggingRecalculation.setData(new Date())
} }
public GetAllFeaturesWithin(bbox: BBox): any[][] { public GetAllFeaturesWithin(bbox: BBox): any[][] {
@ -501,44 +485,4 @@ export default class FeaturePipeline {
return updater; return updater;
} }
private applyMetaTags(src: FeatureSourceForLayer, state: any, reason: string) {
const self = this
if(src === undefined){
throw "Src is undefined"
}
const layerDef = src.layer.layerDef;
console.debug(`Applying metatags onto ${src.name} due to ${reason} which has ${src.features.data?.length} features`)
if(src.features.data.length == 0){
return
}
MetaTagging.addMetatags(
src.features.data,
{
memberships: this.relationTracker,
getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox),
getFeatureById: (id: string) => self.state.allElements.ContainingFeatures.get(id)
},
layerDef,
state,
{
includeDates: true,
// We assume that the non-dated metatags are already set by the cache generator
includeNonDates: layerDef.source.geojsonSource === undefined || !layerDef.source.isOsmCacheLayer
}
)
}
public updateAllMetaTagging(reason: string) {
const self = this;
this.perLayerHierarchy.forEach(hierarchy => {
hierarchy.loadedTiles.forEach(tile => {
self.applyMetaTags(tile, <any> this.state, `${reason} (tile ${tile.tileIndex})`)
})
})
self.metataggingRecalculated.ping()
}
} }

View file

@ -59,7 +59,7 @@ export class GeoOperations {
const coor = feature.geometry.coordinates; const coor = feature.geometry.coordinates;
for (const otherFeature of otherFeatures) { for (const otherFeature of otherFeatures) {
if (feature.id !== undefined && feature.id === otherFeature.id) { if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) {
continue; continue;
} }
@ -79,7 +79,7 @@ export class GeoOperations {
for (const otherFeature of otherFeatures) { for (const otherFeature of otherFeatures) {
if (feature.id !== undefined && feature.id === otherFeature.id) { if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) {
continue; continue;
} }
@ -97,7 +97,7 @@ export class GeoOperations {
for (const otherFeature of otherFeatures) { for (const otherFeature of otherFeatures) {
if (feature.id === otherFeature.id) { if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) {
continue; continue;
} }

View file

@ -1,6 +1,7 @@
import SimpleMetaTaggers, {SimpleMetaTagger} from "./SimpleMetaTagger"; import SimpleMetaTaggers, {SimpleMetaTagger} from "./SimpleMetaTagger";
import {ExtraFuncParams, ExtraFunctions} from "./ExtraFunctions"; import {ExtraFuncParams, ExtraFunctions} from "./ExtraFunctions";
import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import {ElementStorage} from "./ElementStorage";
/** /**
@ -23,7 +24,7 @@ export default class MetaTagging {
public static addMetatags(features: { feature: any; freshness: Date }[], public static addMetatags(features: { feature: any; freshness: Date }[],
params: ExtraFuncParams, params: ExtraFuncParams,
layer: LayerConfig, layer: LayerConfig,
state, state?: {allElements?: ElementStorage},
options?: { options?: {
includeDates?: true | boolean, includeDates?: true | boolean,
includeNonDates?: true | boolean includeNonDates?: true | boolean
@ -35,11 +36,11 @@ export default class MetaTagging {
const metatagsToApply: SimpleMetaTagger[] = [] const metatagsToApply: SimpleMetaTagger[] = []
for (const metatag of SimpleMetaTaggers.metatags) { for (const metatag of SimpleMetaTaggers.metatags) {
if (metatag.includesDates) { if (metatag.includesDates) {
if (options.includeDates ?? true) { if (options?.includeDates ?? true) {
metatagsToApply.push(metatag) metatagsToApply.push(metatag)
} }
} else { } else {
if (options.includeNonDates ?? true) { if (options?.includeNonDates ?? true) {
metatagsToApply.push(metatag) metatagsToApply.push(metatag)
} }
} }
@ -103,7 +104,8 @@ export default class MetaTagging {
} }
return atLeastOneFeatureChanged return atLeastOneFeatureChanged
} }
public static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => void)[] {
private static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => void)[] {
const functions: ((feature: any) => any)[] = []; const functions: ((feature: any) => any)[] = [];
for (const entry of calculatedTags) { for (const entry of calculatedTags) {
const key = entry[0] const key = entry[0]

View file

@ -9,7 +9,7 @@ import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import {CountryCoder} from "latlon2country" import {CountryCoder} from "latlon2country"
export class SimpleMetaTagger { export class SimpleMetaTagger {
public readonly keys: string[]; public readonly keys: string[];
public readonly doc: string; public readonly doc: string;
public readonly isLazy: boolean; public readonly isLazy: boolean;
@ -42,9 +42,9 @@ export class SimpleMetaTagger {
export class CountryTagger extends SimpleMetaTagger { export class CountryTagger extends SimpleMetaTagger {
private static readonly coder = new CountryCoder("https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country", Utils.downloadJson); private static readonly coder = new CountryCoder("https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country", Utils.downloadJson);
public runningTasks: Set<any>; public runningTasks: Set<any>;
constructor() { constructor() {
const runningTasks= new Set<any>(); const runningTasks = new Set<any>();
super super
( (
{ {
@ -77,20 +77,12 @@ export class CountryTagger extends SimpleMetaTagger {
return false; return false;
}) })
) )
this.runningTasks = runningTasks; this.runningTasks = runningTasks;
} }
} }
export default class SimpleMetaTaggers { export default class SimpleMetaTaggers {
private static readonly cardinalDirections = {
N: 0, NNE: 22.5, NE: 45, ENE: 67.5,
E: 90, ESE: 112.5, SE: 135, SSE: 157.5,
S: 180, SSW: 202.5, SW: 225, WSW: 247.5,
W: 270, WNW: 292.5, NW: 315, NNW: 337.5
}
public static readonly objectMetaInfo = new SimpleMetaTagger( public static readonly objectMetaInfo = new SimpleMetaTagger(
{ {
keys: ["_last_edit:contributor", keys: ["_last_edit:contributor",
@ -121,7 +113,25 @@ export default class SimpleMetaTaggers {
return true; return true;
} }
) )
public static country = new CountryTagger()
public static geometryType = new SimpleMetaTagger(
{
keys: ["_geometry:type"],
doc: "Adds the geometry type as property. This is identical to the GoeJson geometry type and is one of `Point`,`LineString`, `Polygon` and exceptionally `MultiPolygon` or `MultiLineString`",
},
(feature, _) => {
const changed = feature.properties["_geometry:type"] === feature.geometry.type;
feature.properties["_geometry:type"] = feature.geometry.type;
return changed
}
)
private static readonly cardinalDirections = {
N: 0, NNE: 22.5, NE: 45, ENE: 67.5,
E: 90, ESE: 112.5, SE: 135, SSE: 157.5,
S: 180, SSW: 202.5, SW: 225, WSW: 247.5,
W: 270, WNW: 292.5, NW: 315, NNW: 337.5
}
private static latlon = new SimpleMetaTagger({ private static latlon = new SimpleMetaTagger({
keys: ["_lat", "_lon"], keys: ["_lat", "_lon"],
doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)" doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)"
@ -201,7 +211,6 @@ export default class SimpleMetaTaggers {
return true; return true;
}) })
); );
private static canonicalize = new SimpleMetaTagger( private static canonicalize = new SimpleMetaTagger(
{ {
doc: "If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`)", doc: "If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`)",
@ -254,7 +263,6 @@ export default class SimpleMetaTaggers {
return rewritten return rewritten
}) })
) )
private static lngth = new SimpleMetaTagger( private static lngth = new SimpleMetaTagger(
{ {
keys: ["_length", "_length:km"], keys: ["_length", "_length:km"],
@ -269,7 +277,6 @@ export default class SimpleMetaTaggers {
return true; return true;
}) })
) )
public static country = new CountryTagger()
private static isOpen = new SimpleMetaTagger( private static isOpen = new SimpleMetaTagger(
{ {
keys: ["_isOpen"], keys: ["_isOpen"],
@ -277,23 +284,33 @@ export default class SimpleMetaTaggers {
includesDates: true, includesDates: true,
isLazy: true isLazy: true
}, },
((feature, _, __ ,state) => { ((feature, _, __, state) => {
if (Utils.runningFromConsole) { if (Utils.runningFromConsole) {
// We are running from console, thus probably creating a cache // We are running from console, thus probably creating a cache
// isOpen is irrelevant // isOpen is irrelevant
return false return false
} }
if(feature.properties.opening_hours === "24/7"){
feature.properties._isOpen = "yes"
return true;
}
Object.defineProperty(feature.properties, "_isOpen", { Object.defineProperty(feature.properties, "_isOpen", {
enumerable: false, enumerable: false,
configurable: true, configurable: true,
get: () => { get: () => {
if(feature.properties.id === "node/7464543832"){
console.log("Getting _isOpen for ", feature.properties.i)
}
delete feature.properties._isOpen delete feature.properties._isOpen
feature.properties._isOpen = undefined feature.properties._isOpen = undefined
const tagsSource = state.allElements.getEventSourceById(feature.properties.id); const tagsSource = state.allElements.getEventSourceById(feature.properties.id);
tagsSource tagsSource.addCallbackAndRunD(tags => {
.addCallbackAndRunD(tags => { // Install a listener to the tags...
if (tags.opening_hours === undefined || tags._country === undefined) { if (tags.opening_hours === undefined){
return;
}
if(tags._country === undefined) {
return; return;
} }
try { try {
@ -305,18 +322,20 @@ export default class SimpleMetaTaggers {
country_code: tags._country.toLowerCase() country_code: tags._country.toLowerCase()
} }
}, {tag_key: "opening_hours"}); }, {tag_key: "opening_hours"});
// AUtomatically triggered on the next change
// AUtomatically triggered on the next change (and a bit below)
const updateTags = () => { const updateTags = () => {
const oldValueIsOpen = tags["_isOpen"]; const oldValueIsOpen = tags["_isOpen"];
const oldNextChange = tags["_isOpen:nextTrigger"] ?? 0; const oldNextChange = tags["_isOpen:nextTrigger"] ?? 0;
if (oldNextChange > (new Date()).getTime() && if (oldNextChange > (new Date()).getTime() &&
tags["_isOpen:oldvalue"] === tags["opening_hours"] tags["_isOpen:oldvalue"] === tags["opening_hours"] // Check that changes have to be made
&& tags["_isOpen"] !== undefined) { && tags["_isOpen"] !== undefined) {
// Already calculated and should not yet be triggered // Already calculated and should not yet be triggered
return false; return false;
} }
// Recalculate!
tags["_isOpen"] = oh.getState() ? "yes" : "no"; tags["_isOpen"] = oh.getState() ? "yes" : "no";
const comment = oh.getComment(); const comment = oh.getComment();
if (comment) { if (comment) {
@ -380,8 +399,6 @@ export default class SimpleMetaTaggers {
return true; return true;
}) })
) )
private static currentTime = new SimpleMetaTagger( private static currentTime = new SimpleMetaTagger(
{ {
keys: ["_now:date", "_now:datetime", "_loaded:date", "_loaded:_datetime"], keys: ["_now:date", "_now:datetime", "_loaded:date", "_loaded:_datetime"],
@ -410,20 +427,6 @@ export default class SimpleMetaTaggers {
return true; return true;
} }
); );
public static geometryType = new SimpleMetaTagger(
{
keys:["_geometry:type"],
doc: "Adds the geometry type as property. This is identical to the GoeJson geometry type and is one of `Point`,`LineString`, `Polygon` and exceptionally `MultiPolygon` or `MultiLineString`",
},
(feature, _) => {
const changed = feature.properties["_geometry:type"] === feature.geometry.type;
feature.properties["_geometry:type"] = feature.geometry.type;
return changed
}
)
public static metatags: SimpleMetaTagger[] = [ public static metatags: SimpleMetaTagger[] = [
SimpleMetaTaggers.latlon, SimpleMetaTaggers.latlon,
SimpleMetaTaggers.layerInfo, SimpleMetaTaggers.layerInfo,
@ -439,11 +442,9 @@ export default class SimpleMetaTaggers {
SimpleMetaTaggers.geometryType SimpleMetaTaggers.geometryType
]; ];
public static readonly lazyTags: string[] = [].concat(...SimpleMetaTaggers.metatags.filter(tagger => tagger.isLazy) public static readonly lazyTags: string[] = [].concat(...SimpleMetaTaggers.metatags.filter(tagger => tagger.isLazy)
.map(tagger => tagger.keys)); .map(tagger => tagger.keys));
/** /**
* Edits the given object to rewrite 'both'-tagging into a 'left-right' tagging scheme. * Edits the given object to rewrite 'both'-tagging into a 'left-right' tagging scheme.
* These changes are performed in-place. * These changes are performed in-place.

View file

@ -10,6 +10,8 @@ import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler";
import Hash from "../Web/Hash"; import Hash from "../Web/Hash";
import {BBox} from "../BBox"; import {BBox} from "../BBox";
import FeatureInfoBox from "../../UI/Popup/FeatureInfoBox"; import FeatureInfoBox from "../../UI/Popup/FeatureInfoBox";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource";
import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator";
export default class FeaturePipelineState extends MapState { export default class FeaturePipelineState extends MapState {
@ -18,7 +20,7 @@ export default class FeaturePipelineState extends MapState {
*/ */
public readonly featurePipeline: FeaturePipeline; public readonly featurePipeline: FeaturePipeline;
private readonly featureAggregator: TileHierarchyAggregator; private readonly featureAggregator: TileHierarchyAggregator;
private readonly metatagRecalculator : MetaTagRecalculator
constructor(layoutToUse: LayoutConfig) { constructor(layoutToUse: LayoutConfig) {
super(layoutToUse); super(layoutToUse);
@ -26,81 +28,106 @@ export default class FeaturePipelineState extends MapState {
this.featureAggregator = TileHierarchyAggregator.createHierarchy(this); this.featureAggregator = TileHierarchyAggregator.createHierarchy(this);
const clusterCounter = this.featureAggregator const clusterCounter = this.featureAggregator
const self = this; const self = this;
this.featurePipeline = new FeaturePipeline(
source => {
clusterCounter.addTile(source) /**
* We are a bit in a bind:
* There is the featurePipeline, which creates some sources during construction
* THere is the metatagger, which needs to have these sources registered AND which takes a FeaturePipeline as argument
*
* This is a bit of a catch-22 (except that it isn't)
* The sources that are registered in the constructor are saved into 'registeredSources' temporary
*
*/
const sourcesToRegister = []
function registerRaw(source: FeatureSourceForLayer & Tiled){
if(self.metatagRecalculator === undefined){
sourcesToRegister.push(source)
}else{
self.metatagRecalculator.registerSource(source)
}
}
function registerSource(source: FeatureSourceForLayer & Tiled) {
const sourceBBox = source.features.map(allFeatures => BBox.bboxAroundAll(allFeatures.map(f => BBox.get(f.feature)))) clusterCounter.addTile(source)
const sourceBBox = source.features.map(allFeatures => BBox.bboxAroundAll(allFeatures.map(f => BBox.get(f.feature))))
// Do show features indicates if the respective 'showDataLayer' should be shown. It can be hidden by e.g. clustering
const doShowFeatures = source.features.map(
f => {
const z = self.locationControl.data.zoom
if (!source.layer.isDisplayed.data) { // Do show features indicates if the respective 'showDataLayer' should be shown. It can be hidden by e.g. clustering
return false; const doShowFeatures = source.features.map(
} f => {
const z = self.locationControl.data.zoom
const bounds = self.currentBounds.data if (!source.layer.isDisplayed.data) {
if (bounds === undefined) { return false;
// Map is not yet displayed
return false;
}
if (!sourceBBox.data.overlapsWith(bounds)) {
// Not within range -> features are hidden
return false
}
if (z < source.layer.layerDef.minzoom) {
// Layer is always hidden for this zoom level
return false;
}
if (z > clustering.maxZoom) {
return true
}
if (f.length > clustering.minNeededElements) {
// This tile alone already has too much features
return false
}
let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex);
if (tileZ >= z) {
while (tileZ > z) {
tileZ--
tileX = Math.floor(tileX / 2)
tileY = Math.floor(tileY / 2)
}
if (clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY))?.totalValue > clustering.minNeededElements) {
// To much elements
return false
}
}
return true
}, [this.currentBounds, source.layer.isDisplayed, sourceBBox]
)
new ShowDataLayer(
{
features: source,
leafletMap: self.leafletMap,
layerToShow: source.layer.layerDef,
doShowLayer: doShowFeatures,
selectedElement: self.selectedElement,
state: self,
popup: (tags, layer) => new FeatureInfoBox(tags, layer, self)
} }
);
}, this const bounds = self.currentBounds.data
); if (bounds === undefined) {
// Map is not yet displayed
return false;
}
if (!sourceBBox.data.overlapsWith(bounds)) {
// Not within range -> features are hidden
return false
}
if (z < source.layer.layerDef.minzoom) {
// Layer is always hidden for this zoom level
return false;
}
if (z > clustering.maxZoom) {
return true
}
if (f.length > clustering.minNeededElements) {
// This tile alone already has too much features
return false
}
let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex);
if (tileZ >= z) {
while (tileZ > z) {
tileZ--
tileX = Math.floor(tileX / 2)
tileY = Math.floor(tileY / 2)
}
if (clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY))?.totalValue > clustering.minNeededElements) {
// To much elements
return false
}
}
return true
}, [self.currentBounds, source.layer.isDisplayed, sourceBBox]
)
new ShowDataLayer(
{
features: source,
leafletMap: self.leafletMap,
layerToShow: source.layer.layerDef,
doShowLayer: doShowFeatures,
selectedElement: self.selectedElement,
state: self,
popup: (tags, layer) => new FeatureInfoBox(tags, layer, self)
}
)
}
this.featurePipeline = new FeaturePipeline(registerSource, this, {handleRawFeatureSource: registerRaw});
this.metatagRecalculator = new MetaTagRecalculator(this, this.featurePipeline)
this.metatagRecalculator.registerSource(this.currentView, true)
sourcesToRegister.forEach(source => self.metatagRecalculator.registerSource(source))
new SelectedFeatureHandler(Hash.hash, this) new SelectedFeatureHandler(Hash.hash, this)
this.AddClusteringToMap(this.leafletMap) this.AddClusteringToMap(this.leafletMap)

View file

@ -85,7 +85,7 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
"geoJsonZoomLevel": 10, "geoJsonZoomLevel": 10,
"maxCacheAge": 0 "maxCacheAge": 0
}, },
"minzoom": 12, "minzoom": Math.min(12, layerJson.minzoom - 2),
"title": { "title": {
"render": t.popupTitle.Subs({title}).translations "render": t.popupTitle.Subs({title}).translations
}, },

View file

@ -214,6 +214,7 @@ export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
if (!hasMinimap) { if (!hasMinimap) {
layerConfig = {...layerConfig} layerConfig = {...layerConfig}
layerConfig.tagRenderings = [...layerConfig.tagRenderings] layerConfig.tagRenderings = [...layerConfig.tagRenderings]
layerConfig.tagRenderings.push(state.tagRenderings.get("questions"))
layerConfig.tagRenderings.push(state.tagRenderings.get("minimap")) layerConfig.tagRenderings.push(state.tagRenderings.get("minimap"))
} }

View file

@ -161,8 +161,6 @@ class AutomationPanel extends Combine{
whenDone("empty") whenDone("empty")
return true; return true;
} }
stateToShow.setData("Applying metatags")
pipeline.updateAllMetaTagging("triggered by automaton")
stateToShow.setData("Gathering applicable elements") stateToShow.setData("Gathering applicable elements")
let handled = 0 let handled = 0
@ -178,7 +176,7 @@ class AutomationPanel extends Combine{
const feature = ffs.feature const feature = ffs.feature
const renderingTr = targetAction.GetRenderValue(feature.properties) const renderingTr = targetAction.GetRenderValue(feature.properties)
const rendering = renderingTr.txt const rendering = renderingTr.txt
log.push("<a href='https://openstreetmap.org/"+feature.properties.id+"' target='_blank'>"+feature.properties.id+"</a>: "+new SubstitutedTranslation(renderingTr, new UIEventSource<any>(feature.properties), state).ConstructElement().innerText) log.push("<a href='https://openstreetmap.org/"+feature.properties.id+"' target='_blank'>"+feature.properties.id+"</a>: "+new SubstitutedTranslation(renderingTr, new UIEventSource<any>(feature.properties), undefined).ConstructElement().innerText)
const actions = Utils.NoNull(SubstitutedTranslation.ExtractSpecialComponents(rendering) const actions = Utils.NoNull(SubstitutedTranslation.ExtractSpecialComponents(rendering)
.map(obj => obj.special)) .map(obj => obj.special))
for (const action of actions) { for (const action of actions) {

View file

@ -18,6 +18,8 @@ import {SubstitutedTranslation} from "../SubstitutedTranslation";
import ValidatedTextField from "../Input/ValidatedTextField"; import ValidatedTextField from "../Input/ValidatedTextField";
import {QueryParameters} from "../../Logic/Web/QueryParameters"; import {QueryParameters} from "../../Logic/Web/QueryParameters";
import {TagUtils} from "../../Logic/Tags/TagUtils"; import {TagUtils} from "../../Logic/Tags/TagUtils";
import {InputElement} from "../Input/InputElement";
import {DropDown} from "../Input/DropDown";
export default class FilterView extends VariableUiElement { export default class FilterView extends VariableUiElement {
constructor(filteredLayer: UIEventSource<FilteredLayer[]>, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]) { constructor(filteredLayer: UIEventSource<FilteredLayer[]>, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]) {
@ -242,17 +244,26 @@ export default class FilterView extends VariableUiElement {
const values : FilterState[] = options.map((f, i) => ({ const values : FilterState[] = options.map((f, i) => ({
currentFilter: f.osmTags, state: i currentFilter: f.osmTags, state: i
})) }))
const radio = new RadioButton( let filterPicker : InputElement<number>
options.map(
(option, i) => if(options.length <= 6){
new FixedInputElement(option.question.Clone().SetClass("block"), i) filterPicker = new RadioButton(
), options.map(
{ (option, i) =>
dontStyle: true new FixedInputElement(option.question.Clone().SetClass("block"), i)
} ),
); {
return [radio, dontStyle: true
radio.GetValue().map( }
);
}else{
filterPicker = new DropDown("", options.map((option, i) => ({
value: i, shown: option.question.Clone()
})))
}
return [filterPicker,
filterPicker.GetValue().map(
i => values[i], i => values[i],
[], [],
selected => { selected => {

View file

@ -1,6 +1,7 @@
import Translations from "./i18n/Translations"; import Translations from "./i18n/Translations";
import {VariableUiElement} from "./Base/VariableUIElement"; import {VariableUiElement} from "./Base/VariableUIElement";
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; import FeaturePipelineState from "../Logic/State/FeaturePipelineState";
import Loading from "./Base/Loading";
export default class CenterMessageBox extends VariableUiElement { export default class CenterMessageBox extends VariableUiElement {
@ -10,7 +11,7 @@ export default class CenterMessageBox extends VariableUiElement {
const message = updater.runningQuery.map( const message = updater.runningQuery.map(
isRunning => { isRunning => {
if (isRunning) { if (isRunning) {
return {el: t.loadingData}; return {el: new Loading(t.loadingData)};
} }
if (!updater.sufficientlyZoomed.data) { if (!updater.sufficientlyZoomed.data) {
return {el: t.zoomIn} return {el: t.zoomIn}
@ -26,8 +27,8 @@ export default class CenterMessageBox extends VariableUiElement {
super(message.map(toShow => toShow.el)) super(message.map(toShow => toShow.el))
this.SetClass("block " + this.SetClass("flex justify-around " +
"rounded-3xl bg-white text-xl font-bold text-center pointer-events-none p-4") "rounded-3xl bg-white text-xl font-bold pointer-events-none p-4")
this.SetStyle("transition: opacity 750ms linear") this.SetStyle("transition: opacity 750ms linear")
message.addCallbackAndRun(toShow => { message.addCallbackAndRun(toShow => {

View file

@ -658,6 +658,6 @@
], ],
"hideFromOverview": true, "hideFromOverview": true,
"defaultBackgroundId": "AGIVFlandersGRB", "defaultBackgroundId": "AGIVFlandersGRB",
"overpassMaxZoom": 15, "overpassMaxZoom": 17,
"osmApiTileSize": 17 "osmApiTileSize": 17
} }

View file

@ -1,4 +1,7 @@
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
import List from "./UI/Base/List";
import Link from "./UI/Base/Link";
import {FixedUiElement} from "./UI/Base/FixedUiElement"; import {FixedUiElement} from "./UI/Base/FixedUiElement";
import Loading from "./UI/Base/Loading";
new Loading(new FixedUiElement("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")).AttachTo("maindiv") const allHidden = AllKnownLayouts.layoutsList.filter(l => l.hideFromOverview)
new List(allHidden.map(th => new Link(new FixedUiElement(th.id), "theme.html?layout="+th.id))).AttachTo("maindiv")