├── docs ├── release.png ├── pre-release.png └── jazzy.yml ├── codecov.yml ├── Tests └── TurfTests │ ├── Fixtures │ ├── point.geojson │ ├── simple-line.geojson │ ├── simplify │ │ ├── in │ │ │ ├── point.geojson │ │ │ ├── multipoint.geojson │ │ │ ├── issue-#1144.geojson │ │ │ ├── poly-issue#555-5.geojson │ │ │ ├── fiji-hiQ.geojson │ │ │ ├── linestring.geojson │ │ │ ├── simple-polygon.geojson │ │ │ ├── argentina.geojson │ │ │ ├── featurecollection.geojson │ │ │ └── polygon.geojson │ │ └── out │ │ │ ├── point.geojson │ │ │ ├── multipoint.geojson │ │ │ ├── simple-polygon.geojson │ │ │ ├── polygon.geojson │ │ │ ├── issue-#1144.geojson │ │ │ ├── poly-issue#555-5.geojson │ │ │ ├── fiji-hiQ.geojson │ │ │ ├── linestring.geojson │ │ │ ├── featurecollection.geojson │ │ │ └── argentina.geojson │ ├── multiline.geojson │ ├── multipoint.geojson │ ├── multipolygon.geojson │ ├── polygon.geojson │ ├── featurecollection-no-properties.geojson │ ├── dc-line.geojson │ ├── geometry-collection.geojson │ └── featurecollection.geojson │ ├── FeatureIdentifier.swift │ ├── Info.plist │ ├── MultiPointTests.swift │ ├── MultiLineStringTests.swift │ ├── RadianCoordinate2DTests.swift │ ├── LocationCoordinate2DTests.swift │ ├── GeometryTests.swift │ ├── GeometryCollectionTests.swift │ ├── PointTests.swift │ ├── BoundingBoxTests.swift │ ├── Fixture.swift │ └── FeatureCollectionTests.swift ├── Gemfile ├── Turf.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ ├── xcbaselines │ └── 35650AF81F150DC500B5C158.xcbaseline │ │ ├── Info.plist │ │ └── 1DE3DB67-1DC9-4894-9456-734078D13A16.plist │ └── xcschemes │ └── Turf.xcscheme ├── Sources └── Turf │ ├── Turf.h │ ├── Codable.swift │ ├── Info.plist │ ├── FeatureCollection.swift │ ├── Geometries │ ├── MultiPoint.swift │ ├── Point.swift │ ├── GeometryCollection.swift │ ├── MultiLineString.swift │ └── MultiPolygon.swift │ ├── Feature.swift │ ├── Turf.swift │ ├── RadianCoordinate2D.swift │ ├── FeatureIdentifier.swift │ ├── Simplifier.swift │ ├── BoundingBox.swift │ ├── WKT.swift │ ├── Spline.swift │ ├── Ring.swift │ ├── GeoJSON.swift │ ├── Geometry.swift │ ├── CoreLocation.swift │ └── JSON.swift ├── .fastlane └── Fastfile ├── LICENSE.md ├── RELEASE.md ├── scripts ├── document.sh ├── xcframework.sh ├── release.sh └── pre-release.sh ├── Package.swift ├── Turf.podspec ├── .gitignore ├── .circleci └── config.yml ├── Gemfile.lock └── README.md /docs/release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/turf-swift/HEAD/docs/release.png -------------------------------------------------------------------------------- /docs/pre-release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/turf-swift/HEAD/docs/pre-release.png -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "Tests" 3 | 4 | coverage: 5 | status: 6 | project: 7 | default: 8 | threshold: 1% 9 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/point.geojson: -------------------------------------------------------------------------------- 1 | {"type":"Feature", "id": 1, "properties":{},"geometry":{"type":"Point","coordinates":[14.765625,26.194876675795218]}} 2 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simple-line.geojson: -------------------------------------------------------------------------------- 1 | {"type":"Feature", "id": "1", "properties":{},"geometry":{"type":"LineString","coordinates":[[0,0],[0,2],[0,5],[0,8],[0,10],[0,10]]}} 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Fastlane 2.220.0 introduced a new crypto algo for Match, which is not compatible with the pre-existed versions 4 | gem "fastlane", '= 2.219.0' 5 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/in/point.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Point", 6 | "coordinates": [5, 1] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/out/point.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Point", 6 | "coordinates": [5, 1] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/multiline.geojson: -------------------------------------------------------------------------------- 1 | {"type":"Feature","properties":{},"geometry":{"type":"MultiLineString","coordinates":[[[0,0],[0,2],[0,5],[0,8],[0,10],[0,10]],[[1,1],[2,2],[3,3],[4,4],[5,5],[6,6]]]}} 2 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/multipoint.geojson: -------------------------------------------------------------------------------- 1 | {"type":"Feature","properties":{},"geometry":{"type":"MultiPoint","coordinates":[[14.765625,26.194876675795218],[8.61328125,23.483400654325642],[17.75390625,24.926294766395593]]}} 2 | -------------------------------------------------------------------------------- /Turf.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/in/multipoint.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "MultiPoint", 6 | "coordinates": [ 7 | [0, 0], 8 | [1, 2] 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/out/multipoint.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "MultiPoint", 6 | "coordinates": [ 7 | [0, 0], 8 | [1, 2] 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Turf.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/multipolygon.geojson: -------------------------------------------------------------------------------- 1 | {"type":"Feature","properties":{},"geometry":{"type":"MultiPolygon","coordinates":[[[[0,0],[5,0],[5,0],[10,0],[10,10],[0,10],[0,5],[0,0]],[[1,5],[1,7],[1,8.5],[4.5,8.5],[4.5,7],[4.5,5],[1,5]]],[[[11,11],[11.5,11.5],[12,12],[12,11],[11.5,11],[11,11],[11,11]]]]}} 2 | -------------------------------------------------------------------------------- /docs/jazzy.yml: -------------------------------------------------------------------------------- 1 | module: Turf 2 | author: Mapbox 3 | title: Turf for Swift 4 | author_url: https://github.com/mapbox/turf-swift/ 5 | github_url: https://github.com/mapbox/turf-swift 6 | copyright: '© 2014–2024 [Mapbox](https://www.mapbox.com/). See [license](https://github.com/mapbox/turf-swift/blob/main/LICENSE.md) for more details.' 7 | -------------------------------------------------------------------------------- /Turf.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/Turf/Turf.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for Turf. 4 | FOUNDATION_EXPORT double TurfVersionNumber; 5 | 6 | //! Project version string for Turf. 7 | FOUNDATION_EXPORT const unsigned char TurfVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | 11 | 12 | -------------------------------------------------------------------------------- /.fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:ios) 2 | 3 | platform :ios do 4 | lane :setup_distribution_cert do 5 | setup_ci 6 | match( 7 | git_url: "git@github.com:mapbox/apple-certificates.git", 8 | type: "appstore", 9 | readonly: true, 10 | skip_provisioning_profiles: true, 11 | app_identifier: [] 12 | ) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/out/simple-polygon.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": { 4 | "tolerance": 100 5 | }, 6 | "geometry": { 7 | "type": "Polygon", 8 | "coordinates": [ 9 | [ 10 | [26.14843, -28.297552], 11 | [26.150354, -28.302606], 12 | [26.135463, -28.304283], 13 | [26.14843, -28.297552] 14 | ] 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/out/polygon.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": { 4 | "tolerance": 1, 5 | "elevation": 25 6 | }, 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [-75.51527, 39.11245], 12 | [-75.142602, 39.875538], 13 | [-75.813087, 39.904921], 14 | [-75.51527, 39.11245] 15 | ] 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/TurfTests/FeatureIdentifier.swift: -------------------------------------------------------------------------------- 1 | import Turf 2 | import XCTest 3 | 4 | final class FeatureIdentifierTests: XCTestCase { 5 | func testConvenienceAccessors() { 6 | XCTAssertEqual(FeatureIdentifier("foo").string, "foo") 7 | XCTAssertEqual(FeatureIdentifier("foo").number, nil) 8 | 9 | XCTAssertEqual(FeatureIdentifier(3.14).string, nil) 10 | XCTAssertEqual(FeatureIdentifier(3.14).number, 3.14) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Turf/Codable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A coding key as an extensible enumeration. 5 | */ 6 | struct AnyCodingKey: CodingKey { 7 | var stringValue: String 8 | var intValue: Int? 9 | 10 | init?(stringValue: String) { 11 | self.stringValue = stringValue 12 | self.intValue = nil 13 | } 14 | 15 | init?(intValue: Int) { 16 | self.stringValue = String(intValue) 17 | self.intValue = intValue 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/polygon.geojson: -------------------------------------------------------------------------------- 1 | {"type":"Feature", "id": 1.01, "properties":{"null_test": null},"geometry":{"type":"Polygon","coordinates":[[[-109.05029296875,37.00255267215955],[-102.0849609375,37.020098201368114],[-102.041015625,41.0130657870063],[-109.072265625,40.97989806962013],[-109.05029296875,37.00255267215955]],[[-108.56689453125,40.6306300839918],[-108.61083984375,37.43997405227057],[-102.50244140624999,37.405073750176925],[-102.4365234375,40.66397287638688],[-108.56689453125,40.6306300839918]]]}} 2 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/out/issue-#1144.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Polygon", 6 | "coordinates": [ 7 | [ 8 | [-70.603637, -33.399918], 9 | [-70.683975, -33.404504], 10 | [-70.701141, -33.434306], 11 | [-70.694274, -33.458369], 12 | [-70.668869, -33.472117], 13 | [-70.609817, -33.468107], 14 | [-70.587158, -33.442901], 15 | [-70.603637, -33.399918] 16 | ] 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/out/poly-issue#555-5.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": { 4 | "tolerance": 0.00005 5 | }, 6 | "geometry": { 7 | "type": "Polygon", 8 | "coordinates": [ 9 | [ 10 | [-75.788024, 45.345283], 11 | [-75.787931, 45.345237], 12 | [-75.787975, 45.345143], 13 | [-75.787855, 45.345099], 14 | [-75.788023, 45.345028], 15 | [-75.78814, 45.345236], 16 | [-75.788024, 45.345283] 17 | ], 18 | [ 19 | [-75.787933, 45.345065], 20 | [-75.78793, 45.34506], 21 | [-75.78793, 45.345066], 22 | [-75.787933, 45.345065] 23 | ] 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2014–2024, Mapbox 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -------------------------------------------------------------------------------- /Tests/TurfTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 4.0.0 19 | CFBundleVersion 20 | 38 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/out/fiji-hiQ.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": { 4 | "tolerance": 0.005, 5 | "highQuality": true 6 | }, 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [179.975281, -16.51477], 12 | [179.980431, -16.539127], 13 | [180.0103, -16.523328], 14 | [180.007553, -16.534848], 15 | [180.018196, -16.539127], 16 | [180.061455, -16.525632], 17 | [180.066605, -16.513124], 18 | [180.046349, -16.479547], 19 | [180.086861, -16.44761], 20 | [180.084114, -16.441354], 21 | [180.055618, -16.439707], 22 | [180.026093, -16.464732], 23 | [180.01442, -16.464073], 24 | [179.975281, -16.51477] 25 | ] 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Turf/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 4.0.0 19 | CFBundleVersion 20 | 38 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/featurecollection-no-properties.geojson: -------------------------------------------------------------------------------- 1 | 2 | {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"id":1},"geometry":{"type":"LineString","coordinates":[[27.977542877197266,-26.17500493262446],[27.975482940673828,-26.17870225771557],[27.969818115234375,-26.177931991326645],[27.967071533203125,-26.177623883345735],[27.966899871826172,-26.1810130263384],[27.967758178710938,-26.1853263385099],[27.97290802001953,-26.1853263385099],[27.97496795654297,-26.18270756087535],[27.97840118408203,-26.1810130263384],[27.98011779785156,-26.183323749143113],[27.98011779785156,-26.18655868408986],[27.978744506835938,-26.18933141398614],[27.97496795654297,-26.19025564262006],[27.97119140625,-26.19040968001282],[27.969303131103516,-26.1899475672235],[27.96741485595703,-26.189639491012183],[27.9656982421875,-26.187945057286793],[27.965354919433594,-26.18563442612686],[27.96432495117187,-26.183015655416536]]}}]} 3 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/in/issue-#1144.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Polygon", 6 | "coordinates": [ 7 | [ 8 | [-70.603637, -33.399918], 9 | [-70.614624, -33.395332], 10 | [-70.639343, -33.392466], 11 | [-70.659942, -33.394759], 12 | [-70.683975, -33.404504], 13 | [-70.697021, -33.419406], 14 | [-70.701141, -33.434306], 15 | [-70.700454, -33.446339], 16 | [-70.694274, -33.458369], 17 | [-70.682601, -33.465816], 18 | [-70.668869, -33.472117], 19 | [-70.646209, -33.473835], 20 | [-70.624923, -33.472117], 21 | [-70.609817, -33.468107], 22 | [-70.595397, -33.458369], 23 | [-70.587158, -33.442901], 24 | [-70.587158, -33.426283], 25 | [-70.590591, -33.414248], 26 | [-70.594711, -33.406224], 27 | [-70.603637, -33.399918] 28 | ] 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | 1. Trigger pipeline on `main` branch from CircleCI UI with the following parameters. 4 | ```text 5 | flow: pre-release 6 | version: v 7 | ``` 8 | ![pre-release](docs/pre-release.png) 9 | This pipeline will update version in all manifests, create binary artifact, calculate checksum for SwiftPM and attach binary artifact to draft GitHub release. 10 | 11 | 2. Review and merge PR created by CI automation. 12 | 3. Edit GitHub release description and add proper CHANGELOG. 13 | 4. Trigger pipeline on `main` branch from CircleCI UI with the following parameters. 14 | ```text 15 | flow: release 16 | version: v 17 | ``` 18 | ![release](docs/release.png) 19 | This pipeline will validate that checksum of binary artifact equal to checksum in Package.swift, publish GitHub release as pre-release, validate SwfitPM and CocoaPods manifests, publish CocoaPods release. 20 | 5. Set newly published GitHub release as latest. -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/out/linestring.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "LineString", 6 | "coordinates": [ 7 | [-80.513992, 28.069557], 8 | [-80.48584, 28.042289], 9 | [-80.505753, 28.028349], 10 | [-80.476913, 28.021075], 11 | [-80.49202, 27.998039], 12 | [-80.4673, 27.962263], 13 | [-80.46524, 27.9198], 14 | [-80.405502, 27.930114], 15 | [-80.396576, 27.980456], 16 | [-80.429535, 27.990764], 17 | [-80.414429, 28.009558], 18 | [-80.359497, 27.972572], 19 | [-80.382156, 27.913733], 20 | [-80.417862, 27.88157], 21 | [-80.393829, 27.854254], 22 | [-80.368423, 27.888246], 23 | [-80.354691, 27.868824], 24 | [-80.359497, 27.842112], 25 | [-80.399323, 27.82511], 26 | [-80.400696, 27.793528], 27 | [-80.361557, 27.786846], 28 | [-80.359325, 27.806853], 29 | [-80.354991, 27.796831], 30 | [-80.328727, 27.808485] 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /scripts/document.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -u 6 | 7 | if [ -z `which jazzy` ]; then 8 | echo "Installing jazzy…" 9 | gem install jazzy 10 | if [ -z `which jazzy` ]; then 11 | echo "Unable to install jazzy." 12 | exit 1 13 | fi 14 | fi 15 | 16 | 17 | OUTPUT=${OUTPUT:-documentation} 18 | 19 | BRANCH=$( git describe --tags --match=v*.*.* --abbrev=0 ) 20 | SHORT_VERSION=$( echo ${BRANCH} | sed 's/^v//' ) 21 | RELEASE_VERSION=$( echo ${SHORT_VERSION} | sed -e 's/-.*//' ) 22 | MINOR_VERSION=$( echo ${SHORT_VERSION} | grep -Eo '^\d+\.\d+' ) 23 | 24 | rm -rf ${OUTPUT} 25 | mkdir -p ${OUTPUT} 26 | 27 | #cp -r docs/img "${OUTPUT}" 28 | 29 | jazzy \ 30 | --config docs/jazzy.yml \ 31 | --sdk macosx \ 32 | --module-version ${SHORT_VERSION} \ 33 | --github-file-prefix "https://github.com/mapbox/turf-swift/tree/${BRANCH}" \ 34 | --readme README.md \ 35 | --root-url "https://mapbox.github.io/turf-swift/${RELEASE_VERSION}/" \ 36 | --output ${OUTPUT} \ 37 | --build-tool-arguments CODE_SIGN_IDENTITY=,CODE_SIGNING_REQUIRED=NO,CODE_SIGNING_ALLOWED=NO 38 | 39 | echo $SHORT_VERSION > $OUTPUT/latest_version 40 | -------------------------------------------------------------------------------- /Turf.xcodeproj/xcshareddata/xcbaselines/35650AF81F150DC500B5C158.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 1DE3DB67-1DC9-4894-9456-734078D13A16 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i7 17 | cpuSpeedInMHz 18 | 3300 19 | logicalCPUCoresPerPackage 20 | 4 21 | modelCode 22 | MacBookPro13,2 23 | physicalCPUCoresPerPackage 24 | 2 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | targetDevice 31 | 32 | modelCode 33 | iPhone8,4 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Tests/TurfTests/MultiPointTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | import Turf 6 | 7 | class MultiPointTests: XCTestCase { 8 | 9 | func testMultiPointFeature() { 10 | let data = try! Fixture.geojsonData(from: "multipoint")! 11 | let firstCoordinate = LocationCoordinate2D(latitude: 26.194876675795218, longitude: 14.765625) 12 | let lastCoordinate = LocationCoordinate2D(latitude: 24.926294766395593, longitude: 17.75390625) 13 | 14 | let geojson = try! JSONDecoder().decode(Feature.self, from: data) 15 | 16 | guard case let .multiPoint(multipointCoordinates) = geojson.geometry else { 17 | XCTFail() 18 | return 19 | } 20 | XCTAssert(multipointCoordinates.coordinates.first == firstCoordinate) 21 | XCTAssert(multipointCoordinates.coordinates.last == lastCoordinate) 22 | 23 | let encodedData = try! JSONEncoder().encode(geojson) 24 | let decoded = try! JSONDecoder().decode(Feature.self, from: encodedData) 25 | guard case let .multiPoint(decodedMultipointCoordinates) = decoded.geometry else { 26 | XCTFail() 27 | return 28 | } 29 | XCTAssert(decodedMultipointCoordinates.coordinates.first == firstCoordinate) 30 | XCTAssert(decodedMultipointCoordinates.coordinates.last == lastCoordinate) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/TurfTests/MultiLineStringTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | import Turf 6 | 7 | class MultiLineStringTests: XCTestCase { 8 | 9 | func testMultiLineStringFeature() { 10 | let data = try! Fixture.geojsonData(from: "multiline")! 11 | let firstCoordinate = LocationCoordinate2D(latitude: 0, longitude: 0) 12 | let lastCoordinate = LocationCoordinate2D(latitude: 6, longitude: 6) 13 | 14 | let geojson = try! JSONDecoder().decode(Feature.self, from: data) 15 | 16 | guard case let .multiLineString(multiLineStringCoordinates) = geojson.geometry else { 17 | XCTFail() 18 | return 19 | } 20 | XCTAssert(multiLineStringCoordinates.coordinates.first?.first == firstCoordinate) 21 | XCTAssert(multiLineStringCoordinates.coordinates.last?.last == lastCoordinate) 22 | 23 | let encodedData = try! JSONEncoder().encode(geojson) 24 | let decoded = try! JSONDecoder().decode(Feature.self, from: encodedData) 25 | guard case let .multiLineString(decodedMultiLineStringCoordinates) = decoded.geometry else { 26 | XCTFail() 27 | return 28 | } 29 | 30 | XCTAssert(decodedMultiLineStringCoordinates.coordinates.first?.first == firstCoordinate) 31 | XCTAssert(decodedMultiLineStringCoordinates.coordinates.last?.last == lastCoordinate) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | /// In order to keep Linux comatibility we leave the source based target for Linux. 7 | /// Apple platforms will use the binary target in order to be compatible with binary dependency in MapboxSDK stack. 8 | #if canImport(Darwin) 9 | let targets: [Target] = [ 10 | .binaryTarget( 11 | name: "Turf", 12 | url: "https://github.com/mapbox/turf-swift/releases/download/v4.0.0/Turf.xcframework.zip", 13 | checksum: "ce43384a6f875ab4becdd6bdb7ca60447e5e9133f2acf325dc57be381b52a34c" 14 | ) 15 | ] 16 | #else 17 | let targets: [Target] = [ 18 | .target( 19 | name: "Turf", 20 | dependencies: [], 21 | exclude: ["Info.plist"] 22 | ), 23 | .testTarget( 24 | name: "TurfTests", 25 | dependencies: ["Turf"], 26 | exclude: ["Info.plist", "Fixtures/simplify"], 27 | resources: [ 28 | .process("Fixtures"), 29 | ], 30 | swiftSettings: [.define("SPM_TESTING")] 31 | ) 32 | ] 33 | #endif 34 | 35 | let package = Package( 36 | name: "Turf", 37 | platforms: [ 38 | .macOS(.v10_13), .iOS(.v11), .watchOS(.v4), .tvOS(.v11), .custom("visionos", versionString: "1.0") 39 | ], 40 | products: [ 41 | .library(name: "Turf", targets: ["Turf"]), 42 | ], 43 | targets: targets 44 | ) -------------------------------------------------------------------------------- /scripts/xcframework.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eou pipefail 4 | 5 | BUILD_DIRECTORY=${1:-"."} 6 | echo "Temporary directory: $BUILD_DIRECTORY" 7 | 8 | platforms=("iOS" "iOS Simulator" "macOS" "macOS,variant=Mac Catalyst" "tvOS" "tvOS Simulator" "watchOS" "watchOS Simulator" "visionOS" "visionOS Simulator") 9 | 10 | # build Turf for each platform 11 | 12 | commands=() 13 | for platform in "${platforms[@]}" 14 | do 15 | xcodebuild archive \ 16 | -scheme "Turf" \ 17 | -configuration Release \ 18 | -archivePath "$BUILD_DIRECTORY/archives/Turf-$platform.xcarchive" \ 19 | -destination "generic/platform=$platform" \ 20 | SKIP_INSTALL=NO \ 21 | BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ 22 | SUPPORTS_MACCATALYST=YES 23 | 24 | commands+=("-archive" "$BUILD_DIRECTORY/archives/Turf-$platform.xcarchive") 25 | commands+=("-framework" "Turf.framework") 26 | 27 | done 28 | 29 | xcodebuild -create-xcframework "${commands[@]}" -output "$BUILD_DIRECTORY/Turf.xcframework" 30 | codesign --timestamp -v --sign "Apple Distribution: Mapbox, Inc." "$BUILD_DIRECTORY/Turf.xcframework" 31 | 32 | cp "LICENSE.md" "$BUILD_DIRECTORY/LICENSE.md" 33 | cd "$BUILD_DIRECTORY" 34 | 35 | ZIP_OUTPUT_PATH="Turf.xcframework.zip" 36 | rm -rf "$ZIP_OUTPUT_PATH" 37 | 38 | zip --symlinks -r "$ZIP_OUTPUT_PATH" \ 39 | Turf.xcframework \ 40 | LICENSE.md 41 | 42 | CHECKSUM=$(swift package compute-checksum "$ZIP_OUTPUT_PATH") 43 | echo "$CHECKSUM" > "xcframework_checksum.txt" 44 | echo "Checksum: $CHECKSUM" 45 | -------------------------------------------------------------------------------- /Turf.xcodeproj/xcshareddata/xcbaselines/35650AF81F150DC500B5C158.xcbaseline/1DE3DB67-1DC9-4894-9456-734078D13A16.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | GeoJSONTests 8 | 9 | testPerformanceDecodeEncodeFeatureCollection() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 2 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | maxPercentRelativeStandardDeviation 18 | 40 19 | 20 | 21 | testPerformanceDecodeFeatureCollection() 22 | 23 | com.apple.XCTPerformanceMetric_WallClockTime 24 | 25 | baselineAverage 26 | 1.23 27 | baselineIntegrationDisplayName 28 | Local Baseline 29 | maxPercentRelativeStandardDeviation 30 | 30 31 | 32 | 33 | testPerformanceEncodeFeatureCollection() 34 | 35 | com.apple.XCTPerformanceMetric_WallClockTime 36 | 37 | baselineAverage 38 | 0.893 39 | baselineIntegrationDisplayName 40 | Local Baseline 41 | maxPercentRelativeStandardDeviation 42 | 40 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Sources/Turf/FeatureCollection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A [FeatureCollection object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.3) is a collection of Feature objects. 5 | */ 6 | public struct FeatureCollection: Equatable, ForeignMemberContainer { 7 | /// The features that the collection contains. 8 | public var features: [Feature] = [] 9 | 10 | public var foreignMembers: JSONObject = [:] 11 | 12 | /** 13 | Initializes a feature collection containing the given features. 14 | 15 | - parameter features: The features that the collection contains. 16 | */ 17 | public init(features: [Feature]) { 18 | self.features = features 19 | } 20 | } 21 | 22 | extension FeatureCollection: Codable { 23 | private enum CodingKeys: String, CodingKey { 24 | case kind = "type" 25 | case features 26 | } 27 | 28 | enum Kind: String, Codable { 29 | case FeatureCollection 30 | } 31 | 32 | public init(from decoder: Decoder) throws { 33 | let container = try decoder.container(keyedBy: CodingKeys.self) 34 | _ = try container.decode(Kind.self, forKey: .kind) 35 | features = try container.decode([Feature].self, forKey: .features) 36 | try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) 37 | } 38 | 39 | public func encode(to encoder: Encoder) throws { 40 | var container = encoder.container(keyedBy: CodingKeys.self) 41 | try container.encode(Kind.FeatureCollection, forKey: .kind) 42 | try container.encode(features, forKey: .features) 43 | try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Turf.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 4 | 5 | s.name = "Turf" 6 | s.version = "4.0.0" 7 | s.summary = "Simple spatial analysis." 8 | s.description = "A spatial analysis library written in Swift for native iOS, macOS, tvOS, watchOS, visionOS, and Linux applications, ported from Turf.js." 9 | 10 | s.homepage = "https://github.com/mapbox/turf-swift" 11 | 12 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 13 | 14 | s.license = { :type => "ISC", :file => "LICENSE.md" } 15 | 16 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 17 | 18 | s.author = { "Mapbox" => "mobile@mapbox.com" } 19 | s.social_media_url = "https://twitter.com/mapbox" 20 | 21 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 22 | 23 | s.ios.deployment_target = "11.0" 24 | s.osx.deployment_target = "10.13" 25 | s.tvos.deployment_target = "11.0" 26 | s.watchos.deployment_target = "4.0" 27 | # CocoaPods doesn't support releasing of visionOS pods yet, need to wait for v1.15.0 release of CocoaPods 28 | # with this fix https://github.com/CocoaPods/CocoaPods/pull/12159. 29 | # s.visionos.deployment_target = "1.0" 30 | 31 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 32 | 33 | s.source = { 34 | :http => "https://github.com/mapbox/turf-swift/releases/download/v#{s.version.to_s}/Turf.xcframework.zip" 35 | } 36 | 37 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 38 | 39 | s.requires_arc = true 40 | s.module_name = "Turf" 41 | s.frameworks = 'CoreLocation' 42 | s.swift_version = "5.7" 43 | s.vendored_frameworks = 'Turf.xcframework' 44 | 45 | end 46 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/in/poly-issue#555-5.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": { 4 | "tolerance": 0.00005 5 | }, 6 | "geometry": { 7 | "type": "Polygon", 8 | "coordinates": [ 9 | [ 10 | [-75.788024, 45.345283], 11 | [-75.788012, 45.345262], 12 | [-75.787966, 45.345274], 13 | [-75.787959, 45.345262], 14 | [-75.787947, 45.345266], 15 | [-75.787931, 45.345237], 16 | [-75.787943, 45.345234], 17 | [-75.787928, 45.345207], 18 | [-75.787999, 45.345187], 19 | [-75.787975, 45.345143], 20 | [-75.787906, 45.345162], 21 | [-75.7879, 45.345151], 22 | [-75.787897, 45.345152], 23 | [-75.787882, 45.345125], 24 | [-75.787872, 45.345128], 25 | [-75.787855, 45.345099], 26 | [-75.787866, 45.345096], 27 | [-75.787862, 45.345088], 28 | [-75.787855, 45.34509], 29 | [-75.787832, 45.345086], 30 | [-75.787825, 45.345069], 31 | [-75.787842, 45.345056], 32 | [-75.787867, 45.34506], 33 | [-75.787872, 45.345081], 34 | [-75.787903, 45.345073], 35 | [-75.787897, 45.345052], 36 | [-75.787913, 45.345048], 37 | [-75.78792, 45.345059], 38 | [-75.787928, 45.345056], 39 | [-75.787928, 45.345055], 40 | [-75.788023, 45.345028], 41 | [-75.78814, 45.345236], 42 | [-75.788044, 45.345263], 43 | [-75.788043, 45.345262], 44 | [-75.78804, 45.345263], 45 | [-75.788032, 45.345265], 46 | [-75.78804, 45.345279], 47 | [-75.788024, 45.345283] 48 | ], 49 | [ 50 | [-75.787933, 45.345065], 51 | [-75.78793, 45.34506], 52 | [-75.78793, 45.345066], 53 | [-75.787933, 45.345065] 54 | ] 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | .swiftpm/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | .fastlane/report.xml 66 | .fastlane/Preview.html 67 | .fastlane/screenshots 68 | .fastlane/test_output 69 | .fastlane/README.md 70 | 71 | Turf.xcframework.zip 72 | Turf.xcframework 73 | xcframework_checksum.txt 74 | archives 75 | -------------------------------------------------------------------------------- /Sources/Turf/Geometries/MultiPoint.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | /** 7 | A [MultiPoint geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.3) represents a collection of disconnected but related positions. 8 | */ 9 | public struct MultiPoint: Equatable, ForeignMemberContainer { 10 | /// The positions at which the multipoint is located. 11 | public var coordinates: [LocationCoordinate2D] 12 | 13 | public var foreignMembers: JSONObject = [:] 14 | 15 | /** 16 | Initializes a multipoint defined by the given positions. 17 | 18 | - parameter coordinates: The positions at which the multipoint is located. 19 | */ 20 | public init(_ coordinates: [LocationCoordinate2D]) { 21 | self.coordinates = coordinates 22 | } 23 | } 24 | 25 | extension MultiPoint: Codable { 26 | enum CodingKeys: String, CodingKey { 27 | case kind = "type" 28 | case coordinates 29 | } 30 | 31 | enum Kind: String, Codable { 32 | case MultiPoint 33 | } 34 | 35 | public init(from decoder: Decoder) throws { 36 | let container = try decoder.container(keyedBy: CodingKeys.self) 37 | _ = try container.decode(Kind.self, forKey: .kind) 38 | let coordinates = try container.decode([LocationCoordinate2DCodable].self, forKey: .coordinates).decodedCoordinates 39 | self = .init(coordinates) 40 | try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) 41 | } 42 | 43 | public func encode(to encoder: Encoder) throws { 44 | var container = encoder.container(keyedBy: CodingKeys.self) 45 | try container.encode(Kind.MultiPoint, forKey: .kind) 46 | try container.encode(coordinates.codableCoordinates, forKey: .coordinates) 47 | try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/TurfTests/RadianCoordinate2DTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | @testable import Turf 6 | 7 | class RadianCoordinate2DTests: XCTestCase { 8 | 9 | func testCalculatesDirection() { 10 | let startCoordinate = RadianCoordinate2D(latitude: 35, longitude: 35) 11 | let endCoordianate = RadianCoordinate2D(latitude: -10, longitude: -10) 12 | let angle = startCoordinate.direction(to: endCoordianate) 13 | XCTAssertEqual(angle.value, 2.3, accuracy: 0.1) 14 | XCTAssertEqual(angle.unit, .radians) 15 | } 16 | 17 | func testCalculatesCoordinateFacingDirectionInDegrees() { 18 | let startCoordianate = RadianCoordinate2D(latitude: 35, longitude: 35) 19 | let angleInDegrees = Measurement(value: 45, unit: .degrees) 20 | let endCoordinate = startCoordianate.coordinate(at: 20, facing: angleInDegrees) 21 | XCTAssertEqual(endCoordinate.latitude, -0.8, accuracy: 0.1) 22 | XCTAssertEqual(endCoordinate.longitude, 33.6, accuracy: 0.1) 23 | } 24 | 25 | func testCalculatesCoordinateFacingDirectionInRadians() { 26 | let startCoordianate = RadianCoordinate2D(latitude: 35, longitude: 35) 27 | let angleInRadians = Measurement(value: 0.35, unit: .radians) 28 | let endCoordinate = startCoordianate.coordinate(at: 20, facing: angleInRadians) 29 | XCTAssertEqual(endCoordinate.latitude, -1.25, accuracy: 0.1) 30 | XCTAssertEqual(endCoordinate.longitude, 33.4, accuracy: 0.1) 31 | } 32 | 33 | func testCalculatesDistanceBetweenCoordinates() { 34 | let startCoordianate = RadianCoordinate2D(latitude: 35, longitude: 35) 35 | let endCoordianate = RadianCoordinate2D(latitude: -10, longitude: -10) 36 | let distance = startCoordianate.distance(to: endCoordianate) 37 | XCTAssertEqual(distance, 1.4, accuracy: 0.1) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/TurfTests/LocationCoordinate2DTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | @testable import Turf 6 | 7 | class LocationCoordinate2DTests: XCTestCase { 8 | 9 | func testCalculatesDirection() { 10 | let startCoordinate = LocationCoordinate2D(latitude: 35, longitude: 35) 11 | let endCoordianate = LocationCoordinate2D(latitude: -10, longitude: -10) 12 | let angle = startCoordinate.direction(to: endCoordianate) 13 | XCTAssertEqual(angle, -128, accuracy: 1) 14 | } 15 | 16 | func testCalculatesCoordinateFacingDirectionInDegrees() { 17 | let startCoordianate = LocationCoordinate2D(latitude: 35, longitude: 35) 18 | let angleInDegrees = Measurement(value: 45, unit: .degrees) 19 | let endCoordinate = startCoordianate.coordinate(at: 20 * metersPerRadian, facing: angleInDegrees) 20 | XCTAssertEqual(endCoordinate.latitude, 49.7, accuracy: 0.1) 21 | XCTAssertEqual(endCoordinate.longitude, 128.2, accuracy: 0.1) 22 | } 23 | 24 | func testCalculatesCoordinateFacingDirectionInRadians() { 25 | let startCoordianate = LocationCoordinate2D(latitude: 35, longitude: 35) 26 | let angleInRadians = Measurement(value: 0.35, unit: .radians) 27 | let endCoordinate = startCoordianate.coordinate(at: 20 * metersPerRadian, facing: angleInRadians) 28 | XCTAssertEqual(endCoordinate.latitude, 69.5, accuracy: 0.1) 29 | XCTAssertEqual(endCoordinate.longitude, 151.7, accuracy: 0.1) 30 | } 31 | 32 | func testDeprecatedCalculationOfCoordinateFacingDirectionInDegrees() { 33 | let startCoordianate = LocationCoordinate2D(latitude: 35, longitude: 35) 34 | let endCoordinate = startCoordianate.coordinate(at: 20 * metersPerRadian, facing: 0) 35 | XCTAssertEqual(endCoordinate.latitude, 79, accuracy: 0.1) 36 | XCTAssertEqual(endCoordinate.longitude, 215, accuracy: 0.1) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/dc-line.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "LineString", 6 | "coordinates": [ 7 | [ 8 | -77.0316696166992, 9 | 38.878605901789236 10 | ], 11 | [ 12 | -77.02960968017578, 13 | 38.88194668656296 14 | ], 15 | [ 16 | -77.02033996582031, 17 | 38.88408470638821 18 | ], 19 | [ 20 | -77.02566146850586, 21 | 38.885821800123196 22 | ], 23 | [ 24 | -77.02188491821289, 25 | 38.88956308852534 26 | ], 27 | [ 28 | -77.01982498168944, 29 | 38.89236892551996 30 | ], 31 | [ 32 | -77.02291488647461, 33 | 38.89370499941828 34 | ], 35 | [ 36 | -77.02291488647461, 37 | 38.89958342598271 38 | ], 39 | [ 40 | -77.01896667480469, 41 | 38.90011780426885 42 | ], 43 | [ 44 | -77.01845169067383, 45 | 38.90733151751689 46 | ], 47 | [ 48 | -77.02291488647461, 49 | 38.907865837489105 50 | ], 51 | [ 52 | -77.02377319335936, 53 | 38.91200668090932 54 | ], 55 | [ 56 | -77.02995300292969, 57 | 38.91254096569048 58 | ], 59 | [ 60 | -77.03338623046875, 61 | 38.91708222394378 62 | ], 63 | [ 64 | -77.03784942626953, 65 | 38.920821865485834 66 | ], 67 | [ 68 | -77.03115463256836, 69 | 38.92830055730587 70 | ], 71 | [ 72 | -77.03596115112305, 73 | 38.931505469602044 74 | ] 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/TurfTests/GeometryTests.swift: -------------------------------------------------------------------------------- 1 | import Turf 2 | import XCTest 3 | 4 | final class GeometryTests: XCTestCase { 5 | func testConvenienceAccessors() { 6 | let point = Point(LocationCoordinate2D(latitude: 1, longitude: 2)) 7 | XCTAssertEqual(Geometry.point(point).point, point) 8 | XCTAssertEqual(Geometry.point(point).lineString, nil) 9 | 10 | let lineString = LineString([LocationCoordinate2D(latitude: 1, longitude: 2)]) 11 | XCTAssertEqual(Geometry.lineString(lineString).lineString, lineString) 12 | XCTAssertEqual(Geometry.lineString(lineString).point, nil) 13 | 14 | let polygon = Polygon([[LocationCoordinate2D(latitude: 1, longitude: 2)]]) 15 | XCTAssertEqual(Geometry.polygon(polygon).polygon, polygon) 16 | XCTAssertEqual(Geometry.polygon(polygon).point, nil) 17 | 18 | 19 | let multiPoint = MultiPoint([LocationCoordinate2D(latitude: 1, longitude: 2)]) 20 | XCTAssertEqual(Geometry.multiPoint(multiPoint).multiPoint, multiPoint) 21 | XCTAssertEqual(Geometry.multiPoint(multiPoint).point, nil) 22 | 23 | let multiLineString = MultiLineString([[LocationCoordinate2D(latitude: 1, longitude: 2)]]) 24 | XCTAssertEqual(Geometry.multiLineString(multiLineString).multiLineString, multiLineString) 25 | XCTAssertEqual(Geometry.multiLineString(multiLineString).point, nil) 26 | 27 | let multiPolygon = MultiPolygon([[[LocationCoordinate2D(latitude: 1, longitude: 2)]]]) 28 | XCTAssertEqual(Geometry.multiPolygon(multiPolygon).multiPolygon, multiPolygon) 29 | XCTAssertEqual(Geometry.multiPolygon(multiPolygon).point, nil) 30 | 31 | let geometryCollection = GeometryCollection(geometries: [ 32 | Geometry(point), Geometry(lineString) 33 | ]) 34 | XCTAssertEqual(Geometry.geometryCollection(geometryCollection).geometryCollection, geometryCollection) 35 | XCTAssertEqual(Geometry.geometryCollection(geometryCollection).point, nil) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/out/featurecollection.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": { 7 | "id": 1, 8 | "tolerance": 0.01 9 | }, 10 | "geometry": { 11 | "type": "LineString", 12 | "coordinates": [ 13 | [27.977543, -26.175005], 14 | [27.964325, -26.183016] 15 | ] 16 | } 17 | }, 18 | { 19 | "type": "Feature", 20 | "properties": { 21 | "id": 2, 22 | "tolerance": 0.01 23 | }, 24 | "geometry": { 25 | "type": "Polygon", 26 | "coordinates": [ 27 | [ 28 | [27.97205, -26.199035], 29 | [27.984066, -26.192258], 30 | [27.987156, -26.201346], 31 | [27.97205, -26.199035] 32 | ] 33 | ] 34 | } 35 | }, 36 | { 37 | "type": "Feature", 38 | "properties": { 39 | "id": 3, 40 | "tolerance": 0.01 41 | }, 42 | "geometry": { 43 | "type": "Polygon", 44 | "coordinates": [ 45 | [ 46 | [27.946644, -26.170845], 47 | [27.942696, -26.183632], 48 | [27.928619, -26.165299], 49 | [27.945099, -26.158982], 50 | [27.954025, -26.173464], 51 | [27.936172, -26.194877], 52 | [27.916603, -26.16684], 53 | [27.930336, -26.188561], 54 | [27.946644, -26.170845] 55 | ], 56 | [ 57 | [27.936859, -26.165915], 58 | [27.934971, -26.173927], 59 | [27.941494, -26.170075], 60 | [27.936859, -26.165915] 61 | ] 62 | ] 63 | } 64 | }, 65 | { 66 | "type": "Feature", 67 | "properties": { 68 | "id": 4, 69 | "tolerance": 0.01 70 | }, 71 | "geometry": { 72 | "type": "Point", 73 | "coordinates": [27.956429, -26.15251] 74 | } 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eou pipefail 4 | 5 | if [ $# -eq 0 ]; then 6 | echo "Usage: v" 7 | exit 1 8 | fi 9 | 10 | RELEASE_TAG=$1 11 | ARTIFACT_NAME="Turf.xcframework.zip" 12 | REPO="mapbox/turf-swift" 13 | 14 | function setup_token { 15 | GH_TOKEN=$(mbx-ci github writer public token) 16 | export GH_TOKEN 17 | } 18 | 19 | function validate_release_artifact_checksum { 20 | echo "Fetching draft release with tag: $RELEASE_TAG" 21 | RELEASE_ID=$(gh release view "$RELEASE_TAG" --repo "$REPO" --json id -q '.id') 22 | 23 | if [[ -z "$RELEASE_ID" ]]; then 24 | echo "Release with tag $RELEASE_TAG not found." 25 | exit 1 26 | fi 27 | 28 | echo "Downloading artifact: $ARTIFACT_NAME" 29 | gh release download "$RELEASE_TAG" --repo "$REPO" --pattern "$ARTIFACT_NAME" --dir . 30 | 31 | CHECKSUM=$(swift package compute-checksum "$ARTIFACT_NAME") 32 | EXPECTED_CHECKSUM=$(grep -Eo 'checksum: "[a-f0-9]+"' "Package.swift" | awk -F'"' '{print $2}') 33 | 34 | echo "Computed checksum: $CHECKSUM" 35 | echo "Expected checksum: $EXPECTED_CHECKSUM" 36 | 37 | if [ "$CHECKSUM" != "$EXPECTED_CHECKSUM" ]; then 38 | echo "Checksums do not match." 39 | exit 1 40 | fi 41 | 42 | echo "Checksums match." 43 | } 44 | 45 | function publish_github_release { 46 | echo "Publishing release $RELEASE_TAG..." 47 | gh release edit "$RELEASE_TAG" --repo "$REPO" --draft=false --prerelease=true 48 | echo "Release $RELEASE_TAG is now published." 49 | } 50 | 51 | function validate_manifests { 52 | git fetch --tags 53 | git checkout "tags/$RELEASE_TAG" 54 | 55 | echo "Resolve Swift package dependencies" 56 | swift package resolve 57 | 58 | echo "Lint CocoaPods podspec" 59 | pod spec lint 60 | } 61 | 62 | function publish_cocoapods_release { 63 | echo "Push CocoaPods podspec" 64 | pod trunk push 65 | } 66 | 67 | setup_token 68 | validate_release_artifact_checksum 69 | publish_github_release 70 | validate_manifests 71 | publish_cocoapods_release 72 | -------------------------------------------------------------------------------- /Sources/Turf/Geometries/Point.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | /** 7 | A [Point geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.2) represents a single position. 8 | */ 9 | public struct Point: Equatable, ForeignMemberContainer, Sendable { 10 | /** 11 | The position at which the point is located. 12 | 13 | This property has a plural name for consistency with [RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.2). For convenience, it is represented by a `LocationCoordinate2D` instead of a dedicated `Position` type. 14 | */ 15 | public var coordinates: LocationCoordinate2D 16 | 17 | public var foreignMembers: JSONObject = [:] 18 | 19 | /** 20 | Initializes a point defined by the given position. 21 | 22 | - parameter coordinates: The position at which the point is located. 23 | */ 24 | public init(_ coordinates: LocationCoordinate2D) { 25 | self.coordinates = coordinates 26 | } 27 | } 28 | 29 | extension Point: Codable { 30 | enum CodingKeys: String, CodingKey { 31 | case kind = "type" 32 | case coordinates 33 | } 34 | 35 | enum Kind: String, Codable { 36 | case Point 37 | } 38 | 39 | public init(from decoder: Decoder) throws { 40 | let container = try decoder.container(keyedBy: CodingKeys.self) 41 | _ = try container.decode(Kind.self, forKey: .kind) 42 | let coordinates = try container.decode(LocationCoordinate2DCodable.self, forKey: .coordinates).decodedCoordinates 43 | self = .init(coordinates) 44 | try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) 45 | } 46 | 47 | public func encode(to encoder: Encoder) throws { 48 | var container = encoder.container(keyedBy: CodingKeys.self) 49 | try container.encode(Kind.Point, forKey: .kind) 50 | try container.encode(coordinates.codableCoordinates, forKey: .coordinates) 51 | try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/TurfTests/GeometryCollectionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | import Turf 6 | 7 | class GeometryCollectionTests: XCTestCase { 8 | 9 | func testGeometryCollectionFeatureDeserialization() { 10 | // Arrange 11 | let data = try! Fixture.geojsonData(from: "geometry-collection")! 12 | let multiPolygonCoordinate = LocationCoordinate2D(latitude: 8.5, longitude: 1) 13 | 14 | // Act 15 | let geoJSON = try! JSONDecoder().decode(GeoJSONObject.self, from: data) 16 | 17 | // Assert 18 | guard case let .feature(geometryCollectionFeature) = geoJSON else { 19 | XCTFail() 20 | return 21 | } 22 | 23 | guard case let .geometryCollection(geometries) = geometryCollectionFeature.geometry else { 24 | XCTFail() 25 | return 26 | } 27 | 28 | guard case let .multiPolygon(decodedMultiPolygonCoordinate) = geometries.geometries[2] else { 29 | XCTFail() 30 | return 31 | } 32 | XCTAssertEqual(decodedMultiPolygonCoordinate.coordinates[0][1][2], multiPolygonCoordinate) 33 | } 34 | 35 | func testGeometryCollectionFeatureSerialization() { 36 | // Arrange 37 | let multiPolygonCoordinate = LocationCoordinate2D(latitude: 8.5, longitude: 1) 38 | let data = try! Fixture.geojsonData(from: "geometry-collection")! 39 | let geoJSON = try! JSONDecoder().decode(GeoJSONObject.self, from: data) 40 | 41 | // Act 42 | let encodedData = try! JSONEncoder().encode(geoJSON) 43 | let encodedJSON = try! JSONDecoder().decode(GeoJSONObject.self, from: encodedData) 44 | 45 | // Assert 46 | guard case let .feature(geometryCollectionFeature) = encodedJSON else { 47 | XCTFail() 48 | return 49 | } 50 | 51 | guard case let .geometryCollection(geometries) = geometryCollectionFeature.geometry else { 52 | XCTFail() 53 | return 54 | } 55 | 56 | guard case let .multiPolygon(decodedMultiPolygonCoordinate) = geometries.geometries[2] else { 57 | XCTFail() 58 | return 59 | } 60 | XCTAssertEqual(decodedMultiPolygonCoordinate.coordinates[0][1][2], multiPolygonCoordinate) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/geometry-collection.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": { 4 | "id": 1 5 | }, 6 | "geometry": { 7 | "type": "GeometryCollection", 8 | "geometries": [ 9 | { 10 | "type": "Point", 11 | "coordinates": [ 12 | 100, 0 13 | ] 14 | }, 15 | { 16 | "type": "LineString", 17 | "coordinates": [ 18 | [ 19 | 101, 0 20 | ], 21 | [ 22 | 102, 1 23 | ] 24 | ] 25 | }, 26 | { 27 | "type": "MultiPolygon", 28 | "coordinates": [ 29 | [ 30 | [ 31 | [ 32 | 0, 0 33 | ], 34 | [ 35 | 5, 0 36 | ], 37 | [ 38 | 5, 0 39 | ], 40 | [ 41 | 10, 0 42 | ], 43 | [ 44 | 10, 10 45 | ], 46 | [ 47 | 0, 10 48 | ], 49 | [ 50 | 0, 5 51 | ], 52 | [ 53 | 0, 0 54 | ] 55 | ], 56 | [ 57 | [ 58 | 1, 5 59 | ], 60 | [ 61 | 1, 7 62 | ], 63 | [ 64 | 1, 8.5 65 | ], 66 | [ 67 | 4.5, 8.5 68 | ], 69 | [ 70 | 4.5, 7 71 | ], 72 | [ 73 | 4.5, 5 74 | ], 75 | [ 76 | 1, 5 77 | ] 78 | ] 79 | ], 80 | [ 81 | [ 82 | [ 83 | 11, 11 84 | ], 85 | [ 86 | 11.5, 11.5 87 | ], 88 | [ 89 | 12, 12 90 | ], 91 | [ 92 | 12, 11 93 | ], 94 | [ 95 | 11.5, 11 96 | ], 97 | [ 98 | 11, 11 99 | ], 100 | [ 101 | 11, 11 102 | ] 103 | ] 104 | ] 105 | ] 106 | } 107 | ] 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Tests/TurfTests/PointTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | import Turf 6 | 7 | class PointTests: XCTestCase { 8 | 9 | func testPointFeature() { 10 | let data = try! Fixture.geojsonData(from: "point")! 11 | let geojson = try! JSONDecoder().decode(GeoJSONObject.self, from: data) 12 | let coordinate = LocationCoordinate2D(latitude: 26.194876675795218, longitude: 14.765625) 13 | 14 | guard case let .feature(feature) = geojson, 15 | case let .point(point) = feature.geometry else { 16 | XCTFail() 17 | return 18 | } 19 | XCTAssertEqual(point.coordinates, coordinate) 20 | if case let .number(number) = feature.identifier { 21 | XCTAssertEqual(number, 1) 22 | } else { 23 | XCTFail() 24 | } 25 | 26 | let encodedData = try! JSONEncoder().encode(geojson) 27 | let decoded = try! JSONDecoder().decode(GeoJSONObject.self, from: encodedData) 28 | 29 | guard case let .feature(decodedFeature) = decoded, 30 | case let .point(decodedPoint) = decodedFeature.geometry else { 31 | return XCTFail() 32 | } 33 | 34 | XCTAssertEqual(point, decodedPoint) 35 | 36 | if case let .number(number) = feature.identifier, 37 | case let .number(decodedNumber) = decodedFeature.identifier { 38 | XCTAssertEqual(number, decodedNumber) 39 | } else { 40 | XCTFail() 41 | } 42 | } 43 | 44 | func testUnkownPointFeature() { 45 | let data = try! Fixture.geojsonData(from: "point")! 46 | let geojson = try! JSONDecoder().decode(GeoJSONObject.self, from: data) 47 | 48 | guard case let .feature(feature) = geojson, 49 | case let .point(point) = feature.geometry else { 50 | return XCTFail() 51 | } 52 | 53 | var encodedData: Data? 54 | XCTAssertNoThrow(encodedData = try JSONEncoder().encode(GeoJSONObject.geometry(XCTUnwrap(feature.geometry)))) 55 | XCTAssertNotNil(encodedData) 56 | 57 | var decoded: GeoJSONObject? 58 | XCTAssertNoThrow(decoded = try JSONDecoder().decode(GeoJSONObject.self, from: encodedData!)) 59 | XCTAssertNotNil(decoded) 60 | 61 | guard case let .geometry(.point(decodedPoint)) = decoded else { return XCTFail() } 62 | XCTAssertEqual(point, decodedPoint) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Turf/Geometries/GeometryCollection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | /** 7 | A [GeometryCollection geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.8) is a heterogeneous collection of `Geometry` objects that are related. 8 | */ 9 | public struct GeometryCollection: Equatable, ForeignMemberContainer, Sendable { 10 | /// The geometries contained by the geometry collection. 11 | public var geometries: [Geometry] 12 | 13 | public var foreignMembers: JSONObject = [:] 14 | 15 | /** 16 | Initializes a geometry collection defined by the given geometries. 17 | 18 | - parameter geometries: The geometries contained by the geometry collection. 19 | */ 20 | public init(geometries: [Geometry]) { 21 | self.geometries = geometries 22 | } 23 | 24 | /** 25 | Initializes a geometry collection coincident to the given multipolygon. 26 | 27 | You should only use this initializer if you intend to add geometries other than multipolygons to the geometry collection after initializing it. 28 | 29 | - parameter multiPolygon: The multipolygon that is coincident to the geometry collection. 30 | */ 31 | public init(_ multiPolygon: MultiPolygon) { 32 | self.geometries = multiPolygon.coordinates.map { 33 | $0.count > 1 ? 34 | .multiLineString(.init($0)) : 35 | .lineString(.init($0[0])) 36 | } 37 | } 38 | } 39 | 40 | extension GeometryCollection: Codable { 41 | enum CodingKeys: String, CodingKey { 42 | case kind = "type" 43 | case geometries 44 | } 45 | 46 | enum Kind: String, Codable { 47 | case GeometryCollection 48 | } 49 | 50 | public init(from decoder: Decoder) throws { 51 | let container = try decoder.container(keyedBy: CodingKeys.self) 52 | _ = try container.decode(Kind.self, forKey: .kind) 53 | let geometries = try container.decode([Geometry].self, forKey: .geometries) 54 | self = .init(geometries: geometries) 55 | try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) 56 | } 57 | 58 | public func encode(to encoder: Encoder) throws { 59 | var container = encoder.container(keyedBy: CodingKeys.self) 60 | try container.encode(Kind.GeometryCollection, forKey: .kind) 61 | try container.encode(geometries, forKey: .geometries) 62 | try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Turf/Geometries/MultiLineString.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | /** 7 | A [MultiLineString geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.5) is a collection of `LineString` geometries that are disconnected but related. 8 | */ 9 | public struct MultiLineString: Equatable, ForeignMemberContainer { 10 | /// The positions at which the multi–line string is located. Each nested array corresponds to one line string. 11 | public var coordinates: [[LocationCoordinate2D]] 12 | 13 | public var foreignMembers: JSONObject = [:] 14 | 15 | /** 16 | Initializes a multi–line string defined by the given positions. 17 | 18 | - parameter coordinates: The positions at which the multi–line string is located. Each nested array corresponds to one line string. 19 | */ 20 | public init(_ coordinates: [[LocationCoordinate2D]]) { 21 | self.coordinates = coordinates 22 | } 23 | 24 | /** 25 | Initializes a multi–line string coincident to the given polygon’s linear rings. 26 | 27 | This initializer is equivalent to the [`polygon-to-line`](https://turfjs.org/docs/#polygonToLine) package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-polygon-to-line/)). 28 | 29 | - parameter polygon: The polygon whose linear rings are coincident to the multi–line string. 30 | */ 31 | public init(_ polygon: Polygon) { 32 | self.coordinates = polygon.coordinates 33 | } 34 | } 35 | 36 | extension MultiLineString: Codable { 37 | enum CodingKeys: String, CodingKey { 38 | case kind = "type" 39 | case coordinates 40 | } 41 | 42 | enum Kind: String, Codable { 43 | case MultiLineString 44 | } 45 | 46 | public init(from decoder: Decoder) throws { 47 | let container = try decoder.container(keyedBy: CodingKeys.self) 48 | _ = try container.decode(Kind.self, forKey: .kind) 49 | let coordinates = try container.decode([[LocationCoordinate2DCodable]].self, forKey: .coordinates).decodedCoordinates 50 | self = .init(coordinates) 51 | try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) 52 | } 53 | 54 | public func encode(to encoder: Encoder) throws { 55 | var container = encoder.container(keyedBy: CodingKeys.self) 56 | try container.encode(Kind.MultiLineString, forKey: .kind) 57 | try container.encode(coordinates.codableCoordinates, forKey: .coordinates) 58 | try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Turf/Feature.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | /** 7 | A [Feature object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2) represents a spatially bounded thing. 8 | */ 9 | public struct Feature: Equatable, ForeignMemberContainer { 10 | /** 11 | A string or number that commonly identifies the feature in the context of a data set. 12 | 13 | Turf does not guarantee that the feature is unique; however, a data set may make such a guarantee. 14 | */ 15 | public var identifier: FeatureIdentifier? 16 | 17 | /// Arbitrary, JSON-compatible attributes to associate with the feature. 18 | public var properties: JSONObject? 19 | 20 | /// The geometry at which the feature is located. 21 | public var geometry: Geometry? 22 | 23 | public var foreignMembers: JSONObject = [:] 24 | 25 | /** 26 | Initializes a feature located at the given geometry. 27 | 28 | - parameter geometry: The geometry at which the feature is located. 29 | */ 30 | public init(geometry: Geometry) { 31 | self.geometry = geometry 32 | } 33 | 34 | /** 35 | Initializes a feature defined by the given geometry-convertible instance. 36 | 37 | - parameter geometry: The geometry-convertible instance that bounds the feature. 38 | */ 39 | public init(geometry: GeometryConvertible?) { 40 | self.geometry = geometry?.geometry 41 | } 42 | } 43 | 44 | extension Feature: Codable { 45 | private enum CodingKeys: String, CodingKey { 46 | case kind = "type" 47 | case geometry 48 | case properties 49 | case identifier = "id" 50 | } 51 | 52 | enum Kind: String, Codable { 53 | case Feature 54 | } 55 | 56 | public init(from decoder: Decoder) throws { 57 | let container = try decoder.container(keyedBy: CodingKeys.self) 58 | _ = try container.decode(Kind.self, forKey: .kind) 59 | geometry = try container.decodeIfPresent(Geometry.self, forKey: .geometry) 60 | properties = try container.decodeIfPresent(JSONObject.self, forKey: .properties) 61 | identifier = try container.decodeIfPresent(FeatureIdentifier.self, forKey: .identifier) 62 | try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) 63 | } 64 | 65 | public func encode(to encoder: Encoder) throws { 66 | var container = encoder.container(keyedBy: CodingKeys.self) 67 | try container.encode(Kind.Feature, forKey: .kind) 68 | try container.encode(geometry, forKey: .geometry) 69 | try container.encodeIfPresent(properties, forKey: .properties) 70 | try container.encodeIfPresent(identifier, forKey: .identifier) 71 | try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/Turf/Turf.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | let metersPerRadian: LocationDistance = 6_373_000.0 7 | // WGS84 equatorial radius as specified by the International Union of Geodesy and Geophysics 8 | let equatorialRadius: LocationDistance = 6_378_137 9 | 10 | /// A segment between two positions in a `LineString` geometry or `Ring`. 11 | public typealias LineSegment = (LocationCoordinate2D, LocationCoordinate2D) 12 | 13 | /** 14 | Returns the intersection of two line segments. 15 | 16 | This function is roughly equivalent to the [turf-line-intersect](https://turfjs.org/docs/#lineIntersect) package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-line-intersect/)), except that it only accepts individual line segments instead of whole line strings. 17 | 18 | - seealso: `LineString.intersection(with:)` 19 | */ 20 | public func intersection(_ line1: LineSegment, _ line2: LineSegment) -> LocationCoordinate2D? { 21 | // Ported from https://github.com/Turfjs/turf/blob/142e137ce0c758e2825a260ab32b24db0aa19439/packages/turf-point-on-line/index.js, in turn adapted from http://jsfiddle.net/justin_c_rounds/Gd2S2/light/ 22 | let denominator = ((line2.1.latitude - line2.0.latitude) * (line1.1.longitude - line1.0.longitude)) 23 | - ((line2.1.longitude - line2.0.longitude) * (line1.1.latitude - line1.0.latitude)) 24 | guard denominator != 0 else { 25 | return nil 26 | } 27 | 28 | let dStartY = line1.0.latitude - line2.0.latitude 29 | let dStartX = line1.0.longitude - line2.0.longitude 30 | let numerator1 = (line2.1.longitude - line2.0.longitude) * dStartY - (line2.1.latitude - line2.0.latitude) * dStartX 31 | let numerator2 = (line1.1.longitude - line1.0.longitude) * dStartY - (line1.1.latitude - line1.0.latitude) * dStartX 32 | let a = numerator1 / denominator 33 | let b = numerator2 / denominator 34 | 35 | /// Intersection when the lines are cast infinitely in both directions. 36 | let intersection = LocationCoordinate2D(latitude: line1.0.latitude + a * (line1.1.latitude - line1.0.latitude), 37 | longitude: line1.0.longitude + a * (line1.1.longitude - line1.0.longitude)) 38 | 39 | /// True if line 1 is finite and line 2 is infinite. 40 | let intersectsWithLine1 = a >= 0 && a <= 1 41 | /// True if line 2 is finite and line 1 is infinite. 42 | let intersectsWithLine2 = b >= 0 && b <= 1 43 | return intersectsWithLine1 && intersectsWithLine2 ? intersection : nil 44 | } 45 | 46 | /** 47 | Returns the point midway between two coordinates measured in degrees. 48 | 49 | This function is equivalent to the [turf-midpoint](https://turfjs.org/docs/#midpoint) package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-midpoint/)). 50 | */ 51 | public func mid(_ coord1: LocationCoordinate2D, _ coord2: LocationCoordinate2D) -> LocationCoordinate2D { 52 | let dist = coord1.distance(to: coord2) 53 | let heading = coord1.direction(to: coord2) 54 | return coord1.coordinate(at: dist / 2, facing: heading) 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Turf/Geometries/MultiPolygon.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | /** 7 | A [MultiPolygon geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.7) is a collection of `Polygon` geometries that are disconnected but related. 8 | */ 9 | public struct MultiPolygon: Equatable, ForeignMemberContainer { 10 | /// The positions at which the multipolygon is located. Each nested array corresponds to one polygon. 11 | public var coordinates: [[[LocationCoordinate2D]]] 12 | 13 | public var foreignMembers: JSONObject = [:] 14 | 15 | /// The polygon geometries that conceptually form the multipolygon. 16 | public var polygons: [Polygon] { 17 | return coordinates.map { (coordinates) -> Polygon in 18 | return Polygon(coordinates) 19 | } 20 | } 21 | 22 | /** 23 | Initializes a multipolygon defined by the given positions. 24 | 25 | - parameter coordinates: The positions at which the multipolygon is located. Each nested array corresponds to one polygon. 26 | */ 27 | public init(_ coordinates: [[[LocationCoordinate2D]]]) { 28 | self.coordinates = coordinates 29 | } 30 | 31 | /** 32 | Initializes a multipolygon coincident to the given polygons. 33 | 34 | - parameter polygons: The polygons that together are coincident to the multipolygon. 35 | */ 36 | public init(_ polygons: [Polygon]) { 37 | self.coordinates = polygons.map { (polygon) -> [[LocationCoordinate2D]] in 38 | return polygon.coordinates 39 | } 40 | } 41 | } 42 | 43 | extension MultiPolygon: Codable { 44 | enum CodingKeys: String, CodingKey { 45 | case kind = "type" 46 | case coordinates 47 | } 48 | 49 | enum Kind: String, Codable { 50 | case MultiPolygon 51 | } 52 | 53 | public init(from decoder: Decoder) throws { 54 | let container = try decoder.container(keyedBy: CodingKeys.self) 55 | _ = try container.decode(Kind.self, forKey: .kind) 56 | let coordinates = try container.decode([[[LocationCoordinate2DCodable]]].self, forKey: .coordinates).decodedCoordinates 57 | self = .init(coordinates) 58 | try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) 59 | } 60 | 61 | public func encode(to encoder: Encoder) throws { 62 | var container = encoder.container(keyedBy: CodingKeys.self) 63 | try container.encode(Kind.MultiPolygon, forKey: .kind) 64 | try container.encode(coordinates.codableCoordinates, forKey: .coordinates) 65 | try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) 66 | } 67 | } 68 | 69 | extension MultiPolygon { 70 | /** 71 | * Determines if the given coordinate falls within any of the polygons. 72 | * The optional parameter `ignoreBoundary` will result in the method returning true if the given coordinate 73 | * lies on the boundary line of the polygon or its interior rings. 74 | * 75 | * Calls contains function for each contained polygon 76 | */ 77 | public func contains(_ coordinate: LocationCoordinate2D, ignoreBoundary: Bool = false) -> Bool { 78 | return polygons.contains { 79 | $0.contains(coordinate, ignoreBoundary: ignoreBoundary) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Tests/TurfTests/BoundingBoxTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | @testable import Turf 7 | 8 | class BoundingBoxTests: XCTestCase { 9 | 10 | func testAllPositive() { 11 | let coordinates = [ 12 | LocationCoordinate2D(latitude: 1, longitude: 2), 13 | LocationCoordinate2D(latitude: 2, longitude: 1) 14 | ] 15 | let bbox = BoundingBox(from: coordinates) 16 | XCTAssertEqual(bbox!.southWest, LocationCoordinate2D(latitude: 1, longitude: 1)) 17 | XCTAssertEqual(bbox!.northEast, LocationCoordinate2D(latitude: 2, longitude: 2)) 18 | } 19 | 20 | func testAllNegative() { 21 | let coordinates = [ 22 | LocationCoordinate2D(latitude: -1, longitude: -2), 23 | LocationCoordinate2D(latitude: -2, longitude: -1) 24 | ] 25 | let bbox = BoundingBox(from: coordinates) 26 | XCTAssertEqual(bbox!.southWest, LocationCoordinate2D(latitude: -2, longitude: -2)) 27 | XCTAssertEqual(bbox!.northEast, LocationCoordinate2D(latitude: -1, longitude: -1)) 28 | } 29 | 30 | func testPositiveLatNegativeLon() { 31 | let coordinates = [ 32 | LocationCoordinate2D(latitude: 1, longitude: -2), 33 | LocationCoordinate2D(latitude: 2, longitude: -1) 34 | ] 35 | let bbox = BoundingBox(from: coordinates) 36 | XCTAssertEqual(bbox!.southWest, LocationCoordinate2D(latitude: 1, longitude: -2)) 37 | XCTAssertEqual(bbox!.northEast, LocationCoordinate2D(latitude: 2, longitude: -1)) 38 | } 39 | 40 | func testNegativeLatPositiveLon() { 41 | let coordinates = [ 42 | LocationCoordinate2D(latitude: -1, longitude: 2), 43 | LocationCoordinate2D(latitude: -2, longitude: 1) 44 | ] 45 | let bbox = BoundingBox(from: coordinates) 46 | XCTAssertEqual(bbox!.southWest, LocationCoordinate2D(latitude: -2, longitude: 1)) 47 | XCTAssertEqual(bbox!.northEast, LocationCoordinate2D(latitude: -1, longitude: 2)) 48 | } 49 | 50 | func testContains() { 51 | let coordinate = LocationCoordinate2D(latitude: 1, longitude: 1) 52 | let coordinates = [ 53 | LocationCoordinate2D(latitude: 0, longitude: 0), 54 | LocationCoordinate2D(latitude: 2, longitude: 2) 55 | ] 56 | let bbox = BoundingBox(from: coordinates) 57 | 58 | XCTAssertTrue(bbox!.contains(coordinate)) 59 | } 60 | 61 | func testDoesNotContain() { 62 | let coordinate = LocationCoordinate2D(latitude: 2, longitude: 3) 63 | let coordinates = [ 64 | LocationCoordinate2D(latitude: 0, longitude: 0), 65 | LocationCoordinate2D(latitude: 2, longitude: 2) 66 | ] 67 | let bbox = BoundingBox(from: coordinates) 68 | 69 | XCTAssertFalse(bbox!.contains(coordinate)) 70 | } 71 | 72 | func testContainsAtBoundary() { 73 | let coordinate = LocationCoordinate2D(latitude: 0, longitude: 2) 74 | let coordinates = [ 75 | LocationCoordinate2D(latitude: 0, longitude: 0), 76 | LocationCoordinate2D(latitude: 2, longitude: 2) 77 | ] 78 | let bbox = BoundingBox(from: coordinates) 79 | 80 | XCTAssertFalse(bbox!.contains(coordinate, ignoreBoundary: true)) 81 | XCTAssertTrue(bbox!.contains(coordinate, ignoreBoundary: false)) 82 | XCTAssertFalse(bbox!.contains(coordinate)) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/Turf/RadianCoordinate2D.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | /// A latitude or longitude measured in radians, as opposed to `LocationDegrees`, which is measured in degrees of arc. 7 | public typealias LocationRadians = Double 8 | 9 | /// A difference in latitude or longitude measured in radians, as opposed to `CLLocationDegrees`, which is used by some libraries to represent a similar distance measured in degrees of arc. 10 | public typealias RadianDistance = Double 11 | 12 | /** 13 | A coordinate pair measured in radians, as opposed to `LocationCoordinate2D`, which is measured in degrees of arc. 14 | */ 15 | public struct RadianCoordinate2D: Sendable { 16 | /// The latitude measured in radians. 17 | private(set) var latitude: LocationRadians 18 | 19 | /// The longitude measured in radians. 20 | private(set) var longitude: LocationRadians 21 | 22 | /** 23 | Initializes a coordinate pair located at the given latitude and longitude. 24 | 25 | - parameter latitude: The latitude measured in radians. 26 | - parameter longitude: The longitude measured in radians. 27 | */ 28 | public init(latitude: LocationRadians, longitude: LocationRadians) { 29 | self.latitude = latitude 30 | self.longitude = longitude 31 | } 32 | 33 | /** 34 | Initializes a coordinate pair measured in radians that is coincident to the given coordinate pair measured in degrees of arc. 35 | 36 | - parameter degreeCoordinate: A coordinate pair measured in degrees of arc. 37 | */ 38 | public init(_ degreeCoordinate: LocationCoordinate2D) { 39 | latitude = degreeCoordinate.latitude.toRadians() 40 | longitude = degreeCoordinate.longitude.toRadians() 41 | } 42 | 43 | /** 44 | Returns direction given two coordinates. 45 | */ 46 | public func direction(to coordinate: RadianCoordinate2D) -> Measurement { 47 | let a = sin(coordinate.longitude - longitude) * cos(coordinate.latitude) 48 | let b = cos(latitude) * sin(coordinate.latitude) 49 | - sin(latitude) * cos(coordinate.latitude) * cos(coordinate.longitude - longitude) 50 | return Measurement(value: atan2(a, b), unit: UnitAngle.radians) 51 | } 52 | 53 | /** 54 | Returns coordinate at a given distance and direction away from coordinate. 55 | */ 56 | public func coordinate(at distance: RadianDistance, facing direction: Measurement) -> RadianCoordinate2D { 57 | let distance = distance, direction = direction 58 | let radiansDirection = direction.converted(to: .radians).value 59 | let otherLatitude = asin(sin(latitude) * cos(distance) 60 | + cos(latitude) * sin(distance) * cos(radiansDirection)) 61 | let otherLongitude = longitude + atan2(sin(radiansDirection) * sin(distance) * cos(latitude), 62 | cos(distance) - sin(latitude) * sin(otherLatitude)) 63 | return RadianCoordinate2D(latitude: otherLatitude, longitude: otherLongitude) 64 | } 65 | 66 | /** 67 | Returns the Haversine distance between two coordinates measured in radians. 68 | */ 69 | public func distance(to coordinate: RadianCoordinate2D) -> RadianDistance { 70 | let a = pow(sin((coordinate.latitude - self.latitude) / 2), 2) 71 | + pow(sin((coordinate.longitude - self.longitude) / 2), 2) * cos(self.latitude) * cos(coordinate.latitude) 72 | return 2 * atan2(sqrt(a), sqrt(1 - a)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/in/fiji-hiQ.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": { 4 | "tolerance": 0.005, 5 | "highQuality": true 6 | }, 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [179.97528076171875, -16.514770282237], 12 | [179.97665405273438, -16.519707611417825], 13 | [179.97390747070312, -16.523986527973733], 14 | [179.98043060302734, -16.539126548282127], 15 | [179.985237121582, -16.536822708755626], 16 | [179.98695373535156, -16.531227555400562], 17 | [179.99073028564453, -16.531556686558297], 18 | [180.00205993652344, -16.525632239869275], 19 | [180.00686645507812, -16.525961380565427], 20 | [180.01029968261716, -16.523328239288848], 21 | [180.01304626464844, -16.524644814414863], 22 | [180.01235961914062, -16.526619660274722], 23 | [180.00995635986328, -16.52694879928784], 24 | [180.00823974609372, -16.532873205577378], 25 | [180.00755310058594, -16.534847967269407], 26 | [180.01819610595703, -16.539126548282127], 27 | [180.02334594726562, -16.537810071920884], 28 | [180.02437591552734, -16.534189715617043], 29 | [180.03707885742188, -16.534189715617043], 30 | [180.04188537597656, -16.526619660274722], 31 | [180.0487518310547, -16.52826534972986], 32 | [180.0497817993164, -16.525632239869275], 33 | [180.054931640625, -16.52727793773992], 34 | [180.05939483642578, -16.52497395679397], 35 | [180.06145477294922, -16.525632239869275], 36 | [180.06488800048825, -16.515757758166256], 37 | [180.0666046142578, -16.513124477808333], 38 | [180.0655746459961, -16.5108203280625], 39 | [180.06454467773438, -16.507528637922594], 40 | [180.05596160888672, -16.490410945973114], 41 | [180.0542449951172, -16.485802077854263], 42 | [180.0494384765625, -16.484814448981634], 43 | [180.04669189453125, -16.482839176122972], 44 | [180.04634857177734, -16.479547009916406], 45 | [180.05390167236328, -16.47460865568325], 46 | [180.05596160888672, -16.47526711018803], 47 | [180.0597381591797, -16.470987115907786], 48 | [180.0600814819336, -16.467365508451035], 49 | [180.06282806396482, -16.46110984528612], 50 | [180.07347106933594, -16.455183241385555], 51 | [180.07930755615232, -16.45057353538345], 52 | [180.08686065673828, -16.44761009512282], 53 | [180.08411407470703, -16.441353794870704], 54 | [180.06832122802734, -16.437402343465862], 55 | [180.05561828613278, -16.439707366557357], 56 | [180.04806518554688, -16.448597913571174], 57 | [180.0398254394531, -16.45057353538345], 58 | [180.02609252929688, -16.46473156961583], 59 | [180.02094268798828, -16.4624268437789], 60 | [180.01441955566406, -16.464073079315213], 61 | [180.00961303710935, -16.468682464447188], 62 | [180.00446319580078, -16.476584012483762], 63 | [179.99725341796875, -16.48250996202084], 64 | [179.99553680419922, -16.487118908513903], 65 | [179.99176025390625, -16.48975254296043], 66 | [179.99038696289062, -16.49271533887906], 67 | [179.9883270263672, -16.49403212250543], 68 | [179.98661041259763, -16.50226181712924], 69 | [179.98111724853516, -16.50489524545779], 70 | [179.97940063476562, -16.50719946582597], 71 | [179.97528076171875, -16.514770282237] 72 | ] 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/featurecollection.geojson: -------------------------------------------------------------------------------- 1 | {"type":"FeatureCollection","properties":{"tolerance":0.01},"features":[{"type":"Feature","properties":{"id":1,"tolerance":0.01},"geometry":{"type":"LineString","coordinates":[[27.977542877197266,-26.17500493262446],[27.975482940673828,-26.17870225771557],[27.969818115234375,-26.177931991326645],[27.967071533203125,-26.177623883345735],[27.966899871826172,-26.1810130263384],[27.967758178710938,-26.1853263385099],[27.97290802001953,-26.1853263385099],[27.97496795654297,-26.18270756087535],[27.97840118408203,-26.1810130263384],[27.98011779785156,-26.183323749143113],[27.98011779785156,-26.18655868408986],[27.978744506835938,-26.18933141398614],[27.97496795654297,-26.19025564262006],[27.97119140625,-26.19040968001282],[27.969303131103516,-26.1899475672235],[27.96741485595703,-26.189639491012183],[27.9656982421875,-26.187945057286793],[27.965354919433594,-26.18563442612686],[27.96432495117187,-26.183015655416536]]}},{"type":"Feature","properties":{"id":2,"tolerance":0.01},"geometry":{"type":"Polygon","coordinates":[[[27.972049713134762,-26.199035448897074],[27.9741096496582,-26.196108920345292],[27.977371215820312,-26.197495179879635],[27.978572845458984,-26.20042167359348],[27.980976104736328,-26.200729721284862],[27.982349395751953,-26.197803235312957],[27.982177734375,-26.194414580727656],[27.982177734375,-26.19256618212382],[27.98406600952148,-26.192258112838022],[27.985267639160156,-26.191950042737417],[27.986125946044922,-26.19426054863105],[27.986984252929688,-26.196416979445644],[27.987327575683594,-26.198881422912123],[27.98715591430664,-26.201345814222698],[27.985095977783203,-26.20381015337393],[27.983036041259766,-26.20550435628209],[27.979946136474606,-26.20550435628209],[27.97719955444336,-26.20488828535003],[27.97445297241211,-26.203656133705152],[27.972564697265625,-26.201961903900578],[27.972049713134762,-26.199035448897074]]]}},{"type":"Feature","properties":{"id":3,"tolerance":0.01},"geometry":{"type":"Polygon","coordinates":[[[27.946643829345703,-26.170845301716803],[27.94269561767578,-26.183631842055114],[27.935657501220703,-26.183323749143113],[27.92741775512695,-26.17685360983018],[27.926902770996094,-26.171153427614488],[27.928619384765625,-26.165298896316028],[27.936859130859375,-26.161292995018652],[27.94509887695312,-26.158981835530525],[27.950420379638672,-26.161601146157146],[27.951793670654297,-26.166223315536712],[27.954025268554688,-26.173464345889972],[27.954025268554688,-26.179626570662702],[27.951278686523438,-26.187945057286793],[27.944583892822266,-26.19395248382672],[27.936172485351562,-26.194876675795218],[27.930850982666016,-26.19379845111899],[27.925701141357422,-26.190563717201886],[27.92278289794922,-26.18655868408986],[27.92072296142578,-26.180858976522302],[27.917118072509766,-26.174080583026957],[27.916603088378906,-26.16683959094609],[27.917118072509766,-26.162987816205614],[27.920207977294922,-26.162987816205614],[27.920894622802734,-26.166069246175482],[27.9217529296875,-26.17146155269785],[27.923297882080078,-26.177469829049862],[27.92673110961914,-26.184248025435295],[27.930335998535156,-26.18856121785662],[27.936687469482422,-26.18871525748988],[27.942352294921875,-26.187945057286793],[27.94647216796875,-26.184248025435295],[27.946815490722653,-26.178548204845022],[27.946643829345703,-26.170845301716803]],[[27.936859130859375,-26.16591517661071],[27.934799194335938,-26.16945872510008],[27.93497085571289,-26.173926524048102],[27.93874740600586,-26.175621161617432],[27.94149398803711,-26.17007498340995],[27.94321060180664,-26.166223315536712],[27.939090728759762,-26.164528541367826],[27.937545776367188,-26.16406632595636],[27.936859130859375,-26.16591517661071]]]}},{"type":"Feature","properties":{"id":4,"tolerance":0.01},"geometry":{"type":"Point","coordinates":[27.95642852783203,-26.152510345365126]}}]} 2 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixture.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | 4 | class Fixture { 5 | class func moduleBundle() -> Bundle { 6 | #if SPM_TESTING 7 | return Bundle.module 8 | #else 9 | return Bundle(for: self) 10 | #endif 11 | } 12 | class func stringFromFileNamed(name: String) -> String { 13 | guard let path = Bundle(for: self).path(forResource: name, ofType: "json") ?? Bundle(for: self).path(forResource: name, ofType: "geojson") else { 14 | XCTAssert(false, "Fixture \(name) not found.") 15 | return "" 16 | } 17 | do { 18 | return try String(contentsOfFile: path, encoding: .utf8) 19 | } catch { 20 | XCTAssert(false, "Unable to decode fixture at \(path): \(error).") 21 | return "" 22 | } 23 | } 24 | 25 | class func geojsonData(from name: String) throws -> Data? { 26 | guard let path = moduleBundle().path(forResource: name, ofType: "geojson") else { 27 | XCTAssert(false, "Fixture \(name) not found.") 28 | return nil 29 | } 30 | let filePath = URL(fileURLWithPath: path) 31 | return try Data(contentsOf: filePath) 32 | } 33 | 34 | class func JSONFromFileNamed(name: String) -> [String: Any] { 35 | guard let path = moduleBundle().path(forResource: name, ofType: "json") ?? Bundle(for: self).path(forResource: name, ofType: "geojson") else { 36 | XCTAssert(false, "Fixture \(name) not found.") 37 | return [:] 38 | } 39 | guard let data = NSData(contentsOfFile: path) else { 40 | XCTAssert(false, "No data found at \(path).") 41 | return [:] 42 | } 43 | do { 44 | return try JSONSerialization.jsonObject(with: data as Data, options: []) as! [String: AnyObject] 45 | } catch { 46 | XCTAssert(false, "Unable to decode JSON fixture at \(path): \(error).") 47 | return [:] 48 | } 49 | } 50 | 51 | class func JSONFromGEOJSONFileNamed(name: String) -> [String: Any] { 52 | guard let path = moduleBundle().path(forResource: name, ofType: "geojson") ?? Bundle(for: self).path(forResource: name, ofType: "geojson") else { 53 | XCTAssert(false, "Fixture \(name) not found.") 54 | return [:] 55 | } 56 | guard let data = NSData(contentsOfFile: path) else { 57 | XCTAssert(false, "No data found at \(path).") 58 | return [:] 59 | } 60 | do { 61 | return try JSONSerialization.jsonObject(with: data as Data, options: []) as! [String: AnyObject] 62 | } catch { 63 | XCTAssert(false, "Unable to decode JSON fixture at \(path): \(error).") 64 | return [:] 65 | } 66 | } 67 | 68 | static func fixtures(folder: String, pair: (String, Data, Data) throws -> Void) throws { 69 | let thisSourceFile = URL(fileURLWithPath: #file) 70 | let thisDirectory = thisSourceFile.deletingLastPathComponent() 71 | 72 | let path = thisDirectory 73 | .appendingPathComponent("Fixtures", isDirectory: true) 74 | .appendingPathComponent(folder, isDirectory: true) 75 | let inDir = path.appendingPathComponent("in", isDirectory: true) 76 | let outDir = path.appendingPathComponent("out", isDirectory: true) 77 | 78 | let inputs = try FileManager.default.contentsOfDirectory(at: inDir, includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants) 79 | 80 | for inPath in inputs { 81 | let outPath = outDir.appendingPathComponent(inPath.lastPathComponent) 82 | try pair( 83 | inPath.lastPathComponent, 84 | try Data(contentsOf: inPath), 85 | try Data(contentsOf: outPath) 86 | ) 87 | } 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /Sources/Turf/FeatureIdentifier.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A [feature identifier](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2) identifies a `Feature` object. 5 | */ 6 | public enum FeatureIdentifier: Hashable, Sendable { 7 | /// A string. 8 | case string(_ string: String) 9 | 10 | /** 11 | A floating-point number. 12 | 13 | - parameter number: A floating-point number. JSON does not distinguish numeric types of different precisions. If you need integer precision, cast this associated value to an `Int`. 14 | */ 15 | case number(_ number: Double) 16 | 17 | /// Initializes a feature identifier representing the given string. 18 | public init(_ string: String) { 19 | self = .string(string) 20 | } 21 | 22 | /** 23 | Initializes a feature identifier representing the given integer. 24 | 25 | - parameter number: An integer. JSON does not distinguish numeric types of different precisions, so the integer is stored as a floating-point number. 26 | */ 27 | public init(_ number: Source) where Source: BinaryInteger { 28 | self = .number(Double(number)) 29 | } 30 | 31 | /// Initializes a feature identifier representing the given floating-point number. 32 | public init(_ number: Source) where Source: BinaryFloatingPoint { 33 | self = .number(Double(number)) 34 | } 35 | } 36 | 37 | extension FeatureIdentifier: RawRepresentable { 38 | public typealias RawValue = Any 39 | 40 | public init?(rawValue: Any) { 41 | // Like `JSONSerialization.jsonObject(with:options:)` with `JSONSerialization.ReadingOptions.fragmentsAllowed` specified. 42 | if let string = rawValue as? String { 43 | self = .string(string) 44 | } else if let number = rawValue as? NSNumber { 45 | self = .number(number.doubleValue) 46 | } else { 47 | return nil 48 | } 49 | } 50 | 51 | public var rawValue: Any { 52 | switch self { 53 | case let .string(value): 54 | return value 55 | case let .number(value): 56 | return value 57 | } 58 | } 59 | } 60 | 61 | extension FeatureIdentifier { 62 | /// A string. 63 | public var string: String? { 64 | if case let .string(value) = self { 65 | return value 66 | } 67 | return nil 68 | } 69 | 70 | /// A floating-point number. 71 | public var number: Double? { 72 | if case let .number(value) = self { 73 | return value 74 | } 75 | return nil 76 | } 77 | } 78 | 79 | extension FeatureIdentifier: ExpressibleByStringLiteral { 80 | public init(stringLiteral value: StringLiteralType) { 81 | self = .init(value) 82 | } 83 | } 84 | 85 | extension FeatureIdentifier: ExpressibleByIntegerLiteral { 86 | public init(integerLiteral value: IntegerLiteralType) { 87 | self = .init(value) 88 | } 89 | } 90 | 91 | extension FeatureIdentifier: ExpressibleByFloatLiteral { 92 | public init(floatLiteral value: FloatLiteralType) { 93 | self = .number(value) 94 | } 95 | } 96 | 97 | extension FeatureIdentifier: Codable { 98 | enum CodingKeys: String, CodingKey { 99 | case string, number 100 | } 101 | 102 | public init(from decoder: Decoder) throws { 103 | let container = try decoder.singleValueContainer() 104 | if let value = try? container.decode(String.self) { 105 | self = .string(value) 106 | } else { 107 | self = .number(try container.decode(Double.self)) 108 | } 109 | } 110 | 111 | public func encode(to encoder: Encoder) throws { 112 | var container = encoder.singleValueContainer() 113 | switch self { 114 | case .string(let value): 115 | try container.encode(value) 116 | case .number(let value): 117 | try container.encode(value) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/out/argentina.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "id": "ARG", 4 | "properties": { 5 | "tolerance": 0.1, 6 | "name": "Argentina" 7 | }, 8 | "geometry": { 9 | "type": "Polygon", 10 | "coordinates": [ 11 | [ 12 | [-64.964892, -22.075862], 13 | [-64.377021, -22.798091], 14 | [-63.986838, -21.993644], 15 | [-62.846468, -22.034985], 16 | [-60.846565, -23.880713], 17 | [-60.028966, -24.032796], 18 | [-58.807128, -24.771459], 19 | [-57.777217, -25.16234], 20 | [-57.63366, -25.603657], 21 | [-58.618174, -27.123719], 22 | [-56.486702, -27.548499], 23 | [-55.695846, -27.387837], 24 | [-54.788795, -26.621786], 25 | [-54.625291, -25.739255], 26 | [-54.13005, -25.547639], 27 | [-53.628349, -26.124865], 28 | [-53.648735, -26.923473], 29 | [-55.162286, -27.881915], 30 | [-57.625133, -30.216295], 31 | [-58.14244, -32.044504], 32 | [-58.132648, -33.040567], 33 | [-58.349611, -33.263189], 34 | [-58.495442, -34.43149], 35 | [-57.22583, -35.288027], 36 | [-57.362359, -35.97739], 37 | [-56.737487, -36.413126], 38 | [-56.788285, -36.901572], 39 | [-57.749157, -38.183871], 40 | [-59.231857, -38.72022], 41 | [-61.237445, -38.928425], 42 | [-62.335957, -38.827707], 43 | [-62.125763, -39.424105], 44 | [-62.330531, -40.172586], 45 | [-62.145994, -40.676897], 46 | [-62.745803, -41.028761], 47 | [-63.770495, -41.166789], 48 | [-64.73209, -40.802677], 49 | [-65.118035, -41.064315], 50 | [-64.978561, -42.058001], 51 | [-64.303408, -42.359016], 52 | [-63.755948, -42.043687], 53 | [-63.458059, -42.563138], 54 | [-64.378804, -42.873558], 55 | [-65.181804, -43.495381], 56 | [-65.328823, -44.501366], 57 | [-65.565269, -45.036786], 58 | [-66.509966, -45.039628], 59 | [-67.293794, -45.551896], 60 | [-67.580546, -46.301773], 61 | [-66.597066, -47.033925], 62 | [-65.641027, -47.236135], 63 | [-65.985088, -48.133289], 64 | [-67.166179, -48.697337], 65 | [-67.816088, -49.869669], 66 | [-68.728745, -50.264218], 67 | [-69.138539, -50.73251], 68 | [-68.815561, -51.771104], 69 | [-68.149995, -52.349983], 70 | [-71.914804, -52.009022], 71 | [-72.329404, -51.425956], 72 | [-72.309974, -50.67701], 73 | [-72.975747, -50.74145], 74 | [-73.328051, -50.378785], 75 | [-73.415436, -49.318436], 76 | [-72.648247, -48.878618], 77 | [-72.331161, -48.244238], 78 | [-72.447355, -47.738533], 79 | [-71.917258, -46.884838], 80 | [-71.552009, -45.560733], 81 | [-71.659316, -44.973689], 82 | [-71.222779, -44.784243], 83 | [-71.329801, -44.407522], 84 | [-71.793623, -44.207172], 85 | [-71.464056, -43.787611], 86 | [-71.915424, -43.408565], 87 | [-72.148898, -42.254888], 88 | [-71.746804, -42.051386], 89 | [-71.915734, -40.832339], 90 | [-71.413517, -38.916022], 91 | [-70.814664, -38.552995], 92 | [-71.118625, -37.576827], 93 | [-71.121881, -36.658124], 94 | [-70.364769, -36.005089], 95 | [-70.388049, -35.169688], 96 | [-69.817309, -34.193571], 97 | [-69.814777, -33.273886], 98 | [-70.074399, -33.09121], 99 | [-70.535069, -31.36501], 100 | [-69.919008, -30.336339], 101 | [-70.01355, -29.367923], 102 | [-69.65613, -28.459141], 103 | [-69.001235, -27.521214], 104 | [-68.295542, -26.89934], 105 | [-68.5948, -26.506909], 106 | [-68.386001, -26.185016], 107 | [-68.417653, -24.518555], 108 | [-67.328443, -24.025303], 109 | [-66.985234, -22.986349], 110 | [-67.106674, -22.735925], 111 | [-66.273339, -21.83231], 112 | [-64.964892, -22.075862] 113 | ] 114 | ] 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/in/linestring.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "LineString", 6 | "coordinates": [ 7 | [-80.51399230957031, 28.069556808283608], 8 | [-80.51193237304688, 28.057438520876673], 9 | [-80.49819946289062, 28.05622661698537], 10 | [-80.5023193359375, 28.04471284867091], 11 | [-80.48583984375, 28.042288740362853], 12 | [-80.50575256347656, 28.028349057505775], 13 | [-80.50163269042969, 28.02168161433489], 14 | [-80.49476623535156, 28.021075462659883], 15 | [-80.48652648925781, 28.021075462659883], 16 | [-80.47691345214844, 28.021075462659883], 17 | [-80.46936035156249, 28.015619944017807], 18 | [-80.47760009765624, 28.007133032319448], 19 | [-80.49201965332031, 27.998039170620494], 20 | [-80.46730041503906, 27.962262536875905], 21 | [-80.46524047851562, 27.91980029694533], 22 | [-80.40550231933594, 27.930114089618602], 23 | [-80.39657592773438, 27.980455528671527], 24 | [-80.41305541992188, 27.982274659104082], 25 | [-80.42953491210938, 27.990763528690582], 26 | [-80.4144287109375, 28.00955793247135], 27 | [-80.3594970703125, 27.972572275562527], 28 | [-80.36224365234375, 27.948919060105453], 29 | [-80.38215637207031, 27.913732900444284], 30 | [-80.41786193847656, 27.881570017022806], 31 | [-80.40550231933594, 27.860932192608534], 32 | [-80.39382934570312, 27.85425440786446], 33 | [-80.37803649902344, 27.86336037597851], 34 | [-80.38215637207031, 27.880963078302393], 35 | [-80.36842346191405, 27.888246118437756], 36 | [-80.35743713378906, 27.882176952341734], 37 | [-80.35469055175781, 27.86882358965466], 38 | [-80.3594970703125, 27.8421119273228], 39 | [-80.37940979003906, 27.83300417483936], 40 | [-80.39932250976561, 27.82511017099003], 41 | [-80.40069580078125, 27.79352841586229], 42 | [-80.36155700683594, 27.786846483587688], 43 | [-80.35537719726562, 27.794743268514615], 44 | [-80.36705017089844, 27.800209937418252], 45 | [-80.36889553070068, 27.801918215058347], 46 | [-80.3690242767334, 27.803930152059845], 47 | [-80.36713600158691, 27.805942051806845], 48 | [-80.36584854125977, 27.805524490772143], 49 | [-80.36563396453857, 27.80465140342285], 50 | [-80.36619186401367, 27.803095012921272], 51 | [-80.36623477935791, 27.801842292177923], 52 | [-80.36524772644043, 27.80127286888392], 53 | [-80.36224365234375, 27.801158983867033], 54 | [-80.36065578460693, 27.802639479776524], 55 | [-80.36138534545898, 27.803740348273823], 56 | [-80.36220073699951, 27.804803245204976], 57 | [-80.36190032958984, 27.806625330038287], 58 | [-80.3609561920166, 27.80742248254359], 59 | [-80.35932540893555, 27.806853088493792], 60 | [-80.35889625549315, 27.806321651354835], 61 | [-80.35902500152588, 27.805448570411585], 62 | [-80.35863876342773, 27.804461600896783], 63 | [-80.35739421844482, 27.804461600896783], 64 | [-80.35700798034668, 27.805334689771293], 65 | [-80.35696506500244, 27.80673920932572], 66 | [-80.35726547241211, 27.80772615814989], 67 | [-80.35808086395264, 27.808295547623707], 68 | [-80.3585958480835, 27.80928248230861], 69 | [-80.35653591156006, 27.80943431761813], 70 | [-80.35572052001953, 27.808637179875486], 71 | [-80.3555917739868, 27.80772615814989], 72 | [-80.3555917739868, 27.806055931810487], 73 | [-80.35572052001953, 27.803778309057556], 74 | [-80.35537719726562, 27.801804330717825], 75 | [-80.3554630279541, 27.799564581098746], 76 | [-80.35670757293701, 27.799564581098746], 77 | [-80.35499095916748, 27.796831264786892], 78 | [-80.34610748291016, 27.79478123244122], 79 | [-80.34404754638672, 27.802070060660014], 80 | [-80.34748077392578, 27.804955086774896], 81 | [-80.3433609008789, 27.805790211616266], 82 | [-80.34353256225586, 27.8101555324401], 83 | [-80.33499240875244, 27.810079615315917], 84 | [-80.33383369445801, 27.805676331334084], 85 | [-80.33022880554199, 27.801652484744796], 86 | [-80.32872676849365, 27.80848534345178] 87 | ] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /scripts/pre-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eou pipefail 4 | 5 | if [ $# -eq 0 ]; then 6 | echo "Usage: v" 7 | exit 1 8 | fi 9 | 10 | SEM_VERSION=$1 11 | CHECKSUM="" 12 | 13 | BRANCH_NAME="update-versions-$SEM_VERSION" 14 | 15 | function checkout { 16 | git config --global user.name "MapboxCI" 17 | git config --global user.email "no-reply@mapbox.com" 18 | git checkout -B "$BRANCH_NAME" 19 | } 20 | 21 | function setup_token { 22 | GH_TOKEN=$(mbx-ci github writer public token) 23 | export GH_TOKEN 24 | } 25 | 26 | function update_versions { 27 | SEM_VERSION=${SEM_VERSION/#v} 28 | SHORT_VERSION=${SEM_VERSION%-*} 29 | MINOR_VERSION=${SEM_VERSION%.*} 30 | YEAR=$(date '+%Y') 31 | 32 | echo "Version ${SEM_VERSION}" 33 | echo "Updating Xcode targets to version ${SHORT_VERSION}…" 34 | 35 | xcrun agvtool bump -all 36 | xcrun agvtool new-marketing-version "${SHORT_VERSION}" 37 | 38 | echo "Updating CocoaPods podspecs to version ${SEM_VERSION}…" 39 | 40 | find . -type f -name '*.podspec' -exec sed -i '' "s/^ *s.version *=.*$/ s.version = \"${SEM_VERSION}\"/" {} + 41 | 42 | if [[ $SHORT_VERSION == "$SEM_VERSION" && $SHORT_VERSION == "*.0" ]]; then 43 | echo "Updating readmes to version ${SEM_VERSION}…" 44 | sed -i '' -E "s/~> *[^']+/~> ${MINOR_VERSION}/g; s/.git\", from: \"*[^\"]+/.git\", from: \"${SEM_VERSION}/g" README.md 45 | elif [[ $SHORT_VERSION != "$SEM_VERSION" ]]; then 46 | echo "Updating readmes to version ${SEM_VERSION}…" 47 | sed -i '' -E "s/:tag => 'v[^']+'/:tag => 'v${SEM_VERSION}'/g; s/\"mapbox\/turf-swift\" \"v[^\"]+\"/\"mapbox\/turf-swift\" \"v${SEM_VERSION}\"/g; s/\.exact\(\"*[^\"]+/.exact(\"${SEM_VERSION}/g" README.md 48 | fi 49 | 50 | # Skip updating the documentation badge for prereleases. 51 | if [[ $SHORT_VERSION == "$SEM_VERSION" ]]; then 52 | echo "Updating readmes to version ${SEM_VERSION}…" 53 | sed -i '' -E "s/turf-swift\/[^/]+\/badge\.svg/turf-swift\/${SEM_VERSION}\/badge.svg/g" README.md 54 | fi 55 | 56 | echo "Updating copyright year to ${YEAR}…" 57 | 58 | sed -i '' -E "s/© ([0-9]{4})[–-][0-9]{4}/© \\1–${YEAR}/g" LICENSE.md docs/jazzy.yml 59 | } 60 | 61 | function update_swift_package { 62 | sh scripts/xcframework.sh build 63 | CHECKSUM=$( 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | 51 | 53 | 59 | 60 | 61 | 62 | 63 | 73 | 74 | 80 | 81 | 82 | 83 | 89 | 90 | 96 | 97 | 98 | 99 | 101 | 102 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /Sources/Turf/Simplifier.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Simplifier { 4 | 5 | static func simplify(_ coordinates: [LocationCoordinate2D], tolerance: Double, highestQuality: Bool) -> [LocationCoordinate2D] { 6 | guard coordinates.count > 2 else { return coordinates } 7 | 8 | let squareTolerance = tolerance * tolerance 9 | 10 | let input = highestQuality ? coordinates : Simplifier.simplifyRadial(coordinates, squareTolerance: squareTolerance) 11 | 12 | return Simplifier.simplifyDouglasPeucker(input, tolerance: squareTolerance) 13 | } 14 | 15 | // MARK: - Douglas-Peucker 16 | 17 | private static func squareSegmentDistance(_ coordinate: LocationCoordinate2D, segmentStart: LocationCoordinate2D, segmentEnd: LocationCoordinate2D) -> LocationDistance { 18 | 19 | var x = segmentStart.latitude 20 | var y = segmentStart.longitude 21 | var dx = segmentEnd.latitude - x 22 | var dy = segmentEnd.longitude - y 23 | 24 | if dx != 0 || dy != 0 { 25 | let t = ((coordinate.latitude - x) * dx + (coordinate.longitude - y) * dy) / (dx * dx + dy * dy) 26 | if t > 1 { 27 | x = segmentEnd.latitude 28 | y = segmentEnd.longitude 29 | } else if t > 0 { 30 | x += dx * t 31 | y += dy * t 32 | } 33 | } 34 | 35 | dx = coordinate.latitude - x 36 | dy = coordinate.longitude - y 37 | 38 | return dx * dx + dy * dy 39 | } 40 | 41 | private static func simplifyDouglasPeuckerStep(_ coordinates: [LocationCoordinate2D], first: Int, last: Int, tolerance: Double, simplified: inout [LocationCoordinate2D]) { 42 | 43 | var maxSquareDistance = tolerance 44 | var index = 0 45 | 46 | for i in first + 1 ..< last { 47 | let squareDistance = squareSegmentDistance(coordinates[i], segmentStart: coordinates[first], segmentEnd: coordinates[last]) 48 | 49 | if squareDistance > maxSquareDistance { 50 | index = i 51 | maxSquareDistance = squareDistance 52 | } 53 | } 54 | 55 | if maxSquareDistance > tolerance { 56 | if index - first > 1 { 57 | simplifyDouglasPeuckerStep(coordinates, first: first, last: index, tolerance: tolerance, simplified: &simplified) 58 | } 59 | simplified.append(coordinates[index]) 60 | if last - index > 1 { 61 | simplifyDouglasPeuckerStep(coordinates, first: index, last: last, tolerance: tolerance, simplified: &simplified) 62 | } 63 | } 64 | } 65 | 66 | private static func simplifyDouglasPeucker(_ coordinates: [LocationCoordinate2D], tolerance: Double) -> [LocationCoordinate2D] { 67 | if coordinates.count <= 2 { 68 | return coordinates 69 | } 70 | 71 | let lastPoint = coordinates.count - 1 72 | var result = [coordinates[0]] 73 | simplifyDouglasPeuckerStep(coordinates, first: 0, last: lastPoint, tolerance: tolerance, simplified: &result) 74 | result.append(coordinates[lastPoint]) 75 | return result 76 | } 77 | 78 | //MARK: - Radial simplification 79 | 80 | private static func squareDistance(from origin: LocationCoordinate2D, to destination: LocationCoordinate2D) -> Double { 81 | let dx = origin.longitude - destination.longitude 82 | let dy = origin.latitude - destination.latitude 83 | return dx * dx + dy * dy 84 | } 85 | 86 | private static func simplifyRadial(_ coordinates: [LocationCoordinate2D], squareTolerance: Double) -> [LocationCoordinate2D] { 87 | guard coordinates.count > 2 else { return coordinates } 88 | 89 | var prevCoordinate = coordinates[0] 90 | var newCoordinates = [prevCoordinate] 91 | var coordinate = coordinates[1] 92 | 93 | for index in 1 ..< coordinates.count { 94 | coordinate = coordinates[index] 95 | 96 | if squareDistance(from: coordinate, to: prevCoordinate) > squareTolerance { 97 | newCoordinates.append(coordinate) 98 | prevCoordinate = coordinate 99 | } 100 | } 101 | 102 | if prevCoordinate != coordinate { 103 | newCoordinates.append(coordinate) 104 | } 105 | 106 | return newCoordinates 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /Sources/Turf/BoundingBox.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | /** 7 | A [bounding box](https://datatracker.ietf.org/doc/html/rfc7946#section-5) indicates the extremes of a `GeoJSONObject` along the x- and y-axes (longitude and latitude, respectively). 8 | */ 9 | public struct BoundingBox: Sendable { 10 | /// The southwesternmost position contained in the bounding box. 11 | public var southWest: LocationCoordinate2D 12 | 13 | /// The northeasternmost position contained in the bounding box. 14 | public var northEast: LocationCoordinate2D 15 | 16 | /** 17 | Initializes the smallest bounding box that contains all the given coordinates. 18 | 19 | - parameter coordinates: The coordinates to fit in the bounding box. 20 | */ 21 | public init?(from coordinates: [LocationCoordinate2D]?) { 22 | guard coordinates?.count ?? 0 > 0 else { 23 | return nil 24 | } 25 | let startValue = (minLat: coordinates!.first!.latitude, maxLat: coordinates!.first!.latitude, minLon: coordinates!.first!.longitude, maxLon: coordinates!.first!.longitude) 26 | let (minLat, maxLat, minLon, maxLon) = coordinates! 27 | .reduce(startValue) { (result, coordinate) -> (minLat: Double, maxLat: Double, minLon: Double, maxLon: Double) in 28 | let minLat = min(coordinate.latitude, result.0) 29 | let maxLat = max(coordinate.latitude, result.1) 30 | let minLon = min(coordinate.longitude, result.2) 31 | let maxLon = max(coordinate.longitude, result.3) 32 | return (minLat: minLat, maxLat: maxLat, minLon: minLon, maxLon: maxLon) 33 | } 34 | southWest = LocationCoordinate2D(latitude: minLat, longitude: minLon) 35 | northEast = LocationCoordinate2D(latitude: maxLat, longitude: maxLon) 36 | } 37 | 38 | /** 39 | Initializes a bounding box defined by its southwesternmost and northeasternmost positions. 40 | 41 | - parameter southWest: The southwesternmost position contained in the bounding box. 42 | - parameter northEast: The northeasternmost position contained in the bounding box. 43 | */ 44 | public init(southWest: LocationCoordinate2D, northEast: LocationCoordinate2D) { 45 | self.southWest = southWest 46 | self.northEast = northEast 47 | } 48 | 49 | /** 50 | Returns a Boolean value indicating whether the bounding box contains the given position. 51 | 52 | - parameter coordinate: The coordinate that may or may not be contained by the bounding box. 53 | - parameter ignoreBoundary: A Boolean value indicating whether a position lying exactly on the edge of the bounding box should be considered to be contained in the bounding box. 54 | - returns: `true` if the bounding box contains the position; `false` otherwise. 55 | */ 56 | public func contains(_ coordinate: LocationCoordinate2D, ignoreBoundary: Bool = true) -> Bool { 57 | if ignoreBoundary { 58 | return southWest.latitude < coordinate.latitude 59 | && northEast.latitude > coordinate.latitude 60 | && southWest.longitude < coordinate.longitude 61 | && northEast.longitude > coordinate.longitude 62 | } else { 63 | return southWest.latitude <= coordinate.latitude 64 | && northEast.latitude >= coordinate.latitude 65 | && southWest.longitude <= coordinate.longitude 66 | && northEast.longitude >= coordinate.longitude 67 | } 68 | } 69 | } 70 | 71 | extension BoundingBox: Hashable { 72 | public func hash(into hasher: inout Hasher) { 73 | hasher.combine(southWest.longitude) 74 | hasher.combine(southWest.latitude) 75 | hasher.combine(northEast.longitude) 76 | hasher.combine(northEast.latitude) 77 | } 78 | } 79 | 80 | extension BoundingBox: Codable { 81 | public func encode(to encoder: Encoder) throws { 82 | var container = encoder.unkeyedContainer() 83 | try container.encode(southWest.codableCoordinates) 84 | try container.encode(northEast.codableCoordinates) 85 | } 86 | 87 | public init(from decoder: Decoder) throws { 88 | var container = try decoder.unkeyedContainer() 89 | southWest = try container.decode(LocationCoordinate2DCodable.self).decodedCoordinates 90 | northEast = try container.decode(LocationCoordinate2DCodable.self).decodedCoordinates 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/in/argentina.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "id": "ARG", 4 | "properties": { 5 | "tolerance": 0.1, 6 | "name": "Argentina" 7 | }, 8 | "geometry": { 9 | "type": "Polygon", 10 | "coordinates": [ 11 | [ 12 | [-64.964892, -22.075862], 13 | [-64.377021, -22.798091], 14 | [-63.986838, -21.993644], 15 | [-62.846468, -22.034985], 16 | [-62.685057, -22.249029], 17 | [-60.846565, -23.880713], 18 | [-60.028966, -24.032796], 19 | [-58.807128, -24.771459], 20 | [-57.777217, -25.16234], 21 | [-57.63366, -25.603657], 22 | [-58.618174, -27.123719], 23 | [-57.60976, -27.395899], 24 | [-56.486702, -27.548499], 25 | [-55.695846, -27.387837], 26 | [-54.788795, -26.621786], 27 | [-54.625291, -25.739255], 28 | [-54.13005, -25.547639], 29 | [-53.628349, -26.124865], 30 | [-53.648735, -26.923473], 31 | [-54.490725, -27.474757], 32 | [-55.162286, -27.881915], 33 | [-56.2909, -28.852761], 34 | [-57.625133, -30.216295], 35 | [-57.874937, -31.016556], 36 | [-58.14244, -32.044504], 37 | [-58.132648, -33.040567], 38 | [-58.349611, -33.263189], 39 | [-58.427074, -33.909454], 40 | [-58.495442, -34.43149], 41 | [-57.22583, -35.288027], 42 | [-57.362359, -35.97739], 43 | [-56.737487, -36.413126], 44 | [-56.788285, -36.901572], 45 | [-57.749157, -38.183871], 46 | [-59.231857, -38.72022], 47 | [-61.237445, -38.928425], 48 | [-62.335957, -38.827707], 49 | [-62.125763, -39.424105], 50 | [-62.330531, -40.172586], 51 | [-62.145994, -40.676897], 52 | [-62.745803, -41.028761], 53 | [-63.770495, -41.166789], 54 | [-64.73209, -40.802677], 55 | [-65.118035, -41.064315], 56 | [-64.978561, -42.058001], 57 | [-64.303408, -42.359016], 58 | [-63.755948, -42.043687], 59 | [-63.458059, -42.563138], 60 | [-64.378804, -42.873558], 61 | [-65.181804, -43.495381], 62 | [-65.328823, -44.501366], 63 | [-65.565269, -45.036786], 64 | [-66.509966, -45.039628], 65 | [-67.293794, -45.551896], 66 | [-67.580546, -46.301773], 67 | [-66.597066, -47.033925], 68 | [-65.641027, -47.236135], 69 | [-65.985088, -48.133289], 70 | [-67.166179, -48.697337], 71 | [-67.816088, -49.869669], 72 | [-68.728745, -50.264218], 73 | [-69.138539, -50.73251], 74 | [-68.815561, -51.771104], 75 | [-68.149995, -52.349983], 76 | [-68.571545, -52.299444], 77 | [-69.498362, -52.142761], 78 | [-71.914804, -52.009022], 79 | [-72.329404, -51.425956], 80 | [-72.309974, -50.67701], 81 | [-72.975747, -50.74145], 82 | [-73.328051, -50.378785], 83 | [-73.415436, -49.318436], 84 | [-72.648247, -48.878618], 85 | [-72.331161, -48.244238], 86 | [-72.447355, -47.738533], 87 | [-71.917258, -46.884838], 88 | [-71.552009, -45.560733], 89 | [-71.659316, -44.973689], 90 | [-71.222779, -44.784243], 91 | [-71.329801, -44.407522], 92 | [-71.793623, -44.207172], 93 | [-71.464056, -43.787611], 94 | [-71.915424, -43.408565], 95 | [-72.148898, -42.254888], 96 | [-71.746804, -42.051386], 97 | [-71.915734, -40.832339], 98 | [-71.680761, -39.808164], 99 | [-71.413517, -38.916022], 100 | [-70.814664, -38.552995], 101 | [-71.118625, -37.576827], 102 | [-71.121881, -36.658124], 103 | [-70.364769, -36.005089], 104 | [-70.388049, -35.169688], 105 | [-69.817309, -34.193571], 106 | [-69.814777, -33.273886], 107 | [-70.074399, -33.09121], 108 | [-70.535069, -31.36501], 109 | [-69.919008, -30.336339], 110 | [-70.01355, -29.367923], 111 | [-69.65613, -28.459141], 112 | [-69.001235, -27.521214], 113 | [-68.295542, -26.89934], 114 | [-68.5948, -26.506909], 115 | [-68.386001, -26.185016], 116 | [-68.417653, -24.518555], 117 | [-67.328443, -24.025303], 118 | [-66.985234, -22.986349], 119 | [-67.106674, -22.735925], 120 | [-66.273339, -21.83231], 121 | [-64.964892, -22.075862] 122 | ] 123 | ] 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/Turf/WKT.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | Entity which can be converted to and from 'Well Known Text'. 5 | */ 6 | public protocol WKTConvertible { 7 | var wkt: String { get } 8 | init(wkt: String) throws 9 | } 10 | 11 | extension Point: WKTConvertible { 12 | public var wkt: String { 13 | return "POINT(\(coordinates.longitude) \(coordinates.latitude))" 14 | } 15 | 16 | public init(wkt: String) throws { 17 | self = try WKTParser.parse(wkt) 18 | } 19 | } 20 | 21 | extension MultiPoint: WKTConvertible { 22 | public var wkt: String { 23 | return "MULTIPOINT\(coordinates.wktCoordinatesString)" 24 | } 25 | 26 | public init(wkt: String) throws { 27 | self = try WKTParser.parse(wkt) 28 | } 29 | } 30 | 31 | extension LineString: WKTConvertible { 32 | public var wkt: String { 33 | return "LINESTRING\(coordinates.wktCoordinatesString)" 34 | } 35 | 36 | public init(wkt: String) throws { 37 | self = try WKTParser.parse(wkt) 38 | } 39 | } 40 | 41 | extension MultiLineString: WKTConvertible { 42 | public var wkt: String { 43 | return "MULTILINESTRING\(coordinates.wktCoordinatesString)" 44 | } 45 | 46 | public init(wkt: String) throws { 47 | self = try WKTParser.parse(wkt) 48 | } 49 | } 50 | 51 | extension Polygon: WKTConvertible { 52 | public var wkt: String { 53 | return "POLYGON\(coordinates.wktCoordinatesString)" 54 | } 55 | 56 | public init(wkt: String) throws { 57 | self = try WKTParser.parse(wkt) 58 | } 59 | } 60 | 61 | extension MultiPolygon: WKTConvertible { 62 | public var wkt: String { 63 | return "MULTIPOLYGON\(coordinates.wktCoordinatesString)" 64 | } 65 | 66 | public init(wkt: String) throws { 67 | self = try WKTParser.parse(wkt) 68 | } 69 | } 70 | 71 | extension Geometry: WKTConvertible { 72 | public var wkt: String { 73 | switch self { 74 | case .point(let geometry): 75 | return geometry.wkt 76 | case .lineString(let geometry): 77 | return geometry.wkt 78 | case .polygon(let geometry): 79 | return geometry.wkt 80 | case .multiPoint(let geometry): 81 | return geometry.wkt 82 | case .multiLineString(let geometry): 83 | return geometry.wkt 84 | case .multiPolygon(let geometry): 85 | return geometry.wkt 86 | case .geometryCollection(let geometry): 87 | return geometry.wkt 88 | } 89 | } 90 | 91 | public init(wkt: String) throws { 92 | let object: GeometryConvertible = try WKTParser.parse(wkt) 93 | self = object.geometry 94 | } 95 | } 96 | 97 | extension GeometryCollection: WKTConvertible { 98 | public var wkt: String { 99 | let geometriesWKT = geometries.map { 100 | switch $0 { 101 | case .point(let object): 102 | return object.wkt 103 | case .lineString(let object): 104 | return object.wkt 105 | case .polygon(let object): 106 | return object.wkt 107 | case .multiPoint(let object): 108 | return object.wkt 109 | case .multiLineString(let object): 110 | return object.wkt 111 | case .multiPolygon(let object): 112 | return object.wkt 113 | case .geometryCollection(let object): 114 | return object.wkt 115 | } 116 | }.joined(separator: ",") 117 | return "GEOMETRYCOLLECTION(\(geometriesWKT))" 118 | } 119 | 120 | public init(wkt: String) throws { 121 | self = try WKTParser.parse(wkt) 122 | } 123 | } 124 | 125 | 126 | extension Array where Element == LocationCoordinate2D { 127 | fileprivate var wktCoordinatesString: String { 128 | let string = map { 129 | return "\($0.longitude) \($0.latitude)" 130 | }.joined(separator:",") 131 | return "(\(string))" 132 | } 133 | } 134 | 135 | extension Array where Element == [LocationCoordinate2D] { 136 | fileprivate var wktCoordinatesString: String { 137 | let string = map { 138 | return $0.wktCoordinatesString 139 | }.joined(separator:",") 140 | return "(\(string))" 141 | } 142 | } 143 | 144 | extension Array where Element == [[LocationCoordinate2D]] { 145 | fileprivate var wktCoordinatesString: String { 146 | let string = map { 147 | return $0.wktCoordinatesString 148 | }.joined(separator:",") 149 | return "(\(string))" 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Sources/Turf/Spline.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | 7 | struct SplinePoint { 8 | let x: LocationDegrees 9 | let y: LocationDegrees 10 | let z: LocationDegrees 11 | 12 | init(coordinate: LocationCoordinate2D) { 13 | x = coordinate.longitude 14 | y = coordinate.latitude 15 | z = 0 16 | } 17 | 18 | init(x: LocationDegrees, y: LocationDegrees, z: LocationDegrees) { 19 | self.x = x 20 | self.y = y 21 | self.z = z 22 | } 23 | 24 | var coordinate: LocationCoordinate2D { 25 | return LocationCoordinate2D(latitude: y, longitude: x) 26 | } 27 | } 28 | 29 | struct Spline { 30 | private let points: [SplinePoint] 31 | private let duration: Int 32 | private let sharpness: Double 33 | private let stepLength: Double 34 | private let length: Int 35 | private let delay: Int = 0 36 | private var centers = [SplinePoint]() 37 | private var controls = [(SplinePoint, SplinePoint)]() 38 | private var steps = [Int]() 39 | 40 | init?(points: [SplinePoint], duration: Int, sharpness: Double, stepLength: Double = 60) { 41 | guard points.count >= 2 else { return nil } 42 | self.points = points 43 | self.duration = duration 44 | self.sharpness = sharpness 45 | self.stepLength = stepLength 46 | 47 | length = points.count 48 | 49 | centers = (0..<(points.count - 1)).map { (index) in 50 | let point = points[index] 51 | let nextPoint = points[index + 1] 52 | let center = SplinePoint(x: (point.x + nextPoint.x) / 2, y: (point.y + nextPoint.y) / 2, z: (point.z + nextPoint.z) / 2) 53 | return center 54 | } 55 | 56 | controls = (0..<(centers.count - 1)).map { (index) in 57 | let center = centers[index] 58 | let nextCenter = centers[index + 1] 59 | let nextPoint = points[index + 1] 60 | let dx = nextPoint.x - (center.x + nextCenter.x) / 2 61 | let dy = nextPoint.y - (center.y + nextCenter.y) / 2 62 | let dz = nextPoint.z - (center.z + nextCenter.z) / 2 63 | let control1 = SplinePoint(x: (1 - sharpness) * nextPoint.x + sharpness * (center.x + dx), 64 | y: (1 - sharpness) * nextPoint.y + sharpness * (center.y + dy), 65 | z: (1 - sharpness) * nextPoint.z + sharpness * (center.z + dz)) 66 | let control2 = SplinePoint(x: (1 - sharpness) * nextPoint.x + sharpness * (nextCenter.x + dx), 67 | y: (1 - sharpness) * nextPoint.y + sharpness * (nextCenter.y + dy), 68 | z: (1 - sharpness) * nextPoint.z + sharpness * (nextCenter.z + dz)) 69 | return (control1, control2) 70 | } 71 | let firstPoint = points.first! 72 | controls.insert((firstPoint, firstPoint), at: 0) 73 | let lastPoint = points.last! 74 | controls.append((lastPoint, lastPoint)) 75 | 76 | var lastStep = position(at: 0) 77 | steps.append(0) 78 | for t in stride(from: 0, to: duration, by: 10) { 79 | let step = position(at: t) 80 | let dist = sqrt((step.x - lastStep.x) * (step.x - lastStep.x) + 81 | (step.y - lastStep.y) * (step.y - lastStep.y) + 82 | (step.z - lastStep.z) * (step.z - lastStep.z)) 83 | if dist > stepLength { 84 | steps.append(t) 85 | lastStep = step 86 | } 87 | } 88 | } 89 | 90 | func position(at time: Int) -> SplinePoint { 91 | var t = max(0, time - delay) 92 | if t > duration { 93 | t = duration - 1 94 | } 95 | let t2: Double = Double(t) / Double(duration) 96 | if t2 >= 1 { 97 | return points.last! 98 | } 99 | let n = floor(Double(points.count - 1) * t2) 100 | let t1 = Double(points.count - 1) * t2 - n 101 | let index = Int(n) 102 | return bezier(t: t1, p1: points[index], c1: controls[index].1, c2: controls[index + 1].0, p2: points[index + 1]) 103 | } 104 | 105 | //MARK: - Private 106 | 107 | private func bezier(t: Double, p1: SplinePoint, c1: SplinePoint, c2: SplinePoint, p2: SplinePoint) -> SplinePoint { 108 | let b = B(t) 109 | let pos = SplinePoint(x: p2.x * b.0 + c2.x * b.1 + c1.x * b.2 + p1.x * b.3, 110 | y: p2.y * b.0 + c2.y * b.1 + c1.y * b.2 + p1.y * b.3, 111 | z: p2.z * b.0 + c2.z * b.1 + c1.z * b.2 + p1.z * b.3) 112 | return pos 113 | } 114 | 115 | private func B(_ t: Double) -> (Double, Double, Double, Double) { 116 | let t2 = t * t 117 | let t3 = t * t2 118 | return (t3, (3 * t2 * (1 - t)), (3 * t * (1 - t) * (1 - t)), ((1 - t) * (1 - t) * (1 - t))) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/Turf/Ring.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | /** 7 | A [linear ring](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.6) is a closed figure bounded by three or more straight line segments. 8 | */ 9 | 10 | public struct Ring: Sendable { 11 | /// The positions at which the linear ring is located. 12 | public var coordinates: [LocationCoordinate2D] 13 | 14 | /** 15 | Initializes a linear ring defined by the given positions. 16 | 17 | - parameter coordinates: The positions at which the linear ring is located. 18 | */ 19 | public init(coordinates: [LocationCoordinate2D]) { 20 | self.coordinates = coordinates 21 | } 22 | 23 | /** 24 | * Calculate the approximate area of the polygon were it projected onto the earth, in square meters. 25 | * Note that this area will be positive if ring is oriented clockwise, otherwise it will be negative. 26 | * 27 | * Reference: 28 | * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion 29 | * Laboratory, Pasadena, CA, June 2007 https://trs.jpl.nasa.gov/handle/2014/41271 30 | * 31 | */ 32 | public var area: Double { 33 | var area: Double = 0 34 | let coordinatesCount: Int = coordinates.count 35 | 36 | if coordinatesCount > 2 { 37 | for index in 0.. Bool { 71 | let bbox = BoundingBox(from: coordinates) 72 | guard bbox?.contains(coordinate, ignoreBoundary: ignoreBoundary) ?? false else { 73 | return false 74 | } 75 | 76 | var ring: ArraySlice! 77 | var isInside = false 78 | if coordinates.first == coordinates.last { 79 | ring = coordinates.prefix(coordinates.count - 1) 80 | } 81 | else { 82 | ring = coordinates.prefix(coordinates.count) 83 | } 84 | var i = 0 85 | var j = ring.count - 1 86 | while i < ring.count { 87 | let xi = ring[i].longitude 88 | let yi = ring[i].latitude 89 | let xj = ring[j].longitude 90 | let yj = ring[j].latitude 91 | let onBoundary = (coordinate.latitude * (xi - xj) + yi * (xj - coordinate.longitude) + yj * (coordinate.longitude - xi) == 0) && 92 | ((xi - coordinate.longitude) * (xj - coordinate.longitude) <= 0) && ((yi - coordinate.latitude) * (yj - coordinate.latitude) <= 0) 93 | if onBoundary { 94 | return !ignoreBoundary 95 | } 96 | let intersect = ((yi > coordinate.latitude) != (yj > coordinate.latitude)) && 97 | (coordinate.longitude < (xj - xi) * (coordinate.latitude - yi) / (yj - yi) + xi); 98 | if (intersect) { 99 | isInside = !isInside; 100 | } 101 | j = i 102 | i = i + 1 103 | } 104 | return isInside 105 | } 106 | } 107 | 108 | extension Ring: Codable { 109 | public init(from decoder: Decoder) throws { 110 | let container = try decoder.singleValueContainer() 111 | self = Ring(coordinates: try container.decode([LocationCoordinate2DCodable].self).decodedCoordinates) 112 | } 113 | 114 | public func encode(to encoder: Encoder) throws { 115 | var container = encoder.singleValueContainer() 116 | try container.encode(coordinates.codableCoordinates) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | parameters: 4 | flow: 5 | type: enum 6 | enum: [build, pre-release, release] 7 | default: build 8 | version: 9 | type: string 10 | default: "" 11 | 12 | commands: 13 | install-gh: 14 | steps: 15 | - run: 16 | name: "Install GH" 17 | command: | 18 | brew install gh 19 | - run: 20 | name: Install mbx-ci 21 | command: | 22 | curl -Ls https://mapbox-release-engineering.s3.amazonaws.com/mbx-ci/latest/mbx-ci-darwin-arm64 > /usr/local/bin/mbx-ci 23 | chmod 755 /usr/local/bin/mbx-ci 24 | setup_environment: 25 | steps: 26 | - add_ssh_keys 27 | - run: 28 | name: "Download distribution certificate for codesigning" 29 | command: | 30 | bundle install 31 | bundle exec fastlane setup_distribution_cert 32 | - install-gh 33 | 34 | jobs: 35 | build_and_test_linux: 36 | docker: 37 | - image: swift:latest 38 | steps: 39 | - checkout 40 | - run: 41 | name: "Build" 42 | command: swift build 43 | - run: 44 | name: "Test" 45 | command: swift test 46 | 47 | build_and_test_macos: 48 | parameters: 49 | xcode_version: 50 | type: string 51 | macos: 52 | xcode: << parameters.xcode_version >> 53 | steps: 54 | - checkout 55 | - run: 56 | name: "Build" 57 | command: xcodebuild -scheme Turf -configuration Debug -destination 'platform=macOS' build 58 | - run: 59 | name: "Test" 60 | command: xcodebuild -scheme Turf -configuration Debug -destination 'platform=macOS' test CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO 61 | 62 | test_simulator: 63 | parameters: 64 | destination: 65 | type: string 66 | macos: 67 | xcode: 15.2.0 68 | resource_class: macos.m1.medium.gen1 69 | steps: 70 | - checkout 71 | - run: 72 | name: "Test << parameters.destination >>" 73 | command: | 74 | xcodebuild test \ 75 | -scheme Turf \ 76 | -destination "<< parameters.destination >>" \ 77 | -derivedDataPath build 78 | 79 | create_xcframework: 80 | macos: 81 | xcode: 15.2.0 82 | steps: 83 | - checkout 84 | - setup_environment 85 | - run: 86 | name: "Build and create xcframework" 87 | command: ./scripts/xcframework.sh build 88 | - run: 89 | name: Validate framework stripping 90 | command: | 91 | mkdir strip 92 | cp -r Turf.xcframework strip 93 | 94 | find strip/Turf.xcframework -type f -name "Turf" -not -path "*dSYM*" -exec strip -rDSTx {} \; 95 | 96 | error=0 97 | for file in $(find Turf.xcframework -type f -name "Turf" -not -path "*dSYM*"); do 98 | size1=$(stat -f %z "$file") 99 | size2=$(stat -f %z "strip/$file") 100 | 101 | diff=$(( size1 - size2 )) 102 | 103 | if (( size1 != size2 )); then 104 | echo "File sizes differ: $file (original: $size1 bytes, strip: $size2 bytes)" 105 | error=1 106 | fi 107 | done 108 | exit $error 109 | 110 | working_directory: build 111 | - persist_to_workspace: 112 | root: build 113 | paths: 114 | - xcframework_checksum.txt 115 | - Turf.xcframework.zip 116 | 117 | pre-release-job: 118 | macos: 119 | xcode: 15.2.0 120 | steps: 121 | - checkout 122 | - setup_environment 123 | - run: 124 | name: "Prepare branch for release and make PR" 125 | command: ./scripts/pre-release.sh << pipeline.parameters.version >> 126 | 127 | release-job: 128 | macos: 129 | xcode: 15.2.0 130 | steps: 131 | - checkout 132 | - install-gh 133 | - run: 134 | name: "Validate cheksum, publish GitHub release, validate manifests, publish CocoaPods" 135 | command: ./scripts/release.sh << pipeline.parameters.version >> 136 | 137 | workflows: 138 | build-and-test: 139 | when: 140 | equal: [ build, << pipeline.parameters.flow >> ] 141 | jobs: 142 | - create_xcframework 143 | - build_and_test_linux 144 | - build_and_test_macos: 145 | matrix: 146 | parameters: 147 | xcode_version: [14.3.1, 15.2.0] 148 | - test_simulator: 149 | matrix: 150 | parameters: 151 | destination: 152 | - "platform=visionOS Simulator,OS=1.0,name=Apple Vision Pro" 153 | - "platform=iOS Simulator,OS=17.2,name=iPhone 15" 154 | 155 | pre-release: 156 | when: 157 | equal: [ pre-release, << pipeline.parameters.flow >> ] 158 | jobs: 159 | - pre-release-job: 160 | filters: 161 | branches: 162 | only: 163 | - main 164 | 165 | release: 166 | when: 167 | equal: [ release, << pipeline.parameters.flow >> ] 168 | jobs: 169 | - release-job: 170 | filters: 171 | branches: 172 | only: 173 | - main 174 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/in/featurecollection.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": { 7 | "id": 1, 8 | "tolerance": 0.01 9 | }, 10 | "geometry": { 11 | "type": "LineString", 12 | "coordinates": [ 13 | [27.977542877197266, -26.17500493262446], 14 | [27.975482940673828, -26.17870225771557], 15 | [27.969818115234375, -26.177931991326645], 16 | [27.967071533203125, -26.177623883345735], 17 | [27.966899871826172, -26.1810130263384], 18 | [27.967758178710938, -26.1853263385099], 19 | [27.97290802001953, -26.1853263385099], 20 | [27.97496795654297, -26.18270756087535], 21 | [27.97840118408203, -26.1810130263384], 22 | [27.98011779785156, -26.183323749143113], 23 | [27.98011779785156, -26.18655868408986], 24 | [27.978744506835938, -26.18933141398614], 25 | [27.97496795654297, -26.19025564262006], 26 | [27.97119140625, -26.19040968001282], 27 | [27.969303131103516, -26.1899475672235], 28 | [27.96741485595703, -26.189639491012183], 29 | [27.9656982421875, -26.187945057286793], 30 | [27.965354919433594, -26.18563442612686], 31 | [27.96432495117187, -26.183015655416536] 32 | ] 33 | } 34 | }, 35 | { 36 | "type": "Feature", 37 | "properties": { 38 | "id": 2, 39 | "tolerance": 0.01 40 | }, 41 | "geometry": { 42 | "type": "Polygon", 43 | "coordinates": [ 44 | [ 45 | [27.972049713134762, -26.199035448897074], 46 | [27.9741096496582, -26.196108920345292], 47 | [27.977371215820312, -26.197495179879635], 48 | [27.978572845458984, -26.20042167359348], 49 | [27.980976104736328, -26.200729721284862], 50 | [27.982349395751953, -26.197803235312957], 51 | [27.982177734375, -26.194414580727656], 52 | [27.982177734375, -26.19256618212382], 53 | [27.98406600952148, -26.192258112838022], 54 | [27.985267639160156, -26.191950042737417], 55 | [27.986125946044922, -26.19426054863105], 56 | [27.986984252929688, -26.196416979445644], 57 | [27.987327575683594, -26.198881422912123], 58 | [27.98715591430664, -26.201345814222698], 59 | [27.985095977783203, -26.20381015337393], 60 | [27.983036041259766, -26.20550435628209], 61 | [27.979946136474606, -26.20550435628209], 62 | [27.97719955444336, -26.20488828535003], 63 | [27.97445297241211, -26.203656133705152], 64 | [27.972564697265625, -26.201961903900578], 65 | [27.972049713134762, -26.199035448897074] 66 | ] 67 | ] 68 | } 69 | }, 70 | { 71 | "type": "Feature", 72 | "properties": { 73 | "id": 3, 74 | "tolerance": 0.01 75 | }, 76 | "geometry": { 77 | "type": "Polygon", 78 | "coordinates": [ 79 | [ 80 | [27.946643829345703, -26.170845301716803], 81 | [27.94269561767578, -26.183631842055114], 82 | [27.935657501220703, -26.183323749143113], 83 | [27.92741775512695, -26.17685360983018], 84 | [27.926902770996094, -26.171153427614488], 85 | [27.928619384765625, -26.165298896316028], 86 | [27.936859130859375, -26.161292995018652], 87 | [27.94509887695312, -26.158981835530525], 88 | [27.950420379638672, -26.161601146157146], 89 | [27.951793670654297, -26.166223315536712], 90 | [27.954025268554688, -26.173464345889972], 91 | [27.954025268554688, -26.179626570662702], 92 | [27.951278686523438, -26.187945057286793], 93 | [27.944583892822266, -26.19395248382672], 94 | [27.936172485351562, -26.194876675795218], 95 | [27.930850982666016, -26.19379845111899], 96 | [27.925701141357422, -26.190563717201886], 97 | [27.92278289794922, -26.18655868408986], 98 | [27.92072296142578, -26.180858976522302], 99 | [27.917118072509766, -26.174080583026957], 100 | [27.916603088378906, -26.16683959094609], 101 | [27.917118072509766, -26.162987816205614], 102 | [27.920207977294922, -26.162987816205614], 103 | [27.920894622802734, -26.166069246175482], 104 | [27.9217529296875, -26.17146155269785], 105 | [27.923297882080078, -26.177469829049862], 106 | [27.92673110961914, -26.184248025435295], 107 | [27.930335998535156, -26.18856121785662], 108 | [27.936687469482422, -26.18871525748988], 109 | [27.942352294921875, -26.187945057286793], 110 | [27.94647216796875, -26.184248025435295], 111 | [27.946815490722653, -26.178548204845022], 112 | [27.946643829345703, -26.170845301716803] 113 | ], 114 | [ 115 | [27.936859130859375, -26.16591517661071], 116 | [27.934799194335938, -26.16945872510008], 117 | [27.93497085571289, -26.173926524048102], 118 | [27.93874740600586, -26.175621161617432], 119 | [27.94149398803711, -26.17007498340995], 120 | [27.94321060180664, -26.166223315536712], 121 | [27.939090728759762, -26.164528541367826], 122 | [27.937545776367188, -26.16406632595636], 123 | [27.936859130859375, -26.16591517661071] 124 | ] 125 | ] 126 | } 127 | }, 128 | { 129 | "type": "Feature", 130 | "properties": { 131 | "id": 4, 132 | "tolerance": 0.01 133 | }, 134 | "geometry": { 135 | "type": "Point", 136 | "coordinates": [27.95642852783203, -26.152510345365126] 137 | } 138 | } 139 | ] 140 | } 141 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | addressable (2.8.7) 9 | public_suffix (>= 2.0.2, < 7.0) 10 | artifactory (3.0.17) 11 | atomos (0.1.3) 12 | aws-eventstream (1.3.0) 13 | aws-partitions (1.955.0) 14 | aws-sdk-core (3.201.1) 15 | aws-eventstream (~> 1, >= 1.3.0) 16 | aws-partitions (~> 1, >= 1.651.0) 17 | aws-sigv4 (~> 1.8) 18 | jmespath (~> 1, >= 1.6.1) 19 | aws-sdk-kms (1.88.0) 20 | aws-sdk-core (~> 3, >= 3.201.0) 21 | aws-sigv4 (~> 1.5) 22 | aws-sdk-s3 (1.156.0) 23 | aws-sdk-core (~> 3, >= 3.201.0) 24 | aws-sdk-kms (~> 1) 25 | aws-sigv4 (~> 1.5) 26 | aws-sigv4 (1.8.0) 27 | aws-eventstream (~> 1, >= 1.0.2) 28 | babosa (1.0.4) 29 | base64 (0.2.0) 30 | claide (1.1.0) 31 | colored (1.2) 32 | colored2 (3.1.2) 33 | commander (4.6.0) 34 | highline (~> 2.0.0) 35 | declarative (0.0.20) 36 | digest-crc (0.6.5) 37 | rake (>= 12.0.0, < 14.0.0) 38 | domain_name (0.6.20240107) 39 | dotenv (2.8.1) 40 | emoji_regex (3.2.3) 41 | excon (0.111.0) 42 | faraday (1.10.3) 43 | faraday-em_http (~> 1.0) 44 | faraday-em_synchrony (~> 1.0) 45 | faraday-excon (~> 1.1) 46 | faraday-httpclient (~> 1.0) 47 | faraday-multipart (~> 1.0) 48 | faraday-net_http (~> 1.0) 49 | faraday-net_http_persistent (~> 1.0) 50 | faraday-patron (~> 1.0) 51 | faraday-rack (~> 1.0) 52 | faraday-retry (~> 1.0) 53 | ruby2_keywords (>= 0.0.4) 54 | faraday-cookie_jar (0.0.7) 55 | faraday (>= 0.8.0) 56 | http-cookie (~> 1.0.0) 57 | faraday-em_http (1.0.0) 58 | faraday-em_synchrony (1.0.0) 59 | faraday-excon (1.1.0) 60 | faraday-httpclient (1.0.1) 61 | faraday-multipart (1.0.4) 62 | multipart-post (~> 2) 63 | faraday-net_http (1.0.1) 64 | faraday-net_http_persistent (1.2.0) 65 | faraday-patron (1.0.0) 66 | faraday-rack (1.0.0) 67 | faraday-retry (1.0.3) 68 | faraday_middleware (1.2.0) 69 | faraday (~> 1.0) 70 | fastimage (2.3.1) 71 | fastlane (2.219.0) 72 | CFPropertyList (>= 2.3, < 4.0.0) 73 | addressable (>= 2.8, < 3.0.0) 74 | artifactory (~> 3.0) 75 | aws-sdk-s3 (~> 1.0) 76 | babosa (>= 1.0.3, < 2.0.0) 77 | bundler (>= 1.12.0, < 3.0.0) 78 | colored 79 | commander (~> 4.6) 80 | dotenv (>= 2.1.1, < 3.0.0) 81 | emoji_regex (>= 0.1, < 4.0) 82 | excon (>= 0.71.0, < 1.0.0) 83 | faraday (~> 1.0) 84 | faraday-cookie_jar (~> 0.0.6) 85 | faraday_middleware (~> 1.0) 86 | fastimage (>= 2.1.0, < 3.0.0) 87 | gh_inspector (>= 1.1.2, < 2.0.0) 88 | google-apis-androidpublisher_v3 (~> 0.3) 89 | google-apis-playcustomapp_v1 (~> 0.1) 90 | google-cloud-env (>= 1.6.0, < 2.0.0) 91 | google-cloud-storage (~> 1.31) 92 | highline (~> 2.0) 93 | http-cookie (~> 1.0.5) 94 | json (< 3.0.0) 95 | jwt (>= 2.1.0, < 3) 96 | mini_magick (>= 4.9.4, < 5.0.0) 97 | multipart-post (>= 2.0.0, < 3.0.0) 98 | naturally (~> 2.2) 99 | optparse (>= 0.1.1) 100 | plist (>= 3.1.0, < 4.0.0) 101 | rubyzip (>= 2.0.0, < 3.0.0) 102 | security (= 0.1.3) 103 | simctl (~> 1.6.3) 104 | terminal-notifier (>= 2.0.0, < 3.0.0) 105 | terminal-table (~> 3) 106 | tty-screen (>= 0.6.3, < 1.0.0) 107 | tty-spinner (>= 0.8.0, < 1.0.0) 108 | word_wrap (~> 1.0.0) 109 | xcodeproj (>= 1.13.0, < 2.0.0) 110 | xcpretty (~> 0.3.0) 111 | xcpretty-travis-formatter (>= 0.0.3) 112 | gh_inspector (1.1.3) 113 | google-apis-androidpublisher_v3 (0.54.0) 114 | google-apis-core (>= 0.11.0, < 2.a) 115 | google-apis-core (0.11.3) 116 | addressable (~> 2.5, >= 2.5.1) 117 | googleauth (>= 0.16.2, < 2.a) 118 | httpclient (>= 2.8.1, < 3.a) 119 | mini_mime (~> 1.0) 120 | representable (~> 3.0) 121 | retriable (>= 2.0, < 4.a) 122 | rexml 123 | google-apis-iamcredentials_v1 (0.17.0) 124 | google-apis-core (>= 0.11.0, < 2.a) 125 | google-apis-playcustomapp_v1 (0.13.0) 126 | google-apis-core (>= 0.11.0, < 2.a) 127 | google-apis-storage_v1 (0.31.0) 128 | google-apis-core (>= 0.11.0, < 2.a) 129 | google-cloud-core (1.7.0) 130 | google-cloud-env (>= 1.0, < 3.a) 131 | google-cloud-errors (~> 1.0) 132 | google-cloud-env (1.6.0) 133 | faraday (>= 0.17.3, < 3.0) 134 | google-cloud-errors (1.4.0) 135 | google-cloud-storage (1.47.0) 136 | addressable (~> 2.8) 137 | digest-crc (~> 0.4) 138 | google-apis-iamcredentials_v1 (~> 0.1) 139 | google-apis-storage_v1 (~> 0.31.0) 140 | google-cloud-core (~> 1.6) 141 | googleauth (>= 0.16.2, < 2.a) 142 | mini_mime (~> 1.0) 143 | googleauth (1.8.1) 144 | faraday (>= 0.17.3, < 3.a) 145 | jwt (>= 1.4, < 3.0) 146 | multi_json (~> 1.11) 147 | os (>= 0.9, < 2.0) 148 | signet (>= 0.16, < 2.a) 149 | highline (2.0.3) 150 | http-cookie (1.0.6) 151 | domain_name (~> 0.5) 152 | httpclient (2.8.3) 153 | jmespath (1.6.2) 154 | json (2.7.2) 155 | jwt (2.8.2) 156 | base64 157 | mini_magick (4.13.2) 158 | mini_mime (1.1.5) 159 | multi_json (1.15.0) 160 | multipart-post (2.4.1) 161 | nanaimo (0.3.0) 162 | naturally (2.2.1) 163 | nkf (0.2.0) 164 | optparse (0.5.0) 165 | os (1.1.4) 166 | plist (3.7.1) 167 | public_suffix (6.0.0) 168 | rake (13.2.1) 169 | representable (3.2.0) 170 | declarative (< 0.1.0) 171 | trailblazer-option (>= 0.1.1, < 0.2.0) 172 | uber (< 0.2.0) 173 | retriable (3.1.2) 174 | rexml (3.3.8) 175 | rouge (2.0.7) 176 | ruby2_keywords (0.0.5) 177 | rubyzip (2.3.2) 178 | security (0.1.3) 179 | signet (0.19.0) 180 | addressable (~> 2.8) 181 | faraday (>= 0.17.5, < 3.a) 182 | jwt (>= 1.5, < 3.0) 183 | multi_json (~> 1.10) 184 | simctl (1.6.10) 185 | CFPropertyList 186 | naturally 187 | terminal-notifier (2.0.0) 188 | terminal-table (3.0.2) 189 | unicode-display_width (>= 1.1.1, < 3) 190 | trailblazer-option (0.1.2) 191 | tty-cursor (0.7.1) 192 | tty-screen (0.8.2) 193 | tty-spinner (0.9.3) 194 | tty-cursor (~> 0.7) 195 | uber (0.1.0) 196 | unicode-display_width (2.5.0) 197 | word_wrap (1.0.0) 198 | xcodeproj (1.25.1) 199 | CFPropertyList (>= 2.3.3, < 4.0) 200 | atomos (~> 0.1.3) 201 | claide (>= 1.0.2, < 2.0) 202 | colored2 (~> 3.1) 203 | nanaimo (~> 0.3.0) 204 | rexml (>= 3.3.6, < 4.0) 205 | xcpretty (0.3.0) 206 | rouge (~> 2.0.7) 207 | xcpretty-travis-formatter (1.0.1) 208 | xcpretty (~> 0.2, >= 0.0.7) 209 | 210 | PLATFORMS 211 | arm64-darwin-23 212 | ruby 213 | 214 | DEPENDENCIES 215 | fastlane (= 2.219.0) 216 | 217 | BUNDLED WITH 218 | 2.5.4 219 | -------------------------------------------------------------------------------- /Sources/Turf/GeoJSON.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | /** 7 | A [GeoJSON object](https://datatracker.ietf.org/doc/html/rfc7946#section-3) represents a Geometry, Feature, or collection of Features. 8 | 9 | - Note: [Foreign members](https://datatracker.ietf.org/doc/html/rfc7946#section-6.1) which may be present inside are coded only if used `JSONEncoder` or `JSONDecoder` has `userInfo[.includesForeignMembers] = true`. 10 | */ 11 | public enum GeoJSONObject: Equatable, Sendable { 12 | /** 13 | A [Geometry object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1) represents points, curves, and surfaces in coordinate space. 14 | 15 | - parameter geometry: The GeoJSON object as a Geometry object. 16 | */ 17 | case geometry(_ geometry: Geometry) 18 | 19 | /** 20 | A [Feature object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2) represents a spatially bounded thing. 21 | 22 | - parameter feature: The GeoJSON object as a Feature object. 23 | */ 24 | case feature(_ feature: Feature) 25 | 26 | /** 27 | A [FeatureCollection object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.3) is a collection of Feature objects. 28 | 29 | - parameter featureCollection: The GeoJSON object as a FeatureCollection object. 30 | */ 31 | case featureCollection(_ featureCollection: FeatureCollection) 32 | 33 | /// Initializes a GeoJSON object representing the given GeoJSON object–convertible instance. 34 | public init(_ object: GeoJSONObjectConvertible) { 35 | self = object.geoJSONObject 36 | } 37 | } 38 | 39 | extension GeoJSONObject { 40 | /// A geometry object. 41 | public var geometry: Geometry? { 42 | if case let .geometry(geometry) = self { 43 | return geometry 44 | } 45 | return nil 46 | } 47 | 48 | /// A feature object. 49 | public var feature: Feature? { 50 | if case let .feature(feature) = self { 51 | return feature 52 | } 53 | return nil 54 | } 55 | 56 | /// A feature collection object. 57 | public var featureCollection: FeatureCollection? { 58 | if case let .featureCollection(featureCollection) = self { 59 | return featureCollection 60 | } 61 | return nil 62 | } 63 | } 64 | 65 | extension GeoJSONObject: Codable { 66 | private enum CodingKeys: String, CodingKey { 67 | case kind = "type" 68 | } 69 | 70 | public init(from decoder: Decoder) throws { 71 | let kindContainer = try decoder.container(keyedBy: CodingKeys.self) 72 | let container = try decoder.singleValueContainer() 73 | switch try kindContainer.decode(String.self, forKey: .kind) { 74 | case Feature.Kind.Feature.rawValue: 75 | self = .feature(try container.decode(Feature.self)) 76 | case FeatureCollection.Kind.FeatureCollection.rawValue: 77 | self = .featureCollection(try container.decode(FeatureCollection.self)) 78 | default: 79 | self = .geometry(try container.decode(Geometry.self)) 80 | } 81 | } 82 | 83 | public func encode(to encoder: Encoder) throws { 84 | var container = encoder.singleValueContainer() 85 | switch self { 86 | case .geometry(let geometry): 87 | try container.encode(geometry) 88 | case .feature(let feature): 89 | try container.encode(feature) 90 | case .featureCollection(let featureCollection): 91 | try container.encode(featureCollection) 92 | } 93 | } 94 | } 95 | 96 | /** 97 | A type that can be represented as a `GeoJSONObject` instance. 98 | */ 99 | public protocol GeoJSONObjectConvertible { 100 | /// The instance wrapped in a `GeoJSONObject` instance. 101 | var geoJSONObject: GeoJSONObject { get } 102 | } 103 | 104 | extension GeoJSONObject: GeoJSONObjectConvertible { 105 | public var geoJSONObject: GeoJSONObject { return self } 106 | } 107 | 108 | extension Geometry: GeoJSONObjectConvertible { 109 | public var geoJSONObject: GeoJSONObject { return .geometry(self) } 110 | } 111 | 112 | extension Feature: GeoJSONObjectConvertible { 113 | public var geoJSONObject: GeoJSONObject { return .feature(self) } 114 | } 115 | 116 | extension FeatureCollection: GeoJSONObjectConvertible { 117 | public var geoJSONObject: GeoJSONObject { return .featureCollection(self) } 118 | } 119 | 120 | /** 121 | A GeoJSON object that can contain [foreign members](https://datatracker.ietf.org/doc/html/rfc7946#section-6.1) in arbitrary keys. 122 | */ 123 | public protocol ForeignMemberContainer: Sendable { 124 | /// [Foreign members](https://datatracker.ietf.org/doc/html/rfc7946#section-6.1) to round-trip to JSON. 125 | /// 126 | /// Members are coded only if used `JSONEncoder` or `JSONDecoder` has `userInfo[.includesForeignMembers] = true`. 127 | var foreignMembers: JSONObject { get set } 128 | } 129 | 130 | /** 131 | Key to pass to populate a `userInfo` dictionary, which is passed to the `JSONDecoder` or `JSONEncoder` to enable processing foreign members. 132 | */ 133 | public extension CodingUserInfoKey { 134 | /** 135 | Indicates if coding of foreign members is enabled. 136 | 137 | Boolean flag to enable coding. Default (or missing) value is to ignore foreign members. 138 | */ 139 | static let includesForeignMembers = CodingUserInfoKey(rawValue: "com.mapbox.turf.coding.includesForeignMembers")! 140 | } 141 | 142 | extension ForeignMemberContainer { 143 | /** 144 | Decodes any foreign members using the given decoder. 145 | */ 146 | mutating func decodeForeignMembers(notKeyedBy _: WellKnownCodingKeys.Type, with decoder: Decoder) throws where WellKnownCodingKeys: CodingKey { 147 | guard let allowCoding = decoder.userInfo[.includesForeignMembers] as? Bool, 148 | allowCoding else { return } 149 | 150 | let foreignMemberContainer = try decoder.container(keyedBy: AnyCodingKey.self) 151 | for key in foreignMemberContainer.allKeys { 152 | if WellKnownCodingKeys(stringValue: key.stringValue) == nil { 153 | foreignMembers[key.stringValue] = try foreignMemberContainer.decode(JSONValue?.self, forKey: key) 154 | } 155 | } 156 | } 157 | 158 | /** 159 | Encodes any foreign members using the given encoder. 160 | */ 161 | func encodeForeignMembers(notKeyedBy _: WellKnownCodingKeys.Type, to encoder: Encoder) throws where WellKnownCodingKeys: CodingKey { 162 | guard let allowCoding = encoder.userInfo[.includesForeignMembers] as? Bool, 163 | allowCoding else { return } 164 | 165 | var foreignMemberContainer = encoder.container(keyedBy: AnyCodingKey.self) 166 | for (key, value) in foreignMembers { 167 | if let key = AnyCodingKey(stringValue: key), 168 | WellKnownCodingKeys(stringValue: key.stringValue) == nil { 169 | try foreignMemberContainer.encode(value, forKey: key) 170 | } 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Sources/Turf/Geometry.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | 6 | /** 7 | A [Geometry object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1) represents points, curves, and surfaces in coordinate space. Use an instance of this enumeration whenever a value could be any kind of Geometry object. 8 | */ 9 | public enum Geometry: Equatable, Sendable { 10 | /// A single position. 11 | case point(_ geometry: Point) 12 | 13 | /// A collection of two or more positions, each position connected to the next position linearly. 14 | case lineString(_ geometry: LineString) 15 | 16 | /// Conceptually, a collection of `Ring`s that form a single connected geometry. 17 | case polygon(_ geometry: Polygon) 18 | 19 | /// A collection of positions that are disconnected but related. 20 | case multiPoint(_ geometry: MultiPoint) 21 | 22 | /// A collection of `LineString` geometries that are disconnected but related. 23 | case multiLineString(_ geometry: MultiLineString) 24 | 25 | /// A collection of `Polygon` geometries that are disconnected but related. 26 | case multiPolygon(_ geometry: MultiPolygon) 27 | 28 | /// A heterogeneous collection of geometries that are related. 29 | case geometryCollection(_ geometry: GeometryCollection) 30 | 31 | /// Initializes a geometry representing the given geometry–convertible instance. 32 | public init(_ geometry: GeometryConvertible) { 33 | self = geometry.geometry 34 | } 35 | } 36 | 37 | extension Geometry: Codable { 38 | private enum CodingKeys: String, CodingKey { 39 | case kind = "type" 40 | } 41 | 42 | enum Kind: String, Codable, CaseIterable { 43 | case Point 44 | case LineString 45 | case Polygon 46 | case MultiPoint 47 | case MultiLineString 48 | case MultiPolygon 49 | case GeometryCollection 50 | } 51 | 52 | public init(from decoder: Decoder) throws { 53 | let kindContainer = try decoder.container(keyedBy: CodingKeys.self) 54 | let container = try decoder.singleValueContainer() 55 | switch try kindContainer.decode(Kind.self, forKey: .kind) { 56 | case .Point: 57 | self = .point(try container.decode(Point.self)) 58 | case .LineString: 59 | self = .lineString(try container.decode(LineString.self)) 60 | case .Polygon: 61 | self = .polygon(try container.decode(Polygon.self)) 62 | case .MultiPoint: 63 | self = .multiPoint(try container.decode(MultiPoint.self)) 64 | case .MultiLineString: 65 | self = .multiLineString(try container.decode(MultiLineString.self)) 66 | case .MultiPolygon: 67 | self = .multiPolygon(try container.decode(MultiPolygon.self)) 68 | case .GeometryCollection: 69 | self = .geometryCollection(try container.decode(GeometryCollection.self)) 70 | } 71 | } 72 | 73 | public func encode(to encoder: Encoder) throws { 74 | var container = encoder.singleValueContainer() 75 | switch self { 76 | case .point(let point): 77 | try container.encode(point) 78 | case .lineString(let lineString): 79 | try container.encode(lineString) 80 | case .polygon(let polygon): 81 | try container.encode(polygon) 82 | case .multiPoint(let multiPoint): 83 | try container.encode(multiPoint) 84 | case .multiLineString(let multiLineString): 85 | try container.encode(multiLineString) 86 | case .multiPolygon(let multiPolygon): 87 | try container.encode(multiPolygon) 88 | case .geometryCollection(let geometryCollection): 89 | try container.encode(geometryCollection) 90 | } 91 | } 92 | } 93 | 94 | extension Geometry { 95 | /// A single position. 96 | public var point: Point? { 97 | if case let .point(point) = self { 98 | return point 99 | } else { 100 | return nil 101 | } 102 | 103 | } 104 | 105 | /// A collection of two or more positions, each position connected to the next position linearly. 106 | public var lineString: LineString? { 107 | if case let .lineString(lineString) = self { 108 | return lineString 109 | } else { 110 | return nil 111 | } 112 | 113 | } 114 | 115 | /// Conceptually, a collection of `Ring`s that form a single connected geometry. 116 | public var polygon: Polygon? { 117 | if case let .polygon(polygon) = self { 118 | return polygon 119 | } else { 120 | return nil 121 | } 122 | 123 | } 124 | 125 | /// A collection of positions that are disconnected but related. 126 | public var multiPoint: MultiPoint? { 127 | if case let .multiPoint(multiPoint) = self { 128 | return multiPoint 129 | } else { 130 | return nil 131 | } 132 | 133 | } 134 | 135 | /// A collection of `LineString` geometries that are disconnected but related. 136 | public var multiLineString: MultiLineString? { 137 | if case let .multiLineString(multiLineString) = self { 138 | return multiLineString 139 | } else { 140 | return nil 141 | } 142 | 143 | } 144 | 145 | /// A collection of `Polygon` geometries that are disconnected but related. 146 | public var multiPolygon: MultiPolygon? { 147 | if case let .multiPolygon(multiPolygon) = self { 148 | return multiPolygon 149 | } else { 150 | return nil 151 | } 152 | 153 | } 154 | 155 | /// A heterogeneous collection of geometries that are related. 156 | public var geometryCollection: GeometryCollection? { 157 | if case let .geometryCollection(geometryCollection) = self { 158 | return geometryCollection 159 | } else { 160 | return nil 161 | } 162 | 163 | } 164 | } 165 | 166 | /** 167 | A type that can be represented as a `Geometry` instance. 168 | */ 169 | public protocol GeometryConvertible: Sendable { 170 | /// The instance wrapped in a `Geometry` instance. 171 | var geometry: Geometry { get } 172 | } 173 | 174 | extension Geometry: GeometryConvertible { 175 | public var geometry: Geometry { return self } 176 | } 177 | 178 | extension Point: GeometryConvertible { 179 | public var geometry: Geometry { return .point(self) } 180 | } 181 | 182 | extension LineString: GeometryConvertible { 183 | public var geometry: Geometry { return .lineString(self) } 184 | } 185 | 186 | extension Polygon: GeometryConvertible { 187 | public var geometry: Geometry { return .polygon(self) } 188 | } 189 | 190 | extension MultiPoint: GeometryConvertible { 191 | public var geometry: Geometry { return .multiPoint(self) } 192 | } 193 | 194 | extension MultiLineString: GeometryConvertible { 195 | public var geometry: Geometry { return .multiLineString(self) } 196 | } 197 | 198 | extension MultiPolygon: GeometryConvertible { 199 | public var geometry: Geometry { return .multiPolygon(self) } 200 | } 201 | 202 | extension GeometryCollection: GeometryConvertible { 203 | public var geometry: Geometry { return .geometryCollection(self) } 204 | } 205 | -------------------------------------------------------------------------------- /Tests/TurfTests/FeatureCollectionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | #if !os(Linux) 3 | import CoreLocation 4 | #endif 5 | import Turf 6 | 7 | class FeatureCollectionTests: XCTestCase { 8 | 9 | func testFeatureCollection() { 10 | let data = try! Fixture.geojsonData(from: "featurecollection")! 11 | let geojson = try! JSONDecoder().decode(GeoJSONObject.self, from: data) 12 | guard case let .featureCollection(featureCollection) = geojson else { return XCTFail() } 13 | 14 | if case .lineString = featureCollection.features[0].geometry {} else { XCTFail() } 15 | if case .polygon = featureCollection.features[1].geometry {} else { XCTFail() } 16 | if case .polygon = featureCollection.features[2].geometry {} else { XCTFail() } 17 | if case .point = featureCollection.features[3].geometry {} else { XCTFail() } 18 | 19 | let lineStringFeature = featureCollection.features[0] 20 | guard case let .lineString(lineStringCoordinates) = lineStringFeature.geometry else { 21 | XCTFail() 22 | return 23 | } 24 | XCTAssert(lineStringCoordinates.coordinates.count == 19) 25 | if case let .number(number) = lineStringFeature.properties?["id"] { 26 | XCTAssertEqual(number, 1) 27 | } else { 28 | XCTFail() 29 | } 30 | XCTAssert(lineStringCoordinates.coordinates.first!.latitude == -26.17500493262446) 31 | XCTAssert(lineStringCoordinates.coordinates.first!.longitude == 27.977542877197266) 32 | 33 | let polygonFeature = featureCollection.features[1] 34 | guard case let .polygon(polygonCoordinates) = polygonFeature.geometry else { 35 | XCTFail() 36 | return 37 | } 38 | if case let .number(number) = polygonFeature.properties?["id"] { 39 | XCTAssertEqual(number, 2) 40 | } else { 41 | XCTFail() 42 | } 43 | XCTAssert(polygonCoordinates.coordinates[0].count == 21) 44 | XCTAssert(polygonCoordinates.coordinates[0].first!.latitude == -26.199035448897074) 45 | XCTAssert(polygonCoordinates.coordinates[0].first!.longitude == 27.972049713134762) 46 | 47 | let pointFeature = featureCollection.features[3] 48 | guard case let .point(pointCoordinates) = pointFeature.geometry else { 49 | XCTFail() 50 | return 51 | } 52 | if case let .number(number) = pointFeature.properties?["id"] { 53 | XCTAssertEqual(number, 4) 54 | } else { 55 | XCTFail() 56 | } 57 | XCTAssert(pointCoordinates.coordinates.latitude == -26.152510345365126) 58 | XCTAssert(pointCoordinates.coordinates.longitude == 27.95642852783203) 59 | 60 | let encodedData = try! JSONEncoder().encode(geojson) 61 | let decoded = try! JSONDecoder().decode(GeoJSONObject.self, from: encodedData) 62 | guard case let .featureCollection(decodedFeatureCollection) = decoded else { return XCTFail() } 63 | 64 | if case .lineString = decodedFeatureCollection.features[0].geometry {} else { XCTFail() } 65 | if case .polygon = decodedFeatureCollection.features[1].geometry {} else { XCTFail() } 66 | if case .polygon = decodedFeatureCollection.features[2].geometry {} else { XCTFail() } 67 | if case .point = decodedFeatureCollection.features[3].geometry {} else { XCTFail() } 68 | 69 | let decodedLineStringFeature = decodedFeatureCollection.features[0] 70 | guard case let .lineString(decodedLineStringCoordinates) = decodedLineStringFeature.geometry else { 71 | XCTFail() 72 | return 73 | } 74 | XCTAssert(decodedLineStringCoordinates.coordinates.count == 19) 75 | if case let .number(number) = decodedLineStringFeature.properties?["id"] { 76 | XCTAssertEqual(number, 1) 77 | } else { 78 | XCTFail() 79 | } 80 | XCTAssert(decodedLineStringCoordinates.coordinates.first!.latitude == -26.17500493262446) 81 | XCTAssert(decodedLineStringCoordinates.coordinates.first!.longitude == 27.977542877197266) 82 | 83 | let decodedPolygonFeature = decodedFeatureCollection.features[1] 84 | guard case let .polygon(decodedPolygonCoordinates) = decodedPolygonFeature.geometry else { 85 | XCTFail() 86 | return 87 | } 88 | if case let .number(number) = decodedPolygonFeature.properties?["id"] { 89 | XCTAssertEqual(number, 2) 90 | } else { 91 | XCTFail() 92 | } 93 | XCTAssert(decodedPolygonCoordinates.coordinates[0].count == 21) 94 | XCTAssert(decodedPolygonCoordinates.coordinates[0].first!.latitude == -26.199035448897074) 95 | XCTAssert(decodedPolygonCoordinates.coordinates[0].first!.longitude == 27.972049713134762) 96 | 97 | let decodedPointFeature = decodedFeatureCollection.features[3] 98 | guard case let .point(decodedPointCoordinates) = decodedPointFeature.geometry else { 99 | XCTFail() 100 | return 101 | } 102 | if case let .number(number) = decodedPointFeature.properties?["id"] { 103 | XCTAssertEqual(number, 4) 104 | } else { 105 | XCTFail() 106 | } 107 | XCTAssert(decodedPointCoordinates.coordinates.latitude == -26.152510345365126) 108 | XCTAssert(decodedPointCoordinates.coordinates.longitude == 27.95642852783203) 109 | } 110 | 111 | func testFeatureCollectionDecodeWithoutProperties() { 112 | let data = try! Fixture.geojsonData(from: "featurecollection-no-properties")! 113 | let geojson = try! JSONDecoder().decode(GeoJSONObject.self, from: data) 114 | guard case .featureCollection = geojson else { return XCTFail() } 115 | } 116 | 117 | func testUnkownFeatureCollection() { 118 | let data = try! Fixture.geojsonData(from: "featurecollection")! 119 | let geojson = try! JSONDecoder().decode(GeoJSONObject.self, from: data) 120 | guard case .featureCollection = geojson else { return XCTFail() } 121 | } 122 | 123 | func testPerformanceDecodeFeatureCollection() { 124 | let data = try! Fixture.geojsonData(from: "featurecollection")! 125 | 126 | measure { 127 | for _ in 0...100 { 128 | _ = try! JSONDecoder().decode(FeatureCollection.self, from: data) 129 | } 130 | } 131 | } 132 | 133 | func testPerformanceEncodeFeatureCollection() { 134 | let data = try! Fixture.geojsonData(from: "featurecollection")! 135 | let decoded = try! JSONDecoder().decode(FeatureCollection.self, from: data) 136 | 137 | measure { 138 | for _ in 0...100 { 139 | _ = try! JSONEncoder().encode(decoded) 140 | } 141 | } 142 | } 143 | 144 | func testPerformanceDecodeEncodeFeatureCollection() { 145 | let data = try! Fixture.geojsonData(from: "featurecollection")! 146 | 147 | measure { 148 | for _ in 0...100 { 149 | let decoded = try! JSONDecoder().decode(FeatureCollection.self, from: data) 150 | _ = try! JSONEncoder().encode(decoded) 151 | } 152 | } 153 | } 154 | 155 | func testDecodedFeatureCollection() { 156 | let data = try! Fixture.geojsonData(from: "featurecollection")! 157 | let geojson = try! JSONDecoder().decode(GeoJSONObject.self, from: data) 158 | 159 | guard case let .featureCollection(featureCollection) = geojson else { return XCTFail() } 160 | XCTAssertEqual(featureCollection.features.count, 4) 161 | for feature in featureCollection.features { 162 | if case let .number(tolerance) = feature.properties?["tolerance"] { 163 | XCTAssertEqual(tolerance, 0.01) 164 | } else { 165 | XCTFail() 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Turf for Swift 2 | 3 | 📱[![iOS](https://app.bitrise.io/app/49f5bcca71bf6c8d/status.svg?token=SzGBTkEtxsbuAnbcF9MTog&branch=main)](https://www.bitrise.io/app/49f5bcca71bf6c8d)     4 | 🖥💻[![macOS](https://app.bitrise.io/app/b72273651db53613/status.svg?token=ODv2UnyAHoOxV8APATEBFw&branch=main)](https://www.bitrise.io/app/b72273651db53613)     5 | 📺[![tvOS](https://app.bitrise.io/app/0b037542c2395ffb/status.svg?token=yOtMqbu-5bj8grB1Jmoefg)](https://www.bitrise.io/app/0b037542c2395ffb)     6 | ⌚️[![watchOS](https://app.bitrise.io/app/0d4d611f02295183/status.svg?token=NiLB_E_0IvYYqV4Mj973TQ)](https://www.bitrise.io/app/0d4d611f02295183)     7 | Linux[![](https://api.travis-ci.com/mapbox/turf-swift.svg?branch=main)](https://travis-ci.com/mapbox/turf-swift)     8 | [![Documentation](https://mapbox.github.io/turf-swift/4.0.0/badge.svg)](https://mapbox.github.io/turf-swift/)     9 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)     10 | [![CocoaPods](https://img.shields.io/cocoapods/v/Turf.svg)](https://cocoapods.org/pods/Turf/)     11 | [![SPM compatible](https://img.shields.io/badge/SPM-compatible-4BC51D.svg?style=flat)](https://swift.org/package-manager/)     12 | 13 | A [spatial analysis](http://en.wikipedia.org/wiki/Spatial_analysis) library written in Swift for native iOS, macOS, tvOS, watchOS, visionOS, and Linux applications, ported from [Turf.js](https://github.com/Turfjs/turf/). 14 | 15 | ## Requirements 16 | 17 | Turf requires Xcode 14.1 or above and supports the following minimum deployment targets: 18 | 19 | * iOS 11.0 and above 20 | * macOS 10.13 (High Sierra) and above 21 | * tvOS 11.0 and above 22 | * watchOS 4.0 and above 23 | * visionOS 1.0 and above (not supported via CocoaPods) 24 | 25 | Alternatively, you can incorporate Turf into a command line tool without Xcode on any platform that [Swift](https://swift.org/download/) supports, including Linux. 26 | 27 | If your project is written in Objective-C, you’ll need to write a compatibility layer between turf-swift and your Objective-C code. If your project is written in Objective-C++, you may be able to use [spatial-algorithms](https://github.com/mapbox/spatial-algorithms/) as an alternative to Turf. 28 | 29 | ## Installation 30 | 31 | Releases are available for installation using any of the popular Swift dependency managers. 32 | 33 | ### CocoaPods 34 | 35 | To install Turf using [CocoaPods](https://cocoapods.org/): 36 | 37 | 1. Specify the following dependency in your Podfile: 38 | ```rb 39 | pod 'Turf', '~> 4.0' 40 | ``` 41 | 1. Run `pod repo update` if you haven’t lately. 42 | 1. Run `pod install` and open the resulting Xcode workspace. 43 | 1. Add `import Turf` to any Swift file in your application target. 44 | 45 | ### Carthage 46 | 47 | To install Turf using [Carthage](https://github.com/Carthage/Carthage/): 48 | 49 | 1. Add the following dependency to your Cartfile: 50 | ``` 51 | github "mapbox/turf-swift" ~> 4.0 52 | ``` 53 | 1. Run `carthage bootstrap`. 54 | 1. Follow the rest of [Carthage’s integration instructions](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application). Your application target’s Embedded Frameworks should include Turf.framework. 55 | 1. Add `import Turf` to any Swift file in your application target. 56 | 57 | ### Swift Package Manager 58 | 59 | To install Turf using the [Swift Package Manager](https://swift.org/package-manager/), add the following package to the `dependencies` in your Package.swift file: 60 | 61 | ```swift 62 | .package(url: "https://github.com/mapbox/turf-swift.git", from: "4.0.0") 63 | ``` 64 | 65 | Then `import Turf` in any Swift file in your module. 66 | 67 | 68 | ## Available functionality 69 | 70 | This work-in-progress port of [Turf.js](https://github.com/Turfjs/turf/) contains the following functionality: 71 | 72 | Turf.js | Turf for Swift 73 | ----|---- 74 | [turf-along#along](https://turfjs.org/docs/#along) | `LineString.coordinateFromStart(distance:)` 75 | [turf-area#area](https://turfjs.org/docs/#area) | `Polygon.area` 76 | [turf-bearing#bearing](https://turfjs.org/docs/#bearing) | `CLLocationCoordinate2D.direction(to:)`
`LocationCoordinate2D.direction(to:)` on Linux
`RadianCoordinate2D.direction(to:)` 77 | [turf-bezier-spline#bezierSpline](https://turfjs.org/docs/#bezierSpline) | `LineString.bezier(resolution:sharpness:)` 78 | [turf-boolean-point-in-polygon#booleanPointInPolygon](https://turfjs.org/docs/#booleanPointInPolygon) | `Polygon.contains(_:ignoreBoundary:)` 79 | [turf-center#center](https://turfjs.org/docs/#center) | `Polygon.center` 80 | [turf-center-of-mass#centerOfMass](https://turfjs.org/docs/#centerOfMass) | `Polygon.centerOfMass` 81 | [turf-centroid#centroid](https://turfjs.org/docs/#centroid) | `Polygon.centroid` 82 | [turf-circle#circle](https://turfjs.org/docs/#circle) | `Polygon(center:radius:vertices:)` | 83 | [turf-destination#destination](https://turfjs.org/docs/#destination) | `CLLocationCoordinate2D.coordinate(at:facing:)`
`LocationCoordinate2D.coordinate(at:facing:)` on Linux
`RadianCoordinate2D.coordinate(at:facing:)` 84 | [turf-distance#distance](https://turfjs.org/docs/#distance) | `CLLocationCoordinate2D.distance(to:)`
`LocationCoordinate2D.distance(to:)` on Linux
`RadianCoordinate2D.distance(to:)` 85 | [turf-helpers#polygon](https://turfjs.org/docs/#polygon) | `Polygon(_:)` 86 | [turf-helpers#lineString](https://turfjs.org/docs/#lineString) | `LineString(_:)` 87 | [turf-helpers#degreesToRadians](https://turfjs.org/docs/#degreesToRadians) | `CLLocationDegrees.toRadians()`
`LocationDegrees.toRadians()` on Linux 88 | [turf-helpers#radiansToDegrees](https://turfjs.org/docs/#radiansToDegrees) | `CLLocationDegrees.toDegrees()`
`LocationDegrees.toDegrees()` on Linux 89 | [turf-helpers#convertLength](https://turfjs.org/docs/#convertLength)
[turf-helpers#convertArea](https://turfjs.org/docs/#convertArea) | `Measurement.converted(to:)` 90 | [turf-length#length](https://turfjs.org/docs/#length) | `LineString.distance(from:to:)` 91 | [turf-line-intersect#lineIntersect](https://turfjs.org/docs/#lineIntersect) | `LineString.intersections(with:)` 92 | [turf-line-slice#lineSlice](https://turfjs.org/docs/#lineSlice) | `LineString.sliced(from:to:)` 93 | [turf-line-slice-along#lineSliceAlong](https://turfjs.org/docs/#lineSliceAlong) | `LineString.trimmed(from:to:)` 94 | [turf-midpoint#midpoint](https://turfjs.org/docs/#midpoint) | `mid(_:_:)` 95 | [turf-nearest-point-on-line#nearestPointOnLine](https://turfjs.org/docs/#nearestPointOnLine) | `LineString.closestCoordinate(to:)` 96 | [turf-polygon-to-line#polygonToLine](https://turfjs.org/docs/#polygonToLine) | `LineString(_:)`
`MultiLineString(_:)` 97 | [turf-simplify#simplify](https://turfjs.org/docs/#simplify) | `LineString.simplify(tolerance:highestQuality:)`
`LineString.simplified(tolerance:highestQuality:)` 98 | [turf-polygon-smooth#polygonSmooth](https://turfjs.org/docs/#polygonSmooth) | `Polygon.smooth(iterations:)` 99 | — | `CLLocationDirection.difference(from:)`
`LocationDirection.difference(from:)` on Linux 100 | — | `CLLocationDirection.wrap(min:max:)`
`LocationDirection.wrap(min:max:)` on Linux 101 | 102 | ## GeoJSON 103 | 104 | turf-swift also contains a GeoJSON encoder/decoder with support for Codable. 105 | 106 | ```swift 107 | // Decode an unknown GeoJSON object. 108 | let geojson = try JSONDecoder().decode(GeoJSONObject.self, from: data) 109 | guard case let .feature(feature) = geojson, 110 | case let .point(point) = feature.geometry else { 111 | return 112 | } 113 | 114 | // Decode a known GeoJSON object. 115 | let featureCollection = try JSONDecoder().decode(FeatureCollection.self, from: data) 116 | 117 | // Initialize a Point feature and encode it as GeoJSON. 118 | let coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 1) 119 | let point = Point(coordinate) 120 | let pointFeature = Feature(geometry: .point(point)) 121 | let data = try JSONEncoder().encode(pointFeature) 122 | let json = String(data: data, encoding: .utf8) 123 | print(json) 124 | 125 | /* 126 | { 127 | "type": "Feature", 128 | "geometry": { 129 | "type": "Point", 130 | "coordinates": [ 131 | 1, 132 | 0 133 | ] 134 | } 135 | } 136 | */ 137 | 138 | ``` 139 | 140 | ## Well Known Text (WKT) 141 | 142 | turf-swift contains a minimal WKT encoding/decoding support for geometries implementing `WKTCodable` protocol. 143 | 144 | ```swift 145 | let wktString = "POINT(123.53 -12.12)" 146 | 147 | // Decoding is done using an init method 148 | let point = try? Point(wkt: wktString) 149 | let geometry = try? Geometry(wkt: wktString) 150 | 151 | print(point?.coordinates) 152 | 153 | // ... 154 | 155 | // Geometries then can be serialized using a property getter 156 | let serializedWKTString = point?.wkt 157 | print(serializedWKTString) 158 | 159 | ``` 160 | -------------------------------------------------------------------------------- /Tests/TurfTests/Fixtures/simplify/in/polygon.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": { 4 | "tolerance": 1, 5 | "elevation": 25 6 | }, 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [-75.51527, 39.11245], 12 | [-75.39282224024916, 39.12823474024917], 13 | [-75.3770375, 39.135447305104925], 14 | [-75.35235519043113, 39.13713230956888], 15 | [-75.34721355956887, 39.13713230956888], 16 | [-75.32253125, 39.139083755473514], 17 | [-75.29670096335326, 39.141125963353275], 18 | [-75.29326540004632, 39.14171584995367], 19 | [-75.268025, 39.14408623760705], 20 | [-75.25277832507136, 39.15170957507138], 21 | [-75.24515498760704, 39.166956250000005], 22 | [-75.23985410433178, 39.1932916043318], 23 | [-75.23985410433178, 39.19512714566821], 24 | [-75.23607469121049, 39.2214625], 25 | [-75.23324391324394, 39.24118766324395], 26 | [-75.23324391324394, 39.25624358675606], 27 | [-75.23104443417348, 39.27596875], 28 | [-75.22621125895654, 39.28866125895655], 29 | [-75.21351874999999, 39.30405720540773], 30 | [-75.19590688693847, 39.31286313693849], 31 | [-75.18710095540773, 39.33047500000001], 32 | [-75.18476066926435, 39.35622316926436], 33 | [-75.18476066926435, 39.359233080735656], 34 | [-75.1827803677246, 39.38498125], 35 | [-75.18108292305178, 39.407051673051775], 36 | [-75.18108292305178, 39.41741707694823], 37 | [-75.17961177196392, 39.439487500000006], 38 | [-75.17842510611122, 39.4589001061112], 39 | [-75.17842510611122, 39.474581143888805], 40 | [-75.1773677140917, 39.49399375], 41 | [-75.17806096906195, 39.513042219061944], 42 | [-75.17806096906195, 39.52945153093805], 43 | [-75.17880864635877, 39.548500000000004], 44 | [-75.17436569692443, 39.56385319692442], 45 | [-75.1590125, 39.57861883514683], 46 | [-75.14275422343123, 39.586747973431216], 47 | [-75.13462508514682, 39.60300625000001], 48 | [-75.13199158752832, 39.630027162471684], 49 | [-75.13192154096123, 39.63042154096124], 50 | [-75.12971192190646, 39.6575125], 51 | [-75.12958254641065, 39.68258879641065], 52 | [-75.12958254641065, 39.68694245358936], 53 | [-75.12945449224796, 39.712018750000006], 54 | [-75.13075282512311, 39.73826532512311], 55 | [-75.13075282512311, 39.74027842487689], 56 | [-75.13219370977114, 39.766525], 57 | [-75.13492951020307, 39.79060798979694], 58 | [-75.13711471606355, 39.799133466063545], 59 | [-75.14033360353974, 39.821031250000004], 60 | [-75.1408737447648, 39.839170005235225], 61 | [-75.14206588619443, 39.85859088619443], 62 | [-75.142602, 39.875538], 63 | [-75.14807249033687, 39.88647750966314], 64 | [-75.1590125, 39.8919475144947], 65 | [-75.17435420501783, 39.89087920501784], 66 | [-75.20237093707816, 39.886685312921855], 67 | [-75.21351874999999, 39.88578343964871], 68 | [-75.22299781995783, 39.88501656995784], 69 | [-75.26614108689465, 39.87742141310536], 70 | [-75.268025, 39.877239581040996], 71 | [-75.26957725960418, 39.87708975960419], 72 | [-75.27684240189953, 39.87553750000001], 73 | [-75.28419540106952, 39.85936709893049], 74 | [-75.29112392832597, 39.84413017832599], 75 | [-75.30162746500874, 39.821031250000004], 76 | [-75.30816161875036, 39.806661618750354], 77 | [-75.32253125, 39.77506054072003], 78 | [-75.32519930917472, 39.76919305917473], 79 | [-75.32641252811797, 39.766525], 80 | [-75.3296491997708, 39.75940705022919], 81 | [-75.34223699959911, 39.73172449959911], 82 | [-75.35119759122719, 39.712018750000006], 83 | [-75.36046052535204, 39.69544177535203], 84 | [-75.3754130206531, 39.659136979346904], 85 | [-75.37616437518106, 39.6575125], 86 | [-75.3766762492428, 39.65715124924281], 87 | [-75.3770375, 39.65615200822954], 88 | [-75.39832547007784, 39.62429422007785], 89 | [-75.4082395514723, 39.60300625000001], 90 | [-75.4215797545908, 39.5930422545908], 91 | [-75.43154375, 39.56834216367474], 92 | [-75.45835566060614, 39.57531191060614], 93 | [-75.45895426960922, 39.5755957303908], 94 | [-75.48605, 39.57947734440668], 95 | [-75.48898805047145, 39.60006819952855], 96 | [-75.48994051537133, 39.60300625], 97 | [-75.49123821601205, 39.60819446601205], 98 | [-75.49220788576012, 39.651354614239885], 99 | [-75.49509104207014, 39.6575125], 100 | [-75.50305100880664, 39.67451350880663], 101 | [-75.50416029911807, 39.69390845088193], 102 | [-75.52933401544658, 39.70079651544659], 103 | [-75.54055625, 39.707797525000004], 104 | [-75.5417414003631, 39.71083359963691], 105 | [-75.54210361986804, 39.71201875000001], 106 | [-75.5424789600527, 39.71394146005272], 107 | [-75.5438968233805, 39.76318442661949], 108 | [-75.54488896795854, 39.766525], 109 | [-75.54664835702378, 39.77261710702379], 110 | [-75.54776976280363, 39.81381773719638], 111 | [-75.55227698676161, 39.821031250000004], 112 | [-75.56289470095811, 39.84336970095812], 113 | [-75.56358754189749, 39.852506208102504], 114 | [-75.57539756961809, 39.8558725696181], 115 | [-75.59506249999998, 39.868174228685255], 116 | [-75.59931300492002, 39.871286995079984], 117 | [-75.60432992735852, 39.87553750000001], 118 | [-75.63504292712292, 39.89006332287709], 119 | [-75.64956874999999, 39.89284139354837], 120 | [-75.66838020678718, 39.89434895678719], 121 | [-75.68308195946902, 39.896530540530996], 122 | [-75.704075, 39.89798053092993], 123 | [-75.72818318448331, 39.89964568448332], 124 | [-75.73375331940231, 39.9003654305977], 125 | [-75.75858125, 39.901872157624666], 126 | [-75.78514101585267, 39.90348398414733], 127 | [-75.78652773414733, 39.90348398414733], 128 | [-75.813087, 39.904921], 129 | [-75.83722610887358, 39.905905141126446], 130 | [-75.84345514112644, 39.905905141126446], 131 | [-75.86759375, 39.906814783587755], 132 | [-75.88844527239183, 39.89638902239184], 133 | [-75.89887103358774, 39.87553750000001], 134 | [-75.89854841478977, 39.85198591478977], 135 | [-75.89854841478977, 39.84458283521024], 136 | [-75.89821670830216, 39.821031250000004], 137 | [-75.89787552465965, 39.79680677465965], 138 | [-75.89787552465965, 39.79074947534036], 139 | [-75.89752445181962, 39.766525], 140 | [-75.89716305350223, 39.74158805350222], 141 | [-75.89716305350223, 39.73695569649778], 142 | [-75.89679086738143, 39.712018750000006], 143 | [-75.89640740311208, 39.686326153112084], 144 | [-75.89640740311208, 39.683205096887924], 145 | [-75.89601214017446, 39.6575125], 146 | [-75.89560452551711, 39.63101702551712], 147 | [-75.89560452551711, 39.6295017244829], 148 | [-75.8951839709748, 39.60300625000001], 149 | [-75.89475288239396, 39.57584711760604], 150 | [-75.8947466163539, 39.57565286635391], 151 | [-75.89430149673764, 39.548500000000004], 152 | [-75.89387073552223, 39.52222301447779], 153 | [-75.89380341704393, 39.520203417043916], 154 | [-75.8933591950366, 39.49399375], 155 | [-75.89292978011362, 39.468657719886366], 156 | [-75.89279284789451, 39.46468659789453], 157 | [-75.89235076838531, 39.439487500000006], 158 | [-75.8913260472361, 39.4157552027639], 159 | [-75.89062746202809, 39.408014962028105], 160 | [-75.88953890003162, 39.38498125], 161 | [-75.88715925199065, 39.365415748009355], 162 | [-75.88127062628152, 39.34415187628153], 163 | [-75.87907248368842, 39.33047500000001], 164 | [-75.87748307496805, 39.32058567503195], 165 | [-75.86759375, 39.29476457315633], 166 | [-75.85538716547042, 39.288175334529576], 167 | [-75.8282870350059, 39.275968750000004], 168 | [-75.8173286270748, 39.27172762292521], 169 | [-75.8130875, 39.25578472660005], 170 | [-75.79794874486377, 39.236601255136236], 171 | [-75.79760439720079, 39.2214625], 172 | [-75.7676722437337, 39.2123715062663], 173 | [-75.75858125, 39.21117295929072], 174 | [-75.74836987958975, 39.21125112958973], 175 | [-75.7137522708571, 39.21178522914291], 176 | [-75.704075, 39.21186046264999], 177 | [-75.67996146438593, 39.19734896438593], 178 | [-75.66694581884633, 39.20408543115368], 179 | [-75.67566107970288, 39.193048579702875], 180 | [-75.67054949218115, 39.166956250000005], 181 | [-75.6635559114541, 39.15296908854591], 182 | [-75.64956874999999, 39.145975507818854], 183 | [-75.62914609464218, 39.14653359464219], 184 | [-75.61467186011258, 39.14734688988742], 185 | [-75.5950625, 39.14791373192817], 186 | [-75.57631446045869, 39.148208210458705], 187 | [-75.5587435012753, 39.148768998724705], 188 | [-75.54055625, 39.14906393405724], 189 | [-75.52104203854152, 39.13196421145847], 190 | [-75.51527, 39.11245] 191 | ] 192 | ] 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /Sources/Turf/CoreLocation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if canImport(CoreLocation) 3 | import CoreLocation 4 | #endif 5 | 6 | #if canImport(CoreLocation) 7 | /** 8 | An azimuth measured in degrees clockwise from true north. 9 | 10 | This is a compatibility shim to keep the library’s public interface consistent between Apple and non-Apple platforms that lack Core Location. On Apple platforms, you can use `CLLocationDirection` anywhere you see this type. 11 | */ 12 | public typealias LocationDirection = CLLocationDirection 13 | 14 | /** 15 | A distance in meters. 16 | 17 | This is a compatibility shim to keep the library’s public interface consistent between Apple and non-Apple platforms that lack Core Location. On Apple platforms, you can use `CLLocationDistance` anywhere you see this type. 18 | */ 19 | public typealias LocationDistance = CLLocationDistance 20 | 21 | /** 22 | A latitude or longitude in degrees. 23 | 24 | This is a compatibility shim to keep the library’s public interface consistent between Apple and non-Apple platforms that lack Core Location. On Apple platforms, you can use `CLLocationDegrees` anywhere you see this type. 25 | */ 26 | public typealias LocationDegrees = CLLocationDegrees 27 | 28 | /** 29 | A geographic coordinate. 30 | 31 | This is a compatibility shim to keep the library’s public interface consistent between Apple and non-Apple platforms that lack Core Location. On Apple platforms, you can use `CLLocationCoordinate2D` anywhere you see this type. 32 | */ 33 | public typealias LocationCoordinate2D = CLLocationCoordinate2D 34 | #else 35 | /** 36 | An azimuth measured in degrees clockwise from true north. 37 | */ 38 | public typealias LocationDirection = Double 39 | 40 | /** 41 | A distance in meters. 42 | */ 43 | public typealias LocationDistance = Double 44 | 45 | /** 46 | A latitude or longitude in degrees. 47 | */ 48 | public typealias LocationDegrees = Double 49 | 50 | /** 51 | A geographic coordinate with its components measured in degrees. 52 | */ 53 | public struct LocationCoordinate2D: Sendable { 54 | /** 55 | The latitude in degrees. 56 | */ 57 | public var latitude: LocationDegrees 58 | 59 | /** 60 | The longitude in degrees. 61 | */ 62 | public var longitude: LocationDegrees 63 | 64 | /** 65 | Creates a degree-based geographic coordinate. 66 | */ 67 | public init(latitude: LocationDegrees, longitude: LocationDegrees) { 68 | self.latitude = latitude 69 | self.longitude = longitude 70 | } 71 | } 72 | #endif 73 | 74 | extension LocationCoordinate2D { 75 | /** 76 | Returns a normalized coordinate, wrapped to -180 and 180 degrees latitude 77 | */ 78 | var normalized: LocationCoordinate2D { 79 | return .init( 80 | latitude: latitude, 81 | longitude: longitude.wrap(min: -180, max: 180) 82 | ) 83 | } 84 | } 85 | 86 | extension LocationDirection { 87 | /** 88 | Returns a normalized number given min and max bounds. 89 | */ 90 | public func wrap(min minimumValue: LocationDirection, max maximumValue: LocationDirection) -> LocationDirection { 91 | let d = maximumValue - minimumValue 92 | return fmod((fmod((self - minimumValue), d) + d), d) + minimumValue 93 | } 94 | 95 | /** 96 | Returns the smaller difference between the receiver and another direction. 97 | 98 | To obtain the larger difference between the two directions, subtract the 99 | return value from 360°. 100 | */ 101 | public func difference(from beta: LocationDirection) -> LocationDirection { 102 | let phi = abs(beta - self).truncatingRemainder(dividingBy: 360) 103 | return phi > 180 ? 360 - phi : phi 104 | } 105 | } 106 | 107 | extension LocationDegrees { 108 | /** 109 | Returns the direction in radians. 110 | 111 | This method is equivalent to the [`degreesToRadians`](https://turfjs.org/docs/#degreesToRadians) method of the turf-helpers package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers/)). 112 | */ 113 | public func toRadians() -> LocationRadians { 114 | return self * .pi / 180.0 115 | } 116 | 117 | /** 118 | Returns the direction in degrees. 119 | 120 | This method is equivalent to the [`radiansToDegrees`](https://turfjs.org/docs/#radiansToDegrees) method of the turf-helpers package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers/)). 121 | */ 122 | public func toDegrees() -> LocationDirection { 123 | return self * 180.0 / .pi 124 | } 125 | } 126 | 127 | struct LocationCoordinate2DCodable: Codable { 128 | var latitude: LocationDegrees 129 | var longitude: LocationDegrees 130 | var decodedCoordinates: LocationCoordinate2D { 131 | return LocationCoordinate2D(latitude: latitude, longitude: longitude) 132 | } 133 | 134 | func encode(to encoder: Encoder) throws { 135 | var container = encoder.unkeyedContainer() 136 | try container.encode(longitude) 137 | try container.encode(latitude) 138 | } 139 | 140 | init(from decoder: Decoder) throws { 141 | var container = try decoder.unkeyedContainer() 142 | longitude = try container.decode(LocationDegrees.self) 143 | latitude = try container.decode(LocationDegrees.self) 144 | } 145 | 146 | init(_ coordinate: LocationCoordinate2D) { 147 | latitude = coordinate.latitude 148 | longitude = coordinate.longitude 149 | } 150 | } 151 | 152 | extension LocationCoordinate2D { 153 | var codableCoordinates: LocationCoordinate2DCodable { 154 | return LocationCoordinate2DCodable(self) 155 | } 156 | } 157 | 158 | extension Array where Element == LocationCoordinate2DCodable { 159 | var decodedCoordinates: [LocationCoordinate2D] { 160 | return map { $0.decodedCoordinates } 161 | } 162 | } 163 | 164 | extension Array where Element == [LocationCoordinate2DCodable] { 165 | var decodedCoordinates: [[LocationCoordinate2D]] { 166 | return map { $0.decodedCoordinates } 167 | } 168 | } 169 | 170 | extension Array where Element == [[LocationCoordinate2DCodable]] { 171 | var decodedCoordinates: [[[LocationCoordinate2D]]] { 172 | return map { $0.decodedCoordinates } 173 | } 174 | } 175 | 176 | extension Array where Element == LocationCoordinate2D { 177 | var codableCoordinates: [LocationCoordinate2DCodable] { 178 | return map { $0.codableCoordinates } 179 | } 180 | } 181 | 182 | extension Array where Element == [LocationCoordinate2D] { 183 | var codableCoordinates: [[LocationCoordinate2DCodable]] { 184 | return map { $0.codableCoordinates } 185 | } 186 | } 187 | 188 | extension Array where Element == [[LocationCoordinate2D]] { 189 | var codableCoordinates: [[[LocationCoordinate2DCodable]]] { 190 | return map { $0.codableCoordinates } 191 | } 192 | } 193 | 194 | extension LocationCoordinate2D: Equatable { 195 | 196 | /// Instantiates a LocationCoordinate2D from a RadianCoordinate2D 197 | public init(_ radianCoordinate: RadianCoordinate2D) { 198 | self.init(latitude: radianCoordinate.latitude.toDegrees(), longitude: radianCoordinate.longitude.toDegrees()) 199 | } 200 | 201 | public static func ==(lhs: LocationCoordinate2D, rhs: LocationCoordinate2D) -> Bool { 202 | return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude 203 | } 204 | 205 | /** 206 | Returns the direction from the receiver to the given coordinate. 207 | 208 | This method is equivalent to the [turf-bearing](https://turfjs.org/docs/#bearing) package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-bearing/)). 209 | */ 210 | public func direction(to coordinate: LocationCoordinate2D) -> LocationDirection { 211 | return RadianCoordinate2D(self).direction(to: RadianCoordinate2D(coordinate)).converted(to: .degrees).value 212 | } 213 | 214 | /// Returns a coordinate a certain Haversine distance away in the given direction. 215 | public func coordinate(at distance: LocationDistance, facing direction: LocationDirection) -> LocationCoordinate2D { 216 | let angle = Measurement(value: direction, unit: UnitAngle.degrees) 217 | return coordinate(at: distance, facing: angle) 218 | } 219 | 220 | /** 221 | Returns a coordinate a certain Haversine distance away in the given direction. 222 | 223 | This method is equivalent to the [turf-destination](https://turfjs.org/docs/#destination) package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-destination/)). 224 | */ 225 | public func coordinate(at distance: LocationDistance, facing direction: Measurement) -> LocationCoordinate2D { 226 | let radianCoordinate = RadianCoordinate2D(self).coordinate(at: distance / metersPerRadian, facing: direction) 227 | return LocationCoordinate2D(radianCoordinate) 228 | } 229 | 230 | /** 231 | Returns the Haversine distance between two coordinates measured in degrees. 232 | 233 | This method is equivalent to the [turf-distance](https://turfjs.org/docs/#distance) package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-distance/)). 234 | */ 235 | public func distance(to coordinate: LocationCoordinate2D) -> LocationDistance { 236 | return RadianCoordinate2D(self).distance(to: RadianCoordinate2D(coordinate)) * metersPerRadian 237 | } 238 | } 239 | 240 | -------------------------------------------------------------------------------- /Sources/Turf/JSON.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A JSON value represents an object, array, or fragment. 5 | 6 | This type does not represent the `null` value in JSON. Use `Optional` wherever `null` is accepted. 7 | */ 8 | public enum JSONValue: Hashable, Sendable { 9 | // case null would be redundant to Optional.none 10 | 11 | /// A string. 12 | case string(_ string: String) 13 | 14 | /** 15 | A floating-point number. 16 | 17 | JSON does not distinguish numeric types of different precisions. If you need integer precision, cast the value to an `Int`. 18 | */ 19 | case number(_ number: Double) 20 | 21 | /// A Boolean value. 22 | case boolean(_ bool: Bool) 23 | 24 | /// A heterogeneous array of JSON values and `null` values. 25 | case array(_ values: JSONArray) 26 | 27 | /// An object containing JSON values and `null` values keyed by strings. 28 | case object(_ properties: JSONObject) 29 | 30 | /// Initializes a JSON value representing the given string. 31 | public init(_ string: String) { 32 | self = .string(string) 33 | } 34 | 35 | /** 36 | Initializes a JSON value representing the given integer. 37 | 38 | - parameter number: An integer. JSON does not distinguish numeric types of different precisions, so the integer is stored as a floating-point number. 39 | */ 40 | public init(_ number: Source) where Source: BinaryInteger { 41 | self = .number(Double(number)) 42 | } 43 | 44 | /// Initializes a JSON value representing the given floating-point number. 45 | public init(_ number: Source) where Source: BinaryFloatingPoint { 46 | self = .number(Double(number)) 47 | } 48 | 49 | /// Initializes a JSON value representing the given Boolean value. 50 | public init(_ bool: Bool) { 51 | self = .boolean(bool) 52 | } 53 | 54 | /// Initializes a JSON value representing the given JSON array. 55 | public init(_ values: JSONArray) { 56 | self = .array(values) 57 | } 58 | 59 | /// Initializes a JSON value representing the given JSON object. 60 | public init(_ properties: JSONObject) { 61 | self = .object(properties) 62 | } 63 | } 64 | 65 | extension JSONValue { 66 | /// A string value, if the JSON value represents a string. 67 | public var string: String? { 68 | if case let .string(value) = self { 69 | return value 70 | } 71 | return nil 72 | } 73 | 74 | /// A floating-point number value, if the JSON value represents a number. 75 | public var number: Double? { 76 | if case let .number(value) = self { 77 | return value 78 | } 79 | return nil 80 | } 81 | 82 | /// A Boolean value, if the JSON value represents a Boolean. 83 | public var boolean: Bool? { 84 | if case let .boolean(value) = self { 85 | return value 86 | } 87 | return nil 88 | } 89 | 90 | /// An array of JSON values, if the JSON value represents an array. 91 | public var array: JSONArray? { 92 | if case let .array(value) = self { 93 | return value 94 | } 95 | return nil 96 | } 97 | 98 | /// An object containing JSON values keyed by strings, if the JSON value represents an object. 99 | public var object: JSONObject? { 100 | if case let .object(value) = self { 101 | return value 102 | } 103 | return nil 104 | } 105 | } 106 | 107 | extension JSONValue: RawRepresentable { 108 | public typealias RawValue = Any 109 | 110 | public init?(rawValue: Any) { 111 | // Like `JSONSerialization.jsonObject(with:options:)` with `JSONSerialization.ReadingOptions.fragmentsAllowed` specified. 112 | if let string = rawValue as? String { 113 | self = .string(string) 114 | } else if let number = rawValue as? NSNumber { 115 | /// When a Swift Bool or Objective-C BOOL is boxed with NSNumber, the value of the 116 | /// resulting NSNumber's objCType property is 'c' (Int8 (aka CChar) in Swift, char in 117 | /// Objective-C) and the value is 0 for false/NO and 1 for true/YES. 118 | /// 119 | /// Strictly speaking, an NSNumber with those characteristics can be created by boxing 120 | /// other non-boolean values (e.g. boxing 0 or 1 using the `init(value: CChar)` 121 | /// initializer). Moreover, NSNumber doesn't guarantee to preserve the type suggested 122 | /// by the initializer that's used to create it. 123 | /// 124 | /// This means that when these values are encountered, it is ambiguous whether to 125 | /// decode to JSONValue.number or JSONValue.boolean. 126 | /// 127 | /// In practice, choosing .boolean yields the desired result more often since it is more 128 | /// common to work with Bool than it is Int8. 129 | switch String(cString: number.objCType) { 130 | case "c": // char 131 | if number.int8Value == 0 { 132 | self = .boolean(false) 133 | } else if number.int8Value == 1 { 134 | self = .boolean(true) 135 | } else { 136 | self = .number(number.doubleValue) 137 | } 138 | default: 139 | self = .number(number.doubleValue) 140 | } 141 | } else if let boolean = rawValue as? Bool { 142 | /// This branch must happen after the `NSNumber` branch 143 | /// to avoid converting `NSNumber` instances with values 144 | /// 0 and 1 but of objCType != 'c' to `Bool` since `as? Bool` 145 | /// can succeed when the NSNumber's value is 0 or 1 even 146 | /// when its objCType is not 'c'. 147 | self = .boolean(boolean) 148 | } else if let rawArray = rawValue as? JSONArray.TurfRawValue, 149 | let array = JSONArray(turfRawValue: rawArray) { 150 | self = .array(array) 151 | } else if let rawObject = rawValue as? JSONObject.TurfRawValue, 152 | let object = JSONObject(turfRawValue: rawObject) { 153 | self = .object(object) 154 | } else { 155 | return nil 156 | } 157 | } 158 | 159 | public var rawValue: Any { 160 | switch self { 161 | case let .boolean(value): 162 | return value 163 | case let .string(value): 164 | return value 165 | case let .number(value): 166 | return value 167 | case let .object(value): 168 | return value.turfRawValue 169 | case let .array(value): 170 | return value.turfRawValue 171 | } 172 | } 173 | } 174 | 175 | /** 176 | A JSON array of `JSONValue` instances. 177 | */ 178 | public typealias JSONArray = [JSONValue?] 179 | 180 | extension JSONArray { 181 | public typealias TurfRawValue = [Any?] 182 | 183 | public init?(turfRawValue values: TurfRawValue) { 184 | self = values.map(JSONValue.init(rawValue:)) 185 | } 186 | 187 | public var turfRawValue: TurfRawValue { 188 | return map { $0?.rawValue } 189 | } 190 | } 191 | 192 | /** 193 | A JSON object represented in memory by a dictionary with strings as keys and `JSONValue` instances as values. 194 | */ 195 | public typealias JSONObject = [String: JSONValue?] 196 | 197 | extension JSONObject { 198 | public typealias TurfRawValue = [String: Any?] 199 | 200 | public init?(turfRawValue: TurfRawValue) { 201 | self = turfRawValue.mapValues { $0.flatMap(JSONValue.init(rawValue:)) } 202 | } 203 | 204 | public var turfRawValue: TurfRawValue { 205 | return mapValues { $0?.rawValue } 206 | } 207 | } 208 | 209 | extension JSONValue: ExpressibleByStringLiteral { 210 | public init(stringLiteral value: StringLiteralType) { 211 | self = .init(value) 212 | } 213 | } 214 | 215 | extension JSONValue: ExpressibleByIntegerLiteral { 216 | public init(integerLiteral value: IntegerLiteralType) { 217 | self = .init(value) 218 | } 219 | } 220 | 221 | extension JSONValue: ExpressibleByFloatLiteral { 222 | public init(floatLiteral value: FloatLiteralType) { 223 | self = .init(value) 224 | } 225 | } 226 | 227 | extension JSONValue: ExpressibleByBooleanLiteral { 228 | public init(booleanLiteral value: BooleanLiteralType) { 229 | self = .init(value) 230 | } 231 | } 232 | 233 | extension JSONValue: ExpressibleByArrayLiteral { 234 | public typealias ArrayLiteralElement = JSONValue? 235 | 236 | public init(arrayLiteral elements: ArrayLiteralElement...) { 237 | self = .init(elements) 238 | } 239 | } 240 | 241 | extension JSONValue: ExpressibleByDictionaryLiteral { 242 | public typealias Key = String 243 | public typealias Value = JSONValue? 244 | 245 | public init(dictionaryLiteral elements: (Key, Value)...) { 246 | self = .init(.init(uniqueKeysWithValues: elements)) 247 | } 248 | } 249 | 250 | extension JSONValue: Codable { 251 | public init(from decoder: Decoder) throws { 252 | let container = try decoder.singleValueContainer() 253 | if let boolean = try? container.decode(Bool.self) { 254 | self = .boolean(boolean) 255 | } else if let number = try? container.decode(Double.self) { 256 | self = .number(number) 257 | } else if let string = try? container.decode(String.self) { 258 | self = .string(string) 259 | } else if let object = try? container.decode(JSONObject.self) { 260 | self = .object(object) 261 | } else if let array = try? container.decode(JSONArray.self) { 262 | self = .array(array) 263 | } else { 264 | throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unable to decode as a JSONValue.")) 265 | } 266 | } 267 | 268 | public func encode(to encoder: Encoder) throws { 269 | var container = encoder.singleValueContainer() 270 | switch self { 271 | case let .boolean(value): 272 | try container.encode(value) 273 | case let .string(value): 274 | try container.encode(value) 275 | case let .number(value): 276 | try container.encode(value) 277 | case let .object(value): 278 | try container.encode(value) 279 | case let .array(value): 280 | try container.encode(value) 281 | } 282 | } 283 | } 284 | --------------------------------------------------------------------------------