From 40bf33e8874c50b0d921ebad3c78cbc22cb52a00 Mon Sep 17 00:00:00 2001 From: Michael Barry Date: Sat, 2 Dec 2023 15:34:35 -0500 Subject: [PATCH] Access OSM metadata in yaml profiles (#739) --- .../planetiler/reader/SimpleFeature.java | 98 +++++++++++++++---- planetiler-custommap/README.md | 5 + .../planetiler/custommap/Contexts.java | 26 ++++- .../custommap/ConfiguredFeatureTest.java | 19 +++- 4 files changed, 122 insertions(+), 26 deletions(-) diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/SimpleFeature.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/SimpleFeature.java index 5f117267..675a8549 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/SimpleFeature.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/SimpleFeature.java @@ -1,8 +1,10 @@ package com.onthegomap.planetiler.reader; import com.onthegomap.planetiler.geo.GeoUtils; +import com.onthegomap.planetiler.reader.osm.OsmElement; import com.onthegomap.planetiler.reader.osm.OsmReader; import com.onthegomap.planetiler.reader.osm.OsmRelationInfo; +import com.onthegomap.planetiler.reader.osm.OsmSourceFeature; import java.util.List; import java.util.Map; import java.util.Objects; @@ -76,29 +78,87 @@ public class SimpleFeature extends SourceFeature { return new SimpleFeature(latLonGeometry, null, tags, null, null, idGenerator.incrementAndGet(), null); } + private static class SimpleOsmFeature extends SimpleFeature implements OsmSourceFeature { + + private final String area; + private final OsmElement.Info info; + + private SimpleOsmFeature(Geometry latLonGeometry, Geometry worldGeometry, Map tags, String source, + String sourceLayer, long id, List> relations, OsmElement.Info info) { + super(latLonGeometry, worldGeometry, tags, source, sourceLayer, id, relations); + this.area = (String) tags.get("area"); + this.info = info; + } + + @Override + public boolean canBePolygon() { + return latLonGeometry() instanceof Polygonal || (latLonGeometry() instanceof LineString line && + OsmReader.canBePolygon(line.isClosed(), area, latLonGeometry().getNumPoints())); + } + + @Override + public boolean canBeLine() { + return latLonGeometry() instanceof MultiLineString || (latLonGeometry() instanceof LineString line && + OsmReader.canBeLine(line.isClosed(), area, latLonGeometry().getNumPoints())); + } + + @Override + protected Geometry computePolygon() { + var geom = worldGeometry(); + return geom instanceof LineString line ? GeoUtils.JTS_FACTORY.createPolygon(line.getCoordinates()) : geom; + } + + + @Override + public OsmElement originalElement() { + return new OsmElement() { + @Override + public long id() { + return SimpleOsmFeature.this.id(); + } + + @Override + public Info info() { + return info; + } + + @Override + public int cost() { + return 1; + } + + @Override + public Map tags() { + return tags(); + } + }; + } + + @Override + public boolean equals(Object o) { + return this == o || (o instanceof SimpleOsmFeature other && super.equals(other) && + Objects.equals(area, other.area) && Objects.equals(info, other.info)); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (area != null ? area.hashCode() : 0); + result = 31 * result + (info != null ? info.hashCode() : 0); + return result; + } + } + /** Returns a new feature with OSM relation info. Useful for setting up inputs for OSM unit tests. */ public static SimpleFeature createFakeOsmFeature(Geometry latLonGeometry, Map tags, String source, String sourceLayer, long id, List> relations) { - String area = (String) tags.get("area"); - return new SimpleFeature(latLonGeometry, null, tags, source, sourceLayer, id, relations) { - @Override - public boolean canBePolygon() { - return latLonGeometry instanceof Polygonal || (latLonGeometry instanceof LineString line && - OsmReader.canBePolygon(line.isClosed(), area, latLonGeometry.getNumPoints())); - } + return createFakeOsmFeature(latLonGeometry, tags, source, sourceLayer, id, relations, null); + } - @Override - public boolean canBeLine() { - return latLonGeometry instanceof MultiLineString || (latLonGeometry instanceof LineString line && - OsmReader.canBeLine(line.isClosed(), area, latLonGeometry.getNumPoints())); - } - - @Override - protected Geometry computePolygon() { - var geom = worldGeometry(); - return geom instanceof LineString line ? GeoUtils.JTS_FACTORY.createPolygon(line.getCoordinates()) : geom; - } - }; + /** Returns a new feature with OSM relation info and metadata. Useful for setting up inputs for OSM unit tests. */ + public static SimpleFeature createFakeOsmFeature(Geometry latLonGeometry, Map tags, String source, + String sourceLayer, long id, List> relations, OsmElement.Info info) { + return new SimpleOsmFeature(latLonGeometry, null, tags, source, sourceLayer, id, relations, info); } @Override diff --git a/planetiler-custommap/README.md b/planetiler-custommap/README.md index 81624a41..539140d8 100644 --- a/planetiler-custommap/README.md +++ b/planetiler-custommap/README.md @@ -485,6 +485,11 @@ nested, so each child context can also access the variables from its parent. >> - `feature.id` - numeric ID of the input feature >> - `feature.source` - string source ID this feature came from >> - `feature.source_layer` - optional layer within the source the feature came from +>> - `feature.osm_changeset` - optional OSM changeset ID for this feature +>> - `feature.osm_version` - optional OSM element version for this feature +>> - `feature.osm_timestamp` - optional OSM last modified timestamp for this feature +>> - `feature.osm_user_id` - optional ID of the OSM user that last modified this feature +>> - `feature.osm_user_name` - optional name of the OSM user that last modified this feature >> >>> ##### post-match context >>> diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/Contexts.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/Contexts.java index 1ad9a409..bd90ac8b 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/Contexts.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/Contexts.java @@ -13,6 +13,8 @@ import com.onthegomap.planetiler.expression.DataType; import com.onthegomap.planetiler.reader.SourceFeature; import com.onthegomap.planetiler.reader.WithGeometryType; import com.onthegomap.planetiler.reader.WithTags; +import com.onthegomap.planetiler.reader.osm.OsmElement; +import com.onthegomap.planetiler.reader.osm.OsmSourceFeature; import com.onthegomap.planetiler.util.Try; import java.util.HashMap; import java.util.LinkedHashMap; @@ -340,6 +342,11 @@ public class Contexts { private static final String FEATURE_ID = "feature.id"; private static final String FEATURE_SOURCE = "feature.source"; private static final String FEATURE_SOURCE_LAYER = "feature.source_layer"; + private static final String FEATURE_OSM_CHANGESET = "feature.osm_changeset"; + private static final String FEATURE_OSM_VERSION = "feature.osm_version"; + private static final String FEATURE_OSM_TIMESTAMP = "feature.osm_timestamp"; + private static final String FEATURE_OSM_USER_ID = "feature.osm_user_id"; + private static final String FEATURE_OSM_USER_NAME = "feature.osm_user_name"; public static ScriptEnvironment description(Root root) { return root.description() @@ -348,7 +355,12 @@ public class Contexts { Decls.newVar(FEATURE_TAGS, Decls.newMapType(Decls.String, Decls.Any)), Decls.newVar(FEATURE_ID, Decls.Int), Decls.newVar(FEATURE_SOURCE, Decls.String), - Decls.newVar(FEATURE_SOURCE_LAYER, Decls.String) + Decls.newVar(FEATURE_SOURCE_LAYER, Decls.String), + Decls.newVar(FEATURE_OSM_CHANGESET, Decls.Int), + Decls.newVar(FEATURE_OSM_VERSION, Decls.Int), + Decls.newVar(FEATURE_OSM_TIMESTAMP, Decls.Int), + Decls.newVar(FEATURE_OSM_USER_ID, Decls.Int), + Decls.newVar(FEATURE_OSM_USER_NAME, Decls.String) ); } @@ -360,7 +372,17 @@ public class Contexts { case FEATURE_ID -> feature.id(); case FEATURE_SOURCE -> feature.getSource(); case FEATURE_SOURCE_LAYER -> wrapNullable(feature.getSourceLayer()); - default -> null; + default -> { + OsmElement.Info info = feature instanceof OsmSourceFeature osm ? osm.originalElement().info() : null; + yield info == null ? null : switch (key) { + case FEATURE_OSM_CHANGESET -> info.changeset(); + case FEATURE_OSM_VERSION -> info.version(); + case FEATURE_OSM_TIMESTAMP -> info.timestamp(); + case FEATURE_OSM_USER_ID -> info.userId(); + case FEATURE_OSM_USER_NAME -> wrapNullable(info.user()); + default -> null; + }; + } }; } else { return null; diff --git a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfiguredFeatureTest.java b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfiguredFeatureTest.java index 848d8339..f17d253f 100644 --- a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfiguredFeatureTest.java +++ b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfiguredFeatureTest.java @@ -22,6 +22,7 @@ import com.onthegomap.planetiler.geo.GeoUtils; import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.reader.SimpleFeature; import com.onthegomap.planetiler.reader.SourceFeature; +import com.onthegomap.planetiler.reader.osm.OsmElement; import com.onthegomap.planetiler.stats.Stats; import java.nio.file.Path; import java.util.List; @@ -40,6 +41,7 @@ class ConfiguredFeatureTest { private static final Function TEST_RESOURCE = TestConfigurableUtils::pathToTestResource; private static final Function SAMPLE_RESOURCE = TestConfigurableUtils::pathToSample; private static final Function TEST_INVALID_RESOURCE = TestConfigurableUtils::pathToTestInvalidResource; + private static final OsmElement.Info OSM_INFO = new OsmElement.Info(2, 3, 4, 5, "user"); private static final Map waterTags = Map.of( "natural", "water", @@ -130,14 +132,15 @@ class ConfiguredFeatureTest { private void testPolygon(String config, Map tags, Consumer test, int expectedMatchCount) { var sf = - SimpleFeature.createFakeOsmFeature(newPolygon(0, 0, 1, 0, 1, 1, 0, 0), tags, "osm", null, 1, emptyList()); + SimpleFeature.createFakeOsmFeature(newPolygon(0, 0, 1, 0, 1, 1, 0, 0), tags, "osm", null, 1, emptyList(), + OSM_INFO); testFeature(config, sf, test, expectedMatchCount); } private void testPoint(String config, Map tags, Consumer test, int expectedMatchCount) { var sf = - SimpleFeature.createFakeOsmFeature(newPoint(0, 0), tags, "osm", null, 1, emptyList()); + SimpleFeature.createFakeOsmFeature(newPoint(0, 0), tags, "osm", null, 1, emptyList(), OSM_INFO); testFeature(config, sf, test, expectedMatchCount); } @@ -145,21 +148,22 @@ class ConfiguredFeatureTest { private void testLinestring(String config, Map tags, Consumer test, int expectedMatchCount) { var sf = - SimpleFeature.createFakeOsmFeature(newLineString(0, 0, 1, 0, 1, 1), tags, "osm", null, 1, emptyList()); + SimpleFeature.createFakeOsmFeature(newLineString(0, 0, 1, 0, 1, 1), tags, "osm", null, 1, emptyList(), OSM_INFO); testFeature(config, sf, test, expectedMatchCount); } private void testPolygon(Function pathFunction, String schemaFilename, Map tags, Consumer test, int expectedMatchCount) { var sf = - SimpleFeature.createFakeOsmFeature(newPolygon(0, 0, 1, 0, 1, 1, 0, 0), tags, "osm", null, 1, emptyList()); + SimpleFeature.createFakeOsmFeature(newPolygon(0, 0, 1, 0, 1, 1, 0, 0), tags, "osm", null, 1, emptyList(), + OSM_INFO); testFeature(pathFunction, schemaFilename, sf, test, expectedMatchCount); } private void testLinestring(Function pathFunction, String schemaFilename, Map tags, Consumer test, int expectedMatchCount) { var sf = - SimpleFeature.createFakeOsmFeature(newLineString(0, 0, 1, 0, 1, 1), tags, "osm", null, 1, emptyList()); + SimpleFeature.createFakeOsmFeature(newLineString(0, 0, 1, 0, 1, 1), tags, "osm", null, 1, emptyList(), OSM_INFO); testFeature(pathFunction, schemaFilename, sf, test, expectedMatchCount); } @@ -547,6 +551,11 @@ class ConfiguredFeatureTest { "\\\\${feature.id}|\\${feature.id}", "${feature.source}|osm", "${feature.source_layer}|null", + "${feature.osm_changeset}|2", + "${feature.osm_timestamp}|3", + "${feature.osm_user_id}|4", + "${feature.osm_version}|5", + "${feature.osm_user_name}|user", "${coalesce(feature.source_layer, 'missing')}|missing", "{match: {test: {natural: water}}}|test", "{match: {test: {natural: not_water}}}|null",