kopia lustrzana https://github.com/onthegomap/planetiler
Add Visvalingam Whyatt Simplifier (#1109)
rodzic
292dc78ac0
commit
cbeba1bc8f
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.onthegomap.planetiler.config.PlanetilerConfig;
|
|||
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;
|
||||
|
|
@ -502,6 +503,9 @@ public class FeatureCollector implements Iterable<FeatureCollector.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;
|
||||
|
||||
|
|
@ -714,6 +718,28 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Licensed to GraphHopper GmbH under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with this work for
|
||||
* additional information regarding copyright ownership.
|
||||
*
|
||||
* GraphHopper GmbH licenses this file to you under the Apache License,
|
||||
* Version 2.0 (the "License"); you may not use this file except in
|
||||
* compliance with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.onthegomap.planetiler.collection;
|
||||
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
/**
|
||||
* API for min-heaps that keeps track of {@code int} keys in a range from {@code [0, size)} ordered by {@code double}
|
||||
* values.
|
||||
* <p>
|
||||
* Ported from <a href=
|
||||
* "https://github.com/graphhopper/graphhopper/blob/master/core/src/main/java/com/graphhopper/coll/MinHeapWithUpdate.java">GraphHopper</a>
|
||||
* and modified to extract a common interface for subclass implementations.
|
||||
*/
|
||||
public interface DoubleMinHeap {
|
||||
/**
|
||||
* Returns a new min-heap where each element has 4 children backed by elements in an array.
|
||||
* <p>
|
||||
* This is slightly faster than a traditional binary min heap due to a shallower, more cache-friendly memory layout.
|
||||
*/
|
||||
static DoubleMinHeap newArrayHeap(int elements, IntBinaryOperator tieBreaker) {
|
||||
return new ArrayDoubleMinHeap(elements, tieBreaker);
|
||||
}
|
||||
|
||||
int size();
|
||||
|
||||
boolean isEmpty();
|
||||
|
||||
/**
|
||||
* Adds an element to the heap, the given id must not exceed the size specified in the constructor. Its illegal to
|
||||
* push the same id twice (unless it was polled/removed before). To update the value of an id contained in the heap
|
||||
* use the {@link #update} method.
|
||||
*/
|
||||
void push(int id, double value);
|
||||
|
||||
/**
|
||||
* @return true if the heap contains an element with the given id
|
||||
*/
|
||||
boolean contains(int id);
|
||||
|
||||
/**
|
||||
* Updates the element with the given id. The complexity of this method is O(log(N)), just like push/poll. Its illegal
|
||||
* to update elements that are not contained in the heap. Use {@link #contains} to check the existence of an id.
|
||||
*/
|
||||
void update(int id, double value);
|
||||
|
||||
/**
|
||||
* Updates the weight of the head element in the heap, pushing it down and bubbling up the new min element if
|
||||
* necessary.
|
||||
*/
|
||||
void updateHead(double value);
|
||||
|
||||
/**
|
||||
* @return the id of the next element to be polled, i.e. the same as calling poll() without removing the element
|
||||
*/
|
||||
int peekId();
|
||||
|
||||
double peekValue();
|
||||
|
||||
/**
|
||||
* Extracts the element with minimum value from the heap
|
||||
*/
|
||||
int poll();
|
||||
|
||||
void clear();
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.onthegomap.planetiler.geo;
|
||||
|
||||
public enum SimplifyMethod {
|
||||
RETAIN_IMPORTANT_POINTS,
|
||||
RETAIN_EFFECTIVE_AREAS,
|
||||
RETAIN_WEIGHTED_EFFECTIVE_AREAS;
|
||||
|
||||
public static final SimplifyMethod DOUGLAS_PEUCKER = RETAIN_IMPORTANT_POINTS;
|
||||
public static final SimplifyMethod VISVALINGAM_WHYATT = RETAIN_EFFECTIVE_AREAS;
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
package com.onthegomap.planetiler.geo;
|
||||
|
||||
import com.onthegomap.planetiler.collection.DoubleMinHeap;
|
||||
import java.util.function.Function;
|
||||
import org.locationtech.jts.geom.CoordinateSequence;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.LinearRing;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
import org.locationtech.jts.geom.util.GeometryTransformer;
|
||||
|
||||
/**
|
||||
* A utility to simplify geometries using Visvalingam Whyatt simplification algorithm without any attempt to repair
|
||||
* geometries that become invalid due to simplification.
|
||||
*/
|
||||
public class VWSimplifier extends GeometryTransformer implements Function<Geometry, Geometry> {
|
||||
|
||||
private double tolerance;
|
||||
private double k;
|
||||
|
||||
/** Sets the minimum effective triangle area created by 3 consecutive vertices in order to retain that vertex. */
|
||||
public VWSimplifier setTolerance(double tolerance) {
|
||||
this.tolerance = tolerance;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a penalty from {@code k=0} to {@code k=1} to drop more sharp corners from the resulting geometry.
|
||||
* <p>
|
||||
* {@code k=0} is the default to apply no penalty for corner sharpness and just drop based on effective triangle area
|
||||
* at the vertex. {@code k=0.7} is the recommended setting to drop corners based on weighted effective area.
|
||||
*/
|
||||
public VWSimplifier setWeight(double k) {
|
||||
this.k = k;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Geometry apply(Geometry geometry) {
|
||||
return transform(geometry);
|
||||
}
|
||||
|
||||
private class Vertex {
|
||||
int idx;
|
||||
double x;
|
||||
double y;
|
||||
double area;
|
||||
Vertex prev;
|
||||
Vertex next;
|
||||
|
||||
Vertex(int idx, CoordinateSequence seq) {
|
||||
this.idx = idx;
|
||||
this.x = seq.getX(idx);
|
||||
this.y = seq.getY(idx);
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
if (prev != null) {
|
||||
prev.next = next;
|
||||
}
|
||||
if (next != null) {
|
||||
next.prev = prev;
|
||||
}
|
||||
}
|
||||
|
||||
public double updateArea() {
|
||||
if (prev == null || next == null) {
|
||||
return area = Double.POSITIVE_INFINITY;
|
||||
}
|
||||
return area = weightedArea(prev.x, prev.y, x, y, next.x, next.y);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent) {
|
||||
boolean area = parent instanceof LinearRing;
|
||||
int num = coords.size();
|
||||
if (num == 0) {
|
||||
return coords;
|
||||
}
|
||||
|
||||
DoubleMinHeap heap = DoubleMinHeap.newArrayHeap(num, Integer::compare);
|
||||
Vertex[] points = new Vertex[num];
|
||||
// TODO
|
||||
// Stack<Vertex> intersecting = new Stack<>();
|
||||
Vertex prev = null;
|
||||
for (int i = 0; i < num; i++) {
|
||||
Vertex cur = new Vertex(i, coords);
|
||||
points[i] = cur;
|
||||
if (prev != null) {
|
||||
cur.prev = prev;
|
||||
prev.next = cur;
|
||||
heap.push(prev.idx, prev.updateArea());
|
||||
}
|
||||
prev = cur;
|
||||
}
|
||||
heap.push(prev.idx, prev.updateArea());
|
||||
|
||||
int left = num;
|
||||
int min = area ? 4 : 2;
|
||||
|
||||
while (!heap.isEmpty()) {
|
||||
var id = heap.poll();
|
||||
Vertex point = points[id];
|
||||
|
||||
if (point.area > tolerance || left <= min) {
|
||||
break;
|
||||
}
|
||||
// TODO
|
||||
// // Check that the new segment doesn’t intersect with
|
||||
// // any existing segments, except for the point’s
|
||||
// // immediate neighbours.
|
||||
// if (intersect(heap, point.previous, point.next))
|
||||
// intersecting.push(point);
|
||||
// continue
|
||||
// // Reattempt to process points that previously would
|
||||
// // have caused intersections when removed.
|
||||
// while (i = intersecting.pop()) heap.push(i)
|
||||
|
||||
point.remove();
|
||||
left--;
|
||||
if (point.prev != null) {
|
||||
heap.update(point.prev.idx, point.prev.updateArea());
|
||||
}
|
||||
if (point.next != null) {
|
||||
heap.update(point.next.idx, point.next.updateArea());
|
||||
}
|
||||
}
|
||||
MutableCoordinateSequence result = new MutableCoordinateSequence();
|
||||
for (Vertex point = points[0]; point != null; point = point.next) {
|
||||
result.forceAddPoint(point.x, point.y);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
private static double triangleArea(double ax, double ay, double bx, double by, double cx, double cy) {
|
||||
return Math.abs(((ay - cy) * (bx - cx) + (by - cy) * (cx - ax)) / 2);
|
||||
}
|
||||
|
||||
private static double cos(double ax, double ay, double bx, double by, double cx, double cy) {
|
||||
double den = Math.hypot(bx - ax, by - ay) * Math.hypot(cx - bx, cy - by),
|
||||
cos = 0;
|
||||
if (den > 0) {
|
||||
cos = Math.clamp((ax - bx) * (cx - bx) + (ay - by) * (cy - by) / den, -1, 1);
|
||||
}
|
||||
return cos;
|
||||
}
|
||||
|
||||
private double weight(double cos) {
|
||||
return (-cos) * k + 1;
|
||||
}
|
||||
|
||||
private double weightedArea(double ax, double ay, double bx, double by, double cx, double cy) {
|
||||
double area = triangleArea(ax, ay, bx, by, cx, cy);
|
||||
return k == 0 ? area : (area * weight(cos(ax, ay, bx, by, cx, cy)));
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,10 @@ import com.onthegomap.planetiler.config.PlanetilerConfig;
|
|||
import com.onthegomap.planetiler.geo.DouglasPeuckerSimplifier;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.geo.SimplifyMethod;
|
||||
import com.onthegomap.planetiler.geo.TileCoord;
|
||||
import com.onthegomap.planetiler.geo.TileExtents;
|
||||
import com.onthegomap.planetiler.geo.VWSimplifier;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
|
@ -194,6 +196,7 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
|
|||
int z, double minSize, boolean area) {
|
||||
double scale = 1 << z;
|
||||
double tolerance = feature.getPixelToleranceAtZoom(z) / 256d;
|
||||
SimplifyMethod simplifyMethod = feature.getSimplifyMethodAtZoom(z);
|
||||
double buffer = feature.getBufferPixelsAtZoom(z) / 256;
|
||||
TileExtents.ForZoom extents = config.bounds().tileExtents().getForZoom(z);
|
||||
|
||||
|
|
@ -201,7 +204,15 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
|
|||
// simplify only takes 4-5 minutes of wall time when generating the planet though, so not a big deal
|
||||
Geometry scaled = AffineTransformation.scaleInstance(scale, scale).transform(input);
|
||||
TiledGeometry sliced;
|
||||
Geometry geom = DouglasPeuckerSimplifier.simplify(scaled, tolerance);
|
||||
// TODO replace with geometry pipeline when available
|
||||
Geometry geom = switch (simplifyMethod) {
|
||||
case RETAIN_IMPORTANT_POINTS -> DouglasPeuckerSimplifier.simplify(scaled, tolerance);
|
||||
// DP tolerance is displacement, and VW tolerance is area, so square what the user entered to convert from
|
||||
// DP to VW tolerance
|
||||
case RETAIN_EFFECTIVE_AREAS -> new VWSimplifier().setTolerance(tolerance * tolerance).transform(scaled);
|
||||
case RETAIN_WEIGHTED_EFFECTIVE_AREAS ->
|
||||
new VWSimplifier().setWeight(0.7).setTolerance(tolerance * tolerance).transform(scaled);
|
||||
};
|
||||
List<List<CoordinateSequence>> groups = GeometryCoordinateSequences.extractGroups(geom, minSize);
|
||||
try {
|
||||
sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents);
|
||||
|
|
|
|||
|
|
@ -45,6 +45,15 @@ public interface ZoomFunction<T> extends IntFunction<T> {
|
|||
return result == null ? defaultValue : result.intValue();
|
||||
}
|
||||
|
||||
/** Invoke a function at a zoom level and returns {@code defaultValue} if the function or result were null. */
|
||||
static <T> T applyOrElse(ZoomFunction<T> fn, int zoom, T defaultValue) {
|
||||
if (fn == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
T result = fn.apply(zoom);
|
||||
return result == null ? defaultValue : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a zoom function that returns the value from the next higher key in {@code thresholds} or {@code null} if
|
||||
* over the max key.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.onthegomap.planetiler.config.PlanetilerConfig;
|
|||
import com.onthegomap.planetiler.files.ReadableFilesArchive;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.geo.SimplifyMethod;
|
||||
import com.onthegomap.planetiler.geo.TileCoord;
|
||||
import com.onthegomap.planetiler.geo.TileOrder;
|
||||
import com.onthegomap.planetiler.mbtiles.Mbtiles;
|
||||
|
|
@ -430,26 +431,37 @@ class PlanetilerTests {
|
|||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void testLineString(boolean anyGeom) throws Exception {
|
||||
@CsvSource({
|
||||
"false,RETAIN_IMPORTANT_POINTS",
|
||||
"false,RETAIN_EFFECTIVE_AREAS",
|
||||
"false,RETAIN_WEIGHTED_EFFECTIVE_AREAS",
|
||||
"true,RETAIN_IMPORTANT_POINTS",
|
||||
})
|
||||
void testLineString(boolean anyGeom, SimplifyMethod simplifyStrategy) throws Exception {
|
||||
double x1 = 0.5 + Z14_WIDTH / 2;
|
||||
double y1 = 0.5 + Z14_WIDTH / 2;
|
||||
double x2 = x1 + Z14_WIDTH;
|
||||
double y2 = y1 + Z14_WIDTH;
|
||||
double ymid = (y1 + y2) / 2;
|
||||
double xmid = (x1 + x2) / 2;
|
||||
double lat1 = GeoUtils.getWorldLat(y1);
|
||||
double lng1 = GeoUtils.getWorldLon(x1);
|
||||
double latMid = GeoUtils.getWorldLat(ymid);
|
||||
double lngMid = GeoUtils.getWorldLon(xmid);
|
||||
double lat2 = GeoUtils.getWorldLat(y2);
|
||||
double lng2 = GeoUtils.getWorldLon(x2);
|
||||
|
||||
var results = runWithReaderFeatures(
|
||||
Map.of("threads", "1"),
|
||||
List.of(
|
||||
newReaderFeature(newLineString(lng1, lat1, lng2, lat2), Map.of(
|
||||
newReaderFeature(newLineString(lng1, lat1, lngMid, latMid, lng2, lat2), Map.of(
|
||||
"attr", "value"
|
||||
))
|
||||
),
|
||||
(in, features) -> (anyGeom ? features.anyGeometry("layer") : features.line("layer"))
|
||||
.setZoomRange(13, 14)
|
||||
.setPixelTolerance(1)
|
||||
.setSimplifyMethod(simplifyStrategy)
|
||||
.setBufferPixels(4)
|
||||
);
|
||||
|
||||
|
|
@ -2553,8 +2565,13 @@ class PlanetilerTests {
|
|||
assertEquals(bboxResult.tiles, polyResult.tiles);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimplePolygon() throws Exception {
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"RETAIN_IMPORTANT_POINTS",
|
||||
"RETAIN_EFFECTIVE_AREAS",
|
||||
"RETAIN_WEIGHTED_EFFECTIVE_AREAS",
|
||||
})
|
||||
void testSimplePolygon(SimplifyMethod strategy) throws Exception {
|
||||
List<Coordinate> points = z14PixelRectangle(0, 40);
|
||||
|
||||
var results = runWithReaderFeatures(
|
||||
|
|
@ -2565,6 +2582,8 @@ class PlanetilerTests {
|
|||
(in, features) -> features.polygon("layer")
|
||||
.setZoomRange(0, 14)
|
||||
.setBufferPixels(0)
|
||||
.setPixelTolerance(1)
|
||||
.setSimplifyMethod(strategy)
|
||||
.setMinPixelSize(10) // should only show up z14 (40) z13 (20) and z12 (10)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import com.carrotsearch.hppc.IntHashSet;
|
|||
import com.carrotsearch.hppc.IntSet;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Random;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
|
@ -39,12 +40,112 @@ import org.junit.jupiter.params.provider.CsvSource;
|
|||
* and modified to use long instead of float values, use stable random seed for reproducibility, and to use new
|
||||
* implementations.
|
||||
*/
|
||||
class LongMinHeapTest {
|
||||
abstract class MinHeapTest {
|
||||
|
||||
protected LongMinHeap heap;
|
||||
|
||||
void create(int capacity) {
|
||||
heap = LongMinHeap.newArrayHeap(capacity, Integer::compare);
|
||||
final void create(int capacity) {
|
||||
create(capacity, Integer::compare);
|
||||
}
|
||||
|
||||
abstract void create(int capacity, IntBinaryOperator tieBreaker);
|
||||
|
||||
|
||||
static class LongMinHeapTest extends MinHeapTest {
|
||||
|
||||
@Override
|
||||
void create(int capacity, IntBinaryOperator tieBreaker) {
|
||||
heap = LongMinHeap.newArrayHeap(capacity, tieBreaker);
|
||||
}
|
||||
}
|
||||
|
||||
static class DoubleMinHeapTest extends MinHeapTest {
|
||||
|
||||
private DoubleMinHeap doubleHeap;
|
||||
|
||||
@Test
|
||||
void testDoubles() {
|
||||
create(5);
|
||||
|
||||
doubleHeap.push(4, 1.5d);
|
||||
doubleHeap.push(1, 1.4d);
|
||||
assertEquals(2, doubleHeap.size());
|
||||
assertEquals(1, doubleHeap.peekId());
|
||||
assertEquals(1.4d, doubleHeap.peekValue());
|
||||
assertEquals(1, doubleHeap.poll());
|
||||
assertEquals(4, doubleHeap.poll());
|
||||
assertTrue(heap.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoublesReverse() {
|
||||
create(5);
|
||||
|
||||
doubleHeap.push(4, 1.4d);
|
||||
doubleHeap.push(1, 1.5d);
|
||||
assertEquals(2, doubleHeap.size());
|
||||
assertEquals(4, doubleHeap.peekId());
|
||||
assertEquals(1.4d, doubleHeap.peekValue());
|
||||
assertEquals(4, doubleHeap.poll());
|
||||
assertEquals(1, doubleHeap.poll());
|
||||
assertTrue(heap.isEmpty());
|
||||
}
|
||||
|
||||
@Override
|
||||
void create(int capacity, IntBinaryOperator tieBreaker) {
|
||||
doubleHeap = DoubleMinHeap.newArrayHeap(capacity, tieBreaker);
|
||||
heap = new LongMinHeap() {
|
||||
@Override
|
||||
public int size() {
|
||||
return doubleHeap.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return doubleHeap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void push(int id, long value) {
|
||||
doubleHeap.push(id, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(int id) {
|
||||
return doubleHeap.contains(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(int id, long value) {
|
||||
doubleHeap.update(id, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateHead(long value) {
|
||||
doubleHeap.updateHead(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int peekId() {
|
||||
return doubleHeap.peekId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long peekValue() {
|
||||
return (long) doubleHeap.peekValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int poll() {
|
||||
return doubleHeap.poll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
doubleHeap.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package com.onthegomap.planetiler.geo;
|
||||
|
||||
import static com.onthegomap.planetiler.TestUtils.assertSameNormalizedFeature;
|
||||
import static com.onthegomap.planetiler.TestUtils.newLineString;
|
||||
import static com.onthegomap.planetiler.TestUtils.newPolygon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.util.AffineTransformation;
|
||||
|
||||
class VWSimplifierTest {
|
||||
|
||||
final int[] rotations = new int[]{0, 45, 90, 180, 270};
|
||||
|
||||
private void testSimplify(Geometry in, Geometry expected, double amount) {
|
||||
for (int rotation : rotations) {
|
||||
var rotate = AffineTransformation.rotationInstance(Math.PI * rotation / 180);
|
||||
assertSameNormalizedFeature(
|
||||
rotate.transform(expected),
|
||||
new VWSimplifier().setTolerance(amount).setWeight(0).transform(rotate.transform(in))
|
||||
);
|
||||
assertSameNormalizedFeature(
|
||||
rotate.transform(expected.reverse()),
|
||||
new VWSimplifier().setTolerance(amount).setWeight(0).transform(rotate.transform(in.reverse()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimplify2Points() {
|
||||
testSimplify(newLineString(
|
||||
0, 0,
|
||||
10, 10
|
||||
), newLineString(
|
||||
0, 0,
|
||||
10, 10
|
||||
), 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveAPoint() {
|
||||
testSimplify(newLineString(
|
||||
0, 0,
|
||||
5, 0.9,
|
||||
10, 0
|
||||
), newLineString(
|
||||
0, 0,
|
||||
10, 0
|
||||
), 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testKeepAPoint() {
|
||||
testSimplify(newLineString(
|
||||
0, 0,
|
||||
5, 1.1,
|
||||
10, 0
|
||||
), newLineString(
|
||||
0, 0,
|
||||
5, 1.1,
|
||||
10, 0
|
||||
), 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPolygonLeaveAPoint() {
|
||||
testSimplify(
|
||||
newPolygon(
|
||||
0, 0,
|
||||
10, 10,
|
||||
9, 10,
|
||||
0, 8,
|
||||
0, 0
|
||||
),
|
||||
newPolygon(
|
||||
0, 0,
|
||||
0, 8,
|
||||
10, 10,
|
||||
0, 0
|
||||
),
|
||||
200
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLine() {
|
||||
testSimplify(
|
||||
newLineString(
|
||||
0, 0,
|
||||
1, 0.1,
|
||||
2, 0,
|
||||
3, 0.1
|
||||
),
|
||||
newLineString(
|
||||
0, 0,
|
||||
1, 0.1,
|
||||
2, 0,
|
||||
3, 0.1
|
||||
),
|
||||
0.09
|
||||
);
|
||||
testSimplify(
|
||||
newLineString(
|
||||
0, 0,
|
||||
1, 0.1,
|
||||
2, 0,
|
||||
3, 0.1
|
||||
),
|
||||
newLineString(
|
||||
0, 0,
|
||||
3, 0.1
|
||||
),
|
||||
0.11
|
||||
);
|
||||
}
|
||||
}
|
||||
Ładowanie…
Reference in New Issue