Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmaparoni%2FGeoJSONKit-Turf%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/maparoni/GeoJSONKit-Turf)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmaparoni%2FGeoJSONKit-Turf%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/maparoni/GeoJSONKit-Turf)

This package provides various geospatial extensions for [GeoJSONKit](https://github.com/maparoni/geojsonkit). It is a fork of [turf-swift](https://github.com/mapbox/turf-swift.git), which is ported from [Turf.js](https://github.com/Turfjs/turf/).
This package provides various geospatial extensions for [GeoJSONKit](https://github.com/maparoni/geojsonkit). It is a fork of [turf-swift](https://github.com/mapbox/turf-swift.git), which itself is a partial Swift-port of [Turf.js](https://github.com/Turfjs/turf/).

## Requirements

Expand All @@ -13,6 +13,7 @@ GeoJSONKitTurf requires Xcode 14.x and supports the following minimum deployment
- iOS 15 and above
- macOS 12 and above
- tvOS 15 and above
- visionOS 1.0 and above
- watchOS 8.0 and above

It's also compatible with Linux (and possibly other platforms), as long as you have [Swift](https://swift.org/download/) 5.7 (or above) installed.
Expand All @@ -24,7 +25,7 @@ It's also compatible with Linux (and possibly other platforms), as long as you h
To install GeoJSONKitTurf using the [Swift Package Manager](https://swift.org/package-manager/), add the following package to the `dependencies` in your Package.swift file:

```swift
.package(name: "GeoJSONKitTurf", url: "https://github.com/maparoni/geojsonkit-turf", from: "0.1.0")
.package(name: "GeoJSONKitTurf", url: "https://github.com/maparoni/geojsonkit-turf", from: "0.3.0")
```

Then use:
Expand All @@ -42,7 +43,7 @@ Turf.js | GeoJSONKit-Turf
----|----
[turf-along](https://github.com/Turfjs/turf/tree/master/packages/turf-along/) | `GeoJSON.LineString.coordinateFromStart(distance:)`
[turf-area](https://github.com/Turfjs/turf/blob/master/packages/turf-area/) | `GeoJSON.Polygon.area`
[turf-bbox-clip](https://turfjs.org/docs/#bboxClip) | `GeoJSON.Polygon.clip(to:)`
[turf-bbox-clip](https://turfjs.org/docs/#bboxClip) | `GeoJSON.LineString.clipped(to:)`<br/>`GeoJSON.Polygon.clipped(to:)`
[turf-bearing](https://turfjs.org/docs/#bearing) | `GeoJSON.Position.direction(to:)`<br/> `RadianCoordinate2D.direction(to:)`
[turf-bezier-spline](https://github.com/Turfjs/turf/tree/master/packages/turf-bezier-spline/) | `GeoJSON.LineString.bezier(resolution:sharpness:)`
[turf-boolean-point-in-polygon](https://github.com/Turfjs/turf/tree/master/packages/turf-boolean-point-in-polygon) | `GeoJSON.Polygon.contains(_:)`
Expand All @@ -57,6 +58,7 @@ Turf.js | GeoJSONKit-Turf
[turf-helpers#radiansToDegrees](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers/#radiansToDegrees) | `GeoJSON.DegreesRadians.toDegrees()`
[turf-helpers#convertLength](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers#convertlength)<br/>[turf-helpers#convertArea](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers#convertarea) | `Measurement.converted(to:)`
[turf-length](https://github.com/Turfjs/turf/tree/master/packages/turf-length/) | `GeoJSON.LineString.distance(from:to:)`
[turf-line-chunk](http://turfjs.org/docs/#lineChunk) | `GeoJSON.LineString.chunked(length:)`<br/>`GeoJSON.Polygon.LinearRing.chunked(length:)` |
[turf-line-intersect](https://github.com/Turfjs/turf/tree/master/packages/turf-line-intersect/) | `GeoJSON.LineString.intersection(with:)`
[turf-line-slice](https://github.com/Turfjs/turf/tree/master/packages/turf-line-slice/) | `GeoJSON.LineString.sliced(from:to:)`
[turf-line-slice-along](https://github.com/Turfjs/turf/tree/master/packages/turf-line-slice-along/) | `GeoJSON.LineString.trimmed(from:distance:)`<br/>`GeoJSON.LineString.trimmed(from:to:)`
Expand All @@ -66,8 +68,13 @@ Turf.js | GeoJSONKit-Turf
[turf-polygon-smooth](https://github.com/Turfjs/turf/tree/master/packages/turf-polygon-smooth) | `GeoJSON.Polygon.smooth(iterations:)`
[turf-union](https://github.com/Turfjs/turf/tree/master/packages/turf-union) | Not provided, but see [ASPolygonKit](https://github.com/nighthawk/ASPolygonKit)
[turf-simplify](https://github.com/Turfjs/turf/tree/master/packages/turf-simplify) | `GeoJSON.simplify(options:)`
— | `GeoJSON.Direction.difference(from:)`
— | `GeoJSON.Direction.wrap(min:max:)`

Additionally, it adds the following features, which do not have a direct equivalent in turf.js:

* `GeoJSON.Direction.difference(from:)`
* `GeoJSON.Direction.wrap(min:max:)`
* `GeoJSON.LineString.frechetDistance(to:)`: Determines the [Fréchet distance](https://en.wikipedia.org/wiki/Fréchet_distance) between two line strings, which is a measure of their similarity.
* `GeoJSON.GeometryObject(splittingWhenCrossingAntiMeridian:)`: Breaks up a LineString or Polygon into two when crossing the anti-meridian.

## CLI

Expand Down
5 changes: 5 additions & 0 deletions Sources/GeoJSONKitTurf/Position+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ extension GeoJSON.Position {
return dx * dx + dy * dy
}

static func length(of positions: [GeoJSON.Position]) -> GeoJSON.Distance {
let pairs = zip(positions.prefix(upTo: positions.count - 1), positions.suffix(from: 1))
return pairs.map { $0.distance(to: $1) }.reduce(0, +)
}

}
84 changes: 84 additions & 0 deletions Sources/GeoJSONKitTurf/Turf+GeometryObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ extension GeoJSON.Geometry {
return polygon.nearestPoint(to: position)
}
}

}

extension GeoJSON.GeometryObject {
Expand All @@ -119,4 +120,87 @@ extension GeoJSON.GeometryObject {
return objects.contains(where: { $0.contains(coordinate, ignoreBoundary: ignoreBoundary, checkBoundingBox: checkBoundingBox) })
}
}

}

// MARK: - Anti-meridian safety

extension GeoJSON.Geometry {

func breakUpIfNecessary() -> [GeoJSON.Geometry] {
let bbox = GeoJSON.BoundingBox(positions: positions, allowSpanningAntimeridian: true)
guard bbox.spansAntimeridian else {
return [self]
}

let normalized = self.wrapped(min: 0, max: 360)

// Western-most point to anti-meridian
// No need to wrap, as already positive
let easterlyBox = GeoJSON.BoundingBox(positions: [
.init(latitude: bbox.northEasterlyLatitude, longitude: 180),
.init(latitude: bbox.southWesterlyLatitude, longitude: bbox.southWesterlyLongitude)
])

// Anti-meridian to eastern-mode point
// This needs to be wrapped sa the clipping doesn't work otherwise
let westerlyBox = GeoJSON.BoundingBox(positions: [
.init(latitude: bbox.northEasterlyLatitude, longitude: bbox.northEasterlyLongitude.wrap(min: 0, max: 360)),
.init(latitude: bbox.southWesterlyLatitude, longitude: 180)
])

let easterly = normalized.clipped(to: easterlyBox)
let westerly = normalized.clipped(to: westerlyBox)
return [
easterly,
westerly.wrapped(min: -180, max: 180),
]
}

private func clipped(to bbox: GeoJSON.BoundingBox) -> GeoJSON.Geometry {
switch self {
case .point:
return self
case .lineString(let line):
return .lineString(line.clipped(to: bbox))
case .polygon(let polygon):
return .polygon(polygon.clipped(to: bbox))
}
}

private func wrapped(min: GeoJSON.Degrees, max: GeoJSON.Degrees) -> GeoJSON.Geometry {
switch self {
case .point:
return self

case .lineString(let line):
let normalized = line.positions.map {
GeoJSON.Position(latitude: $0.latitude, longitude: $0.longitude.wrap(min: min, max: max))
}
return .lineString(.init(positions: normalized))

case .polygon(let polygon):
let normalized = polygon.exterior.positions.map {
GeoJSON.Position(latitude: $0.latitude, longitude: $0.longitude.wrap(min: min, max: max))
}
return .polygon(.init(exterior: .init(positions: normalized)))
}
}
}


extension GeoJSON.GeometryObject {

/// Checks the provided geometry, if it crosses the anti-meridian. If it doesn't, it's returned as
/// a `.single`, otherwise it's broken up into two and returned as a `.multi`.
///
/// - Parameter candidate: A geometry, which might cross the anti-meridian
public init(splittingWhenCrossingAntiMeridian candidate: GeoJSON.Geometry) {
let components = candidate.breakUpIfNecessary()
if components.count == 1, let only = components.first {
self = .single(only)
} else {
self = .multi(components)
}
}
}
180 changes: 120 additions & 60 deletions Sources/GeoJSONKitTurf/Turf+LineString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ extension GeoJSON.LineString {
var coordinates: [GeoJSON.Position] { positions }

/**
Representation of current `LineString` as an array of `LineSegment`s.
*/
var segments: [LineSegment] {
return zip(coordinates.dropLast(), coordinates.dropFirst()).map { LineSegment($0.0, $0.1) }
}
Representation of current `LineString` as an array of `LineSegment`s.
*/
var segments: [LineSegment] {
return zip(coordinates.dropLast(), coordinates.dropFirst()).map { LineSegment($0.0, $0.1) }
}

/// The length of the line, in metres
public var length: GeoJSON.Distance {
GeoJSON.Position.length(of: positions)
}

/// Returns a new line string based on bezier transformation of the input line.
///
Expand All @@ -28,64 +33,19 @@ extension GeoJSON.LineString {
return GeoJSON.LineString(positions: coords)
}

public func clipped(to boundingBox: GeoJSON.BoundingBox) -> GeoJSON.LineString {
return .init(positions: SutherlandHodgeman.clip(positions, to: boundingBox, close: false))
}

/**
Returns the portion of the line string that begins at the given start distance and extends the given stop distance along the line string.

This method is equivalent to the [turf-line-slice-along](https://turfjs.org/docs/#lineSliceAlong) package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-line-slice-along/)).
*/
public func trimmed(from startDistance: GeoJSON.Distance, to stopDistance: GeoJSON.Distance) -> GeoJSON.LineString? {
// The method is porting from https://github.com/Turfjs/turf/blob/5375941072b90d489389db22b43bfe809d5e451e/packages/turf-line-slice-along/index.js
guard startDistance >= 0, stopDistance >= startDistance else { return nil }
let positions = self.coordinates
var traveled: GeoJSON.Distance = 0
var slice = [GeoJSON.Position]()

for i in 0..<positions.endIndex {
if startDistance >= traveled, i == positions.endIndex - 1 {
break
} else if traveled > startDistance, slice.isEmpty {
let overshoot = startDistance - traveled
if overshoot == 0.0 {
slice.append(positions[i])
return GeoJSON.LineString(positions: slice)
}
let direction = positions[i].direction(to: positions[i - 1]) - 180
let interpolated = positions[i].coordinate(at: overshoot, facing: direction)
slice.append(interpolated)
}

if traveled >= stopDistance {
let overshoot = stopDistance - traveled
if overshoot == 0.0 {
slice.append(positions[i])
return GeoJSON.LineString(positions: slice)
}
let direction = positions[i].direction(to: positions[i - 1]) - 180
let interpolated = positions[i].coordinate(at: overshoot, facing: direction)
slice.append(interpolated)
return GeoJSON.LineString(positions: slice)
}

if traveled >= startDistance {
slice.append(positions[i])
}

if i == positions.count - 1 {
return GeoJSON.LineString(positions: slice)
}

traveled += positions[i].distance(to: positions[i + 1])
}

if traveled < startDistance {
return nil
}

if let last = positions.last {
return GeoJSON.LineString(positions: [last, last])
}

return nil
let trimmed = Self.trimmed(positions, from: startDistance, to: stopDistance)
guard !trimmed.isEmpty else { return nil }
return .init(positions: trimmed)
}

/// Returns the portion of the line string that begins at the given coordinate and extends the given distance along the line string.
Expand Down Expand Up @@ -219,7 +179,7 @@ extension GeoJSON.LineString {
public func closestCoordinate(to coordinate: GeoJSON.Position) -> IndexedCoordinate? {
.findClosest(to: coordinate, on: positions)
}

/**
Returns all intersections with another `LineString`.

Expand All @@ -237,8 +197,109 @@ extension GeoJSON.LineString {
}
return intersections
}
}

// MARK: - Turf-Slice

extension GeoJSON.LineString {

/// Divides a ``GeoJSON.LineString`` into chunks of a specified length.
/// If the line is shorter than the segment length then the original line is returned.
///
/// Adopted This function is roughly equivalent to the [turf-line-chunk](https://turfjs.org/docs/#lineChunk) package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-line-chunk/)). However, it returns another line string
/// rather than a feature collection.
///
/// - Parameter length: How long to make each segment, in metres.
/// - Returns: Line string with positions repeating as requested.
public func chunked(length: GeoJSON.Distance) -> GeoJSON.LineString {
return .init(positions: GeoJSON.LineString.sliceLineSegments(positions, length: length))
}

// Ported from https://github.com/Turfjs/turf/blob/master/packages/turf-line-chunk/index.js
static func sliceLineSegments(_ positions: [GeoJSON.Position], length: GeoJSON.Distance) -> [GeoJSON.Position] {
let lineLength = GeoJSON.Position.length(of: positions)

// If the line is shorter than the segment length then the orginal line is returned.
if lineLength <= length {
return positions
}

let segmentProportion = lineLength / length
var segmentCount = Int(segmentProportion)
if Double(segmentCount) < segmentProportion {
segmentCount += 1
}

return (0..<segmentCount)
.flatMap { i -> [GeoJSON.Position] in
let trimmed = Self.trimmed(
positions,
from: length * Double(i),
to: length * Double(i + 1)
)
if i == 0 {
return trimmed
} else {
return Array(trimmed.dropFirst())
}
}
}

// Ported from https://github.com/Turfjs/turf/tree/master/packages/turf-line-slice-along/
static func trimmed(_ positions: [GeoJSON.Position], from startDistance: GeoJSON.Distance, to stopDistance: GeoJSON.Distance) -> [GeoJSON.Position] {
guard startDistance >= 0.0, stopDistance >= startDistance else { return [] }
var traveled: GeoJSON.Distance = 0
var slice = [GeoJSON.Position]()

for i in 0..<positions.endIndex {
if startDistance >= traveled && i == positions.endIndex - 1 {
break
} else if traveled > startDistance && slice.isEmpty {
let overshoot = startDistance - traveled
if overshoot == 0.0 {
slice.append(positions[i])
return slice
}
let direction = positions[i].direction(to: positions[i - 1]) - 180
let interpolated = positions[i].coordinate(at: overshoot, facing: direction)
slice.append(interpolated)
}

if traveled >= stopDistance {
let overshoot = stopDistance - traveled
if overshoot == 0.0 {
slice.append(positions[i])
return slice
}
let direction = positions[i].direction(to: positions[i - 1]) - 180
let interpolated = positions[i].coordinate(at: overshoot, facing: direction)
slice.append(interpolated)
return slice
}

if traveled >= startDistance {
slice.append(positions[i])
}

if i == positions.count - 1 {
return slice
}

traveled += positions[i].distance(to: positions[i + 1])
}

if traveled < startDistance {
return []
}

if let last = positions.last {
return [last, last]
}

return []
}

// MARK: - Fretched Distance
// MARK: - Fretchet Distance

/// Frechet distance to another line, which is a measure of how similar the the lines are
///
Expand Down Expand Up @@ -271,5 +332,4 @@ extension GeoJSON.LineString {
return c(i: path1.count - 1, j: path2.count - 1)
}


}
Loading