Skip to content

Commit

Permalink
Merge branch 'main' into line_overlap_refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
trasch committed Sep 11, 2024
2 parents 26b2fb9 + 96aeecd commit 0609941
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 20 deletions.
46 changes: 27 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
[![][image-1]][1]
[![][image-2]][2]
[![][image-1]][1]
[![][image-2]][2]
[![](https://img.shields.io/github/license/Outdooractive/gis-tools)](https://github.com/Outdooractive/gis-tools/blob/main/LICENSE)
[![](https://img.shields.io/github/v/release/Outdooractive/gis-tools?sort=semver&display_name=tag)](https://github.com/Outdooractive/gis-tools/releases) [![](https://img.shields.io/github/release-date/Outdooractive/gis-tools?display_date=published_at
)](https://github.com/Outdooractive/gis-tools/releases)
[![](https://img.shields.io/github/issues/Outdooractive/gis-tools
)](https://github.com/Outdooractive/gis-tools/issues) [![](https://img.shields.io/github/issues-pr/Outdooractive/gis-tools
)](https://github.com/Outdooractive/gis-tools/pulls)
[![](https://img.shields.io/github/check-runs/Outdooractive/gis-tools/main)](https://github.com/Outdooractive/gis-tools/actions)


# GISTools
GIS tools for Swift, including a [GeoJSON][3] implementation and many algorithms ported from [https://turfjs.org][4].

## Features

- Supports the full [GeoJSON standard][6], with some exceptions (see [TODO.md][7])
- Load and write GeoJSON objects from and to `[String:Any]`, `URL`, `Data` and `String`
- Supports `Codable` and `SwiftData` (see below)
- Supports EPSG:3857 (web mercator) and EPSG:4326 (geodetic) conversions
- Supports WKT/WKB, also with different projections
- Spatial search with a R-tree
- Includes many spatial algorithms (ported from turf.js), and more to come
- Has a helper for working with x/y/z map tiles (center/bounding box/resolution/…)
- Can encode/decode Polylines
- Pure Swift without external dependencies
- Swift 6 ready

## Notes

This package makes some assumptions about what is equal, i.e. coordinates that are inside of `1e-10` degrees are regarded as equal (that's μm precision and is probably overkill). See [GISTool.equalityDelta][5].
Expand All @@ -16,7 +38,7 @@ This package requires Swift 5.10 or higher (at least Xcode 14), and compiles on

```swift
dependencies: [
.package(url: "https://github.com/Outdooractive/gis-tools", from: "1.7.0"),
.package(url: "https://github.com/Outdooractive/gis-tools", from: "1.8.2"),
],
targets: [
.target(name: "MyTarget", dependencies: [
Expand All @@ -25,23 +47,9 @@ targets: [
]
```

## Features

- Supports the full [GeoJSON standard][6], with some exceptions (see [TODO.md][7])
- Load and write GeoJSON objects from and to `[String:Any]`, `URL`, `Data` and `String`
- Supports `Codable` and `SwiftData` (see below)
- Supports EPSG:3857 (web mercator) and EPSG:4326 (geodetic) conversions
- Supports WKT/WKB, also with different projections
- Spatial search with a R-tree
- Includes many spatial algorithms (ported from turf.js), and more to come
- Has a helper for working with x/y/z map tiles (center/bounding box/resolution/…)
- Can encode/decode Polylines
- Pure Swift without external dependencies
- Swift 6 ready

## Usage

Please see also the [API documentation][8].
Please see also the [API documentation][8] (via Swift Package Index).

```swift
import GISTools
Expand Down Expand Up @@ -877,7 +885,7 @@ Currently only two:
- [mvt-postgis][130]: Creates vector tiles from Postgis databases

# Contributing
Please create an issue or open a pull request with a fix or enhancement.
Please [create an issue](https://github.com/Outdooractive/gis-tools/issues) or [open a pull request](https://github.com/Outdooractive/gis-tools/pulls) with a fix or enhancement.

# License
MIT
Expand Down
6 changes: 5 additions & 1 deletion Sources/GISTools/Algorithms/Center.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ extension GeoJson {
public var centroid: Point? {
let allCoordinates = self.allCoordinates

guard !allCoordinates.isEmpty else { return nil }
guard allCoordinates.isNotEmpty else { return nil }

if allCoordinates.count == 1 {
return Point(allCoordinates[0])
}

var sumLongitude: Double = 0.0
var sumLatitude: Double = 0.0
Expand Down
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 0609941

Please sign in to comment.