kopia lustrzana https://github.com/onthegomap/planetiler
Fix geometry errors (#526)
rodzic
65f620d663
commit
509795401e
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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 diff jest za duży
Load Diff
Plik binarny nie jest wyświetlany.
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue