diff --git a/Package.resolved b/Package.resolved index b838dac1c..a8103b1bc 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,15 +9,6 @@ "version" : "6.22.1" } }, - { - "identity" : "mockable", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Kolos65/Mockable.git", - "state" : { - "revision" : "55f846e4ea37ca37166a6d533b5144b956385b41", - "version" : "0.5.1" - } - }, { "identity" : "swift-custom-dump", "kind" : "remoteSourceControl", @@ -50,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/maplibre/swiftui-dsl", "state" : { - "revision" : "e2da0e969a345c00e7524adf391d9be232be9c26", - "version" : "0.21.1" + "revision" : "cecdd8143ad58bfe36072680cd47d750d2a7479e", + "version" : "0.23.0" } }, { diff --git a/apple/DemoApp/Demo/DemoNavigationView.swift b/apple/DemoApp/Demo/DemoNavigationView.swift index 6d0c48e51..814bd0481 100644 --- a/apple/DemoApp/Demo/DemoNavigationView.swift +++ b/apple/DemoApp/Demo/DemoNavigationView.swift @@ -88,58 +88,15 @@ struct DemoNavigationView: View { Text("Loading route...") } } - }, - bottomTrailing: { - VStack { - Text(locationLabel) - .font(.caption) - .padding(.all, 8) - .foregroundColor(.white) - .background(Color.black.opacity(0.7).clipShape(.buttonBorder, style: FillStyle())) - - if locationServicesEnabled { - if model.appState.showStateButton { - NavigationUIButton { - switch model.appState { - case .idle: - showSearch = true - case let .destination(coordinate): - Task { - isFetchingRoutes = true - await model.loadRoute(coordinate) - isFetchingRoutes = false - } - case let .routes(routes): - model.selectRoute(from: routes) - case let .selectedRoute(route): - startNavigation(route) - case .navigating: - // Should not reach this. - break - } - } label: { - Text(model.appState.buttonText) - .lineLimit(1) - .minimumScaleFactor(0.5) - .font(.body.bold()) - } - } - } else { - NavigationUIButton { - // TODO: enable location services. - } label: { - Text("Enable Location Services") - } - } - Button { - model.toggleLocationSimulation() - } label: { - model.locationProvider.type.label - } - .buttonStyle(NavigationUIButtonStyle()) - } } ) + .overlay(alignment: .bottomTrailing) { + if model.appState.showStateButton { + browseControls(locationServicesEnabled: locationServicesEnabled) + .padding(.trailing, 16) + .padding(.bottom, 16) + } + } if showSearch { model.searchView @@ -162,6 +119,56 @@ struct DemoNavigationView: View { allowAutoLock() } + func browseControls(locationServicesEnabled: Bool) -> some View { + VStack { + Text(locationLabel) + .font(.caption) + .padding(.all, 8) + .foregroundColor(.white) + .background(Color.black.opacity(0.7).clipShape(.buttonBorder, style: FillStyle())) + + if locationServicesEnabled { + NavigationUIButton { + switch model.appState { + case .idle: + showSearch = true + case let .destination(coordinate): + Task { + isFetchingRoutes = true + await model.loadRoute(coordinate) + isFetchingRoutes = false + } + case let .routes(routes): + model.selectRoute(from: routes) + case let .selectedRoute(route): + startNavigation(route) + case .navigating: + // Should not reach this. + break + } + } label: { + Text(model.appState.buttonText) + .lineLimit(1) + .minimumScaleFactor(0.5) + .font(.body.bold()) + } + } else { + NavigationUIButton { + // TODO: enable location services. + } label: { + Text("Enable Location Services") + } + } + + Button { + model.toggleLocationSimulation() + } label: { + model.locationProvider.type.label + } + .buttonStyle(NavigationUIButtonStyle()) + } + } + var locationLabel: String { guard let horizontalAccuracy = model.horizontalAccuracy else { return "Not Authorized" diff --git a/apple/DemoApp/Ferrostar Demo.xcodeproj/project.pbxproj b/apple/DemoApp/Ferrostar Demo.xcodeproj/project.pbxproj index 20fe17227..94910f1a2 100644 --- a/apple/DemoApp/Ferrostar Demo.xcodeproj/project.pbxproj +++ b/apple/DemoApp/Ferrostar Demo.xcodeproj/project.pbxproj @@ -290,7 +290,7 @@ mainGroup = E9505FAE2AD449700016BF0A; packageReferences = ( FE0519072E2051EF0084240B /* XCRemoteSwiftPackageReference "swiftui-autocomplete-search" */, - 16731E072E380B9500B3E2C9 /* XCLocalSwiftPackageReference "../../../ferrostar" */, + 16731E072E380B9500B3E2C9 /* XCLocalSwiftPackageReference "../.." */, ); productRefGroup = E9505FB82AD449700016BF0A /* Products */; projectDirPath = ""; @@ -640,9 +640,9 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 16731E072E380B9500B3E2C9 /* XCLocalSwiftPackageReference "../../../ferrostar" */ = { + 16731E072E380B9500B3E2C9 /* XCLocalSwiftPackageReference "../.." */ = { isa = XCLocalSwiftPackageReference; - relativePath = ../../../ferrostar; + relativePath = ../..; }; /* End XCLocalSwiftPackageReference section */ diff --git a/apple/DemoApp/Ferrostar Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apple/DemoApp/Ferrostar Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9826f67d8..a4111dde7 100644 --- a/apple/DemoApp/Ferrostar Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apple/DemoApp/Ferrostar Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "3e90030686b7a7719f19b1ef259d5f047c01f9fcd16dbba125c6e11e490ae70e", + "originHash" : "48fe0f662ed3d8b1be1c0fcdd6a2251b748927c127e92f3b461490d0a1f723b7", "pins" : [ { "identity" : "anycodable", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git", "state" : { - "revision" : "fa12216f30833c2b4d897714f7c1ca2f5608f685", - "version" : "6.22.1" + "revision" : "5d121591b60e837413391814ed88deca2aa7c1d8", + "version" : "6.24.0" } }, { diff --git a/apple/Sources/FerrostarMapLibreUI/Extensions/MapViewCamera.swift b/apple/Sources/FerrostarMapLibreUI/Extensions/MapViewCamera.swift index 0a1bb2025..4c14f43f2 100644 --- a/apple/Sources/FerrostarMapLibreUI/Extensions/MapViewCamera.swift +++ b/apple/Sources/FerrostarMapLibreUI/Extensions/MapViewCamera.swift @@ -1,6 +1,32 @@ import Foundation import MapLibreSwiftUI +public enum NavigationActivity { + case automotive + case bicycle + case pedestrian + + public var zoom: Double { + switch self { + case .automotive: + 16.0 + case .bicycle: + 18.0 + case .pedestrian: + 20.0 + } + } + + public var pitch: Double { + switch self { + case .automotive, .bicycle: + 45.0 + case .pedestrian: + 10.0 + } + } +} + public extension MapViewCamera { /// Is the camera currently tracking (navigating) var isTrackingUserLocationWithCourse: Bool { @@ -10,13 +36,58 @@ public extension MapViewCamera { return false } + /// The default camera for navigation based on activity type. + /// + /// - Parameter activity: The navigation activity profile. + /// - Returns: The configured MapViewCamera + static func navigation(activity: NavigationActivity = .automotive) -> MapViewCamera { + MapViewCamera.trackUserLocationWithCourse( + zoom: activity.zoom, + pitch: activity.pitch, + pitchRange: .fixed(activity.pitch) + ) + } + /// The default camera for automotive navigation. /// /// - Parameters: /// - zoom: The zoom value (default is 18.0) /// - pitch: The pitch (default is 45.0) /// - Returns: The configured MapViewCamera - static func automotiveNavigation(zoom: Double = 18.0, pitch: Double = 45.0) -> MapViewCamera { + static func automotiveNavigation( + zoom: Double = NavigationActivity.automotive.zoom, + pitch: Double = NavigationActivity.automotive.pitch + ) -> MapViewCamera { + MapViewCamera.trackUserLocationWithCourse(zoom: zoom, + pitch: pitch, + pitchRange: .fixed(pitch)) + } + + /// The default camera for bicycle navigation. + /// + /// - Parameters: + /// - zoom: The zoom value (default is 18.0) + /// - pitch: The pitch (default is 45.0) + /// - Returns: The configured MapViewCamera + static func bicycleNavigation( + zoom: Double = NavigationActivity.bicycle.zoom, + pitch: Double = NavigationActivity.bicycle.pitch + ) -> MapViewCamera { + MapViewCamera.trackUserLocationWithCourse(zoom: zoom, + pitch: pitch, + pitchRange: .fixed(pitch)) + } + + /// The default camera for pedestrian navigation. + /// + /// - Parameters: + /// - zoom: The zoom value (default is 20.0) + /// - pitch: The pitch (default is 10.0) + /// - Returns: The configured MapViewCamera + static func pedestrianNavigation( + zoom: Double = NavigationActivity.pedestrian.zoom, + pitch: Double = NavigationActivity.pedestrian.pitch + ) -> MapViewCamera { MapViewCamera.trackUserLocationWithCourse(zoom: zoom, pitch: pitch, pitchRange: .fixed(pitch)) diff --git a/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift b/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift index 78d6519fb..c4558935e 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift @@ -68,13 +68,17 @@ public struct DynamicallyOrientingNavigationView: View { public var body: some View { GeometryReader { geometry in + let isNavigating = navigationState?.isNavigating == true + ZStack { NavigationMapView( styleURL: styleURL, camera: $camera, navigationState: navigationState, onStyleLoaded: { _ in - camera = navigationCamera + if isNavigating { + camera = navigationCamera + } } ) { userLayers @@ -88,16 +92,10 @@ public struct DynamicallyOrientingNavigationView: View { isMuted: isMuted, showMute: navigationState?.isNavigating == true, onMute: onTapMute, - showZoom: true, + showZoom: isNavigating, onZoomIn: { camera.incrementZoom(by: 1) }, onZoomOut: { camera.incrementZoom(by: -1) }, - cameraControlState: camera.isTrackingUserLocationWithCourse ? .showRouteOverview { - if let overviewCamera = navigationState?.routeOverviewCamera { - camera = overviewCamera - } - } : .showRecenter { // TODO: Third case when not navigating! - camera = navigationCamera - }, + cameraControlState: cameraControlState, onTapExit: onTapExit ) .navigationViewInnerGrid { @@ -119,16 +117,10 @@ public struct DynamicallyOrientingNavigationView: View { isMuted: isMuted, showMute: navigationState?.isNavigating == true, onMute: onTapMute, - showZoom: true, + showZoom: isNavigating, onZoomIn: { camera.incrementZoom(by: 1) }, onZoomOut: { camera.incrementZoom(by: -1) }, - cameraControlState: camera.isTrackingUserLocationWithCourse ? .showRouteOverview { - if let overviewCamera = navigationState?.routeOverviewCamera { - camera = overviewCamera - } - } : .showRecenter { // TODO: Third case when not navigating! - camera = navigationCamera - }, + cameraControlState: cameraControlState, onTapExit: onTapExit ) .navigationViewInnerGrid { @@ -146,6 +138,23 @@ public struct DynamicallyOrientingNavigationView: View { } } } + + private var cameraControlState: CameraControlState { + if navigationState?.isNavigating != true { + return .hidden + } + if camera.isTrackingUserLocationWithCourse { + guard let overviewCamera = navigationState?.routeOverviewCamera else { + return .hidden + } + return .showRouteOverview { + camera = overviewCamera + } + } + return .showRecenter { + camera = navigationCamera + } + } } #Preview("Portrait Navigation View (Imperial)") { diff --git a/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift b/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift index b6cc8dff2..6d45e2061 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift @@ -89,11 +89,7 @@ public struct LandscapeNavigationView: View { showZoom: true, onZoomIn: { camera.incrementZoom(by: 1) }, onZoomOut: { camera.incrementZoom(by: -1) }, - cameraControlState: camera.isTrackingUserLocationWithCourse ? CameraControlState.showRecenter { - // TODO: - } : .showRecenter { - camera = navigationCamera - }, + cameraControlState: cameraControlState, onTapExit: onTapExit ) .navigationViewInnerGrid { @@ -111,6 +107,23 @@ public struct LandscapeNavigationView: View { } } } + + private var cameraControlState: CameraControlState { + if navigationState?.isNavigating != true { + return .hidden + } + if camera.isTrackingUserLocationWithCourse { + guard let overviewCamera = navigationState?.routeOverviewCamera else { + return .hidden + } + return .showRouteOverview { + camera = overviewCamera + } + } + return .showRecenter { + camera = navigationCamera + } + } } @available(iOS 17, *) diff --git a/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift b/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift index 363f41153..69a4ed967 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift @@ -89,11 +89,7 @@ public struct PortraitNavigationView: View { showZoom: true, onZoomIn: { camera.incrementZoom(by: 1) }, onZoomOut: { camera.incrementZoom(by: -1) }, - cameraControlState: camera.isTrackingUserLocationWithCourse ? CameraControlState.showRecenter { - // TODO: - } : .showRecenter { // TODO: Third case when not navigating! - camera = navigationCamera - }, + cameraControlState: cameraControlState, onTapExit: onTapExit ) .navigationViewInnerGrid { @@ -111,6 +107,23 @@ public struct PortraitNavigationView: View { } } } + + private var cameraControlState: CameraControlState { + if navigationState?.isNavigating != true { + return .hidden + } + if camera.isTrackingUserLocationWithCourse { + guard let overviewCamera = navigationState?.routeOverviewCamera else { + return .hidden + } + return .showRouteOverview { + camera = overviewCamera + } + } + return .showRecenter { + camera = navigationCamera + } + } } #Preview("Portrait Navigation View (Imperial)") { diff --git a/apple/Tests/FerrostarMapLibreUITests/Extensions/MapViewCameraActivityTests.swift b/apple/Tests/FerrostarMapLibreUITests/Extensions/MapViewCameraActivityTests.swift new file mode 100644 index 000000000..5257fe4d9 --- /dev/null +++ b/apple/Tests/FerrostarMapLibreUITests/Extensions/MapViewCameraActivityTests.swift @@ -0,0 +1,17 @@ +import FerrostarMapLibreUI +import MapLibreSwiftUI +import XCTest + +final class MapViewCameraActivityTests: XCTestCase { + func testActivityNavigationCamerasTrackUserLocationWithCourse() { + XCTAssertTrue(MapViewCamera.navigation(activity: .automotive).isTrackingUserLocationWithCourse) + XCTAssertTrue(MapViewCamera.navigation(activity: .bicycle).isTrackingUserLocationWithCourse) + XCTAssertTrue(MapViewCamera.navigation(activity: .pedestrian).isTrackingUserLocationWithCourse) + } + + func testNamedNavigationCameraFactoriesTrackUserLocationWithCourse() { + XCTAssertTrue(MapViewCamera.automotiveNavigation().isTrackingUserLocationWithCourse) + XCTAssertTrue(MapViewCamera.bicycleNavigation().isTrackingUserLocationWithCourse) + XCTAssertTrue(MapViewCamera.pedestrianNavigation().isTrackingUserLocationWithCourse) + } +} diff --git a/apple/Tests/FerrostarMapLibreUITests/Views/__Snapshots__/LandscapeNavigationViewTests/testCustomized.1.png b/apple/Tests/FerrostarMapLibreUITests/Views/__Snapshots__/LandscapeNavigationViewTests/testCustomized.1.png index 2a7582391..e1a775deb 100644 Binary files a/apple/Tests/FerrostarMapLibreUITests/Views/__Snapshots__/LandscapeNavigationViewTests/testCustomized.1.png and b/apple/Tests/FerrostarMapLibreUITests/Views/__Snapshots__/LandscapeNavigationViewTests/testCustomized.1.png differ diff --git a/apple/Tests/FerrostarMapLibreUITests/Views/__Snapshots__/PortraitNavigationViewTests/testCustomized.1.png b/apple/Tests/FerrostarMapLibreUITests/Views/__Snapshots__/PortraitNavigationViewTests/testCustomized.1.png index 0cd775c44..2eee87cac 100644 Binary files a/apple/Tests/FerrostarMapLibreUITests/Views/__Snapshots__/PortraitNavigationViewTests/testCustomized.1.png and b/apple/Tests/FerrostarMapLibreUITests/Views/__Snapshots__/PortraitNavigationViewTests/testCustomized.1.png differ