├── 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 | 
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 | 
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 | 📱[](https://www.bitrise.io/app/49f5bcca71bf6c8d)
4 | 🖥💻[](https://www.bitrise.io/app/b72273651db53613)
5 | 📺[](https://www.bitrise.io/app/0b037542c2395ffb)
6 | ⌚️[](https://www.bitrise.io/app/0d4d611f02295183)
7 |
[](https://travis-ci.com/mapbox/turf-swift)
8 | [](https://mapbox.github.io/turf-swift/)
9 | [](https://github.com/Carthage/Carthage)
10 | [](https://cocoapods.org/pods/Turf/)
11 | [](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 |
--------------------------------------------------------------------------------