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 Visvalingam Whyatt Simplifier #1109

Merged
merged 9 commits into from
Nov 25, 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
@@ -0,0 +1,84 @@
package com.onthegomap.planetiler.benchmarks;

import com.onthegomap.planetiler.geo.DouglasPeuckerSimplifier;
import com.onthegomap.planetiler.geo.VWSimplifier;
import com.onthegomap.planetiler.util.Format;
import com.onthegomap.planetiler.util.FunctionThatThrows;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.Duration;
import org.locationtech.jts.geom.CoordinateXY;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.util.GeometricShapeFactory;

public class BenchmarkSimplify {
private static int numLines;

public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
time(" DP(0.1)", geom -> DouglasPeuckerSimplifier.simplify(geom, 0.1));
time(" DP(1)", geom -> DouglasPeuckerSimplifier.simplify(geom, 1));
time(" DP(20)", geom -> DouglasPeuckerSimplifier.simplify(geom, 20));
time(" JTS VW(0)", geom -> org.locationtech.jts.simplify.VWSimplifier.simplify(geom, 0.01));
time("JTS VW(0.1)", geom -> org.locationtech.jts.simplify.VWSimplifier.simplify(geom, 0.1));
time(" JTS VW(1)", geom -> org.locationtech.jts.simplify.VWSimplifier.simplify(geom, 1));
time(" JTS VW(20)", geom -> org.locationtech.jts.simplify.VWSimplifier.simplify(geom, 20));
time(" VW(0)", geom -> new VWSimplifier().setTolerance(0).setWeight(0.7).transform(geom));
time(" VW(0.1)", geom -> new VWSimplifier().setTolerance(0.1).setWeight(0.7).transform(geom));
time(" VW(1)", geom -> new VWSimplifier().setTolerance(1).setWeight(0.7).transform(geom));
time(" VW(20)", geom -> new VWSimplifier().setTolerance(20).setWeight(0.7).transform(geom));
}
System.err.println(numLines);
}

private static void time(String name, FunctionThatThrows<Geometry, Geometry> fn) throws Exception {
System.err.println(String.join("\t",
name,
timePerSec(makeLines(2), fn),
timePerSec(makeLines(10), fn),
timePerSec(makeLines(50), fn),
timePerSec(makeLines(100), fn),
timePerSec(makeLines(10_000), fn)
));
}

private static String timePerSec(Geometry geometry, FunctionThatThrows<Geometry, Geometry> fn)
throws Exception {
long start = System.nanoTime();
long end = start + Duration.ofSeconds(1).toNanos();
int num = 0;
boolean first = true;
for (; System.nanoTime() < end;) {
numLines += fn.apply(geometry).getNumPoints();
if (first) {
first = false;
}
num++;
}
return Format.defaultInstance()
.numeric(Math.round(num * 1d / ((System.nanoTime() - start) * 1d / Duration.ofSeconds(1).toNanos())), true);
}

private static String timeMillis(Geometry geometry, FunctionThatThrows<Geometry, Geometry> fn)
throws Exception {
long start = System.nanoTime();
long end = start + Duration.ofSeconds(1).toNanos();
int num = 0;
for (; System.nanoTime() < end;) {
numLines += fn.apply(geometry).getNumPoints();
num++;
}
// equivalent of toPrecision(3)
long nanosPer = (System.nanoTime() - start) / num;
var bd = new BigDecimal(nanosPer, new MathContext(3));
return Format.padRight(Duration.ofNanos(bd.longValue()).toString().replace("PT", ""), 6);
}

private static Geometry makeLines(int parts) {
var shapeFactory = new GeometricShapeFactory();
shapeFactory.setNumPoints(parts);
shapeFactory.setCentre(new CoordinateXY(0, 0));
shapeFactory.setSize(10);
return shapeFactory.createCircle();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.geo.GeometryType;
import com.onthegomap.planetiler.geo.SimplifyMethod;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.reader.Struct;
import com.onthegomap.planetiler.render.FeatureRenderer;
Expand Down Expand Up @@ -502,6 +503,9 @@ public final class Feature implements WithZoomRange<Feature>, WithAttrs<Feature>
private double pixelToleranceAtMaxZoom = config.simplifyToleranceAtMaxZoom();
private ZoomFunction<Number> pixelTolerance = null;

private SimplifyMethod defaultSimplifyMethod = SimplifyMethod.DOUGLAS_PEUCKER;
private ZoomFunction<SimplifyMethod> simplifyMethod = null;

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

Expand Down Expand Up @@ -714,6 +718,28 @@ public double getPixelToleranceAtZoom(int zoom) {
ZoomFunction.applyAsDoubleOrElse(pixelTolerance, zoom, defaultPixelTolerance);
}

/**
* Sets the fallback line and polygon simplify method when not overriden by *
* {@link #setSimplifyMethodOverrides(ZoomFunction)}.
*/
public FeatureCollector.Feature setSimplifyMethod(SimplifyMethod strategy) {
defaultSimplifyMethod = strategy;
return this;
}

/** Set simplification algorithm to use at different zoom levels. */
public FeatureCollector.Feature setSimplifyMethodOverrides(ZoomFunction<SimplifyMethod> overrides) {
simplifyMethod = overrides;
return this;
}

/**
* Returns the simplification method for lines and polygons in tile pixels at {@code zoom}.
*/
public SimplifyMethod getSimplifyMethodAtZoom(int zoom) {
return ZoomFunction.applyOrElse(simplifyMethod, zoom, defaultSimplifyMethod);
}

/**
* Sets the simplification tolerance for lines and polygons in tile pixels below the maximum zoom-level of the map.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package com.onthegomap.planetiler.collection;

import java.util.Arrays;
import java.util.function.IntBinaryOperator;

/**
* A min-heap stored in an array where each element has 4 children.
* <p>
* This is about 5-10% faster than the standard binary min-heap for the case of merging sorted lists.
* <p>
* Ported from <a href=
* "https://github.com/graphhopper/graphhopper/blob/master/core/src/main/java/com/graphhopper/coll/MinHeapWithUpdate.java">GraphHopper</a>
* and:
* <ul>
* <li>modified to use {@code double} values instead of {@code float}</li>
* <li>extracted a common interface for subclass implementations</li>
* <li>modified so that each element has 4 children instead of 2 (improves performance by 5-10%)</li>
* <li>performance improvements to minimize array lookups</li>
* </ul>
*
* @see <a href="https://en.wikipedia.org/wiki/D-ary_heap">d-ary heap (wikipedia)</a>
*/
class ArrayDoubleMinHeap implements DoubleMinHeap {
protected static final int NOT_PRESENT = -1;
protected final int[] posToId;
protected final int[] idToPos;
protected final double[] posToValue;
protected final int max;
protected int size;
private final IntBinaryOperator tieBreaker;

/**
* @param elements the number of elements that can be stored in this heap. Currently the heap cannot be resized or
* shrunk/trimmed after initial creation. elements-1 is the maximum id that can be stored in this heap
*/
ArrayDoubleMinHeap(int elements, IntBinaryOperator tieBreaker) {
// we use an offset of one to make the arithmetic a bit simpler/more efficient, the 0th elements are not used!
posToId = new int[elements + 1];
idToPos = new int[elements + 1];
Arrays.fill(idToPos, NOT_PRESENT);
posToValue = new double[elements + 1];
posToValue[0] = Double.NEGATIVE_INFINITY;
this.max = elements;
this.tieBreaker = tieBreaker;
}

private static int firstChild(int index) {
return (index << 2) - 2;
}

private static int parent(int index) {
return (index + 2) >> 2;
}

@Override
public int size() {
return size;
}

@Override
public boolean isEmpty() {
return size == 0;
}

@Override
public void push(int id, double value) {
checkIdInRange(id);
if (size == max) {
throw new IllegalStateException("Cannot push anymore, the heap is already full. size: " + size);
}
if (contains(id)) {
throw new IllegalStateException("Element with id: " + id +
" was pushed already, you need to use the update method if you want to change its value");
}
size++;
posToId[size] = id;
idToPos[id] = size;
posToValue[size] = value;
percolateUp(size);
}

@Override
public boolean contains(int id) {
checkIdInRange(id);
return idToPos[id] != NOT_PRESENT;
}

@Override
public void update(int id, double value) {
checkIdInRange(id);
int pos = idToPos[id];
if (pos < 0) {
throw new IllegalStateException(
"The heap does not contain: " + id + ". Use the contains method to check this before calling update");
}
double prev = posToValue[pos];
posToValue[pos] = value;
int cmp = compareIdPos(value, prev, id, pos);
if (cmp > 0) {
percolateDown(pos);
} else if (cmp < 0) {
percolateUp(pos);
}
}

@Override
public void updateHead(double value) {
posToValue[1] = value;
percolateDown(1);
}

@Override
public int peekId() {
return posToId[1];
}

@Override
public double peekValue() {
return posToValue[1];
}

@Override
public int poll() {
int id = peekId();
posToId[1] = posToId[size];
posToValue[1] = posToValue[size];
idToPos[posToId[1]] = 1;
idToPos[id] = NOT_PRESENT;
size--;
percolateDown(1);
return id;
}

@Override
public void clear() {
for (int i = 1; i <= size; i++) {
idToPos[posToId[i]] = NOT_PRESENT;
}
size = 0;
}

private void percolateUp(int pos) {
assert pos != 0;
if (pos == 1) {
return;
}
final int id = posToId[pos];
final double val = posToValue[pos];
// the finish condition (index==0) is covered here automatically because we set vals[0]=-inf
int parent;
double parentValue;
while (compareIdPos(val, parentValue = posToValue[parent = parent(pos)], id, parent) < 0) {
posToValue[pos] = parentValue;
idToPos[posToId[pos] = posToId[parent]] = pos;
pos = parent;
}
posToId[pos] = id;
posToValue[pos] = val;
idToPos[posToId[pos]] = pos;
}

private void checkIdInRange(int id) {
if (id < 0 || id >= max) {
throw new IllegalArgumentException("Illegal id: " + id + ", legal range: [0, " + max + "[");
}
}

private void percolateDown(int pos) {
if (size == 0) {
return;
}
assert pos > 0;
assert pos <= size;
final int id = posToId[pos];
final double value = posToValue[pos];
int child;
while ((child = firstChild(pos)) <= size) {
// optimization: this is a very hot code path for performance of k-way merging,
// so manually-unroll the loop over the 4 child elements to find the minimum value
int minChild = child;
double minValue = posToValue[child], childValue;
if (++child <= size) {
if (comparePosPos(childValue = posToValue[child], minValue, child, minChild) < 0) {
minChild = child;
minValue = childValue;
}
if (++child <= size) {
if (comparePosPos(childValue = posToValue[child], minValue, child, minChild) < 0) {
minChild = child;
minValue = childValue;
}
if (++child <= size &&
comparePosPos(childValue = posToValue[child], minValue, child, minChild) < 0) {
minChild = child;
minValue = childValue;
}
}
}
if (compareIdPos(value, minValue, id, minChild) <= 0) {
break;
}
posToValue[pos] = minValue;
idToPos[posToId[pos] = posToId[minChild]] = pos;
pos = minChild;
}
posToId[pos] = id;
posToValue[pos] = value;
idToPos[id] = pos;
}

private int comparePosPos(double val1, double val2, int pos1, int pos2) {
if (val1 < val2) {
return -1;
} else if (val1 == val2 && val1 != Double.NEGATIVE_INFINITY) {
return tieBreaker.applyAsInt(posToId[pos1], posToId[pos2]);
}
return 1;
}

private int compareIdPos(double val1, double val2, int id1, int pos2) {
if (val1 < val2) {
return -1;
} else if (val1 == val2 && val1 != Double.NEGATIVE_INFINITY) {
return tieBreaker.applyAsInt(id1, posToId[pos2]);
}
return 1;
}

}
Loading
Loading