Overpass Performance, ApplicationConfig

This commit is contained in:
Dimitris
2026-05-09 09:33:50 +02:00
parent 29e58f6a24
commit ffa0d47e7b
9 changed files with 256 additions and 140 deletions
+149 -92
View File
@@ -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 | Routing | `https://api.tomtom.com/routing/1/calculateRoute/` |
| TomTom | Traffic incidents | `https://api.tomtom.com/traffic/services/5/incidentDetails` | | TomTom | Traffic incidents | `https://api.tomtom.com/traffic/services/5/incidentDetails` |
| Nominatim | Geocoding search | `https://kouros-online.de/nominatim/` | | Nominatim | Geocoding (search/reverse) | `https://nominatim.openstreetmap.org/` |
| Overpass | POI & speed limits | OpenStreetMap Overpass API | | 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
+2 -2
View File
@@ -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"
} }
+1
View File
@@ -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 (element.type != "way") continue
val geometry = element.geometry
if (geometry.isEmpty()) continue
var minDistanceSq = Double.MAX_VALUE
for (geo in geometry) {
val dx = (geo.lon - userLon) * metersPerDegLon
val dy = (geo.lat - userLat) * metersPerDegLat
val sq = dx * dx + dy * dy
if (sq < minDistanceSq) minDistanceSq = sq
} }
if (geoIndex == it.geometry.size - 1) { val minDistance = sqrt(minDistanceSq)
geometryLastLocation = location(geo.lon, geo.lat)
} val first = geometry.first()
val geometryLocation = location(geo.lon, geo.lat) val last = geometry.last()
distance = geometryLocation.distanceTo(location) val streetBearing = location(first.lon, first.lat)
if (distance < maxDistance) { .bearingPositive(location(last.lon, last.lat))
maxDistance = distance
} if (isBearingValid(element, streetBearing, routeBearing)) {
}
val streetBearing = geometryFirstLocation.bearingPositive(geometryLastLocation)
if (isBearingValid(it, 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