From 762c7961d79dd93a7e07c9810475f1ec3f976c34 Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Wed, 15 Nov 2023 06:40:07 -0500 Subject: [PATCH 01/13] min zoom for pixel size --- .../planetiler/FeatureCollector.java | 34 ++++++++- .../onthegomap/planetiler/geo/GeoUtils.java | 14 ++++ .../planetiler/PlanetilerTests.java | 70 +++++++++++++++++++ .../planetiler/geo/GeoUtilsTest.java | 26 +++++++ 4 files changed, 143 insertions(+), 1 deletion(-) diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java index 680b393faa..58d824f00f 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java @@ -703,6 +703,27 @@ 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. + *

+ * If you need more flexibility, use {@link #getMinZoomForPixelSize(double)} directly. + */ + 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}. @@ -766,9 +787,20 @@ public double getSourceFeaturePixelSizeAtZoom(int zoom) { try { return source.size() * (256 << zoom); } catch (GeometryException e) { - e.log(stats, "point_get_size_failure", "Error getting min size for point from geometry " + source.id()); + e.log(stats, "source_feature_pixel_size_at_zoom_failure", + "Error getting source feature pixel size at zoom from geometry " + source.id()); return 0; } } + + /** Returns the minimum zoom level at which this feature is at least {@code pixelSize} pixels large. */ + public int getMinZoomForPixelSize(double pixelSize) { + try { + return GeoUtils.minZoomForPixelSize(source.size(), pixelSize); + } catch (GeometryException e) { + e.log(stats, "min_zoom_for_size_failure", "Error getting min zoom for size from geometry " + source.id()); + return config.maxzoom(); + } + } } } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java index 0809f4de4b..f6d0aceb7b 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java @@ -1,6 +1,7 @@ package com.onthegomap.planetiler.geo; import com.onthegomap.planetiler.collection.LongLongMap; +import com.onthegomap.planetiler.config.PlanetilerConfig; import com.onthegomap.planetiler.stats.Stats; import java.util.ArrayList; import java.util.List; @@ -51,6 +52,7 @@ public class GeoUtils { public static final double WORLD_CIRCUMFERENCE_METERS = Math.PI * 2 * WORLD_RADIUS_METERS; private static final double RADIANS_PER_DEGREE = Math.PI / 180; private static final double DEGREES_PER_RADIAN = 180 / Math.PI; + private static final double LOG2 = Math.log(2); /** * Transform web mercator coordinates where top-left corner of the planet is (0,0) and bottom-right is (1,1) to * latitude/longitude coordinates. @@ -534,6 +536,18 @@ public static Geometry combine(Geometry... geometries) { JTS_FACTORY.createGeometryCollection(innerGeometries.toArray(Geometry[]::new)); } + /** + * For a feature of size {@code worldGeometrySize} (where 1=full planet), determine the minimum zoom level at which + * the feature appears at least {@code minPixelSize} pixels large. + *

+ * The result will be clamped to the range [0, {@link PlanetilerConfig#MAX_MAXZOOM}]. + */ + public static int minZoomForPixelSize(double worldGeometrySize, double minPixelSize) { + double worldPixels = worldGeometrySize * 256; + return Math.clamp((int) Math.ceil(Math.log(minPixelSize / worldPixels) / LOG2), 0, + PlanetilerConfig.MAX_MAXZOOM); + } + /** Helper class to sort polygons by area of their outer shell. */ private record PolyAndArea(Polygon poly, double area) implements Comparable { diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java index f44ada3556..d1224780f3 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java @@ -87,6 +87,8 @@ class PlanetilerTests { private static final double Z13_WIDTH = 1d / Z13_TILES; private static final int Z12_TILES = 1 << 12; private static final double Z12_WIDTH = 1d / Z12_TILES; + private static final int Z11_TILES = 1 << 11; + private static final double Z11_WIDTH = 1d / Z11_TILES; private static final int Z4_TILES = 1 << 4; private static final Polygon WORLD_POLYGON = newPolygon( worldCoordinateList( @@ -2434,6 +2436,74 @@ void testCentroidWithLineMinSize() throws Exception { ), results.tiles); } + @Test + void testAttributeMinSizeLine() throws Exception { + List points = z14CoordinatePixelList(0, 4, 40, 4); + + var results = runWithReaderFeatures( + Map.of("threads", "1"), + List.of( + newReaderFeature(newLineString(points), Map.of()) + ), + (in, features) -> features.line("layer") + .setZoomRange(11, 14) + .setBufferPixels(0) + .setAttrWithMinSize("a", "1", 10) + .setAttrWithMinSize("b", "2", 20) + .setAttrWithMinSize("c", "3", 40) + .setAttrWithMinSize("d", "4", 40, 0, 13) // should show up at z13 and above + ); + + assertEquals(Map.ofEntries( + newTileEntry(Z11_TILES / 2, Z11_TILES / 2, 11, List.of( + feature(newLineString(0, 0.5, 5, 0.5), Map.of()) + )), + newTileEntry(Z12_TILES / 2, Z12_TILES / 2, 12, List.of( + feature(newLineString(0, 1, 10, 1), Map.of("a", "1")) + )), + newTileEntry(Z13_TILES / 2, Z13_TILES / 2, 13, List.of( + feature(newLineString(0, 2, 20, 2), Map.of("a", "1", "b", "2", "d", "4")) + )), + newTileEntry(Z14_TILES / 2, Z14_TILES / 2, 14, List.of( + feature(newLineString(0, 4, 40, 4), Map.of("a", "1", "b", "2", "c", "3", "d", "4")) + )) + ), results.tiles); + } + + @Test + void testAttributeMinSizePoint() throws Exception { + List points = z14CoordinatePixelList(0, 4, 40, 4); + + var results = runWithReaderFeatures( + Map.of("threads", "1"), + List.of( + newReaderFeature(newLineString(points), Map.of()) + ), + (in, features) -> features.centroid("layer") + .setZoomRange(11, 14) + .setBufferPixels(0) + .setAttrWithMinSize("a", "1", 10) + .setAttrWithMinSize("b", "2", 20) + .setAttrWithMinSize("c", "3", 40) + .setAttrWithMinSize("d", "4", 40, 0, 13) // should show up at z13 and above + ); + + assertEquals(Map.ofEntries( + newTileEntry(Z11_TILES / 2, Z11_TILES / 2, 11, List.of( + feature(newPoint(2.5, 0.5), Map.of()) + )), + newTileEntry(Z12_TILES / 2, Z12_TILES / 2, 12, List.of( + feature(newPoint(5, 1), Map.of("a", "1")) + )), + newTileEntry(Z13_TILES / 2, Z13_TILES / 2, 13, List.of( + feature(newPoint(10, 2), Map.of("a", "1", "b", "2", "d", "4")) + )), + newTileEntry(Z14_TILES / 2, Z14_TILES / 2, 14, List.of( + feature(newPoint(20, 4), Map.of("a", "1", "b", "2", "c", "3", "d", "4")) + )) + ), results.tiles); + } + @Test void testBoundFiltersFill() throws Exception { var polyResultz8 = runForBoundsTest(8, 8, "polygon", TestUtils.pathToResource("bottomrightearth.poly").toString()); diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/GeoUtilsTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/GeoUtilsTest.java index f1b528f9ac..dead314178 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/GeoUtilsTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/GeoUtilsTest.java @@ -447,4 +447,30 @@ void testSnapAndFixIssue546_3() throws GeometryException, ParseException { assertTrue(result.isValid()); assertFalse(result.contains(point)); } + + @ParameterizedTest + @CsvSource({ + "1,0,0", + "1,10,0", + "1,255,0", + + "0.5,0,0", + "0.5,128,0", + "0.5,129,1", + "0.5,256,1", + + "0.25,0,0", + "0.25,128,1", + "0.25,129,2", + "0.25,256,2", + }) + void minZoomForPixelSize(double worldGeometrySize, double minPixelSize, int expectedMinZoom) { + assertEquals(expectedMinZoom, GeoUtils.minZoomForPixelSize(worldGeometrySize, minPixelSize)); + } + + @Test + void minZoomForPixelSizesAtZ9_10() { + assertEquals(10, GeoUtils.minZoomForPixelSize(3.1 / (256 << 10), 3)); + assertEquals(9, GeoUtils.minZoomForPixelSize(6.1 / (256 << 10), 3)); + } } From 1316a4240074d7a64bc2f276cff6b7fb2507535b Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Wed, 15 Nov 2023 06:55:03 -0500 Subject: [PATCH 02/13] move around methods --- .../planetiler/FeatureCollector.java | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java index 58d824f00f..57ef9a1f18 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java @@ -200,6 +200,28 @@ public Feature innermostPoint(String layer) { return innermostPoint(layer, 0.1); } + /** Returns the minimum zoom level at which this feature is at least {@code pixelSize} pixels large. */ + public int getMinZoomForPixelSize(double pixelSize) { + try { + return GeoUtils.minZoomForPixelSize(source.size(), pixelSize); + } catch (GeometryException e) { + e.log(stats, "min_zoom_for_size_failure", "Error getting min zoom for size from geometry " + source.id()); + return config.maxzoom(); + } + } + + + /** Returns the actual pixel size of the source feature at {@code zoom} (length if line, sqrt(area) if polygon). */ + public double getPixelSizeAtZoom(int zoom) { + try { + return source.size() * (256 << zoom); + } catch (GeometryException e) { + e.log(stats, "source_feature_pixel_size_at_zoom_failure", + "Error getting source feature pixel size at zoom from geometry " + source.id()); + return 0; + } + } + /** * Creates new feature collector instances for each source feature that we encounter. */ @@ -757,20 +779,20 @@ public Feature putAttrs(Map attrs) { } /** - * Sets a special attribute key that the renderer will 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. */ - public Feature setNumPointsAttr(String numPointsAttr) { - this.numPointsAttr = numPointsAttr; - return this; + public String getNumPointsAttr() { + return numPointsAttr; } /** - * Returns the attribute key that the renderer should use to store the number of points in the simplified geometry + * Sets a special attribute key that the renderer will use to store the number of points in the simplified geometry * before slicing it into tiles. */ - public String getNumPointsAttr() { - return numPointsAttr; + public Feature setNumPointsAttr(String numPointsAttr) { + this.numPointsAttr = numPointsAttr; + return this; } @Override @@ -784,23 +806,7 @@ public String toString() { /** Returns the actual pixel size of the source feature at {@code zoom} (length if line, sqrt(area) if polygon). */ public double getSourceFeaturePixelSizeAtZoom(int zoom) { - try { - return source.size() * (256 << zoom); - } catch (GeometryException e) { - e.log(stats, "source_feature_pixel_size_at_zoom_failure", - "Error getting source feature pixel size at zoom from geometry " + source.id()); - return 0; - } - } - - /** Returns the minimum zoom level at which this feature is at least {@code pixelSize} pixels large. */ - public int getMinZoomForPixelSize(double pixelSize) { - try { - return GeoUtils.minZoomForPixelSize(source.size(), pixelSize); - } catch (GeometryException e) { - e.log(stats, "min_zoom_for_size_failure", "Error getting min zoom for size from geometry " + source.id()); - return config.maxzoom(); - } + return getPixelSizeAtZoom(zoom); } } } From b73f132d7e76b0a61fdf24238a2b970c29e458f8 Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Wed, 15 Nov 2023 08:08:37 -0500 Subject: [PATCH 03/13] comment --- .../main/java/com/onthegomap/planetiler/FeatureCollector.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java index 57ef9a1f18..f1e76d8a27 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java @@ -738,7 +738,9 @@ public Feature setAttrWithMinSize(String key, Object value, double minPixelSize) * shows when {@code minZoomIfBigEnough <= zoom_level < minZoomToShowAlways} when it is at least * {@code minPixelSize} pixels in size. *

- * If you need more flexibility, use {@link #getMinZoomForPixelSize(double)} directly. + * 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) { From f5e7dcaf8d37bb9b7f891b465753db74af59718b Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Thu, 16 Nov 2023 20:37:55 -0500 Subject: [PATCH 04/13] tweaks --- .../planetiler/util/Interpolator.java | 134 ++++++++++++++++++ .../planetiler/util/InterpolatorTest.java | 125 ++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java create mode 100644 planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java new file mode 100644 index 0000000000..a41c9e7cf5 --- /dev/null +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java @@ -0,0 +1,134 @@ +package com.onthegomap.planetiler.util; + +import java.util.TreeMap; +import java.util.function.DoubleUnaryOperator; + +public class Interpolator> implements DoubleUnaryOperator { + private static final DoubleUnaryOperator IDENTITY = x -> x; + + private DoubleUnaryOperator transform = IDENTITY; + private DoubleUnaryOperator reverseTransform = IDENTITY; + private boolean clamp = false; + private double defaultValue = Double.NaN; + private final TreeMap stops = new TreeMap<>(); + private TreeMap forwardMap; + private TreeMap reverseMap; + private double minKey = Double.POSITIVE_INFINITY; + private double maxKey = Double.NEGATIVE_INFINITY; + + T setTransforms(DoubleUnaryOperator forward, DoubleUnaryOperator reverse) { + this.transform = forward; + this.reverseTransform = reverse; + return self(); + } + + @Override + public double applyAsDouble(double operand) { + if (clamp) { + operand = Math.clamp(operand, minKey, maxKey); + } + if (Double.isNaN(operand)) { + return defaultValue; + } + var lo = stops.floorEntry(operand); + var hi = stops.higherEntry(operand); + if (lo == null) { + if (hi == null) { + return defaultValue; + } + lo = hi; + hi = stops.higherEntry(lo.getKey()); + if (hi == null) { + return lo.getValue(); + } + } else if (hi == null) { + hi = lo; + lo = stops.lowerEntry(hi.getKey()); + if (lo == null) { + return hi.getValue(); + } + } + double x = transform.applyAsDouble(operand); + double x1 = transform.applyAsDouble(lo.getKey()), + x2 = transform.applyAsDouble(hi.getKey()), + y1 = lo.getValue(), y2 = hi.getValue(); + return (x - x1) / (x2 - x1) * (y2 - y1) + y1; + } + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } + + public T clamp(boolean clamp) { + this.clamp = clamp; + return self(); + } + + public T defaultValue(double value) { + this.defaultValue = value; + return self(); + } + + public T put(double stop, double value) { + minKey = Math.min(stop, minKey); + maxKey = Math.max(stop, maxKey); + stops.put(stop, value); + return self(); + } + + private static class Inverted extends Interpolator {} + + public DoubleUnaryOperator invert() { + var result = new Inverted().setTransforms(IDENTITY, IDENTITY); + for (var entry : stops.entrySet()) { + result.put(entry.getValue(), transform.applyAsDouble(entry.getKey())); + } + DoubleUnaryOperator retVal = result.andThen(reverseTransform); + return clamp ? retVal.andThen(x -> Math.clamp(x, minKey, maxKey)) : retVal; + } + + public static class Power extends Interpolator { + public Power exponent(double exponent) { + double inverse = 1d / exponent; + return setTransforms(x -> Math.pow(x, exponent), y -> Math.pow(y, inverse)); + } + } + + public static class Log extends Interpolator { + + private static DoubleUnaryOperator log(double base) { + double logBase = Math.log(base); + return x -> Math.log(x) / logBase; + } + + public Log base(double base) { + DoubleUnaryOperator forward = + base == 10d ? Math::log10 : + base == Math.E ? Math::log : + log(base); + DoubleUnaryOperator reverse = + base == Math.E ? Math::exp : + x -> Math.pow(base, x); + return setTransforms(forward, reverse); + } + } + + public static class Linear extends Interpolator {} + + public static Linear linear() { + return new Linear(); + } + + public static Log log() { + return new Log().base(10); + } + + public static Power power() { + return new Power().exponent(1); + } + + public static Power sqrt() { + return power().exponent(0.5); + } +} diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java new file mode 100644 index 0000000000..47fbc48301 --- /dev/null +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java @@ -0,0 +1,125 @@ +package com.onthegomap.planetiler.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class InterpolatorTest { + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testLinear(boolean clamp) { + var interpolator = Interpolator.linear() + .put(0, 10) + .put(1, 20) + .clamp(clamp); + assertTransform(interpolator, 0, 10); + assertTransform(interpolator, 1, 20); + assertTransform(interpolator, 0.5, 15); + // clamp + assertClose(clamp ? 20 : 30, interpolator.applyAsDouble(2)); + assertClose(clamp ? 1 : 2, interpolator.invert().applyAsDouble(30)); + assertClose(clamp ? 10 : 0, interpolator.applyAsDouble(-1)); + assertClose(clamp ? 0 : -1, interpolator.invert().applyAsDouble(0)); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testLog(boolean clamp) { + var interpolator = Interpolator.log() + .put(10, 1) + .put(1_000, 2) + .clamp(clamp); + assertTransform(interpolator, 10, 1); + assertTransform(interpolator, 100, 1.5); + assertTransform(interpolator, 1_000, 2); + // clamp + assertClose(clamp ? 1 : 0.5, interpolator.applyAsDouble(1)); + assertClose(clamp ? 10 : 1, interpolator.invert().applyAsDouble(0.5)); + assertClose(clamp ? 2 : 2.5, interpolator.applyAsDouble(10_000)); + assertClose(clamp ? 1_000 : 10_000, interpolator.invert().applyAsDouble(2.5)); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testLog2(boolean clamp) { + var interpolator = Interpolator.log() + .base(2) + .put(2, 1) + .put(8, 2) + .clamp(clamp); + assertTransform(interpolator, 2, 1); + assertTransform(interpolator, 4, 1.5); + assertTransform(interpolator, 8, 2); + // clamp + assertClose(clamp ? 1 : 0.5, interpolator.applyAsDouble(1)); + assertClose(clamp ? 2 : 1, interpolator.invert().applyAsDouble(0.5)); + assertClose(clamp ? 2 : 2.5, interpolator.applyAsDouble(16)); + assertClose(clamp ? 8 : 16, interpolator.invert().applyAsDouble(2.5)); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testPower(boolean clamp) { + var interpolator = Interpolator.power() + .exponent(2) + .put(Math.sqrt(2), 1) + .put(Math.sqrt(4), 2) + .clamp(clamp); + assertTransform(interpolator, Math.sqrt(2), 1); + assertTransform(interpolator, Math.sqrt(3), 1.5); + assertTransform(interpolator, Math.sqrt(4), 2); + // clamp + assertClose(clamp ? 1 : 0.5, interpolator.applyAsDouble(1)); + assertClose(clamp ? Math.sqrt(2) : 1, interpolator.invert().applyAsDouble(0.5)); + assertClose(clamp ? 2 : 2.5, interpolator.applyAsDouble(Math.sqrt(5))); + assertClose(clamp ? Math.sqrt(4) : Math.sqrt(5), interpolator.invert().applyAsDouble(2.5)); + } + + @Test + void testNegativePower() { + var interpolator = Interpolator.power() + .exponent(-1) + .put(1, 0) + .put(2, 1); + assertTransform(interpolator, 1, 0); + assertTransform(interpolator, 1.5, 2d / 3); + assertTransform(interpolator, 2, 1); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testSqrt(boolean clamp) { + var interpolator = Interpolator.sqrt() + .put(4, 1) + .put(16, 2) + .clamp(clamp); + assertTransform(interpolator, 4, 1); + assertTransform(interpolator, 9, 1.5); + assertTransform(interpolator, 16, 2); + // clamp + assertClose(clamp ? 1 : 0.5, interpolator.applyAsDouble(1)); + assertClose(clamp ? 4 : 1, interpolator.invert().applyAsDouble(0.5)); + assertClose(clamp ? 2 : 2.5, interpolator.applyAsDouble(25)); + assertClose(clamp ? 16 : 25, interpolator.invert().applyAsDouble(2.5)); + } + + @Test + void testUnknown() { + var interpolator = Interpolator.linear() + .put(0, 10) + .put(1, 20) + .defaultValue(100); + assertEquals(100, interpolator.applyAsDouble(Double.NaN)); + } + + private static void assertClose(double expected, double actual) { + assertEquals(expected, actual, 1e-10); + } + + private static void assertTransform(Interpolator interp, double x, double y) { + assertClose(y, interp.applyAsDouble(x)); + assertClose(x, interp.invert().applyAsDouble(y)); + } +} From 0fd893021dfa6bae1b31b85e79b63dc4ddce9b0f Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Fri, 17 Nov 2023 05:50:31 -0500 Subject: [PATCH 05/13] more cases and benchmark --- .../benchmarks/BenchmarkInterpolator.java | 51 +++++++++++++++++++ .../planetiler/util/InterpolatorTest.java | 40 +++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java diff --git a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java new file mode 100644 index 0000000000..3fea59f661 --- /dev/null +++ b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java @@ -0,0 +1,51 @@ +package com.onthegomap.planetiler.benchmarks; + +import com.onthegomap.planetiler.util.Interpolator; +import java.util.function.DoubleUnaryOperator; +import java.util.function.Supplier; + +public class BenchmarkInterpolator { + private static double sum = 0; + + public static void main(String[] args) { + long times = 10_000_000; + benchmarkInterpolator("linear", times, Interpolator::linear); + benchmarkInterpolator("sqrt", times, Interpolator::sqrt); + benchmarkInterpolator("pow2", times, () -> Interpolator.power().exponent(2)); + benchmarkInterpolator("pow", times, Interpolator::power); + benchmarkInterpolator("log", times, Interpolator::log); + benchmarkInterpolator("log2", times, () -> Interpolator.log().base(2)); + System.err.println(sum); + } + + private static void benchmarkInterpolator(String name, long times, Supplier> get) { + benchmarkAndInverted(name + "_2", 1, 2, times, () -> get.get().put(1, 1).put(2, 2)); + benchmarkAndInverted(name + "_3", 1, 2, times, () -> get.get().put(1, 1).put(1.5, 2).put(2, 3)); + } + + private static void benchmarkAndInverted(String name, double start, double end, long steps, + Supplier> build) { + benchmark(name + "_f", start, end, steps, build::get); + benchmark(name + "_i", start, end, steps, () -> build.get().invert()); + } + + private static void benchmark(String name, double start, double end, long steps, + Supplier build) { + long a = System.currentTimeMillis(); + double delta = (end - start) / steps; + double x = start; + double result = 0; + for (long i = 0; i < steps; i++) { + result += build.get().applyAsDouble(x += delta); + } + long b = System.currentTimeMillis(); + x = start; + var preBuilt = build.get(); + for (long i = 0; i < steps; i++) { + result += preBuilt.applyAsDouble(x += delta); + } + long c = System.currentTimeMillis(); + sum += result; + System.err.println(name + "\t" + (b - a) + "\t" + (c - b)); + } +} diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java index 47fbc48301..058b5d59e7 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java @@ -105,6 +105,46 @@ void testSqrt(boolean clamp) { assertClose(clamp ? 16 : 25, interpolator.invert().applyAsDouble(2.5)); } + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testMultipartDescending(boolean clamp) { + var interpolator = Interpolator.linear() + .put(4, 1) + .put(2, 2) + .put(1, 4) + .clamp(clamp); + assertTransform(interpolator, 4, 1); + assertTransform(interpolator, 3, 1.5); + assertTransform(interpolator, 2, 2); + assertTransform(interpolator, 1.5, 3); + assertTransform(interpolator, 1, 4); + // clamp + assertClose(clamp ? 1 : 0.5, interpolator.applyAsDouble(5)); + assertClose(clamp ? 4 : 5, interpolator.invert().applyAsDouble(0.5)); + assertClose(clamp ? 4 : 6, interpolator.applyAsDouble(0)); + assertClose(clamp ? 1 : 0, interpolator.invert().applyAsDouble(6)); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testMultipartAscending(boolean clamp) { + var interpolator = Interpolator.linear() + .put(1, 4) + .put(2, 2) + .put(4, 1) + .clamp(clamp); + assertTransform(interpolator, 1, 4); + assertTransform(interpolator, 1.5, 3); + assertTransform(interpolator, 2, 2); + assertTransform(interpolator, 3, 1.5); + assertTransform(interpolator, 4, 1); + // clamp + assertClose(clamp ? 1 : 0.5, interpolator.invert().applyAsDouble(5)); + assertClose(clamp ? 4 : 5, interpolator.applyAsDouble(0.5)); + assertClose(clamp ? 4 : 6, interpolator.invert().applyAsDouble(0)); + assertClose(clamp ? 1 : 0, interpolator.applyAsDouble(6)); + } + @Test void testUnknown() { var interpolator = Interpolator.linear() From 3d182c2e9bbf850bc563c3a487b989e084b05e89 Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Fri, 17 Nov 2023 09:12:12 -0500 Subject: [PATCH 06/13] d3 style --- .../benchmarks/BenchmarkInterpolator.java | 8 +- .../planetiler/util/Interpolator.java | 108 +++++++++++++----- 2 files changed, 86 insertions(+), 30 deletions(-) diff --git a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java index 3fea59f661..a2e5b98366 100644 --- a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java +++ b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java @@ -31,16 +31,20 @@ private static void benchmarkAndInverted(String name, double start, double end, private static void benchmark(String name, double start, double end, long steps, Supplier build) { - long a = System.currentTimeMillis(); double delta = (end - start) / steps; double x = start; double result = 0; + for (long i = 0; i < steps / 10_000; i++) { + result += build.get().applyAsDouble(x += delta); + } + x = start; + long a = System.currentTimeMillis(); for (long i = 0; i < steps; i++) { result += build.get().applyAsDouble(x += delta); } - long b = System.currentTimeMillis(); x = start; var preBuilt = build.get(); + long b = System.currentTimeMillis(); for (long i = 0; i < steps; i++) { result += preBuilt.applyAsDouble(x += delta); } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java index a41c9e7cf5..0c864b7bca 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java @@ -1,7 +1,8 @@ package com.onthegomap.planetiler.util; -import java.util.TreeMap; +import com.carrotsearch.hppc.DoubleArrayList; import java.util.function.DoubleUnaryOperator; +import org.apache.commons.lang3.ArrayUtils; public class Interpolator> implements DoubleUnaryOperator { private static final DoubleUnaryOperator IDENTITY = x -> x; @@ -10,13 +11,14 @@ public class Interpolator> implements DoubleUnaryOpera private DoubleUnaryOperator reverseTransform = IDENTITY; private boolean clamp = false; private double defaultValue = Double.NaN; - private final TreeMap stops = new TreeMap<>(); - private TreeMap forwardMap; - private TreeMap reverseMap; + private final DoubleArrayList domain = new DoubleArrayList(); + private final DoubleArrayList range = new DoubleArrayList(); + private DoubleUnaryOperator fn; private double minKey = Double.POSITIVE_INFINITY; private double maxKey = Double.NEGATIVE_INFINITY; T setTransforms(DoubleUnaryOperator forward, DoubleUnaryOperator reverse) { + fn = null; this.transform = forward; this.reverseTransform = reverse; return self(); @@ -30,29 +32,76 @@ public double applyAsDouble(double operand) { if (Double.isNaN(operand)) { return defaultValue; } - var lo = stops.floorEntry(operand); - var hi = stops.higherEntry(operand); - if (lo == null) { - if (hi == null) { - return defaultValue; + if (fn == null) { + fn = rescale(); + } + return fn.applyAsDouble(transform.applyAsDouble(operand)); + } + + private DoubleUnaryOperator rescale() { + if (domain.size() > 2) { + int j = Math.min(domain.size(), range.size()) - 1; + DoubleUnaryOperator[] d = new DoubleUnaryOperator[j]; + DoubleUnaryOperator[] r = new DoubleUnaryOperator[j]; + int i = -1; + + double[] domainItems = new double[domain.size()]; + for (int k = 0; k < domainItems.length; k++) { + domainItems[k] = transform.applyAsDouble(domain.get(k)); } - lo = hi; - hi = stops.higherEntry(lo.getKey()); - if (hi == null) { - return lo.getValue(); + double[] rangeItems = range.toArray(); + + // Reverse descending domains. + if (domainItems[j] < domainItems[0]) { + ArrayUtils.reverse(domainItems); + ArrayUtils.reverse(rangeItems); } - } else if (hi == null) { - hi = lo; - lo = stops.lowerEntry(hi.getKey()); - if (lo == null) { - return hi.getValue(); + + while (++i < j) { + d[i] = normalize(domainItems[i], domainItems[i + 1]); + r[i] = interpolate(rangeItems[i], rangeItems[i + 1]); } + + return x -> { + int ii = bisect(domainItems, x, 1, j) - 1; + return r[ii].applyAsDouble(d[ii].applyAsDouble(x)); + }; + } else { + double d0 = transform.applyAsDouble(domain.get(0)), d1 = transform.applyAsDouble(domain.get(1)), + r0 = range.get(0), r1 = range.get(1); + boolean reverse = d1 < d0; + final double dlo = reverse ? d1 : d0; + final double dhi = reverse ? d0 : d1; + final double rlo = reverse ? r1 : r0; + final double rhi = reverse ? r0 : r1; + double delta = dhi - dlo; + return x -> { + double t = delta == 0 ? 0.5 : Double.isNaN(delta) ? Double.NaN : (x - dlo) / delta; + return rlo * (1 - t) + rhi * t; + }; } - double x = transform.applyAsDouble(operand); - double x1 = transform.applyAsDouble(lo.getKey()), - x2 = transform.applyAsDouble(hi.getKey()), - y1 = lo.getValue(), y2 = hi.getValue(); - return (x - x1) / (x2 - x1) * (y2 - y1) + y1; + } + + private static int bisect(double[] a, double x, int lo, int hi) { + if (lo < hi) { + do { + int mid = (lo + hi) >>> 1; + if (a[mid] <= x) + lo = mid + 1; + else + hi = mid; + } while (lo < hi); + } + return lo; + } + + private static DoubleUnaryOperator interpolate(double a, double b) { + return t -> a * (1 - t) + b * t; + } + + private static DoubleUnaryOperator normalize(double a, double b) { + double delta = b - a; + return delta == 0 ? x -> 0.5 : Double.isNaN(delta) ? x -> Double.NaN : x -> (x - a) / delta; } @SuppressWarnings("unchecked") @@ -71,20 +120,23 @@ public T defaultValue(double value) { } public T put(double stop, double value) { + fn = null; minKey = Math.min(stop, minKey); maxKey = Math.max(stop, maxKey); - stops.put(stop, value); + domain.add(stop); + range.add(value); return self(); } private static class Inverted extends Interpolator {} public DoubleUnaryOperator invert() { - var result = new Inverted().setTransforms(IDENTITY, IDENTITY); - for (var entry : stops.entrySet()) { - result.put(entry.getValue(), transform.applyAsDouble(entry.getKey())); + Interpolator result = new Inverted(); + result.domain.addAll(range); + for (int i = 0; i < domain.size(); i++) { + result.range.add(transform.applyAsDouble(domain.get(i))); } - DoubleUnaryOperator retVal = result.andThen(reverseTransform); + DoubleUnaryOperator retVal = reverseTransform == IDENTITY ? result : result.andThen(reverseTransform); return clamp ? retVal.andThen(x -> Math.clamp(x, minKey, maxKey)) : retVal; } From ac3ad9165909a679fbe329e96977c682b9e9fba6 Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Sat, 18 Nov 2023 05:31:50 -0500 Subject: [PATCH 07/13] arbitrary type --- .../benchmarks/BenchmarkInterpolator.java | 10 +- .../planetiler/util/Interpolator.java | 126 ++++++++++++------ .../planetiler/util/InterpolatorTest.java | 44 +++--- 3 files changed, 111 insertions(+), 69 deletions(-) diff --git a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java index a2e5b98366..9d089b76fa 100644 --- a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java +++ b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java @@ -18,14 +18,14 @@ public static void main(String[] args) { System.err.println(sum); } - private static void benchmarkInterpolator(String name, long times, Supplier> get) { - benchmarkAndInverted(name + "_2", 1, 2, times, () -> get.get().put(1, 1).put(2, 2)); - benchmarkAndInverted(name + "_3", 1, 2, times, () -> get.get().put(1, 1).put(1.5, 2).put(2, 3)); + private static void benchmarkInterpolator(String name, long times, Supplier> get) { + benchmarkAndInverted(name + "_2", 1, 2, times, () -> get.get().put(1, 1d).put(2, 2d)); + benchmarkAndInverted(name + "_3", 1, 2, times, () -> get.get().put(1, 1d).put(1.5, 2d).put(2, 3d)); } private static void benchmarkAndInverted(String name, double start, double end, long steps, - Supplier> build) { - benchmark(name + "_f", start, end, steps, build::get); + Supplier> build) { + benchmark(name + "_f", start, end, steps, () -> build.get()::applyAsDouble); benchmark(name + "_i", start, end, steps, () -> build.get().invert()); } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java index 0c864b7bca..142af32f7c 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java @@ -1,22 +1,33 @@ package com.onthegomap.planetiler.util; import com.carrotsearch.hppc.DoubleArrayList; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.DoubleFunction; import java.util.function.DoubleUnaryOperator; import org.apache.commons.lang3.ArrayUtils; -public class Interpolator> implements DoubleUnaryOperator { +public class Interpolator, V> implements DoubleFunction { + private static final ValueInterpolator INTERPOLATE_NUMERIC = + (a, b) -> t -> a * (1 - t) + b * t; private static final DoubleUnaryOperator IDENTITY = x -> x; + private final ValueInterpolator valueInterpolator; private DoubleUnaryOperator transform = IDENTITY; private DoubleUnaryOperator reverseTransform = IDENTITY; private boolean clamp = false; - private double defaultValue = Double.NaN; + private V defaultValue; private final DoubleArrayList domain = new DoubleArrayList(); - private final DoubleArrayList range = new DoubleArrayList(); - private DoubleUnaryOperator fn; + private final List range = new ArrayList<>(); + private DoubleFunction fn; private double minKey = Double.POSITIVE_INFINITY; private double maxKey = Double.NEGATIVE_INFINITY; + protected Interpolator(ValueInterpolator valueInterpolator) { + this.valueInterpolator = valueInterpolator; + } + T setTransforms(DoubleUnaryOperator forward, DoubleUnaryOperator reverse) { fn = null; this.transform = forward; @@ -25,7 +36,7 @@ T setTransforms(DoubleUnaryOperator forward, DoubleUnaryOperator reverse) { } @Override - public double applyAsDouble(double operand) { + public V apply(double operand) { if (clamp) { operand = Math.clamp(operand, minKey, maxKey); } @@ -35,50 +46,51 @@ public double applyAsDouble(double operand) { if (fn == null) { fn = rescale(); } - return fn.applyAsDouble(transform.applyAsDouble(operand)); + return fn.apply(transform.applyAsDouble(operand)); + } + + public double applyAsDouble(double operand) { + return apply(operand) instanceof Number n ? n.doubleValue() : Double.NaN; } - private DoubleUnaryOperator rescale() { + private DoubleFunction rescale() { if (domain.size() > 2) { int j = Math.min(domain.size(), range.size()) - 1; DoubleUnaryOperator[] d = new DoubleUnaryOperator[j]; - DoubleUnaryOperator[] r = new DoubleUnaryOperator[j]; + DoubleFunction[] r = new DoubleFunction[j]; int i = -1; double[] domainItems = new double[domain.size()]; for (int k = 0; k < domainItems.length; k++) { domainItems[k] = transform.applyAsDouble(domain.get(k)); } - double[] rangeItems = range.toArray(); + List rangeItems = domainItems[j] < domainItems[0] ? range.reversed() : range; // Reverse descending domains. if (domainItems[j] < domainItems[0]) { ArrayUtils.reverse(domainItems); - ArrayUtils.reverse(rangeItems); } while (++i < j) { d[i] = normalize(domainItems[i], domainItems[i + 1]); - r[i] = interpolate(rangeItems[i], rangeItems[i + 1]); + r[i] = valueInterpolator.apply(rangeItems.get(i), rangeItems.get(i + 1)); } return x -> { int ii = bisect(domainItems, x, 1, j) - 1; - return r[ii].applyAsDouble(d[ii].applyAsDouble(x)); + return r[ii].apply(d[ii].applyAsDouble(x)); }; } else { - double d0 = transform.applyAsDouble(domain.get(0)), d1 = transform.applyAsDouble(domain.get(1)), - r0 = range.get(0), r1 = range.get(1); + double d0 = transform.applyAsDouble(domain.get(0)), d1 = transform.applyAsDouble(domain.get(1)); + V r0 = range.get(0), r1 = range.get(1); boolean reverse = d1 < d0; final double dlo = reverse ? d1 : d0; final double dhi = reverse ? d0 : d1; - final double rlo = reverse ? r1 : r0; - final double rhi = reverse ? r0 : r1; - double delta = dhi - dlo; - return x -> { - double t = delta == 0 ? 0.5 : Double.isNaN(delta) ? Double.NaN : (x - dlo) / delta; - return rlo * (1 - t) + rhi * t; - }; + final V rlo = reverse ? r1 : r0; + final V rhi = reverse ? r0 : r1; + DoubleUnaryOperator normalize = normalize(dlo, dhi); + DoubleFunction interpolate = valueInterpolator.apply(rlo, rhi); + return x -> interpolate.apply(normalize.applyAsDouble(x)); } } @@ -114,12 +126,12 @@ public T clamp(boolean clamp) { return self(); } - public T defaultValue(double value) { + public T defaultValue(V value) { this.defaultValue = value; return self(); } - public T put(double stop, double value) { + public T put(double stop, V value) { fn = null; minKey = Math.min(stop, minKey); maxKey = Math.max(stop, maxKey); @@ -128,33 +140,44 @@ public T put(double stop, double value) { return self(); } - private static class Inverted extends Interpolator {} + // TODO + // private static class Inverted extends Interpolator {} public DoubleUnaryOperator invert() { - Interpolator result = new Inverted(); - result.domain.addAll(range); - for (int i = 0; i < domain.size(); i++) { - result.range.add(transform.applyAsDouble(domain.get(i))); + var result = linear(); + int j = Math.min(domain.size(), range.size()); + for (int i = 0; i < j; i++) { + result.put((Double) range.get(i), transform.applyAsDouble(domain.get(i))); } - DoubleUnaryOperator retVal = reverseTransform == IDENTITY ? result : result.andThen(reverseTransform); + DoubleUnaryOperator retVal = + reverseTransform == IDENTITY ? result::applyAsDouble : x -> reverseTransform.applyAsDouble(result.apply(x)); return clamp ? retVal.andThen(x -> Math.clamp(x, minKey, maxKey)) : retVal; } - public static class Power extends Interpolator { - public Power exponent(double exponent) { + public static class Power extends Interpolator, V> { + + public Power(ValueInterpolator valueInterpolator) { + super(valueInterpolator); + } + + public Power exponent(double exponent) { double inverse = 1d / exponent; return setTransforms(x -> Math.pow(x, exponent), y -> Math.pow(y, inverse)); } } - public static class Log extends Interpolator { + public static class Log extends Interpolator, V> { + + public Log(ValueInterpolator valueInterpolator) { + super(valueInterpolator); + } private static DoubleUnaryOperator log(double base) { double logBase = Math.log(base); return x -> Math.log(x) / logBase; } - public Log base(double base) { + public Log base(double base) { DoubleUnaryOperator forward = base == 10d ? Math::log10 : base == Math.E ? Math::log : @@ -166,21 +189,40 @@ public Log base(double base) { } } - public static class Linear extends Interpolator {} + public static class Linear extends Interpolator, V> { + + public Linear(ValueInterpolator valueInterpolator) { + super(valueInterpolator); + } + } + + public static Linear linear(ValueInterpolator valueInterpolator) { + return new Linear<>(valueInterpolator); + } - public static Linear linear() { - return new Linear(); + public static Linear linear() { + return new Linear<>(INTERPOLATE_NUMERIC); } - public static Log log() { - return new Log().base(10); + public static Log log(ValueInterpolator valueInterpolator) { + return new Log<>(valueInterpolator).base(10); } - public static Power power() { - return new Power().exponent(1); + public static Log log() { + return log(INTERPOLATE_NUMERIC).base(10); } - public static Power sqrt() { - return power().exponent(0.5); + public static Power power(ValueInterpolator valueInterpolator) { + return new Power<>(valueInterpolator).exponent(1); } + + public static Power power() { + return new Power<>(INTERPOLATE_NUMERIC).exponent(1); + } + + public static Power sqrt() { + return power(INTERPOLATE_NUMERIC).exponent(0.5); + } + + public interface ValueInterpolator extends BiFunction> {} } diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java index 058b5d59e7..9a2a927618 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java @@ -11,8 +11,8 @@ class InterpolatorTest { @ValueSource(booleans = {false, true}) void testLinear(boolean clamp) { var interpolator = Interpolator.linear() - .put(0, 10) - .put(1, 20) + .put(0, 10d) + .put(1, 20d) .clamp(clamp); assertTransform(interpolator, 0, 10); assertTransform(interpolator, 1, 20); @@ -28,8 +28,8 @@ void testLinear(boolean clamp) { @ValueSource(booleans = {false, true}) void testLog(boolean clamp) { var interpolator = Interpolator.log() - .put(10, 1) - .put(1_000, 2) + .put(10, 1d) + .put(1_000, 2d) .clamp(clamp); assertTransform(interpolator, 10, 1); assertTransform(interpolator, 100, 1.5); @@ -46,8 +46,8 @@ void testLog(boolean clamp) { void testLog2(boolean clamp) { var interpolator = Interpolator.log() .base(2) - .put(2, 1) - .put(8, 2) + .put(2, 1d) + .put(8, 2d) .clamp(clamp); assertTransform(interpolator, 2, 1); assertTransform(interpolator, 4, 1.5); @@ -64,8 +64,8 @@ void testLog2(boolean clamp) { void testPower(boolean clamp) { var interpolator = Interpolator.power() .exponent(2) - .put(Math.sqrt(2), 1) - .put(Math.sqrt(4), 2) + .put(Math.sqrt(2), 1d) + .put(Math.sqrt(4), 2d) .clamp(clamp); assertTransform(interpolator, Math.sqrt(2), 1); assertTransform(interpolator, Math.sqrt(3), 1.5); @@ -81,8 +81,8 @@ void testPower(boolean clamp) { void testNegativePower() { var interpolator = Interpolator.power() .exponent(-1) - .put(1, 0) - .put(2, 1); + .put(1, 0d) + .put(2, 1d); assertTransform(interpolator, 1, 0); assertTransform(interpolator, 1.5, 2d / 3); assertTransform(interpolator, 2, 1); @@ -92,8 +92,8 @@ void testNegativePower() { @ValueSource(booleans = {false, true}) void testSqrt(boolean clamp) { var interpolator = Interpolator.sqrt() - .put(4, 1) - .put(16, 2) + .put(4, 1d) + .put(16, 2d) .clamp(clamp); assertTransform(interpolator, 4, 1); assertTransform(interpolator, 9, 1.5); @@ -109,9 +109,9 @@ void testSqrt(boolean clamp) { @ValueSource(booleans = {false, true}) void testMultipartDescending(boolean clamp) { var interpolator = Interpolator.linear() - .put(4, 1) - .put(2, 2) - .put(1, 4) + .put(4, 1d) + .put(2, 2d) + .put(1, 4d) .clamp(clamp); assertTransform(interpolator, 4, 1); assertTransform(interpolator, 3, 1.5); @@ -129,9 +129,9 @@ void testMultipartDescending(boolean clamp) { @ValueSource(booleans = {false, true}) void testMultipartAscending(boolean clamp) { var interpolator = Interpolator.linear() - .put(1, 4) - .put(2, 2) - .put(4, 1) + .put(1, 4d) + .put(2, 2d) + .put(4, 1d) .clamp(clamp); assertTransform(interpolator, 1, 4); assertTransform(interpolator, 1.5, 3); @@ -148,9 +148,9 @@ void testMultipartAscending(boolean clamp) { @Test void testUnknown() { var interpolator = Interpolator.linear() - .put(0, 10) - .put(1, 20) - .defaultValue(100); + .put(0, 10d) + .put(1, 20d) + .defaultValue(100d); assertEquals(100, interpolator.applyAsDouble(Double.NaN)); } @@ -158,7 +158,7 @@ private static void assertClose(double expected, double actual) { assertEquals(expected, actual, 1e-10); } - private static void assertTransform(Interpolator interp, double x, double y) { + private static void assertTransform(Interpolator interp, double x, double y) { assertClose(y, interp.applyAsDouble(x)); assertClose(x, interp.invert().applyAsDouble(y)); } From 567ff063c11d1326132701fd721123708c90f0a0 Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Sat, 18 Nov 2023 05:40:08 -0500 Subject: [PATCH 08/13] static invert --- .../benchmarks/BenchmarkInterpolator.java | 2 +- .../planetiler/util/Interpolator.java | 29 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java index 9d089b76fa..17461c8dea 100644 --- a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java +++ b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java @@ -26,7 +26,7 @@ private static void benchmarkInterpolator(String name, long times, Supplier> build) { benchmark(name + "_f", start, end, steps, () -> build.get()::applyAsDouble); - benchmark(name + "_i", start, end, steps, () -> build.get().invert()); + benchmark(name + "_i", start, end, steps, () -> Interpolator.invert(build.get())); } private static void benchmark(String name, double start, double end, long steps, diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java index 142af32f7c..313f028e32 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java @@ -14,15 +14,15 @@ public class Interpolator, V> implements DoubleFunc private static final DoubleUnaryOperator IDENTITY = x -> x; private final ValueInterpolator valueInterpolator; - private DoubleUnaryOperator transform = IDENTITY; - private DoubleUnaryOperator reverseTransform = IDENTITY; - private boolean clamp = false; + DoubleUnaryOperator transform = IDENTITY; + DoubleUnaryOperator reverseTransform = IDENTITY; + boolean clamp = false; private V defaultValue; - private final DoubleArrayList domain = new DoubleArrayList(); - private final List range = new ArrayList<>(); + final DoubleArrayList domain = new DoubleArrayList(); + final List range = new ArrayList<>(); private DoubleFunction fn; - private double minKey = Double.POSITIVE_INFINITY; - private double maxKey = Double.NEGATIVE_INFINITY; + double minKey = Double.POSITIVE_INFINITY; + double maxKey = Double.NEGATIVE_INFINITY; protected Interpolator(ValueInterpolator valueInterpolator) { this.valueInterpolator = valueInterpolator; @@ -144,6 +144,7 @@ public T put(double stop, V value) { // private static class Inverted extends Interpolator {} public DoubleUnaryOperator invert() { + // TODO static or instance? var result = linear(); int j = Math.min(domain.size(), range.size()); for (int i = 0; i < j; i++) { @@ -154,6 +155,20 @@ public DoubleUnaryOperator invert() { return clamp ? retVal.andThen(x -> Math.clamp(x, minKey, maxKey)) : retVal; } + public static > DoubleUnaryOperator invert( + Interpolator interpolator) { + var result = linear(); + int j = Math.min(interpolator.domain.size(), interpolator.range.size()); + for (int i = 0; i < j; i++) { + result.put(interpolator.range.get(i).doubleValue(), + interpolator.transform.applyAsDouble(interpolator.domain.get(i))); + } + DoubleUnaryOperator retVal = + interpolator.reverseTransform == IDENTITY ? result::applyAsDouble : + x -> interpolator.reverseTransform.applyAsDouble(result.apply(x)); + return interpolator.clamp ? retVal.andThen(x -> Math.clamp(x, interpolator.minKey, interpolator.maxKey)) : retVal; + } + public static class Power extends Interpolator, V> { public Power(ValueInterpolator valueInterpolator) { From 69b20a4d47c5e7e625cf0261e8506509dce9f1c9 Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Sat, 18 Nov 2023 15:34:08 -0500 Subject: [PATCH 09/13] scales --- .../benchmarks/BenchmarkInterpolator.java | 22 +- .../planetiler/util/IInterpolator.java | 24 ++ .../planetiler/util/Interpolator.java | 101 ++++--- .../onthegomap/planetiler/util/Scales.java | 278 ++++++++++++++++++ .../planetiler/util/InterpolatorTest.java | 27 +- 5 files changed, 390 insertions(+), 62 deletions(-) create mode 100644 planetiler-core/src/main/java/com/onthegomap/planetiler/util/IInterpolator.java create mode 100644 planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java diff --git a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java index 17461c8dea..94f113cac1 100644 --- a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java +++ b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkInterpolator.java @@ -1,6 +1,6 @@ package com.onthegomap.planetiler.benchmarks; -import com.onthegomap.planetiler.util.Interpolator; +import com.onthegomap.planetiler.util.Scales; import java.util.function.DoubleUnaryOperator; import java.util.function.Supplier; @@ -9,24 +9,24 @@ public class BenchmarkInterpolator { public static void main(String[] args) { long times = 10_000_000; - benchmarkInterpolator("linear", times, Interpolator::linear); - benchmarkInterpolator("sqrt", times, Interpolator::sqrt); - benchmarkInterpolator("pow2", times, () -> Interpolator.power().exponent(2)); - benchmarkInterpolator("pow", times, Interpolator::power); - benchmarkInterpolator("log", times, Interpolator::log); - benchmarkInterpolator("log2", times, () -> Interpolator.log().base(2)); + benchmarkInterpolator("linear", times, Scales::linear); + benchmarkInterpolator("sqrt", times, Scales::sqrt); + benchmarkInterpolator("pow2", times, () -> Scales.power(2)); + benchmarkInterpolator("pow10", times, () -> Scales.power(1)); + benchmarkInterpolator("log", times, () -> Scales.log(10)); + benchmarkInterpolator("log2", times, () -> Scales.log(2)); System.err.println(sum); } - private static void benchmarkInterpolator(String name, long times, Supplier> get) { + private static void benchmarkInterpolator(String name, long times, Supplier get) { benchmarkAndInverted(name + "_2", 1, 2, times, () -> get.get().put(1, 1d).put(2, 2d)); benchmarkAndInverted(name + "_3", 1, 2, times, () -> get.get().put(1, 1d).put(1.5, 2d).put(2, 3d)); } private static void benchmarkAndInverted(String name, double start, double end, long steps, - Supplier> build) { - benchmark(name + "_f", start, end, steps, () -> build.get()::applyAsDouble); - benchmark(name + "_i", start, end, steps, () -> Interpolator.invert(build.get())); + Supplier build) { + benchmark(name + "_f", start, end, steps, build::get); + benchmark(name + "_i", start, end, steps, () -> build.get().invert()); } private static void benchmark(String name, double start, double end, long steps, diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/IInterpolator.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/IInterpolator.java new file mode 100644 index 0000000000..870d7c9c7c --- /dev/null +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/IInterpolator.java @@ -0,0 +1,24 @@ +package com.onthegomap.planetiler.util; + +import java.util.function.BiFunction; +import java.util.function.DoubleFunction; +import java.util.function.DoubleUnaryOperator; + +public interface IInterpolator, V> extends DoubleFunction { + T self(); + + T put(double x, V y); + + + interface ValueInterpolator extends BiFunction> {} + interface Continuous & Continuous> + extends IInterpolator, DoubleUnaryOperator { + default DoubleUnaryOperator invert() { + return Interpolator.invertIt(this.self()); + } + + default T put(double x, double y) { + return put(x, Double.valueOf(y)); + } + } +} diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java index 313f028e32..4714638e8c 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Interpolator.java @@ -3,12 +3,11 @@ import com.carrotsearch.hppc.DoubleArrayList; import java.util.ArrayList; import java.util.List; -import java.util.function.BiFunction; import java.util.function.DoubleFunction; import java.util.function.DoubleUnaryOperator; import org.apache.commons.lang3.ArrayUtils; -public class Interpolator, V> implements DoubleFunction { +public class Interpolator, V> implements IInterpolator { private static final ValueInterpolator INTERPOLATE_NUMERIC = (a, b) -> t -> a * (1 - t) + b * t; private static final DoubleUnaryOperator IDENTITY = x -> x; @@ -117,7 +116,7 @@ private static DoubleUnaryOperator normalize(double a, double b) { } @SuppressWarnings("unchecked") - private T self() { + public T self() { return (T) this; } @@ -143,19 +142,7 @@ public T put(double stop, V value) { // TODO // private static class Inverted extends Interpolator {} - public DoubleUnaryOperator invert() { - // TODO static or instance? - var result = linear(); - int j = Math.min(domain.size(), range.size()); - for (int i = 0; i < j; i++) { - result.put((Double) range.get(i), transform.applyAsDouble(domain.get(i))); - } - DoubleUnaryOperator retVal = - reverseTransform == IDENTITY ? result::applyAsDouble : x -> reverseTransform.applyAsDouble(result.apply(x)); - return clamp ? retVal.andThen(x -> Math.clamp(x, minKey, maxKey)) : retVal; - } - - public static > DoubleUnaryOperator invert( + public static > DoubleUnaryOperator invertIt( Interpolator interpolator) { var result = linear(); int j = Math.min(interpolator.domain.size(), interpolator.range.size()); @@ -169,19 +156,32 @@ public DoubleUnaryOperator invert() { return interpolator.clamp ? retVal.andThen(x -> Math.clamp(x, interpolator.minKey, interpolator.maxKey)) : retVal; } - public static class Power extends Interpolator, V> { + public static class Power, V> extends Interpolator { public Power(ValueInterpolator valueInterpolator) { super(valueInterpolator); } - public Power exponent(double exponent) { + public T exponent(double exponent) { double inverse = 1d / exponent; return setTransforms(x -> Math.pow(x, exponent), y -> Math.pow(y, inverse)); } + + public static class Numeric extends Power implements IInterpolator.Continuous { + + public Numeric(ValueInterpolator valueInterpolator) { + super(valueInterpolator); + } + } + public static class Other extends Power, V> { + + public Other(ValueInterpolator valueInterpolator) { + super(valueInterpolator); + } + } } - public static class Log extends Interpolator, V> { + public static class Log, V> extends Interpolator { public Log(ValueInterpolator valueInterpolator) { super(valueInterpolator); @@ -192,7 +192,7 @@ private static DoubleUnaryOperator log(double base) { return x -> Math.log(x) / logBase; } - public Log base(double base) { + public T base(double base) { DoubleUnaryOperator forward = base == 10d ? Math::log10 : base == Math.E ? Math::log : @@ -202,42 +202,71 @@ public Log base(double base) { x -> Math.pow(base, x); return setTransforms(forward, reverse); } + + public static class Numeric extends Log implements IInterpolator.Continuous { + + public Numeric(ValueInterpolator valueInterpolator) { + super(valueInterpolator); + } + } + public static class Other extends Log, V> { + + public Other(ValueInterpolator valueInterpolator) { + super(valueInterpolator); + } + } } - public static class Linear extends Interpolator, V> { + public static class Linear, V> extends Interpolator { public Linear(ValueInterpolator valueInterpolator) { super(valueInterpolator); } + + public static class Numeric extends Linear + implements IInterpolator.Continuous { + + public Numeric(ValueInterpolator valueInterpolator) { + super(valueInterpolator); + } + } + public static class Other extends Linear, V> { + + public Other(ValueInterpolator valueInterpolator) { + super(valueInterpolator); + } + } } - public static Linear linear(ValueInterpolator valueInterpolator) { - return new Linear<>(valueInterpolator); + public static Linear.Numeric linear(ValueInterpolator valueInterpolator) { + return new Linear.Numeric(valueInterpolator); } - public static Linear linear() { - return new Linear<>(INTERPOLATE_NUMERIC); + public static Linear.Numeric linear() { + return new Linear.Numeric(INTERPOLATE_NUMERIC); } - public static Log log(ValueInterpolator valueInterpolator) { - return new Log<>(valueInterpolator).base(10); + public static Log.Numeric log(ValueInterpolator valueInterpolator) { + return new Log.Numeric(valueInterpolator).base(10); } - public static Log log() { + public static Log.Numeric log() { return log(INTERPOLATE_NUMERIC).base(10); } - public static Power power(ValueInterpolator valueInterpolator) { - return new Power<>(valueInterpolator).exponent(1); - } - public static Power power() { - return new Power<>(INTERPOLATE_NUMERIC).exponent(1); + // TODO specialized double implementation without boxing (interpolator, values?) ? + // TODO set special args before returning reusable thing? forget self type + + public static Power.Numeric npower(ValueInterpolator valueInterpolator) { + return new Power.Numeric(valueInterpolator).exponent(1); } - public static Power sqrt() { - return power(INTERPOLATE_NUMERIC).exponent(0.5); + public static Power.Numeric power() { + return npower(INTERPOLATE_NUMERIC).exponent(1); } - public interface ValueInterpolator extends BiFunction> {} + public static Power.Numeric sqrt() { + return power().exponent(0.5); + } } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java new file mode 100644 index 0000000000..3bd9a5469d --- /dev/null +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java @@ -0,0 +1,278 @@ +package com.onthegomap.planetiler.util; + +import com.carrotsearch.hppc.DoubleArrayList; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.DoubleFunction; +import java.util.function.DoubleUnaryOperator; +import java.util.function.Function; +import org.apache.commons.lang3.ArrayUtils; + +public class Scales { + private static final DoubleInterpolator INTERPOLATE_NUMERIC = + (a, b) -> t -> a * (1 - t) + b * t; + + public interface Interpolator extends BiFunction> {} + + @FunctionalInterface + public interface DoubleInterpolator extends Interpolator { + + DoubleUnaryOperator apply(double x, double y); + + @Override + default DoubleFunction apply(Double x, Double y) { + return apply(x.doubleValue(), y.doubleValue())::applyAsDouble; + } + } + + private interface Scale, V> extends DoubleFunction, Function { + + @Override + default V apply(Double x) { + return apply(x.doubleValue()); + } + + T defaultValue(V defaultValue); + + @SuppressWarnings("unchecked") + default T self() { + return (T) this; + } + } + + private interface Continuous, V> extends Scale { + + T put(double x, V y); + + T clamp(boolean clamp); + } + + private interface Threshold, V> { + + T putBelow(double x, V y); + + T putAbove(V y); + + double invertMin(V x); + + double invertMax(V x); + + double[] invertExtent(V x); + } + + private interface ContinuousDoubleScale> + extends Continuous, DoubleUnaryOperator { + + default T put(double x, double y) { + return put(x, Double.valueOf(y)); + } + + @Override + default double applyAsDouble(double operand) { + return apply(operand); + } + + DoubleUnaryOperator invert(); + } + + + private static class BaseContinuous, V> implements Continuous { + static final DoubleUnaryOperator IDENTITY = x -> x; + + private final Interpolator valueInterpolator; + final DoubleUnaryOperator transform; + boolean clamp = false; + private V defaultValue; + final DoubleArrayList domain = new DoubleArrayList(); + final List range = new ArrayList<>(); + private DoubleFunction fn; + double minKey = Double.POSITIVE_INFINITY; + double maxKey = Double.NEGATIVE_INFINITY; + + protected BaseContinuous(Interpolator valueInterpolator, DoubleUnaryOperator transform) { + this.valueInterpolator = valueInterpolator; + this.transform = transform; + } + + @Override + public V apply(double operand) { + if (clamp) { + operand = Math.clamp(operand, minKey, maxKey); + } + if (Double.isNaN(operand)) { + return defaultValue; + } + if (fn == null) { + fn = rescale(); + } + return fn.apply(transform.applyAsDouble(operand)); + } + + private DoubleFunction rescale() { + if (domain.size() > 2) { + int j = Math.min(domain.size(), range.size()) - 1; + DoubleUnaryOperator[] d = new DoubleUnaryOperator[j]; + DoubleFunction[] r = new DoubleFunction[j]; + int i = -1; + + double[] domainItems = new double[domain.size()]; + for (int k = 0; k < domainItems.length; k++) { + domainItems[k] = transform.applyAsDouble(domain.get(k)); + } + List rangeItems = domainItems[j] < domainItems[0] ? range.reversed() : range; + + // Reverse descending domains. + if (domainItems[j] < domainItems[0]) { + ArrayUtils.reverse(domainItems); + } + + while (++i < j) { + d[i] = normalize(domainItems[i], domainItems[i + 1]); + r[i] = valueInterpolator.apply(rangeItems.get(i), rangeItems.get(i + 1)); + } + + return x -> { + int ii = bisect(domainItems, x, 1, j) - 1; + return r[ii].apply(d[ii].applyAsDouble(x)); + }; + } else { + double d0 = transform.applyAsDouble(domain.get(0)), d1 = transform.applyAsDouble(domain.get(1)); + V r0 = range.get(0), r1 = range.get(1); + boolean reverse = d1 < d0; + final double dlo = reverse ? d1 : d0; + final double dhi = reverse ? d0 : d1; + final V rlo = reverse ? r1 : r0; + final V rhi = reverse ? r0 : r1; + DoubleUnaryOperator normalize = normalize(dlo, dhi); + DoubleFunction interpolate = valueInterpolator.apply(rlo, rhi); + return x -> interpolate.apply(normalize.applyAsDouble(x)); + } + } + + private static int bisect(double[] a, double x, int lo, int hi) { + if (lo < hi) { + do { + int mid = (lo + hi) >>> 1; + if (a[mid] <= x) + lo = mid + 1; + else + hi = mid; + } while (lo < hi); + } + return lo; + } + + private static DoubleUnaryOperator normalize(double a, double b) { + double delta = b - a; + return delta == 0 ? x -> 0.5 : Double.isNaN(delta) ? x -> Double.NaN : x -> (x - a) / delta; + } + + @Override + public T put(double x, V y) { + fn = null; + minKey = Math.min(x, minKey); + maxKey = Math.max(x, maxKey); + domain.add(x); + range.add(y); + return self(); + } + + @Override + public T clamp(boolean clamp) { + this.clamp = clamp; + return self(); + } + + @Override + public T defaultValue(V defaultValue) { + this.defaultValue = defaultValue; + return self(); + } + } + + public static class DoubleContinuous extends BaseContinuous + implements ContinuousDoubleScale { + private final DoubleUnaryOperator reverseTransform; + + protected DoubleContinuous(Interpolator valueInterpolator, DoubleUnaryOperator transform, + DoubleUnaryOperator reverseTransform) { + super(valueInterpolator, transform); + this.reverseTransform = reverseTransform; + } + + @Override + public DoubleUnaryOperator invert() { + var result = linear(); + int j = Math.min(domain.size(), range.size()); + for (int i = 0; i < j; i++) { + result.put(range.get(i).doubleValue(), + transform.applyAsDouble(domain.get(i))); + } + DoubleUnaryOperator retVal = + reverseTransform == IDENTITY ? result : x -> reverseTransform.applyAsDouble(result.apply(x)); + return clamp ? retVal.andThen(x -> Math.clamp(x, minKey, maxKey)) : retVal; + } + } + + public static class OtherContinuous extends BaseContinuous, V> { + + protected OtherContinuous(Interpolator valueInterpolator, DoubleUnaryOperator transform) { + super(valueInterpolator, transform); + } + } + + public static DoubleContinuous linear() { + return new DoubleContinuous(INTERPOLATE_NUMERIC, BaseContinuous.IDENTITY, BaseContinuous.IDENTITY); + } + + public static OtherContinuous linear(Interpolator valueInterpolator) { + return new OtherContinuous<>(valueInterpolator, BaseContinuous.IDENTITY); + } + + + public static DoubleContinuous power(double exponent) { + double inverse = 1d / exponent; + return new DoubleContinuous(INTERPOLATE_NUMERIC, + x -> Math.pow(x, exponent), + y -> Math.pow(y, inverse) + ); + } + + public static OtherContinuous power(double exponent, Interpolator valueInterpolator) { + return new OtherContinuous<>(valueInterpolator, x -> Math.pow(x, exponent)); + } + + public static DoubleContinuous sqrt() { + return power(0.5); + } + + public static OtherContinuous sqrt(Interpolator valueInterpolator) { + return power(0.5, valueInterpolator); + } + + + private static DoubleUnaryOperator logBase(double base) { + double logBase = Math.log(base); + return x -> Math.log(x) / logBase; + } + + public static DoubleContinuous log(double base) { + return new DoubleContinuous(INTERPOLATE_NUMERIC, logFn(base), expFn(base)); + } + + private static DoubleUnaryOperator expFn(double base) { + return base == Math.E ? Math::exp : + x -> Math.pow(base, x); + } + + private static DoubleUnaryOperator logFn(double base) { + return base == 10d ? Math::log10 : + base == Math.E ? Math::log : + logBase(base); + } + + public static OtherContinuous log(double base, Interpolator valueInterpolator) { + return new OtherContinuous<>(valueInterpolator, logFn(base)); + } +} diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java index 9a2a927618..c53aeed301 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java @@ -10,9 +10,9 @@ class InterpolatorTest { @ParameterizedTest @ValueSource(booleans = {false, true}) void testLinear(boolean clamp) { - var interpolator = Interpolator.linear() - .put(0, 10d) - .put(1, 20d) + var interpolator = Scales.linear() + .put(0, 10) + .put(1, 20) .clamp(clamp); assertTransform(interpolator, 0, 10); assertTransform(interpolator, 1, 20); @@ -27,7 +27,7 @@ void testLinear(boolean clamp) { @ParameterizedTest @ValueSource(booleans = {false, true}) void testLog(boolean clamp) { - var interpolator = Interpolator.log() + var interpolator = Scales.log(10) .put(10, 1d) .put(1_000, 2d) .clamp(clamp); @@ -44,8 +44,7 @@ void testLog(boolean clamp) { @ParameterizedTest @ValueSource(booleans = {false, true}) void testLog2(boolean clamp) { - var interpolator = Interpolator.log() - .base(2) + var interpolator = Scales.log(2) .put(2, 1d) .put(8, 2d) .clamp(clamp); @@ -62,8 +61,7 @@ void testLog2(boolean clamp) { @ParameterizedTest @ValueSource(booleans = {false, true}) void testPower(boolean clamp) { - var interpolator = Interpolator.power() - .exponent(2) + var interpolator = Scales.power(2) .put(Math.sqrt(2), 1d) .put(Math.sqrt(4), 2d) .clamp(clamp); @@ -79,8 +77,7 @@ void testPower(boolean clamp) { @Test void testNegativePower() { - var interpolator = Interpolator.power() - .exponent(-1) + var interpolator = Scales.power(-1) .put(1, 0d) .put(2, 1d); assertTransform(interpolator, 1, 0); @@ -91,7 +88,7 @@ void testNegativePower() { @ParameterizedTest @ValueSource(booleans = {false, true}) void testSqrt(boolean clamp) { - var interpolator = Interpolator.sqrt() + var interpolator = Scales.sqrt() .put(4, 1d) .put(16, 2d) .clamp(clamp); @@ -108,7 +105,7 @@ void testSqrt(boolean clamp) { @ParameterizedTest @ValueSource(booleans = {false, true}) void testMultipartDescending(boolean clamp) { - var interpolator = Interpolator.linear() + var interpolator = Scales.linear() .put(4, 1d) .put(2, 2d) .put(1, 4d) @@ -128,7 +125,7 @@ void testMultipartDescending(boolean clamp) { @ParameterizedTest @ValueSource(booleans = {false, true}) void testMultipartAscending(boolean clamp) { - var interpolator = Interpolator.linear() + var interpolator = Scales.linear() .put(1, 4d) .put(2, 2d) .put(4, 1d) @@ -147,7 +144,7 @@ void testMultipartAscending(boolean clamp) { @Test void testUnknown() { - var interpolator = Interpolator.linear() + var interpolator = Scales.linear() .put(0, 10d) .put(1, 20d) .defaultValue(100d); @@ -158,7 +155,7 @@ private static void assertClose(double expected, double actual) { assertEquals(expected, actual, 1e-10); } - private static void assertTransform(Interpolator interp, double x, double y) { + private static void assertTransform(Scales.DoubleContinuous interp, double x, double y) { assertClose(y, interp.applyAsDouble(x)); assertClose(x, interp.invert().applyAsDouble(y)); } From 3ff08facbc99f79604574cca3b6337bc4359f9a4 Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Sun, 19 Nov 2023 06:21:53 -0500 Subject: [PATCH 10/13] simplify --- .../onthegomap/planetiler/util/Scales.java | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java index 3bd9a5469d..1e5949e96b 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java @@ -6,32 +6,15 @@ import java.util.function.BiFunction; import java.util.function.DoubleFunction; import java.util.function.DoubleUnaryOperator; -import java.util.function.Function; import org.apache.commons.lang3.ArrayUtils; public class Scales { - private static final DoubleInterpolator INTERPOLATE_NUMERIC = + private static final Interpolator INTERPOLATE_NUMERIC = (a, b) -> t -> a * (1 - t) + b * t; public interface Interpolator extends BiFunction> {} - @FunctionalInterface - public interface DoubleInterpolator extends Interpolator { - - DoubleUnaryOperator apply(double x, double y); - - @Override - default DoubleFunction apply(Double x, Double y) { - return apply(x.doubleValue(), y.doubleValue())::applyAsDouble; - } - } - - private interface Scale, V> extends DoubleFunction, Function { - - @Override - default V apply(Double x) { - return apply(x.doubleValue()); - } + private interface Scale, V> extends DoubleFunction { T defaultValue(V defaultValue); From 1b1c2395110915c3f61416c1c8dd5f8f91ed14a1 Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Sun, 19 Nov 2023 06:51:15 -0500 Subject: [PATCH 11/13] threshold --- .../onthegomap/planetiler/util/Scales.java | 111 ++++++++++++++---- .../planetiler/util/InterpolatorTest.java | 23 ++++ 2 files changed, 113 insertions(+), 21 deletions(-) diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java index 1e5949e96b..33ce132038 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java @@ -12,6 +12,7 @@ public class Scales { private static final Interpolator INTERPOLATE_NUMERIC = (a, b) -> t -> a * (1 - t) + b * t; + public interface Interpolator extends BiFunction> {} private interface Scale, V> extends DoubleFunction { @@ -31,17 +32,80 @@ private interface Continuous, V> extends Scale T clamp(boolean clamp); } - private interface Threshold, V> { - - T putBelow(double x, V y); + private interface Threshold, V> extends Scale { - T putAbove(V y); + T putAbove(double x, V y); double invertMin(V x); double invertMax(V x); - double[] invertExtent(V x); + Extent invertExtent(V x); + } + + public record Extent(double min, double max) {} + + public static class ThresholdScale implements Threshold, V> { + + private V defaultValue = null; + private final List domain = new ArrayList<>(); + private final List range = new ArrayList<>(); + private double[] domainArray = null; + + private ThresholdScale(V minValue) { + range.add(minValue); + } + + @Override + public ThresholdScale putAbove(double x, V y) { + domainArray = null; + domain.add(x); + range.add(y); + return self(); + } + + @Override + public double invertMin(V x) { + int idx = range.indexOf(x); + return idx < 0 ? Double.NaN : idx == 0 ? Double.NEGATIVE_INFINITY : domain.get(idx - 1); + } + + @Override + public double invertMax(V x) { + int idx = range.indexOf(x); + return idx < 0 ? Double.NaN : idx == range.size() - 1 ? Double.POSITIVE_INFINITY : domain.get(idx); + } + + @Override + public Extent invertExtent(V x) { + int idx = range.indexOf(x); + return new Extent( + idx < 0 ? Double.NaN : idx == 0 ? Double.NEGATIVE_INFINITY : domain.get(idx - 1), + idx < 0 ? Double.NaN : idx == range.size() - 1 ? Double.POSITIVE_INFINITY : domain.get(idx) + ); + } + + @Override + public ThresholdScale defaultValue(V defaultValue) { + this.defaultValue = defaultValue; + return self(); + } + + @Override + public V apply(double x) { + if (domainArray == null) { + domainArray = new double[domain.size()]; + int i = 0; + for (Double d : domain) { + domainArray[i++] = d; + } + } + if (Double.isNaN(x)) { + return defaultValue; + } + int idx = bisect(domainArray, x, 0, Math.min(domainArray.length, range.size() - 1)); + return range.get(idx); + } } private interface ContinuousDoubleScale> @@ -73,7 +137,7 @@ private static class BaseContinuous, V> implement double minKey = Double.POSITIVE_INFINITY; double maxKey = Double.NEGATIVE_INFINITY; - protected BaseContinuous(Interpolator valueInterpolator, DoubleUnaryOperator transform) { + private BaseContinuous(Interpolator valueInterpolator, DoubleUnaryOperator transform) { this.valueInterpolator = valueInterpolator; this.transform = transform; } @@ -133,19 +197,6 @@ private DoubleFunction rescale() { } } - private static int bisect(double[] a, double x, int lo, int hi) { - if (lo < hi) { - do { - int mid = (lo + hi) >>> 1; - if (a[mid] <= x) - lo = mid + 1; - else - hi = mid; - } while (lo < hi); - } - return lo; - } - private static DoubleUnaryOperator normalize(double a, double b) { double delta = b - a; return delta == 0 ? x -> 0.5 : Double.isNaN(delta) ? x -> Double.NaN : x -> (x - a) / delta; @@ -174,11 +225,25 @@ public T defaultValue(V defaultValue) { } } + + private static int bisect(double[] a, double x, int lo, int hi) { + if (lo < hi) { + do { + int mid = (lo + hi) >>> 1; + if (a[mid] <= x) + lo = mid + 1; + else + hi = mid; + } while (lo < hi); + } + return lo; + } + public static class DoubleContinuous extends BaseContinuous implements ContinuousDoubleScale { private final DoubleUnaryOperator reverseTransform; - protected DoubleContinuous(Interpolator valueInterpolator, DoubleUnaryOperator transform, + private DoubleContinuous(Interpolator valueInterpolator, DoubleUnaryOperator transform, DoubleUnaryOperator reverseTransform) { super(valueInterpolator, transform); this.reverseTransform = reverseTransform; @@ -200,7 +265,7 @@ public DoubleUnaryOperator invert() { public static class OtherContinuous extends BaseContinuous, V> { - protected OtherContinuous(Interpolator valueInterpolator, DoubleUnaryOperator transform) { + private OtherContinuous(Interpolator valueInterpolator, DoubleUnaryOperator transform) { super(valueInterpolator, transform); } } @@ -258,4 +323,8 @@ private static DoubleUnaryOperator logFn(double base) { public static OtherContinuous log(double base, Interpolator valueInterpolator) { return new OtherContinuous<>(valueInterpolator, logFn(base)); } + + public static ThresholdScale threshold(V minValue) { + return new ThresholdScale<>(minValue); + } } diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java index c53aeed301..746b7e4e5e 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java @@ -151,6 +151,29 @@ void testUnknown() { assertEquals(100, interpolator.applyAsDouble(Double.NaN)); } + @Test + void testThreshold() { + var scale = Scales.threshold("a") + .putAbove(1.5, "b") + .putAbove(3, "c"); + assertEquals("a", scale.apply(1.4)); + assertEquals("b", scale.apply(1.5)); + assertEquals("b", scale.apply(2.99)); + assertEquals("c", scale.apply(3)); + assertEquals("c", scale.apply(3.1)); + + testInvert(scale, "a", Double.NEGATIVE_INFINITY, 1.5); + testInvert(scale, "b", 1.5, 3); + testInvert(scale, "c", 3, Double.POSITIVE_INFINITY); + testInvert(scale, "d", Double.NaN, Double.NaN); + } + + private static void testInvert(Scales.ThresholdScale scale, V val, double min, double max) { + assertEquals(min, scale.invertMin(val)); + assertEquals(max, scale.invertMax(val)); + assertEquals(max, scale.invertMax(val)); + } + private static void assertClose(double expected, double actual) { assertEquals(expected, actual, 1e-10); } From d5426bedad5cec5e356d3bdd04e543b622fa8dca Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Sun, 19 Nov 2023 07:06:24 -0500 Subject: [PATCH 12/13] quantize --- .../onthegomap/planetiler/util/Scales.java | 19 +++++++++++++++++++ .../planetiler/util/InterpolatorTest.java | 17 +++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java index 33ce132038..19af68d8b7 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java @@ -12,6 +12,25 @@ public class Scales { private static final Interpolator INTERPOLATE_NUMERIC = (a, b) -> t -> a * (1 - t) + b * t; + public static ThresholdScale quantize(int min, int max, V minValue, V... values) { + ThresholdScale result = threshold(minValue); + int n = values.length; + for (int i = 0; i < n; i++) { + result.putAbove(((i + 1d) * max - (i - n) * min) / (n + 1), values[i]); + } + return result; + } + + public static ThresholdScale quantize(int min, int max, List values) { + ThresholdScale result = threshold(values.getFirst()); + int n = values.size() - 1; + int i = -1; + while (++i < n) { + result.putAbove(((i + 1d) * max - (i - n) * min) / (n + 1), values.get(i + 1)); + } + return result; + } + public interface Interpolator extends BiFunction> {} diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java index 746b7e4e5e..b84eddf1e9 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -168,6 +169,22 @@ void testThreshold() { testInvert(scale, "d", Double.NaN, Double.NaN); } + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testQuantize(boolean list) { + var scale = list ? + Scales.quantize(0, 3, List.of("a", "b", "c")) : + Scales.quantize(0, 3, "a", "b", "c"); + assertEquals("a", scale.apply(0)); + assertEquals("a", scale.apply(0.99)); + assertEquals("b", scale.apply(1)); + assertEquals("b", scale.apply(1.01)); + assertEquals("b", scale.apply(1.99)); + assertEquals("c", scale.apply(2)); + assertEquals("c", scale.apply(2.01)); + assertEquals("c", scale.apply(99)); + } + private static void testInvert(Scales.ThresholdScale scale, V val, double min, double max) { assertEquals(min, scale.invertMin(val)); assertEquals(max, scale.invertMax(val)); From eca9ddf074be4923799fa282437e6723e6c29b80 Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Sun, 19 Nov 2023 15:54:21 -0500 Subject: [PATCH 13/13] cubic bezier --- .../onthegomap/planetiler/util/Scales.java | 35 +++++-- .../planetiler/util/UnitBezier.java | 94 +++++++++++++++++++ .../planetiler/util/InterpolatorTest.java | 34 +++++++ 3 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 planetiler-core/src/main/java/com/onthegomap/planetiler/util/UnitBezier.java diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java index 19af68d8b7..5eab80e51c 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Scales.java @@ -210,9 +210,11 @@ private DoubleFunction rescale() { final double dhi = reverse ? d0 : d1; final V rlo = reverse ? r1 : r0; final V rhi = reverse ? r0 : r1; - DoubleUnaryOperator normalize = normalize(dlo, dhi); DoubleFunction interpolate = valueInterpolator.apply(rlo, rhi); - return x -> interpolate.apply(normalize.applyAsDouble(x)); + return x -> { + double value = (x - dlo) / (dhi - dlo); + return interpolate.apply(value); + }; } } @@ -293,6 +295,10 @@ public static DoubleContinuous linear() { return new DoubleContinuous(INTERPOLATE_NUMERIC, BaseContinuous.IDENTITY, BaseContinuous.IDENTITY); } + public static DoubleContinuous linearDouble(Interpolator interp) { + return new DoubleContinuous(interp, BaseContinuous.IDENTITY, BaseContinuous.IDENTITY); + } + public static OtherContinuous linear(Interpolator valueInterpolator) { return new OtherContinuous<>(valueInterpolator, BaseContinuous.IDENTITY); } @@ -324,10 +330,6 @@ private static DoubleUnaryOperator logBase(double base) { return x -> Math.log(x) / logBase; } - public static DoubleContinuous log(double base) { - return new DoubleContinuous(INTERPOLATE_NUMERIC, logFn(base), expFn(base)); - } - private static DoubleUnaryOperator expFn(double base) { return base == Math.E ? Math::exp : x -> Math.pow(base, x); @@ -339,10 +341,31 @@ private static DoubleUnaryOperator logFn(double base) { logBase(base); } + public static DoubleContinuous log(double base) { + return new DoubleContinuous(INTERPOLATE_NUMERIC, logFn(base), expFn(base)); + } + public static OtherContinuous log(double base, Interpolator valueInterpolator) { return new OtherContinuous<>(valueInterpolator, logFn(base)); } + public static DoubleContinuous exponential(double base) { + return new DoubleContinuous(INTERPOLATE_NUMERIC, expFn(base), logFn(base)); + } + + public static OtherContinuous exponential(double base, Interpolator valueInterpolator) { + return new OtherContinuous<>(valueInterpolator, expFn(base)); + } + + public static OtherContinuous bezier(double x1, double y1, double x2, double y2) { + var bezier = new UnitBezier(x1, y1, x2, y2); + return new OtherContinuous<>((a, b) -> { + double diff = b - a; + return t -> bezier.solve(t) * diff + a; + }, BaseContinuous.IDENTITY); + } + + public static ThresholdScale threshold(V minValue) { return new ThresholdScale<>(minValue); } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/UnitBezier.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/UnitBezier.java new file mode 100644 index 0000000000..1425573c1d --- /dev/null +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/UnitBezier.java @@ -0,0 +1,94 @@ +package com.onthegomap.planetiler.util; + +public class UnitBezier { + private final double ax; + private final double bx; + private final double cx; + + private final double ay; + private final double by; + private final double cy; + + public UnitBezier(double p1x, double p1y, double p2x, double p2y) { + // Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1). + cx = 3.0 * p1x; + bx = 3.0 * (p2x - p1x) - cx; + ax = 1.0 - cx - bx; + + cy = 3.0 * p1y; + by = 3.0 * (p2y - p1y) - cy; + ay = 1.0 - cy - by; + } + + double sampleCurveX(double t) { + // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule. + return ((ax * t + bx) * t + cx) * t; + } + + double sampleCurveY(double t) { + return ((ay * t + by) * t + cy) * t; + } + + double sampleCurveDerivativeX(double t) { + return (3.0 * ax * t + 2.0 * bx) * t + cx; + } + + // Given an x value, find a parametric value it came from. + double solveCurveX(double x, double epsilon) { + double t0; + double t1; + double t2; + double x2; + double d2; + int i; + + if (x < 0) + return 0; + if (x > 1) + return 1; + + // First try a few iterations of Newton's method -- normally very fast. + for (t2 = x, i = 0; i < 8; i++) { + x2 = sampleCurveX(t2) - x; + if (Math.abs(x2) < epsilon) + return t2; + d2 = sampleCurveDerivativeX(t2); + if (Math.abs(d2) < 1e-6) + break; + t2 = t2 - x2 / d2; + } + + // Fall back to the bisection method for reliability. + t0 = 0.0; + t1 = 1.0; + t2 = x; + + if (t2 < t0) + return t0; + if (t2 > t1) + return t1; + + while (t0 < t1) { + x2 = sampleCurveX(t2); + if (Math.abs(x2 - x) < epsilon) + return t2; + if (x > x2) + t0 = t2; + else + t1 = t2; + t2 = (t1 - t0) * .5 + t0; + } + + // Failure. + return t2; + } + + double solve(double x, double epsilon) { + return sampleCurveY(solveCurveX(x, epsilon)); + } + + + double solve(double x) { + return solve(x, 1e-6); + } +} diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java index b84eddf1e9..91c106dbe3 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/InterpolatorTest.java @@ -185,6 +185,40 @@ void testQuantize(boolean list) { assertEquals("c", scale.apply(99)); } + @Test + void testExponential() { + var scale = Scales.exponential(2) + .put(1, 2) + .put(3, 6) + .clamp(true); + + assertClose(2, scale.apply(0)); + assertClose(2, scale.apply(1)); + assertClose(3.3333333333, scale.apply(2)); + assertClose(6, scale.apply(3)); + assertClose(6, scale.apply(5)); + } + + @Test + void testBezier() { + var scale = Scales.bezier(0.42, 0, 0.58, 1) + .put(0, 0d) + .put(100, 100d) + .clamp(true); + + assertEquals(0, scale.apply(0), 1e-4); + assertEquals(1.97224, scale.apply(10), 1e-4); + assertEquals(8.16597, scale.apply(20), 1e-4); + assertEquals(18.7395, scale.apply(30), 1e-4); + assertEquals(33.1883, scale.apply(40), 1e-4); + assertEquals(50, scale.apply(50), 1e-4); + assertEquals(66.8116, scale.apply(60), 1e-4); + assertEquals(81.2604, scale.apply(70), 1e-4); + assertEquals(91.834, scale.apply(80), 1e-4); + assertEquals(98.0277, scale.apply(90), 1e-4); + assertEquals(100, scale.apply(100), 1e-4); + } + private static void testInvert(Scales.ThresholdScale scale, V val, double min, double max) { assertEquals(min, scale.invertMin(val)); assertEquals(max, scale.invertMax(val));