kopia lustrzana https://github.com/onthegomap/planetiler
466 wiersze
16 KiB
Java
466 wiersze
16 KiB
Java
/*
|
|
* ISC License
|
|
* <p>
|
|
* Copyright (c) 2015, Mapbox
|
|
* <p>
|
|
* Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby
|
|
* granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
* <p>
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
|
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
* PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
package com.onthegomap.flatmap;
|
|
|
|
import com.carrotsearch.hppc.IntObjectMap;
|
|
import com.carrotsearch.hppc.cursors.IntCursor;
|
|
import com.carrotsearch.hppc.cursors.IntObjectCursor;
|
|
import com.graphhopper.coll.GHIntObjectHashMap;
|
|
import com.onthegomap.flatmap.collections.IntRange;
|
|
import com.onthegomap.flatmap.collections.MutableCoordinateSequence;
|
|
import com.onthegomap.flatmap.geo.TileCoord;
|
|
import java.util.ArrayList;
|
|
import java.util.EnumSet;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.SortedMap;
|
|
import java.util.TreeMap;
|
|
import java.util.TreeSet;
|
|
import org.jetbrains.annotations.NotNull;
|
|
import org.locationtech.jts.geom.CoordinateSequence;
|
|
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
/**
|
|
* This class is adapted from the stripe clipping algorithm in https://github.com/mapbox/geojson-vt/ and modified so
|
|
* that it eagerly produces all sliced tiles at a zoom level for each input geometry.
|
|
*/
|
|
public class TiledGeometry {
|
|
|
|
private static final Logger LOGGER = LoggerFactory.getLogger(TiledGeometry.class);
|
|
|
|
private final Map<TileCoord, List<List<CoordinateSequence>>> tileContents = new HashMap<>();
|
|
private final SortedMap<Column, IntRange> filledRanges = new TreeMap<>();
|
|
private final TileExtents.ForZoom extents;
|
|
private final double buffer;
|
|
private final int z;
|
|
private final boolean area;
|
|
private final int max;
|
|
|
|
private TiledGeometry(TileExtents.ForZoom extents, double buffer, int z, boolean area) {
|
|
this.extents = extents;
|
|
this.buffer = buffer;
|
|
this.z = z;
|
|
this.area = area;
|
|
this.max = 1 << z;
|
|
}
|
|
|
|
public static TiledGeometry sliceIntoTiles(List<List<CoordinateSequence>> groups, double buffer, boolean area, int z,
|
|
TileExtents.ForZoom extents) {
|
|
int worldExtent = 1 << z;
|
|
TiledGeometry result = new TiledGeometry(extents, buffer, z, area);
|
|
EnumSet<Direction> wrapResult = result.sliceWorldCopy(groups, 0);
|
|
if (wrapResult.contains(Direction.RIGHT)) {
|
|
result.sliceWorldCopy(groups, -worldExtent);
|
|
}
|
|
if (wrapResult.contains(Direction.LEFT)) {
|
|
result.sliceWorldCopy(groups, worldExtent);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public Iterable<TileCoord> getFilledTilesOrderedByZXY() {
|
|
return () -> filledRanges.entrySet().stream()
|
|
.<TileCoord>mapMulti((entry, next) -> {
|
|
Column column = entry.getKey();
|
|
int x = column.x, z = column.z;
|
|
for (int y : entry.getValue()) {
|
|
TileCoord coord = TileCoord.ofXYZ(x, y, z);
|
|
if (!tileContents.containsKey(coord)) {
|
|
next.accept(coord);
|
|
}
|
|
}
|
|
}).iterator();
|
|
}
|
|
|
|
public Iterable<Map.Entry<TileCoord, List<List<CoordinateSequence>>>> getTileData() {
|
|
return tileContents.entrySet();
|
|
}
|
|
|
|
private static int wrapX(int x, int max) {
|
|
x %= max;
|
|
if (x < 0) {
|
|
x += max;
|
|
}
|
|
return x;
|
|
}
|
|
|
|
private static void intersectX(MutableCoordinateSequence out, double ax, double ay, double bx, double by, double x) {
|
|
double t = (x - ax) / (bx - ax);
|
|
out.addPoint(x, ay + (by - ay) * t);
|
|
}
|
|
|
|
private static void intersectY(MutableCoordinateSequence out, double ax, double ay, double bx, double by, double y) {
|
|
double t = (y - ay) / (by - ay);
|
|
out.addPoint(ax + (bx - ax) * t, y);
|
|
}
|
|
|
|
private static CoordinateSequence fill(double buffer) {
|
|
buffer += 1d / 4096;
|
|
double min = -256d * buffer;
|
|
double max = 256d - min;
|
|
return new PackedCoordinateSequence.Double(new double[]{
|
|
min, min,
|
|
max, min,
|
|
max, max,
|
|
min, max,
|
|
min, min
|
|
}, 2, 0);
|
|
}
|
|
|
|
private EnumSet<Direction> sliceWorldCopy(List<List<CoordinateSequence>> groups, int xOffset) {
|
|
EnumSet<Direction> overflow = EnumSet.noneOf(Direction.class);
|
|
for (List<CoordinateSequence> group : groups) {
|
|
Map<TileCoord, List<CoordinateSequence>> inProgressShapes = new HashMap<>();
|
|
for (int i = 0; i < group.size(); i++) {
|
|
CoordinateSequence segment = group.get(i);
|
|
boolean outer = i == 0;
|
|
IntObjectMap<List<MutableCoordinateSequence>> xSlices = sliceX(segment);
|
|
if (z >= 6 && xSlices.size() >= Math.pow(2, z) - 1) {
|
|
LOGGER.warn("Feature crosses world at z" + z + ": " + xSlices.size());
|
|
}
|
|
for (IntObjectCursor<List<MutableCoordinateSequence>> xCursor : xSlices) {
|
|
int x = xCursor.key + xOffset;
|
|
if (x >= max) {
|
|
overflow.add(Direction.RIGHT);
|
|
} else if (x < 0) {
|
|
overflow.add(Direction.LEFT);
|
|
} else {
|
|
for (CoordinateSequence stripeSegment : xCursor.value) {
|
|
IntRange filled = sliceY(stripeSegment, x, outer, inProgressShapes);
|
|
if (area && filled != null) {
|
|
if (outer) {
|
|
addFilledRange(z, x, filled);
|
|
} else {
|
|
removeFilledRange(z, x, filled);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
addShapeToResults(inProgressShapes);
|
|
}
|
|
|
|
return overflow;
|
|
}
|
|
|
|
private void addShapeToResults(Map<TileCoord, List<CoordinateSequence>> inProgressShapes) {
|
|
for (var entry : inProgressShapes.entrySet()) {
|
|
TileCoord tileID = entry.getKey();
|
|
List<CoordinateSequence> inSeqs = entry.getValue();
|
|
if (area && inSeqs.get(0).size() < 4) {
|
|
// not enough points in outer polygon, ignore
|
|
continue;
|
|
}
|
|
int minPoints = area ? 4 : 2;
|
|
List<CoordinateSequence> outSeqs = inSeqs.stream()
|
|
.filter(seq -> seq.size() >= minPoints)
|
|
.toList();
|
|
if (!outSeqs.isEmpty()) {
|
|
tileContents.computeIfAbsent(tileID, tile -> new ArrayList<>()).add(outSeqs);
|
|
}
|
|
}
|
|
}
|
|
|
|
private IntObjectMap<List<MutableCoordinateSequence>> sliceX(CoordinateSequence segment) {
|
|
int maxIndex = 1 << z;
|
|
double k1 = -buffer;
|
|
double k2 = 1 + buffer;
|
|
IntObjectMap<List<MutableCoordinateSequence>> newGeoms = new GHIntObjectHashMap<>();
|
|
IntObjectMap<MutableCoordinateSequence> xSlices = new GHIntObjectHashMap<>();
|
|
int end = segment.size() - 1;
|
|
for (int i = 0; i < end; i++) {
|
|
double _ax = segment.getX(i);
|
|
double ay = segment.getY(i);
|
|
double _bx = segment.getX(i + 1);
|
|
double by = segment.getY(i + 1);
|
|
|
|
double minX = Math.min(_ax, _bx);
|
|
double maxX = Math.max(_ax, _bx);
|
|
|
|
int startX = (int) Math.floor(minX - buffer);
|
|
int endX = (int) Math.floor(maxX + buffer);
|
|
|
|
for (int x = startX; x <= endX; x++) {
|
|
double ax = _ax - x;
|
|
double bx = _bx - x;
|
|
MutableCoordinateSequence slice = xSlices.get(x);
|
|
if (slice == null) {
|
|
xSlices.put(x, slice = new MutableCoordinateSequence());
|
|
List<MutableCoordinateSequence> newGeom = newGeoms.get(x);
|
|
if (newGeom == null) {
|
|
newGeoms.put(x, newGeom = new ArrayList<>());
|
|
}
|
|
newGeom.add(slice);
|
|
}
|
|
|
|
boolean exited = false;
|
|
|
|
if (ax < k1) {
|
|
// ---|--> | (line enters the clip region from the left)
|
|
if (bx > k1) {
|
|
intersectX(slice, ax, ay, bx, by, k1);
|
|
}
|
|
} else if (ax > k2) {
|
|
// | <--|--- (line enters the clip region from the right)
|
|
if (bx < k2) {
|
|
intersectX(slice, ax, ay, bx, by, k2);
|
|
}
|
|
} else {
|
|
slice.addPoint(ax, ay);
|
|
}
|
|
if (bx < k1 && ax >= k1) {
|
|
// <--|--- | or <--|-----|--- (line exits the clip region on the left)
|
|
intersectX(slice, ax, ay, bx, by, k1);
|
|
exited = true;
|
|
}
|
|
if (bx > k2 && ax <= k2) {
|
|
// | ---|--> or ---|-----|--> (line exits the clip region on the right)
|
|
intersectX(slice, ax, ay, bx, by, k2);
|
|
exited = true;
|
|
}
|
|
|
|
if (!area && exited) {
|
|
xSlices.remove(x);
|
|
}
|
|
}
|
|
}
|
|
// add the last point
|
|
double _ax = segment.getX(segment.size() - 1);
|
|
double ay = segment.getY(segment.size() - 1);
|
|
int startX = (int) Math.floor(_ax - buffer);
|
|
int endX = (int) Math.floor(_ax + buffer);
|
|
|
|
for (int x = startX - 1; x <= endX + 1; x++) {
|
|
double ax = _ax - x;
|
|
MutableCoordinateSequence slice = xSlices.get(x);
|
|
if (slice != null && ax >= k1 && ax <= k2) {
|
|
slice.addPoint(ax, ay);
|
|
}
|
|
}
|
|
|
|
// close the polygons if endpoints are not the same after clipping
|
|
if (area) {
|
|
for (IntObjectCursor<MutableCoordinateSequence> cursor : xSlices) {
|
|
cursor.value.closeRing();
|
|
}
|
|
}
|
|
newGeoms.removeAll((x, value) -> {
|
|
int wrapped = wrapX(x, maxIndex);
|
|
return !extents.testX(wrapped);
|
|
});
|
|
return newGeoms;
|
|
}
|
|
|
|
private IntRange sliceY(CoordinateSequence stripeSegment, int x, boolean outer,
|
|
Map<TileCoord, List<CoordinateSequence>> inProgressShapes) {
|
|
double leftEdge = -buffer;
|
|
double rightEdge = 1 + buffer;
|
|
|
|
TreeSet<Integer> tiles = null;
|
|
IntRange rightFilled = null;
|
|
IntRange leftFilled = null;
|
|
|
|
IntObjectMap<MutableCoordinateSequence> ySlices = new GHIntObjectHashMap<>();
|
|
int max = 1 << z;
|
|
if (x < 0 || x >= max) {
|
|
return null;
|
|
}
|
|
record SkippedSegment(Direction side, int lo, int hi) {
|
|
|
|
}
|
|
List<SkippedSegment> skipped = null;
|
|
for (int i = 0; i < stripeSegment.size() - 1; i++) {
|
|
double ax = stripeSegment.getX(i);
|
|
double ay = stripeSegment.getY(i);
|
|
double bx = stripeSegment.getX(i + 1);
|
|
double by = stripeSegment.getY(i + 1);
|
|
|
|
double minY = Math.min(ay, by);
|
|
double maxY = Math.max(ay, by);
|
|
|
|
int extentMinY = extents.minY();
|
|
int extentMaxY = extents.maxY();
|
|
int startY = Math.max(extentMinY, (int) Math.floor(minY - buffer));
|
|
int endStartY = Math.max(extentMinY, (int) Math.floor(minY + buffer));
|
|
int startEndY = Math.min(extentMaxY - 1, (int) Math.floor(maxY - buffer));
|
|
int endY = Math.min(extentMaxY - 1, (int) Math.floor(maxY + buffer));
|
|
|
|
boolean onRightEdge = area && ax == bx && ax == rightEdge && by > ay;
|
|
boolean onLeftEdge = area && ax == bx && ax == leftEdge && by < ay;
|
|
|
|
for (int y = startY; y <= endY; y++) {
|
|
if (area && y > endStartY && y < startEndY) {
|
|
if (onRightEdge || onLeftEdge) {
|
|
// skip over filled tile
|
|
if (tiles == null) {
|
|
tiles = new TreeSet<>();
|
|
for (IntCursor cursor : ySlices.keys()) {
|
|
tiles.add(cursor.value);
|
|
}
|
|
}
|
|
Integer next = tiles.ceiling(y);
|
|
int nextNonEdgeTile = next == null ? startEndY : Math.min(next, startEndY);
|
|
int endSkip = nextNonEdgeTile - 1;
|
|
if (skipped == null) {
|
|
skipped = new ArrayList<>();
|
|
}
|
|
skipped.add(new SkippedSegment(
|
|
onLeftEdge ? Direction.LEFT : Direction.RIGHT,
|
|
y,
|
|
endSkip
|
|
));
|
|
|
|
if (rightFilled == null) {
|
|
rightFilled = new IntRange();
|
|
leftFilled = new IntRange();
|
|
}
|
|
(onRightEdge ? rightFilled : leftFilled).add(y, endSkip);
|
|
|
|
y = nextNonEdgeTile;
|
|
}
|
|
}
|
|
double k1 = y - buffer;
|
|
double k2 = y + 1 + buffer;
|
|
MutableCoordinateSequence slice = ySlices.get(y);
|
|
if (slice == null) {
|
|
if (tiles != null) {
|
|
tiles.add(y);
|
|
}
|
|
// x is already relative to tile
|
|
ySlices.put(y, slice = MutableCoordinateSequence.newScalingSequence(0, y, 256));
|
|
TileCoord tileID = TileCoord.ofXYZ(x, y, z);
|
|
List<CoordinateSequence> toAddTo = inProgressShapes.computeIfAbsent(tileID, tile -> new ArrayList<>());
|
|
|
|
// if this is tile is inside a fill from an outer tile, infer that fill here
|
|
if (area && !outer && toAddTo.isEmpty()) {
|
|
toAddTo.add(fill(buffer));
|
|
}
|
|
toAddTo.add(slice);
|
|
|
|
// if this tile was skipped because we skipped an edge and now it needs more points,
|
|
// backfill all of the edges that we skipped for it
|
|
if (area && leftFilled != null && skipped != null && (leftFilled.contains(y) || rightFilled.contains(y))) {
|
|
for (SkippedSegment skippedSegment : skipped) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
boolean exited = false;
|
|
|
|
if (ay < k1) {
|
|
// ---|--> | (line enters the clip region from the top)
|
|
if (by > k1) {
|
|
intersectY(slice, ax, ay, bx, by, k1);
|
|
}
|
|
} else if (ay > k2) {
|
|
// | <--|--- (line enters the clip region from the bottom)
|
|
if (by < k2) {
|
|
intersectY(slice, ax, ay, bx, by, k2);
|
|
}
|
|
} else {
|
|
slice.addPoint(ax, ay);
|
|
}
|
|
if (by < k1 && ay >= k1) {
|
|
// <--|--- | or <--|-----|--- (line exits the clip region on the top)
|
|
intersectY(slice, ax, ay, bx, by, k1);
|
|
exited = true;
|
|
}
|
|
if (by > k2 && ay <= k2) {
|
|
// | ---|--> or ---|-----|--> (line exits the clip region on the bottom)
|
|
intersectY(slice, ax, ay, bx, by, k2);
|
|
exited = true;
|
|
}
|
|
|
|
if (!area && exited) {
|
|
ySlices.remove(y);
|
|
}
|
|
}
|
|
}
|
|
|
|
// add the last point
|
|
int last = stripeSegment.size() - 1;
|
|
double ax = stripeSegment.getX(last);
|
|
double ay = stripeSegment.getY(last);
|
|
int startY = (int) (ay - buffer);
|
|
int endY = (int) (ay + buffer);
|
|
|
|
for (int y = startY - 1; y <= endY + 1; y++) {
|
|
MutableCoordinateSequence slice = ySlices.get(y);
|
|
double k1 = y - buffer;
|
|
double k2 = y + 1 + buffer;
|
|
if (ay >= k1 && ay <= k2 && slice != null) {
|
|
slice.addPoint(ax, ay);
|
|
}
|
|
}
|
|
// close the polygons if endpoints are not the same after clipping
|
|
if (area) {
|
|
for (IntObjectCursor<MutableCoordinateSequence> cursor : ySlices) {
|
|
cursor.value.closeRing();
|
|
}
|
|
}
|
|
return rightFilled != null ? rightFilled.intersect(leftFilled) : null;
|
|
}
|
|
|
|
private void addFilledRange(int z, int x, IntRange range) {
|
|
if (range == null) {
|
|
return;
|
|
}
|
|
Column key = new Column(z, x);
|
|
IntRange existing = filledRanges.get(key);
|
|
if (existing == null) {
|
|
filledRanges.put(key, range);
|
|
} else {
|
|
existing.addAll(range);
|
|
}
|
|
}
|
|
|
|
private void removeFilledRange(int z, int x, IntRange range) {
|
|
if (range == null) {
|
|
return;
|
|
}
|
|
Column key = new Column(z, x);
|
|
IntRange existing = filledRanges.get(key);
|
|
if (existing != null) {
|
|
existing.removeAll(range);
|
|
}
|
|
}
|
|
|
|
private enum Direction {RIGHT, LEFT}
|
|
|
|
private static record Column(int z, int x) implements Comparable<Column> {
|
|
|
|
@Override
|
|
public int compareTo(@NotNull Column o) {
|
|
int result = Integer.compare(z, o.z);
|
|
return result == 0 ? Integer.compare(x, o.x) : result;
|
|
}
|
|
}
|
|
}
|