2021-12-23 10:42:24 +00:00
|
|
|
package com.onthegomap.planetiler.geo;
|
2021-04-25 11:42:13 +00:00
|
|
|
|
2022-07-26 11:51:31 +00:00
|
|
|
import static com.onthegomap.planetiler.config.PlanetilerConfig.MAX_MAXZOOM;
|
|
|
|
|
2023-01-27 02:43:07 +00:00
|
|
|
import com.onthegomap.planetiler.util.Hilbert;
|
2023-09-22 01:44:09 +00:00
|
|
|
import java.text.DecimalFormat;
|
|
|
|
import java.text.DecimalFormatSymbols;
|
|
|
|
import java.util.Locale;
|
2021-09-10 00:46:20 +00:00
|
|
|
import javax.annotation.concurrent.Immutable;
|
2021-07-26 00:49:58 +00:00
|
|
|
import org.locationtech.jts.geom.Coordinate;
|
|
|
|
import org.locationtech.jts.geom.CoordinateXY;
|
2023-03-20 20:41:18 +00:00
|
|
|
import org.locationtech.jts.geom.Envelope;
|
2021-04-29 10:22:41 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* The coordinate of a <a href="https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames">slippy map tile</a>.
|
|
|
|
* <p>
|
2023-01-27 02:43:07 +00:00
|
|
|
* Tile coords are sorted by consecutive Z levels in ascending order: 0 coords for z=0, 4 coords for z=1, etc. The
|
|
|
|
* default is TMS order: a level is sorted by x ascending, y descending to match the ordering of the MBTiles sqlite
|
|
|
|
* index.
|
2021-09-10 00:46:20 +00:00
|
|
|
* <p>
|
|
|
|
*
|
|
|
|
* @param encoded the tile ID encoded as a 32-bit integer
|
|
|
|
* @param x x coordinate of the tile where 0 is the western-most tile just to the east the international date line
|
|
|
|
* and 2^z-1 is the eastern-most tile
|
|
|
|
* @param y y coordinate of the tile where 0 is the northern-most tile and 2^z-1 is the southern-most tile
|
2022-07-24 10:40:43 +00:00
|
|
|
* @param z zoom level ({@code <= 15})
|
2021-09-10 00:46:20 +00:00
|
|
|
*/
|
|
|
|
@Immutable
|
2021-04-29 10:22:41 +00:00
|
|
|
public record TileCoord(int encoded, int x, int y, int z) implements Comparable<TileCoord> {
|
2022-07-26 11:51:31 +00:00
|
|
|
|
|
|
|
private static final int[] ZOOM_START_INDEX = new int[MAX_MAXZOOM + 1];
|
|
|
|
|
|
|
|
static {
|
|
|
|
int idx = 0;
|
|
|
|
for (int z = 0; z <= MAX_MAXZOOM; z++) {
|
|
|
|
ZOOM_START_INDEX[z] = idx;
|
|
|
|
int count = (1 << z) * (1 << z);
|
|
|
|
if (Integer.MAX_VALUE - idx < count) {
|
|
|
|
throw new IllegalStateException("Too many zoom levels " + MAX_MAXZOOM);
|
|
|
|
}
|
|
|
|
idx += count;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-23 18:18:10 +00:00
|
|
|
public static int startIndexForZoom(int z) {
|
2022-07-26 11:51:31 +00:00
|
|
|
return ZOOM_START_INDEX[z];
|
|
|
|
}
|
|
|
|
|
2024-01-23 18:18:10 +00:00
|
|
|
public static int endIndexForZoom(int z) {
|
|
|
|
return ZOOM_START_INDEX[z] + (1 << z) * (1 << z) - 1;
|
|
|
|
}
|
|
|
|
|
2022-07-26 11:51:31 +00:00
|
|
|
private static int zoomForIndex(int idx) {
|
|
|
|
for (int z = MAX_MAXZOOM; z >= 0; z--) {
|
|
|
|
if (ZOOM_START_INDEX[z] <= idx) {
|
|
|
|
return z;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw new IllegalArgumentException("Bad index: " + idx);
|
|
|
|
}
|
|
|
|
|
2021-04-28 09:45:33 +00:00
|
|
|
public TileCoord {
|
2022-07-26 11:51:31 +00:00
|
|
|
assert z <= MAX_MAXZOOM;
|
2021-04-25 11:42:13 +00:00
|
|
|
}
|
|
|
|
|
2021-04-25 20:29:47 +00:00
|
|
|
public static TileCoord ofXYZ(int x, int y, int z) {
|
2021-04-25 11:42:13 +00:00
|
|
|
return new TileCoord(encode(x, y, z), x, y, z);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static TileCoord decode(int encoded) {
|
2022-07-26 11:51:31 +00:00
|
|
|
int z = zoomForIndex(encoded);
|
|
|
|
long xy = tmsPositionToXY(z, encoded - startIndexForZoom(z));
|
|
|
|
return new TileCoord(encoded, (int) (xy >>> 32 & 0xFFFFFFFFL), (int) (xy & 0xFFFFFFFFL), z);
|
2021-04-25 11:42:13 +00:00
|
|
|
}
|
|
|
|
|
2023-01-27 02:43:07 +00:00
|
|
|
/** Decode an integer using Hilbert ordering on a zoom level back to TMS ordering. */
|
|
|
|
public static TileCoord hilbertDecode(int encoded) {
|
|
|
|
int z = TileCoord.zoomForIndex(encoded);
|
|
|
|
long xy = Hilbert.hilbertPositionToXY(z, encoded - TileCoord.startIndexForZoom(z));
|
|
|
|
return TileCoord.ofXYZ(Hilbert.extractX(xy), Hilbert.extractY(xy), z);
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/** Returns the tile containing a latitude/longitude coordinate at a given zoom level. */
|
2021-08-17 01:51:49 +00:00
|
|
|
public static TileCoord aroundLngLat(double lng, double lat, int zoom) {
|
|
|
|
double factor = 1 << zoom;
|
|
|
|
double x = GeoUtils.getWorldX(lng) * factor;
|
|
|
|
double y = GeoUtils.getWorldY(lat) * factor;
|
|
|
|
return TileCoord.ofXYZ((int) Math.floor(x), (int) Math.floor(y), zoom);
|
|
|
|
}
|
|
|
|
|
2022-07-24 10:40:43 +00:00
|
|
|
public static int encode(int x, int y, int z) {
|
2022-07-26 11:51:31 +00:00
|
|
|
return startIndexForZoom(z) + tmsXYToPosition(z, x, y);
|
2021-04-25 11:42:13 +00:00
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
@Override
|
|
|
|
public boolean equals(Object o) {
|
|
|
|
if (this == o) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (o == null || getClass() != o.getClass()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
TileCoord tileCoord = (TileCoord) o;
|
|
|
|
|
|
|
|
return encoded == tileCoord.encoded;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int hashCode() {
|
|
|
|
return encoded;
|
|
|
|
}
|
|
|
|
|
2021-04-25 11:42:13 +00:00
|
|
|
@Override
|
|
|
|
public String toString() {
|
2021-05-16 10:42:57 +00:00
|
|
|
return "{x=" + x + " y=" + y + " z=" + z + '}';
|
2021-04-25 11:42:13 +00:00
|
|
|
}
|
2021-04-29 10:22:41 +00:00
|
|
|
|
2022-07-26 11:51:31 +00:00
|
|
|
public double progressOnLevel(TileExtents extents) {
|
|
|
|
// approximate percent complete within a bounding box by computing what % of the way through the columns we are
|
|
|
|
var zoomBounds = extents.getForZoom(z);
|
|
|
|
return 1d * (x - zoomBounds.minX()) / (zoomBounds.maxX() - zoomBounds.minX());
|
2022-07-24 10:40:43 +00:00
|
|
|
}
|
|
|
|
|
2023-01-27 02:43:07 +00:00
|
|
|
public double hilbertProgressOnLevel(TileExtents extents) {
|
|
|
|
return 1d * Hilbert.hilbertXYToIndex(this.z, this.x, this.y) / (1 << 2 * this.z);
|
|
|
|
}
|
|
|
|
|
2021-04-29 10:22:41 +00:00
|
|
|
@Override
|
2021-08-22 09:37:57 +00:00
|
|
|
public int compareTo(TileCoord o) {
|
2021-04-29 10:22:41 +00:00
|
|
|
return Long.compare(encoded, o.encoded);
|
|
|
|
}
|
2021-07-26 00:49:58 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/** Returns the latitude/longitude of the northwest corner of this tile. */
|
2023-09-22 01:44:09 +00:00
|
|
|
public Envelope getEnvelope() {
|
2021-07-26 00:49:58 +00:00
|
|
|
double worldWidthAtZoom = Math.pow(2, z);
|
2023-09-22 01:44:09 +00:00
|
|
|
return new Envelope(
|
2021-07-26 00:49:58 +00:00
|
|
|
GeoUtils.getWorldLon(x / worldWidthAtZoom),
|
2023-09-22 01:44:09 +00:00
|
|
|
GeoUtils.getWorldLon((x + 1) / worldWidthAtZoom),
|
|
|
|
GeoUtils.getWorldLat((y + 1) / worldWidthAtZoom),
|
2021-07-26 00:49:58 +00:00
|
|
|
GeoUtils.getWorldLat(y / worldWidthAtZoom)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-07-22 10:48:04 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/** Returns a URL that displays the openstreetmap data for this tile. */
|
2023-09-22 01:44:09 +00:00
|
|
|
public String getDebugUrl(String pattern) {
|
|
|
|
Coordinate center = getEnvelope().centre();
|
|
|
|
DecimalFormat format = new DecimalFormat("0.#####", DecimalFormatSymbols.getInstance(Locale.US));
|
|
|
|
return pattern
|
|
|
|
.replaceAll("\\{(lat|latitude)}", format.format(center.y))
|
|
|
|
.replaceAll("\\{(lon|longitude)}", format.format(center.x))
|
|
|
|
.replaceAll("\\{(z|zoom)}", z + ".5");
|
2021-07-26 00:49:58 +00:00
|
|
|
}
|
2021-08-17 01:51:49 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/** Returns the pixel coordinate on this tile of a given latitude/longitude (assuming 256x256 px tiles). */
|
2021-08-17 01:51:49 +00:00
|
|
|
public Coordinate lngLatToTileCoords(double lng, double lat) {
|
|
|
|
double factor = 1 << z;
|
|
|
|
double x = GeoUtils.getWorldX(lng) * factor;
|
|
|
|
double y = GeoUtils.getWorldY(lat) * factor;
|
|
|
|
return new CoordinateXY((x - Math.floor(x)) * 256, (y - Math.floor(y)) * 256);
|
|
|
|
}
|
2022-07-24 10:40:43 +00:00
|
|
|
|
2023-01-27 02:43:07 +00:00
|
|
|
/** Return the equivalent tile index using Hilbert ordering on a single level instead of TMS. */
|
|
|
|
public int hilbertEncoded() {
|
|
|
|
return startIndexForZoom(this.z) +
|
|
|
|
Hilbert.hilbertXYToIndex(this.z, this.x, this.y);
|
|
|
|
}
|
|
|
|
|
2022-07-24 10:40:43 +00:00
|
|
|
public static long tmsPositionToXY(int z, int pos) {
|
|
|
|
if (z == 0)
|
|
|
|
return 0;
|
|
|
|
int dim = 1 << z;
|
|
|
|
int x = pos / dim;
|
|
|
|
int y = dim - 1 - (pos % dim);
|
|
|
|
return ((long) x << 32) | y;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static int tmsXYToPosition(int z, int x, int y) {
|
|
|
|
int dim = 1 << z;
|
|
|
|
return x * dim + (dim - 1 - y);
|
|
|
|
}
|
2023-02-24 18:14:50 +00:00
|
|
|
|
|
|
|
public TileCoord parent() {
|
|
|
|
return ofXYZ(x / 2, y / 2, z - 1);
|
|
|
|
}
|
2023-03-20 20:41:18 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
);
|
|
|
|
}
|
2021-04-25 11:42:13 +00:00
|
|
|
}
|