Easily handle a geometric object model (points, linestrings, polygons etc.) and related topological operations (intersections, overlapping etc.). A type-safe, MIT-licensed Swift interface to the OSGeo's GEOS library routines.
For MapKit integration visit: https://github.com/GEOSwift/GEOSwiftMapKit
For MapboxGL integration visit: https://github.com/GEOSwift/GEOSwiftMapboxGL
Version 5 constitutes a ground-up rewrite of GEOSwift. For full details and help migrating from version 4, see VERSION_5.md.
- A pure-Swift, type-safe, optional-aware programming interface
- Support for XY, XYZ, XYM, and XYZM geometries
- WKT and WKB reading & writing
- Robust support for GeoJSON via Codable
- Thread-safe
- Swift-native error handling
- Extensively tested
- iOS 16.0, tvOS 16.0, macOS 13.0, watchOS 9.0, visionOS 1.0, Linux
- Swift 5.9
Note
GEOS is licensed under LGPL 2.1 and its compatibility with static linking is at least controversial. Use of geos without dynamic linking is discouraged.
-
Update the top-level dependencies in your
Package.swiftto include:.package(url: "https://github.com/GEOSwift/GEOSwift.git", from: "11.2.0") -
Update the target dependencies in your
Package.swiftto include"GEOSwift"
In certain cases, you may also need to explicitly include geos as a dependency. See issue #195 for details.
// If you know the expected geometry coordinate types, you can decode directly from the Geometry<> type
let geometryXY = try Geometry<XY>(wkb: wkb) // All valid geometries will successfully decode as XY
let pointXY = try Point<XY>(wkt: "LINESTRING(35 10, 45 45.5)") // Fails since it is not a POINT
let pointXYZ = try Point<XYZ>(wkt: "POINT(10 45)") // Fails since the encoded geometry has no Z coordinate
// If you don't know the expected coordinate types of the encoded geometry, use a WKBReader/WKTReader
let anyGeometry = try WKBReader().readAny(wkb: wkb) // Returns an `AnyGeometry` enum that you can use to recover the coordinate and geometry types.
switch anyGeometry {
case .xyz(let geometry):
doSomethingWith(geometry)
default:
throw error
}
// or
let geometryXY = anyGeometry.asXY() // will always succeed
switch geometryXY {
case .point(let point):
doSomethingWith(point)
default:
throw error
}When decoding from GeoJSON, you must specify the expected CoordinateType of the geometry.
The two compatible CoordinateTypes are XY and XYZ. GeoJSON does not support M coordinates. Decoding as
Geometry<XY> will drop any Z values present. The behavior of decoding as Geometry<XYZ> depends on
the CodingUserInfo.geoJSONSetMissingZNan value in the decoder's userInfo:
false: (default) Decoding will throw aGEOSwiftError.invalidCoordinatesif no Z value exists.true: Any missing Z values will be treated asDouble.nan.
This setting is useful if you are dealing with geometry with unknown dimensionality and you want to preserve Z where possible, or if you have geometry with mixed dimensionality.
Note
Take special care working with Double.nan in Swift. It can often produce unexpected results such as
Double.nan != Double.nan which may interfere with equality checks. Converting the geometry to XY using
the conversion initializers (e.g. let geometryXY = Geometry<XY>(geometry)) is a safe way to mitigate this
when Z is not needed. The underlying GEOS operations generally handle Z == NaN cases fine since Z is not
considered in topological operations.
// When decoding GeoJSON, you must explicitly declare the expected geometry coordinate type (e.g. GeoJSON<XY>)
var decoder = JSONDecoder()
let json = #"{"coordinates":[1,2],"type":"Point"}"#
let data = json.data(using: .utf8)!
let geometryXY = try decoder.decode(Geometry<XY>.self, from: data) // This works
let geometryXYZ = try decoder.decode(Geometry<XYZ>.self, from: data) // This throws an error because no Z value
decoder.userInfo[.geoJSONSetMissingZNan] = true
let geometryXYNan = try decoder.decode(Geometry<XYZ>.self, from: data) // This gives a NaN value for Z
let jsonWithZ = #"{"coordinates":[1,2,3],"type":"Point"}"#
let dataWithZ = jsonWithZ.data(using: .utf8)!
let geometryDropZ = try decoder.decode(Geometry<XY>.self, from: dataWithZ) // Drops the Z coordinateYou can also initialize geometry types directly from coordinate types.
let pointXY = Point(x: 1, y: 2) // Equivalent to Point(XY(1, 2))
let lineStringXY = LineString(coordinates: [XYZ(1, 2, 3), XYZ(4, 5, 6)]) // CoordinateTypes must be consistent in one geometryGEOSwift, like GEOS, supports geometry with 2 (XY), 3 (XYZ/XYM), and 4 (XYZM) coordinates. There are some important
things to know about using various coordinate types, mixing dimensionalities, and the impact on encoding/decoding
and geometric operations.
- The
XYcoordinate type is generally the safest and most intuitive. If you don't need Z/M, prefer keeping things 2D. - With the exception of decoding, see below, you usually don't need to explicitly specialize the geometry to a specific
CoordinateType. The dimensionality is infered from the initializer used (e.g.Point(x: 1, y: 2), the base geometry that you are composing with (e.g.MultiLine,GeometryCollection), or the result of a goemtric operation. - When decoding geometry directly from GeoJSON or WKB/WKT, you must specifiy the expected coordinate dimensions, e.g.
try Geometry<XY>(wkb: wkb)ordecoder.decode(GeoJSON<XY>.self, from: data).XYcoordinates will always work assuming the geometry is valid. Higher dimensions will throw an error if they don't have the appropriate coordinates available to decode. WKBReader/WKTReadercan be used to read geometries of unknown coordinate types into anAnyGeometry.- GeoJSON does not support M coordinates, so you cannot decode from JSON into that coordinate type.
- You can initialize a lower-dimensioned coordinate from a higher one assuming it has the relevant coordinates, e.g
let pointXY = Point<XY>(Point(x: 1, y: 2, z: 3)). You cannot initialize aXYMtype from aXYZtype or vice versa.
For geographic operations, specifically:
- For the most part, Z/M coordinates are treated as user data that do not impact the result of the topological operations and predicates which are XY planar by nature.
- To the extent that GEOS handles Z/M, there are 4 behaviors: preserve, drop, interpolate, set NaN. The behavior varies by
function in a relatively intuitive way, but there are some inconsistencies (e.g.
simplifydrops M coordinates). - It is common for GEOS to set Z and--especially--M coordinates to
nanin the cases that it is creating new coordinates. Be aware that in Swift,nan != nan, so if you need to do equality checks on coordinates from an operation, it is safest to create anXYversion of the geometry since X and Y coordinates will always be non-nan. Another option is to use a topological predicate--which don't check Z/M coordinates. - It is also common to receive back
XYgeometry even when using higher-dimension inputs because the semantics of the operation imply onlyXYresults (e.g.minimumWidthornearestPoints). - GEOSwift encodes the proper return dimensions in the type given by an operation, so other than being aware of the information above, you can trust the dimensions of the return type.
Let's say we have two geometries:
GEOSwift let you perform a set of operations on these two geometries:
- equals: returns true if this geometric object is “spatially equal” to another geometry.
- disjoint: returns true if this geometric object is “spatially disjoint” from another geometry.
- intersects: returns true if this geometric object “spatially intersects” another geometry.
- touches: returns true if this geometric object “spatially touches” another geometry.
- crosses: returns true if this geometric object “spatially crosses’ another geometry.
- within: returns true if this geometric object is “spatially within” another geometry.
- contains: returns true if this geometric object “spatially contains” another geometry.
- overlaps: returns true if this geometric object “spatially overlaps” another geometry.
- relate: returns true if this geometric object is spatially related to another geometry by testing for intersections between the interior, boundary and exterior of the two geometric objects as specified by the values in the intersectionPatternMatrix.
Explore more, interactively, in the playground, which is available in the
GEOSwiftMapKit project. It can be
found inside GEOSwiftMapKit workspace. Open the workspace in Xcode, build the
GEOSwiftMapKit framework and open the playground file.
To make a contribution:
- Fork the repo
- Start from the
mainbranch and create a branch with a name that describes your contribution - Run
$ xed Package.swiftto open the project in Xcode. - Run
$ swiftlintfrom the repo root and resolve any issues. - Push your branch and create a pull request to
main - One of the maintainers will review your code and may request changes
- If your pull request is accepted, one of the maintainers should update the changelog before merging it
- Andrew Hershberger (@macdrevx)
- Virgilio Favero Neto (@vfn)
- Andrea Cremaschi (@andreacremaschi) (original author)
- GEOSwift was released by Andrea Cremaschi (@andreacremaschi) under a MIT license. See LICENSE for more information.
- GEOS stands for Geometry Engine - Open Source, and is a C++ library, ported from the Java Topology Suite. GEOS implements the OpenGIS Simple Features for SQL spatial predicate functions and spatial operators. GEOS, now an OSGeo project, was initially developed and maintained by Refractions Research of Victoria, Canada.



