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/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 7dfdf6e842..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 @@ -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();
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/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-custommap/README.md b/planetiler-custommap/README.md
index d89d9d21e0..d3059f0965 100644
--- a/planetiler-custommap/README.md
+++ b/planetiler-custommap/README.md
@@ -221,12 +221,15 @@ A feature is a defined set of objects that meet a specified filter criteria.
- `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 437d691c16..636c6cf52f 100644
--- a/planetiler-custommap/planetiler.schema.json
+++ b/planetiler-custommap/planetiler.schema.json
@@ -353,10 +353,12 @@
"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": {
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 a0575046df..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
@@ -21,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")
@@ -28,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