diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ed856844f..4094826a2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## v2.11.0 +### Map + +* Added `NavigationMapView.addDestinationAnnotation(_:identifier:styleLoaded:)`, `NavigationMapView.removeDestinationAnnotation(_:)` to present and remove the final destination annotation on a NavigationMapView. ([#4253](https://github.com/mapbox/mapbox-navigation-ios/pull/4253)) + ### CarPlay * Added `CarPlayManagerDelegate.carPlayManagerDidCancelPreview(_:)` to notify developers after CarPlay canceled routes preview, and `CarPlayManager.cancelRoutesPreview()` method to cancel routes preview on CarPlay. ([#4311](https://github.com/mapbox/mapbox-navigation-ios/pull/4311)) diff --git a/Sources/MapboxNavigation/NavigationMapView.swift b/Sources/MapboxNavigation/NavigationMapView.swift index b018b8076b..4478354a04 100755 --- a/Sources/MapboxNavigation/NavigationMapView.swift +++ b/Sources/MapboxNavigation/NavigationMapView.swift @@ -1273,7 +1273,7 @@ open class NavigationMapView: UIView { final destination `PointAnnotation` will be stored in this property and added to the `MapView` later on. */ - var finalDestinationAnnotations: [PointAnnotation] = [] + private(set) var finalDestinationAnnotations: [PointAnnotation] = [] /** Adds the route waypoints to the map given the current leg index. Previous waypoints for completed legs will be omitted. @@ -1335,31 +1335,25 @@ open class NavigationMapView: UIView { if let lastLeg = route.legs.last, let destinationCoordinate = lastLeg.destination?.coordinate { - addDestinationAnnotation(destinationCoordinate) { [weak self] in - guard self != nil else { return } - } + addDestinationAnnotation(destinationCoordinate, identifier: AnnotationIdentifier.finalDestinationAnnotation) } } - + /** - Adds a final destination annotation to the map. - + Adds a final destination annotation to the map. The annotation will be added only after fully loading `MapView` style. In such case + final destination will be stored and added to the `MapView` later on. `delegate` will be notified about the change via + `NavigationMapViewDelegate.navigationMapView(_:didAdd:pointAnnotationManager:)` method. + - parameter coordinate: Coordinate which represents the annotation location. - parameter identifier: String to uniquely identify the destination annotation. Defaults to `nil` and a default identifier will be provided. - - parameter styleLoaded: An escaping closure to be executed when the `MapView` style has finished loading. */ public func addDestinationAnnotation(_ coordinate: CLLocationCoordinate2D, - identifier: String? = nil, - styleLoaded: @escaping () -> Void) { - let identifier = identifier ?? String("finalDestinationAnnotation_\(finalDestinationAnnotations.count)") + identifier: String? = nil) { + let count = pointAnnotationManager?.annotations.count ?? finalDestinationAnnotations.count + let identifier = identifier ?? "\(AnnotationIdentifier.finalDestinationAnnotation)_\(count)" var destinationAnnotation = PointAnnotation(id: identifier, coordinate: coordinate) destinationAnnotation.image = .init(image: .defaultMarkerImage, name: ImageIdentifier.markerImage) - - mapView.mapboxMap.onNext(event: .styleLoaded) { [weak self] _ in - guard self != nil else { return } - styleLoaded() - } - + // If `PointAnnotationManager` is available - add `PointAnnotation`, if not - remember it // and add it only after fully loading `MapView` style. if let pointAnnotationManager = pointAnnotationManager { @@ -1373,18 +1367,20 @@ open class NavigationMapView: UIView { } /** - Removes a final destination annotation to the map. - - - parameter identifier: String to uniquely identify the destination annotation to be removed. Defaults to `nil` and removes all destination annotations. + Removes a final destination annotation from the map. + + - parameter identifier: String to uniquely identify the destination annotation to be removed. Defaults to `nil` and removes all destination annotations with non-custom identifiers. */ - public func removeDestinationAnnotation(_ identifier: String? = nil) { - if let identifier { - finalDestinationAnnotations.removeAll(where: { $0.id == identifier }) - pointAnnotationManager?.annotations.removeAll(where: { $0.id == identifier }) + public func removeDestinationAnnotation(identifier: String? = nil) { + let filter: (PointAnnotation) -> Bool + if let identifier = identifier { + filter = { $0.id == identifier } } else { - finalDestinationAnnotations.removeAll(where: { $0.id.contains("finalDestinationAnnotation") }) - pointAnnotationManager?.annotations.removeAll(where: { $0.id.contains("finalDestinationAnnotation") }) + filter = { $0.id.contains(AnnotationIdentifier.finalDestinationAnnotation) } } + + finalDestinationAnnotations.removeAll(where: filter) + pointAnnotationManager?.annotations.removeAll(where: filter) } /** @@ -1456,11 +1452,6 @@ open class NavigationMapView: UIView { */ public var pointAnnotationManager: PointAnnotationManager? - func annotationsToRemove() -> [Annotation] { - let identifier = NavigationMapView.AnnotationIdentifier.finalDestinationAnnotation - return pointAnnotationManager?.annotations.filter({ $0.id == identifier }) ?? [] - } - // MARK: Map Rendering and Observing var routes: [Route]? diff --git a/Tests/MapboxNavigationTests/NavigationMapViewTests.swift b/Tests/MapboxNavigationTests/NavigationMapViewTests.swift index ae070f713c..4750208f8c 100644 --- a/Tests/MapboxNavigationTests/NavigationMapViewTests.swift +++ b/Tests/MapboxNavigationTests/NavigationMapViewTests.swift @@ -2,7 +2,7 @@ import XCTest import MapboxDirections import TestHelper import Turf -import MapboxMaps +@testable import MapboxMaps @testable import MapboxNavigation @testable import MapboxCoreNavigation @@ -12,6 +12,7 @@ class NavigationMapViewTests: TestCase { CLLocationCoordinate2D(latitude: 29.99908, longitude: -102.828197), ])) var navigationMapView: NavigationMapView! + var pointAnnotationManager: PointAnnotationManager! let options: NavigationRouteOptions = .init(coordinates: [ CLLocationCoordinate2D(latitude: 40.311012, longitude: -112.47926), @@ -21,10 +22,25 @@ class NavigationMapViewTests: TestCase { let route = response.routes!.first! return route }() + + private let coordinate1 = CLLocationCoordinate2D(latitude: 40.311012, longitude: -112.47926) + private let coordinate2 = CLLocationCoordinate2D(latitude: 30.176322, longitude: -102.806108) + private let finalDestinationAnnotationTestPrefix = "MapboxNavigation-MapboxNavigation-resources_finalDestinationAnnotation" + + private final class DisplayLinkCoordinatorSpy: DisplayLinkCoordinator { + func add(_ participant: DisplayLinkParticipant) {} + func remove(_ participant: DisplayLinkParticipant) {} + } override func setUp() { super.setUp() navigationMapView = NavigationMapView(frame: CGRect(origin: .zero, size: .iPhone6Plus)) + let mapboxMap = navigationMapView.mapView.mapboxMap! + pointAnnotationManager = PointAnnotationManager(id: "", + style: mapboxMap.style, + layerPosition: nil, + displayLinkCoordinator: DisplayLinkCoordinatorSpy(), + offsetPointCalculator: OffsetPointCalculator(mapboxMap: mapboxMap)) } override func tearDown() { @@ -584,6 +600,82 @@ class NavigationMapViewTests: TestCase { XCTAssertFalse(style.layerExists(withId: NavigationMapView.LayerIdentifier.intersectionAnnotationsLayer)) } + func testAddFinalDestinationWithDefaultIdentifierIfStyleNotLoaded() { + XCTAssertTrue(navigationMapView.finalDestinationAnnotations.isEmpty) + XCTAssertTrue(pointAnnotationManager.annotations.isEmpty) + + navigationMapView.addDestinationAnnotation(coordinate1) + XCTAssertEqual(navigationMapView.finalDestinationAnnotations.count, 1) + XCTAssertEqual(navigationMapView.finalDestinationAnnotations[0].id, "\(finalDestinationAnnotationTestPrefix)_0") + XCTAssertTrue(pointAnnotationManager.annotations.isEmpty) + + navigationMapView.addDestinationAnnotation(coordinate2) + XCTAssertEqual(navigationMapView.finalDestinationAnnotations.count, 2) + XCTAssertEqual(navigationMapView.finalDestinationAnnotations[1].id, "\(finalDestinationAnnotationTestPrefix)_1") + XCTAssertTrue(pointAnnotationManager.annotations.isEmpty) + } + + func testAddFinalDestinationWithCustomIdentifierIfStyleNotLoaded() { + navigationMapView.addDestinationAnnotation(coordinate1, identifier: "custom") + XCTAssertEqual(navigationMapView.finalDestinationAnnotations.count, 1) + XCTAssertEqual(navigationMapView.finalDestinationAnnotations[0].id, "custom") + } + + func testRemovesFinalDestinationIfStyleNotLoaded() { + navigationMapView.addDestinationAnnotation(coordinate1) + navigationMapView.addDestinationAnnotation(coordinate2) + + navigationMapView.removeDestinationAnnotation(identifier: "custom") + XCTAssertEqual(navigationMapView.finalDestinationAnnotations.count, 2) + + navigationMapView.addDestinationAnnotation(coordinate2, identifier: "custom") + XCTAssertEqual(navigationMapView.finalDestinationAnnotations.count, 3) + navigationMapView.removeDestinationAnnotation(identifier: "custom") + XCTAssertEqual(navigationMapView.finalDestinationAnnotations.count, 2) + + navigationMapView.removeDestinationAnnotation() + XCTAssertEqual(navigationMapView.finalDestinationAnnotations.count, 0) + } + + func testAddFinalDestinationWithDefaultIdentifierIfStyleLoaded() { + navigationMapView.pointAnnotationManager = pointAnnotationManager + + navigationMapView.addDestinationAnnotation(coordinate1) + XCTAssertTrue(navigationMapView.finalDestinationAnnotations.isEmpty) + XCTAssertEqual(pointAnnotationManager.annotations.count, 1) + XCTAssertEqual(pointAnnotationManager.annotations[0].id, "\(finalDestinationAnnotationTestPrefix)_0") + + navigationMapView.addDestinationAnnotation(coordinate2) + XCTAssertTrue(navigationMapView.finalDestinationAnnotations.isEmpty) + XCTAssertEqual(pointAnnotationManager.annotations.count, 2) + XCTAssertEqual(pointAnnotationManager.annotations[1].id, "\(finalDestinationAnnotationTestPrefix)_1") + } + + func testAddFinalDestinationWithCustomIdentifierIfStyleLoaded() { + navigationMapView.pointAnnotationManager = pointAnnotationManager + + navigationMapView.addDestinationAnnotation(coordinate1, identifier: "custom") + XCTAssertEqual(pointAnnotationManager.annotations.count, 1) + XCTAssertEqual(pointAnnotationManager.annotations[0].id, "custom") + } + + func testRemovesFinalDestinationIfStyleLoaded() { + navigationMapView.pointAnnotationManager = pointAnnotationManager + navigationMapView.addDestinationAnnotation(coordinate1) + navigationMapView.addDestinationAnnotation(coordinate2) + + navigationMapView.removeDestinationAnnotation(identifier: "custom") + XCTAssertEqual(pointAnnotationManager.annotations.count, 2) + + navigationMapView.addDestinationAnnotation(coordinate2, identifier: "custom") + XCTAssertEqual(pointAnnotationManager.annotations.count, 3) + navigationMapView.removeDestinationAnnotation(identifier: "custom") + XCTAssertEqual(pointAnnotationManager.annotations.count, 2) + + navigationMapView.removeDestinationAnnotation() + XCTAssertEqual(pointAnnotationManager.annotations.count, 0) + } + private func configureIntersections() { let style = navigationMapView.mapView.mapboxMap.style var source = GeoJSONSource()