diff --git a/CHANGELOG.md b/CHANGELOG.md index 80e9e7646..fcb5b81b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ * Removed `TransportType.none`, `ManeuverType.none`, and `ManeuverDirection.none`. Unrecognized `TransportType` and `ManeuverDirection` values now raise decoding errors. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) * `RouteStep.maneuverType` is now optional. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) * Renamed the `Tracepoint.alternateCount` property to `Tracepoint.countOfAlternatives`. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) +* The `Intersection.approachIndex` and `Intersection.outletIndex` properties are now optional, not −1, in the case of a departure or arrival maneuver. ([#393](https://github.com/mapbox/MapboxDirections.swift/pull/393)) +* Added initializers for `Route`, `Match`, `RouteLeg`, and `RouteStep`. ([#393](https://github.com/mapbox/MapboxDirections.swift/pull/393)) +* Various properties of `Route`, `RouteLeg`, and `RouteStep` are now writable. ([#393](https://github.com/mapbox/MapboxDirections.swift/pull/393)) ## v0.30.0 diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index 8a0b5e22e..00b045332 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -251,6 +251,12 @@ DADD27C81E5AAE3100D31FAD /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DADD27C71E5AAE3100D31FAD /* Launch Screen.storyboard */; }; DADD27F31E5ABD4300D31FAD /* Mapbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DADD27F21E5ABD4300D31FAD /* Mapbox.framework */; }; DADD27F71E5AC8E900D31FAD /* MapboxDirections.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA6C9D881CAE442B00094FBC /* MapboxDirections.framework */; }; + DAE2DF6823AECB120065057A /* QuickLookTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2DF6723AECB120065057A /* QuickLookTests.swift */; }; + DAE2DF6923AECB120065057A /* QuickLookTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2DF6723AECB120065057A /* QuickLookTests.swift */; }; + DAE2DF6A23AECB120065057A /* QuickLookTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2DF6723AECB120065057A /* QuickLookTests.swift */; }; + DAE2DF6C23AED2280065057A /* RouteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2DF6B23AED2280065057A /* RouteTests.swift */; }; + DAE2DF6D23AED2280065057A /* RouteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2DF6B23AED2280065057A /* RouteTests.swift */; }; + DAE2DF6E23AED2280065057A /* RouteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2DF6B23AED2280065057A /* RouteTests.swift */; }; DAE33A1B1F215DF600C06039 /* IntersectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE33A1A1F215DF600C06039 /* IntersectionTests.swift */; }; DAE33A1C1F215DF600C06039 /* IntersectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE33A1A1F215DF600C06039 /* IntersectionTests.swift */; }; DAE33A1D1F215DF600C06039 /* IntersectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE33A1A1F215DF600C06039 /* IntersectionTests.swift */; }; @@ -402,6 +408,8 @@ DADD27C31E5AAAD800D31FAD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DADD27C71E5AAE3100D31FAD /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; DADD27F21E5ABD4300D31FAD /* Mapbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Mapbox.framework; path = Carthage/Build/iOS/Mapbox.framework; sourceTree = ""; }; + DAE2DF6723AECB120065057A /* QuickLookTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookTests.swift; sourceTree = ""; }; + DAE2DF6B23AED2280065057A /* RouteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteTests.swift; sourceTree = ""; }; DAE33A1A1F215DF600C06039 /* IntersectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntersectionTests.swift; sourceTree = ""; }; DAE9E0F31EB7DE2E001E8E8B /* RouteOptionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RouteOptionsTests.swift; path = Tests/MapboxDirectionsTests/RouteOptionsTests.swift; sourceTree = SOURCE_ROOT; }; DD6254731AE70CB700017857 /* Directions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Directions.swift; sourceTree = ""; }; @@ -635,7 +643,9 @@ 3556CE9922649CF2009397B5 /* MapboxDirectionsTests-Bridging-Header.h */, C5DAACAE201AA92B001F9261 /* MatchTests.swift */, 35DBF013217E199E0009D2AE /* OfflineDirectionsTests.swift */, + DAE2DF6723AECB120065057A /* QuickLookTests.swift */, C59666382048A20E00C45CE5 /* RoutableMatchTests.swift */, + DAE2DF6B23AED2280065057A /* RouteTests.swift */, DAE9E0F31EB7DE2E001E8E8B /* RouteOptionsTests.swift */, F4D785EE1DDD82C100FF4665 /* RouteStepTests.swift */, DAABF78D2395ABA900CEEB61 /* SpokenInstructionTests.swift */, @@ -1196,6 +1206,7 @@ buildActionMask = 2147483647; files = ( DAE9E0F51EB7DE2E001E8E8B /* RouteOptionsTests.swift in Sources */, + DAE2DF6D23AED2280065057A /* RouteTests.swift in Sources */, 35DBF015217E199E0009D2AE /* OfflineDirectionsTests.swift in Sources */, C53A02291E92C27A009837BD /* AnnotationTests.swift in Sources */, DA688B3F21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, @@ -1205,6 +1216,7 @@ DAE33A1C1F215DF600C06039 /* IntersectionTests.swift in Sources */, C5DAACB0201AA92B001F9261 /* MatchTests.swift in Sources */, DA1A10CE1D00F972009F82FA /* Fixture.swift in Sources */, + DAE2DF6923AECB120065057A /* QuickLookTests.swift in Sources */, DA1A110C1D01045E009F82FA /* DirectionsTests.swift in Sources */, C5D1D7F31F6AFBD600A1C4F1 /* VisualInstructionTests.swift in Sources */, DA4F84EE21C08BFB008A0434 /* WaypointTests.swift in Sources */, @@ -1265,6 +1277,7 @@ buildActionMask = 2147483647; files = ( DAE9E0F61EB7DE2E001E8E8B /* RouteOptionsTests.swift in Sources */, + DAE2DF6E23AED2280065057A /* RouteTests.swift in Sources */, 35DBF016217E199E0009D2AE /* OfflineDirectionsTests.swift in Sources */, C53A022A1E92C27B009837BD /* AnnotationTests.swift in Sources */, DA688B4021B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, @@ -1274,6 +1287,7 @@ DAE33A1D1F215DF600C06039 /* IntersectionTests.swift in Sources */, C5DAACB1201AA92B001F9261 /* MatchTests.swift in Sources */, DA1A10F51D010251009F82FA /* Fixture.swift in Sources */, + DAE2DF6A23AECB120065057A /* QuickLookTests.swift in Sources */, DA1A110D1D01045E009F82FA /* DirectionsTests.swift in Sources */, C5D1D7F41F6AFBD600A1C4F1 /* VisualInstructionTests.swift in Sources */, DA4F84EF21C08BFB008A0434 /* WaypointTests.swift in Sources */, @@ -1378,6 +1392,7 @@ buildActionMask = 2147483647; files = ( DAE9E0F41EB7DE2E001E8E8B /* RouteOptionsTests.swift in Sources */, + DAE2DF6C23AED2280065057A /* RouteTests.swift in Sources */, 35DBF014217E199E0009D2AE /* OfflineDirectionsTests.swift in Sources */, C5247D711E818A24004B6154 /* AnnotationTests.swift in Sources */, DA688B3E21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, @@ -1387,6 +1402,7 @@ DAE33A1B1F215DF600C06039 /* IntersectionTests.swift in Sources */, C5DAACAF201AA92B001F9261 /* MatchTests.swift in Sources */, DA6C9DB21CAECA0E00094FBC /* Fixture.swift in Sources */, + DAE2DF6823AECB120065057A /* QuickLookTests.swift in Sources */, DA1A110B1D01045E009F82FA /* DirectionsTests.swift in Sources */, C52CE3931F6AF6E70069963D /* VisualInstructionTests.swift in Sources */, DA4F84ED21C08BFB008A0434 /* WaypointTests.swift in Sources */, diff --git a/Sources/MapboxDirections/AttributeOptions.swift b/Sources/MapboxDirections/AttributeOptions.swift index 0406ec732..1504e663f 100644 --- a/Sources/MapboxDirections/AttributeOptions.swift +++ b/Sources/MapboxDirections/AttributeOptions.swift @@ -87,7 +87,7 @@ public struct AttributeOptions: OptionSet, CustomStringConvertible { extension AttributeOptions: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - try container.encode(description.components(separatedBy: ",")) + try container.encode(description.components(separatedBy: ",").filter { !$0.isEmpty }) } public init(from decoder: Decoder) throws { diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index d79a1e2b4..6f110796a 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -178,12 +178,16 @@ open class Directions: NSObject { let result = try decoder.decode(RouteResponse.self, from: data) guard (result.code == nil && result.message == nil) || result.code == "Ok" else { let apiError = Directions.informativeError(code: result.code, message: result.message, response: response, underlyingError: possibleError) - completionHandler(nil, nil, apiError) + DispatchQueue.main.async { + completionHandler(nil, nil, apiError) + } return } guard let routes = result.routes else { - completionHandler(result.waypoints, nil, .unableToRoute) + DispatchQueue.main.async { + completionHandler(result.waypoints, nil, .unableToRoute) + } return } @@ -243,12 +247,16 @@ open class Directions: NSObject { let result = try decoder.decode(MatchResponse.self, from: data) guard result.code == "Ok" else { let apiError = Directions.informativeError(code: result.code, message: result.message, response: response, underlyingError: possibleError) - completionHandler(nil, apiError) + DispatchQueue.main.async { + completionHandler(nil, apiError) + } return } guard let matches = result.matches else { - completionHandler(nil, .unableToRoute) + DispatchQueue.main.async { + completionHandler(nil, .unableToRoute) + } return } @@ -307,12 +315,16 @@ open class Directions: NSObject { let result = try decoder.decode(MapMatchingResponse.self, from: data) guard result.code == "Ok" else { let apiError = Directions.informativeError(code: result.code, message:nil, response: response, underlyingError: possibleError) - completionHandler(nil, nil, apiError) + DispatchQueue.main.async { + completionHandler(nil, nil, apiError) + } return } guard let routes = result.routes else { - completionHandler(result.waypoints, nil, .unableToRoute) + DispatchQueue.main.async { + completionHandler(result.waypoints, nil, .unableToRoute) + } return } diff --git a/Sources/MapboxDirections/DirectionsOptions.swift b/Sources/MapboxDirections/DirectionsOptions.swift index 0eb831f05..204a8bfc4 100644 --- a/Sources/MapboxDirections/DirectionsOptions.swift +++ b/Sources/MapboxDirections/DirectionsOptions.swift @@ -145,12 +145,12 @@ open class DirectionsOptions: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(waypoints, forKey: .waypoints) - try container.encode(profileIdentifier.rawValue, forKey: .profileIdentifier) + try container.encode(profileIdentifier, forKey: .profileIdentifier) try container.encode(includesSteps, forKey: .includesSteps) try container.encode(shapeFormat, forKey: .shapeFormat) try container.encode(routeShapeResolution, forKey: .routeShapeResolution) try container.encode(attributeOptions, forKey: .attributeOptions) - try container.encode(locale, forKey: .locale) + try container.encode(locale.identifier, forKey: .locale) try container.encode(includesSpokenInstructions, forKey: .includesSpokenInstructions) try container.encode(distanceMeasurementSystem, forKey: .distanceMeasurementSystem) try container.encode(includesVisualInstructions, forKey: .includesVisualInstructions) @@ -168,7 +168,8 @@ open class DirectionsOptions: Codable { shapeFormat = try container.decode(RouteShapeFormat.self, forKey: .shapeFormat) routeShapeResolution = try container.decode(RouteShapeResolution.self, forKey: .routeShapeResolution) attributeOptions = try container.decode(AttributeOptions.self, forKey: .attributeOptions) - locale = try container.decode(Locale.self, forKey: .locale) + let identifier = try container.decode(String.self, forKey: .locale) + locale = Locale(identifier: identifier) includesSpokenInstructions = try container.decode(Bool.self, forKey: .includesSpokenInstructions) distanceMeasurementSystem = try container.decode(MeasurementSystem.self, forKey: .distanceMeasurementSystem) includesVisualInstructions = try container.decode(Bool.self, forKey: .includesVisualInstructions) @@ -352,20 +353,20 @@ open class DirectionsOptions: Codable { return queryItems } - private var bearings: String? { - if waypoints.compactMap({ $0.heading }).isEmpty { + var bearings: String? { + guard waypoints.contains(where: { $0.heading ?? -1 >= 0 }) else { return nil } return waypoints.map({ $0.headingDescription }).joined(separator: ";") } - private var radiuses: String? { - guard !self.waypoints.filter({ $0.coordinateAccuracy != nil}).isEmpty else { + var radiuses: String? { + guard waypoints.contains(where: { $0.coordinateAccuracy ?? -1 >= 0 }) else { return nil } - let accuracies = self.waypoints.map { (waypoint) -> String in - guard let accuracy = waypoint.coordinateAccuracy else { + let accuracies = self.waypoints.map { (waypoint) -> String in + guard let accuracy = waypoint.coordinateAccuracy, accuracy >= 0 else { return "unlimited" } return String(accuracy) diff --git a/Sources/MapboxDirections/DirectionsResult.swift b/Sources/MapboxDirections/DirectionsResult.swift index f5db27075..dc614272c 100644 --- a/Sources/MapboxDirections/DirectionsResult.swift +++ b/Sources/MapboxDirections/DirectionsResult.swift @@ -23,6 +23,14 @@ open class DirectionsResult: Codable { // MARK: Creating a Directions Result + init(legs: [RouteLeg], shape: LineString?, distance: CLLocationDistance, expectedTravelTime: TimeInterval, options: DirectionsOptions) { + self.legs = legs + self.shape = shape + self.distance = distance + self.expectedTravelTime = expectedTravelTime + _directionsOptions = options + } + public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) legs = try container.decode([RouteLeg].self, forKey: .legs) @@ -54,14 +62,10 @@ open class DirectionsResult: Codable { apiEndpoint = try container.decodeIfPresent(URL.self, forKey: .apiEndpoint) routeIdentifier = try container.decodeIfPresent(String.self, forKey: .routeIdentifier) - do { - speechLocale = try container.decodeIfPresent(Locale.self, forKey: .speechLocale) - } catch let DecodingError.typeMismatch(mismatchedType, context) { - guard mismatchedType == [String: Any].self else { - throw DecodingError.typeMismatch(mismatchedType, context) - } - let identifier = try container.decode(String.self, forKey: .speechLocale) + if let identifier = try container.decodeIfPresent(String.self, forKey: .speechLocale) { speechLocale = Locale(identifier: identifier) + } else { + speechLocale = nil } } @@ -77,15 +81,15 @@ open class DirectionsResult: Codable { try container.encodeIfPresent(accessToken, forKey: .accessToken) try container.encodeIfPresent(apiEndpoint, forKey: .apiEndpoint) try container.encodeIfPresent(routeIdentifier, forKey: .routeIdentifier) - try container.encodeIfPresent(speechLocale, forKey: .speechLocale) + try container.encodeIfPresent(speechLocale?.identifier, forKey: .speechLocale) } // MARK: Getting the Shape of the Route /** - An array of geographic coordinates defining the path of the route from start to finish. + The roads or paths taken as a contiguous polyline. - This array may be `nil` or simplified depending on the `DirectionsOptions.routeShapeResolution` property of the original `RouteOptions` or `MatchOptions` object. + The shape may be `nil` or simplified depending on the `DirectionsOptions.routeShapeResolution` property of the original `RouteOptions` or `MatchOptions` object. Using the [Mapbox Maps SDK for iOS](https://docs.mapbox.com/ios/maps/) or [Mapbox Maps SDK for macOS](https://mapbox.github.io/mapbox-gl-native/macos/), you can create an `MGLPolyline` object using these coordinates to display an overview of the route on an `MGLMapView`. */ @@ -94,7 +98,7 @@ open class DirectionsResult: Codable { // MARK: Getting the Legs Along the Route /** - An array of `RouteLeg` objects representing the legs of the route. + The legs that are traversed in order. The number of legs in this array depends on the number of waypoints. A route with two waypoints (the source and destination) has one leg, a route with three waypoints (the source, an intermediate waypoint, and the destination) has two legs, and so on. @@ -131,7 +135,7 @@ open class DirectionsResult: Codable { 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. */ - public let expectedTravelTime: TimeInterval + open var expectedTravelTime: TimeInterval // MARK: Configuring Speech Synthesis @@ -145,7 +149,7 @@ open class DirectionsResult: Codable { // MARK: Reproducing the Route /** - `RouteOptions` used to create the directions request. + Criteria for reproducing this route. The route options object’s profileIdentifier property reflects the primary mode of transportation used for the route. Individual steps along the route might use different modes of transportation as necessary. */ diff --git a/Sources/MapboxDirections/Intersection.swift b/Sources/MapboxDirections/Intersection.swift index 5ab3b5625..705b2751c 100644 --- a/Sources/MapboxDirections/Intersection.swift +++ b/Sources/MapboxDirections/Intersection.swift @@ -54,13 +54,17 @@ public struct Intersection { /** The index of the item in the `headings` array that corresponds to the road that the containing route step uses to approach the intersection. + + This property is set to `nil` for a departure maneuver. */ - public let approachIndex: Int + public let approachIndex: Int? /** The index of the item in the `headings` array that corresponds to the road that the containing route step uses to leave the intersection. + + This property is set to `nil` for an arrival maneuver. */ - public let outletIndex: Int + public let outletIndex: Int? /** The road classes of the road that the containing step uses to leave the intersection. @@ -102,11 +106,10 @@ extension Intersection: Codable { try container.encode(location, forKey: .location) try container.encode(headings, forKey: .headings) - try container.encode(approachIndex, forKey: .approachIndex) - try container.encode(outletIndex, forKey: .outletIndex) + try container.encodeIfPresent(approachIndex, forKey: .approachIndex) + try container.encodeIfPresent(outletIndex, forKey: .outletIndex) - var outletArray: [Bool] = Array.init(repeating: false, count: (outletIndexes.max()! + 1)) - + var outletArray = headings.map { _ in false } for index in outletIndexes { outletArray[index] = true } @@ -121,9 +124,9 @@ extension Intersection: Codable { lanes![i].isValid = true } } - try container.encode(lanes, forKey: .lanes) + try container.encodeIfPresent(lanes, forKey: .lanes) - if let classes = outletRoadClasses?.description.components(separatedBy: ",") { + if let classes = outletRoadClasses?.description.components(separatedBy: ",").filter({ !$0.isEmpty }) { try container.encode(classes, forKey: .outletRoadClasses) } } @@ -145,8 +148,8 @@ extension Intersection: Codable { let outletsArray = try container.decode([Bool].self, forKey: .outletIndexes) outletIndexes = outletsArray.indices { $0 } - outletIndex = try container.decodeIfPresent(Int.self, forKey: .outletIndex) ?? -1 - approachIndex = try container.decodeIfPresent(Int.self, forKey: .approachIndex) ?? -1 + outletIndex = try container.decodeIfPresent(Int.self, forKey: .outletIndex) + approachIndex = try container.decodeIfPresent(Int.self, forKey: .approachIndex) } } diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index 4520bb18d..6bb2c6c8b 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -1,23 +1,26 @@ import Foundation -class MapMatchingResponse: Decodable { - var code: String - var routes : [Route]? - var waypoints: [Waypoint] - +public struct MapMatchingResponse { + public var code: String + public var routes : [Route]? + public var waypoints: [Waypoint] +} + +extension MapMatchingResponse: Decodable { private enum CodingKeys: String, CodingKey { case code case matches = "matchings" case tracepoints } - public required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) code = try container.decode(String.self, forKey: .code) routes = try container.decodeIfPresent([Route].self, forKey: .matches) // Decode waypoints from the response and update their names according to the waypoints from DirectionsOptions.waypoints. - let decodedWaypoints = try container.decode([Waypoint].self, forKey: .tracepoints) + // Map Matching API responses can contain null tracepoints. Null tracepoints can’t correspond to waypoints, so they’re irrelevant to the decoded structure. + let decodedWaypoints = try container.decode([Waypoint?].self, forKey: .tracepoints).compactMap { $0 } if let options = decoder.userInfo[.options] as? DirectionsOptions { // The response lists the same number of tracepoints as the waypoints in the request, whether or not a given waypoint is leg-separating. waypoints = zip(decodedWaypoints, options.waypoints).map { (pair) -> Waypoint in diff --git a/Sources/MapboxDirections/MapMatching/Match.swift b/Sources/MapboxDirections/MapMatching/Match.swift index 1a82e3464..b2052555a 100644 --- a/Sources/MapboxDirections/MapMatching/Match.swift +++ b/Sources/MapboxDirections/MapMatching/Match.swift @@ -1,6 +1,7 @@ import Foundation import CoreLocation import Polyline +import struct Turf.LineString extension CodingUserInfoKey { static let tracepoints = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.tracepoints")! @@ -8,7 +9,7 @@ extension CodingUserInfoKey { /** A `Match` object defines a single route that was created from a series of points that were matched against a road network. - Typically, you do not create instances of this class directly. Instead, you receive match objects when you pass a `MatchOptions` object into the `Directions.calculate(_:completionHandler:)` or `Directions.calculateRoutes(matching:completionHandler:)` method. + Typically, you do not create instances of this class directly. Instead, you receive match objects when you pass a `MatchOptions` object into the `Directions.calculate(_:completionHandler:)` method. */ open class Match: DirectionsResult { private enum CodingKeys: String, CodingKey { @@ -17,6 +18,26 @@ open class Match: DirectionsResult { case matchOptions } + /** + Initializes a match. + + Typically, you do not create instances of this class directly. Instead, you receive match objects when you request matches using the `Directions.calculate(_:completionHandler:)` method. + + - parameter legs: The legs that are traversed in order. + - parameter shape: The matching roads or paths as a contiguous polyline. + - parameter distance: The matched path’s cumulative distance, measured in meters. + - parameter expectedTravelTime: The route’s expected travel time, measured in seconds. + - parameter confidence: A number between 0 and 1 that indicates the Map Matching API’s confidence that the match is accurate. A higher confidence means the match is more likely to be accurate. + - parameter tracepoints: Tracepoints on the road network that match the tracepoints in `options`. + - parameter options: The criteria to match. + */ + public init(legs: [RouteLeg], shape: LineString?, distance: CLLocationDistance, expectedTravelTime: TimeInterval, confidence: Float, tracepoints: [Tracepoint?], options: MatchOptions) { + matchOptions = options + self.confidence = confidence + self.tracepoints = tracepoints + super.init(legs: legs, shape: shape, distance: distance, expectedTravelTime: expectedTravelTime, options: options) + } + /** Creates a match from a decoder. @@ -65,3 +86,16 @@ open class Match: DirectionsResult { */ public let matchOptions: MatchOptions } + +extension Match: Equatable { + public static func ==(lhs: Match, rhs: Match) -> Bool { + return lhs.routeIdentifier == rhs.routeIdentifier && + lhs.distance == rhs.distance && + lhs.expectedTravelTime == rhs.expectedTravelTime && + lhs.speechLocale == rhs.speechLocale && + lhs.confidence == rhs.confidence && + lhs.tracepoints == rhs.tracepoints && + lhs.legs == rhs.legs && + lhs.shape == rhs.shape + } +} diff --git a/Sources/MapboxDirections/MapMatching/MatchResponse.swift b/Sources/MapboxDirections/MapMatching/MatchResponse.swift index 328186d59..a665a6cbd 100644 --- a/Sources/MapboxDirections/MapMatching/MatchResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MatchResponse.swift @@ -1,11 +1,13 @@ import Foundation -class MatchResponse: Codable { - var code: String - var message: String? - var matches : [Match]? - var tracepoints: [Tracepoint?]? - +public struct MatchResponse { + public var code: String + public var message: String? + public var matches : [Match]? + public var tracepoints: [Tracepoint?]? +} + +extension MatchResponse: Codable { private enum CodingKeys: String, CodingKey { case code case message @@ -13,7 +15,7 @@ class MatchResponse: Codable { case tracepoints } - public required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) code = try container.decode(String.self, forKey: .code) message = try container.decodeIfPresent(String.self, forKey: .message) diff --git a/Sources/MapboxDirections/QuickLook.swift b/Sources/MapboxDirections/QuickLook.swift index a8b08bd3b..83d1dd7b2 100644 --- a/Sources/MapboxDirections/QuickLook.swift +++ b/Sources/MapboxDirections/QuickLook.swift @@ -15,8 +15,8 @@ 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) -> URL? { - guard let accessToken = defaultAccessToken else { +func debugQuickLookURL(illustrating shape: LineString, profileIdentifier: DirectionsProfileIdentifier = .automobile, accessToken: String? = defaultAccessToken) -> URL? { + guard let accessToken = accessToken else { return nil } @@ -48,5 +48,5 @@ func debugQuickLookURL(illustrating shape: LineString, profileIdentifier: Direct URLQueryItem(name: "access_token", value: accessToken), ] - return URL(string: "https://api.mapbox.com\(path)?\(components.percentEncodedQuery!)") + return URL(string: "\(defaultApiEndPointURLString ?? "https://api.mapbox.com")\(path)?\(components.percentEncodedQuery!)") } diff --git a/Sources/MapboxDirections/RoadClasses.swift b/Sources/MapboxDirections/RoadClasses.swift index e2cfcbfc1..8cc0fd519 100644 --- a/Sources/MapboxDirections/RoadClasses.swift +++ b/Sources/MapboxDirections/RoadClasses.swift @@ -60,8 +60,8 @@ public struct RoadClasses: OptionSet, CustomStringConvertible { roadClasses.insert(.ferry) case "tunnel": roadClasses.insert(.tunnel) - case "none": - break + case "": + continue default: return nil } @@ -70,10 +70,6 @@ public struct RoadClasses: OptionSet, CustomStringConvertible { } public var description: String { - if isEmpty { - return "" - } - var descriptions: [String] = [] if contains(.toll) { descriptions.append("toll") @@ -97,7 +93,7 @@ public struct RoadClasses: OptionSet, CustomStringConvertible { extension RoadClasses: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - try container.encode(description.components(separatedBy: ",")) + try container.encode(description.components(separatedBy: ",").filter { !$0.isEmpty }) } public init(from decoder: Decoder) throws { diff --git a/Sources/MapboxDirections/Route.swift b/Sources/MapboxDirections/Route.swift index 84a20ff3d..44bfd02d6 100644 --- a/Sources/MapboxDirections/Route.swift +++ b/Sources/MapboxDirections/Route.swift @@ -1,13 +1,32 @@ +import CoreLocation +import Turf + /** A `Route` object defines a single route that the user can follow to visit a series of waypoints in order. The route object includes information about the route, such as its distance and expected travel time. Depending on the criteria used to calculate the route, the route object may also include detailed turn-by-turn instructions. - Typically, you do not create instances of this class directly. Instead, you receive route objects when you request directions using the `Directions.calculate(_:completionHandler:)` method. However, if you use the `Directions.url(forCalculating:)` method instead, you can pass the results of the HTTP request into this class’s initializer. + Typically, you do not create instances of this class directly. Instead, you receive route objects when you request directions using the `Directions.calculate(_:completionHandler:)` or `Directions.calculateRoutes(matching:completionHandler:)` method. However, if you use the `Directions.url(forCalculating:)` method instead, you can use `JSONDecoder` to convert the HTTP response into a `RouteResponse` or `MapMatchingResponse` object and access the `RouteResponse.routes` or `MapMatchingResponse.routes` property. */ open class Route: DirectionsResult { private enum CodingKeys: String, CodingKey { case routeOptions } + /** + Initializes a route. + + Typically, you do not create instances of this class directly. Instead, you receive route objects when you request directions using the `Directions.calculate(_:completionHandler:)` method. + + - parameter legs: The legs that are traversed in order. + - parameter shape: The roads or paths taken as a contiguous polyline. + - parameter distance: The route’s distance, measured in meters. + - parameter expectedTravelTime: The route’s expected travel time, measured in seconds. + - parameter options: The criteria for producing a route with these parameters. + */ + public init(legs: [RouteLeg], shape: LineString?, distance: CLLocationDistance, expectedTravelTime: TimeInterval, options: RouteOptions) { + routeOptions = options + super.init(legs: legs, shape: shape, distance: distance, expectedTravelTime: expectedTravelTime, options: options) + } + /** Creates a route from a decoder. @@ -31,3 +50,14 @@ open class Route: DirectionsResult { } public var routeOptions: RouteOptions } + +extension Route: Equatable { + public static func ==(lhs: Route, rhs: Route) -> Bool { + return lhs.routeIdentifier == rhs.routeIdentifier && + lhs.distance == rhs.distance && + lhs.expectedTravelTime == rhs.expectedTravelTime && + lhs.speechLocale == rhs.speechLocale && + lhs.legs == rhs.legs && + lhs.shape == rhs.shape + } +} diff --git a/Sources/MapboxDirections/RouteLeg.swift b/Sources/MapboxDirections/RouteLeg.swift index 987793ae5..b2169de54 100644 --- a/Sources/MapboxDirections/RouteLeg.swift +++ b/Sources/MapboxDirections/RouteLeg.swift @@ -29,6 +29,27 @@ open class RouteLeg: Codable { // MARK: Creating a Leg + /** + Initializes a route leg. + + - parameter steps: The steps that are traversed in order. + - parameter name: A name that describes the route leg. + - parameter expectedTravelTime: The route leg’s expected travel time, measured in seconds. + - parameter profileIdentifier: The primary mode of transportation for the route leg. + */ + public init(steps: [RouteStep], name: String, distance: CLLocationDistance, expectedTravelTime: TimeInterval, profileIdentifier: DirectionsProfileIdentifier) { + self.steps = steps + self.name = name + self.distance = distance + self.expectedTravelTime = expectedTravelTime + self.profileIdentifier = profileIdentifier + + segmentDistances = nil + expectedSegmentTravelTimes = nil + segmentSpeeds = nil + segmentCongestionLevels = nil + } + /** Creates a route leg from a decoder. @@ -69,11 +90,13 @@ open class RouteLeg: Codable { try container.encode(expectedTravelTime, forKey: .expectedTravelTime) try container.encode(profileIdentifier, forKey: .profileIdentifier) - var annotation = container.nestedContainer(keyedBy: AnnotationCodingKeys.self, forKey: .annotation) - try annotation.encode(segmentDistances, forKey: .segmentDistances) - try annotation.encode(expectedSegmentTravelTimes, forKey: .expectedSegmentTravelTimes) - try annotation.encode(segmentSpeeds, forKey: .segmentSpeeds) - try annotation.encode(segmentCongestionLevels, forKey: .segmentCongestionLevels) + if segmentDistances != nil || expectedSegmentTravelTimes != nil || segmentSpeeds != nil || segmentCongestionLevels != nil { + var annotationContainer = container.nestedContainer(keyedBy: AnnotationCodingKeys.self, forKey: .annotation) + try annotationContainer.encodeIfPresent(segmentDistances, forKey: .segmentDistances) + try annotationContainer.encodeIfPresent(expectedSegmentTravelTimes, forKey: .expectedSegmentTravelTimes) + try annotationContainer.encodeIfPresent(segmentSpeeds, forKey: .segmentSpeeds) + try annotationContainer.encodeIfPresent(segmentCongestionLevels, forKey: .segmentCongestionLevels) + } } // MARK: Getting the Endpoints of the Leg @@ -114,7 +137,7 @@ open class RouteLeg: Codable { This property is set if the `RouteOptions.attributeOptions` property contains `.distance`. */ - public let segmentDistances: [CLLocationDistance]? + open var segmentDistances: [CLLocationDistance]? /** An array containing the expected travel time (measured in seconds) between each coordinate in the route leg geometry. @@ -123,7 +146,7 @@ open class RouteLeg: Codable { This property is set if the `RouteOptions.attributeOptions` property contains `.expectedTravelTime`. */ - public let expectedSegmentTravelTimes: [TimeInterval]? + open var expectedSegmentTravelTimes: [TimeInterval]? /** An array containing the expected average speed (measured in meters per second) between each coordinate in the route leg geometry. @@ -132,7 +155,7 @@ open class RouteLeg: Codable { This property is set if the `RouteOptions.attributeOptions` property contains `.speed`. */ - public let segmentSpeeds: [CLLocationSpeed]? + open var segmentSpeeds: [CLLocationSpeed]? /** An array containing the traffic congestion level along each road segment in the route leg geometry. @@ -143,7 +166,7 @@ open class RouteLeg: Codable { This property is set if the `RouteOptions.attributeOptions` property contains `.congestionLevel`. */ - public let segmentCongestionLevels: [CongestionLevel]? + open var segmentCongestionLevels: [CongestionLevel]? // MARK: Getting Statistics About the Leg @@ -170,12 +193,12 @@ open class RouteLeg: Codable { 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. */ - public let expectedTravelTime: TimeInterval + open var expectedTravelTime: TimeInterval // MARK: Reproducing the Route /** - A string specifying the primary mode of transportation for the route leg. + The primary mode of transportation for the route leg. 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. */ diff --git a/Sources/MapboxDirections/RouteOptions.swift b/Sources/MapboxDirections/RouteOptions.swift index 9ad37999a..3563c3c21 100644 --- a/Sources/MapboxDirections/RouteOptions.swift +++ b/Sources/MapboxDirections/RouteOptions.swift @@ -176,7 +176,7 @@ open class RouteOptions: DirectionsOptions { } if !roadClassesToAvoid.isEmpty { - let allRoadClasses = roadClassesToAvoid.description.components(separatedBy: ",") + let allRoadClasses = roadClassesToAvoid.description.components(separatedBy: ",").filter { !$0.isEmpty } precondition(allRoadClasses.count < 2, "You can only avoid one road class at a time.") if let firstRoadClass = allRoadClasses.first { params.append(URLQueryItem(name: "exclude", value: firstRoadClass)) diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index ee628bdbd..b5ee41f7f 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -1,12 +1,12 @@ import Foundation -struct RouteResponse { - var code: String? - var message: String? - var error: String? - let uuid: String? - let routes: [Route]? - let waypoints: [Waypoint]? +public struct RouteResponse { + public var code: String? + public var message: String? + public var error: String? + public let uuid: String? + public let routes: [Route]? + public let waypoints: [Waypoint]? } extension RouteResponse: Codable { @@ -19,7 +19,7 @@ extension RouteResponse: Codable { case waypoints } - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.code = try container.decodeIfPresent(String.self, forKey: .code) diff --git a/Sources/MapboxDirections/RouteStep.swift b/Sources/MapboxDirections/RouteStep.swift index cb0a78fef..095d6bc3b 100644 --- a/Sources/MapboxDirections/RouteStep.swift +++ b/Sources/MapboxDirections/RouteStep.swift @@ -384,6 +384,55 @@ open class RouteStep: Codable { // MARK: Creating a Step + /** + Initializes a step. + + - parameter transportType: The mode of transportation used for the step. + - parameter maneuverLocation: The location of the maneuver at the beginning of this step. + - parameter maneuverType: The type of maneuver required for beginning this step. + - parameter maneuverDirection: Additional directional information to clarify the maneuver type. + - parameter instructions: A string with instructions explaining how to perform the step’s maneuver. + - parameter initialHeading: The user’s heading immediately before performing the maneuver. + - parameter finalHeading: The user’s heading immediately after performing the maneuver. + - parameter drivingSide: Indicates what side of a bidirectional road the driver must be driving on. Also referred to as the rule of the road. + - parameter exitCodes: Any [exit numbers](https://en.wikipedia.org/wiki/Exit_number) assigned to the highway exit at the maneuver. + - parameter exitNames: The names of the roundabout exit. + - parameter phoneticExitNames: A phonetic or phonemic transcription indicating how to pronounce the names in the `exitNames` property. + - parameter distance: The step’s distance, measured in meters. + - parameter expectedTravelTime: expectedTravelTime + - parameter names: The names of the road or path leading from this step’s maneuver to the next step’s maneuver. + - parameter phoneticNames: A phonetic or phonemic transcription indicating how to pronounce the names in the `names` property. + - parameter codes: Any route reference codes assigned to the road or path leading from this step’s maneuver to the next step’s maneuver. + - parameter destinationCodes: Any route reference codes that appear on guide signage for the road leading from this step’s maneuver to the next step’s maneuver. + - parameter destinations: Destinations, such as [control cities](https://en.wikipedia.org/wiki/Control_city), that appear on guide signage for the road leading from this step’s maneuver to the next step’s maneuver. + - parameter intersections: An array of intersections along the step. + - parameter instructionsSpokenAlongStep: Instructions about the next step’s maneuver, optimized for speech synthesis. + - parameter instructionsDisplayedAlongStep: Instructions about the next step’s maneuver, optimized for display in real time. + */ + public init(transportType: TransportType, maneuverLocation: CLLocationCoordinate2D, maneuverType: ManeuverType, maneuverDirection: ManeuverDirection? = nil, instructions: String, initialHeading: CLLocationDirection? = nil, finalHeading: CLLocationDirection? = nil, drivingSide: DrivingSide, exitCodes: [String]? = nil, exitNames: [String]? = nil, phoneticExitNames: [String]? = nil, distance: CLLocationDistance, expectedTravelTime: TimeInterval, names: [String]? = nil, phoneticNames: [String]? = nil, codes: [String]? = nil, destinationCodes: [String]? = nil, destinations: [String]? = nil, intersections: [Intersection]? = nil, instructionsSpokenAlongStep: [SpokenInstruction]? = nil, instructionsDisplayedAlongStep: [VisualInstructionBanner]? = nil) { + self.transportType = transportType + self.maneuverLocation = maneuverLocation + self.maneuverType = maneuverType + self.maneuverDirection = maneuverDirection + self.instructions = instructions + self.initialHeading = initialHeading + self.finalHeading = finalHeading + self.drivingSide = drivingSide + self.exitCodes = exitCodes + self.exitNames = exitNames + self.phoneticExitNames = phoneticExitNames + self.distance = distance + self.expectedTravelTime = expectedTravelTime + self.names = names + self.phoneticNames = phoneticNames + self.codes = codes + self.destinationCodes = destinationCodes + self.destinations = destinations + self.intersections = nil + self.instructionsSpokenAlongStep = nil + self.instructionsDisplayedAlongStep = nil + } + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(instructionsSpokenAlongStep, forKey: .instructionsSpokenAlongStep) @@ -420,7 +469,7 @@ open class RouteStep: Codable { var maneuver = container.nestedContainer(keyedBy: ManeuverCodingKeys.self, forKey: .maneuver) try maneuver.encode(instructions, forKey: .instruction) try maneuver.encode(maneuverType, forKey: .type) - try maneuver.encode(maneuverDirection, forKey: .direction) + try maneuver.encodeIfPresent(maneuverDirection, forKey: .direction) try maneuver.encodeIfPresent(maneuverLocation, forKey: .location) try maneuver.encodeIfPresent(initialHeading, forKey: .initialHeading) try maneuver.encodeIfPresent(finalHeading, forKey: .finalHeading) @@ -499,8 +548,32 @@ open class RouteStep: Codable { */ public var shape: LineString? + // MARK: Getting the Mode of Transportation + + /** + The mode of transportation used for the step. + + This step may use a different mode of transportation than the overall route. + */ + public let transportType: TransportType + // MARK: Getting Details About the Maneuver + /** + The location of the maneuver at the beginning of this step. + */ + public let maneuverLocation: CLLocationCoordinate2D + + /** + The type of maneuver required for beginning this step. + */ + public let maneuverType: ManeuverType + + /** + Additional directional information to clarify the maneuver type. + */ + public let maneuverDirection: ManeuverDirection? + /** A string with instructions explaining how to perform the step’s maneuver. @@ -522,26 +595,11 @@ open class RouteStep: Codable { */ public let finalHeading: CLLocationDirection? - /** - The type of maneuver required for beginning this step. - */ - public let maneuverType: ManeuverType - - /** - Additional directional information to clarify the maneuver type. - */ - public let maneuverDirection: ManeuverDirection? - /** Indicates what side of a bidirectional road the driver must be driving on. Also referred to as the rule of the road. */ public let drivingSide: DrivingSide - /** - The location of the maneuver at the beginning of this step. - */ - public let maneuverLocation: CLLocationCoordinate2D - /** The number of exits from the previous maneuver up to and including this step’s maneuver. @@ -549,7 +607,7 @@ open class RouteStep: Codable { In some cases, the number of exits leading to a maneuver may be more useful to the user than the distance to the maneuver. */ - public let exitIndex: Int? + open var exitIndex: Int? /** Any [exit numbers](https://en.wikipedia.org/wiki/Exit_number) assigned to the highway exit at the maneuver. @@ -594,7 +652,7 @@ open class RouteStep: Codable { 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. */ - public let expectedTravelTime: TimeInterval + open var expectedTravelTime: TimeInterval /** The names of the road or path leading from this step’s maneuver to the next step’s maneuver. @@ -659,19 +717,12 @@ open class RouteStep: Codable { /** Instructions about the next step’s maneuver, optimized for display in real time. + As the user traverses this step, you can give them advance notice of the upcoming maneuver by displaying each item in this array in order as the user reaches the specified distances along this step. The text and images of the visual instructions refer to the details in the next step, but the distances are measured from the beginning of this step. + This property is non-`nil` if the `RouteOptions.includesVisualInstructions` option is set to `true`. For instructions designed for speech synthesis, use the `instructionsSpokenAlongStep` property. For instructions designed for display in a static list, use the `instructions` property. */ public let instructionsDisplayedAlongStep: [VisualInstructionBanner]? - - // MARK: Getting the Mode of Transportation - - /** - The mode of transportation used for the step. - - This step may use a different mode of transportation than the overall route. - */ - public let transportType: TransportType } extension RouteStep: Equatable { diff --git a/Sources/MapboxDirections/VisualInstruction.swift b/Sources/MapboxDirections/VisualInstruction.swift index ac8ef4960..518815165 100644 --- a/Sources/MapboxDirections/VisualInstruction.swift +++ b/Sources/MapboxDirections/VisualInstruction.swift @@ -18,7 +18,7 @@ open class VisualInstruction: Codable { /** Initializes a new visual instruction banner object that displays the given information. */ - public init(text: String?, maneuverType: ManeuverType, maneuverDirection: ManeuverDirection, components: [Component], degrees: CLLocationDegrees? = nil) { + public init(text: String?, maneuverType: ManeuverType?, maneuverDirection: ManeuverDirection?, components: [Component], degrees: CLLocationDegrees? = nil) { self.text = text self.maneuverType = maneuverType self.maneuverDirection = maneuverDirection diff --git a/Sources/MapboxDirections/VisualInstructionComponent.swift b/Sources/MapboxDirections/VisualInstructionComponent.swift index 42af6c023..de01b268a 100644 --- a/Sources/MapboxDirections/VisualInstructionComponent.swift +++ b/Sources/MapboxDirections/VisualInstructionComponent.swift @@ -63,6 +63,15 @@ public extension VisualInstruction.Component { A textual representation of a visual instruction component. */ struct TextRepresentation: Equatable { + /** + Initializes a text representation bearing the given abbreviatable text. + */ + public init(text: String, abbreviation: String?, abbreviationPriority: Int?) { + self.text = text + self.abbreviation = abbreviation + self.abbreviationPriority = abbreviationPriority + } + /** The plain text representation of this component. */ @@ -95,6 +104,13 @@ public extension VisualInstruction.Component { case svg } + /** + Initializes an image representation bearing the image at the given base URL. + */ + public init(imageBaseURL: URL?) { + self.imageBaseURL = imageBaseURL + } + /** The URL whose path is the prefix of all the possible URLs returned by `imageURL(scale:format:)`. */ @@ -213,7 +229,7 @@ extension VisualInstruction.Component: Codable { textRepresentation = text case .lane(let indications, let isUsable): try container.encode(Kind.lane, forKey: .kind) - textRepresentation = nil + textRepresentation = .init(text: "", abbreviation: nil, abbreviationPriority: nil) try container.encode(indications, forKey: .directions) try container.encode(isUsable, forKey: .isActive) } diff --git a/Sources/MapboxDirections/Waypoint.swift b/Sources/MapboxDirections/Waypoint.swift index 53bc38cbc..679f09190 100644 --- a/Sources/MapboxDirections/Waypoint.swift +++ b/Sources/MapboxDirections/Waypoint.swift @@ -155,7 +155,8 @@ public class Waypoint: Codable { public var headingAccuracy: CLLocationDirection? = nil internal var headingDescription: String { - guard let heading = self.heading, let accuracy = self.headingAccuracy else { + guard let heading = heading, heading >= 0, + let accuracy = headingAccuracy, accuracy >= 0 else { return "" } diff --git a/Tests/MapboxDirectionsTests/IntersectionTests.swift b/Tests/MapboxDirectionsTests/IntersectionTests.swift index 694f470a2..c3134cec2 100644 --- a/Tests/MapboxDirectionsTests/IntersectionTests.swift +++ b/Tests/MapboxDirectionsTests/IntersectionTests.swift @@ -5,32 +5,47 @@ class IntersectionTests: XCTestCase { func testCoding() { let intersectionsJSON: [[String: Any?]] = [ [ - "location": [13.426579, 52.508068], + "out": 0, "in": -1, - "classes": ["toll", "restricted"], - "bearings": [80], "entry": [true], - "out": 0, - "lanes": nil, + "bearings": [80], + "location": [13.426579, 52.508068], + "classes": ["toll", "restricted"], ], [ - "location": [13.426688, 52.508022], + "out": 1, "in": 2, - "bearings": [30, 120, 300], "entry": [false, true, true], - "out": 1, - "lanes": nil, + "bearings": [30, 120, 300], + "location": [13.426688, 52.508022], + ], + [ + "lanes": [ + [ + "valid": true, + "indications": ["straight"], + ], + [ + "valid": true, + "indications": ["right", "straight"], + ], + ], + "out": 0, + "in": 2, + "entry": [true, true, false], + "bearings": [45, 135, 255], + "location": [-84.503956, 39.102483], ], ] let intersectionsData = try! JSONSerialization.data(withJSONObject: intersectionsJSON, options: []) var intersections: [Intersection]? XCTAssertNoThrow(intersections = try JSONDecoder().decode([Intersection].self, from: intersectionsData)) - XCTAssertEqual(intersections?.count, 2) + XCTAssertEqual(intersections?.count, 3) if let intersection = intersections?.first { - XCTAssert(intersection.outletRoadClasses == [.toll, .restricted]) - XCTAssert(intersection.headings == [80.0]) - XCTAssert(intersection.location == CLLocationCoordinate2D(latitude: 52.508068, longitude: 13.426579)) + XCTAssertEqual(intersection.outletRoadClasses, [.toll, .restricted]) + XCTAssertEqual(intersection.headings, [80.0]) + XCTAssertEqual(intersection.location, CLLocationCoordinate2D(latitude: 52.508068, longitude: 13.426579)) } intersections = [ @@ -49,6 +64,14 @@ class IntersectionTests: XCTestCase { outletIndexes: IndexSet([1, 2]), approachLanes: nil, usableApproachLanes: nil, + outletRoadClasses: nil), + Intersection(location: CLLocationCoordinate2D(latitude: 39.102483, longitude: -84.503956), + headings: [45, 135, 255], + approachIndex: 2, + outletIndex: 0, + outletIndexes: IndexSet([0, 1]), + approachLanes: [.straightAhead, [.straightAhead, .right]], + usableApproachLanes: IndexSet([0, 1]), outletRoadClasses: nil) ] diff --git a/Tests/MapboxDirectionsTests/MatchTests.swift b/Tests/MapboxDirectionsTests/MatchTests.swift index b22be28b3..2e56a586f 100644 --- a/Tests/MapboxDirectionsTests/MatchTests.swift +++ b/Tests/MapboxDirectionsTests/MatchTests.swift @@ -1,14 +1,18 @@ import XCTest #if !SWIFT_PACKAGE import OHHTTPStubs +#endif @testable import MapboxDirections class MatchTests: XCTestCase { override func tearDown() { + #if !SWIFT_PACKAGE OHHTTPStubs.removeAllStubs() + #endif super.tearDown() } + #if !SWIFT_PACKAGE func testMatch() { let expectation = self.expectation(description: "calculating directions should return results") let locations = [CLLocationCoordinate2D(latitude: 32.712041, longitude: -117.172836), @@ -148,5 +152,47 @@ class MatchTests: XCTestCase { XCTAssertEqual(match.matchOptions, unarchivedMatch.matchOptions) XCTAssertEqual(match.tracepoints, unarchivedMatch.tracepoints) } + #endif + + func testCoding() { + // https://api.mapbox.com/matching/v5/mapbox/driving/-84.51200,39.09740;-84.51118,39.09638;-84.51021,39.09687.json?geometries=polyline&overview=false&tidy=false&access_token=… + let matchJSON: [String: Any?] = [ + "confidence": 0.00007401405321383336, + "legs": [ + [ + "summary": "", + "weight": 46.7, + "duration": 34.7, + "steps": [], + "distance": 169, + ], + [ + "summary": "", + "weight": 31, + "duration": 25.6, + "steps": [], + "distance": 128.1, + ], + ], + "weight_name": "routability", + "weight": 77.7, + "duration": 60.300000000000004, + "distance": 297.1, + ] + let matchData = try! JSONSerialization.data(withJSONObject: matchJSON, options: []) + + let options = MatchOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 39.09740, longitude: -84.51200), + CLLocationCoordinate2D(latitude: 39.09638, longitude: -84.51118), + CLLocationCoordinate2D(latitude: 39.09687, longitude: -84.51021), + ]) + options.routeShapeResolution = .none + + let decoder = JSONDecoder() + var match: Match? + XCTAssertThrowsError(match = try decoder.decode(Match.self, from: matchData)) + decoder.userInfo[.options] = options + XCTAssertNoThrow(match = try decoder.decode(Match.self, from: matchData)) + XCTAssertNotNil(match) + } } -#endif diff --git a/Tests/MapboxDirectionsTests/QuickLookTests.swift b/Tests/MapboxDirectionsTests/QuickLookTests.swift new file mode 100644 index 000000000..d52dfad40 --- /dev/null +++ b/Tests/MapboxDirectionsTests/QuickLookTests.swift @@ -0,0 +1,16 @@ +import XCTest +import Turf +@testable import MapboxDirections + +class QuickLookTests: XCTestCase { + func testQuickLookURL() { + let lineString = LineString([ + CLLocationCoordinate2D(latitude: 0, longitude: 0), + CLLocationCoordinate2D(latitude: 1, longitude: 1), + ]) + XCTAssertNil(debugQuickLookURL(illustrating: lineString)) + XCTAssertEqual(debugQuickLookURL(illustrating: lineString, accessToken: BogusToken), URL(string: "https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/path-10+3802DA-0.6(%3F%3F_ibE_ibE)/auto/680x360@2x?before_layer=building-number-label&access_token=\(BogusToken)")) + XCTAssertEqual(debugQuickLookURL(illustrating: lineString, profileIdentifier: .automobileAvoidingTraffic, accessToken: BogusToken), URL(string: "https://api.mapbox.com/styles/v1/mapbox/navigation-preview-day-v4/static/path-10+3802DA-0.6(%3F%3F_ibE_ibE)/auto/680x360@2x?before_layer=waterway-label&access_token=\(BogusToken)")) + XCTAssertEqual(debugQuickLookURL(illustrating: lineString, profileIdentifier: .cycling, accessToken: BogusToken), URL(string: "https://api.mapbox.com/styles/v1/mapbox/outdoors-v11/static/path-10+3802DA-0.6(%3F%3F_ibE_ibE)/auto/680x360@2x?before_layer=contour-label&access_token=\(BogusToken)")) + } +} diff --git a/Tests/MapboxDirectionsTests/RouteStepTests.swift b/Tests/MapboxDirectionsTests/RouteStepTests.swift index aa94e089a..d5e3221c1 100644 --- a/Tests/MapboxDirectionsTests/RouteStepTests.swift +++ b/Tests/MapboxDirectionsTests/RouteStepTests.swift @@ -146,7 +146,13 @@ class RouteStepTests: XCTestCase { } func testCoding() { - let stepJSON = [ + let options = RouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 52.50881, longitude: 13.42467), + CLLocationCoordinate2D(latitude: 52.506794, longitude: 13.42326), + ]) + options.shapeFormat = .polyline + + var stepJSON = [ "intersections": [ [ "out": 1, @@ -177,21 +183,47 @@ class RouteStepTests: XCTestCase { "name": "Adalbertstraße", "mode": "driving", ] as [String : Any?] - let stepData = try! JSONSerialization.data(withJSONObject: stepJSON, options: []) + var stepData = try! JSONSerialization.data(withJSONObject: stepJSON, options: []) + + let decoder = JSONDecoder() + decoder.userInfo[.options] = options var step: RouteStep? - XCTAssertNoThrow(step = try JSONDecoder().decode(RouteStep.self, from: stepData)) + XCTAssertNoThrow(step = try decoder.decode(RouteStep.self, from: stepData)) + XCTAssertNotNil(step) + + let encoder = JSONEncoder() + encoder.userInfo[.options] = options + var encodedStepData: Data? + if let step = step { + XCTAssertNoThrow(encodedStepData = try encoder.encode(step)) + XCTAssertNotNil(encodedStepData) + + if let encodedStepData = encodedStepData { + var encodedStepJSON: Any? + XCTAssertNoThrow(encodedStepJSON = try JSONSerialization.jsonObject(with: encodedStepData, options: [])) + XCTAssertNotNil(encodedStepJSON) + + // https://github.com/mapbox/MapboxDirections.swift/issues/125 + var referenceStepJSON = stepJSON + referenceStepJSON.removeValue(forKey: "weight") + + XCTAssert(JSONSerialization.objectsAreEqual(referenceStepJSON, encodedStepJSON, approximate: true)) + } + } + + options.shapeFormat = .polyline6 + stepJSON["geometry"] = "sg{ccB{`krXzxBbwAvB?" + stepData = try! JSONSerialization.data(withJSONObject: stepJSON, options: []) + XCTAssertNoThrow(step = try decoder.decode(RouteStep.self, from: stepData)) XCTAssertNotNil(step) if let step = step { - let options = RouteOptions(coordinates: [ - CLLocationCoordinate2D(latitude: 0, longitude: 0), - CLLocationCoordinate2D(latitude: 1, longitude: 1), - ]) - options.shapeFormat = .polyline + XCTAssertEqual(step.shape?.coordinates.count, 3) + XCTAssertEqual(step.shape?.coordinates.first?.latitude ?? 0, 52.50881, accuracy: 1e-5) + XCTAssertEqual(step.shape?.coordinates.first?.longitude ?? 0, 13.42467, accuracy: 1e-5) + XCTAssertEqual(step.shape?.coordinates.last?.latitude ?? 0, 52.506794, accuracy: 1e-5) + XCTAssertEqual(step.shape?.coordinates.last?.longitude ?? 0, 13.42326, accuracy: 1e-5) - let encoder = JSONEncoder() - encoder.userInfo[.options] = options - var encodedStepData: Data? XCTAssertNoThrow(encodedStepData = try encoder.encode(step)) XCTAssertNotNil(encodedStepData) diff --git a/Tests/MapboxDirectionsTests/RouteTests.swift b/Tests/MapboxDirectionsTests/RouteTests.swift new file mode 100644 index 000000000..9f16374c5 --- /dev/null +++ b/Tests/MapboxDirectionsTests/RouteTests.swift @@ -0,0 +1,77 @@ +import XCTest +@testable import MapboxDirections + +class RouteTests: XCTestCase { + func testCoding() { + // https://api.mapbox.com/directions/v5/mapbox/driving-traffic/-105.08198579860195%2C39.73843005470756;-104.954255,39.662569.json?overview=false&access_token=… + let routeJSON: [String: Any?] = [ + "legs": [ + [ + "summary": "West 6th Avenue Freeway, South University Boulevard", + "weight": 1346.3, + "duration": 1083.4, + "steps": [], + "distance": 17036.8, + ], + ], + "weight_name": "routability", + "weight": 1346.3, + "duration": 1083.4, + "distance": 17036.8, + ] + let routeData = try! JSONSerialization.data(withJSONObject: routeJSON, options: []) + + let options = RouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 39.73843005470756, longitude: -105.08198579860195), + CLLocationCoordinate2D(latitude: 39.662569, longitude: -104.954255), + ], profileIdentifier: .automobileAvoidingTraffic) + options.routeShapeResolution = .none + + let decoder = JSONDecoder() + var route: Route? + XCTAssertThrowsError(route = try decoder.decode(Route.self, from: routeData)) + decoder.userInfo[.options] = options + XCTAssertNoThrow(route = try decoder.decode(Route.self, from: routeData)) + + let expectedLeg = RouteLeg(steps: [], name: "West 6th Avenue Freeway, South University Boulevard", distance: 17036.8, expectedTravelTime: 1083.4, profileIdentifier: .automobileAvoidingTraffic) + expectedLeg.source = options.waypoints[0] + expectedLeg.destination = options.waypoints[1] + let expectedRoute = Route(legs: [expectedLeg], shape: nil, distance: 17036.8, expectedTravelTime: 1083.4, options: options) + XCTAssertEqual(route, expectedRoute) + + if let route = route { + let encoder = JSONEncoder() + encoder.userInfo[.options] = options + var encodedRouteData: Data? + XCTAssertNoThrow(encodedRouteData = try encoder.encode(route)) + XCTAssertNotNil(encodedRouteData) + + if let encodedRouteData = encodedRouteData { + var encodedRouteJSON: [String: Any?]? + XCTAssertNoThrow(encodedRouteJSON = try JSONSerialization.jsonObject(with: encodedRouteData, options: []) as? [String: Any?]) + XCTAssertNotNil(encodedRouteJSON) + + // Remove keys not found in the original API response. + encodedRouteJSON?.removeValue(forKey: "source") + encodedRouteJSON?.removeValue(forKey: "destination") + encodedRouteJSON?.removeValue(forKey: "profileIdentifier") + if var encodedLegJSON = encodedRouteJSON?["legs"] as? [[String: Any?]] { + encodedLegJSON[0].removeValue(forKey: "source") + encodedLegJSON[0].removeValue(forKey: "destination") + encodedLegJSON[0].removeValue(forKey: "profileIdentifier") + encodedRouteJSON?["legs"] = encodedLegJSON + } + + // https://github.com/mapbox/MapboxDirections.swift/issues/125 + var referenceRouteJSON = routeJSON + referenceRouteJSON.removeValue(forKey: "weight") + referenceRouteJSON.removeValue(forKey: "weight_name") + var referenceLegJSON = referenceRouteJSON["legs"] as! [[String: Any?]] + referenceLegJSON[0].removeValue(forKey: "weight") + referenceRouteJSON["legs"] = referenceLegJSON + + XCTAssert(JSONSerialization.objectsAreEqual(referenceRouteJSON, encodedRouteJSON, approximate: true)) + } + } + } +} diff --git a/Tests/MapboxDirectionsTests/VisualInstructionComponentTests.swift b/Tests/MapboxDirectionsTests/VisualInstructionComponentTests.swift index 765e0aa77..f91f900c7 100644 --- a/Tests/MapboxDirectionsTests/VisualInstructionComponentTests.swift +++ b/Tests/MapboxDirectionsTests/VisualInstructionComponentTests.swift @@ -63,6 +63,41 @@ class VisualInstructionComponentTests: XCTestCase { } } + func testLaneComponent() { + let componentJSON: [String: Any?] = [ + "text": "", + "type": "lane", + "active": true, + "directions": ["right", "straight"], + ] + let componentData = try! JSONSerialization.data(withJSONObject: componentJSON, options: []) + var component: VisualInstruction.Component? + XCTAssertNoThrow(component = try JSONDecoder().decode(VisualInstruction.Component.self, from: componentData)) + XCTAssertNotNil(component) + if let component = component { + if case let .lane(indications, isUsable) = component { + XCTAssertEqual(indications, [.straightAhead, .right]) + XCTAssertTrue(isUsable) + } else { + XCTFail("Lane component should not be decoded as any other kind of component.") + } + } + + component = .lane(indications: [.straightAhead, .right], isUsable: true) + let encoder = JSONEncoder() + var encodedData: Data? + XCTAssertNoThrow(encodedData = try encoder.encode(component)) + XCTAssertNotNil(encodedData) + + if let encodedData = encodedData { + var encodedComponentJSON: [String: Any?]? + XCTAssertNoThrow(encodedComponentJSON = try JSONSerialization.jsonObject(with: encodedData, options: []) as? [String: Any?]) + XCTAssertNotNil(encodedComponentJSON) + + XCTAssert(JSONSerialization.objectsAreEqual(componentJSON, encodedComponentJSON, approximate: false)) + } + } + func testUnrecognizedComponent() { let componentJSON = [ "type": "emoji", diff --git a/Tests/MapboxDirectionsTests/WaypointTests.swift b/Tests/MapboxDirectionsTests/WaypointTests.swift index 60933fce1..ffaf2322b 100644 --- a/Tests/MapboxDirectionsTests/WaypointTests.swift +++ b/Tests/MapboxDirectionsTests/WaypointTests.swift @@ -135,4 +135,19 @@ class WaypointTests: XCTestCase { // right = Tracepoint(coordinate: CLLocationCoordinate2D(), countOfAlternatives: 0, name: "") // XCTAssertNotEqual(left, right) } + + func testAccuracies() { + let from = Waypoint(location: CLLocation(coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0), altitude: -1, horizontalAccuracy: -1, verticalAccuracy: -1, timestamp: Date())) + let to = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0)) + let options = RouteOptions(waypoints: [from, to]) + XCTAssertNil(options.bearings) + XCTAssertNil(options.radiuses) + + from.heading = 90 + from.headingAccuracy = 45 + XCTAssertEqual(options.bearings, "90.0,45.0;") + + from.coordinateAccuracy = 5 + XCTAssertEqual(options.radiuses, "5.0;unlimited") + } }