diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts
index 34d0a8c..f0f88c3 100644
--- a/Customizations/JSON/LayerConfigJson.ts
+++ b/Customizations/JSON/LayerConfigJson.ts
@@ -130,8 +130,11 @@ export interface LayerConfigJson {
*/
rotation?: string | TagRenderingConfigJson;
/**
- * A HTML-fragment that is shown at the center of the icon, for example:
+ * A HTML-fragment that is shown below the icon, for example:
*
{name}
+ *
+ * If the icon is undefined, then the label is shown in the center of the feature.
+ * Note that, if the wayhandling hides the icon then no label is shown as well.
*/
label?: string | TagRenderingConfigJson ;
diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts
index 05661b7..d5d237a 100644
--- a/Customizations/JSON/TagRenderingConfig.ts
+++ b/Customizations/JSON/TagRenderingConfig.ts
@@ -77,7 +77,8 @@ export default class TagRenderingConfig {
throw `Freeform.key is undefined or the empty string - this is not allowed; either fill out something or remove the freeform block alltogether. Error in ${context}`
}
if (ValidatedTextField.AllTypes[this.freeform.type] === undefined) {
- throw `Freeform.key ${this.freeform.key} is an invalid type`
+ const knownKeys = ValidatedTextField.tpList.map(tp => tp.name).join(", ");
+ throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}`
}
if (this.freeform.addExtraTags) {
const usedKeys = new And(this.freeform.addExtraTags).usedKeys();
@@ -204,7 +205,7 @@ export default class TagRenderingConfig {
return true;
}
if (this.multiAnswer) {
- for (const m of this.mappings) {
+ for (const m of this.mappings ?? []) {
if (TagUtils.MatchesMultiAnswer(m.if, tags)) {
return true;
}
diff --git a/Docs/CalculatedTags.md b/Docs/CalculatedTags.md
index d531ca3..3393737 100644
--- a/Docs/CalculatedTags.md
+++ b/Docs/CalculatedTags.md
@@ -53,6 +53,7 @@ The above code will be executed for every feature in the layer. The feature is a
* distanceTo
* overlapWith
* closest
+* memberships
### distanceTo
@@ -71,4 +72,8 @@ Gives a list of features from the specified layer which this feature overlaps wi
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.
-* list of features
\ No newline at end of file
+* list of features
+
+### memberships
+
+Gives a list of {role: string, relation: Relation}-objects, containing all the relations that this feature is part of. For example: \`\_part\_of\_walking\_routes=feat.memberships().map(r => r.relation.tags.name).join(';')\`
\ No newline at end of file
diff --git a/Logic/ExtraFunction.ts b/Logic/ExtraFunction.ts
index 1689e67..e7aae02 100644
--- a/Logic/ExtraFunction.ts
+++ b/Logic/ExtraFunction.ts
@@ -1,6 +1,7 @@
import {GeoOperations} from "./GeoOperations";
import {UIElement} from "../UI/UIElement";
import Combine from "../UI/Base/Combine";
+import State from "../State";
export class ExtraFunction {
@@ -35,7 +36,7 @@ The above code will be executed for every feature in the layer. The feature is a
Some advanced functions are available on feat as well:
`
- private static OverlapFunc = new ExtraFunction(
+ private static readonly OverlapFunc = new ExtraFunction(
"overlapWith",
"Gives a list of features from the specified layer which this feature overlaps with, the amount of overlap in m². The returned value is { feat: GeoJSONFeature, overlap: number}",
["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"],
@@ -56,26 +57,26 @@ Some advanced functions are available on feat as well:
}
}
)
- private static DistanceToFunc = new ExtraFunction(
+ private static readonly DistanceToFunc = new ExtraFunction(
"distanceTo",
"Calculates the distance between the feature and a specified point",
["longitude", "latitude"],
(featuresPerLayer, feature) => {
return (arg0, lat) => {
- if(typeof arg0 === "number"){
+ if (typeof arg0 === "number") {
const lon = arg0
// Feature._lon and ._lat is conveniently place by one of the other metatags
return GeoOperations.distanceBetween([lon, lat], [feature._lon, feature._lat]);
- }else{
+ } else {
// arg0 is probably a feature
- return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0),[feature._lon, feature._lat])
+ return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0), [feature._lon, feature._lat])
}
-
+
}
}
)
- private static ClosestObjectFunc = new ExtraFunction(
+ private static readonly ClosestObjectFunc = new ExtraFunction(
"closest",
"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.",
["list of features"],
@@ -87,7 +88,7 @@ Some advanced functions are available on feat as well:
let closestFeature = undefined;
let closestDistance = undefined;
for (const otherFeature of features) {
- if(otherFeature == feature){
+ if (otherFeature == feature) {
continue; // We ignore self
}
let distance = undefined;
@@ -99,10 +100,10 @@ Some advanced functions are available on feat as well:
[feature._lon, feature._lat]
)
}
- if(distance === undefined){
+ if (distance === undefined) {
throw "Undefined distance!"
}
- if(closestFeature === undefined || distance < closestDistance){
+ if (closestFeature === undefined || distance < closestDistance) {
closestFeature = otherFeature
closestDistance = distance;
}
@@ -113,7 +114,19 @@ Some advanced functions are available on feat as well:
)
- private static readonly allFuncs: ExtraFunction[] = [ExtraFunction.DistanceToFunc, ExtraFunction.OverlapFunc, ExtraFunction.ClosestObjectFunc];
+ private static readonly Memberships = new ExtraFunction(
+ "memberships",
+ "Gives a list of {role: string, relation: Relation}-objects, containing all the relations that this feature is part of. \n\nFor example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`",
+ [],
+ (featuresPerLayer, feature) => {
+ return () => {
+ return State.state.knownRelations.data?.get(feature.id) ?? [];
+ }
+
+ }
+ )
+
+ private static readonly allFuncs: ExtraFunction[] = [ExtraFunction.DistanceToFunc, ExtraFunction.OverlapFunc, ExtraFunction.ClosestObjectFunc, ExtraFunction.Memberships];
private readonly _name: string;
private readonly _args: string[];
private readonly _doc: string;
diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts
index f6cca66..4b79cfb 100644
--- a/Logic/FeatureSource/FeaturePipeline.ts
+++ b/Logic/FeatureSource/FeaturePipeline.ts
@@ -12,6 +12,7 @@ import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
import Loc from "../../Models/Loc";
import GeoJsonSource from "./GeoJsonSource";
import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource";
+import RegisteringFeatureSource from "./RegisteringFeatureSource";
export default class FeaturePipeline implements FeatureSource {
@@ -24,33 +25,38 @@ export default class FeaturePipeline implements FeatureSource {
locationControl: UIEventSource) {
const amendedOverpassSource =
- new RememberingSource(new FeatureDuplicatorPerLayer(flayers,
- new LocalStorageSaver(updater, layout))
- );
+ new RememberingSource(
+ new LocalStorageSaver(
+ new MetaTaggingFeatureSource( // first we metatag, then we save to get the metatags into storage too
+ new RegisteringFeatureSource(
+ new FeatureDuplicatorPerLayer(flayers,
+ updater)
+ )), layout));
const geojsonSources: GeoJsonSource [] = []
for (const flayer of flayers.data) {
const sourceUrl = flayer.layerDef.source.geojsonSource
if (sourceUrl !== undefined) {
- geojsonSources.push(
- new GeoJsonSource(flayer.layerDef.id, sourceUrl))
+ geojsonSources.push(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers,
+ new GeoJsonSource(flayer.layerDef.id, sourceUrl))))
}
}
const amendedLocalStorageSource =
- new RememberingSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout))
- );
+ new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout))
+ ));
- newPoints = new FeatureDuplicatorPerLayer(flayers, newPoints);
+ newPoints = new MetaTaggingFeatureSource(new FeatureDuplicatorPerLayer(flayers,
+ new RegisteringFeatureSource(newPoints)));
const merged =
- new MetaTaggingFeatureSource(
- new FeatureSourceMerger([
- amendedOverpassSource,
- amendedLocalStorageSource,
- newPoints,
- ...geojsonSources
- ]));
+
+ new FeatureSourceMerger([
+ amendedOverpassSource,
+ amendedLocalStorageSource,
+ newPoints,
+ ...geojsonSources
+ ]);
const source =
new WayHandlingApplyingFeatureSource(flayers,
diff --git a/Logic/FeatureSource/MetaTaggingFeatureSource.ts b/Logic/FeatureSource/MetaTaggingFeatureSource.ts
index 5b3ef12..83f91d4 100644
--- a/Logic/FeatureSource/MetaTaggingFeatureSource.ts
+++ b/Logic/FeatureSource/MetaTaggingFeatureSource.ts
@@ -16,10 +16,6 @@ export default class MetaTaggingFeatureSource implements FeatureSource {
featuresFreshness.forEach(featureFresh => {
const feature = featureFresh.feature;
- if(!State.state.allElements.has(feature.properties.id)){
- State.state.allElements.addOrGetElement(feature)
- }
-
if (Hash.hash.data === feature.properties.id) {
State.state.selectedElement.setData(feature);
}
diff --git a/Logic/FeatureSource/RegisteringFeatureSource.ts b/Logic/FeatureSource/RegisteringFeatureSource.ts
new file mode 100644
index 0000000..9e5d19d
--- /dev/null
+++ b/Logic/FeatureSource/RegisteringFeatureSource.ts
@@ -0,0 +1,19 @@
+import FeatureSource from "./FeatureSource";
+import {UIEventSource} from "../UIEventSource";
+import State from "../../State";
+
+export default class RegisteringFeatureSource implements FeatureSource {
+ features: UIEventSource<{ feature: any; freshness: Date }[]>;
+
+ constructor(source: FeatureSource) {
+ this.features = source.features;
+ this.features.addCallbackAndRun(features => {
+ for (const feature of features ?? []) {
+ if (!State.state.allElements.has(feature.feature.properties.id)) {
+ State.state.allElements.addOrGetElement(feature.feature)
+ }
+ }
+ })
+ }
+
+}
\ No newline at end of file
diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts
index a687dee..6697fb4 100644
--- a/Logic/GeoOperations.ts
+++ b/Logic/GeoOperations.ts
@@ -1,4 +1,4 @@
-import * as turf from 'turf'
+import * as turf from '@turf/turf'
export class GeoOperations {
@@ -118,6 +118,9 @@ export class GeoOperations {
return inside;
};
+ static lengthInMeters(feature: any) {
+ return turf.length(feature) * 1000
+ }
}
diff --git a/Logic/Osm/ExtractRelations.ts b/Logic/Osm/ExtractRelations.ts
new file mode 100644
index 0000000..4c64d63
--- /dev/null
+++ b/Logic/Osm/ExtractRelations.ts
@@ -0,0 +1,58 @@
+import State from "../../State";
+
+export interface Relation {
+ id: number,
+ type: "relation"
+ members: {
+ type: ("way" | "node" | "relation"),
+ ref: number,
+ role: string
+ }[],
+ tags: any,
+ // Alias for tags; tags == properties
+ properties: any
+}
+
+export default class ExtractRelations {
+
+ public static RegisterRelations(overpassJson: any) : void{
+ const memberships = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(overpassJson))
+ console.log("Assigned memberships: ", memberships)
+ State.state.knownRelations.setData(memberships)
+ }
+
+ private static GetRelationElements(overpassJson: any): Relation[] {
+ const relations = overpassJson.elements.filter(element => element.type === "relation")
+ for (const relation of relations) {
+ relation.properties = relation.tags
+ }
+ return relations
+ }
+
+ /**
+ * Build a mapping of {memberId --> {role in relation, id of relation} }
+ * @param relations
+ * @constructor
+ */
+ private static BuildMembershipTable(relations: Relation[]): Map {
+ const memberships = new Map()
+
+ for (const relation of relations) {
+ for (const member of relation.members) {
+
+ const role = {
+ role: member.role,
+ relation: relation
+ }
+ const key = member.type + "/" + member.ref
+ if (!memberships.has(key)) {
+ memberships.set(key, [])
+ }
+ memberships.get(key).push(role)
+ }
+ }
+
+ return memberships
+ }
+
+}
\ No newline at end of file
diff --git a/Logic/Osm/Overpass.ts b/Logic/Osm/Overpass.ts
index 401d96b..b62749e 100644
--- a/Logic/Osm/Overpass.ts
+++ b/Logic/Osm/Overpass.ts
@@ -1,7 +1,8 @@
import * as $ from "jquery"
import * as OsmToGeoJson from "osmtogeojson";
import Bounds from "../../Models/Bounds";
-import {TagsFilter} from "../TagsFilter";
+import {TagsFilter} from "../Tags/TagsFilter";
+import ExtractRelations from "./ExtractRelations";
/**
* Interfaces overpass to get all the latest data
@@ -38,9 +39,9 @@ export class Overpass {
return;
}
+ ExtractRelations.RegisterRelations(json)
// @ts-ignore
const geojson = OsmToGeoJson.default(json);
- console.log("Received geojson", geojson)
const osmTime = new Date(json.osm3s.timestamp_osm_base);
continuation(geojson, osmTime);
diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts
index 30bf191..7a059a2 100644
--- a/Logic/SimpleMetaTagger.ts
+++ b/Logic/SimpleMetaTagger.ts
@@ -52,6 +52,18 @@ export default class SimpleMetaTagger {
feature.area = sqMeters;
})
);
+
+ private static lngth = new SimpleMetaTagger(
+ ["_length", "_length:km"], "The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter",
+ (feature => {
+ const l = GeoOperations.lengthInMeters(feature)
+ feature.properties["_length"] = "" + l
+ const km = Math.floor(l / 1000)
+ const kmRest = Math.round((l - km * 1000) / 100)
+ feature.properties["_length:km"] = "" + km+ "." + kmRest
+ })
+ )
+
private static country = new SimpleMetaTagger(
["_country"], "The country code of the property (with latlon2country)",
feature => {
@@ -294,6 +306,7 @@ export default class SimpleMetaTagger {
public static metatags = [
SimpleMetaTagger.latlon,
SimpleMetaTagger.surfaceArea,
+ SimpleMetaTagger.lngth,
SimpleMetaTagger.country,
SimpleMetaTagger.isOpen,
SimpleMetaTagger.carriageWayWidth,
diff --git a/Models/Constants.ts b/Models/Constants.ts
index efc8ffe..2dfe67a 100644
--- a/Models/Constants.ts
+++ b/Models/Constants.ts
@@ -2,7 +2,7 @@ import { Utils } from "../Utils";
export default class Constants {
- public static vNumber = "0.6.8c";
+ public static vNumber = "0.6.9";
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {
diff --git a/State.ts b/State.ts
index 91912f0..7fd9997 100644
--- a/State.ts
+++ b/State.ts
@@ -17,6 +17,7 @@ import UpdateFromOverpass from "./Logic/Actors/UpdateFromOverpass";
import LayerConfig from "./Customizations/JSON/LayerConfig";
import TitleHandler from "./Logic/Actors/TitleHandler";
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
+import {Relation} from "./Logic/Osm/ExtractRelations";
/**
* Contains the global state: a bunch of UI-event sources
@@ -76,6 +77,11 @@ export default class State {
*/
public readonly selectedElement = new UIEventSource(undefined, "Selected element")
+ /**
+ * Keeps track of relations: which way is part of which other way?
+ * Set by the overpass-updater; used in the metatagging
+ */
+ public readonly knownRelations = new UIEventSource