diff --git a/Logic/ExtraFunction.ts b/Logic/ExtraFunction.ts index b3b10ac2a..4deb3e01f 100644 --- a/Logic/ExtraFunction.ts +++ b/Logic/ExtraFunction.ts @@ -45,7 +45,11 @@ export class ExtraFunction { private static readonly OverlapFunc = new ExtraFunction( { name: "overlapWith", - doc: "Gives a list of features from the specified layer which this feature (partly) overlaps with. If the current feature is a point, all features that embed the point are given. The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point", + doc: "Gives a list of features from the specified layer which this feature (partly) overlaps with. " + + "If the current feature is a point, all features that embed the point are given. " + + "The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.\n" + + "\n" + + "For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`", args: ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"] }, (params, feat) => { @@ -92,59 +96,32 @@ export class ExtraFunction { } } ) - private static readonly ClosestObjectFunc = new ExtraFunction( { name: "closest", - doc: "Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered.", + doc: "Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. Returns a single geojson feature or undefined if nothing is found (or not yet laoded)", args: ["list of features"] }, (params, feature) => { - return (features) => { - if (typeof features === "string") { - const name = features - features = params.featuresPerLayer.get(features) - if (features === undefined) { - var keys = Utils.NoNull(Array.from(params.featuresPerLayer.keys())); - if (keys.length > 0) { - throw `No features defined for ${name}. Defined layers are ${keys.join(", ")}`; - } else { - // This is the first pass over an external dataset - // Other data probably still has to load! - return undefined; - } - - } - } - - let closestFeature = undefined; - let closestDistance = undefined; - for (const otherFeature of features) { - 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) { - throw "Undefined distance!" - } - if (closestFeature === undefined || distance < closestDistance) { - closestFeature = otherFeature - closestDistance = distance; - } - } - return closestFeature; - } + return (features) => ExtraFunction.GetClosestNFeatures(params, feature, features)[0].feat } ) + private static readonly ClosestNObjectFunc = new ExtraFunction( + { + name: "closestn", + doc: "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. " + + "Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet laoded)\n\n" + + "If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)", + args: ["list of features", "amount of features", "unique tag key (optional)"] + }, + (params, feature) => { + return (features, amount, uniqueTag) => ExtraFunction.GetClosestNFeatures(params, feature, features, { + maxFeatures: Number(amount), + uniqueTag: uniqueTag + }) + } + ) private static readonly Memberships = new ExtraFunction( { @@ -158,7 +135,6 @@ export class ExtraFunction { return () => params.relations ?? []; } ) - private static readonly AspectedRouting = new ExtraFunction( { name: "score", @@ -178,11 +154,11 @@ export class ExtraFunction { } } ) - private static readonly allFuncs: ExtraFunction[] = [ ExtraFunction.DistanceToFunc, ExtraFunction.OverlapFunc, ExtraFunction.ClosestObjectFunc, + ExtraFunction.ClosestNObjectFunc, ExtraFunction.Memberships, ExtraFunction.AspectedRouting ]; @@ -221,6 +197,121 @@ export class ExtraFunction { ]); } + /** + * Gets the closes N features, sorted by ascending distance + */ + private static GetClosestNFeatures(params, feature, features, options?: { maxFeatures?: number, uniqueTag?: string | undefined }): { feat: any, distance: number }[] { + const maxFeatures = options?.maxFeatures ?? 1 + const uniqueTag : string | undefined = options?.uniqueTag + if (typeof features === "string") { + const name = features + features = params.featuresPerLayer.get(features) + if (features === undefined) { + var keys = Utils.NoNull(Array.from(params.featuresPerLayer.keys())); + if (keys.length > 0) { + throw `No features defined for ${name}. Defined layers are ${keys.join(", ")}`; + } else { + // This is the first pass over an external dataset + // Other data probably still has to load! + return undefined; + } + + } + } + + let closestFeatures: { feat: any, distance: number }[] = []; + for (const otherFeature of features) { + 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) { + throw "Undefined distance!" + } + + if (closestFeatures.length === 0) { + closestFeatures.push({ + feat: otherFeature, + distance: distance + }) + continue; + } + + if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { + // The last feature of the list (and thus the furthest away is still closer + // No use for checking, as we already have plenty of features! + continue + } + + let targetIndex = closestFeatures.length + for (let i = 0; i < closestFeatures.length; i++) { + const closestFeature = closestFeatures[i]; + + if (uniqueTag !== undefined) { + const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined && + closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag] + if (uniqueTagsMatch) { + targetIndex = -1 + if (closestFeature.distance > distance) { + // This is a very special situation: + // We want to see the tag `uniquetag=some_value` only once in the entire list (e.g. to prevent road segements of identical names to fill up the list of 'names of nearby roads') + // AT this point, we have found a closer segment with the same, identical tag + // so we replace directly + closestFeatures[i] = {feat: otherFeature, distance: distance} + } + break; + } + } + + if (closestFeature.distance > distance) { + targetIndex = i + + if (uniqueTag !== undefined) { + const uniqueValue = otherFeature.properties[uniqueTag] + // We might still have some other values later one with the same uniquetag that have to be cleaned + for (let j = i; j < closestFeatures.length; j++) { + if(closestFeatures[j].feat.properties[uniqueTag] === uniqueValue){ + closestFeatures.splice(j, 1) + } + } + } + break; + } + } + + if (targetIndex == -1) { + continue; // value is already swapped by the unique tag + } + + if (targetIndex < maxFeatures) { + // insert and drop one + closestFeatures.splice(targetIndex, 0, { + feat: otherFeature, + distance: distance + }) + if (closestFeatures.length >= maxFeatures) { + closestFeatures.splice(maxFeatures, 1) + } + } else { + // Overwrite the last element + closestFeatures[targetIndex] = { + feat: otherFeature, + distance: distance + } + + } + } + return closestFeatures; + } + public PatchFeature(featuresPerLayer: Map, relations: { role: string, relation: Relation }[], feature: any) { feature[this._name] = this._f({featuresPerLayer: featuresPerLayer, relations: relations}, feature) }