Skip to content

Commit eff911c

Browse files
committed
Extract TileXYZ and add some basic tests
Signed-off-by: Taylor Smock <smocktaylor@gmail.com>
1 parent d13d61f commit eff911c

3 files changed

Lines changed: 167 additions & 110 deletions

File tree

src/main/java/org/openstreetmap/josm/plugins/mapwithai/backend/BoundingBoxMapWithAIDownloader.java

Lines changed: 0 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
import java.util.concurrent.ExecutionException;
2727
import java.util.concurrent.TimeUnit;
2828
import java.util.concurrent.TimeoutException;
29-
import java.util.stream.IntStream;
30-
import java.util.stream.Stream;
3129

3230
import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
3331
import org.openstreetmap.josm.data.Bounds;
@@ -86,114 +84,6 @@
8684
* @author Taylor Smock
8785
*/
8886
public class BoundingBoxMapWithAIDownloader extends BoundingBoxDownloader {
89-
private record TileXYZ(int x, int y, int z) {
90-
/**
91-
* Checks to see if the given bounds are functionally equal to this tile
92-
*
93-
* @param left left
94-
* @param bottom bottom
95-
* @param right right
96-
* @param top top
97-
*/
98-
boolean checkBounds(double left, double bottom, double right, double top) {
99-
final var thisLeft = xToLongitude(this.x, this.z);
100-
final var thisRight = xToLongitude(this.x + 1, this.z);
101-
final var thisBottom = yToLatitude(this.y + 1, this.z);
102-
final var thisTop = yToLatitude(this.y, this.z);
103-
return equalsEpsilon(thisLeft, left, this.z) && equalsEpsilon(thisRight, right, this.z)
104-
&& equalsEpsilon(thisBottom, bottom, this.z) && equalsEpsilon(thisTop, top, this.z);
105-
}
106-
107-
private static boolean equalsEpsilon(double first, double second, int z) {
108-
// 0.1% of tile size is considered to be "equal"
109-
final var maxDiff = (360 / Math.pow(2, z)) / 1000;
110-
final var diff = Math.abs(first - second);
111-
return diff <= maxDiff;
112-
}
113-
114-
private static double xToLongitude(int x, int z) {
115-
return (x / Math.pow(2, z)) * 360 - 180;
116-
}
117-
118-
private static double yToLatitude(int y, int z) {
119-
var t = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
120-
return 180 / Math.PI * Math.atan((Math.exp(t) - Math.exp(-t)) / 2);
121-
}
122-
123-
/**
124-
* Convert bounds to tiles
125-
*
126-
* @param zoom The zoom level to use
127-
* @param bounds The bounds to convert to tiles
128-
* @return A stream of tiles for the bounds at the given zoom level
129-
*/
130-
private static Stream<TileXYZ> tilesFromBBox(int zoom, Bounds bounds) {
131-
final var left = bounds.getMinLon();
132-
final var bottom = bounds.getMinLat();
133-
final var right = bounds.getMaxLon();
134-
final var top = bounds.getMaxLat();
135-
final var tile1 = tileFromLatLonZoom(left, bottom, zoom);
136-
final var tile2 = tileFromLatLonZoom(right, top, zoom);
137-
return IntStream.rangeClosed(tile1.x, tile2.x)
138-
.mapToObj(x -> IntStream.rangeClosed(tile2.y, tile1.y).mapToObj(y -> new TileXYZ(x, y, zoom)))
139-
.flatMap(stream -> stream);
140-
}
141-
142-
/**
143-
* Checks to see if the given bounds are functionally equal to this tile
144-
*
145-
* @param left left lon
146-
* @param bottom bottom lat
147-
* @param right right lon
148-
* @param top top lat
149-
*/
150-
private static TileXYZ tileFromBBox(double left, double bottom, double right, double top) {
151-
var zoom = 18;
152-
while (zoom > 0) {
153-
final var tile1 = tileFromLatLonZoom(left, bottom, zoom);
154-
final var tile2 = tileFromLatLonZoom(right, top, zoom);
155-
if (tile1.equals(tile2)) {
156-
return tile1;
157-
} else if (tile1.checkBounds(left, bottom, right, top)) {
158-
return tile1;
159-
} else if (tile2.checkBounds(left, bottom, right, top)) {
160-
return tile2;
161-
// Just in case the coordinates are _barely_ in other tiles and not the "common"
162-
// tile
163-
} else if (Math.abs(tile1.x() - tile2.x()) <= 2 && Math.abs(tile1.y() - tile2.y()) <= 2) {
164-
final var tileT = new TileXYZ((tile1.x() + tile2.x()) / 2, (tile1.y() + tile2.y()) / 2, zoom);
165-
if (tileT.checkBounds(left, bottom, right, top)) {
166-
return tileT;
167-
}
168-
}
169-
zoom--;
170-
}
171-
return new TileXYZ(0, 0, 0);
172-
}
173-
174-
private static TileXYZ tileFromLatLonZoom(double lon, double lat, int zoom) {
175-
var xCoordinate = Math.toIntExact(Math.round(Math.floor(Math.pow(2, zoom) * (180 + lon) / 360)));
176-
var yCoordinate = Math.toIntExact(Math.round(Math.floor(Math.pow(2, zoom)
177-
* (1 - (Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI))
178-
/ 2)));
179-
return new TileXYZ(xCoordinate, yCoordinate, zoom);
180-
}
181-
182-
/**
183-
* Extends a bounds object to contain this tile
184-
*
185-
* @param currentBounds The bounds to extend
186-
*/
187-
private void expandBounds(Bounds currentBounds) {
188-
final var thisLeft = xToLongitude(this.x, this.z);
189-
final var thisRight = xToLongitude(this.x + 1, this.z);
190-
final var thisBottom = yToLatitude(this.y + 1, this.z);
191-
final var thisTop = yToLatitude(this.y, this.z);
192-
currentBounds.extend(thisBottom, thisLeft);
193-
currentBounds.extend(thisTop, thisRight);
194-
}
195-
}
196-
19787
private final String url;
19888
private final boolean crop;
19989
private final int start;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// License: GPL. For details, see LICENSE file.
2+
package org.openstreetmap.josm.plugins.mapwithai.backend;
3+
4+
import java.util.stream.IntStream;
5+
import java.util.stream.Stream;
6+
7+
import org.openstreetmap.josm.data.Bounds;
8+
9+
/**
10+
* Create a tile
11+
*
12+
* @param x The x coordinate of the tile
13+
* @param y The y coordinate of the tile
14+
* @param z The zoom level
15+
*/
16+
record TileXYZ(int x, int y, int z) {
17+
/**
18+
* Checks to see if the given bounds are functionally equal to this tile
19+
*
20+
* @param left left
21+
* @param bottom bottom
22+
* @param right right
23+
* @param top top
24+
*/
25+
boolean checkBounds(double left, double bottom, double right, double top) {
26+
final var thisLeft = xToLongitude(this.x, this.z);
27+
final var thisRight = xToLongitude(this.x + 1, this.z);
28+
final var thisBottom = yToLatitude(this.y + 1, this.z);
29+
final var thisTop = yToLatitude(this.y, this.z);
30+
return equalsEpsilon(thisLeft, left, this.z) && equalsEpsilon(thisRight, right, this.z)
31+
&& equalsEpsilon(thisBottom, bottom, this.z) && equalsEpsilon(thisTop, top, this.z);
32+
}
33+
34+
private static boolean equalsEpsilon(double first, double second, int z) {
35+
// 0.1% of tile size is considered to be "equal"
36+
final var maxDiff = (360 / Math.pow(2, z)) / 1000;
37+
final var diff = Math.abs(first - second);
38+
return diff <= maxDiff;
39+
}
40+
41+
private static double xToLongitude(int x, int z) {
42+
return (x / Math.pow(2, z)) * 360 - 180;
43+
}
44+
45+
private static double yToLatitude(int y, int z) {
46+
var t = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
47+
return 180 / Math.PI * Math.atan((Math.exp(t) - Math.exp(-t)) / 2);
48+
}
49+
50+
/**
51+
* Convert bounds to tiles
52+
*
53+
* @param zoom The zoom level to use
54+
* @param bounds The bounds to convert to tiles
55+
* @return A stream of tiles for the bounds at the given zoom level
56+
*/
57+
static Stream<TileXYZ> tilesFromBBox(int zoom, Bounds bounds) {
58+
final var left = bounds.getMinLon();
59+
final var bottom = bounds.getMinLat();
60+
final var right = bounds.getMaxLon();
61+
final var top = bounds.getMaxLat();
62+
final var tile1 = tileFromLatLonZoom(left, bottom, zoom);
63+
final var tile2 = tileFromLatLonZoom(right, top, zoom);
64+
return IntStream.rangeClosed(tile1.x, tile2.x)
65+
.mapToObj(x -> IntStream.rangeClosed(tile2.y, tile1.y).mapToObj(y -> new TileXYZ(x, y, zoom)))
66+
.flatMap(stream -> stream);
67+
}
68+
69+
/**
70+
* Checks to see if the given bounds are functionally equal to this tile
71+
*
72+
* @param left left lon
73+
* @param bottom bottom lat
74+
* @param right right lon
75+
* @param top top lat
76+
*/
77+
static TileXYZ tileFromBBox(double left, double bottom, double right, double top) {
78+
var zoom = 18;
79+
while (zoom > 0) {
80+
final var tile1 = tileFromLatLonZoom(left, bottom, zoom);
81+
final var tile2 = tileFromLatLonZoom(right, top, zoom);
82+
if (tile1.equals(tile2)) {
83+
return tile1;
84+
} else if (tile1.checkBounds(left, bottom, right, top)) {
85+
return tile1;
86+
} else if (tile2.checkBounds(left, bottom, right, top)) {
87+
return tile2;
88+
// Just in case the coordinates are _barely_ in other tiles and not the "common"
89+
// tile
90+
} else if (Math.abs(tile1.x() - tile2.x()) <= 2 && Math.abs(tile1.y() - tile2.y()) <= 2) {
91+
final var tileT = new TileXYZ((tile1.x() + tile2.x()) / 2, (tile1.y() + tile2.y()) / 2, zoom);
92+
if (tileT.checkBounds(left, bottom, right, top)) {
93+
return tileT;
94+
}
95+
}
96+
zoom--;
97+
}
98+
return new TileXYZ(0, 0, 0);
99+
}
100+
101+
static TileXYZ tileFromLatLonZoom(double lon, double lat, int zoom) {
102+
var xCoordinate = Math.toIntExact(Math.round(Math.floor(Math.pow(2, zoom) * (180 + lon) / 360)));
103+
var yCoordinate = Math.toIntExact(Math.round(Math.floor(Math.pow(2, zoom)
104+
* (1 - (Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI)) / 2)));
105+
return new TileXYZ(xCoordinate, yCoordinate, zoom);
106+
}
107+
108+
/**
109+
* Extends a bounds object to contain this tile
110+
*
111+
* @param currentBounds The bounds to extend
112+
*/
113+
void expandBounds(Bounds currentBounds) {
114+
final var thisLeft = xToLongitude(this.x, this.z);
115+
final var thisRight = xToLongitude(this.x + 1, this.z);
116+
final var thisBottom = yToLatitude(this.y + 1, this.z);
117+
final var thisTop = yToLatitude(this.y, this.z);
118+
currentBounds.extend(thisBottom, thisLeft);
119+
currentBounds.extend(thisTop, thisRight);
120+
}
121+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// License: GPL. For details, see LICENSE file.
2+
package org.openstreetmap.josm.plugins.mapwithai.backend;
3+
4+
import static org.junit.jupiter.api.Assertions.*;
5+
6+
import java.util.Arrays;
7+
import java.util.stream.Stream;
8+
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.params.ParameterizedTest;
11+
import org.junit.jupiter.params.provider.Arguments;
12+
import org.junit.jupiter.params.provider.MethodSource;
13+
import org.openstreetmap.josm.data.Bounds;
14+
15+
/**
16+
* Test class for {@link TileXYZ}
17+
*/
18+
class TileXYZTest {
19+
static Stream<Arguments> testTileCalculations() {
20+
return Stream.of(Arguments.of(39.07035, -108.5709286, 52013, 100120, 18),
21+
Arguments.of(39.0643941, -108.5610312, 52020, 100125, 18),
22+
Arguments.of(39.0643941, -108.5709286, 52013, 100125, 18),
23+
Arguments.of(39.07035, -108.5610312, 52020, 100120, 18));
24+
}
25+
26+
@ParameterizedTest
27+
@MethodSource
28+
void testTileCalculations(double lat, double lon, int x, int y, int z) {
29+
final var tile = TileXYZ.tileFromLatLonZoom(lon, lat, z);
30+
assertAll(() -> assertEquals(x, tile.x()), () -> assertEquals(y, tile.y()), () -> assertEquals(z, tile.z()));
31+
}
32+
33+
/**
34+
* Check that the tiles calculated for a bbox are correct
35+
*/
36+
@Test
37+
void testNonRegressionGH44() {
38+
final var tiles = TileXYZ.tilesFromBBox(18, new Bounds(39.0643941, -108.5709286, 39.07035, -108.5610312))
39+
.toArray(TileXYZ[]::new);
40+
assertAll(() -> assertEquals(100125, Arrays.stream(tiles).mapToInt(TileXYZ::y).max().orElse(0)),
41+
() -> assertEquals(100120, Arrays.stream(tiles).mapToInt(TileXYZ::y).min().orElse(0)),
42+
() -> assertEquals(52013, Arrays.stream(tiles).mapToInt(TileXYZ::x).min().orElse(0)),
43+
() -> assertEquals(52020, Arrays.stream(tiles).mapToInt(TileXYZ::x).max().orElse(0)));
44+
assertEquals(48, tiles.length, "Should be 6x8 tiles (rangeClosed)");
45+
}
46+
}

0 commit comments

Comments
 (0)