Skip to content

Commit

Permalink
Merge pull request #154 from mapbox/1ec5-geojson-collection-conform-150
Browse files Browse the repository at this point in the history
Compliant, codable, equatable GeoJSON
  • Loading branch information
1ec5 authored Sep 28, 2021
2 parents c120ea5 + 4e7a387 commit 3f3fb5b
Show file tree
Hide file tree
Showing 31 changed files with 1,265 additions and 582 deletions.
48 changes: 39 additions & 9 deletions Sources/Turf/BoundingBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,21 @@ import Foundation
import CoreLocation
#endif

public struct BoundingBox: Codable {
/**
A [bounding box](https://datatracker.ietf.org/doc/html/rfc7946#section-5) indicates the extremes of a `GeoJSONObject` along the x- and y-axes (longitude and latitude, respectively).
*/
public struct BoundingBox {
/// The southwesternmost position contained in the bounding box.
public var southWest: LocationCoordinate2D

/// The northeasternmost position contained in the bounding box.
public var northEast: LocationCoordinate2D

/**
Initializes the smallest bounding box that contains all the given coordinates.

- parameter coordinates: The coordinates to fit in the bounding box.
*/
public init?(from coordinates: [LocationCoordinate2D]?) {
guard coordinates?.count ?? 0 > 0 else {
return nil
Expand All @@ -22,11 +35,24 @@ public struct BoundingBox: Codable {
northEast = LocationCoordinate2D(latitude: maxLat, longitude: maxLon)
}

/**
Initializes a bounding box defined by its southwesternmost and northeasternmost positions.

- parameter southWest: The southwesternmost position contained in the bounding box.
- parameter northEast: The northeasternmost position contained in the bounding box.
*/
public init(southWest: LocationCoordinate2D, northEast: LocationCoordinate2D) {
self.southWest = southWest
self.northEast = northEast
}

/**
Returns a Boolean value indicating whether the bounding box contains the given position.

- parameter coordinate: The coordinate that may or may not be contained by the bounding box.
- parameter ignoreBoundary: A Boolean value indicating whether a position lying exactly on the edge of the bounding box should be considered to be contained in the bounding box.
- returns: `true` if the bounding box contains the position; `false` otherwise.
*/
public func contains(_ coordinate: LocationCoordinate2D, ignoreBoundary: Bool = true) -> Bool {
if ignoreBoundary {
return southWest.latitude < coordinate.latitude
Expand All @@ -40,9 +66,18 @@ public struct BoundingBox: Codable {
&& northEast.longitude >= coordinate.longitude
}
}

// MARK: - Codable

}

extension BoundingBox: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(southWest.longitude)
hasher.combine(southWest.latitude)
hasher.combine(northEast.longitude)
hasher.combine(northEast.latitude)
}
}

extension BoundingBox: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(southWest.codableCoordinates)
Expand All @@ -54,9 +89,4 @@ public struct BoundingBox: Codable {
southWest = try container.decode(LocationCoordinate2DCodable.self).decodedCoordinates
northEast = try container.decode(LocationCoordinate2DCodable.self).decodedCoordinates
}

// MARK: - Properties

public var southWest: LocationCoordinate2D
public var northEast: LocationCoordinate2D
}
174 changes: 0 additions & 174 deletions Sources/Turf/Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,180 +3,6 @@ import Foundation
import CoreLocation
#endif


struct JSONCodingKeys: CodingKey {
var stringValue: String

init?(stringValue: String) {
self.stringValue = stringValue
}

var intValue: Int?

init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}

extension KeyedDecodingContainer {

public func decode(_ type: [String: Any?].Type, forKey key: K) throws -> [String: Any?] {
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}

public func decodeIfPresent(_ type: [String: Any?].Type, forKey key: K) throws -> [String: Any?]? {
guard contains(key) else {
return nil
}
return try decode(type, forKey: key)
}

public func decode(_ type: [Any?].Type, forKey key: K) throws -> [Any?] {
var container = try self.nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}

public func decodeIfPresent(_ type: [Any?].Type, forKey key: K) throws -> [Any?]? {
guard contains(key) else {
return nil
}
return try decode(type, forKey: key)
}

public func decode(_ type: [String: Any?].Type) throws -> [String: Any?] {
var dictionary = [String: Any?]()

for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode([String: Any?].self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode([Any?].self, forKey: key) {
dictionary[key.stringValue] = nestedArray
} else if (try? decodeNil(forKey: key)) ?? false {
dictionary[key.stringValue] = .none
}
}
return dictionary
}
}

extension UnkeyedDecodingContainer {

public mutating func decode(_ type: [Any?].Type) throws -> [Any?] {
var array: [Any?] = []
while isAtEnd == false {
if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(Int.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode([String: Any?].self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode([Any?].self) {
array.append(nestedArray)
} else if (try? decodeNil()) ?? false {
array.append(.none)
}
}
return array
}

public mutating func decode(_ type: [String: Any?].Type) throws -> [String: Any?] {

let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
return try nestedContainer.decode(type)
}
}

extension KeyedEncodingContainerProtocol {

public mutating func encodeIfPresent(_ value: [String: Any?]?, forKey key: Self.Key) throws {
guard let value = value else { return }
return try encode(value, forKey: key)
}

public mutating func encode(_ value: [String: Any?], forKey key: Key) throws {
var container = self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
try container.encode(value)
}

public mutating func encodeIfPresent(_ value: [Any?]?, forKey key: Self.Key) throws {
guard let value = value else { return }
return try encode(value, forKey: key)
}

public mutating func encode(_ value: [Any?], forKey key: Key) throws {
var container = self.nestedUnkeyedContainer(forKey: key)
try container.encode(value)
}

public mutating func encode(_ value: [String: Any?]) throws {
try value.forEach({ (key, value) in
guard let key = Key(stringValue: key) else { return }
switch value {
case let value as Bool:
try encode(value, forKey: key)
case let value as Int:
try encode(value, forKey: key)
case let value as String:
try encode(value, forKey: key)
case let value as Double:
try encode(value, forKey: key)
case let value as [String: Any?]:
try encode(value, forKey: key)
case let value as Array<Any>:
try encode(value, forKey: key)
case Optional<Any>.none:
try encodeNil(forKey: key)
default:
return
}
})
}
}

extension UnkeyedEncodingContainer {
public mutating func encode(_ value: [String: Any?]) throws {
var nestedContainer = self.nestedContainer(keyedBy: JSONCodingKeys.self)
try nestedContainer.encode(value)
}

public mutating func encode(_ value: [Any?]) throws {
try value.enumerated().forEach({ (index, value) in
switch value {
case let value as Bool:
try encode(value)
case let value as Int:
try encode(value)
case let value as String:
try encode(value)
case let value as Double:
try encode(value)
case let value as [String: Any?]:
try encode(value)
case let value as Array<Any>:
try encode(value)
case Optional<Any>.none:
try encodeNil()
default:
return
}
})
}
}

extension Ring: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
Expand Down
38 changes: 23 additions & 15 deletions Sources/Turf/Feature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,43 @@ import Foundation
import CoreLocation
#endif


public struct Feature: GeoJSONObject {
public var type: FeatureType = .feature
/**
A [Feature object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2) represents a spatially bounded thing.
*/
public struct Feature: Equatable {
public var identifier: FeatureIdentifier?
public var properties: [String : Any?]?
public var geometry: Geometry
public var properties: JSONObject?
public var geometry: Geometry?

public init(geometry: Geometry?) {
self.geometry = geometry
}
}

extension Feature: Codable {
private enum CodingKeys: String, CodingKey {
case type
case geometry
case properties
case identifier = "id"
case kind = "type"
case geometry
case properties
case identifier = "id"
}

public init(geometry: Geometry) {
self.geometry = geometry
enum Kind: String, Codable {
case Feature
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
geometry = try container.decode(Geometry.self, forKey: .geometry)
properties = try container.decodeIfPresent([String: Any?].self, forKey: .properties)
_ = try container.decode(Kind.self, forKey: .kind)
geometry = try container.decodeIfPresent(Geometry.self, forKey: .geometry)
properties = try container.decodeIfPresent(JSONObject.self, forKey: .properties)
identifier = try container.decodeIfPresent(FeatureIdentifier.self, forKey: .identifier)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type.rawValue, forKey: .type)
try container.encodeIfPresent(geometry, forKey: .geometry)
try container.encode(Kind.Feature, forKey: .kind)
try container.encode(geometry, forKey: .geometry)
try container.encodeIfPresent(properties, forKey: .properties)
try container.encodeIfPresent(identifier, forKey: .identifier)
}
Expand Down
29 changes: 16 additions & 13 deletions Sources/Turf/FeatureCollection.swift
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
import Foundation


public struct FeatureCollection: GeoJSONObject {
public let type: FeatureType = .featureCollection
public var identifier: FeatureIdentifier?
/**
A [FeatureCollection object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.3) is a collection of Feature objects.
*/
public struct FeatureCollection: Equatable {
public var features: Array<Feature> = []
public var properties: [String : Any?]?

public init(features: [Feature]) {
self.features = features
}
}

extension FeatureCollection: Codable {
private enum CodingKeys: String, CodingKey {
case type
case properties
case kind = "type"
case features
}

public init(features: [Feature]) {
self.features = features
enum Kind: String, Codable {
case FeatureCollection
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.features = try container.decode([Feature].self, forKey: .features)
self.properties = try container.decodeIfPresent([String: Any?].self, forKey: .properties)
_ = try container.decode(Kind.self, forKey: .kind)
features = try container.decode([Feature].self, forKey: .features)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type, forKey: .type)
try container.encode(Kind.FeatureCollection, forKey: .kind)
try container.encode(features, forKey: .features)
try container.encodeIfPresent(properties, forKey: .properties)
}
}
Loading

0 comments on commit 3f3fb5b

Please sign in to comment.