diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..94869ee28b --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: onthegomap diff --git a/planetiler-core/pom.xml b/planetiler-core/pom.xml index 66b9203251..81e29f35f4 100644 --- a/planetiler-core/pom.xml +++ b/planetiler-core/pom.xml @@ -43,7 +43,7 @@ com.github.jnr jnr-ffi - 2.2.16 + 2.2.17 org.locationtech.jts @@ -179,7 +179,7 @@ blue.strategic.parquet parquet-floor - 1.46 + 1.47 diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java index 3f8caa9329..7d0a327a1b 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java @@ -246,6 +246,40 @@ public Feature innermostPoint(String layer) { return innermostPoint(layer, 0.1); } + + /** + * Starts building a new point map feature at the midpoint of this line, or the longest line segment if a + * multilinestring. + * + * @param layer the output vector tile layer this feature will be written to + * @return a feature that can be configured further. + */ + public Feature lineMidpoint(String layer) { + try { + return geometry(layer, source.lineMidpoint()); + } catch (GeometryException e) { + e.log(stats, "feature_line_midpoint", "Error getting midpoint for " + source); + return empty(layer); + } + } + + /** + * Starts building a new point map feature at a certain ratio along the linestring or longest segment if it is a + * multilinestring. + * + * @param layer the output vector tile layer this feature will be written to + * @param ratio the ratio along the line: 0 for start, 1 for end, 0.5 for midpoint + * @return a feature that can be configured further. + */ + public Feature pointAlongLine(String layer, double ratio) { + try { + return geometry(layer, source.pointAlongLine(ratio)); + } catch (GeometryException e) { + e.log(stats, "feature_point_along_line", "Error getting point along line for " + source); + return empty(layer); + } + } + /** Returns the minimum zoom level at which this feature is at least {@code pixelSize} pixels large. */ public int getMinZoomForPixelSize(double pixelSize) { try { diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/DataType.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/DataType.java index 04c3b3f229..33063451da 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/DataType.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/DataType.java @@ -2,13 +2,12 @@ import com.onthegomap.planetiler.reader.WithTags; import com.onthegomap.planetiler.util.Parse; -import java.util.function.BiFunction; import java.util.function.UnaryOperator; /** * Destination data types for an attribute that link the type to functions that can parse the value from an input object */ -public enum DataType implements BiFunction { +public enum DataType implements TypedGetter { GET_STRING("string", WithTags::getString, Parse::parseStringOrNull), GET_BOOLEAN("boolean", WithTags::getBoolean, Parse::bool), GET_DIRECTION("direction", WithTags::getDirection, Parse::direction), @@ -17,11 +16,11 @@ public enum DataType implements BiFunction { GET_DOUBLE("double", Parse::parseDoubleOrNull), GET_TAG("get", WithTags::getTag, s -> s); - private final BiFunction getter; + private final TypedGetter getter; private final String id; private final UnaryOperator parser; - DataType(String id, BiFunction getter, UnaryOperator parser) { + DataType(String id, TypedGetter getter, UnaryOperator parser) { this.id = id; this.getter = getter; this.parser = parser; diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/Expression.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/Expression.java index 79a51f140f..c4168f6a44 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/Expression.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/Expression.java @@ -13,7 +13,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -51,7 +50,7 @@ static And and(Expression... children) { return and(List.of(children)); } - static And and(List children) { + static And and(List children) { return new And(children); } @@ -59,7 +58,7 @@ static Or or(Expression... children) { return or(List.of(children)); } - static Or or(List children) { + static Or or(List children) { return new Or(children); } @@ -91,7 +90,7 @@ static MatchAny matchAny(String field, List values) { *

* {@code values} can contain exact matches, "%text%" to match any value containing "text", or "" to match any value. */ - static MatchAny matchAnyTyped(String field, BiFunction typeGetter, Object... values) { + static MatchAny matchAnyTyped(String field, TypedGetter typeGetter, Object... values) { return matchAnyTyped(field, typeGetter, List.of(values)); } @@ -101,8 +100,7 @@ static MatchAny matchAnyTyped(String field, BiFunction *

* {@code values} can contain exact matches, "%text%" to match any value containing "text", or "" to match any value. */ - static MatchAny matchAnyTyped(String field, BiFunction typeGetter, - List values) { + static MatchAny matchAnyTyped(String field, TypedGetter typeGetter, List values) { return MatchAny.from(field, typeGetter, values); } @@ -154,7 +152,7 @@ static MatchSourceLayer matchSourceLayer(String layer) { return new MatchSourceLayer(layer); } - private static String generateJavaCodeList(List items) { + private static String generateJavaCodeList(List items) { return items.stream().map(Expression::generateJavaCode).collect(Collectors.joining(", ")); } @@ -269,7 +267,7 @@ public boolean evaluate(WithTags input, List matchKeys) { } } - record And(List children) implements Expression { + record And(List children) implements Expression { @Override public String generateJavaCode() { @@ -307,7 +305,7 @@ public Expression simplifyOnce() { } } - record Or(List children) implements Expression { + record Or(List children) implements Expression { @Override public String generateJavaCode() { @@ -405,10 +403,10 @@ record MatchAny( String field, List values, Set exactMatches, Pattern pattern, boolean matchWhenMissing, - BiFunction valueGetter + TypedGetter valueGetter ) implements Expression { - static MatchAny from(String field, BiFunction valueGetter, List values) { + static MatchAny from(String field, TypedGetter valueGetter, List values) { List exactMatches = new ArrayList<>(); List patterns = new ArrayList<>(); @@ -474,6 +472,10 @@ public boolean evaluate(WithTags input, List matchKeys) { @Override public Expression partialEvaluate(PartialInput input) { + if (field == null) { + // dynamic getters always need to be evaluated + return this; + } Object value = input.getTag(field); return value == null ? this : constBool(evaluate(new ArrayList<>(), value)); } @@ -496,7 +498,9 @@ private boolean evaluate(List matchKeys, Object value) { return false; } else { String str = value.toString(); - if (exactMatches.contains(str)) { + // when field is null, we rely on a dynamic getter function so when exactMatches is empty we match + // on any value + if (exactMatches.contains(str) || (field == null && exactMatches.isEmpty())) { matchKeys.add(field); return true; } @@ -510,7 +514,13 @@ private boolean evaluate(List matchKeys, Object value) { @Override public Expression simplifyOnce() { - return isMatchAnything() ? matchField(field) : this; + if (isMatchAnything()) { + return matchField(field); + } else if (valueGetter instanceof Simplifiable simplifiable) { + return new MatchAny(field, values, exactMatches, pattern, matchWhenMissing, + (TypedGetter) simplifiable.simplifyOnce()); + } + return this; } @Override @@ -557,6 +567,11 @@ private String patternString() { public int hashCode() { return Objects.hash(field, values, exactMatches, patternString(), matchWhenMissing, valueGetter); } + + public boolean mustAlwaysEvaluate() { + // when field is null we rely on a dynamic getter function + return field == null || matchWhenMissing; + } } /** Evaluates to true if an input element contains any value for {@code field} tag. */ diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/MultiExpression.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/MultiExpression.java index ae50bfc88d..db08f14607 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/MultiExpression.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/MultiExpression.java @@ -4,8 +4,9 @@ import static com.onthegomap.planetiler.expression.Expression.TRUE; import static com.onthegomap.planetiler.expression.Expression.matchType; -import com.onthegomap.planetiler.reader.SourceFeature; import com.onthegomap.planetiler.reader.WithGeometryType; +import com.onthegomap.planetiler.reader.WithSource; +import com.onthegomap.planetiler.reader.WithSourceLayer; import com.onthegomap.planetiler.reader.WithTags; import java.util.ArrayList; import java.util.Comparator; @@ -60,7 +61,7 @@ private static boolean mustAlwaysEvaluate(Expression expression) { case Expression.Or(var children) -> children.stream().anyMatch(MultiExpression::mustAlwaysEvaluate); case Expression.And(var children) -> children.stream().allMatch(MultiExpression::mustAlwaysEvaluate); case Expression.Not(var child) -> !mustAlwaysEvaluate(child); - case Expression.MatchAny any when any.matchWhenMissing() -> true; + case Expression.MatchAny any when any.mustAlwaysEvaluate() -> true; case null, default -> !(expression instanceof Expression.MatchAny) && !(expression instanceof Expression.MatchField) && !FALSE.equals(expression); @@ -79,7 +80,7 @@ private static void getRelevantKeys(Expression exp, Consumer acceptKey) or.children().forEach(child -> getRelevantKeys(child, acceptKey)); } else if (exp instanceof Expression.MatchField field) { acceptKey.accept(field.field()); - } else if (exp instanceof Expression.MatchAny any && !any.matchWhenMissing()) { + } else if (exp instanceof Expression.MatchAny any && !any.mustAlwaysEvaluate()) { acceptKey.accept(any.field()); } // ignore not case since not(matchAny("field", "")) should track "field" as a relevant key, but that gets @@ -436,7 +437,7 @@ private SourceLayerIndex(MultiExpression expressions, boolean warn) { @Override String extract(WithTags input) { - return input instanceof SourceFeature feature ? feature.getSourceLayer() : null; + return input instanceof WithSourceLayer feature ? feature.getSourceLayer() : null; } } @@ -451,7 +452,7 @@ private SourceIndex(MultiExpression expressions, boolean warn) { @Override String extract(WithTags input) { - return input instanceof SourceFeature feature ? feature.getSource() : null; + return input instanceof WithSource feature ? feature.getSource() : null; } } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/TypedGetter.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/TypedGetter.java new file mode 100644 index 0000000000..c7e959258f --- /dev/null +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/expression/TypedGetter.java @@ -0,0 +1,8 @@ +package com.onthegomap.planetiler.expression; + +import com.onthegomap.planetiler.reader.WithTags; + +@FunctionalInterface +public interface TypedGetter { + Object apply(WithTags withTags, String tag); +} diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java index 540933b4b8..f0e0c13189 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java @@ -552,6 +552,21 @@ public static int minZoomForPixelSize(double worldGeometrySize, double minPixelS PlanetilerConfig.MAX_MAXZOOM); } + public static LineString getLongestLine(MultiLineString multiLineString) { + LineString result = null; + double max = -1; + for (int i = 0; i < multiLineString.getNumGeometries(); i++) { + if (multiLineString.getGeometryN(i) instanceof LineString ls) { + double length = ls.getLength(); + if (length > max) { + max = length; + result = ls; + } + } + } + return result; + } + public static WKBReader wkbReader() { return new WKBReader(JTS_FACTORY); } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/LineSplitter.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/LineSplitter.java index 45f367d28e..ff73396803 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/LineSplitter.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/LineSplitter.java @@ -4,6 +4,7 @@ import org.locationtech.jts.geom.CoordinateSequence; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; /** * Utility for extracting sub-ranges of a line. @@ -29,6 +30,22 @@ public LineSplitter(Geometry geom) { } } + /** + * Returns a point at {@code ratio} along this line segment where 0 is the beginning of the line and 1 is the end. + */ + public Point get(double ratio) { + if (ratio < 0d || ratio > 1d) { + throw new IllegalArgumentException("Invalid ratio: " + ratio); + } + init(); + double pos = ratio * length; + var cs = line.getCoordinateSequence(); + var idx = Math.max(lowerIndex(pos), 0); + MutableCoordinateSequence result = new MutableCoordinateSequence(1); + addInterpolated(result, cs, idx, pos); + return GeoUtils.JTS_FACTORY.createPoint(result); + } + /** * Returns a partial segment of this line from {@code start} to {@code end} where 0 is the beginning of the line and 1 * is the end. @@ -40,6 +57,24 @@ public LineString get(double start, double end) { if (start <= 0 && end >= 1) { return line; } + var cs = line.getCoordinateSequence(); + init(); + MutableCoordinateSequence result = new MutableCoordinateSequence(); + + double startPos = start * length; + double endPos = end * length; + var first = floorIndex(startPos); + var last = lowerIndex(endPos); + addInterpolated(result, cs, first, startPos); + for (int i = first + 1; i <= last; i++) { + result.addPoint(cs.getX(i), cs.getY(i)); + } + addInterpolated(result, cs, last, endPos); + + return GeoUtils.JTS_FACTORY.createLineString(result); + } + + private void init() { var cs = line.getCoordinateSequence(); if (nodeLocations == null) { nodeLocations = new double[cs.size()]; @@ -57,19 +92,6 @@ public LineString get(double start, double end) { y1 = y2; } } - MutableCoordinateSequence result = new MutableCoordinateSequence(); - - double startPos = start * length; - double endPos = end * length; - var first = floorIndex(startPos); - var last = lowerIndex(endPos); - addInterpolated(result, cs, first, startPos); - for (int i = first + 1; i <= last; i++) { - result.addPoint(cs.getX(i), cs.getY(i)); - } - addInterpolated(result, cs, last, endPos); - - return GeoUtils.JTS_FACTORY.createLineString(result); } private int floorIndex(double length) { diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/MutableCoordinateSequence.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/MutableCoordinateSequence.java index 8dc0c442f0..3607b55571 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/MutableCoordinateSequence.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/MutableCoordinateSequence.java @@ -15,10 +15,15 @@ */ public class MutableCoordinateSequence extends PackedCoordinateSequence { - private final DoubleArrayList points = new DoubleArrayList(); + private final DoubleArrayList points; public MutableCoordinateSequence() { + this(2); + } + + public MutableCoordinateSequence(int size) { super(2, 0); + points = new DoubleArrayList(2 * size); } /** diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/SourceFeature.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/SourceFeature.java index 68caa9d6a1..10a04a6a7f 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/SourceFeature.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/SourceFeature.java @@ -26,7 +26,7 @@ * All geometries except for {@link #latLonGeometry()} return elements in world web mercator coordinates where (0,0) is * the northwest corner and (1,1) is the southeast corner of the planet. */ -public abstract class SourceFeature implements WithTags, WithGeometryType { +public abstract class SourceFeature implements WithTags, WithGeometryType, WithSource, WithSourceLayer { private final Map tags; private final String source; @@ -124,11 +124,37 @@ public final Geometry innermostPoint(double tolerance) throws GeometryException innermostPointTolerance = tolerance; } return innermostPoint; + } else if (canBeLine()) { + return lineMidpoint(); } else { return pointOnSurface(); } } + /** + * Returns the midpoint of this line, or the longest segment if it is a multilinestring. + */ + public final Geometry lineMidpoint() throws GeometryException { + if (innermostPoint == null) { + innermostPoint = pointAlongLine(0.5); + } + return innermostPoint; + } + + /** + * Returns along this line where {@code ratio=0} is the start {@code ratio=1} is the end and {@code ratio=0.5} is the + * midpoint. + *

+ * When this is a multilinestring, the longest segment is used. + */ + public final Geometry pointAlongLine(double ratio) throws GeometryException { + if (lineSplitter == null) { + var line = line(); + lineSplitter = new LineSplitter(line instanceof MultiLineString multi ? GeoUtils.getLongestLine(multi) : line); + } + return lineSplitter.get(ratio); + } + private Geometry computeCentroidIfConvex() throws GeometryException { if (!canBePolygon()) { return centroid(); @@ -279,11 +305,13 @@ public double size() throws GeometryException { } /** Returns the ID of the source that this feature came from. */ + @Override public String getSource() { return source; } /** Returns the layer ID within a source that this feature comes from. */ + @Override public String getSourceLayer() { return sourceLayer; } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/WithSource.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/WithSource.java new file mode 100644 index 0000000000..34aaaae44d --- /dev/null +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/WithSource.java @@ -0,0 +1,5 @@ +package com.onthegomap.planetiler.reader; + +public interface WithSource { + String getSource(); +} diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/WithSourceLayer.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/WithSourceLayer.java new file mode 100644 index 0000000000..3e043d9a1a --- /dev/null +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/WithSourceLayer.java @@ -0,0 +1,5 @@ +package com.onthegomap.planetiler.reader; + +public interface WithSourceLayer { + String getSourceLayer(); +} diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/LanguageUtils.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/LanguageUtils.java index be64e06ae1..76b7af55c4 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/LanguageUtils.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/LanguageUtils.java @@ -4,7 +4,6 @@ import java.util.Set; import java.util.function.Predicate; import java.util.regex.Pattern; -import java.util.stream.Stream; public class LanguageUtils { // Name tags that should be eligible for finding a latin name. @@ -39,6 +38,10 @@ public static String nullIfEmpty(String a) { return (a == null || a.isEmpty()) ? null : a; } + /** + * @deprecated Use {@code OmtLanguageUtils.string()} + */ + @Deprecated(forRemoval = true) public static String string(Object obj) { return nullIfEmpty(obj == null ? null : obj.toString()); } @@ -47,6 +50,10 @@ public static boolean containsOnlyLatinCharacters(String string) { return string != null && ONLY_LATIN.test(string); } + /** + * @deprecated Use {@code Translations.transliterate(string(tags.get("name")))} + */ + @Deprecated(forRemoval = true) public static String transliteratedName(Map tags) { return Translations.transliterate(string(tags.get("name"))); } @@ -73,26 +80,4 @@ public static boolean isValidOsmNameTag(String tag) { return VALID_NAME_TAGS.test(tag); } - public static String getLatinName(Map tags, boolean transliterate) { - String name = string(tags.get("name")); - if (containsOnlyLatinCharacters(name)) { - return name; - } else { - return getNameTranslations(tags) - .filter(LanguageUtils::containsOnlyLatinCharacters) - .findFirst() - .orElse(transliterate ? Translations.transliterate(name) : null); - } - } - - - private static Stream getNameTranslations(Map tags) { - return Stream.concat( - Stream.of("name:en", "int_name", "name:de").map(tag -> string(tags.get(tag))), - tags.entrySet().stream() - .filter(e -> !EN_DE_NAME_KEYS.contains(e.getKey()) && VALID_NAME_TAGS.test(e.getKey())) - .map(Map.Entry::getValue) - .map(LanguageUtils::string) - ); - } } diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/FeatureCollectorTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/FeatureCollectorTest.java index daab902c6a..799e2ae81f 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/FeatureCollectorTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/FeatureCollectorTest.java @@ -852,4 +852,42 @@ void testSetAttrPartialWithMinSize() { assertEquals(7, line.linearRange(0, 0.5).getMinZoomForPixelSize(50)); assertEquals(7, line.linearRange(0, 0.25).getMinZoomForPixelSize(25)); } + + + @Test + void testLineMidpoint() { + var sourceLine = newReaderFeature(newLineString(worldToLatLon( + 0, 0, + 1, 0 + )), Map.of()); + + var fc = factory.get(sourceLine); + fc.lineMidpoint("layer").setZoomRange(0, 10); + var iter = fc.iterator(); + + var item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(round(newPoint(0.5, 0)), round(item.getGeometry())); + + assertFalse(iter.hasNext()); + } + + + @Test + void testPointAlongLine() { + var sourceLine = newReaderFeature(newLineString(worldToLatLon( + 0, 0, + 1, 0 + )), Map.of()); + + var fc = factory.get(sourceLine); + fc.pointAlongLine("layer", 0.25).setZoomRange(0, 10); + var iter = fc.iterator(); + + var item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(round(newPoint(0.25, 0)), round(item.getGeometry())); + + assertFalse(iter.hasNext()); + } } diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/expression/ExpressionTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/expression/ExpressionTest.java index 6d931a5159..0b47f45b53 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/expression/ExpressionTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/expression/ExpressionTest.java @@ -350,6 +350,19 @@ void testPartialEvaluateMatchAny() { new PartialInput(Set.of(), Set.of(), Map.of("field", "not a value"), Set.of()))); } + @Test + void testPartialEvaluateMatchAnyDynamicGetter() { + var expr = matchAnyTyped(null, ((withTags, tag) -> ""), 1, 2, 3); + assertEquals(expr, expr.partialEvaluate(new PartialInput(Set.of(), Set.of(), Map.of("other", "value"), Set.of()))); + assertEquals(expr, expr.partialEvaluate(new PartialInput(Set.of(), Set.of(), null, Set.of()))); + assertEquals(expr, expr.partialEvaluate(new PartialInput(Set.of(), Set.of(), Map.of("field", "value1"), Set.of()))); + assertEquals(expr, expr.partialEvaluate(new PartialInput(Set.of(), Set.of(), Map.of("field", "other"), Set.of()))); + assertEquals(expr, + expr.partialEvaluate(new PartialInput(Set.of(), Set.of(), Map.of("field", "other..."), Set.of()))); + assertEquals(expr, expr.partialEvaluate( + new PartialInput(Set.of(), Set.of(), Map.of("field", "not a value"), Set.of()))); + } + @Test void testPartialEvaluateMatchGeometryType() { var expr = matchGeometryType(GeometryType.POINT); diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/GeoUtilsTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/GeoUtilsTest.java index 76c4076cc8..64809ec0e7 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/GeoUtilsTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/GeoUtilsTest.java @@ -4,6 +4,7 @@ import static com.onthegomap.planetiler.geo.GeoUtils.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import com.onthegomap.planetiler.stats.Stats; @@ -447,4 +448,13 @@ void minZoomForPixelSizesAtZ9_10() { assertEquals(10, GeoUtils.minZoomForPixelSize(3.1 / (256 << 10), 3)); assertEquals(9, GeoUtils.minZoomForPixelSize(6.1 / (256 << 10), 3)); } + + @Test + void getLongestLine() { + var line1 = newLineString(0, 0, 1, 1); + var line2 = newLineString(0, 0, 2, 2); + assertNull(GeoUtils.getLongestLine(newMultiLineString())); + assertEquals(line1, GeoUtils.getLongestLine(newMultiLineString(line1))); + assertEquals(line2, GeoUtils.getLongestLine(newMultiLineString(line1, line2))); + } } diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/LineSplitterTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/LineSplitterTest.java index 859bd5aa94..82c8be3ba9 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/LineSplitterTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/LineSplitterTest.java @@ -1,12 +1,15 @@ package com.onthegomap.planetiler.geo; import static com.onthegomap.planetiler.TestUtils.newLineString; +import static com.onthegomap.planetiler.TestUtils.newPoint; +import static com.onthegomap.planetiler.TestUtils.round; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; class LineSplitterTest { @ParameterizedTest @@ -57,6 +60,18 @@ void testLength2() { ); } + @ParameterizedTest + @ValueSource(doubles = { + 0, 0.00001, 0.1, 0.49999, 0.5, 0.50001, 0.9, 0.99999, 1.0 + }) + void testDistanceAlongLine(double ratio) { + var l = new LineSplitter(newLineString(0, 0, 1, 0.5, 2, 1)); + assertEquals( + round(newPoint(ratio * 2, ratio)), + round(l.get(ratio)) + ); + } + @Test void testInvalid() { var l = new LineSplitter(newLineString(0, 0, 1, 2, 2, 4)); diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/LanguageUtilsTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/LanguageUtilsTest.java index 341d95d00b..049dc3f76d 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/LanguageUtilsTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/LanguageUtilsTest.java @@ -1,44 +1,12 @@ package com.onthegomap.planetiler.util; -import static com.onthegomap.planetiler.util.LanguageUtils.containsOnlyLatinCharacters; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.Map; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.ValueSource; class LanguageUtilsTest { - @ParameterizedTest - @CsvSource({ - "abc, true", - "5!, true", - "5~, true", - "é, true", - "éś, true", - "ɏə, true", - "ɐ, true", - "ᵿἀ, false", - "Ḁỿ, true", - "\u02ff\u0370, false", - "\u0030\u036f, true", - "日本, false", - "abc本123, false", - }) - void testIsLatin(String in, boolean isLatin) { - if (!isLatin) { - assertFalse(containsOnlyLatinCharacters(in)); - } else { - assertEquals(in, LanguageUtils.getLatinName(Map.of( - "name", in - ), true)); - } - } - @ParameterizedTest @CsvSource(value = { "null,null", @@ -58,102 +26,28 @@ void testRemoveNonLatin(String in, String out) { } @ParameterizedTest - @ValueSource(strings = { - // OSM tags that SHOULD be eligible for name:latin feature in the output - "name:en", - "name:en-US", - "name:en-010", - "int_name", - "name:fr", - "name:es", - "name:pt", - "name:de", - "name:ar", - "name:it", - "name:ko-Latn", - "name:be-tarask", - // https://wiki.openstreetmap.org/wiki/Multilingual_names#Japan - "name:ja", - "name:ja-Latn", - "name:ja_rm", - "name:ja_kana", - // https://wiki.openstreetmap.org/wiki/Multilingual_names#China - "name:zh-CN", - "name:zh-hant-CN", - "name:zh_pinyin", - "name:zh_zhuyin", - "name:zh-Latn-tongyong", - "name:zh-Latn-pinyin", - "name:zh-Latn-wadegiles", - "name:yue-Latn-jyutping", - // https://wiki.openstreetmap.org/wiki/Multilingual_names#France - "name:fr", - "name:fr-x-gallo", - "name:br", - "name:oc", - "name:vls", - "name:frp", - "name:gcf", - "name:gsw", - }) - void testLatinFallbacks(String key) { - if (key.startsWith("name:")) { - assertTrue(LanguageUtils.isValidOsmNameTag(key)); - } - assertEquals("a", LanguageUtils.getLatinName(Map.of( - key, "a" - ), true)); - assertNull(LanguageUtils.getLatinName(Map.of( - key, "ア" - ), true)); - assertNull(LanguageUtils.getLatinName(Map.of( - key, "غ" - ), true)); - } - - @ParameterizedTest - @ValueSource(strings = { - // OSM tags that should NOT be eligible for name:latin feature in the output - "name:signed", - "name:prefix", - "name:abbreviation", - "name:source", - "name:full", - "name:adjective", - "name:proposed", - "name:pronunciation", - "name:etymology", - "name:etymology:wikidata", - "name:etymology:wikipedia", - "name:etymology:right", - "name:etymology:left", - "name:genitive", - }) - void testNoLatinFallback(String key) { - assertFalse(LanguageUtils.isValidOsmNameTag(key)); - assertEquals("Branch Hill–Loveland Road", LanguageUtils.getLatinName(Map.of( - "name", "Branch Hill–Loveland Road", - key, "Q22133584;Q843993" - ), true)); - assertEquals("rì", LanguageUtils.getLatinName(Map.of( - "name", "日", - key, "other" - ), true)); - } - - @ParameterizedTest - @CsvSource({ - "キャンパス, kyanpasu", - "Αλφαβητικός Κατάλογος, Alphabētikós Katálogos", - "биологическом, biologičeskom", - }) - void testTransliterate(String in, String out) { - assertEquals(out, LanguageUtils.getLatinName(Map.of( - "name", in - ), true)); - assertNull(LanguageUtils.getLatinName(Map.of( - "name", in - ), false)); + @CsvSource(value = { + "name:es, true", + "name:en-US, true", + "name:fr-x-gallo, true", + "name:ko-Latn, true", + "name:be-tarask, true", + "name:ja_rm, true", + "name:ja_kana, true", + "name:vls, true", + "name:zh-hant-CN, true", + "name:zh_pinyin, true", + "name:zh_zhuyin, true", + "name:zh-Latn-tongyong, true", + "name:zh-Latn-pinyin, true", + "name:zh-Latn-wadegiles, true", + "name:yue-Latn-jyutping, true", + "nombre, false", + "name:, false", + "name:xxxxx, false", + }, nullValues = "null") + void testIsValidOsmNameTag(String in, boolean out) { + assertEquals(out, LanguageUtils.isValidOsmNameTag(in)); } } diff --git a/planetiler-custommap/README.md b/planetiler-custommap/README.md index 7b54733967..b10c74ed75 100644 --- a/planetiler-custommap/README.md +++ b/planetiler-custommap/README.md @@ -65,6 +65,10 @@ A description that tells planetiler how to read geospatial objects with tags fro For [geofabrik](https://download.geofabrik.de/) named areas, use `geofabrik:` prefixes, for example `geofabrik:rhode-island`. Can be a string or [expression](#expression) that can reference [argument values](#arguments). +- `projection` - Planetiler will try to determine the projection automatically for shapefile/geopackage sources, but if + that is not correct you can override the projection by specifying a coordinate reference system authority code + like `EPSG:3857` or `EPSG:4326` here. Can be a string or [expression](#expression) that can + reference [argument values](#arguments). For example: @@ -214,17 +218,22 @@ layers: A feature is a defined set of objects that meet a specified filter criteria. -- `source` - A string [source](#source) ID, or list of source IDs from which features should be extracted +- `source` - A string [source](#source) ID, or list of source IDs from which features should be extracted. If missing, + features from all sources are included. - `geometry` - A string enum that indicates which geometry types to include, and how to transform them. Can be one of: - `point` `line` or `polygon` to pass the original feature through + - `any` (default) to pass the original feature through regardless of geometry type - `polygon_centroid` to match on polygons, and emit a point at the center - - `line_centroid` to match on lines, and emit a point at the center + - `line_centroid` to match on lines, and emit a point at the centroid of the line + - `line_midpoint` to match on lines, and emit a point at midpoint of the line - `centroid` to match any geometry, and emit a point at the center - `polygon_point_on_surface` to match on polygons, and emit an interior point - `point_on_line` to match on lines, and emit a point somewhere along the line - `polygon_centroid_if_convex` to match on polygons, and if the polygon is convex emit the centroid, otherwise emit an interior point + - `innermost_point` to match on any geometry and for polygons, emit the furthest point from an edge, or for lines emit + the midpoint. - `include_when` - A [Boolean Expression](#boolean-expression) which determines the features to include. If unspecified, all features from the specified sources are included. - `exclude_when` - A [Boolean Expression](#boolean-expression) which determines if a feature that matched the include diff --git a/planetiler-custommap/planetiler.schema.json b/planetiler-custommap/planetiler.schema.json index 2a5644d31d..e4863ea1e9 100644 --- a/planetiler-custommap/planetiler.schema.json +++ b/planetiler-custommap/planetiler.schema.json @@ -51,6 +51,20 @@ "local_path": { "description": "Local path to the file to use, inferred from `url` if missing", "$ref": "#/$defs/expression" + }, + "projection": { + "description": "Override the coordinate reference system authority code for a shapefile or geopackage source if it can not be determined automatically", + "anyOf": [ + { + "enum": [ + "EPSG:3857", + "EPSG:4326" + ] + }, + { + "type": "#/$defs/expression" + } + ] } }, "anyOf": [ @@ -342,27 +356,27 @@ }, "feature": { "type": "object", - "required": [ - "geometry" - ], "properties": { "geometry": { "description": "Include objects of a certain geometry type", "type": "string", "enum": [ + "any", "point", "line", "polygon", "polygon_centroid", "line_centroid", + "line_midpoint", "centroid", "polygon_centroid_if_convex", "polygon_point_on_surface", - "point_on_line" + "point_on_line", + "innermost_point" ] }, "source": { - "description": "A source ID or list of source IDs from which features should be extracted", + "description": "A source ID or list of source IDs from which features should be extracted. If unspecified, all sources are included", "oneOf": [ { "type": "string" diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/BooleanExpressionParser.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/BooleanExpressionParser.java index 1c3489a9ef..011df98d28 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/BooleanExpressionParser.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/BooleanExpressionParser.java @@ -3,8 +3,10 @@ import static com.onthegomap.planetiler.expression.Expression.matchAnyTyped; import static com.onthegomap.planetiler.expression.Expression.matchField; import static com.onthegomap.planetiler.expression.Expression.not; +import static com.onthegomap.planetiler.expression.Expression.or; import com.onthegomap.planetiler.custommap.expression.BooleanExpressionScript; +import com.onthegomap.planetiler.custommap.expression.ConfigExpression; import com.onthegomap.planetiler.custommap.expression.ConfigExpressionScript; import com.onthegomap.planetiler.custommap.expression.ParseException; import com.onthegomap.planetiler.custommap.expression.ScriptContext; @@ -110,25 +112,38 @@ private Expression tagCriterionToExpression(String key, Object value) { } else if (IS_NOT.test(key)) { // __not__ negates its children return not(parse(value)); - } else if (value == null || IS_ANY.test(value.toString()) || - (value instanceof Collection values && - values.stream().anyMatch(d -> d != null && IS_ANY.test(d.toString().trim())))) { - //If only a key is provided, with no value, match any object tagged with that key. - return matchField(unescape(key)); - - } else if (value instanceof Collection values) { - //If a collection is provided, match any of these values. - return matchAnyTyped( - unescape(key), - tagValueProducer.valueGetterForKey(key), - values.stream().map(BooleanExpressionParser::unescape).toList()); - } else { - //Otherwise, a key and single value were passed, so match that exact tag - return matchAnyTyped( - unescape(key), - tagValueProducer.valueGetterForKey(key), - unescape(value)); + //If only a key is provided, with no value, match any object tagged with that key. + boolean isAny = value == null || IS_ANY.test(value.toString()) || + (value instanceof Collection values && + values.stream().anyMatch(d -> d != null && IS_ANY.test(d.toString().trim()))); + //If a collection or single item are provided, match any of these values. + List values = (value instanceof Collection items ? items : value == null ? List.of() : List.of(value)) + .stream().map(BooleanExpressionParser::unescape).toList(); + if (ConfigExpressionScript.isScript(key)) { + var expression = ConfigExpressionScript.parse(ConfigExpressionScript.extractScript(key), context).simplify(); + if (isAny) { + values = List.of(); + } + var result = matchAnyTyped(null, expression, values); + if (!values.isEmpty() && result.pattern() == null && !result.isMatchAnything() && !result.matchWhenMissing() && + expression instanceof ConfigExpression.Variable(var ignored,var name)) { + if (name.equals("feature.source")) { + return or(values.stream().filter(String.class::isInstance).map(String.class::cast) + .map(Expression::matchSource).toList()); + } else if (name.equals("feature.source_layer")) { + return or(values.stream().filter(String.class::isInstance).map(String.class::cast) + .map(Expression::matchSourceLayer).toList()); + } + } + return result; + } + String field = unescape(key); + if (isAny) { + return matchField(field); + } else { + return matchAnyTyped(field, tagValueProducer.valueGetterForKey(key), values); + } } } } diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredFeature.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredFeature.java index a43e7f64cd..ed23968876 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredFeature.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredFeature.java @@ -64,6 +64,12 @@ public ConfiguredFeature(String layer, TagValueProducer tagValueProducer, Featur BooleanExpressionParser.parse(feature.includeWhen(), tagValueProducer, processFeatureContext); } + if (!feature.source().isEmpty()) { + filter = Expression.and( + filter, + Expression.or(feature.source().stream().map(Expression::matchSource).toList()) + ); + } if (feature.excludeWhen() != null) { filter = Expression.and( filter, @@ -274,7 +280,7 @@ public void processFeature(Contexts.FeaturePostMatch context, FeatureCollector f var sourceFeature = context.feature(); // Ensure that this feature is from the correct source (index should enforce this, so just check when assertions enabled) - assert sources.contains(sourceFeature.getSource()); + assert sources.isEmpty() || sources.contains(sourceFeature.getSource()); var f = geometryFactory.apply(features); for (var processor : featureProcessors) { diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredMapMain.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredMapMain.java index ff73a1f56c..c4a11437aa 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredMapMain.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredMapMain.java @@ -62,6 +62,7 @@ private static void configureSource(Planetiler planetiler, Path sourcesDir, Sour DataSourceType sourceType = source.type(); Path localPath = source.localPath(); + String projection = source.projection(); if (localPath == null) { if (source.url() == null) { throw new ParseException("Must provide either a url or path for " + source.id()); @@ -71,8 +72,8 @@ private static void configureSource(Planetiler planetiler, Path sourcesDir, Sour switch (sourceType) { case OSM -> planetiler.addOsmSource(source.id(), localPath, source.url()); - case SHAPEFILE -> planetiler.addShapefileSource(source.id(), localPath, source.url()); - case GEOPACKAGE -> planetiler.addGeoPackageSource(source.id(), localPath, source.url()); + case SHAPEFILE -> planetiler.addShapefileSource(projection, source.id(), localPath, source.url()); + case GEOPACKAGE -> planetiler.addGeoPackageSource(projection, source.id(), localPath, source.url()); default -> throw new IllegalArgumentException("Unhandled source type for " + source.id() + ": " + sourceType); } } diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredProfile.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredProfile.java index 1acd372f16..aa00ae9be9 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredProfile.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredProfile.java @@ -1,7 +1,6 @@ package com.onthegomap.planetiler.custommap; import static com.onthegomap.planetiler.expression.MultiExpression.Entry; -import static java.util.Map.entry; import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.FeatureMerge; @@ -19,7 +18,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; /** * A profile configured from a yml file. @@ -27,10 +25,8 @@ public class ConfiguredProfile implements Profile { private final SchemaConfig schema; - - private final Collection layers; private final Map layersById = new HashMap<>(); - private final Map> featureLayerMatcher; + private final Index featureLayerMatcher; private final TagValueProducer tagValueProducer; private final Contexts.Root rootContext; @@ -38,14 +34,14 @@ public ConfiguredProfile(SchemaConfig schema, Contexts.Root rootContext) { this.schema = schema; this.rootContext = rootContext; - layers = schema.layers(); + Collection layers = schema.layers(); if (layers == null || layers.isEmpty()) { throw new IllegalArgumentException("No layers defined"); } tagValueProducer = new TagValueProducer(schema.inputMappings()); - Map>> configuredFeatureEntries = new HashMap<>(); + List> configuredFeatureEntries = new ArrayList<>(); for (var layer : layers) { String layerId = layer.id(); @@ -53,16 +49,12 @@ public ConfiguredProfile(SchemaConfig schema, Contexts.Root rootContext) { for (var feature : layer.features()) { var configuredFeature = new ConfiguredFeature(layerId, tagValueProducer, feature, rootContext); var entry = new Entry<>(configuredFeature, configuredFeature.matchExpression()); - for (var source : feature.source()) { - var list = configuredFeatureEntries.computeIfAbsent(source, s -> new ArrayList<>()); - list.add(entry); - } + configuredFeatureEntries.add(entry); } } - featureLayerMatcher = configuredFeatureEntries.entrySet().stream() - .map(entry -> entry(entry.getKey(), MultiExpression.of(entry.getValue()).index())) - .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); + featureLayerMatcher = MultiExpression.of(configuredFeatureEntries).index(); + } @Override @@ -78,15 +70,12 @@ public String attribution() { @Override public void processFeature(SourceFeature sourceFeature, FeatureCollector featureCollector) { var context = rootContext.createProcessFeatureContext(sourceFeature, tagValueProducer); - var index = featureLayerMatcher.get(sourceFeature.getSource()); - if (index != null) { - var matches = index.getMatchesWithTriggers(context); - for (var configuredFeature : matches) { - configuredFeature.match().processFeature( - context.createPostMatchContext(configuredFeature.keys()), - featureCollector - ); - } + var matches = featureLayerMatcher.getMatchesWithTriggers(context); + for (var configuredFeature : matches) { + configuredFeature.match().processFeature( + context.createPostMatchContext(configuredFeature.keys()), + featureCollector + ); } } @@ -128,13 +117,18 @@ public String description() { public List sources() { List sources = new ArrayList<>(); schema.sources().forEach((key, value) -> { - var url = ConfigExpressionParser.tryStaticEvaluate(rootContext, value.url(), String.class).get(); - var path = ConfigExpressionParser.tryStaticEvaluate(rootContext, value.localPath(), String.class).get(); - sources.add(new Source(key, value.type(), url, path == null ? null : Path.of(path))); + var url = evaluate(value.url(), String.class); + var path = evaluate(value.localPath(), String.class); + var projection = evaluate(value.projection(), String.class); + sources.add(new Source(key, value.type(), url, path == null ? null : Path.of(path), projection)); }); return sources; } + private T evaluate(Object expression, Class returnType) { + return ConfigExpressionParser.tryStaticEvaluate(rootContext, expression, returnType).get(); + } + public FeatureLayer findFeatureLayer(String layerId) { return layersById.get(layerId); } 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 f4977bfaaa..f01a8a41f4 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.WithSource; +import com.onthegomap.planetiler.reader.WithSourceLayer; import com.onthegomap.planetiler.reader.WithTags; import com.onthegomap.planetiler.reader.osm.OsmElement; import com.onthegomap.planetiler.reader.osm.OsmSourceFeature; @@ -285,7 +287,8 @@ default Object argument(String key) { * Makes nested contexts adhere to {@link WithTags} and {@link WithGeometryType} by recursively fetching source * feature from the root context. */ - private interface FeatureContext extends ScriptContext, WithTags, WithGeometryType, NestedContext { + private interface FeatureContext extends ScriptContext, WithTags, WithGeometryType, NestedContext, WithSourceLayer, + WithSource { default FeatureContext parent() { return null; @@ -325,6 +328,16 @@ default boolean canBeLine() { default boolean canBePolygon() { return feature().canBePolygon(); } + + @Override + default String getSource() { + return feature().getSource(); + } + + @Override + default String getSourceLayer() { + return feature().getSourceLayer(); + } } /** diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/Source.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/Source.java index 102e120937..31019340b7 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/Source.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/Source.java @@ -8,7 +8,8 @@ public record Source( String id, DataSourceType type, String url, - Path localPath + Path localPath, + String projection ) { public String defaultFileUrl() { diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/TagValueProducer.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/TagValueProducer.java index 3532cc35a4..b135b983ab 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/TagValueProducer.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/TagValueProducer.java @@ -3,11 +3,11 @@ import static com.onthegomap.planetiler.expression.DataType.GET_TAG; import com.onthegomap.planetiler.expression.DataType; +import com.onthegomap.planetiler.expression.TypedGetter; import com.onthegomap.planetiler.reader.WithTags; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.UnaryOperator; @@ -17,7 +17,7 @@ public class TagValueProducer { public static final TagValueProducer EMPTY = new TagValueProducer(null); - private final Map> valueRetriever = new HashMap<>(); + private final Map valueRetriever = new HashMap<>(); private final Map keyType = new HashMap<>(); @@ -50,7 +50,7 @@ public TagValueProducer(Map map) { /** * Returns a function that extracts the value for {@code key} from a {@link WithTags} instance. */ - public BiFunction valueGetterForKey(String key) { + public TypedGetter valueGetterForKey(String key) { return valueRetriever.getOrDefault(key, GET_TAG); } diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/TypeConversion.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/TypeConversion.java index 0b9b45eea2..8356f6ce10 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/TypeConversion.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/TypeConversion.java @@ -3,6 +3,7 @@ import com.onthegomap.planetiler.util.Parse; import java.util.List; import java.util.function.Function; +import org.projectnessie.cel.common.types.NullT; /** * Utility for convert between types in a forgiving way (parse strings to get a number, call toString to get a string, @@ -49,6 +50,9 @@ private static Converter converter(Class in, Class out, Funct */ @SuppressWarnings("unchecked") public static O convert(Object in, Class out) { + if (in == NullT.NullValue) { + return null; + } if (in == null || out.isInstance(in)) { return (O) in; } diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/DataSource.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/DataSource.java index 6296a64d3e..4c21e70cef 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/DataSource.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/DataSource.java @@ -5,5 +5,6 @@ public record DataSource( DataSourceType type, Object url, - @JsonProperty("local_path") Object localPath + @JsonProperty("local_path") Object localPath, + Object projection ) {} diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/FeatureGeometry.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/FeatureGeometry.java index 96f31b66ce..4a2cfc6509 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/FeatureGeometry.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/FeatureGeometry.java @@ -1,5 +1,6 @@ package com.onthegomap.planetiler.custommap.configschema; +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import com.fasterxml.jackson.annotation.JsonProperty; import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.expression.Expression; @@ -8,6 +9,8 @@ import java.util.function.Function; public enum FeatureGeometry { + @JsonProperty("any") @JsonEnumDefaultValue + ANY(GeometryType.UNKNOWN, FeatureCollector::anyGeometry), @JsonProperty("point") POINT(GeometryType.POINT, FeatureCollector::point), @JsonProperty("line") @@ -18,6 +21,8 @@ public enum FeatureGeometry { POLYGON_CENTROID(GeometryType.POLYGON, FeatureCollector::centroid), @JsonProperty("line_centroid") LINE_CENTROID(GeometryType.LINE, FeatureCollector::centroid), + @JsonProperty("line_midpoint") + LINE_MIDPOINT(GeometryType.LINE, FeatureCollector::lineMidpoint), @JsonProperty("centroid") CENTROID(GeometryType.UNKNOWN, FeatureCollector::centroid), @JsonProperty("polygon_centroid_if_convex") @@ -25,7 +30,9 @@ public enum FeatureGeometry { @JsonProperty("polygon_point_on_surface") POLYGON_POINT_ON_SURFACE(GeometryType.POLYGON, FeatureCollector::pointOnSurface), @JsonProperty("point_on_line") - POINT_ON_LINE(GeometryType.LINE, FeatureCollector::pointOnSurface); + POINT_ON_LINE(GeometryType.LINE, FeatureCollector::pointOnSurface), + @JsonProperty("innermost_point") + INNERMOST_POINT(GeometryType.UNKNOWN, FeatureCollector::innermostPoint); public final GeometryType geometryType; public final BiFunction geometryFactory; diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/FeatureItem.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/FeatureItem.java index 151f9b23c6..7f94bc87ad 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/FeatureItem.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/FeatureItem.java @@ -10,7 +10,7 @@ public record FeatureItem( @JsonProperty("min_zoom") Object minZoom, @JsonProperty("max_zoom") Object maxZoom, @JsonProperty("min_size") Object minSize, - @JsonProperty(required = true) FeatureGeometry geometry, + @JsonProperty FeatureGeometry geometry, @JsonProperty("include_when") Object includeWhen, @JsonProperty("exclude_when") Object excludeWhen, Collection attributes @@ -20,4 +20,14 @@ public record FeatureItem( public Collection attributes() { return attributes == null ? List.of() : attributes; } + + @Override + public FeatureGeometry geometry() { + return geometry == null ? FeatureGeometry.ANY : geometry; + } + + @Override + public List source() { + return source == null ? List.of() : source; + } } diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/expression/ConfigExpression.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/expression/ConfigExpression.java index a6be26191c..4e4853bbd2 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/expression/ConfigExpression.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/expression/ConfigExpression.java @@ -5,6 +5,8 @@ import com.onthegomap.planetiler.expression.Expression; import com.onthegomap.planetiler.expression.MultiExpression; import com.onthegomap.planetiler.expression.Simplifiable; +import com.onthegomap.planetiler.expression.TypedGetter; +import com.onthegomap.planetiler.reader.WithTags; import java.util.List; import java.util.Objects; import java.util.function.Function; @@ -21,7 +23,15 @@ * @param Output type */ public interface ConfigExpression - extends Function, Simplifiable> { + extends Function, Simplifiable>, TypedGetter { + + ScriptEnvironment environment(); + + @Override + default Object apply(WithTags withTags, String tag) { + Class clazz = environment().clazz(); + return clazz.isInstance(withTags) ? apply(clazz.cast(withTags)) : null; + } static ConfigExpression script(Signature signature, String script) { return ConfigExpressionScript.parse(script, signature.in(), signature.out()); @@ -32,12 +42,16 @@ static ConfigExpression variable(Signature ConfigExpression constOf(O value) { - return new Const<>(value); + return new Const<>(any(), value); } static ConfigExpression coalesce( List> values) { - return new Coalesce<>(values); + return new Coalesce<>(any(), values); + } + + static Signature any() { + return new Signature<>(null, null); } static ConfigExpression getTag(Signature signature, @@ -70,12 +84,18 @@ static Signature signature(ScriptEnvironment< } /** An expression that always returns {@code value}. */ - record Const(O value) implements ConfigExpression { + record Const(Signature signature, O value) + implements ConfigExpression { @Override public O apply(I i) { return value; } + + @Override + public ScriptEnvironment environment() { + return signature.in; + } } /** An expression that returns the value associated with the first matching boolean expression. */ @@ -143,10 +163,18 @@ public ConfigExpression simplifyOnce() { public Match withDefaultValue(ConfigExpression newFallback) { return new Match<>(signature, multiExpression, newFallback); } + + @Override + public ScriptEnvironment environment() { + return signature.in; + } } /** An expression that returns the first non-null result of evaluating each child expression. */ - record Coalesce(List> children) + record Coalesce( + Signature signature, + List> children + ) implements ConfigExpression { @Override @@ -181,6 +209,11 @@ yield coalesce(result.stream().map(d -> { } }; } + + @Override + public ScriptEnvironment environment() { + return signature.in; + } } /** An expression that returns the value associated a given variable name at runtime. */ @@ -199,6 +232,11 @@ record Variable( public O apply(I i) { return TypeConversion.convert(i.apply(name), signature.out); } + + @Override + public ScriptEnvironment environment() { + return signature.in; + } } /** An expression that returns the value associated a given tag of the input feature at runtime. */ @@ -216,6 +254,11 @@ public O apply(I i) { public ConfigExpression simplifyOnce() { return new GetTag<>(signature, tag.simplifyOnce()); } + + @Override + public ScriptEnvironment environment() { + return signature.in; + } } /** An expression that returns the value associated a given argument at runtime. */ @@ -239,6 +282,11 @@ public ConfigExpression simplifyOnce() { return new GetArg<>(signature, key); } } + + @Override + public ScriptEnvironment environment() { + return signature.in; + } } /** An expression that converts the input to a desired output {@link DataType} at runtime. */ @@ -266,6 +314,11 @@ public ConfigExpression simplifyOnce() { return new Cast<>(signature, input.simplifyOnce(), output); } } + + @Override + public ScriptEnvironment environment() { + return signature.in; + } } record Signature(ScriptEnvironment in, Class out) { diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/expression/ConfigExpressionScript.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/expression/ConfigExpressionScript.java index 1aa77b0107..f83c2c2a55 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/expression/ConfigExpressionScript.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/expression/ConfigExpressionScript.java @@ -173,4 +173,9 @@ public ConfigExpression simplifyOnce() { } return this; } + + @Override + public ScriptEnvironment environment() { + return descriptor; + } } diff --git a/planetiler-custommap/src/main/resources/samples/owg_simple.yml b/planetiler-custommap/src/main/resources/samples/owg_simple.yml index 929d6de7a1..df4594af67 100644 --- a/planetiler-custommap/src/main/resources/samples/owg_simple.yml +++ b/planetiler-custommap/src/main/resources/samples/owg_simple.yml @@ -6,6 +6,7 @@ sources: water_polygons: type: shapefile url: https://osmdata.openstreetmap.de/download/water-polygons-split-3857.zip + projection: EPSG:3857 osm: type: osm url: geofabrik:monaco diff --git a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfigExpressionParserTest.java b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfigExpressionParserTest.java index 46c25652ea..825561b8e6 100644 --- a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfigExpressionParserTest.java +++ b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfigExpressionParserTest.java @@ -13,6 +13,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; class ConfigExpressionParserTest { @@ -42,11 +43,19 @@ void testConst(String input) { assertParse(input, constOf(1d), Double.class); } - @Test - void testVar() { - assertParse("${feature.id}", variable(FEATURE_SIGNATURE.withOutput(Integer.class), "feature.id"), Integer.class); - assertParse("${feature.id}", variable(FEATURE_SIGNATURE.withOutput(Long.class), "feature.id"), Long.class); - assertParse("${feature.id}", variable(FEATURE_SIGNATURE.withOutput(Double.class), "feature.id"), Double.class); + @ParameterizedTest + @CsvSource({ + "feature.id", + "feature.source", + "feature.source_layer", + "feature.osm_user_name" + }) + void testVar(String var) { + String script = "${" + var + "}"; + assertParse(script, variable(FEATURE_SIGNATURE.withOutput(Integer.class), var), Integer.class); + assertParse(script, variable(FEATURE_SIGNATURE.withOutput(Long.class), var), Long.class); + assertParse(script, variable(FEATURE_SIGNATURE.withOutput(Double.class), var), Double.class); + assertParse(script, variable(FEATURE_SIGNATURE.withOutput(String.class), var), String.class); } @Test 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 f38397f440..d4585bc1a0 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 @@ -3,6 +3,7 @@ import static com.onthegomap.planetiler.TestUtils.newLineString; import static com.onthegomap.planetiler.TestUtils.newPoint; import static com.onthegomap.planetiler.TestUtils.newPolygon; +import static com.onthegomap.planetiler.TestUtils.rectangle; import static java.util.Collections.emptyList; import static org.junit.jupiter.api.Assertions.*; @@ -34,6 +35,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; +import org.locationtech.jts.geom.Lineal; +import org.locationtech.jts.geom.Polygonal; import org.locationtech.jts.geom.Puntal; class ConfiguredFeatureTest { @@ -1114,6 +1117,7 @@ void testUseArgumentInSourceUrlPath() { "osm", DataSourceType.OSM, "geofabrik:boston", + null, null )), loadConfig(config).sources()); @@ -1122,6 +1126,7 @@ void testUseArgumentInSourceUrlPath() { "osm", DataSourceType.OSM, "geofabrik:rhode-island", + null, null )), loadConfig(config).sources()); @@ -1134,6 +1139,35 @@ void testUseArgumentInSourceUrlPath() { assertEquals("example.com_file.osm.pbf", loadConfig(config).sources().get(0).defaultFileUrl()); } + @ParameterizedTest + @CsvSource({ + "EPSG:3875, EPSG:3875", + "${'EPSG:' + '3875'}, EPSG:3875", + }) + void testSetProjection(String in, String out) { + var config = """ + sources: + osm: + type: osm + url: geofabrik:rhode-island + projection: %s + layers: + - id: testLayer + features: + - source: osm + geometry: point + """.formatted(in); + + this.planetilerConfig = PlanetilerConfig.from(Arguments.of(Map.of())); + assertEquals(List.of(new Source( + "osm", + DataSourceType.OSM, + "geofabrik:rhode-island", + null, + out + )), loadConfig(config).sources()); + } + @ParameterizedTest @CsvSource(""" 10,10 @@ -1288,6 +1322,62 @@ void testLineCentroid(String type) { }, 1); } + @ParameterizedTest + @ValueSource(strings = {"geometry: any", ""}) + void testAnyGeometry(String expression) { + var config = """ + sources: + osm: + type: osm + url: geofabrik:rhode-island + local_path: data/rhode-island.osm.pbf + layers: + - id: testLayer + features: + - source: osm + %s + """.formatted(expression).strip(); + this.planetilerConfig = PlanetilerConfig.from(Arguments.of(Map.of())); + testLinestring(config, Map.of( + ), feature -> { + assertInstanceOf(Lineal.class, feature.getGeometry()); + }, 1); + testPoint(config, Map.of( + ), feature -> { + assertInstanceOf(Puntal.class, feature.getGeometry()); + }, 1); + testPolygon(config, Map.of( + ), feature -> { + assertInstanceOf(Polygonal.class, feature.getGeometry()); + }, 1); + } + + @ParameterizedTest + @ValueSource(strings = {"source: []", ""}) + void testAnySource(String expression) { + var config = """ + sources: + osm: + type: osm + url: geofabrik:rhode-island + local_path: data/rhode-island.osm.pbf + layers: + - id: testLayer + features: + - geometry: point + %s + """.formatted(expression).strip(); + this.planetilerConfig = PlanetilerConfig.from(Arguments.of(Map.of())); + testFeature(config, SimpleFeature.createFakeOsmFeature(newPoint(0, 0), Map.of( + ), "osm", null, 1, emptyList(), OSM_INFO), feature -> { + assertInstanceOf(Puntal.class, feature.getGeometry()); + }, 1); + testFeature(config, SimpleFeature.createFakeOsmFeature(newPoint(0, 0), Map.of( + ), "other", null, 1, emptyList(), OSM_INFO), feature -> { + assertInstanceOf(Puntal.class, feature.getGeometry()); + }, 1); + } + @Test void testWikidataParse() { var config = """ @@ -1321,4 +1411,111 @@ void testWikidataParse() { assertEquals(Map.of("wikidata", 0L), feature.getAttrsAtZoom(14)); }, 1); } + + @ParameterizedTest + @CsvSource(value = { + "${feature.id}: 1", + "${feature.id + 1}: 2", + "${feature.id}: [1, 3]", + "${feature.source_layer}: layer", + "${ feature . source_layer }: [layer, layer2]", + "${feature.osm_changeset}: 2", + "${feature.osm_version}: 5", + "${feature.osm_timestamp}: 3", + "${feature.osm_user_id}: 4", + "${feature.osm_user_name}: user", + "${feature.osm_type}: way", + }, delimiter = '\t') + void testLeftHandSideExpression(String matchString) { + var config = """ + sources: + osm: + type: osm + url: geofabrik:rhode-island + local_path: data/rhode-island.osm.pbf + layers: + - id: testLayer + features: + - source: osm + include_when: + %s + """.formatted(matchString); + var sfMatch = + SimpleFeature.createFakeOsmFeature(rectangle(0, 1), Map.of(), "osm", "layer", 1, emptyList(), + new OsmElement.Info(2, 3, 4, 5, "user")); + var sfNoMatch = + SimpleFeature.createFakeOsmFeature(newPoint(0, 0), Map.of(), "osm", "other layer", 2, emptyList(), + new OsmElement.Info(6, 7, 8, 9, "other user")); + testFeature(config, sfMatch, any -> { + }, 1); + testFeature(config, sfNoMatch, any -> { + }, 0); + } + + + @ParameterizedTest + @CsvSource(value = { + "${feature.osm_user_name}: __any__", + "${feature.osm_user_name}: null", + "${feature.source_layer}: __any__", + "${feature.source_layer}: null", + }, delimiter = '\t') + void testLeftHandSideExpressionMatchAny(String matchString) { + var config = """ + sources: + osm: + type: osm + url: geofabrik:rhode-island + local_path: data/rhode-island.osm.pbf + layers: + - id: testLayer + features: + - source: osm + include_when: + %s + """.formatted(matchString); + var sfMatch = + SimpleFeature.createFakeOsmFeature(rectangle(0, 1), Map.of(), "osm", "layer", 1, emptyList(), + new OsmElement.Info(2, 3, 4, 5, "user")); + var sfNoMatch = + SimpleFeature.createFakeOsmFeature(newPoint(0, 0), Map.of(), "osm", null, 2, emptyList(), + new OsmElement.Info(6, 7, 8, 9, "")); + testFeature(config, sfMatch, any -> { + }, 1); + testFeature(config, sfNoMatch, any -> { + }, 0); + } + + @ParameterizedTest + @CsvSource(value = { + "${feature.osm_user_name}: ''", + "${feature.osm_user_name}: ['']", + "${feature.source_layer}: ''", + "${feature.source_layer}: ['']", + }, delimiter = '\t') + void testLeftHandSideExpressionMatchNone(String matchString) { + var config = """ + sources: + osm: + type: osm + url: geofabrik:rhode-island + local_path: data/rhode-island.osm.pbf + layers: + - id: testLayer + features: + - source: osm + include_when: + %s + """.formatted(matchString); + var sfMatch = + SimpleFeature.createFakeOsmFeature(rectangle(0, 1), Map.of(), "osm", "layer", 1, emptyList(), + new OsmElement.Info(2, 3, 4, 5, "user")); + var sfNoMatch = + SimpleFeature.createFakeOsmFeature(newPoint(0, 0), Map.of(), "osm", null, 2, emptyList(), + new OsmElement.Info(6, 7, 8, 9, "")); + testFeature(config, sfMatch, any -> { + }, 0); + testFeature(config, sfNoMatch, any -> { + }, 1); + } } diff --git a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfiguredMapTest.java b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfiguredMapTest.java index 8b2bffa1fd..a8b7493d0d 100644 --- a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfiguredMapTest.java +++ b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfiguredMapTest.java @@ -81,11 +81,11 @@ void ensureValidGeometries() throws Exception { } } - // @Test --TODO FIX after adding water layer + @Test void testContainsOceanPolyons() { assertMinFeatures("water", Map.of( "natural", "water" - ), 0, 1, Polygon.class); + ), 6, 1, Polygon.class); } @Test diff --git a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/expression/ConfigExpressionTest.java b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/expression/ConfigExpressionTest.java index 4d0bdad1d7..c6cce6dbaa 100644 --- a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/expression/ConfigExpressionTest.java +++ b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/expression/ConfigExpressionTest.java @@ -5,23 +5,25 @@ import static com.onthegomap.planetiler.custommap.TestContexts.PROCESS_FEATURE; import static com.onthegomap.planetiler.custommap.TestContexts.ROOT_CONTEXT; import static com.onthegomap.planetiler.custommap.expression.ConfigExpression.*; -import static com.onthegomap.planetiler.expression.Expression.matchAny; -import static com.onthegomap.planetiler.expression.Expression.or; +import static com.onthegomap.planetiler.expression.Expression.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.google.common.collect.Lists; import com.onthegomap.planetiler.config.Arguments; +import com.onthegomap.planetiler.custommap.BooleanExpressionParser; import com.onthegomap.planetiler.custommap.Contexts; import com.onthegomap.planetiler.custommap.TagValueProducer; import com.onthegomap.planetiler.custommap.TestContexts; import com.onthegomap.planetiler.expression.DataType; -import com.onthegomap.planetiler.expression.Expression; import com.onthegomap.planetiler.expression.MultiExpression; import com.onthegomap.planetiler.reader.SimpleFeature; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; class ConfigExpressionTest { private static final ConfigExpression.Signature ROOT = signature(ROOT_CONTEXT, Integer.class); @@ -104,8 +106,8 @@ void testMatch() { FEATURE_SIGNATURE, MultiExpression.of(List.of( MultiExpression.entry(script(FEATURE_SIGNATURE, "1 + size(feature.tags.a)"), - Expression.matchAny("a", "b")), - MultiExpression.entry(constOf(1), Expression.matchAny("a", "c")) + matchAny("a", "b")), + MultiExpression.entry(constOf(1), matchAny("a", "c")) )) ).apply(context)); } @@ -133,6 +135,96 @@ void testSimplifyCelFunctionThatJustAccessesVar() { ); } + @Test + void testCantSimplifyDynamicLeftExpression() { + assertEquals( + match(FEATURE_SIGNATURE, MultiExpression.of(List.of( + MultiExpression.entry(constOf(1), + matchAnyTyped(null, script(FEATURE_SIGNATURE.withOutput(Object.class), "feature.id + 1"), List.of(1, 2) + ))))), + + match(FEATURE_SIGNATURE, MultiExpression.of(List.of( + MultiExpression.entry(constOf(1), + BooleanExpressionParser.parse(Map.of("${feature.id + 1}", List.of(1, 2)), null, FEATURE_SIGNATURE.in()) + )))).simplify() + ); + } + + @ParameterizedTest + @CsvSource({ + "feature.id", + "feature.osm_user_name", + "feature.osm_type", + }) + void testSimplifySimpleLeftExpression(String varname) { + assertEquals( + match(FEATURE_SIGNATURE, MultiExpression.of(List.of( + MultiExpression.entry(constOf(1), + matchAnyTyped(null, variable(FEATURE_SIGNATURE.withOutput(Object.class), varname), List.of(1, 2) + ))))), + + match(FEATURE_SIGNATURE, MultiExpression.of(List.of( + MultiExpression.entry(constOf(1), + BooleanExpressionParser.parse(Map.of("${" + varname + "}", List.of(1, 2)), null, FEATURE_SIGNATURE.in()) + )))).simplify() + ); + } + + @Test + void testSimplifyExpressionToMatchSource() { + assertEquals( + match(FEATURE_SIGNATURE, MultiExpression.of(List.of( + MultiExpression.entry(constOf(1), + or(matchSource("source1"), matchSource("source2")) + )))), + + match(FEATURE_SIGNATURE, MultiExpression.of(List.of( + MultiExpression.entry(constOf(1), + BooleanExpressionParser.parse(Map.of("${feature.source}", List.of("source1", "source2")), null, + FEATURE_SIGNATURE.in()) + )))).simplify() + ); + } + + @Test + void testSimplifyExpressionToMatchSourceLayer() { + assertEquals( + match(FEATURE_SIGNATURE, MultiExpression.of(List.of( + MultiExpression.entry(constOf(1), + or(matchSourceLayer("layer1"), matchSourceLayer("layer2")) + )))), + + match(FEATURE_SIGNATURE, MultiExpression.of(List.of( + MultiExpression.entry(constOf(1), + BooleanExpressionParser.parse(Map.of("${feature.source_layer}", List.of("layer1", "layer2")), null, + FEATURE_SIGNATURE.in()) + )))).simplify() + ); + } + + @Test + void testDontSimplifyOtherSourceLayerCases() { + testDontSimplifyOtherSourceLayerCases("__any__", List.of()); + testDontSimplifyOtherSourceLayerCases("", List.of("")); + testDontSimplifyOtherSourceLayerCases("prefix%", List.of("prefix%")); + } + + private static void testDontSimplifyOtherSourceLayerCases(String in, List out) { + assertEquals( + match(FEATURE_SIGNATURE, MultiExpression.of(List.of( + MultiExpression.entry(constOf(1), + matchAnyTyped(null, variable(FEATURE_SIGNATURE.withOutput(Object.class), "feature.source_layer"), + out + ))))), + + match(FEATURE_SIGNATURE, MultiExpression.of(List.of( + MultiExpression.entry(constOf(1), + BooleanExpressionParser.parse(Map.of("${feature.source_layer}", Lists.newArrayList(in)), null, + FEATURE_SIGNATURE.in()) + )))).simplify() + ); + } + @Test void testSimplifyCoalesce() { assertEquals( diff --git a/planetiler-examples/standalone.pom.xml b/planetiler-examples/standalone.pom.xml index 5132e9ff65..e2720815e8 100644 --- a/planetiler-examples/standalone.pom.xml +++ b/planetiler-examples/standalone.pom.xml @@ -12,7 +12,7 @@ 21 21 0.8.3-SNAPSHOT - 5.11.2 + 5.11.3 com.onthegomap.planetiler.examples.BikeRouteOverlay diff --git a/pom.xml b/pom.xml index 0d14d94af9..027dccfc7a 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ 21 true 2.18.0 - 5.11.2 + 5.11.3 1.20.0 https://sonarcloud.io onthegomap @@ -126,7 +126,7 @@ org.mockito mockito-core - 5.14.1 + 5.14.2 test @@ -299,7 +299,7 @@ com.google.cloud.tools jib-maven-plugin - 3.4.3 + 3.4.4 true @@ -308,7 +308,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.10.0 + 3.10.1 true -missing