From 1edb6c0ded7977c3a00fae8bc8143e59026e1696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Sch=C3=B6nig?= Date: Tue, 14 Jun 2022 18:58:22 +1000 Subject: [PATCH] Adds Collection.convexHull() (#7) Adds convexHull() Also: - SPM update to get closed polygon rings - Refactoring --- Package.resolved | 8 +- Package.swift | 3 +- README.md | 7 +- Sources/GeoJSONKitTurf/ConvexHull.swift | 40 +++ Sources/GeoJSONKitTurf/GeoJSON+Helpers.swift | 52 ++++ Sources/GeoJSONKitTurf/Simplify.swift | 70 +++++- Sources/GeoJSONKitTurf/Turf+BoundingBox.swift | 27 --- .../algorithms/MonotoneChain.swift | 83 +++++++ .../Fixtures/convex/in/elevation1.geojson | 158 ++++++++++++ .../Fixtures/convex/in/elevation2.geojson | 45 ++++ .../Fixtures/convex/in/elevation3.geojson | 158 ++++++++++++ .../Fixtures/convex/in/elevation4.geojson | 45 ++++ .../Fixtures/convex/in/elevation5.geojson | 32 +++ .../Fixtures/convex/out/elevation1.geojson | 229 ++++++++++++++++++ .../Fixtures/convex/out/elevation2.geojson | 62 +++++ .../Fixtures/convex/out/elevation3.geojson | 229 ++++++++++++++++++ .../Fixtures/convex/out/elevation4.geojson | 62 +++++ .../Fixtures/convex/out/elevation5.geojson | 58 +++++ Tests/GeoJSONKitTurfTests/PolygonTests.swift | 25 +- Tests/GeoJSONKitTurfTests/PositionTests.swift | 36 +++ Tests/GeoJSONKitTurfTests/XCTest+Save.swift | 20 ++ 21 files changed, 1386 insertions(+), 63 deletions(-) create mode 100644 Sources/GeoJSONKitTurf/ConvexHull.swift create mode 100644 Sources/GeoJSONKitTurf/GeoJSON+Helpers.swift create mode 100644 Sources/GeoJSONKitTurf/algorithms/MonotoneChain.swift create mode 100755 Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation1.geojson create mode 100755 Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation2.geojson create mode 100755 Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation3.geojson create mode 100755 Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation4.geojson create mode 100755 Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation5.geojson create mode 100755 Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation1.geojson create mode 100755 Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation2.geojson create mode 100755 Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation3.geojson create mode 100755 Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation4.geojson create mode 100755 Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation5.geojson create mode 100644 Tests/GeoJSONKitTurfTests/XCTest+Save.swift diff --git a/Package.resolved b/Package.resolved index 4a9b8ca..fe4a630 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/maparoni/geojsonkit.git", "state": { "branch": null, - "revision": "a37addb9dcfe634dedece856e5f4d098d5290d1b", - "version": "0.4.8" + "revision": "8f1e5094a8ab8cbb020074cd4848a5804c17bab0", + "version": "0.5.2" } }, { @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/apple/swift-argument-parser", "state": { "branch": null, - "revision": "e394bf350e38cb100b6bc4172834770ede1b7232", - "version": "1.0.3" + "revision": "f3c9084a71ef4376f2fabbdf1d3d90a49f1fabdb", + "version": "1.1.2" } } ] diff --git a/Package.swift b/Package.swift index 167dc96..78c9bb2 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,8 @@ let package = Package( targets: ["GeoKitten"]), ], dependencies: [ - .package(name: "GeoJSONKit", url: "https://github.com/maparoni/geojsonkit.git", from: "0.4.2"), + .package(name: "GeoJSONKit", url: "https://github.com/maparoni/geojsonkit.git", from: "0.5.2"), +// .package(name: "GeoJSONKit", path: "../GeoJSONKit"), .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMajor(from: "1.0.0")), ], targets: [ diff --git a/README.md b/README.md index bd12783..9671aaf 100644 --- a/README.md +++ b/README.md @@ -49,15 +49,16 @@ Turf.js | Turf-swift [turf-center-of-mass](http://turfjs.org/docs/#centerOfMass) | `GeoJSON.Geometry.centerOfMass()` | [turf-centroid](http://turfjs.org/docs/#centroid) | `GeoJSON.Geometry.centroid()` | [turf-circle](https://turfjs.org/docs/#circle) | `GeoJSON.Polygon(center:radius:vertices:)` | +[turf-convex](https://turfjs.org/docs/#convex) | `GeoJSON.convexHull()`
`Collection.convexHull()` | [turf-destination](https://github.com/Turfjs/turf/tree/master/packages/turf-destination/) | `GeoJSON.Position.coordinate(at:facing:)`
`RadianCoordinate2D.coordinate(at:facing:)` -[turf-distance](https://github.com/Turfjs/turf/tree/master/packages/turf-distance/) | `GeoJSON.Position.distance(to:)`
`RadianCoordinate2D.distance(to:)` +[turf-distance](https://github.com/Turfjs/turf/tree/master/packages/turf-distance/) | `GeoJSON.Position.distance(to:)`
`RadianCoordinate2D.distance(to:)` [turf-helpers#degreesToRadians](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers/#degreesToRadians) | `GeoJSON.Degrees.toRadians()` [turf-helpers#radiansToDegrees](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers/#radiansToDegrees) | `GeoJSON.DegreesRadians.toDegrees()` -[turf-helpers#convertLength](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers#convertlength)
[turf-helpers#convertArea](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers#convertarea) | `Measurement.converted(to:)` +[turf-helpers#convertLength](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers#convertlength)
[turf-helpers#convertArea](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers#convertarea) | `Measurement.converted(to:)` [turf-length](https://github.com/Turfjs/turf/tree/master/packages/turf-length/) | `GeoJSON.LineString.distance(from:to:)` [turf-line-intersect](https://github.com/Turfjs/turf/tree/master/packages/turf-line-intersect/) | `GeoJSON.LineString.intersection(with:)` [turf-line-slice](https://github.com/Turfjs/turf/tree/master/packages/turf-line-slice/) | `GeoJSON.LineString.sliced(from:to:)` -[turf-line-slice-along](https://github.com/Turfjs/turf/tree/master/packages/turf-line-slice-along/) | `GeoJSON.LineString.trimmed(from:distance:)`, `GeoJSON.LineString.trimmed(from:to:)` +[turf-line-slice-along](https://github.com/Turfjs/turf/tree/master/packages/turf-line-slice-along/) | `GeoJSON.LineString.trimmed(from:distance:)`
`GeoJSON.LineString.trimmed(from:to:)` [turf-midpoint](https://github.com/Turfjs/turf/blob/master/packages/turf-midpoint/index.js) | `mid(_:_:)` [turf-nearest-point-on-line](https://github.com/Turfjs/turf/tree/master/packages/turf-nearest-point-on-line/) | `GeoJSON.LineString.closestCoordinate(to:)` [turf-polygon-smooth](https://github.com/Turfjs/turf/tree/master/packages/turf-polygon-smooth) | `GeoJSON.Polygon.smooth(iterations:)` diff --git a/Sources/GeoJSONKitTurf/ConvexHull.swift b/Sources/GeoJSONKitTurf/ConvexHull.swift new file mode 100644 index 0000000..37a4430 --- /dev/null +++ b/Sources/GeoJSONKitTurf/ConvexHull.swift @@ -0,0 +1,40 @@ +// +// Turf+ConvexHull.swift +// +// +// Created by Adrian Schönig on 14/6/2022. +// + +import Foundation + +import GeoJSONKit + +extension Collection where Element == GeoJSON.Position { + /// Calculates the convex hull of a given sequence of positions. + /// + /// - Complexity: O(*n* log *n*), where *n* is the count of `points`. + /// + /// - Returns: The convex hull of this sequence as a polygon + public func convexHull() -> GeoJSON.Polygon { + let positions = AndrewsMonotoneChain.convexHull(self).map { $0.removingAltitude } + return .init(exterior: .init(positions: positions)) + } +} + +extension GeoJSON { + /// Calculates the convex hull of all the elements of this GeoJSON + /// + /// - Returns: The convex hull of this GeoJSON as a polygon + public func convexHull() -> GeoJSON.Polygon { + return positions.convexHull() + } +} + +extension GeoJSON.Position { + fileprivate var removingAltitude: GeoJSON.Position { + guard altitude != nil else { return self } + var updated = self + updated.altitude = nil + return updated + } +} diff --git a/Sources/GeoJSONKitTurf/GeoJSON+Helpers.swift b/Sources/GeoJSONKitTurf/GeoJSON+Helpers.swift new file mode 100644 index 0000000..f439226 --- /dev/null +++ b/Sources/GeoJSONKitTurf/GeoJSON+Helpers.swift @@ -0,0 +1,52 @@ +// +// GeoJSON+Helpers.swift +// +// +// Created by Adrian Schönig on 14/6/2022. +// + +import GeoJSONKit + +extension GeoJSON.GeometryObject { + public var geometries: [GeoJSON.Geometry] { + switch self { + case .single(let geo): return [geo] + case .multi(let geos): return geos + case .collection(let geoObjects): return geoObjects.flatMap(\.geometries) + } + } + + public var positions: [GeoJSON.Position] { + geometries.flatMap(\.positions) + } +} + +extension GeoJSON.Geometry { + public var positions: [GeoJSON.Position] { + switch self { + case .point(let position): return [position] + case .lineString(let line): return line.positions + case .polygon(let polygon): + // Ignore the interior positions as the purpose of this is getting + // bounding boxes, convex hull or alike + return polygon.exterior.positions + } + } +} + +extension GeoJSON { + public var geometries: [GeoJSON.Geometry] { + switch object { + case .feature(let feature): + return feature.geometry.geometries + case .featureCollection(let features): + return features.flatMap(\.geometry.geometries) + case .geometry(let geometry): + return geometry.geometries + } + } + + public var positions: [GeoJSON.Position] { + geometries.flatMap(\.positions) + } +} diff --git a/Sources/GeoJSONKitTurf/Simplify.swift b/Sources/GeoJSONKitTurf/Simplify.swift index 5e5bbea..32e8397 100644 --- a/Sources/GeoJSONKitTurf/Simplify.swift +++ b/Sources/GeoJSONKitTurf/Simplify.swift @@ -109,14 +109,13 @@ extension GeoJSON.Geometry { } } -extension GeoJSON.LineString { - - private static func simplifiedRadialDistance(_ positions: [GeoJSON.Position], squaredTolerance: Double) -> [GeoJSON.Position] { - guard positions.count > 2, let first = positions.first, let last = positions.last else { return positions } +extension Array where Element == GeoJSON.Position { + fileprivate func simplifiedRadialDistance(squaredTolerance: Double) -> [GeoJSON.Position] { + guard count > 2, let first = first, let last = last else { return self } var newPositions = [first] - for position in positions[1...] { + for position in self[1...] { if let lastNew = newPositions.last, position.squaredDistance(from: lastNew) > squaredTolerance { newPositions.append(position) } @@ -127,7 +126,9 @@ extension GeoJSON.LineString { } return newPositions } - +} + +extension GeoJSON.LineString { /// Returns a copy of the LineString with the Ramer–Douglas–Peucker algorithm applied to it. /// @@ -167,7 +168,60 @@ extension GeoJSON.LineString { let squared = tolerance * tolerance let input = options.highestQuality ? positions - : Self.simplifiedRadialDistance(positions, squaredTolerance: squared) + : positions.simplifiedRadialDistance(squaredTolerance: squared) + + simplified = DouglasPeucker.simplify(input, sqTolerance: squared) + + //remove 1 percent of tolerance if not verified + tolerance -= tolerance * 0.01 + } while !verifier(simplified) + positions = simplified + } + } + +} + +extension GeoJSON.Polygon.LinearRing { + + /// Returns a copy of the LinearRing with the Ramer–Douglas–Peucker algorithm applied to it. + /// + /// tolerance: Controls the level of simplification by specifying the maximum allowed distance between the original line point + /// and the simplified point. Higher tolerance values results in higher simplification. + /// + /// highestQuality: Excludes distance-based preprocessing step which leads to highest quality simplification. High quality simplification runs considerably slower so consider how much precision is needed in your application. + /// + /// Ported from https://github.com/Turfjs/turf/blob/master/packages/turf-simplify/lib/simplify.js + public func simplified(options: SimplifyOptions = .init()) -> GeoJSON.Polygon.LinearRing { + guard positions.count > 2 else { return GeoJSON.Polygon.LinearRing(positions: positions) } + + var copy = self + copy.simplify(options: options) + return copy + } + + /// Mutates the LinearRing into a simplified version using the Ramer–Douglas–Peucker algorithm. + /// + /// tolerance: Controls the level of simplification by specifying the maximum allowed distance between the original line point + /// and the simplified point. Higher tolerance values results in higher simplification. + /// + /// highestQuality: Excludes distance-based preprocessing step which leads to highest quality simplification. High quality simplification runs considerably slower so consider how much precision is needed in your application. + /// + /// Ported from https://github.com/Turfjs/turf/blob/master/packages/turf-simplify/lib/simplify.js + public mutating func simplify(options: SimplifyOptions = .init()) { + simplify(options: options, verifier: { _ in true }) + } + + mutating func simplify(options: SimplifyOptions, verifier: ([GeoJSON.Position]) -> Bool) { + guard positions.count > 2 else { return } + + switch options.algorithm { + case .RamerDouglasPeucker(var tolerance): + var simplified: [GeoJSON.Position] + repeat { + let squared = tolerance * tolerance + let input = options.highestQuality + ? positions + : positions.simplifiedRadialDistance(squaredTolerance: squared) simplified = DouglasPeucker.simplify(input, sqTolerance: squared) @@ -199,8 +253,6 @@ extension GeoJSON.Polygon { updated.simplify(options: options, verifier: Self.checkValidity(ring:)) return updated } - - // TODO: Close exterior + interiors } /// Checks if a ring has at least 3 coordinates. Will return false for a 3 coordinate ring diff --git a/Sources/GeoJSONKitTurf/Turf+BoundingBox.swift b/Sources/GeoJSONKitTurf/Turf+BoundingBox.swift index 55925d7..b0d3657 100644 --- a/Sources/GeoJSONKitTurf/Turf+BoundingBox.swift +++ b/Sources/GeoJSONKitTurf/Turf+BoundingBox.swift @@ -113,30 +113,3 @@ extension GeoJSON.BoundingBox { } } - -extension GeoJSON.GeometryObject { - public var geometries: [GeoJSON.Geometry] { - switch self { - case .single(let geo): return [geo] - case .multi(let geos): return geos - case .collection(let geoObjects): return geoObjects.flatMap(\.geometries) - } - } - - public var positions: [GeoJSON.Position] { - geometries.flatMap(\.positions) - } -} - -extension GeoJSON.Geometry { - public var positions: [GeoJSON.Position] { - switch self { - case .point(let position): return [position] - case .lineString(let line): return line.positions - case .polygon(let polygon): - // Ignore the interior positions as the purpose of this is getting - // bounding boxes or alike - return polygon.exterior.positions - } - } -} diff --git a/Sources/GeoJSONKitTurf/algorithms/MonotoneChain.swift b/Sources/GeoJSONKitTurf/algorithms/MonotoneChain.swift new file mode 100644 index 0000000..473f491 --- /dev/null +++ b/Sources/GeoJSONKitTurf/algorithms/MonotoneChain.swift @@ -0,0 +1,83 @@ +// +// MonotoneChain.swift +// +// +// Created by Adrian Schönig on 14/6/2022. +// + +import Foundation + +import GeoJSONKit + +// Adopted from https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain +struct AndrewsMonotoneChain { + private static func cross(_ o: GeoJSON.Position, _ a: GeoJSON.Position, _ b: GeoJSON.Position) -> Double { + let lhs = (a.x - o.x) * (b.y - o.y) + let rhs = (a.y - o.y) * (b.x - o.x) + return lhs - rhs + } + + /// Calculate and return the convex hull of a given sequence of points. + /// + /// - Remark: Implements Andrew’s monotone chain convex hull algorithm. + /// + /// - Complexity: O(*n* log *n*), where *n* is the count of `points`. + /// + /// - Parameter points: A sequence of `GeoJSON.Position` elements. + /// + /// - Returns: An array containing the convex hull of `points`, ordered + /// lexicographically from the smallest coordinates to the largest, + /// turning counterclockwise. + /// + static func convexHull(_ points: Source) -> [GeoJSON.Position] + where Source : Collection, + Source.Element == GeoJSON.Position + { + // Exit early if there aren’t enough points to work with. + guard points.count > 1 else { return Array(points) } + + // Create storage for the lower and upper hulls. + var lower = [GeoJSON.Position]() + var upper = [GeoJSON.Position]() + + // Sort points in lexicographical order. + let points = points.sorted { a, b in + a.x < b.x || a.x == b.x && a.y < b.y + } + + // Construct the lower hull. + for point in points { + while lower.count >= 2 { + let a = lower[lower.count - 2] + let b = lower[lower.count - 1] + if cross(a, b, point) > 0 { break } + lower.removeLast() + } + lower.append(point) + } + + // Construct the upper hull. + for point in points.lazy.reversed() { + while upper.count >= 2 { + let a = upper[upper.count - 2] + let b = upper[upper.count - 1] + if cross(a, b, point) > 0 { break } + upper.removeLast() + } + upper.append(point) + } + + // Remove each array’s last point, as it’s the same as the first point + // in the opposite array, respectively. + lower.removeLast() + upper.removeLast() + + // Join the arrays to form the convex hull. + return lower + upper + } +} + +fileprivate extension GeoJSON.Position { + var x: Double { longitude } + var y: Double { latitude } +} diff --git a/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation1.geojson b/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation1.geojson new file mode 100755 index 0000000..19a74fe --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation1.geojson @@ -0,0 +1,158 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.833, 39.284] }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 25 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.6, 39.984] }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 23 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.221, 39.125] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 29 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.358, 39.987] }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 12 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.9221, 39.27] }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 11 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.534, 39.123] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 49 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.21, 39.12] }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 50 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.22, 39.33] }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 90 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.44, 39.55] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 22 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.77, 39.66] }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 99 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.44, 39.11] }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 55 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.05, 39.92] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 41 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.88, 39.98] }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 52 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.55, 39.55] }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 143 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.33, 39.44] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 76 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.56, 39.24] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 18 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.56, 39.36] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 52 + } + } + ] +} diff --git a/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation2.geojson b/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation2.geojson new file mode 100755 index 0000000..1c7cc20 --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation2.geojson @@ -0,0 +1,45 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-75.1300048828125, 40.157885249506506] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-74.5587158203125, 39.816975090490004] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-76.06109619140625, 40.0759697987031] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-76.21490478515625, 39.60145584096999] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-74.77020263671875, 39.35766163717121] + } + } + ] +} diff --git a/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation3.geojson b/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation3.geojson new file mode 100755 index 0000000..19a74fe --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation3.geojson @@ -0,0 +1,158 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.833, 39.284] }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 25 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.6, 39.984] }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 23 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.221, 39.125] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 29 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.358, 39.987] }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 12 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.9221, 39.27] }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 11 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.534, 39.123] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 49 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.21, 39.12] }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 50 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.22, 39.33] }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 90 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.44, 39.55] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 22 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.77, 39.66] }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 99 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.44, 39.11] }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 55 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.05, 39.92] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 41 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.88, 39.98] }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 52 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.55, 39.55] }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 143 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.33, 39.44] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 76 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.56, 39.24] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 18 + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-75.56, 39.36] }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 52 + } + } + ] +} diff --git a/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation4.geojson b/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation4.geojson new file mode 100755 index 0000000..53d1aa6 --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation4.geojson @@ -0,0 +1,45 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-75.1300048828125, 40.157885249506506, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-74.5587158203125, 39.816975090490004, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-76.06109619140625, 40.0759697987031, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-76.21490478515625, 39.60145584096999, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-74.77020263671875, 39.35766163717121, 1] + } + } + ] +} diff --git a/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation5.geojson b/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation5.geojson new file mode 100755 index 0000000..546ca32 --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/elevation5.geojson @@ -0,0 +1,32 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-74.08355712890625, 40.6723059714534], + [-73.7567138671875, 40.8595252289932], + [-73.86383056640625, 40.97575093157534], + [-74.0203857421875, 41.04621681452063], + [-74.24285888671875, 41.04828819952275], + [-74.410400390625, 40.977824533189526], + [-74.5257568359375, 40.851215574282456], + [-74.5697021484375, 40.74309523218185], + [-74.59991455078125, 40.60144147645398], + [-74.56146240234375, 40.47620304302563], + [-74.41864013671875, 40.386304853509046], + [-74.2236328125, 40.32141999593439], + [-74.01214599609375, 40.317231732315236], + [-73.85009765625, 40.34654412118006], + [-73.76220703125, 40.444856858961764], + [-74.08355712890625, 40.6723059714534] + ] + ] + } + } + ] +} diff --git a/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation1.geojson b/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation1.geojson new file mode 100755 index 0000000..65023b6 --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation1.geojson @@ -0,0 +1,229 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.833, 39.284] + }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 25 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.6, 39.984] + }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 23 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.221, 39.125] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 29 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.358, 39.987] + }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 12 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.9221, 39.27] + }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 11 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.534, 39.123] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 49 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.21, 39.12] + }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 50 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.22, 39.33] + }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 90 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.44, 39.55] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 22 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.77, 39.66] + }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 99 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.44, 39.11] + }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 55 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.05, 39.92] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 41 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.88, 39.98] + }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 52 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.55, 39.55] + }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 143 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.33, 39.44] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 76 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.56, 39.24] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 18 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.56, 39.36] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 52 + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-75.9221, 39.27], + [-75.534, 39.123], + [-75.44, 39.11], + [-75.21, 39.12], + [-75.05, 39.92], + [-75.358, 39.987], + [-75.6, 39.984], + [-75.88, 39.98], + [-75.9221, 39.27] + ] + ] + } + } + ] +} diff --git a/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation2.geojson b/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation2.geojson new file mode 100755 index 0000000..49f99b5 --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation2.geojson @@ -0,0 +1,62 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-75.1300048828125, 40.157885249506506] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-74.5587158203125, 39.816975090490004] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-76.06109619140625, 40.0759697987031] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-76.21490478515625, 39.60145584096999] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-74.77020263671875, 39.35766163717121] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-76.21490478515625, 39.60145584096999], + [-74.77020263671875, 39.35766163717121], + [-74.5587158203125, 39.816975090490004], + [-75.1300048828125, 40.157885249506506], + [-76.06109619140625, 40.0759697987031], + [-76.21490478515625, 39.60145584096999] + ] + ] + } + } + ] +} diff --git a/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation3.geojson b/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation3.geojson new file mode 100755 index 0000000..65023b6 --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation3.geojson @@ -0,0 +1,229 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.833, 39.284] + }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 25 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.6, 39.984] + }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 23 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.221, 39.125] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 29 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.358, 39.987] + }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 12 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.9221, 39.27] + }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 11 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.534, 39.123] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 49 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.21, 39.12] + }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 50 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.22, 39.33] + }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 90 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.44, 39.55] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 22 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.77, 39.66] + }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 99 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.44, 39.11] + }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 55 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.05, 39.92] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 41 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.88, 39.98] + }, + "properties": { + "name": "Location A", + "category": "Store", + "elevation": 52 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.55, 39.55] + }, + "properties": { + "name": "Location B", + "category": "House", + "elevation": 143 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.33, 39.44] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 76 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.56, 39.24] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 18 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-75.56, 39.36] + }, + "properties": { + "name": "Location C", + "category": "Office", + "elevation": 52 + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-75.9221, 39.27], + [-75.534, 39.123], + [-75.44, 39.11], + [-75.21, 39.12], + [-75.05, 39.92], + [-75.358, 39.987], + [-75.6, 39.984], + [-75.88, 39.98], + [-75.9221, 39.27] + ] + ] + } + } + ] +} diff --git a/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation4.geojson b/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation4.geojson new file mode 100755 index 0000000..6fd7fa4 --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation4.geojson @@ -0,0 +1,62 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-75.1300048828125, 40.157885249506506, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-74.5587158203125, 39.816975090490004, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-76.06109619140625, 40.0759697987031, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-76.21490478515625, 39.60145584096999, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-74.77020263671875, 39.35766163717121, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-76.21490478515625, 39.60145584096999], + [-74.77020263671875, 39.35766163717121], + [-74.5587158203125, 39.816975090490004], + [-75.1300048828125, 40.157885249506506], + [-76.06109619140625, 40.0759697987031], + [-76.21490478515625, 39.60145584096999], + ] + ] + } + } + ] +} diff --git a/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation5.geojson b/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation5.geojson new file mode 100755 index 0000000..ba06447 --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/elevation5.geojson @@ -0,0 +1,58 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-74.08355712890625, 40.6723059714534], + [-73.7567138671875, 40.8595252289932], + [-73.86383056640625, 40.97575093157534], + [-74.0203857421875, 41.04621681452063], + [-74.24285888671875, 41.04828819952275], + [-74.410400390625, 40.977824533189526], + [-74.5257568359375, 40.851215574282456], + [-74.5697021484375, 40.74309523218185], + [-74.59991455078125, 40.60144147645398], + [-74.56146240234375, 40.47620304302563], + [-74.41864013671875, 40.386304853509046], + [-74.2236328125, 40.32141999593439], + [-74.01214599609375, 40.317231732315236], + [-73.85009765625, 40.34654412118006], + [-73.76220703125, 40.444856858961764], + [-74.08355712890625, 40.6723059714534] + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-74.59991455078125, 40.60144147645398], + [-74.56146240234375, 40.47620304302563], + [-74.41864013671875, 40.386304853509046], + [-74.2236328125, 40.32141999593439], + [-74.01214599609375, 40.317231732315236], + [-73.85009765625, 40.34654412118006], + [-73.76220703125, 40.444856858961764], + [-73.7567138671875, 40.8595252289932], + [-73.86383056640625, 40.97575093157534], + [-74.0203857421875, 41.04621681452063], + [-74.24285888671875, 41.04828819952275], + [-74.410400390625, 40.977824533189526], + [-74.5257568359375, 40.851215574282456], + [-74.5697021484375, 40.74309523218185], + [-74.59991455078125, 40.60144147645398] + ] + ] + } + } + ] +} diff --git a/Tests/GeoJSONKitTurfTests/PolygonTests.swift b/Tests/GeoJSONKitTurfTests/PolygonTests.swift index e3035fd..d7ee191 100644 --- a/Tests/GeoJSONKitTurfTests/PolygonTests.swift +++ b/Tests/GeoJSONKitTurfTests/PolygonTests.swift @@ -570,7 +570,7 @@ class PolygonTests: XCTestCase { } func testSimplify() throws { - try Fixture.fixtures(folder: "simplify") { name, input, output in + try Fixture.fixtures(folder: "simplify") { name, input, expected in let properties: [String: AnyHashable]? switch input.object { case .feature(let feature): properties = feature.properties @@ -579,21 +579,20 @@ class PolygonTests: XCTestCase { let tolerance = properties?["tolerance"] as? Double ?? 0.01 let highQuality = properties?["highQuality"] as? Bool ?? false - let simplified = input.simplified(options: .init(algorithm: .RamerDouglasPeucker(tolerance: tolerance), highestQuality: highQuality)) -// XCTAssertEqual(simplified, output, "Fixture check failed for \(name)") + let actual = input.simplified(options: .init(algorithm: .RamerDouglasPeucker(tolerance: tolerance), highestQuality: highQuality)) - if simplified != output { + if actual != expected { // Give it another chance on the data-level, too do { var options: JSONSerialization.WritingOptions = [.prettyPrinted] if #available(iOS 11.0, OSX 10.13, *) { options.insert(.sortedKeys) } - let newData = try simplified.toData(options: options) - let oldData = try output.toData(options: options) + let newData = try actual.toData(options: options) + let oldData = try expected.toData(options: options) if newData != oldData { if true { - try Self.save(newData, filename: "out_simplified", extension: "geojson") + try Self.save(newData, filename: "out_actual", extension: "geojson") try Self.save(oldData, filename: "out_expected", extension: "geojson") } XCTFail("Fixture check failed for \(name)!") @@ -607,15 +606,3 @@ class PolygonTests: XCTestCase { } } } - -extension XCTest { - static func save(_ data: Data, filename: String, extension fileExtension: String) throws { - let thisSourceFile = URL(fileURLWithPath: #file) - let thisDirectory = thisSourceFile.deletingLastPathComponent() - let path = thisDirectory - .appendingPathComponent(filename) - .appendingPathExtension(fileExtension) - return try data.write(to: path) - } - -} diff --git a/Tests/GeoJSONKitTurfTests/PositionTests.swift b/Tests/GeoJSONKitTurfTests/PositionTests.swift index e80efe3..702b6e5 100644 --- a/Tests/GeoJSONKitTurfTests/PositionTests.swift +++ b/Tests/GeoJSONKitTurfTests/PositionTests.swift @@ -34,4 +34,40 @@ class PositionTests: XCTestCase { XCTAssertEqual(endCoordinate.latitude, 79, accuracy: 0.1) XCTAssertEqual(endCoordinate.longitude, 215, accuracy: 0.1) } + + func testConvexHull() throws { + try Fixture.fixtures(folder: "convex") { name, input, expected in + let actual = input.convexHull() + + guard + case .featureCollection(let features) = expected.object, + case .single(.polygon(let expectedPolygon)) = features.last?.geometry + else { + return XCTFail("Unexpected expected output. Should have a polygon last.") + } + + if actual != expectedPolygon { + // Give it another chance on the data-level, too + do { + var options: JSONSerialization.WritingOptions = [.prettyPrinted] + if #available(iOS 11.0, OSX 10.13, *) { + options.insert(.sortedKeys) + } + let newData = try GeoJSON(geometry: .single(.polygon(actual))).toData(options: options) + let oldData = try GeoJSON(geometry: .single(.polygon(expectedPolygon))).toData(options: options) + if newData != oldData { + if true { + try Self.save(newData, filename: "out_actual", extension: "geojson") + try Self.save(oldData, filename: "out_expected", extension: "geojson") + } + XCTFail("Fixture check failed for \(name)!") + } + + } catch { + XCTFail("Fixture check failed for \(name)! Also: Generating JSON failed with: \(error)") + } + } + + } + } } diff --git a/Tests/GeoJSONKitTurfTests/XCTest+Save.swift b/Tests/GeoJSONKitTurfTests/XCTest+Save.swift new file mode 100644 index 0000000..1b88fcf --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/XCTest+Save.swift @@ -0,0 +1,20 @@ +// +// XCTest+Save.swift +// +// +// Created by Adrian Schönig on 14/6/2022. +// + +import XCTest + + +extension XCTest { + static func save(_ data: Data, filename: String, extension fileExtension: String) throws { + let thisSourceFile = URL(fileURLWithPath: #file) + let thisDirectory = thisSourceFile.deletingLastPathComponent() + let path = thisDirectory + .appendingPathComponent(filename) + .appendingPathExtension(fileExtension) + return try data.write(to: path) + } +}