Overpass Performance, ApplicationConfig
This commit is contained in:
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code when working with code in this reposi
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This is an Android navigation app built with Jetpack Compose that supports multiple routing providers (OSRM, Valhalla, TomTom) and includes Android Auto/Automotive OS integration. The app uses MapLibre for rendering, DataStore for local persistence, and Koin for dependency injection.
|
This is an Android navigation app built with Jetpack Compose that supports multiple routing providers (OSRM, Valhalla, TomTom) and includes Android Auto/Automotive OS integration. The app uses MapLibre for rendering, Androidx DataStore for local persistence, and Koin for dependency injection.
|
||||||
|
|
||||||
## Build Commands
|
## Build Commands
|
||||||
|
|
||||||
@@ -12,14 +12,15 @@ This is an Android navigation app built with Jetpack Compose that supports multi
|
|||||||
# Build the app (from repository root)
|
# Build the app (from repository root)
|
||||||
./gradlew :app:assembleDebug
|
./gradlew :app:assembleDebug
|
||||||
|
|
||||||
# Build specific flavor
|
# Build a specific flavor
|
||||||
|
./gradlew :app:assemblePlayDebug
|
||||||
./gradlew :app:assembleDemoDebug
|
./gradlew :app:assembleDemoDebug
|
||||||
./gradlew :app:assembleFullDebug
|
./gradlew :app:assembleFullDebug
|
||||||
|
|
||||||
# Run tests
|
# Run unit tests
|
||||||
./gradlew test
|
./gradlew test
|
||||||
|
|
||||||
# Run tests for specific module
|
# Run tests for a specific module
|
||||||
./gradlew :common:data:test
|
./gradlew :common:data:test
|
||||||
./gradlew :common:car:test
|
./gradlew :common:car:test
|
||||||
|
|
||||||
@@ -32,12 +33,12 @@ This is an Android navigation app built with Jetpack Compose that supports multi
|
|||||||
|
|
||||||
## Module Structure
|
## Module Structure
|
||||||
|
|
||||||
The project uses a multi-module architecture:
|
The project uses a multi-module architecture (see `settings.gradle.kts`):
|
||||||
|
|
||||||
- **app/** - Main Android app with Jetpack Compose UI for phone
|
- **app/** - Main Android app with Jetpack Compose UI for phone (`com.kouros.navigation`)
|
||||||
- **common/data/** - Core data layer with routing logic, repositories, and data models (shared by all modules)
|
- **common/data/** - Core data layer with routing logic, repositories, view models, persistence (`com.kouros.data`)
|
||||||
- **common/car/** - Android Auto/Automotive OS UI implementation
|
- **common/car/** - Android Auto/Automotive OS UI implementation
|
||||||
- **automotive/** - Placeholder for future native Automotive OS app
|
- **automotive/** - Placeholder for future native Automotive OS app (no Kotlin sources yet)
|
||||||
|
|
||||||
Dependencies flow: `app` → `common:car` → `common:data`
|
Dependencies flow: `app` → `common:car` → `common:data`
|
||||||
|
|
||||||
@@ -45,156 +46,212 @@ Dependencies flow: `app` → `common:car` → `common:data`
|
|||||||
|
|
||||||
### Routing Providers (Pluggable System)
|
### Routing Providers (Pluggable System)
|
||||||
|
|
||||||
The app supports three routing engines that implement the `NavigationRepository` abstract class:
|
The app supports three routing engines that extend the `NavigationRepository` abstract class (`common/data/.../data/NavigationRepository.kt`):
|
||||||
|
|
||||||
1. **OsrmRepository** - OSRM routing engine
|
1. **ValhallaRepository** - Valhalla routing engine (ordinal 0)
|
||||||
2. **ValhallaRepository** - Valhalla routing engine
|
2. **OsrmRepository** - OSRM routing engine (ordinal 1)
|
||||||
3. **TomTomRepository** - TomTom routing engine
|
3. **TomTomRepository** - TomTom routing engine (ordinal 2, default)
|
||||||
|
|
||||||
Each provider has a corresponding mapper class (`OsrmRoute`, `ValhallaRoute`, `TomTomRoute`) that converts provider-specific JSON responses to the universal `Route` data model.
|
Selection is driven by `RouteEngine` enum order (see `Data.kt`). `NavigationRepository` also exposes shared HTTP helpers (`fetchUrl`, `searchPlaces`, `reverseAddress`) that use Nominatim for geocoding and `ApplicationConfig`-backed HTTP basic auth for protected endpoints.
|
||||||
|
|
||||||
|
Each provider has a corresponding mapper (`OsrmRoute`, `ValhallaRoute`, `TomTomRoute`) that converts provider-specific JSON into the universal `Route` model. The universal model is decomposed into a dedicated `data/route/` package: `Routes`, `Leg`, `Step`, `Maneuver`, `Intersection`, `Lane`, `Summary`.
|
||||||
|
|
||||||
**Adding a new routing provider:**
|
**Adding a new routing provider:**
|
||||||
1. Create `NewProviderRepository` extending `NavigationRepository` in `common/data/src/main/java/com/kouros/navigation/data/`
|
1. Create `NewProviderRepository` extending `NavigationRepository` under `common/data/src/main/java/com/kouros/navigation/data/<provider>/`
|
||||||
2. Implement `getRoute()` method
|
2. Implement `getRoute()` and `getTraffic()`
|
||||||
3. Create `NewProviderRoute.kt` with `mapToRoute()` function
|
3. Create `NewProviderRoute.kt` with a `mapToRoute(response, builder)` function that populates a `Route.Builder`
|
||||||
4. Add provider detection logic in `Route.Builder.route()`
|
4. Add a `RouteEngine` enum entry and provider branch in `Route.Builder.route()` (`data/Route.kt`)
|
||||||
5. Update `NavigationUtils.getViewModel()` to return appropriate ViewModel
|
5. Update `NavigationUtils.getViewModel()` (`utils/NavigationUtils.kt`) to return a `NavigationViewModel` wired to the new repository
|
||||||
|
|
||||||
### Data Flow
|
### Data Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
User Action (search/select destination)
|
User action (search / select destination)
|
||||||
↓
|
↓
|
||||||
ViewModel.loadRoute() [LiveData]
|
NavigationViewModel.loadRoute() [LiveData / Flow]
|
||||||
↓
|
↓
|
||||||
NavigationRepository.getRoute() [Selected provider]
|
NavigationRepository.getRoute() [selected provider]
|
||||||
↓
|
↓
|
||||||
*Route.mapToRoute() [Convert to universal Route model]
|
*Route.mapToRoute() [convert to universal Route]
|
||||||
↓
|
↓
|
||||||
RouteModel.startNavigation()
|
RouteModel.startNavigation()
|
||||||
↓
|
↓
|
||||||
RouteModel.updateLocation() [On each location update]
|
RouteCalculator.findStep() [on each location update]
|
||||||
↓
|
↓
|
||||||
UI observes LiveData and displays current step
|
NavigationState updated → UI observes and renders current step
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Classes
|
### Key Classes
|
||||||
|
|
||||||
**Navigation Logic:**
|
**Navigation logic** (`common/data/.../model/`):
|
||||||
- `RouteModel.kt` - Core navigation engine (tracks position, calculates distances, manages steps)
|
- `RouteModel.kt` - Core navigation engine; tracks position, manages step progression, owns `NavigationState`
|
||||||
- `RouteCarModel.kt` - Extends RouteModel with Android Auto-specific formatting
|
- `RouteCalculator.kt` - Step-finding algorithm: snaps current location to the nearest waypoint, computes leftover distance, handles snap correction and reroute thresholds
|
||||||
- `ViewModel.kt` - androidx.ViewModel with LiveData for route, traffic, places, etc.
|
- `RouteCarModel.kt` (`common/car/.../navigation/`) - Extends RouteModel with Android Auto-specific formatting
|
||||||
|
- `NavigationViewModel.kt` - androidx ViewModel exposing route, traffic, places (Nominatim), amenities (Overpass), and fuel prices (Tankerkönig) as LiveData
|
||||||
|
- `SettingsViewModel.kt` - State holder for DataStore-backed settings (dark mode, 3D, routing engine, avoid preferences, etc.)
|
||||||
|
- `BaseStyleModel.kt` - Map style state
|
||||||
|
|
||||||
**Data Models:**
|
**Data models** (`common/data/.../data/`):
|
||||||
- `Route.kt` - Universal route structure used by all providers
|
- `Route.kt` - Universal route wrapper with `Route.Builder` and provider dispatch
|
||||||
- `Place.kt` - ObjectBox entity for favorites/recent locations
|
- `data/route/*` - Decomposed route components (`Routes`, `Leg`, `Step`, `Maneuver`, `Intersection`, `Lane`, `Summary`)
|
||||||
- `StepData.kt` - Display data for current navigation instruction
|
- `NavigationState.kt` - Immutable navigation state (route, flags, location, bearing, maneuver, destination)
|
||||||
|
- `Data.kt` - Shared types (`Place`, `StepData`, `SearchFilter`, `Locations`, `ValhallaLocation`) plus `object Constants` and the `RouteEngine`, `DarkMode`, `EngineType`, `ViewStyle`, `NavigationThemeColor` enums
|
||||||
|
- `ApplicationConfig.kt` - Loads `USER` / `PASSWORD` from `BuildConfig` for HTTP basic auth on protected endpoints
|
||||||
|
|
||||||
**Repositories:**
|
**Persistence** (`common/data/.../data/datastore/` and `common/data/.../repository/`):
|
||||||
- `NavigationRepository.kt` - Abstract base class for all routing providers
|
- `DataStoreManager.kt` - Single source of truth for preference keys; exposes `Flow<T>` reads and `suspend` writes for each setting
|
||||||
- Also handles Nominatim geocoding search and TomTom traffic incidents
|
- `SettingsRepository.kt` - Higher-level wrapper over `DataStoreManager`
|
||||||
|
|
||||||
**Android Auto:**
|
**Repositories** (`common/data/.../data/`):
|
||||||
- `NavigationCarAppService.kt` - Entry point for Android Auto/Automotive OS
|
- `NavigationRepository.kt` - Abstract base class for routing providers; also handles Nominatim geocoding (search and reverse)
|
||||||
- `NavigationSession.kt` - Session management
|
- `osrm/`, `valhalla/`, `tomtom/` - Provider implementations and JSON DTOs
|
||||||
- `NavigationScreen.kt` - Car screen templates with NavigationType state machine
|
- `fuel/FuelPrices.kt` - Tankerkönig fuel-price client (returns `List<Station>`)
|
||||||
- `SurfaceRenderer.kt` - Handles virtual display and map rendering
|
- `overpass/` - Overpass API client for POIs and speed limits
|
||||||
|
|
||||||
|
**Android Auto / Automotive** (`common/car/`):
|
||||||
|
- `NavigationCarAppService.kt` - CarAppService entry point
|
||||||
|
- `CarSession.kt` (abstract) and concrete `NavigationSession.kt`, `ClusterSession.kt`
|
||||||
|
- `screen/NavigationScreen.kt` - Main navigation template; `NavigationType` enum drives template selection (VIEW, NAVIGATION, REROUTE, RECENT, ARRIVAL)
|
||||||
|
- `screen/SearchScreen.kt`, `RoutePreviewScreen.kt`, `PlaceListScreen.kt`, `CategoriesScreen.kt`, `CategoryScreen.kt`, `StopOverScreen.kt`, `RequestPermissionScreen.kt`
|
||||||
|
- `screen/settings/*` - Per-setting screens (RoutingSettings, NavigationSettings, DisplaySettings, DarkModeSettings, DistanceSettings, AudioSettings, CarSettings, PasswordSettings)
|
||||||
|
- `screen/observers/*` - Observer pattern that bridges LiveData to car screens. `NavigationObserverManager` orchestrates `RouteObserver`, `TrafficObserver`, `TrafficMessageObserver`, `PlaceSearchObserver`, `CategoryObserver`, `MaxSpeedObserver`, `SpeedCameraObserver`
|
||||||
|
- Supporting managers: `SurfaceRenderer.kt` (virtual display + map), `DeviceLocationManager.kt`, `CarSensorManager.kt`, `NavigationNotificationManager.kt` + `NavigationNotificationService.kt`, `TextToSpeechManager.kt`, `CustomLifecycleOwner.kt`
|
||||||
|
- `map/MapView.kt`, `map/LocationPuck.kt` - MapLibre rendering for the car surface
|
||||||
|
|
||||||
### External APIs
|
### External APIs
|
||||||
|
|
||||||
| Service | Purpose | Base URL |
|
| Service | Purpose | URL |
|
||||||
|---------|---------|----------|
|
|---------------|--------------------------|------------------------------------------------------------------------------|
|
||||||
| OSRM | Routing | `https://kouros-online.de/osrm/route/v1/driving/` |
|
| OSRM | Routing | `https://router.project-osrm.org/route/v1/driving/` |
|
||||||
| Valhalla | Routing | `https://kouros-online.de/valhalla/route` |
|
| Valhalla | Routing | `https://kouros-online.de/valhalla/route?json=` (HTTP basic auth) |
|
||||||
| TomTom | Traffic incidents | `https://api.tomtom.com/traffic/services/5/incidentDetails` |
|
| TomTom | Routing | `https://api.tomtom.com/routing/1/calculateRoute/` |
|
||||||
| Nominatim | Geocoding search | `https://kouros-online.de/nominatim/` |
|
| TomTom | Traffic incidents | `https://api.tomtom.com/traffic/services/5/incidentDetails` |
|
||||||
| Overpass | POI & speed limits | OpenStreetMap Overpass API |
|
| Nominatim | Geocoding (search/reverse) | `https://nominatim.openstreetmap.org/` |
|
||||||
|
| Overpass | POIs & speed limits | OpenStreetMap Overpass API (DEBUG builds use `https://kouros-online.de/api/interpreter`) |
|
||||||
|
| Tankerkönig | Fuel prices | `https://creativecommons.tankerkoenig.de/json/list.php?` (API key in `fuel/FuelPrices.kt`) |
|
||||||
|
|
||||||
|
In DEBUG builds, `TomTomRepository` and `FuelPrices` can be pointed at a local fixture (see `useLocal` flags) — TomTom can fall back to `R.raw.tomom_routing`, fuel falls back to a local JSON URL.
|
||||||
|
|
||||||
## Important Constants
|
## Important Constants
|
||||||
|
|
||||||
Located in `Constants.kt` (`common/data`):
|
Defined in `object Constants` inside `common/data/.../data/Data.kt`:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
NEXT_STEP_THRESHOLD = 120.0 m // Distance to show next maneuver
|
NEXT_STEP_THRESHOLD = 500.0 // Distance (m) to show next maneuver
|
||||||
DESTINATION_ARRIVAL_DISTANCE = 40.0 m // Distance to trigger arrival
|
DESTINATION_ARRIVAL_DISTANCE = 10.0 // Distance (m) to trigger arrival
|
||||||
MAXIMAL_SNAP_CORRECTION = 50.0 m // Max distance to snap to route
|
MAXIMAL_SNAP_CORRECTION = 50.0 // Max distance (m) to snap to route
|
||||||
MAXIMAL_ROUTE_DEVIATION = 80.0 m // Max deviation before reroute
|
MAXIMAL_ROUTE_DEVIATION = 100.0 // Max deviation (m) before reroute
|
||||||
|
NEAREST_LOCATION_DISTANCE = 10F
|
||||||
|
SPEED_UPDATE_DISTANCE = 600F
|
||||||
|
INSTRUCTION_DISTANCE = 50
|
||||||
|
SPEED_BEARING_DEVIATION = 60
|
||||||
|
TILT = 60.0 // Map tilt in degrees during navigation
|
||||||
|
TANKER_KOENIG_DELAY = 300_000 // ms between fuel price refreshes
|
||||||
```
|
```
|
||||||
|
|
||||||
SharedPreferences keys:
|
DataStore keys (see `DataStoreManager.PreferencesKeys`):
|
||||||
- `ROUTING_ENGINE` - Selected provider (0=Valhalla, 1=OSRM, 2=TomTom)
|
- `RoutingEngine` (Int) — `0=Valhalla`, `1=OSRM`, `2=TomTom` (default 2)
|
||||||
- `DARK_MODE_SETTINGS` - Theme preference
|
- `DarkMode` (Int), `Show3D` (Bool), `CarLocation` (Bool)
|
||||||
- `AVOID_MOTORWAY`, `AVOID_TOLLWAY` - Route preferences
|
- `AvoidMotorway`, `AvoidTollway`, `AvoidFerry` (Bool)
|
||||||
|
- `LastRoute`, `RecentPlaces`, `FuelPrices`, `LastFuelPrices`
|
||||||
|
- `TomTomApiKey`, `DistanceMode`, `GuidanceAudio`, `Traffic`, `TripSuggestion`, `EngineType`, `AlternativeRoutes`
|
||||||
|
|
||||||
## Navigation Flow
|
## Navigation Flow
|
||||||
|
|
||||||
1. **Route Loading**: User searches via Nominatim → selects place → ViewModel.loadRoute() calls selected repository
|
1. **Route loading** — User searches via Nominatim → selects place → `NavigationViewModel.loadRoute()` calls the selected repository
|
||||||
2. **Route Parsing**: Provider JSON → mapper converts to universal Route → RouteModel.startNavigation()
|
2. **Route parsing** — Provider JSON → `*Route.mapToRoute()` populates `Route.Builder` → universal `Route` → `RouteModel.startNavigation()`
|
||||||
3. **Location Tracking**: FusedLocationProviderClient provides updates → RouteModel.updateLocation()
|
3. **Location tracking** — `FusedLocationProviderClient` updates → `RouteModel.updateLocation()`
|
||||||
4. **Step Calculation**: findStep() snaps location to nearest waypoint → updates current step
|
4. **Step calculation** — `RouteCalculator.findStep()` snaps location to the nearest waypoint, returns `StepMatch`, and updates the current step index
|
||||||
5. **UI Updates**: currentStep() and nextStep() provide display data (instruction, distance, icon, lanes)
|
5. **UI updates** — `NavigationState` flows out via LiveData/Compose state; phone and car UIs render the current/next step (instruction, distance, icon, lanes)
|
||||||
6. **Arrival**: When distance < DESTINATION_ARRIVAL_DISTANCE, navigation ends
|
6. **Arrival** — When distance < `DESTINATION_ARRIVAL_DISTANCE`, navigation ends
|
||||||
|
|
||||||
## Testing Navigation
|
## Testing Navigation
|
||||||
|
|
||||||
The app includes mock location support for testing:
|
The phone app supports mock locations for testing:
|
||||||
|
|
||||||
- Set `useMock = true` in MainActivity
|
- Set `useMock = true` in `MainActivity`
|
||||||
- Enable "Mock location app" in Android Developer Options
|
- Enable "Mock location app" in Android Developer Options
|
||||||
- Choose test mode:
|
- Choose mode in `model/Simulation.kt` / `model/MockLocation.kt`:
|
||||||
- `type = 1` - Simulate movement along entire route
|
- `type = 1` — Simulate movement along the entire route
|
||||||
- `type = 2` - Test specific step range
|
- `type = 2` — Test a specific step range
|
||||||
- `type = 3` - Replay GPX track file
|
- `type = 3` — Replay a GPX track file
|
||||||
|
|
||||||
## ObjectBox Database
|
The car module has its own `navigation/Simulation.kt` for car-side simulation.
|
||||||
|
|
||||||
ObjectBox is configured in `common/data/build.gradle.kts` with the kapt plugin. The database stores:
|
### Unit tests
|
||||||
|
|
||||||
- Recent destinations (category: "Recent")
|
- `common/data/src/test/.../model/RouteCalculatorTest.kt`
|
||||||
- Favorite places (category: "Favorites")
|
- `common/data/src/test/.../model/RouteModelTest.kt`
|
||||||
- Imported contacts (category: "Contacts")
|
- `common/data/src/test/.../model/IconMapperTest.kt`
|
||||||
|
- `common/data/src/test/.../model/OverpassTest.kt`
|
||||||
|
- `common/data/src/test/.../utils/GeoUtilsTest.kt`
|
||||||
|
- `common/car/src/test/.../screen/NavigationScreenTest.kt`
|
||||||
|
- `common/car/src/test/.../screen/observers/CategoryObserverTest.kt`, `ObserversTest.kt`
|
||||||
|
|
||||||
Queries use ObjectBox query builder pattern with generated `Place_` property accessors.
|
## Persistence
|
||||||
|
|
||||||
|
All app preferences are stored via Androidx **DataStore Preferences** (`navigation_settings` data store). There is no Room/ObjectBox/SQL database — the only persisted entities are settings and serialized lists (recent places, last route, last fuel prices) kept as strings.
|
||||||
|
|
||||||
|
`DataStoreManager` exposes a `Flow<T>` per setting plus a matching `suspend set*` writer. `SettingsRepository` wraps it for higher-level access. `SettingsViewModel` and `AppViewModel` consume those flows for UI state.
|
||||||
|
|
||||||
## Compose UI Structure
|
## Compose UI Structure
|
||||||
|
|
||||||
**Phone App:**
|
**Phone app** (`app/src/main/java/com/kouros/navigation/`):
|
||||||
- `MainActivity.kt` - Main entry with permission handling and Navigation Compose
|
- `ui/MainActivity.kt` - Entry point with permission handling and Navigation Compose host
|
||||||
- `NavigationScreen.kt` - Turn-by-turn navigation display
|
- `ui/MapView.kt` - MapLibre rendering with camera state management
|
||||||
- `SearchSheet.kt` / `NavigationSheet.kt` - Bottom sheet content
|
- `ui/SheetLayout.kt` - Bottom sheet scaffold
|
||||||
- `MapView.kt` - MapLibre rendering with camera state management
|
- `ui/PermissionScreen.kt`
|
||||||
|
- `ui/navigation/AppNavGraph.kt` - Compose navigation graph
|
||||||
|
- `ui/navigation/NavigationScreen.kt`, `NavigationSheet.kt` - Turn-by-turn UI
|
||||||
|
- `ui/search/SearchScreen.kt`, `SearchSheet.kt`
|
||||||
|
- `ui/settings/SettingsScreen.kt`, `SettingsRoute.kt`, `DisplayScreen.kt`, `NavigationScreen.kt`, `CarScreen.kt`, `Settings.kt`
|
||||||
|
- `ui/components/SettingItem.kt`, `SettingSwitch.kt`, `RadioButtonSingleSelection.kt`, `SectionTitle.kt`
|
||||||
|
- `ui/app/AppViewModel.kt`, `AppViewModelProvider.kt`
|
||||||
|
- `ui/theme/{Color,Type,Shapes,Theme}.kt`
|
||||||
|
- `model/Simulation.kt`, `model/MockLocation.kt` - Mock location for testing
|
||||||
|
- `di/appModule.kt` - Koin module
|
||||||
|
- `MainApplication.kt` - Application class
|
||||||
|
|
||||||
**Android Auto:**
|
**Android Auto** uses CarAppService templates (NavigationTemplate, MessageTemplate, MapWithContentTemplate). `NavigationType` (in `screen/NavigationScreen.kt`) controls which template to render. UI state is synchronized with `NavigationViewModel` through the observers in `screen/observers/`.
|
||||||
- Uses CarAppService Screen templates (NavigationTemplate, MessageTemplate, MapWithContentTemplate)
|
|
||||||
- NavigationType enum controls which template to display (VIEW, NAVIGATION, REROUTE, RECENT, ARRIVAL)
|
|
||||||
|
|
||||||
## Build Flavors
|
## Build Flavors
|
||||||
|
|
||||||
Two product flavors with dimension "version":
|
`app/build.gradle.kts` defines three product flavors under the `store` dimension:
|
||||||
- **demo** - applicationId: `com.kouros.navigation.demo`
|
- **play** - applicationIdSuffix `.play`
|
||||||
- **full** - applicationId: `com.kouros.navigation.full`
|
- **demo** - applicationIdSuffix `.demo`
|
||||||
|
- **full** - applicationIdSuffix `.full`
|
||||||
|
|
||||||
|
Base namespace is `com.kouros.navigation`; current versionName is `0.3.0.109`. Java/Kotlin target is 21 for the app module, 11 for `common:data`. compileSdk/targetSdk = 37, minSdk = 33.
|
||||||
|
|
||||||
|
`signing.properties` (root, gitignored) provides the keystore credentials for both debug and release builds. `local.properties` provides `USER` / `PASSWORD` build config fields consumed by `ApplicationConfig`.
|
||||||
|
|
||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
**Dependency Injection (Koin):**
|
**Dependency injection (Koin):**
|
||||||
```kotlin
|
```kotlin
|
||||||
single { OsrmRepository() }
|
single { OsrmRepository() }
|
||||||
viewModel { ViewModel(get()) }
|
viewModel { NavigationViewModel(get()) }
|
||||||
```
|
```
|
||||||
|
|
||||||
**LiveData Observation:**
|
**LiveData observation:**
|
||||||
```kotlin
|
```kotlin
|
||||||
viewModel.route.observe(this) { routeJson ->
|
viewModel.route.observe(this) { routeJson ->
|
||||||
routeModel.startNavigation(routeJson, context)
|
routeModel.startNavigation(routeJson, context)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Step Finding Algorithm:**
|
**Step-finding algorithm:**
|
||||||
RouteModel iterates through all step waypoints, calculates distance to current location, and snaps to the nearest waypoint to determine current step index.
|
`RouteCalculator` iterates the current step's waypoints, calculates distance from the current location to each, and snaps to the nearest waypoint. Snap is rejected if the closest point exceeds `MAXIMAL_SNAP_CORRECTION`; deviation greater than `MAXIMAL_ROUTE_DEVIATION` triggers a reroute.
|
||||||
|
|
||||||
|
**Settings flow:**
|
||||||
|
```kotlin
|
||||||
|
val darkMode by viewModel.darkMode.collectAsStateWithLifecycle()
|
||||||
|
```
|
||||||
|
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
- Valhalla route mapping is incomplete (search for TODO comments in ValhallaRoute.kt)
|
- Valhalla route mapping is incomplete in places (search for TODO comments in `data/valhalla/ValhallaRoute.kt`)
|
||||||
- Rerouting logic exists but needs more testing
|
- Rerouting logic exists but needs more testing
|
||||||
- Speed limit queries via Overpass API could be optimized for performance
|
- Speed-limit queries via Overpass API could be optimized for performance
|
||||||
- TomTom implementation uses local JSON file (R.raw.tomom_routing) instead of live API
|
- TomTom and Tankerkönig clients have DEBUG-only `useLocal` shortcuts that hit fixture URLs (`http://192.168.1.37/...` and `R.raw.tomom_routing`) — these need to be off for any real-network testing
|
||||||
|
- The `automotive` module is wired into Gradle but has no Kotlin source yet
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ android {
|
|||||||
applicationId = "com.kouros.navigation"
|
applicationId = "com.kouros.navigation"
|
||||||
minSdk = 33
|
minSdk = 33
|
||||||
targetSdk = 37
|
targetSdk = 37
|
||||||
versionCode = 109
|
versionCode = 110
|
||||||
versionName = "0.3.0.109"
|
versionName = "0.3.0.110"
|
||||||
base.archivesName = "navi-$versionName"
|
base.archivesName = "navi-$versionName"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ android {
|
|||||||
|
|
||||||
buildConfigField("String", "USER", "\"${properties.getProperty("USER") ?: ""}\"")
|
buildConfigField("String", "USER", "\"${properties.getProperty("USER") ?: ""}\"")
|
||||||
buildConfigField("String", "PASSWORD", "\"${properties.getProperty("PASSWORD") ?: ""}\"")
|
buildConfigField("String", "PASSWORD", "\"${properties.getProperty("PASSWORD") ?: ""}\"")
|
||||||
|
buildConfigField("String", "TANKER_KOENIG_API_KEY", "\"${properties.getProperty("TANKER_KOENIG_API_KEY") ?: ""}\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ import com.kouros.data.BuildConfig
|
|||||||
|
|
||||||
data class ApplicationConfig(
|
data class ApplicationConfig(
|
||||||
val user: String,
|
val user: String,
|
||||||
val password: String
|
val password: String,
|
||||||
|
val tankerKoenigApiKey: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun load(): ApplicationConfig {
|
fun load(): ApplicationConfig {
|
||||||
return ApplicationConfig(
|
return ApplicationConfig(
|
||||||
user = BuildConfig.USER,
|
user = BuildConfig.USER,
|
||||||
password = BuildConfig.PASSWORD
|
password = BuildConfig.PASSWORD,
|
||||||
|
tankerKoenigApiKey = BuildConfig.TANKER_KOENIG_API_KEY
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,18 +21,19 @@ private val gson = GsonBuilder().serializeNulls().create()
|
|||||||
|
|
||||||
const val sort = "&sort=dist&type=all"
|
const val sort = "&sort=dist&type=all"
|
||||||
|
|
||||||
const val apiKey = "&apikey=fc9900c9-8f02-4b28-990d-dc6067228c59"
|
|
||||||
|
|
||||||
val useLocal = BuildConfig.DEBUG
|
val useLocal = BuildConfig.DEBUG
|
||||||
|
|
||||||
class FuelPrices : NavigationRepository() {
|
class FuelPrices : NavigationRepository() {
|
||||||
|
|
||||||
|
private val config by lazy { com.kouros.navigation.data.ApplicationConfig.load() }
|
||||||
|
|
||||||
fun getFuelPrices(location: Location, radius: Int) : List<Station> {
|
fun getFuelPrices(location: Location, radius: Int) : List<Station> {
|
||||||
|
|
||||||
val url = if (useLocal) {
|
val url = if (useLocal) {
|
||||||
"http://192.168.1.37/fuel.json"
|
"http://192.168.1.37/fuel.json"
|
||||||
} else {
|
} else {
|
||||||
"${tankerKoenigUrl}lat=${location.latitude}&lng=${location.longitude}&rad=${radius}$sort$apiKey"
|
"${tankerKoenigUrl}lat=${location.latitude}&lng=${location.longitude}&rad=${radius}$sort&apikey=${config.tankerKoenigApiKey}"
|
||||||
}
|
}
|
||||||
|
|
||||||
val prices = fetchUrl(
|
val prices = fetchUrl(
|
||||||
@@ -49,7 +50,7 @@ class FuelPrices : NavigationRepository() {
|
|||||||
carOrientation: Float,
|
carOrientation: Float,
|
||||||
searchFilter: SearchFilter
|
searchFilter: SearchFilter
|
||||||
): String {
|
): String {
|
||||||
TODO("Not yet implemented")
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTraffic(
|
override fun getTraffic(
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class Overpass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val searchQuery = """
|
val searchQuery = """
|
||||||
|[out:json];
|
|[out:json][timeout:10];
|
||||||
|(
|
|(
|
||||||
| ${searchClauses.joinToString(";")};
|
| ${searchClauses.joinToString(";")};
|
||||||
|);
|
|);
|
||||||
@@ -75,7 +75,7 @@ class Overpass {
|
|||||||
val searchLocation ="way[\"highway\"~\"^(primary|secondary|tertiary|residential|motorway)$\"][name](around:50, $lineString)";
|
val searchLocation ="way[\"highway\"~\"^(primary|secondary|tertiary|residential|motorway)$\"][name](around:50, $lineString)";
|
||||||
|
|
||||||
val searchQuery = """
|
val searchQuery = """
|
||||||
|[out:json];
|
|[out:json][timeout:10];
|
||||||
|(
|
|(
|
||||||
| ${searchLocation};
|
| ${searchLocation};
|
||||||
|);
|
|);
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ import kotlin.collections.first
|
|||||||
import kotlin.collections.forEach
|
import kotlin.collections.forEach
|
||||||
import kotlin.comparisons.compareBy
|
import kotlin.comparisons.compareBy
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel for navigation-related data operations.
|
* ViewModel for navigation-related data operations.
|
||||||
@@ -58,6 +60,8 @@ import kotlin.math.absoluteValue
|
|||||||
*/
|
*/
|
||||||
class NavigationViewModel(private val repository: NavigationRepository) : ViewModel() {
|
class NavigationViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||||
|
|
||||||
|
private val overpass = Overpass()
|
||||||
|
|
||||||
/** LiveData containing the calculated route JSON string */
|
/** LiveData containing the calculated route JSON string */
|
||||||
val route: MutableLiveData<String> by lazy {
|
val route: MutableLiveData<String> by lazy {
|
||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
@@ -411,7 +415,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
) {
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val repository = getSettingsRepository(carContext)
|
val repository = getSettingsRepository(carContext)
|
||||||
val amenities = Overpass().getAmenities("amenity", category, location, 5.0)
|
val amenities = overpass.getAmenities("amenity", category, location, 5.0)
|
||||||
val fuelPrices = fuelStations(category, lastFuelUpdate, location, repository)
|
val fuelPrices = fuelStations(category, lastFuelUpdate, location, repository)
|
||||||
val distAmenities = mutableListOf<Elements>()
|
val distAmenities = mutableListOf<Elements>()
|
||||||
amenities.forEach {
|
amenities.forEach {
|
||||||
@@ -473,7 +477,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
fun getSpeedCameras(location: Location, radius: Double) {
|
fun getSpeedCameras(location: Location, radius: Double) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius)
|
val amenities = overpass.getAmenities("highway", "speed_camera", location, radius)
|
||||||
val distAmenities = mutableListOf<Elements>()
|
val distAmenities = mutableListOf<Elements>()
|
||||||
amenities.forEach {
|
amenities.forEach {
|
||||||
val plLocation =
|
val plLocation =
|
||||||
@@ -509,43 +513,46 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
*/
|
*/
|
||||||
fun calculateSpeedLimit(location: Location, routeBearing: Float, countryCode: String): Int {
|
fun calculateSpeedLimit(location: Location, routeBearing: Float, countryCode: String): Int {
|
||||||
var speed = 0
|
var speed = 0
|
||||||
var element: Elements?
|
|
||||||
val search = mutableListOf<ElementSearch>()
|
val search = mutableListOf<ElementSearch>()
|
||||||
|
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
speedElements.filter { it.type == "way" }.forEach {
|
// Equirectangular projection at the user's latitude. Closest-point ranking
|
||||||
var distance: Float
|
// doesn't need geodesic accuracy, so we skip Location.distanceTo (Vincenty)
|
||||||
var maxDistance = 1000F
|
// and avoid allocating a Location per geometry vertex.
|
||||||
var geometryFirstLocation = location(0.0, 0.0)
|
val userLat = location.latitude
|
||||||
var geometryLastLocation = location(0.0, 0.0)
|
val userLon = location.longitude
|
||||||
for ((geoIndex, geo) in it.geometry.withIndex()) {
|
val metersPerDegLat = 111_320.0
|
||||||
if (geoIndex == 0) {
|
val metersPerDegLon = metersPerDegLat * cos(Math.toRadians(userLat))
|
||||||
geometryFirstLocation = location(geo.lon, geo.lat)
|
|
||||||
}
|
for (element in speedElements) {
|
||||||
if (geoIndex == it.geometry.size - 1) {
|
if (element.type != "way") continue
|
||||||
geometryLastLocation = location(geo.lon, geo.lat)
|
val geometry = element.geometry
|
||||||
}
|
if (geometry.isEmpty()) continue
|
||||||
val geometryLocation = location(geo.lon, geo.lat)
|
|
||||||
distance = geometryLocation.distanceTo(location)
|
var minDistanceSq = Double.MAX_VALUE
|
||||||
if (distance < maxDistance) {
|
for (geo in geometry) {
|
||||||
maxDistance = distance
|
val dx = (geo.lon - userLon) * metersPerDegLon
|
||||||
}
|
val dy = (geo.lat - userLat) * metersPerDegLat
|
||||||
|
val sq = dx * dx + dy * dy
|
||||||
|
if (sq < minDistanceSq) minDistanceSq = sq
|
||||||
}
|
}
|
||||||
val streetBearing = geometryFirstLocation.bearingPositive(geometryLastLocation)
|
val minDistance = sqrt(minDistanceSq)
|
||||||
if (isBearingValid(it, streetBearing, routeBearing)) {
|
|
||||||
|
val first = geometry.first()
|
||||||
|
val last = geometry.last()
|
||||||
|
val streetBearing = location(first.lon, first.lat)
|
||||||
|
.bearingPositive(location(last.lon, last.lat))
|
||||||
|
|
||||||
|
if (isBearingValid(element, streetBearing, routeBearing)) {
|
||||||
search.add(
|
search.add(
|
||||||
ElementSearch(
|
ElementSearch(element, minDistance, streetBearing.absoluteValue)
|
||||||
it,
|
|
||||||
maxDistance.toDouble(),
|
|
||||||
streetBearing.absoluteValue
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val result =
|
val result =
|
||||||
search.sortedWith(compareBy<ElementSearch> { it.distance }.thenByDescending { it.bearing })
|
search.sortedWith(compareBy<ElementSearch> { it.distance }.thenByDescending { it.bearing })
|
||||||
if (result.isNotEmpty()) {
|
if (result.isNotEmpty()) {
|
||||||
element = result.first().element
|
val element = result.first().element
|
||||||
speed = if (element.tags.maxspeed == "none" && element.tags.highway == "motorway") {
|
speed = if (element.tags.maxspeed == "none" && element.tags.highway == "motorway") {
|
||||||
countryCodeSpeedLimit(countryCode)
|
countryCodeSpeedLimit(countryCode)
|
||||||
} else {
|
} else {
|
||||||
@@ -585,7 +592,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
val lineString = "${location.latitude},${location.longitude}"
|
val lineString = "${location.latitude},${location.longitude}"
|
||||||
val elements =
|
val elements =
|
||||||
Overpass().getSpeedLimit(SPEED_UPDATE_DISTANCE, lineString, street, roadNumbers)
|
overpass.getSpeedLimit(SPEED_UPDATE_DISTANCE, lineString, street, roadNumbers)
|
||||||
speedElements.clear()
|
speedElements.clear()
|
||||||
speedElements.addAll(elements)
|
speedElements.addAll(elements)
|
||||||
}
|
}
|
||||||
@@ -740,7 +747,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
fun loadCurrentLocation(location: Location) {
|
fun loadCurrentLocation(location: Location) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
val elements = Overpass().getStreet(location)
|
val elements = overpass.getStreet(location)
|
||||||
if (elements.isNotEmpty()) {
|
if (elements.isNotEmpty()) {
|
||||||
val points = mutableListOf<Point>()
|
val points = mutableListOf<Point>()
|
||||||
elements.first().geometry.forEach {
|
elements.first().geometry.forEach {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class RouteCalculator(var routeModel: RouteModel) {
|
|||||||
|
|
||||||
var bestMatch: StepMatch? = null
|
var bestMatch: StepMatch? = null
|
||||||
var lastSpeedLocation: Location = location(0.0, 0.0)
|
var lastSpeedLocation: Location = location(0.0, 0.0)
|
||||||
|
var lastLocalSpeedLocation: Location = location(0.0, 0.0)
|
||||||
|
|
||||||
var lastSpeedIndex: Int = 0
|
var lastSpeedIndex: Int = 0
|
||||||
|
|
||||||
@@ -157,13 +158,16 @@ class RouteCalculator(var routeModel: RouteModel) {
|
|||||||
if ((distance > SPEED_UPDATE_DISTANCE * 2) || lastSpeedIndex < routeModel.route.currentStepIndex) {
|
if ((distance > SPEED_UPDATE_DISTANCE * 2) || lastSpeedIndex < routeModel.route.currentStepIndex) {
|
||||||
lastSpeedIndex = routeModel.route.currentStepIndex
|
lastSpeedIndex = routeModel.route.currentStepIndex
|
||||||
lastSpeedLocation = location
|
lastSpeedLocation = location
|
||||||
|
// Force the local re-match on the next GPS fix once new elements arrive.
|
||||||
|
lastLocalSpeedLocation = location(0.0, 0.0)
|
||||||
|
|
||||||
viewModel.updateSpeedLimit(
|
viewModel.updateSpeedLimit(
|
||||||
location,
|
location,
|
||||||
routeModel.route.currentStep().street,
|
routeModel.route.currentStep().street,
|
||||||
routeModel.currentStep().roadNumbers
|
routeModel.currentStep().roadNumbers
|
||||||
)
|
)
|
||||||
} else {
|
} else if (lastLocalSpeedLocation.distanceTo(location) >= NEAREST_LOCATION_DISTANCE) {
|
||||||
|
lastLocalSpeedLocation = location
|
||||||
viewModel.getSpeedLimit(
|
viewModel.getSpeedLimit(
|
||||||
location,
|
location,
|
||||||
routeModel.navState.routeBearing,
|
routeModel.navState.routeBearing,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user