From 9b445619a5fa1fe8aca7f85093b40be05298b6fa Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 9 Oct 2024 10:40:36 +0900 Subject: [PATCH] Revamp contributing guide; freshen some other stale docs (#291) * Revamp contributing guide; freshen some other stale docs * Add typos action * Fix typos... once and for all ;) * Remove extraneous step * Apply suggestions from code review Co-authored-by: Michael Kirk --------- Co-authored-by: Michael Kirk --- .github/workflows/typos.yml | 19 ++ CONTRIBUTING.md | 235 +----------------- _typos.toml | 5 + android/core/build.gradle | 2 +- ...ew.kt => PortraitNavigationOverlayView.kt} | 0 .../DynamicallyOrientingNavigationView.swift | 2 +- .../GridViews/NavigatingInnerGridView.swift | 2 +- guide/book.toml | 2 +- guide/src/SUMMARY.md | 47 ++-- guide/src/android-getting-started.md | 2 +- .../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 ++++++++ 19 files changed, 487 insertions(+), 313 deletions(-) create mode 100644 .github/workflows/typos.yml create mode 100644 _typos.toml rename android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/{PortaitNavigationOverlayView.kt => PortraitNavigationOverlayView.kt} (100%) 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/.github/workflows/typos.yml b/.github/workflows/typos.yml new file mode 100644 index 00000000..e754a1f2 --- /dev/null +++ b/.github/workflows/typos.yml @@ -0,0 +1,19 @@ +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: Checkout repo + uses: actions/checkout@v4 + + - name: Run typos + uses: crate-ci/typos@v1.26.0 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/_typos.toml b/_typos.toml new file mode 100644 index 00000000..8045786d --- /dev/null +++ b/_typos.toml @@ -0,0 +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/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/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) ``` diff --git a/guide/src/configuring-the-navigation-controller.md b/guide/src/configuring-the-navigation-controller.md index 6f8ee94a..52089a77 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 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..1452e391 --- /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 by 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