kopia lustrzana https://github.com/onthegomap/planetiler
Add support for linear-referenced tags. (#900)
rodzic
9dbd5d358e
commit
21f061efe0
|
@ -0,0 +1,74 @@
|
||||||
|
package com.onthegomap.planetiler.benchmarks;
|
||||||
|
|
||||||
|
import com.onthegomap.planetiler.geo.LineSplitter;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import org.locationtech.jts.util.GeometricShapeFactory;
|
||||||
|
|
||||||
|
public class BenchmarkLineSplitter {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
System.err.println(
|
||||||
|
"reused:\t" +
|
||||||
|
timeReused(10, 1_000_000) + "\t" +
|
||||||
|
timeReused(100, 100_000) + "\t" +
|
||||||
|
timeReused(1_000, 10_000) +
|
||||||
|
"\t!reused\t" +
|
||||||
|
timeNotReused(10, 1_000_000) + "\t" +
|
||||||
|
timeNotReused(100, 100_000) + "\t" +
|
||||||
|
timeNotReused(1_000, 10_000) +
|
||||||
|
"\tcacheable\t" +
|
||||||
|
timeCacheable(10, 1_000_000) + "\t" +
|
||||||
|
timeCacheable(100, 100_000) + "\t" +
|
||||||
|
timeCacheable(1_000, 10_000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long timeCacheable(int points, int iters) {
|
||||||
|
var fact = new GeometricShapeFactory();
|
||||||
|
fact.setNumPoints(points);
|
||||||
|
fact.setWidth(10);
|
||||||
|
var shape = fact.createArc(0, Math.PI);
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
LineSplitter splitter = new LineSplitter(shape);
|
||||||
|
var random = ThreadLocalRandom.current();
|
||||||
|
for (int i = 0; i < iters; i++) {
|
||||||
|
int a = random.nextInt(0, 90);
|
||||||
|
int b = random.nextInt(a + 2, 100);
|
||||||
|
splitter.get(a / 100d, b / 100d);
|
||||||
|
}
|
||||||
|
return System.currentTimeMillis() - start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long timeReused(int points, int iters) {
|
||||||
|
var fact = new GeometricShapeFactory();
|
||||||
|
fact.setNumPoints(points);
|
||||||
|
fact.setWidth(10);
|
||||||
|
var shape = fact.createArc(0, Math.PI);
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
LineSplitter splitter = new LineSplitter(shape);
|
||||||
|
var random = ThreadLocalRandom.current();
|
||||||
|
for (int i = 0; i < iters; i++) {
|
||||||
|
var a = random.nextDouble(0, 1);
|
||||||
|
var b = random.nextDouble(a, 1);
|
||||||
|
splitter.get(a, b);
|
||||||
|
}
|
||||||
|
return System.currentTimeMillis() - start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long timeNotReused(int points, int iters) {
|
||||||
|
var fact = new GeometricShapeFactory();
|
||||||
|
fact.setNumPoints(points);
|
||||||
|
fact.setWidth(10);
|
||||||
|
var shape = fact.createArc(0, Math.PI);
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
var random = ThreadLocalRandom.current();
|
||||||
|
for (int i = 0; i < iters; i++) {
|
||||||
|
LineSplitter splitter = new LineSplitter(shape);
|
||||||
|
var a = random.nextDouble(0, 1);
|
||||||
|
var b = random.nextDouble(a, 1);
|
||||||
|
splitter.get(a, b);
|
||||||
|
}
|
||||||
|
return System.currentTimeMillis() - start;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package com.onthegomap.planetiler;
|
package com.onthegomap.planetiler;
|
||||||
|
|
||||||
|
import com.google.common.collect.Range;
|
||||||
import com.onthegomap.planetiler.collection.FeatureGroup;
|
import com.onthegomap.planetiler.collection.FeatureGroup;
|
||||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||||
|
@ -9,6 +10,8 @@ import com.onthegomap.planetiler.reader.SourceFeature;
|
||||||
import com.onthegomap.planetiler.render.FeatureRenderer;
|
import com.onthegomap.planetiler.render.FeatureRenderer;
|
||||||
import com.onthegomap.planetiler.stats.Stats;
|
import com.onthegomap.planetiler.stats.Stats;
|
||||||
import com.onthegomap.planetiler.util.CacheByZoom;
|
import com.onthegomap.planetiler.util.CacheByZoom;
|
||||||
|
import com.onthegomap.planetiler.util.MapUtil;
|
||||||
|
import com.onthegomap.planetiler.util.MergingRangeMap;
|
||||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -24,6 +27,7 @@ import org.locationtech.jts.geom.Geometry;
|
||||||
* <p>
|
* <p>
|
||||||
* For example to add a polygon feature for a lake and a center label point with its name:
|
* For example to add a polygon feature for a lake and a center label point with its name:
|
||||||
* {@snippet :
|
* {@snippet :
|
||||||
|
* FeatureCollector featureCollector;
|
||||||
* featureCollector.polygon("water")
|
* featureCollector.polygon("water")
|
||||||
* .setAttr("class", "lake");
|
* .setAttr("class", "lake");
|
||||||
* featureCollector.centroid("water_name")
|
* featureCollector.centroid("water_name")
|
||||||
|
@ -105,6 +109,26 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts building a new partial line feature from {@code start} to {@code end} where 0 is the beginning of the line
|
||||||
|
* and 1 is the end of the line.
|
||||||
|
* <p>
|
||||||
|
* If the source feature cannot be a line, logs an error and returns a feature that can be configured, but won't
|
||||||
|
* actually emit anything to the map.
|
||||||
|
*
|
||||||
|
* @param layer the output vector tile layer this feature will be written to
|
||||||
|
* @return a feature that can be configured further.
|
||||||
|
*/
|
||||||
|
public Feature partialLine(String layer, double start, double end) {
|
||||||
|
try {
|
||||||
|
return geometry(layer, source.partialLine(start, end));
|
||||||
|
} catch (GeometryException e) {
|
||||||
|
e.log(stats, "feature_partial_line", "Error constructing partial line for " + source);
|
||||||
|
return new Feature(layer, EMPTY_GEOM, source.id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts building a new polygon map feature that expects the source feature to be a polygon.
|
* Starts building a new polygon map feature that expects the source feature to be a polygon.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -222,6 +246,128 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed interface OverrideCommand {
|
||||||
|
Range<Double> range();
|
||||||
|
}
|
||||||
|
private record Minzoom(Range<Double> range, int minzoom) implements OverrideCommand {}
|
||||||
|
private record Maxzoom(Range<Double> range, int maxzoom) implements OverrideCommand {}
|
||||||
|
private record Omit(Range<Double> range) implements OverrideCommand {}
|
||||||
|
private record Attr(Range<Double> range, String key, Object value) implements OverrideCommand {}
|
||||||
|
|
||||||
|
public interface WithZoomRange<T extends WithZoomRange<T>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the zoom range (inclusive) that this feature appears in.
|
||||||
|
* <p>
|
||||||
|
* If not called, then defaults to all zoom levels.
|
||||||
|
*/
|
||||||
|
default T setZoomRange(int min, int max) {
|
||||||
|
assert min <= max;
|
||||||
|
return setMinZoom(min).setMaxZoom(max);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the minimum zoom level (inclusive) that this feature appears in.
|
||||||
|
* <p>
|
||||||
|
* If not called, defaults to minimum zoom-level of the map.
|
||||||
|
*/
|
||||||
|
T setMinZoom(int min);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum zoom level (inclusive) that this feature appears in.
|
||||||
|
* <p>
|
||||||
|
* If not called, defaults to maximum zoom-level of the map.
|
||||||
|
*/
|
||||||
|
T setMaxZoom(int max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface WithSelf<T extends WithSelf<T>> {
|
||||||
|
|
||||||
|
default T self() {
|
||||||
|
return (T) this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface WithAttrs<T extends WithAttrs<T>> extends WithSelf<T> {
|
||||||
|
|
||||||
|
/** Copies the value for {@code key} attribute from source feature to the output feature. */
|
||||||
|
default T inheritAttrFromSource(String key) {
|
||||||
|
return setAttr(key, collector().source.getTag(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets an attribute on the output feature to either a string, number, boolean, or instance of {@link ZoomFunction}
|
||||||
|
* to change the value for {@code key} by zoom-level.
|
||||||
|
*/
|
||||||
|
T setAttr(String key, Object value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value for {@code key} attribute at or above {@code minzoom}. Below {@code minzoom} it will be ignored.
|
||||||
|
* <p>
|
||||||
|
* Replaces all previous value that has been for {@code key} at any zoom level. To have a value that changes at
|
||||||
|
* multiple zoom level thresholds, call {@link #setAttr(String, Object)} with a manually-constructed
|
||||||
|
* {@link ZoomFunction} value.
|
||||||
|
*/
|
||||||
|
default T setAttrWithMinzoom(String key, Object value, int minzoom) {
|
||||||
|
return setAttr(key, ZoomFunction.minZoom(minzoom, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value for {@code key} only at zoom levels where the feature is at least {@code minPixelSize} pixels in
|
||||||
|
* size.
|
||||||
|
*/
|
||||||
|
default T setAttrWithMinSize(String key, Object value, double minPixelSize) {
|
||||||
|
return setAttrWithMinzoom(key, value, collector().getMinZoomForPixelSize(minPixelSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value for {@code key} so that it always shows when {@code zoom_level >= minZoomToShowAlways} but only
|
||||||
|
* shows when {@code minZoomIfBigEnough <= zoom_level < minZoomToShowAlways} when it is at least
|
||||||
|
* {@code minPixelSize} pixels in size.
|
||||||
|
* <p>
|
||||||
|
* If you need more flexibility, use {@link #getMinZoomForPixelSize(double)} directly, or create a
|
||||||
|
* {@link ZoomFunction} that calculates {@link #getPixelSizeAtZoom(int)} and applies a custom threshold based on the
|
||||||
|
* zoom level.
|
||||||
|
*/
|
||||||
|
default T setAttrWithMinSize(String key, Object value, double minPixelSize, int minZoomIfBigEnough,
|
||||||
|
int minZoomToShowAlways) {
|
||||||
|
return setAttrWithMinzoom(key, value,
|
||||||
|
Math.clamp(collector().getMinZoomForPixelSize(minPixelSize), minZoomIfBigEnough, minZoomToShowAlways));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts all key/value pairs in {@code attrs} into the set of attribute to emit on the output feature at or above
|
||||||
|
* {@code minzoom}.
|
||||||
|
* <p>
|
||||||
|
* Replace values that have already been set.
|
||||||
|
*/
|
||||||
|
default T putAttrsWithMinzoom(Map<String, Object> attrs, int minzoom) {
|
||||||
|
for (var entry : attrs.entrySet()) {
|
||||||
|
setAttrWithMinzoom(entry.getKey(), entry.getValue(), minzoom);
|
||||||
|
}
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts all key/value pairs in {@code attrs} into the set of attribute to emit on the output feature.
|
||||||
|
* <p>
|
||||||
|
* Does not touch attributes that have already been set.
|
||||||
|
* <p>
|
||||||
|
* Values in {@code attrs} can either be the raw value to set, or an instance of {@link ZoomFunction} to change the
|
||||||
|
* value for that attribute by zoom level.
|
||||||
|
*/
|
||||||
|
default T putAttrs(Map<String, Object> attrs) {
|
||||||
|
for (var entry : attrs.entrySet()) {
|
||||||
|
setAttr(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the {@link FeatureCollector} this feature came from. */
|
||||||
|
FeatureCollector collector();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates new feature collector instances for each source feature that we encounter.
|
* Creates new feature collector instances for each source feature that we encounter.
|
||||||
*/
|
*/
|
||||||
|
@ -232,6 +378,11 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record PartialOverride(Range<Double> range, Object key, Object value) {}
|
||||||
|
|
||||||
|
/** A fully-configured subset of this line feature with linear-scoped attributes applied to a subset of the range.. */
|
||||||
|
public record RangeWithTags(double start, double end, Geometry geom, Map<String, Object> attrs) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder for an output map feature that contains all the information that will be needed to render vector tile
|
* A builder for an output map feature that contains all the information that will be needed to render vector tile
|
||||||
* features from the input element.
|
* features from the input element.
|
||||||
|
@ -239,7 +390,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
* Some feature attributes are set globally (like sort key), and some allow the value to change by zoom-level (like
|
* Some feature attributes are set globally (like sort key), and some allow the value to change by zoom-level (like
|
||||||
* tags).
|
* tags).
|
||||||
*/
|
*/
|
||||||
public final class Feature {
|
public final class Feature implements WithZoomRange<Feature>, WithAttrs<Feature> {
|
||||||
|
|
||||||
private static final double DEFAULT_LABEL_GRID_SIZE = 0;
|
private static final double DEFAULT_LABEL_GRID_SIZE = 0;
|
||||||
private static final int DEFAULT_LABEL_GRID_LIMIT = 0;
|
private static final int DEFAULT_LABEL_GRID_LIMIT = 0;
|
||||||
|
@ -260,6 +411,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
|
|
||||||
private boolean attrsChangeByZoom = false;
|
private boolean attrsChangeByZoom = false;
|
||||||
private CacheByZoom<Map<String, Object>> attrCache = null;
|
private CacheByZoom<Map<String, Object>> attrCache = null;
|
||||||
|
private CacheByZoom<List<RangeWithTags>> partialRangeCache = null;
|
||||||
|
|
||||||
private double defaultBufferPixels = 4;
|
private double defaultBufferPixels = 4;
|
||||||
private ZoomFunction<Number> bufferPixelOverrides;
|
private ZoomFunction<Number> bufferPixelOverrides;
|
||||||
|
@ -274,6 +426,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
private ZoomFunction<Number> pixelTolerance = null;
|
private ZoomFunction<Number> pixelTolerance = null;
|
||||||
|
|
||||||
private String numPointsAttr = null;
|
private String numPointsAttr = null;
|
||||||
|
private List<OverrideCommand> partialOverrides = null;
|
||||||
|
|
||||||
private Feature(String layer, Geometry geom, long id) {
|
private Feature(String layer, Geometry geom, long id) {
|
||||||
this.layer = layer;
|
this.layer = layer;
|
||||||
|
@ -335,27 +488,12 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
return setSortKey(FeatureGroup.SORT_KEY_MAX + FeatureGroup.SORT_KEY_MIN - sortKey);
|
return setSortKey(FeatureGroup.SORT_KEY_MAX + FeatureGroup.SORT_KEY_MIN - sortKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the zoom range (inclusive) that this feature appears in.
|
|
||||||
* <p>
|
|
||||||
* If not called, then defaults to all zoom levels.
|
|
||||||
*/
|
|
||||||
public Feature setZoomRange(int min, int max) {
|
|
||||||
assert min <= max;
|
|
||||||
return setMinZoom(min).setMaxZoom(max);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the minimum zoom level (inclusive) that this feature appears in. */
|
/** Returns the minimum zoom level (inclusive) that this feature appears in. */
|
||||||
public int getMinZoom() {
|
public int getMinZoom() {
|
||||||
return minzoom;
|
return minzoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
/**
|
|
||||||
* Sets the minimum zoom level (inclusive) that this feature appears in.
|
|
||||||
* <p>
|
|
||||||
* If not called, defaults to minimum zoom-level of the map.
|
|
||||||
*/
|
|
||||||
public Feature setMinZoom(int min) {
|
public Feature setMinZoom(int min) {
|
||||||
minzoom = Math.max(min, config.minzoom());
|
minzoom = Math.max(min, config.minzoom());
|
||||||
return this;
|
return this;
|
||||||
|
@ -366,11 +504,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
return maxzoom;
|
return maxzoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Sets the maximum zoom level (inclusive) that this feature appears in.
|
|
||||||
* <p>
|
|
||||||
* If not called, defaults to maximum zoom-level of the map.
|
|
||||||
*/
|
|
||||||
public Feature setMaxZoom(int max) {
|
public Feature setMaxZoom(int max) {
|
||||||
maxzoom = Math.min(max, config.maxzoom());
|
maxzoom = Math.min(max, config.maxzoom());
|
||||||
return this;
|
return this;
|
||||||
|
@ -695,15 +829,8 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
return attrCache.get(zoom);
|
return attrCache.get(zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Copies the value for {@code key} attribute from source feature to the output feature. */
|
|
||||||
public Feature inheritAttrFromSource(String key) {
|
|
||||||
return setAttr(key, source.getTag(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Sets an attribute on the output feature to either a string, number, boolean, or instance of {@link ZoomFunction}
|
|
||||||
* to change the value for {@code key} by zoom-level.
|
|
||||||
*/
|
|
||||||
public Feature setAttr(String key, Object value) {
|
public Feature setAttr(String key, Object value) {
|
||||||
if (value instanceof ZoomFunction) {
|
if (value instanceof ZoomFunction) {
|
||||||
attrsChangeByZoom = true;
|
attrsChangeByZoom = true;
|
||||||
|
@ -714,61 +841,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Sets the value for {@code key} attribute at or above {@code minzoom}. Below {@code minzoom} it will be ignored.
|
|
||||||
* <p>
|
|
||||||
* Replaces all previous value that has been for {@code key} at any zoom level. To have a value that changes at
|
|
||||||
* multiple zoom level thresholds, call {@link #setAttr(String, Object)} with a manually-constructed
|
|
||||||
* {@link ZoomFunction} value.
|
|
||||||
*/
|
|
||||||
public Feature setAttrWithMinzoom(String key, Object value, int minzoom) {
|
|
||||||
return setAttr(key, ZoomFunction.minZoom(minzoom, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the value for {@code key} only at zoom levels where the feature is at least {@code minPixelSize} pixels in
|
|
||||||
* size.
|
|
||||||
*/
|
|
||||||
public Feature setAttrWithMinSize(String key, Object value, double minPixelSize) {
|
|
||||||
return setAttrWithMinzoom(key, value, getMinZoomForPixelSize(minPixelSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the value for {@code key} so that it always shows when {@code zoom_level >= minZoomToShowAlways} but only
|
|
||||||
* shows when {@code minZoomIfBigEnough <= zoom_level < minZoomToShowAlways} when it is at least
|
|
||||||
* {@code minPixelSize} pixels in size.
|
|
||||||
* <p>
|
|
||||||
* If you need more flexibility, use {@link #getMinZoomForPixelSize(double)} directly, or create a
|
|
||||||
* {@link ZoomFunction} that calculates {@link #getPixelSizeAtZoom(int)} and applies a custom threshold based on the
|
|
||||||
* zoom level.
|
|
||||||
*/
|
|
||||||
public Feature setAttrWithMinSize(String key, Object value, double minPixelSize, int minZoomIfBigEnough,
|
|
||||||
int minZoomToShowAlways) {
|
|
||||||
return setAttrWithMinzoom(key, value,
|
|
||||||
Math.clamp(getMinZoomForPixelSize(minPixelSize), minZoomIfBigEnough, minZoomToShowAlways));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts all key/value pairs in {@code attrs} into the set of attribute to emit on the output feature at or above
|
|
||||||
* {@code minzoom}.
|
|
||||||
* <p>
|
|
||||||
* Replace values that have already been set.
|
|
||||||
*/
|
|
||||||
public Feature putAttrsWithMinzoom(Map<String, Object> attrs, int minzoom) {
|
|
||||||
for (var entry : attrs.entrySet()) {
|
|
||||||
setAttrWithMinzoom(entry.getKey(), entry.getValue(), minzoom);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts all key/value pairs in {@code attrs} into the set of attribute to emit on the output feature.
|
|
||||||
* <p>
|
|
||||||
* Does not touch attributes that have already been set.
|
|
||||||
* <p>
|
|
||||||
* Values in {@code attrs} can either be the raw value to set, or an instance of {@link ZoomFunction} to change the
|
|
||||||
* value for that attribute by zoom level.
|
|
||||||
*/
|
|
||||||
public Feature putAttrs(Map<String, Object> attrs) {
|
public Feature putAttrs(Map<String, Object> attrs) {
|
||||||
for (Object value : attrs.values()) {
|
for (Object value : attrs.values()) {
|
||||||
if (value instanceof ZoomFunction) {
|
if (value instanceof ZoomFunction) {
|
||||||
|
@ -780,6 +853,11 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FeatureCollector collector() {
|
||||||
|
return FeatureCollector.this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the attribute key that the renderer should use to store the number of points in the simplified geometry
|
* Returns the attribute key that the renderer should use to store the number of points in the simplified geometry
|
||||||
* before slicing it into tiles.
|
* before slicing it into tiles.
|
||||||
|
@ -816,5 +894,158 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
public double getSourceFeaturePixelSizeAtZoom(int zoom) {
|
public double getSourceFeaturePixelSizeAtZoom(int zoom) {
|
||||||
return getPixelSizeAtZoom(zoom);
|
return getPixelSizeAtZoom(zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link LinearRange} that can be used to configure attributes that apply to only a portion of this line
|
||||||
|
* from {@code start} to {@code end} where 0 is the beginning of the line and 1 is the end.
|
||||||
|
* <p>
|
||||||
|
* Since mapbox vector tiles can't handle this natively, the line will be broken up into multiple lines in the
|
||||||
|
* output tiles at each zoom level with the unique sets of tags on each line. Adjacent segments with the same tags
|
||||||
|
* will get merged into a single segment.
|
||||||
|
*/
|
||||||
|
public LinearRange linearRange(double start, double end) {
|
||||||
|
return linearRange(Range.closedOpen(start, end));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link LinearRange} that can be used to configure attributes that apply to only a portion of this line
|
||||||
|
* from {@code range.lowerBound} to {@code range.lowerBound} where 0 is the beginning of the line and 1 is the end.
|
||||||
|
* <p>
|
||||||
|
* Since mapbox vector tiles can't handle this natively, the line will be broken up into multiple lines in the
|
||||||
|
* output tiles at each zoom level with the unique sets of tags on each line. Adjacent segments with the same tags
|
||||||
|
* will get merged into a single segment.
|
||||||
|
*/
|
||||||
|
public LinearRange linearRange(Range<Double> range) {
|
||||||
|
return new LinearRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if any attributes have been configured over a subset of this line. */
|
||||||
|
public boolean hasLinearRanges() {
|
||||||
|
return partialOverrides != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Computes and returns the linear-scoped attributes of this line, and the geometry they apply to. */
|
||||||
|
public List<RangeWithTags> getLinearRangesAtZoom(int zoom) {
|
||||||
|
if (partialOverrides == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
if (partialRangeCache == null) {
|
||||||
|
partialRangeCache = CacheByZoom.create(this::computeLinearRangesAtZoom);
|
||||||
|
}
|
||||||
|
return partialRangeCache.get(zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<RangeWithTags> computeLinearRangesAtZoom(int zoom) {
|
||||||
|
record Partial(boolean omit, Map<String, Object> attrs) {
|
||||||
|
Partial withOmit(boolean newValue) {
|
||||||
|
return new Partial(newValue || omit, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
Partial merge(Partial other) {
|
||||||
|
return new Partial(other.omit, MapUtil.merge(attrs, other.attrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
Partial withAttr(String key, Object value) {
|
||||||
|
return new Partial(omit, MapUtil.with(attrs, key, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MergingRangeMap<Partial> result = MergingRangeMap.unit(new Partial(false, attrs), Partial::merge);
|
||||||
|
for (var override : partialOverrides) {
|
||||||
|
result.update(override.range(), m -> switch (override) {
|
||||||
|
case Attr attr ->
|
||||||
|
m.withAttr(attr.key, attr.value instanceof ZoomFunction<?> fn ? fn.apply(zoom) : attr.value);
|
||||||
|
case Maxzoom mz -> m.withOmit(mz.maxzoom < zoom);
|
||||||
|
case Minzoom mz -> m.withOmit(mz.minzoom > zoom);
|
||||||
|
case Omit ignored -> m.withOmit(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var ranges = result.result();
|
||||||
|
List<RangeWithTags> rangesWithGeometries = new ArrayList<>(ranges.size());
|
||||||
|
for (var range : ranges) {
|
||||||
|
var value = range.value();
|
||||||
|
if (!value.omit) {
|
||||||
|
try {
|
||||||
|
rangesWithGeometries.add(new RangeWithTags(
|
||||||
|
range.start(),
|
||||||
|
range.end(),
|
||||||
|
source.partialLine(range.start(), range.end()),
|
||||||
|
value.attrs
|
||||||
|
));
|
||||||
|
} catch (GeometryException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rangesWithGeometries;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that can be used to configure linear-scoped attributes for a partial segment of a line feature.
|
||||||
|
*/
|
||||||
|
public final class LinearRange implements WithZoomRange<LinearRange>, WithAttrs<LinearRange> {
|
||||||
|
|
||||||
|
private final Range<Double> range;
|
||||||
|
|
||||||
|
private LinearRange(Range<Double> range) {
|
||||||
|
this.range = range;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinearRange add(OverrideCommand override) {
|
||||||
|
if (partialOverrides == null) {
|
||||||
|
partialOverrides = new ArrayList<>();
|
||||||
|
}
|
||||||
|
partialOverrides.add(override);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LinearRange setMinZoom(int min) {
|
||||||
|
return add(new Minzoom(range, min));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LinearRange setMaxZoom(int max) {
|
||||||
|
return add(new Maxzoom(range, max));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LinearRange setAttr(String key, Object value) {
|
||||||
|
return add(new Attr(range, key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Exclude this segment of the line feature at all zoom levels. */
|
||||||
|
public LinearRange omit() {
|
||||||
|
return add(new Omit(range));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the full line {@link Feature} that this segment came from. */
|
||||||
|
public Feature entireLine() {
|
||||||
|
return Feature.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a segment of the full parent line (not the current segment) that can be configured further.
|
||||||
|
*
|
||||||
|
* @see Feature#linearRange(double, double)
|
||||||
|
*/
|
||||||
|
public LinearRange linearRange(double start, double end) {
|
||||||
|
return entireLine().linearRange(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a segment of the full parent line (not the current segment) that can be configured further.
|
||||||
|
*
|
||||||
|
* @see Feature#linearRange(Range)
|
||||||
|
*/
|
||||||
|
public LinearRange linearRange(Range<Double> range) {
|
||||||
|
return entireLine().linearRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FeatureCollector collector() {
|
||||||
|
return FeatureCollector.this;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
package com.onthegomap.planetiler.geo;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import org.locationtech.jts.geom.CoordinateSequence;
|
||||||
|
import org.locationtech.jts.geom.Geometry;
|
||||||
|
import org.locationtech.jts.geom.LineString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for extracting sub-ranges of a line.
|
||||||
|
* <p>
|
||||||
|
* For example:
|
||||||
|
* {@snippet :
|
||||||
|
* LineSplitter splitter = new LineSplitter(line);
|
||||||
|
* LineString firstHalf = splitter.get(0, 0.5);
|
||||||
|
* LineString lastQuarter = splitter.get(0.75, 1);
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public class LineSplitter {
|
||||||
|
|
||||||
|
private final LineString line;
|
||||||
|
private double length = 0;
|
||||||
|
private double[] nodeLocations = null;
|
||||||
|
|
||||||
|
public LineSplitter(Geometry geom) {
|
||||||
|
if (geom instanceof LineString linestring) {
|
||||||
|
this.line = linestring;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Expected LineString, got " + geom.getGeometryType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a partial segment of this line from {@code start} to {@code end} where 0 is the beginning of the line and 1
|
||||||
|
* is the end.
|
||||||
|
*/
|
||||||
|
public LineString get(double start, double end) {
|
||||||
|
if (start < 0d || end > 1d || end < start) {
|
||||||
|
throw new IllegalArgumentException("Invalid range: " + start + " to " + end);
|
||||||
|
}
|
||||||
|
if (start <= 0 && end >= 1) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
var cs = line.getCoordinateSequence();
|
||||||
|
if (nodeLocations == null) {
|
||||||
|
nodeLocations = new double[cs.size()];
|
||||||
|
double x1 = cs.getX(0);
|
||||||
|
double y1 = cs.getY(0);
|
||||||
|
nodeLocations[0] = 0d;
|
||||||
|
for (int i = 1; i < cs.size(); i++) {
|
||||||
|
double x2 = cs.getX(i);
|
||||||
|
double y2 = cs.getY(i);
|
||||||
|
double dx = x2 - x1;
|
||||||
|
double dy = y2 - y1;
|
||||||
|
length += Math.sqrt(dx * dx + dy * dy);
|
||||||
|
nodeLocations[i] = length;
|
||||||
|
x1 = x2;
|
||||||
|
y1 = y2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MutableCoordinateSequence result = new MutableCoordinateSequence();
|
||||||
|
|
||||||
|
double startPos = start * length;
|
||||||
|
double endPos = end * length;
|
||||||
|
var first = floorIndex(startPos);
|
||||||
|
var last = lowerIndex(endPos);
|
||||||
|
addInterpolated(result, cs, first, startPos);
|
||||||
|
for (int i = first + 1; i <= last; i++) {
|
||||||
|
result.addPoint(cs.getX(i), cs.getY(i));
|
||||||
|
}
|
||||||
|
addInterpolated(result, cs, last, endPos);
|
||||||
|
|
||||||
|
return GeoUtils.JTS_FACTORY.createLineString(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int floorIndex(double length) {
|
||||||
|
int idx = Arrays.binarySearch(nodeLocations, length);
|
||||||
|
return idx < 0 ? (-idx - 2) : idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int lowerIndex(double length) {
|
||||||
|
int idx = Arrays.binarySearch(nodeLocations, length);
|
||||||
|
return idx < 0 ? (-idx - 2) : idx - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addInterpolated(MutableCoordinateSequence result, CoordinateSequence cs,
|
||||||
|
int startIdx, double position) {
|
||||||
|
int endIdx = startIdx + 1;
|
||||||
|
double startPos = nodeLocations[startIdx];
|
||||||
|
double endPos = nodeLocations[endIdx];
|
||||||
|
double x1 = cs.getX(startIdx);
|
||||||
|
double y1 = cs.getY(startIdx);
|
||||||
|
double x2 = cs.getX(endIdx);
|
||||||
|
double y2 = cs.getY(endIdx);
|
||||||
|
double ratio = (position - startPos) / (endPos - startPos);
|
||||||
|
result.addPoint(
|
||||||
|
x1 + (x2 - x1) * ratio,
|
||||||
|
y1 + (y2 - y1) * ratio
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package com.onthegomap.planetiler.reader;
|
||||||
|
|
||||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||||
import com.onthegomap.planetiler.geo.GeometryException;
|
import com.onthegomap.planetiler.geo.GeometryException;
|
||||||
|
import com.onthegomap.planetiler.geo.LineSplitter;
|
||||||
import com.onthegomap.planetiler.reader.osm.OsmReader;
|
import com.onthegomap.planetiler.reader.osm.OsmReader;
|
||||||
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -43,6 +44,7 @@ public abstract class SourceFeature implements WithTags, WithGeometryType {
|
||||||
private double area = Double.NaN;
|
private double area = Double.NaN;
|
||||||
private double length = Double.NaN;
|
private double length = Double.NaN;
|
||||||
private double size = Double.NaN;
|
private double size = Double.NaN;
|
||||||
|
private LineSplitter lineSplitter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new input feature.
|
* Constructs a new input feature.
|
||||||
|
@ -192,6 +194,27 @@ public abstract class SourceFeature implements WithTags, WithGeometryType {
|
||||||
return linearGeometry;
|
return linearGeometry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a partial line string from {@code start} to {@code end} where 0 is the beginning of the line and 1 is the
|
||||||
|
* end of the line.
|
||||||
|
*
|
||||||
|
* @throws GeometryException if an error occurs constructing the geometry, or of this feature should not be
|
||||||
|
* interpreted as a single line (multilinestrings are not allowed).
|
||||||
|
*/
|
||||||
|
public final Geometry partialLine(double start, double end) throws GeometryException {
|
||||||
|
Geometry line = line();
|
||||||
|
if (start <= 0 && end >= 1) {
|
||||||
|
return line;
|
||||||
|
} else if (line instanceof LineString lineString) {
|
||||||
|
if (this.lineSplitter == null) {
|
||||||
|
this.lineSplitter = new LineSplitter(lineString);
|
||||||
|
}
|
||||||
|
return lineSplitter.get(start, end);
|
||||||
|
} else {
|
||||||
|
throw new GeometryException("partial_multilinestring", "cannot get partial of a multiline", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes this feature as a {@link Polygon} or {@link MultiPolygon} in world web mercator coordinates.
|
* Computes this feature as a {@link Polygon} or {@link MultiPolygon} in world web mercator coordinates.
|
||||||
*
|
*
|
||||||
|
|
|
@ -174,10 +174,8 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
|
||||||
private void renderLineOrPolygon(FeatureCollector.Feature feature, Geometry input) {
|
private void renderLineOrPolygon(FeatureCollector.Feature feature, Geometry input) {
|
||||||
boolean area = input instanceof Polygonal;
|
boolean area = input instanceof Polygonal;
|
||||||
double worldLength = (area || input.getNumGeometries() > 1) ? 0 : input.getLength();
|
double worldLength = (area || input.getNumGeometries() > 1) ? 0 : input.getLength();
|
||||||
String numPointsAttr = feature.getNumPointsAttr();
|
|
||||||
for (int z = feature.getMaxZoom(); z >= feature.getMinZoom(); z--) {
|
for (int z = feature.getMaxZoom(); z >= feature.getMinZoom(); z--) {
|
||||||
double scale = 1 << z;
|
double scale = 1 << z;
|
||||||
double tolerance = feature.getPixelToleranceAtZoom(z) / 256d;
|
|
||||||
double minSize = feature.getMinPixelSizeAtZoom(z) / 256d;
|
double minSize = feature.getMinPixelSizeAtZoom(z) / 256d;
|
||||||
if (area) {
|
if (area) {
|
||||||
// treat minPixelSize as the edge of a square that defines minimum area for features
|
// treat minPixelSize as the edge of a square that defines minimum area for features
|
||||||
|
@ -187,40 +185,55 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
double buffer = feature.getBufferPixelsAtZoom(z) / 256;
|
if (feature.hasLinearRanges()) {
|
||||||
TileExtents.ForZoom extents = config.bounds().tileExtents().getForZoom(z);
|
for (var range : feature.getLinearRangesAtZoom(z)) {
|
||||||
|
if (worldLength * scale * (range.end() - range.start()) >= minSize) {
|
||||||
// TODO potential optimization: iteratively simplify z+1 to get z instead of starting with original geom each time
|
renderLineOrPolygonGeometry(feature, range.geom(), range.attrs(), z, minSize, area);
|
||||||
// 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);
|
|
||||||
List<List<CoordinateSequence>> groups = GeometryCoordinateSequences.extractGroups(geom, minSize);
|
|
||||||
try {
|
|
||||||
sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents);
|
|
||||||
} catch (GeometryException e) {
|
|
||||||
try {
|
|
||||||
geom = GeoUtils.fixPolygon(geom);
|
|
||||||
groups = GeometryCoordinateSequences.extractGroups(geom, minSize);
|
|
||||||
sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents);
|
|
||||||
} catch (GeometryException ex) {
|
|
||||||
ex.log(stats, "slice_line_or_polygon", "Error slicing feature at z" + z + ": " + feature);
|
|
||||||
// omit from this zoom level, but maybe the next will be better
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
renderLineOrPolygonGeometry(feature, input, feature.getAttrsAtZoom(z), z, minSize, area);
|
||||||
}
|
}
|
||||||
Map<String, Object> attrs = feature.getAttrsAtZoom(sliced.zoomLevel());
|
|
||||||
if (numPointsAttr != null) {
|
|
||||||
// if profile wants the original number off points that the simplified but untiled geometry started with
|
|
||||||
attrs = new HashMap<>(attrs);
|
|
||||||
attrs.put(numPointsAttr, geom.getNumPoints());
|
|
||||||
}
|
|
||||||
writeTileFeatures(z, feature.getId(), feature, sliced, attrs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.processedElement(area ? "polygon" : "line", feature.getLayer());
|
stats.processedElement(area ? "polygon" : "line", feature.getLayer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void renderLineOrPolygonGeometry(FeatureCollector.Feature feature, Geometry input, Map<String, Object> attrs,
|
||||||
|
int z, double minSize, boolean area) {
|
||||||
|
double scale = 1 << z;
|
||||||
|
double tolerance = feature.getPixelToleranceAtZoom(z) / 256d;
|
||||||
|
double buffer = feature.getBufferPixelsAtZoom(z) / 256;
|
||||||
|
TileExtents.ForZoom extents = config.bounds().tileExtents().getForZoom(z);
|
||||||
|
|
||||||
|
// TODO potential optimization: iteratively simplify z+1 to get z instead of starting with original geom each time
|
||||||
|
// 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);
|
||||||
|
List<List<CoordinateSequence>> groups = GeometryCoordinateSequences.extractGroups(geom, minSize);
|
||||||
|
try {
|
||||||
|
sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents);
|
||||||
|
} catch (GeometryException e) {
|
||||||
|
try {
|
||||||
|
geom = GeoUtils.fixPolygon(geom);
|
||||||
|
groups = GeometryCoordinateSequences.extractGroups(geom, minSize);
|
||||||
|
sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents);
|
||||||
|
} catch (GeometryException ex) {
|
||||||
|
ex.log(stats, "slice_line_or_polygon", "Error slicing feature at z" + z + ": " + feature);
|
||||||
|
// omit from this zoom level, but maybe the next will be better
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String numPointsAttr = feature.getNumPointsAttr();
|
||||||
|
if (numPointsAttr != null) {
|
||||||
|
// if profile wants the original number off points that the simplified but untiled geometry started with
|
||||||
|
attrs = new HashMap<>(attrs);
|
||||||
|
attrs.put(numPointsAttr, geom.getNumPoints());
|
||||||
|
}
|
||||||
|
writeTileFeatures(z, feature.getId(), feature, sliced, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
private void writeTileFeatures(int zoom, long id, FeatureCollector.Feature feature, TiledGeometry sliced,
|
private void writeTileFeatures(int zoom, long id, FeatureCollector.Feature feature, TiledGeometry sliced,
|
||||||
Map<String, Object> attrs) {
|
Map<String, Object> attrs) {
|
||||||
int emitted = 0;
|
int emitted = 0;
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.onthegomap.planetiler.util;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class MapUtil {
|
||||||
|
private MapUtil() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new map with the union of entries from {@code a} and {@code b} where conflicts take the value from
|
||||||
|
* {@code b}.
|
||||||
|
*/
|
||||||
|
public static <K, V> Map<K, V> merge(Map<K, V> a, Map<K, V> b) {
|
||||||
|
Map<K, V> copy = new HashMap<>(a);
|
||||||
|
copy.putAll(b);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new map the entries of {@code a} added to {@code (k, v)}.
|
||||||
|
*/
|
||||||
|
public static <K, V> Map<K, V> with(Map<K, V> a, K k, V v) {
|
||||||
|
Map<K, V> copy = new HashMap<>(a);
|
||||||
|
if (v == null || "".equals(v)) {
|
||||||
|
copy.remove(k);
|
||||||
|
} else {
|
||||||
|
copy.put(k, v);
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package com.onthegomap.planetiler.util;
|
||||||
|
|
||||||
|
import com.google.common.collect.Range;
|
||||||
|
import com.google.common.collect.RangeMap;
|
||||||
|
import com.google.common.collect.TreeRangeMap;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.BinaryOperator;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mapping from disjoint ranges to values that merges values assigned to overlapping ranges.
|
||||||
|
* <p>
|
||||||
|
* This is similar {@link RangeMap} that merges overlapping values and coalesces ranges with identical values by
|
||||||
|
* default.
|
||||||
|
* <p>
|
||||||
|
* For example:
|
||||||
|
* {@snippet :
|
||||||
|
* MergingRangeMap map = MergingRangeMap.unitMap();
|
||||||
|
* map.put(0, 0.5, Map.of("key", "value"));
|
||||||
|
* // overrides value for key from [0.4, 0.5), and sets from [0.5, 1)
|
||||||
|
* map.put(0.4, 1, Map.of("key", "value2"));
|
||||||
|
* // adds key2 from [0.9, 1)
|
||||||
|
* map.put(0.9, 1, Map.of("key2", "value3"));
|
||||||
|
* // returns [
|
||||||
|
* // Partial[start=0.0, end=0.4, value={key=value}],
|
||||||
|
* // Partial[start=0.4, end=0.9, value={key=value2}],
|
||||||
|
* // Partial[start=0.9, end=1.0, value={key2=value3, key=value2}]
|
||||||
|
* // ]
|
||||||
|
* map.result();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public class MergingRangeMap<T> {
|
||||||
|
|
||||||
|
private final RangeMap<Double, T> items = TreeRangeMap.create();
|
||||||
|
private final BinaryOperator<T> merger;
|
||||||
|
|
||||||
|
private MergingRangeMap(double lo, double hi, T identity, BinaryOperator<T> merger) {
|
||||||
|
items.put(Range.closedOpen(lo, hi), identity);
|
||||||
|
this.merger = merger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new range map where values are {@link Map Maps} that get merged together when they overlap from
|
||||||
|
* {@code [0, 1)}.
|
||||||
|
*/
|
||||||
|
public static <K, V> MergingRangeMap<Map<K, V>> unitMap() {
|
||||||
|
return unit(Map.of(), MapUtil::merge);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new range map with {@code identity} from {@code [0, 1)} and {@code merger} as the default merging
|
||||||
|
* function for {@link #put(Range, Object)}.
|
||||||
|
*/
|
||||||
|
public static <T> MergingRangeMap<T> unit(T identity, BinaryOperator<T> merger) {
|
||||||
|
return create(0, 1, identity, merger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new range map with {@code identity} from {@code [lo, hi)} and {@code merger} as the default merging
|
||||||
|
* function for {@link #put(Range, Object)}.
|
||||||
|
*/
|
||||||
|
public static <T> MergingRangeMap<T> create(double lo, double hi, T identity, BinaryOperator<T> merger) {
|
||||||
|
return new MergingRangeMap<>(lo, hi, identity, merger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the distinct set of ranges and their values where adjacent maps with identical values are merged. */
|
||||||
|
public List<Partial<T>> result() {
|
||||||
|
List<Partial<T>> result = new ArrayList<>();
|
||||||
|
for (var entry : items.asMapOfRanges().entrySet()) {
|
||||||
|
result.add(new Partial<>(entry.getKey().lowerEndpoint(), entry.getKey().upperEndpoint(), entry.getValue()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change each of the distinct values over {@code range} to the result of applying {@code operator} to the existing
|
||||||
|
* value.
|
||||||
|
*/
|
||||||
|
public void update(Range<Double> range, UnaryOperator<T> operator) {
|
||||||
|
var overlaps = new ArrayList<>(items.subRangeMap(range).asMapOfRanges().entrySet());
|
||||||
|
for (var overlap : overlaps) {
|
||||||
|
items.putCoalescing(overlap.getKey(), operator.apply(overlap.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge {@code next} into the value associated with all ranges that overlap {@code [start, end)} using the default
|
||||||
|
* merging function.
|
||||||
|
*/
|
||||||
|
public void put(double start, double end, T next) {
|
||||||
|
put(Range.closedOpen(start, end), next);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge {@code next} into the value associated with all ranges that overlap {@code range} using the default merging
|
||||||
|
* function.
|
||||||
|
*/
|
||||||
|
public void put(Range<Double> range, T next) {
|
||||||
|
update(range, prev -> merger.apply(prev, next));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Subset of the range and value that applies to it. */
|
||||||
|
public record Partial<T>(double start, double end, T value) {}
|
||||||
|
}
|
|
@ -653,4 +653,99 @@ class FeatureCollectorTest {
|
||||||
)
|
)
|
||||||
), collector);
|
), collector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPartialLineFeature() {
|
||||||
|
var collector = factory.get(newReaderFeature(newLineString(worldToLatLon(0, 0, 1, 0)), Map.of()));
|
||||||
|
collector.partialLine("layername", 0.25, 0.5).setAttr("k1", "v1");
|
||||||
|
collector.partialLine("layername", 0.75, 1).setAttr("k2", "v2");
|
||||||
|
assertFeatures(14, List.of(
|
||||||
|
Map.of(
|
||||||
|
"_geom", new RoundGeometry(newLineString(0.25, 0, 0.5, 0)),
|
||||||
|
"k1", "v1",
|
||||||
|
"k2", "<null>"
|
||||||
|
),
|
||||||
|
Map.of(
|
||||||
|
"_geom", new RoundGeometry(newLineString(0.75, 0, 1, 0)),
|
||||||
|
"k1", "<null>",
|
||||||
|
"k2", "v2"
|
||||||
|
)
|
||||||
|
), collector);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLinearReferenceTags() {
|
||||||
|
var collector = factory.get(newReaderFeature(newLineString(worldToLatLon(0, 0, 1, 0)), Map.of()));
|
||||||
|
collector.line("layername")
|
||||||
|
.linearRange(0.1, 0.5).setAttr("k1", "v1")
|
||||||
|
.linearRange(0.3, 0.7).setAttr("k2", "v2")
|
||||||
|
.entireLine().setAttr("k3", "v3");
|
||||||
|
|
||||||
|
var feature = collector.iterator().next();
|
||||||
|
assertTrue(feature.hasLinearRanges());
|
||||||
|
assertEquals(List.of(
|
||||||
|
new FeatureCollector.RangeWithTags(0, 0.1, roundTrip(newLineString(0, 0, 0.1, 0)), Map.of(
|
||||||
|
"k3", "v3"
|
||||||
|
)),
|
||||||
|
new FeatureCollector.RangeWithTags(0.1, 0.3, roundTrip(newLineString(0.1, 0, 0.3, 0)), Map.of(
|
||||||
|
"k1", "v1",
|
||||||
|
"k3", "v3"
|
||||||
|
)),
|
||||||
|
new FeatureCollector.RangeWithTags(0.3, 0.5, roundTrip(newLineString(0.3, 0, 0.5, 0)), Map.of(
|
||||||
|
"k1", "v1",
|
||||||
|
"k2", "v2",
|
||||||
|
"k3", "v3"
|
||||||
|
)),
|
||||||
|
new FeatureCollector.RangeWithTags(0.5, 0.7, roundTrip(newLineString(0.5, 0, 0.7, 0)), Map.of(
|
||||||
|
"k2", "v2",
|
||||||
|
"k3", "v3"
|
||||||
|
)),
|
||||||
|
new FeatureCollector.RangeWithTags(0.7, 1, roundTrip(newLineString(0.7, 0, 1, 0)), Map.of(
|
||||||
|
"k3", "v3"
|
||||||
|
))
|
||||||
|
), feature.getLinearRangesAtZoom(14));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPartialMinzoom() {
|
||||||
|
var collector = factory.get(newReaderFeature(newLineString(worldToLatLon(0, 0, 1, 0)), Map.of()));
|
||||||
|
collector.line("layername")
|
||||||
|
.linearRange(0.25, 0.75).setMinZoom(14);
|
||||||
|
assertFeatures(13, List.of(
|
||||||
|
Map.of("_geom", new RoundGeometry(newLineString(0, 0, 1, 0)))
|
||||||
|
), collector);
|
||||||
|
assertFeatures(14, List.of(
|
||||||
|
Map.of("_geom", new RoundGeometry(newLineString(0, 0, 1, 0)))
|
||||||
|
), collector);
|
||||||
|
var feature = collector.iterator().next();
|
||||||
|
assertTrue(feature.hasLinearRanges());
|
||||||
|
assertEquals(List.of(
|
||||||
|
new FeatureCollector.RangeWithTags(0, 0.25, roundTrip(newLineString(0, 0, 0.25, 0)), Map.of()),
|
||||||
|
new FeatureCollector.RangeWithTags(0.75, 1, roundTrip(newLineString(0.75, 0, 1, 0)), Map.of())
|
||||||
|
), feature.getLinearRangesAtZoom(13));
|
||||||
|
assertEquals(List.of(
|
||||||
|
new FeatureCollector.RangeWithTags(0, 1, roundTrip(newLineString(0, 0, 1, 0)), Map.of())
|
||||||
|
), feature.getLinearRangesAtZoom(14));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Geometry roundTrip(Geometry world) {
|
||||||
|
return GeoUtils.latLonToWorldCoords(GeoUtils.worldToLatLonCoords(world));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPartialOmit() {
|
||||||
|
var collector = factory.get(newReaderFeature(newLineString(worldToLatLon(0, 0, 1, 0)), Map.of()));
|
||||||
|
collector.line("layername")
|
||||||
|
.linearRange(0.25, 0.75).omit();
|
||||||
|
var feature = collector.iterator().next();
|
||||||
|
assertTrue(feature.hasLinearRanges());
|
||||||
|
assertEquals(List.of(
|
||||||
|
new FeatureCollector.RangeWithTags(0, 0.25, roundTrip(newLineString(0, 0, 0.25, 0)), Map.of()),
|
||||||
|
new FeatureCollector.RangeWithTags(0.75, 1, roundTrip(newLineString(0.75, 0, 1, 0)), Map.of())
|
||||||
|
), feature.getLinearRangesAtZoom(13));
|
||||||
|
assertEquals(List.of(
|
||||||
|
new FeatureCollector.RangeWithTags(0, 0.25, roundTrip(newLineString(0, 0, 0.25, 0)), Map.of()),
|
||||||
|
new FeatureCollector.RangeWithTags(0.75, 1, roundTrip(newLineString(0.75, 0, 1, 0)), Map.of())
|
||||||
|
), feature.getLinearRangesAtZoom(14));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -474,6 +474,81 @@ class PlanetilerTests {
|
||||||
), results.tiles);
|
), results.tiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPartialLine() 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 lat1 = GeoUtils.getWorldLat(y1);
|
||||||
|
double lng1 = GeoUtils.getWorldLon(x1);
|
||||||
|
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(
|
||||||
|
"attr", "value"
|
||||||
|
))
|
||||||
|
),
|
||||||
|
(in, features) -> features.partialLine("layer", 0, 0.5)
|
||||||
|
.setZoomRange(13, 14)
|
||||||
|
.setBufferPixels(4)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertSubmap(Map.of(
|
||||||
|
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
|
||||||
|
feature(newLineString(128, 128, 256, 256), Map.of())
|
||||||
|
),
|
||||||
|
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2 + 1, 14), List.of(
|
||||||
|
feature(newLineString(-4, -4, 0, 0), Map.of())
|
||||||
|
),
|
||||||
|
TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of(
|
||||||
|
feature(newLineString(64, 64, 128, 128), Map.of())
|
||||||
|
)
|
||||||
|
), results.tiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLineWithPartialAttr() 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 lat1 = GeoUtils.getWorldLat(y1);
|
||||||
|
double lng1 = GeoUtils.getWorldLon(x1);
|
||||||
|
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(
|
||||||
|
"attr", "value"
|
||||||
|
))
|
||||||
|
),
|
||||||
|
(in, features) -> features.line("layer")
|
||||||
|
.linearRange(0, 0.25).setAttrWithMinzoom("k", "v", 14)
|
||||||
|
.entireLine()
|
||||||
|
.setZoomRange(13, 14)
|
||||||
|
.setBufferPixels(4)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertSubmap(Map.of(
|
||||||
|
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
|
||||||
|
feature(newLineString(192, 192, 260, 260), Map.of()),
|
||||||
|
feature(newLineString(128, 128, 192, 192), Map.of("k", "v"))
|
||||||
|
),
|
||||||
|
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2 + 1, 14), List.of(
|
||||||
|
feature(newLineString(-4, -4, 128, 128), Map.of())
|
||||||
|
),
|
||||||
|
TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of(
|
||||||
|
feature(newLineString(64, 64, 192, 192), Map.of())
|
||||||
|
)
|
||||||
|
), results.tiles);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLineStringDegenerateWhenUnscaled() throws Exception {
|
void testLineStringDegenerateWhenUnscaled() throws Exception {
|
||||||
double x1 = 0.5 + Z12_WIDTH / 2;
|
double x1 = 0.5 + Z12_WIDTH / 2;
|
||||||
|
|
|
@ -428,6 +428,24 @@ public class TestUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record RoundGeometry(Geometry geom) implements GeometryComparision {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
return o instanceof GeometryComparision that && round(geom).equalsNorm(round(that.geom()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Round{" + round(geom).norm() + '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private record ExactGeometry(Geometry geom) implements GeometryComparision {
|
private record ExactGeometry(Geometry geom) implements GeometryComparision {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.onthegomap.planetiler.geo;
|
||||||
|
|
||||||
|
import static com.onthegomap.planetiler.TestUtils.newLineString;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
|
|
||||||
|
class LineSplitterTest {
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource({
|
||||||
|
"0,1",
|
||||||
|
"0,0.25",
|
||||||
|
"0.75, 1",
|
||||||
|
"0.25, 0.75",
|
||||||
|
})
|
||||||
|
void testSingleSegment(double start, double end) {
|
||||||
|
var l = new LineSplitter(newLineString(0, 0, 2, 1));
|
||||||
|
assertEquals(
|
||||||
|
newLineString(start * 2, start, end * 2, end),
|
||||||
|
l.get(start, end)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLength2() {
|
||||||
|
var l = new LineSplitter(newLineString(0, 0, 1, 2, 2, 4));
|
||||||
|
assertEquals(
|
||||||
|
newLineString(0, 0, 0.5, 1),
|
||||||
|
l.get(0, 0.25)
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
newLineString(0.2, 0.4, 0.5, 1),
|
||||||
|
l.get(0.1, 0.25)
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
newLineString(0.5, 1, 1, 2),
|
||||||
|
l.get(0.25, 0.5)
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
newLineString(0.5, 1, 1, 2, 1.5, 3),
|
||||||
|
l.get(0.25, 0.75)
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
newLineString(1, 2, 1.5, 3),
|
||||||
|
l.get(0.5, 0.75)
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
newLineString(1.2, 2.4, 1.5, 3),
|
||||||
|
l.get(0.6, 0.75)
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
newLineString(1.5, 3, 2, 4),
|
||||||
|
l.get(0.75, 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalid() {
|
||||||
|
var l = new LineSplitter(newLineString(0, 0, 1, 2, 2, 4));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> l.get(-0.1, 0.5));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> l.get(0.9, 1.1));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> l.get(0.6, 0.5));
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import com.onthegomap.planetiler.TestUtils;
|
||||||
import com.onthegomap.planetiler.config.Arguments;
|
import com.onthegomap.planetiler.config.Arguments;
|
||||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||||
|
import com.onthegomap.planetiler.geo.GeometryException;
|
||||||
import com.onthegomap.planetiler.geo.TileCoord;
|
import com.onthegomap.planetiler.geo.TileCoord;
|
||||||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||||
import com.onthegomap.planetiler.stats.Stats;
|
import com.onthegomap.planetiler.stats.Stats;
|
||||||
|
@ -23,8 +24,10 @@ import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.DoubleStream;
|
import java.util.stream.DoubleStream;
|
||||||
import org.junit.jupiter.api.DynamicTest;
|
import org.junit.jupiter.api.DynamicTest;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -1425,4 +1428,64 @@ class FeatureRendererTest {
|
||||||
actual
|
actual
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLinearRangeFeature() {
|
||||||
|
var feature = lineFeature(
|
||||||
|
newLineString(
|
||||||
|
0.5 + Z14_WIDTH / 2, 0.5 + Z14_WIDTH / 2,
|
||||||
|
0.5 + Z14_WIDTH / 2 + Z14_PX * 10, 0.5 + Z14_WIDTH / 2 + Z14_PX * 10
|
||||||
|
)
|
||||||
|
).linearRange(0.5, 1).setAttr("k", "v").entireLine();
|
||||||
|
Map<TileCoord, Collection<RenderedFeature>> rendered = renderFeatures(feature);
|
||||||
|
assertEquals(
|
||||||
|
Set.of(
|
||||||
|
List.of(newLineString(128 + 5, 128 + 5, 128 + 10, 128 + 10), Map.of("k", "v")),
|
||||||
|
List.of(newLineString(128, 128, 128 + 5, 128 + 5), Map.of())
|
||||||
|
),
|
||||||
|
rendered.get(TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14)).stream()
|
||||||
|
.map(RenderedFeature::vectorTileFeature)
|
||||||
|
.map(d -> {
|
||||||
|
try {
|
||||||
|
return List.of(d.geometry().decode(), d.tags());
|
||||||
|
} catch (GeometryException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(Collectors.toSet())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(booleans = {false, true})
|
||||||
|
void testLinearRangeFeaturePartialMinzoom(boolean viaMinzoom) {
|
||||||
|
var feature = lineFeature(
|
||||||
|
newLineString(
|
||||||
|
0.5 + Z13_WIDTH / 2, 0.5 + Z13_WIDTH / 2,
|
||||||
|
0.5 + Z13_WIDTH / 2 + Z13_PX * 10, 0.5 + Z13_WIDTH / 2 + Z13_PX * 10
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (viaMinzoom) {
|
||||||
|
feature.linearRange(0.5, 1).setMinZoom(14);
|
||||||
|
} else {
|
||||||
|
feature.linearRange(0.5, 1).omit();
|
||||||
|
}
|
||||||
|
Map<TileCoord, Collection<RenderedFeature>> rendered = renderFeatures(feature);
|
||||||
|
assertEquals(
|
||||||
|
Set.of(
|
||||||
|
List.of(newLineString(128, 128, 128 + 5, 128 + 5), Map.of())
|
||||||
|
),
|
||||||
|
rendered.get(TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13)).stream()
|
||||||
|
.map(RenderedFeature::vectorTileFeature)
|
||||||
|
.map(d -> {
|
||||||
|
try {
|
||||||
|
return List.of(d.geometry().decode(), d.tags());
|
||||||
|
} catch (GeometryException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(Collectors.toSet())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
package com.onthegomap.planetiler.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class MergingRangeMapTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void empty() {
|
||||||
|
var map = MergingRangeMap.unitMap();
|
||||||
|
assertEquals(
|
||||||
|
List.of(
|
||||||
|
new MergingRangeMap.Partial<>(0, 1.0, Map.of())
|
||||||
|
),
|
||||||
|
map.result()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPartialOverlap() {
|
||||||
|
var map = MergingRangeMap.unitMap();
|
||||||
|
map.put(0.25, 0.75, Map.of("b", 3, "c", 4));
|
||||||
|
map.put(0.5, 1.0, Map.of("a", 1, "b", 2));
|
||||||
|
assertEquals(
|
||||||
|
List.of(
|
||||||
|
new MergingRangeMap.Partial<>(0, 0.25, Map.of()),
|
||||||
|
new MergingRangeMap.Partial<>(0.25, 0.5, Map.of("b", 3, "c", 4)),
|
||||||
|
new MergingRangeMap.Partial<>(0.5, 0.75, Map.of("a", 1, "b", 2, "c", 4)),
|
||||||
|
new MergingRangeMap.Partial<>(0.75, 1.0, Map.of("a", 1, "b", 2))
|
||||||
|
),
|
||||||
|
map.result()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPutSingle() {
|
||||||
|
var map = MergingRangeMap.unitMap();
|
||||||
|
map.put(0.25, 0.75, Map.of("b", 3));
|
||||||
|
map.put(0.25, 0.75, Map.of("c", 4));
|
||||||
|
map.put(0.5, 1.0, Map.of("a", 1));
|
||||||
|
map.put(0.5, 1.0, Map.of("b", 2));
|
||||||
|
assertEquals(
|
||||||
|
List.of(
|
||||||
|
new MergingRangeMap.Partial<>(0, 0.25, Map.of()),
|
||||||
|
new MergingRangeMap.Partial<>(0.25, 0.5, Map.of("b", 3, "c", 4)),
|
||||||
|
new MergingRangeMap.Partial<>(0.5, 0.75, Map.of("a", 1, "b", 2, "c", 4)),
|
||||||
|
new MergingRangeMap.Partial<>(0.75, 1.0, Map.of("a", 1, "b", 2))
|
||||||
|
),
|
||||||
|
map.result()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDuplicateKeys() {
|
||||||
|
var map = MergingRangeMap.unitMap();
|
||||||
|
map.put(0.25, 0.75, Map.of("a", 1));
|
||||||
|
map.put(0.5, 1.0, Map.of("a", 1));
|
||||||
|
assertEquals(
|
||||||
|
List.of(
|
||||||
|
new MergingRangeMap.Partial<>(0, 0.25, Map.of()),
|
||||||
|
new MergingRangeMap.Partial<>(0.25, 1d, Map.of("a", 1))
|
||||||
|
),
|
||||||
|
map.result()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Ładowanie…
Reference in New Issue