Fix geometry errors (#526)

pull/527/head
Michael Barry 2023-03-20 16:41:18 -04:00 zatwierdzone przez GitHub
rodzic 65f620d663
commit 509795401e
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
19 zmienionych plików z 41980 dodań i 52 usunięć

Wyświetl plik

@ -78,7 +78,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
public Feature point(String layer) {
try {
if (!source.isPoint()) {
throw new GeometryException("feature_not_point", "not a point");
throw new GeometryException("feature_not_point", "not a point", true);
}
return geometry(layer, source.worldGeometry());
} catch (GeometryException e) {

Wyświetl plik

@ -38,6 +38,12 @@ public class IntRangeSet implements Iterable<Integer> {
return this;
}
/** Mutates and returns this range set, with range {@code a} to {@code b} (inclusive) removed. */
public IntRangeSet remove(int a, int b) {
bitmap.remove(a, (long) b + 1);
return this;
}
/** Mutates and returns this range set, with {@code a} removed. */
public IntRangeSet remove(int a) {
bitmap.remove(a);
@ -59,6 +65,24 @@ public class IntRangeSet implements Iterable<Integer> {
return this;
}
@Override
public String toString() {
return bitmap.toString();
}
public boolean isEmpty() {
return bitmap.isEmpty();
}
/**
* Mutates and returns this range set, with each element from {@code a} to {@code b} (inclusive) added if missing, and
* removed if present.
*/
public IntRangeSet xor(int start, int end) {
bitmap.xor(RoaringBitmap.bitmapOfRange(start, end + 1L));
return this;
}
/** Iterate through all ints in this range */
private record Iter(PeekableIntIterator iter) implements PrimitiveIterator.OfInt {

Wyświetl plik

@ -264,6 +264,20 @@ public class GeoUtils {
}
}
/**
* More aggressive fix for self-intersections than {@link #fixPolygon(Geometry)} that expands then contracts the shape
* by {@code buffer}.
*
* @throws GeometryException if a robustness error occurred
*/
public static Geometry fixPolygon(Geometry geom, double buffer) throws GeometryException {
try {
return geom.buffer(buffer).buffer(-buffer);
} catch (TopologyException e) {
throw new GeometryException("fix_polygon_buffer_topology_error", "robustness error fixing polygon: " + e);
}
}
public static Geometry combineLineStrings(List<LineString> lineStrings) {
return lineStrings.size() == 1 ? lineStrings.get(0) : createMultiLineString(lineStrings);
}
@ -300,8 +314,8 @@ public class GeoUtils {
try {
return GeometryPrecisionReducer.reduce(geom, tilePrecision);
} catch (IllegalArgumentException e2) {
// give it one last try, just in case
geom = fixPolygon(geom);
// give it one last try but with more aggressive fixing, just in case (see issue #511)
geom = fixPolygon(geom, tilePrecision.gridSize() / 2);
try {
return GeometryPrecisionReducer.reduce(geom, tilePrecision);
} catch (IllegalArgumentException e3) {

Wyświetl plik

@ -13,6 +13,7 @@ public class GeometryException extends Exception {
private static final Logger LOGGER = LoggerFactory.getLogger(GeometryException.class);
private final String stat;
private final boolean nonFatal;
/**
* Constructs a new exception with a detailed error message caused by {@code cause}.
@ -25,18 +26,29 @@ public class GeometryException extends Exception {
public GeometryException(String stat, String message, Throwable cause) {
super(message, cause);
this.stat = stat;
this.nonFatal = false;
}
/**
* Constructs a new exception with a detailed error message for. Use
* {@link #GeometryException(String, String, boolean)} for non-fatal exceptions.
*/
public GeometryException(String stat, String message) {
this(stat, message, false);
}
/**
* Constructs a new exception with a detailed error message.
*
* @param stat string that uniquely defines this error that will be used to count number of occurrences in stats
* @param message description of the error to log that should be detailed enough that you can find the offending
* geometry from it
* @param stat string that uniquely defines this error that will be used to count number of occurrences in stats
* @param message description of the error to log that should be detailed enough that you can find the offending
* geometry from it
* @param nonFatal When true, won't cause an assertion error when thrown
*/
public GeometryException(String stat, String message) {
public GeometryException(String stat, String message, boolean nonFatal) {
super(message);
this.stat = stat;
this.nonFatal = nonFatal;
}
/** Returns the unique code for this error condition to use for counting the number of occurrences in stats. */
@ -57,6 +69,7 @@ public class GeometryException extends Exception {
void logMessage(String log) {
LOGGER.warn(log);
assert nonFatal : log; // make unit tests fail if fatal
}
/**

Wyświetl plik

@ -7,6 +7,7 @@ import com.onthegomap.planetiler.util.Hilbert;
import javax.annotation.concurrent.Immutable;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateXY;
import org.locationtech.jts.geom.Envelope;
/**
* The coordinate of a <a href="https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames">slippy map tile</a>.
@ -171,4 +172,14 @@ public record TileCoord(int encoded, int x, int y, int z) implements Comparable<
public TileCoord parent() {
return ofXYZ(x / 2, y / 2, z - 1);
}
public Envelope bounds() {
double worldWidthAtZoom = Math.pow(2, z);
return new Envelope(
GeoUtils.getWorldLon(x / worldWidthAtZoom),
GeoUtils.getWorldLon((x + 1) / worldWidthAtZoom),
GeoUtils.getWorldLat(y / worldWidthAtZoom),
GeoUtils.getWorldLat((y + 1) / worldWidthAtZoom)
);
}
}

Wyświetl plik

@ -103,7 +103,7 @@ public class NaturalEarthReader extends SimpleReader<SimpleFeature> {
extracted = unzippedDir.resolve(URLEncoder.encode(zipEntry.toString(), StandardCharsets.UTF_8));
FileUtils.createParentDirectories(extracted);
if (!keepUnzipped || FileUtils.isNewer(path, extracted)) {
LOGGER.error("unzipping {} to {}", path.toAbsolutePath(), extracted);
LOGGER.info("unzipping {} to {}", path.toAbsolutePath(), extracted);
Files.copy(Files.newInputStream(zipEntry), extracted, StandardCopyOption.REPLACE_EXISTING);
}
if (!keepUnzipped) {

Wyświetl plik

@ -169,7 +169,7 @@ public abstract class SourceFeature implements WithTags, WithGeometryType {
*/
public final Geometry line() throws GeometryException {
if (!canBeLine()) {
throw new GeometryException("feature_not_line", "cannot be line");
throw new GeometryException("feature_not_line", "cannot be line", true);
}
if (linearGeometry == null) {
linearGeometry = computeLine();
@ -198,7 +198,7 @@ public abstract class SourceFeature implements WithTags, WithGeometryType {
*/
public final Geometry polygon() throws GeometryException {
if (!canBePolygon()) {
throw new GeometryException("feature_not_polygon", "cannot be polygon");
throw new GeometryException("feature_not_polygon", "cannot be polygon", true);
}
return polygonGeometry != null ? polygonGeometry : (polygonGeometry = computePolygon());
}
@ -222,7 +222,7 @@ public abstract class SourceFeature implements WithTags, WithGeometryType {
*/
public final Geometry validatedPolygon() throws GeometryException {
if (!canBePolygon()) {
throw new GeometryException("feature_not_polygon", "cannot be polygon");
throw new GeometryException("feature_not_polygon", "cannot be polygon", true);
}
return validPolygon != null ? validPolygon : (validPolygon = computeValidPolygon());
}

Wyświetl plik

@ -517,7 +517,7 @@ public class TiledGeometry {
// keep a record of filled tiles that we skipped because an edge of the polygon that gets processed
// later may intersect the edge of a filled tile, and we'll need to replay all the edges we skipped
record SkippedSegment(Direction side, int lo, int hi) {}
record SkippedSegment(Direction side, int lo, int hi, boolean asc) {}
List<SkippedSegment> skipped = null;
for (int i = 0; i < stripeSegment.size() - 1; i++) {
@ -537,8 +537,8 @@ public class TiledGeometry {
int endY = Math.min(extentMaxY - 1, (int) Math.floor(maxY + neighborBuffer));
// inside a fill if one edge of the polygon runs straight down the right side or up the left side of the column
boolean onRightEdge = area && ax == bx && ax == rightEdge && by > ay;
boolean onLeftEdge = area && ax == bx && ax == leftEdge && by < ay;
boolean onRightEdge = area && ax == bx && ax == rightEdge;
boolean onLeftEdge = area && ax == bx && ax == leftEdge;
for (int y = startY; y <= endY; y++) {
// skip over filled tiles until we get to the next tile that already has detail on it
@ -553,23 +553,47 @@ public class TiledGeometry {
Integer next = tileYsWithDetail.ceiling(y);
int nextNonEdgeTile = next == null ? startEndY : Math.min(next, startEndY);
int endSkip = nextNonEdgeTile - 1;
if (skipped == null) {
skipped = new ArrayList<>();
}
// save the Y range that we skipped in case a later edge intersects a filled tile
skipped.add(new SkippedSegment(
onLeftEdge ? Direction.LEFT : Direction.RIGHT,
y,
endSkip
));
if (endSkip >= y) {
if (skipped == null) {
skipped = new ArrayList<>();
}
var skippedSegment = new SkippedSegment(
onLeftEdge ? Direction.LEFT : Direction.RIGHT,
y,
endSkip,
by > ay
);
skipped.add(skippedSegment);
if (rightFilled == null) {
rightFilled = new IntRangeSet();
leftFilled = new IntRangeSet();
// System.err.println(" " + skippedSegment);
if (rightFilled == null) {
rightFilled = new IntRangeSet();
leftFilled = new IntRangeSet();
}
/*
A tile is inside a filled region when there is an odd number of vertical edges to the left and right
for example a simple shape:
---------
out | in | out
(0/2) | (1/1) | (2/0)
---------
or a more complex shape
--------- ---------
out | in | out | in |
(0/4) | (1/3) | (2/2) | (3/1) |
| --------- |
-------------------------
So we keep track of this number by xor'ing the left and right fills repeatedly,
then and'ing them together at the end.
*/
(onRightEdge ? rightFilled : leftFilled).xor(y, endSkip);
y = nextNonEdgeTile;
}
(onRightEdge ? rightFilled : leftFilled).add(y, endSkip);
y = nextNonEdgeTile;
}
}
@ -606,13 +630,11 @@ public class TiledGeometry {
if (skippedSegment.lo <= y && skippedSegment.hi >= y) {
double top = y - buffer;
double bottom = y + 1 + buffer;
if (skippedSegment.side == Direction.LEFT) {
slice.addPoint(-buffer, bottom);
slice.addPoint(-buffer, top);
} else { // side == RIGHT
slice.addPoint(1 + buffer, top);
slice.addPoint(1 + buffer, bottom);
}
double start = skippedSegment.asc ? top : bottom;
double end = skippedSegment.asc ? bottom : top;
double edgeX = skippedSegment.side == Direction.LEFT ? -buffer : (1 + buffer);
slice.addPoint(edgeX, start);
slice.addPoint(edgeX, end);
}
}
}
@ -677,7 +699,7 @@ public class TiledGeometry {
}
private void addFilledRange(int x, IntRangeSet yRange) {
if (yRange == null) {
if (yRange == null || yRange.isEmpty()) {
return;
}
if (filledRanges == null) {
@ -692,7 +714,7 @@ public class TiledGeometry {
}
private void removeFilledRange(int x, IntRangeSet yRange) {
if (yRange == null) {
if (yRange == null || yRange.isEmpty()) {
return;
}
if (filledRanges == null) {

Wyświetl plik

@ -22,7 +22,7 @@ public class Format {
private static final ConcurrentMap<Locale, Format> instances = new ConcurrentHashMap<>();
@SuppressWarnings("java:S5164")
private static final NumberFormat latLonNF = NumberFormat.getNumberInstance(Locale.US);
private static final NumberFormat latLonNF = NumberFormat.getIntegerInstance(Locale.US);
private static final NavigableMap<Long, String> STORAGE_SUFFIXES = new TreeMap<>(Map.ofEntries(
Map.entry(1_000L, "k"),
Map.entry(1_000_000L, "M"),
@ -40,6 +40,7 @@ public class Format {
static {
latLonNF.setMaximumFractionDigits(5);
latLonNF.setGroupingUsed(false);
}
// `NumberFormat` instances are not thread safe, so we need to wrap them inside a `ThreadLocal`.

Wyświetl plik

@ -735,12 +735,13 @@ class PlanetilerTests {
@CsvSource({
"chesapeake.wkb, 4076",
"mdshore.wkb, 19904",
"njshore.wkb, 10571"
"njshore.wkb, 10571",
"kobroor.wkb, 21693"
})
void testComplexShorelinePolygons__TAKES_A_MINUTE_OR_TWO(String fileName, int expected)
throws Exception {
LOGGER.warn("Testing complex shoreline processing for " + fileName + " ...");
MultiPolygon geometry = (MultiPolygon) new WKBReader()
Geometry geometry = new WKBReader()
.read(new InputStreamInStream(Files.newInputStream(TestUtils.pathToResource(fileName))));
assertNotNull(geometry);
@ -1611,7 +1612,11 @@ class PlanetilerTests {
private static List<OsmElement> convertToOsmElements(OsmXml osmInfo) {
List<OsmElement> elements = new ArrayList<>();
for (var node : orEmpty(osmInfo.nodes())) {
elements.add(new OsmElement.Node(node.id(), node.lat(), node.lon()));
var newNode = new OsmElement.Node(node.id(), node.lat(), node.lon());
elements.add(newNode);
for (var tag : orEmpty(node.tags())) {
newNode.setTag(tag.k(), tag.v());
}
}
for (var way : orEmpty(osmInfo.ways())) {
var readerWay = new OsmElement.Way(way.id());
@ -1676,13 +1681,42 @@ class PlanetilerTests {
assertEquals(8, results.tiles.size());
}
@Test
void testIssue509LenaDelta() throws Exception {
OsmXml osmInfo = TestUtils.readOsmXml("issue_509_lena_delta.xml");
List<OsmElement> elements = convertToOsmElements(osmInfo);
var results = runWithOsmElements(
Map.of("threads", "1"),
elements,
(in, features) -> {
if (in.hasTag("natural", "water")) {
features.polygon("water").setAttr("id", in.id()).setMinZoom(10);
}
}
);
Map<Integer, Integer> counts = new TreeMap<>();
for (var tile : results.tiles().keySet()) {
counts.merge(tile.z(), 1, Integer::sum);
}
assertEquals(Map.of(
10, 39,
11, 125,
12, 397,
13, 1160,
14, 3108
), counts);
}
@ParameterizedTest
@ValueSource(strings = {
"",
"--write-threads=2 --process-threads=2 --feature-read-threads=2 --threads=4",
"--free-osm-after-read",
"--osm-parse-node-bounds",
"--output-format=pmtiles"
"--output-format=pmtiles",
})
void testPlanetilerRunner(String args) throws Exception {
boolean pmtiles = args.contains("pmtiles");

Wyświetl plik

@ -523,7 +523,9 @@ public class TestUtils {
@JacksonXmlRootElement(localName = "node")
public record Node(
long id, double lat, double lon
long id, double lat, double lon,
@JacksonXmlProperty(localName = "tag")
@JacksonXmlElementWrapper(useWrapping = false) List<Tag> tags
) {}
@JacksonXmlRootElement(localName = "nd")

Wyświetl plik

@ -48,10 +48,10 @@ class TileArchiveMetadataTest {
))));
var map = new TreeMap<>(metadata.toMap());
assertNotNull(map.remove("planetiler:version"));
assertNotNull(map.remove("planetiler:githash"));
assertNotNull(map.remove("planetiler:buildtime"));
map.remove("planetiler:githash");
map.remove("planetiler:buildtime");
assertEquals(
new TreeMap<String, String>(Map.of(
new TreeMap<>(Map.of(
"name", "Null",
"type", "baselayer",
"format", "pbf",

Wyświetl plik

@ -1,6 +1,8 @@
package com.onthegomap.planetiler.collection;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.List;
@ -137,4 +139,30 @@ class IntRangeSetTest {
range.intersect(range2);
assertEquals(List.of(3, 5), getInts(range));
}
@Test
void testIsEmpty() {
IntRangeSet range = new IntRangeSet();
assertTrue(range.isEmpty());
range.add(1, 5);
assertFalse(range.isEmpty());
var other = new IntRangeSet();
other.add(6, 10);
range.intersect(other);
assertTrue(range.isEmpty());
}
@Test
void testXor() {
IntRangeSet range = new IntRangeSet();
assertEquals(List.of(), getInts(range));
range.xor(1, 5);
assertEquals(List.of(1, 2, 3, 4, 5), getInts(range));
range.xor(1, 5);
assertEquals(List.of(), getInts(range));
range.xor(1, 5);
assertEquals(List.of(1, 2, 3, 4, 5), getInts(range));
range.xor(3, 6);
assertEquals(List.of(1, 2, 6), getInts(range));
}
}

Wyświetl plik

@ -5,6 +5,7 @@ import static com.onthegomap.planetiler.geo.GeoUtils.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.geotools.geometry.jts.WKTReader2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@ -12,6 +13,7 @@ import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.util.AffineTransformation;
import org.locationtech.jts.io.ParseException;
class GeoUtilsTest {
@ -358,4 +360,13 @@ class GeoUtilsTest {
newLineString(4, 4, 5, 5)
));
}
@Test
void testSnapAndFixIssue511() throws ParseException, GeometryException {
var result = GeoUtils.snapAndFixPolygon(new WKTReader2().read(
"""
MULTIPOLYGON (((198.83750000000003 46.07500000000004, 199.0625 46.375, 199.4375 46.0625, 199.5 46.43750000000001, 199.5625 46, 199.3125 45.5, 198.8912037037037 46.101851851851876, 198.83750000000003 46.07500000000004)), ((198.43750000000003 46.49999999999999, 198.5625 46.43750000000001, 198.6875 46.25, 198.1875 46.25, 198.43750000000003 46.49999999999999)), ((198.6875 46.25, 198.81249999999997 46.062500000000014, 198.6875 46.00000000000002, 198.6875 46.25)), ((196.55199579831933 46.29359243697479, 196.52255639097743 46.941259398496236, 196.5225563909774 46.941259398496236, 196.49999999999997 47.43750000000001, 196.875 47.125, 197 47.5625, 197.47880544905414 46.97729334004497, 197.51505401161464 46.998359569801956, 197.25 47.6875, 198.0625 47.6875, 198.5 46.625, 198.34375 46.546875, 198.34375000000003 46.54687499999999, 197.875 46.3125, 197.875 46.25, 197.875 46.0625, 197.82894736842107 46.20065789473683, 197.25 46.56250000000001, 197.3125 46.125, 196.9375 46.1875, 196.9375 46.21527777777778, 196.73250000000002 46.26083333333334, 196.5625 46.0625, 196.55199579831933 46.29359243697479)), ((196.35213414634146 45.8170731707317, 197.3402027027027 45.93108108108108, 197.875 45.99278846153846, 197.875 45.93750000000002, 197.93749999999997 45.99999999999999, 197.9375 46, 197.90625 45.96874999999999, 197.90625 45.96875, 196.75000000000006 44.81250000000007, 197.1875 45.4375, 196.3125 45.8125, 196.35213414634146 45.8170731707317)), ((195.875 46.124999999999986, 195.8125 46.5625, 196.5 46.31250000000001, 195.9375 46.4375, 195.875 46.124999999999986)), ((196.49999999999997 46.93749999999999, 196.125 46.875, 196.3125 47.125, 196.49999999999997 46.93749999999999)))
"""));
assertEquals(3.146484375, result.getArea(), 1e-5);
}
}

Wyświetl plik

@ -77,7 +77,7 @@ class FeatureRendererTest {
@Test
void testEmptyGeometry() {
var feature = collector(emptyGeometry()).point("layer");
var feature = collector(GeoUtils.JTS_FACTORY.createPoint()).point("layer");
assertSameNormalizedFeatures(Map.of(), renderGeometry(feature));
}

Wyświetl plik

@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.onthegomap.planetiler.TestUtils;
import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.geo.MutableCoordinateSequence;
import com.onthegomap.planetiler.geo.TileCoord;
@ -13,10 +14,14 @@ import com.onthegomap.planetiler.geo.TileExtents;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.locationtech.jts.algorithm.Orientation;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.CoordinateSequences;
import org.locationtech.jts.geom.util.AffineTransformation;
class TiledGeometryTest {
@ -161,6 +166,28 @@ class TiledGeometryTest {
}
}
private void flipAndRotate(CoordinateSequence coordinateSequence, double x, double y, boolean flipX, boolean flipY,
int degrees) {
if (flipX) {
var transformation = AffineTransformation.reflectionInstance(x, y, x, y + 1);
for (int i = 0; i < coordinateSequence.size(); i++) {
transformation.transform(coordinateSequence, i);
}
}
if (flipY) {
var transformation = AffineTransformation.reflectionInstance(x, y, x + 1, y);
for (int i = 0; i < coordinateSequence.size(); i++) {
transformation.transform(coordinateSequence, i);
}
}
rotate(coordinateSequence, x, y, degrees);
// maintain winding order if we did a single flip
// doing a second flip fixes winding order itself
if (flipX ^ flipY) {
CoordinateSequences.reverse(coordinateSequence);
}
}
@ParameterizedTest
@ValueSource(ints = {0, 90, 180, 270})
void testOnlyHoleTouchesOtherCellBottom(int degrees) {
@ -184,4 +211,180 @@ class TiledGeometryTest {
assertThrows(GeometryException.class,
() -> TiledGeometry.sliceIntoTiles(coordinateSequences, 0.1, true, 11, extent));
}
@ParameterizedTest
@CsvSource({
"0,false,false",
"90,false,false",
"180,false,false",
"270,false,false",
"0,true,false",
"0,false,true",
"0,true,true",
})
void testOverlappingHoles(int degrees, boolean flipX, boolean flipY) throws GeometryException {
MutableCoordinateSequence outer = new MutableCoordinateSequence();
outer.addPoint(1, 1);
outer.addPoint(10, 1);
outer.addPoint(10, 10);
outer.addPoint(1, 10);
outer.closeRing();
MutableCoordinateSequence inner1 = new MutableCoordinateSequence();
inner1.addPoint(2, 2);
inner1.addPoint(2, 9);
inner1.addPoint(9, 9);
inner1.addPoint(3, 5);
inner1.addPoint(9, 2);
inner1.closeRing();
MutableCoordinateSequence inner2 = new MutableCoordinateSequence();
inner2.addPoint(9, 3);
inner2.addPoint(9, 8);
inner2.addPoint(4, 5);
inner2.closeRing();
flipAndRotate(outer, 6, 6, flipX, flipY, degrees);
flipAndRotate(inner1, 6, 6, flipX, flipY, degrees);
flipAndRotate(inner2, 6, 6, flipX, flipY, degrees);
testRender(List.of(List.of(outer, inner1)));
testRender(List.of(List.of(outer, inner2)));
testRender(List.of(List.of(outer, inner1, inner2)));
var result = testRender(List.of(List.of(outer, inner2, inner1)));
if (degrees == 0 && !flipX && !flipY) {
assertFalse(result.getCoveredTiles().test(7, 4));
assertFalse(result.getCoveredTiles().test(3, 3));
assertTrue(result.getCoveredTiles().test(1, 1));
assertTrue(result.getCoveredTiles().test(9, 9));
}
}
@ParameterizedTest
@CsvSource({
"0,false,false",
"90,false,false",
"180,false,false",
"270,false,false",
"0,true,false",
"0,false,true",
"0,true,true",
})
void testInsideComplexHole(int degrees, boolean flipX, boolean flipY) throws GeometryException {
MutableCoordinateSequence outer = new MutableCoordinateSequence();
outer.addPoint(1, 1);
outer.addPoint(10, 1);
outer.addPoint(10, 10);
outer.addPoint(1, 10);
outer.closeRing();
MutableCoordinateSequence inner1 = new MutableCoordinateSequence();
inner1.addPoint(6.5, 1.5);
inner1.addPoint(2, 2);
inner1.addPoint(2, 9);
inner1.addPoint(9, 9);
inner1.addPoint(9, 2);
inner1.addPoint(4.6, 2);
inner1.addPoint(8, 8);
inner1.addPoint(3, 8);
inner1.addPoint(4, 2);
inner1.closeRing();
MutableCoordinateSequence inner2 = new MutableCoordinateSequence();
inner2.addPoint(5.5, 6.5);
inner2.addPoint(5.5, 6.6);
inner2.addPoint(5.6, 6.6);
inner2.closeRing();
flipAndRotate(outer, 6, 6, flipX, flipY, degrees);
flipAndRotate(inner1, 6, 6, flipX, flipY, degrees);
flipAndRotate(inner2, 6, 6, flipX, flipY, degrees);
assertTrue(Orientation.isCCW(outer));
assertFalse(Orientation.isCCW(inner1));
assertFalse(Orientation.isCCW(inner2));
testRender(List.of(List.of(outer, inner1)));
testRender(List.of(List.of(outer, inner2)));
testRender(List.of(List.of(outer, inner1, inner2)));
var result = testRender(List.of(List.of(outer, inner2, inner1)));
if (degrees == 0 && !flipX && !flipY) {
var filled = StreamSupport.stream(result.getFilledTiles().spliterator(), false).collect(Collectors.toSet());
assertTrue(filled.contains(TileCoord.ofXYZ(5, 5, 14)));
assertTrue(filled.contains(TileCoord.ofXYZ(4, 6, 14)));
assertFalse(filled.contains(TileCoord.ofXYZ(5, 6, 14)));
}
}
@ParameterizedTest
@CsvSource({
"0,false,false",
"90,false,false",
"180,false,false",
"270,false,false",
"0,true,false",
"0,false,true",
"0,true,true",
})
void testSideOfHoleIntercepted(int degrees, boolean flipX, boolean flipY) throws GeometryException {
MutableCoordinateSequence outer = new MutableCoordinateSequence();
outer.addPoint(1, 1);
outer.addPoint(10, 1);
outer.addPoint(10, 10);
outer.addPoint(1, 10);
outer.closeRing();
MutableCoordinateSequence inner1 = new MutableCoordinateSequence();
inner1.addPoint(2, 2);
inner1.addPoint(2, 9);
inner1.addPoint(9, 9);
inner1.addPoint(3, 5);
inner1.addPoint(9, 2);
inner1.addPoint(9, 4.2);
inner1.addPoint(7.5, 4.2);
inner1.addPoint(7.5, 4.8);
inner1.addPoint(9.5, 4.8);
inner1.addPoint(9.5, 1.8);
inner1.closeRing();
flipAndRotate(outer, 5, 5, flipX, flipY, degrees);
flipAndRotate(inner1, 5, 5, flipX, flipY, degrees);
var result = testRender(List.of(List.of(outer, inner1)));
if (degrees == 0 && !flipX && !flipY) {
var filled = StreamSupport.stream(result.getFilledTiles().spliterator(), false).collect(Collectors.toSet());
assertFalse(filled.contains(TileCoord.ofXYZ(7, 4, 14)), filled.toString());
var tileData = result.getTileData().get(TileCoord.ofXYZ(7, 4, 14));
var normalized = tileData.stream().map(items -> items.stream().map(coordinateSequence -> {
for (int i = 0; i < coordinateSequence.size(); i++) {
coordinateSequence.setOrdinate(i, 0, Math.round(coordinateSequence.getX(i) * 10) / 10d);
coordinateSequence.setOrdinate(i, 1, Math.round(coordinateSequence.getY(i) * 10) / 10d);
}
return List.of(coordinateSequence.toCoordinateArray());
}).toList()).toList();
assertEquals(
List.of(List.of(
List.of(GeoUtils.coordinateSequence(
0, 0,
256, 0,
256, 256,
0, 256,
0, 0
).toCoordinateArray()),
List.of(GeoUtils.coordinateSequence(
-0, 256,
-0, 0,
256, 0,
256, 51.2,
128, 51.2,
128, 204.8,
256, 204.8,
256, 0,
0, 0,
0, 256
).toCoordinateArray())
)),
normalized
);
}
}
private static TiledGeometry testRender(List<List<CoordinateSequence>> coordinateSequences) throws GeometryException {
return TiledGeometry.sliceIntoTiles(
coordinateSequences, 0, true, 14,
new TileExtents.ForZoom(14, -10, -10, 1 << 14, 1 << 14, null)
);
}
}

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -77,12 +77,18 @@ case $AREA in
esac
;;
monaco)
echo "$TASK"
echo "${PLANETILER_ARGS[*]}"
if [ "$TASK" == "openmaptiles" ]; then
# Use mini extracts for monaco
PLANETILER_ARGS+=("--water-polygons-url=https://github.com/onthegomap/planetiler/raw/main/planetiler-core/src/test/resources/water-polygons-split-3857.zip")
PLANETILER_ARGS+=("--water-polygons-path=data/sources/monaco-water.zip")
PLANETILER_ARGS+=("--natural-earth-url=https://github.com/onthegomap/planetiler/raw/main/planetiler-core/src/test/resources/natural_earth_vector.sqlite.zip")
PLANETILER_ARGS+=("--natural-earth-path=data/sources/monaco-natural_earth_vector.sqlite.zip")
if [[ "${PLANETILER_ARGS[*]}" =~ ^.*osm[-_](path|url).*$ ]]; then
: # don't add monaco args
else
# Use mini extracts for monaco
PLANETILER_ARGS+=("--water-polygons-url=https://github.com/onthegomap/planetiler/raw/main/planetiler-core/src/test/resources/water-polygons-split-3857.zip")
PLANETILER_ARGS+=("--water-polygons-path=data/sources/monaco-water.zip")
PLANETILER_ARGS+=("--natural-earth-url=https://github.com/onthegomap/planetiler/raw/main/planetiler-core/src/test/resources/natural_earth_vector.sqlite.zip")
PLANETILER_ARGS+=("--natural-earth-path=data/sources/monaco-natural_earth_vector.sqlite.zip")
fi
fi
;;
esac