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

GeometryPipeline #1118

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
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 @@ -5,6 +5,7 @@
import com.onthegomap.planetiler.config.PlanetilerConfig;
import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.geo.GeometryPipeline;
import com.onthegomap.planetiler.geo.GeometryType;
import com.onthegomap.planetiler.geo.SimplifyMethod;
import com.onthegomap.planetiler.reader.SourceFeature;
Expand Down Expand Up @@ -506,6 +507,9 @@ public final class Feature implements WithZoomRange<Feature>, WithAttrs<Feature>
private SimplifyMethod defaultSimplifyMethod = SimplifyMethod.DOUGLAS_PEUCKER;
private ZoomFunction<SimplifyMethod> simplifyMethod = null;

private GeometryPipeline defaultGeometryPipeline = null;
private ZoomFunction<GeometryPipeline> geometryPipelineByZoom = null;

private String numPointsAttr = null;
private List<OverrideCommand> partialOverrides = null;

Expand Down Expand Up @@ -740,6 +744,34 @@ public SimplifyMethod getSimplifyMethodAtZoom(int zoom) {
return ZoomFunction.applyOrElse(simplifyMethod, zoom, defaultSimplifyMethod);
}

/**
* Sets the default pipeline to apply to geometries scaled to tile coordinates right before emitting vector tile
* features. This function gets run instead of simplification, so should include any simplification if you want
* that.
*/
public Feature transformScaledGeometry(GeometryPipeline pipeline) {
this.defaultGeometryPipeline = pipeline;
return this;
}

/**
* Sets the per-zoom geometry pipeline to apply to geometries scaled to tile coordinates right before emitting
* vector tile features. These functions get run instead of simplification, so should include any simplification if
* you want that.
*/
public Feature transformScaledGeometryByZoom(ZoomFunction<GeometryPipeline> overrides) {
this.geometryPipelineByZoom = overrides;
return this;
}

/**
* Returns the geometry transform function to apply to scaled geometries at {@code zoom}, or null to not update them
* at all.
*/
public GeometryPipeline getScaledGeometryTransformAtZoom(int zoom) {
return ZoomFunction.applyOrElse(geometryPipelineByZoom, zoom, defaultGeometryPipeline);
}

/**
* Sets the simplification tolerance for lines and polygons in tile pixels below the maximum zoom-level of the map.
* <p>
Expand Down Expand Up @@ -1091,6 +1123,10 @@ Partial withAttr(String key, Object value) {
return rangesWithGeometries;
}

public SourceFeature source() {
return source;
}


/**
* A builder that can be used to configure linear-scoped attributes for a partial segment of a line feature.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,31 @@
* accurately and performance improvement to put the results in a {@link MutableCoordinateSequence} which uses a
* primitive double array instead of allocating lots of {@link Coordinate} objects.
*/
public class DouglasPeuckerSimplifier {
public class DouglasPeuckerSimplifier extends GeometryTransformer implements GeometryPipeline {

private final double sqTolerance;

public DouglasPeuckerSimplifier(double distanceTolerance) {
this.sqTolerance = distanceTolerance * Math.abs(distanceTolerance);
}

@Override
public Geometry apply(Geometry input) {
return simplify(input);
}

/**
* Returns a copy of {@code geom}, simplified using Douglas Peucker Algorithm.
*
* @param geom the geometry to simplify (will not be modified)
* @return the simplified geometry
*/
public Geometry simplify(Geometry geom) {
if (geom.isEmpty() || (sqTolerance < 0.0)) {
return geom.copy();
}
return transform(geom);
}

/**
* Returns a copy of {@code geom}, simplified using Douglas Peucker Algorithm.
Expand All @@ -44,7 +68,7 @@ public static Geometry simplify(Geometry geom, double distanceTolerance) {
return geom.copy();
}

return (new DPTransformer(distanceTolerance)).transform(geom);
return (new DouglasPeuckerSimplifier(distanceTolerance)).simplify(geom);
}

/**
Expand All @@ -60,160 +84,154 @@ public static List<Coordinate> simplify(List<Coordinate> coords, double distance
return List.of();
}

return (new DPTransformer(distanceTolerance)).transformCoordinateList(coords, area);
return (new DouglasPeuckerSimplifier(distanceTolerance)).transformCoordinateList(coords, area);
}

private static class DPTransformer extends GeometryTransformer {

private final double sqTolerance;

private DPTransformer(double distanceTolerance) {
this.sqTolerance = distanceTolerance * distanceTolerance;
}

/**
* Returns the square of the number of units that (px, p1) is away from the line segment from (p1x, py1) to (p2x,
* p2y).
*/
private static double getSqSegDist(double px, double py, double p1x, double p1y, double p2x, double p2y) {
/**
* Returns the square of the number of units that (px, p1) is away from the line segment from (p1x, py1) to (p2x,
* p2y).
*/
private static double getSqSegDist(double px, double py, double p1x, double p1y, double p2x, double p2y) {

double x = p1x,
y = p1y,
dx = p2x - x,
dy = p2y - y;
double x = p1x,
y = p1y,
dx = p2x - x,
dy = p2y - y;

if (dx != 0d || dy != 0d) {
if (dx != 0d || dy != 0d) {

double t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);
double t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);

if (t > 1) {
x = p2x;
y = p2y;
if (t > 1) {
x = p2x;
y = p2y;

} else if (t > 0) {
x += dx * t;
y += dy * t;
}
} else if (t > 0) {
x += dx * t;
y += dy * t;
}

dx = px - x;
dy = py - y;

return dx * dx + dy * dy;
}

private void subsimplify(List<Coordinate> in, List<Coordinate> out, int first, int last, int numForcedPoints) {
// numForcePoints lets us keep some points even if they are below simplification threshold
boolean force = numForcedPoints > 0;
double maxSqDist = force ? -1 : sqTolerance;
int index = -1;
Coordinate p1 = in.get(first);
Coordinate p2 = in.get(last);
double p1x = p1.x;
double p1y = p1.y;
double p2x = p2.x;
double p2y = p2.y;

int i = first + 1;
Coordinate furthest = null;
for (Coordinate coord : in.subList(first + 1, last)) {
double sqDist = getSqSegDist(coord.x, coord.y, p1x, p1y, p2x, p2y);

if (sqDist > maxSqDist) {
index = i;
furthest = coord;
maxSqDist = sqDist;
}
i++;
}
dx = px - x;
dy = py - y;

if (force || maxSqDist > sqTolerance) {
if (index - first > 1) {
subsimplify(in, out, first, index, numForcedPoints - 1);
}
out.add(furthest);
if (last - index > 1) {
subsimplify(in, out, index, last, numForcedPoints - 2);
}
return dx * dx + dy * dy;
}

private void subsimplify(List<Coordinate> in, List<Coordinate> out, int first, int last, int numForcedPoints) {
// numForcePoints lets us keep some points even if they are below simplification threshold
boolean force = numForcedPoints > 0;
double maxSqDist = force ? -1 : sqTolerance;
int index = -1;
Coordinate p1 = in.get(first);
Coordinate p2 = in.get(last);
double p1x = p1.x;
double p1y = p1.y;
double p2x = p2.x;
double p2y = p2.y;

int i = first + 1;
Coordinate furthest = null;
for (Coordinate coord : in.subList(first + 1, last)) {
double sqDist = getSqSegDist(coord.x, coord.y, p1x, p1y, p2x, p2y);

if (sqDist > maxSqDist) {
index = i;
furthest = coord;
maxSqDist = sqDist;
}
i++;
}

private void subsimplify(CoordinateSequence in, MutableCoordinateSequence out, int first, int last,
int numForcedPoints) {
// numForcePoints lets us keep some points even if they are below simplification threshold
boolean force = numForcedPoints > 0;
double maxSqDist = force ? -1 : sqTolerance;
int index = -1;
double p1x = in.getX(first);
double p1y = in.getY(first);
double p2x = in.getX(last);
double p2y = in.getY(last);

for (int i = first + 1; i < last; i++) {
double px = in.getX(i);
double py = in.getY(i);
double sqDist = getSqSegDist(px, py, p1x, p1y, p2x, p2y);

if (sqDist > maxSqDist) {
index = i;
maxSqDist = sqDist;
}
if (force || maxSqDist > sqTolerance) {
if (index - first > 1) {
subsimplify(in, out, first, index, numForcedPoints - 1);
}

if (force || maxSqDist > sqTolerance) {
if (index - first > 1) {
subsimplify(in, out, first, index, numForcedPoints - 1);
}
out.forceAddPoint(in.getX(index), in.getY(index));
if (last - index > 1) {
subsimplify(in, out, index, last, numForcedPoints - 2);
}
out.add(furthest);
if (last - index > 1) {
subsimplify(in, out, index, last, numForcedPoints - 2);
}
}
}

protected List<Coordinate> transformCoordinateList(List<Coordinate> coords, boolean area) {
if (coords.isEmpty()) {
return coords;
private void subsimplify(CoordinateSequence in, MutableCoordinateSequence out, int first, int last,
int numForcedPoints) {
// numForcePoints lets us keep some points even if they are below simplification threshold
boolean force = numForcedPoints > 0;
double maxSqDist = force ? -1 : sqTolerance;
int index = -1;
double p1x = in.getX(first);
double p1y = in.getY(first);
double p2x = in.getX(last);
double p2y = in.getY(last);

for (int i = first + 1; i < last; i++) {
double px = in.getX(i);
double py = in.getY(i);
double sqDist = getSqSegDist(px, py, p1x, p1y, p2x, p2y);

if (sqDist > maxSqDist) {
index = i;
maxSqDist = sqDist;
}
// make sure we include the first and last points even if they are closer than the simplification threshold
List<Coordinate> result = new ArrayList<>();
result.add(coords.getFirst());
// for polygons, additionally keep at least 2 intermediate points even if they are below simplification threshold
// to avoid collapse.
subsimplify(coords, result, 0, coords.size() - 1, area ? 2 : 0);
result.add(coords.getLast());
return result;
}

@Override
protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent) {
boolean area = parent instanceof LinearRing;
if (coords.size() == 0) {
return coords;
if (force || maxSqDist > sqTolerance) {
if (index - first > 1) {
subsimplify(in, out, first, index, numForcedPoints - 1);
}
out.forceAddPoint(in.getX(index), in.getY(index));
if (last - index > 1) {
subsimplify(in, out, index, last, numForcedPoints - 2);
}
// make sure we include the first and last points even if they are closer than the simplification threshold
MutableCoordinateSequence result = new MutableCoordinateSequence();
result.forceAddPoint(coords.getX(0), coords.getY(0));
// for polygons, additionally keep at least 2 intermediate points even if they are below simplification threshold
// to avoid collapse.
subsimplify(coords, result, 0, coords.size() - 1, area ? 2 : 0);
result.forceAddPoint(coords.getX(coords.size() - 1), coords.getY(coords.size() - 1));
return result;
}
}

@Override
protected Geometry transformPolygon(Polygon geom, Geometry parent) {
return geom.isEmpty() ? null : super.transformPolygon(geom, parent);
protected List<Coordinate> transformCoordinateList(List<Coordinate> coords, boolean area) {
int minPoints = area ? 4 : 2;
if (coords.size() <= minPoints) {
return coords;
}
// make sure we include the first and last points even if they are closer than the simplification threshold
List<Coordinate> result = new ArrayList<>();
result.add(coords.getFirst());
// for polygons, additionally keep at least 2 intermediate points even if they are below simplification threshold
// to avoid collapse.
subsimplify(coords, result, 0, coords.size() - 1, minPoints - 2);
result.add(coords.getLast());
return result;
}

@Override
protected Geometry transformLinearRing(LinearRing geom, Geometry parent) {
boolean removeDegenerateRings = parent instanceof Polygon;
Geometry simpResult = super.transformLinearRing(geom, parent);
if (removeDegenerateRings && !(simpResult instanceof LinearRing)) {
return null;
}
return simpResult;
@Override
protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent) {
boolean area = parent instanceof LinearRing;
int minPoints = area ? 4 : 2;
if (coords.size() <= minPoints) {
return coords;
}
// make sure we include the first and last points even if they are closer than the simplification threshold
MutableCoordinateSequence result = new MutableCoordinateSequence();
result.forceAddPoint(coords.getX(0), coords.getY(0));
// for polygons, additionally keep at least 2 intermediate points even if they are below simplification threshold
// to avoid collapse.
subsimplify(coords, result, 0, coords.size() - 1, minPoints - 2);
result.forceAddPoint(coords.getX(coords.size() - 1), coords.getY(coords.size() - 1));
return result;
}

@Override
protected Geometry transformPolygon(Polygon geom, Geometry parent) {
return geom.isEmpty() ? null : super.transformPolygon(geom, parent);
}

@Override
protected Geometry transformLinearRing(LinearRing geom, Geometry parent) {
boolean removeDegenerateRings = parent instanceof Polygon;
Geometry simpResult = super.transformLinearRing(geom, parent);
if (removeDegenerateRings && !(simpResult instanceof LinearRing)) {
return null;
}
return simpResult;
}
}
Loading
Loading