2021-04-25 11:42:13 +00:00
|
|
|
package com.onthegomap.flatmap.geo;
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
import com.onthegomap.flatmap.mbiles.Mbtiles;
|
2021-07-26 00:49:58 +00:00
|
|
|
import java.text.NumberFormat;
|
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;
|
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>
|
|
|
|
* In order to encode into a 32-bit integer, only zoom levels <= 14 are supported since we need 4 bits for the
|
|
|
|
* zoom-level, and 14 bits each for the x/y coordinates.
|
|
|
|
* <p>
|
|
|
|
* Tiles are ordered by z ascending, x ascending, y descending to match index ordering of {@link Mbtiles} sqlite
|
|
|
|
* database.
|
|
|
|
*
|
|
|
|
* @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
|
|
|
|
* @param z zoom level (<= 14)
|
|
|
|
*/
|
|
|
|
@Immutable
|
2021-04-29 10:22:41 +00:00
|
|
|
public record TileCoord(int encoded, int x, int y, int z) implements Comparable<TileCoord> {
|
2021-09-10 00:46:20 +00:00
|
|
|
// TODO: support higher than z14
|
|
|
|
// z15 could theoretically fit into a 32-bit integer but needs a different packing strategy
|
|
|
|
// z16+ would need more space
|
|
|
|
// also need to remove hardcoded z14 limits
|
|
|
|
|
|
|
|
private static final int XY_MASK = (1 << 14) - 1;
|
|
|
|
private static final NumberFormat format = NumberFormat.getNumberInstance();
|
|
|
|
|
|
|
|
static {
|
|
|
|
format.setMaximumFractionDigits(5);
|
|
|
|
}
|
2021-04-25 11:42:13 +00:00
|
|
|
|
2021-04-28 09:45:33 +00:00
|
|
|
public TileCoord {
|
2021-04-25 20:29:47 +00:00
|
|
|
assert z <= 14;
|
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) {
|
2021-07-26 00:49:58 +00:00
|
|
|
int z = (encoded >> 28) + 8;
|
|
|
|
int x = (encoded >> 14) & XY_MASK;
|
|
|
|
int y = ((1 << z) - 1) - ((encoded) & XY_MASK);
|
|
|
|
return new TileCoord(encoded, x, y, z);
|
2021-04-25 11:42:13 +00:00
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-04-25 11:42:13 +00:00
|
|
|
private static int encode(int x, int y, int z) {
|
|
|
|
int max = 1 << z;
|
|
|
|
if (x >= max) {
|
|
|
|
x %= max;
|
|
|
|
}
|
|
|
|
if (x < 0) {
|
|
|
|
x += max;
|
|
|
|
}
|
|
|
|
if (y < 0) {
|
|
|
|
y = 0;
|
|
|
|
}
|
|
|
|
if (y >= max) {
|
2021-07-26 00:49:58 +00:00
|
|
|
y = max - 1;
|
2021-04-25 11:42:13 +00:00
|
|
|
}
|
2021-07-17 19:50:00 +00:00
|
|
|
// since most significant bit is treated as the sign bit, make:
|
|
|
|
// z0-7 get encoded from 8 (0b1000) to 15 (0b1111)
|
|
|
|
// z8-14 get encoded from 0 (0b0000) to 6 (0b0110)
|
|
|
|
// so that encoded tile coordinates are ordered by zoom level
|
|
|
|
if (z < 8) {
|
|
|
|
z += 8;
|
|
|
|
} else {
|
|
|
|
z -= 8;
|
|
|
|
}
|
2021-07-26 00:49:58 +00:00
|
|
|
y = max - 1 - y;
|
2021-04-25 11:42:13 +00:00
|
|
|
return (z << 28) | (x << 14) | y;
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
|
|
@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. */
|
2021-07-26 00:49:58 +00:00
|
|
|
public Coordinate getLatLon() {
|
|
|
|
double worldWidthAtZoom = Math.pow(2, z);
|
|
|
|
return new CoordinateXY(
|
|
|
|
GeoUtils.getWorldLon(x / worldWidthAtZoom),
|
|
|
|
GeoUtils.getWorldLat(y / worldWidthAtZoom)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/** Returns a URL that displays the openstreetmap data for this tile. */
|
2021-07-26 00:49:58 +00:00
|
|
|
public String getDebugUrl() {
|
|
|
|
Coordinate coord = getLatLon();
|
|
|
|
return "https://www.openstreetmap.org/#map=" + z + "/" + format.format(coord.y) + "/" + format.format(coord.x);
|
|
|
|
}
|
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);
|
|
|
|
}
|
2021-04-25 11:42:13 +00:00
|
|
|
}
|