Skip to content

Commit

Permalink
Add support for mapbox designed route shields (#644)
Browse files Browse the repository at this point in the history
* Add support for mapbox shields.

* Incorporate feedback.

* Update changelog. Rename mapboxShield to shield for consistency.
  • Loading branch information
jill-cardamon committed Jan 25, 2022
1 parent 0f20e9a commit 1e5310d
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 17 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
75 changes: 69 additions & 6 deletions Sources/MapboxDirections/VisualInstructionComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
Expand All @@ -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"
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
116 changes: 105 additions & 11 deletions Tests/MapboxDirectionsTests/VisualInstructionComponentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,126 @@ 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/[email protected]")
XCTAssertEqual(image.imageURL(scale: 3, format: .svg)?.absoluteString, "https://s3.amazonaws.com/mapbox/shields/v3/[email protected]")
XCTAssertEqual(image.imageURL(scale: 3, format: .png)?.absoluteString, "https://s3.amazonaws.com/mapbox/shields/v3/[email protected]")
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?
XCTAssertNoThrow(component = try JSONDecoder().decode(VisualInstruction.Component.self, from: componentData))
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/[email protected]")
XCTAssertEqual(image.imageURL(scale: 3, format: .svg)?.absoluteString, "https://s3.amazonaws.com/mapbox/shields/v3/[email protected]")
XCTAssertEqual(image.imageURL(scale: 3, format: .png)?.absoluteString, "https://s3.amazonaws.com/mapbox/shields/v3/[email protected]")
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))
Expand Down

0 comments on commit 1e5310d

Please sign in to comment.