Skip to content

Commit 286a462

Browse files
authored
Adds equivalents of turf-line-chunk (#10)
* Adds equivalents of turf-line-chunk Also: - GeoJSON.LineString.length - GeoJSON.Polygon.LinearRing.circumference * Potential help for Linux * Come on, Linux... * Add tests * README review * Improvements: - New `GeoJSON.GeometryObject(splittingWhenCrossingAntiMeridian:)` helper - Rename `clip(to:)` to `clipped(to:)` - Adds `GeoJSON.LineString.clipped(to:)` - Move algorithms to algorithms folder
1 parent bb93b0a commit 286a462

10 files changed

Lines changed: 442 additions & 157 deletions

File tree

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![](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)
55
[![](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)
66

7-
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/).
7+
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/).
88

99
## Requirements
1010

@@ -13,6 +13,7 @@ GeoJSONKitTurf requires Xcode 14.x and supports the following minimum deployment
1313
- iOS 15 and above
1414
- macOS 12 and above
1515
- tvOS 15 and above
16+
- visionOS 1.0 and above
1617
- watchOS 8.0 and above
1718

1819
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.
@@ -24,7 +25,7 @@ It's also compatible with Linux (and possibly other platforms), as long as you h
2425
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:
2526

2627
```swift
27-
.package(name: "GeoJSONKitTurf", url: "https://github.com/maparoni/geojsonkit-turf", from: "0.1.0")
28+
.package(name: "GeoJSONKitTurf", url: "https://github.com/maparoni/geojsonkit-turf", from: "0.3.0")
2829
```
2930

3031
Then use:
@@ -42,7 +43,7 @@ Turf.js | GeoJSONKit-Turf
4243
----|----
4344
[turf-along](https://github.com/Turfjs/turf/tree/master/packages/turf-along/) | `GeoJSON.LineString.coordinateFromStart(distance:)`
4445
[turf-area](https://github.com/Turfjs/turf/blob/master/packages/turf-area/) | `GeoJSON.Polygon.area`
45-
[turf-bbox-clip](https://turfjs.org/docs/#bboxClip) | `GeoJSON.Polygon.clip(to:)`
46+
[turf-bbox-clip](https://turfjs.org/docs/#bboxClip) | `GeoJSON.LineString.clipped(to:)`<br/>`GeoJSON.Polygon.clipped(to:)`
4647
[turf-bearing](https://turfjs.org/docs/#bearing) | `GeoJSON.Position.direction(to:)`<br/> `RadianCoordinate2D.direction(to:)`
4748
[turf-bezier-spline](https://github.com/Turfjs/turf/tree/master/packages/turf-bezier-spline/) | `GeoJSON.LineString.bezier(resolution:sharpness:)`
4849
[turf-boolean-point-in-polygon](https://github.com/Turfjs/turf/tree/master/packages/turf-boolean-point-in-polygon) | `GeoJSON.Polygon.contains(_:)`
@@ -57,6 +58,7 @@ Turf.js | GeoJSONKit-Turf
5758
[turf-helpers#radiansToDegrees](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers/#radiansToDegrees) | `GeoJSON.DegreesRadians.toDegrees()`
5859
[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:)`
5960
[turf-length](https://github.com/Turfjs/turf/tree/master/packages/turf-length/) | `GeoJSON.LineString.distance(from:to:)`
61+
[turf-line-chunk](http://turfjs.org/docs/#lineChunk) | `GeoJSON.LineString.chunked(length:)`<br/>`GeoJSON.Polygon.LinearRing.chunked(length:)` |
6062
[turf-line-intersect](https://github.com/Turfjs/turf/tree/master/packages/turf-line-intersect/) | `GeoJSON.LineString.intersection(with:)`
6163
[turf-line-slice](https://github.com/Turfjs/turf/tree/master/packages/turf-line-slice/) | `GeoJSON.LineString.sliced(from:to:)`
6264
[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:)`
@@ -66,8 +68,13 @@ Turf.js | GeoJSONKit-Turf
6668
[turf-polygon-smooth](https://github.com/Turfjs/turf/tree/master/packages/turf-polygon-smooth) | `GeoJSON.Polygon.smooth(iterations:)`
6769
[turf-union](https://github.com/Turfjs/turf/tree/master/packages/turf-union) | Not provided, but see [ASPolygonKit](https://github.com/nighthawk/ASPolygonKit)
6870
[turf-simplify](https://github.com/Turfjs/turf/tree/master/packages/turf-simplify) | `GeoJSON.simplify(options:)`
69-
— | `GeoJSON.Direction.difference(from:)`
70-
— | `GeoJSON.Direction.wrap(min:max:)`
71+
72+
Additionally, it adds the following features, which do not have a direct equivalent in turf.js:
73+
74+
* `GeoJSON.Direction.difference(from:)`
75+
* `GeoJSON.Direction.wrap(min:max:)`
76+
* `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.
77+
* `GeoJSON.GeometryObject(splittingWhenCrossingAntiMeridian:)`: Breaks up a LineString or Polygon into two when crossing the anti-meridian.
7178

7279
## CLI
7380

Sources/GeoJSONKitTurf/Position+Helpers.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@ extension GeoJSON.Position {
1717
return dx * dx + dy * dy
1818
}
1919

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

Sources/GeoJSONKitTurf/Turf+GeometryObject.swift

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ extension GeoJSON.Geometry {
9898
return polygon.nearestPoint(to: position)
9999
}
100100
}
101+
101102
}
102103

103104
extension GeoJSON.GeometryObject {
@@ -119,4 +120,87 @@ extension GeoJSON.GeometryObject {
119120
return objects.contains(where: { $0.contains(coordinate, ignoreBoundary: ignoreBoundary, checkBoundingBox: checkBoundingBox) })
120121
}
121122
}
123+
124+
}
125+
126+
// MARK: - Anti-meridian safety
127+
128+
extension GeoJSON.Geometry {
129+
130+
func breakUpIfNecessary() -> [GeoJSON.Geometry] {
131+
let bbox = GeoJSON.BoundingBox(positions: positions, allowSpanningAntimeridian: true)
132+
guard bbox.spansAntimeridian else {
133+
return [self]
134+
}
135+
136+
let normalized = self.wrapped(min: 0, max: 360)
137+
138+
// Western-most point to anti-meridian
139+
// No need to wrap, as already positive
140+
let easterlyBox = GeoJSON.BoundingBox(positions: [
141+
.init(latitude: bbox.northEasterlyLatitude, longitude: 180),
142+
.init(latitude: bbox.southWesterlyLatitude, longitude: bbox.southWesterlyLongitude)
143+
])
144+
145+
// Anti-meridian to eastern-mode point
146+
// This needs to be wrapped sa the clipping doesn't work otherwise
147+
let westerlyBox = GeoJSON.BoundingBox(positions: [
148+
.init(latitude: bbox.northEasterlyLatitude, longitude: bbox.northEasterlyLongitude.wrap(min: 0, max: 360)),
149+
.init(latitude: bbox.southWesterlyLatitude, longitude: 180)
150+
])
151+
152+
let easterly = normalized.clipped(to: easterlyBox)
153+
let westerly = normalized.clipped(to: westerlyBox)
154+
return [
155+
easterly,
156+
westerly.wrapped(min: -180, max: 180),
157+
]
158+
}
159+
160+
private func clipped(to bbox: GeoJSON.BoundingBox) -> GeoJSON.Geometry {
161+
switch self {
162+
case .point:
163+
return self
164+
case .lineString(let line):
165+
return .lineString(line.clipped(to: bbox))
166+
case .polygon(let polygon):
167+
return .polygon(polygon.clipped(to: bbox))
168+
}
169+
}
170+
171+
private func wrapped(min: GeoJSON.Degrees, max: GeoJSON.Degrees) -> GeoJSON.Geometry {
172+
switch self {
173+
case .point:
174+
return self
175+
176+
case .lineString(let line):
177+
let normalized = line.positions.map {
178+
GeoJSON.Position(latitude: $0.latitude, longitude: $0.longitude.wrap(min: min, max: max))
179+
}
180+
return .lineString(.init(positions: normalized))
181+
182+
case .polygon(let polygon):
183+
let normalized = polygon.exterior.positions.map {
184+
GeoJSON.Position(latitude: $0.latitude, longitude: $0.longitude.wrap(min: min, max: max))
185+
}
186+
return .polygon(.init(exterior: .init(positions: normalized)))
187+
}
188+
}
189+
}
190+
191+
192+
extension GeoJSON.GeometryObject {
193+
194+
/// Checks the provided geometry, if it crosses the anti-meridian. If it doesn't, it's returned as
195+
/// a `.single`, otherwise it's broken up into two and returned as a `.multi`.
196+
///
197+
/// - Parameter candidate: A geometry, which might cross the anti-meridian
198+
public init(splittingWhenCrossingAntiMeridian candidate: GeoJSON.Geometry) {
199+
let components = candidate.breakUpIfNecessary()
200+
if components.count == 1, let only = components.first {
201+
self = .single(only)
202+
} else {
203+
self = .multi(components)
204+
}
205+
}
122206
}

Sources/GeoJSONKitTurf/Turf+LineString.swift

Lines changed: 120 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ extension GeoJSON.LineString {
66
var coordinates: [GeoJSON.Position] { positions }
77

88
/**
9-
Representation of current `LineString` as an array of `LineSegment`s.
10-
*/
11-
var segments: [LineSegment] {
12-
return zip(coordinates.dropLast(), coordinates.dropFirst()).map { LineSegment($0.0, $0.1) }
13-
}
9+
Representation of current `LineString` as an array of `LineSegment`s.
10+
*/
11+
var segments: [LineSegment] {
12+
return zip(coordinates.dropLast(), coordinates.dropFirst()).map { LineSegment($0.0, $0.1) }
13+
}
14+
15+
/// The length of the line, in metres
16+
public var length: GeoJSON.Distance {
17+
GeoJSON.Position.length(of: positions)
18+
}
1419

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

36+
public func clipped(to boundingBox: GeoJSON.BoundingBox) -> GeoJSON.LineString {
37+
return .init(positions: SutherlandHodgeman.clip(positions, to: boundingBox, close: false))
38+
}
39+
3140
/**
3241
Returns the portion of the line string that begins at the given start distance and extends the given stop distance along the line string.
3342

3443
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/)).
3544
*/
3645
public func trimmed(from startDistance: GeoJSON.Distance, to stopDistance: GeoJSON.Distance) -> GeoJSON.LineString? {
37-
// The method is porting from https://github.com/Turfjs/turf/blob/5375941072b90d489389db22b43bfe809d5e451e/packages/turf-line-slice-along/index.js
38-
guard startDistance >= 0, stopDistance >= startDistance else { return nil }
39-
let positions = self.coordinates
40-
var traveled: GeoJSON.Distance = 0
41-
var slice = [GeoJSON.Position]()
42-
43-
for i in 0..<positions.endIndex {
44-
if startDistance >= traveled, i == positions.endIndex - 1 {
45-
break
46-
} else if traveled > startDistance, slice.isEmpty {
47-
let overshoot = startDistance - traveled
48-
if overshoot == 0.0 {
49-
slice.append(positions[i])
50-
return GeoJSON.LineString(positions: slice)
51-
}
52-
let direction = positions[i].direction(to: positions[i - 1]) - 180
53-
let interpolated = positions[i].coordinate(at: overshoot, facing: direction)
54-
slice.append(interpolated)
55-
}
56-
57-
if traveled >= stopDistance {
58-
let overshoot = stopDistance - traveled
59-
if overshoot == 0.0 {
60-
slice.append(positions[i])
61-
return GeoJSON.LineString(positions: slice)
62-
}
63-
let direction = positions[i].direction(to: positions[i - 1]) - 180
64-
let interpolated = positions[i].coordinate(at: overshoot, facing: direction)
65-
slice.append(interpolated)
66-
return GeoJSON.LineString(positions: slice)
67-
}
68-
69-
if traveled >= startDistance {
70-
slice.append(positions[i])
71-
}
72-
73-
if i == positions.count - 1 {
74-
return GeoJSON.LineString(positions: slice)
75-
}
76-
77-
traveled += positions[i].distance(to: positions[i + 1])
78-
}
79-
80-
if traveled < startDistance {
81-
return nil
82-
}
83-
84-
if let last = positions.last {
85-
return GeoJSON.LineString(positions: [last, last])
86-
}
87-
88-
return nil
46+
let trimmed = Self.trimmed(positions, from: startDistance, to: stopDistance)
47+
guard !trimmed.isEmpty else { return nil }
48+
return .init(positions: trimmed)
8949
}
9050

9151
/// Returns the portion of the line string that begins at the given coordinate and extends the given distance along the line string.
@@ -219,7 +179,7 @@ extension GeoJSON.LineString {
219179
public func closestCoordinate(to coordinate: GeoJSON.Position) -> IndexedCoordinate? {
220180
.findClosest(to: coordinate, on: positions)
221181
}
222-
182+
223183
/**
224184
Returns all intersections with another `LineString`.
225185

@@ -237,8 +197,109 @@ extension GeoJSON.LineString {
237197
}
238198
return intersections
239199
}
200+
}
201+
202+
// MARK: - Turf-Slice
203+
204+
extension GeoJSON.LineString {
205+
206+
/// Divides a ``GeoJSON.LineString`` into chunks of a specified length.
207+
/// If the line is shorter than the segment length then the original line is returned.
208+
///
209+
/// 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
210+
/// rather than a feature collection.
211+
///
212+
/// - Parameter length: How long to make each segment, in metres.
213+
/// - Returns: Line string with positions repeating as requested.
214+
public func chunked(length: GeoJSON.Distance) -> GeoJSON.LineString {
215+
return .init(positions: GeoJSON.LineString.sliceLineSegments(positions, length: length))
216+
}
217+
218+
// Ported from https://github.com/Turfjs/turf/blob/master/packages/turf-line-chunk/index.js
219+
static func sliceLineSegments(_ positions: [GeoJSON.Position], length: GeoJSON.Distance) -> [GeoJSON.Position] {
220+
let lineLength = GeoJSON.Position.length(of: positions)
221+
222+
// If the line is shorter than the segment length then the orginal line is returned.
223+
if lineLength <= length {
224+
return positions
225+
}
226+
227+
let segmentProportion = lineLength / length
228+
var segmentCount = Int(segmentProportion)
229+
if Double(segmentCount) < segmentProportion {
230+
segmentCount += 1
231+
}
232+
233+
return (0..<segmentCount)
234+
.flatMap { i -> [GeoJSON.Position] in
235+
let trimmed = Self.trimmed(
236+
positions,
237+
from: length * Double(i),
238+
to: length * Double(i + 1)
239+
)
240+
if i == 0 {
241+
return trimmed
242+
} else {
243+
return Array(trimmed.dropFirst())
244+
}
245+
}
246+
}
247+
248+
// Ported from https://github.com/Turfjs/turf/tree/master/packages/turf-line-slice-along/
249+
static func trimmed(_ positions: [GeoJSON.Position], from startDistance: GeoJSON.Distance, to stopDistance: GeoJSON.Distance) -> [GeoJSON.Position] {
250+
guard startDistance >= 0.0, stopDistance >= startDistance else { return [] }
251+
var traveled: GeoJSON.Distance = 0
252+
var slice = [GeoJSON.Position]()
253+
254+
for i in 0..<positions.endIndex {
255+
if startDistance >= traveled && i == positions.endIndex - 1 {
256+
break
257+
} else if traveled > startDistance && slice.isEmpty {
258+
let overshoot = startDistance - traveled
259+
if overshoot == 0.0 {
260+
slice.append(positions[i])
261+
return slice
262+
}
263+
let direction = positions[i].direction(to: positions[i - 1]) - 180
264+
let interpolated = positions[i].coordinate(at: overshoot, facing: direction)
265+
slice.append(interpolated)
266+
}
267+
268+
if traveled >= stopDistance {
269+
let overshoot = stopDistance - traveled
270+
if overshoot == 0.0 {
271+
slice.append(positions[i])
272+
return slice
273+
}
274+
let direction = positions[i].direction(to: positions[i - 1]) - 180
275+
let interpolated = positions[i].coordinate(at: overshoot, facing: direction)
276+
slice.append(interpolated)
277+
return slice
278+
}
279+
280+
if traveled >= startDistance {
281+
slice.append(positions[i])
282+
}
283+
284+
if i == positions.count - 1 {
285+
return slice
286+
}
287+
288+
traveled += positions[i].distance(to: positions[i + 1])
289+
}
290+
291+
if traveled < startDistance {
292+
return []
293+
}
294+
295+
if let last = positions.last {
296+
return [last, last]
297+
}
298+
299+
return []
300+
}
240301

241-
// MARK: - Fretched Distance
302+
// MARK: - Fretchet Distance
242303

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

274-
275335
}

0 commit comments

Comments
 (0)