From f46b02eeb196c4b23e96bc3f1a303266010e96ef Mon Sep 17 00:00:00 2001 From: Victor Kononov Date: Thu, 11 Nov 2021 11:53:47 +0300 Subject: [PATCH] Isochrone api (#621) * vk-296-isochrone-api: added Isochrone class with related entities. Unit tests added. * vk-296-isochrone-api: CHANGELOG updated * vk-296-isochrone-api: replace IsochroneCredentials definition to be a typealias; * vk-296-isochrone-api: added README example; * vk-296-isochrone-api: deprecated DirectionsCredentials and DirectionsProfileIdentifier to reuse with Isohrones. Tests updated * vk-296-isochrone-api: moved DirectionsCredentials and DirectionsProfileIdentifier alieases to original implementations; --- CHANGELOG.md | 2 + MapboxDirections.xcodeproj/project.pbxproj | 86 +++-- README.md | 67 +++- .../MapboxDirections/AttributeOptions.swift | 4 +- ...onsCredentials.swift => Credentials.swift} | 4 +- Sources/MapboxDirections/Directions.swift | 16 +- .../MapboxDirections/DirectionsError.swift | 4 +- .../MapboxDirections/DirectionsOptions.swift | 14 +- .../MapboxDirections/DirectionsResult.swift | 4 +- Sources/MapboxDirections/IsochroneError.swift | 75 +++++ .../MapboxDirections/IsochroneOptions.swift | 301 ++++++++++++++++++ Sources/MapboxDirections/Isochrones.swift | 170 ++++++++++ .../MapMatching/MapMatchingResponse.swift | 6 +- .../MapMatching/MatchOptions.swift | 14 +- ...entifier.swift => ProfileIdentifier.swift} | 29 +- Sources/MapboxDirections/QuickLook.swift | 2 +- Sources/MapboxDirections/RouteLeg.swift | 10 +- Sources/MapboxDirections/RouteOptions.swift | 28 +- .../RouteRefreshResponse.swift | 4 +- Sources/MapboxDirections/RouteResponse.swift | 8 +- Sources/MapboxDirections/RouteStep.swift | 16 +- .../MapboxDirectionsCLI/CodingOperation.swift | 2 +- .../CredentialsTests.swift | 49 ++- .../DirectionsCredentialsTests.swift | 51 --- .../DirectionsTests.swift | 2 +- .../IsochroneTests.swift | 197 ++++++++++++ .../OfflineDirectionsTests.swift | 2 +- .../RouteOptionsTests.swift | 2 +- .../RouteResponseTests.swift | 2 +- .../RouteStepTests.swift | 2 +- Tests/MapboxDirectionsTests/V5Tests.swift | 2 +- .../WalkingOptionsTests.swift | 2 +- 32 files changed, 1009 insertions(+), 168 deletions(-) rename Sources/MapboxDirections/{DirectionsCredentials.swift => Credentials.swift} (95%) create mode 100644 Sources/MapboxDirections/IsochroneError.swift create mode 100644 Sources/MapboxDirections/IsochroneOptions.swift create mode 100644 Sources/MapboxDirections/Isochrones.swift rename Sources/MapboxDirections/{DirectionsProfileIdentifier.swift => ProfileIdentifier.swift} (65%) delete mode 100644 Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift create mode 100644 Tests/MapboxDirectionsTests/IsochroneTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index f6c8d53c0..5d44cdc8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ * Added the `RouteOptions.initialManeuverAvoidanceRadius` property to avoid a sudden maneuver when calculating a route while the user is in motion. ([#609](https://github.com/mapbox/mapbox-directions-swift/pull/609)) * Added the `RoadClasses.unpaved` option for avoiding unpaved roads. ([#620](https://github.com/mapbox/mapbox-directions-swift/pull/620)) * Added the `RoadClasses.cashOnlyToll` property for avoiding toll roads that only accept cash payment. ([#620](https://github.com/mapbox/mapbox-directions-swift/pull/620)) +* Added `Isochrones`, which connects to the [Mapbox Isochrone API](https://docs.mapbox.com/api/navigation/isochrone/) to compute areas that are reachable within a specified amount of time from a location and return the reachable regions as contours of polygons or lines that you can display on a map. ([#621](https://github.com/mapbox/mapbox-directions-swift/pull/621)) +* Renamed `DirectionsCredentials` and `DirectionsProfileIdentifier` to `Credentials` and `ProfileIdentifier`, respectively. ([#621](https://github.com/mapbox/mapbox-directions-swift/pull/621)) * Added the `RouteOptions.maximumHeight` and `RouteOptions.maximumWidth` properties for ensuring that the resulting routes can accommodate a vehicle of a certain size. ([#623](https://github.com/mapbox/mapbox-directions-swift/pull/623)) * The `DirectionsPriority` struct now conforms to the `Codable` protocol. ([#623](https://github.com/mapbox/mapbox-directions-swift/pull/623)) * Fixed an issue where the `RouteOptions.alleyPriority`, `RouteOptions.walkwayPriority`, and `RouteOptions.speed` properties were excluded from the encoded representation of a `RouteOptions` object. ([#623](https://github.com/mapbox/mapbox-directions-swift/pull/623)) diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index 2c158ea75..60d15c27f 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -33,6 +33,29 @@ 2B540809245B23BE006C820B /* incorrectRouteRefreshResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B540808245B23BE006C820B /* incorrectRouteRefreshResponse.json */; }; 2B54080A245B23BE006C820B /* incorrectRouteRefreshResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B540808245B23BE006C820B /* incorrectRouteRefreshResponse.json */; }; 2B54080B245B23BE006C820B /* incorrectRouteRefreshResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B540808245B23BE006C820B /* incorrectRouteRefreshResponse.json */; }; + 2B9F3881272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */; }; + 2B9F3882272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */; }; + 2B9F3883272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */; }; + 2B9F3884272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */; }; + 2B9F3885272AE23A001DBA12 /* IsochroneOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387D272AE23A001DBA12 /* IsochroneOptions.swift */; }; + 2B9F3886272AE23A001DBA12 /* IsochroneOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387D272AE23A001DBA12 /* IsochroneOptions.swift */; }; + 2B9F3887272AE23A001DBA12 /* IsochroneOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387D272AE23A001DBA12 /* IsochroneOptions.swift */; }; + 2B9F3888272AE23A001DBA12 /* IsochroneOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387D272AE23A001DBA12 /* IsochroneOptions.swift */; }; + 2B9F3889272AE23A001DBA12 /* IsochroneError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387E272AE23A001DBA12 /* IsochroneError.swift */; }; + 2B9F388A272AE23A001DBA12 /* IsochroneError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387E272AE23A001DBA12 /* IsochroneError.swift */; }; + 2B9F388B272AE23A001DBA12 /* IsochroneError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387E272AE23A001DBA12 /* IsochroneError.swift */; }; + 2B9F388C272AE23A001DBA12 /* IsochroneError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387E272AE23A001DBA12 /* IsochroneError.swift */; }; + 2B9F388D272AE23A001DBA12 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387F272AE23A001DBA12 /* Credentials.swift */; }; + 2B9F388E272AE23A001DBA12 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387F272AE23A001DBA12 /* Credentials.swift */; }; + 2B9F388F272AE23A001DBA12 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387F272AE23A001DBA12 /* Credentials.swift */; }; + 2B9F3890272AE23A001DBA12 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387F272AE23A001DBA12 /* Credentials.swift */; }; + 2B9F3891272AE23A001DBA12 /* Isochrones.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3880272AE23A001DBA12 /* Isochrones.swift */; }; + 2B9F3892272AE23A001DBA12 /* Isochrones.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3880272AE23A001DBA12 /* Isochrones.swift */; }; + 2B9F3893272AE23A001DBA12 /* Isochrones.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3880272AE23A001DBA12 /* Isochrones.swift */; }; + 2B9F3894272AE23A001DBA12 /* Isochrones.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3880272AE23A001DBA12 /* Isochrones.swift */; }; + 2B9F389A272AE28B001DBA12 /* IsochroneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3895272AE277001DBA12 /* IsochroneTests.swift */; }; + 2B9F389B272AE28D001DBA12 /* IsochroneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3895272AE277001DBA12 /* IsochroneTests.swift */; }; + 2B9F389C272AE28E001DBA12 /* IsochroneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3895272AE277001DBA12 /* IsochroneTests.swift */; }; 2BA2E746257A667500D7AFC6 /* incidents.json in Resources */ = {isa = PBXBuildFile; fileRef = 2BA2E745257A667500D7AFC6 /* incidents.json */; }; 2BA2E747257A667500D7AFC6 /* incidents.json in Resources */ = {isa = PBXBuildFile; fileRef = 2BA2E745257A667500D7AFC6 /* incidents.json */; }; 2BA2E748257A667500D7AFC6 /* incidents.json in Resources */ = {isa = PBXBuildFile; fileRef = 2BA2E745257A667500D7AFC6 /* incidents.json */; }; @@ -92,16 +115,9 @@ 43538E3923ED463100E010D4 /* ResponseDisposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */; }; 43538E3A23ED463200E010D4 /* ResponseDisposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */; }; 43538E3B23ED463400E010D4 /* ResponseDisposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */; }; - 43538E3D23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */; }; - 43538E3E23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */; }; - 43538E3F23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */; }; 4376A52723FB13D400C6038D /* MatchOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */; }; 4376A52823FB13D400C6038D /* MatchOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */; }; 4376A52923FB13D400C6038D /* MatchOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */; }; - 438BFEC2233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; - 438BFEC3233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; - 438BFEC4233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; - 438BFEC5233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; 439255772344113B006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; 439255792344113D006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; 4392557A2344113E006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; @@ -113,10 +129,6 @@ 43D992FD2437B93E008A2D74 /* CredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */; }; 43D992FE2437B93F008A2D74 /* CredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */; }; 43D992FF2437B940008A2D74 /* CredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */; }; - 43EBD3AD23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; - 43EBD3AE23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; - 43EBD3AF23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; - 43EBD3B023DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; 43F89F932350F952007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MapMatchingResponse.swift */; }; 43F89F942350F952007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MapMatchingResponse.swift */; }; 43F89F952350F952007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MapMatchingResponse.swift */; }; @@ -456,6 +468,12 @@ 2B5407FF245B097D006C820B /* routeRefreshResponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = routeRefreshResponse.json; sourceTree = ""; }; 2B540804245B09E1006C820B /* routeRefreshRoute.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = routeRefreshRoute.json; sourceTree = ""; }; 2B540808245B23BE006C820B /* incorrectRouteRefreshResponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = incorrectRouteRefreshResponse.json; sourceTree = ""; }; + 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileIdentifier.swift; sourceTree = ""; }; + 2B9F387D272AE23A001DBA12 /* IsochroneOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IsochroneOptions.swift; sourceTree = ""; }; + 2B9F387E272AE23A001DBA12 /* IsochroneError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IsochroneError.swift; sourceTree = ""; }; + 2B9F387F272AE23A001DBA12 /* Credentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = ""; }; + 2B9F3880272AE23A001DBA12 /* Isochrones.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Isochrones.swift; sourceTree = ""; }; + 2B9F3895272AE277001DBA12 /* IsochroneTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IsochroneTests.swift; sourceTree = ""; }; 2BA2E745257A667500D7AFC6 /* incidents.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = incidents.json; sourceTree = ""; }; 2BA98970253F007600B643F6 /* mapbox-directions-swift */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "mapbox-directions-swift"; sourceTree = BUILT_PRODUCTS_DIR; }; 2BBBD05D257E61ED004EB3D6 /* MapboxStreetsRoadClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapboxStreetsRoadClass.swift; sourceTree = ""; }; @@ -474,14 +492,11 @@ 43208BAA2343F81900D8BD89 /* GeoJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSON.swift; sourceTree = ""; }; 43208BAC2343FF5500D8BD89 /* RouteResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteResponse.swift; sourceTree = ""; }; 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseDisposition.swift; sourceTree = ""; }; - 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsCredentialsTests.swift; sourceTree = ""; }; 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchOptionsTests.swift; sourceTree = ""; }; 438BFEBC233D7FA900457294 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 438BFEC0233D805500457294 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsProfileIdentifier.swift; sourceTree = ""; }; 4392557523440EC2006EEE88 /* DirectionsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsError.swift; sourceTree = ""; }; 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialsTests.swift; sourceTree = ""; }; - 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectionsCredentials.swift; sourceTree = ""; }; 43F89F922350F952007B591E /* MapMatchingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapMatchingResponse.swift; sourceTree = ""; }; 8A3B4C9A24EB55F60085DA64 /* RouteResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteResponseTests.swift; sourceTree = ""; }; 8A41B0FC24F5C2390021FFDC /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; @@ -771,19 +786,22 @@ C5DAAC9E20191683001F9261 /* MapMatching */, C51538CB1E807FF00093FF3E /* AttributeOptions.swift */, C58EA7A91E9D7EAD008F98CE /* Congestion.swift */, + 2B9F387F272AE23A001DBA12 /* Credentials.swift */, DD6254731AE70CB700017857 /* Directions.swift */, - 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */, 4392557523440EC2006EEE88 /* DirectionsError.swift */, C59094BE203B800300EB2417 /* DirectionsOptions.swift */, - 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */, C59094C0203DE6BC00EB2417 /* DirectionsResult.swift */, 431E93BE234664A200A71B44 /* DrivingSide.swift */, DA6C9D8C1CAE442B00094FBC /* Info.plist */, C57D55001DB5669600B94B74 /* Intersection.swift */, + 2B9F387E272AE23A001DBA12 /* IsochroneError.swift */, + 2B9F387D272AE23A001DBA12 /* IsochroneOptions.swift */, + 2B9F3880272AE23A001DBA12 /* Isochrones.swift */, C57D55071DB58C0200B94B74 /* Lane.swift */, DAA76D671DD127CB0015EC78 /* LaneIndication.swift */, DA6C9D8A1CAE442B00094FBC /* MapboxDirections.h */, 35828C9D217A003F00ED546E /* OfflineDirections.swift */, + 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */, DAD06E3823A008EB001A917D /* QuickLook.swift */, C59426061F1EA6C400C8E59C /* RoadClasses.swift */, 2BBBD05D257E61ED004EB3D6 /* MapboxStreetsRoadClass.swift */, @@ -819,11 +837,11 @@ 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */, DAD06E34239F0B19001A917D /* DirectionsErrorTests.swift */, DA1A110A1D01045E009F82FA /* DirectionsTests.swift */, - 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */, DA6C9DB11CAECA0E00094FBC /* Fixture.swift */, DAABF7912395AE9800CEEB61 /* GeoJSONTests.swift */, DA6C9D9A1CAE442B00094FBC /* Info.plist */, DAE33A1A1F215DF600C06039 /* IntersectionTests.swift */, + 2B9F3895272AE277001DBA12 /* IsochroneTests.swift */, DABE6C7D236A37E200D370F4 /* JSONSerialization.swift */, 3556CE9922649CF2009397B5 /* MapboxDirectionsTests-Bridging-Header.h */, C5DAACAE201AA92B001F9261 /* MatchTests.swift */, @@ -1356,6 +1374,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2B9F3892272AE23A001DBA12 /* Isochrones.swift in Sources */, 43538E3923ED463100E010D4 /* ResponseDisposition.swift in Sources */, 431E93C0234664A200A71B44 /* DrivingSide.swift in Sources */, C5DAAC9B2019167C001F9261 /* Match.swift in Sources */, @@ -1368,6 +1387,7 @@ 35828C9F217A003F00ED546E /* OfflineDirections.swift in Sources */, DAE7EA95230B5FD10003B211 /* Measurement.swift in Sources */, C5DAAC9F20195AAE001F9261 /* Tracepoint.swift in Sources */, + 2B9F3882272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */, 431E93CC23466C2500A71B44 /* RouteResponse.swift in Sources */, 431E93C423466B0F00A71B44 /* GeoJSON.swift in Sources */, C5990B4A2045E72800D7DFD4 /* DirectionsResult.swift in Sources */, @@ -1383,8 +1403,8 @@ 2B5407F32452FA8C006C820B /* RefreshedRoute.swift in Sources */, 431E93D023466D7500A71B44 /* Codable.swift in Sources */, C5990B4D2045E74800D7DFD4 /* DirectionsOptions.swift in Sources */, - 43EBD3AE23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAA76D691DD127CB0015EC78 /* LaneIndication.swift in Sources */, + 2B9F388E272AE23A001DBA12 /* Credentials.swift in Sources */, 43F89F942350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A10CB1D00F969009F82FA /* RouteStep.swift in Sources */, C57D55031DB566A700B94B74 /* Intersection.swift in Sources */, @@ -1395,13 +1415,14 @@ F4F5084C2524DC280044F2D0 /* AdministrativeRegion.swift in Sources */, 2BBBD05F257E61ED004EB3D6 /* MapboxStreetsRoadClass.swift in Sources */, 2BBBD08E257FA1CD004EB3D6 /* BlockedLanes.swift in Sources */, - 438BFEC3233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */, F4CF2C582523B66300A6D0B6 /* TollCollection.swift in Sources */, F4F508392524D6F10044F2D0 /* RestStop.swift in Sources */, C54549FC2073F1EF002E273F /* Array.swift in Sources */, C547EC691DB59F8F009817F3 /* Lane.swift in Sources */, + 2B9F388A272AE23A001DBA12 /* IsochroneError.swift in Sources */, 431E93C823466B4000A71B44 /* CoreLocation.swift in Sources */, AEDC212120B6125C0052DED8 /* VisualInstructionComponent.swift in Sources */, + 2B9F3886272AE23A001DBA12 /* IsochroneOptions.swift in Sources */, 439255792344113D006EEE88 /* DirectionsError.swift in Sources */, DA1A10C71D00F969009F82FA /* Directions.swift in Sources */, ); @@ -1419,6 +1440,7 @@ C53A02291E92C27A009837BD /* AnnotationTests.swift in Sources */, 4376A52823FB13D400C6038D /* MatchOptionsTests.swift in Sources */, DA688B3F21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, + 2B9F389B272AE28D001DBA12 /* IsochroneTests.swift in Sources */, DAD06E36239F0B19001A917D /* DirectionsErrorTests.swift in Sources */, C596663A2048AECD00C45CE5 /* RoutableMatchTests.swift in Sources */, DA8F3A7323B56D3B00B56786 /* RouteLegTests.swift in Sources */, @@ -1435,7 +1457,6 @@ 35CC310C2285739700EA1966 /* WalkingOptionsTests.swift in Sources */, DABE6C7F236A37E200D370F4 /* JSONSerialization.swift in Sources */, DAABF7932395AE9800CEEB61 /* GeoJSONTests.swift in Sources */, - 43538E3E23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */, F4D785F01DDD82C100FF4665 /* RouteStepTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1444,6 +1465,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2B9F3893272AE23A001DBA12 /* Isochrones.swift in Sources */, 43538E3A23ED463200E010D4 /* ResponseDisposition.swift in Sources */, 431E93C1234664A200A71B44 /* DrivingSide.swift in Sources */, C5DAAC9C2019167D001F9261 /* Match.swift in Sources */, @@ -1456,6 +1478,7 @@ 35828CA0217A003F00ED546E /* OfflineDirections.swift in Sources */, DAE7EA96230B5FD10003B211 /* Measurement.swift in Sources */, C5DAACA020195AAF001F9261 /* Tracepoint.swift in Sources */, + 2B9F3883272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */, 431E93CD23466C2700A71B44 /* RouteResponse.swift in Sources */, 431E93C523466B1000A71B44 /* GeoJSON.swift in Sources */, C5990B4B2045E72900D7DFD4 /* DirectionsResult.swift in Sources */, @@ -1471,8 +1494,8 @@ 2B5407F42452FA8C006C820B /* RefreshedRoute.swift in Sources */, 431E93D123466D7600A71B44 /* Codable.swift in Sources */, C5990B4E2045E74900D7DFD4 /* DirectionsOptions.swift in Sources */, - 43EBD3AF23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAA76D6A1DD127CB0015EC78 /* LaneIndication.swift in Sources */, + 2B9F388F272AE23A001DBA12 /* Credentials.swift in Sources */, 43F89F952350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A10F11D010247009F82FA /* RouteStep.swift in Sources */, C57D55041DB566A800B94B74 /* Intersection.swift in Sources */, @@ -1483,13 +1506,14 @@ F4F5084D2524DC280044F2D0 /* AdministrativeRegion.swift in Sources */, 2BBBD060257E61ED004EB3D6 /* MapboxStreetsRoadClass.swift in Sources */, 2BBBD08F257FA1CD004EB3D6 /* BlockedLanes.swift in Sources */, - 438BFEC4233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */, F4CF2C592523B66300A6D0B6 /* TollCollection.swift in Sources */, F4F5083A2524D6F10044F2D0 /* RestStop.swift in Sources */, C54549FD2073F1F0002E273F /* Array.swift in Sources */, C547EC6A1DB59F90009817F3 /* Lane.swift in Sources */, + 2B9F388B272AE23A001DBA12 /* IsochroneError.swift in Sources */, 431E93C923466B4100A71B44 /* CoreLocation.swift in Sources */, AEDC212220B6125D0052DED8 /* VisualInstructionComponent.swift in Sources */, + 2B9F3887272AE23A001DBA12 /* IsochroneOptions.swift in Sources */, 4392557A2344113E006EEE88 /* DirectionsError.swift in Sources */, DA1A10ED1D010247009F82FA /* Directions.swift in Sources */, ); @@ -1507,6 +1531,7 @@ C53A022A1E92C27B009837BD /* AnnotationTests.swift in Sources */, 4376A52923FB13D400C6038D /* MatchOptionsTests.swift in Sources */, DA688B4021B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, + 2B9F389C272AE28E001DBA12 /* IsochroneTests.swift in Sources */, DAD06E37239F0B19001A917D /* DirectionsErrorTests.swift in Sources */, C596663B2048AECE00C45CE5 /* RoutableMatchTests.swift in Sources */, DA8F3A7423B56D3B00B56786 /* RouteLegTests.swift in Sources */, @@ -1523,7 +1548,6 @@ 35CC310D2285739700EA1966 /* WalkingOptionsTests.swift in Sources */, DABE6C80236A37E200D370F4 /* JSONSerialization.swift in Sources */, DAABF7942395AE9800CEEB61 /* GeoJSONTests.swift in Sources */, - 43538E3F23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */, F4D785F11DDD82C100FF4665 /* RouteStepTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1532,6 +1556,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2B9F3894272AE23A001DBA12 /* Isochrones.swift in Sources */, 43538E3B23ED463400E010D4 /* ResponseDisposition.swift in Sources */, 431E93C2234664A200A71B44 /* DrivingSide.swift in Sources */, C5DAAC9D2019167E001F9261 /* Match.swift in Sources */, @@ -1544,6 +1569,7 @@ 35828CA1217A003F00ED546E /* OfflineDirections.swift in Sources */, DAE7EA97230B5FD10003B211 /* Measurement.swift in Sources */, C5DAACA120195AAF001F9261 /* Tracepoint.swift in Sources */, + 2B9F3884272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */, 431E93CE23466C2800A71B44 /* RouteResponse.swift in Sources */, 431E93C623466B1100A71B44 /* GeoJSON.swift in Sources */, C5990B4C2045E72A00D7DFD4 /* DirectionsResult.swift in Sources */, @@ -1559,8 +1585,8 @@ 2B5407F52452FA8C006C820B /* RefreshedRoute.swift in Sources */, 431E93D223466D7700A71B44 /* Codable.swift in Sources */, C5990B4F2045E74A00D7DFD4 /* DirectionsOptions.swift in Sources */, - 43EBD3B023DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAA76D6B1DD127CB0015EC78 /* LaneIndication.swift in Sources */, + 2B9F3890272AE23A001DBA12 /* Credentials.swift in Sources */, 43F89F962350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A11081D0103A3009F82FA /* RouteStep.swift in Sources */, C57D55051DB566A900B94B74 /* Intersection.swift in Sources */, @@ -1571,13 +1597,14 @@ F4F5084E2524DC280044F2D0 /* AdministrativeRegion.swift in Sources */, 2BBBD061257E61ED004EB3D6 /* MapboxStreetsRoadClass.swift in Sources */, 2BBBD090257FA1CD004EB3D6 /* BlockedLanes.swift in Sources */, - 438BFEC5233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */, F4CF2C5A2523B66300A6D0B6 /* TollCollection.swift in Sources */, F4F5083B2524D6F10044F2D0 /* RestStop.swift in Sources */, C54549FE2073F1F1002E273F /* Array.swift in Sources */, C547EC6B1DB59F91009817F3 /* Lane.swift in Sources */, + 2B9F388C272AE23A001DBA12 /* IsochroneError.swift in Sources */, 431E93CA23466B4200A71B44 /* CoreLocation.swift in Sources */, AEDC212320B6125E0052DED8 /* VisualInstructionComponent.swift in Sources */, + 2B9F3888272AE23A001DBA12 /* IsochroneOptions.swift in Sources */, 4392557B2344113F006EEE88 /* DirectionsError.swift in Sources */, DA1A11041D0103A3009F82FA /* Directions.swift in Sources */, ); @@ -1587,6 +1614,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2B9F3891272AE23A001DBA12 /* Isochrones.swift in Sources */, 43538E3823ED463100E010D4 /* ResponseDisposition.swift in Sources */, 431E93BF234664A200A71B44 /* DrivingSide.swift in Sources */, C51538CC1E807FF00093FF3E /* AttributeOptions.swift in Sources */, @@ -1599,6 +1627,7 @@ 35828C9E217A003F00ED546E /* OfflineDirections.swift in Sources */, DAE7EA94230B5FD10003B211 /* Measurement.swift in Sources */, C59426071F1EA6C400C8E59C /* RoadClasses.swift in Sources */, + 2B9F3881272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */, 431E93CB23466C2400A71B44 /* RouteResponse.swift in Sources */, 431E93C323466B0E00A71B44 /* GeoJSON.swift in Sources */, 35EFD00B207DFACA00BF3873 /* VisualInstruction.swift in Sources */, @@ -1614,8 +1643,8 @@ 2B5407F22452FA8C006C820B /* RefreshedRoute.swift in Sources */, 431E93CF23466D7400A71B44 /* Codable.swift in Sources */, C59094C1203DE6BC00EB2417 /* DirectionsResult.swift in Sources */, - 43EBD3AD23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAC05F1A1CFC077C00FA0071 /* RouteLeg.swift in Sources */, + 2B9F388D272AE23A001DBA12 /* Credentials.swift in Sources */, 43F89F932350F952007B591E /* MapMatchingResponse.swift in Sources */, C5434B8A200693D00069E887 /* Tracepoint.swift in Sources */, DA6C9DA61CAE462800094FBC /* Directions.swift in Sources */, @@ -1629,10 +1658,11 @@ 8D381B6A1FDB101F008D5A58 /* String.swift in Sources */, F4CF2C572523B66300A6D0B6 /* TollCollection.swift in Sources */, F4F508382524D6F10044F2D0 /* RestStop.swift in Sources */, - 438BFEC2233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */, C57D55011DB5669600B94B74 /* Intersection.swift in Sources */, + 2B9F3889272AE23A001DBA12 /* IsochroneError.swift in Sources */, 431E93C723466B3F00A71B44 /* CoreLocation.swift in Sources */, AEDC211D20B6104B0052DED8 /* VisualInstructionComponent.swift in Sources */, + 2B9F3885272AE23A001DBA12 /* IsochroneOptions.swift in Sources */, 439255772344113B006EEE88 /* DirectionsError.swift in Sources */, DA2E03E91CB0E0B000D1269A /* RouteStep.swift in Sources */, ); @@ -1650,6 +1680,7 @@ C5247D711E818A24004B6154 /* AnnotationTests.swift in Sources */, 4376A52723FB13D400C6038D /* MatchOptionsTests.swift in Sources */, DA688B3E21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, + 2B9F389A272AE28B001DBA12 /* IsochroneTests.swift in Sources */, DAD06E35239F0B19001A917D /* DirectionsErrorTests.swift in Sources */, C59666392048A20E00C45CE5 /* RoutableMatchTests.swift in Sources */, DA8F3A7223B56D3B00B56786 /* RouteLegTests.swift in Sources */, @@ -1667,7 +1698,6 @@ 35CC310B2285739700EA1966 /* WalkingOptionsTests.swift in Sources */, DABE6C7E236A37E200D370F4 /* JSONSerialization.swift in Sources */, DAABF7922395AE9800CEEB61 /* GeoJSONTests.swift in Sources */, - 43538E3D23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */, F4D785EF1DDD82C100FF4665 /* RouteStepTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/README.md b/README.md index b2443cc37..ec64801f4 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ The main directions class is `Directions`. Create a directions object using your // main.swift import MapboxDirections -let directions = Directions(credentials: DirectionsCredentials(accessToken: "<#your access token#>")) +let directions = Directions(credentials: Credentials(accessToken: "<#your access token#>")) ``` Alternatively, you can place your access token in the `MBXAccessToken` key of your application’s Info.plist file, then use the shared directions object: @@ -178,6 +178,27 @@ let task = directions.calculate(options) { (session, result) in You can also use the `Directions.calculateRoutes(matching:completionHandler:)` method to get Route objects suitable for use anywhere a standard Directions API response would be used. +### Build an isochrone map + +Tell the user how far they can travel within certain distances or times of a given location using the Isochrone API. `Isochrones` uses the same access token initialization as `Directions`. Once that is configured, you need to fill `IsochronesOptions` parameters to calculate the desired GeoJSON: + +```swift +let isochrones = Isochrones(credentials: Credentials(accessToken: "<#your access token#>")) + +let isochroneOptions = IsochroneOptions(centerCoordinate: CLLocationCoordinate2D(latitude: 45.52, longitude: -122.681944), + contours: .byDistances([ + .init(value: 500, unit: .meters, color: .orange), + .init(value: 1, unit: .kilometers, color: .red) + ])) + +isochrones.calculate(isochroneOptions) { session, result in + if case .success(let response) = result { + print(response) + } +} +``` +... + ## Usage with other Mapbox libraries ### Drawing the route on a map @@ -204,6 +225,50 @@ if var routeCoordinates = route.shape?.coordinates, routeCoordinates.count > 0 { The [Mapbox Navigation SDK for iOS](https://github.com/mapbox/mapbox-navigation-ios/) provides a full-fledged user interface for turn-by-turn navigation along routes supplied by MapboxDirections. +### Drawing Isochrones contours on a map snapshot + +[MapboxStatic.swift](https://github.com/mapbox/MapboxStatic.swift) provides an easy way to draw a isochrone contours on a map. + +```swift +// main.swift +import MapboxStatic +import MapboxDirections + +let centerCoordinate = CLLocationCoordinate2D(latitude: 45.52, longitude: -122.681944) +let accessToken = "<#your access token#>" + +// Setup snapshot parameters +let camera = SnapshotCamera( + lookingAtCenter: centerCoordinate, + zoomLevel: 12) +let options = SnapshotOptions( + styleURL: URL(string: "<#your mapbox: style URL#>")!, + camera: camera, + size: CGSize(width: 200, height: 200)) + +// Request Isochrone contour to draw on a map +let isochrones = Isochrones(credentials: Credentials(accessToken: accessToken)) +isochrones.calculate(IsochroneOptions(centerCoordinate: centerCoordinate, + contours: .byDistances([.init(value: 500, unit: .meters)]))) { session, result in + if case .success(let response) = result { + // Serialize the geoJSON + let encoder = JSONEncoder() + let data = try! encoder.encode(response) + let geoJSONString = String(data: data, encoding: .utf8)! + let geoJSONOverlay = GeoJSON(objectString: geoJSONString) + + // Feed resulting geoJSON to snapshot options + options.overlays.append(geoJSONOverlay) + + let snapshot = Snapshot( + options: options, + accessToken: accessToken) + + // Display the result! + drawImage(snapshot.image) + } +} +``` ## Directions CLI diff --git a/Sources/MapboxDirections/AttributeOptions.swift b/Sources/MapboxDirections/AttributeOptions.swift index 40097885e..f9096a29f 100644 --- a/Sources/MapboxDirections/AttributeOptions.swift +++ b/Sources/MapboxDirections/AttributeOptions.swift @@ -38,7 +38,7 @@ public struct AttributeOptions: OptionSet, CustomStringConvertible { When this attribute is specified, the `RouteLeg.congestionLevels` property contains one value for each segment in the leg’s full geometry. - This attribute requires `DirectionsProfileIdentifier.automobileAvoidingTraffic`. Any other profile identifier produces `CongestionLevel.unknown` for each segment along the route. + This attribute requires `ProfileIdentifier.automobileAvoidingTraffic`. Any other profile identifier produces `CongestionLevel.unknown` for each segment along the route. */ public static let congestionLevel = AttributeOptions(rawValue: 1 << 4) @@ -54,7 +54,7 @@ public struct AttributeOptions: OptionSet, CustomStringConvertible { When this attribute is specified, the `RouteLeg.numericCongestionLevels` property contains one value for each segment in the leg’s full geometry. - This attribute requires `DirectionsProfileIdentifier.automobileAvoidingTraffic`. Any other profile identifier produces `nil` for each segment along the route. + This attribute requires `ProfileIdentifier.automobileAvoidingTraffic`. Any other profile identifier produces `nil` for each segment along the route. */ public static let numericCongestionLevel = AttributeOptions(rawValue: 1 << 6) diff --git a/Sources/MapboxDirections/DirectionsCredentials.swift b/Sources/MapboxDirections/Credentials.swift similarity index 95% rename from Sources/MapboxDirections/DirectionsCredentials.swift rename to Sources/MapboxDirections/Credentials.swift index 9f54496d9..cf38ce273 100644 --- a/Sources/MapboxDirections/DirectionsCredentials.swift +++ b/Sources/MapboxDirections/Credentials.swift @@ -7,7 +7,7 @@ let defaultAccessToken: String? = UserDefaults.standard.string(forKey: "MBXAccessToken") let defaultApiEndPointURLString = Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAPIBaseURL") as? String -public struct DirectionsCredentials: Equatable { +public struct Credentials: Equatable { /** The mapbox access token. You can find this in your Mapbox account dashboard. @@ -65,3 +65,5 @@ public struct DirectionsCredentials: Equatable { } } +@available(*, deprecated, renamed: "Credentials") +public typealias DirectionsCredentials = Credentials diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index b7cd47bcf..ac3de20cf 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -75,7 +75,7 @@ open class Directions: NSObject { - parameter credentials: A object containing the credentials used to make the request. */ - public typealias Session = (options: DirectionsOptions, credentials: DirectionsCredentials) + public typealias Session = (options: DirectionsOptions, credentials: Credentials) /** A closure (block) to be called when a directions request is complete. @@ -103,7 +103,7 @@ open class Directions: NSObject { - postcondition: To update the original route, pass `RouteRefreshResponse.route` into the `Route.refreshLegAttributes(from:)` method. */ - public typealias RouteRefreshCompletionHandler = (_ credentials: DirectionsCredentials, _ result: Result) -> Void + public typealias RouteRefreshCompletionHandler = (_ credentials: Credentials, _ result: Result) -> Void // MARK: Creating a Directions Object @@ -119,14 +119,8 @@ open class Directions: NSObject { If nothing is provided, the default behavior is to read credential values from the developer's Info.plist. */ - public let credentials: DirectionsCredentials + public let credentials: Credentials - /** - Initializes a newly created directions object with an optional access token and host. - - - parameter credentials: A `DirectionsCredentials` object that, optionally, contains customized Token and Endpoint information. If no credentials object is supplied, then defaults are used. - */ - private var authenticationParams: [URLQueryItem] { var params: [URLQueryItem] = [ URLQueryItem(name: "access_token", value: credentials.accessToken) @@ -148,7 +142,7 @@ open class Directions: NSObject { - urlSession: URLSession that will be used to submit API requests to Mapbox Directions API. - processingQueue: A DispatchQueue that will be used for CPU intensive work. */ - public init(credentials: DirectionsCredentials = .init(), + public init(credentials: Credentials = .init(), urlSession: URLSession = .shared, processingQueue: DispatchQueue = .global(qos: .userInitiated)) { self.credentials = credentials @@ -506,7 +500,7 @@ open class Directions: NSObject { open func urlRequest(forRefreshing responseIdentifier: String, routeIndex: Int, fromLegAtIndex startLegIndex: Int) -> URLRequest { let params: [URLQueryItem] = authenticationParams - var unparameterizedURL = URL(string: "directions-refresh/v1/\(DirectionsProfileIdentifier.automobileAvoidingTraffic.rawValue)", relativeTo: credentials.host)! + var unparameterizedURL = URL(string: "directions-refresh/v1/\(ProfileIdentifier.automobileAvoidingTraffic.rawValue)", relativeTo: credentials.host)! unparameterizedURL.appendPathComponent(responseIdentifier) unparameterizedURL.appendPathComponent(String(routeIndex)) unparameterizedURL.appendPathComponent(String(startLegIndex)) diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift index 3a0b6931c..a4de44e34 100644 --- a/Sources/MapboxDirections/DirectionsError.swift +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -87,7 +87,7 @@ public enum DirectionsError: LocalizedError { /** Unrecognized profile identifier. - Make sure the `DirectionsOptions.profileIdentifier` option is set to one of the predefined values, such as `DirectionsProfileIdentifier.automobile`. + Make sure the `DirectionsOptions.profileIdentifier` option is set to one of the predefined values, such as `ProfileIdentifier.automobile`. */ case profileNotFound @@ -166,7 +166,7 @@ public enum DirectionsError: LocalizedError { case .unableToLocate: return "Make sure the locations are close enough to a roadway or pathway. Try setting the coordinateAccuracy property of all the waypoints to nil." case .profileNotFound: - return "Make sure the profileIdentifier option is set to one of the provided constants, such as DirectionsProfileIdentifier.automobile." + return "Make sure the profileIdentifier option is set to one of the provided constants, such as ProfileIdentifier.automobile." case .requestTooLarge: return "Try specifying fewer waypoints or giving the waypoints shorter names." case let .rateLimited(rateLimitInterval: _, rateLimit: _, resetTime: rolloverTime): diff --git a/Sources/MapboxDirections/DirectionsOptions.swift b/Sources/MapboxDirections/DirectionsOptions.swift index 0c36f1264..ed51fbe8c 100644 --- a/Sources/MapboxDirections/DirectionsOptions.swift +++ b/Sources/MapboxDirections/DirectionsOptions.swift @@ -120,10 +120,10 @@ open class DirectionsOptions: Codable { Do not call `DirectionsOptions(waypoints:profileIdentifier:)` directly; instead call the corresponding initializer of `RouteOptions` or `MatchOptions`. - - parameter waypoints: An array of `Waypoint` objects representing locations that the route should visit in chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 waypoints. (Some profiles, such as `DirectionsProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://docs.mapbox.com/api/navigation/#directions).) - - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `DirectionsProfileIdentifier.automobile` is used by default. + - parameter waypoints: An array of `Waypoint` objects representing locations that the route should visit in chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 waypoints. (Some profiles, such as `ProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://docs.mapbox.com/api/navigation/#directions).) + - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `ProfileIdentifier.automobile` is used by default. */ - required public init(waypoints: [Waypoint], profileIdentifier: DirectionsProfileIdentifier? = nil) { + required public init(waypoints: [Waypoint], profileIdentifier: ProfileIdentifier? = nil) { self.waypoints = waypoints self.profileIdentifier = profileIdentifier ?? .automobile } @@ -159,7 +159,7 @@ open class DirectionsOptions: Codable { public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) waypoints = try container.decode([Waypoint].self, forKey: .waypoints) - profileIdentifier = try container.decode(DirectionsProfileIdentifier.self, forKey: .profileIdentifier) + profileIdentifier = try container.decode(ProfileIdentifier.self, forKey: .profileIdentifier) includesSteps = try container.decode(Bool.self, forKey: .includesSteps) shapeFormat = try container.decode(RouteShapeFormat.self, forKey: .shapeFormat) routeShapeResolution = try container.decode(RouteShapeResolution.self, forKey: .routeShapeResolution) @@ -197,9 +197,9 @@ open class DirectionsOptions: Codable { /** A string specifying the primary mode of transportation for the routes. - The default value of this property is `DirectionsProfileIdentifier.automobile`, which specifies driving directions. + The default value of this property is `ProfileIdentifier.automobile`, which specifies driving directions. */ - open var profileIdentifier: DirectionsProfileIdentifier + open var profileIdentifier: ProfileIdentifier // MARK: Specifying the Response Format @@ -291,7 +291,7 @@ open class DirectionsOptions: Codable { // MARK: Getting the Request URL /** - An array of URL query items to include in an HTTP request. + The path of the request URL, specifying service name, version and profile. The query items are included in the URL of a GET request or the body of a POST request. */ diff --git a/Sources/MapboxDirections/DirectionsResult.swift b/Sources/MapboxDirections/DirectionsResult.swift index e28e18194..ec276e056 100644 --- a/Sources/MapboxDirections/DirectionsResult.swift +++ b/Sources/MapboxDirections/DirectionsResult.swift @@ -132,7 +132,7 @@ open class DirectionsResult: Codable { /** The route’s expected travel time, measured in seconds. - The value of this property reflects the time it takes to traverse the entire route. It is the sum of the `expectedTravelTime` properties of the route’s legs. If the route was calculated using the `DirectionsProfileIdentifier.automobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin the route. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the route makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. + The value of this property reflects the time it takes to traverse the entire route. It is the sum of the `expectedTravelTime` properties of the route’s legs. If the route was calculated using the `ProfileIdentifier.automobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin the route. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the route makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. Do not assume that the user would travel along the route at a fixed speed. For more granular travel times, use the `RouteLeg.expectedTravelTime` or `RouteStep.expectedTravelTime`. For even more granularity, specify the `AttributeOptions.expectedTravelTime` option and use the `RouteLeg.expectedSegmentTravelTimes` property. */ @@ -141,7 +141,7 @@ open class DirectionsResult: Codable { /** The route’s typical travel time, measured in seconds. - The value of this property reflects the typical time it takes to traverse the entire route. It is the sum of the `typicalTravelTime` properties of the route’s legs. This property is available when using the `DirectionsProfileIdentifier.automobileAvoidingTraffic` profile. This property reflects typical traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the user would begin the route. If the route makes use of a ferry, the typical travel time may additionally be subject to the schedule of this service. + The value of this property reflects the typical time it takes to traverse the entire route. It is the sum of the `typicalTravelTime` properties of the route’s legs. This property is available when using the `ProfileIdentifier.automobileAvoidingTraffic` profile. This property reflects typical traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the user would begin the route. If the route makes use of a ferry, the typical travel time may additionally be subject to the schedule of this service. Do not assume that the user would travel along the route at a fixed speed. For more granular typical travel times, use the `RouteLeg.typicalTravelTime` or `RouteStep.typicalTravelTime`. */ diff --git a/Sources/MapboxDirections/IsochroneError.swift b/Sources/MapboxDirections/IsochroneError.swift new file mode 100644 index 000000000..e1f23294e --- /dev/null +++ b/Sources/MapboxDirections/IsochroneError.swift @@ -0,0 +1,75 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +/** + An error that occurs when calculating isochrone contours. + */ +public enum IsochroneError: LocalizedError { + + public init(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) { + if let response = response as? HTTPURLResponse { + switch (response.statusCode, code ?? "") { + case (200, "NoSegment"): + self = .unableToLocate + case (404, "ProfileNotFound"): + self = .profileNotFound + case (422, "InvalidInput"): + self = .invalidInput(message: message) + case (429, _): + self = .rateLimited(rateLimitInterval: response.rateLimitInterval, rateLimit: response.rateLimit, resetTime: response.rateLimitResetTime) + default: + self = .unknown(response: response, underlying: error, code: code, message: message) + } + } else { + self = .unknown(response: response, underlying: error, code: code, message: message) + } + } + + /** + There is no network connection available to perform the network request. + */ + case network(_: URLError) + + /** + The server returned a response that isn’t correctly formatted. + */ + case invalidResponse(_: URLResponse?) + + /** + The server returned an empty response. + */ + case noData + + /** + A specified location could not be associated with a roadway or pathway. + + Make sure the locations are close enough to a roadway or pathway. + */ + case unableToLocate + + /** + Unrecognized profile identifier. + + Make sure the `IsochroneOptions.profileIdentifier` option is set to one of the predefined values, such as `IsochroneProfileIdentifier.automobile`. + */ + case profileNotFound + + /** + The API recieved input that it didn't understand. + */ + case invalidInput(message: String?) + + /** + Too many requests have been made with the same access token within a certain period of time. + + Wait before retrying. + */ + case rateLimited(rateLimitInterval: TimeInterval?, rateLimit: UInt?, resetTime: Date?) + + /** + Unknown error case. Look at associated values for more details. + */ + case unknown(response: URLResponse?, underlying: Error?, code: String?, message: String?) +} diff --git a/Sources/MapboxDirections/IsochroneOptions.swift b/Sources/MapboxDirections/IsochroneOptions.swift new file mode 100644 index 000000000..fce2fcb48 --- /dev/null +++ b/Sources/MapboxDirections/IsochroneOptions.swift @@ -0,0 +1,301 @@ +import Foundation +import Turf + +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + + +/** + Options for calculating contours from the Mapbox Isochrone service. +*/ +public class IsochroneOptions { + + public init(centerCoordinate: LocationCoordinate2D, contours: Contours, profileIdentifier: ProfileIdentifier = .automobile) { + self.centerCoordinate = centerCoordinate + self.contours = contours + self.profileIdentifier = profileIdentifier + } + + // MARK: Configuring the Contour + + /** + Contours GeoJSON format. + */ + public enum ContourFormat { + /** + Requested contour will be presented as GeoJSON LineString. + */ + case lineString + /** + Requested contour will be presented as GeoJSON Polygon. + */ + case polygon + } + + /** + A string specifying the primary mode of transportation for the contours. + + The default value of this property is `ProfileIdentifier.automobile`, which specifies driving directions. + */ + public var profileIdentifier: ProfileIdentifier + /** + A coordinate around which to center the isochrone lines. + */ + public var centerCoordinate: LocationCoordinate2D + /** + Contours bounds and color sheme definition. + */ + public var contours: Contours + + /** + Specifies the format of output contours. + + Defaults to `.lineString` which represents contours as linestrings. + */ + public var contoursFormat: ContourFormat = .lineString + + /** + Removes contours which are `denoisingFactor` times smaller than the biggest one. + + The default is 1.0. A value of 1.0 will only return the largest contour for a given value. A value of 0.5 drops any contours that are less than half the area of the largest contour in the set of contours for that same value. + */ + public var denoisingFactor: Float? + + /** + Douglas-Peucker simplification tolerance. + + Higher means simpler geometries and faster performance. There is no upper bound. If no value is specified in the request, the Isochrone API will choose the most optimized value to use for the request. + + - note: Simplification of contours can lead to self-intersections, as well as intersections of adjacent contours. + */ + public var simplificationTolerance: LocationDistance? + + // MARK: Getting the Request URL + + /** + The path of the request URL, specifying service name, version and profile. + */ + var abridgedPath: String { + return "isochrone/v1/\(profileIdentifier.rawValue)" + } + + /** + The path of the request URL, not including the hostname or any parameters. + */ + var path: String { + return "\(abridgedPath)/\(centerCoordinate.requestDescription).json" + } + + /** + An array of URL query items (parameters) to include in an HTTP request. + */ + public var urlQueryItems: [URLQueryItem] { + var queryItems: [URLQueryItem] = [] + + switch contours { + case .byDistances(let definitions): + let fallbackColor = definitions.allSatisfy { $0.color != nil } ? nil : Color.fallbackColor + + queryItems.append(URLQueryItem(name: "contours_meters", + value: definitions.map { $0.queryValueDescription(roundingTo: .meters) }.joined(separator: ","))) + + let colors = definitions.compactMap { $0.queryColorDescription(fallbackColor: fallbackColor) }.joined(separator: ",") + if !colors.isEmpty { + queryItems.append(URLQueryItem(name: "contours_colors", value: colors)) + } + case .byExpectedTravelTimes(let definitions): + let fallbackColor = definitions.allSatisfy { $0.color != nil } ? nil : Color.fallbackColor + + queryItems.append(URLQueryItem(name: "contours_minutes", + value: definitions.map { $0.queryValueDescription(roundingTo: .minutes) }.joined(separator: ","))) + + let colors = definitions.compactMap { $0.queryColorDescription(fallbackColor: fallbackColor) }.joined(separator: ",") + if !colors.isEmpty { + queryItems.append(URLQueryItem(name: "contours_colors", value: colors)) + } + } + + if contoursFormat == .polygon { + queryItems.append(URLQueryItem(name: "polygons", value: "true")) + } + + if let denoise = denoisingFactor { + queryItems.append(URLQueryItem(name: "denoise", value: String(denoise))) + } + + if let tolerance = simplificationTolerance { + queryItems.append(URLQueryItem(name: "generalize", value: String(tolerance))) + } + + return queryItems + } +} + +extension IsochroneOptions { + + /** + Definition of contours limits. + */ + public enum Contours { + + /** + Describes Individual contour bound and color. + */ + public struct Definition { + /** + Bound measurement value. + */ + public var value: Measurement + /** + Contour fill color. + + If this property is unspecified, the contour is colored gray. If this property is not specified for any contour, the contours are rainbow-colored. + */ + public var color: Color? + + /** + Initializes new contour Definition. + */ + public init(value: Measurement, color: Color? = nil) { + self.value = value + self.color = color + } + + /** + Initializes new contour Definition. + + Convenience initializer for encapsulating `Measurement` initialization. + */ + public init(value: Double, unit: Unt, color: Color? = nil) { + self.init(value: Measurement(value: value, unit: unit), + color: color) + } + + func queryValueDescription(roundingTo unit: Unt) -> String { + return String(Int(value.converted(to: unit).value.rounded())) + } + + func queryColorDescription(fallbackColor: Color?) -> String? { + return (color ?? fallbackColor)?.queryDescription + } + } + + /** + The desired travel times to use for each isochrone contour. + + This value will be rounded to minutes. + */ + case byExpectedTravelTimes([Definition]) + + /** + The distances to use for each isochrone contour. + + Will be rounded to meters. + */ + case byDistances([Definition]) + } +} + +extension IsochroneOptions { + #if canImport(UIKit) + /** + RGB-based color representation for Isochrone contour. + */ + public typealias Color = UIColor + #elseif canImport(AppKit) + /** + RGB-based color representation for Isochrone contour. + */ + public typealias Color = NSColor + #else + /** + sRGB color space representation for Isochrone contour. + + This is a compatibility shim to keep the library’s public interface consistent between Apple and non-Apple platforms that lack `UIKit` or `AppKit`. On Apple platforms, you can use `UIColor` or `NSColor` respectively anywhere you see this type. + */ + public struct Color { + /** + Red color component. + + Value ranged from `0` up to `255`. + */ + public var red: Int + /** + Green color component. + + Value ranged from `0` up to `255`. + */ + public var green: Int + /** + Blue color component. + + Value ranged from `0` up to `255`. + */ + public var blue: Int + + /** + Creates new `Color` instance. + */ + public init(red: Int, green: Int, blue: Int) { + self.red = red + self.green = green + self.blue = blue + } + } + #endif +} + +extension IsochroneOptions.Color { + var queryDescription: String { + let hexFormat = "%02X%02X%02X" + + #if canImport(UIKit) + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + + getRed(&red, + green: &green, + blue: &blue, + alpha: nil) + + return String(format: hexFormat, + Int(red * 255), + Int(green * 255), + Int(blue * 255)) + #elseif canImport(AppKit) + var convertedColor = self + if colorSpace != .sRGB { + guard let converted = usingColorSpace(.sRGB) else { + assertionFailure("Failed to convert Isochrone contour color to RGB space.") + return "000000" + } + + convertedColor = converted + } + + return String(format: hexFormat, + Int(convertedColor.redComponent * 255), + Int(convertedColor.greenComponent * 255), + Int(convertedColor.blueComponent * 255)) + #else + return String(format: hexFormat, + red, + green, + blue) + #endif + } + + static var fallbackColor: IsochroneOptions.Color { + #if canImport(UIKit) + return gray + #elseif canImport(AppKit) + return gray + #else + return IsochroneOptions.Color(red: 128, green: 128, blue: 128) + #endif + } +} diff --git a/Sources/MapboxDirections/Isochrones.swift b/Sources/MapboxDirections/Isochrones.swift new file mode 100644 index 000000000..69873916d --- /dev/null +++ b/Sources/MapboxDirections/Isochrones.swift @@ -0,0 +1,170 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import Turf + +/** + Computes areas that are reachable within a specified amount of time or distance from a location, and returns the reachable regions as contours of polygons or lines that you can display on a map. + */ +open class Isochrones { + + /** + A tuple type representing the isochrone session that was generated from the request. + + - parameter options: A `IsochroneOptions ` object representing the request parameter options. + + - parameter credentials: A object containing the credentials used to make the request. + */ + public typealias Session = (options: IsochroneOptions, credentials: Credentials) + + /** + A closure (block) to be called when a isochrone request is complete. + + - parameter session: A `Isochrones.Session` object containing session information + + - parameter result: A `Result` enum that represents the `FeatureCollection` if the request returned successfully, or the error if it did not. + */ + public typealias IsochroneCompletionHandler = (_ session: Session, _ result: Result) -> Void + + // MARK: Creating an Isochrones Object + + /** + The Authorization & Authentication credentials that are used for this service. + + If nothing is provided, the default behavior is to read credential values from the developer's Info.plist. + */ + public let credentials: Credentials + private let urlSession: URLSession + private let processingQueue: DispatchQueue + + /** + The shared isochrones object. + + To use this object, a Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/) should be specified in the `MBXAccessToken` key in the main application bundle’s Info.plist. + */ + public static let shared = Isochrones() + + /** + Creates a new instance of Isochrones object. + - Parameters: + - credentials: Credentials that will be used to make API requests to Mapbox Isochrone API. + - urlSession: URLSession that will be used to submit API requests to Mapbox Isochrone API. + - processingQueue: A DispatchQueue that will be used for CPU intensive work. + */ + public init(credentials: Credentials = .init(), + urlSession: URLSession = .shared, + processingQueue: DispatchQueue = .global(qos: .userInitiated)) { + self.credentials = credentials + self.urlSession = urlSession + self.processingQueue = processingQueue + } + + /** + Begins asynchronously calculating isochrone contours using the given options and delivers the results to a closure. + + This method retrieves the contours asynchronously from the [Mapbox Isochrone API](https://docs.mapbox.com/api/navigation/isochrone/) over a network connection. If a connection error or server error occurs, details about the error are passed into the given completion handler in lieu of the contours. + + Contours may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). + + - parameter options: A `IsochroneOptions` object specifying the requirements for the resulting contours. + - parameter completionHandler: The closure (block) to call with the resulting contours. This closure is executed on the application’s main thread. + - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting contours, cancel this task. + */ + @discardableResult open func calculate(_ options: IsochroneOptions, completionHandler: @escaping IsochroneCompletionHandler) -> URLSessionDataTask { + let session = (options: options, credentials: self.credentials) + let request = urlRequest(forCalculating: options) + let requestTask = urlSession.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in + if let urlError = possibleError as? URLError { + DispatchQueue.main.async { + completionHandler(session, .failure(.network(urlError))) + } + return + } + + guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { + DispatchQueue.main.async { + completionHandler(session, .failure(.invalidResponse(possibleResponse))) + } + return + } + + guard let data = possibleData else { + DispatchQueue.main.async { + completionHandler(session, .failure(.noData)) + } + return + } + + self.processingQueue.async { + do { + let decoder = JSONDecoder() + + guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { + let apiError = IsochroneError(code: nil, message: nil, response: possibleResponse, underlyingError: possibleError) + + DispatchQueue.main.async { + completionHandler(session, .failure(apiError)) + } + return + } + + guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { + let apiError = IsochroneError(code: disposition.code, message: disposition.message, response: response, underlyingError: possibleError) + DispatchQueue.main.async { + completionHandler(session, .failure(apiError)) + } + return + } + + let result = try decoder.decode(FeatureCollection.self, from: data) + + DispatchQueue.main.async { + completionHandler(session, .success(result)) + } + } catch { + DispatchQueue.main.async { + let bailError = IsochroneError(code: nil, message: nil, response: response, underlyingError: error) + completionHandler(session, .failure(bailError)) + } + } + } + } + requestTask.priority = 1 + requestTask.resume() + + return requestTask + } + + // MARK: Request URL Preparation + + /** + The GET HTTP URL used to fetch the contours from the API. + + - parameter options: A `IsochroneOptions` object specifying the requirements for the resulting contours. + - returns: The URL to send the request to. + */ + open func url(forCalculating options: IsochroneOptions) -> URL { + + var params = options.urlQueryItems + params.append(URLQueryItem(name: "access_token", value: credentials.accessToken)) + + let unparameterizedURL = URL(string: options.path, relativeTo: credentials.host)! + var components = URLComponents(url: unparameterizedURL, resolvingAgainstBaseURL: true)! + components.queryItems = params + return components.url! + } + + /** + The HTTP request used to fetch the contours from the API. + + - parameter options: A `IsochroneOptions` object specifying the requirements for the resulting routes. + - returns: A GET HTTP request to calculate the specified options. + */ + open func urlRequest(forCalculating options: IsochroneOptions) -> URLRequest { + let getURL = self.url(forCalculating: options) + var request = URLRequest(url: getURL) + request.setValue(userAgent, forHTTPHeaderField: "User-Agent") + return request + } +} diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index a9f5444b0..84e6b3d9e 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -10,7 +10,7 @@ public struct MapMatchingResponse { public var tracepoints: [Tracepoint?]? public let options: MatchOptions - public let credentials: DirectionsCredentials + public let credentials: Credentials /** The time when this `MapMatchingResponse` object was created, which is immediately upon recieving the raw URL response. @@ -28,7 +28,7 @@ extension MapMatchingResponse: Codable { case tracepoints } - public init(httpResponse: HTTPURLResponse?, matches: [Match]? = nil, tracepoints: [Tracepoint]? = nil, options: MatchOptions, credentials: DirectionsCredentials) { + public init(httpResponse: HTTPURLResponse?, matches: [Match]? = nil, tracepoints: [Tracepoint]? = nil, options: MatchOptions, credentials: Credentials) { self.httpResponse = httpResponse self.matches = matches self.tracepoints = tracepoints @@ -46,7 +46,7 @@ extension MapMatchingResponse: Codable { } self.options = options - guard let credentials = decoder.userInfo[.credentials] as? DirectionsCredentials else { + guard let credentials = decoder.userInfo[.credentials] as? Credentials else { throw DirectionsCodingError.missingCredentials } self.credentials = credentials diff --git a/Sources/MapboxDirections/MapMatching/MatchOptions.swift b/Sources/MapboxDirections/MapMatching/MatchOptions.swift index ac2a6f61c..d7853dac8 100644 --- a/Sources/MapboxDirections/MapMatching/MatchOptions.swift +++ b/Sources/MapboxDirections/MapMatching/MatchOptions.swift @@ -16,10 +16,10 @@ open class MatchOptions: DirectionsOptions { /** Initializes a match options object for matching locations against the road network. - - parameter locations: An array of `CLLocation` objects representing locations to attempt to match against the road network. The array should contain at least two locations (the source and destination) and at most 100 locations. (Some profiles, such as `DirectionsProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://docs.mapbox.com/api/navigation/#directions).) - - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `DirectionsProfileIdentifier.automobile` is used by default. + - parameter locations: An array of `CLLocation` objects representing locations to attempt to match against the road network. The array should contain at least two locations (the source and destination) and at most 100 locations. (Some profiles, such as `ProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://docs.mapbox.com/api/navigation/#directions).) + - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `ProfileIdentifier.automobile` is used by default. */ - public convenience init(locations: [CLLocation], profileIdentifier: DirectionsProfileIdentifier? = nil) { + public convenience init(locations: [CLLocation], profileIdentifier: ProfileIdentifier? = nil) { let waypoints = locations.map { Waypoint(location: $0) } @@ -30,17 +30,17 @@ open class MatchOptions: DirectionsOptions { /** Initializes a match options object for matching geographic coordinates against the road network. - - parameter coordinates: An array of geographic coordinates representing locations to attempt to match against the road network. The array should contain at least two locations (the source and destination) and at most 100 locations. (Some profiles, such as `DirectionsProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://docs.mapbox.com/api/navigation/#directions).) Each coordinate is converted into a `Waypoint` object. - - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `DirectionsProfileIdentifier.automobile` is used by default. + - parameter coordinates: An array of geographic coordinates representing locations to attempt to match against the road network. The array should contain at least two locations (the source and destination) and at most 100 locations. (Some profiles, such as `ProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://docs.mapbox.com/api/navigation/#directions).) Each coordinate is converted into a `Waypoint` object. + - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `ProfileIdentifier.automobile` is used by default. */ - public convenience init(coordinates: [LocationCoordinate2D], profileIdentifier: DirectionsProfileIdentifier? = nil) { + public convenience init(coordinates: [LocationCoordinate2D], profileIdentifier: ProfileIdentifier? = nil) { let waypoints = coordinates.map { Waypoint(coordinate: $0) } self.init(waypoints: waypoints, profileIdentifier: profileIdentifier) } - public required init(waypoints: [Waypoint], profileIdentifier: DirectionsProfileIdentifier? = nil) { + public required init(waypoints: [Waypoint], profileIdentifier: ProfileIdentifier? = nil) { super.init(waypoints: waypoints, profileIdentifier: profileIdentifier) } diff --git a/Sources/MapboxDirections/DirectionsProfileIdentifier.swift b/Sources/MapboxDirections/ProfileIdentifier.swift similarity index 65% rename from Sources/MapboxDirections/DirectionsProfileIdentifier.swift rename to Sources/MapboxDirections/ProfileIdentifier.swift index 026baa5ed..edb8686ff 100644 --- a/Sources/MapboxDirections/DirectionsProfileIdentifier.swift +++ b/Sources/MapboxDirections/ProfileIdentifier.swift @@ -1,12 +1,9 @@ import Foundation -@available(*, deprecated, renamed: "DirectionsProfileIdentifier") -public typealias MBDirectionsProfileIdentifier = DirectionsProfileIdentifier - /** - Options determining the primary mode of transportation for the routes. + Options determining the primary mode of transportation. */ -public struct DirectionsProfileIdentifier: Codable, Hashable, RawRepresentable { +public struct ProfileIdentifier: Codable, Hashable, RawRepresentable { public init(rawValue: String) { self.rawValue = rawValue } @@ -18,28 +15,40 @@ public struct DirectionsProfileIdentifier: Codable, Hashable, RawRepresentable { This profile prioritizes fast routes by preferring high-speed roads like highways. A driving route may use a ferry where necessary. */ - public static let automobile: DirectionsProfileIdentifier = .init(rawValue: "mapbox/driving") + public static let automobile: ProfileIdentifier = .init(rawValue: "mapbox/driving") /** The returned directions are appropriate for driving or riding a car, truck, or motorcycle. This profile avoids traffic congestion based on current traffic data. A driving route may use a ferry where necessary. - Traffic data is available in [a number of countries and territories worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/#traffic-data). Where traffic data is unavailable, this profile prefers high-speed roads like highways, similar to `DirectionsProfileIdentifier.Automobile`. + Traffic data is available in [a number of countries and territories worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/#traffic-data). Where traffic data is unavailable, this profile prefers high-speed roads like highways, similar to `ProfileIdentifier.Automobile`. + + - note: This profile is not supported by `Isochrones` API. */ - public static let automobileAvoidingTraffic: DirectionsProfileIdentifier = .init(rawValue: "mapbox/driving-traffic") + public static let automobileAvoidingTraffic: ProfileIdentifier = .init(rawValue: "mapbox/driving-traffic") /** The returned directions are appropriate for riding a bicycle. This profile prioritizes short, safe routes by avoiding highways and preferring cycling infrastructure, such as bike lanes on surface streets. A cycling route may, where necessary, use other modes of transportation, such as ferries or trains, or require dismounting the bicycle for a distance. */ - public static let cycling: DirectionsProfileIdentifier = .init(rawValue: "mapbox/cycling") + public static let cycling: ProfileIdentifier = .init(rawValue: "mapbox/cycling") /** The returned directions are appropriate for walking or hiking. This profile prioritizes short routes, making use of sidewalks and trails where available. A walking route may use other modes of transportation, such as ferries or trains, where necessary. */ - public static let walking: DirectionsProfileIdentifier = .init(rawValue: "mapbox/walking") + public static let walking: ProfileIdentifier = .init(rawValue: "mapbox/walking") } + + +@available(*, deprecated, renamed: "ProfileIdentifier") +public typealias MBDirectionsProfileIdentifier = ProfileIdentifier + +/** + Options determining the primary mode of transportation for the routes. + */ +@available(*, deprecated, renamed: "ProfileIdentifier") +public typealias DirectionsProfileIdentifier = ProfileIdentifier diff --git a/Sources/MapboxDirections/QuickLook.swift b/Sources/MapboxDirections/QuickLook.swift index 01971e8b0..51b1e4f36 100644 --- a/Sources/MapboxDirections/QuickLook.swift +++ b/Sources/MapboxDirections/QuickLook.swift @@ -15,7 +15,7 @@ protocol CustomQuickLookConvertible { /** Returns a URL to an image representation of the given coordinates via the [Mapbox Static Images API](https://docs.mapbox.com/api/maps/#static-images). */ -func debugQuickLookURL(illustrating shape: LineString, profileIdentifier: DirectionsProfileIdentifier = .automobile, accessToken: String? = defaultAccessToken) -> URL? { +func debugQuickLookURL(illustrating shape: LineString, profileIdentifier: ProfileIdentifier = .automobile, accessToken: String? = defaultAccessToken) -> URL? { guard let accessToken = accessToken else { return nil } diff --git a/Sources/MapboxDirections/RouteLeg.swift b/Sources/MapboxDirections/RouteLeg.swift index ca6d46312..a927bae3a 100644 --- a/Sources/MapboxDirections/RouteLeg.swift +++ b/Sources/MapboxDirections/RouteLeg.swift @@ -33,7 +33,7 @@ open class RouteLeg: Codable { - parameter typicalTravelTime: The route leg’s typical travel time, measured in seconds. - parameter profileIdentifier: The primary mode of transportation for the route leg. */ - public init(steps: [RouteStep], name: String, distance: Turf.LocationDistance, expectedTravelTime: TimeInterval, typicalTravelTime: TimeInterval? = nil, profileIdentifier: DirectionsProfileIdentifier) { + public init(steps: [RouteStep], name: String, distance: Turf.LocationDistance, expectedTravelTime: TimeInterval, typicalTravelTime: TimeInterval? = nil, profileIdentifier: ProfileIdentifier) { self.steps = steps self.name = name self.distance = distance @@ -63,7 +63,7 @@ open class RouteLeg: Codable { expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime) typicalTravelTime = try container.decodeIfPresent(TimeInterval.self, forKey: .typicalTravelTime) - if let profileIdentifier = try container.decodeIfPresent(DirectionsProfileIdentifier.self, forKey: .profileIdentifier) { + if let profileIdentifier = try container.decodeIfPresent(ProfileIdentifier.self, forKey: .profileIdentifier) { self.profileIdentifier = profileIdentifier } else if let options = decoder.userInfo[.options] as? DirectionsOptions { profileIdentifier = options.profileIdentifier @@ -287,7 +287,7 @@ open class RouteLeg: Codable { /** The route leg’s expected travel time, measured in seconds. - The value of this property reflects the time it takes to traverse the route leg. If the route was calculated using the `DirectionsProfileIdentifier.automobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin this leg. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the leg makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. + The value of this property reflects the time it takes to traverse the route leg. If the route was calculated using the `ProfileIdentifier.automobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin this leg. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the leg makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. Do not assume that the user would travel along the leg at a fixed speed. For the expected travel time on each individual segment along the leg, use the `RouteStep.expectedTravelTimes` property. For more granularity, specify the `AttributeOptions.expectedTravelTime` option and use the `expectedSegmentTravelTimes` property. */ @@ -313,7 +313,7 @@ open class RouteLeg: Codable { /** The route leg’s typical travel time, measured in seconds. - The value of this property reflects the typical time it takes to traverse the route leg. This property is available when using the `DirectionsProfileIdentifier.automobileAvoidingTraffic` profile. This property reflects typical traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the user would begin this leg. If the leg makes use of a ferry, the typical travel time may additionally be subject to the schedule of this service. + The value of this property reflects the typical time it takes to traverse the route leg. This property is available when using the `ProfileIdentifier.automobileAvoidingTraffic` profile. This property reflects typical traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the user would begin this leg. If the leg makes use of a ferry, the typical travel time may additionally be subject to the schedule of this service. Do not assume that the user would travel along the route at a fixed speed. For more granular typical travel times, use the `RouteStep.typicalTravelTime` property. */ @@ -326,7 +326,7 @@ open class RouteLeg: Codable { The value of this property depends on the `RouteOptions.profileIdentifier` property of the original `RouteOptions` object. This property reflects the primary mode of transportation used for the route leg. Individual steps along the route leg might use different modes of transportation as necessary. */ - public let profileIdentifier: DirectionsProfileIdentifier + public let profileIdentifier: ProfileIdentifier } extension RouteLeg: Equatable { diff --git a/Sources/MapboxDirections/RouteOptions.swift b/Sources/MapboxDirections/RouteOptions.swift index b1167cdc9..0c830ecf7 100644 --- a/Sources/MapboxDirections/RouteOptions.swift +++ b/Sources/MapboxDirections/RouteOptions.swift @@ -15,11 +15,11 @@ open class RouteOptions: DirectionsOptions { /** Initializes a route options object for routes between the given waypoints and an optional profile identifier. - - parameter waypoints: An array of `Waypoint` objects representing locations that the route should visit in chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 waypoints. (Some profiles, such as `DirectionsProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://www.mapbox.com/api-documentation/#directions).) - - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `DirectionsProfileIdentifier.automobile` is used by default. + - parameter waypoints: An array of `Waypoint` objects representing locations that the route should visit in chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 waypoints. (Some profiles, such as `ProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://www.mapbox.com/api-documentation/#directions).) + - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `ProfileIdentifier.automobile` is used by default. */ - public required init(waypoints: [Waypoint], profileIdentifier: DirectionsProfileIdentifier? = nil) { - let profilesDisallowingUTurns: [DirectionsProfileIdentifier] = [.automobile, .automobileAvoidingTraffic] + public required init(waypoints: [Waypoint], profileIdentifier: ProfileIdentifier? = nil) { + let profilesDisallowingUTurns: [ProfileIdentifier] = [.automobile, .automobileAvoidingTraffic] allowsUTurnAtWaypoint = !profilesDisallowingUTurns.contains(profileIdentifier ?? .automobile) super.init(waypoints: waypoints, profileIdentifier: profileIdentifier) } @@ -31,9 +31,9 @@ open class RouteOptions: DirectionsOptions { - note: This initializer is intended for `CLLocation` objects created using the `CLLocation.init(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid overfitting, since the `Waypoint` class’s `coordinateAccuracy` property represents the maximum allowed deviation from the waypoint. - parameter locations: An array of `CLLocation` objects representing locations that the route should visit in chronological order. The array should contain at least two locations (the source and destination) and at most 25 locations. Each location object is converted into a `Waypoint` object. This class respects the `CLLocation` class’s `coordinate` and `horizontalAccuracy` properties, converting them into the `Waypoint` class’s `coordinate` and `coordinateAccuracy` properties, respectively. - - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `DirectionsProfileIdentifier.automobile` is used by default. + - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `ProfileIdentifier.automobile` is used by default. */ - public convenience init(locations: [CLLocation], profileIdentifier: DirectionsProfileIdentifier? = nil) { + public convenience init(locations: [CLLocation], profileIdentifier: ProfileIdentifier? = nil) { let waypoints = locations.map { Waypoint(location: $0) } self.init(waypoints: waypoints, profileIdentifier: profileIdentifier) } @@ -43,9 +43,9 @@ open class RouteOptions: DirectionsOptions { Initializes a route options object for routes between the given geographic coordinates and an optional profile identifier. - parameter coordinates: An array of geographic coordinates representing locations that the route should visit in chronological order. The array should contain at least two locations (the source and destination) and at most 25 locations. Each coordinate is converted into a `Waypoint` object. - - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `DirectionsProfileIdentifier.automobile` is used by default. + - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `ProfileIdentifier.automobile` is used by default. */ - public convenience init(coordinates: [LocationCoordinate2D], profileIdentifier: DirectionsProfileIdentifier? = nil) { + public convenience init(coordinates: [LocationCoordinate2D], profileIdentifier: ProfileIdentifier? = nil) { let waypoints = coordinates.map { Waypoint(coordinate: $0) } self.init(waypoints: waypoints, profileIdentifier: profileIdentifier) } @@ -145,7 +145,7 @@ open class RouteOptions: DirectionsOptions { Set this property to `true` if you expect the user to traverse each leg of the trip separately. For example, it would be quite easy for the user to effectively “U-turn” at a waypoint if the user first parks the car and patronizes a restaurant there before embarking on the next leg of the trip. Set this property to `false` if you expect the user to proceed to the next waypoint immediately upon arrival. For example, if the user only needs to drop off a passenger or package at the waypoint before continuing, it would be inconvenient to perform a U-turn at that location. - The default value of this property is `false` when the profile identifier is `DirectionsProfileIdentifier.automobile` or `DirectionsProfileIdentifier.automobileAvoidingTraffic` and `true` otherwise. + The default value of this property is `false` when the profile identifier is `ProfileIdentifier.automobile` or `ProfileIdentifier.automobileAvoidingTraffic` and `true` otherwise. */ open var allowsUTurnAtWaypoint: Bool @@ -159,7 +159,7 @@ open class RouteOptions: DirectionsOptions { /** The route classes that the calculated routes will allow. - This property has no effect unless the profile identifier is set to `DirectionsProfileIdentifier.automobile` or `DirectionsProfileIdentifier.automobileAvoidingTraffic`. + This property has no effect unless the profile identifier is set to `ProfileIdentifier.automobile` or `ProfileIdentifier.automobileAvoidingTraffic`. */ open var roadClassesToAllow: RoadClasses = [] @@ -167,7 +167,7 @@ open class RouteOptions: DirectionsOptions { The number that influences whether the route should prefer or avoid alleys or narrow service roads between buildings. If this property isn't explicitly set, the Directions API will choose the most reasonable value. - This property has no effect unless the profile identifier is set to `DirectionsProfileIdentifier.automobile` or `DirectionsProfileIdentifier.walking`. + This property has no effect unless the profile identifier is set to `ProfileIdentifier.automobile` or `ProfileIdentifier.walking`. The value of this property must be at least `DirectionsPriority.low` and at most `DirectionsPriority.high`. `DirectionsPriority.medium` neither prefers nor avoids alleys, while a negative value between `DirectionsPriority.low` and `DirectionsPriority.medium` avoids alleys, and a positive value between `DirectionsPriority.medium` and `DirectionsPriority.high` prefers alleys. A value of 0.9 is suitable for pedestrians who are comfortable with walking down alleys. */ @@ -177,7 +177,7 @@ open class RouteOptions: DirectionsOptions { The number that influences whether the route should prefer or avoid roads or paths that are set aside for pedestrian-only use (walkways or footpaths). If this property isn't explicitly set, the Directions API will choose the most reasonable value. - This property has no effect unless the profile identifier is set to `DirectionsProfileIdentifier.walking`. You can adjust this property to avoid [sidewalks and crosswalks that are mapped as separate footpaths](https://wiki.openstreetmap.org/wiki/Sidewalks#Sidewalk_as_separate_way), which may be more granular than needed for some forms of pedestrian navigation. + This property has no effect unless the profile identifier is set to `ProfileIdentifier.walking`. You can adjust this property to avoid [sidewalks and crosswalks that are mapped as separate footpaths](https://wiki.openstreetmap.org/wiki/Sidewalks#Sidewalk_as_separate_way), which may be more granular than needed for some forms of pedestrian navigation. The value of this property must be at least `DirectionsPriority.low` and at most `DirectionsPriority.high`. `DirectionsPriority.medium` neither prefers nor avoids walkways, while a negative value between `DirectionsPriority.low` and `DirectionsPriority.medium` avoids walkways, and a positive value between `DirectionsPriority.medium` and `DirectionsPriority.high` prefers walkways. A value of −0.1 results in less verbose routes in cities where sidewalks and crosswalks are generally mapped as separate footpaths. */ @@ -187,7 +187,7 @@ open class RouteOptions: DirectionsOptions { The expected uniform travel speed measured in meters per second. If this property isn't explicitly set, the Directions API will choose the most reasonable value. - This property has no effect unless the profile identifier is set to `DirectionsProfileIdentifier.walking`. You can adjust this property to account for running or for faster or slower gaits. When the profile identifier is set to another profile identifier, such as `DirectionsProfileIdentifier.driving`, this property is ignored in favor of the expected travel speed on each road along the route. This property may be supported by other routing profiles in the future. + This property has no effect unless the profile identifier is set to `ProfileIdentifier.walking`. You can adjust this property to account for running or for faster or slower gaits. When the profile identifier is set to another profile identifier, such as `ProfileIdentifier.driving`, this property is ignored in favor of the expected travel speed on each road along the route. This property may be supported by other routing profiles in the future. The value of this property must be at least `CLLocationSpeed.minimumWalking` and at most `CLLocationSpeed.maximumWalking`. `CLLocationSpeed.normalWalking` corresponds to a typical preferred walking speed. */ @@ -216,7 +216,7 @@ open class RouteOptions: DirectionsOptions { /** A Boolean value indicating whether `Directions` can refresh time-dependent properties of the `RouteLeg`s of the resulting `Route`s. - To refresh the `RouteLeg.expectedSegmentTravelTimes`, `RouteLeg.segmentSpeeds`, and `RouteLeg.segmentCongestionLevels` properties, use the `Directions.refreshRoute(responseIdentifier:routeIndex:fromLegAtIndex:completionHandler:)` method. This property is ignored unless `profileIdentifier` is `DirectionsProfileIdentifier.automobileAvoidingTraffic`. This option is set to `false` by default. + To refresh the `RouteLeg.expectedSegmentTravelTimes`, `RouteLeg.segmentSpeeds`, and `RouteLeg.segmentCongestionLevels` properties, use the `Directions.refreshRoute(responseIdentifier:routeIndex:fromLegAtIndex:completionHandler:)` method. This property is ignored unless `profileIdentifier` is `ProfileIdentifier.automobileAvoidingTraffic`. This option is set to `false` by default. */ open var refreshingEnabled = false diff --git a/Sources/MapboxDirections/RouteRefreshResponse.swift b/Sources/MapboxDirections/RouteRefreshResponse.swift index 0e57abaf4..9856d1a06 100644 --- a/Sources/MapboxDirections/RouteRefreshResponse.swift +++ b/Sources/MapboxDirections/RouteRefreshResponse.swift @@ -35,7 +35,7 @@ public struct RouteRefreshResponse { /** The credentials used to make the request. */ - public let credentials: DirectionsCredentials + public let credentials: Credentials /** The time when this `RouteRefreshResponse` object was created, which is immediately upon recieving the raw URL response. @@ -58,7 +58,7 @@ extension RouteRefreshResponse: Codable { self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse - guard let credentials = decoder.userInfo[.credentials] as? DirectionsCredentials else { + guard let credentials = decoder.userInfo[.credentials] as? Credentials else { throw DirectionsCodingError.missingCredentials } diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index 26ae801b4..b042ecd5d 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -16,7 +16,7 @@ public struct RouteResponse { public let waypoints: [Waypoint]? public let options: ResponseOptions - public let credentials: DirectionsCredentials + public let credentials: Credentials /** The time when this `RouteResponse` object was created, which is immediately upon recieving the raw URL response. @@ -38,7 +38,7 @@ extension RouteResponse: Codable { case waypoints } - public init(httpResponse: HTTPURLResponse?, identifier: String? = nil, routes: [Route]? = nil, waypoints: [Waypoint]? = nil, options: ResponseOptions, credentials: DirectionsCredentials) { + public init(httpResponse: HTTPURLResponse?, identifier: String? = nil, routes: [Route]? = nil, waypoints: [Waypoint]? = nil, options: ResponseOptions, credentials: Credentials) { self.httpResponse = httpResponse self.identifier = identifier self.routes = routes @@ -47,7 +47,7 @@ extension RouteResponse: Codable { self.credentials = credentials } - public init(matching response: MapMatchingResponse, options: MatchOptions, credentials: DirectionsCredentials) throws { + public init(matching response: MapMatchingResponse, options: MatchOptions, credentials: Credentials) throws { let decoder = JSONDecoder() let encoder = JSONEncoder() @@ -79,7 +79,7 @@ extension RouteResponse: Codable { self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse - guard let credentials = decoder.userInfo[.credentials] as? DirectionsCredentials else { + guard let credentials = decoder.userInfo[.credentials] as? Credentials else { throw DirectionsCodingError.missingCredentials } diff --git a/Sources/MapboxDirections/RouteStep.swift b/Sources/MapboxDirections/RouteStep.swift index 487d2c63e..82e1e0636 100644 --- a/Sources/MapboxDirections/RouteStep.swift +++ b/Sources/MapboxDirections/RouteStep.swift @@ -6,12 +6,12 @@ import Turf A `TransportType` specifies the mode of transportation used for part of a route. */ public enum TransportType: String, Codable { - // Possible transport types when the `profileIdentifier` is `DirectionsProfileIdentifier.automobile` or `DirectionsProfileIdentifier.automobileAvoidingTraffic` + // Possible transport types when the `profileIdentifier` is `ProfileIdentifier.automobile` or `ProfileIdentifier.automobileAvoidingTraffic` /** The route requires the user to drive or ride a car, truck, or motorcycle. - This is the usual transport type when the `profileIdentifier` is `DirectionsProfileIdentifier.automobile` or `DirectionsProfileIdentifier.automobileAvoidingTraffic`. + This is the usual transport type when the `profileIdentifier` is `ProfileIdentifier.automobile` or `ProfileIdentifier.automobileAvoidingTraffic`. */ case automobile = "driving" // automobile @@ -36,21 +36,21 @@ public enum TransportType: String, Codable { */ case inaccessible = "unaccessible" // automobile, walking, cycling - // Possible transport types when the `profileIdentifier` is `DirectionsProfileIdentifier.walking` + // Possible transport types when the `profileIdentifier` is `ProfileIdentifier.walking` /** The route requires the user to walk. - This is the usual transport type when the `profileIdentifier` is `DirectionsProfileIdentifier.walking`. For cycling directions, this value indicates that the user is expected to dismount. + This is the usual transport type when the `profileIdentifier` is `ProfileIdentifier.walking`. For cycling directions, this value indicates that the user is expected to dismount. */ case walking // walking, cycling - // Possible transport types when the `profileIdentifier` is `DirectionsProfileIdentifier.cycling` + // Possible transport types when the `profileIdentifier` is `ProfileIdentifier.cycling` /** The route requires the user to ride a bicycle. - This is the usual transport type when the `profileIdentifier` is `DirectionsProfileIdentifier.cycling`. + This is the usual transport type when the `profileIdentifier` is `ProfileIdentifier.cycling`. */ case cycling // cycling @@ -776,7 +776,7 @@ open class RouteStep: Codable { /** The step’s expected travel time, measured in seconds. - The value of this property reflects the time it takes to go from this step’s maneuver location to the next step’s maneuver location. If the route was calculated using the `DirectionsProfileIdentifier.automobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin this step. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the step makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. + The value of this property reflects the time it takes to go from this step’s maneuver location to the next step’s maneuver location. If the route was calculated using the `ProfileIdentifier.automobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin this step. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the step makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. Do not assume that the user would travel along the step at a fixed speed. For the expected travel time on each individual segment along the leg, specify the `AttributeOptions.expectedTravelTime` option and use the `RouteLeg.expectedSegmentTravelTimes` property. */ @@ -785,7 +785,7 @@ open class RouteStep: Codable { /** The step’s typical travel time, measured in seconds. - The value of this property reflects the typical time it takes to go from this step’s maneuver location to the next step’s maneuver location. This property is available when using the `DirectionsProfileIdentifier.automobileAvoidingTraffic` profile. This property reflects typical traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the user would begin this step. If the step makes use of a ferry, the typical travel time may additionally be subject to the schedule of this service. + The value of this property reflects the typical time it takes to go from this step’s maneuver location to the next step’s maneuver location. This property is available when using the `ProfileIdentifier.automobileAvoidingTraffic` profile. This property reflects typical traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the user would begin this step. If the step makes use of a ferry, the typical travel time may additionally be subject to the schedule of this service. Do not assume that the user would travel along the step at a fixed speed. */ diff --git a/Sources/MapboxDirectionsCLI/CodingOperation.swift b/Sources/MapboxDirectionsCLI/CodingOperation.swift index dc0e9e429..a0c2ef272 100644 --- a/Sources/MapboxDirectionsCLI/CodingOperation.swift +++ b/Sources/MapboxDirectionsCLI/CodingOperation.swift @@ -2,7 +2,7 @@ import Foundation import MapboxDirections -private let BogusCredentials = DirectionsCredentials(accessToken: "pk.feedCafeDadeDeadBeef-BadeBede.FadeCafeDadeDeed-BadeBede") +private let BogusCredentials = Credentials(accessToken: "pk.feedCafeDadeDeadBeef-BadeBede.FadeCafeDadeDeed-BadeBede") class CodingOperation { diff --git a/Tests/MapboxDirectionsTests/CredentialsTests.swift b/Tests/MapboxDirectionsTests/CredentialsTests.swift index d38ffd5e3..d363b41c9 100644 --- a/Tests/MapboxDirectionsTests/CredentialsTests.swift +++ b/Tests/MapboxDirectionsTests/CredentialsTests.swift @@ -5,9 +5,56 @@ class CredentialsTests: XCTestCase { func testCredentialsCreation() { let testURL = URL(string: "https://example.com")! - let subject = DirectionsCredentials(accessToken: "test", host: testURL) + let subject = Credentials(accessToken: "test", host: testURL) XCTAssertEqual(subject.accessToken, "test") XCTAssertEqual(subject.host, testURL) } + + func testDefaultConfiguration() { + let credentials = Credentials(accessToken: BogusToken) + XCTAssertEqual(credentials.accessToken, BogusToken) + XCTAssertEqual(credentials.host.absoluteString, "https://api.mapbox.com") + } + + func testCustomConfiguration() { + let token = "deadbeefcafebebe" + let host = URL(string: "https://example.com")! + let credentials = Credentials(accessToken: token, host: host) + XCTAssertEqual(credentials.accessToken, token) + XCTAssertEqual(credentials.host, host) + } + + func testAccessTokenInjection() { + let expected = "injected" + UserDefaults.standard.set(expected, forKey: "MBXAccessToken") + XCTAssertEqual(Directions.shared.credentials.accessToken, expected) + } + +#if !os(Linux) + func testSkuToken() { + let expectedToken = "a token" + MBXAccounts.serviceSkuToken = expectedToken + MBXAccounts.serviceAccessToken = Directions.shared.credentials.accessToken + XCTAssertEqual(Directions.shared.credentials.skuToken, expectedToken) + MBXAccounts.serviceSkuToken = nil + MBXAccounts.serviceAccessToken = nil + } + + func testSkuTokenWithMismatchedAccessToken() { + MBXAccounts.serviceSkuToken = "a token" + MBXAccounts.serviceAccessToken = UUID().uuidString + XCTAssertEqual(Directions.shared.credentials.skuToken, nil) + MBXAccounts.serviceSkuToken = nil + MBXAccounts.serviceAccessToken = nil + } +#endif +} + +#if !os(Linux) +@objc(MBXAccounts) +final class MBXAccounts: NSObject { + @objc static var serviceSkuToken: String? + @objc static var serviceAccessToken: String? } +#endif diff --git a/Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift b/Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift deleted file mode 100644 index f82550592..000000000 --- a/Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -import XCTest -@testable import MapboxDirections - -class DirectionsCredentialsTests: XCTestCase { - func testDefaultConfiguration() { - let credentials = DirectionsCredentials(accessToken: BogusToken) - XCTAssertEqual(credentials.accessToken, BogusToken) - XCTAssertEqual(credentials.host.absoluteString, "https://api.mapbox.com") - } - - func testCustomConfiguration() { - let token = "deadbeefcafebebe" - let host = URL(string: "https://example.com")! - let credentials = DirectionsCredentials(accessToken: token, host: host) - XCTAssertEqual(credentials.accessToken, token) - XCTAssertEqual(credentials.host, host) - } - - func testAccessTokenInjection() { - let expected = "injected" - UserDefaults.standard.set(expected, forKey: "MBXAccessToken") - XCTAssertEqual(Directions.shared.credentials.accessToken, expected) - } - -#if !os(Linux) - func testSkuToken() { - let expectedToken = "a token" - MBXAccounts.serviceSkuToken = expectedToken - MBXAccounts.serviceAccessToken = Directions.shared.credentials.accessToken - XCTAssertEqual(Directions.shared.credentials.skuToken, expectedToken) - MBXAccounts.serviceSkuToken = nil - MBXAccounts.serviceAccessToken = nil - } - - func testSkuTokenWithMismatchedAccessToken() { - MBXAccounts.serviceSkuToken = "a token" - MBXAccounts.serviceAccessToken = UUID().uuidString - XCTAssertEqual(Directions.shared.credentials.skuToken, nil) - MBXAccounts.serviceSkuToken = nil - MBXAccounts.serviceAccessToken = nil - } -#endif -} - -#if !os(Linux) -@objc(MBXAccounts) -final class MBXAccounts: NSObject { - @objc static var serviceSkuToken: String? - @objc static var serviceAccessToken: String? -} -#endif diff --git a/Tests/MapboxDirectionsTests/DirectionsTests.swift b/Tests/MapboxDirectionsTests/DirectionsTests.swift index 81e350af4..fc3865983 100644 --- a/Tests/MapboxDirectionsTests/DirectionsTests.swift +++ b/Tests/MapboxDirectionsTests/DirectionsTests.swift @@ -12,7 +12,7 @@ import Turf @testable import MapboxDirections let BogusToken = "pk.feedCafeDadeDeadBeef-BadeBede.FadeCafeDadeDeed-BadeBede" -let BogusCredentials = DirectionsCredentials(accessToken: BogusToken) +let BogusCredentials = Credentials(accessToken: BogusToken) let BadResponse = """ diff --git a/Tests/MapboxDirectionsTests/IsochroneTests.swift b/Tests/MapboxDirectionsTests/IsochroneTests.swift new file mode 100644 index 000000000..7279a642e --- /dev/null +++ b/Tests/MapboxDirectionsTests/IsochroneTests.swift @@ -0,0 +1,197 @@ +import Foundation +@testable import MapboxDirections +#if !os(Linux) +import OHHTTPStubs +#if SWIFT_PACKAGE +import OHHTTPStubsSwift +#endif +#endif +import Turf +import XCTest + +let IsochroneBogusCredentials = Credentials(accessToken: BogusToken) + +let minimalValidResponse = """ +{ + "features": [], + "type": "FeatureCollection" +} +""" + +class IsochroneTests: XCTestCase { + + override func tearDown() { + #if !os(Linux) + HTTPStubs.removeAllStubs() + #endif + super.tearDown() + } + + func testConfiguration() { + let isochrones = Isochrones(credentials: IsochroneBogusCredentials) + XCTAssertEqual(isochrones.credentials, IsochroneBogusCredentials) + } + + func testRequest() { + let location = LocationCoordinate2D(latitude: 0, longitude: 1) + let radius1 = Measurement(value: 99.5, unit: UnitLength.meters) + let radius2 = Measurement(value: 0.2, unit: UnitLength.kilometers) + + #if !os(Linux) + let options = IsochroneOptions(centerCoordinate: location, + contours: .byDistances([ + .init(value: radius1, color: .init(red: 0.1, green: 0.2, blue: 0.3, alpha: 1.0)), + .init(value: radius2, color: .init(red: 0.4, green: 0.5, blue: 0.6, alpha: 1.0)) + ])) + #else + let contour1 = IsochroneOptions.Contours.Definition(value: radius1, color: IsochroneOptions.Color(red: 25, green: 51, blue: 76)) + let contour2 = IsochroneOptions.Contours.Definition(value: radius2, color: IsochroneOptions.Color(red: 102, green: 127, blue: 153)) + let options = IsochroneOptions(centerCoordinate: location, + contours: IsochroneOptions.Contours.byDistances([ + contour1, + contour2 + ])) + #endif + options.contoursFormat = IsochroneOptions.ContourFormat.polygon + options.denoisingFactor = 0.5 + options.simplificationTolerance = 13 + + let isochrones = Isochrones(credentials: IsochroneBogusCredentials) + var url = isochrones.url(forCalculating: options) + let request = isochrones.urlRequest(forCalculating: options) + + guard let components = URLComponents(string: url.absoluteString), + let queryItems = components.queryItems else { + XCTFail("Invalid url"); return + } + XCTAssertEqual(queryItems.count, 6) + XCTAssertTrue(components.path.contains(location.requestDescription) ) + XCTAssertTrue(queryItems.contains(where: { $0.name == "access_token" && $0.value == BogusToken })) + XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_meters" && $0.value == "100,200"})) + XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_colors" && $0.value == "19334C,667F99"})) + XCTAssertTrue(queryItems.contains(where: { $0.name == "polygons" && $0.value == "true"})) + XCTAssertTrue(queryItems.contains(where: { $0.name == "denoise" && $0.value == "0.5"})) + XCTAssertTrue(queryItems.contains(where: { $0.name == "generalize" && $0.value == "13.0"})) + + XCTAssertEqual(request.httpMethod, "GET") + XCTAssertEqual(request.url, url) + + options.contours = IsochroneOptions.Contours.byExpectedTravelTimes([ + IsochroneOptions.Contours.Definition(value: 31, unit: UnitDuration.seconds), + IsochroneOptions.Contours.Definition(value: 2.1, unit: UnitDuration.minutes) + ]) + + url = isochrones.url(forCalculating: options) + + guard let componentsByTravelTime = URLComponents(string: url.absoluteString), + let queryItemsByTravelTime = componentsByTravelTime.queryItems else { + XCTFail("Invalid url"); return + } + + XCTAssertTrue(queryItemsByTravelTime.contains(where: { $0.name == "contours_minutes" && $0.value == "1,2"})) + } + + #if !os(Linux) + func testMinimalValidResponse() { + HTTPStubs.stubRequests(passingTest: { (request) -> Bool in + return request.url!.absoluteString.contains("https://api.mapbox.com/isochrone") + }) { (_) -> HTTPStubsResponse in + return HTTPStubsResponse(data: minimalValidResponse.data(using: .utf8)!, statusCode: 200, headers: ["Content-Type" : "text/html"]) + } + let expectation = self.expectation(description: "Async callback") + let isochrones = Isochrones(credentials: IsochroneBogusCredentials) + let options = IsochroneOptions(centerCoordinate: LocationCoordinate2D(latitude: 0, longitude: 1), + contours: .byDistances([.init(value: 100, + unit: .meters)])) + isochrones.calculate(options, completionHandler: { (session, result) in + defer { expectation.fulfill() } + + guard case let .success(featureCollection) = result else { + XCTFail("Expecting success, error returned. \(result)") + return + } + + guard featureCollection.features.isEmpty else { + XCTFail("Wrong feature decoding.") + return + } + }) + wait(for: [expectation], timeout: 2.0) + } + + func testUnknownBadResponse() { + let message = "Lorem ipsum." + HTTPStubs.stubRequests(passingTest: { (request) -> Bool in + return request.url!.absoluteString.contains("https://api.mapbox.com/isochrone") + }) { (_) -> HTTPStubsResponse in + return HTTPStubsResponse(data: message.data(using: .utf8)!, statusCode: 420, headers: ["Content-Type" : "text/plain"]) + } + let expectation = self.expectation(description: "Async callback") + let isochrones = Isochrones(credentials: IsochroneBogusCredentials) + let options = IsochroneOptions(centerCoordinate: LocationCoordinate2D(latitude: 0, longitude: 1), + contours: .byDistances([.init(value: 100, unit: .meters)])) + isochrones.calculate(options, completionHandler: { (session, result) in + defer { expectation.fulfill() } + + guard case let .failure(error) = result else { + XCTFail("Expecting an error, none returned. \(result)") + return + } + + guard case .invalidResponse(_) = error else { + XCTFail("Wrong error type returned.") + return + } + }) + wait(for: [expectation], timeout: 2.0) + } + + func testDownNetwork() { + let notConnected = NSError(domain: NSURLErrorDomain, code: URLError.notConnectedToInternet.rawValue) as! URLError + + HTTPStubs.stubRequests(passingTest: { (request) -> Bool in + return request.url!.absoluteString.contains("https://api.mapbox.com/isochrone") + }) { (_) -> HTTPStubsResponse in + return HTTPStubsResponse(error: notConnected) + } + + let expectation = self.expectation(description: "Async callback") + let isochrones = Isochrones(credentials: IsochroneBogusCredentials) + let options = IsochroneOptions(centerCoordinate: LocationCoordinate2D(latitude: 0, longitude: 1), + contours: .byDistances([.init(value: 100, unit: .meters)])) + isochrones.calculate(options, completionHandler: { (session, result) in + defer { expectation.fulfill() } + + guard case let .failure(error) = result else { + XCTFail("Error expected, none returned. \(result)") + return + } + + guard case let .network(err) = error else { + XCTFail("Wrong error type returned. \(error)") + return + } + + // Comparing just the code and domain to avoid comparing unessential `UserInfo` that might be added. + XCTAssertEqual(type(of: err).errorDomain, type(of: notConnected).errorDomain) + XCTAssertEqual(err.code, notConnected.code) + }) + wait(for: [expectation], timeout: 2.0) + } + + func testRateLimitErrorParsing() { + let url = URL(string: "https://api.mapbox.com")! + let headerFields = ["X-Rate-Limit-Interval" : "60", "X-Rate-Limit-Limit" : "600", "X-Rate-Limit-Reset" : "1479460584"] + let response = HTTPURLResponse(url: url, statusCode: 429, httpVersion: nil, headerFields: headerFields) + + let resultError = IsochroneError(code: "429", message: "Hit rate limit", response: response, underlyingError: nil) + if case let .rateLimited(rateLimitInterval, rateLimit, resetTime) = resultError { + XCTAssertEqual(rateLimitInterval, 60.0) + XCTAssertEqual(rateLimit, 600) + XCTAssertEqual(resetTime, Date(timeIntervalSince1970: 1479460584)) + } else { + XCTFail("Code 429 should be interpreted as a rate limiting error.") + } + } + #endif +} diff --git a/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift b/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift index a17efdd1c..0d88fc527 100644 --- a/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift +++ b/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift @@ -13,7 +13,7 @@ class OfflineDirectionsTests: XCTestCase { let hostURL = URL(string: "https://api.mapbox.com")! func testAvailableVersions() { - let credentials = DirectionsCredentials(accessToken: token, host: hostURL) + let credentials = Credentials(accessToken: token, host: hostURL) let directions = Directions(credentials: credentials) diff --git a/Tests/MapboxDirectionsTests/RouteOptionsTests.swift b/Tests/MapboxDirectionsTests/RouteOptionsTests.swift index a9f2b2f18..25f11a51d 100644 --- a/Tests/MapboxDirectionsTests/RouteOptionsTests.swift +++ b/Tests/MapboxDirectionsTests/RouteOptionsTests.swift @@ -140,7 +140,7 @@ class RouteOptionsTests: XCTestCase { let subject = RouteOptions(waypoints: waypoints) let decoder = JSONDecoder() decoder.userInfo[.options] = subject - decoder.userInfo[.credentials] = DirectionsCredentials(accessToken: "foo", host: URL(string: "https://test.website")!) + decoder.userInfo[.credentials] = Credentials(accessToken: "foo", host: URL(string: "https://test.website")!) var response: RouteResponse? XCTAssertNoThrow(response = try decoder.decode(RouteResponse.self, from: fixtureData)) XCTAssertNotNil(response) diff --git a/Tests/MapboxDirectionsTests/RouteResponseTests.swift b/Tests/MapboxDirectionsTests/RouteResponseTests.swift index cb31b40b9..e5afa1ca4 100644 --- a/Tests/MapboxDirectionsTests/RouteResponseTests.swift +++ b/Tests/MapboxDirectionsTests/RouteResponseTests.swift @@ -19,7 +19,7 @@ class RouteResponseTests: XCTestCase { let responseOptions = ResponseOptions.route(routeOptions) let accessToken = "deadbeefcafebebe" let host = URL(string: "https://example.com")! - let directionsCredentials = DirectionsCredentials(accessToken: accessToken, host: host) + let directionsCredentials = Credentials(accessToken: accessToken, host: host) let routeResponse = RouteResponse(httpResponse: nil, waypoints: waypoints, diff --git a/Tests/MapboxDirectionsTests/RouteStepTests.swift b/Tests/MapboxDirectionsTests/RouteStepTests.swift index 830679385..c996fdc7d 100644 --- a/Tests/MapboxDirectionsTests/RouteStepTests.swift +++ b/Tests/MapboxDirectionsTests/RouteStepTests.swift @@ -315,7 +315,7 @@ class RouteStepTests: XCTestCase { let decoder = JSONDecoder() decoder.userInfo[.options] = options - decoder.userInfo[.credentials] = DirectionsCredentials(accessToken: "foo", host: URL(string: "http://sample.website")) + decoder.userInfo[.credentials] = Credentials(accessToken: "foo", host: URL(string: "http://sample.website")) let result = try! decoder.decode(RouteResponse.self, from: data) let routes = result.routes diff --git a/Tests/MapboxDirectionsTests/V5Tests.swift b/Tests/MapboxDirectionsTests/V5Tests.swift index d3258dd25..e38072d30 100644 --- a/Tests/MapboxDirectionsTests/V5Tests.swift +++ b/Tests/MapboxDirectionsTests/V5Tests.swift @@ -292,7 +292,7 @@ class V5Tests: XCTestCase { let decoder = JSONDecoder() decoder.userInfo[.options] = options - decoder.userInfo[.credentials] = DirectionsCredentials(accessToken: "foo", host: URL(string: "http://sample.website")) + decoder.userInfo[.credentials] = Credentials(accessToken: "foo", host: URL(string: "http://sample.website")) let result = try! decoder.decode(RouteResponse.self, from: data) let routes = result.routes diff --git a/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift b/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift index 15fbc58f9..1705b35a9 100644 --- a/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift +++ b/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift @@ -10,7 +10,7 @@ class WalkingOptionsTests: XCTestCase { Waypoint(coordinate: LocationCoordinate2D(latitude: 2, longitude: 3)) ] - let options = RouteOptions(waypoints: waypoints, profileIdentifier: DirectionsProfileIdentifier.walking) + let options = RouteOptions(waypoints: waypoints, profileIdentifier: ProfileIdentifier.walking) var queryItems = options.urlQueryItems XCTAssertNil(queryItems.first { $0.name == "alley_bias" }?.value) XCTAssertNil(queryItems.first { $0.name == "walkway_bias" }?.value)