children) {
+ static Or or(List extends Expression> 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 extends Expression> 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 extends Expression> children) implements Expression {
@Override
public String generateJavaCode() {
@@ -307,7 +305,7 @@ public Expression simplifyOnce() {
}
}
- record Or(List children) implements Expression {
+ record Or(List extends Expression> 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 extends ConfigExpression> children)
+ record Coalesce(
+ Signature signature,
+ List extends ConfigExpression> 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