add test coverage

pull/1/head
Mike Barry 2021-05-27 05:54:45 -04:00
rodzic 0b00a82ab4
commit 27d3aa9d75
9 zmienionych plików z 316 dodań i 44 usunięć

Wyświetl plik

@ -80,12 +80,15 @@ public class OpenMapTilesMain {
stats.time("lake_centerlines", () ->
ShapefileReader
.process("EPSG:3857", "lake_centerlines", centerlines, featureMap, config, profile, stats));
.process("EPSG:3857", OpenMapTilesProfile.LAKE_CENTERLINE_SOURCE, centerlines, featureMap, config, profile,
stats));
stats.time("water_polygons", () ->
ShapefileReader.process("water_polygons", waterPolygons, featureMap, config, profile, stats));
ShapefileReader
.process(OpenMapTilesProfile.WATER_POLYGON_SOURCE, waterPolygons, featureMap, config, profile, stats));
stats.time("natural_earth", () ->
NaturalEarthReader
.process("natural_earth", naturalEarth, tmpDir.resolve("natearth.sqlite"), featureMap, config,
.process(OpenMapTilesProfile.NATURAL_EARTH_SOURCE, naturalEarth, tmpDir.resolve("natearth.sqlite"), featureMap,
config,
profile, stats)
);

Wyświetl plik

@ -45,12 +45,6 @@ public class GeoUtils {
public static final GeometryTransformer UNPROJECT_WORLD_COORDS = new GeometryTransformer() {
@Override
protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent) {
if (coords.getDimension() != 2) {
throw new IllegalArgumentException("Dimension must be 2, was: " + coords.getDimension());
}
if (coords.getMeasures() != 0) {
throw new IllegalArgumentException("Measures must be 0, was: " + coords.getMeasures());
}
CoordinateSequence copy = new PackedCoordinateSequence.Double(coords.size(), 2, 0);
for (int i = 0; i < coords.size(); i++) {
copy.setOrdinate(i, 0, getWorldLon(coords.getX(i)));
@ -62,12 +56,6 @@ public class GeoUtils {
public static final GeometryTransformer PROJECT_WORLD_COORDS = new GeometryTransformer() {
@Override
protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent) {
if (coords.getDimension() != 2) {
throw new IllegalArgumentException("Dimension must be 2, was: " + coords.getDimension());
}
if (coords.getMeasures() != 0) {
throw new IllegalArgumentException("Measures must be 0, was: " + coords.getMeasures());
}
CoordinateSequence copy = new PackedCoordinateSequence.Double(coords.size(), 2, 0);
for (int i = 0; i < coords.size(); i++) {
copy.setOrdinate(i, 0, getWorldX(coords.getX(i)));

Wyświetl plik

@ -12,6 +12,9 @@ import org.slf4j.LoggerFactory;
public class OpenMapTilesProfile implements Profile {
public static final String LAKE_CENTERLINE_SOURCE = "lake_centerlines";
public static final String WATER_POLYGON_SOURCE = "water_polygons";
public static final String NATURAL_EARTH_SOURCE = "natural_earth";
private static final Logger LOGGER = LoggerFactory.getLogger(OpenMapTilesProfile.class);
@Override
@ -52,12 +55,30 @@ public class OpenMapTilesProfile implements Profile {
}
@Override
public void processFeature(SourceFeature sourceFeature,
FeatureCollector features) {
public void processFeature(SourceFeature sourceFeature, FeatureCollector features) {
if (sourceFeature.isPoint()) {
if (sourceFeature.hasTag("natural", "peak", "volcano")) {
features.point("mountain_peak")
.setAttr("name", sourceFeature.getTag("name"));
.setAttr("name", sourceFeature.getTag("name"))
.setLabelGridSizeAndLimit(13, 100, 5);
}
}
if (WATER_POLYGON_SOURCE.equals(sourceFeature.getSource())) {
features.polygon("water").setZoomRange(6, 14).setAttr("class", "ocean");
} else if (NATURAL_EARTH_SOURCE.equals(sourceFeature.getSource())) {
String sourceLayer = sourceFeature.getSourceLayer();
boolean lake = sourceLayer.endsWith("_lakes");
switch (sourceLayer) {
case "ne_10m_lakes", "ne_10m_ocean" -> features.polygon("water")
.setZoomRange(4, 5)
.setAttr("class", lake ? "lake" : "ocean");
case "ne_50m_lakes", "ne_50m_ocean" -> features.polygon("water")
.setZoomRange(2, 3)
.setAttr("class", lake ? "lake" : "ocean");
case "ne_110m_lakes", "ne_110m_ocean" -> features.polygon("water")
.setZoomRange(0, 1)
.setAttr("class", lake ? "lake" : "ocean");
}
}
}

Wyświetl plik

@ -34,7 +34,7 @@ public class ShapefileReader extends Reader implements Closeable {
public static void process(String sourceProjection, String sourceName, Path input, FeatureGroup writer,
CommonParams config,
Profile profile, Stats stats) {
try (var reader = new ShapefileReader(sourceName, sourceProjection, input, profile, stats)) {
try (var reader = new ShapefileReader(sourceProjection, sourceName, input, profile, stats)) {
reader.process(writer, config);
}
}

Wyświetl plik

@ -57,16 +57,15 @@ public class FeatureRenderer {
}
private void renderGeometry(Geometry geom, FeatureCollector.Feature feature) {
// TODO what about converting between area and line?
if (geom.isEmpty()) {
LOGGER.warn("Empty geometry " + feature);
} else if (geom instanceof Point point) {
addPointFeature(feature, point.getCoordinates());
renderPoint(feature, point.getCoordinates());
} else if (geom instanceof MultiPoint points) {
addPointFeature(feature, points);
renderPoint(feature, points);
} else if (geom instanceof Polygon || geom instanceof MultiPolygon || geom instanceof LineString
|| geom instanceof MultiLineString) {
addLinearFeature(feature, geom);
renderLineOrPolygon(feature, geom);
} else if (geom instanceof GeometryCollection collection) {
for (int i = 0; i < collection.getNumGeometries(); i++) {
renderGeometry(collection.getGeometryN(i), feature);
@ -77,7 +76,7 @@ public class FeatureRenderer {
}
}
private void addPointFeature(FeatureCollector.Feature feature, Coordinate... coords) {
private void renderPoint(FeatureCollector.Feature feature, Coordinate... coords) {
long id = idGen.incrementAndGet();
boolean hasLabelGrid = feature.hasLabelGrid();
for (int zoom = feature.getMaxZoom(); zoom >= feature.getMinZoom(); zoom--) {
@ -122,17 +121,17 @@ public class FeatureRenderer {
));
}
private void addPointFeature(FeatureCollector.Feature feature, MultiPoint points) {
private void renderPoint(FeatureCollector.Feature feature, MultiPoint points) {
if (feature.hasLabelGrid()) {
for (Coordinate coord : points.getCoordinates()) {
addPointFeature(feature, coord);
renderPoint(feature, coord);
}
} else {
addPointFeature(feature, points.getCoordinates());
renderPoint(feature, points.getCoordinates());
}
}
private void addLinearFeature(FeatureCollector.Feature feature, Geometry input) {
private void renderLineOrPolygon(FeatureCollector.Feature feature, Geometry input) {
long id = idGen.incrementAndGet();
boolean area = input instanceof Polygonal;
double worldLength = (area || input.getNumGeometries() > 1) ? 0 : input.getLength();

Wyświetl plik

@ -156,7 +156,6 @@ class TiledGeometry {
}
private static CoordinateSequence fill(double buffer) {
buffer += 1d / 4096;
double min = -256d * buffer;
double max = 256d - min;
return new PackedCoordinateSequence.Double(new double[]{

Wyświetl plik

@ -446,7 +446,7 @@ public class FlatMapTest {
)),
newTileEntry(Z14_TILES / 2 + 1, Z14_TILES / 2 + 1, 14, List.of(
feature(newPolygon(
tileFill(4 + 256d / 4096),
tileFill(4),
List.of(newCoordinateList(
64, 64,
192, 64,

Wyświetl plik

@ -157,14 +157,14 @@ public class TestUtils {
return GeoUtils.JTS_FACTORY.createGeometryCollection(geoms);
}
public static Geometry round(Geometry input) {
public static Geometry round(Geometry input, double delta) {
return new GeometryTransformer() {
@Override
protected CoordinateSequence transformCoordinates(
CoordinateSequence coords, Geometry parent) {
for (int i = 0; i < coords.size(); i++) {
for (int j = 0; j < coords.getDimension(); j++) {
coords.setOrdinate(i, j, Math.round(coords.getOrdinate(i, j) * 1e5) / 1e5);
coords.setOrdinate(i, j, Math.round(coords.getOrdinate(i, j) * delta) / delta);
}
}
return coords;
@ -172,6 +172,10 @@ public class TestUtils {
}.transform(input.copy());
}
public static Geometry round(Geometry input) {
return round(input, 1e5);
}
private static byte[] gunzip(byte[] zipped) throws IOException {
try (var is = new GZIPInputStream(new ByteArrayInputStream(zipped))) {
return is.readAllBytes();
@ -400,16 +404,6 @@ public class TestUtils {
);
}
public static void assertTopologicallyEquivalentFeatures(
Map<TileCoord, Collection<Geometry>> expected,
Map<TileCoord, Collection<Geometry>> actual
) {
assertEquals(
mapTileFeatures(expected, TopoGeometry::new),
mapTileFeatures(actual, TopoGeometry::new)
);
}
public static void assertSameNormalizedFeatures(
Map<TileCoord, Collection<Geometry>> expected,
Map<TileCoord, Collection<Geometry>> actual
@ -424,6 +418,20 @@ public class TestUtils {
assertEquals(new NormGeometry(expected), new NormGeometry(actual));
}
public static void assertTopologicallyEquivalentFeatures(
Map<TileCoord, Collection<Geometry>> expected,
Map<TileCoord, Collection<Geometry>> actual
) {
assertEquals(
mapTileFeatures(expected, TopoGeometry::new),
mapTileFeatures(actual, TopoGeometry::new)
);
}
public static void assertTopologicallyEquivalentFeature(Geometry expected, Geometry actual) {
assertEquals(new TopoGeometry(expected), new TopoGeometry(actual));
}
public static void assertNormalizedSubmap(
Map<TileCoord, Collection<Geometry>> expectedSubmap,
Map<TileCoord, Collection<Geometry>> actual

Wyświetl plik

@ -15,6 +15,7 @@ import com.onthegomap.flatmap.geo.TileCoord;
import com.onthegomap.flatmap.read.ReaderFeature;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -27,7 +28,13 @@ 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;
public class FeatureRendererTest {
@ -470,6 +477,61 @@ public class FeatureRendererTest {
), renderGeometry(feature));
}
@Test
public 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
*/
@ -798,7 +860,7 @@ public class FeatureRendererTest {
),
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
// the filled tile with a hole!
newPolygon(tileFill(1 + 256d / 4096), List.of(rectangleCoordList(10, 250)))
newPolygon(tileFill(1), List.of(rectangleCoordList(10, 250)))
),
TileCoord.ofXYZ(Z14_TILES / 2 + 1, Z14_TILES / 2, 14), List.of(
tileLeft(1)
@ -1098,7 +1160,7 @@ public class FeatureRendererTest {
var innerTile = rendered.get(TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14));
assertEquals(1, innerTile.size());
assertEquals(new TestUtils.NormGeometry(newPolygon(
rectangleCoordList(-1 - TILE_RESOLUTION_PX, 256 + 1 + TILE_RESOLUTION_PX),
rectangleCoordList(-1, 256 + 1),
List.of(rectangleCoordList(10, 246))
)),
new TestUtils.NormGeometry(innerTile.iterator().next()));
@ -1130,4 +1192,196 @@ public class FeatureRendererTest {
var rendered = renderGeometry(feature);
assertFalse(rendered.containsKey(TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14)));
}
@Test
public 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
public 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
public 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})
public 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})
public 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})
public 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"
})
public 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
);
}
}