diff --git a/CHANGELOG.md b/CHANGELOG.md index e22ac0e46..bfc3f1dbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changes to Mapbox Directions for Swift +## v2.3.0 +* Added `VisualInstruction.Component.ShieldRepresentation` struct for displaying a highway shield. Renamed `VisualInstruction.Component.image(image:alternativeText:)` to `VisualInstruction.Component.image(image:alternativeText:shield:)`. ([#644](https://github.com/mapbox/mapbox-directions-swift/pull/644)) + ## v2.2.0 * Added the `RouteResponse.roadClassViolations` property, which indicates any requested `RouteOptions.roadClassesToAvoid` values that could not be satisfied when calculating the routes. You can use convenience `RouteResponse.exclusionViolations(routeIndex:legIndex:stepIndex:intersectionIndex:)` method to search for a specific item. ([#627](https://github.com/mapbox/mapbox-directions-swift/pull/627)) diff --git a/Sources/MapboxDirections/VisualInstructionComponent.swift b/Sources/MapboxDirections/VisualInstructionComponent.swift index b2c446721..9b74904bc 100644 --- a/Sources/MapboxDirections/VisualInstructionComponent.swift +++ b/Sources/MapboxDirections/VisualInstructionComponent.swift @@ -45,9 +45,10 @@ public extension VisualInstruction { - parameter image: The component’s preferred image representation. - parameter alternativeText: The component’s alternative text representation. Use this representation if the image representation is unavailable or unusable, but consider formatting the text in a special way to distinguish it from an ordinary `.text` component. + - parameter shield: Optionally, a structured image representation for displaying a [highway shield](https://en.wikipedia.org/wiki/Highway_shield). */ - case image(image: ImageRepresentation, alternativeText: TextRepresentation) - + case image(image: ImageRepresentation, alternativeText: TextRepresentation, shield: ShieldRepresentation? = nil) + /** The component is an image of a zoomed junction, with a fallback text representation. */ @@ -169,6 +170,64 @@ public extension VisualInstruction.Component { return scale } } + + /** + A mapbox shield representation of a visual instruction component. + */ + struct ShieldRepresentation: Equatable, Codable { + /** + Initializes a mapbox shield with the given name, text color, and display ref. + */ + public init(baseURL: URL, name: String, textColor: String, text: String) { + self.baseURL = baseURL + self.name = name + self.textColor = textColor + self.text = text + } + + /** + Base URL to query the styles endpoint. + */ + public let baseURL: URL + + /** + String indicating the name of the route shield. + */ + public let name: String + + /** + String indicating the color of the text to be rendered on the route shield. + */ + public let textColor: String + + /** + String indicating the route reference code that will be displayed on the shield. + */ + public let text: String + + private enum CodingKeys: String, CodingKey { + case baseURL = "base_url" + case name + case textColor = "text_color" + case text = "display_ref" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + baseURL = try container.decode(URL.self, forKey: .baseURL) + name = try container.decode(String.self, forKey: .name) + textColor = try container.decode(String.self, forKey: .textColor) + text = try container.decode(String.self, forKey: .text) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(baseURL, forKey: .baseURL) + try container.encode(name, forKey: .name) + try container.encode(textColor, forKey: .textColor) + try container.encode(text, forKey: .text) + } + } } /// A guidance view image representation of a visual instruction component. @@ -195,6 +254,7 @@ extension VisualInstruction.Component: Codable { case abbreviatedTextPriority = "abbr_priority" case imageBaseURL case imageURL + case shield = "mapbox_shield" case directions case isActive = "active" case activeDirection = "active_direction" @@ -226,6 +286,7 @@ extension VisualInstruction.Component: Codable { let abbreviation = try container.decodeIfPresent(String.self, forKey: .abbreviatedText) let abbreviationPriority = try container.decodeIfPresent(Int.self, forKey: .abbreviatedTextPriority) let textRepresentation = TextRepresentation(text: text, abbreviation: abbreviation, abbreviationPriority: abbreviationPriority) + let shieldRepresentation = try container.decodeIfPresent(ShieldRepresentation.self, forKey: .shield) switch kind { case .delimiter: @@ -238,7 +299,7 @@ extension VisualInstruction.Component: Codable { imageBaseURL = URL(string: imageBaseURLString) } let imageRepresentation = ImageRepresentation(imageBaseURL: imageBaseURL) - self = .image(image: imageRepresentation, alternativeText: textRepresentation) + self = .image(image: imageRepresentation, alternativeText: textRepresentation, shield: shieldRepresentation) case .exit: self = .exit(text: textRepresentation) case .exitCode: @@ -266,10 +327,11 @@ extension VisualInstruction.Component: Codable { case .text(let text): try container.encode(Kind.text, forKey: .kind) textRepresentation = text - case .image(let image, let alternativeText): + case .image(let image, let alternativeText, let shield): try container.encode(Kind.image, forKey: .kind) textRepresentation = alternativeText try container.encodeIfPresent(image.imageBaseURL?.absoluteString, forKey: .imageBaseURL) + try container.encodeIfPresent(shield, forKey: .shield) case .exit(let text): try container.encode(Kind.exit, forKey: .kind) textRepresentation = text @@ -304,10 +366,11 @@ extension VisualInstruction.Component: Equatable { (let .exit(lhsText), let .exit(rhsText)), (let .exitCode(lhsText), let .exitCode(rhsText)): return lhsText == rhsText - case (let .image(lhsURL, lhsAlternativeText), - let .image(rhsURL, rhsAlternativeText)): + case (let .image(lhsURL, lhsAlternativeText, lhsShield), + let .image(rhsURL, rhsAlternativeText, rhsShield)): return lhsURL == rhsURL && lhsAlternativeText == rhsAlternativeText + && lhsShield == rhsShield case (let .guidanceView(lhsURL, lhsAlternativeText), let .guidanceView(rhsURL, rhsAlternativeText)): return lhsURL == rhsURL diff --git a/Tests/MapboxDirectionsTests/VisualInstructionComponentTests.swift b/Tests/MapboxDirectionsTests/VisualInstructionComponentTests.swift index 9ab7167dd..c3bb77293 100644 --- a/Tests/MapboxDirectionsTests/VisualInstructionComponentTests.swift +++ b/Tests/MapboxDirectionsTests/VisualInstructionComponentTests.swift @@ -23,10 +23,102 @@ class VisualInstructionComponentTests: XCTestCase { } func testImageComponent() { - let componentJSON = [ - "text": "US 42", + let componentJSON: [String : Any] = [ + "text": "I 95", + "type": "icon", + "imageBaseURL": "https://s3.amazonaws.com/mapbox/shields/v3/i-95", + "mapbox_shield": [ + "base_url": "https://api.mapbox.com/styles/v1/", + "name": "us-interstate", + "text_color": "white", + "display_ref": "95" + ] + ] + 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 { + switch component { + case .image(let image, let alternativeText, let shield): + XCTAssertEqual(image.imageBaseURL?.absoluteString, "https://s3.amazonaws.com/mapbox/shields/v3/i-95") + XCTAssertEqual(image.imageURL(scale: 1, format: .svg)?.absoluteString, "https://s3.amazonaws.com/mapbox/shields/v3/i-95@1x.svg") + XCTAssertEqual(image.imageURL(scale: 3, format: .svg)?.absoluteString, "https://s3.amazonaws.com/mapbox/shields/v3/i-95@3x.svg") + XCTAssertEqual(image.imageURL(scale: 3, format: .png)?.absoluteString, "https://s3.amazonaws.com/mapbox/shields/v3/i-95@3x.png") + XCTAssertEqual(alternativeText.text, "I 95") + XCTAssertNil(alternativeText.abbreviation) + XCTAssertNil(alternativeText.abbreviationPriority) + XCTAssertEqual(shield?.baseURL, URL(string: "https://api.mapbox.com/styles/v1/")!) + XCTAssertEqual(shield?.name, "us-interstate") + XCTAssertEqual(shield?.textColor, "white") + XCTAssertEqual(shield?.text, "95") + default: + XCTFail("Image component should not be decoded as any other kind of component.") + } + } + + component = .image(image: .init(imageBaseURL: URL(string: "https://s3.amazonaws.com/mapbox/shields/v3/i-95")!), + alternativeText: .init(text: "I 95", abbreviation: nil, abbreviationPriority: nil), + shield: .init(baseURL: URL(string: "https://api.mapbox.com/styles/v1/")!, name: "us-interstate", textColor: "white", text: "95")) + 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 testShield() { + let shieldJSON = [ + "base_url": "https://api.mapbox.com/styles/v1/", + "name": "us-interstate", + "text_color": "white", + "display_ref": "95", + ] + + let shieldData = try! JSONSerialization.data(withJSONObject: shieldJSON, options: []) + var shield: VisualInstruction.Component.ShieldRepresentation? + XCTAssertNoThrow(shield = try JSONDecoder().decode(VisualInstruction.Component.ShieldRepresentation.self, from: shieldData)) + XCTAssertNotNil(shield) + let url = URL(string: "https://api.mapbox.com/styles/v1/") + if let shield = shield { + XCTAssertEqual(shield.baseURL, url) + XCTAssertEqual(shield.name, "us-interstate") + XCTAssertEqual(shield.textColor, "white") + XCTAssertEqual(shield.text, "95") + } + shield = .init(baseURL: url!, name: "us-interstate", textColor: "white", text: "95") + + let encoder = JSONEncoder() + var encodedData: Data? + XCTAssertNoThrow(encodedData = try encoder.encode(shield)) + XCTAssertNotNil(encodedData) + + if let encodedData = encodedData { + var encodedShieldJSON: [String: Any]? + XCTAssertNoThrow(encodedShieldJSON = try JSONSerialization.jsonObject(with: encodedData, options: []) as? [String: Any]) + XCTAssertNotNil(encodedShieldJSON) + + XCTAssert(JSONSerialization.objectsAreEqual(shieldJSON, encodedShieldJSON, approximate: false)) + } + } + + func testShieldImageComponent() { + let componentJSON: [String : Any] = [ + "text": "I 95", "type": "icon", - "imageBaseURL": "https://s3.amazonaws.com/mapbox/shields/v3/us-42", + "mapbox_shield": [ + "base_url": "https://api.mapbox.com/styles/v1/", + "name": "us-interstate", + "text_color": "white", + "display_ref": "95" + ] ] let componentData = try! JSONSerialization.data(withJSONObject: componentJSON, options: []) var component: VisualInstruction.Component? @@ -34,21 +126,23 @@ class VisualInstructionComponentTests: XCTestCase { XCTAssertNotNil(component) if let component = component { switch component { - case .image(let image, let alternativeText): - XCTAssertEqual(image.imageBaseURL?.absoluteString, "https://s3.amazonaws.com/mapbox/shields/v3/us-42") - XCTAssertEqual(image.imageURL(scale: 1, format: .svg)?.absoluteString, "https://s3.amazonaws.com/mapbox/shields/v3/us-42@1x.svg") - XCTAssertEqual(image.imageURL(scale: 3, format: .svg)?.absoluteString, "https://s3.amazonaws.com/mapbox/shields/v3/us-42@3x.svg") - XCTAssertEqual(image.imageURL(scale: 3, format: .png)?.absoluteString, "https://s3.amazonaws.com/mapbox/shields/v3/us-42@3x.png") - XCTAssertEqual(alternativeText.text, "US 42") + case .image(let image, let alternativeText, let shield): + XCTAssertNil(image.imageBaseURL?.absoluteString) + XCTAssertEqual(alternativeText.text, "I 95") XCTAssertNil(alternativeText.abbreviation) XCTAssertNil(alternativeText.abbreviationPriority) + XCTAssertEqual(shield?.baseURL, URL(string: "https://api.mapbox.com/styles/v1/")!) + XCTAssertEqual(shield?.name, "us-interstate") + XCTAssertEqual(shield?.textColor, "white") + XCTAssertEqual(shield?.text, "95") default: XCTFail("Image component should not be decoded as any other kind of component.") } } - component = .image(image: .init(imageBaseURL: URL(string: "https://s3.amazonaws.com/mapbox/shields/v3/us-42")!), - alternativeText: .init(text: "US 42", abbreviation: nil, abbreviationPriority: nil)) + component = .image(image: .init(imageBaseURL: nil), + alternativeText: .init(text: "I 95", abbreviation: nil, abbreviationPriority: nil), + shield: .init(baseURL: URL(string: "https://api.mapbox.com/styles/v1/")!, name: "us-interstate", textColor: "white", text: "95")) let encoder = JSONEncoder() var encodedData: Data? XCTAssertNoThrow(encodedData = try encoder.encode(component))