From 73f27330aa1e1d9bc249d2d32cad6d726e804b73 Mon Sep 17 00:00:00 2001 From: PW Date: Mon, 13 May 2024 22:30:31 +0200 Subject: [PATCH 1/9] updating tests --- .../Examples/User Location.swift | 4 +- .../MapLibre/MLNMapViewCameraUpdating.swift | 3 + .../MapViewCameraOperations.swift | 47 ++++---- .../MapLibreSwiftUI/MapViewCoordinator.swift | 108 ++++++++++++++---- .../Models/MapCamera/CameraState.swift | 13 ++- .../Models/MapCamera/MapViewCamera.swift | 22 ++-- .../MapViewCoordinatorCameraTests.swift | 1 + .../Models/MapCamera/CameraStateTests.swift | 16 +-- .../Models/MapCamera/MapViewCameraTests.swift | 10 +- .../testCenterCameraState.1.txt | 2 +- .../testTrackingUserLocation.1.txt | 2 +- .../testTrackingUserLocationWithCourse.1.txt | 2 +- .../testTrackingUserLocationWithHeading.1.txt | 2 +- .../MapViewCameraTests/testCenterCamera.1.txt | 8 +- .../testTrackUserLocationWithCourse.1.txt | 5 +- .../testTrackUserLocationWithHeading.1.txt | 5 +- .../testTrackingUserLocation.1.txt | 5 +- 17 files changed, 163 insertions(+), 92 deletions(-) diff --git a/Sources/MapLibreSwiftUI/Examples/User Location.swift b/Sources/MapLibreSwiftUI/Examples/User Location.swift index 3bafb2f..b30b575 100644 --- a/Sources/MapLibreSwiftUI/Examples/User Location.swift +++ b/Sources/MapLibreSwiftUI/Examples/User Location.swift @@ -16,7 +16,7 @@ private let locationManager = StaticLocationManager(initialLocation: CLLocation( #Preview("Track user location") { MapView( styleURL: demoTilesURL, - camera: .constant(.trackUserLocation(zoom: 4, pitch: .fixed(45))), + camera: .constant(.trackUserLocation(zoom: 4, pitch: 45)), locationManager: locationManager ) .mapViewContentInset(.init(top: 450, left: 0, bottom: 0, right: 0)) @@ -26,7 +26,7 @@ private let locationManager = StaticLocationManager(initialLocation: CLLocation( #Preview("Track user location with Course") { MapView( styleURL: demoTilesURL, - camera: .constant(.trackUserLocationWithCourse(zoom: 4, pitch: .fixed(45))), + camera: .constant(.trackUserLocationWithCourse(zoom: 4, pitch: 45)), locationManager: locationManager ) .mapViewContentInset(.init(top: 450, left: 0, bottom: 0, right: 0)) diff --git a/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCameraUpdating.swift b/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCameraUpdating.swift index d7f01a4..60bc362 100644 --- a/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCameraUpdating.swift +++ b/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCameraUpdating.swift @@ -10,6 +10,9 @@ protocol MLNMapViewCameraUpdating: AnyObject { @MainActor var minimumPitch: CGFloat { get set } @MainActor var maximumPitch: CGFloat { get set } @MainActor var direction: CLLocationDirection { get set } + @MainActor var camera: MLNMapCamera { get set } + @MainActor var frame: CGRect { get set } + @MainActor func setCamera(_ camera: MLNMapCamera, animated: Bool) @MainActor func setCenter(_ coordinate: CLLocationCoordinate2D, zoomLevel: Double, direction: CLLocationDirection, diff --git a/Sources/MapLibreSwiftUI/Extensions/MapViewCamera/MapViewCameraOperations.swift b/Sources/MapLibreSwiftUI/Extensions/MapViewCamera/MapViewCameraOperations.swift index a58f330..4545676 100644 --- a/Sources/MapLibreSwiftUI/Extensions/MapViewCamera/MapViewCameraOperations.swift +++ b/Sources/MapLibreSwiftUI/Extensions/MapViewCamera/MapViewCameraOperations.swift @@ -8,17 +8,18 @@ public extension MapViewCamera { /// - Parameter newZoom: The new zoom value. mutating func setZoom(_ newZoom: Double) { switch state { - case let .centered(onCoordinate, _, pitch, direction): + case let .centered(onCoordinate, _, pitch, pitchRange, direction): state = .centered(onCoordinate: onCoordinate, zoom: newZoom, pitch: pitch, + pitchRange: pitchRange, direction: direction) - case let .trackingUserLocation(_, pitch, direction): - state = .trackingUserLocation(zoom: newZoom, pitch: pitch, direction: direction) - case let .trackingUserLocationWithHeading(_, pitch): - state = .trackingUserLocationWithHeading(zoom: newZoom, pitch: pitch) - case let .trackingUserLocationWithCourse(_, pitch): - state = .trackingUserLocationWithCourse(zoom: newZoom, pitch: pitch) + case let .trackingUserLocation(_, pitch, pitchRange, direction): + state = .trackingUserLocation(zoom: newZoom, pitch: pitch, pitchRange: pitchRange, direction: direction) + case let .trackingUserLocationWithHeading(_, pitch, pitchRange): + state = .trackingUserLocationWithHeading(zoom: newZoom, pitch: pitch, pitchRange: pitchRange) + case let .trackingUserLocationWithCourse(_, pitch, pitchRange): + state = .trackingUserLocationWithCourse(zoom: newZoom, pitch: pitch, pitchRange: pitchRange) case .rect: return case .showcase: @@ -33,17 +34,18 @@ public extension MapViewCamera { /// - Parameter newZoom: The value to increment the zoom by. Negative decrements the value. mutating func incrementZoom(by increment: Double) { switch state { - case let .centered(onCoordinate, zoom, pitch, direction): + case let .centered(onCoordinate, zoom, pitch, pitchRange, direction): state = .centered(onCoordinate: onCoordinate, zoom: zoom + increment, pitch: pitch, + pitchRange: pitchRange, direction: direction) - case let .trackingUserLocation(zoom, pitch, direction): - state = .trackingUserLocation(zoom: zoom + increment, pitch: pitch, direction: direction) - case let .trackingUserLocationWithHeading(zoom, pitch): - state = .trackingUserLocationWithHeading(zoom: zoom + increment, pitch: pitch) - case let .trackingUserLocationWithCourse(zoom, pitch): - state = .trackingUserLocationWithCourse(zoom: zoom + increment, pitch: pitch) + case let .trackingUserLocation(zoom, pitch, pitchRange, direction): + state = .trackingUserLocation(zoom: zoom + increment, pitch: pitch, pitchRange: pitchRange, direction: direction) + case let .trackingUserLocationWithHeading(zoom, pitch, pitchRange): + state = .trackingUserLocationWithHeading(zoom: zoom + increment, pitch: pitch, pitchRange: pitchRange) + case let .trackingUserLocationWithCourse(zoom, pitch, pitchRange): + state = .trackingUserLocationWithCourse(zoom: zoom + increment, pitch: pitch, pitchRange: pitchRange) case .rect: return case .showcase: @@ -58,19 +60,20 @@ public extension MapViewCamera { /// Set a new pitch for the current camera state. /// /// - Parameter newPitch: The new pitch value. - mutating func setPitch(_ newPitch: CameraPitch) { + mutating func setPitch(_ newPitch: Double) { switch state { - case let .centered(onCoordinate, zoom, _, direction): + case let .centered(onCoordinate, zoom, _, pitchRange, direction): state = .centered(onCoordinate: onCoordinate, zoom: zoom, pitch: newPitch, + pitchRange: pitchRange, direction: direction) - case let .trackingUserLocation(zoom, _, direction): - state = .trackingUserLocation(zoom: zoom, pitch: newPitch, direction: direction) - case let .trackingUserLocationWithHeading(zoom, _): - state = .trackingUserLocationWithHeading(zoom: zoom, pitch: newPitch) - case let .trackingUserLocationWithCourse(zoom, _): - state = .trackingUserLocationWithCourse(zoom: zoom, pitch: newPitch) + case let .trackingUserLocation(zoom, _, pitchRange, direction): + state = .trackingUserLocation(zoom: zoom, pitch: newPitch, pitchRange: pitchRange, direction: direction) + case let .trackingUserLocationWithHeading(zoom, _, pitchRange): + state = .trackingUserLocationWithHeading(zoom: zoom, pitch: newPitch, pitchRange: pitchRange) + case let .trackingUserLocationWithCourse(zoom, _, pitchRange): + state = .trackingUserLocationWithCourse(zoom: zoom, pitch: newPitch, pitchRange: pitchRange) case .rect: return case .showcase: diff --git a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift index a2f5446..74883e8 100644 --- a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift +++ b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift @@ -63,33 +63,91 @@ public class MapViewCoordinator: NSObject { } switch camera.state { - case let .centered(onCoordinate: coordinate, zoom: zoom, pitch: pitch, direction: direction): + case let .centered(onCoordinate: coordinate, zoom: zoom, pitch: pitch, pitchRange: pitchRange, direction: direction): mapView.userTrackingMode = .none - mapView.setCenter(coordinate, - zoomLevel: zoom, - direction: direction, - animated: animated) - mapView.minimumPitch = pitch.rangeValue.lowerBound - mapView.maximumPitch = pitch.rangeValue.upperBound - case let .trackingUserLocation(zoom: zoom, pitch: pitch, direction: direction): + + if mapView.frame.size == .zero { + // On init, the mapView's frame is not set up yet, so manipulation via camera is broken, + // so let's do something else instead. + mapView.setCenter(coordinate, + zoomLevel: zoom, + direction: direction, + animated: animated) + } else { + let camera = mapView.camera + camera.centerCoordinate = coordinate + camera.heading = direction + camera.pitch = pitch + + let altitude = MLNAltitudeForZoomLevel(zoom, pitch, coordinate.latitude, mapView.frame.size) + camera.altitude = altitude + mapView.setCamera(camera, animated: animated) + } + + mapView.minimumPitch = pitchRange.rangeValue.lowerBound + mapView.maximumPitch = pitchRange.rangeValue.upperBound + case let .trackingUserLocation(zoom: zoom, pitch: pitch, pitchRange: pitchRange, direction: direction): mapView.userTrackingMode = .follow - // Needs to be non-animated or else it messes up following - mapView.setZoomLevel(zoom, animated: false) - mapView.direction = direction - mapView.minimumPitch = pitch.rangeValue.lowerBound - mapView.maximumPitch = pitch.rangeValue.upperBound - case let .trackingUserLocationWithHeading(zoom: zoom, pitch: pitch): + + if mapView.frame.size == .zero { + // On init, the mapView's frame is not set up yet, so manipulation via camera is broken, + // so let's do something else instead. + // Needs to be non-animated or else it messes up following + + mapView.setZoomLevel(zoom, animated: false) + mapView.direction = direction + + } else { + let camera = mapView.camera + camera.heading = direction + camera.pitch = pitch + + let altitude = MLNAltitudeForZoomLevel(zoom, pitch, mapView.camera.centerCoordinate.latitude, mapView.frame.size) + camera.altitude = altitude + mapView.setCamera(camera, animated: animated) + } + mapView.minimumPitch = pitchRange.rangeValue.lowerBound + mapView.maximumPitch = pitchRange.rangeValue.upperBound + case let .trackingUserLocationWithHeading(zoom: zoom, pitch: pitch, pitchRange: pitchRange): mapView.userTrackingMode = .followWithHeading - // Needs to be non-animated or else it messes up following - mapView.setZoomLevel(zoom, animated: false) - mapView.minimumPitch = pitch.rangeValue.lowerBound - mapView.maximumPitch = pitch.rangeValue.upperBound - case let .trackingUserLocationWithCourse(zoom: zoom, pitch: pitch): + + if mapView.frame.size == .zero { + // On init, the mapView's frame is not set up yet, so manipulation via camera is broken, + // so let's do something else instead. + // Needs to be non-animated or else it messes up following + + mapView.setZoomLevel(zoom, animated: false) + + } else { + let camera = mapView.camera + + let altitude = MLNAltitudeForZoomLevel(zoom, pitch, mapView.camera.centerCoordinate.latitude, mapView.frame.size) + camera.altitude = altitude + mapView.setCamera(camera, animated: animated) + } + + mapView.minimumPitch = pitchRange.rangeValue.lowerBound + mapView.maximumPitch = pitchRange.rangeValue.upperBound + case let .trackingUserLocationWithCourse(zoom: zoom, pitch: pitch, pitchRange: pitchRange): mapView.userTrackingMode = .followWithCourse - // Needs to be non-animated or else it messes up following - mapView.setZoomLevel(zoom, animated: false) - mapView.minimumPitch = pitch.rangeValue.lowerBound - mapView.maximumPitch = pitch.rangeValue.upperBound + + if mapView.frame.size == .zero { + // On init, the mapView's frame is not set up yet, so manipulation via camera is broken, + // so let's do something else instead. + // Needs to be non-animated or else it messes up following + + mapView.setZoomLevel(zoom, animated: false) + + } else { + let camera = mapView.camera + + let altitude = MLNAltitudeForZoomLevel(zoom, pitch, mapView.camera.centerCoordinate.latitude, mapView.frame.size) + camera.altitude = altitude + mapView.setCamera(camera, animated: animated) + } + + mapView.minimumPitch = pitchRange.rangeValue.lowerBound + mapView.maximumPitch = pitchRange.rangeValue.upperBound case let .rect(boundingBox, padding): mapView.setVisibleCoordinateBounds(boundingBox, edgePadding: padding, @@ -244,8 +302,8 @@ extension MapViewCoordinator: MLNMapViewDelegate { // state propagation. let newCamera: MapViewCamera = .center(mapView.centerCoordinate, zoom: mapView.zoomLevel, - // TODO: Pitch doesn't really describe current state - pitch: .freeWithinRange( + pitch: mapView.camera.pitch, + pitchRange: .freeWithinRange( minimum: mapView.minimumPitch, maximum: mapView.maximumPitch ), diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift index a62883c..bf61bc7 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift @@ -7,7 +7,8 @@ public enum CameraState: Hashable { case centered( onCoordinate: CLLocationCoordinate2D, zoom: Double, - pitch: CameraPitch, + pitch: Double, + pitchRange: CameraPitch, direction: CLLocationDirection ) @@ -15,19 +16,19 @@ public enum CameraState: Hashable { /// /// This feature uses the MLNMapView's userTrackingMode to .follow which automatically /// follows the user from within the MLNMapView. - case trackingUserLocation(zoom: Double, pitch: CameraPitch, direction: CLLocationDirection) + case trackingUserLocation(zoom: Double, pitch: Double, pitchRange: CameraPitch, direction: CLLocationDirection) /// Follow the user's location using the MapView's internal camera with the user's heading. /// /// This feature uses the MLNMapView's userTrackingMode to .followWithHeading which automatically /// follows the user from within the MLNMapView. - case trackingUserLocationWithHeading(zoom: Double, pitch: CameraPitch) + case trackingUserLocationWithHeading(zoom: Double, pitch: Double, pitchRange: CameraPitch) /// Follow the user's location using the MapView's internal camera with the users' course /// /// This feature uses the MLNMapView's userTrackingMode to .followWithCourse which automatically /// follows the user from within the MLNMapView. - case trackingUserLocationWithCourse(zoom: Double, pitch: CameraPitch) + case trackingUserLocationWithCourse(zoom: Double, pitch: Double, pitchRange: CameraPitch) /// Centered on a bounding box/rectangle. case rect( @@ -42,8 +43,8 @@ public enum CameraState: Hashable { extension CameraState: CustomDebugStringConvertible { public var debugDescription: String { switch self { - case let .centered(onCoordinate: coordinate, zoom: zoom, pitch: pitch, direction: direction): - "CameraState.centered(onCoordinate: \(coordinate), zoom: \(zoom), pitch: \(pitch), direction: \(direction))" + case let .centered(onCoordinate: coordinate, zoom: zoom, pitch: pitch, pitchRange: pitchRange, direction: direction): + "CameraState.centered(onCoordinate: \(coordinate), zoom: \(zoom), pitch: \(pitch), pitchRange: \(pitchRange), direction: \(direction))" case let .trackingUserLocation(zoom: zoom): "CameraState.trackingUserLocation(zoom: \(zoom))" case let .trackingUserLocationWithHeading(zoom: zoom): diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift index f9d0f68..a7e6c07 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift @@ -9,7 +9,8 @@ public struct MapViewCamera: Hashable { public enum Defaults { public static let coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0) public static let zoom: Double = 10 - public static let pitch: CameraPitch = .free + public static let pitch: Double = 0 + public static let pitchRange: CameraPitch = .free public static let direction: CLLocationDirection = 0 } @@ -32,6 +33,7 @@ public struct MapViewCamera: Hashable { onCoordinate: Defaults.coordinate, zoom: Defaults.zoom, pitch: Defaults.pitch, + pitchRange: Defaults.pitchRange, direction: Defaults.direction ), lastReasonForChange: .programmatic @@ -48,11 +50,12 @@ public struct MapViewCamera: Hashable { /// - Returns: The constructed MapViewCamera. public static func center(_ coordinate: CLLocationCoordinate2D, zoom: Double, - pitch: CameraPitch = Defaults.pitch, + pitch: Double = Defaults.pitch, + pitchRange: CameraPitch = Defaults.pitchRange, direction: CLLocationDirection = Defaults.direction, reason: CameraChangeReason? = nil) -> MapViewCamera { - MapViewCamera(state: .centered(onCoordinate: coordinate, zoom: zoom, pitch: pitch, direction: direction), + MapViewCamera(state: .centered(onCoordinate: coordinate, zoom: zoom, pitch: pitch, pitchRange: pitchRange, direction: direction), lastReasonForChange: reason) } @@ -66,11 +69,12 @@ public struct MapViewCamera: Hashable { /// - pitch: Set the camera pitch method. /// - Returns: The MapViewCamera representing the scenario public static func trackUserLocation(zoom: Double = Defaults.zoom, - pitch: CameraPitch = Defaults.pitch, + pitch: Double = Defaults.pitch, + pitchRange: CameraPitch = Defaults.pitchRange, direction: CLLocationDirection = Defaults.direction) -> MapViewCamera { // Coordinate is ignored when tracking user location. However, pitch and zoom are valid. - MapViewCamera(state: .trackingUserLocation(zoom: zoom, pitch: pitch, direction: direction), + MapViewCamera(state: .trackingUserLocation(zoom: zoom, pitch: pitch, pitchRange: pitchRange, direction: direction), lastReasonForChange: .programmatic) } @@ -84,10 +88,10 @@ public struct MapViewCamera: Hashable { /// - pitch: Set the camera pitch method. /// - Returns: The MapViewCamera representing the scenario public static func trackUserLocationWithHeading(zoom: Double = Defaults.zoom, - pitch: CameraPitch = Defaults.pitch) -> MapViewCamera + pitch: Double = Defaults.pitch, pitchRange: CameraPitch = Defaults.pitchRange) -> MapViewCamera { // Coordinate is ignored when tracking user location. However, pitch and zoom are valid. - MapViewCamera(state: .trackingUserLocationWithHeading(zoom: zoom, pitch: pitch), + MapViewCamera(state: .trackingUserLocationWithHeading(zoom: zoom, pitch: pitch, pitchRange: pitchRange), lastReasonForChange: .programmatic) } @@ -101,10 +105,10 @@ public struct MapViewCamera: Hashable { /// - pitch: Set the camera pitch method. /// - Returns: The MapViewCamera representing the scenario public static func trackUserLocationWithCourse(zoom: Double = Defaults.zoom, - pitch: CameraPitch = Defaults.pitch) -> MapViewCamera + pitch: Double = Defaults.pitch, pitchRange: CameraPitch = Defaults.pitchRange) -> MapViewCamera { // Coordinate is ignored when tracking user location. However, pitch and zoom are valid. - MapViewCamera(state: .trackingUserLocationWithCourse(zoom: zoom, pitch: pitch), + MapViewCamera(state: .trackingUserLocationWithCourse(zoom: zoom, pitch: pitch, pitchRange: pitchRange), lastReasonForChange: .programmatic) } diff --git a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift index adbab17..fab70a6 100644 --- a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift +++ b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift @@ -10,6 +10,7 @@ final class MapViewCoordinatorCameraTests: XCTestCase { override func setUp() async throws { maplibreMapView = MockMLNMapViewCameraUpdating() + given(maplibreMapView).frame.willReturn(.zero) mapView = MapView(styleURL: URL(string: "https://maplibre.org")!) coordinator = MapView.Coordinator(parent: mapView) { _, _ in // No action diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift index 514cc70..8d52510 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift @@ -7,26 +7,26 @@ final class CameraStateTests: XCTestCase { let coordinate = CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4) func testCenterCameraState() { - let state: CameraState = .centered(onCoordinate: coordinate, zoom: 4, pitch: .free, direction: 42) - XCTAssertEqual(state, .centered(onCoordinate: coordinate, zoom: 4, pitch: .free, direction: 42)) + let state: CameraState = .centered(onCoordinate: coordinate, zoom: 4, pitch: 0, pitchRange: .free, direction: 42) + XCTAssertEqual(state, .centered(onCoordinate: coordinate, zoom: 4, pitch: 0, pitchRange: .free, direction: 42)) assertSnapshot(of: state, as: .description) } func testTrackingUserLocation() { - let state: CameraState = .trackingUserLocation(zoom: 4, pitch: .free, direction: 12) - XCTAssertEqual(state, .trackingUserLocation(zoom: 4, pitch: .free, direction: 12)) + let state: CameraState = .trackingUserLocation(zoom: 4, pitch: 0, pitchRange: .free, direction: 12) + XCTAssertEqual(state, .trackingUserLocation(zoom: 4, pitch: 0, pitchRange: .free, direction: 12)) assertSnapshot(of: state, as: .description) } func testTrackingUserLocationWithHeading() { - let state: CameraState = .trackingUserLocationWithHeading(zoom: 4, pitch: .free) - XCTAssertEqual(state, .trackingUserLocationWithHeading(zoom: 4, pitch: .free)) + let state: CameraState = .trackingUserLocationWithHeading(zoom: 4, pitch: 0, pitchRange: .free) + XCTAssertEqual(state, .trackingUserLocationWithHeading(zoom: 4, pitch: 0, pitchRange: .free)) assertSnapshot(of: state, as: .description) } func testTrackingUserLocationWithCourse() { - let state: CameraState = .trackingUserLocationWithCourse(zoom: 4, pitch: .free) - XCTAssertEqual(state, .trackingUserLocationWithCourse(zoom: 4, pitch: .free)) + let state: CameraState = .trackingUserLocationWithCourse(zoom: 4, pitch: 0, pitchRange: .free) + XCTAssertEqual(state, .trackingUserLocationWithCourse(zoom: 4, pitch: 0, pitchRange: .free)) assertSnapshot(of: state, as: .description) } diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift index 31b0cad..b3df381 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift @@ -9,7 +9,7 @@ final class MapViewCameraTests: XCTestCase { let camera = MapViewCamera.center( CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4), zoom: 5, - pitch: .freeWithinRange(minimum: 12, maximum: 34), + pitch: 12, direction: 23 ) @@ -18,20 +18,20 @@ final class MapViewCameraTests: XCTestCase { func testTrackingUserLocation() { let pitch: CameraPitch = .freeWithinRange(minimum: 12, maximum: 34) - let camera = MapViewCamera.trackUserLocation(zoom: 10, pitch: pitch) + let camera = MapViewCamera.trackUserLocation(zoom: 10, pitchRange: pitch) assertSnapshot(of: camera, as: .dump) } func testTrackUserLocationWithCourse() { - let pitch: CameraPitch = .freeWithinRange(minimum: 12, maximum: 34) - let camera = MapViewCamera.trackUserLocationWithCourse(zoom: 18, pitch: pitch) + let pitchRange: CameraPitch = .freeWithinRange(minimum: 12, maximum: 34) + let camera = MapViewCamera.trackUserLocationWithCourse(zoom: 18, pitchRange: pitchRange) assertSnapshot(of: camera, as: .dump) } func testTrackUserLocationWithHeading() { - let camera = MapViewCamera.trackUserLocationWithHeading(zoom: 10, pitch: .free) + let camera = MapViewCamera.trackUserLocationWithHeading(zoom: 10, pitch: 0) assertSnapshot(of: camera, as: .dump) } diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testCenterCameraState.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testCenterCameraState.1.txt index 6d2f3a0..1f4c7be 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testCenterCameraState.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testCenterCameraState.1.txt @@ -1 +1 @@ -CameraState.centered(onCoordinate: CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4), zoom: 4.0, pitch: free, direction: 42.0) \ No newline at end of file +CameraState.centered(onCoordinate: CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4), zoom: 4.0, pitch: 0.0, pitchRange: free, direction: 42.0) \ No newline at end of file diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocation.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocation.1.txt index 8cf7a75..a7ee04a 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocation.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocation.1.txt @@ -1 +1 @@ -CameraState.trackingUserLocation(zoom: (4.0, MapLibreSwiftUI.CameraPitch.free, 12.0)) \ No newline at end of file +CameraState.trackingUserLocation(zoom: (4.0, 0.0, MapLibreSwiftUI.CameraPitch.free, 12.0)) \ No newline at end of file diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithCourse.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithCourse.1.txt index 639e899..a28a609 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithCourse.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithCourse.1.txt @@ -1 +1 @@ -CameraState.trackingUserLocationWithCourse(zoom: (4.0, MapLibreSwiftUI.CameraPitch.free)) \ No newline at end of file +CameraState.trackingUserLocationWithCourse(zoom: (4.0, 0.0, MapLibreSwiftUI.CameraPitch.free)) \ No newline at end of file diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithHeading.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithHeading.1.txt index 13adebe..6ff3f6d 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithHeading.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithHeading.1.txt @@ -1 +1 @@ -CameraState.trackingUserLocationWithHeading(zoom: (4.0, MapLibreSwiftUI.CameraPitch.free)) \ No newline at end of file +CameraState.trackingUserLocationWithHeading(zoom: (4.0, 0.0, MapLibreSwiftUI.CameraPitch.free)) \ No newline at end of file diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testCenterCamera.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testCenterCamera.1.txt index d3c277d..671bb9e 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testCenterCamera.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testCenterCamera.1.txt @@ -1,13 +1,11 @@ ▿ MapViewCamera - lastReasonForChange: Optional.none ▿ state: CameraState - ▿ centered: (4 elements) + ▿ centered: (5 elements) ▿ onCoordinate: CLLocationCoordinate2D - latitude: 12.3 - longitude: 23.4 - zoom: 5.0 - ▿ pitch: CameraPitch - ▿ freeWithinRange: (2 elements) - - minimum: 12.0 - - maximum: 34.0 + - pitch: 12.0 + - pitchRange: CameraPitch.free - direction: 23.0 diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithCourse.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithCourse.1.txt index 4c5c2d5..5b38944 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithCourse.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithCourse.1.txt @@ -2,9 +2,10 @@ ▿ lastReasonForChange: Optional - some: CameraChangeReason.programmatic ▿ state: CameraState - ▿ trackingUserLocationWithCourse: (2 elements) + ▿ trackingUserLocationWithCourse: (3 elements) - zoom: 18.0 - ▿ pitch: CameraPitch + - pitch: 0.0 + ▿ pitchRange: CameraPitch ▿ freeWithinRange: (2 elements) - minimum: 12.0 - maximum: 34.0 diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithHeading.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithHeading.1.txt index 47942b2..f705417 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithHeading.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithHeading.1.txt @@ -2,6 +2,7 @@ ▿ lastReasonForChange: Optional - some: CameraChangeReason.programmatic ▿ state: CameraState - ▿ trackingUserLocationWithHeading: (2 elements) + ▿ trackingUserLocationWithHeading: (3 elements) - zoom: 10.0 - - pitch: CameraPitch.free + - pitch: 0.0 + - pitchRange: CameraPitch.free diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackingUserLocation.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackingUserLocation.1.txt index 9b728f1..cf0ca03 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackingUserLocation.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackingUserLocation.1.txt @@ -2,9 +2,10 @@ ▿ lastReasonForChange: Optional - some: CameraChangeReason.programmatic ▿ state: CameraState - ▿ trackingUserLocation: (3 elements) + ▿ trackingUserLocation: (4 elements) - zoom: 10.0 - ▿ pitch: CameraPitch + - pitch: 0.0 + ▿ pitchRange: CameraPitch ▿ freeWithinRange: (2 elements) - minimum: 12.0 - maximum: 34.0 From bec6c709de7653dbb86e970c0c5f3215a786fcea Mon Sep 17 00:00:00 2001 From: PW Date: Mon, 13 May 2024 23:57:16 +0200 Subject: [PATCH 2/9] adding pitch setup for initial load --- .../MapLibreSwiftUI/MapViewCoordinator.swift | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift index 74883e8..e575b86 100644 --- a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift +++ b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift @@ -73,6 +73,11 @@ public class MapViewCoordinator: NSObject { zoomLevel: zoom, direction: direction, animated: animated) + + let camera = mapView.camera + camera.pitch = pitch + mapView.setCamera(camera, animated: false) + } else { let camera = mapView.camera camera.centerCoordinate = coordinate @@ -96,6 +101,10 @@ public class MapViewCoordinator: NSObject { mapView.setZoomLevel(zoom, animated: false) mapView.direction = direction + + let camera = mapView.camera + camera.pitch = pitch + mapView.setCamera(camera, animated: false) } else { let camera = mapView.camera @@ -112,17 +121,21 @@ public class MapViewCoordinator: NSObject { mapView.userTrackingMode = .followWithHeading if mapView.frame.size == .zero { - // On init, the mapView's frame is not set up yet, so manipulation via camera is broken, + // On init, the mapView's frame is not set up yet, so altitude via camera is broken, // so let's do something else instead. // Needs to be non-animated or else it messes up following mapView.setZoomLevel(zoom, animated: false) + let camera = mapView.camera + camera.pitch = pitch + mapView.setCamera(camera, animated: false) } else { let camera = mapView.camera let altitude = MLNAltitudeForZoomLevel(zoom, pitch, mapView.camera.centerCoordinate.latitude, mapView.frame.size) camera.altitude = altitude + camera.pitch = pitch mapView.setCamera(camera, animated: animated) } @@ -132,17 +145,21 @@ public class MapViewCoordinator: NSObject { mapView.userTrackingMode = .followWithCourse if mapView.frame.size == .zero { - // On init, the mapView's frame is not set up yet, so manipulation via camera is broken, + // On init, the mapView's frame is not set up yet, so altitude via camera is broken, // so let's do something else instead. // Needs to be non-animated or else it messes up following mapView.setZoomLevel(zoom, animated: false) + let camera = mapView.camera + camera.pitch = pitch + mapView.setCamera(camera, animated: false) } else { let camera = mapView.camera let altitude = MLNAltitudeForZoomLevel(zoom, pitch, mapView.camera.centerCoordinate.latitude, mapView.frame.size) camera.altitude = altitude + camera.pitch = pitch mapView.setCamera(camera, animated: animated) } From 420efc1454dce786621af0882809e23e74bbebd3 Mon Sep 17 00:00:00 2001 From: PW Date: Tue, 14 May 2024 00:12:18 +0200 Subject: [PATCH 3/9] renaming CameraPitch --- .../{CameraPitch.swift => CameraPitchRange.swift} | 2 +- .../MapLibreSwiftUI/Models/MapCamera/CameraState.swift | 8 ++++---- .../Models/MapCamera/MapViewCamera.swift | 10 +++++----- .../Models/MapCamera/CameraPitchTests.swift | 6 +++--- .../Models/MapCamera/MapViewCameraTests.swift | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) rename Sources/MapLibreSwiftUI/Models/MapCamera/{CameraPitch.swift => CameraPitchRange.swift} (94%) diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitch.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitchRange.swift similarity index 94% rename from Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitch.swift rename to Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitchRange.swift index 45d3cbd..9379339 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitch.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitchRange.swift @@ -2,7 +2,7 @@ import Foundation import MapLibre /// The current pitch state for the MapViewCamera -public enum CameraPitch: Hashable, Sendable { +public enum CameraPitchRange: Hashable, Sendable { /// The user is free to control pitch from it's default min to max. case free diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift index bf61bc7..00cffc9 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift @@ -8,7 +8,7 @@ public enum CameraState: Hashable { onCoordinate: CLLocationCoordinate2D, zoom: Double, pitch: Double, - pitchRange: CameraPitch, + pitchRange: CameraPitchRange, direction: CLLocationDirection ) @@ -16,19 +16,19 @@ public enum CameraState: Hashable { /// /// This feature uses the MLNMapView's userTrackingMode to .follow which automatically /// follows the user from within the MLNMapView. - case trackingUserLocation(zoom: Double, pitch: Double, pitchRange: CameraPitch, direction: CLLocationDirection) + case trackingUserLocation(zoom: Double, pitch: Double, pitchRange: CameraPitchRange, direction: CLLocationDirection) /// Follow the user's location using the MapView's internal camera with the user's heading. /// /// This feature uses the MLNMapView's userTrackingMode to .followWithHeading which automatically /// follows the user from within the MLNMapView. - case trackingUserLocationWithHeading(zoom: Double, pitch: Double, pitchRange: CameraPitch) + case trackingUserLocationWithHeading(zoom: Double, pitch: Double, pitchRange: CameraPitchRange) /// Follow the user's location using the MapView's internal camera with the users' course /// /// This feature uses the MLNMapView's userTrackingMode to .followWithCourse which automatically /// follows the user from within the MLNMapView. - case trackingUserLocationWithCourse(zoom: Double, pitch: Double, pitchRange: CameraPitch) + case trackingUserLocationWithCourse(zoom: Double, pitch: Double, pitchRange: CameraPitchRange) /// Centered on a bounding box/rectangle. case rect( diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift index a7e6c07..3029732 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift @@ -10,7 +10,7 @@ public struct MapViewCamera: Hashable { public static let coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0) public static let zoom: Double = 10 public static let pitch: Double = 0 - public static let pitchRange: CameraPitch = .free + public static let pitchRange: CameraPitchRange = .free public static let direction: CLLocationDirection = 0 } @@ -51,7 +51,7 @@ public struct MapViewCamera: Hashable { public static func center(_ coordinate: CLLocationCoordinate2D, zoom: Double, pitch: Double = Defaults.pitch, - pitchRange: CameraPitch = Defaults.pitchRange, + pitchRange: CameraPitchRange = Defaults.pitchRange, direction: CLLocationDirection = Defaults.direction, reason: CameraChangeReason? = nil) -> MapViewCamera { @@ -70,7 +70,7 @@ public struct MapViewCamera: Hashable { /// - Returns: The MapViewCamera representing the scenario public static func trackUserLocation(zoom: Double = Defaults.zoom, pitch: Double = Defaults.pitch, - pitchRange: CameraPitch = Defaults.pitchRange, + pitchRange: CameraPitchRange = Defaults.pitchRange, direction: CLLocationDirection = Defaults.direction) -> MapViewCamera { // Coordinate is ignored when tracking user location. However, pitch and zoom are valid. @@ -88,7 +88,7 @@ public struct MapViewCamera: Hashable { /// - pitch: Set the camera pitch method. /// - Returns: The MapViewCamera representing the scenario public static func trackUserLocationWithHeading(zoom: Double = Defaults.zoom, - pitch: Double = Defaults.pitch, pitchRange: CameraPitch = Defaults.pitchRange) -> MapViewCamera + pitch: Double = Defaults.pitch, pitchRange: CameraPitchRange = Defaults.pitchRange) -> MapViewCamera { // Coordinate is ignored when tracking user location. However, pitch and zoom are valid. MapViewCamera(state: .trackingUserLocationWithHeading(zoom: zoom, pitch: pitch, pitchRange: pitchRange), @@ -105,7 +105,7 @@ public struct MapViewCamera: Hashable { /// - pitch: Set the camera pitch method. /// - Returns: The MapViewCamera representing the scenario public static func trackUserLocationWithCourse(zoom: Double = Defaults.zoom, - pitch: Double = Defaults.pitch, pitchRange: CameraPitch = Defaults.pitchRange) -> MapViewCamera + pitch: Double = Defaults.pitch, pitchRange: CameraPitchRange = Defaults.pitchRange) -> MapViewCamera { // Coordinate is ignored when tracking user location. However, pitch and zoom are valid. MapViewCamera(state: .trackingUserLocationWithCourse(zoom: zoom, pitch: pitch, pitchRange: pitchRange), diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraPitchTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraPitchTests.swift index 1cb717f..f3e0c34 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraPitchTests.swift +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraPitchTests.swift @@ -3,19 +3,19 @@ import XCTest final class CameraPitchTests: XCTestCase { func testFreePitch() { - let pitch: CameraPitch = .free + let pitch: CameraPitchRange = .free XCTAssertEqual(pitch.rangeValue.lowerBound, 0) XCTAssertEqual(pitch.rangeValue.upperBound, 60) } func testRangePitch() { - let pitch = CameraPitch.freeWithinRange(minimum: 9, maximum: 29) + let pitch = CameraPitchRange.freeWithinRange(minimum: 9, maximum: 29) XCTAssertEqual(pitch.rangeValue.lowerBound, 9) XCTAssertEqual(pitch.rangeValue.upperBound, 29) } func testFixedPitch() { - let pitch = CameraPitch.fixed(41) + let pitch = CameraPitchRange.fixed(41) XCTAssertEqual(pitch.rangeValue.lowerBound, 41) XCTAssertEqual(pitch.rangeValue.upperBound, 41) } diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift index b3df381..4f01c41 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift @@ -17,14 +17,14 @@ final class MapViewCameraTests: XCTestCase { } func testTrackingUserLocation() { - let pitch: CameraPitch = .freeWithinRange(minimum: 12, maximum: 34) + let pitch: CameraPitchRange = .freeWithinRange(minimum: 12, maximum: 34) let camera = MapViewCamera.trackUserLocation(zoom: 10, pitchRange: pitch) assertSnapshot(of: camera, as: .dump) } func testTrackUserLocationWithCourse() { - let pitchRange: CameraPitch = .freeWithinRange(minimum: 12, maximum: 34) + let pitchRange: CameraPitchRange = .freeWithinRange(minimum: 12, maximum: 34) let camera = MapViewCamera.trackUserLocationWithCourse(zoom: 18, pitchRange: pitchRange) assertSnapshot(of: camera, as: .dump) From 2dffff2ad5ec0527f8c53ba3fd00e4d955b5adb9 Mon Sep 17 00:00:00 2001 From: PW Date: Tue, 14 May 2024 00:20:43 +0200 Subject: [PATCH 4/9] pitch workaround for initial load --- .../MapLibreSwiftUI/MapViewCoordinator.swift | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift index e575b86..6f0cc30 100644 --- a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift +++ b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift @@ -74,9 +74,9 @@ public class MapViewCoordinator: NSObject { direction: direction, animated: animated) - let camera = mapView.camera - camera.pitch = pitch - mapView.setCamera(camera, animated: false) + // this is a workaround for no camera - minimum and maximum will be reset below, but this adjusts it. + mapView.minimumPitch = pitch + mapView.maximumPitch = pitch } else { let camera = mapView.camera @@ -102,9 +102,8 @@ public class MapViewCoordinator: NSObject { mapView.setZoomLevel(zoom, animated: false) mapView.direction = direction - let camera = mapView.camera - camera.pitch = pitch - mapView.setCamera(camera, animated: false) + mapView.minimumPitch = pitch + mapView.maximumPitch = pitch } else { let camera = mapView.camera @@ -121,14 +120,13 @@ public class MapViewCoordinator: NSObject { mapView.userTrackingMode = .followWithHeading if mapView.frame.size == .zero { - // On init, the mapView's frame is not set up yet, so altitude via camera is broken, + // On init, the mapView's frame is not set up yet, so manipulation via camera is broken, // so let's do something else instead. // Needs to be non-animated or else it messes up following mapView.setZoomLevel(zoom, animated: false) - let camera = mapView.camera - camera.pitch = pitch - mapView.setCamera(camera, animated: false) + mapView.minimumPitch = pitch + mapView.maximumPitch = pitch } else { let camera = mapView.camera @@ -145,14 +143,13 @@ public class MapViewCoordinator: NSObject { mapView.userTrackingMode = .followWithCourse if mapView.frame.size == .zero { - // On init, the mapView's frame is not set up yet, so altitude via camera is broken, + // On init, the mapView's frame is not set up yet, so manipulation via camera is broken, // so let's do something else instead. // Needs to be non-animated or else it messes up following mapView.setZoomLevel(zoom, animated: false) - let camera = mapView.camera - camera.pitch = pitch - mapView.setCamera(camera, animated: false) + mapView.minimumPitch = pitch + mapView.maximumPitch = pitch } else { let camera = mapView.camera From bc75a5ae2bc6f93408f1d0082b4d3806f5944fb9 Mon Sep 17 00:00:00 2001 From: PW Date: Tue, 14 May 2024 00:33:30 +0200 Subject: [PATCH 5/9] format improvements --- .../MapViewCameraOperations.swift | 7 +- .../MapLibreSwiftUI/MapViewCoordinator.swift | 65 ++++++++++++------- .../Models/MapCamera/CameraState.swift | 8 ++- .../Models/MapCamera/MapViewCamera.swift | 34 +++++++--- .../Models/MapCamera/CameraStateTests.swift | 8 ++- 5 files changed, 87 insertions(+), 35 deletions(-) diff --git a/Sources/MapLibreSwiftUI/Extensions/MapViewCamera/MapViewCameraOperations.swift b/Sources/MapLibreSwiftUI/Extensions/MapViewCamera/MapViewCameraOperations.swift index 4545676..ef6db95 100644 --- a/Sources/MapLibreSwiftUI/Extensions/MapViewCamera/MapViewCameraOperations.swift +++ b/Sources/MapLibreSwiftUI/Extensions/MapViewCamera/MapViewCameraOperations.swift @@ -41,7 +41,12 @@ public extension MapViewCamera { pitchRange: pitchRange, direction: direction) case let .trackingUserLocation(zoom, pitch, pitchRange, direction): - state = .trackingUserLocation(zoom: zoom + increment, pitch: pitch, pitchRange: pitchRange, direction: direction) + state = .trackingUserLocation( + zoom: zoom + increment, + pitch: pitch, + pitchRange: pitchRange, + direction: direction + ) case let .trackingUserLocationWithHeading(zoom, pitch, pitchRange): state = .trackingUserLocationWithHeading(zoom: zoom + increment, pitch: pitch, pitchRange: pitchRange) case let .trackingUserLocationWithCourse(zoom, pitch, pitchRange): diff --git a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift index 6f0cc30..d0886ed 100644 --- a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift +++ b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift @@ -63,9 +63,15 @@ public class MapViewCoordinator: NSObject { } switch camera.state { - case let .centered(onCoordinate: coordinate, zoom: zoom, pitch: pitch, pitchRange: pitchRange, direction: direction): + case let .centered( + onCoordinate: coordinate, + zoom: zoom, + pitch: pitch, + pitchRange: pitchRange, + direction: direction + ): mapView.userTrackingMode = .none - + if mapView.frame.size == .zero { // On init, the mapView's frame is not set up yet, so manipulation via camera is broken, // so let's do something else instead. @@ -73,35 +79,35 @@ public class MapViewCoordinator: NSObject { zoomLevel: zoom, direction: direction, animated: animated) - + // this is a workaround for no camera - minimum and maximum will be reset below, but this adjusts it. mapView.minimumPitch = pitch mapView.maximumPitch = pitch - + } else { let camera = mapView.camera camera.centerCoordinate = coordinate camera.heading = direction camera.pitch = pitch - + let altitude = MLNAltitudeForZoomLevel(zoom, pitch, coordinate.latitude, mapView.frame.size) camera.altitude = altitude mapView.setCamera(camera, animated: animated) } - + mapView.minimumPitch = pitchRange.rangeValue.lowerBound mapView.maximumPitch = pitchRange.rangeValue.upperBound case let .trackingUserLocation(zoom: zoom, pitch: pitch, pitchRange: pitchRange, direction: direction): mapView.userTrackingMode = .follow - + if mapView.frame.size == .zero { // On init, the mapView's frame is not set up yet, so manipulation via camera is broken, // so let's do something else instead. // Needs to be non-animated or else it messes up following - + mapView.setZoomLevel(zoom, animated: false) mapView.direction = direction - + mapView.minimumPitch = pitch mapView.maximumPitch = pitch @@ -109,11 +115,16 @@ public class MapViewCoordinator: NSObject { let camera = mapView.camera camera.heading = direction camera.pitch = pitch - - let altitude = MLNAltitudeForZoomLevel(zoom, pitch, mapView.camera.centerCoordinate.latitude, mapView.frame.size) + + let altitude = MLNAltitudeForZoomLevel( + zoom, + pitch, + mapView.camera.centerCoordinate.latitude, + mapView.frame.size + ) camera.altitude = altitude mapView.setCamera(camera, animated: animated) - } + } mapView.minimumPitch = pitchRange.rangeValue.lowerBound mapView.maximumPitch = pitchRange.rangeValue.upperBound case let .trackingUserLocationWithHeading(zoom: zoom, pitch: pitch, pitchRange: pitchRange): @@ -123,20 +134,25 @@ public class MapViewCoordinator: NSObject { // On init, the mapView's frame is not set up yet, so manipulation via camera is broken, // so let's do something else instead. // Needs to be non-animated or else it messes up following - + mapView.setZoomLevel(zoom, animated: false) mapView.minimumPitch = pitch mapView.maximumPitch = pitch - + } else { let camera = mapView.camera - - let altitude = MLNAltitudeForZoomLevel(zoom, pitch, mapView.camera.centerCoordinate.latitude, mapView.frame.size) + + let altitude = MLNAltitudeForZoomLevel( + zoom, + pitch, + mapView.camera.centerCoordinate.latitude, + mapView.frame.size + ) camera.altitude = altitude camera.pitch = pitch mapView.setCamera(camera, animated: animated) } - + mapView.minimumPitch = pitchRange.rangeValue.lowerBound mapView.maximumPitch = pitchRange.rangeValue.upperBound case let .trackingUserLocationWithCourse(zoom: zoom, pitch: pitch, pitchRange: pitchRange): @@ -146,20 +162,25 @@ public class MapViewCoordinator: NSObject { // On init, the mapView's frame is not set up yet, so manipulation via camera is broken, // so let's do something else instead. // Needs to be non-animated or else it messes up following - + mapView.setZoomLevel(zoom, animated: false) mapView.minimumPitch = pitch mapView.maximumPitch = pitch - + } else { let camera = mapView.camera - - let altitude = MLNAltitudeForZoomLevel(zoom, pitch, mapView.camera.centerCoordinate.latitude, mapView.frame.size) + + let altitude = MLNAltitudeForZoomLevel( + zoom, + pitch, + mapView.camera.centerCoordinate.latitude, + mapView.frame.size + ) camera.altitude = altitude camera.pitch = pitch mapView.setCamera(camera, animated: animated) } - + mapView.minimumPitch = pitchRange.rangeValue.lowerBound mapView.maximumPitch = pitchRange.rangeValue.upperBound case let .rect(boundingBox, padding): diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift index 00cffc9..6a09a9a 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift @@ -43,7 +43,13 @@ public enum CameraState: Hashable { extension CameraState: CustomDebugStringConvertible { public var debugDescription: String { switch self { - case let .centered(onCoordinate: coordinate, zoom: zoom, pitch: pitch, pitchRange: pitchRange, direction: direction): + case let .centered( + onCoordinate: coordinate, + zoom: zoom, + pitch: pitch, + pitchRange: pitchRange, + direction: direction + ): "CameraState.centered(onCoordinate: \(coordinate), zoom: \(zoom), pitch: \(pitch), pitchRange: \(pitchRange), direction: \(direction))" case let .trackingUserLocation(zoom: zoom): "CameraState.trackingUserLocation(zoom: \(zoom))" diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift index 3029732..f6181af 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift @@ -55,8 +55,16 @@ public struct MapViewCamera: Hashable { direction: CLLocationDirection = Defaults.direction, reason: CameraChangeReason? = nil) -> MapViewCamera { - MapViewCamera(state: .centered(onCoordinate: coordinate, zoom: zoom, pitch: pitch, pitchRange: pitchRange, direction: direction), - lastReasonForChange: reason) + MapViewCamera( + state: .centered( + onCoordinate: coordinate, + zoom: zoom, + pitch: pitch, + pitchRange: pitchRange, + direction: direction + ), + lastReasonForChange: reason + ) } /// Enables user location tracking within the MapView. @@ -74,8 +82,10 @@ public struct MapViewCamera: Hashable { direction: CLLocationDirection = Defaults.direction) -> MapViewCamera { // Coordinate is ignored when tracking user location. However, pitch and zoom are valid. - MapViewCamera(state: .trackingUserLocation(zoom: zoom, pitch: pitch, pitchRange: pitchRange, direction: direction), - lastReasonForChange: .programmatic) + MapViewCamera( + state: .trackingUserLocation(zoom: zoom, pitch: pitch, pitchRange: pitchRange, direction: direction), + lastReasonForChange: .programmatic + ) } /// Enables user location tracking within the MapView. @@ -87,9 +97,11 @@ public struct MapViewCamera: Hashable { /// pitch. /// - pitch: Set the camera pitch method. /// - Returns: The MapViewCamera representing the scenario - public static func trackUserLocationWithHeading(zoom: Double = Defaults.zoom, - pitch: Double = Defaults.pitch, pitchRange: CameraPitchRange = Defaults.pitchRange) -> MapViewCamera - { + public static func trackUserLocationWithHeading( + zoom: Double = Defaults.zoom, + pitch: Double = Defaults.pitch, + pitchRange: CameraPitchRange = Defaults.pitchRange + ) -> MapViewCamera { // Coordinate is ignored when tracking user location. However, pitch and zoom are valid. MapViewCamera(state: .trackingUserLocationWithHeading(zoom: zoom, pitch: pitch, pitchRange: pitchRange), lastReasonForChange: .programmatic) @@ -104,9 +116,11 @@ public struct MapViewCamera: Hashable { /// pitch. /// - pitch: Set the camera pitch method. /// - Returns: The MapViewCamera representing the scenario - public static func trackUserLocationWithCourse(zoom: Double = Defaults.zoom, - pitch: Double = Defaults.pitch, pitchRange: CameraPitchRange = Defaults.pitchRange) -> MapViewCamera - { + public static func trackUserLocationWithCourse( + zoom: Double = Defaults.zoom, + pitch: Double = Defaults.pitch, + pitchRange: CameraPitchRange = Defaults.pitchRange + ) -> MapViewCamera { // Coordinate is ignored when tracking user location. However, pitch and zoom are valid. MapViewCamera(state: .trackingUserLocationWithCourse(zoom: zoom, pitch: pitch, pitchRange: pitchRange), lastReasonForChange: .programmatic) diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift index 8d52510..0fb061e 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift @@ -7,7 +7,13 @@ final class CameraStateTests: XCTestCase { let coordinate = CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4) func testCenterCameraState() { - let state: CameraState = .centered(onCoordinate: coordinate, zoom: 4, pitch: 0, pitchRange: .free, direction: 42) + let state: CameraState = .centered( + onCoordinate: coordinate, + zoom: 4, + pitch: 0, + pitchRange: .free, + direction: 42 + ) XCTAssertEqual(state, .centered(onCoordinate: coordinate, zoom: 4, pitch: 0, pitchRange: .free, direction: 42)) assertSnapshot(of: state, as: .description) } From 6653538918fb1ee687fc9f7fc1e5eac68f8a5a2d Mon Sep 17 00:00:00 2001 From: PW Date: Tue, 14 May 2024 13:11:53 +0200 Subject: [PATCH 6/9] fixing tests --- .../MapViewCoordinatorCameraTests.swift | 38 +++++++++++++++++-- .../testTrackingUserLocation.1.txt | 2 +- .../testTrackingUserLocationWithCourse.1.txt | 2 +- .../testTrackingUserLocationWithHeading.1.txt | 2 +- .../MapViewCameraTests/testCenterCamera.1.txt | 2 +- .../testTrackUserLocationWithCourse.1.txt | 2 +- .../testTrackUserLocationWithHeading.1.txt | 2 +- .../testTrackingUserLocation.1.txt | 2 +- 8 files changed, 41 insertions(+), 11 deletions(-) diff --git a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift index fab70a6..4d4b21d 100644 --- a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift +++ b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift @@ -40,8 +40,14 @@ final class MapViewCoordinatorCameraTests: XCTestCase { animated: .value(false)) .called(count: 1) + // Due to the .frame == .zero workaround, min/max pitch setting is called twice, once to set the + // pitch, and then once to set the actual range. verify(maplibreMapView) .minimumPitch(newValue: .value(0)) + .setterCalled(count: 2) + + verify(maplibreMapView) + .maximumPitch(newValue: .value(0)) .setterCalled(count: 1) verify(maplibreMapView) @@ -70,10 +76,16 @@ final class MapViewCoordinatorCameraTests: XCTestCase { animated: .value(false)) .called(count: 1) + // Due to the .frame == .zero workaround, min/max pitch setting is called twice, once to set the + // pitch, and then once to set the actual range. verify(maplibreMapView) .minimumPitch(newValue: .value(0)) + .setterCalled(count: 2) + + verify(maplibreMapView) + .maximumPitch(newValue: .value(0)) .setterCalled(count: 1) - + verify(maplibreMapView) .maximumPitch(newValue: .value(60)) .setterCalled(count: 1) @@ -99,10 +111,16 @@ final class MapViewCoordinatorCameraTests: XCTestCase { animated: .any) .called(count: 0) + // Due to the .frame == .zero workaround, min/max pitch setting is called twice, once to set the + // pitch, and then once to set the actual range. verify(maplibreMapView) .minimumPitch(newValue: .value(0)) + .setterCalled(count: 2) + + verify(maplibreMapView) + .maximumPitch(newValue: .value(0)) .setterCalled(count: 1) - + verify(maplibreMapView) .maximumPitch(newValue: .value(60)) .setterCalled(count: 1) @@ -128,10 +146,16 @@ final class MapViewCoordinatorCameraTests: XCTestCase { animated: .any) .called(count: 0) + // Due to the .frame == .zero workaround, min/max pitch setting is called twice, once to set the + // pitch, and then once to set the actual range. verify(maplibreMapView) .minimumPitch(newValue: .value(0)) + .setterCalled(count: 2) + + verify(maplibreMapView) + .maximumPitch(newValue: .value(0)) .setterCalled(count: 1) - + verify(maplibreMapView) .maximumPitch(newValue: .value(60)) .setterCalled(count: 1) @@ -157,10 +181,16 @@ final class MapViewCoordinatorCameraTests: XCTestCase { animated: .any) .called(count: 0) + // Due to the .frame == .zero workaround, min/max pitch setting is called twice, once to set the + // pitch, and then once to set the actual range. verify(maplibreMapView) .minimumPitch(newValue: .value(0)) + .setterCalled(count: 2) + + verify(maplibreMapView) + .maximumPitch(newValue: .value(0)) .setterCalled(count: 1) - + verify(maplibreMapView) .maximumPitch(newValue: .value(60)) .setterCalled(count: 1) diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocation.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocation.1.txt index a7ee04a..0955934 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocation.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocation.1.txt @@ -1 +1 @@ -CameraState.trackingUserLocation(zoom: (4.0, 0.0, MapLibreSwiftUI.CameraPitch.free, 12.0)) \ No newline at end of file +CameraState.trackingUserLocation(zoom: (4.0, 0.0, MapLibreSwiftUI.CameraPitchRange.free, 12.0)) \ No newline at end of file diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithCourse.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithCourse.1.txt index a28a609..13eace7 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithCourse.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithCourse.1.txt @@ -1 +1 @@ -CameraState.trackingUserLocationWithCourse(zoom: (4.0, 0.0, MapLibreSwiftUI.CameraPitch.free)) \ No newline at end of file +CameraState.trackingUserLocationWithCourse(zoom: (4.0, 0.0, MapLibreSwiftUI.CameraPitchRange.free)) \ No newline at end of file diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithHeading.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithHeading.1.txt index 6ff3f6d..a071639 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithHeading.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/CameraStateTests/testTrackingUserLocationWithHeading.1.txt @@ -1 +1 @@ -CameraState.trackingUserLocationWithHeading(zoom: (4.0, 0.0, MapLibreSwiftUI.CameraPitch.free)) \ No newline at end of file +CameraState.trackingUserLocationWithHeading(zoom: (4.0, 0.0, MapLibreSwiftUI.CameraPitchRange.free)) \ No newline at end of file diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testCenterCamera.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testCenterCamera.1.txt index 671bb9e..318c577 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testCenterCamera.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testCenterCamera.1.txt @@ -7,5 +7,5 @@ - longitude: 23.4 - zoom: 5.0 - pitch: 12.0 - - pitchRange: CameraPitch.free + - pitchRange: CameraPitchRange.free - direction: 23.0 diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithCourse.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithCourse.1.txt index 5b38944..f405f82 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithCourse.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithCourse.1.txt @@ -5,7 +5,7 @@ ▿ trackingUserLocationWithCourse: (3 elements) - zoom: 18.0 - pitch: 0.0 - ▿ pitchRange: CameraPitch + ▿ pitchRange: CameraPitchRange ▿ freeWithinRange: (2 elements) - minimum: 12.0 - maximum: 34.0 diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithHeading.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithHeading.1.txt index f705417..cb5da4b 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithHeading.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithHeading.1.txt @@ -5,4 +5,4 @@ ▿ trackingUserLocationWithHeading: (3 elements) - zoom: 10.0 - pitch: 0.0 - - pitchRange: CameraPitch.free + - pitchRange: CameraPitchRange.free diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackingUserLocation.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackingUserLocation.1.txt index cf0ca03..4252dc9 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackingUserLocation.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackingUserLocation.1.txt @@ -5,7 +5,7 @@ ▿ trackingUserLocation: (4 elements) - zoom: 10.0 - pitch: 0.0 - ▿ pitchRange: CameraPitch + ▿ pitchRange: CameraPitchRange ▿ freeWithinRange: (2 elements) - minimum: 12.0 - maximum: 34.0 From 8c67dc6141be44971dc7862e785bd8dd8c222c9d Mon Sep 17 00:00:00 2001 From: PW Date: Tue, 14 May 2024 13:15:18 +0200 Subject: [PATCH 7/9] linting --- .../MapViewCoordinatorCameraTests.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift index 4d4b21d..aad4f21 100644 --- a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift +++ b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift @@ -45,7 +45,7 @@ final class MapViewCoordinatorCameraTests: XCTestCase { verify(maplibreMapView) .minimumPitch(newValue: .value(0)) .setterCalled(count: 2) - + verify(maplibreMapView) .maximumPitch(newValue: .value(0)) .setterCalled(count: 1) @@ -81,11 +81,11 @@ final class MapViewCoordinatorCameraTests: XCTestCase { verify(maplibreMapView) .minimumPitch(newValue: .value(0)) .setterCalled(count: 2) - + verify(maplibreMapView) .maximumPitch(newValue: .value(0)) .setterCalled(count: 1) - + verify(maplibreMapView) .maximumPitch(newValue: .value(60)) .setterCalled(count: 1) @@ -116,11 +116,11 @@ final class MapViewCoordinatorCameraTests: XCTestCase { verify(maplibreMapView) .minimumPitch(newValue: .value(0)) .setterCalled(count: 2) - + verify(maplibreMapView) .maximumPitch(newValue: .value(0)) .setterCalled(count: 1) - + verify(maplibreMapView) .maximumPitch(newValue: .value(60)) .setterCalled(count: 1) @@ -151,11 +151,11 @@ final class MapViewCoordinatorCameraTests: XCTestCase { verify(maplibreMapView) .minimumPitch(newValue: .value(0)) .setterCalled(count: 2) - + verify(maplibreMapView) .maximumPitch(newValue: .value(0)) .setterCalled(count: 1) - + verify(maplibreMapView) .maximumPitch(newValue: .value(60)) .setterCalled(count: 1) @@ -186,11 +186,11 @@ final class MapViewCoordinatorCameraTests: XCTestCase { verify(maplibreMapView) .minimumPitch(newValue: .value(0)) .setterCalled(count: 2) - + verify(maplibreMapView) .maximumPitch(newValue: .value(0)) .setterCalled(count: 1) - + verify(maplibreMapView) .maximumPitch(newValue: .value(60)) .setterCalled(count: 1) From ea40ab1cf5a2f38c8be917291dc88912ae0a33cf Mon Sep 17 00:00:00 2001 From: PW Date: Wed, 15 May 2024 13:49:24 +0200 Subject: [PATCH 8/9] adding expand clusters modifier --- .../Extensions/MapView/MapViewGestures.swift | 34 +++++++++++++++++++ Sources/MapLibreSwiftUI/MapView.swift | 2 ++ .../MapLibreSwiftUI/MapViewModifiers.swift | 12 +++++++ 3 files changed, 48 insertions(+) diff --git a/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift b/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift index 3c46bf0..18b321c 100644 --- a/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift +++ b/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift @@ -58,6 +58,29 @@ extension MapView { return } + if let clusteredLayers { + if let gestureRecognizer = sender as? UITapGestureRecognizer, gestureRecognizer.numberOfTouches == 1 { + let point = gestureRecognizer.location(in: sender.view) + for clusteredLayer in clusteredLayers { + let features = mapView.visibleFeatures( + at: point, + styleLayerIdentifiers: [clusteredLayer.layerIdentifier] + ) + if let cluster = features.first as? MLNPointFeatureCluster, + let source = mapView.style? + .source(withIdentifier: clusteredLayer.sourceIdentifier) as? MLNShapeSource + { + let zoomLevel = source.zoomLevel(forExpanding: cluster) + + if zoomLevel > 0 { + mapView.setCenter(cluster.coordinate, zoomLevel: zoomLevel, animated: true) + break // since we can only zoom on one thing, we can abort the for loop here + } + } + } + } + } + // Process the gesture into a context response. let context = processContextFromGesture(mapView, gesture: gesture, sender: sender) // Run the context through the gesture held on the MapView (emitting to the MapView modifier). @@ -97,3 +120,14 @@ extension MapView { coordinate: mapView.convert(point, toCoordinateFrom: mapView)) } } + +/// Provides the layer identifier and it's source identifier. +public struct ClusterLayer { + public let layerIdentifier: String + public let sourceIdentifier: String + + public init(layerIdentifier: String, sourceIdentifier: String) { + self.layerIdentifier = layerIdentifier + self.sourceIdentifier = sourceIdentifier + } +} diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift index b98e164..7dc92b4 100644 --- a/Sources/MapLibreSwiftUI/MapView.swift +++ b/Sources/MapLibreSwiftUI/MapView.swift @@ -28,6 +28,8 @@ public struct MapView: UIViewRepresentable { private var locationManager: MLNLocationManager? + var clusteredLayers: [ClusterLayer]? + public init( styleURL: URL, camera: Binding = .constant(.default()), diff --git a/Sources/MapLibreSwiftUI/MapViewModifiers.swift b/Sources/MapLibreSwiftUI/MapViewModifiers.swift index 63c093f..8b82d14 100644 --- a/Sources/MapLibreSwiftUI/MapViewModifiers.swift +++ b/Sources/MapLibreSwiftUI/MapViewModifiers.swift @@ -108,6 +108,18 @@ public extension MapView { return newMapView } + /// Add a default implementation for tapping clustered features. When tapped, the map zooms so that the cluster is + /// expanded. + /// - Parameter clusteredLayers: An array of layers to monitor that can contain clustered features. + /// - Returns: The modified MapView + func expandClustersOnTapping(clusteredLayers: [ClusterLayer]) -> MapView { + var newMapView = self + + newMapView.clusteredLayers = clusteredLayers + + return newMapView + } + func mapViewContentInset(_ inset: UIEdgeInsets) -> Self { var result = self From 857c1ce1327f233d30f37c6d99eb158e7c5d938c Mon Sep 17 00:00:00 2001 From: PW Date: Wed, 15 May 2024 14:02:58 +0200 Subject: [PATCH 9/9] adding example --- Sources/MapLibreSwiftUI/Examples/Layers.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/MapLibreSwiftUI/Examples/Layers.swift b/Sources/MapLibreSwiftUI/Examples/Layers.swift index 05b7013..27e09bd 100644 --- a/Sources/MapLibreSwiftUI/Examples/Layers.swift +++ b/Sources/MapLibreSwiftUI/Examples/Layers.swift @@ -123,6 +123,13 @@ let clustered = ShapeSource(identifier: "points", options: [.clustered: true, .c .iconColor(.white) .predicate(NSPredicate(format: "cluster != YES")) } + .onTapMapGesture(on: ["simple-circles-non-clusters"], onTapChanged: { _, features in + print("Tapped on \(features.first)") + }) + .expandClustersOnTapping(clusteredLayers: [ClusterLayer( + layerIdentifier: "simple-circles-clusters", + sourceIdentifier: "points" + )]) .ignoresSafeArea(.all) }