Skip to content

Commit 780730d

Browse files
authored
Merge pull request #37 from hactar/pitch-improvements
Pitch improvements
2 parents aa159ad + 8c67dc6 commit 780730d

19 files changed

+270
-103
lines changed

Sources/MapLibreSwiftUI/Examples/User Location.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ private let locationManager = StaticLocationManager(initialLocation: CLLocation(
1616
#Preview("Track user location") {
1717
MapView(
1818
styleURL: demoTilesURL,
19-
camera: .constant(.trackUserLocation(zoom: 4, pitch: .fixed(45))),
19+
camera: .constant(.trackUserLocation(zoom: 4, pitch: 45)),
2020
locationManager: locationManager
2121
)
2222
.mapViewContentInset(.init(top: 450, left: 0, bottom: 0, right: 0))
@@ -26,7 +26,7 @@ private let locationManager = StaticLocationManager(initialLocation: CLLocation(
2626
#Preview("Track user location with Course") {
2727
MapView(
2828
styleURL: demoTilesURL,
29-
camera: .constant(.trackUserLocationWithCourse(zoom: 4, pitch: .fixed(45))),
29+
camera: .constant(.trackUserLocationWithCourse(zoom: 4, pitch: 45)),
3030
locationManager: locationManager
3131
)
3232
.mapViewContentInset(.init(top: 450, left: 0, bottom: 0, right: 0))

Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCameraUpdating.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ protocol MLNMapViewCameraUpdating: AnyObject {
1010
@MainActor var minimumPitch: CGFloat { get set }
1111
@MainActor var maximumPitch: CGFloat { get set }
1212
@MainActor var direction: CLLocationDirection { get set }
13+
@MainActor var camera: MLNMapCamera { get set }
14+
@MainActor var frame: CGRect { get set }
15+
@MainActor func setCamera(_ camera: MLNMapCamera, animated: Bool)
1316
@MainActor func setCenter(_ coordinate: CLLocationCoordinate2D,
1417
zoomLevel: Double,
1518
direction: CLLocationDirection,

Sources/MapLibreSwiftUI/Extensions/MapViewCamera/MapViewCameraOperations.swift

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@ public extension MapViewCamera {
88
/// - Parameter newZoom: The new zoom value.
99
mutating func setZoom(_ newZoom: Double) {
1010
switch state {
11-
case let .centered(onCoordinate, _, pitch, direction):
11+
case let .centered(onCoordinate, _, pitch, pitchRange, direction):
1212
state = .centered(onCoordinate: onCoordinate,
1313
zoom: newZoom,
1414
pitch: pitch,
15+
pitchRange: pitchRange,
1516
direction: direction)
16-
case let .trackingUserLocation(_, pitch, direction):
17-
state = .trackingUserLocation(zoom: newZoom, pitch: pitch, direction: direction)
18-
case let .trackingUserLocationWithHeading(_, pitch):
19-
state = .trackingUserLocationWithHeading(zoom: newZoom, pitch: pitch)
20-
case let .trackingUserLocationWithCourse(_, pitch):
21-
state = .trackingUserLocationWithCourse(zoom: newZoom, pitch: pitch)
17+
case let .trackingUserLocation(_, pitch, pitchRange, direction):
18+
state = .trackingUserLocation(zoom: newZoom, pitch: pitch, pitchRange: pitchRange, direction: direction)
19+
case let .trackingUserLocationWithHeading(_, pitch, pitchRange):
20+
state = .trackingUserLocationWithHeading(zoom: newZoom, pitch: pitch, pitchRange: pitchRange)
21+
case let .trackingUserLocationWithCourse(_, pitch, pitchRange):
22+
state = .trackingUserLocationWithCourse(zoom: newZoom, pitch: pitch, pitchRange: pitchRange)
2223
case .rect:
2324
return
2425
case .showcase:
@@ -33,17 +34,23 @@ public extension MapViewCamera {
3334
/// - Parameter newZoom: The value to increment the zoom by. Negative decrements the value.
3435
mutating func incrementZoom(by increment: Double) {
3536
switch state {
36-
case let .centered(onCoordinate, zoom, pitch, direction):
37+
case let .centered(onCoordinate, zoom, pitch, pitchRange, direction):
3738
state = .centered(onCoordinate: onCoordinate,
3839
zoom: zoom + increment,
3940
pitch: pitch,
41+
pitchRange: pitchRange,
4042
direction: direction)
41-
case let .trackingUserLocation(zoom, pitch, direction):
42-
state = .trackingUserLocation(zoom: zoom + increment, pitch: pitch, direction: direction)
43-
case let .trackingUserLocationWithHeading(zoom, pitch):
44-
state = .trackingUserLocationWithHeading(zoom: zoom + increment, pitch: pitch)
45-
case let .trackingUserLocationWithCourse(zoom, pitch):
46-
state = .trackingUserLocationWithCourse(zoom: zoom + increment, pitch: pitch)
43+
case let .trackingUserLocation(zoom, pitch, pitchRange, direction):
44+
state = .trackingUserLocation(
45+
zoom: zoom + increment,
46+
pitch: pitch,
47+
pitchRange: pitchRange,
48+
direction: direction
49+
)
50+
case let .trackingUserLocationWithHeading(zoom, pitch, pitchRange):
51+
state = .trackingUserLocationWithHeading(zoom: zoom + increment, pitch: pitch, pitchRange: pitchRange)
52+
case let .trackingUserLocationWithCourse(zoom, pitch, pitchRange):
53+
state = .trackingUserLocationWithCourse(zoom: zoom + increment, pitch: pitch, pitchRange: pitchRange)
4754
case .rect:
4855
return
4956
case .showcase:
@@ -58,19 +65,20 @@ public extension MapViewCamera {
5865
/// Set a new pitch for the current camera state.
5966
///
6067
/// - Parameter newPitch: The new pitch value.
61-
mutating func setPitch(_ newPitch: CameraPitch) {
68+
mutating func setPitch(_ newPitch: Double) {
6269
switch state {
63-
case let .centered(onCoordinate, zoom, _, direction):
70+
case let .centered(onCoordinate, zoom, _, pitchRange, direction):
6471
state = .centered(onCoordinate: onCoordinate,
6572
zoom: zoom,
6673
pitch: newPitch,
74+
pitchRange: pitchRange,
6775
direction: direction)
68-
case let .trackingUserLocation(zoom, _, direction):
69-
state = .trackingUserLocation(zoom: zoom, pitch: newPitch, direction: direction)
70-
case let .trackingUserLocationWithHeading(zoom, _):
71-
state = .trackingUserLocationWithHeading(zoom: zoom, pitch: newPitch)
72-
case let .trackingUserLocationWithCourse(zoom, _):
73-
state = .trackingUserLocationWithCourse(zoom: zoom, pitch: newPitch)
76+
case let .trackingUserLocation(zoom, _, pitchRange, direction):
77+
state = .trackingUserLocation(zoom: zoom, pitch: newPitch, pitchRange: pitchRange, direction: direction)
78+
case let .trackingUserLocationWithHeading(zoom, _, pitchRange):
79+
state = .trackingUserLocationWithHeading(zoom: zoom, pitch: newPitch, pitchRange: pitchRange)
80+
case let .trackingUserLocationWithCourse(zoom, _, pitchRange):
81+
state = .trackingUserLocationWithCourse(zoom: zoom, pitch: newPitch, pitchRange: pitchRange)
7482
case .rect:
7583
return
7684
case .showcase:

Sources/MapLibreSwiftUI/MapViewCoordinator.swift

Lines changed: 118 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,33 +63,126 @@ public class MapViewCoordinator: NSObject {
6363
}
6464

6565
switch camera.state {
66-
case let .centered(onCoordinate: coordinate, zoom: zoom, pitch: pitch, direction: direction):
66+
case let .centered(
67+
onCoordinate: coordinate,
68+
zoom: zoom,
69+
pitch: pitch,
70+
pitchRange: pitchRange,
71+
direction: direction
72+
):
6773
mapView.userTrackingMode = .none
68-
mapView.setCenter(coordinate,
69-
zoomLevel: zoom,
70-
direction: direction,
71-
animated: animated)
72-
mapView.minimumPitch = pitch.rangeValue.lowerBound
73-
mapView.maximumPitch = pitch.rangeValue.upperBound
74-
case let .trackingUserLocation(zoom: zoom, pitch: pitch, direction: direction):
74+
75+
if mapView.frame.size == .zero {
76+
// On init, the mapView's frame is not set up yet, so manipulation via camera is broken,
77+
// so let's do something else instead.
78+
mapView.setCenter(coordinate,
79+
zoomLevel: zoom,
80+
direction: direction,
81+
animated: animated)
82+
83+
// this is a workaround for no camera - minimum and maximum will be reset below, but this adjusts it.
84+
mapView.minimumPitch = pitch
85+
mapView.maximumPitch = pitch
86+
87+
} else {
88+
let camera = mapView.camera
89+
camera.centerCoordinate = coordinate
90+
camera.heading = direction
91+
camera.pitch = pitch
92+
93+
let altitude = MLNAltitudeForZoomLevel(zoom, pitch, coordinate.latitude, mapView.frame.size)
94+
camera.altitude = altitude
95+
mapView.setCamera(camera, animated: animated)
96+
}
97+
98+
mapView.minimumPitch = pitchRange.rangeValue.lowerBound
99+
mapView.maximumPitch = pitchRange.rangeValue.upperBound
100+
case let .trackingUserLocation(zoom: zoom, pitch: pitch, pitchRange: pitchRange, direction: direction):
75101
mapView.userTrackingMode = .follow
76-
// Needs to be non-animated or else it messes up following
77-
mapView.setZoomLevel(zoom, animated: false)
78-
mapView.direction = direction
79-
mapView.minimumPitch = pitch.rangeValue.lowerBound
80-
mapView.maximumPitch = pitch.rangeValue.upperBound
81-
case let .trackingUserLocationWithHeading(zoom: zoom, pitch: pitch):
102+
103+
if mapView.frame.size == .zero {
104+
// On init, the mapView's frame is not set up yet, so manipulation via camera is broken,
105+
// so let's do something else instead.
106+
// Needs to be non-animated or else it messes up following
107+
108+
mapView.setZoomLevel(zoom, animated: false)
109+
mapView.direction = direction
110+
111+
mapView.minimumPitch = pitch
112+
mapView.maximumPitch = pitch
113+
114+
} else {
115+
let camera = mapView.camera
116+
camera.heading = direction
117+
camera.pitch = pitch
118+
119+
let altitude = MLNAltitudeForZoomLevel(
120+
zoom,
121+
pitch,
122+
mapView.camera.centerCoordinate.latitude,
123+
mapView.frame.size
124+
)
125+
camera.altitude = altitude
126+
mapView.setCamera(camera, animated: animated)
127+
}
128+
mapView.minimumPitch = pitchRange.rangeValue.lowerBound
129+
mapView.maximumPitch = pitchRange.rangeValue.upperBound
130+
case let .trackingUserLocationWithHeading(zoom: zoom, pitch: pitch, pitchRange: pitchRange):
82131
mapView.userTrackingMode = .followWithHeading
83-
// Needs to be non-animated or else it messes up following
84-
mapView.setZoomLevel(zoom, animated: false)
85-
mapView.minimumPitch = pitch.rangeValue.lowerBound
86-
mapView.maximumPitch = pitch.rangeValue.upperBound
87-
case let .trackingUserLocationWithCourse(zoom: zoom, pitch: pitch):
132+
133+
if mapView.frame.size == .zero {
134+
// On init, the mapView's frame is not set up yet, so manipulation via camera is broken,
135+
// so let's do something else instead.
136+
// Needs to be non-animated or else it messes up following
137+
138+
mapView.setZoomLevel(zoom, animated: false)
139+
mapView.minimumPitch = pitch
140+
mapView.maximumPitch = pitch
141+
142+
} else {
143+
let camera = mapView.camera
144+
145+
let altitude = MLNAltitudeForZoomLevel(
146+
zoom,
147+
pitch,
148+
mapView.camera.centerCoordinate.latitude,
149+
mapView.frame.size
150+
)
151+
camera.altitude = altitude
152+
camera.pitch = pitch
153+
mapView.setCamera(camera, animated: animated)
154+
}
155+
156+
mapView.minimumPitch = pitchRange.rangeValue.lowerBound
157+
mapView.maximumPitch = pitchRange.rangeValue.upperBound
158+
case let .trackingUserLocationWithCourse(zoom: zoom, pitch: pitch, pitchRange: pitchRange):
88159
mapView.userTrackingMode = .followWithCourse
89-
// Needs to be non-animated or else it messes up following
90-
mapView.setZoomLevel(zoom, animated: false)
91-
mapView.minimumPitch = pitch.rangeValue.lowerBound
92-
mapView.maximumPitch = pitch.rangeValue.upperBound
160+
161+
if mapView.frame.size == .zero {
162+
// On init, the mapView's frame is not set up yet, so manipulation via camera is broken,
163+
// so let's do something else instead.
164+
// Needs to be non-animated or else it messes up following
165+
166+
mapView.setZoomLevel(zoom, animated: false)
167+
mapView.minimumPitch = pitch
168+
mapView.maximumPitch = pitch
169+
170+
} else {
171+
let camera = mapView.camera
172+
173+
let altitude = MLNAltitudeForZoomLevel(
174+
zoom,
175+
pitch,
176+
mapView.camera.centerCoordinate.latitude,
177+
mapView.frame.size
178+
)
179+
camera.altitude = altitude
180+
camera.pitch = pitch
181+
mapView.setCamera(camera, animated: animated)
182+
}
183+
184+
mapView.minimumPitch = pitchRange.rangeValue.lowerBound
185+
mapView.maximumPitch = pitchRange.rangeValue.upperBound
93186
case let .rect(boundingBox, padding):
94187
mapView.setVisibleCoordinateBounds(boundingBox,
95188
edgePadding: padding,
@@ -244,8 +337,8 @@ extension MapViewCoordinator: MLNMapViewDelegate {
244337
// state propagation.
245338
let newCamera: MapViewCamera = .center(mapView.centerCoordinate,
246339
zoom: mapView.zoomLevel,
247-
// TODO: Pitch doesn't really describe current state
248-
pitch: .freeWithinRange(
340+
pitch: mapView.camera.pitch,
341+
pitchRange: .freeWithinRange(
249342
minimum: mapView.minimumPitch,
250343
maximum: mapView.maximumPitch
251344
),

Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitch.swift renamed to Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitchRange.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22
import MapLibre
33

44
/// The current pitch state for the MapViewCamera
5-
public enum CameraPitch: Hashable, Sendable {
5+
public enum CameraPitchRange: Hashable, Sendable {
66
/// The user is free to control pitch from it's default min to max.
77
case free
88

Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,28 @@ public enum CameraState: Hashable {
77
case centered(
88
onCoordinate: CLLocationCoordinate2D,
99
zoom: Double,
10-
pitch: CameraPitch,
10+
pitch: Double,
11+
pitchRange: CameraPitchRange,
1112
direction: CLLocationDirection
1213
)
1314

1415
/// Follow the user's location using the MapView's internal camera.
1516
///
1617
/// This feature uses the MLNMapView's userTrackingMode to .follow which automatically
1718
/// follows the user from within the MLNMapView.
18-
case trackingUserLocation(zoom: Double, pitch: CameraPitch, direction: CLLocationDirection)
19+
case trackingUserLocation(zoom: Double, pitch: Double, pitchRange: CameraPitchRange, direction: CLLocationDirection)
1920

2021
/// Follow the user's location using the MapView's internal camera with the user's heading.
2122
///
2223
/// This feature uses the MLNMapView's userTrackingMode to .followWithHeading which automatically
2324
/// follows the user from within the MLNMapView.
24-
case trackingUserLocationWithHeading(zoom: Double, pitch: CameraPitch)
25+
case trackingUserLocationWithHeading(zoom: Double, pitch: Double, pitchRange: CameraPitchRange)
2526

2627
/// Follow the user's location using the MapView's internal camera with the users' course
2728
///
2829
/// This feature uses the MLNMapView's userTrackingMode to .followWithCourse which automatically
2930
/// follows the user from within the MLNMapView.
30-
case trackingUserLocationWithCourse(zoom: Double, pitch: CameraPitch)
31+
case trackingUserLocationWithCourse(zoom: Double, pitch: Double, pitchRange: CameraPitchRange)
3132

3233
/// Centered on a bounding box/rectangle.
3334
case rect(
@@ -42,8 +43,14 @@ public enum CameraState: Hashable {
4243
extension CameraState: CustomDebugStringConvertible {
4344
public var debugDescription: String {
4445
switch self {
45-
case let .centered(onCoordinate: coordinate, zoom: zoom, pitch: pitch, direction: direction):
46-
"CameraState.centered(onCoordinate: \(coordinate), zoom: \(zoom), pitch: \(pitch), direction: \(direction))"
46+
case let .centered(
47+
onCoordinate: coordinate,
48+
zoom: zoom,
49+
pitch: pitch,
50+
pitchRange: pitchRange,
51+
direction: direction
52+
):
53+
"CameraState.centered(onCoordinate: \(coordinate), zoom: \(zoom), pitch: \(pitch), pitchRange: \(pitchRange), direction: \(direction))"
4754
case let .trackingUserLocation(zoom: zoom):
4855
"CameraState.trackingUserLocation(zoom: \(zoom))"
4956
case let .trackingUserLocationWithHeading(zoom: zoom):

0 commit comments

Comments
 (0)