Skip to content

Commit

Permalink
Merge pull request #37 from hactar/pitch-improvements
Browse files Browse the repository at this point in the history
Pitch improvements
  • Loading branch information
ianthetechie authored Jun 12, 2024
2 parents aa159ad + 8c67dc6 commit 780730d
Show file tree
Hide file tree
Showing 19 changed files with 270 additions and 103 deletions.
4 changes: 2 additions & 2 deletions Sources/MapLibreSwiftUI/Examples/User Location.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -33,17 +34,23 @@ 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:
Expand All @@ -58,19 +65,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:
Expand Down
143 changes: 118 additions & 25 deletions Sources/MapLibreSwiftUI/MapViewCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,33 +63,126 @@ 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)

// 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
// 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

mapView.minimumPitch = pitch
mapView.maximumPitch = pitch

} 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)
mapView.minimumPitch = pitch
mapView.maximumPitch = pitch

} 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)
}

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)
mapView.minimumPitch = pitch
mapView.maximumPitch = pitch

} 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)
}

mapView.minimumPitch = pitchRange.rangeValue.lowerBound
mapView.maximumPitch = pitchRange.rangeValue.upperBound
case let .rect(boundingBox, padding):
mapView.setVisibleCoordinateBounds(boundingBox,
edgePadding: padding,
Expand Down Expand Up @@ -244,8 +337,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
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 13 additions & 6 deletions Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,28 @@ public enum CameraState: Hashable {
case centered(
onCoordinate: CLLocationCoordinate2D,
zoom: Double,
pitch: CameraPitch,
pitch: Double,
pitchRange: CameraPitchRange,
direction: CLLocationDirection
)

/// Follow the user's location using the MapView's internal camera.
///
/// 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: 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: 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: CameraPitch)
case trackingUserLocationWithCourse(zoom: Double, pitch: Double, pitchRange: CameraPitchRange)

/// Centered on a bounding box/rectangle.
case rect(
Expand All @@ -42,8 +43,14 @@ 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):
Expand Down
Loading

0 comments on commit 780730d

Please sign in to comment.