Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add line midpoint geometry type #1072

Merged
merged 1 commit into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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()];
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)));
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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));
Expand Down
5 changes: 4 additions & 1 deletion planetiler-custommap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion planetiler-custommap/planetiler.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ 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")
POLYGON_CENTROID_IF_CONVEX(GeometryType.POLYGON, FeatureCollector::centroidIfConvex),
@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<FeatureCollector, String, FeatureCollector.Feature> geometryFactory;
Expand Down
Loading