diff --git a/Logic/Actors/SelectedElementTagsUpdater.ts b/Logic/Actors/SelectedElementTagsUpdater.ts index bb46734d28..a105f6d985 100644 --- a/Logic/Actors/SelectedElementTagsUpdater.ts +++ b/Logic/Actors/SelectedElementTagsUpdater.ts @@ -35,9 +35,20 @@ export default class SelectedElementTagsUpdater { state.selectedElement.addCallbackAndRunD(s => { - const id = s.properties?.id - OsmObject.DownloadObjectAsync(id).then(obj => { - SelectedElementTagsUpdater.applyUpdate(state, obj, id) + let id = s.properties?.id + + const backendUrl = state.osmConnection._oauth_config.url + if(id.startsWith(backendUrl)){ + id = id.substring(backendUrl.length) + } + + if(!(id.startsWith("way") || id.startsWith("node") || id.startsWith("relation"))){ + // This object is _not_ from OSM, so we skip it! + return; + } + + OsmObject.DownloadPropertiesOf(id).then(tags => { + SelectedElementTagsUpdater.applyUpdate(state, tags, id) }).catch(e => { console.error("Could not update tags of ", id, "due to", e) }) @@ -50,14 +61,12 @@ export default class SelectedElementTagsUpdater { allElements: ElementStorage, changes: Changes, osmConnection: OsmConnection - }, obj: OsmObject, id: string + }, latestTags: any, id: string ) { const pendingChanges = state.changes.pendingChanges.data - .filter(change => change.type === obj.type && change.id === obj.id) + .filter(change => change.type +"/"+ change.id === id) .filter(change => change.tags !== undefined); - const latestTags = obj.tags - console.log("Applying updates of ", id, " got tags", latestTags, "and still have to apply changes: ", pendingChanges) - + for (const pendingChange of pendingChanges) { const tagChanges = pendingChange.tags; for (const tagChange of tagChanges) { @@ -84,7 +93,7 @@ export default class SelectedElementTagsUpdater { const localValue = currentTags[key] if (localValue !== osmValue) { - console.log("Local value:", localValue, "upstream", osmValue) + console.log("Local value for ", key ,":", localValue, "upstream", osmValue) somethingChanged = true; currentTags[key] = osmValue } @@ -92,6 +101,8 @@ export default class SelectedElementTagsUpdater { if (somethingChanged) { console.log("Detected upstream changes to the object when opening it, updating...") currentTagsSource.ping() + }else{ + console.debug("Fetched latest tags for ", id, "but detected no changes") } } diff --git a/Logic/ExtraFunction.ts b/Logic/ExtraFunction.ts index 76c28c5a9b..d86cbf91ba 100644 --- a/Logic/ExtraFunction.ts +++ b/Logic/ExtraFunction.ts @@ -135,10 +135,10 @@ export class ExtraFunction { args: ["list of features or layer name", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"] }, (params, feature) => { - + return (features, amount, uniqueTag, maxDistanceInMeters) => { - let distance : number = Number(maxDistanceInMeters) - if(isNaN(distance)){ + let distance: number = Number(maxDistanceInMeters) + if (isNaN(distance)) { distance = undefined } return ExtraFunction.GetClosestNFeatures(params, feature, features, { @@ -164,32 +164,13 @@ export class ExtraFunction { } ) - private static readonly AspectedRouting = new ExtraFunction( - { - name: "score", - doc: "Given the path of an aspected routing json file, will calculate the score. This score is wrapped in a UIEventSource, so for further calculations, use `.map(score => ...)`" + - "\n\n" + - "For example: `_comfort_score=feat.score('https://raw.githubusercontent.com/pietervdvn/AspectedRouting/master/Examples/bicycle/aspects/bicycle.comfort.json')`", - args: ["path"] - }, - (_, feature) => { - return (path) => { - return UIEventSourceTools.downloadJsonCached(path).map(config => { - if (config === undefined) { - return - } - return new AspectedRouting(config).evaluate(feature.properties) - }) - } - } - ) + private static readonly allFuncs: ExtraFunction[] = [ ExtraFunction.DistanceToFunc, ExtraFunction.OverlapFunc, ExtraFunction.ClosestObjectFunc, ExtraFunction.ClosestNObjectFunc, - ExtraFunction.Memberships, - ExtraFunction.AspectedRouting + ExtraFunction.Memberships ]; private readonly _name: string; private readonly _args: string[]; @@ -243,33 +224,30 @@ export class ExtraFunction { const maxFeatures = options?.maxFeatures ?? 1 const maxDistance = options?.maxDistance ?? 500 const uniqueTag: string | undefined = options?.uniqueTag + console.log("Requested closestN") if (typeof features === "string") { const name = features const bbox = GeoOperations.bbox(GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance)) features = params.getFeaturesWithin(name, new BBox(bbox.geometry.coordinates)) - }else{ + } else { features = [features] } if (features === undefined) { return; } + const selfCenter = GeoOperations.centerpointCoordinates(feature) let closestFeatures: { feat: any, distance: number }[] = []; - for(const featureList of features) { + for (const featureList of features) { for (const otherFeature of featureList) { if (otherFeature === feature || otherFeature.id === feature.id) { continue; // We ignore self } - let distance = undefined; - if (otherFeature._lon !== undefined && otherFeature._lat !== undefined) { - distance = GeoOperations.distanceBetween([otherFeature._lon, otherFeature._lat], [feature._lon, feature._lat]); - } else { - distance = GeoOperations.distanceBetween( - GeoOperations.centerpointCoordinates(otherFeature), - [feature._lon, feature._lat] - ) - } - if (distance === undefined || distance === null) { + const distance = GeoOperations.distanceBetween( + GeoOperations.centerpointCoordinates(otherFeature), + selfCenter + ) + if (distance === undefined || distance === null || isNaN(distance)) { console.error("Could not calculate the distance between", feature, "and", otherFeature) throw "Undefined distance!" } diff --git a/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts b/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts index dd1aec169d..b0fbc25ded 100644 --- a/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts +++ b/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts @@ -4,12 +4,14 @@ * Technically, more an Actor then a featuresource, but it fits more neatly this ay */ import {FeatureSourceForLayer} from "../FeatureSource"; +import SimpleMetaTagger from "../../SimpleMetaTagger"; export default class SaveTileToLocalStorageActor { public static readonly storageKey: string = "cached-features"; public static readonly formatVersion: string = "1" constructor(source: FeatureSourceForLayer, tileIndex: number) { + source.features.addCallbackAndRunD(features => { const key = `${SaveTileToLocalStorageActor.storageKey}-${source.layer.layerDef.id}-${tileIndex}` const now = new Date() diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index 17ab91ffb8..8d8b2e4b19 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -243,7 +243,7 @@ export default class FeaturePipeline { this.runningQuery = updater.runningQuery.map( overpass => { console.log("FeaturePipeline: runningQuery state changed. Overpass", overpass ? "is querying," : "is idle,", - "osmFeatureSource is", osmFeatureSource.isRunning ? "is running ("+ +")" : "is idle") + "osmFeatureSource is", osmFeatureSource.isRunning ? "is running and needs "+neededTilesFromOsm.data?.length+" tiles (already got "+ osmFeatureSource.downloadedTiles.size +" tiles )" : "is idle") return overpass || osmFeatureSource.isRunning.data; }, [osmFeatureSource.isRunning] ) @@ -361,7 +361,6 @@ export default class FeaturePipeline { const self = this window.setTimeout( () => { - console.debug("Applying metatagging onto ", src.name) const layerDef = src.layer.layerDef; MetaTagging.addMetatags( src.features.data, @@ -384,7 +383,6 @@ export default class FeaturePipeline { private updateAllMetaTagging() { const self = this; - console.log("Reupdating all metatagging") this.perLayerHierarchy.forEach(hierarchy => { hierarchy.loadedTiles.forEach(src => { self.applyMetaTags(src) diff --git a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts index e07efe9f7a..043414649c 100644 --- a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts @@ -28,7 +28,7 @@ export default class OsmFeatureSource { }, markTileVisited?: (tileId: number) => void }; - private readonly downloadedTiles = new Set() + public readonly downloadedTiles = new Set() private readonly allowedTags: TagsFilter; constructor(options: { @@ -52,14 +52,17 @@ export default class OsmFeatureSource { if (options.isActive?.data === false) { return; } + + neededTiles = neededTiles.filter(tile => !self.downloadedTiles.has(tile)) + if(neededTiles.length == 0){ + return; + } + self.isRunning.setData(true) try { for (const neededTile of neededTiles) { - if (self.downloadedTiles.has(neededTile)) { - continue; - } console.log("Tile download", Tiles.tile_from_index(neededTile).join("/"), "started") self.downloadedTiles.add(neededTile) self.LoadTile(...Tiles.tile_from_index(neededTile)).then(_ => { diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index 9f96e77309..8b7eec75ba 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -1,6 +1,5 @@ import SimpleMetaTagger from "./SimpleMetaTagger"; import {ExtraFuncParams, ExtraFunction} from "./ExtraFunction"; -import {UIEventSource} from "./UIEventSource"; import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import State from "../State"; @@ -33,34 +32,43 @@ export default class MetaTagging { } - const metatagsToApply: SimpleMetaTagger [] = [] - for (const metatag of SimpleMetaTagger.metatags) { - if (metatag.includesDates) { - if (options.includeDates ?? true) { - metatagsToApply.push(metatag) - } - } else { - if (options.includeNonDates ?? true) { - metatagsToApply.push(metatag) - } + const metatagsToApply: SimpleMetaTagger [] = [] + for (const metatag of SimpleMetaTagger.metatags) { + if (metatag.includesDates) { + if (options.includeDates ?? true) { + metatagsToApply.push(metatag) + } + } else { + if (options.includeNonDates ?? true) { + metatagsToApply.push(metatag) } } + } // The calculated functions - per layer - which add the new keys const layerFuncs = this.createRetaggingFunc(layer) for (let i = 0; i < features.length; i++) { - const ff = features[i]; - const feature = ff.feature - const freshness = ff.freshness - let somethingChanged = false - for (const metatag of metatagsToApply) { - try { - if(!metatag.keys.some(key => feature.properties[key] === undefined)){ - // All keys are already defined, we probably already ran this one - continue - } + 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 + } + + if(metatag.isLazy){ + somethingChanged = true; + + metatag.applyMetaTagsOnFeature(feature, freshness) + + }else{ + + const newValueAdded = metatag.applyMetaTagsOnFeature(feature, freshness) /* Note that the expression: * `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)` @@ -70,35 +78,30 @@ export default class MetaTagging { * thus not running an update! */ somethingChanged = newValueAdded || somethingChanged - } catch (e) { - console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e, e.stack) } - } - - if(layerFuncs !== undefined){ - try { - layerFuncs(params, feature) - } catch (e) { - console.error(e) - } - somethingChanged = true - } - - if(somethingChanged){ - State.state?.allElements?.getEventSourceById(feature.properties.id)?.ping() + } catch (e) { + console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e, e.stack) } } + + 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; - if (calculatedTags === undefined) { - return undefined; - } - - const functions: ((params: ExtraFuncParams, feature: any) => void)[] = []; + private static createFunctionsForFeature(calculatedTags: [string, string][]): ((feature: any) => void)[] { + const functions: ((feature: any) => void)[] = []; for (const entry of calculatedTags) { const key = entry[0] const code = entry[1]; @@ -108,59 +111,72 @@ export default class MetaTagging { const func = new Function("feat", "return " + code + ";"); - try { - const f = (featuresPerLayer, feature: any) => { - try { - let result = func(feature); - if (result instanceof UIEventSource) { - result.addCallbackAndRunD(d => { - if (typeof d !== "string") { - // Make sure it is a string! - d = JSON.stringify(d); - } - feature.properties[key] = d; - }) - result = result.data - } + const f = (feature: any) => { + delete feature.properties[key] - if (result === undefined || result === "") { - return; - } - if (typeof result !== "string") { - // Make sure it is a string! - result = JSON.stringify(result); - } - feature.properties[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,e.stack) - MetaTagging.errorPrintCount++; - if (MetaTagging.errorPrintCount == MetaTagging.stopErrorOutputAt) { - console.error("Got ", MetaTagging.stopErrorOutputAt, " errors calculating this metatagging - stopping output now") + Object.defineProperty(feature.properties, key, { + configurable: true, + enumerable: false, // By setting this as not enumerable, the localTileSaver will _not_ calculate this + get: function () { + try { + // Lazyness for the win! + let result = func(feature); + + if (result === "") { + result === undefined + } + if (result !== undefined && typeof result !== "string") { + // Make sure it is a string! + result = JSON.stringify(result); + } + delete feature.properties[key] + feature.properties[key] = result; + return 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, e.stack) + MetaTagging.errorPrintCount++; + if (MetaTagging.errorPrintCount == MetaTagging.stopErrorOutputAt) { + console.error("Got ", MetaTagging.stopErrorOutputAt, " errors calculating this metatagging - stopping output now") + } } } - } + + }} ) - } - functions.push(f) - } catch (e) { - console.error("Could not create a dynamic function: ", e) } + + + functions.push(f) } + return functions; + } + + private static createRetaggingFunc(layer: LayerConfig): + ((params: ExtraFuncParams, feature: any) => void) { + + const calculatedTags: [string, string][] = layer.calculatedTags; + if (calculatedTags === undefined || calculatedTags.length === 0) { + return undefined; + } + return (params: ExtraFuncParams, feature) => { const tags = feature.properties if (tags === undefined) { return; } - ExtraFunction.FullPatchFeature(params, feature); try { + const functions = MetaTagging.createFunctionsForFeature(calculatedTags) + + + ExtraFunction.FullPatchFeature(params, feature); for (const f of functions) { - f(params, feature); + f(feature); } - State.state?.allElements?.getEventSourceById(feature.properties.id)?.ping(); + State.state?.allElements?.getEventSourceById(feature.properties.id)?.ping(); } catch (e) { - console.error("While calculating a tag value: ", e) + console.error("Invalid syntax in calculated tags or some other error: ", e) } } } diff --git a/Logic/Osm/OsmObject.ts b/Logic/Osm/OsmObject.ts index 2c88b01c50..fe4f48abcc 100644 --- a/Logic/Osm/OsmObject.ts +++ b/Logic/Osm/OsmObject.ts @@ -56,6 +56,19 @@ export abstract class OsmObject { OsmObject.objectCache.set(id, src); return src; } + + static async DownloadPropertiesOf(id: string): Promise { + const splitted = id.split("/"); + const type = splitted[0]; + const idN = Number(splitted[1]); + if (idN < 0) { + return undefined; + } + + const url = `${OsmObject.backendURL}api/0.6/${id}`; + const rawData = await Utils.downloadJson(url) + return rawData.elements[0].tags + } static async DownloadObjectAsync(id: string): Promise { const splitted = id.split("/"); @@ -65,7 +78,7 @@ export abstract class OsmObject { return undefined; } - const full = !id.startsWith("way") ? "" : "/full"; + const full = (id.startsWith("way")) ? "/full" : ""; const url = `${OsmObject.backendURL}api/0.6/${id}${full}`; const rawData = await Utils.downloadJson(url) // A full query might contain more then just the requested object (e.g. nodes that are part of a way, where we only want the way) diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index 2845223f8d..4cf5a8c471 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -65,13 +65,34 @@ export default class SimpleMetaTagger { private static surfaceArea = new SimpleMetaTagger( { keys: ["_surface", "_surface:ha"], - doc: "The surface area of the feature, in square meters and in hectare. Not set on points and ways" + doc: "The surface area of the feature, in square meters and in hectare. Not set on points and ways", + isLazy: true }, (feature => { - const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature); - feature.properties["_surface"] = "" + sqMeters; - feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10; - feature.area = sqMeters; + + Object.defineProperty(feature.properties, "_surface", { + enumerable: false, + configurable: true, + get: () => { + const sqMeters = ""+ GeoOperations.surfaceAreaInSqMeters(feature); + delete feature.properties["_surface"] + feature.properties["_surface"] = sqMeters; + return sqMeters + } + }) + + Object.defineProperty(feature.properties, "_surface:ha", { + enumerable: false, + configurable: true, + get: () => { + const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature); + const sqMetersHa = "" + Math.floor(sqMeters / 1000) / 10; + delete feature.properties["_surface:ha"] + feature.properties["_surface:ha"] = sqMetersHa; + return sqMetersHa + } + }) + return true; }) ); @@ -153,7 +174,7 @@ export default class SimpleMetaTagger { let centerPoint: any = GeoOperations.centerpoint(feature); const lat = centerPoint.geometry.coordinates[1]; const lon = centerPoint.geometry.coordinates[0]; - + SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, (countries: string[]) => { try { const oldCountry = feature.properties["_country"]; @@ -173,7 +194,8 @@ export default class SimpleMetaTagger { { keys: ["_isOpen", "_isOpen:description"], doc: "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')", - includesDates: true + includesDates: true, + isLazy: true }, (feature => { if (Utils.runningFromConsole) { @@ -181,64 +203,74 @@ export default class SimpleMetaTagger { // isOpen is irrelevant return false } + + Object.defineProperty(feature.properties, "_isOpen",{ + enumerable: false, + configurable: true, + get: () => { + delete feature.properties._isOpen + feature.properties._isOpen = "" + const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); + tagsSource.addCallbackAndRunD(tags => { + if (tags.opening_hours === undefined || tags._country === undefined) { + return; + } + try { + const [lon, lat] = GeoOperations.centerpointCoordinates(feature) + const oh = new opening_hours(tags["opening_hours"], { + lat: lat, + lon: lon, + address: { + country_code: tags._country.toLowerCase() + } + }, {tag_key: "opening_hours"}); + // AUtomatically triggered on the next change + const updateTags = () => { + const oldValueIsOpen = tags["_isOpen"]; + const oldNextChange = tags["_isOpen:nextTrigger"] ?? 0; - const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); - tagsSource.addCallbackAndRunD(tags => { - if (tags.opening_hours === undefined || tags._country === undefined) { - return; + if (oldNextChange > (new Date()).getTime() && + tags["_isOpen:oldvalue"] === tags["opening_hours"]) { + // Already calculated and should not yet be triggered + return false; + } + + tags["_isOpen"] = oh.getState() ? "yes" : "no"; + const comment = oh.getComment(); + if (comment) { + tags["_isOpen:description"] = comment; + } + + if (oldValueIsOpen !== tags._isOpen) { + tagsSource.ping(); + } + + const nextChange = oh.getNextChange(); + if (nextChange !== undefined) { + const timeout = nextChange.getTime() - (new Date()).getTime(); + tags["_isOpen:nextTrigger"] = nextChange.getTime(); + tags["_isOpen:oldvalue"] = tags.opening_hours + window.setTimeout( + () => { + console.log("Updating the _isOpen tag for ", tags.id, ", it's timer expired after", timeout); + updateTags(); + }, + timeout + ) + } + } + updateTags(); + return true; // Our job is done, lets unregister! + } catch (e) { + console.warn("Error while parsing opening hours of ", tags.id, e); + tags["_isOpen"] = "parse_error"; + } + + }) + return feature.properties["_isOpen"] } - try { - - const oh = new opening_hours(tags["opening_hours"], { - lat: tags._lat, - lon: tags._lon, - address: { - country_code: tags._country.toLowerCase() - } - }, {tag_key: "opening_hours"}); - // AUtomatically triggered on the next change - const updateTags = () => { - const oldValueIsOpen = tags["_isOpen"]; - const oldNextChange = tags["_isOpen:nextTrigger"] ?? 0; - - if (oldNextChange > (new Date()).getTime() && - tags["_isOpen:oldvalue"] === tags["opening_hours"]) { - // Already calculated and should not yet be triggered - return false; - } - - tags["_isOpen"] = oh.getState() ? "yes" : "no"; - const comment = oh.getComment(); - if (comment) { - tags["_isOpen:description"] = comment; - } - - if (oldValueIsOpen !== tags._isOpen) { - tagsSource.ping(); - } - - const nextChange = oh.getNextChange(); - if (nextChange !== undefined) { - const timeout = nextChange.getTime() - (new Date()).getTime(); - tags["_isOpen:nextTrigger"] = nextChange.getTime(); - tags["_isOpen:oldvalue"] = tags.opening_hours - window.setTimeout( - () => { - console.log("Updating the _isOpen tag for ", tags.id, ", it's timer expired after", timeout); - updateTags(); - }, - timeout - ) - } - } - updateTags(); - return true; - } catch (e) { - console.warn("Error while parsing opening hours of ", tags.id, e); - tags["_isOpen"] = "parse_error"; - } - }) + }) ) private static directionSimplified = new SimpleMetaTagger( @@ -306,20 +338,25 @@ export default class SimpleMetaTagger { SimpleMetaTagger.objectMetaInfo ]; + public static readonly lazyTags: string[] = [].concat(...SimpleMetaTagger.metatags.filter(tagger => tagger.isLazy) + .map(tagger => tagger.keys)); + public readonly keys: string[]; public readonly doc: string; + public readonly isLazy: boolean; public readonly includesDates: boolean public readonly applyMetaTagsOnFeature: (feature: any, freshness: Date) => boolean; - + /*** * 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 }, + constructor(docs: { keys: string[], doc: string, includesDates?: boolean, isLazy?: boolean }, f: ((feature: any, freshness: Date) => boolean)) { this.keys = docs.keys; this.doc = docs.doc; + this.isLazy = docs.isLazy this.applyMetaTagsOnFeature = f; this.includesDates = docs.includesDates ?? false; for (const key of docs.keys) { @@ -345,7 +382,8 @@ export default class SimpleMetaTagger { for (const metatag of SimpleMetaTagger.metatags) { subElements.push( new Title(metatag.keys.join(", "), 3), - metatag.doc + metatag.doc, + metatag.isLazy ? "This is a lazy metatag and is only calculated when needed" : "" ) } diff --git a/Logic/Tags/RegexTag.ts b/Logic/Tags/RegexTag.ts index fae2fd70ba..68840188b3 100644 --- a/Logic/Tags/RegexTag.ts +++ b/Logic/Tags/RegexTag.ts @@ -47,6 +47,11 @@ export class RegexTag extends TagsFilter { } matchesProperties(tags: any): boolean { + if(typeof this.key === "string"){ + const value = tags[this.key] ?? "" + return RegexTag.doesMatch(value, this.value) != this.invert; + } + for (const key in tags) { if (key === undefined) { continue; diff --git a/Logic/Tags/Tag.ts b/Logic/Tags/Tag.ts index 66bb993f93..f2539e1bc6 100644 --- a/Logic/Tags/Tag.ts +++ b/Logic/Tags/Tag.ts @@ -23,26 +23,14 @@ export class Tag extends TagsFilter { matchesProperties(properties: any): boolean { - for (const propertiesKey in properties) { - if (!properties.hasOwnProperty(propertiesKey)) { - continue - } - if (this.key === propertiesKey) { - const value = properties[propertiesKey]; - if (value === undefined) { - continue - } - return value === this.value; - } - } - // The tag was not found - - if (this.value === "") { + const foundValue = properties[this.key] + if (foundValue === undefined && (this.value === "" || this.value === undefined)) { + // The tag was not found // and it shouldn't be found! return true; } - return false; + return foundValue === this.value; } asOverpass(): string[] { diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 343f5e1b64..d289ca9cff 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -26,7 +26,8 @@ import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSou import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer"; import Minimap from "./Base/Minimap"; import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"; -import WikipediaBox from "./Wikipedia/WikipediaBox"; +import WikipediaBox from "./WikipediaBox"; +import SimpleMetaTagger from "../Logic/SimpleMetaTagger"; export interface SpecialVisualization { funcName: string, @@ -45,14 +46,26 @@ export default class SpecialVisualizations { docs: "Prints all key-value pairs of the object - used for debugging", args: [], constr: ((state: State, tags: UIEventSource) => { + const calculatedTags = [].concat( + SimpleMetaTagger.lazyTags, + ... state.layoutToUse.layers.map(l => l.calculatedTags?.map(c => c[0]) ?? [])) return new VariableUiElement(tags.map(tags => { const parts = []; for (const key in tags) { if (!tags.hasOwnProperty(key)) { - continue; + continue } parts.push([key, tags[key] ?? "undefined"]); } + + for(const key of calculatedTags){ + const value = tags[key] + if(value === undefined){ + continue + } + parts.push([ ""+key+"", value ]) + } + return new Table( ["key", "value"], parts diff --git a/assets/layers/cycleways_and_roads/cycleways_and_roads.json b/assets/layers/cycleways_and_roads/cycleways_and_roads.json index 2b10d7ebc8..5e0078ee67 100644 --- a/assets/layers/cycleways_and_roads/cycleways_and_roads.json +++ b/assets/layers/cycleways_and_roads/cycleways_and_roads.json @@ -27,9 +27,6 @@ ] } }, - "calculatedTags": [ - "_comfort_score=feat.score('https://raw.githubusercontent.com/pietervdvn/AspectedRouting/master/Examples/bicycle/aspects/bicycle.comfort.json')" - ], "title": { "render": { "en": "Cycleways", diff --git a/assets/themes/speelplekken/speelplekken.json b/assets/themes/speelplekken/speelplekken.json index b0caffcc9c..4b4449c6fe 100644 --- a/assets/themes/speelplekken/speelplekken.json +++ b/assets/themes/speelplekken/speelplekken.json @@ -152,7 +152,7 @@ ] }, "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson", - "geoJsonZoomLevel": 11, + "geoJsonZoomLevel": 14, "isOsmCache": true }, "title": { diff --git a/assets/themes/uk_addresses/uk_addresses.json b/assets/themes/uk_addresses/uk_addresses.json index c9a4a237b9..2114beac28 100644 --- a/assets/themes/uk_addresses/uk_addresses.json +++ b/assets/themes/uk_addresses/uk_addresses.json @@ -80,8 +80,7 @@ { "id": "uk_addresses_import_button", "render": "{import_button(ref:inspireid=$inspireid, Add this address, ./assets/themes/uk_addresses/housenumber_add.svg)}" - }, - "all_tags" + } ], "calculatedTags": [ "_embedding_object=feat.overlapWith('addresses')[0]?.feat?.properties ?? null", @@ -122,16 +121,10 @@ } }, "calculatedTags": [ - "_closest_3_street_names=feat.properties['addr:street'] === undefined ? feat.closestn('named_streets',3, 'name').map(f => ({name: f.feat.properties.name, distance: Math.round(1000*f.distance), id: f.id})) : []", - "_closest_street:0:name=JSON.parse(feat.properties._closest_3_street_names)[0]?.name", - "_closest_street:1:name=JSON.parse(feat.properties._closest_3_street_names)[1]?.name", - "_closest_street:2:name=JSON.parse(feat.properties._closest_3_street_names)[2]?.name", - "_closest_street:0:distance=JSON.parse(feat.properties._closest_3_street_names)[0]?.distance", - "_closest_street:1:distance=JSON.parse(feat.properties._closest_3_street_names)[1]?.distance", - "_closest_street:2:distance=JSON.parse(feat.properties._closest_3_street_names)[2]?.distance", - "_closest_street:0:id=JSON.parse(feat.properties._closest_3_street_names)[0]?.id", - "_closest_street:1:id=JSON.parse(feat.properties._closest_3_street_names)[1]?.id", - "_closest_street:2:id=JSON.parse(feat.properties._closest_3_street_names)[2]?.id" + "_closest_3_street_names=feat.closestn('named_streets',3, 'name').map(f => f.feat.properties.name)", + "_closest_street:0:name=JSON.parse(feat.properties._closest_3_street_names)[0]", + "_closest_street:1:name=JSON.parse(feat.properties._closest_3_street_names)[1]", + "_closest_street:2:name=JSON.parse(feat.properties._closest_3_street_names)[2]" ], "title": { "render": { @@ -157,7 +150,8 @@ "en": "What is the number of this house?" }, "freeform": { - "key": "addr:housenumber" + "key": "addr:housenumber", + "addExtraTags": "nohousenumber=" }, "mappings": [ { @@ -186,17 +180,17 @@ "mappings": [ { "if": "addr:street:={_closest_street:0:name}", - "then": "Located in {_closest_street:0:name} (~{_closest_street:0:distance}m away)", + "then": "Located in {_closest_street:0:name}", "hideInAnswer": "_closest_street:0:name=" }, { "if": "addr:street:={_closest_street:1:name}", - "then": "Located in {_closest_street:1:name} (~{_closest_street:1:distance}m away)", + "then": "Located in {_closest_street:1:name}", "hideInAnswer": "_closest_street:1:name=" }, { "if": "addr:street:={_closest_street:2:name}", - "then": "Located in {_closest_street:2:name} (~{_closest_street:2:distance}m away)", + "then": "Located in {_closest_street:2:name}", "hideInAnswer": "_closest_street:2:name=" } ], @@ -254,7 +248,6 @@ }, { "id": "named_streets", - "name": "Named streets", "minzoom": 18, "source": { "osmTags": { @@ -264,16 +257,11 @@ ] } }, - "title": { - "render": { - "en": "{name}" - } - }, "color": { "render": "#ccc" }, "width": { - "render": "3" + "render": "0" } } ], diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index a85d3c58dd..d3ae8b9c65 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -9,6 +9,7 @@ import {Tag} from "../Logic/Tags/Tag"; import {And} from "../Logic/Tags/And"; import {TagUtils} from "../Logic/Tags/TagUtils"; import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; +import {RegexTag} from "../Logic/Tags/RegexTag"; Utils.runningFromConsole = true; @@ -173,7 +174,6 @@ export default class TagSpec extends T { equal(undefined, tr.GetRenderValue({"foo": "bar"})); })], - [ "Empty match test", () => { @@ -214,7 +214,8 @@ export default class TagSpec extends T { const overpassOrInor = TagUtils.Tag(orInOr).asOverpass() equal(3, overpassOrInor.length) } - ], [ + ], + [ "Merge touching opening hours", () => { const oh1: OpeningHour = { @@ -239,7 +240,8 @@ export default class TagSpec extends T { equal(r.endHour, 12) } - ], [ + ], + [ "Merge overlapping opening hours", () => { const oh1: OpeningHour = { @@ -394,7 +396,8 @@ export default class TagSpec extends T { ])); equal(rules, "Tu 23:00-00:00"); - }], ["JOIN OH with overflowed hours", () => { + }], + ["JOIN OH with overflowed hours", () => { const rules = OH.ToString( OH.MergeTimes([ @@ -483,7 +486,41 @@ export default class TagSpec extends T { const tagRendering = new TagRenderingConfig(config, null, "test"); equal(true, tagRendering.IsKnown({bottle: "yes"})) equal(false, tagRendering.IsKnown({})) - }]]); + }], + [ + "Tag matches a lazy property", + () => { + const properties = {} + const key = "_key" + Object.defineProperty(properties, key, { + configurable: true, + get: function () { + delete properties[key] + properties[key] = "yes" + return "yes" + } + }) + const filter = new Tag("_key", "yes") + T.isTrue(filter.matchesProperties(properties), "Lazy value not matched") + } + ], + [ + "RegextTag matches a lazy property", + () => { + const properties = {} + const key = "_key" + Object.defineProperty(properties, key, { + configurable: true, + get: function () { + delete properties[key] + properties[key] = "yes" + return "yes" + } + }) + const filter = TagUtils.Tag("_key~*") + T.isTrue(filter.matchesProperties(properties), "Lazy value not matched") + } + ]]); } }