Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usage as non navigation map #275

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
4 changes: 3 additions & 1 deletion apple/DemoApp/Demo/DemoNavigationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ struct DemoNavigationView: View {
}
}
CircleStyleLayer(identifier: "foo", source: source)

}
)
.innerGrid(
Expand Down Expand Up @@ -146,7 +147,7 @@ struct DemoNavigationView: View {
}
)
.task {
await getRoutes()
await getRoutes()
}
}
}
Expand Down Expand Up @@ -216,6 +217,7 @@ struct DemoNavigationView: View {
return "No location - authed as \(locationProvider.authorizationStatus)"
}


return "±\(Int(userLocation.horizontalAccuracy))m accuracy"
}

Expand Down
44 changes: 37 additions & 7 deletions apple/DemoApp/Ferrostar Demo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 60;
objectVersion = 56;
objects = {

/* Begin PBXBuildFile section */
Expand All @@ -20,6 +20,9 @@
169A1D582B2E8280006CE59E /* FerrostarMapLibreUI in Frameworks */ = {isa = PBXBuildFile; productRef = 169A1D572B2E8280006CE59E /* FerrostarMapLibreUI */; };
169B50132B2E46800008EBB7 /* FerrostarCore in Frameworks */ = {isa = PBXBuildFile; productRef = 169B50122B2E46800008EBB7 /* FerrostarCore */; };
169B50152B2E46800008EBB7 /* FerrostarMapLibreUI in Frameworks */ = {isa = PBXBuildFile; productRef = 169B50142B2E46800008EBB7 /* FerrostarMapLibreUI */; };
CDCC2A2F2C89B876007D7940 /* FerrostarCore in Frameworks */ = {isa = PBXBuildFile; productRef = CDCC2A2E2C89B876007D7940 /* FerrostarCore */; };
CDCC2A312C89B876007D7940 /* FerrostarMapLibreUI in Frameworks */ = {isa = PBXBuildFile; productRef = CDCC2A302C89B876007D7940 /* FerrostarMapLibreUI */; };
CDCC2A332C89B876007D7940 /* FerrostarSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = CDCC2A322C89B876007D7940 /* FerrostarSwiftUI */; };
E90D97912B8AF507005E43F8 /* Navigation Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90D97902B8AF507005E43F8 /* Navigation Delegate.swift */; };
E9505FCA2AD449B30016BF0A /* FerrostarCore in Frameworks */ = {isa = PBXBuildFile; productRef = E9505FC92AD449B30016BF0A /* FerrostarCore */; };
E9505FCC2AD449B30016BF0A /* FerrostarMapLibreUI in Frameworks */ = {isa = PBXBuildFile; productRef = E9505FCB2AD449B30016BF0A /* FerrostarMapLibreUI */; };
Expand All @@ -35,6 +38,7 @@
1663679E2B2F8FB3008BFF1F /* MockLocationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockLocationData.swift; sourceTree = "<group>"; };
168ECA792B2E8C42007B11DE /* API-Keys.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "API-Keys.plist"; sourceTree = "<group>"; };
168ECA7B2B2F6A1E007B11DE /* Ferrostar Demo-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Ferrostar Demo-Info.plist"; sourceTree = "<group>"; };
CD343C722CA00A6500E59E95 /* ferrostar */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ferrostar; path = /Users/patrick/Downloads/ferrostar; sourceTree = "<absolute>"; };
Copy link
Contributor

@michaelkirk michaelkirk Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a weird entry with your local machine details.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, yes I was referencing my local copy so that I could edit it directly in Xcode. Will fix in next commit.

E90D97902B8AF507005E43F8 /* Navigation Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Navigation Delegate.swift"; sourceTree = "<group>"; };
E9505FB72AD449700016BF0A /* Ferrostar Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ferrostar Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; };
E9DD18E42B18EE7A00CAF29A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
Expand All @@ -48,10 +52,13 @@
files = (
E9505FCA2AD449B30016BF0A /* FerrostarCore in Frameworks */,
169B50152B2E46800008EBB7 /* FerrostarMapLibreUI in Frameworks */,
CDCC2A2F2C89B876007D7940 /* FerrostarCore in Frameworks */,
167FD69B2B54B55A00CB4445 /* FerrostarCore in Frameworks */,
E9505FCC2AD449B30016BF0A /* FerrostarMapLibreUI in Frameworks */,
169A1D582B2E8280006CE59E /* FerrostarMapLibreUI in Frameworks */,
167FD69D2B54B55A00CB4445 /* FerrostarMapLibreUI in Frameworks */,
CDCC2A332C89B876007D7940 /* FerrostarSwiftUI in Frameworks */,
CDCC2A312C89B876007D7940 /* FerrostarMapLibreUI in Frameworks */,
169B50132B2E46800008EBB7 /* FerrostarCore in Frameworks */,
169A1D562B2E8280006CE59E /* FerrostarCore in Frameworks */,
);
Expand Down Expand Up @@ -94,6 +101,7 @@
E9505FAE2AD449700016BF0A = {
isa = PBXGroup;
children = (
CD343C722CA00A6500E59E95 /* ferrostar */,
168ECA792B2E8C42007B11DE /* API-Keys.plist */,
168ECA7B2B2F6A1E007B11DE /* Ferrostar Demo-Info.plist */,
1611A5522B2E6E98006B131D /* Demo */,
Expand Down Expand Up @@ -136,6 +144,9 @@
169A1D572B2E8280006CE59E /* FerrostarMapLibreUI */,
167FD69A2B54B55A00CB4445 /* FerrostarCore */,
167FD69C2B54B55A00CB4445 /* FerrostarMapLibreUI */,
CDCC2A2E2C89B876007D7940 /* FerrostarCore */,
CDCC2A302C89B876007D7940 /* FerrostarMapLibreUI */,
CDCC2A322C89B876007D7940 /* FerrostarSwiftUI */,
);
productName = "iOS Demo";
productReference = E9505FB72AD449700016BF0A /* Ferrostar Demo.app */;
Expand Down Expand Up @@ -166,7 +177,7 @@
);
mainGroup = E9505FAE2AD449700016BF0A;
packageReferences = (
167FD6992B54B55A00CB4445 /* XCLocalSwiftPackageReference "../.." */,
CDCC2A2D2C89B876007D7940 /* XCRemoteSwiftPackageReference "ferrostar" */,
);
productRefGroup = E9505FB82AD449700016BF0A /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -412,12 +423,16 @@
};
/* End XCConfigurationList section */

/* Begin XCLocalSwiftPackageReference section */
167FD6992B54B55A00CB4445 /* XCLocalSwiftPackageReference "../.." */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../..;
/* Begin XCRemoteSwiftPackageReference section */
CDCC2A2D2C89B876007D7940 /* XCRemoteSwiftPackageReference "ferrostar" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/stadiamaps/ferrostar";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.10.1;
};
};
/* End XCLocalSwiftPackageReference section */
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
167FD69A2B54B55A00CB4445 /* FerrostarCore */ = {
Expand All @@ -444,6 +459,21 @@
isa = XCSwiftPackageProductDependency;
productName = FerrostarMapLibreUI;
};
CDCC2A2E2C89B876007D7940 /* FerrostarCore */ = {
isa = XCSwiftPackageProductDependency;
package = CDCC2A2D2C89B876007D7940 /* XCRemoteSwiftPackageReference "ferrostar" */;
productName = FerrostarCore;
};
CDCC2A302C89B876007D7940 /* FerrostarMapLibreUI */ = {
isa = XCSwiftPackageProductDependency;
package = CDCC2A2D2C89B876007D7940 /* XCRemoteSwiftPackageReference "ferrostar" */;
productName = FerrostarMapLibreUI;
};
CDCC2A322C89B876007D7940 /* FerrostarSwiftUI */ = {
isa = XCSwiftPackageProductDependency;
package = CDCC2A2D2C89B876007D7940 /* XCRemoteSwiftPackageReference "ferrostar" */;
productName = FerrostarSwiftUI;
};
E9505FC92AD449B30016BF0A /* FerrostarCore */ = {
isa = XCSwiftPackageProductDependency;
productName = FerrostarCore;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"originHash" : "744a0ae659b64807871ea942d6e59965bff2d0e86920ea6d1ba0dc9587578660",
"pins" : [
{
"identity" : "maplibre-gl-native-distribution",
Expand Down Expand Up @@ -28,6 +27,15 @@
"version" : "0.0.10"
}
},
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : {
"revision" : "7b0bbbae90c41f848f90ac7b4df6c4f50068256d",
"version" : "1.17.5"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
Expand All @@ -47,5 +55,5 @@
}
}
],
"version" : 3
"version" : 2
}
2 changes: 1 addition & 1 deletion apple/Sources/FerrostarCore/FerrostarCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public protocol FerrostarCoreDelegate: AnyObject {

private let networkSession: URLRequestLoading
private let routeProvider: RouteProvider
private let locationProvider: LocationProviding
public let locationProvider: LocationProviding
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this become public?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed access to this to help me with debugging, could be turned to private again, though one thing that might be useful is making this a public var, so that one can switch between the simulated location provider and the real location provider if needed (either for debugging, or for tunnel simulation, unless ferrostar core is planning to handle that internally.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open to making this public so that it would be easier to switch (though I personally have a hard time thinking this will get any use outside debug builds, but I also know I lack imagination sometimes; call me out an whatever I'm missing :D).

The main reason it's private right now is that it's not actually safe to just go resetting it at will. It would need setter logic to ensure that everything below is aware and subscribes to updates correctly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think 99% of the cases will be debug cases. That said, I do think its an important debug case:

  • exposing it as read only gives devs important information on why ferrostar might be behaving how its behaving (where does ferrostar think it is, what accuracy does it have, etc)
  • we have a debug menu where in maplibre navigation, we switch between the simulated and real location manager. We use this frequently as switching between real locations and simulated locations is very powerful, we do not need to rebuild (or have access to xcode location simulating) to quickly test both modes, depending on our situation (working in the office, on the go, on the go but stationary). So a set-able version would be great - and I think we might need to be able to do this anyway for continuing navigation while accuracy is bad like when users are in a tunnel?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those debug use cases make sense.

I think we might need to be able to do this anyway for continuing navigation while accuracy is bad like when users are in a tunnel?

You might be righte about this... Some use cases will be better solved with a custom LocationProvider (ex: if you have wheel sensors, sophisticated inertial processing, etc.). I haven't yet examined the existing approaches closely enough to see how these work.

One approach would be the developer using the simulated provider directly and replacing it as you're implying here. This would actually be pretty straightforward; it has some shortcomings right now like not being able to very easily set the speed, but those are easy to address. Another could be a "fallback" configuration where the dev doesn't have to reset the location provider directly but rather provides a fallback designed to simulate progress + parameters on when to switch back and forth, what speed to simulate progress at, how long (distance) to simulate progress, decay in speed as you get past some threshold, etc.

This is all pretty half-baked, but the idea behind that all would be to let you do something like "my router tells me that this tunnel is 3km long. My average speed over 30 seconds leading up to the tunnel has been 70kph with minimal variance, so we'll simulate progress at 68kph and hopefully we get a smooth transition." And at the end of the tunnel idk just stop or slow way down? And kick back over to "live" location once we get a location update from CoreLocation with an accuracy of better than 50m.

TL;DR though, yes, switching should be possible, both directly and maybe indirectly ;)

private var navigationController: NavigationControllerProtocol?
private var routeRequestInFlight = false
private var lastAutomaticRecalculation: Date? = nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
let navigationCamera: MapViewCamera

private var navigationState: NavigationState?
private let userLayers: [StyleLayerDefinition]
private let userLayers: () -> [StyleLayerDefinition]

private let mapViewModifiersWhenNotNavigating: (MapView<MLNMapViewController>) -> AnyView

public var topCenter: (() -> AnyView)?
public var topTrailing: (() -> AnyView)?
public var midLeading: (() -> AnyView)?
public var bottomTrailing: (() -> AnyView)?

var onTapExit: (() -> Void)?

public var minimumSafeAreaInsets: EdgeInsets
Expand All @@ -44,21 +46,26 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
public init(
styleURL: URL,
camera: Binding<MapViewCamera>,
navigationCamera: MapViewCamera = .automotiveNavigation(),
navigationCamera: MapViewCamera = .automotiveNavigation()
,
navigationState: NavigationState?,
minimumSafeAreaInsets: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16),
onTapExit: (() -> Void)? = nil,
@MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] }
@MapViewContentBuilder makeMapContent: @escaping () -> [StyleLayerDefinition] = { [] },
mapViewModifiersWhenNotNavigating: @escaping (MapView<MLNMapViewController>) -> AnyView = { transferView in
AnyView(transferView)
}
) {
self.styleURL = styleURL
self.navigationState = navigationState
self.minimumSafeAreaInsets = minimumSafeAreaInsets
self.onTapExit = onTapExit

userLayers = makeMapContent()
userLayers = makeMapContent

_camera = camera
self.navigationCamera = navigationCamera
self.mapViewModifiersWhenNotNavigating = mapViewModifiersWhenNotNavigating
}

public var body: some View {
Expand All @@ -69,11 +76,14 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
camera: $camera,
navigationState: navigationState,
onStyleLoaded: { _ in
camera = navigationCamera
}
) {
userLayers
}
if navigationState?.isNavigating == true {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this be called when the user calls ferrostarCore.startNavigation(route: route)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope - unless start navigation is called before the style completes. But this call here should only be seen as a fallback, something else should set the camera to navigation camera, like startNavigation.

Copy link
Collaborator

@Archdoog Archdoog Sep 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea for this concept generally. Could we extend the NavigationMapView with navigations state event view modifier(s) (e.g. func navigationDidStart(onStart: () -> Void)). Allowing the developer to apply modifications to the map view's configuration when certain navigationState events occur? This might be a bit overkill, but seems like it would first allow us to do default behaviors like setting the navigationCamera one time when navigation starts as well as let the developer modify the view's behavior when certain nav event occur? It may also be quite scalable as an approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might not actually need this if we go with your suggestion (which I'll elaborate on in my review comments), since the modifiers get applied any time state changes ;)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we might not need it, it might still be convenient? one can already achieve this by a .onChange(ferrostar.state) but this would be easier to use and understand for people starting with the framework?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, fair point ;) I'm open to draft implementations from either of you if you think it's helpful. I originally was super enthusiastic when @Archdoog described it on our call but then realized it might not be a hard requirement with the other proposed changes.

camera = navigationCamera
}

},
makeMapContent: userLayers,
mapViewModifiersWhenNotNavigating: mapViewModifiersWhenNotNavigating)

.navigationMapViewContentInset(NavigationMapViewContentInsetMode(
orientation: orientation,
geometry: geometry
Expand All @@ -84,7 +94,7 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
LandscapeNavigationOverlayView(
navigationState: navigationState,
speedLimit: nil,
showZoom: true,
showZoom: navigationState?.isNavigating == true,
Copy link
Contributor

@michaelkirk michaelkirk Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I'd actually never like to show the zoom buttons, and rely only on pinch/tap to zoom in all cases.

I wonder if a delegate query would be a better choice

(or maybe init parameter is more conventional in SwiftUI? I'm still pretty new to SwiftUI and not confident on the conventions)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I'm not a fan of zoom buttons either, but I wanted to keep it as close as possible to the current setup first :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All for that. The only reason I added the zoom buttons in the first place was to include them in the available UI components and to provide a simple verification for zoom behavior while navigating.

Long term goal is the existing configurable static zoom as well as a fancier dynamic zoom per activity. E.g. zooms out when as route annotation speed increases. We're almost there on the "current annotation" state, so this probably won't be hard shortly.

This can become as fancy as we want as long as various use cases are supported 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this SHOULD be configurable. Thanks for pointing it out!

On zoom or no zoom, that's a decision that app developers can make. I don't like the screen real estate, but OTOH, 80% of the time pinch zoom makes the camera stop following me (in every nav app I can think of), and that's ALMOST never what I wanted, so I can appreciate the button option ;)

onZoomIn: { camera.incrementZoom(by: 1) },
onZoomOut: { camera.incrementZoom(by: -1) },
showCentering: !camera.isTrackingUserLocationWithCourse,
Expand All @@ -104,7 +114,7 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
PortraitNavigationOverlayView(
navigationState: navigationState,
speedLimit: nil,
showZoom: true,
showZoom: navigationState?.isNavigating == true,
onZoomIn: { camera.incrementZoom(by: 1) },
onZoomOut: { camera.incrementZoom(by: -1) },
showCentering: !camera.isTrackingUserLocationWithCourse,
Expand Down
35 changes: 31 additions & 4 deletions apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,22 @@ public struct NavigationMapView: View {
var mapViewContentInset: UIEdgeInsets = .zero
var onStyleLoaded: (MLNStyle) -> Void
let userLayers: [StyleLayerDefinition]

let mapViewModifiersWhenNotNavigating: (MapView<MLNMapViewController>) -> AnyView

// TODO: Configurable camera and user "puck" rotation modes

private var navigationState: NavigationState?
private var navigationState: NavigationState?

@State private var locationManager = StaticLocationManager(initialLocation: CLLocation())

// MARK: Camera Settings

@Binding var camera: MapViewCamera

private var effectiveMapViewContentInset: UIEdgeInsets {
return navigationState?.isNavigating == true ? mapViewContentInset : .zero
}

/// Initialize a map view tuned for turn by turn navigation.
///
Expand All @@ -41,15 +47,20 @@ public struct NavigationMapView: View {
camera: Binding<MapViewCamera>,
navigationState: NavigationState?,
onStyleLoaded: @escaping ((MLNStyle) -> Void),
@MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] }
@MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] },
mapViewModifiersWhenNotNavigating: @escaping (MapView<MLNMapViewController>) -> AnyView = { transferView in
AnyView(transferView)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish we wouldn't have to type-erase here, but I didn't find a way to get this to work with it returning some View or a MapView as this closure may contain both MapView modifiers such as onMapTapGesture, but also View modifiers such as onTapGesture.

}
) {
self.styleURL = styleURL
_camera = camera
self.navigationState = navigationState
self.onStyleLoaded = onStyleLoaded
userLayers = makeMapContent()
self.userLayers = makeMapContent()
self.mapViewModifiersWhenNotNavigating = mapViewModifiersWhenNotNavigating
}

@ViewBuilder
public var body: some View {
MapView(
styleURL: styleURL,
Expand All @@ -74,11 +85,12 @@ public struct NavigationMapView: View {
// Overlay any additional user layers.
userLayers
}
.mapViewContentInset(mapViewContentInset)
.mapViewContentInset(effectiveMapViewContentInset)
.mapControls {
// No controls
}
.onStyleLoaded(onStyleLoaded)
.applyTransform(if: navigationState?.isNavigating != true, transform: mapViewModifiersWhenNotNavigating)
.ignoresSafeArea(.all)
}

Expand All @@ -94,6 +106,21 @@ public struct NavigationMapView: View {
}
}

extension MapView<MLNMapViewController> {
@ViewBuilder
func applyTransform<Content: View>(
if condition: Bool,
transform: (MapView<MLNMapViewController>) -> Content
) -> some View {
if condition {
transform(self)
} else {
self
}
}
}


#Preview("Navigation Map View") {
// TODO: Make map URL configurable but gitignored
let state = NavigationState.modifiedPedestrianExample(droppingNWaypoints: 4)
Expand Down
Loading