From 395b366757eb00e104703ef18cc75c9049ea717d Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Tue, 8 Oct 2024 14:11:20 +0900 Subject: [PATCH 1/5] Revamp contributing guide; freshen some other stale docs --- CONTRIBUTING.md | 235 +----------------- guide/book.toml | 2 +- guide/src/SUMMARY.md | 47 ++-- .../configuring-the-navigation-controller.md | 109 ++++++-- guide/src/contributing.md | 45 ++++ guide/src/dev-env-setup.md | 123 +++++++++ guide/src/getting-started.md | 5 + guide/src/jetpack-compose-customization.md | 2 +- guide/src/route-providers.md | 45 +++- guide/src/rust-getting-started.md | 34 ++- guide/src/swiftui-customization.md | 24 +- guide/src/testing.md | 97 ++++++++ 12 files changed, 459 insertions(+), 309 deletions(-) create mode 100644 guide/src/contributing.md create mode 100644 guide/src/dev-env-setup.md create mode 100644 guide/src/getting-started.md create mode 100644 guide/src/testing.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf0aaa84..3f8a2d66 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,234 +1 @@ -# Contributing to Ferrostar - -We're stoked that you're interested in working on Ferrostar! -This contribution guide will get you started developing in no time, -as well as provide some guidelines to follow when submitting an issue or PR. - -## Best Practices for Contributions - -We welcome contributions from community! -Due to the size and complexity of the code, below are some best practices that ensure smooth collaboration. - -It is a good idea to discuss large proposed changes before proceeding to an issue ticket or PR. -The project team is active in the following forums: - -* For informal chat discussions, visit the `#ferrostar` channel in the OSMUS Slack. - You can get an invite to the workspace at [slack.openstreetmap.us](https://slack.openstreetmap.us/). -* For larger discussions where it would be desirable to have wider input / a less ephemeral record, - consider starting a thread on [GitHub Discussions](https://github.com/stadiamaps/ferrostar/discussions). - This makes it easier to find and reference the discussion in the future. - -### Testing - -Both new features and bugfixes should update or add unit test cases where possible -to prevent regressions and demonstrate correctness. -This is particularly true of the common core. - -We are a bit more lax with the frontend code as this may be difficult or impractical to test. -We have been gradually introducing snapshot testing on iOS as a way to overcome these difficulties, -but it's not perfect. -Suggestions welcome for Android. - -### New Features - -For new features, you should generally start by opening a new issue. -That will allow for separate tracking of discussion of the feature itself -and (if you're proposing code as well) the implementation of the feature. - -### Bug Fixes - -If you've identified a significant bug, or one that you don't intend to fix yourself, -please write up an issue ticket describing the problem. -For minor or straightforward bug fixes, feel free to proceed directly to a PR. - -### Pull Request Tips - -To speed up reviews, it's helpful if you enable edits from maintainers when opening the PR. -In the case of minor changes, formatting, or style nitpicks, we can make edits directly to avoid wasting your time. -In order to enable edits from maintainers, **you'll need to make the PR from a fork owned an individual**, -not an organization. -GitHub org-owned forks lack this flexibility. - -Note: we enforce formatting checks on PRs. -If you forget to do this, CI will eventually fail on your PR. - -## Preparing your Development Environment - - -To ensure that everything can be developed properly in parallel, -we use a monorepo structure. -This, combined with CI, will ensure that changes in the core must be immediately reflected in platform code -like Apple and Android. - -Let's look at what's involved to get hacking on each platform. - -### Rust - -1. Install [Rust](https://www.rust-lang.org/). - If at all possible, install `rustup`. - We use [rust-toolchain.yml](common/rust-toolchain.yml) - to synchronize the toolchain and install targets automatically - (otherwise you will need to manage toolchains manually). -2. Open the cargo workspace (`common/`) in your preferred editing environment. - -The Rust project is a cargo workspace, -and nothing beyond the above should be needed to start hacking! - -Before pushing, run the following in the `common` folder: - -1. Run `cargo fmt` to automatically format any new rust code. -2. Run `cargo insta review` to update snapshot testing changes. -3. Run `cargo test` to validate testing and ensure snapshot changes were correctly applied by step 2. -4. Manually bump the version on the ferrostar Cargo.toml at `common/ferrostar/Cargo.toml`. If you forget to do this and make breaking changes, CI will fail. -if you don't. - -### Web - -Perform all commands unless otherwise noted from the `web` directory. - -1. Install `wasm-pack`: - -```shell -cargo install wasm-pack -``` - -2. Build the WASM package for the core: - -```shell -npm run prepare:core -``` - -3. Install dependencies: - -```shell -npm install -``` - -4. Run a local dev server or do a release build: - -```shell -# This will start a local web server for the demo app with hot reload -npm run dev - -# Or you can do a release build (we test this in CI) -npm run build -``` - -### iOS - -1. Install the latest version of Xcode. -2. Install the Xcode Command Line Tools. - -```shell -xcode-select --install -``` - -3. Install [`swiftformat`](https://github.com/nicklockwood/SwiftFormat). -4. Since you're developing locally, set `let useLocalFramework = true` in `Package.swift`. - (TODO: Figure out a way to extract this so it doesn't get accidentally committed.) -5. Run the iOS build script: - -```shell -cd common -./build-ios.sh -``` - -**IMPORTANT:** every time you make changes to the common core, -you will need to run [`build-ios.sh`](common/build-ios.sh) to see your changes on iOS! -We want to integrate this into the Xcode build flow in the future, -but at the time of this writing, -it is not possible with the Swift package flow. -Further, the "normal" Xcode build flow always assumes xcframeworks can't change during build, -so it processes them before any other build rules. -Given these limitations, we opted for a shell script until further notice. - -5. Open the Swift package in Xcode. - (NOTE: Due to the quirks of how SPM is designed, - Package.swift must live in the repo root. - This makes the project view in Xcode slightly more cluttered, - but there isn't much we can do about this given how SPM works.) - -Run `swiftformat .` from the `apple` directory before committing -to ensure consistent formatting. - -### Android - -1. Install [Android Studio](https://developer.android.com/studio). -2. Install cargo-ndk to allow gradle to build the local library `libferrostar.so` and `libuniffi_ferrostar.so`. - With cargo-ndk installed you can load and sync Android Studio then build the demo app allowing gradle to - automatically build what it needs. - -```sh -cargo install cargo-ndk -``` - -3. Ensure that the latest NDK is installed - (refer to the `ndkVersion` number in [`core/build.gradle`](android/core/build.gradle) - and ensure you have the same version available). - This is easiest to install via Android Studio's SDK Manager (under SDK Tools > NDK). -4. Set up Github Packages authentication if you haven't already done so. - - - Get a Personal Access Token with permission to read packages - - Save your GitHub username and PAT in a Gradle properties file (ex: ~/.gradle/gradle.properties) like so: - - See [GitHub's guide](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#authenticating-to-github-packages) for more details. - - ``` - gpr.user=username - gpr.token=token - ``` -5. Open the Gradle workspace ('android/') in Android Studio. - Gradle builds automatically ensure the core is built, - so there are no funky scripts needed as on iOS. - -Run the `ktfmtFormat` gradle action before committing to ensure consistent formatting. - -## Writing & Running Tests - -### Common Core - -Run `cargo test -p ferrostar` from within the `common` directory to run tests. - -### Web - -Run `wasm-pack test --firefox --headless ferrostar --no-default-features --features wasm_js` from within the `common` directory to run tests. - -### iOS - -Run unit tests as usual from within Xcode. - -### Android - -Android uses both tests and androidTests (connectedChecks) to verify functionality. Included in normal unit tests are paparazzi snapshot tests for UI components. - -#### Recording Snapshots - -Run the gradle task `recordPaparazziDebug`. This can be done from the gradle menu under `verification`. - -## Code Conventions - -* Format all Rust code using `cargo fmt` -* Run `cargo clippy` and either fix any warnings or document clearly why you think the linter should be ignored -* All iOS code must be written in Swift -* SwiftFormat is used to automatically format all swift code. This must be run manually from within the project folder using the command line tool `swiftformat .`. For more information on installation see [SwiftFormat/Installation](https://github.com/nicklockwood/SwiftFormat?tab=readme-ov-file#how-do-i-install-it) -* All Android code must be written in Kotlin -* ktfmt is used to automatically format all kotlin code. This can be run using the `ktfmtFormat` step under `formatting` in the gradle menu. - -## Changelog Conventions - -NOTE: We'll be *extremely* loose with this -until we have solid beta quality releases for both iOS and Android. - -What warrants a changelog entry? - -- Any change that affects the public API, visual appearance or user security *must* have a changelog entry -- Any performance improvement or bugfix *should* have a changelog entry -- Any contribution from a community member *may* have a changelog entry, no matter how small -- Any documentation related changes *should not* have a changelog entry -- Any regression change introduced and fixed within the same release *should not* have a changelog entry -- Any internal refactoring, technical debt reduction, test, or benchmark related change *should not* have a changelog entry - -How to add your changelog? - -- Edit the [`CHANGELOG.md`](CHANGELOG.md) file directly, inserting a new entry at the top of the appropriate list -- Any changelog entry should be descriptive and concise; it should explain the change to a reader without context - +The contributing guide has been moved to the [Ferrostar Book](https://stadiamaps.github.io/ferrostar/contributing.html). diff --git a/guide/book.toml b/guide/book.toml index 94522950..ae65a51c 100644 --- a/guide/book.toml +++ b/guide/book.toml @@ -3,7 +3,7 @@ authors = ["Ian Wagner"] language = "en" multilingual = false src = "src" -title = "The Ferrostar User Guide" +title = "The Ferrostar Book" [preprocessor] diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index a6eab51d..978ade6c 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -4,28 +4,31 @@ [Terminology and Conventions](./terminology-and-conventions.md) [Platform Support Targets](./platform-support-targets.md) -# Getting Started - -- [iOS](./ios-getting-started.md) -- [Android](./android-getting-started.md) -- [Web](./web-getting-started.md) -- [Rust](./rust-getting-started.md) - -# Customization - -- [General](./general-customization.md) -- [Navigation Behavior](./configuring-the-navigation-controller.md) -- [SwiftUI](./swiftui-customization.md) -- [Jetpack Compose](./jetpack-compose-customization.md) -- [Android Foreground Services](./android-foreground-service.md) -- [Web](./web-customization.md) - -# Architecture - +--- + +# User Guide + +- [Quick Start Tutorials](./getting-started.md) + - [iOS](./ios-getting-started.md) + - [Android](./android-getting-started.md) + - [Web](./web-getting-started.md) + - [Rust](./rust-getting-started.md) +- [Customization](./general-customization.md) + - [Navigation Behavior](./configuring-the-navigation-controller.md) + - [Route Providers](./route-providers.md) + - [Location Providers](./location-providers.md) + - [SwiftUI](./swiftui-customization.md) + - [Jetpack Compose](./jetpack-compose-customization.md) + - [Android Foreground Services](./android-foreground-service.md) + - [Web](./web-customization.md) + +# Contributor Guide + +- [Contributing to Ferrostar](./contributing.md) +- [Developer Environment Setup](./dev-env-setup.md) +- [Testing](./testing.md) - [Architecture Overview](./architecture.md) -- [Route Providers](./route-providers.md) -- [Location Providers](./location-providers.md) -# Appendices +--- -- [Routing and Basemap Vendors](./vendors.md) +[Routing and Basemap Vendors](./vendors.md) diff --git a/guide/src/configuring-the-navigation-controller.md b/guide/src/configuring-the-navigation-controller.md index 6f8ee94a..86eb8be3 100644 --- a/guide/src/configuring-the-navigation-controller.md +++ b/guide/src/configuring-the-navigation-controller.md @@ -4,7 +4,7 @@ Not all navigation experiences should behave the same, so Ferrostar lets you customize many important aspects of navigation. These options are surfaced when calling `startNavigation` on most platforms. -The higher-level platform interfaces wrap the `NavigationControllerConfig` in the Rust core. +The higher-level platform interfaces wrap [`NavigationControllerConfig`](https://docs.rs/ferrostar/latest/ferrostar/navigation_controller/models/struct.NavigationControllerConfig.html) in the Rust core. ## `StepAdvanceMode` @@ -14,8 +14,10 @@ We have a few built-in variants in the core, which you can find in the [Rust documentation](https://docs.rs/ferrostar/latest/ferrostar/navigation_controller/models/enum.StepAdvanceMode.html). The high-level platform wrappers also have this and should show in your IDE documentation panel. -If you want to build your own custom step advance logic, you can observe the `TripState` -in your application code and manually call `advanceToNextStep` on the `NavigationController`. +If you want to build your own custom step advance logic, +set the `StepAdvanceMode` to manual, +and observe the `TripState` in your application code. +Then, you can manually call `advanceToNextStep` on the `NavigationController`. ## `RouteDeviationTracking` @@ -23,25 +25,74 @@ This determines when the user is off the route. Certain applications (pedestrian navigation, for example) may want to disable this. If the built-in deviation tracking options aren’t enough -(for example, if you want to do map matching), +(for example, if you want to do local map matching), you can decide this yourself by implementing the `RouteDeviationDetector` interface. -You can do this directly in your Swift or Kotlin code! + +PRs are welcome for improvements or new general-purpose behaviors. +You can also implement the interfaces directly in your Swift or Kotlin code! +Here are some trivial examples. + +Swift: + +```swift +let config = SwiftNavigationControllerConfig( + stepAdvance: .relativeLineStringDistance(minimumHorizontalAccuracy: 16, automaticAdvanceDistance: 16), + routeDeviationTracking: .custom(detector: { _, _, _ in + // Pretend that the user is always off route + .offRoute(deviationFromRouteLine: 42) + }), + snappedLocationCourseFiltering: .raw +) + +try core.startNavigation(route: route, config: config) +``` + +Kotlin: + +```kotlin +val config = NavigationControllerConfig( + stepAdvance = StepAdvanceMode.RelativeLineStringDistance(16U, 16U), + routeDeviationTracking = + RouteDeviationTracking.Custom( + detector = + object : RouteDeviationDetector { + override fun checkRouteDeviation( + location: UserLocation, + route: Route, + currentRouteStep: RouteStep + ): RouteDeviation { + // Pretend that the user is always off route + return RouteDeviation.OffRoute(42.0) + } + }), + CourseFiltering.RAW) +core.startNavigation(route, config) +``` ### Recalculation -Recalculation is separate, but related to whether the user is off the route. -If you provide your own deviation detector, keep this in mind! -Some apps may wish to display a flashing red overlay, for example, but not recalculate immediately. +NOTE: This section is currently specific to Swift and Kotlin. +The Rust core does not expose any primitives for handling recalculation; +this is currently at the platform level, and is not yet implemented for web. + +The *default* behavior on supported platforms +is to recalculate whenever the core determines that the user is off the route. +Skip to the next section if the default behavior works for you. -NOTE: The default behavior is to recalculate whenever the core determines that the user is off the route. -You can skip to the next section if the default behavior works for you. +If you want to do something more advanced though, you can! +In Ferrostar, **determining whether the user is off route and whether to recalculate the route are two separate concerns.** +Keep this in mind when writing your custom deviation detector. +For example, if you want to display a flashing red overlay +but not recalculate immediately, +you could immediately report the user as off route, but delay recalculation. #### Interfaces for signaling when to recalculate To reflect these separate responsibilities, you can set a delegate (`FerrostarCoreDelegate`) on iOS or `RouteDeviationHandler` on Android. -This lets you specify what corrective action to take when the user deviates from the route. +This lets you tell the core what corrective action (if any) +to take when the user deviates from the route. To initiate recalculation, return an appropriate `CorrectiveAction`. The higher-level platform layer will automatically handle the details @@ -54,24 +105,28 @@ without a high-level wrapper. ## Interfaces for handling alternative routes -If you’re following closely, you may have noticed that this section is a higher heading level. -What’s up with that? - -Well, alternative route handling is potentially broader than just recalculations after missing a turn! -But let’s discuss that case first. - -As usual, Ferrostar tries to have sensible defaults, -so if you don’t specify custom behavior, -the platform layer will automatically start a new navigation session -with the first route it finds after recalculation -*if* the user is still off-course (if the user is back on track, it is discarded). +Closely related to recalculation due to going off route is alternative route handling. +This can occur either because you missed a turn and went off the route, +or for other reasons like live traffic info suggesting that +the current route is no longer optimal. +Both scenarios are handled via alternative route hooks. + +As usual, Ferrostar tries to have sensible defaults. + +Considering the recalculation case, +if you don’t specify custom behavior, +the platform layer (again, currently iOS and Android only) +will automatically start a new navigation session +with the first route it receives finds after recalculation. +As a sanity check, +this behavior only triggers if the user is *still* off-course +(if the user went back on track in the interim, nothing happens). If you want to customize this behavior, set a `FerrostarCoreDelegate` on iOS or an `AlternativeRouteProcessor` on Android. -Be sure to handle edge cases like the user going back on the route! So, what about other cases besides recalculation? -While the interfaces don’t yet exist to build this, -we envision some cases with live traffic benefiting from periodic -“route revalidation” based on current conditions. +We envision live traffic, incidents, etc. being used to feed periodic +“route revalidation” in the future. The same “alternative route” notification mechanism -will accommodate this case. \ No newline at end of file +can be extended (ex: with a reason why the alternative is being supplied) +for this purpose. \ No newline at end of file diff --git a/guide/src/contributing.md b/guide/src/contributing.md new file mode 100644 index 00000000..fa3af514 --- /dev/null +++ b/guide/src/contributing.md @@ -0,0 +1,45 @@ +# Contributing to Ferrostar + +We're stoked that you're interested in working on Ferrostar! +This contribution guide will get you started developing in no time, +and provides some guidelines to follow when submitting an issue or PR. + +## How we communicate + +It is a good idea to discuss large proposed changes +before proceeding to an issue ticket or PR. +The maintainers and active contributors use the following forums: + +* For informal chat discussions, visit the `#ferrostar` channel in the OSMUS Slack. + You can get an invite to the workspace at [slack.openstreetmap.us](https://slack.openstreetmap.us/). +* For larger discussions where it would be desirable to have wider input / a less ephemeral record, + you can open an issue or discussion on GitHub. + +### Issues vs Discussions? + +If you have a pretty clear feature request or bug report, +just open a GitHub Issue. + +If it’s a bit more open-ended, consider [GitHub Discussions](https://github.com/stadiamaps/ferrostar/discussions) first. + +Discussions are organized into two categories: engineering RFDs and Q&A. +Q&A should be self-explanatory; if you have a question about something, +need help with an integration, etc., then post it here! +Engineering RFDs are designed to start a discussion about the best way to do something, +share research, and discuss larger projects before the issue stage. +If you’re curious what an RFD is, +[check out this document from Oxide Computer Company](https://rfd.shared.oxide.computer/rfd/0001) + +## Pull Request Tips + +To speed up reviews, +it's helpful if you enable edits from maintainers when opening the PR. +In the case of minor changes, formatting, or style nitpicks, +we can make edits directly to avoid wasting your time. +In order to enable edits from maintainers, **you'll need to make the PR from a fork owned an individual**, +not an organization. +GitHub org-owned forks lack this flexibility. + +If your change is visual in nature and isn’t covered by snapshot tests, +before+after screenshots or videos are *greatly* appreciated! + diff --git a/guide/src/dev-env-setup.md b/guide/src/dev-env-setup.md new file mode 100644 index 00000000..f6e1db5b --- /dev/null +++ b/guide/src/dev-env-setup.md @@ -0,0 +1,123 @@ +# Developer Environment Setup + +To ensure that everything can be developed properly in parallel, +we use a monorepo structure. +This, combined with CI, will ensure that changes in the core must be immediately reflected in platform code +like Apple and Android. + +Let's look at what's involved to get hacking on each platform. + +### Rust + +1. Install [Rust](https://www.rust-lang.org/). + If at all possible, install `rustup`. + We use [rust-toolchain.yml](common/rust-toolchain.yml) + to synchronize the toolchain and install targets automatically + (otherwise you will need to manage toolchains manually). +2. Open the cargo workspace (`common/`) in your preferred editing environment. + +The Rust project is a cargo workspace, +and nothing beyond the above should be needed to start hacking! +Make some changes and run the tests! + +#### PR checklist + +Before pushing, run the following from the `common` folder: + +* Run `cargo fmt` to automatically format any new rust code. +* Bump the version on the ferrostar Cargo.toml at `common/ferrostar/Cargo.toml` (if necessary). + If you forget to do this and make breaking changes, CI will let you know. + +### Web + +Perform all commands unless otherwise noted from the `web` directory. + +1. Install `wasm-pack`: + +```shell +cargo install wasm-pack +``` + +2. Build the WASM package for the core: + +```shell +npm run prepare:core +``` + +3. Install dependencies: + +```shell +npm install +``` + +4. Run a local dev server or do a release build: + +```shell +# This will start a local web server for the demo app with hot reload +npm run dev + +# Or you can do a release build (we test this in CI) +npm run build +``` + +### iOS + +1. Install the latest version of Xcode. +2. Install the Xcode Command Line Tools. + +```shell +xcode-select --install +``` + +3. Install [`swiftformat`](https://github.com/nicklockwood/SwiftFormat). +4. Since you're developing locally, set `let useLocalFramework = true` in `Package.swift`. + (TODO: Figure out a way to extract this so it doesn't get accidentally committed.) +5. Run the iOS build script: + +```shell +cd common +./build-ios.sh +``` + +**IMPORTANT:** every time you make changes to the common core, +you will need to run [`build-ios.sh`](common/build-ios.sh) to see your changes on iOS! +We want to integrate this into the Xcode build flow in the future, +but at the time of this writing, +it is not possible with the Swift package flow. +Further, the "normal" Xcode build flow always assumes `xcframeworks` can't change during build, +so it processes them before any other build rules. +Given these limitations, we opted for a shell script until further notice. + +5. Open the Swift package in Xcode. + (NOTE: Due to the quirks of how SPM is designed, + Package.swift must live in the repo root. + This makes the project view in Xcode slightly more cluttered, + but there isn't much we can do about this given how SPM works.) + +#### PR checklist + +Run `swiftformat .` from the `apple` directory before committing +to ensure consistent formatting. + +### Android + +1. Install [Android Studio](https://developer.android.com/studio) (NOTE: We assume you are using a recent version no more than ~a month out of date). +2. Install cargo-ndk to allow gradle to build the local library `libferrostar.so` and `libuniffi_ferrostar.so`. + With cargo-ndk installed you can load and sync Android Studio then build the demo app allowing gradle to + automatically build what it needs. + +```sh +cargo install cargo-ndk +``` + +3. Ensure that the latest NDK is installed + (refer to the `ndkVersion` number in [`core/build.gradle`](android/core/build.gradle) + and ensure you have the same version available). + This is easiest to install via Android Studio's SDK Manager (under SDK Tools > NDK). +4. Open the Gradle workspace ('android/') in Android Studio. + Gradle builds automatically ensure the core is built, + so there are no funky scripts needed as on iOS. + +#### PR checklist + +Run the `ktfmtFormat` gradle action before committing to ensure consistent formatting. diff --git a/guide/src/getting-started.md b/guide/src/getting-started.md new file mode 100644 index 00000000..8c8b1986 --- /dev/null +++ b/guide/src/getting-started.md @@ -0,0 +1,5 @@ +# Quick Start Tutorials + +This section contains quick start tutorials to get you going quickly +with a basic navigation experience and the “batteries included” UI. +Pick a platform to get started! \ No newline at end of file diff --git a/guide/src/jetpack-compose-customization.md b/guide/src/jetpack-compose-customization.md index 9f9f4837..9c1f3f96 100644 --- a/guide/src/jetpack-compose-customization.md +++ b/guide/src/jetpack-compose-customization.md @@ -23,7 +23,7 @@ See the [vendors page](./vendors.md) for some ideas. ### Camera -TODO: Ability to override the built-in camera behavior (probably define a protocol for this). +TODO: Docs on how this works. ### Adding map layers diff --git a/guide/src/route-providers.md b/guide/src/route-providers.md index bab28590..6800c168 100644 --- a/guide/src/route-providers.md +++ b/guide/src/route-providers.md @@ -13,8 +13,8 @@ Contributions and discussion around the best ways to enable this are very much w ## `RouteProvider` There are two types of route providers: -one more suited to HTTP servers, -and another designed for other use cases like local route generation. +one more suited to HTTP servers (`RouteAdapter`), +and another designed for other use cases like local route generation (`CustomRouteProvider`). The core ships with common implementations, but you’re free to define your own *in your application code* without waiting for a PR to land or running a custom fork! @@ -40,19 +40,39 @@ but it also has more compact Protobuf and (much) more verbose OSRM serializers. The OSRM serializer is the one that’s typically used for navigation use cases, so with a single `RouteResponseParser` implementation, we can parse responses from a Valhalla server or an OSRM server! -In fact, we can also parse responses from the Mapbox Directions API too! -Both Valhalla and Mapbox have OSRM serializers that add a few extra fields +In fact, we can also parse responses from +the Mapbox Directions API or GraphHopper (in certain modes), +as they offer OSRM-compatible responses. + +Every “extended OSRM” API includes additional data which are useful for navigation applications. -The OSRM parser in the core of Ferrostar handles all of these “flavors” gracefully. +**The parser in the core of Ferrostar handles all of these “flavors” gracefully,** +**and provides either direct or indirect support for most extensions.** +Of special note, the voice and banner instructions +(popularized by Mapbox and supported in Valhalla) +are always parsed, when available, and included in the route object. +Annotations, which are available in some form or other on all OSRM-like APIs, +can be parsed as *anything*. +This leaves annotations open to extension, +since it’s already used this way in practice. OSRM parser in hand, all that we need to do to support these different APIs is a `RouteRequestGenerator` for each. -All three speak HTTP, but they have different request formats. +While all services mentioned are HTTP-based, +each has a different request format. Writing a `RouteRequestGenerator` is pretty easy though. Request generators return a sum type (enum/sealed class) -indicating the type of request to make (only HTTP POST is supported at this time) +indicating the type of request to make and associated data like the URL, headers, and request body. +By splitting up the request generation, +request execution, and response parsing into distinct steps, +we reduce the work required to support a new API. +In this example, all we had to do was supply a `RouteRequestGenerator`. +Our `RouteAdapter` was able to use the existing `OsrmResponseParser`, +and the core (ex: `FerrostarCore` on iOS or Android) +used the platform native HTTP stack to execute the request on our behalf. + Here’s a sequence diagram illustrating the flow of data between components. Don’t worry too much about the internal complexity; you’ll only interface with `FerrostarCore` at the boundary. @@ -70,9 +90,9 @@ sequenceDiagram #### Bundled support -The Ferrostar crate includes support for the following out of the box. +Ferrostar includes support for the following APIs out of the box. -##### Valhalla +##### Valhalla (Request + Response) Valhalla APIs are supported out of the box with full request and response parsing. @@ -88,7 +108,7 @@ You can construct an instance of `ValhallaHttpRequestGenerator` directly or using the convenience method `createValhallaRequestGenerator` from Swift or Kotlin. -##### OSRM +##### OSRM (Response only) OSRM has become something of a de facto *linga franca* for navigation APIs. Ferrostar comes bundled with support for decoding OSRM responses, @@ -102,7 +122,7 @@ The autogenerated FFI bindings expose a `createOsrmResponseParser` method in case you want to roll your own `RouteAdapter` for an API that uses a different request format but returns OSRM format responses. -#### Implementing your own +#### Implementing your own `RouteAdapter` If you’re working with a routing engine and want to see it directly supported in Ferrostar, @@ -121,6 +141,9 @@ For example, to integrate with an API that returns OSRM responses but has a different request format, you only need a `RouteRequestGenerator`; you can re-use the OSRM response parser. +Refer to the core test code on GitHub for examples which mock both halves. +TODO: Examples here after 1.0. + ### `CustomRouteProvider` Custom route providers are most commonly used for local route generation, diff --git a/guide/src/rust-getting-started.md b/guide/src/rust-getting-started.md index 8bcfd342..1df18573 100644 --- a/guide/src/rust-getting-started.md +++ b/guide/src/rust-getting-started.md @@ -1,8 +1,36 @@ # Rust Not using a platform listed / building an embedded application? -You're in the right spot. +You're in the right spot! -Documentation on [docs.rs](https://docs.rs/ferrostar/latest). Start with the navigation controller. +Ferrostar is built in Rust, +which makes it portable to a wide range of OS and CPU combinations. +The core includes common data models, traits, common routing backend integrations, +and more. -TODO: Tutorial. +The core documentation is hosted, like every crate, on [docs.rs](https://docs.rs/ferrostar/latest). + +The core of a custom navigation experience is the [`NavigationController`](https://docs.rs/ferrostar/latest/ferrostar/navigation_controller/struct.NavigationController.html). +The controller is initialized with a route and configuration. + +You can either construct a route yourself manually, +or use some of the existing tooling to get started. +Unlike the higher level platforms like iOS and Swift, +no high-level core wrapper handles HTTP for you (to keep the core light), +but you can add your own using a crate like `reqwest`, +or you could run an embedded routing engine for offline routing. +If your routing API uses a common response format like OSRM, +[check out the included parsers](https://docs.rs/ferrostar/latest/ferrostar/routing_adapters/index.html). + +The `NavigationController` is pure (in a functional sense), +and it is up to integrators to decide on the most appropriate state storage mechanism. +Use the `get_initial_state` function to create an initial state with the user’s location. +Then, as new location updates arrive, or you decide to manually advance to the next step, +call the appropriate methods. +Each method call returns a new `TripState`, +which you can store and take any actions on +(such as updating the UI or deciding to recalculate the route). + +At a high level, that’s pretty much it! +The Ferrostar core includes most of the proverbial legos; +the rest is up to you. \ No newline at end of file diff --git a/guide/src/swiftui-customization.md b/guide/src/swiftui-customization.md index 02faa1fe..7e02748a 100644 --- a/guide/src/swiftui-customization.md +++ b/guide/src/swiftui-customization.md @@ -7,33 +7,37 @@ This page walks you through the ways to customize the SwiftUI frontend to your l ## Customizing the map -Ferrostar includes a map view based on [MapLibre Native](https://maplibre.org/). -This is configurable with a number of constructor parameters. -If the existing customizations don’t work for you, -first we’d love to hear why via an issue on GitHub! +Ferrostar includes a map view built with the +[MapLibre SwiftUI DSL](https://github.com/maplibre/swiftui-dsl). +This is designed to be fairly configurable, +so if the existing customizations don’t work for you, +we’d love to hear why via an issue on GitHub! + In the case that you want complete control though, -the map view itself is actually not that complex. +the provided wrappers around map view are not that complex. + +TODO: Docs on how to build your own navigation views + describe the current overlay layers. The demo app is designed to be instructive in showing many available options, so be sure to look at that to build intuition. ### Style -We allow you to pass a style URL to any of the map view constructors. +You can pass a style URL to any of the navigation map view constructors. You can vary this dynamically as your app theme changes (ex: in dark mode). -TODO: Passing a view builder to add layers to the map (WIP) - ### Camera -TODO: Ability to override the built-in camera behavior (probably define a protocol for this). +The camera supports two-way manipulation via SwiftUI bindings. +TODO: more documentation ### Adding map layers You can add your own overlays too! The `makeMapContent` closure argument of the various map and navigation views enables you to add more layers. -See the demo app for an example. +See the demo app for an example, where we add a little dot showing the raw location +in addition to the puck, which snaps to the route line. ## Customizing the instruction banners diff --git a/guide/src/testing.md b/guide/src/testing.md new file mode 100644 index 00000000..1679147b --- /dev/null +++ b/guide/src/testing.md @@ -0,0 +1,97 @@ +# Testing + +We employ a mix of testing tools and methodologies across the Ferrostar stack. +We use a mix of unit, integration, snapshot, and property testing. +When possible, please include tests in your PRs. +If you can employ multiple strategies +(ex: unit testing + property testing), +please do! + +Tests are automatically run as part of CI. + +## Types of tests + +Unit tests typically verify that some function gives an expected output +for some known inputs. +This is great for verifying specific properties +of pure functions. +For example, checking that 1+1 = 2. +However, just because you checked a few examples doesn’t tell us that the code is correct. +Despite this limitation, unit tests are a great tool, and are very fast to run. +Add them where possible, as they complement other strategies. + +Property testing lets you specify some random variables +(possibly with limits; ex: floating point numbers between -180 and 180) +rather than static inputs. +This lets you test *invariants* rather than specific cases. +For example, asserting that *any* integer plus zero equals itself. +Property testing is a great fit for highly “algorithmic” code, +and we use it extensively. + +Snapshot testing executes some code +and then takes a “snapshot” of the state. +This can be applied to UI code, +where an image snapshot of a view is saved after rendering. +We use this extensively for ensuring that overlays render correctly +with static inputs, for example. +It can also be applied to arbitrary data structures. +We use snapshot tests to test things like +“given a fixed route and a stream of GPS updates, +what do all the intermediate state transitions look like?” + +Finally, integration testing is similar to unit testing +in that the inputs are static. +However, it’s designed to test much more of a system “end to end” +whereas unit tests are usually targeted at a single function. + +All of the approaches above combine to give us great confidence +in the correctness of Ferrostar’s core business logic, +and let us refactor without fear of breakage. + +Let’s look at the specific tooling for each platform. + +## Rust + +On Rust, we employ both unit and integration tests using the standard cargo tooling. +In addition, we employ both property testing via [`proptest`](https://crates.io/crates/proptest) +and snapshot testing via [`insta`](https://crates.io/crates/cargo-insta). + +There is nothing special about running unit, integration, and property tests; +just run `cargo test`! +For snapshot tests, when a snapshot does not exist (new test) +or the test output doesn’t match the stored snapshot, +you’ll get a test error. +You can review the changes using `cargo insta review`, +a CLI tool which shows a colorized diff and prompts whether to update the snapshot. +Snapshot files are committed to the repo and will show in the PR diffs. + +## iOS + +iOS testing is best done in Xcode, but you can also use the terminal +(see the github actions for commands we run). +Open up Xcode and press cmd+u to run tests. + +Most of the tests are just regular XCUnit tests that you’re already used to. +Of note, we include some snapshot testing via macros +which snapshot the views. +When adding a new snapshot test or changing a view, +you’ll get an error. +Modify the snapshot assertion function to include the keyword argument +`record: true` to update the snapshot. +This will be committed to git and visible in diffs. + +## Android + +On Android, we use a mix of standard JUnit unit tests (fast), +snapshot tests with Paparazzi (pretty fast), +and connected checks (SLOW). +You can invoke tests with `./gradlew test`, +`./gradlew verifyPaparazziDebug`, +and `./gradlew connectedCheck` respectively. + +To record snapshots for a new test or update old ones, +run `./gradlew recordPaparazziDebug`. + +## Web + +TBD \ No newline at end of file From 0fd1bc20408024914de88d93a3551c0aaa134532 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Tue, 8 Oct 2024 15:51:35 +0900 Subject: [PATCH 2/5] Add typos action --- .github/workflows/typos.yml | 22 ++++++++++++++++++++++ _typos.toml | 2 ++ 2 files changed, 24 insertions(+) create mode 100644 .github/workflows/typos.yml create mode 100644 _typos.toml diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml new file mode 100644 index 00000000..06d93f75 --- /dev/null +++ b/.github/workflows/typos.yml @@ -0,0 +1,22 @@ +name: Check for Typos + +on: + pull_request: + branches: [ main ] + +jobs: + typos: + runs-on: macos-14 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-ios-swiftformat + cancel-in-progress: true + + steps: + - name: Upgrade swiftformat to latest + run: brew install swiftformat + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Run typos + uses: crate-ci/typos@v1.26.0 diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 00000000..6d45184d --- /dev/null +++ b/_typos.toml @@ -0,0 +1,2 @@ +[files] +extend-exclude = ["**/build/*", "*.pbxproj", "**/dist/*", "guide/**/*.js"] \ No newline at end of file From c36b8cf3a357ca8a4c6ef0b0b8f64280180bf970 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Tue, 8 Oct 2024 16:08:34 +0900 Subject: [PATCH 3/5] Fix typos... once and for all ;) --- _typos.toml | 3 +++ android/core/build.gradle | 2 +- ...vigationOverlayView.kt => PortraitNavigationOverlayView.kt} | 0 .../Views/DynamicallyOrientingNavigationView.swift | 2 +- .../Views/GridViews/NavigatingInnerGridView.swift | 2 +- guide/src/android-getting-started.md | 2 +- 6 files changed, 7 insertions(+), 4 deletions(-) rename android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/{PortaitNavigationOverlayView.kt => PortraitNavigationOverlayView.kt} (100%) diff --git a/_typos.toml b/_typos.toml index 6d45184d..8045786d 100644 --- a/_typos.toml +++ b/_typos.toml @@ -1,2 +1,5 @@ +[default.extend-words] +rememberable = "rememberable" + [files] extend-exclude = ["**/build/*", "*.pbxproj", "**/dist/*", "guide/**/*.js"] \ No newline at end of file diff --git a/android/core/build.gradle b/android/core/build.gradle index 7c8bab72..cbc1e228 100644 --- a/android/core/build.gradle +++ b/android/core/build.gradle @@ -116,7 +116,7 @@ mavenPublishing { pom { name = "Ferrostar Core" - description = "Core libray, models, and navigation business logic for Ferrostar" + description = "Core library, models, and navigation business logic for Ferrostar" commonPomConfig(it) } } diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/PortaitNavigationOverlayView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/PortraitNavigationOverlayView.kt similarity index 100% rename from android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/PortaitNavigationOverlayView.kt rename to android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/PortraitNavigationOverlayView.kt diff --git a/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift b/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift index bb56e0fd..9306aa45 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift @@ -28,7 +28,7 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn public var minimumSafeAreaInsets: EdgeInsets - /// Create a dynamically orienting navigation view. This view automatically arranges child views for both portait + /// Create a dynamically orienting navigation view. This view automatically arranges child views for both portrait /// and landscape orientations. /// /// - Parameters: diff --git a/apple/Sources/FerrostarSwiftUI/Views/GridViews/NavigatingInnerGridView.swift b/apple/Sources/FerrostarSwiftUI/Views/GridViews/NavigatingInnerGridView.swift index 0712db7e..3207d814 100644 --- a/apple/Sources/FerrostarSwiftUI/Views/GridViews/NavigatingInnerGridView.swift +++ b/apple/Sources/FerrostarSwiftUI/Views/GridViews/NavigatingInnerGridView.swift @@ -25,7 +25,7 @@ public struct NavigatingInnerGridView: View, CustomizableNavigatingInnerGridView /// The default navigation inner grid view. /// /// This view provides all default navigation UI views that are used in the open map area. This area is defined as - /// between the header/banner view and footer/arrival view in portait mode. + /// between the header/banner view and footer/arrival view in portrait mode. /// On landscape mode it is the trailing half of the screen. /// /// - Parameters: diff --git a/guide/src/android-getting-started.md b/guide/src/android-getting-started.md index 4c826377..336376bf 100644 --- a/guide/src/android-getting-started.md +++ b/guide/src/android-getting-started.md @@ -150,7 +150,7 @@ and then initialize later once the `Context` is available. // Instance variable definition private lateinit var locationProvider: FusedLocationProvider -// Later when the activity loads and a context is avaialable +// Later when the activity loads and a context is available locationProvider = FusedLocationProvider(context = this) ``` From 831aee70ea928df08954e6b9bf161a0017d63076 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Tue, 8 Oct 2024 16:20:03 +0900 Subject: [PATCH 4/5] Remove extraneous step --- .github/workflows/typos.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 06d93f75..e754a1f2 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -12,9 +12,6 @@ jobs: cancel-in-progress: true steps: - - name: Upgrade swiftformat to latest - run: brew install swiftformat - - name: Checkout repo uses: actions/checkout@v4 From e8e9c25ba680b0f71d5aed5c18f4ebec7b650bb5 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 9 Oct 2024 10:04:34 +0900 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Michael Kirk --- guide/src/configuring-the-navigation-controller.md | 2 +- guide/src/contributing.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/guide/src/configuring-the-navigation-controller.md b/guide/src/configuring-the-navigation-controller.md index 86eb8be3..52089a77 100644 --- a/guide/src/configuring-the-navigation-controller.md +++ b/guide/src/configuring-the-navigation-controller.md @@ -117,7 +117,7 @@ Considering the recalculation case, if you don’t specify custom behavior, the platform layer (again, currently iOS and Android only) will automatically start a new navigation session -with the first route it receives finds after recalculation. +with the first route it receives after recalculation. As a sanity check, this behavior only triggers if the user is *still* off-course (if the user went back on track in the interim, nothing happens). diff --git a/guide/src/contributing.md b/guide/src/contributing.md index fa3af514..1452e391 100644 --- a/guide/src/contributing.md +++ b/guide/src/contributing.md @@ -36,7 +36,7 @@ To speed up reviews, it's helpful if you enable edits from maintainers when opening the PR. In the case of minor changes, formatting, or style nitpicks, we can make edits directly to avoid wasting your time. -In order to enable edits from maintainers, **you'll need to make the PR from a fork owned an individual**, +In order to enable edits from maintainers, **you'll need to make the PR from a fork owned by an individual**, not an organization. GitHub org-owned forks lack this flexibility.