GIS tools for Swift, including a GeoJSON implementation and many algorithms ported from https://turfjs.org.
- Supports the full GeoJSON standard, with some exceptions (see TODO.md)
- Load and write GeoJSON objects from and to
[String:Any]
,URL
,Data
andString
- Supports
Codable
andSwiftData
(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
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.
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.
dependencies: [
.package(url: "https://github.com/Outdooractive/gis-tools", from: "1.8.2"),
],
targets: [
.target(name: "MyTarget", dependencies: [
.product(name: "GISTools", package: "gis-tools"),
]),
]
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.
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… 🙂).
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
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
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.
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.
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)
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!)
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))
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)
])!
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)
Implementation / MultiLineString test cases
A MultiLineString
is array of LineString
s:
/// 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)],
])!
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)
Implementation / MultiPolygon test cases
A MultiPolygon
is an array of Polygon
s:
/// 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),
]
]
])!
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
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
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
.
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...
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.
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
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
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)
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)
Provides an encoder/decoder for Polylines.
let polyline = [Coordinate3D(latitude: 47.56, longitude: 10.22)].encodePolyline()
let coordinates = polyline.decodePolyline()
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 |
Currently only two:
- mvt-tools: Vector tiles reader/writer for Swift
- mvt-postgis: Creates vector tiles from Postgis databases
Please create an issue or open a pull request with a fix or enhancement.
MIT
Thomas Rasch, Outdooractive