Skip to content

A collection of GIS tools, including a GeoJSON implementation with projection support and WKB/WKT coders as well as many algorithms ported from turf.js

License

Notifications You must be signed in to change notification settings

Outdooractive/gis-tools

Repository files navigation






GISTools

GIS tools for Swift, including a GeoJSON implementation and many algorithms ported from https://turfjs.org.

Features

  • Supports the full GeoJSON standard, with some exceptions (see TODO.md)
  • Load and write GeoJSON objects from and to [String:Any], URL, Data and String
  • Supports Codable and SwiftData (see below)
  • Supports EPSG:3857 (web mercator) and EPSG:4326 (geodetic) conversions
  • Supports WKT/WKB, also with different projections
  • Spatial search with a R-tree
  • Includes many spatial algorithms (ported from turf.js), and more to come
  • Has a helper for working with x/y/z map tiles (center/bounding box/resolution/…)
  • Can encode/decode Polylines
  • Pure Swift without external dependencies
  • Swift 6 ready

Notes

This package makes some assumptions about what is equal, i.e. coordinates that are inside of 1e-10 degrees are regarded as equal (that's μm precision and is probably overkill). See GISTool.equalityDelta.

Requirements

This package requires Swift 5.10 or higher (at least Xcode 14), and compiles on iOS (>= iOS 13), macOS (>= macOS 10.15), tvOS (>= tvOS 13), watchOS (>= watchOS 6) as well as Linux.

Installation with Swift Package Manager

dependencies: [
    .package(url: "https://github.com/Outdooractive/gis-tools", from: "1.8.2"),
],
targets: [
    .target(name: "MyTarget", dependencies: [
        .product(name: "GISTools", package: "gis-tools"),
    ]),
]

Usage

Please see also the API documentation (via Swift Package Index).

import GISTools

var feature = Feature(Point(Coordinate3D(latitude: 3.870163, longitude: 11.518585)))
feature.properties = [
    "test": 1,
    "test2": 5.567,
    "test3": [1, 2, 3],
    "test4": [
        "sub1": 1,
        "sub2": 2
    ]
]

// To and from String:
let jsonString = feature.asJsonString(prettyPrinted: true)
let feature = Feature(jsonString: jsonString)

// To and from Data:
let jsonData = feature.asJsonData(prettyPrinted: true)
let feature = Feature(jsonData: jsonData)

// Using Codable:
let jsonData = try JSONEncoder().encode(feature)
let feature = try JSONDecoder().decode(Feature.self, from: jsonData)

// Generic:
let someGeoJson = GeoJsonReader.geoJsonFrom(json: [
    "type": "Point",
    "coordinates": [100.0, 0.0],
])
let someGeoJson = GeoJsonReader.geoJsonFrom(contentsOf: URL(...))
let someGeoJson = GeoJsonReader.geoJsonFrom(jsonData: Data(...))
let someGeoJson = GeoJsonReader.geoJsonFrom(jsonString: "{\"type\":\"Point\",\"coordinates\":[100.0,0.0]}")

switch someGeoJson {
case let point as Point: ...
}
// or
switch someGeoJson.type {
case .point: ...
}

// Wraps *any* GeoJSON into a FeatureCollection
let featureCollection = FeatureCollection(jsonData: someData)
let featureCollection = try JSONDecoder().decode(FeatureCollection.self, from: someData)

...

See the tests for more examples and also the API documentation.

GeoJSON

To quote from the RFC 7946:

GeoJSON is a geospatial data interchange format based on JavaScript Object Notation (JSON).
It defines several types of JSON objects and the manner in which they are combined to represent data about geographic features, their properties, and their spatial extents.
GeoJSON uses a geographic coordinate reference system, World Geodetic System 1984, and units of decimal degrees.

Please read the RFC first to get an overview of what GeoJSON is and is not (in the somewhat unlikely case that you don’t already know all of this… 🙂).

GeoJson protocol

Implementation

The basics for every GeoJSON object:

/// All permitted GeoJSON types.
public enum GeoJsonType: String {
    case point              = "Point"
    case multiPoint         = "MultiPoint"
    case lineString         = "LineString"
    case multiLineString    = "MultiLineString"
    case polygon            = "Polygon"
    case multiPolygon       = "MultiPolygon"
    case geometryCollection = "GeometryCollection"
    case feature            = "Feature"
    case featureCollection  = "FeatureCollection"
}

/// GeoJSON object type.
var type: GeoJsonType { get }

/// The GeoJSON's projection, which should typically be EPSG:4326.
var projection: Projection { get }

/// All of the receiver's coordinates.
var allCoordinates: [Coordinate3D] { get }

/// Any foreign members, i.e. keys in the JSON that are
/// not part of the GeoJSON standard.
var foreignMembers: [String: Any] { get set }

/// Try to initialize a GeoJSON object from any JSON and calculate a bounding box if necessary.
init?(json: Any?, calculateBoundingBox: Bool)

/// Type erased equality check.
func isEqualTo(_ other: GeoJson) -> Bool

BoundingBoxRepresentable protocol

Implementation

All GeoJSON objects may have a bounding box. It is required though if you want to use the R-tree spatial index (see below).

/// The GeoJSON's projection.
var projection: Projection { get }

/// The receiver's bounding box.
var boundingBox: BoundingBox? { get set }

/// Calculates and returns the receiver's bounding box.
func calculateBoundingBox() -> BoundingBox?

/// Calculates the receiver's bounding box and updates the `boundingBox` property.
///
/// - parameter ifNecessary: Only update the bounding box if the receiver doesn't already have one.
@discardableResult
mutating func updateBoundingBox(onlyIfNecessary ifNecessary: Bool) -> BoundingBox?

/// Check if the receiver is inside or crosses  the other bounding box.
///
/// - parameter otherBoundingBox: The bounding box to check.
func intersects(_ otherBoundingBox: BoundingBox) -> Bool

GeoJsonConvertible protocol / GeoJsonCodable

Implementation

GeoJSON objects can be initialized from a variety of sources:

/// Try to initialize a GeoJSON object from any JSON.
init?(json: Any?)

/// Try to initialize a GeoJSON object from a file.
init?(contentsOf url: URL)

/// Try to initialize a GeoJSON object from a data object.
init?(jsonData: Data)

/// Try to initialize a GeoJSON object from a string.
init?(jsonString: String)

/// Try to initialize a GeoJSON object from a Decoder.
init(from decoder: Decoder) throws

They can also be exported in several ways:

/// Return the GeoJson object as Key/Value pairs.
var asJson: [String: Any] { get }

/// Dump the object as JSON data.
func asJsonData(prettyPrinted: Bool = false) -> Data?

/// Dump the object as a JSON string.
func asJsonString(prettyPrinted: Bool = false) -> String?

/// Write the object in it's JSON represenation to a file.
func write(to url: URL, prettyPrinted: Bool = false) throws

/// Write the GeoJSON object to an Encoder.
func encode(to encoder: Encoder) throws

Example:

let point = Point(jsonString: "{\"type\":\"Point\",\"coordinates\":[100.0,0.0]}")!
print(point.allCoordinates)
print(point.asJsonString(prettyPrinted: true)!)

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(point)

// This works because `FeatureCollection` will wrap any valid GeoJSON object.
// This is a good way to enforce a common structure for all loaded objects.
let featureCollection = FeatureCollection(jsonData: data)!

Important note: Import and export will always be done in EPSG:4326, with one exception: GeoJSON objects with no SRID will be exported as-is.

GeoJsonReader

Implementation

This is a generic way to create GeoJSON objects from anything that looks like GeoJSON:

/// Try to initialize a GeoJSON object from any JSON.
static func geoJsonFrom(json: Any?) -> GeoJson?

/// Try to initialize a GeoJSON object from a file.
static func geoJsonFrom(contentsOf url: URL) -> GeoJson?

/// Try to initialize a GeoJSON object from a data object.
static func geoJsonFrom(jsonData: Data) -> GeoJson?

/// Try to initialize a GeoJSON object from a string.
static func geoJsonFrom(jsonString: String) -> GeoJson?

Example:

let json: [String: Any] = [
    "type": "Point",
    "coordinates": [100.0, 0.0],
    "other": "something",
]
let geoJson = GeoJsonReader.geoJsonFrom(json: json)!
print("Type is \(geoJson.type.rawValue)")
print("Foreign members: \(geoJson.foreignMembers)")

switch geoJson {
case let point as Point:
    print("It's a Point!")
case let multiPoint as MultiPoint:
    print("It's a MultiPoint!")
case let lineString as LineString:
    print("It's a LineString!")
case let multiLineString as MultiLineString:
    print("It's a MultiLineString!")
case let polygon as Polygon:
    print("It's a Polygon!")
case let multiPolygon as MultiPolygon:
    print("It's a MultiPolygon!")
case let geometryCollection as GeometryCollection:
    print("It's a GeometryCollection!")
case let feature as Feature:
    print("It's a Feature!")
case let featureCollection as FeatureCollection:
    print("It's a FeatureCollection!")
default: 
    assertionFailure("Missed an object type?")
}

Important note: Import will always be done in EPSG:4326.

Coordinate3D

Implementation / Coordinate test cases

Coordinates are the most basic building block in this package. Every object and algorithm builds on them:

/// The coordinates projection, either EPSG:4326 or EPSG:3857.
let projection: Projection

/// The coordinate's `latitude`.
var latitude: CLLocationDegrees
/// The coordinate's `longitude`.
var longitude: CLLocationDegrees
/// The coordinate's `altitude`.
var altitude: CLLocationDistance?

/// Linear referencing, timestamp or whatever you want it to use for.
///
/// The GeoJSON specification doesn't specifiy the meaning of this value,
/// and it doesn't guarantee that parsers won't ignore or discard it. See
/// https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.1.
/// - Important: The JSON for a coordinate will contain a `null` altitude value
///              if `altitude` is `nil` so that `m` won't get lost (since it is
///              the 4th value).
///              This might lead to compatibilty issues with other GeoJSON readers.
var m: Double?

/// Alias for longitude
var x: Double { longitude }

/// Alias for latitude
var y: Double { latitude }

/// Create a coordinate with `latitude`, `longitude`, `altitude` and `m`.
/// Projection will be EPSG:4326.
init(latitude: CLLocationDegrees,
     longitude: CLLocationDegrees,
     altitude: CLLocationDistance? = nil,
     m: Double? = nil)

/// Create a coordinate with ``x``, ``y``, ``z`` and ``m``.
/// Default projection will we EPSG:3857 but can be overridden.
init(
    x: Double,
    y: Double,
    z: Double? = nil,
    m: Double? = nil,
    projection: Projection = .epsg3857)

/// Reproject this coordinate.
func projected(to newProjection: Projection) -> Coordinate3D

Example:

let coordinate = Coordinate3D(latitude: 0.0, longitude: 0.0)
print(coordinate.isZero)

BoundingBox

Implementation / BoundingBox test cases

Each GeoJSON object can have a rectangular BoundingBox (see BoundingBoxRepresentable above):

/// The bounding box's `projection`.
let projection: Projection

/// The bounding boxes south-west (bottom-left) coordinate.
var southWest: Coordinate3D
/// The bounding boxes north-east (upper-right) coordinate.
var northEast: Coordinate3D

/// Create a bounding box with a `southWest` and `northEast` coordinate.
init(southWest: Coordinate3D, northEast: Coordinate3D)

/// Create a bounding box from `coordinates` and an optional padding in kilometers.
init?(coordinates: [Coordinate3D], paddingKilometers: Double = 0.0)

/// Create a bounding box from other bounding boxes.
init?(boundingBoxes: [BoundingBox])

/// Reproject this bounding box.
func projected(to newProjection: Projection) -> BoundingBox

Example:

let point = Point(Coordinat3D(latitude: 47.56, longitude: 10.22), calculateBoundingBox: true)
print(point.boundingBox!)

Point

Implementation / Point test cases

A Point is a wrapper around a single coordinate:

/// The receiver's coordinate.
let coordinate: Coordinate3D

/// Initialize a Point with a coordinate.
init(_ coordinate: Coordinate3D, calculateBoundingBox: Bool = false)

/// Reproject the Point.
func projected(to newProjection: Projection) -> Point

Example:

let point = Point(Coordinate3D(latitude: 47.56, longitude: 10.22))

MultiPoint

Implementation / MultiPoint test cases

A MultiPoint is an array of coordinates:

/// The receiver's coordinates.
let coordinates: [Coordinate3D]

/// The receiver’s coordinates converted to Points.
var points: [Point]

/// Try to initialize a MultiPoint with some coordinates.
init?(_ coordinates: [Coordinate3D], calculateBoundingBox: Bool = false)

/// Try to initialize a MultiPoint with some Points.
init?(_ points: [Point], calculateBoundingBox: Bool = false)

/// Reproject the MultiPoint.
func projected(to newProjection: Projection) -> MultiPoint

Example:

let multiPoint = MultiPoint([
    Coordinate3D(latitude: 0.0, longitude: 100.0),
    Coordinate3D(latitude: 1.0, longitude: 101.0)
])!

LineString

Implementation / LineString test cases

LineString is an array of two or more coordinates that form a line:

/// The LineString's coordinates.
let coordinates: [Coordinate3D]

/// Try to initialize a LineString with some coordinates.
init?(_ coordinates: [Coordinate3D], calculateBoundingBox: Bool = false)

/// Initialize a LineString with a LineSegment.
init(_ lineSegment: LineSegment, calculateBoundingBox: Bool = false)

/// Try to initialize a LineString with some LineSegments.
init?(_ lineSegments: [LineSegment], calculateBoundingBox: Bool = false)

/// Reproject the LineString.
func projected(to newProjection: Projection) -> LineString

Example:

let lineString = LineString([
    Coordinate3D(latitude: 0.0, longitude: 100.0),
    Coordinate3D(latitude: 1.0, longitude: 101.0)
])!

let segment = LineSegment(
    first: Coordinate3D(latitude: 0.0, longitude: 100.0),
    second: Coordinate3D(latitude: 1.0, longitude: 101.0))
let lineString = LineString(lineSegment)

MultiLineString

Implementation / MultiLineString test cases

A MultiLineString is array of LineStrings:

/// The MultiLineString's coordinates.
let coordinates: [[Coordinate3D]]

/// The receiver’s coordinates converted to LineStrings.
var lineStrings: [LineString]

/// Try to initialize a MultiLineString with some coordinates.
init?(_ coordinates: [[Coordinate3D]], calculateBoundingBox: Bool = false)

/// Try to initialize a MultiLineString with some LineStrings.
init?(_ lineStrings: [LineString], calculateBoundingBox: Bool = false)

/// Try to initialize a MultiLineString with some LineSegments. Each LineSegment will result in one LineString.
init?(_ lineSegments: [LineSegment], calculateBoundingBox: Bool = false)

/// Reproject the MultiLineString.
func projected(to newProjection: Projection) -> MultiLineString

Example:

let multiLineString = MultiLineString([
    [Coordinate3D(latitude: 0.0, longitude: 100.0), Coordinate3D(latitude: 1.0, longitude: 101.0)],
    [Coordinate3D(latitude: 2.0, longitude: 102.0), Coordinate3D(latitude: 3.0, longitude: 103.0)],
])!

Polygon

Implementation / Polygon test cases

A Polygon is a shape consisting of one or more rings, where the first ring is the outer ring bounding the surface, and the inner rings bound holes within the surface. Please see section 3.1.6 in the RFC for more information.

/// The receiver's coordinates.
let coordinates: [[Coordinate3D]]

/// The receiver's outer ring.
var outerRing: Ring?

/// All of the receiver's inner rings.
var innerRings: [Ring]?

/// All of the receiver's rings (outer + inner).
var rings: [Ring]

/// Try to initialize a Polygon with some coordinates.
init?(_ coordinates: [[Coordinate3D]], calculateBoundingBox: Bool = false)

/// Try to initialize a Polygon with some Rings.
init?(_ rings: [Ring], calculateBoundingBox: Bool = false)

/// Reproject the Polygon.
func projected(to newProjection: Projection) -> Polygon

Example:

let polygonWithHole = Polygon([
    [
        Coordinate3D(latitude: 0.0, longitude: 100.0),
        Coordinate3D(latitude: 0.0, longitude: 101.0),
        Coordinate3D(latitude: 1.0, longitude: 101.0),
        Coordinate3D(latitude: 1.0, longitude: 100.0),
        Coordinate3D(latitude: 0.0, longitude: 100.0)
    ],
    [
        Coordinate3D(latitude: 1.0, longitude: 100.8),
        Coordinate3D(latitude: 0.0, longitude: 100.8),
        Coordinate3D(latitude: 0.0, longitude: 100.2),
        Coordinate3D(latitude: 1.0, longitude: 100.2),
        Coordinate3D(latitude: 1.0, longitude: 100.8)
    ],
])!
print(polygonWithHole.area)

MultiPolygon

Implementation / MultiPolygon test cases

A MultiPolygon is an array of Polygons:

/// The receiver's coordinates.
let coordinates: [[[Coordinate3D]]]

/// The receiver’s coordinates converted to Polygons.
var polygons: [Polygon]

/// Try to initialize a MultiPolygon with some coordinates.
init?(_ coordinates: [[[Coordinate3D]]], calculateBoundingBox: Bool = false)

/// Try to initialize a MultiPolygon with some Polygons.
init?(_ polygons: [Polygon], calculateBoundingBox: Bool = false)

/// Reproject the MultiPolygon.
func projected(to newProjection: Projection) -> MultiPolygon

Example:

let multiPolygon = MultiPolygon([
    [
        [
            Coordinate3D(latitude: 2.0, longitude: 102.0),
            Coordinate3D(latitude: 2.0, longitude: 103.0),
            Coordinate3D(latitude: 3.0, longitude: 103.0),
            Coordinate3D(latitude: 3.0, longitude: 102.0),
            Coordinate3D(latitude: 2.0, longitude: 102.0),
        ]
    ],
    [
        [
            Coordinate3D(latitude: 0.0, longitude: 100.0),
            Coordinate3D(latitude: 0.0, longitude: 101.0),
            Coordinate3D(latitude: 1.0, longitude: 101.0),
            Coordinate3D(latitude: 1.0, longitude: 100.0),
            Coordinate3D(latitude: 0.0, longitude: 100.0),
        ],
        [
            Coordinate3D(latitude: 0.0, longitude: 100.2),
            Coordinate3D(latitude: 1.0, longitude: 100.2),
            Coordinate3D(latitude: 1.0, longitude: 100.8),
            Coordinate3D(latitude: 0.0, longitude: 100.8),
            Coordinate3D(latitude: 0.0, longitude: 100.2),
        ]
    ]
])!

GeometryCollection

Implementation / GeometryCollection test cases

A GeometryCollection is an array of GeoJSON geometries, i.e. Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon or even GeometryCollection, though the latter is not recommended. Please see section 3.1.8 in the RFC for more information.

/// The GeometryCollection's geometry objects.
let geometries: [GeoJsonGeometry]

/// Initialize a GeometryCollection with a geometry object.
init(_ geometry: GeoJsonGeometry, calculateBoundingBox: Bool = false)

/// Initialize a GeometryCollection with some geometry objects.
init(_ geometries: [GeoJsonGeometry], calculateBoundingBox: Bool = false)

/// Reproject the GeometryCollection.
func projected(to newProjection: Projection) -> GeometryCollection

Feature

Implementation / Feature test cases

A Feature is sort of a container for exactly one GeoJSON geometry (Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection) together with some properties and an optional id:

/// A GeoJSON identifier that can either be a string or number.
/// Any parsed integer value `Int64.min ⪬ i ⪬ Int64.max`  will be cast to `Int`
/// (or `Int64` on 32-bit platforms), values above `Int64.max` will be cast to `UInt`
/// (or `UInt64` on 32-bit platforms).
enum Identifier: Equatable, Hashable, CustomStringConvertible {
    case string(String)
    case int(Int)
    case uint(UInt)
    case double(Double)
}

/// An arbitrary identifier.
var id: Identifier?

/// The `Feature`s geometry object.
let geometry: GeoJsonGeometry

/// Only 'Feature' objects may have properties.
var properties: [String: Any]

/// Create a ``Feature`` from any ``GeoJsonGeometry`` object.
init(_ geometry: GeoJsonGeometry,
     id: Identifier? = nil,
     properties: [String: Any] = [:],
     calculateBoundingBox: Bool = false)

/// Reproject the Feature.
func projected(to newProjection: Projection) -> Feature

FeatureCollection

Implementation / FeatureCollection test cases

A FeatureCollection is an array of Feature objects:

/// The FeatureCollection's Feature objects.
private(set) var features: [Feature]

/// Initialize a FeatureCollection with one Feature.
init(_ feature: Feature, calculateBoundingBox: Bool = false)

/// Initialize a FeatureCollection with some geometry objects.
init(_ geometries: [GeoJsonGeometry], calculateBoundingBox: Bool = false)

/// Normalize any GeoJSON object into a FeatureCollection.
init?(_ geoJson: GeoJson?, calculateBoundingBox: Bool = false)

/// Reproject the FeatureCollection.
func projected(to newProjection: Projection) -> FeatureCollection

This type is somewhat special since its initializers will accept any valid GeoJSON object and return a FeatureCollection with the input wrapped in Feature objects if the input are geometries, or by collecting the input if it’s a Feature.

SwiftData

You need to use a transformer for using GeoJson with SwiftData (also have a look at the SwiftData test cases).

First, register the transformer like this:

GeoJsonTransformer.register()

Then create your models like this:

@Attribute(.transformable(by: GeoJsonTransformer.name.rawValue)) var geoJson: GeoJson?
@Attribute(.transformable(by: GeoJsonTransformer.name.rawValue)) var point: Point?
...

This is necessary because SwiftData doesn't work well with the default Codable implementation, so you need to do the serialization for yourself...

WKB/WKT

The following geometry types are supported: point, linestring, linearring, polygon, multipoint, multilinestring, multipolygon, geometrycollection and triangle. Please open an issue if you need more.

Every GeoJSON object has convenience methods to encode and decode themselves to and from WKB/WKT, and there are extensions for Data and String to decode from WKB and WKT to GeoJSON. In the end, they all forward to WKBCoder and WKTCoder which do the heavy lifting.

WKB

Also have a look at the WKB test cases.

Decoding:

// SELECT 'POINT Z (1 2 3)'::geometry;
private let pointZData = Data(hex: "0101000080000000000000F03F00000000000000400000000000000840")!

// Generic
let point = try WKBCoder.decode(wkb: pointData, sourceProjection: .epsg4326) as! Point
let point = pointZData.asGeoJsonGeometry(sourceProjection: .epsg4326) as! Point

// Or create the geometry directly
let point = Point(wkb: pointZData, sourceProjection: .epsg4326)!

// Or create a Feature that contains the geometry
let feature = Feature(wkb: pointZData, sourceProjection: .epsg4326)
let feature = pointZData.asFeature(sourceProjection: .epsg4326)

// Or create a FeatureCollection that contains a feature with the geometry
let featureCollection = FeatureCollection(wkb: pointZData, sourceProjection: .epsg4326)
let featureCollection = pointZData.asFeatureCollection(sourceProjection: .epsg4326)

// Can also reproject on the fly
let point = try WKBCoder.decode(
    wkb: pointData,
    sourceProjection: .epsg4326,
    targetProjection: .epsg3857
) as! Point
print(point.projection)

Encoding:

let point = Point(Coordinate3D(latitude: 0.0, longitude: 100.0))

// Generic
let encodedPoint = WKBCoder.encode(geometry: point, targetProjection: nil)

// Convenience
let encodedPoint = point.asWKB

WKT

This is exactly the same as WKB… Also have a look at the tests to see how it works: WKT test cases

Decoding:

private let pointZString = "POINT Z (1 2 3)"

// Generic
let point = try WKTCoder.decode(wkt: pointZString, sourceProjection: .epsg4326) as! Point
let point = pointZString.asGeoJsonGeometry(sourceProjection: .epsg4326) as! Point

// Or create the geometry directly
let point = Point(wkt: pointZString, sourceProjection: .epsg4326)!

// Or create a Feature that contains the geometry
let feature = Feature(wkt: pointZString, sourceProjection: .epsg4326)
let feature = pointZString.asFeature(sourceProjection: .epsg4326)

// Or create a FeatureCollection that contains a feature with the geometry
let featureCollection = FeatureCollection(wkt: pointZString, sourceProjection: .epsg4326)
let featureCollection = pointZString.asFeatureCollection(sourceProjection: .epsg4326)

// Can also reproject on the fly
let point = try WKTCoder.decode(
    wkt: pointZString,
    sourceProjection: .epsg4326,
    targetProjection: .epsg3857
) as! Point
print(point.projection) // EPSG:3857

Encoding:

let point = Point(Coordinate3D(latitude: 0.0, longitude: 100.0))

// Generic
let encodedPoint = WKTCoder.encode(geometry: point, targetProjection: nil)

// Convenience
let encodedPoint = point.asWKT

Spatial index

This package includes a simple R-tree implementation: RTree test cases

var nodes: [Point] = []
50.times {
    nodes.append(Point(Coordinate3D(
        latitude: Double.random(in: -10.0 ... 10.0),
        longitude: Double.random(in: -10.0 ... 10.0))))
    }

let rTree = RTree(nodes)
let objects = rTree.search(inBoundingBox: boundingBox)
let objectsAround = rTree.search(aroundCoordinate: center, maximumDistance: maximumDistance)

MapTile

This is a helper for working with x/y/z map tiles.

let tile1 = MapTile(x: 138513, y: 91601, z: 18)
let center = tile1.centerCoordinate(projection: .epsg4326) // default
let boundingBox = tile1.boundingBox(projection: .epsg4326) // default

let tile2 = MapTile(coordinate: Coordinate3D(latitude: 47.56, longitude: 10.22), atZoom: 14)
let parent = tile2.parent
let firstChild = tile2.child
let allChildren = tile2.children

let quadkey = tile1.quadkey
let tile3 = MapTile(quadkey: "1202211303220032")

Also, not directly related to map tiles:

let mpp = MapTile.metersPerPixel(at: 15.0, latitude: 45.0)

Polylines

Provides an encoder/decoder for Polylines.

let polyline = [Coordinate3D(latitude: 47.56, longitude: 10.22)].encodePolyline()
let coordinates = polyline.decodePolyline()

Algorithms

Hint: Most algorithms are optimized for EPSG:4326. Using other projections will have a performance penalty due to added projections.

Name Example Source/Tests
along let coordinate = lineString.coordinateAlong(distance: 100.0) Source / Tests
area Polygon(…).area Source
bearing Coordinate3D(…).bearing(to: Coordinate3D(…)) Source / Tests
boolean-clockwise Polygon(…).outerRing?.isClockwise Source / Tests
boolean-crosses TODO Source
boolean-disjoint let result = polygon.isDisjoint(with: lineString) Source / Tests
boolean-intersects let result = polygon.intersects(with: lineString) Source
boolean-overlap lineString1.isOverlapping(with: lineString2) Source / Tests
boolean-parallel lineString1.isParallel(to: lineString2) Source / Tests
boolean-point-in-polygon polygon.contains(Coordinate3D(…)) Source
boolean-point-on-line lineString.checkIsOnLine(Coordinate3D(…)) Source
boolean-valid anyGeometry.isValid Source
bbox-clip let clipped = lineString.clipped(to: boundingBox) Source / Tests
buffer TODO Source
center/centroid/center-mean let center = polygon.center Source
circle let circle = point.circle(radius: 5000.0) Source / Tests
conversions/helpers let distance = GISTool.convert(length: 1.0, from: .miles, to: .meters) Source
destination let destination = coordinate.destination(distance: 1000.0, bearing: 173.0) Source / Tests
distance let distance = coordinate1.distance(from: coordinate2) Source / Tests
flatten let featureCollection = anyGeometry.flattened Source / Tests
frechetDistance let distance = lineString.frechetDistance(from: other) Source / Tests
length let length = lineString.length Source / Tests
line-arc let lineArc = point.lineArc(radius: 5000.0, bearing1: 20.0, bearing2: 60.0) Source / Tests
line-chunk let chunks = lineString.chunked(segmentLength: 1000.0).lineStrings let dividedLine = lineString.evenlyDivided(segmentLength: 1.0) Source / Tests
line-intersect let intersections = feature1.intersections(other: feature2) Source / Tests
line-overlap let overlappingSegments = lineString1.overlappingSegments(with: lineString2) Source / Tests
line-segments let segments = anyGeometry.lineSegments Source / Tests
line-slice let slice = lineString.slice(start: Coordinate3D(…), end: Coordinate3D(…)) Source / Tests
line-slice-along let sliced = lineString.sliceAlong(startDistance: 50.0, stopDistance: 2000.0) Source / Tests
midpoint let middle = coordinate1.midpoint(to: coordinate2) Source / Tests
nearest-point let nearest = anyGeometry.nearestCoordinate(from: Coordinate3D(…)) Source
nearest-point-on-feature let nearest = anyGeometry. nearestCoordinateOnFeature(from: Coordinate3D(…)) Source
nearest-point-on-line let nearest = lineString.nearestCoordinateOnLine(from: Coordinate3D(…))?.coordinate Source / Tests
nearest-point-to-line let nearest = lineString. nearestCoordinate(outOf: coordinates) Source
point-on-feature let coordinate = anyGeometry.coordinateOnFeature Source
points-within-polygon let within = polygon.coordinatesWithin(coordinates) Source
point-to-line-distance let distance = lineString.distanceFrom(coordinate: Coordinate3D(…)) Source / Tests
pole-of-inaccessibility TODO Source
polygon-to-line var lineStrings = polygon.lineStrings Source
reverse let lineStringReversed = lineString.reversed Source / Tests
rhumb-bearing let bearing = start.rhumbBearing(to: end) Source / Tests
rhumb-destination let destination = coordinate.rhumbDestination(distance: 1000.0, bearing: 0.0) Source / Tests
rhumb-distance let distance = coordinate1.rhumbDistance(from: coordinate2) Source / Tests
simplify let simplified = lineString. simplified(tolerance: 5.0, highQuality: false) Source / Tests
tile-cover let tileCover = anyGeometry.tileCover(atZoom: 14) Source / Tests
transform-coordinates let transformed = anyGeometry.transformCoordinates({ $0 }) Source / Tests
transform-rotate let transformed = anyGeometry. transformedRotate(angle: 25.0, pivot: Coordinate3D(…)) Source / Tests
transform-scale let transformed = anyGeometry. transformedScale(factor: 2.5, anchor: .center) Source / Tests
transform-translate let transformed = anyGeometry. transformedTranslate(distance: 1000.0, direction: 25.0) Source / Tests
truncate let truncated = lineString.truncated(precision: 2, removeAltitude: true) Source / Tests
union TODO Source

Related packages

Currently only two:

  • mvt-tools: Vector tiles reader/writer for Swift
  • mvt-postgis: Creates vector tiles from Postgis databases

Contributing

Please create an issue or open a pull request with a fix or enhancement.

License

MIT

Authors

Thomas Rasch, Outdooractive