Skip to content

Latest commit

 

History

History
251 lines (211 loc) · 24.6 KB

VERSION_5.md

File metadata and controls

251 lines (211 loc) · 24.6 KB

Version 5

Introduction

GEOSwift was started in May 2015 by Andrea Cremaschi. At this time Swift 1.2 had been out for just over a month. Over the years, the library and Swift both grew and evolved, and new features in Swift like the error handling model (Swift 2) and Codable (Swift 4) started to make GEOSwift feel a bit less Swifty than it did originally. Even early on, there was a desire for GEOSwift to use structs instead of classes, but the design did not allow it.

Additionally, other desires were difficult to address within the parameters of the original design:

  • Thread safety
  • Increased test coverage (version 4 was at 71%)
  • Support for general-purpose 2D geometry
  • Useful errors when GeoJSON decoding fails

A New Design

Version 5 brings a completely re-imagined, yet familiar, design to GEOSwift. The changes can be grouped into 3 major areas:

  1. Confine the use of GEOS
  2. Move map-based features into a separate library
  3. Adopt modern Swift features

Confine the use of GEOS

Prior to version 5, each GEOSwift object was backed by a GEOS object. This meant that GEOSwift objects needed to carefully manage C pointers and could not be safely shared across threads. This was also the reason why it used classes instead of structs. Furthermore, the library used a single, shared context for all interaction with GEOS. Even though it was using the thread-safe GEOS API, that API only achieves thread-safety by allowing you to create separate contexts per thread.

In version 5, GEOSwift represents all geometry types as Swift structs. GEOS is still used, but only as an implementation detail for certain methods. In fact, GEOS contexts never last beyond a single method call. This means that you can pass GEOSwift values between threads and threads will never share GEOS contexts, which, combined with the value semantics of struct, results in a thread-safe API.

Additionally, this approach of confining the use of GEOS within the library, opens up interesting future possibilities like splitting GEOS-dependent features into a separate library for people who would prefer to avoid the GEOS dependency or replacing GEOS-based implementations (which require careful error handling) with Swift-native implementations.

Move map-based features into a separate library

One of the appeals of GEOSwift 4 and earlier was its tight-knit integration with MapKit and QuickLook. These features made it fun for experimentation in playgrounds and quick to get something working in iOS projects.

Even so, there were other use cases that were made more difficult by the inclusion of these features. Some developers were interested in using GEOSwift for general-purpose geometry, but the QuickLook integration could crash if your geometry was outside of the typical bounds of latitude and longitude. Others wanted to use the library on Linux where UIKit and MapKit are not available.

To address these needs, version 5 moves the MapKit and UIKit-dependent features, including QuickLook and the demo playground, into a separate library called GEOSwiftMapKit. This allows us to broaden the usefulness of the library to include all kinds of general-purpose 2D geometry needs (not just geography). This is possible because GEOS is already a general-purpose geometry engine.

This change suggested that we ought to re-brand the library from "The Swift Geographic Engine" to "The Swift Geometry Engine".

Adopt modern Swift features

By adopting the Swift error handling model, version 5 reduces the use of optionals, and can now bubble up errors from GEOS in a more programmer-friendly way.

In adopting Codable, GEOSwift is now easy to combine with your app's own API request and response objects that need to handle GeoJSON. You also get more helpful error messages when decoding fails.

Common Questions

I decoded my data into GeoJSON. How do I access the values?

In keeping with the GeoJSON spec, the top-level object in GeoJSON data can either be a feature collection, a feature, or a geometry. GEOSwift's GeoJSON enum models this directly using enums with associated values, so when you decode to that type, you then need to see which top-level type you received:

let myGeoJSON = try decoder.decode(GeoJSON.self, from: data)

switch myGeoJSON {
case let .featureCollection(myFeatureCollection):
    // the variable myFeatureCollection is now bound to the associated value and is of type FeatureCollection
case let .feature(myFeature):
    // the variable myFeature is now bound to the associated value and is of type Feature
case let .geometry(myGeometry):
    // the variable myGeometry is now bound to the associated value and is of type Geometry
}

FeatureCollection and Feature are structs, but Geometry follows the same pattern as GeoJSON, so when you need to, you can access its associated values too using the same technique.

One nice thing about pattern matching is that you can compose patterns, so if, for example, you only cared about the case where the GeoJSON was a line string, you could write:

switch myGeoJSON {
case let .geometry(.lineString(myLineString)):
    // the variable myLineString is now bound to the associated value and is of type LineString
default:
    // your data was of some other type
}

In the above situation you could also use guard:

guard let .geometry(.lineString(myLineString)) = myGeoJSON else {
    // your data was of some other type
    return / throw / etc
}

// the variable myLineString is now bound to the associated value and is of type LineString

if works in a similar way.

One final gotcha about this is that if myGeoJSON is optional (type GeoJSON? instead of GeoJSON), you'll need to either unwrap it before you switch/guard/if or incorporate that into the pattern matching. Here's what that would look like in the previous example:

guard let .some(.geometry(.lineString(myLineString))) = mySomething else {
    // your data was of some other type
    return / throw / etc
}

// the variable myLineString is now bound to the associated data and is of type LineString

Summary of API Changes

Version 4 API Version 5 Equivalent Moved to GEOSwiftMapKit?
CLLocationCoordinate2D.init(_ coord: Coordinate) CLLocationCoordinate2D.init(_ point: Point) Yes
Coordinate Point
Coordinate.init(_ coord: CLLocationCoordinate2D) Point.init(_ coordinate: CLLocationCoordinate2D) Yes
CoordinateDegrees Double
CoordinatesCollection [Point]
Envelope Envelope
Envelope.bottomLeft: Coordinate Envelope.minXMinY: Point
Envelope.bottomRight: Coordinate Envelope.maxXMinY: Point
Envelope.byExpanding(_ base: Envelope, toInclude geom: Geometry) -> Envelope? GeometryCollection.init(geometries: [GeometryConvertible]).envelope() throws -> Envelope
Envelope.byExpanding(_ base: Envelope, toIncludeCoordinate coord: Coordinate) -> Envelope? GeometryCollection.init(geometries: [GeometryConvertible]).envelope() throws -> Envelope
Envelope.init?(p1: Coordinate, p2: Coordinate) GeometryCollection.init(geometries: [GeometryConvertible]).envelope() throws -> Envelope
Envelope.maxX: Double Envelope.maxX: Double
Envelope.maxY: Double Envelope.maxY: Double
Envelope.minX: Double Envelope.minX: Double
Envelope.minY: Double Envelope.minY: Double
Envelope.topLeft: Coordinate Envelope.minXMaxY: Point
Envelope.topRight: Coordinate Envelope.maxXMaxY: Point
Feature.geometries: [GEOSwift.Geometry]? Feature.geometry: Geometry?
Feature.id: Any? Feature.id: Feature.FeatureId?
Feature.properties: NSDictionary? Feature.properties: [String : JSON]?
Features FeatureCollection
Features.fromGeoJSON(_ data: Data) throws -> Features? FeatureCollection: Decodable
Features.fromGeoJSON(_ string: String) throws -> Features? FeatureCollection: Decodable
Features.fromGeoJSON(_ URL: URL) throws -> Features? FeatureCollection: Decodable
Features.fromGeoJSONDictionary(_ dictionary: [String : AnyObject]) -> Features? FeatureCollection: Decodable
GeometriesCollection [GeometryConvertible]
Geometry.area() -> Double? GeometryConvertible.area() throws -> Double
Geometry.boundary() -> Geometry? Boundable.boundary() throws -> Geometry
Geometry.buffer(width: Double) -> Geometry? GeometryConvertible.buffer(by width: Double) throws -> Geometry
Geometry.centroid() -> Waypoint? GeometryConvertible.centroid() throws -> Point
Geometry.contains(_ geometry: Geometry) -> Bool GeometryConvertible.contains(_ geometry: GeometryConvertible) throws -> Bool
Geometry.convexHull() -> Polygon? GeometryConvertible.convexHull() throws -> Geometry
Geometry.covers(_ geometry: Geometry) -> Bool GeometryConvertible.covers(_ geometry: GeometryConvertible) throws -> Bool
Geometry.create(_ WKB: UnsafePointer, size: Int) -> Geometry? WKBInitializable.init(wkb: Data) throws
Geometry.create(_ WKT: String) -> Geometry? WKTInitializable.init(wkt: String) throws
Geometry.crosses(_ geometry: Geometry) -> Bool GeometryConvertible.crosses(_ geometry: GeometryConvertible) throws -> Bool
Geometry.difference(_ geometry: Geometry) -> Geometry? GeometryConvertible.difference(with geometry: GeometryConvertible) throws -> Geometry
Geometry.disjoint(_ geometry: Geometry) -> Bool GeometryConvertible.isDisjoint(with geometry: GeometryConvertible) throws -> Bool
Geometry.distance(geometry: Geometry) -> Double GeometryConvertible.distance(to geometry: GeometryConvertible) throws -> Double
Geometry.envelope() -> Envelope? GeometryConvertible.envelope() throws -> Envelope
Geometry.equals(_ geometry: Geometry) -> Bool GeometryCollection.isTopologicallyEquivalent(to geometry: GeometryConvertible) throws -> Bool
Geometry.geometryTypeId() -> Int32 Removed
Geometry.init?(data: Data) WKBInitializable.init(wkb: Data) throws
Geometry.init?(WKB: [UInt8]) WKBInitializable.init(wkb: Data) throws
Geometry.init?(WKT: String) WKTInitializable.init(wkt: String) throws
Geometry.intersection(_ geometry: Geometry) -> Geometry? GeometryConvertible.intersection(with geometry: GeometryConvertible) throws -> Geometry
Geometry.intersects(_ geometry: Geometry) -> Bool GeometryConvertible.intersects(_ geometry: GeometryConvertible) throws -> Bool
Geometry.mapShape() -> MKShape? MKPointAnnotation.init(point: Point)
MKPlacemark.init(point: Point)
MKPolyline.init(lineString: LineString)
MKPolygon.init(linearRing: Polygon.LinearRing)
MKPolygon.init(polygon: Polygon)
GeometryMapShape.init(geometry: GeometryConvertible) throws
Yes
Geometry.nearestPoint(_ geometry: Geometry) -> Coordinate GeometryConvertible.nearestPoints(with geometry: GeometryConvertible)[0]
Geometry.nearestPoints(_ geometry: Geometry) -> [Coordinate] GeometryConvertible.nearestPoints(with geometry: GeometryConvertible) throws -> [Point]
Geometry.overlaps(_ geometry: Geometry) -> Bool GeometryConvertible.overlaps(_ geometry: GeometryConvertible) throws -> Bool
Geometry.pointOnSurface() -> Waypoint? GeometryConvertible.pointOnSurface() throws -> Point
Geometry.relate(_ geometry: Geometry, pattern: String) -> Bool GeometryConvertible.relate(_ geometry: GeometryConvertible, mask: String) throws -> Bool
Geometry.relationship(_ geometry: Geometry) -> String GeometryConvertible.relate(_ geometry: GeometryConvertible) throws -> String
Geometry.touches(_ geometry: Geometry) -> Bool GeometryConvertible.touches(_ geometry: GeometryConvertible) throws -> Bool
Geometry.unaryUnion() -> Geometry? GeometryConvertible.unaryUnion() throws -> Geometry
Geometry.union(_ geometry: Geometry) -> Geometry? GeometryConvertible.union(with geometry: GeometryConvertible) throws -> Geometry
Geometry.within(_ geometry: Geometry) -> Bool GeometryConvertible.isWithin(_ geometry: GeometryConvertible) throws -> Bool
Geometry.WKB: [UInt8]? WKBConvertible.wkb() throws -> Data
Geometry.WKT: String? WKTConvertible.wkt() throws -> String
GeometryCollection.init?(geometries: [T]) where T : Geometry GeometryCollection.init(geometries: [GeometryConvertible])
HumboldtVersionNumber GEOSwiftVersionNumber
LineString.distanceFromOriginToProjectionOfPoint(point: Waypoint) -> Double LineStringConvertible.distanceFromStart(toProjectionOf point: Point) throws -> Double
LineString.init?(points: [Coordinate]) LineString.init(points: [Point]) throws
LineString.interpolatePoint(distance: Double) -> Waypoint? LineStringConvertible.interpolatedPoint(withDistance distance: Double) throws -> Point
LineString.interpolatePoint(fraction: Double) -> Waypoint? LineStringConvertible.interpolatedPoint(withFraction fraction: Double) throws -> Point
LineString.middlePoint() -> Waypoint? LineStringConvertible.interpolatedPoint(withFraction: 0.5) throws -> Point
LineString.normalizedDistanceFromOriginToProjectionOfPoint(point: Waypoint) -> Double LineStringConvertible.normalizedDistanceFromStart(toProjectionOf point: Point) throws -> Double
MKShapesCollection GeometryMapShape Yes
Polygon.exteriorRing Polygon.exterior
Polygon.init?(shell: LinearRing, holes: [LinearRing]?) Polygon.init(exterior: Polygon.LinearRing, holes: [Polygon.LinearRing] = [])
Polygon.interiorRings Polygon.holes
Waypoint Point
Waypoint.coordinate: Coordinate Point
Waypoint.init?(latitude: CoordinateDegrees, longitude: CoordinateDegrees) Point.init(x: Double, y: Double)
Waypoint.init?(latitude: CoordinateDegrees, longitude: CoordinateDegrees) Point.init(longitude: Double, latitude: Double) Yes

What about Objective-C?

The new GEOSwift is all struct based, so to add ObjC support, we'd either need to switch back to using classes or create a set of wrapper classes. Neither of these options make much sense right now though: Switching to classes would lose many of the advantages of the new design, and it's not clear how we could make the MapKit and QuickLook add-ons work with both libraries. Initially it seemed like ObjC support was a must-have to match the features of the previous version, but after reviewing the existing ObjC interface, it turns out that there was never much ObjC support to begin with. If the community still wants this badly enough, we should consider spinning up a sister project (GEOObjC?) so that we can focus on making GEOSwift a truly first-rate experience for Swift projects.