Skip to content

Commit

Permalink
#44: SwiftData compatibility (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
trasch authored May 23, 2024
1 parent 3a72a66 commit 3555473
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 47 deletions.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ targets: [

- Supports the full [GeoJSON standard][6], with some exceptions (see [TODO.md][7])
- Load and write GeoJSON objects from and to `[String:Any]`, `URL`, `Data` and `String`
- Supports `Codable`
- 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
Expand Down Expand Up @@ -655,6 +655,24 @@ 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](https://github.com/Outdooractive/gis-tools/blob/main/Tests/GISToolsTests/GeoJson/SwiftDataTests.swift)).

First, register the transformer like this:
```swift
GeoJsonTransformer.register()
```

Then create your models like this:
```swift
@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.

Expand Down
38 changes: 38 additions & 0 deletions Sources/GISTools/GeoJson/GeoJsonCodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,44 @@ extension Coordinate3D: Codable {

}

// MARK: - SwiftData compatibility (see the README)

#if canImport(SwiftData)
@objc(GeoJsonTransformer)
public final class GeoJsonTransformer: ValueTransformer {

public static let name = NSValueTransformerName(rawValue: "GeoJsonTransformer")

public static func register() {
ValueTransformer.setValueTransformer(GeoJsonTransformer(), forName: name)
}

public override class func transformedValueClass() -> AnyClass {
// returns __SwiftValue
type(of: Point(Coordinate3D.zero) as AnyObject)
}

public override class func allowsReverseTransformation() -> Bool {
true
}

// Encode GeoJSON to Data
public override func transformedValue(_ value: Any?) -> Any? {
guard let geoJson = value as? GeoJson else { return nil }

return geoJson.asJsonData()
}

// Decode Data to GeoJSON
public override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? Data else { return nil }

return GeoJsonReader.geoJsonFrom(jsonData: data)
}

}
#endif

// MARK: - Private

private struct GeoJsonCodingKey: CodingKey {
Expand Down
16 changes: 8 additions & 8 deletions Tests/GISToolsTests/GeoJson/FeatureCollectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import XCTest

final class FeatureCollectionTests: XCTestCase {

private let featureCollectionJson = """
static let featureCollectionJson = """
{
"type": "FeatureCollection",
"features": [{
Expand Down Expand Up @@ -56,7 +56,7 @@ final class FeatureCollectionTests: XCTestCase {
"""

func testLoadJson() throws {
let featureCollection = try XCTUnwrap(FeatureCollection(jsonString: featureCollectionJson))
let featureCollection = try XCTUnwrap(FeatureCollection(jsonString: FeatureCollectionTests.featureCollectionJson))

XCTAssertEqual(featureCollection.type, GeoJsonType.featureCollection)
XCTAssertEqual(featureCollection.projection, .epsg4326)
Expand Down Expand Up @@ -94,7 +94,7 @@ final class FeatureCollectionTests: XCTestCase {
}

func testMap() throws {
var featureCollection = try XCTUnwrap(FeatureCollection(jsonString: featureCollectionJson))
var featureCollection = try XCTUnwrap(FeatureCollection(jsonString: FeatureCollectionTests.featureCollectionJson))

let prop0: String? = featureCollection.features.first?.property(for: "prop0")
XCTAssertEqual(prop0, "value0")
Expand All @@ -110,7 +110,7 @@ final class FeatureCollectionTests: XCTestCase {
}

func testCompactMap() throws {
var featureCollection = try XCTUnwrap(FeatureCollection(jsonString: featureCollectionJson))
var featureCollection = try XCTUnwrap(FeatureCollection(jsonString: FeatureCollectionTests.featureCollectionJson))

XCTAssertEqual(featureCollection.features.count, 3)

Expand All @@ -123,7 +123,7 @@ final class FeatureCollectionTests: XCTestCase {
}

func testFilter() throws {
var featureCollection = try XCTUnwrap(FeatureCollection(jsonString: featureCollectionJson))
var featureCollection = try XCTUnwrap(FeatureCollection(jsonString: FeatureCollectionTests.featureCollectionJson))

XCTAssertEqual(featureCollection.features.count, 3)

Expand All @@ -135,7 +135,7 @@ final class FeatureCollectionTests: XCTestCase {
}

func testEnumerate() throws {
let featureCollection = try XCTUnwrap(FeatureCollection(jsonString: featureCollectionJson))
let featureCollection = try XCTUnwrap(FeatureCollection(jsonString: FeatureCollectionTests.featureCollectionJson))

let expected: [(Int, Int, Coordinate3D)] = [
(0, 0, Coordinate3D(latitude: 0.5, longitude: 102.0)),
Expand Down Expand Up @@ -165,7 +165,7 @@ final class FeatureCollectionTests: XCTestCase {
}

func testEncodable() throws {
let featureCollection = try XCTUnwrap(FeatureCollection(jsonString: featureCollectionJson))
let featureCollection = try XCTUnwrap(FeatureCollection(jsonString: FeatureCollectionTests.featureCollectionJson))

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
Expand All @@ -174,7 +174,7 @@ final class FeatureCollectionTests: XCTestCase {
}

func testDecodable() throws {
let featureCollectionData = try XCTUnwrap(FeatureCollection(jsonString: featureCollectionJson)?.asJsonData(prettyPrinted: true))
let featureCollectionData = try XCTUnwrap(FeatureCollection(jsonString: FeatureCollectionTests.featureCollectionJson)?.asJsonData(prettyPrinted: true))
let featureCollection = try JSONDecoder().decode(FeatureCollection.self, from: featureCollectionData)

XCTAssertEqual(featureCollection.projection, .epsg4326)
Expand Down
12 changes: 6 additions & 6 deletions Tests/GISToolsTests/GeoJson/FeatureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import XCTest

final class FeatureTests: XCTestCase {

private let featureJson = """
static let featureJson = """
{
"type": "Feature",
"geometry": {
Expand All @@ -30,7 +30,7 @@ final class FeatureTests: XCTestCase {
"""

func testLoadJson() throws {
let feature = try XCTUnwrap(Feature(jsonString: featureJson))
let feature = try XCTUnwrap(Feature(jsonString: FeatureTests.featureJson))

XCTAssertEqual(feature.type, GeoJsonType.feature)
XCTAssertEqual(feature.projection, .epsg4326)
Expand All @@ -44,7 +44,7 @@ final class FeatureTests: XCTestCase {
XCTAssertEqual(feature.id, .string("abcd.1234"))
}

private let featureJsonWithIntId = """
static let featureJsonWithIntId = """
{
"type": "Feature",
"geometry": {
Expand All @@ -71,7 +71,7 @@ final class FeatureTests: XCTestCase {
"""

func testLoadJsonWithIntId() throws {
let feature = try XCTUnwrap(Feature(jsonString: featureJsonWithIntId))
let feature = try XCTUnwrap(Feature(jsonString: FeatureTests.featureJsonWithIntId))

XCTAssertEqual(feature.id, .int(1234))
XCTAssertEqual(feature.projection, .epsg4326)
Expand All @@ -89,7 +89,7 @@ final class FeatureTests: XCTestCase {
}

func testEncodable() throws {
let feature = try XCTUnwrap(Feature(jsonString: featureJson))
let feature = try XCTUnwrap(Feature(jsonString: FeatureTests.featureJson))

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
Expand All @@ -98,7 +98,7 @@ final class FeatureTests: XCTestCase {
}

func testDecodable() throws {
let featureData = try XCTUnwrap(Feature(jsonString: featureJson)?.asJsonData(prettyPrinted: true))
let featureData = try XCTUnwrap(Feature(jsonString: FeatureTests.featureJson)?.asJsonData(prettyPrinted: true))
let feature = try JSONDecoder().decode(Feature.self, from: featureData)

XCTAssertEqual(feature.projection, .epsg4326)
Expand Down
8 changes: 4 additions & 4 deletions Tests/GISToolsTests/GeoJson/GeometryCollectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import XCTest

final class GeometryCollectionTests: XCTestCase {

private let geometryCollectionJson = """
static let geometryCollectionJson = """
{
"type": "GeometryCollection",
"geometries": [{
Expand All @@ -21,7 +21,7 @@ final class GeometryCollectionTests: XCTestCase {
"""

func testLoadJson() throws {
let geometryCollection = try XCTUnwrap(GeometryCollection(jsonString: geometryCollectionJson))
let geometryCollection = try XCTUnwrap(GeometryCollection(jsonString: GeometryCollectionTests.geometryCollectionJson))

XCTAssertNotNil(geometryCollection)
XCTAssertEqual(geometryCollection.type, GeoJsonType.geometryCollection)
Expand All @@ -44,7 +44,7 @@ final class GeometryCollectionTests: XCTestCase {
}

func testEncodable() throws {
let geometryCollection = try XCTUnwrap(GeometryCollection(jsonString: geometryCollectionJson))
let geometryCollection = try XCTUnwrap(GeometryCollection(jsonString: GeometryCollectionTests.geometryCollectionJson))

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
Expand All @@ -53,7 +53,7 @@ final class GeometryCollectionTests: XCTestCase {
}

func testDecodable() throws {
let geometryCollectionData = try XCTUnwrap(GeometryCollection(jsonString: geometryCollectionJson)?.asJsonData(prettyPrinted: true))
let geometryCollectionData = try XCTUnwrap(GeometryCollection(jsonString: GeometryCollectionTests.geometryCollectionJson)?.asJsonData(prettyPrinted: true))
let geometryCollection = try JSONDecoder().decode(GeometryCollection.self, from: geometryCollectionData)

XCTAssertEqual(geometryCollection.projection, .epsg4326)
Expand Down
8 changes: 4 additions & 4 deletions Tests/GISToolsTests/GeoJson/LineStringTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import XCTest

final class LineStringTests: XCTestCase {

private let lineStringJson = """
static let lineStringJson = """
{
"type": "LineString",
"coordinates": [
Expand All @@ -15,7 +15,7 @@ final class LineStringTests: XCTestCase {
"""

func testLoadJson() throws {
let lineString = try XCTUnwrap(LineString(jsonString: lineStringJson))
let lineString = try XCTUnwrap(LineString(jsonString: LineStringTests.lineStringJson))

XCTAssertEqual(lineString.type, GeoJsonType.lineString)
XCTAssertEqual(lineString.projection, .epsg4326)
Expand Down Expand Up @@ -52,7 +52,7 @@ final class LineStringTests: XCTestCase {
}

func testEncodable() throws {
let lineString = try XCTUnwrap(LineString(jsonString: lineStringJson))
let lineString = try XCTUnwrap(LineString(jsonString: LineStringTests.lineStringJson))

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
Expand All @@ -61,7 +61,7 @@ final class LineStringTests: XCTestCase {
}

func testDecodable() throws {
let lineStringData = try XCTUnwrap(LineString(jsonString: lineStringJson)?.asJsonData(prettyPrinted: true))
let lineStringData = try XCTUnwrap(LineString(jsonString: LineStringTests.lineStringJson)?.asJsonData(prettyPrinted: true))
let lineString = try JSONDecoder().decode(LineString.self, from: lineStringData)

XCTAssertEqual(lineString.projection, .epsg4326)
Expand Down
8 changes: 4 additions & 4 deletions Tests/GISToolsTests/GeoJson/MultiLineStringTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import XCTest

final class MultiLineStringTests: XCTestCase {

private let multiLineStringJson = """
static let multiLineStringJson = """
{
"type": "MultiLineString",
"coordinates": [
Expand All @@ -21,7 +21,7 @@ final class MultiLineStringTests: XCTestCase {
"""

func testLoadJson() throws {
let multiLineString = try XCTUnwrap(MultiLineString(jsonString: multiLineStringJson))
let multiLineString = try XCTUnwrap(MultiLineString(jsonString: MultiLineStringTests.multiLineStringJson))

XCTAssertNotNil(multiLineString)
XCTAssertEqual(multiLineString.type, GeoJsonType.multiLineString)
Expand Down Expand Up @@ -50,7 +50,7 @@ final class MultiLineStringTests: XCTestCase {
}

func testEncodable() throws {
let multiLineString = try XCTUnwrap(MultiLineString(jsonString: multiLineStringJson))
let multiLineString = try XCTUnwrap(MultiLineString(jsonString: MultiLineStringTests.multiLineStringJson))

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
Expand All @@ -59,7 +59,7 @@ final class MultiLineStringTests: XCTestCase {
}

func testDecodable() throws {
let multiLineStringData = try XCTUnwrap(MultiLineString(jsonString: multiLineStringJson)?.asJsonData(prettyPrinted: true))
let multiLineStringData = try XCTUnwrap(MultiLineString(jsonString: MultiLineStringTests.multiLineStringJson)?.asJsonData(prettyPrinted: true))
let multiLineString = try JSONDecoder().decode(MultiLineString.self, from: multiLineStringData)

XCTAssertEqual(multiLineString.projection, .epsg4326)
Expand Down
8 changes: 4 additions & 4 deletions Tests/GISToolsTests/GeoJson/MultiPointTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import XCTest

final class MultiPointTests: XCTestCase {

private let multiPointJson = """
static let multiPointJson = """
{
"type": "MultiPoint",
"coordinates": [
Expand All @@ -15,7 +15,7 @@ final class MultiPointTests: XCTestCase {
"""

func testLoadJson() throws {
let multiPoint = try XCTUnwrap(MultiPoint(jsonString: multiPointJson))
let multiPoint = try XCTUnwrap(MultiPoint(jsonString: MultiPointTests.multiPointJson))

XCTAssertNotNil(multiPoint)
XCTAssertEqual(multiPoint.type, GeoJsonType.multiPoint)
Expand All @@ -35,7 +35,7 @@ final class MultiPointTests: XCTestCase {
}

func testEncodable() throws {
let multiPoint = try XCTUnwrap(MultiPoint(jsonString: multiPointJson))
let multiPoint = try XCTUnwrap(MultiPoint(jsonString: MultiPointTests.multiPointJson))

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
Expand All @@ -44,7 +44,7 @@ final class MultiPointTests: XCTestCase {
}

func testDecodable() throws {
let multiPointData = try XCTUnwrap(MultiPoint(jsonString: multiPointJson)?.asJsonData(prettyPrinted: true))
let multiPointData = try XCTUnwrap(MultiPoint(jsonString: MultiPointTests.multiPointJson)?.asJsonData(prettyPrinted: true))
let multiPoint = try JSONDecoder().decode(MultiPoint.self, from: multiPointData)

XCTAssertEqual(multiPoint.projection, .epsg4326)
Expand Down
8 changes: 4 additions & 4 deletions Tests/GISToolsTests/GeoJson/MultiPolygonTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import XCTest

final class MultiPolygonTests: XCTestCase {

private let multiPolygonJson = """
static let multiPolygonJson = """
{
"type": "MultiPolygon",
"coordinates": [
Expand Down Expand Up @@ -38,7 +38,7 @@ final class MultiPolygonTests: XCTestCase {
"""

func testLoadJson() throws {
let multiPolygon = try XCTUnwrap(MultiPolygon(jsonString: multiPolygonJson))
let multiPolygon = try XCTUnwrap(MultiPolygon(jsonString: MultiPolygonTests.multiPolygonJson))

XCTAssertEqual(multiPolygon.type, GeoJsonType.multiPolygon)
XCTAssertEqual(multiPolygon.projection, .epsg4326)
Expand All @@ -57,7 +57,7 @@ final class MultiPolygonTests: XCTestCase {
}

func testEncodable() throws {
let multiPolygon = try XCTUnwrap(MultiPolygon(jsonString: multiPolygonJson))
let multiPolygon = try XCTUnwrap(MultiPolygon(jsonString: MultiPolygonTests.multiPolygonJson))

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
Expand All @@ -66,7 +66,7 @@ final class MultiPolygonTests: XCTestCase {
}

func testDecodable() throws {
let multiPolygonData = try XCTUnwrap(MultiPolygon(jsonString: multiPolygonJson)?.asJsonData(prettyPrinted: true))
let multiPolygonData = try XCTUnwrap(MultiPolygon(jsonString: MultiPolygonTests.multiPolygonJson)?.asJsonData(prettyPrinted: true))
let multiPolygon = try JSONDecoder().decode(MultiPolygon.self, from: multiPolygonData)

XCTAssertEqual(multiPolygon.projection, .epsg4326)
Expand Down
Loading

0 comments on commit 3555473

Please sign in to comment.