planetiler/planetiler-core/src/test/java/com/onthegomap/planetiler/render/FeatureRendererTest.java

1429 wiersze
42 KiB
Java

package com.onthegomap.planetiler.render;
import static com.onthegomap.planetiler.TestUtils.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import com.onthegomap.planetiler.FeatureCollector;
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.TileCoord;
import com.onthegomap.planetiler.reader.SimpleFeature;
import com.onthegomap.planetiler.stats.Stats;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.DoubleStream;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateXY;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.geom.util.AffineTransformation;
import org.locationtech.jts.precision.GeometryPrecisionReducer;
class FeatureRendererTest {
private PlanetilerConfig config = PlanetilerConfig.defaults();
private final Stats stats = Stats.inMemory();
private FeatureCollector collector(Geometry worldGeom) {
var latLonGeom = GeoUtils.worldToLatLonCoords(worldGeom);
return new FeatureCollector.Factory(config, stats)
.get(SimpleFeature.create(latLonGeom, new HashMap<>(0), null, null,
1));
}
private Map<TileCoord, Collection<Geometry>> renderGeometry(FeatureCollector.Feature feature) {
Map<TileCoord, Collection<Geometry>> result = new TreeMap<>();
new FeatureRenderer(config, rendered -> result.computeIfAbsent(rendered.tile(), tile -> new HashSet<>())
.add(decodeSilently(rendered.vectorTileFeature().geometry())), Stats.inMemory()).accept(feature);
result.values().forEach(gs -> gs.forEach(TestUtils::validateGeometry));
return result;
}
private Map<TileCoord, Collection<RenderedFeature>> renderFeatures(FeatureCollector.Feature feature) {
Map<TileCoord, Collection<RenderedFeature>> result = new TreeMap<>();
new FeatureRenderer(config, rendered -> result.computeIfAbsent(rendered.tile(), tile -> new HashSet<>())
.add(rendered), Stats.inMemory()).accept(feature);
result.values()
.forEach(gs -> gs.forEach(f -> TestUtils.validateGeometry(decodeSilently(f.vectorTileFeature().geometry()))));
return result;
}
private static final int Z14_TILES = 1 << 14;
private static final double Z14_WIDTH = 1d / Z14_TILES;
private static final double Z14_PX = Z14_WIDTH / 256;
private static final int Z13_TILES = 1 << 13;
private static final double Z13_WIDTH = 1d / Z13_TILES;
private static final double Z13_PX = Z13_WIDTH / 256;
@Test
void testEmptyGeometry() {
var feature = collector(GeoUtils.JTS_FACTORY.createPoint()).point("layer");
assertSameNormalizedFeatures(Map.of(), renderGeometry(feature));
}
/*
* POINT TESTS
*/
@Test
void testSinglePoint() {
var feature = pointFeature(newPoint(0.5 + Z14_WIDTH / 2, 0.5 + Z14_WIDTH / 2))
.setZoomRange(14, 14);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newPoint(128, 128)
)
), renderGeometry(feature));
}
@Test
void testRepeatSinglePointNeighboringTiles() {
var feature = pointFeature(newPoint(0.5 + 1d / 512, 0.5 + 1d / 512))
.setZoomRange(0, 1)
.setBufferPixels(2);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(0, 0, 0), List.of(newPoint(128.5, 128.5)),
TileCoord.ofXYZ(0, 0, 1), List.of(newPoint(257, 257)),
TileCoord.ofXYZ(1, 0, 1), List.of(newPoint(1, 257)),
TileCoord.ofXYZ(0, 1, 1), List.of(newPoint(257, 1)),
TileCoord.ofXYZ(1, 1, 1), List.of(newPoint(1, 1))
), renderGeometry(feature));
}
@Test
void testRepeatSinglePointNeighboringTilesBuffer0() {
var feature = pointFeature(newPoint(0.5, 0.5))
.setZoomRange(1, 1)
.setBufferPixels(0);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(0, 0, 1), List.of(newPoint(256, 256)),
TileCoord.ofXYZ(1, 0, 1), List.of(newPoint(0, 256)),
TileCoord.ofXYZ(0, 1, 1), List.of(newPoint(256, 0)),
TileCoord.ofXYZ(1, 1, 1), List.of(newPoint(0, 0))
), renderGeometry(feature));
}
@Test
void testEmitPointsRespectExtents() {
config = PlanetilerConfig.from(Arguments.of(
"bounds", "0,-80,180,0"
));
var feature = pointFeature(newPoint(0.5 + 1d / 512, 0.5 + 1d / 512))
.setZoomRange(0, 1)
.setBufferPixels(2);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(0, 0, 0), List.of(newPoint(128.5, 128.5)),
TileCoord.ofXYZ(1, 1, 1), List.of(newPoint(1, 1))
), renderGeometry(feature));
}
@Test
void testEmitPointsRespectShape() {
config = PlanetilerConfig.from(Arguments.of(
"polygon", TestUtils.pathToResource("bottomrightearth.poly")
));
var feature = pointFeature(newPoint(0.5 + 1d / 512, 0.5 + 1d / 512))
.setZoomRange(0, 2)
.setBufferPixels(2);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(0, 0, 0), List.of(newPoint(128.5, 128.5)),
TileCoord.ofXYZ(1, 1, 1), List.of(newPoint(1, 1))
), renderGeometry(feature));
}
@TestFactory
List<DynamicTest> testProcessPointsNearInternationalDateLineAndPoles() {
double d = 1d / 512;
record X(double x, double wrapped, double z1x0, double z1x1) {}
record Y(double y, int z1ty, double tyoff) {}
var xs = List.of(
new X(-d, 1 - d, -1, 255),
new X(d, 1 + d, 1, 257),
new X(1 - d, -d, -1, 255),
new X(1 + d, d, 1, 257)
);
var ys = List.of(
new Y(0.25, 0, 128),
new Y(-d, 0, -1),
new Y(d, 0, 1),
new Y(1 - d, 1, 255),
new Y(1 + d, 1, 257)
);
List<DynamicTest> tests = new ArrayList<>();
for (X x : xs) {
for (Y y : ys) {
tests.add(dynamicTest((x.x * 256) + ", " + (y.y * 256), () -> {
var feature = pointFeature(newPoint(x.x, y.y))
.setZoomRange(0, 1)
.setBufferPixels(2);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(0, 0, 0), List.of(newMultiPoint(
newPoint(x.x * 256, y.y * 256),
newPoint(x.wrapped * 256, y.y * 256)
)),
TileCoord.ofXYZ(0, y.z1ty, 1), List.of(newPoint(x.z1x0, y.tyoff)),
TileCoord.ofXYZ(1, y.z1ty, 1), List.of(newPoint(x.z1x1, y.tyoff))
), renderGeometry(feature));
}));
}
}
return tests;
}
@Test
void testZ0FullTileBuffer() {
var feature = pointFeature(newPoint(0.25, 0.25))
.setZoomRange(0, 1)
.setBufferPixels(256);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(0, 0, 0), List.of(newMultiPoint(
newPoint(-192, 64),
newPoint(64, 64),
newPoint(320, 64)
)),
TileCoord.ofXYZ(0, 0, 1), List.of(newPoint(128, 128)),
TileCoord.ofXYZ(1, 0, 1), List.of(newMultiPoint(
newPoint(-128, 128),
newPoint(256 + 128, 128)
)),
TileCoord.ofXYZ(0, 1, 1), List.of(newPoint(128, -128)),
TileCoord.ofXYZ(1, 1, 1), List.of(newMultiPoint(
newPoint(-128, -128),
newPoint(256 + 128, -128)
))
), renderGeometry(feature));
}
@Test
void testMultipointNoLabelGrid() {
var feature = pointFeature(newMultiPoint(
newPoint(0.25, 0.25),
newPoint(0.25 + 1d / 256, 0.25 + 1d / 256)
))
.setZoomRange(0, 1)
.setBufferPixels(4);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(0, 0, 0), List.of(newMultiPoint(
newPoint(64, 64),
newPoint(65, 65)
)),
TileCoord.ofXYZ(0, 0, 1), List.of(newMultiPoint(
newPoint(128, 128),
newPoint(130, 130)
))
), renderGeometry(feature));
}
@Test
void testMultipointWithLabelGridSplits() {
var feature = pointFeature(newMultiPoint(
newPoint(0.25, 0.25),
newPoint(0.25 + 1d / 256, 0.25 + 1d / 256)
))
.setPointLabelGridPixelSize(10, 10)
.setZoomRange(0, 1)
.setBufferPixels(10);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(0, 0, 0), List.of(
newPoint(64, 64),
newPoint(65, 65)
),
TileCoord.ofXYZ(0, 0, 1), List.of(
newPoint(128, 128),
newPoint(130, 130)
)
), renderGeometry(feature));
}
@Test
void testLabelGridRequiresBufferPixelsGreaterThanGridSize() {
assertThrows(AssertionError.class, () -> renderFeatures(pointFeature(newPoint(0.75, 0.75))
.setPointLabelGridSizeAndLimit(10, 10, 2)
.setBufferPixels(9)));
renderFeatures(pointFeature(newPoint(0.75, 0.75))
.setPointLabelGridSizeAndLimit(10, 10, 2)
.setBufferPixels(10));
}
@Test
void testLabelGrid() {
var feature = pointFeature(newPoint(0.75, 0.75))
.setPointLabelGridSizeAndLimit(10, 256, 2)
.setZoomRange(0, 1)
.setBufferPixels(256);
var rendered = renderFeatures(feature);
var z0Feature = rendered.get(TileCoord.ofXYZ(0, 0, 0)).iterator().next();
var z1Feature = rendered.get(TileCoord.ofXYZ(1, 1, 1)).iterator().next();
assertEquals(Optional.of(new RenderedFeature.Group(0, 2)), z0Feature.group());
assertEquals(Optional.of(new RenderedFeature.Group((1L << 32) + 1, 2)), z1Feature.group());
}
@Test
void testWrapLabelGrid() {
var feature = pointFeature(newPoint(1.1, -0.1))
.setPointLabelGridSizeAndLimit(10, 256, 2)
.setZoomRange(0, 1)
.setBufferPixels(256);
var rendered = renderFeatures(feature);
var z0Feature = rendered.get(TileCoord.ofXYZ(0, 0, 0)).iterator().next();
var z1Feature = rendered.get(TileCoord.ofXYZ(0, 0, 1)).iterator().next();
assertEquals(Optional.of(new RenderedFeature.Group((1L << 32) - 1, 2)), z0Feature.group());
assertEquals(Optional.of(new RenderedFeature.Group((1L << 32) - 1, 2)), z1Feature.group());
}
private FeatureCollector.Feature pointFeature(Geometry geom) {
return collector(geom).point("layer");
}
/*
* LINE TESTS
*/
private FeatureCollector.Feature lineFeature(Geometry geom) {
return collector(geom).line("layer");
}
@Test
void testSplitLineFeatureSingleTile() {
double z14hypot = Math.sqrt(Z14_WIDTH * Z14_WIDTH);
var feature = lineFeature(newLineString(
0.5 + z14hypot / 4, 0.5 + z14hypot / 4,
0.5 + z14hypot * 3 / 4, 0.5 + z14hypot * 3 / 4
))
.setZoomRange(14, 14)
.setBufferPixels(8);
assertExactSameFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newLineString(64, 64, 192, 192)
)
), renderGeometry(feature));
}
@Test
void testSimplifyLine() {
double z14hypot = Math.sqrt(Z14_WIDTH * Z14_WIDTH);
var feature = lineFeature(newLineString(
0.5 + z14hypot / 4, 0.5 + z14hypot / 4,
0.5 + z14hypot / 2, 0.5 + z14hypot / 2,
0.5 + z14hypot * 3 / 4, 0.5 + z14hypot * 3 / 4
))
.setZoomRange(14, 14)
.setBufferPixels(8);
assertExactSameFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newLineString(64, 64, 192, 192)
)
), renderGeometry(feature));
}
@Test
void testSplitLineFeatureTouchingNeighboringTile() {
double z14hypot = Math.sqrt(Z14_WIDTH * Z14_WIDTH);
var feature = lineFeature(newLineString(
0.5 + z14hypot / 4, 0.5 + z14hypot / 4,
0.5 + Z14_WIDTH * (256 - 8) / 256d, 0.5 + Z14_WIDTH * (256 - 8) / 256d
))
.setZoomRange(14, 14)
.setBufferPixels(8);
assertExactSameFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newLineString(64, 64, 256 - 8, 256 - 8)
)
// only a single point in neighboring tile, exclude
), renderGeometry(feature));
}
@Test
void testSplitLineFeatureEnteringNeighboringTileBoudary() {
double z14hypot = Math.sqrt(Z14_WIDTH * Z14_WIDTH);
var feature = lineFeature(newLineString(
0.5 + z14hypot / 4, 0.5 + z14hypot / 4,
0.5 + Z14_WIDTH * (256 - 7) / 256d, 0.5 + Z14_WIDTH * (256 - 7) / 256d
))
.setZoomRange(14, 14)
.setBufferPixels(8);
assertExactSameFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newLineString(64, 64, 256 - 7, 256 - 7)
),
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2, 14), List.of(
newLineString(-8, 248, -7, 249)
),
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2 + 1, 14), List.of(
newLineString(248, -8, 249, -7)
),
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2 + 1, 14), List.of(
newLineString(-8, -8, -7, -7)
)
), renderGeometry(feature));
}
@Test
void test3PointLine() {
var feature = lineFeature(newLineString(
0.5 + Z14_WIDTH / 2, 0.5 + Z14_WIDTH / 2,
0.5 + 3 * Z14_WIDTH / 2, 0.5 + Z14_WIDTH / 2,
0.5 + 3 * Z14_WIDTH / 2, 0.5 + 3 * Z14_WIDTH / 2
))
.setZoomRange(14, 14)
.setBufferPixels(8);
assertExactSameFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newLineString(128, 128, 256 + 8, 128)
),
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2, 14), List.of(
newLineString(-8, 128, 128, 128, 128, 256 + 8)
),
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2 + 1, 14), List.of(
newLineString(128, -8, 128, 128)
)
), renderGeometry(feature));
}
@Test
void testLimitSingleLineStringLength() {
var eps = Z13_WIDTH / 4096;
var pixel = Z13_WIDTH / 256;
var featureBelow = lineFeature(newMultiLineString(
// below limit - ignore
newLineString(0.5, 0.5 + pixel, 0.5 + pixel - eps, 0.5 + pixel)
))
.setMinPixelSize(1)
.setZoomRange(13, 13)
.setBufferPixels(0);
var featureAbove = lineFeature(newMultiLineString(
// above limit - allow
newLineString(0.5, 0.5 + pixel, 0.5 + pixel + eps, 0.5 + pixel)
))
.setMinPixelSize(1)
.setZoomRange(13, 13)
.setBufferPixels(0);
assertExactSameFeatures(Map.of(), renderGeometry(featureBelow));
assertExactSameFeatures(Map.of(
TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of(
newLineString(0, 1, 1 + 256d / 4096, 1)
)
), renderGeometry(featureAbove));
}
@Test
void testLimitMultiLineStringLength() {
var eps = Z13_WIDTH / 4096;
var pixel = Z13_WIDTH / 256;
var feature = lineFeature(newMultiLineString(
// below limit - ignore
newLineString(0.5, 0.5 + pixel, 0.5 + pixel - eps, 0.5 + pixel),
// above limit - allow
newLineString(0.5, 0.5 + pixel, 0.5 + pixel + eps, 0.5 + pixel)
))
.setMinPixelSize(1)
.setZoomRange(13, 13)
.setBufferPixels(0);
assertExactSameFeatures(Map.of(
TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of(
newLineString(0, 1, 1 + 256d / 4096, 1)
)
), renderGeometry(feature));
}
@Test
void testDuplicatePointsRemovedAfterRounding() {
var eps = Z14_WIDTH / 4096;
var pixel = Z14_WIDTH / 256;
var feature = lineFeature(newLineString(
0.5 + pixel * 10, 0.5 + pixel * 10,
0.5 + pixel * 20, 0.5 + pixel * 10,
0.5 + pixel * 20, 0.5 + pixel * 10 + eps / 3
))
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(0)
.setPixelToleranceAtAllZooms(0);
assertExactSameFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newLineString(
10, 10,
20, 10
)
)
), renderGeometry(feature));
}
@Test
void testLineStringCollapsesToPointWithRounding() {
var eps = Z14_WIDTH / 4096;
var pixel = Z14_WIDTH / 256;
var feature = lineFeature(newLineString(
0.5 + pixel * 10, 0.5 + pixel * 10,
0.5 + pixel * 10 + eps / 3, 0.5 + pixel * 10
))
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(0)
.setPixelToleranceAtAllZooms(0);
assertExactSameFeatures(Map.of(), renderGeometry(feature));
}
@Test
void testSelfIntersectingLineStringOK() {
var feature = lineFeature(newLineString(z14WorldCoords(
10, 10,
20, 20,
10, 20,
20, 10,
10, 10
)))
.setMinPixelSize(1)
.setZoomRange(14, 14);
assertExactSameFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(newLineString(
10, 10,
20, 20,
10, 20,
20, 10,
10, 10
))
), renderGeometry(feature));
}
@Test
void testLineWrap() {
var feature = lineFeature(newLineString(
-1d / 256, -1d / 256,
257d / 256, 257d / 256
))
.setMinPixelSize(1)
.setBufferPixels(4)
.setZoomRange(0, 1);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(0, 0, 0), List.of(newMultiLineString(
newLineString(
-1, -1,
257, 257
),
newLineString(
-4, 252,
1, 257
),
newLineString(
255, -1,
260, 4
)
)),
TileCoord.ofXYZ(0, 0, 1), List.of(newLineString(
-2, -2,
260, 260
)),
TileCoord.ofXYZ(1, 0, 1), List.of(newMultiLineString(
newLineString(
-4, 252,
4, 260
),
newLineString(
254, -2,
260, 4
)
)),
TileCoord.ofXYZ(0, 1, 1), List.of(newMultiLineString(
newLineString(
252, -4,
260, 4
),
newLineString(
-4, 252,
2, 258
)
)),
TileCoord.ofXYZ(1, 1, 1), List.of(newLineString(
-4, -4,
258, 258
))
), renderGeometry(feature));
}
/*
* POLYGON TESTS
*/
private FeatureCollector.Feature polygonFeature(Geometry geom) {
return collector(geom).polygon("layer");
}
@Test
void testSimpleTriangleCCW() {
var feature = polygonFeature(
newPolygon(
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 10,
0.5 + Z14_PX * 20, 0.5 + Z14_PX * 10,
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 20,
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 10
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(0);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newPolygon(
10, 10,
20, 10,
10, 20,
10, 10
)
)
), renderGeometry(feature));
}
@Test
void testSimpleTriangleCW() {
var feature = polygonFeature(
newPolygon(
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 10,
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 20,
0.5 + Z14_PX * 20, 0.5 + Z14_PX * 10,
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 10
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(0);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newPolygon(
10, 10,
10, 20,
20, 10,
10, 10
)
)
), renderGeometry(feature));
}
@Test
void testTriangleTouchingNeighboringTileDoesNotEmit() {
var feature = polygonFeature(
newPolygon(
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 10,
0.5 + Z14_PX * 256, 0.5 + Z14_PX * 10,
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 20,
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 10
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(0);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newPolygon(
10, 10,
256, 10,
10, 20,
10, 10
)
)
), renderGeometry(feature));
}
@Test
void testTriangleTouchingNeighboringTileBelowDoesNotEmit() {
var feature = polygonFeature(
newPolygon(
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 10,
0.5 + Z14_PX * 20, 0.5 + Z14_PX * 10,
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 256,
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 10
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(0);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newPolygon(
10, 10,
20, 10,
10, 256,
10, 10
)
)
), renderGeometry(feature));
}
@ParameterizedTest
@CsvSource({
"0,256, 0,256", // all
"0,10, 0,10", // top-left
"5,10, 0,10", // top partial
"250,256, 0,10", // top all
"250,256, 0,10", // top-right
"250,256, 0,256", // right all
"250,256, 10,250", // right partial
"250,256, 250,256", // right bottom
"0,256, 250,256", // bottom all
"240,250, 250,256", // bottom partial
"0,10, 250,256", // bottom left
"0,10, 0,256", // left all
"0,10, 240,250", // left partial
})
void testRectangleTouchingNeighboringTilesDoesNotEmit(int x1, int x2, int y1, int y2) {
var feature = polygonFeature(
rectangle(
0.5 + Z14_PX * x1,
0.5 + Z14_PX * y1,
0.5 + Z14_PX * x2,
0.5 + Z14_PX * y2
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(0);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
rectangle(x1, y1, x2, y2)
)
), renderGeometry(feature));
}
@Test
void testOverlapTileHorizontal() {
var feature = polygonFeature(
rectangle(
0.5 + Z14_PX * 10,
0.5 + Z14_PX * 10,
0.5 + Z14_PX * 258,
0.5 + Z14_PX * 20
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(1);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
rectangle(10, 10, 257, 20)
),
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2, 14), List.of(
rectangle(-1, 10, 2, 20)
)
), renderGeometry(feature));
}
@Test
void testOverlapTileVertical() {
var feature = polygonFeature(
rectangle(
0.5 + Z14_PX * 10,
0.5 + Z14_PX * 10,
0.5 + Z14_PX * 20,
0.5 + Z14_PX * 258
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(1);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
rectangle(10, 10, 20, 257)
),
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2 + 1, 14), List.of(
rectangle(10, -1, 20, 2)
)
), renderGeometry(feature));
}
@Test
void testOverlapTileCorner() {
var feature = polygonFeature(
rectangle(
0.5 - Z14_PX * 10,
0.5 - Z14_PX * 10,
0.5 + Z14_PX * 10,
0.5 + Z14_PX * 10
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(1);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2 - 1, Z14_TILES / 2 - 1, 14), List.of(
rectangle(246, 257)
),
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2 - 1, 14), List.of(
rectangle(-1, 246, 10, 257)
),
TileCoord.ofXYZ(Z14_TILES / 2 - 1, Z14_TILES / 2, 14), List.of(
rectangle(246, -1, 257, 10)
),
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
rectangle(-1, 10)
)
), renderGeometry(feature));
}
@Test
void testFill() {
var feature = polygonFeature(
rectangle(
0.5 - Z14_WIDTH / 2,
0.5 + Z14_WIDTH * 3 / 2
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(1);
assertSameNormalizedFeatures(Map.of(
// row 1
TileCoord.ofXYZ(Z14_TILES / 2 - 1, Z14_TILES / 2 - 1, 14), List.of(
tileBottomRight(1)
),
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2 - 1, 14), List.of(
tileBottom(1)
),
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2 - 1, 14), List.of(
tileBottomLeft(1)
),
// row 2
TileCoord.ofXYZ(Z14_TILES / 2 - 1, Z14_TILES / 2, 14), List.of(
tileRight(1)
),
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newPolygon(tileFill(5), List.of()) // <<<<---- the filled tile!
),
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2, 14), List.of(
tileLeft(1)
),
// row 1
TileCoord.ofXYZ(Z14_TILES / 2 - 1, Z14_TILES / 2 + 1, 14), List.of(
tileTopRight(1)
),
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2 + 1, 14), List.of(
tileTop(1)
),
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2 + 1, 14), List.of(
tileTopLeft(1)
)
), renderGeometry(feature));
}
@Test
void testWorldFill() {
int maxZoom = 8;
var feature = polygonFeature(rectangle(Z14_WIDTH / 2, 1 - Z14_WIDTH / 2))
.setMinPixelSize(1)
.setZoomRange(maxZoom, maxZoom)
.setBufferPixels(0);
AtomicLong num = new AtomicLong(0);
new FeatureRenderer(config, rendered1 -> num.incrementAndGet(), Stats.inMemory())
.accept(feature);
assertEquals(num.get(), Math.pow(4, maxZoom));
}
@Test
void testComplexPolygon() {
var feature = polygonFeature(
newPolygon(
rectangleCoordList(0.5 + Z14_PX * 1, 0.5 + Z14_PX * 255),
List.of(rectangleCoordList(0.5 + Z14_PX * 10, 0.5 + Z14_PX * 250))
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(1);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newPolygon(
rectangleCoordList(1, 255),
List.of(rectangleCoordList(10, 250))
)
)
), renderGeometry(feature));
}
@Test
void testComplexPolygonHoleInfersOuterFill() {
var feature = polygonFeature(
newPolygon(
rectangleCoordList(0.5 - Z14_WIDTH / 2, 0.5 + 3 * Z14_WIDTH / 2),
List.of(rectangleCoordList(0.5 + Z14_PX * 10, 0.5 + Z14_PX * 250))
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(1);
assertSameNormalizedFeatures(Map.of(
// row 1
TileCoord.ofXYZ(Z14_TILES / 2 - 1, Z14_TILES / 2 - 1, 14), List.of(
tileBottomRight(1)
),
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2 - 1, 14), List.of(
tileBottom(1)
),
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2 - 1, 14), List.of(
tileBottomLeft(1)
),
// row 2
TileCoord.ofXYZ(Z14_TILES / 2 - 1, Z14_TILES / 2, 14), List.of(
tileRight(1)
),
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
// the filled tile with a hole!
newPolygon(tileFill(1), List.of(rectangleCoordList(10, 250)))
),
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2, 14), List.of(
tileLeft(1)
),
// row 1
TileCoord.ofXYZ(Z14_TILES / 2 - 1, Z14_TILES / 2 + 1, 14), List.of(
tileTopRight(1)
),
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2 + 1, 14), List.of(
tileTop(1)
),
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2 + 1, 14), List.of(
tileTopLeft(1)
)
), renderGeometry(feature));
}
@Test
void testComplexPolygonHoleBlocksFill() {
var feature = polygonFeature(
newPolygon(
rectangleCoordList(0.5 - Z14_WIDTH / 2, 0.5 + 3 * Z14_WIDTH / 2),
List.of(rectangleCoordList(0.5 - Z14_PX * 10, 0.5 + Z14_PX * 260))
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(1);
Map<TileCoord, Collection<Geometry>> rendered = renderGeometry(feature);
// should be no data for center tile since it's inside the inner fill
assertNull(rendered.get(TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14)));
// notch taken out of bottom right
assertEquals(
List.of(new TestUtils.NormGeometry(newPolygon(
128, 128,
257, 128,
257, 246,
246, 246,
246, 257,
128, 257,
128, 128
))),
rendered.get(TileCoord.ofXYZ(Z14_TILES / 2 - 1, Z14_TILES / 2 - 1, 14)).stream()
.map(TestUtils.NormGeometry::new)
.toList()
);
// 4px taken out of top
assertEquals(
List.of(new TestUtils.NormGeometry(rectangle(-1, 4, 257, 128))),
rendered.get(TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2 + 1, 14)).stream()
.map(TestUtils.NormGeometry::new)
.toList()
);
}
@Test
void testMultipolygon() {
var feature = polygonFeature(
newMultiPolygon(
rectangle(0.5 + Z14_PX * 10, 0.5 + Z14_PX * 20),
rectangle(0.5 + Z14_PX * 30, 0.5 + Z14_PX * 40)
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(1);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newMultiPolygon(
rectangle(10, 20),
rectangle(30, 40)
)
)
), renderGeometry(feature));
}
@Test
void testFixInvalidInputGeometry() {
var feature = polygonFeature(
// bow tie
newPolygon(
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 10,
0.5 + Z14_PX * 30, 0.5 + Z14_PX * 10,
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 20,
0.5 + Z14_PX * 20, 0.5 + Z14_PX * 20,
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 10
)
)
.setMinPixelSize(1)
.setZoomRange(14, 14)
.setBufferPixels(1);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newPolygon(
// it's not perfect, but at least it doesn't have a self-intersection
10, 10,
30, 10,
16.6875, 16.6875,
10, 10
)
)
), renderGeometry(feature));
}
@Test
void testOmitsPolygonUnderMinSize() {
var feature = polygonFeature(rectangle(0.5 + Z13_PX * 10, 0.5 + Z13_PX * 11.9))
.setMinPixelSize(2)
.setZoomRange(13, 13)
.setBufferPixels(1);
assertEquals(0, renderGeometry(feature).size());
feature = polygonFeature(rectangle(0.5 + Z13_PX * 10, 0.5 + Z13_PX * 12.1))
.setMinPixelSize(2)
.setZoomRange(13, 13)
.setBufferPixels(1);
assertEquals(1, renderGeometry(feature).size());
}
@Test
void testIncludesPolygonUnderMinTolerance() {
var feature = polygonFeature(rectangle(0.5 + Z13_PX * 10, 0.5 + Z13_PX * 11.9))
.setMinPixelSize(1)
.setPixelTolerance(2)
.setZoomRange(13, 13)
.setBufferPixels(1);
assertEquals(1, renderGeometry(feature).size());
}
@Test
void testUses1pxMinAreaAtMaxZoom() {
double base = 0.5 + Z14_WIDTH / 2;
var feature = polygonFeature(rectangle(base, base + Z14_WIDTH / 4096 / 2))
.setMinPixelSize(4)
.setZoomRange(14, 14)
.setBufferPixels(1);
assertEquals(0, renderGeometry(feature).size());
feature = polygonFeature(rectangle(base, base + 2 * Z14_WIDTH / 4096))
.setMinPixelSize(4)
.setZoomRange(14, 14)
.setBufferPixels(1);
assertEquals(1, renderGeometry(feature).size());
}
@Test
void testRoundingCollapsesPolygonToLine() {
var feature = polygonFeature(
newPolygon(
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 10,
0.5 + Z14_PX * (10 + 256d / 4096d / 2d), 0.5 + Z14_PX * 100,
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 200,
0.5 + Z14_PX * 10, 0.5 + Z14_PX * 10
)
)
.setMinPixelSize(1d / 4096)
.setZoomRange(13, 13)
.setBufferPixels(1);
assertSameNormalizedFeatures(Map.of(), renderGeometry(feature));
}
private double[] z14WorldCoords(double... coords) {
return DoubleStream.of(coords).map(c -> 0.5 + Z14_PX * c).toArray();
}
private static final double TILE_RESOLUTION_PX = 256d / 4096;
@Test
void testRoundingMakesOutputInvalid() {
var feature = polygonFeature(
newPolygon(z14WorldCoords(
10, 10,
10 + TILE_RESOLUTION_PX, 200,
20, 200,
10 + TILE_RESOLUTION_PX / 3d, 11,
10, 10
))
)
.setMinPixelSize(1d / 4096)
.setZoomRange(14, 14)
.setBufferPixels(1);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newPolygon(
// it's not perfect, but at least it doesn't have a self-intersection
10, 10,
20, 200,
10.0625, 200,
10, 10
)
)
), renderGeometry(feature));
}
@Test
void testSimplifyMakesOutputInvalid() {
var feature = polygonFeature(
newPolygon(z14WorldCoords(
10, 10,
20, 20,
10, 30,
20 - TILE_RESOLUTION_PX / 0.5, 30,
20 + TILE_RESOLUTION_PX / 10, 20,
20 - TILE_RESOLUTION_PX / 0.5, 10,
10, 10
))
)
.setMinPixelSize(1d / 4096)
.setZoomRange(14, 14)
.setBufferPixels(1);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newMultiPolygon(
newPolygon(
19.875, 10,
20, 20,
10, 10,
19.875, 10
),
newPolygon(
10, 30,
20, 20,
19.875, 30,
10, 30
)
)
)
), renderGeometry(feature));
}
@Test
void testNestedMultipolygon() {
var feature = polygonFeature(
newMultiPolygon(
newPolygon(rectangleCoordList(
0.5 + Z14_PX * 10,
0.5 + Z14_PX * 200
), List.of(rectangleCoordList(
0.5 + Z14_PX * 20,
0.5 + Z14_PX * 190
))),
rectangle(0.5 + Z14_PX * 30, 0.5 + Z14_PX * 180)
)
)
.setMinPixelSize(1d / 4096)
.setZoomRange(14, 14)
.setBufferPixels(1);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
newMultiPolygon(
newPolygon(
rectangleCoordList(10, 200),
List.of(rectangleCoordList(20, 190))
),
rectangle(30, 180)
)
)
), renderGeometry(feature));
}
@Test
void testNestedMultipolygonFill() {
var feature = polygonFeature(
newMultiPolygon(
newPolygon(rectangleCoordList(
0.5 - Z14_PX * 30,
0.5 + Z14_PX * (256 + 30)
), List.of(rectangleCoordList(
0.5 - Z14_PX * 20,
0.5 + Z14_PX * (256 + 20)
))),
rectangle(0.5 - Z14_PX * 10, 0.5 + Z14_PX * (256 + 10))
)
)
.setMinPixelSize(1d / 4096)
.setZoomRange(14, 14)
.setBufferPixels(1);
var rendered = renderGeometry(feature);
var innerTile = rendered.get(TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14));
assertEquals(1, innerTile.size());
assertEquals(new TestUtils.NormGeometry(rectangle(-5, 256 + 5)),
new TestUtils.NormGeometry(innerTile.iterator().next()));
}
@Test
void testNestedMultipolygonInfersOuterFill() {
var feature = polygonFeature(
newMultiPolygon(
newPolygon(rectangleCoordList(
0.5 - Z14_PX * 30,
0.5 + Z14_PX * (256 + 30)
), List.of(rectangleCoordList(
0.5 - Z14_PX * 20,
0.5 + Z14_PX * (256 + 20)
))),
newPolygon(rectangleCoordList(
0.5 - Z14_PX * 10,
0.5 + Z14_PX * (256 + 10)
), List.of(rectangleCoordList(
0.5 + Z14_PX * 10,
0.5 + Z14_PX * (256 - 10)
)))
)
)
.setMinPixelSize(1d / 4096)
.setZoomRange(14, 14)
.setBufferPixels(1);
var rendered = renderGeometry(feature);
var innerTile = rendered.get(TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14));
assertEquals(1, innerTile.size());
assertEquals(new TestUtils.NormGeometry(newPolygon(
rectangleCoordList(-1, 256 + 1),
List.of(rectangleCoordList(10, 246))
)),
new TestUtils.NormGeometry(innerTile.iterator().next()));
}
@Test
void testNestedMultipolygonCancelsOutInnerFill() {
var feature = polygonFeature(
newMultiPolygon(
newPolygon(rectangleCoordList(
0.5 - Z14_PX * 30,
0.5 + Z14_PX * (256 + 30)
), List.of(rectangleCoordList(
0.5 - Z14_PX * 20,
0.5 + Z14_PX * (256 + 20)
))),
newPolygon(rectangleCoordList(
0.5 - Z14_PX * 10,
0.5 + Z14_PX * (256 + 10)
), List.of(rectangleCoordList(
0.5 - Z14_PX * 5,
0.5 + Z14_PX * (256 + 5)
)))
)
)
.setMinPixelSize(1d / 4096)
.setZoomRange(14, 14)
.setBufferPixels(1);
var rendered = renderGeometry(feature);
assertFalse(rendered.containsKey(TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14)));
}
@Test
void testOverlappingMultipolygon() {
var feature = polygonFeature(newMultiPolygon(
rectangle(10d / 256, 10d / 256, 30d / 256, 30d / 256),
rectangle(20d / 256, 20d / 256, 40d / 256, 40d / 256)
))
.setMinPixelSize(1)
.setBufferPixels(4)
.setZoomRange(0, 0);
assertSameNormalizedFeatures(Map.of(
TileCoord.ofXYZ(0, 0, 0), List.of(newPolygon(
10, 10,
30, 10,
30, 20,
40, 20,
40, 40,
20, 40,
20, 30,
10, 30,
10, 10
))
), renderGeometry(feature));
}
@Test
void testOverlappingMultipolygonSideBySide() {
var feature = polygonFeature(newMultiPolygon(
rectangle(10d / 256, 10d / 256, 20d / 256, 20d / 256),
rectangle(15d / 256, 10d / 256, 25d / 256, 20d / 256)
))
.setMinPixelSize(1)
.setBufferPixels(4)
.setZoomRange(0, 0);
assertTopologicallyEquivalentFeatures(Map.of(
TileCoord.ofXYZ(0, 0, 0), List.of(rectangle(
10, 10,
25, 20
))
), renderGeometry(feature));
}
@Test
void testPolygonWrap() {
var feature = polygonFeature(rectangle(
-1d / 256, -1d / 256, 257d / 256, 1d / 256
))
.setMinPixelSize(1)
.setBufferPixels(4)
.setZoomRange(0, 1);
assertTopologicallyEquivalentFeatures(Map.of(
TileCoord.ofXYZ(0, 0, 0), List.of(
rectangle(-4, -1, 260, 1)
),
TileCoord.ofXYZ(0, 0, 1), List.of(
rectangle(-4, -2, 260, 2)
),
TileCoord.ofXYZ(1, 0, 1), List.of(
rectangle(-4, -2, 260, 2)
)
), renderGeometry(feature));
}
private static Geometry rotateWorld(Geometry geom, double degrees) {
return AffineTransformation.rotationInstance(-Math.PI * degrees / 180, 0.5 + Z14_WIDTH / 2, 0.5 + Z14_WIDTH / 2)
.transform(geom);
}
private static Geometry rotateTile(Geometry geom, double degrees) {
return AffineTransformation.rotationInstance(-Math.PI * degrees / 180, 128, 128)
.transform(geom);
}
private void testClipWithRotation(double rotation, Geometry inputTile) {
Geometry input = new AffineTransformation()
.scale(1d / 256 / Z14_TILES, 1d / 256 / Z14_TILES)
.translate(0.5, 0.5)
.transform(inputTile);
Geometry expectedOutput = inputTile.intersection(rectangle(-4, 260));
var feature = polygonFeature(rotateWorld(input, rotation))
.setBufferPixels(4)
.setZoomRange(14, 14);
var geom = renderGeometry(feature)
.get(TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14))
.iterator().next();
assertTopologicallyEquivalentFeature(
round(rotateTile(expectedOutput, rotation)),
round(geom)
);
}
@ParameterizedTest
@ValueSource(ints = {0, 90, 180, -90})
void testBackAndForthsOutsideTile(int rotation) {
testClipWithRotation(rotation, newPolygon(
300, -10,
310, 300,
320, -10,
330, 300,
340, 400,
128, 400,
128, 128,
128, -10,
300, -10
));
}
@ParameterizedTest
@ValueSource(ints = {0, 90, 180, -90})
void testReplayEdgesOuterPoly(int rotation) {
testClipWithRotation(rotation, newPolygon(
130, -10,
270, -10,
270, 270,
-10, 270,
-10, -10,
120, -10,
120, 10,
130, 10,
130, -10
));
}
@ParameterizedTest
@ValueSource(ints = {0, 90, 180, -90})
void testReplayEdgesInnerPoly(int rotation) {
var innerShape = newCoordinateList(
130, -10,
270, -10,
270, 270,
-10, 270,
-10, -10,
120, -10,
120, 10,
130, 10,
130, -10
);
Collections.reverse(innerShape);
testClipWithRotation(rotation, newPolygon(
rectangleCoordList(-20, 300),
List.of(innerShape)
));
}
@ParameterizedTest
@CsvSource({
"0, 0",
"0.5, 0",
"0.5, 0.5",
"0, 0.5",
"-0.5, 0.5",
"-0.5, 0",
"-0.5, -0.5",
"0, -0.5",
"0.5, -0.5"
})
void testSpiral(double dx, double dy) {
// generate spirals at different offsets and make sure that tile clipping
// returns the same result as JTS intersection with the tile's boundary
List<Coordinate> coords = new ArrayList<>();
int outerRadius = 300;
int iters = 25;
for (int i = 0; i < iters; i++) {
int radius = outerRadius - i * 10;
coords.add(new CoordinateXY(-radius, 0));
coords.add(new CoordinateXY(0, -radius));
coords.add(new CoordinateXY(radius, 0));
coords.add(new CoordinateXY(0, radius));
}
Geometry poly = newLineString(coords).buffer(1, 1);
poly = AffineTransformation.translationInstance(128 + dx * 256, 128 + dy * 256).transform(poly);
Geometry input = new AffineTransformation()
.scale(1d / 256d / Z14_TILES, 1d / 256d / Z14_TILES)
.translate(0.5, 0.5)
.transform(poly);
Geometry expectedOutput = poly.intersection(rectangle(-4, 260));
var feature = polygonFeature(input)
.setBufferPixels(4)
.setZoomRange(14, 14);
var actual = renderGeometry(feature)
.get(TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14))
.iterator().next();
assertTopologicallyEquivalentFeature(
GeometryPrecisionReducer.reduce(expectedOutput, new PrecisionModel(4096d / 256d)),
actual
);
}
}