Skip to content

Commit

Permalink
mapbox#89: added extensions for custom methods for geometries
Browse files Browse the repository at this point in the history
  • Loading branch information
Victor Kononov committed Feb 27, 2020
1 parent 9eb017e commit ecaa6fa
Show file tree
Hide file tree
Showing 12 changed files with 397 additions and 74 deletions.
8 changes: 6 additions & 2 deletions Sources/Turf/GeoJSON.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,6 @@ public enum GeoJSONType: String, CaseIterable {
case Feature
case FeatureCollection
case Unknown

// static let allValues: [GeoJSONType] = [.Feature, .FeatureCollection]
}

public enum GeoJSONError: Error {
Expand All @@ -136,6 +134,12 @@ public enum GeoJSONError: Error {
public class GeoJSON: Codable {

public var decoded: Codable?
public var decodedFeature: _Feature? {
decoded as? _Feature
}
public var decodedFeatueCollection: FeatureCollection? {
decoded as? FeatureCollection
}

public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
Expand Down
10 changes: 0 additions & 10 deletions Sources/Turf/Geometry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ public enum GeometryType: String, Codable, CaseIterable {
case MultiLineString
case MultiPolygon
case GeometryCollection

// static let allValues: [GeometryType] = [.Point, .LineString, .Polygon, .MultiPoint, .MultiLineString, .MultiPolygon]
}

public enum _Geometry {
Expand Down Expand Up @@ -122,11 +120,3 @@ extension _Geometry: Codable {
}
}
}

//public struct Geometry: Codable {
// public var type: String
//
// public var geometryType: GeometryType? {
// return GeometryType(rawValue: type)
// }
//}
220 changes: 220 additions & 0 deletions Sources/Turf/LineString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ public typealias Polyline = LineString
/**
`LineString` geometry represents a shape consisting of two or more coordinates.
*/
@available(*, deprecated, message: "Use `Geometry.LineString` instead.")
public struct LineString: Codable, Equatable {
var type: String = GeometryType.LineString.rawValue
public var coordinates: [CLLocationCoordinate2D]
}

@available(*, deprecated, message: "Use `Feature` instead.")
public struct LineStringFeature: GeoJSONObject {
public var type: FeatureType = .feature
public var identifier: FeatureIdentifier?
Expand Down Expand Up @@ -245,3 +247,221 @@ extension LineString {
return closestCoordinate
}
}

extension _Geometry {
/// Returns a new `.LineString` based on bezier transformation of the input line.
/// If current enum case is not `.LineString` - always returns `nil` instead
///
/// ported from https://github.com/Turfjs/turf/blob/1ea264853e1be7469c8b7d2795651c9114a069aa/packages/turf-bezier-spline/index.ts
func bezier(resolution: Int = 10000, sharpness: Double = 0.85) -> _Geometry? {
guard case let .LineString(coordinates: coordinates) = self else {
return nil
}
let points = coordinates.map {
SplinePoint(coordinate: $0)
}
guard let spline = Spline(points: points, duration: resolution, sharpness: sharpness) else {
return nil
}
let coords = stride(from: 0, to: resolution, by: 10)
.filter { Int(floor(Double($0) / 100)) % 2 == 0 }
.map { spline.position(at: $0).coordinate }
return .LineString(coordinates: coords)
}

/// Returns a `.LineString` along a `.LineString` within a distance from a coordinate.
/// If current enum case is not `.LineString` - always returns `nil` instead
public func trimmed(from coordinate: CLLocationCoordinate2D, distance: CLLocationDistance) -> _Geometry? {
guard case let .LineString(coordinates: coordinates) = self else {
return nil
}
let startVertex = closestCoordinate(to: coordinate)
guard startVertex != nil && distance != 0 else {
return nil
}

var vertices: [CLLocationCoordinate2D] = [startVertex!.coordinate]
var cumulativeDistance: CLLocationDistance = 0
let addVertex = { (vertex: CLLocationCoordinate2D) -> Bool in
let lastVertex = vertices.last!
let incrementalDistance = lastVertex.distance(to: vertex)
if cumulativeDistance + incrementalDistance <= abs(distance) {
vertices.append(vertex)
cumulativeDistance += incrementalDistance
return true
} else {
let remainingDistance = abs(distance) - cumulativeDistance
let direction = lastVertex.direction(to: vertex)
let endpoint = lastVertex.coordinate(at: remainingDistance, facing: direction)
vertices.append(endpoint)
cumulativeDistance += remainingDistance
return false
}
}

if distance > 0 {
for vertex in coordinates.suffix(from: startVertex!.index) {
if !addVertex(vertex) {
break
}
}
} else {
for vertex in coordinates.prefix(through: startVertex!.index).reversed() {
if !addVertex(vertex) {
break
}
}
}
assert(round(cumulativeDistance) <= round(abs(distance)))
return .LineString(coordinates: vertices)
}

/// `IndexedCoordinate` is a coordinate with additional information such as
/// the index from its position in the polyline and distance from the start
/// of the polyline.
public struct IndexedCoordinate {
/// The coordinate
public let coordinate: Array<CLLocationCoordinate2D>.Element
/// The index of the coordinate
public let index: Array<CLLocationCoordinate2D>.Index
/// The coordinate’s distance from the start of the polyline
public let distance: CLLocationDistance
}

/// Initializes a `.LineString` from the given ring.
public init(_ ring: Ring) {
self = .LineString(coordinates: ring.coordinates)
}

/// Returns a coordinate along a `.LineString` at a certain distance from the start of the polyline.
public func coordinateFromStart(distance: CLLocationDistance) -> CLLocationCoordinate2D? {
return indexedCoordinateFromStart(distance: distance)?.coordinate
}

/// Returns an indexed coordinate along a `.LineString` at a certain distance from the start of the polyline.
/// If current enum case is not `.LineString` - always returns `nil` instead
///
/// Ported from https://github.com/Turfjs/turf/blob/142e137ce0c758e2825a260ab32b24db0aa19439/packages/turf-along/index.js
public func indexedCoordinateFromStart(distance: CLLocationDistance) -> IndexedCoordinate? {
guard case let .LineString(coordinates: coordinates) = self else {
return nil
}
var traveled: CLLocationDistance = 0

guard let firstCoordinate = coordinates.first else {
return nil
}
guard distance >= 0 else {
return IndexedCoordinate(coordinate: firstCoordinate, index: 0, distance: 0)
}

for i in 0..<coordinates.count {
guard distance < traveled || i < coordinates.count - 1 else {
break
}

if traveled >= distance {
let overshoot = distance - traveled
if overshoot == 0 {
return IndexedCoordinate(coordinate: coordinates[i], index: i, distance: traveled)
}

let direction = coordinates[i].direction(to: coordinates[i - 1]) - 180
let coordinate = coordinates[i].coordinate(at: overshoot, facing: direction)
return IndexedCoordinate(coordinate: coordinate, index: i - 1, distance: distance)
}

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

return IndexedCoordinate(coordinate: coordinates.last!, index: coordinates.endIndex - 1, distance: traveled)
}


/// Returns the distance along a slice of a `.LineString` with the given endpoints.
/// If current enum case is not `.LineString` - always returns `nil` instead
///
/// Ported from https://github.com/Turfjs/turf/blob/142e137ce0c758e2825a260ab32b24db0aa19439/packages/turf-line-slice/index.js
public func distance(from start: CLLocationCoordinate2D? = nil, to end: CLLocationCoordinate2D? = nil) -> CLLocationDistance? {
guard case let .LineString(coordinates: coordinates) = self, !coordinates.isEmpty else {
return nil
}
guard let slicedCoordinates = sliced(from: start, to: end)?.value as? [CLLocationCoordinate2D] else {
return nil
}

let zippedCoordinates = zip(slicedCoordinates.prefix(upTo: slicedCoordinates.count - 1), slicedCoordinates.suffix(from: 1))
return zippedCoordinates.map { $0.distance(to: $1) }.reduce(0, +)
}

/// Returns a subset of the `.LineString` between given coordinates.
/// If current enum case is not `.LineString` - always returns `nil` instead
///
/// Ported from https://github.com/Turfjs/turf/blob/142e137ce0c758e2825a260ab32b24db0aa19439/packages/turf-line-slice/index.js
public func sliced(from start: CLLocationCoordinate2D? = nil, to end: CLLocationCoordinate2D? = nil) -> _Geometry? {
guard case let .LineString(coordinates: coordinates) = self else {
return nil
}
guard !coordinates.isEmpty else {
return .LineString(coordinates: [])
}

let startVertex = (start != nil ? closestCoordinate(to: start!) : nil) ?? IndexedCoordinate(coordinate: coordinates.first!, index: 0, distance: 0)
let endVertex = (end != nil ? closestCoordinate(to: end!) : nil) ?? IndexedCoordinate(coordinate: coordinates.last!, index: coordinates.indices.last!, distance: 0)
let ends: (IndexedCoordinate, IndexedCoordinate)
if startVertex.index <= endVertex.index {
ends = (startVertex, endVertex)
} else {
ends = (endVertex, startVertex)
}

var coords = ends.0.index == ends.1.index ? [] : Array(coordinates[ends.0.index + 1...ends.1.index])
coords.insert(ends.0.coordinate, at: 0)
if coords.last != ends.1.coordinate {
coords.append(ends.1.coordinate)
}

return .LineString(coordinates: coords)
}

/// Returns the geographic coordinate along the `.LineString` that is closest to the given coordinate as the crow flies.
/// The returned coordinate may not correspond to one of the polyline’s vertices, but it always lies along the polyline.
/// If current enum case is not `.LineString` - always returns `nil` instead
///
/// Ported from https://github.com/Turfjs/turf/blob/142e137ce0c758e2825a260ab32b24db0aa19439/packages/turf-point-on-line/index.js

public func closestCoordinate(to coordinate: CLLocationCoordinate2D) -> IndexedCoordinate? {
guard case let .LineString(coordinates: coordinates) = self, !coordinates.isEmpty else {
return nil
}
guard coordinates.count > 1 else {
return IndexedCoordinate(coordinate: coordinates.first!, index: 0, distance: coordinate.distance(to: coordinates.first!))
}

var closestCoordinate: IndexedCoordinate?

for index in 0..<coordinates.count - 1 {
let segment = (coordinates[index], coordinates[index + 1])
let distances = (coordinate.distance(to: segment.0), coordinate.distance(to: segment.1))

let maxDistance = max(distances.0, distances.1)
let direction = segment.0.direction(to: segment.1)
let perpendicularPoint1 = coordinate.coordinate(at: maxDistance, facing: direction + 90)
let perpendicularPoint2 = coordinate.coordinate(at: maxDistance, facing: direction - 90)
let intersectionPoint = Turf.intersection((perpendicularPoint1, perpendicularPoint2), segment)
let intersectionDistance: CLLocationDistance? = intersectionPoint != nil ? coordinate.distance(to: intersectionPoint!) : nil

if distances.0 < closestCoordinate?.distance ?? .greatestFiniteMagnitude {
closestCoordinate = IndexedCoordinate(coordinate: segment.0, index: index, distance: distances.0)
}
if distances.1 < closestCoordinate?.distance ?? .greatestFiniteMagnitude {
closestCoordinate = IndexedCoordinate(coordinate: segment.1, index: index+1, distance: distances.1)
}
if intersectionDistance != nil && intersectionDistance! < closestCoordinate?.distance ?? .greatestFiniteMagnitude {
closestCoordinate = IndexedCoordinate(coordinate: intersectionPoint!, index: index, distance: intersectionDistance!)
}
}

return closestCoordinate
}
}
2 changes: 2 additions & 0 deletions Sources/Turf/MultiLineString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import CoreLocation
/**
A `MultiLineString` geometry. The coordinates property represent a `[CLLocationCoordinate2D]` of two or more coordinates.
*/
@available(*, deprecated, message: "Use `Geometry.MultiLineString` instead.")
public struct MultiLineString: Codable, Equatable {
var type: String = GeometryType.MultiLineString.rawValue
public var coordinates: [[CLLocationCoordinate2D]]
Expand All @@ -20,6 +21,7 @@ public struct MultiLineString: Codable, Equatable {
}
}

@available(*, deprecated, message: "Use `Feature` instead.")
public struct MultiLineStringFeature: GeoJSONObject {
public var type: FeatureType = .feature
public var identifier: FeatureIdentifier?
Expand Down
2 changes: 2 additions & 0 deletions Sources/Turf/MultiPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import CoreLocation
/**
A `MultiPint` geometry. The coordinates property represents a `[CLLocationCoordinate2D]`.
*/
@available(*, deprecated, message: "Use `Geometry.MultiPoint` instead.")
public struct MultiPoint: Codable, Equatable {
var type: String = GeometryType.MultiPoint.rawValue
public var coordinates: [CLLocationCoordinate2D]
Expand All @@ -16,6 +17,7 @@ public struct MultiPoint: Codable, Equatable {
}
}

@available(*, deprecated, message: "Use `Feature` instead.")
public struct MultiPointFeature: GeoJSONObject {
public var type: FeatureType = .feature
public var identifier: FeatureIdentifier?
Expand Down
2 changes: 2 additions & 0 deletions Sources/Turf/MultiPolygon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import CoreLocation
/**
A `MultiLineString` geometry. The coordinates property represents a `[LineString]`.
*/
@available(*, deprecated, message: "Use `Geometry.MultiPolygon` instead.")
public struct MultiPolygon: Codable, Equatable {
var type: String = GeometryType.MultiPolygon.rawValue
public var coordinates: [[[CLLocationCoordinate2D]]]
Expand All @@ -16,6 +17,7 @@ public struct MultiPolygon: Codable, Equatable {
}
}

@available(*, deprecated, message: "Use `Feature` instead.")
public struct MultiPolygonFeature: GeoJSONObject {
public var type: FeatureType = .feature
public var identifier: FeatureIdentifier?
Expand Down
2 changes: 2 additions & 0 deletions Sources/Turf/Point.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import CoreLocation
/**
A `Point` geometry. The `coordinates` property represents a single position.
*/
@available(*, deprecated, message: "Use `Geometry.Point` instead.")
public struct Point: Codable, Equatable {
var type: String = GeometryType.Point.rawValue
public var coordinates: CLLocationCoordinate2D
Expand All @@ -16,6 +17,7 @@ public struct Point: Codable, Equatable {
}
}

@available(*, deprecated, message: "Use `Feature` instead.")
public struct PointFeature: GeoJSONObject {
public var type: FeatureType = .feature
public var identifier: FeatureIdentifier?
Expand Down
Loading

0 comments on commit ecaa6fa

Please sign in to comment.