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;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
import com.onthegomap.planetiler.collection.FeatureGroup;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
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.stats.Stats;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
|
@ -24,6 +27,7 @@ import org.locationtech.jts.geom.Geometry;
|
|||
* <p>
|
||||
* For example to add a polygon feature for a lake and a center label point with its name:
|
||||
* {@snippet :
|
||||
* FeatureCollector featureCollector;
|
||||
* featureCollector.polygon("water")
|
||||
* .setAttr("class", "lake");
|
||||
* 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.
|
||||
* <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.
|
||||
*/
|
||||
|
@ -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
|
||||
* 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
|
||||
* 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 int DEFAULT_LABEL_GRID_LIMIT = 0;
|
||||
|
@ -260,6 +411,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
|
||||
private boolean attrsChangeByZoom = false;
|
||||
private CacheByZoom<Map<String, Object>> attrCache = null;
|
||||
private CacheByZoom<List<RangeWithTags>> partialRangeCache = null;
|
||||
|
||||
private double defaultBufferPixels = 4;
|
||||
private ZoomFunction<Number> bufferPixelOverrides;
|
||||
|
@ -274,6 +426,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
private ZoomFunction<Number> pixelTolerance = null;
|
||||
|
||||
private String numPointsAttr = null;
|
||||
private List<OverrideCommand> partialOverrides = null;
|
||||
|
||||
private Feature(String layer, Geometry geom, long id) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
public int getMinZoom() {
|
||||
return minzoom;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the minimum zoom level (inclusive) that this feature appears in.
|
||||
* <p>
|
||||
* If not called, defaults to minimum zoom-level of the map.
|
||||
*/
|
||||
@Override
|
||||
public Feature setMinZoom(int min) {
|
||||
minzoom = Math.max(min, config.minzoom());
|
||||
return this;
|
||||
|
@ -366,11 +504,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
return maxzoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum zoom level (inclusive) that this feature appears in.
|
||||
* <p>
|
||||
* If not called, defaults to maximum zoom-level of the map.
|
||||
*/
|
||||
@Override
|
||||
public Feature setMaxZoom(int max) {
|
||||
maxzoom = Math.min(max, config.maxzoom());
|
||||
return this;
|
||||
|
@ -695,15 +829,8 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Override
|
||||
public Feature setAttr(String key, Object value) {
|
||||
if (value instanceof ZoomFunction) {
|
||||
attrsChangeByZoom = true;
|
||||
|
@ -714,61 +841,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Override
|
||||
public Feature putAttrs(Map<String, Object> attrs) {
|
||||
for (Object value : attrs.values()) {
|
||||
if (value instanceof ZoomFunction) {
|
||||
|
@ -780,6 +853,11 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
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
|
||||
* before slicing it into tiles.
|
||||
|
@ -816,5 +894,158 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
public double getSourceFeaturePixelSizeAtZoom(int 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.GeometryException;
|
||||
import com.onthegomap.planetiler.geo.LineSplitter;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmReader;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
||||
import java.util.ArrayList;
|
||||
|
@ -43,6 +44,7 @@ public abstract class SourceFeature implements WithTags, WithGeometryType {
|
|||
private double area = Double.NaN;
|
||||
private double length = Double.NaN;
|
||||
private double size = Double.NaN;
|
||||
private LineSplitter lineSplitter;
|
||||
|
||||
/**
|
||||
* Constructs a new input feature.
|
||||
|
@ -192,6 +194,27 @@ public abstract class SourceFeature implements WithTags, WithGeometryType {
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -174,10 +174,8 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
|
|||
private void renderLineOrPolygon(FeatureCollector.Feature feature, Geometry input) {
|
||||
boolean area = input instanceof Polygonal;
|
||||
double worldLength = (area || input.getNumGeometries() > 1) ? 0 : input.getLength();
|
||||
String numPointsAttr = feature.getNumPointsAttr();
|
||||
for (int z = feature.getMaxZoom(); z >= feature.getMinZoom(); z--) {
|
||||
double scale = 1 << z;
|
||||
double tolerance = feature.getPixelToleranceAtZoom(z) / 256d;
|
||||
double minSize = feature.getMinPixelSizeAtZoom(z) / 256d;
|
||||
if (area) {
|
||||
// treat minPixelSize as the edge of a square that defines minimum area for features
|
||||
|
@ -187,6 +185,24 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
|
|||
continue;
|
||||
}
|
||||
|
||||
if (feature.hasLinearRanges()) {
|
||||
for (var range : feature.getLinearRangesAtZoom(z)) {
|
||||
if (worldLength * scale * (range.end() - range.start()) >= minSize) {
|
||||
renderLineOrPolygonGeometry(feature, range.geom(), range.attrs(), z, minSize, area);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
renderLineOrPolygonGeometry(feature, input, feature.getAttrsAtZoom(z), z, minSize, area);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
@ -206,10 +222,10 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
|
|||
} 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;
|
||||
return;
|
||||
}
|
||||
}
|
||||
Map<String, Object> attrs = feature.getAttrsAtZoom(sliced.zoomLevel());
|
||||
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);
|
||||
|
@ -218,9 +234,6 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
|
|||
writeTileFeatures(z, feature.getId(), feature, sliced, attrs);
|
||||
}
|
||||
|
||||
stats.processedElement(area ? "polygon" : "line", feature.getLayer());
|
||||
}
|
||||
|
||||
private void writeTileFeatures(int zoom, long id, FeatureCollector.Feature feature, TiledGeometry sliced,
|
||||
Map<String, Object> attrs) {
|
||||
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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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
|
||||
void testLineStringDegenerateWhenUnscaled() throws Exception {
|
||||
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 {
|
||||
|
||||
@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.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.geo.TileCoord;
|
||||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
|
@ -23,8 +24,10 @@ import java.util.HashSet;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.DoubleStream;
|
||||
import org.junit.jupiter.api.DynamicTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -1425,4 +1428,64 @@ class FeatureRendererTest {
|
|||
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