Skip to content

Commit

Permalink
add line midpoint and innermost point for lines (#1072)
Browse files Browse the repository at this point in the history
  • Loading branch information
msbarry authored Oct 21, 2024
1 parent 6233725 commit 78db782
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 17 deletions.
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 @@ -225,12 +225,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 @@ -367,10 +367,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

0 comments on commit 78db782

Please sign in to comment.