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