Skip to content

Commit

Permalink
Added MapTile(boundingBox:maxZoom:) (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
trasch authored Aug 12, 2024
1 parent a2d37e2 commit a8118bc
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 0 deletions.
7 changes: 7 additions & 0 deletions Sources/GISTools/GeoJson/BoundingBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,13 @@ extension BoundingBox {
return nil
}

/// `true` if the receiver crosses the anti-meridian.
public var crossesAntiMeridian: Bool {
let boundingBox = self.normalized()

return boundingBox.southWest.longitude > boundingBox.northEast.longitude
}

}

// MARK: - Helpers
Expand Down
48 changes: 48 additions & 0 deletions Sources/GISTools/Other/MapTile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public struct MapTile: CustomStringConvertible, Sendable {
]
}

public var siblings: [MapTile] {
parent.children
}

public init(x: Int, y: Int, z: Int) {
self.x = x
self.y = y
Expand All @@ -52,6 +56,50 @@ public struct MapTile: CustomStringConvertible, Sendable {
self.z = zoom
}

// Ported from https://github.com/mapbox/tilebelt/blob/master/index.js
/// Initialize a tile from a bounding box.
/// The resulting tile will have a zoom level in `0...maxZoom`.
///
/// - parameter boundingBox: The bounding box that the tile should completely contain
/// - parameter maxZoom: The maximum zoom level of the resulting tile, 0...32
public init(
boundingBox: BoundingBox,
maxZoom: Int = 32)
{
if boundingBox.crossesAntiMeridian {
self.init(x: 0, y: 0, z: 0)
return
}

let maxZoom = max(0, min(32, maxZoom))

let min = MapTile(coordinate: boundingBox.southWest, atZoom: 32)
let max = MapTile(coordinate: boundingBox.northEast, atZoom: 32)

var bestZ = -1
for z in 0 ..< maxZoom {
let mask = 1 << (32 - (z + 1))
if (min.x & mask) != (max.x & mask)
|| (min.y & mask) != (max.y & mask)
{
bestZ = z
break
}
}
if bestZ == 0 {
self.init(x: 0, y: 0, z: 0)
return
}
if bestZ == -1 {
bestZ = maxZoom
}

self.init(
x: min.x >> (32 - bestZ),
y: min.y >> (32 - bestZ),
z: bestZ)
}

public init?(string: String) {
guard let components = string.components(separatedBy: "/").nilIfEmpty,
components.count == 3,
Expand Down
19 changes: 19 additions & 0 deletions Tests/GISToolsTests/Other/MapTileTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ final class MapTileTests: XCTestCase {

}

func testTileFromBoundingBox() throws {
let boundingBox1 = BoundingBox(
southWest: Coordinate3D(latitude: 46.5, longitude: 10.5),
northEast: Coordinate3D(latitude: 48.5, longitude: 11.0))
let boundingBox2 = BoundingBox(
southWest: Coordinate3D(latitude: 46.5, longitude: 10.5),
northEast: Coordinate3D(latitude: 48.5, longitude: 11.25))
let boundingBox3 = try XCTUnwrap(BoundingBox(coordinates: [Coordinate3D(latitude: 47.56, longitude: 10.22)]))

XCTAssertEqual(MapTile(boundingBox: boundingBox1), MapTile(x: 33, y: 22, z: 6))
XCTAssertEqual(MapTile(boundingBox: boundingBox2), MapTile(x: 8, y: 5, z: 4))

XCTAssertEqual(MapTile(boundingBox: boundingBox3), MapTile(x: 2269412997, y: 1500804469, z: 32))
XCTAssertEqual(MapTile(boundingBox: boundingBox3, maxZoom: 14), MapTile(x: 8657, y: 5725, z: 14))
XCTAssertEqual(MapTile(boundingBox: boundingBox3, maxZoom: 8), MapTile(x: 135, y: 89, z: 8))
XCTAssertEqual(MapTile(boundingBox: boundingBox3, maxZoom: 4), MapTile(x: 8, y: 5, z: 4))
XCTAssertEqual(MapTile(boundingBox: boundingBox3, maxZoom: 0), MapTile(x: 0, y: 0, z: 0))
}

func testCenter() {
let coordinate1 = MapTile(x: 138513, y: 91601, z: 18).centerCoordinate()
XCTAssertEqual(coordinate1.latitude, 47.56031069944929, accuracy: 0.00001)
Expand Down

0 comments on commit a8118bc

Please sign in to comment.