Compare commits

8 Commits
Author SHA1 Message Date
Dimitris b99ebfd36f Service 2026-04-04 13:16:15 +02:00
Dimitris 69b27d3b6c Service 2026-04-04 09:39:54 +02:00
Dimitris 8b886c36b1 Service 2026-04-03 13:23:26 +02:00
Dimitris 2ce079a7c1 Service 2026-04-03 12:30:39 +02:00
Dimitris 8af2d3ad0b Service 2026-04-03 10:00:00 +02:00
Dimitris 1d67b3cc06 Arrival Issue 2026-04-03 09:59:32 +02:00
Dimitris a4227c80d3 NavigationService 2026-04-03 09:57:57 +02:00
Dimitris 757c4c8d8d NavigationService 2026-04-03 09:57:52 +02:00
91 changed files with 2227 additions and 3357 deletions
+93 -150
View File
@@ -1,10 +1,10 @@
# CLAUDE.md # CLAUDE.md
This file provides guidance to Claude Code when working with code in this repository. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 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, Androidx 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, ObjectBox for local persistence, and Koin for dependency injection.
## Build Commands ## Build Commands
@@ -12,15 +12,14 @@ 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 a specific flavor # Build specific flavor
./gradlew :app:assemblePlayDebug
./gradlew :app:assembleDemoDebug ./gradlew :app:assembleDemoDebug
./gradlew :app:assembleFullDebug ./gradlew :app:assembleFullDebug
# Run unit tests # Run tests
./gradlew test ./gradlew test
# Run tests for a specific module # Run tests for specific module
./gradlew :common:data:test ./gradlew :common:data:test
./gradlew :common:car:test ./gradlew :common:car:test
@@ -33,12 +32,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 (see `settings.gradle.kts`): The project uses a multi-module architecture:
- **app/** - Main Android app with Jetpack Compose UI for phone (`com.kouros.navigation`) - **app/** - Main Android app with Jetpack Compose UI for phone
- **common/data/** - Core data layer with routing logic, repositories, view models, persistence (`com.kouros.data`) - **common/data/** - Core data layer with routing logic, repositories, and data models (shared by all modules)
- **common/car/** - Android Auto/Automotive OS UI implementation - **common/car/** - Android Auto/Automotive OS UI implementation
- **automotive/** - Placeholder for future native Automotive OS app (no Kotlin sources yet) - **automotive/** - Placeholder for future native Automotive OS app
Dependencies flow: `app``common:car``common:data` Dependencies flow: `app``common:car``common:data`
@@ -46,212 +45,156 @@ Dependencies flow: `app` → `common:car` → `common:data`
### Routing Providers (Pluggable System) ### Routing Providers (Pluggable System)
The app supports three routing engines that extend the `NavigationRepository` abstract class (`common/data/.../data/NavigationRepository.kt`): The app supports three routing engines that implement the `NavigationRepository` abstract class:
1. **ValhallaRepository** - Valhalla routing engine (ordinal 0) 1. **OsrmRepository** - OSRM routing engine
2. **OsrmRepository** - OSRM routing engine (ordinal 1) 2. **ValhallaRepository** - Valhalla routing engine
3. **TomTomRepository** - TomTom routing engine (ordinal 2, default) 3. **TomTomRepository** - TomTom routing engine
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 class (`OsrmRoute`, `ValhallaRoute`, `TomTomRoute`) that converts provider-specific JSON responses to the universal `Route` data model.
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` under `common/data/src/main/java/com/kouros/navigation/data/<provider>/` 1. Create `NewProviderRepository` extending `NavigationRepository` in `common/data/src/main/java/com/kouros/navigation/data/`
2. Implement `getRoute()` and `getTraffic()` 2. Implement `getRoute()` method
3. Create `NewProviderRoute.kt` with a `mapToRoute(response, builder)` function that populates a `Route.Builder` 3. Create `NewProviderRoute.kt` with `mapToRoute()` function
4. Add a `RouteEngine` enum entry and provider branch in `Route.Builder.route()` (`data/Route.kt`) 4. Add provider detection logic in `Route.Builder.route()`
5. Update `NavigationUtils.getViewModel()` (`utils/NavigationUtils.kt`) to return a `NavigationViewModel` wired to the new repository 5. Update `NavigationUtils.getViewModel()` to return appropriate ViewModel
### Data Flow ### Data Flow
``` ```
User action (search / select destination) User Action (search/select destination)
NavigationViewModel.loadRoute() [LiveData / Flow] ViewModel.loadRoute() [LiveData]
NavigationRepository.getRoute() [selected provider] NavigationRepository.getRoute() [Selected provider]
*Route.mapToRoute() [convert to universal Route] *Route.mapToRoute() [Convert to universal Route model]
RouteModel.startNavigation() RouteModel.startNavigation()
RouteCalculator.findStep() [on each location update] RouteModel.updateLocation() [On each location update]
NavigationState updated → UI observes and renders current step UI observes LiveData and displays current step
``` ```
### Key Classes ### Key Classes
**Navigation logic** (`common/data/.../model/`): **Navigation Logic:**
- `RouteModel.kt` - Core navigation engine; tracks position, manages step progression, owns `NavigationState` - `RouteModel.kt` - Core navigation engine (tracks position, calculates distances, manages steps)
- `RouteCalculator.kt` - Step-finding algorithm: snaps current location to the nearest waypoint, computes leftover distance, handles snap correction and reroute thresholds - `RouteCarModel.kt` - Extends RouteModel with Android Auto-specific formatting
- `RouteCarModel.kt` (`common/car/.../navigation/`) - Extends RouteModel with Android Auto-specific formatting - `ViewModel.kt` - androidx.ViewModel with LiveData for route, traffic, places, etc.
- `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** (`common/data/.../data/`): **Data Models:**
- `Route.kt` - Universal route wrapper with `Route.Builder` and provider dispatch - `Route.kt` - Universal route structure used by all providers
- `data/route/*` - Decomposed route components (`Routes`, `Leg`, `Step`, `Maneuver`, `Intersection`, `Lane`, `Summary`) - `Place.kt` - ObjectBox entity for favorites/recent locations
- `NavigationState.kt` - Immutable navigation state (route, flags, location, bearing, maneuver, destination) - `StepData.kt` - Display data for current navigation instruction
- `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
**Persistence** (`common/data/.../data/datastore/` and `common/data/.../repository/`): **Repositories:**
- `DataStoreManager.kt` - Single source of truth for preference keys; exposes `Flow<T>` reads and `suspend` writes for each setting - `NavigationRepository.kt` - Abstract base class for all routing providers
- `SettingsRepository.kt` - Higher-level wrapper over `DataStoreManager` - Also handles Nominatim geocoding search and TomTom traffic incidents
**Repositories** (`common/data/.../data/`): **Android Auto:**
- `NavigationRepository.kt` - Abstract base class for routing providers; also handles Nominatim geocoding (search and reverse) - `NavigationCarAppService.kt` - Entry point for Android Auto/Automotive OS
- `osrm/`, `valhalla/`, `tomtom/` - Provider implementations and JSON DTOs - `NavigationSession.kt` - Session management
- `fuel/FuelPrices.kt` - Tankerkönig fuel-price client (returns `List<Station>`) - `NavigationScreen.kt` - Car screen templates with NavigationType state machine
- `overpass/` - Overpass API client for POIs and speed limits - `SurfaceRenderer.kt` - Handles virtual display and map rendering
**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 | URL | | Service | Purpose | Base URL |
|---------------|--------------------------|------------------------------------------------------------------------------| |---------|---------|----------|
| OSRM | Routing | `https://router.project-osrm.org/route/v1/driving/` | | OSRM | Routing | `https://kouros-online.de/osrm/route/v1/driving/` |
| Valhalla | Routing | `https://kouros-online.de/valhalla/route?json=` (HTTP basic auth) | | Valhalla | Routing | `https://kouros-online.de/valhalla/route` |
| 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/reverse) | `https://nominatim.openstreetmap.org/` | | Nominatim | Geocoding search | `https://kouros-online.de/nominatim/` |
| Overpass | POIs & speed limits | OpenStreetMap Overpass API (DEBUG builds use `https://kouros-online.de/api/interpreter`) | | Overpass | POI & speed limits | OpenStreetMap Overpass API |
| 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
Defined in `object Constants` inside `common/data/.../data/Data.kt`: Located in `Constants.kt` (`common/data`):
```kotlin ```kotlin
NEXT_STEP_THRESHOLD = 500.0 // Distance (m) to show next maneuver NEXT_STEP_THRESHOLD = 120.0 m // Distance to show next maneuver
DESTINATION_ARRIVAL_DISTANCE = 10.0 // Distance (m) to trigger arrival DESTINATION_ARRIVAL_DISTANCE = 40.0 m // Distance to trigger arrival
MAXIMAL_SNAP_CORRECTION = 50.0 // Max distance (m) to snap to route MAXIMAL_SNAP_CORRECTION = 50.0 m // Max distance to snap to route
MAXIMAL_ROUTE_DEVIATION = 100.0 // Max deviation (m) before reroute MAXIMAL_ROUTE_DEVIATION = 80.0 m // Max deviation 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
``` ```
DataStore keys (see `DataStoreManager.PreferencesKeys`): SharedPreferences keys:
- `RoutingEngine` (Int) — `0=Valhalla`, `1=OSRM`, `2=TomTom` (default 2) - `ROUTING_ENGINE` - Selected provider (0=Valhalla, 1=OSRM, 2=TomTom)
- `DarkMode` (Int), `Show3D` (Bool), `CarLocation` (Bool) - `DARK_MODE_SETTINGS` - Theme preference
- `AvoidMotorway`, `AvoidTollway`, `AvoidFerry` (Bool) - `AVOID_MOTORWAY`, `AVOID_TOLLWAY` - Route preferences
- `LastRoute`, `RecentPlaces`, `FuelPrices`, `LastFuelPrices`
- `TomTomApiKey`, `DistanceMode`, `GuidanceAudio`, `Traffic`, `TripSuggestion`, `EngineType`, `AlternativeRoutes`
## Navigation Flow ## Navigation Flow
1. **Route loading** User searches via Nominatim → selects place → `NavigationViewModel.loadRoute()` calls the selected repository 1. **Route Loading**: User searches via Nominatim → selects place → ViewModel.loadRoute() calls selected repository
2. **Route parsing** Provider JSON → `*Route.mapToRoute()` populates `Route.Builder` universal `Route``RouteModel.startNavigation()` 2. **Route Parsing**: Provider JSON → mapper converts to universal Route → RouteModel.startNavigation()
3. **Location tracking** `FusedLocationProviderClient` updates → `RouteModel.updateLocation()` 3. **Location Tracking**: FusedLocationProviderClient provides updates → RouteModel.updateLocation()
4. **Step calculation** `RouteCalculator.findStep()` snaps location to the nearest waypoint, returns `StepMatch`, and updates the current step index 4. **Step Calculation**: findStep() snaps location to nearest waypoint updates current step
5. **UI updates**`NavigationState` flows out via LiveData/Compose state; phone and car UIs render the current/next step (instruction, distance, icon, lanes) 5. **UI Updates**: currentStep() and nextStep() provide display data (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 phone app supports mock locations for testing: The app includes mock location support 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 mode in `model/Simulation.kt` / `model/MockLocation.kt`: - Choose test mode:
- `type = 1` Simulate movement along the entire route - `type = 1` - Simulate movement along entire route
- `type = 2` Test a specific step range - `type = 2` - Test specific step range
- `type = 3` Replay a GPX track file - `type = 3` - Replay GPX track file
The car module has its own `navigation/Simulation.kt` for car-side simulation. ## ObjectBox Database
### Unit tests ObjectBox is configured in `common/data/build.gradle.kts` with the kapt plugin. The database stores:
- `common/data/src/test/.../model/RouteCalculatorTest.kt` - Recent destinations (category: "Recent")
- `common/data/src/test/.../model/RouteModelTest.kt` - Favorite places (category: "Favorites")
- `common/data/src/test/.../model/IconMapperTest.kt` - Imported contacts (category: "Contacts")
- `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`
## Persistence Queries use ObjectBox query builder pattern with generated `Place_` property accessors.
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** (`app/src/main/java/com/kouros/navigation/`): **Phone App:**
- `ui/MainActivity.kt` - Entry point with permission handling and Navigation Compose host - `MainActivity.kt` - Main entry with permission handling and Navigation Compose
- `ui/MapView.kt` - MapLibre rendering with camera state management - `NavigationScreen.kt` - Turn-by-turn navigation display
- `ui/SheetLayout.kt` - Bottom sheet scaffold - `SearchSheet.kt` / `NavigationSheet.kt` - Bottom sheet content
- `ui/PermissionScreen.kt` - `MapView.kt` - MapLibre rendering with camera state management
- `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** 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/`. **Android Auto:**
- Uses CarAppService Screen templates (NavigationTemplate, MessageTemplate, MapWithContentTemplate)
- NavigationType enum controls which template to display (VIEW, NAVIGATION, REROUTE, RECENT, ARRIVAL)
## Build Flavors ## Build Flavors
`app/build.gradle.kts` defines three product flavors under the `store` dimension: Two product flavors with dimension "version":
- **play** - applicationIdSuffix `.play` - **demo** - applicationId: `com.kouros.navigation.demo`
- **demo** - applicationIdSuffix `.demo` - **full** - applicationId: `com.kouros.navigation.full`
- **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 { NavigationViewModel(get()) } viewModel { ViewModel(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:**
`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. RouteModel iterates through all step waypoints, calculates distance to current location, and snaps to the nearest waypoint to determine current step index.
**Settings flow:**
```kotlin
val darkMode by viewModel.darkMode.collectAsStateWithLifecycle()
```
## Known Limitations ## Known Limitations
- Valhalla route mapping is incomplete in places (search for TODO comments in `data/valhalla/ValhallaRoute.kt`) - Valhalla route mapping is incomplete (search for TODO comments in 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 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 - TomTom implementation uses local JSON file (R.raw.tomom_routing) instead of live API
- The `automotive` module is wired into Gradle but has no Kotlin source yet
+5 -5
View File
@@ -11,14 +11,14 @@ val properties = Properties().apply {
android { android {
namespace = "com.kouros.navigation" namespace = "com.kouros.navigation"
compileSdk = 37 compileSdk = 36
defaultConfig { defaultConfig {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 37 targetSdk = 36
versionCode = 131 versionCode = 91
versionName = "0.4.0.131" versionName = "0.2.3.91"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -42,7 +42,7 @@ android {
release { release {
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false isMinifyEnabled = false
//isShrinkResources = false isShrinkResources = false
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
+6
View File
@@ -45,6 +45,12 @@
android:foregroundServiceType="location" android:foregroundServiceType="location"
android:exported="true"> android:exported="true">
</service> </service>
<service
android:name=".car.navigation.NavigationService"
android:enabled="true"
android:foregroundServiceType="location"
android:exported="true">
</service>
</application> </application>
</manifest> </manifest>
@@ -31,7 +31,7 @@ fun test(applicationContext: Context, routeModel: RouteModel) {
for ((index, step) in routeModel.curLeg.steps.withIndex()) { for ((index, step) in routeModel.curLeg.steps.withIndex()) {
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) { for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
routeModel.updateLocation( routeModel.updateLocation(
waypoint, navigationViewModel location(waypoint[0], waypoint[1]), navigationViewModel
) )
val step = routeModel.currentStep() val step = routeModel.currentStep()
val nextStep = routeModel.nextStep() val nextStep = routeModel.nextStep()
@@ -1,8 +1,13 @@
package com.kouros.navigation.ui package com.kouros.navigation.ui
import android.Manifest import android.Manifest
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.location.LocationManager import android.location.LocationManager
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
@@ -40,6 +45,7 @@ import com.kouros.navigation.MainApplication.Companion.navigationViewModel
import com.kouros.navigation.car.TextToSpeechManager import com.kouros.navigation.car.TextToSpeechManager
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TILT import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.BaseStyleModel import com.kouros.navigation.model.BaseStyleModel
@@ -53,7 +59,7 @@ import com.kouros.navigation.ui.navigation.NavigationSheet
import com.kouros.navigation.ui.search.SearchSheet import com.kouros.navigation.ui.search.SearchSheet
import com.kouros.navigation.ui.theme.NavigationTheme import com.kouros.navigation.ui.theme.NavigationTheme
import com.kouros.navigation.utils.GeoUtils.snapLocation import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.bearingPositive import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
@@ -272,7 +278,7 @@ class MainActivity : ComponentActivity() {
val bearing = if (currentLocation.hasBearing()) { val bearing = if (currentLocation.hasBearing()) {
currentLocation.bearing.toDouble() currentLocation.bearing.toDouble()
} else { } else {
bearingPositive(lastLocation, currentLocation, cameraPosition.value!!.bearing) bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
} }
with(routeModel) { with(routeModel) {
@@ -92,7 +92,7 @@ fun MapView(
duration = 1.seconds duration = 1.seconds
) )
} }
NavigationImage(paddingValues, width, height / 6, "", dark, tilt) NavigationImage(paddingValues, width, height / 6, "", dark)
} }
} }
} }
@@ -243,7 +243,7 @@ private fun SearchPlaces(
latitude = place.lat.toDouble(), latitude = place.lat.toDouble(),
postalCode = place.address.postcode, postalCode = place.address.postcode,
city = place.address.city, city = place.address.city,
street = place.address.road, street = place.address.road
) )
viewModel.saveRecent(context, pl) viewModel.saveRecent(context, pl)
val toLocation = val toLocation =
+1 -1
View File
@@ -7,7 +7,7 @@ plugins {
android { android {
namespace = "com.kouros.navigation" namespace = "com.kouros.navigation"
compileSdk { compileSdk {
version = release(37) version = release(36)
} }
defaultConfig { defaultConfig {
+1 -1
View File
@@ -6,7 +6,7 @@ plugins {
android { android {
namespace = "com.kouros.android.cars.carappservice" namespace = "com.kouros.android.cars.carappservice"
compileSdk = 37 compileSdk = 36
defaultConfig { defaultConfig {
minSdk = 33 minSdk = 33
@@ -1,108 +0,0 @@
package com.kouros.navigation.car
import android.location.Location
import android.location.LocationManager
import com.kouros.navigation.data.overpass.Overpass
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Test
class OverpassTest {
@Test
fun `maxSpeed Schmalkaldener 30 `() {
val curLocation = location(11.582495, 48.186863)
executeSpeedTest(curLocation, "Schmalkaldener Straße", emptyList(), 90, 30)
}
@Test
fun `maxSpeed Ingolstädter 50 `() {
val curLocation = location(11.584384, 48.186338)
executeSpeedTest(curLocation, "Ingolstädter Straße", listOf("B13"), 180, 50)
}
@Test
fun `maxSpeed Isarring `() {
var curLocation = location(11.5999989, 48.1732329)
executeSpeedTest(curLocation, "Isarring", listOf("B2R"), 200, 60)
curLocation = location(11.5995437, 48.1703931)
executeSpeedTest(curLocation, "Isarring", listOf("B2R"), 204, 50)
val locations = listOf(
location( 11.5964253, 48.1658679),
location( 11.5960449, 48.1650396),
location( 11.5959427, 48.1645584),
location( 11.5959714, 48.1641186),
)
locations.forEach {
executeSpeedTest(it, "Biedersteiner", listOf("B2R"), 190, 60)
}
}
@Test
fun `maxSpeed A94 `() {
val curLocation = location(11.88117, 48.16595)
executeSpeedTest(curLocation, "", listOf("A94", "E552"), 90, 130)
}
@Test
fun `maxSpeed Fendsbach `() {
val curLocation = location(11.94989, 48.21522)
executeSpeedTest(curLocation, "Fendsbach", listOf("St 2331"), 0, 60)
}
@Test
fun `maxSpeed Leopoldstraße `() {
val locations = listOf(
location(11.5854771, 48.1778470),
location(11.5855582, 48.1756081),
location(11.5854672, 48.1753093),
location(11.5850147, 48.1774400)
)
executeSpeedTest(locations[0], "Leopoldstraße", emptyList(), 0, 50)
executeSpeedTest(locations[1], "Leopoldstraße", emptyList(), 180, 30)
executeSpeedTest(locations[2], "Leopoldstraße", emptyList(), 180, 30)
executeSpeedTest(locations[3], "Leopoldstraße", emptyList(), 180, 50)
}
@Test
fun `maxSpeed Egnatia `() {
val locations = listOf(
location(20.645487, 39.552875),
location(20.686672, 39.838547),
)
executeSpeedTest(locations[0], "", listOf("E90", "E92"), 100, 120)
executeSpeedTest(locations[1], "", listOf("E853"), 320, 90)
}
fun executeSpeedTest(
curLocation: Location,
street: String,
roadNumbers: List<String>,
routeBearing: Int,
result: Int
) {
val viewModel = NavigationViewModel(TomTomRepository())
val lineString = "${curLocation.latitude},${curLocation.longitude}"
val elements = Overpass().getSpeedLimit(600F, lineString, street, roadNumbers)
viewModel.speedElements.addAll(elements)
assertNotEquals(0, viewModel.speedElements.size)
val speed = viewModel.calculateSpeedLimit(
curLocation,
routeBearing.toFloat(),
"DEU",
)
assertEquals(result, speed)
}
}
@@ -2,24 +2,25 @@ package com.kouros.navigation.car
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kouros.navigation.data.Constants.homeHohenwaldeck import com.kouros.navigation.data.Constants.homeHohenwaldeck
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.route.ManeuverType import com.kouros.navigation.data.route.ManeuverType
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.Assert.*
import org.junit.Before
import kotlin.collections.forEach
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
* *
@@ -31,386 +32,44 @@ class RouteModelTest {
val routeModel = RouteModel() val routeModel = RouteModel()
val location = Location(LocationManager.GPS_PROVIDER) val location = Location(LocationManager.GPS_PROVIDER)
val leftDistance = listOf(
64.0,
0.0,
143.7469482421875,
111.36382293701172,
101.35636901855469,
0.0,
356.0272216796875,
250.2962646484375,
245.09136962890625,
213.86209106445312,
173.69473266601562,
90.41661834716797,
40.58605194091797,
22.74074363708496,
12.33098030090332,
0.0,
1046.3043212890625,
1026.2342529296875,
1012.8703002929688,
979.4789428710938,
972.7660522460938,
971.6541137695312,
914.8672485351562,
873.6181030273438,
831.3056030273438,
781.1799926757812,
719.982666015625,
653.1173095703125,
578.4839477539062,
566.2300415039062,
490.4866943359375,
482.66766357421875,
451.45355224609375,
443.6345520019531,
391.28887939453125,
346.71197509765625,
316.65283203125,
237.5792236328125,
141.71861267089844,
78.22917175292969,
55.94073486328125,
41.31438064575195,
31.061260223388672,
20.943891525268555,
16.43439483642578,
0.0,
346.5630798339844,
339.0434875488281,
329.3117370605469,
321.055908203125,
304.0730285644531,
285.6786193847656,
272.10870361328125,
233.37289428710938,
213.9094696044922,
198.87030029296875,
164.74563598632812,
136.4317626953125,
91.64434051513672,
82.11692810058594,
60.832786560058594,
33.411705017089844,
0.0,
338.5975341796875,
318.1346435546875,
211.02232360839844,
201.35411071777344,
187.75144958496094,
179.0858154296875,
170.42019653320312,
123.4335708618164,
90.03872680664062,
74.55345916748047,
60.95069122314453,
39.9212532043457,
0.0,
314.19586181640625,
306.1593017578125,
289.4617004394531,
223.2646484375,
129.7374725341797,
76.47386932373047,
56.635841369628906,
52.30287170410156,
33.61341857910156,
20.613277435302734,
0.0,
5621.87744140625,
5612.591796875,
5587.8486328125,
5483.76806640625,
5447.275390625,
5422.7744140625,
5398.21875,
5373.927734375,
5358.28955078125,
5347.1455078125,
5339.36181640625,
5314.6181640625,
5303.107421875,
5278.109375,
5253.859375,
5244.88720703125,
5200.0400390625,
5151.9326171875,
5138.63916015625,
5085.87548828125,
5054.32080078125,
5012.51220703125,
4962.390625,
4938.140625,
4898.97607421875,
4862.1416015625,
4788.78564453125,
4767.880859375,
4761.89013671875,
4756.5390625,
4749.23388671875,
4724.98388671875,
4645.017578125,
4596.826171875,
4573.98583984375,
4554.85009765625,
4521.48388671875,
4499.232421875,
4484.6064453125,
4471.07763671875,
4457.54931640625,
4415.10986328125,
4389.8359375,
4359.6376953125,
4347.59716796875,
4342.62109375,
4334.59375,
4320.33447265625,
4303.408203125,
4289.998046875,
4261.86962890625,
4215.78515625,
4207.08251953125,
4192.81201171875,
4080.384521484375,
4065.3955078125,
4009.8798828125,
3975.99853515625,
3971.5283203125,
3953.9345703125,
3915.152587890625,
3877.74365234375,
3865.702880859375,
3857.67578125,
3850.37060546875,
3845.019287109375,
3843.161865234375,
3819.26904296875,
3803.01171875,
3686.892578125,
3661.30908203125,
3629.2001953125,
3613.607177734375,
3591.255859375,
3580.988525390625,
3573.37060546875,
3563.377685546875,
3552.08349609375,
3537.639404296875,
3502.25146484375,
3484.319580078125,
3445.313720703125,
3437.883544921875,
3431.69873046875,
3413.72021484375,
3405.05224609375,
3340.725830078125,
3312.782470703125,
3302.888671875,
3292.994873046875,
3240.986328125,
3229.830322265625,
3185.277099609375,
3096.74755859375,
2981.00146484375,
2907.40869140625,
2865.943115234375,
2859.75830078125,
2851.719482421875,
2813.998779296875,
2778.027587890625,
2767.468505859375,
2729.052001953125,
2694.80712890625,
2651.47265625,
2613.131591796875,
2602.36376953125,
2588.665771484375,
2549.015380859375,
2530.110107421875,
2521.1689453125,
2510.401123046875,
2478.26953125,
2437.71337890625,
2400.48681640625,
2350.667236328125,
2320.4267578125,
2294.857666015625,
2268.75830078125,
2260.833740234375,
2249.615234375,
2229.476318359375,
2210.573486328125,
2192.78271484375,
2184.963623046875,
2148.23974609375,
2114.8818359375,
2105.955078125,
2052.49951171875,
2013.5179443359375,
2006.804931640625,
1959.955810546875,
1869.44775390625,
1787.787109375,
1740.873046875,
1719.6939697265625,
1554.079833984375,
1395.2606201171875,
1349.453369140625,
1296.93359375,
1144.57080078125,
1030.08203125,
972.0895385742188,
926.4032592773438,
921.95556640625,
911.9205322265625,
902.9940185546875,
887.3560180664062,
803.21728515625,
774.1542358398438,
767.318603515625,
762.62841796875,
754.703857421875,
751.3680419921875,
738.9346923828125,
725.406005859375,
710.6475219726562,
668.3836669921875,
615.11279296875,
605.7323608398438,
572.6571044921875,
552.6024780273438,
545.567138671875,
474.7272644042969,
459.36187744140625,
445.29119873046875,
427.3172607421875,
406.21124267578125,
378.6969909667969,
371.8613586425781,
362.934814453125,
347.29681396484375,
329.3663330078125,
320.4398193359375,
301.3039855957031,
237.61085510253906,
112.25553131103516,
94.07954406738281,
75.73666381835938,
0.0,
1023.4342651367188,
1003.37939453125,
973.33203125,
901.591796875,
805.4495849609375,
776.5214233398438,
723.6364135742188,
713.1954956054688,
693.5875854492188,
646.0399169921875,
565.5601196289062,
509.7200622558594,
484.1024169921875,
422.78350830078125,
276.693603515625,
265.35223388671875,
187.42837524414062,
160.74212646484375,
97.36228942871094,
29.534751892089844,
18.390586853027344,
0.0,
462.20318603515625,
450.08526611328125,
439.66217041015625,
397.6007080078125,
346.669921875,
290.0765380859375,
270.7193603515625,
208.91537475585938,
191.0126495361328,
177.56549072265625,
165.6016082763672,
117.09382629394531,
108.90425109863281,
75.23577117919922,
37.74562072753906,
0.0,
940.9969482421875,
921.5716552734375,
891.173583984375,
876.2035522460938,
865.4982299804688,
851.6742553710938,
808.6068725585938,
793.0664672851562,
757.3865356445312,
716.390380859375,
701.561767578125,
696.2328491210938,
657.3816528320312,
641.1264038085938,
584.9859619140625,
581.8070678710938,
552.8590087890625,
473.6039733886719,
401.5708923339844,
369.44659423828125,
360.9601745605469,
353.1898498535156,
309.3813781738281,
269.4887390136719,
226.76416015625,
141.3342742919922,
135.6678466796875,
126.8246078491211,
94.33628845214844,
68.04391479492188,
0.0,
149.6364288330078,
118.28071594238281,
0.0,
)
val distance = listOf( val distance = listOf(
1046.5, 1025.5,
1026.5, 989.8,
1012.0, 963.5,
979.4, 923.7,
972.7, 915.8,
971.6, 914.6,
914.8, 871.0,
873.6, 822.7,
831.3, 769.7,
781.1, 713.8,
719.9, 644.8,
653.1, 577.6,
578.4, 501.7,
566.2, 489.7,
490.4, 452.5,
482.6, 437.4,
451.4, 398.0,
443.6, 390.1,
391.2, 341.3,
346.7, 266.6,
316.6, 219.5,
237.5, 140.7,
141.7, 77.4,
78.22, 55.1,
55.94, 40.0,
41.31, 30.0,
31.0, 19.0,
20.9, 4.0
) )
@Before @Before
fun setup() { fun setup() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val repository = getSettingsRepository(appContext) val repository = getSettingsRepository(appContext)
runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) } runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) }
val routeJsonString = TomTomRepository().fetchUrl( val routeJsonString = TomTomRepository().fetchUrl(
"http://192.168.1.37/tomtom_routing.json", "https://kouros-online.de/tomtom_routing.json",
false false
) )
assertNotEquals("", routeJsonString) assertNotEquals("", routeJsonString)
@@ -433,36 +92,25 @@ class RouteModelTest {
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(stepData.instruction, "Silcherstraße") assertEquals(stepData.instruction, "Silcherstraße")
assertEquals(stepData.leftStepDistance, 46.0, 5.0) assertEquals(stepData.leftStepDistance, 20.0, 5.0)
val nextStepData = routeModel.nextStep() val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value) assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(nextStepData.instruction, "Schmalkaldener Straße") assertEquals(nextStepData.instruction, "Schmalkaldener Straße")
} }
@Test
fun checkArrival() {
location.latitude = 48.116829
location.longitude = 11.594309
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_DESTINATION_LEFT.value)
assertEquals(stepData.instruction, "Hohenwaldeckstraße")
assertEquals(stepData.leftDistance, 0.0, 5.0)
}
@Test @Test
fun checkSchmalkadener20() { fun checkSchmalkadener20() {
location.latitude = 48.186943 location.latitude = 48.187057
location.longitude = 11.579195 location.longitude = 11.576652
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(stepData.instruction, "Ingolstädter Straße") assertEquals(stepData.instruction, "Schmalkaldener Straße")
assertEquals(stepData.leftStepDistance, 326.0, 1.0) assertEquals(stepData.leftStepDistance, 0.0, 1.0)
val nextStepData = routeModel.nextStep() val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value) assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(nextStepData.instruction, "Schenkendorfstraße") assertEquals(nextStepData.instruction, "Ingolstädter Straße")
} }
@Test @Test
@@ -480,7 +128,7 @@ class RouteModelTest {
} else { } else {
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
} }
assertEquals(stepData.leftStepDistance, 327.0, 1.0) assertEquals(stepData.leftStepDistance, 301.0, 1.0)
} }
@Test @Test
@@ -492,7 +140,7 @@ class RouteModelTest {
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
assertEquals(stepData.instruction, "Schenkendorfstraße") assertEquals(stepData.instruction, "Schenkendorfstraße")
assertEquals(stepData.leftStepDistance, 212.0, 10.0) assertEquals(stepData.leftStepDistance, 170.0, 10.0)
assertEquals(stepData.lane.size, 4) assertEquals(stepData.lane.size, 4)
assertEquals(stepData.lane.first().valid, true) assertEquals(stepData.lane.first().valid, true)
assertEquals(stepData.lane.last().valid, false) assertEquals(stepData.lane.last().valid, false)
@@ -557,24 +205,29 @@ class RouteModelTest {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
val curLocation = location(waypoint[0], waypoint[1]) val curLocation = location(waypoint[0], waypoint[1])
if (index in 0..routeModel.curRoute.waypoints.size) { if (index in 0..routeModel.curRoute.waypoints.size) {
//runBlocking { delay(1000) }
val start = System.currentTimeMillis()
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.leftStepDistance, leftDistance[index], 1.0) //println("${stepData.instruction} ${System.currentTimeMillis() - start}")
if (stepData.lane.isNotEmpty()) {
// println(stepData.street)
stepData.lane.forEach {
// println("${it.indications} ${it.valid}")
}
}
// val nextData = routeModel.nextStep()
} }
} }
} }
} }
@Test @Test
fun `leftStepDistance Ingolstädter `() { fun `leftStepDistance Inglolstädter `() {
var location = location(11.584352, 48.186771) val location: Location = location(11.584578, 48.183653)
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
var step = routeModel.currentStep() val step = routeModel.currentStep()
assertEquals(step.leftStepDistance, 1039.0, 1.0) assertEquals(step.leftStepDistance, 645.0, 1.0)
location = location(11.584578, 48.183653)
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
step = routeModel.currentStep()
assertEquals(step.leftStepDistance, 705.0, 1.0)
} }
@Test @Test
@@ -582,66 +235,21 @@ class RouteModelTest {
val location: Location = location(11.578911, 48.185565) val location: Location = location(11.578911, 48.185565)
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val step = routeModel.currentStep() val step = routeModel.currentStep()
assertEquals(step.leftStepDistance, 37.0, 1.0) assertEquals(step.leftStepDistance, 26.0, 1.0)
} }
@Test @Test
fun leftStepDistance() { fun leftStepDistance() {
val start = System.currentTimeMillis()
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) { for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
val curLocation = location(waypoint[0], waypoint[1]) val curLocation = location(waypoint[0], waypoint[1])
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
if (index in 16..43) { if (index in 16..43) {
routeModel.updateLocation( routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
curLocation,
NavigationViewModel(TomTomRepository())
)
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.leftStepDistance, distance[index-16], 1.0) assertEquals(stepData.leftStepDistance, distance[index-16], 1.0)
} }
} }
} }
val end = System.currentTimeMillis() - start
println("Time $end")
}
@Test
fun `check leftStepDistance Vogelhart Gpx `() {
routeModel.navState = routeModel.navState.copy(routeBearing = 270F)
var location = location(11.57927955, 48.18554854)
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
var stepData = routeModel.currentStep()
assertEquals(stepData.leftStepDistance, 62.0, 1.0)
location = location(11.57919950, 48.18556317)
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
stepData = routeModel.currentStep()
assertEquals(stepData.leftStepDistance, 58.0, 1.0)
location = location(11.57906832, 48.18556405)
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
stepData = routeModel.currentStep()
assertEquals(stepData.leftStepDistance, 48.0, 1.0)
routeModel.navState = routeModel.navState.copy(routeBearing = 270F)
location = location(11.57875945, 48.18558161)
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
//assertEquals(routeModel.currentStep().leftStepDistance, 0.0, 1.0)
}
@Test
fun `check deviation `() {
val navigationViewModel = NavigationViewModel(TomTomRepository())
// Schmalkaldener Straße
val firstLocation = location(11.579903, 48.186906)
routeModel.updateLocation(firstLocation, navigationViewModel)
// Frankfurter Ring
val location = location(11.579794, 48.187700)
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = snappedLocation.distanceTo(firstLocation)
assertEquals(distance.toDouble(), 12.0, 1.0)
assertEquals(snappedLocation.latitude, 48.18691500607897, 0.0)
} }
} }
@@ -132,12 +132,12 @@ class CarSensorManager(
val carSensors = carHardwareManager.carSensors val carSensors = carHardwareManager.carSensors
carSensors.addCompassListener( carSensors.addCompassListener(
CarSensors.UPDATE_RATE_FASTEST, CarSensors.UPDATE_RATE_NORMAL,
carContext.mainExecutor, carContext.mainExecutor,
carCompassListener carCompassListener
) )
carSensors.addCarHardwareLocationListener( carSensors.addCarHardwareLocationListener(
CarSensors.UPDATE_RATE_FASTEST, CarSensors.UPDATE_RATE_UI,
carContext.mainExecutor, carContext.mainExecutor,
carLocationListener carLocationListener
) )
@@ -134,6 +134,7 @@ internal class ClusterSession : CarSession(), NavigationListener {
fun updateLocation(location: Location) { fun updateLocation(location: Location) {
Log.d(TAG, "updateLocation $location")
surfaceRenderer.updateLocation(location, "") surfaceRenderer.updateLocation(location, "")
} }
@@ -3,7 +3,6 @@ package com.kouros.navigation.car
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.location.LocationListener
import android.location.LocationManager import android.location.LocationManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.core.location.LocationListenerCompat import androidx.core.location.LocationListenerCompat
@@ -11,12 +10,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.kouros.data.BuildConfig
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.Constants.widenmayer
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -65,26 +59,6 @@ class DeviceLocationManager(
} }
} }
/**
* Provides a flow of location updates from device GPS.
*
* deviceLocationManager.locationFlow().asLiveData().observe(this, ::updateLocation2)
*
* @return Flow emitting location updates
*/
@SuppressLint("MissingPermission")
fun locationFlow(): Flow<Location> = callbackFlow {
val listener = LocationListener { location ->
if (shouldUseDeviceLocation && isListening) {
trySend(location) // Emit the location update to the flow
}
}
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000L, 5f, listener)
awaitClose {
locationManager.removeUpdates(listener) // Unregister listener on cancellation
}
}
/** /**
* Starts requesting location updates from device GPS. * Starts requesting location updates from device GPS.
* Provides initial location via callback and then starts continuous updates. * Provides initial location via callback and then starts continuous updates.
@@ -95,16 +69,14 @@ class DeviceLocationManager(
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5f) { fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5f) {
if (isListening) return if (isListening) return
if (BuildConfig.DEBUG) {
onInitialLocation(homeVogelhart)
onLocationUpdate(homeVogelhart)
} else {
// Get and deliver last known location first // Get and deliver last known location first
val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (lastLocation != null) { if (lastLocation != null) {
onInitialLocation(lastLocation) onInitialLocation(lastLocation)
onLocationUpdate(lastLocation) onLocationUpdate(lastLocation)
} }
// Start continuous location updates // Start continuous location updates
locationManager.requestLocationUpdates( locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, LocationManager.GPS_PROVIDER,
@@ -112,7 +84,6 @@ class DeviceLocationManager(
minDistanceM, minDistanceM,
locationListener locationListener
) )
}
isListening = true isListening = true
} }
@@ -0,0 +1,97 @@
package com.kouros.navigation.car
import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import android.location.LocationManager
import androidx.car.app.CarContext
import androidx.core.location.LocationListenerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kouros.navigation.car.navigation.NavigationService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/**
* Manages device GPS location updates for navigation.
* Coordinates with car hardware sensors to avoid duplicate location sources.
*
* @param carContext The car context for accessing system services
* @param serviceOwner Owner of the lifecycle for coroutine management
* @param shouldUseCarLocationFlow Flow indicating whether car location hardware should be used
* @param onLocationUpdate Callback invoked when location updates are received
* @param onInitialLocation Callback invoked with the last known location when starting
*/
class DeviceLocationManagerService(
private val carContext: CarContext,
private val onLocationUpdate: (Location) -> Unit,
private val onInitialLocation: (Location) -> Unit
) {
private val locationManager: LocationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private var shouldUseDeviceLocation = true
private var isListening = false
/**
* Location listener that receives GPS updates from the device.
* Only processes location if car location hardware is not being used.
*/
private val locationListener: LocationListenerCompat = LocationListenerCompat { location ->
if (shouldUseDeviceLocation) {
onLocationUpdate(location)
}
}
init {
}
/**
* Starts requesting location updates from device GPS.
* Provides initial location via callback and then starts continuous updates.
*
* @param minTimeMs Minimum time interval between updates in milliseconds (default: 500ms)
* @param minDistanceM Minimum distance between updates in meters (default: 5m)
*/
@SuppressLint("MissingPermission")
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5.0f) {
if (isListening) return
// Get and deliver last known location first
val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (lastLocation != null) {
onInitialLocation(lastLocation)
onLocationUpdate(lastLocation)
}
// Start continuous location updates
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
minTimeMs,
minDistanceM,
locationListener
)
isListening = true
}
/**
* Stops receiving location updates from device GPS.
* Should be called when the session is destroyed to prevent memory leaks.
*/
fun stopLocationUpdates() {
if (!isListening) return
locationManager.removeUpdates(locationListener)
isListening = false
}
/**
* Checks if location updates are currently active.
*/
fun isListeningForUpdates(): Boolean = isListening
}
@@ -35,7 +35,8 @@ class NavigationCarAppService : CarAppService() {
return ClusterSession() return ClusterSession()
} else { } else {
createNotificationChannel() createNotificationChannel()
return NavigationSession() //return NavigationSession()
return NavigationServiceSession()
} }
} }
@@ -111,7 +111,7 @@ class NavigationNotificationService : Service() {
* Initializes the notifications, if needed. * Initializes the notifications, if needed.
* *
* *
* [NavigationNotificationManager.IMPORTANCE_HIGH] is needed to show the alerts on top of the car * [NotificationManager.IMPORTANCE_HIGH] is needed to show the alerts on top of the car
* screen. However, the rail widget at the bottom of the screen will show regardless of the * screen. However, the rail widget at the bottom of the screen will show regardless of the
* importance setting. * importance setting.
*/ */
@@ -0,0 +1,724 @@
package com.kouros.navigation.car
import android.Manifest.permission
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.location.Location
import android.os.IBinder
import android.util.Log
import androidx.annotation.RequiresPermission
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.ScreenManager
import androidx.car.app.connection.CarConnection
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import com.kouros.navigation.car.navigation.NavigationService
import com.kouros.navigation.car.screen.NavigationListener
import com.kouros.navigation.car.screen.NavigationScreen
import com.kouros.navigation.car.screen.NavigationType
import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.car.screen.checkPermission
import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
import com.kouros.navigation.car.screen.observers.NavigationObserverManager
import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.GMS_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
import com.kouros.navigation.data.Constants.homeHohenwaldeck
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.absoluteValue
/**
* Main session for Android Auto/Automotive OS navigation.
* Manages the lifecycle of the navigation session, including location updates,
* car hardware sensors, routing engine selection, and screen navigation.
* Implements NavigationScreen.Listener for handling navigation events.
*/
class NavigationServiceSession : CarSession(), NavigationListener, NavigationObserverCallback {
// Flag to enable/disable contact access feature
val useContacts = false
var route = ""
// Main navigation screen displayed to the user
lateinit var navigationScreen: NavigationScreen
// Handles map surface rendering on the car display
lateinit var surfaceRenderer: SurfaceRenderer
// Manages car hardware sensors (location, compass, speed)
lateinit var carSensorManager: CarSensorManager
var initialLocation = true;
lateinit var textToSpeechManager: TextToSpeechManager
lateinit var notificationManager: NotificationManager
private var routingEngine = 0
private var showTraffic = false;
private var distanceMode = 0
var lastCameraSearch = 0
var speedCameras = listOf<Elements>()
var recentPlaces = mutableListOf<Place>()
var lastRouteDate: LocalDateTime = LocalDateTime.now()
var destination = Place()
var notificationActive = false
var navigationService: NavigationService? = null
val serviceListener: NavigationService.Listener = object : NavigationService.Listener {
override fun navigationStateChanged(
isNavigating: Boolean,
isRerouting: Boolean,
hasArrived: Boolean,
destinations: MutableList<Destination>,
steps: MutableList<Step>,
destinationTravelEstimate: TravelEstimate,
stepTravelEstimate: TravelEstimate,
stepRemainingDistance: Distance,
shouldShowNextStep: Boolean,
shouldShowLanes: Boolean,
junctionImage: CarIcon?,
backGroundColor: CarColor
) {
navigationScreen.updateTrip(
isNavigating = isNavigating,
isRerouting = isRerouting,
hasArrived = hasArrived,
destinationTravelEstimate = destinationTravelEstimate,
stepTravelEstimate = stepTravelEstimate,
destinations = destinations,
steps = steps,
stepRemainingDistance = stepRemainingDistance,
shouldShowNextStep = shouldShowNextStep,
shouldShowLanes = shouldShowLanes,
junctionImage = junctionImage,
backGroundColor = backGroundColor
)
}
override fun updateServiceLocation(location: Location) {
if (initialLocation) {
navigationViewModel.loadRecentPlaces(
carContext,
location,
surfaceRenderer.carOrientation,
)
initialLocation = false
}
updateLocation(location)
}
}
// Monitors the state of the connection to the Navigation service.
val serviceConnection: ServiceConnection = object : ServiceConnection {
@RequiresPermission(allOf = [permission.ACCESS_FINE_LOCATION, permission.ACCESS_COARSE_LOCATION])
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.d("NavigationService", "In onServiceConnected() Session component:$service")
val binder: NavigationService.LocalBinder = service as NavigationService.LocalBinder
navigationService = binder.service
navigationService!!.setCarContext(carContext, serviceListener)
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.d("NavigationService", "In onServiceDisconnected() Session component: $name")
// Unhook map models here
navigationService!!.clearCarContext()
navigationService = null
}
}
/**
* Lifecycle observer for managing session lifecycle events.
* Cleans up resources when the session is destroyed.
*/
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
Log.i(TAG, "In onStart() Session")
carContext
.bindService(
Intent(carContext, NavigationService::class.java),
serviceConnection,
Context.BIND_AUTO_CREATE
)
}
override fun onPause(owner: LifecycleOwner) {
Log.d(TAG, "NavigationSession paused")
super.onPause(owner)
}
override fun onResume(owner: LifecycleOwner) {
Log.d(TAG, "NavigationSession resumed")
super.onResume(owner)
}
override fun onStop(owner: LifecycleOwner) {
Log.i(TAG, "In onStop()")
carContext.unbindService(serviceConnection)
navigationService = null
}
override fun onDestroy(owner: LifecycleOwner) {
if (::carSensorManager.isInitialized) {
carSensorManager.cleanup()
}
if (::textToSpeechManager.isInitialized) {
textToSpeechManager.cleanup()
}
carContext
.stopService(
Intent(
carContext,
NavigationNotificationService::class.java
)
)
Log.i(TAG, "NavigationSession destroyed")
}
}
// ViewModel for navigation data and business logic
lateinit var navigationViewModel: NavigationViewModel
// Store for ViewModels to survive configuration changes
lateinit var viewModelStoreOwner: ViewModelStoreOwner
var lastStepIndex = -1
var guidanceAudio = 0
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
lateinit var observerManager: NavigationObserverManager
lateinit var repository: SettingsRepository
lateinit var settingsViewModel: SettingsViewModel
var carConnection: Int = 0
init {
lifecycle.addObserver(lifecycleObserver)
}
/**
* Called when routing engine preference changes.
* Creates appropriate repository based on user selection.
*/
fun onRoutingEngineStateUpdated(routeEngine: Int) {
Log.d(TAG, "onRoutingEngineStateUpdated $routeEngine")
if (!::navigationViewModel.isInitialized || routeEngine != routingEngine) {
navigationViewModel = when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
else -> NavigationViewModel(TomTomRepository())
}
observerManager = NavigationObserverManager(navigationViewModel, this)
observerManager.attachAllObservers(this)
}
}
/**
* Called when location permission is granted.
* Initializes car hardware sensors if available.
*/
fun onPermissionGranted(permission: Boolean) {
if (::carSensorManager.isInitialized && permission) {
carSensorManager.updateConnectionState(carConnection)
}
}
/**
* Called when car connection state changes.
* Handles different connection types: Not Connected, Automotive OS Native, Android Auto Projection.
* Requests appropriate car speed permissions based on connection type.
*/
fun onConnectionStateUpdated(connectionState: Int) {
carConnection = connectionState
when (connectionState) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit
CarConnection.CONNECTION_TYPE_NATIVE -> {
navigationViewModel.permissionGranted.value =
checkPermission(carContext, AUTOMOTIVE_CAR_SPEED_PERMISSION)
}
CarConnection.CONNECTION_TYPE_PROJECTION -> {
navigationViewModel.permissionGranted.value =
checkPermission(carContext, GMS_CAR_SPEED_PERMISSION)
}
}
}
/**
* Creates the initial screen for the session.
* Sets up ViewModel store, initializes settings, components, checks permissions,
* and returns appropriate starting screen.
*/
override fun onCreateScreen(intent: Intent): Screen {
initializeSettings()
setupViewModelStore()
initializeManagers()
initializeViewModels()
initializeScreen()
return checkPermissionsAndGetScreen()
}
/*
* Initializes the settings repository and ViewModel.
*/
private fun initializeSettings() {
repository = getSettingsRepository(carContext)
settingsViewModel = getSettingsViewModel(carContext)
repository.routingEngineFlow.asLiveData().observe(this, Observer {
onRoutingEngineStateUpdated(it)
routingEngine = it
})
repository.trafficFlow.asLiveData().observe(this, Observer {
showTraffic = it
})
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
}
/**
* Sets up ViewModelStoreOwner and manages its lifecycle.
*/
private fun setupViewModelStore() {
viewModelStoreOwner = object : ViewModelStoreOwner {
override val viewModelStore = ViewModelStore()
}
lifecycleScope.launch {
try {
awaitCancellation()
} finally {
viewModelStoreOwner.viewModelStore.clear()
}
}
}
/**
* Initializes ViewModels and observes their state changes.
*/
private fun initializeViewModels() {
navigationViewModel = getViewModel(carContext)
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
navigationViewModel.permissionGranted.observe(this, ::onPermissionGranted)
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
}
/**
* Initializes managers for rendering, sensors, and location.
*/
private fun initializeManagers() {
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
carSensorManager = CarSensorManager(
carContext = carContext,
lifecycleOwner = this,
onLocationUpdate = ::updateLocation,
onCompassUpdate = { orientation -> surfaceRenderer.carOrientation = orientation },
onSpeedUpdate = { speed -> surfaceRenderer.updateCarSpeed(speed) }
)
textToSpeechManager = TextToSpeechManager(carContext)
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
guidanceAudio = it
})
notificationManager = NotificationManager(carContext, this)
}
/**
* Creates the main navigation screen.
*/
private fun initializeScreen() {
navigationScreen = NavigationScreen(
carContext,
surfaceRenderer,
this,
navigationViewModel
)
}
/**
* Checks required permissions and returns appropriate screen.
* Shows permission request screen if needed, otherwise starts location updates.
*/
private fun checkPermissionsAndGetScreen(): Screen {
val hasLocationPermission =
carContext.checkSelfPermission(permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
val hasContactsPermission = !useContacts ||
carContext.checkSelfPermission(permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
return if (hasLocationPermission && hasContactsPermission) {
navigationScreen
} else {
showPermissionScreen()
}
}
/**
* Shows the permission request screen.
*/
private fun showPermissionScreen(): Screen {
val screenManager = carContext.getCarService(ScreenManager::class.java)
screenManager.push(navigationScreen)
return RequestPermissionScreen(
carContext,
listOf(
permission.ACCESS_COARSE_LOCATION,
permission.ACCESS_FINE_LOCATION,
),
permissionCheckCallback = { screenManager.pop() }
)
}
/**
* Handles new intents, primarily for navigation deep links from other apps.
* Supports ACTION_NAVIGATE for starting navigation to a specific location.
*/
override fun onNewIntent(intent: Intent) {
val screenManager = carContext.getCarService(ScreenManager::class.java)
// Handle Android Auto ACTION_NAVIGATE intent
if (CarContext.ACTION_NAVIGATE == intent.action) {
handleNavigateIntent(screenManager)
return
}
// Handle custom deep links
handleDeepLink(intent, screenManager)
}
/**
* Handles ACTION_NAVIGATE intent by showing search screen.
*/
private fun handleNavigateIntent(screenManager: ScreenManager) {
screenManager.popToRoot()
screenManager.pushForResult(
SearchScreen(carContext, surfaceRenderer, navigationViewModel, recentPlaces)
) { result ->
// Handle search result if needed
}
}
/**
* Handles custom deep link URIs.
*/
private fun handleDeepLink(intent: Intent, screenManager: ScreenManager) {
val uri = intent.data ?: return
if (uri.scheme != uriScheme || uri.schemeSpecificPart != uriHost) return
when (uri.fragment) {
"DEEP_LINK_ACTION" -> {
if (screenManager.getTop() !is NavigationScreen) {
screenManager.popToRoot()
}
}
}
}
/**
* Updates navigation state with new location.
* Handles route snapping, deviation detection for rerouting, and map updates.
*/
fun updateLocation(location: Location) {
if (carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
surfaceRenderer.updateCarSpeed(location.speed)
}
updateBearing(location)
checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
surfaceRenderer.updateLocation(location, "")
}
/**
* Updates route bearing if location has bearing information.
*/
private fun updateBearing(location: Location) {
if (location.hasBearing()) {
//routeModel.navState = routeModel.navState.copy(routeBearing = location.bearing)
}
}
/**
* Start navigation process.
* Called when user starts navigation
*/
override fun startNavigation() {
Log.d(TAG, "startNavigation")
navigationService!!.startNavigation(route, destination)
if (notificationActive)
notificationManager.startNotificationService()
}
/**
* Stops active navigation and clears route state.
* Called when user exits navigation or arrives at destination.
*/
override fun stopNavigation() {
Log.d(TAG, "stopNavigation")
navigationService!!.stopNavigation()
surfaceRenderer.navigation = false
surfaceRenderer.routeData.value = ""
lastCameraSearch = 0
surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW
if (notificationActive)
notificationManager.stopNotificationService()
Log.d(TAG, "end stopNavigation")
}
override fun updateTrip(trip: Trip) {
}
/**
* Recalculates a route for the specified place.
*/
override fun recalcRoute(destination: Place) {
val destination = location(destination.longitude, destination.latitude)
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
listOf(destination),
surfaceRenderer.carOrientation
)
}
/**
* Handles the received route string.
* Starts navigation and invalidates the screen.
*/
override fun onRouteReceived(route: String) {
Log.d(TAG, "onRouteReceived")
if (route.isNotEmpty()) {
prepareRoute(route)
}
}
override fun isNavigating(): Boolean {
return navigationService!!.isNavigating()
}
/**
* Prepare route and start navigation
*/
private fun prepareRoute(route: String) {
this.route = route
startNavigation()
surfaceRenderer.setRouteData(navigationService!!.routeModel.curRoute.routeGeoJson)
}
/**
* Handles received traffic data and updates the surface renderer.
*/
override fun onTrafficReceived(traffic: Map<String, String>) {
if (traffic.isNotEmpty()) {
surfaceRenderer.setTrafficData(traffic)
}
}
/**
* Handles the received place search result.
* Navigates to the specified place.
*/
override fun onPlaceSearchResultReceived(place: Place) {
navigateToPlace(place)
}
/**
* Handles received speed camera data.
* Updates the surface renderer with the camera locations.
*/
override fun onSpeedCamerasReceived(cameras: List<Elements>) {
speedCameras = cameras
val coordinates = mutableListOf<List<Double>>()
cameras.forEach {
coordinates.add(listOf(it.lon, it.lat))
}
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCameraData.value = speedData
}
/**
* Handles received maximum speed data and updates the surface renderer.
*/
override fun onMaxSpeedReceived(speed: Int) {
surfaceRenderer.maxSpeed.value = speed
}
override fun onRecentPlacesReceived(places: List<Place>) {
Log.d(TAG, "onRecentPlacesReceived ${places.size}")
recentPlaces = places.toMutableList()
navigationScreen.recentPlaces = places.toMutableList()
navigationScreen.invalidate()
}
override fun invalidateScreen() {
navigationScreen.invalidate()
}
/**
* Loads a route to the specified place and sets it as the destination.
*/
override fun navigateToPlace(place: Place) {
Log.d(TAG, "navigateToPlace ${place.street}")
var prevDestination = Place()
if (surfaceRenderer.navigation) {
prevDestination = place
stopNavigation()
}
val preview = place.route
navigationViewModel.previewRoute.value = ""
val location = if (place.stopOver && prevDestination.latitude != 0.0) {
listOf(
location(place.longitude, place.latitude),
location(prevDestination.longitude, prevDestination.latitude)
)
} else {
listOf(location(place.longitude, place.latitude))
}
navigationViewModel.saveRecent(carContext, place)
destination = place
// routeModel.navState = routeModel.navState.copy(destination = place)
if (preview.isEmpty()) {
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
} else {
//routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
onRouteReceived(preview)
}
surfaceRenderer.activateNavigationView()
}
/**
* Checks if traffic data needs to be updated based on the time since the last update.
*/
fun checkTraffic(current: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastTrafficDate)
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
lastTrafficDate = current
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
}
/**
* Periodically requests speed camera information near the current location.
*/
private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
navigationViewModel.getSpeedCameras(location, 5.0)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)
}
}
/**
* Updates distances to nearby speed cameras and checks for proximity alerts.
*/
private fun updateDistance(
location: Location,
) {
val updatedCameras = mutableListOf<Elements>()
speedCameras.forEach {
val plLocation =
location(longitude = it.lon, latitude = it.lat)
val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble()
updatedCameras.add(it)
}
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val camera = sortedList.firstOrNull() ?: return
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
val bearingSpeedCamera = if (camera.tags.direction != null) {
try {
camera.tags.direction!!.toFloat()
} catch (e: Exception) {
0F
}
} else {
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
}
if (camera.distance < 80) {
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
// routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
}
}
}
override fun invalidateNavigationScreen() {
navigationScreen.invalidate()
}
companion object {
// URI host for deep linking
var uriHost: String = "navigation"
// URI scheme for deep linking
var uriScheme: String = "samples"
}
}
@@ -4,16 +4,17 @@ import android.Manifest.permission
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.Location import android.location.Location
import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.ScreenManager import androidx.car.app.ScreenManager
import androidx.car.app.connection.CarConnection import androidx.car.app.connection.CarConnection
import androidx.car.app.model.Distance
import androidx.car.app.navigation.NavigationManager import androidx.car.app.navigation.NavigationManager
import androidx.car.app.navigation.NavigationManagerCallback import androidx.car.app.navigation.NavigationManagerCallback
import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.Trip import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
@@ -56,7 +57,7 @@ import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.GeoUtils.snapLocation import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.bearingPositive import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
@@ -100,6 +101,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
lateinit var textToSpeechManager: TextToSpeechManager lateinit var textToSpeechManager: TextToSpeechManager
lateinit var notificationManager: NotificationManager
var autoDriveEnabled = false var autoDriveEnabled = false
val simulation = Simulation() val simulation = Simulation()
@@ -113,11 +117,11 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
var speedCameras = listOf<Elements>() var speedCameras = listOf<Elements>()
private var lastRouteCheckLocation = location(0.0, 0.0) var lastRouteDate: LocalDateTime = LocalDateTime.now()
var navigationManagerStarted = false var navigationManagerStarted = false
var updateLocationIndex = 0 var notificationActive = false
/** /**
* Lifecycle observer for managing session lifecycle events. * Lifecycle observer for managing session lifecycle events.
@@ -125,6 +129,16 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
*/ */
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver { private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
Log.d(TAG, "NavigationSession paused")
super.onPause(owner)
}
override fun onResume(owner: LifecycleOwner) {
Log.d(TAG, "NavigationSession resumed")
super.onResume(owner)
}
override fun onDestroy(owner: LifecycleOwner) { override fun onDestroy(owner: LifecycleOwner) {
if (::navigationManager.isInitialized) { if (::navigationManager.isInitialized) {
navigationManager.clearNavigationManagerCallback() navigationManager.clearNavigationManagerCallback()
@@ -138,6 +152,13 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
if (::textToSpeechManager.isInitialized) { if (::textToSpeechManager.isInitialized) {
textToSpeechManager.cleanup() textToSpeechManager.cleanup()
} }
carContext
.stopService(
Intent(
carContext,
NavigationNotificationService::class.java
)
)
Log.i(TAG, "NavigationSession destroyed") Log.i(TAG, "NavigationSession destroyed")
} }
} }
@@ -272,10 +293,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
routeModel = RouteCarModel() routeModel = RouteCarModel()
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated) CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
navigationViewModel.initialSnapLocation.observe(this, Observer {
surfaceRenderer.updateLocation(it, "")
})
} }
/** /**
@@ -318,7 +335,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(), shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(),
onLocationUpdate = ::updateLocation, onLocationUpdate = ::updateLocation,
onInitialLocation = { location -> onInitialLocation = { location ->
navigationViewModel.loadCurrentLocation(location)
navigationViewModel.loadRecentPlaces( navigationViewModel.loadRecentPlaces(
carContext, carContext,
location, location,
@@ -327,10 +343,12 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
} }
) )
textToSpeechManager = TextToSpeechManager(carContext) textToSpeechManager = TextToSpeechManager(carContext)
repository.guidanceAudioFlow.asLiveData().observe(this, Observer { repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
guidanceAudio = it guidanceAudio = it
}) })
notificationManager = NotificationManager(carContext, this)
} }
/** /**
@@ -405,7 +423,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
screenManager.popToRoot() screenManager.popToRoot()
screenManager.pushForResult( screenManager.pushForResult(
SearchScreen(carContext, surfaceRenderer, navigationViewModel, mutableListOf()) SearchScreen(carContext, surfaceRenderer, navigationViewModel, mutableListOf())
) { _ -> ) { result ->
// Handle search result if needed // Handle search result if needed
} }
} }
@@ -446,7 +464,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location) checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
surfaceRenderer.updateLocation(location, streetName) surfaceRenderer.updateLocation(location, streetName)
} }
updateLocationIndex++
} }
/** /**
@@ -459,29 +476,38 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
} }
/** /**
* Checks if the location deviation is acceptable. * Handles location updates during active navigation.
* Snaps location to route and checks for deviation requiring reroute.
*/ */
private fun checkLocationDeviation( private fun handleNavigationLocation(location: Location) {
location: Location, routeModel.updateLocation(location, navigationViewModel)
snappedLocation: Location, if (routeModel.navState.arrived) return
streetName: String if (guidanceAudio == 1) {
): Boolean { handleGuidanceAudio()
var maximalRouteDeviation = MAXIMAL_ROUTE_DEVIATION }
val speed = surfaceRenderer.speed.value val streetName = routeModel.currentStep().street
if (speed != null) { val currentDate = LocalDateTime.now(ZoneOffset.UTC)
when (speed) {
in 0.0..10.0 -> maximalRouteDeviation = MAXIMAL_ROUTE_DEVIATION if (snapLocation(location, streetName)) {
in 10.0..20.0 -> maximalRouteDeviation = MAXIMAL_ROUTE_DEVIATION + 200 checkTraffic(currentDate, location)
in 20.0..30.0 -> maximalRouteDeviation = MAXIMAL_ROUTE_DEVIATION + 400 updateSpeedCamera(location)
in 30.0..100.0 -> maximalRouteDeviation = MAXIMAL_ROUTE_DEVIATION + 500 checkRoute(currentDate, location)
updateNavigationScreen()
checkArrival()
} }
} }
/**
* Updates the surface renderer with snapped location and street name.
* Checks if maximal route deviation is exceeded and reroutes if needed.
*/
private fun snapLocation(location: Location, streetName: String): Boolean {
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snappedLocation) val distance = location.distanceTo(snappedLocation)
Log.d(TAG, "Distance: $distance $maximalRouteDeviation $speed")
when { when {
distance > maximalRouteDeviation -> { distance > MAXIMAL_ROUTE_DEVIATION -> {
stopNavigation() stopNavigation()
navigationScreen.calculateNewRoute(routeModel.navState.destination, distance) navigationScreen.calculateNewRoute(routeModel.navState.destination)
return false return false
} }
@@ -496,31 +522,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
return true return true
} }
/**
* Handles location updates during active navigation.
* Snaps location to route and checks for deviation requiring reroute.
*/
private fun handleNavigationLocation(location: Location) {
val start = System.currentTimeMillis()
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
routeModel.updateLocation(snappedLocation, navigationViewModel)
val streetName = routeModel.currentStep().street
if (checkLocationDeviation(location, snappedLocation, streetName)) {
if (routeModel.navState.arrived) return
if (guidanceAudio == 1) {
handleGuidanceAudio()
}
val currentDate = LocalDateTime.now(ZoneOffset.UTC)
checkTraffic(currentDate, snappedLocation)
updateSpeedCamera(snappedLocation)
checkRoute(snappedLocation)
updateNavigationScreen()
checkArrival()
}
val end = System.currentTimeMillis()
//Log.d(TAG, "UpdateLocation ${end-start} ms")
}
/** /**
* Updates the navigation screen with new trip information. * Updates the navigation screen with new trip information.
*/ */
@@ -530,37 +531,56 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
) { ) {
return return
} }
val travelEstimateTrip = routeModel.travelEstimateTrip(carContext, distanceMode)
val travelEstimateStep = routeModel.travelEstimateStep(carContext, distanceMode)
val steps = mutableListOf<Step>()
val destination = Destination.Builder()
.setName(routeModel.navState.destination.name)
.setAddress(routeModel.navState.destination.street)
.build()
val distance =
formattedDistance(0, routeModel.routeCalculator.leftStepDistance())
steps.add(routeModel.currentStep(carContext))
if (routeModel.navState.nextStep) {
steps.add(routeModel.nextStep(carContext = carContext))
val stepData = routeModel.currentStep() }
navigationScreen.updateTrip( navigationScreen.updateTrip(
isNavigating = routeModel.isNavigating(), isNavigating = routeModel.isNavigating(),
isRerouting = false, isRerouting = false,
hasArrived = routeModel.isManeuverArrival(), hasArrived = routeModel.isArrival(),
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext), destinationTravelEstimate = travelEstimateTrip,
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext), stepTravelEstimate = travelEstimateStep,
destinations = mutableListOf(routeModel.getDestination()), destinations = mutableListOf(destination),
steps = routeModel.getSteps(carContext), steps = steps,
stepRemainingDistance = routeModel.getDistance(), stepRemainingDistance = Distance.create(distance.first, distance.second),
shouldShowNextStep = false, shouldShowNextStep = false,
shouldShowLanes = true, shouldShowLanes = true,
junctionImage = null, junctionImage = null,
backGroundColor = routeModel.backGroundColor(), backGroundColor = routeModel.backGroundColor()
message = stepData.message,
) )
/** /**
* Updates the trip information and notifies the listener with a new Trip object. * Updates the trip information and notifies the listener with a new Trip object.
* This includes destination name, address, travel estimate, and loading status. * This includes destination name, address, travel estimate, and loading status.
*/ */
updateTrip(routeModel.getTrip(carContext))
val tripBuilder = Trip.Builder()
tripBuilder.addDestination(
destination,
travelEstimateTrip
)
tripBuilder.setLoading(false)
tripBuilder.setCurrentRoad(destination.name.toString())
tripBuilder.addStep(steps.first(), travelEstimateStep)
updateTrip(tripBuilder.build())
} }
/** /**
* Checks for arrival * Checks for arrival
*/ */
fun checkArrival() { fun checkArrival() {
if (routeModel.isManeuverArrival() if (routeModel.isArrival()
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE && routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) { ) {
stopNavigation() stopNavigation()
@@ -577,6 +597,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
* Called when user starts navigation * Called when user starts navigation
*/ */
override fun startNavigation() { override fun startNavigation() {
Log.d(TAG, "startNavigation")
surfaceRenderer.navigation = true surfaceRenderer.navigation = true
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationManager.navigationStarted() navigationManager.navigationStarted()
@@ -588,6 +609,8 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
updateLocation(location) updateLocation(location)
} }
} }
if (notificationActive)
notificationManager.startNotificationService()
} }
/** /**
@@ -595,6 +618,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
* Called when user exits navigation or arrives at destination. * Called when user exits navigation or arrives at destination.
*/ */
override fun stopNavigation() { override fun stopNavigation() {
Log.d(TAG, "stopNavigation")
surfaceRenderer.navigation = false surfaceRenderer.navigation = false
routeModel.stopNavigation() routeModel.stopNavigation()
navigationManager.navigationEnded() navigationManager.navigationEnded()
@@ -606,7 +630,8 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
lastCameraSearch = 0 lastCameraSearch = 0
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW navigationScreen.navigationType = NavigationType.VIEW
navigationScreen.invalidate() if (notificationActive)
notificationManager.stopNotificationService()
} }
override fun updateTrip(trip: Trip) { override fun updateTrip(trip: Trip) {
@@ -624,7 +649,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
carContext, carContext,
surfaceRenderer.lastLocation, surfaceRenderer.lastLocation,
listOf(destination), listOf(destination),
surfaceRenderer.cameraPosition.value!!.bearing.toFloat() surfaceRenderer.carOrientation
) )
} }
@@ -638,6 +663,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) { if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
textToSpeechManager.speak(stepData.message) textToSpeechManager.speak(stepData.message)
lastStepIndex = currentStep.index lastStepIndex = currentStep.index
if (notificationActive) {
notificationManager.sendMessage(stepData.message)
}
} }
} }
@@ -646,6 +674,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
* Starts navigation and invalidates the screen. * Starts navigation and invalidates the screen.
*/ */
override fun onRouteReceived(route: String) { override fun onRouteReceived(route: String) {
Log.d(TAG, "onRouteReceived")
if (route.isNotEmpty()) { if (route.isNotEmpty()) {
this.route = route this.route = route
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
@@ -678,11 +707,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
val newRouteModel = RouteModel() val newRouteModel = RouteModel()
newRouteModel.navState = routeModel.navState.copy(routingEngine = routingEngine) newRouteModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
newRouteModel.startNavigation(route) newRouteModel.startNavigation(route)
if ((routeModel.curRoute.summary.trafficDelay - newRouteModel.curRoute.summary.trafficDelay).absoluteValue > 300) { routeModel.curRoute.summary.trafficDelay = newRouteModel.curRoute.summary.trafficDelay
routeModel.startNavigation(route)
updateNavigationScreen() updateNavigationScreen()
} }
}
override fun isNavigating(): Boolean = routeModel.isNavigating() override fun isNavigating(): Boolean = routeModel.isNavigating()
@@ -714,11 +741,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
cameras.forEach { cameras.forEach {
coordinates.add(listOf(it.lon, it.lat)) coordinates.add(listOf(it.lon, it.lat))
} }
synchronized(this) {
val speedData = GeoUtils.createPointCollection(coordinates, "radar") val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCameraData.value = speedData surfaceRenderer.speedCameraData.value = speedData
} }
}
/** /**
* Handles received maximum speed data and updates the surface renderer. * Handles received maximum speed data and updates the surface renderer.
@@ -727,18 +752,18 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
surfaceRenderer.maxSpeed.value = speed surfaceRenderer.maxSpeed.value = speed
} }
override fun invalidateScreen() { override fun onRecentPlacesReceived(places: List<Place>) {
navigationScreen.invalidate()
} }
override fun onTrafficMessageReceived(trafficMessage: String) { override fun invalidateScreen() {
navigationScreen.invalidate()
} }
/** /**
* Loads a route to the specified place and sets it as the destination. * Loads a route to the specified place and sets it as the destination.
*/ */
override fun navigateToPlace(place: Place) { override fun navigateToPlace(place: Place) {
Log.d(TAG, "navigateToPlace ${place.street}")
var prevDestination = Place() var prevDestination = Place()
if (surfaceRenderer.navigation) { if (surfaceRenderer.navigation) {
prevDestination = routeModel.navState.destination prevDestination = routeModel.navState.destination
@@ -778,11 +803,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
val duration = Duration.between(current, lastTrafficDate) val duration = Duration.between(current, lastTrafficDate)
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) { if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
lastTrafficDate = current lastTrafficDate = current
navigationViewModel.loadTraffic( navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
carContext,
location,
surfaceRenderer.carOrientation
)
} }
} }
@@ -790,13 +811,12 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
* Periodically requests speed camera information near the current location. * Periodically requests speed camera information near the current location.
*/ */
private fun updateSpeedCamera(location: Location) { private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch % 200 == 0) { if (lastCameraSearch++ % 100 == 0) {
navigationViewModel.getSpeedCameras(location, 5.0) navigationViewModel.getSpeedCameras(location, 5.0)
} }
if (speedCameras.isNotEmpty() && lastCameraSearch % 15 == 0) { if (speedCameras.isNotEmpty()) {
updateDistance(location) updateDistance(location)
} }
lastCameraSearch++
} }
/** /**
@@ -805,7 +825,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
private fun updateDistance( private fun updateDistance(
location: Location, location: Location,
) { ) {
synchronized(this) {
val updatedCameras = mutableListOf<Elements>() val updatedCameras = mutableListOf<Elements>()
speedCameras.forEach { speedCameras.forEach {
val plLocation = val plLocation =
@@ -816,35 +835,31 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
} }
val sortedList = updatedCameras.sortedWith(compareBy { it.distance }) val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val camera = sortedList.firstOrNull() ?: return val camera = sortedList.firstOrNull() ?: return
val bearingRoute = surfaceRenderer.lastLocation.bearingPositive(location) val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
val bearingSpeedCamera = try { val bearingSpeedCamera = if (camera.tags.direction != null) {
camera.tags.direction.toFloat() try {
} catch (_: Exception) { camera.tags.direction!!.toFloat()
} catch (e: Exception) {
0F 0F
} }
} else {
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
}
if (camera.distance < 80) { if (camera.distance < 80) {
if ((bearingSpeedCamera - bearingRoute).absoluteValue < 15.0) { if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed) routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
val cameras = mutableListOf<Elements>()
speedCameras.forEach {
if (!(camera.lon == it.lon && camera.lat == it.lat)) {
cameras.add(it)
}
}
speedCameras = cameras
} }
} }
} }
}
/** /**
* Checks if a new route is needed based on the time since the last update. * Checks if a new route is needed based on the time since the last update.
*/ */
private fun checkRoute(location: Location) { private fun checkRoute(currentDate: LocalDateTime, location: Location) {
val distance = location.distanceTo(lastRouteCheckLocation) val duration = Duration.between(currentDate, lastRouteDate)
if (distance > checkDistance(routeModel)) { val routeUpdate = routeModel.curRoute.summary.duration / 4
if (lastRouteCheckLocation.latitude != 0.0) { if (duration.abs().seconds > routeUpdate) {
lastRouteDate = currentDate
val destination = location( val destination = location(
routeModel.navState.destination.longitude, routeModel.navState.destination.longitude,
routeModel.navState.destination.latitude routeModel.navState.destination.latitude
@@ -856,18 +871,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
surfaceRenderer.carOrientation surfaceRenderer.carOrientation
) )
} }
lastRouteCheckLocation = location
}
}
private fun checkDistance(routeModel: RouteModel): Double {
val factor = when (routeModel.curRoute.summary.distance) {
in 0.0..20000.0 -> 4
in 20000.0..100000.0 -> 5
in 10000.0..200000.0 -> 6
else -> 7
}
return (routeModel.curRoute.summary.distance / factor)
} }
override fun invalidateNavigationScreen() { override fun invalidateNavigationScreen() {
@@ -1,15 +1,22 @@
package com.kouros.navigation.car package com.kouros.navigation.car
import android.content.Intent import android.content.Intent
import android.location.Location
import android.os.Message
import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.hardware.CarHardwareManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.kouros.navigation.data.Constants.TAG
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class NavigationNotificationManager( class NotificationManager(
private val carContext: CarContext, private val carContext: CarContext,
private val lifecycleOwner: LifecycleOwner,
) { ) {
private var notificationServiceStarted = false private var notificationServiceStarted = false
@@ -17,8 +24,17 @@ class NavigationNotificationManager(
private var serviceStarted = false private var serviceStarted = false
init { init {
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
} }
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.DESTROYED) {
if (notificationServiceStarted) {
stopNotificationService()
}
}
}
}
fun startNotificationService() { fun startNotificationService() {
val intent = Intent(carContext, NavigationNotificationService::class.java) val intent = Intent(carContext, NavigationNotificationService::class.java)
@@ -8,6 +8,7 @@ import android.location.Location
import android.util.Log import android.util.Log
import androidx.car.app.AppManager import androidx.car.app.AppManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Session
import androidx.car.app.SurfaceCallback import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer import androidx.car.app.SurfaceContainer
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -34,7 +35,7 @@ import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.DarkMode import com.kouros.navigation.data.DarkMode
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.BaseStyleModel import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.utils.bearingPositive import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateTilt import com.kouros.navigation.utils.calculateTilt
import com.kouros.navigation.utils.calculateZoom import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.duration import com.kouros.navigation.utils.duration
@@ -46,10 +47,6 @@ import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
/** /**
@@ -159,13 +156,15 @@ class SurfaceRenderer(
Log.i(TAG, "Surface available $surfaceContainer") Log.i(TAG, "Surface available $surfaceContainer")
lifecycleOwner = CustomLifecycleOwner() lifecycleOwner = CustomLifecycleOwner()
lifecycleOwner.performRestore(null) lifecycleOwner.performRestore(null)
// technically, we only really need any one of these instead of all 3
// add them to be consistent with the actual lifecycle.
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START) lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
virtualDisplay = carContext.getSystemService(DisplayManager::class.java) virtualDisplay = carContext.getSystemService(DisplayManager::class.java)
.createVirtualDisplay( .createVirtualDisplay(
"Navigation", "Maps",
surfaceContainer.width, surfaceContainer.width,
surfaceContainer.height, surfaceContainer.height,
surfaceContainer.dpi, surfaceContainer.dpi,
@@ -229,39 +228,15 @@ class SurfaceRenderer(
*/ */
override fun onScroll(distanceX: Float, distanceY: Float) { override fun onScroll(distanceX: Float, distanceY: Float) {
synchronized(this@SurfaceRenderer) { synchronized(this@SurfaceRenderer) {
val currentCamera = cameraPosition.value ?: return@synchronized
val zoom = currentCamera.zoom
val bearing = currentCamera.bearing
// MapLibre typically uses 512px tiles.
// At zoom level z, the world (360 degrees) is 512 * 2^z pixels wide.
val pixelsPerDegreeLon = (512.0 * 2.0.pow(zoom)) / 360.0
// Latitude correction: In Mercator, the vertical scale is stretched by 1/cos(lat).
val latRad = lastLocation.latitude * PI / 180.0
val pixelsPerDegreeLat = pixelsPerDegreeLon / cos(latRad)
// Rotation compensation (bearing is degrees clockwise from North)
val bearingRad = bearing * PI / 180.0
val cosB = cos(bearingRad)
val sinB = sin(bearingRad)
// Rotate screen-space scroll to map-space scroll
val rotatedDx = distanceX * cosB - distanceY * sinB
val rotatedDy = distanceX * sinB + distanceY * cosB
viewStyle = ViewStyle.PAN_VIEW viewStyle = ViewStyle.PAN_VIEW
if (distanceX != 0.0F) {
// Update location based on rotated scroll distances and calculated factors lastLocation.longitude += (distanceX / 1000) / cameraPosition.value!!.zoom
lastLocation.longitude += (rotatedDx / pixelsPerDegreeLon) }
lastLocation.latitude -= (rotatedDy / pixelsPerDegreeLat) if (distanceY != 0.0F) {
lastLocation.latitude += (distanceY / 1000) / cameraPosition.value!!.zoom
}
val pos = Position(lastLocation.longitude, lastLocation.latitude) val pos = Position(lastLocation.longitude, lastLocation.latitude)
updateCameraPosition( updateCameraPosition( target = pos)
bearing = bearing,
zoom = zoom,
target = pos
)
navigationSession.invalidateNavigationScreen() navigationSession.invalidateNavigationScreen()
} }
} }
@@ -271,6 +246,7 @@ class SurfaceRenderer(
*/ */
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) { override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
synchronized(this@SurfaceRenderer) { synchronized(this@SurfaceRenderer) {
Log.d(TAG, "onScale")
} }
} }
} }
@@ -325,7 +301,7 @@ class SurfaceRenderer(
) { ) {
val cameraDuration = val cameraDuration =
duration( duration(
viewStyle, viewStyle == ViewStyle.PREVIEW,
position!!.bearing, position!!.bearing,
lastBearing, lastBearing,
lastLocationUpdate lastLocationUpdate
@@ -341,8 +317,7 @@ class SurfaceRenderer(
width, width,
height, height,
streetName, streetName,
darkMode, darkMode
tilt
) )
} }
LaunchedEffect(position, viewStyle) { LaunchedEffect(position, viewStyle) {
@@ -376,11 +351,13 @@ class SurfaceRenderer(
viewStyle = ViewStyle.PAN_VIEW viewStyle = ViewStyle.PAN_VIEW
} }
val newZoom = if (zoomSign < 0) { val newZoom = if (zoomSign < 0) {
cameraPosition.value!!.zoom - 1 cameraPosition.value!!.zoom - 0.2
} else { } else {
cameraPosition.value!!.zoom + 1 cameraPosition.value!!.zoom + 0.2
}
if (viewStyle == ViewStyle.VIEW) {
tilt = calculateTilt(newZoom, tilt)
} }
tilt = calculateTilt(viewStyle, newZoom, tilt)
updateCameraPosition( updateCameraPosition(
cameraPosition.value!!.bearing, cameraPosition.value!!.bearing,
newZoom, newZoom,
@@ -395,26 +372,23 @@ class SurfaceRenderer(
* Uses car orientation sensor if available, otherwise falls back to location bearing. * Uses car orientation sensor if available, otherwise falls back to location bearing.
*/ */
fun updateLocation(location: Location, streetName: String) { fun updateLocation(location: Location, streetName: String) {
Log.d(TAG, "updateLocation Surface $location $streetName")
synchronized(this) { synchronized(this) {
street.value = streetName street.value = streetName
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) { if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
//val bearing = val bearing = if (carOrientation == 999F) {
// location.bearing.toDouble() if (location.hasBearing()) {
//carOrientation = bearing.toFloat()
// val bearing = if (carOrientation == 999F) {
val bearing = if (location.hasBearing()) {
location.bearing.toDouble() location.bearing.toDouble()
} else { } else {
bearingPositive( bearing(
lastLocation, lastLocation,
location, location,
cameraPosition.value!!.bearing cameraPosition.value!!.bearing
) )
} }
carOrientation = bearing.toFloat() } else {
// } else { carOrientation.toDouble()
// carOrientation.toDouble() }
// }
val zoom = if (viewStyle == ViewStyle.VIEW) { val zoom = if (viewStyle == ViewStyle.VIEW) {
calculateZoom(location.speed.toDouble()) calculateZoom(location.speed.toDouble())
} else { } else {
@@ -435,11 +409,8 @@ class SurfaceRenderer(
* Sets route data for active navigation and switches to VIEW mode. * Sets route data for active navigation and switches to VIEW mode.
*/ */
fun setRouteData(routeGeoJson: String) { fun setRouteData(routeGeoJson: String) {
synchronized(this) {
routeData.value = routeGeoJson routeData.value = routeGeoJson
viewStyle = ViewStyle.VIEW viewStyle = ViewStyle.VIEW
updateLocation(lastLocation, "")
}
} }
/** /**
@@ -507,7 +478,7 @@ class SurfaceRenderer(
} }
viewStyle = ViewStyle.VIEW viewStyle = ViewStyle.VIEW
val zoom = calculateZoom(0.0) val zoom = calculateZoom(0.0)
tilt = calculateTilt(viewStyle, zoom, tilt) tilt = calculateTilt(zoom, tilt)
updateCameraPosition( updateCameraPosition(
tilt = tilt, tilt = tilt,
zoom = zoom, zoom = zoom,
@@ -49,6 +49,7 @@ class TextToSpeechManager(private val carContext: Context) {
}) })
} }
initialized = true initialized = true
Log.d("TTS", "Initialization Success")
} else { } else {
Log.e("TTS", "Initialization Failed") Log.e("TTS", "Initialization Failed")
} }
@@ -1,6 +1,7 @@
package com.kouros.navigation.car.map package com.kouros.navigation.car.map
import android.location.Location import android.location.Location
import android.util.Log
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -23,25 +24,15 @@ import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.HeavyColor
import com.kouros.navigation.data.LaneColor
import com.kouros.navigation.data.NavigationCircle
import com.kouros.navigation.data.NavigationColorDark import com.kouros.navigation.data.NavigationColorDark
import com.kouros.navigation.data.NavigationColorLight import com.kouros.navigation.data.NavigationColorLight
import com.kouros.navigation.data.PharmacyColor
import com.kouros.navigation.data.QueuingColor
import com.kouros.navigation.data.RoadworksColor
import com.kouros.navigation.data.RouteColor import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SlowColor
import com.kouros.navigation.data.SpeedColor import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.data.StationaryColor
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.isMetricSystem import com.kouros.navigation.utils.isMetricSystem
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
@@ -54,14 +45,15 @@ import org.maplibre.compose.expressions.dsl.image
import org.maplibre.compose.expressions.dsl.interpolate import org.maplibre.compose.expressions.dsl.interpolate
import org.maplibre.compose.expressions.dsl.zoom import org.maplibre.compose.expressions.dsl.zoom
import org.maplibre.compose.expressions.value.ColorValue import org.maplibre.compose.expressions.value.ColorValue
import org.maplibre.compose.expressions.value.DpValue
import org.maplibre.compose.expressions.value.ImageValue
import org.maplibre.compose.layers.Anchor import org.maplibre.compose.layers.Anchor
import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.FillLayer
import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.layers.LineLayer
import org.maplibre.compose.layers.SymbolLayer import org.maplibre.compose.layers.SymbolLayer
import org.maplibre.compose.location.LocationPuck
import org.maplibre.compose.location.LocationPuckColors import org.maplibre.compose.location.LocationPuckColors
import org.maplibre.compose.location.LocationPuckSizes import org.maplibre.compose.location.LocationPuckSizes
import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.map.GestureOptions
import org.maplibre.compose.map.MapOptions import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions import org.maplibre.compose.map.OrnamentOptions
@@ -70,8 +62,6 @@ import org.maplibre.compose.sources.Source
import org.maplibre.compose.sources.getBaseSource import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.sources.rememberGeoJsonSource import org.maplibre.compose.sources.rememberGeoJsonSource
import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.style.BaseStyle
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.LineString
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -120,182 +110,94 @@ fun MapLibre(
BuildingLayer(tiles) BuildingLayer(tiles)
} }
if (viewStyle == ViewStyle.AMENITY_VIEW) { if (viewStyle == ViewStyle.AMENITY_VIEW) {
val lastLocation = location( val lastLocation = location(cameraState.position.target.longitude, cameraState.position.target.latitude)
cameraState.position.target.longitude,
cameraState.position.target.latitude
)
Puck(cameraState, lastLocation) Puck(cameraState, lastLocation)
AmenityLayer(route) AmenityLayer(route)
} else { } else {
TrafficLayer(traffic!!) RouteLayer(route, traffic!!)
RouteLayer(route)
StartEndLayer(route)
// change also createLineStringCollection in GeoUtils
// Uncomment StartEndLayer
//RouteLayerPoint(route ) //RouteLayerPoint(route )
} }
SpeedCameraLayer(speedCameras) SpeedCameraLayer(speedCameras)
} }
} }
} }
@Composable @Composable
fun RouteLayer(routeData: String?) { fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
if (!routeData.isNullOrEmpty()) { if (!routeData.isNullOrEmpty()) {
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData)) val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
LineLayer( LineLayer(
id = "routes-casing", id = "routes-casing",
source = routes, source = routes,
color = const(Color.Green), color = const(Color.White),
width = routeLineWidth(base = 1.dp, isCasing = true), width =
interpolate(
type = exponential(1.2f),
input = zoom(),
5 to const(0.4.dp),
6 to const(0.8.dp),
7 to const(2.0.dp),
20 to const(24.dp),
),
) )
LineLayer( LineLayer(
id = "routes", id = "routes",
source = routes, source = routes,
color = const(RouteColor), color = const(RouteColor),
width = routeLineWidth(base = 1.dp, isCasing = false), width =
interpolate(
type = exponential(1.2f),
input = zoom(),
5 to const(0.7.dp),
6 to const(1.0.dp),
7 to const(2.4.dp),
20 to const(26.dp),
),
) )
} }
}
@Composable
fun TrafficLayer(trafficData: Map<String, String>) {
trafficData.forEach { trafficData.forEach {
if (it.key == "closed") {
//ClosedLayer(it)
} else {
val traffic = rememberGeoJsonSource(GeoJsonData.JsonString(it.value)) val traffic = rememberGeoJsonSource(GeoJsonData.JsonString(it.value))
LineLayer( LineLayer(
id = "traffic-${it.key}-casing", id = "traffic-${it.key}-casing",
source = traffic, source = traffic,
color = const(Color.White), color = const(Color.White),
width = routeLineWidth(base = 2.dp, isCasing = true), width =
interpolate(
type = exponential(1.2f),
input = zoom(),
5 to const(0.4.dp),
6 to const(0.6.dp),
7 to const(1.8.dp),
20 to const(20.dp),
),
) )
LineLayer( LineLayer(
id = "traffic-${it.key}", id = "traffic-${it.key}",
source = traffic, source = traffic,
color = trafficColor(it.key), color = trafficColor(it.key),
width = routeLineWidth(base = 1.dp, isCasing = false), width =
)
}
// TrafficIcons(it)
}
}
@Composable
fun StartEndLayer(routeData: String?) {
if (!routeData.isNullOrEmpty()) {
val end = createPointCollection(routeData)
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(end))
val img = image(painterResource(R.drawable.sports_score_48px), drawAsSdf = true)
SymbolLayer(
id = "end-layer",
source = routes,
iconColor = const(Color.Black),
iconImage = img,
iconSize =
interpolate( interpolate(
type = exponential(2.0f), type = exponential(1.2f),
input = zoom(), input = zoom(),
5 to const(2.0f), 5 to const(0.4.dp),
10 to const(2.0f), 6 to const(0.5.dp),
15 to const(3.0f), 7 to const(1.6.dp),
20 to const(4.0f), 20 to const(18.dp),
), ),
) )
} }
} }
@Composable
private fun ClosedLayer(entry: Map.Entry<String, String>) {
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(entry.value))
val img = image(painterResource(R.drawable.do_not_disturb_on_24px), drawAsSdf = true)
SymbolLayer(
id = "closed-layer",
source = routes,
iconColor = const(Color.Red),
iconImage = img,
iconSize =
interpolate(
type = exponential(2.0f),
input = zoom(),
5 to const(2.0f),
10 to const(2.0f),
15 to const(3.0f),
20 to const(4.0f),
),
)
}
@Composable
private fun TrafficIcons(entry: Map.Entry<String, String>) {
val featureCollection = FeatureCollection.fromJson(entry.value)
if (featureCollection.features()!!.isNotEmpty()) {
val points = mutableListOf<List<Double>>()
featureCollection.features()!!.forEach { geo ->
val coordinates = (geo.geometry() as LineString).coordinates()
if (coordinates.size > 5) {
val first = coordinates.first()
val second = coordinates[1]
points.add(listOf(first.coordinates()[0], second.coordinates()[1]))
}
}
val collection = createPointCollection(
points, entry.key
)
TrafficPoint(entry.key, "${entry.key}-point-layer", collection)
}
}
@Composable
fun TrafficPoint(id: String, layer: String, routeData: String?) {
if (!routeData.isNullOrEmpty()) {
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
val img = trafficImage(id)
SymbolLayer(
id = layer,
source = routes,
iconColor = const(Color.Red),
iconImage = img,
iconSize =
interpolate(
type = exponential(2f),
input = zoom(),
5 to const(0.4f),
6 to const(1.6f),
7 to const(2.0f),
20 to const(5.0f),
),
)
}
}
/**
* Reusable helper for consistent line widths across all route layers.
*/
@Composable
private fun routeLineWidth(base: Dp, isCasing: Boolean = false): Expression<DpValue> {
val extra = if (isCasing) 1.dp else 0.dp
val width = base + extra
return interpolate(
type = exponential(2f),
input = zoom(),
1 to const(width + 3.dp),
15 to const(width + 13.dp),
18 to const(width + 15.dp),
24 to const(width + 24.dp),
)
}
@Composable @Composable
fun RouteLayerPoint(routeData: String?) { fun RouteLayerPoint(routeData: String?) {
if (!routeData.isNullOrEmpty()) { if (!routeData.isNullOrEmpty()) {
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData)) val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
val img = image(painterResource(R.drawable.settings_48px), drawAsSdf = true) val img = image(painterResource(R.drawable.ic_favorite_filled_white_24dp), drawAsSdf = true)
SymbolLayer( SymbolLayer(
id = "route-point-layer", id = "point-layer",
source = routes, source = routes,
iconOpacity = const(2.0f), iconOpacity = const(2.0f),
iconColor = const(Color.Red), iconColor = const(Color.Red),
@@ -304,34 +206,23 @@ fun RouteLayerPoint(routeData: String?) {
interpolate( interpolate(
type = exponential(1.2f), type = exponential(1.2f),
input = zoom(), input = zoom(),
5 to const(0.8f), 5 to const(0.4f),
6 to const(1.0f), 6 to const(0.6f),
7 to const(1.2f), 7 to const(0.8f),
20 to const(1.4f), 20 to const(1.0f),
), ),
) )
} }
} }
@Composable
fun trafficImage(key: String): Expression<ImageValue> {
when (key) {
"heavy", "queuing", "slow", "stationary" -> return image(painterResource(R.drawable.traffic_jam_24px), drawAsSdf = true)
"roadworks" -> return image(painterResource(R.drawable.construction_24px), drawAsSdf = true)
"closed", "lane" -> return image(painterResource(R.drawable.remove_road_24px), drawAsSdf = true)
}
return image(painterResource(R.drawable.traffic_jam_24px), drawAsSdf = true)
}
fun trafficColor(key: String): Expression<ColorValue> { fun trafficColor(key: String): Expression<ColorValue> {
when (key) { when (key) {
"queuing" -> return const(QueuingColor) "queuing" -> return const(Color(0xFFC46E53))
"slow" -> return const(SlowColor) "slow" -> return const(Color(0xFFC43E3E))
"stationary" -> return const(StationaryColor) "stationary" -> return const(Color(0xFF910A0A))
"heavy" -> return const(HeavyColor) "heavy" -> return const(Color(0xFF6B0404))
"roadworks" -> return const(RoadworksColor) "roadworks" -> return const(Color(0xFF443506))
"lane" -> return const(LaneColor)
} }
return const(Color.Blue) return const(Color.Blue)
} }
@@ -342,14 +233,11 @@ fun AmenityLayer(routeData: String?) {
var color = const(Color.Red) var color = const(Color.Red)
var img = image(painterResource(R.drawable.local_pharmacy_24px), drawAsSdf = true) var img = image(painterResource(R.drawable.local_pharmacy_24px), drawAsSdf = true)
if (routeData.contains(Constants.CHARGING_STATION)) { if (routeData.contains(Constants.CHARGING_STATION)) {
color = const(PharmacyColor) color = const(Color(0xFF054603))
img = image(painterResource(R.drawable.ev_station_24px), drawAsSdf = true) img = image(painterResource(R.drawable.ev_station_24px), drawAsSdf = true)
} else if (routeData.contains(Constants.FUEL_STATION)) { } else if (routeData.contains(Constants.FUEL_STATION)) {
color = const(Color.Blue) color = const(Color.Blue)
img = image(painterResource(R.drawable.local_gas_station_24), drawAsSdf = true) img = image(painterResource(R.drawable.local_gas_station_24), drawAsSdf = true)
} else if (routeData.contains(Constants.RESTAURANT)) {
color = const(Color.Magenta)
img = image(painterResource(R.drawable.restaurant_24px), drawAsSdf = true)
} }
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData)) val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
SymbolLayer( SymbolLayer(
@@ -415,14 +303,13 @@ fun DrawNavigationImages(
height: Int, height: Int,
streetName: String?, streetName: String?,
darkMode: Boolean, darkMode: Boolean,
tilt: Double,
) { ) {
NavigationImage(padding, width, height, streetName, darkMode, tilt) NavigationImage(padding, width, height, streetName, darkMode)
if (speed != null) { if (speed != null) {
CurrentSpeed(width, height, speed, maxSpeed) CurrentSpeed(width, height, speed, maxSpeed)
} }
if (speed != null && maxSpeed > 0) { if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) {
MaxSpeed(width, height, maxSpeed, speed) MaxSpeed(width, height, maxSpeed)
} }
//DebugInfo(width, height, lat!!) //DebugInfo(width, height, lat!!)
} }
@@ -433,20 +320,19 @@ fun NavigationImage(
width: Int, width: Int,
height: Int, height: Int,
streetName: String?, streetName: String?,
darkMode: Boolean, darkMode: Boolean
tilt: Double
) { ) {
val imageSize = (height / 8) val imageSize = (height / 8)
val navigationColor = if (darkMode) val navigationColor = if (darkMode)
remember { NavigationColorLight }
else
remember { NavigationColorDark } remember { NavigationColorDark }
else
remember { NavigationColorLight }
val textMeasurerStreet = rememberTextMeasurer() val textMeasurerStreet = rememberTextMeasurer()
val street = streetName.toString() val street = streetName.toString()
val styleStreet = TextStyle( val styleStreet = TextStyle(
fontSize = 18.sp, fontSize = 16.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = if (darkMode) Color.White else navigationColor, color = if (darkMode) Color.White else navigationColor,
) )
@@ -454,18 +340,13 @@ fun NavigationImage(
textMeasurerStreet.measure(street, styleStreet, overflow = TextOverflow.Ellipsis) textMeasurerStreet.measure(street, styleStreet, overflow = TextOverflow.Ellipsis)
} }
val scaleY = if (tilt == 0.0) {
1F
} else {
0.7F
}
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) { Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) {
Canvas( Canvas(
modifier = Modifier modifier = Modifier
.size(imageSize.dp, imageSize.dp) .size(imageSize.dp, imageSize.dp)
) { ) {
scale(scaleX = 1f, scaleY = scaleY) { scale(scaleX = 1f, scaleY = 0.7f) {
drawCircle(NavigationCircle.copy(alpha = 0.4f)) drawCircle(navigationColor.copy(alpha = 0.3f))
} }
} }
Icon( Icon(
@@ -474,7 +355,7 @@ fun NavigationImage(
tint = navigationColor.copy(alpha = 0.7f), tint = navigationColor.copy(alpha = 0.7f),
modifier = Modifier modifier = Modifier
.size(imageSize.dp, imageSize.dp) .size(imageSize.dp, imageSize.dp)
.scale(scaleX = 1f, scaleY = scaleY), .scale(scaleX = 1f, scaleY = 0.7f),
) )
Canvas( Canvas(
@@ -516,7 +397,7 @@ private fun CurrentSpeed(
maxSpeed: Int maxSpeed: Int
) { ) {
val radius = 36 val radius = 34
Box( Box(
modifier = Modifier modifier = Modifier
.padding( .padding(
@@ -528,18 +409,17 @@ private fun CurrentSpeed(
val textMeasurerSpeed = rememberTextMeasurer() val textMeasurerSpeed = rememberTextMeasurer()
val textMeasurerKm = rememberTextMeasurer() val textMeasurerKm = rememberTextMeasurer()
val speed = if (isMetricSystem()) (curSpeed * 3.6).toInt() val speed = if (isMetricSystem()) (curSpeed * 3.6).toInt().toString() else (curSpeed * 3.6 * 0.6214).toInt().toString()
.toString() else (curSpeed * 3.6 * 0.6214).toInt().toString()
val kmh = if (isMetricSystem()) "km/h" else "mph" val kmh = if (isMetricSystem()) "km/h" else "mph"
val styleSpeed = TextStyle( val styleSpeed = TextStyle(
fontSize = 24.sp, fontSize = 22.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = Color.White, color = Color.White,
) )
val styleKm = TextStyle( val styleKm = TextStyle(
fontSize = 14.sp, fontSize = 12.sp,
color = Color.White, color = Color.White,
) )
val textLayoutSpeed = remember(speed, maxSpeed) { val textLayoutSpeed = remember(speed, maxSpeed) {
@@ -584,7 +464,6 @@ private fun MaxSpeed(
width: Int, width: Int,
height: Int, height: Int,
maxSpeed: Int, maxSpeed: Int,
curSpeed: Float,
) { ) {
val radius = 24 val radius = 24
Box( Box(
@@ -605,11 +484,6 @@ private fun MaxSpeed(
val textLayoutSpeed = remember(speed) { val textLayoutSpeed = remember(speed) {
textMeasurerSpeed.measure(speed, styleSpeed) textMeasurerSpeed.measure(speed, styleSpeed)
} }
val signColor = if (curSpeed * 3.6 > (maxSpeed + 3)) {
Color.Red
} else {
Color.Green
}
Canvas(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle( drawCircle(
center = Offset( center = Offset(
@@ -617,7 +491,7 @@ private fun MaxSpeed(
y = center.y y = center.y
), ),
radius = radius * 1.3.toFloat(), radius = radius * 1.3.toFloat(),
color = signColor, color = Color.Red,
) )
drawCircle( drawCircle(
center = Offset( center = Offset(
@@ -705,7 +579,7 @@ fun Puck(cameraState: CameraState, location: Location) {
locationState = location, locationState = location,
cameraState = cameraState, cameraState = cameraState,
accuracyThreshold = 10f, accuracyThreshold = 10f,
oldLocationThreshold = 1.seconds, oldLocationThreshold = 2.seconds,
showBearing = false, showBearing = false,
sizes = LocationPuckSizes(dotRadius = 10.dp), sizes = LocationPuckSizes(dotRadius = 10.dp),
colors = LocationPuckColors( colors = LocationPuckColors(
@@ -0,0 +1,349 @@
package com.kouros.navigation.car.navigation
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.location.Location
import android.location.LocationManager
import android.os.Binder
import android.os.IBinder
import android.text.TextUtils
import android.util.Log
import androidx.annotation.RequiresPermission
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.model.Distance.UNIT_METERS
import androidx.car.app.navigation.NavigationManager
import androidx.car.app.navigation.NavigationManagerCallback
import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import com.kouros.navigation.car.DeviceLocationManagerService
import com.kouros.navigation.car.screen.NavigationType
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Place
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlin.collections.copy
import kotlin.compareTo
class NavigationService : Service() {
val TAG: String = "NavigationService"
val DEEP_LINK_ACTION: String = ("com.kouros.navigation.car.navigation"
+ ".NavigationDeepLinkAction")
val channelId: String = "NavigationServiceChannel"
/** The identifier for the navigation notification displayed for the foreground service. */
val NAV_NOTIFICATION_ID: Int = 87356325
/** The identifier for the non-navigation notifications, such as a traffic accident warning. */
val NOTIFICATION_ID: Int = 71653346
// Constants for location broadcast
val PACKAGE_NAME: String =
"androidx.car.app.sample.navigation.common.nav.navigationservice"
val EXTRA_STARTED_FROM_NOTIFICATION: String = PACKAGE_NAME + ".started_from_notification"
val CANCEL_ACTION: String = "CANCEL"
private var notificationManager: NotificationManager? = null
private var carContext: CarContext? = null
var autoDriveEnabled = false
val simulation = Simulation()
private lateinit var listener: Listener
// Model for managing route state and navigation logic for Android Auto
var routeModel = RouteCarModel()
// Manages device GPS location updates
lateinit var deviceLocationManager: DeviceLocationManagerService
var currentLocation = location(0.0, 0.0)
lateinit var navigationViewModel: NavigationViewModel
private lateinit var navigationManager: NavigationManager
private var navigationManagerInitialized = false
var binder: IBinder = LocalBinder()
/** A listener for the navigation state changes. */
interface Listener {
/** Callback called when the navigation state changes. */
fun navigationStateChanged(
isNavigating: Boolean,
isRerouting: Boolean,
hasArrived: Boolean,
destinations: MutableList<Destination>,
steps: MutableList<Step>,
destinationTravelEstimate: TravelEstimate,
stepTravelEstimate: TravelEstimate,
stepRemainingDistance: Distance,
shouldShowNextStep: Boolean,
shouldShowLanes: Boolean,
junctionImage: CarIcon?,
backGroundColor: CarColor
)
fun updateServiceLocation(location: Location)
}
/**
* Class used for the client Binder. Since this service runs in the same process as its clients,
* we don't need to deal with IPC.
*/
inner class LocalBinder : Binder() {
val service: NavigationService
get() = this@NavigationService
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
override fun onCreate() {
Log.i(TAG, "In onCreate()");
createNotificationChannel();
}
override fun onBind(p0: Intent?): IBinder {
Log.d(TAG, "in onBind")
return binder
}
override fun onUnbind(intent: Intent): Boolean {
Log.d(TAG, "in UnBind")
if (!routeModel.isNavigating()) {
Log.d(TAG, "Stopping location updates")
if (::deviceLocationManager.isInitialized) {
deviceLocationManager.stopLocationUpdates()
}
}
return true
}
override fun onDestroy() {
if (::deviceLocationManager.isInitialized) {
deviceLocationManager.stopLocationUpdates()
}
Log.i(TAG, "In onDestroy()");
}
private fun createNotificationChannel() {
val serviceChannel = NotificationChannel(
"CHANNEL_ID",
"Location Service Channel",
NotificationManager.IMPORTANCE_HIGH
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
}
/** Sets the [CarContext] to use while the service is connected. */
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
fun setCarContext(
carContext: CarContext,
listener: Listener
) {
Log.d(TAG, "in setCarContext")
this.carContext = carContext
navigationViewModel = getViewModel(carContext)
this.listener = listener
deviceLocationManager = DeviceLocationManagerService(
carContext = carContext,
onLocationUpdate = ::updateLocation,
onInitialLocation = { location ->
updateLocation(location)
}
)
deviceLocationManager.startLocationUpdates()
navigationManagerInitialized = true
navigationManager =
carContext.getCarService(NavigationManager::class.java)
navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
override fun onAutoDriveEnabled() {
Log.d(TAG, "onAutoDriveEnabled")
// Called when the app should simulate navigation (e.g., for testing)
deviceLocationManager.stopLocationUpdates()
autoDriveEnabled = true
simulation()
CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
.show()
}
private fun simulation() {
simulation.gpxSimulation {
listener.updateServiceLocation(it)
}
}
override fun onStopNavigation() {
// Called when the user stops navigation in the car screen
// Stop turn-by-turn logic and clean up
stopNavigation()
if (autoDriveEnabled) {
deviceLocationManager.startLocationUpdates()
}
}
})
// Uncomment if navigating
// mNavigationManager.navigationStarted();
}
/** Clears the currently used {@link CarContext}. */
fun clearCarContext() {
Log.i(TAG, "clearContext");
carContext = null;
navigationManager.clearNavigationManagerCallback();
}
/** Starts navigation. */
fun startNavigation(route: String, destination: Place) {
Log.i(TAG, "Starting Navigation")
startService(Intent(applicationContext, NavigationService::class.java))
routeModel.navState = routeModel.navState.copy(destination = destination)
routeModel.navState = routeModel.navState.copy(routingEngine = 2)
routeModel.startNavigation(route)
if (routeModel.isNavigating()) {
routeModel.updateLocation(currentLocation, navigationViewModel)
listener.navigationStateChanged(
isNavigating = true,
isRerouting = false,
hasArrived = false,
destinations = mutableListOf(routeModel.getDestination()),
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext!!),
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext!!),
steps = routeModel.getSteps(carContext!!),
stepRemainingDistance = routeModel.getDistance(),
shouldShowNextStep = false,
shouldShowLanes = false,
junctionImage = null,
backGroundColor = routeModel.backGroundColor()
)
}
}
/** Starts navigation. */
fun stopNavigation() {
if (autoDriveEnabled) {
autoDriveEnabled = false
}
if (navigationManagerInitialized)
navigationManager.navigationEnded()
listener.navigationStateChanged(
isNavigating = false,
isRerouting = false,
hasArrived = false,
destinations = emptyList<Destination>().toMutableList(),
steps = emptyList<Step>().toMutableList(),
destinationTravelEstimate = routeModel.travelEstimate(carContext!!, 0.0, 0),
stepTravelEstimate = routeModel.travelEstimate(carContext!!, 0.0, 0),
stepRemainingDistance = Distance.create(0.0, UNIT_METERS),
shouldShowNextStep = false,
shouldShowLanes = false,
junctionImage = null,
backGroundColor = CarColor.BLUE
)
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
fun updateLocation(location: Location) {
Log.d(TAG, "updateLocation")
currentLocation = location
if (routeModel.isNavigating()) {
routeModel.updateLocation(location, navigationViewModel)
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
listener.updateServiceLocation(snappedLocation)
checkArrival()
updateNavigationScreen( 0)
} else {
listener.updateServiceLocation(location)
}
}
fun isNavigating(): Boolean {
return routeModel.isNavigating()
}
fun updateNavigationScreen(distanceMode: Int) {
if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty()
&& routeModel.navState.destination.street.isEmpty()
) {
return
}
listener.navigationStateChanged(
isNavigating = routeModel.isNavigating(),
isRerouting = false,
hasArrived = routeModel.isArrival(),
destinations = mutableListOf(routeModel.getDestination()),
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext!!),
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext!!),
steps = routeModel.getSteps(carContext!!),
stepRemainingDistance = routeModel.getDistance(),
shouldShowNextStep = false,
shouldShowLanes = false,
junctionImage = null,
backGroundColor = routeModel.backGroundColor()
)
/**
* Updates the trip information and notifies the listener with a new Trip object.
* This includes destination name, address, travel estimate, and loading status.
*/
val tripBuilder = Trip.Builder()
tripBuilder.addDestination(
routeModel.getDestination(),
routeModel.getTravelEstimateTrip(carContext!!)
)
tripBuilder.setLoading(false)
tripBuilder.setCurrentRoad(routeModel.getDestination().name.toString())
tripBuilder.addStep(routeModel.getSteps(carContext!!).first(), routeModel.getTravelEstimateStep(carContext!!))
navigationManager.updateTrip(tripBuilder.build())
}
/**
* Checks for arrival
*/
fun checkArrival() {
if (routeModel.isArrival()
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
stopNavigation()
routeModel.navState = routeModel.navState.copy(arrived = true)
}
}
}
@@ -24,11 +24,9 @@ import androidx.car.app.navigation.model.LaneDirection
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate import androidx.car.app.navigation.model.TravelEstimate
import androidx.car.app.navigation.model.Trip
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.screen.createCarIcon import com.kouros.navigation.car.screen.createCarIcon
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.route.ManeuverType import com.kouros.navigation.data.route.ManeuverType
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
@@ -126,9 +124,10 @@ class RouteCarModel : RouteModel() {
.setRemainingTimeColor(CarColor.GREEN) .setRemainingTimeColor(CarColor.GREEN)
.setRemainingDistanceColor(CarColor.BLUE) .setRemainingDistanceColor(CarColor.BLUE)
if (traffic > 0) { if (traffic > 0) {
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.traffic_jam_48px))
travelBuilder.setTripText(createDelay(traffic)) travelBuilder.setTripText(createDelay(traffic))
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.traffic_jam_48px))
} }
if (navState.travelMessage.isNotEmpty()) { if (navState.travelMessage.isNotEmpty()) {
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px)) travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px))
travelBuilder.setTripText(CarText.create(navState.travelMessage)) travelBuilder.setTripText(CarText.create(navState.travelMessage))
@@ -166,21 +165,6 @@ class RouteCarModel : RouteModel() {
.build() .build()
} }
/**
* Create a trip builder object
*/
fun getTrip(carContext: CarContext): Trip {
val tripBuilder = Trip.Builder()
tripBuilder.addDestination(
getDestination(),
getTravelEstimateTrip(carContext)
)
tripBuilder.setLoading(false)
tripBuilder.setCurrentRoad( getDestination().name.toString())
tripBuilder.addStep(getSteps(carContext).first(), getTravelEstimateStep(carContext))
return tripBuilder.build()
}
private fun createDelay(delay: Int): CarText { private fun createDelay(delay: Int): CarText {
val delayBuilder = SpannableStringBuilder() val delayBuilder = SpannableStringBuilder()
delayBuilder.append( delayBuilder.append(
@@ -266,7 +250,7 @@ class RouteCarModel : RouteModel() {
R.string.exit_action_title, R.string.exit_action_title, R.string.exit_action_title, R.string.exit_action_title,
FLAG_DEFAULT FLAG_DEFAULT
) )
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */4000) return Alert.Builder( /* alertId: */0, title, /* durationMillis: */5000)
.setSubtitle(subtitle) .setSubtitle(subtitle)
.setIcon(icon) .setIcon(icon)
.addAction(dismissAction).setCallback(object : AlertCallback { .addAction(dismissAction).setCallback(object : AlertCallback {
@@ -3,10 +3,11 @@ package com.kouros.navigation.car.navigation
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.os.SystemClock import android.os.SystemClock
import android.util.Log
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import com.kouros.data.BuildConfig import com.kouros.data.BuildConfig
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.utils.location
import io.ticofab.androidgpxparser.parser.GPXParser import io.ticofab.androidgpxparser.parser.GPXParser
import io.ticofab.androidgpxparser.parser.domain.Gpx import io.ticofab.androidgpxparser.parser.domain.Gpx
import io.ticofab.androidgpxparser.parser.domain.TrackSegment import io.ticofab.androidgpxparser.parser.domain.TrackSegment
@@ -28,8 +29,9 @@ class Simulation {
) { ) {
if (routeModel.navState.route.isRouteValid()) { if (routeModel.navState.route.isRouteValid()) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
//gpxSimulation(routeModel, lifecycleScope, updateLocation) gpxSimulation(routeModel, lifecycleScope, updateLocation)
currentSimulation(routeModel, lifecycleScope, updateLocation) //gpxSimulation(updateLocation)
//currentSimulation(routeModel, lifecycleScope, updateLocation)
} else { } else {
currentSimulation(routeModel, lifecycleScope, updateLocation) currentSimulation(routeModel, lifecycleScope, updateLocation)
} }
@@ -46,20 +48,20 @@ class Simulation {
if (points.isEmpty()) return if (points.isEmpty()) return
simulationJob?.cancel() simulationJob?.cancel()
var lastLocation = Location(LocationManager.FUSED_PROVIDER) var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing: Float var curBearing = 0f
simulationJob = lifecycleScope.launch { simulationJob = lifecycleScope.launch {
for ((index, point) in points.withIndex()) { for ((index, point) in points.withIndex()) {
if (index >= 0) { if (index >= 0) {
curBearing = lastLocation.bearingTo(location(point[0], point[1]))
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply { val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = point[1] latitude = point[1]
longitude = point[0] longitude = point[0]
bearing = curBearing bearing = curBearing
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = 10.0f speed = 5.0f
time = System.currentTimeMillis() time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
} }
curBearing = lastLocation.bearingTo(fakeLocation)
// Update your app's state as if a real GPS update occurred // Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation) updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second) // Wait before moving to the next point (e.g., every 1 second)
@@ -81,7 +83,7 @@ class Simulation {
runBlocking { runBlocking {
simulationJob = launch(Dispatchers.IO) { simulationJob = launch(Dispatchers.IO) {
route = TomTomRepository().fetchUrl( route = TomTomRepository().fetchUrl(
"https://kouros-online.de/VH.gpx", "https://kouros-online.de/vh.gpx",
false false
) )
} }
@@ -90,7 +92,7 @@ class Simulation {
simulationJob?.cancel() simulationJob?.cancel()
simulationJob = lifecycleScope.launch() { simulationJob = lifecycleScope.launch() {
var lastLocation = Location(LocationManager.FUSED_PROVIDER) var lastLocation = Location(LocationManager.FUSED_PROVIDER)
val curBearing = 0f var curBearing = 0f
val parser = GPXParser() val parser = GPXParser()
val parsedGpx: Gpx? = val parsedGpx: Gpx? =
parser.parse(route.byteInputStream()) parser.parse(route.byteInputStream())
@@ -118,7 +120,10 @@ class Simulation {
// Update your app's state as if a real GPS update occurred // Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation) updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second) // Wait before moving to the next point (e.g., every 1 second)
delay(200) if (duration > 100) {
// delay(duration / 4)
}
delay(500)
lastTime = p.time lastTime = p.time
lastLocation = fakeLocation lastLocation = fakeLocation
} }
@@ -132,4 +137,62 @@ class Simulation {
fun stopSimulation() { fun stopSimulation() {
simulationJob?.cancel() simulationJob?.cancel()
} }
fun gpxSimulation(
updateLocation: (Location) -> Unit
) {
Runnable {
var route = ""
simulationJob?.cancel()
runBlocking {
simulationJob = launch(Dispatchers.IO) {
route = TomTomRepository().fetchUrl(
"https://kouros-online.de/vh.gpx",
false
)
}
simulationJob?.join()
}
simulationJob?.cancel()
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f
val parser = GPXParser()
val parsedGpx: Gpx? =
parser.parse(route.byteInputStream())
parsedGpx?.let {
val tracks = parsedGpx.tracks
tracks.forEach { tr ->
val segments: MutableList<TrackSegment?>? = tr.trackSegments
segments!!.forEach { seg ->
var lastTime = DateTime.now()
seg!!.trackPoints.forEach { p ->
val ext = p.extensions
var curSpeed = 0F
if (ext != null) {
curSpeed = ext.speed.toFloat()
}
val duration = p.time.millis - lastTime.millis
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = p.latitude
longitude = p.longitude
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = curSpeed
time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
}
// Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second)
if (duration > 100) {
// delay(duration / 4)
}
Thread.sleep(2000)
lastTime = p.time
lastLocation = fakeLocation
}
}
}
}
}.run()
}
} }
@@ -1,481 +0,0 @@
package com.kouros.navigation.car.screen
import android.util.Log
import androidx.annotation.GuardedBy
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.common.CarValue
import androidx.car.app.hardware.common.OnCarDataAvailableListener
import androidx.car.app.hardware.info.CarHardwareLocation
import androidx.car.app.hardware.info.CarSensors
import androidx.car.app.hardware.info.Compass
import androidx.car.app.hardware.info.EnergyProfile
import androidx.car.app.hardware.info.ExteriorDimensions
import androidx.car.app.hardware.info.Model
import androidx.car.app.hardware.info.Speed
import androidx.car.app.model.Action
import androidx.car.app.model.Header
import androidx.car.app.model.Pane
import androidx.car.app.model.PaneTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.core.content.ContextCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.kouros.data.R
import java.util.concurrent.Executor
/**
* Creates a screen that show the static information (such as model and energy profile) available
* via CarHardware interfaces.
*/
class CarHardwareInfoScreen(carContext: CarContext) : Screen(carContext) {
// Package private for inner class reference
var mHasLocationPermission: Boolean = false
var mHasCompassPermission: Boolean = false
var mHasSpeedPermission: Boolean = false
var mHasModelPermission: Boolean = false
var mHasEnergyProfilePermission: Boolean = false
var mHasExteriorDimensionsPermission: Boolean = false
val mCarHardwareExecutor: Executor = ContextCompat.getMainExecutor(getCarContext())
@GuardedBy("this")
var mLocation: CarHardwareLocation? = null
@GuardedBy("this")
var mCompass: Compass? = null
@GuardedBy("this")
var mSpeed: Speed? = null
/**
* Value fetched from CarHardwareManager containing model information.
*
*
* It is requested asynchronously and can be `null` until the response is
* received.
*/
@GuardedBy("this")
var mModel: Model? = null
/**
* Value fetched from CarHardwareManager containing what type of fuel/ports the car has.
*
*
* It is requested asynchronously and can be `null` until the response is
* received.
*/
@GuardedBy("this")
var mEnergyProfile: EnergyProfile? = null
@GuardedBy("this")
var mExteriorDimensions: ExteriorDimensions? = null
val carLocationListener: OnCarDataAvailableListener<CarHardwareLocation?> =
OnCarDataAvailableListener { data ->
synchronized(this) {
Log.i(TAG, "Received locaction: " + data)
mLocation = data
invalidate()
}
}
val mCompassListener: OnCarDataAvailableListener<Compass?> =
OnCarDataAvailableListener { data: Compass? ->
synchronized(this) {
Log.i(TAG, "Received compass: " + data)
mCompass = data
invalidate()
}
}
var mModelListener: OnCarDataAvailableListener<Model?> =
OnCarDataAvailableListener { data: Model? ->
synchronized(this) {
Log.i(TAG, "Received model information: " + data)
mModel = data
invalidate()
}
}
var mSpeedListener: OnCarDataAvailableListener<Speed?> =
OnCarDataAvailableListener { data: Speed? ->
synchronized(this) {
Log.i(TAG, "Received speed information: " + data)
mSpeed = data
invalidate()
}
}
var mEnergyProfileListener: OnCarDataAvailableListener<EnergyProfile?> =
OnCarDataAvailableListener { data: EnergyProfile? ->
synchronized(this) {
Log.i(TAG, "Received energy profile information: " + data)
mEnergyProfile = data
invalidate()
}
}
var mExteriorDimensionsListener: OnCarDataAvailableListener<ExteriorDimensions?> =
OnCarDataAvailableListener { data: ExteriorDimensions? ->
synchronized(this) {
Log.i(TAG, "Received exterior dimensions: " + data)
mExteriorDimensions = data
invalidate()
}
}
init {
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
val carHardwareManager =
getCarContext().getCarService(CarHardwareManager::class.java)
val carInfo = carHardwareManager.carInfo
val carSensors = carHardwareManager.carSensors
// Request any single shot values.
synchronized(this@CarHardwareInfoScreen) {
mLocation = null
try {
carSensors.addCarHardwareLocationListener(
CarSensors.UPDATE_RATE_NORMAL, mCarHardwareExecutor,
carLocationListener
)
mHasLocationPermission = true
} catch (_: SecurityException) {
mHasLocationPermission = false
}
mCompass = null
try {
carSensors.addCompassListener(
CarSensors.UPDATE_RATE_NORMAL, mCarHardwareExecutor,
mCompassListener
)
mHasCompassPermission = true
} catch (_: SecurityException) {
mHasCompassPermission = false
}
mSpeed = null
try {
carInfo.addSpeedListener(mCarHardwareExecutor, mSpeedListener)
mHasSpeedPermission = true
} catch (_: SecurityException) {
mHasSpeedPermission = false
}
mModel = null
try {
carInfo.fetchModel(mCarHardwareExecutor, mModelListener)
mHasModelPermission = true
} catch (_: SecurityException) {
mHasModelPermission = false
}
mEnergyProfile = null
try {
carInfo.fetchEnergyProfile(mCarHardwareExecutor, mEnergyProfileListener)
mHasEnergyProfilePermission = true
} catch (_: SecurityException) {
mHasEnergyProfilePermission = false
}
mExteriorDimensions = null
try {
carInfo.fetchExteriorDimensions(
mCarHardwareExecutor,
mExteriorDimensionsListener
)
mHasExteriorDimensionsPermission = true
} catch (_: SecurityException) {
mHasExteriorDimensionsPermission = false
}
}
}
})
}
override fun onGetTemplate(): Template {
val paneBuilder = Pane.Builder()
if (allInfoAvailable()) {
val carInfoRowBuilder = Row.Builder()
.setTitle(getCarContext().getString(R.string.car_sensors))
if (!mHasCompassPermission) {
carInfoRowBuilder.addText(getCarContext().getString(R.string.no_model_permission))
} else {
val info = StringBuilder()
synchronized(this@CarHardwareInfoScreen) {
if (mCompass != null) {
if (mCompass!!.orientations.status == CarValue.STATUS_SUCCESS) {
info.append(mCompass!!.orientations)
}
if (mCompass!!.orientations
.status == CarValue.STATUS_UNAVAILABLE
) {
info.append("Compass unavailable")
}
if (mCompass!!.orientations
.status == CarValue.STATUS_UNIMPLEMENTED
) {
info.append("Compass unimplemented")
}
}
}
carInfoRowBuilder.addText(info)
}
if (!mHasLocationPermission) {
carInfoRowBuilder.addText(carContext.getString(R.string.no_model_permission))
} else {
val info = StringBuilder()
synchronized(this@CarHardwareInfoScreen) {
if (mLocation != null) {
if (mLocation!!.location.status == CarValue.STATUS_SUCCESS) {
info.append(mLocation!!.location)
}
if (mLocation!!.location.status
== CarValue.STATUS_UNAVAILABLE
) {
info.append("Location unavailable")
}
if (mLocation!!.location
.status == CarValue.STATUS_UNIMPLEMENTED
) {
info.append("Location unimplemented")
}
}
}
carInfoRowBuilder.addText(info)
}
paneBuilder.addRow(carInfoRowBuilder.build())
val modelRowBuilder = Row.Builder()
.setTitle(getCarContext().getString(R.string.model_info))
if (!mHasModelPermission) {
modelRowBuilder.addText(getCarContext().getString(R.string.no_model_permission))
} else {
val info = StringBuilder()
synchronized(this@CarHardwareInfoScreen) {
checkNotNull(mModel)
if (mModel!!.manufacturer.status != CarValue.STATUS_SUCCESS) {
info.append(getCarContext().getString(R.string.manufacturer_unavailable))
info.append(", ")
} else {
info.append(mModel!!.manufacturer.getValue())
info.append(", ")
}
if (mModel!!.name.status != CarValue.STATUS_SUCCESS) {
info.append(getCarContext().getString(R.string.model_unavailable))
info.append(", ")
} else {
info.append(mModel!!.name.getValue())
info.append(", ")
}
if (mModel!!.year.status != CarValue.STATUS_SUCCESS) {
info.append(getCarContext().getString(R.string.year_unavailable))
} else {
info.append(mModel!!.year.getValue())
}
}
modelRowBuilder.addText(info)
}
paneBuilder.addRow(modelRowBuilder.build())
val speedRowBuilder = Row.Builder()
.setTitle(carContext.getString(R.string.speed))
if (!mHasSpeedPermission) {
speedRowBuilder.addText(carContext.getString(R.string.no_speed_permission))
} else {
if (mSpeed != null) {
val info = StringBuilder()
synchronized(this@CarHardwareInfoScreen) {
if (mSpeed!!.displaySpeedMetersPerSecond.status != CarValue.STATUS_SUCCESS) {
info.append(getCarContext().getString(R.string.manufacturer_unavailable))
info.append(", ")
} else {
info.append(mSpeed!!.displaySpeedMetersPerSecond.value)
info.append(", ")
}
}
speedRowBuilder.addText(info)
} else {
val info = StringBuilder()
info.append(carContext.getString(R.string.speed_unavailable))
speedRowBuilder.addText(info)
}
}
paneBuilder.addRow(speedRowBuilder.build())
val energyProfileRowBuilder = Row.Builder()
.setTitle(getCarContext().getString(R.string.energy_profile))
if (!mHasEnergyProfilePermission) {
energyProfileRowBuilder.addText(
getCarContext()
.getString(R.string.no_energy_profile_permission)
)
} else {
val fuelInfo = StringBuilder()
synchronized(this) {
if (mEnergyProfile!!.fuelTypes.status != CarValue.STATUS_SUCCESS) {
fuelInfo.append(getCarContext().getString(R.string.fuel_types))
fuelInfo.append(": ")
fuelInfo.append(getCarContext().getString(R.string.unavailable))
} else {
fuelInfo.append(getCarContext().getString(R.string.fuel_types))
fuelInfo.append(": ")
for (fuelType in mEnergyProfile!!.fuelTypes.getValue()!!) {
fuelInfo.append(fuelTypeAsString(fuelType))
fuelInfo.append(" ")
}
}
energyProfileRowBuilder.addText(fuelInfo)
val evInfo = StringBuilder()
if (mEnergyProfile!!.evConnectorTypes.status
!= CarValue.STATUS_SUCCESS
) {
evInfo.append(" ")
evInfo.append(getCarContext().getString(R.string.ev_connector_types))
evInfo.append(": ")
evInfo.append(getCarContext().getString(R.string.unavailable))
} else {
evInfo.append(getCarContext().getString(R.string.ev_connector_types))
evInfo.append(": ")
for (connectorType in mEnergyProfile!!.evConnectorTypes.getValue()!!) {
evInfo.append(evConnectorAsString(connectorType))
evInfo.append(" ")
}
}
energyProfileRowBuilder.addText(evInfo)
}
}
paneBuilder.addRow(energyProfileRowBuilder.build())
synchronized(this) {
paneBuilder.addRow(
buildExteriorDimensionsRow(
mExteriorDimensions,
mHasExteriorDimensionsPermission
)
)
}
} else {
paneBuilder.setLoading(true)
}
return PaneTemplate.Builder(paneBuilder.build())
.setHeader(
Header.Builder()
.setStartHeaderAction(Action.BACK)
.setTitle(getCarContext().getString(R.string.car_hardware_info))
.build()
)
.build()
}
private fun allInfoAvailable(): Boolean {
synchronized(this) {
if (mHasModelPermission && mModel == null) {
return false
}
if (mHasEnergyProfilePermission && mEnergyProfile == null) {
return false
}
if (mHasExteriorDimensionsPermission && mExteriorDimensions == null) {
return false
}
}
return true
}
companion object {
private const val TAG = "showcase"
private fun fuelTypeAsString(fuelType: Int): String {
when (fuelType) {
EnergyProfile.FUEL_TYPE_UNLEADED -> return "UNLEADED"
EnergyProfile.FUEL_TYPE_LEADED -> return "LEADED"
EnergyProfile.FUEL_TYPE_DIESEL_1 -> return "DIESEL_1"
EnergyProfile.FUEL_TYPE_DIESEL_2 -> return "DIESEL_2"
EnergyProfile.FUEL_TYPE_BIODIESEL -> return "BIODIESEL"
EnergyProfile.FUEL_TYPE_E85 -> return "E85"
EnergyProfile.FUEL_TYPE_LPG -> return "LPG"
EnergyProfile.FUEL_TYPE_CNG -> return "CNG"
EnergyProfile.FUEL_TYPE_LNG -> return "LNG"
EnergyProfile.FUEL_TYPE_ELECTRIC -> return "ELECTRIC"
EnergyProfile.FUEL_TYPE_HYDROGEN -> return "HYDROGEN"
EnergyProfile.FUEL_TYPE_OTHER -> return "OTHER"
EnergyProfile.FUEL_TYPE_UNKNOWN -> return "UNKNOWN"
else -> return "UNKNOWN"
}
}
private fun evConnectorAsString(evConnectorType: Int): String {
when (evConnectorType) {
EnergyProfile.EVCONNECTOR_TYPE_J1772 -> return "J1772"
EnergyProfile.EVCONNECTOR_TYPE_MENNEKES -> return "MENNEKES"
EnergyProfile.EVCONNECTOR_TYPE_CHADEMO -> return "CHADEMO"
EnergyProfile.EVCONNECTOR_TYPE_COMBO_1 -> return "COMBO_1"
EnergyProfile.EVCONNECTOR_TYPE_COMBO_2 -> return "COMBO_2"
EnergyProfile.EVCONNECTOR_TYPE_TESLA_ROADSTER -> return "TESLA_ROADSTER"
EnergyProfile.EVCONNECTOR_TYPE_TESLA_HPWC -> return "TESLA_HPWC"
EnergyProfile.EVCONNECTOR_TYPE_TESLA_SUPERCHARGER -> return "TESLA_SUPERCHARGER"
EnergyProfile.EVCONNECTOR_TYPE_GBT -> return "GBT"
EnergyProfile.EVCONNECTOR_TYPE_GBT_DC -> return "GBT_DC"
EnergyProfile.EVCONNECTOR_TYPE_SCAME -> return "SCAME"
EnergyProfile.EVCONNECTOR_TYPE_OTHER -> return "OTHER"
EnergyProfile.EVCONNECTOR_TYPE_UNKNOWN -> return "UNKNOWN"
else -> return "UNKNOWN"
}
}
private fun buildExteriorDimensionsRow(
exteriorDimensions: ExteriorDimensions?, hasPermissions: Boolean
): Row {
val builder = Row.Builder().setTitle("Exterior dimensions")
if (!hasPermissions) {
builder.addText("Permissions not granted. This vehicle property requires CAR_INFO")
return builder.build()
}
if (exteriorDimensions == null) {
builder.addText("Pending callback from vehicle fetch request")
return builder.build()
}
val carValue = exteriorDimensions.getExteriorDimensions()
if (carValue.getStatus() != CarValue.STATUS_SUCCESS) {
builder.addText("Fetch failed because the vehicle hasn't implemented this field")
return builder.build()
}
val dimensionsArray = carValue.getValue()
if (dimensionsArray == null || dimensionsArray.size != 8) {
builder.addText(
"Fetch succeeded, but the reply was not an int array of length 8: "
+ dimensionsArray.contentToString()
)
return builder.build()
}
builder.addText(
("Height: " + dimensionsArray[ExteriorDimensions.HEIGHT_INDEX]
+ ", Length: " + dimensionsArray[ExteriorDimensions.LENGTH_INDEX]
+ ", Width: " + dimensionsArray[ExteriorDimensions.WIDTH_INDEX]
+ ", Width + mirrors: "
+ dimensionsArray[ExteriorDimensions.WIDTH_INCLUDING_MIRRORS_INDEX])
)
builder.addText(
("Wheel base: " + dimensionsArray[ExteriorDimensions.WHEEL_BASE_INDEX]
+ ", Front width: " + dimensionsArray[ExteriorDimensions.TRACK_WIDTH_FRONT_INDEX]
+ ", Rear width: " + dimensionsArray[ExteriorDimensions.TRACK_WIDTH_REAR_INDEX]
+ ", Turning radius: "
+ dimensionsArray[ExteriorDimensions.CURB_TO_CURB_TURNING_RADIUS_INDEX])
)
return builder.build()
}
}
}
@@ -3,23 +3,19 @@ package com.kouros.navigation.car.screen
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.GridItem
import androidx.car.app.model.GridTemplate
import androidx.car.app.model.Header import androidx.car.app.model.Header
import androidx.car.app.model.ItemList import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template import androidx.car.app.model.Template
import androidx.compose.ui.graphics.Color
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.toColorInt
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.data.Category import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants.CHARGING_STATION import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.data.Constants.RESTAURANT
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
@@ -34,18 +30,22 @@ class CategoriesScreen(
var categories: List<Category> = listOf( var categories: List<Category> = listOf(
Category(id = FUEL_STATION, name = carContext.getString(R.string.fuel_station)), Category(id = FUEL_STATION, name = carContext.getString(R.string.fuel_station)),
Category(id = PHARMACY, name = carContext.getString(R.string.pharmacy)), Category(id = PHARMACY, name = carContext.getString(R.string.pharmacy)),
Category(id = CHARGING_STATION, name = carContext.getString(R.string.charging_station)), Category(id = CHARGING_STATION, name = carContext.getString(R.string.charging_station))
Category(id = RESTAURANT, name = carContext.getString(R.string.restaurant))
) )
init {
}
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val itemListBuilder = ItemList.Builder() val itemListBuilder = ItemList.Builder()
.setNoItemsMessage("No categories to show") .setNoItemsMessage("No categories to show")
categories.forEach { categories.forEach {
itemListBuilder.addItem( itemListBuilder.addItem(
GridItem.Builder() Row.Builder()
.setImage(carIcon(carContext, it.id, -1))
.setTitle(it.name) .setTitle(it.name)
.setImage(carIcon(carContext, it.id, -1))
.setOnClickListener { .setOnClickListener {
category = it.id category = it.id
screenManager screenManager
@@ -63,68 +63,34 @@ class CategoriesScreen(
} }
} }
} }
.setBrowsable(true)
.build() .build()
) )
} }
surfaceRenderer.viewStyle = ViewStyle.AMENITY_VIEW surfaceRenderer.viewStyle = ViewStyle.AMENITY_VIEW
return GridTemplate.Builder() val header = Header.Builder()
.setHeader(
Header.Builder()
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.category_title)) .setTitle(carContext.getString(R.string.category_title))
.build() .build()
)
return ListTemplate.Builder()
.setHeader(header)
.setSingleList(itemListBuilder.build()) .setSingleList(itemListBuilder.build())
.build() .build()
} }
} }
fun carIcon(context: CarContext, category: String, index: Int): CarIcon { fun carIcon(context: CarContext, category: String, index: Int): CarIcon {
val customCarColor =
CarColor.createCustom(android.graphics.Color.MAGENTA, android.graphics.Color.MAGENTA)
if (index == -1) { if (index == -1) {
val icon = when (category) { val resId = when (category) {
CHARGING_STATION -> CarIcon.Builder( CHARGING_STATION -> R.drawable.ev_station_24px
IconCompat.createWithResource( FUEL_STATION -> R.drawable.local_gas_station_24
context, PHARMACY -> R.drawable.local_pharmacy_24px
R.drawable.ev_station_24px else -> R.drawable.ic_place_white_24dp
)
).setTint(CarColor.GREEN)
FUEL_STATION -> CarIcon.Builder(
IconCompat.createWithResource(
context,
R.drawable.local_gas_station_24
)
).setTint(CarColor.BLUE)
PHARMACY -> CarIcon.Builder(
IconCompat.createWithResource(
context,
R.drawable.local_pharmacy_24px
)
).setTint(CarColor.RED)
RESTAURANT -> CarIcon.Builder(
IconCompat.createWithResource(
context,
R.drawable.restaurant_24px
)
).setTint(customCarColor)
else -> CarIcon.Builder(
IconCompat.createWithResource(
context,
R.drawable.traffic_jam_48px
)
).setTint(CarColor.YELLOW)
} }
return icon.build() return CarIcon.Builder(IconCompat.createWithResource(context, resId)).build()
} else { } else {
return CarIcon.Builder( return CarIcon.Builder(
createNumberIcon( createNumberIcon(
@@ -1,6 +1,5 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import android.util.Log
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
@@ -20,9 +19,6 @@ import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.versioning.CarAppApiLevels import androidx.car.app.versioning.CarAppApiLevels
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.screen.observers.CategoryObserver import com.kouros.navigation.car.screen.observers.CategoryObserver
@@ -31,18 +27,12 @@ import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.CHARGING_STATION import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.data.Constants.RESTAURANT
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.GeoUtils.createPointCollection import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import com.kouros.navigation.utils.round import com.kouros.navigation.utils.round
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlin.math.min import kotlin.math.min
class CategoryScreen( class CategoryScreen(
@@ -53,14 +43,9 @@ class CategoryScreen(
) : Screen(carContext), CategoryObserverCallback { ) : Screen(carContext), CategoryObserverCallback {
val repository = getSettingsRepository(carContext)
val settingsViewModel = getSettingsViewModel(carContext)
val maxListItems: Int = 30 val maxListItems: Int = 30
var elements: List<Elements> = emptyList() var elements: List<Elements> = emptyList()
private val categoryObserver = CategoryObserver(this) private val categoryObserver = CategoryObserver(this)
private var loading = true private var loading = true
@@ -71,12 +56,8 @@ class CategoryScreen(
navigationViewModel.elements.value = emptyList() navigationViewModel.elements.value = emptyList()
} }
}) })
repository.lastFuelPricesFlow.asLiveData().observe(this, Observer {
navigationViewModel.getAmenities(carContext, category, surfaceRenderer.lastLocation, it)
})
navigationViewModel.elements.observe(this, categoryObserver) navigationViewModel.elements.observe(this, categoryObserver)
navigationViewModel.getAmenities(category, surfaceRenderer.lastLocation)
} }
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
@@ -93,6 +74,7 @@ class CategoryScreen(
) )
) )
elements.forEach { elements.forEach {
if (it.tags.operator != null) {
if (index++ < listLimit) { if (index++ < listLimit) {
listBuilder.addItem( listBuilder.addItem(
createItem(it, category, index) createItem(it, category, index)
@@ -100,6 +82,7 @@ class CategoryScreen(
} }
} }
} }
}
val header = Header.Builder() val header = Header.Builder()
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)
@@ -130,19 +113,18 @@ class CategoryScreen(
CHARGING_STATION -> R.string.charging_station CHARGING_STATION -> R.string.charging_station
FUEL_STATION -> R.string.fuel_station FUEL_STATION -> R.string.fuel_station
PHARMACY -> R.string.pharmacy PHARMACY -> R.string.pharmacy
else -> R.string.restaurant else -> R.string.no_places
} }
return carContext.getString(resId) return carContext.getString(resId)
} }
private fun createItem(it: Elements, category: String, index: Int): Row { private fun createItem(it: Elements, category: String, index: Int): Row {
var name = "" var name = ""
name = it.tags.name if (it.tags.name != null) {
if (name.isEmpty()) { name = it.tags.name.toString()
name = it.tags.operator
} }
if (name.isEmpty()) { if (name.isEmpty()) {
name = "Empty" name = it.tags.operator.toString()
} }
val row = Row.Builder() val row = Row.Builder()
.setOnClickListener { .setOnClickListener {
@@ -151,17 +133,17 @@ class CategoryScreen(
} }
.setTitle(name) .setTitle(name)
.setImage(carIcon(carContext, category, index)) .setImage(carIcon(carContext, category, index))
when (category) {
CHARGING_STATION -> row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}")
FUEL_STATION -> row.addText(carText("${it.e5} / ${it.e10}${it.tags.openingHours}"))
RESTAURANT, PHARMACY ->row.addText(carText(it.tags.openingHours))
}
if (it.distance < 1000) { if (it.distance < 1000) {
row.addText("${(it.distance).toInt()} m") row.addText("${(it.distance).toInt()} m")
} else { } else {
row.addText("${(it.distance / 1000).round(1)} km") row.addText("${(it.distance / 1000).round(1)} km")
} }
if (category == CHARGING_STATION) {
if (it.tags.socketType2 != null)
row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}")
} else {
row.addText(carText("${it.tags.openingHours}"))
}
row.addAction( row.addAction(
createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT, { createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT, {
navigationViewModel.loadRoute( navigationViewModel.loadRoute(
@@ -2,7 +2,9 @@ package com.kouros.navigation.car.screen
import android.os.CountDownTimer import android.os.CountDownTimer
import android.os.Handler import android.os.Handler
import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
@@ -19,6 +21,7 @@ import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.navigation.model.MessageInfo import androidx.car.app.navigation.model.MessageInfo
import androidx.car.app.navigation.model.NavigationTemplate import androidx.car.app.navigation.model.NavigationTemplate
import androidx.car.app.navigation.model.PanModeListener
import androidx.car.app.navigation.model.RoutingInfo import androidx.car.app.navigation.model.RoutingInfo
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate import androidx.car.app.navigation.model.TravelEstimate
@@ -32,6 +35,7 @@ import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.screen.settings.SettingsScreen import com.kouros.navigation.car.screen.settings.SettingsScreen
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
@@ -51,9 +55,7 @@ open class NavigationScreen(
private val navigationViewModel: NavigationViewModel private val navigationViewModel: NavigationViewModel
) : Screen(carContext) { ) : Screen(carContext) {
var deviation = 0F
var recentPlaces = mutableListOf<Place>() var recentPlaces = mutableListOf<Place>()
var recentPlace: Place = Place() var recentPlace: Place = Place()
var navigationType = NavigationType.VIEW var navigationType = NavigationType.VIEW
@@ -80,10 +82,9 @@ open class NavigationScreen(
private var junctionImage: CarIcon? = null private var junctionImage: CarIcon? = null
private var backGroundColor = CarColor.BLUE private var backGroundColor = CarColor.BLUE
private var message = ""
private var showAlternativeRoute = false private var showAlternativeRoute = false
val observerRecentPlaces = Observer<List<Place>> { newPlaces -> val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
Log.d(TAG, "NavigationScreen 4")
recentPlaces.addAll(newPlaces) recentPlaces.addAll(newPlaces)
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) { if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
tripSuggestionCalled = true tripSuggestionCalled = true
@@ -98,9 +99,11 @@ open class NavigationScreen(
} }
repository.tripSuggestionFlow.asLiveData().observe(this, Observer { repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
Log.d(TAG, "NavigationScreen 3")
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces) navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
tripSuggestion = it tripSuggestion = it
}) })
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer { repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
showAlternativeRoute = it showAlternativeRoute = it
}) })
@@ -116,6 +119,7 @@ open class NavigationScreen(
* Returns the appropriate template based on the current navigation state. * Returns the appropriate template based on the current navigation state.
*/ */
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
Log.d(TAG, "NavigationScreen 2")
val actionStripBuilder = createActionStripBuilder({ val actionStripBuilder = createActionStripBuilder({
createAction( createAction(
carContext, carContext,
@@ -136,14 +140,14 @@ open class NavigationScreen(
* Creates and returns a NavigationTemplate for the active navigation state. * Creates and returns a NavigationTemplate for the active navigation state.
*/ */
private fun navigation(actionStripBuilder: ActionStrip.Builder): Template { private fun navigation(actionStripBuilder: ActionStrip.Builder): Template {
// actionStripBuilder.addAction( actionStripBuilder.addAction(
// createAction( createAction(
// carContext, carContext,
// R.drawable.ic_close_white_24dp, R.drawable.ic_close_white_24dp,
// 0 0,
// ) { stopNavigation() } { stopNavigation() })
// ) )
val navigationTemplate = NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo( .setNavigationInfo(
getRoutingInfo() getRoutingInfo()
) )
@@ -166,7 +170,6 @@ open class NavigationScreen(
) )
.setBackgroundColor(backGroundColor) .setBackgroundColor(backGroundColor)
.build() .build()
return navigationTemplate
} }
/** /**
@@ -181,7 +184,7 @@ open class NavigationScreen(
carContext = carContext, R.drawable.ic_recenter_24, carContext = carContext, R.drawable.ic_recenter_24,
0, 0,
onClickAction = { onClickAction = {
surfaceRenderer.setStandardView() surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate() invalidate()
}) })
}) })
@@ -190,7 +193,7 @@ open class NavigationScreen(
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStrip) .setMapActionStrip(mapActionStrip)
.setPanModeListener { isInPanMode: Boolean -> .setPanModeListener { isInPanMode: Boolean ->
Log.d(TAG, "PanMode $isInPanMode")
} }
.build() .build()
} }
@@ -216,12 +219,16 @@ open class NavigationScreen(
* Creates and returns a NavigationTemplate specifically for when the destination is reached. * Creates and returns a NavigationTemplate specifically for when the destination is reached.
*/ */
fun navigationArrived(actionStripBuilder: ActionStrip.Builder): NavigationTemplate { fun navigationArrived(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
var street = ""
if (destinations.first().address != null) {
street = destinations.first().address.toString()
}
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo( .setNavigationInfo(
MessageInfo.Builder( MessageInfo.Builder(
carContext.getString(R.string.arrived_exclamation_msg) carContext.getString(R.string.arrived_exclamation_msg)
) )
.setText(message) .setText(street)
.setImage( .setImage(
CarIcon.Builder( CarIcon.Builder(
IconCompat.createWithResource( IconCompat.createWithResource(
@@ -338,10 +345,12 @@ open class NavigationScreen(
*/ */
fun getRoutingInfo(): RoutingInfo { fun getRoutingInfo(): RoutingInfo {
val routingInfo = RoutingInfo.Builder() val routingInfo = RoutingInfo.Builder()
.setCurrentStep( if (steps.isNotEmpty()) {
routingInfo.setCurrentStep(
steps.first(), steps.first(),
stepRemainingDistance stepRemainingDistance
) )
}
if (shouldShowNextStep && steps.size > 1) { if (shouldShowNextStep && steps.size > 1) {
routingInfo.setNextStep(steps[1]) routingInfo.setNextStep(steps[1])
} }
@@ -471,14 +480,13 @@ open class NavigationScreen(
/** /**
* Initiates recalculation for a new route to the destination. * Initiates recalculation for a new route to the destination.
*/ */
fun calculateNewRoute(destination: Place, distance: Float) { fun calculateNewRoute(destination: Place) {
deviation = distance
navigationType = NavigationType.REROUTE navigationType = NavigationType.REROUTE
invalidate() invalidate()
val mainThreadHandler = Handler(carContext.mainLooper) val mainThreadHandler = Handler(carContext.mainLooper)
mainThreadHandler.post { mainThreadHandler.post {
reRouteTimer?.cancel() reRouteTimer?.cancel()
reRouteTimer = object : CountDownTimer(3000, 1000) { reRouteTimer = object : CountDownTimer(2000, 1000) {
override fun onTick(millisUntilFinished: Long) {} override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() { override fun onFinish() {
navigationType = NavigationType.NAVIGATION navigationType = NavigationType.NAVIGATION
@@ -489,6 +497,7 @@ open class NavigationScreen(
} }
} }
/** /**
* Updates navigation state with the current location, checks for arrival, and traffic updates. * Updates navigation state with the current location, checks for arrival, and traffic updates.
*/ */
@@ -504,8 +513,7 @@ open class NavigationScreen(
shouldShowNextStep: Boolean, shouldShowNextStep: Boolean,
shouldShowLanes: Boolean, shouldShowLanes: Boolean,
junctionImage: CarIcon?, junctionImage: CarIcon?,
backGroundColor: CarColor, backGroundColor: CarColor
message: String
) { ) {
this.isNavigating = isNavigating this.isNavigating = isNavigating
this.isRerouting = isRerouting this.isRerouting = isRerouting
@@ -519,7 +527,6 @@ open class NavigationScreen(
this.shouldShowLanes = shouldShowLanes this.shouldShowLanes = shouldShowLanes
this.junctionImage = junctionImage this.junctionImage = junctionImage
this.backGroundColor = backGroundColor this.backGroundColor = backGroundColor
this.message = message
navigationType = NavigationType.NAVIGATION navigationType = NavigationType.NAVIGATION
invalidate() invalidate()
} }
@@ -29,7 +29,6 @@ import com.kouros.navigation.data.Constants.RECENT
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location
class PlaceListScreen( class PlaceListScreen(
private val carContext: CarContext, private val carContext: CarContext,
@@ -60,12 +59,6 @@ class PlaceListScreen(
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
navigationViewModel.recentPlaces.value = emptyList() navigationViewModel.recentPlaces.value = emptyList()
} }
override fun onStart(owner: LifecycleOwner) {
recentPlaces.forEach {
it.distance = location(it.longitude, it.latitude).distanceTo(surfaceRenderer.lastLocation)
}
}
}) })
} }
@@ -75,11 +68,8 @@ class PlaceListScreen(
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val itemListBuilder = ItemList.Builder() val itemListBuilder = ItemList.Builder()
.setNoItemsMessage(carContext.getString(R.string.no_places)) .setNoItemsMessage(carContext.getString(R.string.no_places))
recentPlaces.filter { it.category == category && it.distance > 500F }.forEach { recentPlaces.filter { it.category == category }.forEach {
val street = it.street.ifEmpty { val street = it.street
it.name
}
val row = Row.Builder() val row = Row.Builder()
.setImage(contactIcon(null, it.category)) .setImage(contactIcon(null, it.category))
.setTitle("$street ${it.city}") .setTitle("$street ${it.city}")
@@ -125,11 +115,22 @@ class PlaceListScreen(
/** /**
* Creates an Action to navigate to a specific place. * Creates an Action to navigate to a specific place.
*/ */
private fun clickOnPlace(itPlace: Place) { private fun clickOnPlace(it: Place) {
place = Place(
0,
it.name,
it.category,
it.latitude,
it.longitude,
it.postalCode,
it.city,
it.street,
// avatar = null
)
if (surfaceRenderer.navigation) { if (surfaceRenderer.navigation) {
startStopOverScreen(itPlace) startStopOverScreen(place)
} else { } else {
starPreviewScreen(itPlace) starPreviewScreen(place)
} }
} }
@@ -13,6 +13,7 @@ import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
import androidx.car.app.model.CarColor import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText import androidx.car.app.model.CarText
import androidx.car.app.model.DurationSpan import androidx.car.app.model.DurationSpan
import androidx.car.app.model.ForegroundCarColorSpan import androidx.car.app.model.ForegroundCarColorSpan
@@ -25,6 +26,7 @@ import androidx.car.app.model.Template
import androidx.car.app.navigation.model.MapController import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.versioning.CarAppApiLevels import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
@@ -33,7 +35,6 @@ import androidx.lifecycle.lifecycleScope
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants.FAVORITES
import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
@@ -57,7 +58,7 @@ class RoutePreviewScreen(
private var showAlternativeRoute: Boolean private var showAlternativeRoute: Boolean
) : ) :
Screen(carContext) { Screen(carContext) {
private var isFavorite = destination.favorite private var isFavorite = false
val maxListItems: Int = 3 val maxListItems: Int = 3
@@ -71,7 +72,6 @@ class RoutePreviewScreen(
var loading = true var loading = true
var previewReady = false;
var flag = FLAG_DEFAULT var flag = FLAG_DEFAULT
private val backPressedCallback = object : OnBackPressedCallback(false) { private val backPressedCallback = object : OnBackPressedCallback(false) {
@@ -85,7 +85,6 @@ class RoutePreviewScreen(
routeModel.startNavigation(route) routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel) surfaceRenderer.setPreviewRouteData(routeModel)
loading = false loading = false
previewReady = true
if (routeModel.route.routes.size == 1 && showAlternativeRoute) { if (routeModel.route.routes.size == 1 && showAlternativeRoute) {
routeType = RoutePreviewType.SINGLE_ROUTE routeType = RoutePreviewType.SINGLE_ROUTE
showAlternativeRoute = false showAlternativeRoute = false
@@ -93,7 +92,6 @@ class RoutePreviewScreen(
invalidate() invalidate()
} }
} }
val trafficObserver = Observer<Map<String, String>> { traffic -> val trafficObserver = Observer<Map<String, String>> { traffic ->
if (traffic.isNotEmpty()) { if (traffic.isNotEmpty()) {
navigationViewModel.traffic.value = emptyMap() navigationViewModel.traffic.value = emptyMap()
@@ -117,6 +115,7 @@ class RoutePreviewScreen(
}) })
repository.routingEngineFlow.asLiveData().observe(this, Observer { repository.routingEngineFlow.asLiveData().observe(this, Observer {
routingEngine = it routingEngine = it
}) })
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer { repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
@@ -129,6 +128,7 @@ class RoutePreviewScreen(
location(destination.longitude, destination.latitude), location(destination.longitude, destination.latitude),
surfaceRenderer.carOrientation surfaceRenderer.carOrientation
) )
} }
} }
@@ -160,13 +160,15 @@ class RoutePreviewScreen(
header.addEndHeaderAction( header.addEndHeaderAction(
favoriteAction() favoriteAction()
) )
header.addEndHeaderAction(
deleteFavoriteAction()
)
} }
val message = val message =
if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) { if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) {
createRouteText(routeModel.route.routes.first()) createRouteText(routeModel.route.routes.first())
} else { } else {
loading = true CarText.Builder("Wait")
CarText.Builder(carContext.getString(R.string.wait))
.build() .build()
} }
val content = if (routeType == RoutePreviewType.MULTI_ROUTE) { val content = if (routeType == RoutePreviewType.MULTI_ROUTE) {
@@ -190,10 +192,8 @@ class RoutePreviewScreen(
}) })
val listContent = MessageTemplate.Builder(message) val listContent = MessageTemplate.Builder(message)
.setHeader(header.build()) .setHeader(header.build())
.addAction(navigateAction)
if (previewReady) {
listContent.addAction(navigateAction)
}
if (showAlternativeRoute) { if (showAlternativeRoute) {
listContent.addAction(selectRouteAction) listContent.addAction(selectRouteAction)
} }
@@ -213,7 +213,6 @@ class RoutePreviewScreen(
) )
if (routeModel.route.routes.size > 1 && routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) { if (routeModel.route.routes.size > 1 && routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) {
if (previewReady) {
template.setActionStrip(createActionStrip { template.setActionStrip(createActionStrip {
createAction( createAction(
carContext, R.drawable.navigation_48px, carContext, R.drawable.navigation_48px,
@@ -223,7 +222,6 @@ class RoutePreviewScreen(
) )
}) })
} }
}
return template.build() return template.build()
} }
@@ -261,17 +259,34 @@ class RoutePreviewScreen(
else else
R.drawable.ic_favorite_white_24dp R.drawable.ic_favorite_white_24dp
, FLAG_IS_PERSISTENT, , FLAG_IS_PERSISTENT,
onClickAction = { ) {
isFavorite = !isFavorite isFavorite = !isFavorite
destination.favorite = isFavorite CarToast.makeText(
if (isFavorite) { carContext,
if (isFavorite)
carContext
.getString(R.string.favorites)
else
carContext.getString(
R.string.favorites
),
CarToast.LENGTH_SHORT
)
.show()
navigationViewModel.saveFavorite(carContext, destination) navigationViewModel.saveFavorite(carContext, destination)
} else {
navigationViewModel.deleteFavorite(carContext, destination)
}
invalidate() invalidate()
} }
)
private fun deleteFavoriteAction(): Action =
createAction(carContext, R.drawable.heart_minus_48px, FLAG_IS_PERSISTENT,{
if (isFavorite) {
navigationViewModel.deleteFavorite(carContext, destination)
}
isFavorite = !isFavorite
finish()
})
private fun createRouteText(route: Routes): CarText { private fun createRouteText(route: Routes): CarText {
val time = route.summary.duration val time = route.summary.duration
@@ -306,9 +321,7 @@ class RoutePreviewScreen(
.setTitle(routeText) .setTitle(routeText)
.setOnClickListener { onRouteSelected(index) } .setOnClickListener { onRouteSelected(index) }
.addText(street) .addText(street)
if (previewReady) { .addAction(navigateAction)
row.addAction(navigateAction)
}
if (route.summary.trafficDelay > 60) { if (route.summary.trafficDelay > 60) {
row.addText(createDelay(route)) row.addText(createDelay(route))
row.setImage(createCarIcon(carContext = carContext, R.drawable.traffic_jam_48px)) row.setImage(createCarIcon(carContext = carContext, R.drawable.traffic_jam_48px))
@@ -334,13 +347,11 @@ class RoutePreviewScreen(
} }
private fun onNavigate(index: Int) { private fun onNavigate(index: Int) {
if (previewReady) {
destination.routeIndex = index destination.routeIndex = index
destination.route = navigationViewModel.previewRoute.value.toString() destination.route = navigationViewModel.previewRoute.value.toString()
setResult(destination) setResult(destination)
finish() finish()
} }
}
private fun onRouteSelected(index: Int) { private fun onRouteSelected(index: Int) {
routeModel.navState = routeModel.navState.copy(currentRouteIndex = index) routeModel.navState = routeModel.navState.copy(currentRouteIndex = index)
@@ -356,6 +367,7 @@ class RoutePreviewScreen(
surfaceRenderer.carOrientation surfaceRenderer.carOrientation
) )
} }
} }
enum class RoutePreviewType { enum class RoutePreviewType {
@@ -50,7 +50,7 @@ fun createNumberIcon(category: String, number: String): IconCompat {
CHARGING_STATION -> Color.GREEN CHARGING_STATION -> Color.GREEN
FUEL_STATION -> Color.BLUE FUEL_STATION -> Color.BLUE
PHARMACY -> Color.RED PHARMACY -> Color.RED
else -> Color.MAGENTA else -> Color.WHITE
} }
paint.color = color paint.color = color
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint) canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
@@ -19,6 +19,7 @@ import com.kouros.navigation.data.Constants.CATEGORIES
import com.kouros.navigation.data.Constants.FAVORITES import com.kouros.navigation.data.Constants.FAVORITES
import com.kouros.navigation.data.Constants.RECENT import com.kouros.navigation.data.Constants.RECENT
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
@@ -85,7 +86,22 @@ class SearchScreen(
} }
} }
} else { } else {
startPlaceListScreen(it) screenManager
.pushForResult(
PlaceListScreen(
carContext,
surfaceRenderer,
it.id,
navigationViewModel,
recentPlaces
)
) { obj: Any? ->
surfaceRenderer.setStandardView()
if (obj != null) {
setResult(obj)
finish()
}
}
} }
} }
.setBrowsable(true) .setBrowsable(true)
@@ -115,24 +131,6 @@ class SearchScreen(
.build() .build()
} }
fun startPlaceListScreen(it: Category) {
screenManager
.pushForResult(
PlaceListScreen(
carContext,
surfaceRenderer,
it.id,
navigationViewModel,
recentPlaces
)
) { obj: Any? ->
surfaceRenderer.setStandardView()
if (obj != null) {
setResult(obj)
finish()
}
}
}
fun categoryIcon(category: String?): CarIcon { fun categoryIcon(category: String?): CarIcon {
val resId: Int = when (category) { val resId: Int = when (category) {
RECENT -> { RECENT -> {
@@ -184,28 +182,7 @@ class SearchScreen(
distance = result.distance distance = result.distance
) )
recentPlaces.add(place) recentPlaces.add(place)
startPreviewScreen(place)
setResult(place) setResult(place)
finish() finish()
} }
fun startPreviewScreen(it: Place) {
screenManager
.pushForResult(
RoutePreviewScreen(
carContext,
RoutePreviewType.SINGLE_ROUTE,
surfaceRenderer,
destination = it,
navigationViewModel,
false
)
) { obj: Any? ->
surfaceRenderer.setStandardView()
if (obj != null) {
setResult(obj)
finish()
}
}
}
} }
@@ -1,7 +1,6 @@
package com.kouros.navigation.car.screen.observers package com.kouros.navigation.car.screen.observers
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
/** /**
@@ -26,9 +25,9 @@ interface NavigationObserverCallback {
/** Called when max speed is updated */ /** Called when max speed is updated */
fun onMaxSpeedReceived(speed: Int) fun onMaxSpeedReceived(speed: Int)
fun onRecentPlacesReceived(places: List<Place>)
/** Called to request UI invalidation/refresh */ /** Called to request UI invalidation/refresh */
fun invalidateScreen() fun invalidateScreen()
fun onTrafficMessageReceived(trafficMessage: String)
} }
@@ -1,5 +1,6 @@
package com.kouros.navigation.car.screen.observers package com.kouros.navigation.car.screen.observers
import com.kouros.navigation.car.CarSession
import com.kouros.navigation.car.NavigationSession import com.kouros.navigation.car.NavigationSession
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
@@ -14,19 +15,22 @@ class NavigationObserverManager(
val routeObserver = RouteObserver(callback) val routeObserver = RouteObserver(callback)
val trafficObserver = TrafficObserver(callback) val trafficObserver = TrafficObserver(callback)
val trafficMessageObserver = TrafficMessageObserver(callback)
val placeSearchObserver = PlaceSearchObserver(callback) val placeSearchObserver = PlaceSearchObserver(callback)
val speedCameraObserver = SpeedCameraObserver(callback) val speedCameraObserver = SpeedCameraObserver(callback)
val maxSpeedObserver = MaxSpeedObserver(callback) val maxSpeedObserver = MaxSpeedObserver(callback)
fun attachAllObservers(session: NavigationSession) { val recentPlacesObserver = RecentPlacesObserver(callback)
fun attachAllObservers(session: CarSession) {
viewModel.route.observe(session, routeObserver) viewModel.route.observe(session, routeObserver)
viewModel.traffic.observe(session, trafficObserver) viewModel.traffic.observe(session, trafficObserver)
viewModel.trafficMessage.observe(session, trafficMessageObserver)
viewModel.placeLocation.observe(session, placeSearchObserver) viewModel.placeLocation.observe(session, placeSearchObserver)
viewModel.speedCameras.observe(session, speedCameraObserver) viewModel.speedCameras.observe(session, speedCameraObserver)
viewModel.maxSpeed.observe(session, maxSpeedObserver) viewModel.maxSpeed.observe(session, maxSpeedObserver)
viewModel.recentPlaces.observe(session, recentPlacesObserver)
} }
/** /**
@@ -0,0 +1,18 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
import com.kouros.navigation.data.Place
/**
* Observer for route updates. Triggers navigation start when a non-empty route is received.
*/
class RecentPlacesObserver(
private val callback: NavigationObserverCallback
) : Observer<List<Place>> {
override fun onChanged(value: List<Place>) {
if (value.isNotEmpty()) {
callback.onRecentPlacesReceived(value)
}
}
}
@@ -1,16 +0,0 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
/**
* Observer for traffic data updates.
*/
class TrafficMessageObserver(
private val callback: NavigationObserverCallback
) : Observer<String> {
override fun onChanged(value: String) {
callback.onTrafficMessageReceived(value)
callback.invalidateScreen()
}
}
@@ -1,9 +1,7 @@
package com.kouros.navigation.car.screen.settings package com.kouros.navigation.car.screen.settings
import androidx.annotation.OptIn
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.Header import androidx.car.app.model.Header
@@ -18,7 +16,6 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.screen.CarHardwareInfoScreen
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -40,7 +37,6 @@ class SettingsScreen(
} }
} }
@OptIn(ExperimentalCarApi::class)
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
settingsViewModel.guidanceAudio.asLiveData().observe(this, Observer { settingsViewModel.guidanceAudio.asLiveData().observe(this, Observer {
audioToggleState = settingsViewModel.guidanceAudio.value == 1 audioToggleState = settingsViewModel.guidanceAudio.value == 1
@@ -98,13 +94,6 @@ class SettingsScreen(
) )
) )
listBuilder.addItem(
buildRowForTemplate(
CarHardwareInfoScreen(carContext),
R.string.model_info
)
)
listBuilder.addItem( listBuilder.addItem(
buildRowForTemplate( buildRowForTemplate(
CarSettings(carContext, navigationViewModel), CarSettings(carContext, navigationViewModel),
@@ -2,6 +2,7 @@ package com.kouros.navigation.car.screen
import androidx.car.app.testing.ScreenController import androidx.car.app.testing.ScreenController
import androidx.car.app.testing.TestCarContext import androidx.car.app.testing.TestCarContext
import androidx.car.app.navigation.model.NavigationTemplate
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
@@ -62,6 +63,7 @@ class NavigationScreenTest {
navigationScreen = NavigationScreen( navigationScreen = NavigationScreen(
testCarContext, testCarContext,
mockSurfaceRenderer, mockSurfaceRenderer,
mockRouteModel,
mockListener, mockListener,
mockViewModel mockViewModel
) )
@@ -107,7 +109,7 @@ class NavigationScreenTest {
navigationScreen.navigationType = NavigationType.NAVIGATION navigationScreen.navigationType = NavigationType.NAVIGATION
// Act // Act
navigationScreen.calculateNewRoute(Place(), distance) navigationScreen.calculateNewRoute(Place())
// Assert // Assert
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.REROUTE) assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.REROUTE)
@@ -118,14 +120,14 @@ class NavigationScreenTest {
// Arrange // Arrange
navigationScreen.navigationType = NavigationType.NAVIGATION navigationScreen.navigationType = NavigationType.NAVIGATION
`when`(mockRouteModel.isManeuverArrival()).thenReturn(true) `when`(mockRouteModel.isArrival()).thenReturn(true)
`when`(mockRouteModel.routeCalculator).thenReturn(mockRouteCalculator) `when`(mockRouteModel.routeCalculator).thenReturn(mockRouteCalculator)
`when`(mockRouteCalculator.leftStepDistance()).thenReturn(9.0) `when`(mockRouteCalculator.leftStepDistance()).thenReturn(19.0)
`when`(mockRouteModel.navState).thenReturn(NavigationState()) `when`(mockRouteModel.navState).thenReturn(NavigationState())
// Act // Act
navigationScreen.checkArrival()
// Assert // Assert
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.NAVIGATION) assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.ARRIVAL)
} }
} }
@@ -98,9 +98,6 @@ class CategoryObserverTest {
} }
private fun createElement(lon: Double, lat: Double): Elements { private fun createElement(lon: Double, lat: Double): Elements {
return Elements( return Elements(lon = lon, lat = lat, tags = Tags())
lon = lon, lat = lat,
tags = Tags(maxspeed = "0", direction = ""),
bounds = com.kouros.navigation.data.overpass.Bounds(0.0, 0.0, 0.0, 0.0))
} }
} }
@@ -123,8 +123,7 @@ class ObserversTest {
return Elements( return Elements(
lon = lon, lon = lon,
lat = lat, lat = lat,
tags = Tags(maxspeed = maxSpeed, direction = ""), tags = Tags(maxspeed = maxSpeed, direction = null)
bounds = com.kouros.navigation.data.overpass.Bounds(0.0, 0.0, 0.0, 0.0),
) )
} }
} }
+1 -17
View File
@@ -1,7 +1,3 @@
import com.android.build.gradle.internal.tasks.AarMetadataReader.Companion.load
import java.util.Properties
import kotlin.apply
plugins { plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
@@ -12,24 +8,12 @@ plugins {
android { android {
namespace = "com.kouros.data" namespace = "com.kouros.data"
compileSdk = 37 compileSdk = 36
val properties = Properties().apply {
val localPropertiesFile = project.rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
load(localPropertiesFile.inputStream())
}
}
defaultConfig { defaultConfig {
minSdk = 33 minSdk = 33
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro") consumerProguardFiles("consumer-rules.pro")
buildConfigField("String", "USER", "\"${properties.getProperty("USER") ?: ""}\"")
buildConfigField("String", "PASSWORD", "\"${properties.getProperty("PASSWORD") ?: ""}\"")
buildConfigField("String", "TANKER_KOENIG_API_KEY", "\"${properties.getProperty("TANKER_KOENIG_API_KEY") ?: ""}\"")
} }
buildFeatures { buildFeatures {
@@ -1,20 +0,0 @@
package com.kouros.navigation.data
import com.kouros.data.BuildConfig
data class ApplicationConfig(
val user: String,
val password: String,
val tankerKoenigApiKey: String
) {
companion object {
fun load(): ApplicationConfig {
return ApplicationConfig(
user = BuildConfig.USER,
password = BuildConfig.PASSWORD,
tankerKoenigApiKey = BuildConfig.TANKER_KOENIG_API_KEY
)
}
}
}
@@ -3,23 +3,13 @@ package com.kouros.navigation.data
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val NavigationColorLight = Color(0xFF17A119) val NavigationColorLight = Color(0xFF17A119)
val NavigationColorDark = Color(0xFF2B007A)
val NavigationCircle = Color(0xFFFFEB3B) val NavigationColorDark = Color(0xFF4EDE10)
val RouteColor = Color(0xFF5201B4) val RouteColor = Color(0xFF195D02)
val SpeedColor = Color(0xFF262525) val SpeedColor = Color(0xFF262525)
val MaxSpeedColor = Color(0xFFB71515) val MaxSpeedColor = Color(0xFFB71515)
val PlaceColor = Color(0xFF868005) val PlaceColor = Color(0xFF868005)
val QueuingColor = Color(0xFFC46E53)
val SlowColor = Color(0xFFC43E3E)
val StationaryColor = Color(0xFF910A0A)
val HeavyColor = Color(0xFF6B0404)
val RoadworksColor = Color(0xFFDAA707)
val LaneColor = Color(0xFFEFBAC1)
val PharmacyColor = Color(0xFF054603)
@@ -16,7 +16,6 @@
package com.kouros.navigation.data package com.kouros.navigation.data
import android.location.Location
import android.net.Uri import android.net.Uri
import com.google.gson.annotations.Expose import com.google.gson.annotations.Expose
import com.kouros.navigation.data.route.Lane import com.kouros.navigation.data.route.Lane
@@ -29,7 +28,7 @@ data class Category(
val name: String, val name: String,
) )
data class StepMatch(val stepIndex: Int, val waypointIndex: Int, val location: Location)
data class Places( data class Places(
val places: List<Place>, val places: List<Place>,
@@ -45,7 +44,6 @@ data class Place(
var postalCode: String = "", var postalCode: String = "",
var city: String = "", var city: String = "",
var street: String = "", var street: String = "",
var navigations: Int = 0,
@Transient @Transient
var distance: Float = 0F, var distance: Float = 0F,
//var avatar: Uri? = null, //var avatar: Uri? = null,
@@ -56,8 +54,6 @@ data class Place(
var route: String = "", var route: String = "",
@Transient @Transient
var stopOver: Boolean = false, var stopOver: Boolean = false,
@Transient
var favorite: Boolean = false
) )
data class ContactData( data class ContactData(
@@ -78,8 +74,6 @@ data class StepData (
var lane: List<Lane> = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList(), 0, 0)), var lane: List<Lane> = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList(), 0, 0)),
var exitNumber: Int = 0, var exitNumber: Int = 0,
var message: String = "", var message: String = "",
var roadNumbers: List<String> = emptyList(),
) )
@@ -126,51 +120,31 @@ object Constants {
const val CHARGING_STATION: String ="charging_station" const val CHARGING_STATION: String ="charging_station"
const val RESTAURANT: String ="restaurant"
/** The initial location to use as an anchor for searches. */ /** The initial location to use as an anchor for searches. */
val widenmayer = location( 11.595721, 48.146113)
val homeVogelhart = location(11.5793748, 48.185749) val homeVogelhart = location(11.5793748, 48.185749)
val homeHohenwaldeck = location( 11.594322, 48.1164817) val homeHohenwaldeck = location( 11.594322, 48.1164817)
val ioannina = location( 20.826237, 39.690174)
val subislawa = location(18.570808, 54.420647)
val a94 = location(11.947101,48.213251)
val a9 = location(11.621556, 48.204402,)
val a22 = location(10.845749, 45.602507)
val isarring = location( 11.609696, 48.155116)
const val NEXT_STEP_THRESHOLD = 500.0 const val NEXT_STEP_THRESHOLD = 500.0
const val MAXIMAL_SNAP_CORRECTION = 50.0 const val MAXIMAL_SNAP_CORRECTION = 50.0
const val MAXIMAL_ROUTE_DEVIATION = 100.0 const val MAXIMAL_ROUTE_DEVIATION = 80.0
const val DESTINATION_ARRIVAL_DISTANCE = 10.0 const val DESTINATION_ARRIVAL_DISTANCE = 20.0
const val NEAREST_LOCATION_DISTANCE = 10F const val NEAREST_LOCATION_DISTANCE = 10F
const val MAXIMUM_LOCATION_DISTANCE = Float.MAX_VALUE const val MAXIMUM_LOCATION_DISTANCE = 100000F
const val TRAFFIC_UPDATE = 300 const val TRAFFIC_UPDATE = 300
const val TRAFFIC_MESSAGE_UPDATE = 10
const val SPEED_UPDATE_DISTANCE = 600F
const val INSTRUCTION_DISTANCE = 50 const val INSTRUCTION_DISTANCE = 50
const val SPEED_BEARING_DEVIATION = 60
const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED" const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED"
const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED" const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED"
const val TILT = 60.0 const val TILT = 60.0
const val TANKER_KOENIG_DELAY = 300000
const val LAST_CHECK_ROUTE_DISTANCE = 5000
} }
/** /**
@@ -12,7 +12,6 @@ import java.net.URL
abstract class NavigationRepository { abstract class NavigationRepository {
private val config by lazy { ApplicationConfig.load() }
private val nominatimUrl = "https://nominatim.openstreetmap.org/" private val nominatimUrl = "https://nominatim.openstreetmap.org/"
//private val nominatimUrl = "https://kouros-online.de/nominatim/" //private val nominatimUrl = "https://kouros-online.de/nominatim/"
@@ -62,16 +61,12 @@ abstract class NavigationRepository {
try { try {
if (authenticator) { if (authenticator) {
Authenticator.setDefault(object : Authenticator() { Authenticator.setDefault(object : Authenticator() {
override fun getPasswordAuthentication(): PasswordAuthentication? { override fun getPasswordAuthentication(): PasswordAuthentication {
return if (config.user.isEmpty() || config.password.isEmpty()) { return PasswordAuthentication(
null "kouros",
} else { "eo7sbjyWpmjSVFyELgbfrryqJ6ddNeq9".toCharArray()
PasswordAuthentication(
config.user,
config.password.toCharArray()
) )
} }
}
}) })
} }
Log.d("NavigationRepository", url) Log.d("NavigationRepository", url)
@@ -80,7 +75,7 @@ abstract class NavigationRepository {
"Accept", "Accept",
"application/json" "application/json"
) // The format of response we want to get from the server ) // The format of response we want to get from the server
httpURLConnection.setRequestProperty("User-Agent", "email=online@kouros-online.de") httpURLConnection.setRequestProperty("User-Agent", "email=nominatim@kouros-online.de")
httpURLConnection.requestMethod = "GET" httpURLConnection.requestMethod = "GET"
val responseCode = httpURLConnection.responseCode val responseCode = httpURLConnection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) { if (responseCode == HttpURLConnection.HTTP_OK) {
@@ -13,7 +13,6 @@ import com.kouros.navigation.data.tomtom.TomTomRoute
import com.kouros.navigation.data.valhalla.ValhallaResponse import com.kouros.navigation.data.valhalla.ValhallaResponse
import com.kouros.navigation.data.valhalla.ValhallaRoute import com.kouros.navigation.data.valhalla.ValhallaRoute
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlinx.coroutines.selects.whileSelect
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
@@ -103,23 +102,17 @@ data class Route(
return if (isRouteValid()) { return if (isRouteValid()) {
legs().first().steps[currentStepIndex] legs().first().steps[currentStepIndex]
} else { } else {
Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0), leftDistance = emptyList())) Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0)))
} }
} }
/**
* Maneuver locations to snap location
*/
fun maneuverLocations(): List<Point> { fun maneuverLocations(): List<Point> {
val wayPointIndex = currentStep().waypointIndex
val waypoints = currentStep().maneuver.waypoints val waypoints = currentStep().maneuver.waypoints
val points = mutableListOf<Point>() val points = mutableListOf<Point>()
for ((index,loc) in waypoints.withIndex()) { for (loc in waypoints) {
if (index >= wayPointIndex && points.size < 20) { val point = Point.fromLngLat(loc[0], loc[1])
val point = Point.fromLngLat(loc.longitude, loc.latitude)
points.add(point) points.add(point)
} }
}
return points return points
} }
@@ -6,7 +6,6 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.kouros.navigation.data.EngineType import com.kouros.navigation.data.EngineType
@@ -60,10 +59,6 @@ class DataStoreManager(private val context: Context) {
val ALTERNATIVE_ROUTES = booleanPreferencesKey("AlternativeRoutes") val ALTERNATIVE_ROUTES = booleanPreferencesKey("AlternativeRoutes")
val LAST_FUEL_PRICES = longPreferencesKey("LastFuelPrices")
val FUEL_PRICES = stringPreferencesKey("FuelPrices")
} }
// Read values // Read values
@@ -156,18 +151,6 @@ class DataStoreManager(private val context: Context) {
preferences[ALTERNATIVE_ROUTES] == true preferences[ALTERNATIVE_ROUTES] == true
} }
val lastFuelPricesFlow: Flow<Long> =
context.dataStore.data.map { preferences ->
preferences[LAST_FUEL_PRICES]
?: 0
}
val fuelPricesFlow: Flow<String> =
context.dataStore.data.map { preferences ->
preferences[FUEL_PRICES]
?: ""
}
// Save values // Save values
suspend fun setShow3D(enabled: Boolean) { suspend fun setShow3D(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
@@ -265,16 +248,4 @@ class DataStoreManager(private val context: Context) {
} }
} }
suspend fun setLastFuelPrices(lastFuelPrices: Long) {
context.dataStore.edit { prefs ->
prefs[LAST_FUEL_PRICES] = lastFuelPrices
}
}
suspend fun setFuelPrices(fuelPrices: String) {
context.dataStore.edit { prefs ->
prefs[FUEL_PRICES] = fuelPrices
}
}
} }
@@ -1,9 +0,0 @@
package com.kouros.navigation.data.fuel
data class FuelPrice(
val `data`: String,
val license: String,
val ok: Boolean,
val stations: List<Station>,
val status: String
)
@@ -1,63 +0,0 @@
package com.kouros.navigation.data.fuel
import android.content.Context
import android.location.Location
import android.util.Log
import com.google.gson.GsonBuilder
import com.kouros.data.BuildConfig
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.overpass.Amenity
import java.io.OutputStreamWriter
import java.net.Authenticator
import java.net.HttpURLConnection
import java.net.PasswordAuthentication
import java.net.URL
const val tankerKoenigUrl = "https://creativecommons.tankerkoenig.de/json/list.php?"
private val gson = GsonBuilder().serializeNulls().create()
const val sort = "&sort=dist&type=all"
val useLocal = BuildConfig.DEBUG
class FuelPrices : NavigationRepository() {
private val config by lazy { com.kouros.navigation.data.ApplicationConfig.load() }
fun getFuelPrices(location: Location, radius: Int) : List<Station> {
val url = if (useLocal) {
"http://192.168.1.37/fuel.json"
} else {
"${tankerKoenigUrl}lat=${location.latitude}&lng=${location.longitude}&rad=${radius}$sort&apikey=${config.tankerKoenigApiKey}"
}
val prices = fetchUrl(
url,
false
)
return gson.fromJson(prices, FuelPrice::class.java).stations
}
override fun getRoute(
context: Context,
currentLocation: Location,
location: List<Location>,
carOrientation: Float,
searchFilter: SearchFilter
): String {
return ""
}
override fun getTraffic(
context: Context,
location: Location,
carOrientation: Float
): String {
TODO("Not yet implemented")
}
}
@@ -1,23 +0,0 @@
package com.kouros.navigation.data.fuel
import com.kouros.navigation.data.Place
data class Stations(
val stations: List<Station>,
)
data class Station(
val brand: String,
val diesel: Double,
val dist: Double,
val e10: Double,
val e5: Double,
val houseNumber: String,
val id: String,
val isOpen: Boolean,
val lat: Double,
val lng: Double,
val name: String,
val place: String,
val postCode: Int,
val street: String
)
@@ -12,7 +12,6 @@ import com.kouros.navigation.utils.GeoUtils.createCenterLocation
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
import com.kouros.navigation.utils.GeoUtils.decodePolyline import com.kouros.navigation.utils.GeoUtils.decodePolyline
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlin.math.absoluteValue
class OsrmRoute { class OsrmRoute {
@@ -28,33 +27,18 @@ class OsrmRoute {
val steps = mutableListOf<Step>() val steps = mutableListOf<Step>()
leg.steps.forEach { step -> leg.steps.forEach { step ->
val intersections = mutableListOf<Intersection>() val intersections = mutableListOf<Intersection>()
val leftDistance = mutableListOf<Float>()
var lastLocation = location(0.0,0.0)
val points = decodePolyline(step.geometry, 5) val points = decodePolyline(step.geometry, 5)
waypoints.addAll(points) waypoints.addAll(points)
// calculate left step distance for each point
var leftStepDistance = step.distance
leftDistance.add(leftStepDistance.toFloat())
points.forEach {
if (lastLocation.latitude != 0.0) {
val curLocation = location(it[0], it[1])
val dist = curLocation.distanceTo(lastLocation).absoluteValue
leftStepDistance -= dist
leftDistance.add(leftStepDistance.toFloat())
}
lastLocation = location(it[0], it[1])
}
val maneuver = RouteManeuver( val maneuver = RouteManeuver(
bearingBefore = step.maneuver.bearingBefore, bearingBefore = step.maneuver.bearingBefore,
bearingAfter = step.maneuver.bearingAfter, bearingAfter = step.maneuver.bearingAfter,
type = convertType(step.maneuver), type = convertType(step.maneuver),
waypoints = points.map { location(it[0], it[1]) }, waypoints = points,
exit = step.maneuver.exit, exit = step.maneuver.exit,
location = location( location = location(
step.maneuver.location[0], step.maneuver.location[0],
step.maneuver.location[1] step.maneuver.location[1]
), )
leftDistance = leftDistance
) )
step.intersections.forEach { it2 -> step.intersections.forEach { it2 ->
if (it2.location[0] != 0.0) { if (it2.location[0] != 0.0) {
@@ -1,5 +1,13 @@
package com.kouros.navigation.data.overpass package com.kouros.navigation.data.overpass
import com.google.gson.annotations.SerializedName
data class Amenity ( data class Amenity (
val elements: List<Elements>,
@SerializedName("version" ) var version : Double? = null,
@SerializedName("generator" ) var generator : String? = null,
@SerializedName("osm3s" ) var osm3s : Osm3s? = Osm3s(),
@SerializedName("elements" ) var elements : ArrayList<Elements> = arrayListOf()
) )
@@ -1,8 +0,0 @@
package com.kouros.navigation.data.overpass
data class Bounds(
val maxlat: Double,
val maxlon: Double,
val minlat: Double,
val minlon: Double
)
@@ -1,7 +0,0 @@
package com.kouros.navigation.data.overpass
data class ElementSearch(
val element: Elements,
val distance: Double,
val bearing: Float,
)
@@ -1,17 +1,15 @@
package com.kouros.navigation.data.overpass package com.kouros.navigation.data.overpass
import com.google.gson.annotations.SerializedName
data class Elements ( data class Elements (
val bounds: Bounds,
val geometry: List<Geometry> = emptyList(), @SerializedName("type" ) var type : String = "",
val id: Long = 0, @SerializedName("id" ) var id : Long = 0,
val lat: Double= 0.0, @SerializedName("lat" ) var lat : Double = 0.0,
val lon: Double = 0.0, @SerializedName("lon" ) var lon : Double = 0.0,
val tags: Tags, @SerializedName("tags" ) var tags : Tags = Tags(),
val type: String = "", var distance : Double = 0.0
var distance : Double = 0.0,
var e5 : Double = 0.0,
var e10: Double = 0.0,
var diesel: Double = 0.0
) )
@@ -1,6 +0,0 @@
package com.kouros.navigation.data.overpass
data class Geometry(
val lat: Double = 0.0,
val lon: Double = 0.0
)
@@ -1,96 +1,40 @@
package com.kouros.navigation.data.overpass package com.kouros.navigation.data.overpass
import android.location.Location import android.location.Location
import android.util.Log
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.kouros.data.BuildConfig
import com.kouros.navigation.utils.GeoUtils.getBoundingBox import com.kouros.navigation.utils.GeoUtils.getBoundingBox
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
import java.net.Authenticator
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.PasswordAuthentication
import java.net.URL import java.net.URL
class Overpass { class Overpass {
private val config by lazy { com.kouros.navigation.data.ApplicationConfig.load() } //val overpassUrl = "https://overpass.kumi.systems/api/interpreter"
private val gson = GsonBuilder().serializeNulls().create() //val overpassUrl = "https://overpass-api.de/api"
val overpassUrl = "https://kouros-online.de/overpass/interpreter"
var overpassUrl = if (BuildConfig.DEBUG)
"http://192.168.1.37/api/interpreter"
else
"https://kouros-online.de/api/interpreter"
val destination = "[!destination][highway!=\"motorway_link\"]"
fun getSpeedLimit(radius: Float, linestring: String, street: String, roadNumbers: List<String>): List<Elements> {
val wayAround = "way[maxspeed](around:$radius,$linestring)"
val searchClauses = mutableListOf<String>()
// 1. Search by street name (fuzzy match with first 10 characters)
val streetPrefix = street.take(10)
if (streetPrefix.isNotEmpty()) {
searchClauses.add("$wayAround[name~\"^$streetPrefix\"]")
}
// 2. Search by road numbers (ref or int_ref)
val partRegex = Regex("""\d+|\D+""")
roadNumbers.forEach { number ->
val parts = partRegex.findAll(number).map { it.value.trim() }.filter { it.isNotEmpty() }.toList()
if (parts.isNotEmpty()) {
// Construct ref value: e.g., "A1" -> "A 1", "E30" -> "E 30"
val refValue = if (parts.size > 1) "${parts[0]} ${parts.drop(1).joinToString("")}" else parts[0]
val tag = if (number.startsWith("E", ignoreCase = true)) "int_ref" else "ref"
searchClauses.add("$wayAround$destination[$tag~\"$refValue\"]")
}
}
// 3. Fallback to searching everything if no specific filters were added
if (searchClauses.isEmpty()) {
searchClauses.add(wayAround)
}
fun getAround(radius: Int, linestring: String): List<Elements> {
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST"
httpURLConnection.setRequestProperty(
"Accept",
"application/json"
)
httpURLConnection.setDoOutput(true);
// define search query
val searchQuery = """ val searchQuery = """
|[out:json][timeout:10]; |[out:json];
|( |(
| ${searchClauses.joinToString(";")}; | way[highway](around:$radius,$linestring)
| ;
|); |);
|out body geom; |out body;
""".trimMargin() """.trimMargin()
//println("way[highway](around:$radius,$linestring)")
//Log.d("OverpassApi", "Overpass Query: $searchQuery") return overpassApi(httpURLConnection, searchQuery)
val connection = (URL(overpassUrl).openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
setRequestProperty("Accept", "application/json")
doOutput = true
} }
return overpassApi(connection, searchQuery)
}
fun getStreet(location: Location): List<Elements> {
val lineString = "${location.latitude},${location.longitude}"
val searchLocation ="way[\"highway\"~\"^(primary|secondary|tertiary|residential|motorway)$\"][name](around:50, $lineString)";
val searchQuery = """
|[out:json][timeout:10];
|(
| ${searchLocation};
|);
|out body geom;
""".trimMargin()
val connection = (URL(overpassUrl).openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
setRequestProperty("Accept", "application/json")
doOutput = true
}
return overpassApi(connection, searchQuery)
}
fun getAmenities( fun getAmenities(
type: String, type: String,
category: String, category: String,
@@ -98,7 +42,16 @@ class Overpass {
radius: Double radius: Double
): List<Elements> { ): List<Elements> {
val boundingBox = getBoundingBox(location.latitude, location.longitude, radius) val boundingBox = getBoundingBox(location.latitude, location.longitude, radius)
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST"
// node["highway"="speed_camera"]
// node[amenity=$category]
httpURLConnection.setDoOutput(true);
httpURLConnection.setRequestProperty(
"Accept",
"application/json"
)
// define search query
val searchQuery = """ val searchQuery = """
|[out:json]; |[out:json];
|( |(
@@ -106,57 +59,29 @@ class Overpass {
| ($boundingBox); | ($boundingBox);
|); |);
|(._;>;); |(._;>;);
|out body geom; |out body;
""".trimMargin() """.trimMargin()
return overpassApi(httpURLConnection, searchQuery)
val connection = (URL(overpassUrl).openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
setRequestProperty("Accept", "application/json")
doOutput = true
} }
return overpassApi(connection, searchQuery) fun overpassApi(httpURLConnection: HttpURLConnection, searchQuery: String): List<Elements> {
} try {
val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream)
private fun overpassApi(connection: HttpURLConnection, searchQuery: String): List<Elements> { outputStreamWriter.write(searchQuery)
outputStreamWriter.flush()
return try { // Check if the connection is successful
Authenticator.setDefault(object : Authenticator() { val responseCode = httpURLConnection.responseCode
override fun getPasswordAuthentication(): PasswordAuthentication? {
return if (config.user.isEmpty() || config.password.isEmpty()) {
null
} else {
PasswordAuthentication(
config.user,
config.password.toCharArray()
)
}
}
})
connection.outputStream.use { os ->
OutputStreamWriter(os).use { writer ->
writer.write(searchQuery)
writer.flush()
}
}
Log.w("OverpassApi", searchQuery)
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) { if (responseCode == HttpURLConnection.HTTP_OK) {
val response = connection.inputStream.bufferedReader().use { it.readText() } val response = httpURLConnection.inputStream.bufferedReader()
if (response.startsWith("<?xml")) { .use { it.readText() } // defaults to UTF-8
Log.w("OverpassApi", "Received XML instead of JSON") val gson = GsonBuilder().serializeNulls().create()
return emptyList() val overpass = gson.fromJson(response, Amenity::class.java)
} return overpass.elements
gson.fromJson(response, Amenity::class.java).elements
} else {
Log.e("OverpassApi", "Error code: $responseCode")
emptyList()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("OverpassApi", "Exception in Overpass API call", e) println("Speed $e")
emptyList() }
} return emptyList()
} }
} }
@@ -4,29 +4,21 @@ import com.google.gson.annotations.SerializedName
data class Tags( data class Tags(
val destination: String = "", @SerializedName("name") var name: String? = null,
val highway: String = "", @SerializedName("amenity") var amenity: String? = null,
val lanes: String = "", @SerializedName("authentication:none") var authenticationNone: String? = null,
val lit: String = "", @SerializedName("capacity") var capacity: String? = null,
val maxspeed: String = "0", @SerializedName("motorcar") var motorcar: String? = null,
@SerializedName("maxspeed:variable") val maxSpeedVariable: String = "", @SerializedName("network") var network: String? = null,
val name: String = "", @SerializedName("opening_hours") var openingHours: String? = null,
val oneway: String = "", @SerializedName("operator") var operator: String? = null,
val ref: String = "", @SerializedName("operator:short") var operatorShort: String? = null,
@SerializedName("int_ref") val intRef: String = "", @SerializedName("operator:wikidata") var operatorWikidata: String? = null,
val sidewalk: String = "", @SerializedName("operator:wikipedia") var operatorWikipedia: String? = null,
val smoothness: String = "", @SerializedName("ref") var ref: String? = null,
val surface: String = "", @SerializedName("socket:type2") var socketType2: String? = null,
val amenity: String = "", @SerializedName("socket:type2:output") var socketType2Output: String? = null,
val capacity: String = "", @SerializedName("maxspeed") var maxspeed: String = "0",
val motorcar: String = "", @SerializedName("direction") var direction: String? = null,
val network: String = "",
@SerializedName("opening_hours") val openingHours: String = "",
val operator: String = "",
val operatorShort: String = "",
val operatorWikidata: String = "",
val operatorWikipedia: String = "",
val socketType2: String = "",
val socketType2Output: String = "",
val direction: String = "",
) )
@@ -6,13 +6,12 @@ data class Maneuver(
val bearingBefore: Int = 0, val bearingBefore: Int = 0,
val bearingAfter: Int = 0, val bearingAfter: Int = 0,
val type: Int = 0, val type: Int = 0,
val waypoints: List<Location>, val waypoints: List<List<Double>>,
val location: Location, val location: Location,
val exit: Int = 0, val exit: Int = 0,
val street: String = "", val street: String = "",
val message: String = "", val message: String = "",
val pointIndex: Int = 0, val pointIndex: Int = 0,
val leftDistance: List<Float>
) )
enum class ManeuverType(val value: Int) { enum class ManeuverType(val value: Int) {
@@ -12,6 +12,5 @@ data class Step(
val distance: Double = 0.0, val distance: Double = 0.0,
val street : String = "", val street : String = "",
val intersection: List<Intersection> = mutableListOf(), val intersection: List<Intersection> = mutableListOf(),
val countryCode : String = "", val countryCode : String = ""
val roadNumbers : List<String> = emptyList(),
) )
@@ -11,7 +11,7 @@ data class Instruction(
val point: Point, val point: Point,
val pointIndex: Int, val pointIndex: Int,
val possibleCombineWithNext: Boolean, val possibleCombineWithNext: Boolean,
val roadNumbers: List<String> = emptyList(), val roadNumbers: List<String>,
val routeOffsetInMeters: Int, val routeOffsetInMeters: Int,
val signpostText: String, val signpostText: String,
val street: String? = "", val street: String? = "",
@@ -25,8 +25,8 @@ val useLocal = BuildConfig.DEBUG
val useLocalTraffic = BuildConfig.DEBUG val useLocalTraffic = BuildConfig.DEBUG
class TomTomRepository : NavigationRepository() {
class TomTomRepository : NavigationRepository() {
override fun getRoute( override fun getRoute(
context: Context, context: Context,
currentLocation: Location, currentLocation: Location,
@@ -34,15 +34,9 @@ class TomTomRepository : NavigationRepository() {
carOrientation: Float, carOrientation: Float,
searchFilter: SearchFilter searchFilter: SearchFilter
): String { ): String {
val vehicleHeading = if (carOrientation !in 0.0..360.0) {
0
} else {
carOrientation.toInt()
}
if (useLocal) { if (useLocal) {
return fetchUrl( return fetchUrl(
"http://192.168.1.37/tomtom_routing.json", "https://kouros-online.de/tomtom_routing.json",
//"http://192.168.1.37/verona.json",
false false
) )
} }
@@ -89,7 +83,7 @@ class TomTomRepository : NavigationRepository() {
"&vehicleMaxSpeed=120&vehicleCommercial=false" + "&vehicleMaxSpeed=120&vehicleCommercial=false" +
"&instructionsType=text&language=$language&sectionType=lanes" + "&instructionsType=text&language=$language&sectionType=lanes" +
"&routeRepresentation=encodedPolyline$altRoutes" + "&routeRepresentation=encodedPolyline$altRoutes" +
"&vehicleHeading=$vehicleHeading&vehicleEngineType=$engineType$filter&key=$tomtomApiKey" "&vehicleEngineType=$engineType$filter&key=$tomtomApiKey"
return fetchUrl( return fetchUrl(
url, url,
false false
@@ -103,10 +97,10 @@ class TomTomRepository : NavigationRepository() {
if (!showTraffic) { if (!showTraffic) {
return "" return ""
} }
val bbox = calculateSquareRadius(location.latitude, location.longitude, 10.0) val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
return if (useLocalTraffic) { return if (useLocalTraffic) {
fetchUrl( fetchUrl(
"http://192.168.1.37/tomtom_traffic.json", "https://kouros-online.de/tomtom_traffic.json",
false false
) )
} else { } else {
@@ -13,9 +13,7 @@ import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.utils.GeoUtils.createCenterLocation import com.kouros.navigation.utils.GeoUtils.createCenterLocation
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
import com.kouros.navigation.utils.GeoUtils.decodePolyline import com.kouros.navigation.utils.GeoUtils.decodePolyline
import com.kouros.navigation.utils.isNumeric
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlin.math.absoluteValue
import com.kouros.navigation.data.route.Maneuver as RouteManeuver import com.kouros.navigation.data.route.Maneuver as RouteManeuver
@@ -55,41 +53,23 @@ class TomTomRoute {
val instruction = route.guidance.instructions[index] val instruction = route.guidance.instructions[index]
val street = lastInstruction.street ?: "" val street = lastInstruction.street ?: ""
val maneuverStreet = instruction.street ?: "" val maneuverStreet = instruction.street ?: ""
val leftDistance = mutableListOf<Float>()
var lastLocation = location(0.0, 0.0)
stepDistance =
route.guidance.instructions[index].routeOffsetInMeters - stepDistance
stepDuration =
route.guidance.instructions[index].travelTimeInSeconds - stepDuration
// calculate left step distance for each point
var leftStepDistance = stepDistance
leftDistance.add(leftStepDistance.toFloat())
val subPoints = points.subList(
lastPointIndex,
instruction.pointIndex + 1)
subPoints.forEach {
if (lastLocation.latitude != 0.0) {
val curLocation = location(it[0], it[1])
val dist = curLocation.distanceTo(lastLocation).absoluteValue
leftStepDistance -= dist
leftDistance.add(leftStepDistance.toFloat())
}
lastLocation = location(it[0], it[1])
}
val maneuver = RouteManeuver( val maneuver = RouteManeuver(
bearingBefore = 0, bearingBefore = 0,
bearingAfter = 0, bearingAfter = 0,
type = convertType(instruction.maneuver), type = convertType(instruction.maneuver),
waypoints = subPoints.map { location(it[0], it[1]) }, waypoints = points.subList(
lastPointIndex,
instruction.pointIndex + 1,
),
exit = exitNumber(instruction), exit = exitNumber(instruction),
location = location( location = location(
instruction.point.longitude, instruction.point.latitude instruction.point.longitude, instruction.point.latitude
), ),
street = maneuverStreet, street = maneuverStreet,
message = instruction.message, message = instruction.message,
pointIndex = instruction.pointIndex, pointIndex = instruction.pointIndex
leftDistance = leftDistance
) )
lastPointIndex = instruction.pointIndex lastPointIndex = instruction.pointIndex
val intersections = mutableListOf<Intersection>() val intersections = mutableListOf<Intersection>()
route.sections?.forEach { section -> route.sections?.forEach { section ->
@@ -115,12 +95,10 @@ class TomTomRoute {
intersections.add(Intersection(waypoints[startIndex], lanes)) intersections.add(Intersection(waypoints[startIndex], lanes))
} }
} }
stepDistance =
val roadNumbers = if (lastInstruction.roadNumbers != null) { route.guidance.instructions[index].routeOffsetInMeters - stepDistance
lastInstruction.roadNumbers stepDuration =
} else { route.guidance.instructions[index].travelTimeInSeconds - stepDuration
emptyList()
}
val step = Step( val step = Step(
index = stepIndex, index = stepIndex,
street = street, street = street,
@@ -128,8 +106,7 @@ class TomTomRoute {
duration = stepDuration, duration = stepDuration,
maneuver = maneuver, maneuver = maneuver,
intersection = intersections, intersection = intersections,
countryCode = lastInstruction.countryCode, countryCode = lastInstruction.countryCode
roadNumbers = roadNumbers
) )
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble() stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble() stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
@@ -229,7 +206,6 @@ class TomTomRoute {
"TAKE_EXIT" -> { "TAKE_EXIT" -> {
newType = ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value newType = ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value
} }
"WAYPOINT_RIGHT" -> { "WAYPOINT_RIGHT" -> {
newType = ManeuverType.TYPE_WAYPOINT_RIGHT.value newType = ManeuverType.TYPE_WAYPOINT_RIGHT.value
} }
@@ -245,10 +221,6 @@ private fun exitNumber(
) { ) {
0 0
} else { } else {
if (isNumeric(instruction.exitNumber)) {
instruction.exitNumber.toInt() instruction.exitNumber.toInt()
} else {
0
}
} }
} }
@@ -25,10 +25,9 @@ class ValhallaRoute {
bearingAfter = it.bearingAfter, bearingAfter = it.bearingAfter,
//type = it.type, //type = it.type,
type = convertType(it), type = convertType(it),
waypoints = waypoints.subList(it.beginShapeIndex, it.endShapeIndex + 1).map { location(it[0], it[1]) }, waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1),
// TODO: calculate from ShapeIndex ! // TODO: calculate from ShapeIndex !
location = location(0.0, 0.0), location = location(0.0, 0.0)
leftDistance = emptyList()
) )
var name = "" var name = ""
@@ -1,5 +1,6 @@
package com.kouros.navigation.model package com.kouros.navigation.model
//import com.kouros.navigation.data.Preferences.boxStore
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.util.Log import android.util.Log
@@ -8,52 +9,28 @@ import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.FAVORITES
import com.kouros.navigation.data.Constants.SPEED_BEARING_DEVIATION
import com.kouros.navigation.data.Constants.SPEED_UPDATE_DISTANCE
import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TANKER_KOENIG_DELAY
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Places import com.kouros.navigation.data.Places
import com.kouros.navigation.data.SearchFilter import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.fuel.FuelPrices
import com.kouros.navigation.data.fuel.Station
import com.kouros.navigation.data.fuel.Stations
import com.kouros.navigation.data.nominatim.Search import com.kouros.navigation.data.nominatim.Search
import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.ElementSearch
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.overpass.Overpass import com.kouros.navigation.data.overpass.Overpass
import com.kouros.navigation.repository.SettingsRepository import com.kouros.navigation.utils.Levenshtein
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.GeoUtils.buildLineStrings
import com.kouros.navigation.utils.GeoUtils.buildPointCollection
import com.kouros.navigation.utils.bearingPositive
import com.kouros.navigation.utils.countryCodeSpeedLimit
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.isNumeric
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point import java.lang.reflect.Modifier
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import kotlin.collections.first
import kotlin.collections.forEach
import kotlin.comparisons.compareBy
import kotlin.math.absoluteValue
import kotlin.math.cos
import kotlin.math.sqrt
/** /**
* ViewModel for navigation-related data operations. * ViewModel for navigation-related data operations.
@@ -61,8 +38,6 @@ import kotlin.math.sqrt
*/ */
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()
@@ -73,11 +48,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
MutableLiveData() MutableLiveData()
} }
/** LiveData containing categorized traffic messages map */
val trafficMessage: MutableLiveData<String> by lazy {
MutableLiveData()
}
/** LiveData containing a preview route JSON string for route preview screens */ /** LiveData containing a preview route JSON string for route preview screens */
val previewRoute: MutableLiveData<String> by lazy { val previewRoute: MutableLiveData<String> by lazy {
MutableLiveData() MutableLiveData()
@@ -113,10 +83,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
MutableLiveData() MutableLiveData()
} }
/** LiveData containing POI elements from Overpass API */
val speedElements = mutableListOf<Elements>()
/** LiveData containing speed camera locations */ /** LiveData containing speed camera locations */
val speedCameras: MutableLiveData<List<Elements>> by lazy { val speedCameras: MutableLiveData<List<Elements>> by lazy {
MutableLiveData() MutableLiveData()
@@ -137,22 +103,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
MutableLiveData() MutableLiveData()
} }
val initialSnapLocation: MutableLiveData<Location> by lazy { val gson: com.google.gson.Gson = GsonBuilder().create()
MutableLiveData()
}
val gson: Gson = GsonBuilder().create()
/**
* Retrieves recent places from Preferences as a Flow.
*/
fun recentPlacesFlow(context: Context, location: Location): Flow<Place> = callbackFlow {
for (place in recentPlaces.value!!) {
trySend(place)
}
awaitClose {
}
}
/** /**
* Loads all recent places from Preferences and calculates distances. * Loads all recent places from Preferences and calculates distances.
@@ -169,11 +120,8 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
if (rp.isNotEmpty()) { if (rp.isNotEmpty()) {
for (place in places.places) { for (place in places.places) {
if (place.category == Constants.RECENT if (place.category == Constants.RECENT
|| place.category == FAVORITES || place.category == Constants.FAVORITES
) { ) {
if (place.category == FAVORITES) {
place.favorite = true
}
val plLocation = location(place.longitude, place.latitude) val plLocation = location(place.longitude, place.latitude)
if (place.latitude != 0.0) { if (place.latitude != 0.0) {
val distance = val distance =
@@ -189,9 +137,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
} }
val sortedList = pl.sortedWith(compareByDescending<Place> { it.navigations } recentPlaces.postValue(pl.sortedBy { it.distance })
.thenByDescending { it.distance })
recentPlaces.postValue(sortedList)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -229,11 +175,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
* Fetches traffic incident data and categorizes by severity. * Fetches traffic incident data and categorizes by severity.
* Posts categorized traffic map to traffic LiveData. * Posts categorized traffic map to traffic LiveData.
*/ */
fun loadTraffic( fun loadTraffic(context: Context, currentLocation: Location, carOrientation: Float) {
context: Context,
currentLocation: Location,
carOrientation: Float,
) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val data = repository.getTraffic( val data = repository.getTraffic(
@@ -255,6 +197,33 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Categorizes traffic incidents by type (queuing, stationary, slow, heavy, roadworks).
* @param data Raw traffic GeoJSON string
* @return Map of incident type to GeoJSON FeatureCollection
*/
private fun rebuildTraffic(data: String): Map<String, String> {
val featureCollection = FeatureCollection.fromJson(data)
val incidents = mutableMapOf<String, String>()
val queuing = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Queuing traffic") }
incidents["queuing"] = FeatureCollection.fromFeatures(queuing).toJson()
val stationary = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Stationary traffic") }
incidents["stationary"] = FeatureCollection.fromFeatures(stationary).toJson()
val slow = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Slow traffic") }
incidents["slow"] = FeatureCollection.fromFeatures(slow).toJson()
val heavy = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Heavy traffic") }
incidents["heavy"] = FeatureCollection.fromFeatures(heavy).toJson()
val roadworks = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Roadworks") }
incidents["roadworks"] = FeatureCollection.fromFeatures(roadworks).toJson()
return incidents
}
/** /**
* Calculates a preview route for route preview screen. * Calculates a preview route for route preview screen.
* Posts the route JSON to previewRoute LiveData. * Posts the route JSON to previewRoute LiveData.
@@ -283,39 +252,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return previewRoute.value return previewRoute.value
} }
/**
* Categorizes traffic incidents by type (queuing, stationary, slow, heavy, roadworks).
* @param data Raw traffic GeoJSON string
* @return Map of incident type to GeoJSON FeatureCollection
*/
private fun rebuildTraffic(data: String): Map<String, String> {
val featureCollection = FeatureCollection.fromJson(data)
val incidents = mutableMapOf<String, String>()
val queuing = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Queuing traffic") }
incidents["queuing"] = buildLineStrings(queuing)
val stationary = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Stationary traffic") }
incidents["stationary"] = buildLineStrings(stationary)
val slow = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Slow traffic") }
incidents["slow"] = buildLineStrings(slow)
val heavy = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Heavy traffic") }
incidents["heavy"] = buildLineStrings(heavy)
val roadworks = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Roadworks") }
incidents["roadworks"] = buildLineStrings(roadworks)
val laneClosed = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Lane closed") }
incidents["lane"] = buildLineStrings(laneClosed)
val closed = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Closed") }
incidents["closed"] = buildPointCollection(closed)
return incidents
}
/** /**
* Loads device contacts with addresses and converts to Place objects. * Loads device contacts with addresses and converts to Place objects.
@@ -410,25 +346,15 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
* Queries Overpass API for nearby amenities of a specific category. * Queries Overpass API for nearby amenities of a specific category.
* Posts sorted results to elements LiveData. * Posts sorted results to elements LiveData.
*/ */
fun getAmenities( fun getAmenities(category: String, location: Location) {
carContext: Context,
category: String,
location: Location,
lastFuelUpdate: Long = 0
) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
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 distAmenities = mutableListOf<Elements>() val distAmenities = mutableListOf<Elements>()
amenities.forEach { amenities.forEach {
val plLocation = val plLocation =
location(longitude = it.lon, latitude = it.lat) location(longitude = it.lon, latitude = it.lat)
val distance = plLocation.distanceTo(location) val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble() it.distance = distance.toDouble()
if (category == Constants.FUEL_STATION) {
addFuelPrice(it, fuelPrices, location)
}
distAmenities.add(it) distAmenities.add(it)
} }
val sortedList = distAmenities.sortedWith(compareBy { it.distance }) val sortedList = distAmenities.sortedWith(compareBy { it.distance })
@@ -436,51 +362,13 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
private suspend fun fuelStations(
category: String,
lastFuelUpdate: Long,
location: Location,
repository: SettingsRepository
): List<Station> {
var fuelPrices = emptyList<Station>()
if (category == Constants.FUEL_STATION) {
val now = System.currentTimeMillis()
if ((now - lastFuelUpdate) > TANKER_KOENIG_DELAY) {
fuelPrices = FuelPrices().getFuelPrices(location, 5)
repository.setLastFuelPrices(System.currentTimeMillis())
repository.setFuelPrices(gson.toJson(Stations(fuelPrices)))
} else {
val fuels = repository.fuelPricesFlow.first()
fuelPrices =
gson.fromJson(fuels, Stations::class.java).stations
}
}
return fuelPrices
}
fun addFuelPrice(fuel: Elements, fuelPrices: List<Station>, location: Location) {
fuelPrices.forEach {
if (fuel.tags.name.equals(it.brand, ignoreCase = true)) {
val plLocation =
location(longitude = it.lng, latitude = it.lat)
val distance = plLocation.distanceTo(location)
if ((distance - fuel.distance).absoluteValue < 30) {
fuel.e10 = it.e10
fuel.e5 = it.e5
fuel.diesel = it.diesel
}
}
}
}
/** /**
* Queries Overpass API for speed cameras within a radius. * Queries Overpass API for speed cameras within a radius.
* Posts sorted results to speedCameras LiveData. * Posts sorted results to speedCameras LiveData.
*/ */
fun getSpeedCameras(location: Location, radius: Double) { fun getSpeedCameras(location: Location, radius: Double) {
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 =
@@ -493,111 +381,25 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
speedCameras.postValue(sortedList) speedCameras.postValue(sortedList)
} }
} }
}
/** /**
* Queries Overpass API for speed limit on current road using fuzzy matching. * Queries Overpass API for speed limit on current road using fuzzy matching.
* Posts speed limit to maxSpeed LiveData. * Posts speed limit to maxSpeed LiveData.
*/ */
fun getSpeedLimit( fun getMaxSpeed(location: Location, street: String) {
location: Location,
routeBearing: Float,
countryCode: String,
) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
synchronized(this) { val levenshtein = Levenshtein()
maxSpeed.postValue(calculateSpeedLimit(location, routeBearing, countryCode))
}
}
}
/**
* Queries Overpass API for speed limit on current road using fuzzy matching.
*/
fun calculateSpeedLimit(location: Location, routeBearing: Float, countryCode: String): Int {
var speed = 0
val search = mutableListOf<ElementSearch>()
synchronized(this) {
// Equirectangular projection at the user's latitude. Closest-point ranking
// doesn't need geodesic accuracy, so we skip Location.distanceTo (Vincenty)
// and avoid allocating a Location per geometry vertex.
val userLat = location.latitude
val userLon = location.longitude
val metersPerDegLat = 111_320.0
val metersPerDegLon = metersPerDegLat * cos(Math.toRadians(userLat))
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
}
val minDistance = sqrt(minDistanceSq)
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(
ElementSearch(element, minDistance, streetBearing.absoluteValue)
)
}
}
val result =
search.sortedWith(compareBy<ElementSearch> { it.distance }.thenByDescending { it.bearing })
if (result.isNotEmpty()) {
val element = result.first().element
speed = if (element.tags.maxspeed == "none" && element.tags.highway == "motorway") {
countryCodeSpeedLimit(countryCode)
} else {
if (isNumeric(element.tags.maxspeed)) {
element.tags.maxspeed.toInt()
} else {
0
}
}
}
}
return speed
}
private fun isBearingValid(
element: Elements,
streetBearing: Float,
routeBearing: Float
): Boolean {
return if (element.tags.oneway.isNotEmpty()) {
(streetBearing - routeBearing.absoluteValue) < SPEED_BEARING_DEVIATION
} else {
true
}
}
/**
* Queries Overpass API for speed limit on current road.
* Posts speed elements to speedElements.
*/
fun updateSpeedLimit(
location: Location,
street: String,
roadNumbers: List<String>
) {
viewModelScope.launch(Dispatchers.IO) {
synchronized(this) {
val lineString = "${location.latitude},${location.longitude}" val lineString = "${location.latitude},${location.longitude}"
val elements = val amenities = Overpass().getAround(10, lineString)
overpass.getSpeedLimit(SPEED_UPDATE_DISTANCE, lineString, street, roadNumbers) amenities.forEach {
speedElements.clear() if (it.tags.name != null) {
speedElements.addAll(elements) val distance =
levenshtein.distance(it.tags.name!!, street)
if (distance < 5) {
val speed = it.tags.maxspeed.toInt()
maxSpeed.postValue(speed)
}
}
} }
} }
} }
@@ -651,9 +453,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
val current = LocalDateTime.now(ZoneOffset.UTC) val current = LocalDateTime.now(ZoneOffset.UTC)
place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond() place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond()
place.route = "" place.route = ""
place.navigations += 1
places.add(place) places.add(place)
recentPlaces.postValue(places)
settingsRepository.setRecentPlaces(gson.toJson(Places(places))) settingsRepository.setRecentPlaces(gson.toJson(Places(places)))
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -688,7 +488,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
settingsRepository.setRecentPlaces(gson.toJson(Places(places))) settingsRepository.setRecentPlaces(gson.toJson(Places(places)))
recentPlaces.postValue(places) recentPlaces.value = places
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -727,31 +527,4 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
return pl.toMutableStateList() return pl.toMutableStateList()
} }
/**
* Loads current location nearest street
* and snap to location to street
*/
fun loadCurrentLocation(location: Location) {
viewModelScope.launch(Dispatchers.IO) {
synchronized(this) {
val elements = overpass.getStreet(location)
if (elements.isNotEmpty()) {
val points = mutableListOf<Point>()
elements.first().geometry.forEach {
points.add(Point.fromLngLat(it.lon, it.lat))
}
val snappedLocation = snapLocation(location, points)
if (points.size > 1) {
val bearing = location(
points.first().longitude(),
points.first().latitude()
).bearingPositive(location(points[1].longitude(), points[1].latitude()))
snappedLocation.bearing = bearing
}
initialSnapLocation.postValue(snappedLocation)
}
}
}
}
} }
@@ -5,61 +5,38 @@ import android.util.Log
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import com.kouros.navigation.data.Constants.MAXIMUM_LOCATION_DISTANCE import com.kouros.navigation.data.Constants.MAXIMUM_LOCATION_DISTANCE
import com.kouros.navigation.data.Constants.NEAREST_LOCATION_DISTANCE import com.kouros.navigation.data.Constants.NEAREST_LOCATION_DISTANCE
import com.kouros.navigation.data.Constants.SPEED_UPDATE_DISTANCE
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.StepMatch
import com.kouros.navigation.data.osrm.Waypoints
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue import kotlin.math.roundToInt
class RouteCalculator(var routeModel: RouteModel) { class RouteCalculator(var routeModel: RouteModel) {
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
fun findStep(location: Location) { fun findStep(location: Location) {
var nearestDistance = MAXIMUM_LOCATION_DISTANCE var nearestDistance = MAXIMUM_LOCATION_DISTANCE
var count = 0
var lastDistance = 0F
var increaseDistance = 0
for ((index, step) in routeModel.curLeg.steps.withIndex()) { for ((index, step) in routeModel.curLeg.steps.withIndex()) {
count++
var distance = 0F
if (index >= routeModel.navState.route.currentStepIndex) { if (index >= routeModel.navState.route.currentStepIndex) {
for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) { for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) {
count++
if (wayIndex >= step.waypointIndex) { if (wayIndex >= step.waypointIndex) {
distance = location.distanceTo(waypoint) val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
if (distance < nearestDistance) { if (distance < nearestDistance) {
nearestDistance = distance nearestDistance = distance
routeModel.navState.route.currentStepIndex = step.index routeModel.navState.route.currentStepIndex = step.index
step.waypointIndex = wayIndex step.waypointIndex = wayIndex
step.wayPointLocation = waypoint step.wayPointLocation = location(waypoint[0], waypoint[1])
} }
} }
if (stopSearch(nearestDistance, distance)) {
break
}
if (distance > lastDistance) {
increaseDistance++
}
lastDistance = distance
} }
} }
if (stopSearch(nearestDistance, distance)) { if (nearestDistance < NEAREST_LOCATION_DISTANCE) {
break break
} }
} }
} }
private fun stopSearch(nearestDistance: Float, distance: Float): Boolean {
return nearestDistance < NEAREST_LOCATION_DISTANCE && distance > NEAREST_LOCATION_DISTANCE * 10
}
fun travelLeftTime(): Double { fun travelLeftTime(): Double {
var timeLeft = 0.0 var timeLeft = 0.0
// time for next step until end step // time for next step until end step
@@ -93,15 +70,19 @@ class RouteCalculator(var routeModel: RouteModel) {
fun leftStepDistance(): Double { fun leftStepDistance(): Double {
val step = routeModel.route.currentStep() val step = routeModel.route.currentStep()
var leftDistance = 0F var leftDistance = 0F
if (step.waypointIndex < step.maneuver.waypoints.size - 1) { for (i in step.waypointIndex..<step.maneuver.waypoints.size - 1) {
leftDistance = step.maneuver.leftDistance[step.waypointIndex] val loc1 = location(step.maneuver.waypoints[i][0], step.maneuver.waypoints[i][1])
val waypointLocation = step.maneuver.waypoints[step.waypointIndex] val loc2 =
if (routeModel.navState.lastLocation.latitude != 0.0) { location(step.maneuver.waypoints[i + 1][0], step.maneuver.waypoints[i + 1][1])
val locationDistance = waypointLocation.distanceTo(routeModel.navState.lastLocation) val locationDistance = loc1.distanceTo(routeModel.navState.lastLocation)
leftDistance -= locationDistance val distance = loc1.distanceTo(loc2)
leftDistance += if (locationDistance < distance) {
locationDistance
} else {
distance
} }
} }
return leftDistance.absoluteValue.toDouble() return leftDistance.toDouble()
} }
/** Returns the left distance in m. */ /** Returns the left distance in m. */
@@ -126,31 +107,14 @@ class RouteCalculator(var routeModel: RouteModel) {
return nowUtcMillis + timeToDestinationMillis return nowUtcMillis + timeToDestinationMillis
} }
/**
* Updates the speed limit in the view model.
*/
fun updateSpeedLimit(location: Location, viewModel: NavigationViewModel) { fun updateSpeedLimit(location: Location, viewModel: NavigationViewModel) {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
// speed limit // speed limit
val distance = lastSpeedLocation.distanceTo(location) val distance = lastSpeedLocation.distanceTo(location)
if ((distance > SPEED_UPDATE_DISTANCE * 2) || lastSpeedIndex < routeModel.route.currentStepIndex) { if (distance > 500 || 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. viewModel.getMaxSpeed(location, routeModel.route.currentStep().street)
lastLocalSpeedLocation = location(0.0, 0.0)
viewModel.updateSpeedLimit(
location,
routeModel.route.currentStep().street,
routeModel.currentStep().roadNumbers
)
} else if (lastLocalSpeedLocation.distanceTo(location) >= NEAREST_LOCATION_DISTANCE) {
lastLocalSpeedLocation = location
viewModel.getSpeedLimit(
location,
routeModel.navState.routeBearing,
routeModel.currentStep.countryCode
)
} }
} }
} }
@@ -1,12 +1,10 @@
package com.kouros.navigation.model package com.kouros.navigation.model
import android.location.Location import android.location.Location
import android.util.Log
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.NavigationState import com.kouros.navigation.data.NavigationState
import com.kouros.navigation.data.Route import com.kouros.navigation.data.Route
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
@@ -107,8 +105,7 @@ open class RouteModel {
leftDistance = routeCalculator.travelLeftDistance(), leftDistance = routeCalculator.travelLeftDistance(),
lane = currentLanes, lane = currentLanes,
exitNumber = exitNumber, exitNumber = exitNumber,
message = currentStep.maneuver.message, message = currentStep.maneuver.message
roadNumbers = currentStep.roadNumbers
) )
} }
@@ -170,7 +167,7 @@ open class RouteModel {
/** /**
* Checks for arrival * Checks for arrival
*/ */
fun isManeuverArrival(): Boolean { fun isArrival(): Boolean {
return navState.maneuverType == Maneuver.TYPE_DESTINATION return navState.maneuverType == Maneuver.TYPE_DESTINATION
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT || navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT || navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
@@ -105,18 +105,6 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
false false
) )
val lastFuelPrices = repository.lastFuelPricesFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
0
)
val fuelPrices = repository.fuelPricesFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
0
)
fun onShow3DChanged(enabled: Boolean) { fun onShow3DChanged(enabled: Boolean) {
viewModelScope.launch { repository.setShow3D(enabled) } viewModelScope.launch { repository.setShow3D(enabled) }
} }
@@ -177,12 +165,4 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
fun onAlternativeRoutes(enabled: Boolean) { fun onAlternativeRoutes(enabled: Boolean) {
viewModelScope.launch { repository.setAlternativeRoutes(enabled) } viewModelScope.launch { repository.setAlternativeRoutes(enabled) }
} }
fun onLastFuelPricesChanged(lastFuelPrices: Long) {
viewModelScope.launch { repository.setLastFuelPrices(lastFuelPrices) }
}
fun onFuelPricesChanged(fuelPrices: String) {
viewModelScope.launch { repository.setFuelPrices(fuelPrices) }
}
} }
@@ -2,7 +2,6 @@ package com.kouros.navigation.repository
import android.util.Log import android.util.Log
import com.kouros.navigation.data.datastore.DataStoreManager import com.kouros.navigation.data.datastore.DataStoreManager
import com.kouros.navigation.data.fuel.FuelPrices
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class SettingsRepository( class SettingsRepository(
@@ -55,13 +54,6 @@ class SettingsRepository(
val alternativeRoutesFlow: Flow<Boolean> = val alternativeRoutesFlow: Flow<Boolean> =
dataStoreManager.alternativeRoutesFlow dataStoreManager.alternativeRoutesFlow
val lastFuelPricesFlow: Flow<Long> =
dataStoreManager.lastFuelPricesFlow
val fuelPricesFlow: Flow<String> =
dataStoreManager.fuelPricesFlow
suspend fun setShow3D(enabled: Boolean) { suspend fun setShow3D(enabled: Boolean) {
dataStoreManager.setShow3D(enabled) dataStoreManager.setShow3D(enabled)
} }
@@ -125,12 +117,4 @@ class SettingsRepository(
suspend fun setAlternativeRoutes(enabled: Boolean) { suspend fun setAlternativeRoutes(enabled: Boolean) {
dataStoreManager.setAlternativeRoutes(enabled) dataStoreManager.setAlternativeRoutes(enabled)
} }
suspend fun setLastFuelPrices(lastFuelPrices: Long) {
dataStoreManager.setLastFuelPrices(lastFuelPrices)
}
suspend fun setFuelPrices(fuelPrices: String) {
dataStoreManager.setFuelPrices(fuelPrices)
}
} }
@@ -4,28 +4,21 @@ import android.location.Location
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.LineString
import org.maplibre.geojson.Point import org.maplibre.geojson.Point
import org.maplibre.spatialk.geojson.Feature import org.maplibre.spatialk.geojson.Feature
import org.maplibre.spatialk.geojson.dsl.addFeature import org.maplibre.spatialk.geojson.dsl.addFeature
import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection
import org.maplibre.spatialk.geojson.dsl.buildLineString import org.maplibre.spatialk.geojson.dsl.buildLineString
import org.maplibre.spatialk.geojson.dsl.buildMultiPoint
import org.maplibre.spatialk.geojson.toJson import org.maplibre.spatialk.geojson.toJson
import org.maplibre.turf.TurfMeasurement import org.maplibre.turf.TurfMeasurement
import org.maplibre.turf.TurfMisc import org.maplibre.turf.TurfMisc
import java.lang.Math.toDegrees import java.lang.Math.toDegrees
import java.lang.Math.toRadians import java.lang.Math.toRadians
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sin
object GeoUtils { object GeoUtils {
const val EARTH_RADIUS = 6371.0 // in km
fun snapLocation(location: Location, stepCoordinates: List<Point>) : Location { fun snapLocation(location: Location, stepCoordinates: List<Point>) : Location {
val newLocation = Location(location) val newLocation = Location(location)
val oldPoint = Point.fromLngLat(location.longitude, location.latitude) val oldPoint = Point.fromLngLat(location.longitude, location.latitude)
@@ -101,12 +94,10 @@ object GeoUtils {
// return createPointCollection(lineCoordinates, "Route") // return createPointCollection(lineCoordinates, "Route")
val lineString = buildLineString { val lineString = buildLineString {
lineCoordinates.forEach { lineCoordinates.forEach {
add( add(org.maplibre.spatialk.geojson.Point(
org.maplibre.spatialk.geojson.Point(
it[0], it[0],
it[1] it[1]
) ))
)
} }
} }
val feature = Feature(lineString, null) val feature = Feature(lineString, null)
@@ -126,150 +117,31 @@ object GeoUtils {
return featureCollection.toJson() return featureCollection.toJson()
} }
fun createStartCollection(geoJson: String): String {
val featureCollection = FeatureCollection.fromJson(geoJson)
val geometry = featureCollection.features()!!.first().geometry()
val coordinates = (geometry as LineString)
val first = coordinates.coordinates().first()
val points = createPointCollection(
listOf(listOf(first.coordinates()[0], first.coordinates()[1])), "End"
)
return points
}
fun createPointCollection(geoJson: String): String {
val featureCollection = FeatureCollection.fromJson(geoJson)
val geometry = featureCollection.features()!!.first().geometry()
val coordinates = (geometry as LineString)
val last = coordinates.coordinates().last()
val points = createPointCollection(
listOf(listOf(last.coordinates()[0], last.coordinates()[1])), "End"
)
return points
}
/** /**
* Calculate the lat and len of a square around a point. * Calculate the lat and len of a square around a point.
* @return latMin, latMax, lngMin, lngMax * @return latMin, latMax, lngMin, lngMax
*/ */
fun calculateSquareRadius(lat: Double, lng: Double, radius: Double): String { fun calculateSquareRadius(lat: Double, lng: Double, radius: Double): String {
val latMin = lat - toDegrees(radius / EARTH_RADIUS) val earthRadius = 6371.0 // earth radius in km
val latMax = lat + toDegrees(radius / EARTH_RADIUS) val latMin = lat - toDegrees(radius / earthRadius)
val lngMin = lng - toDegrees(radius / EARTH_RADIUS / cos(toRadians(lat))) val latMax = lat + toDegrees(radius / earthRadius)
val lngMax = lng + toDegrees(radius / EARTH_RADIUS / cos(toRadians(lat))) val lngMin = lng - toDegrees(radius / earthRadius / cos(toRadians(lat)))
val lngMax = lng + toDegrees(radius / earthRadius / cos(toRadians(lat)))
return "$lngMin,$latMin,$lngMax,$latMax" return "$lngMin,$latMin,$lngMax,$latMax"
} }
fun getBoundingBox( fun getBoundingBox(
lat: Double, lat: Double,
lon: Double, lon: Double,
radius: Double radius: Double
): String { ): String {
val maxLat = lat + toDegrees(radius / EARTH_RADIUS) val earthRadius = 6371.0
val minLat = lat - toDegrees(radius / EARTH_RADIUS) val maxLat = lat + toDegrees(radius / earthRadius)
val maxLon = lon + toDegrees(radius / EARTH_RADIUS / cos(toRadians(lat))) val minLat = lat - toDegrees(radius / earthRadius)
val minLon = lon - toDegrees(radius / EARTH_RADIUS / cos(toRadians(lat))) val maxLon = lon + toDegrees(radius / earthRadius / cos(toRadians(lat)))
val minLon = lon - toDegrees(radius / earthRadius / cos(toRadians(lat)))
return "$minLat,$minLon,$maxLat,$maxLon" return "$minLat,$minLon,$maxLat,$maxLon"
} }
fun isLocationInBoundingBox(
bottomLeftLat: Double,
bottomLeftLon: Double,
topRightLat: Double,
topRightLon: Double,
location: Location
): Boolean {
val isInside =
location.latitude in bottomLeftLat..topRightLat
&& location.longitude >= bottomLeftLon
&& location.longitude <= topRightLon
return isInside
}
fun buildLineStrings(features: List<org.maplibre.geojson.Feature>): String {
return buildFeatureCollection {
features.forEach {
val movedCoordinates = arrayListOf<org.maplibre.spatialk.geojson.Point>()
val coordinates = it.geometry() as LineString
val geo = coordinates.coordinates()
var bearing = 0.0F
for (index in 0..<geo.size) {
val current = location(geo[index].longitude(), geo[index].latitude())
bearing = if (index < geo.size - 1) {
val next =
location(geo[index + 1].longitude(), geo[index + 1].latitude())
current.bearingPositive(next)
} else {
bearing
}
val point = movePoint(
current.latitude,
current.longitude,
bearingDegrees = bearing + 90.0
)
movedCoordinates.add(point)
}
val lineString = buildLineString {
movedCoordinates.forEach { ft ->
add(
ft
)
}
}
addFeature {
geometry = lineString
properties = buildJsonObject { null }
}
}
}.toJson()
}
fun buildPointCollection(features: List<org.maplibre.geojson.Feature>): String {
return buildFeatureCollection {
features.forEach {
val movedCoordinates = arrayListOf<org.maplibre.spatialk.geojson.Point>()
val coordinates = it.geometry() as LineString
val geo = coordinates.coordinates()
var point = org.maplibre.spatialk.geojson.Point(geo.first().longitude(), geo.first().latitude())
movedCoordinates.add(point)
point = org.maplibre.spatialk.geojson.Point(geo.last().longitude(), geo.last().latitude())
movedCoordinates.add(point)
val multiPoint = buildMultiPoint {
movedCoordinates.forEach { ft ->
add(
ft
)
}
}
addFeature {
geometry = multiPoint
properties = buildJsonObject { null }
}
}
}.toJson()
}
fun movePoint(
latitude: Double,
longitude: Double,
distanceMeters: Double = 5.0,
bearingDegrees: Double = 0.0
): org.maplibre.spatialk.geojson.Point {
val radius = EARTH_RADIUS * 1000 // meters
val bearingRadians = toRadians(bearingDegrees)
val lat1 = toRadians(latitude)
val lon1 = toRadians(longitude)
val lat2 = asin(
sin(lat1) * cos(distanceMeters / radius) +
cos(lat1) * sin(distanceMeters / radius) * cos(bearingRadians)
)
val lon2 = lon1 + atan2(
sin(bearingRadians) * sin(distanceMeters / radius) * cos(lat1),
cos(distanceMeters / radius) - sin(lat1) * sin(lat2)
)
return org.maplibre.spatialk.geojson.Point(toDegrees(lon2), toDegrees(lat2))
}
} }
@@ -25,11 +25,10 @@ class Levenshtein {
* @param limit the maximum result to compute before stopping, terminating calculation early. * @param limit the maximum result to compute before stopping, terminating calculation early.
* @return the computed Levenshtein distance. * @return the computed Levenshtein distance.
*/ */
fun distance(first: CharSequence, second: CharSequence, countryCode: String, limit: Int = Int.MAX_VALUE): Int { fun distance(first: CharSequence, second: CharSequence, limit: Int = Int.MAX_VALUE): Int {
if (countryCode == "GRC") return 0
if (first == second) return 0 if (first == second) return 0
if (first.isEmpty()) return 0 if (first.isEmpty()) return second.length
if (second.isEmpty()) return 0 if (second.isEmpty()) return first.length
// initial costs is the edit distance from an empty string, which corresponds to the characters to inserts. // initial costs is the edit distance from an empty string, which corresponds to the characters to inserts.
// the array size is : length + 1 (empty string) // the array size is : length + 1 (empty string)
@@ -3,10 +3,11 @@ package com.kouros.navigation.utils
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.util.Log
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TILT import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.osrm.OsrmRepository import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.data.valhalla.ValhallaRepository
@@ -27,7 +28,6 @@ import kotlin.math.cos
import kotlin.math.ln import kotlin.math.ln
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.ranges.contains
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -49,20 +49,18 @@ object NavigationUtils {
} }
fun calculateZoom(speed: Double?): Double { fun calculateZoom(speed: Double?): Double {
val zoom = 17.0
if (speed == null) { if (speed == null) {
return zoom return 17.0
} }
val speedKmh = (speed * 3.6).toInt() val speedKmh = (speed * 3.6).toInt()
return when (speedKmh) { val zoom = when (speedKmh) {
in 0..10 -> zoom + 1.0 in 0..10 -> 17.0
in 11..30 -> zoom + 0.5 in 11..30 -> 17.5
in 31..65 -> zoom in 31..65 -> 17.0
in 66..70 -> zoom - 0.5 in 66..70 -> 16.5
in 71..90 -> zoom - 1.0 else -> 16.0
in 91..100 -> zoom - 2.0
else -> zoom - 3.0
} }
return zoom
} }
fun previewZoom(centerLocation: Location, previewDistance: Double): Double { fun previewZoom(centerLocation: Location, previewDistance: Double): Double {
@@ -95,8 +93,7 @@ fun calculateZoomFromBoundingBox(centerLocation: Location, previewDistance: Doub
} }
fun calculateTilt(viewStyle: ViewStyle, newZoom: Double, tilt: Double): Double = fun calculateTilt(newZoom: Double, tilt: Double): Double =
if (viewStyle == ViewStyle.VIEW) {
if (newZoom < 13) { if (newZoom < 13) {
0.0 0.0
} else { } else {
@@ -106,16 +103,14 @@ fun calculateTilt(viewStyle: ViewStyle, newZoom: Double, tilt: Double): Double =
tilt tilt
} }
} }
} else {
return 0.0
}
fun bearingPositive(fromLocation: Location, toLocation: Location, oldBearing: Double): Double { fun bearing(fromLocation: Location, toLocation: Location, oldBearing: Double): Double {
val distance = fromLocation.distanceTo(toLocation) val distance = fromLocation.distanceTo(toLocation)
if (distance < 1.0) { if (distance < 1.0) {
return oldBearing return oldBearing
} }
return fromLocation.bearingPositive(toLocation).toInt().toDouble() val bearing = fromLocation.bearingTo(toLocation).toInt().toDouble()
return bearing
} }
fun location(longitude: Double, latitude: Double): Location { fun location(longitude: Double, latitude: Double): Location {
@@ -125,10 +120,6 @@ fun location(longitude: Double, latitude: Double): Location {
return location return location
} }
fun Location.bearingPositive(locationTo: Location): Float {
return (this.bearingTo (locationTo) + 360) % 360
}
fun formatDateTime(time: Long): String { fun formatDateTime(time: Long): String {
val dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) val dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
val dateTime = LocalDateTime.ofEpochSecond(time / 1000, 0, ZoneOffset.UTC) val dateTime = LocalDateTime.ofEpochSecond(time / 1000, 0, ZoneOffset.UTC)
@@ -141,26 +132,20 @@ fun Double.round(numFractionDigits: Int): Double {
return (this * factor).roundToInt() / factor return (this * factor).roundToInt() / factor
} }
fun isNumeric(toCheck: String): Boolean {
val regex = "-?[0-9]+(\\.[0-9]+)?".toRegex()
return toCheck.matches(regex)
}
fun duration( fun duration(
viewStyle: ViewStyle, preview: Boolean,
bearing: Double, bearing: Double,
lastBearing: Double, lastBearing: Double,
lastLocationUpdate: LocalDateTime lastLocationUpdate: LocalDateTime
): Duration { ): Duration {
if (viewStyle == ViewStyle.PREVIEW || if (preview) {
viewStyle == ViewStyle.AMENITY_VIEW) { return 10.milliseconds
return 100.milliseconds
} }
val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) { val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) {
2.seconds 2.seconds
} else { } else {
val updateDuration = java.time.Duration.between(LocalDateTime.now(), lastLocationUpdate) val updateDuration = java.time.Duration.between(LocalDateTime.now(), lastLocationUpdate)
if (updateDuration.toMillis().absoluteValue !in 1000..2000) { if (updateDuration.toMillis().absoluteValue < 1000) {
2.seconds 2.seconds
} else { } else {
((updateDuration!!.toMillis().absoluteValue * 1.8).toDuration(DurationUnit.MILLISECONDS)) ((updateDuration!!.toMillis().absoluteValue * 1.8).toDuration(DurationUnit.MILLISECONDS))
@@ -198,11 +183,3 @@ fun formattedDistance(distanceMode: Int, distance: Double): Pair<Double, Int> {
} }
return Pair(currentDistance, displayUnit) return Pair(currentDistance, displayUnit)
} }
fun countryCodeSpeedLimit(countryCode: String) : Int {
return when (countryCode) {
"DEU", "FRA", "AUT", "GRE", "NLD", "ITA", "SLO", "SVK", "CZE" -> 130
"POL", "BEL", "ESP", "PRT", "BGR", "HUN", "FIN" -> 120
else -> 100
}
}
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M620,680Q645,680 662.5,662.5Q680,645 680,620Q680,595 662.5,577.5Q645,560 620,560Q595,560 577.5,577.5Q560,595 560,620Q560,645 577.5,662.5Q595,680 620,680ZM260,680Q285,680 302.5,662.5Q320,645 320,620Q320,595 302.5,577.5Q285,560 260,560Q235,560 217.5,577.5Q200,595 200,620Q200,645 217.5,662.5Q235,680 260,680ZM680,480Q597,480 538.5,421.5Q480,363 480,280Q480,198 538,139Q596,80 680,80Q763,80 821.5,138.5Q880,197 880,280Q880,363 821.5,421.5Q763,480 680,480ZM660,320L700,320L700,160L660,160L660,320ZM680,400Q688,400 694,394Q700,388 700,380Q700,372 694,366Q688,360 680,360Q672,360 666,366Q660,372 660,380Q660,388 666,394Q672,400 680,400ZM120,880Q103,880 91.5,868.5Q80,857 80,840L80,520L164,280Q170,262 185.5,251Q201,240 220,240L403,240Q400,260 400,280Q400,300 403,320L234,320L192,440L451,440Q491,497 551,528.5Q611,560 680,560Q711,560 741.5,553.5Q772,547 800,533L800,840Q800,857 788.5,868.5Q777,880 760,880L720,880Q703,880 691.5,868.5Q680,857 680,840L680,800L200,800L200,840Q200,857 188.5,868.5Q177,880 160,880L120,880Z"/>
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M756,840L537,621L621,537L840,756L756,840ZM204,840L120,756L396,480L328,412L300,440L249,389L249,471L221,499L100,378L128,350L210,350L160,300L302,158Q322,138 345,129Q368,120 392,120Q416,120 439,129Q462,138 482,158L390,250L440,300L412,328L480,396L570,306Q566,295 563.5,283Q561,271 561,259Q561,200 601.5,159.5Q642,119 701,119Q716,119 729.5,122Q743,125 757,131L658,230L730,302L829,203Q836,217 838.5,230.5Q841,244 841,259Q841,318 800.5,358.5Q760,399 701,399Q689,399 677,397Q665,395 654,390L204,840Z"/>
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M280,520L680,520L680,440L280,440L280,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z"/>
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M720,520L720,160L800,160L800,520L720,520ZM160,800L160,160L240,160L240,800L160,800ZM440,320L440,160L520,160L520,320L440,320ZM440,560L440,400L520,400L520,560L440,560ZM440,800L440,640L520,640L520,800L440,800ZM617,823L702,738L617,654L674,597L759,682L844,597L900,654L815,739L899,824L844,880L758,795L673,880L617,823Z"/>
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M280,880L280,514Q229,500 194.5,458Q160,416 160,360L160,80L240,80L240,360L280,360L280,80L360,80L360,360L400,360L400,80L480,80L480,360Q480,416 445.5,458Q411,500 360,514L360,880L280,880ZM680,880L680,560L560,560L560,280Q560,197 618.5,138.5Q677,80 760,80L760,880L680,880Z"/>
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M360,240L440,240L440,160L360,160L360,240ZM520,240L520,160L600,160L600,240L520,240ZM360,560L360,480L440,480L440,560L360,560ZM680,400L680,320L760,320L760,400L680,400ZM680,560L680,480L760,480L760,560L680,560ZM520,560L520,480L600,480L600,560L520,560ZM680,240L680,160L760,160L760,240L680,240ZM440,320L440,240L520,240L520,320L440,320ZM200,800L200,160L280,160L280,240L360,240L360,320L280,320L280,400L360,400L360,480L280,480L280,800L200,800ZM600,480L600,400L680,400L680,480L600,480ZM440,480L440,400L520,400L520,480L440,480ZM360,400L360,320L440,320L440,400L360,400ZM520,400L520,320L600,320L600,400L520,400ZM600,320L600,240L680,240L680,320L600,320Z"/>
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M390,220L450,220L450,160L390,160L390,220ZM510,220L510,160L570,160L570,220L510,220ZM390,460L390,400L450,400L450,460L390,460ZM630,340L630,280L690,280L690,340L630,340ZM630,460L630,400L690,400L690,460L630,460ZM510,460L510,400L570,400L570,460L510,460ZM630,220L630,160L690,160L690,220L630,220ZM450,280L450,220L510,220L510,280L450,280ZM270,800L270,160L330,160L330,220L390,220L390,280L330,280L330,340L390,340L390,400L330,400L330,800L270,800ZM570,400L570,340L630,340L630,400L570,400ZM450,400L450,340L510,340L510,400L450,400ZM390,340L390,280L450,280L450,340L390,340ZM510,340L510,280L570,280L570,340L510,340ZM570,280L570,220L630,220L630,280L570,280Z"/>
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M80,880Q63,880 51.5,868.5Q40,857 40,840L40,520L125,317Q132,300 147,290Q162,280 180,280L540,280Q558,280 573,290Q588,300 595,317L680,520L680,840Q680,857 668.5,868.5Q657,880 640,880L600,880Q583,880 571.5,868.5Q560,857 560,840L560,800L160,800L160,840Q160,857 148.5,868.5Q137,880 120,880L80,880ZM152,440L567,440L534,360L186,360L152,440ZM262.5,662.5Q280,645 280,620Q280,595 262.5,577.5Q245,560 220,560Q195,560 177.5,577.5Q160,595 160,620Q160,645 177.5,662.5Q195,680 220,680Q245,680 262.5,662.5ZM542.5,662.5Q560,645 560,620Q560,595 542.5,577.5Q525,560 500,560Q475,560 457.5,577.5Q440,595 440,620Q440,645 457.5,662.5Q475,680 500,680Q525,680 542.5,662.5ZM720,760L720,416L647,240L227,240L245,197Q252,180 267,170Q282,160 300,160L660,160Q678,160 693,170Q708,180 715,197L800,400L800,720Q800,737 788.5,748.5Q777,760 760,760L720,760ZM840,640L840,296L767,120L347,120L365,77Q372,60 387,50Q402,40 420,40L780,40Q798,40 813,50Q828,60 835,77L920,280L920,600Q920,617 908.5,628.5Q897,640 880,640L840,640Z"/>
</vector>
+6 -26
View File
@@ -66,30 +66,10 @@
<string name="general">Allgemein</string> <string name="general">Allgemein</string>
<string name="traffic">Verkehr anzeigen</string> <string name="traffic">Verkehr anzeigen</string>
<string name="trip_suggestion">Fahrten-Vorschläge</string> <string name="trip_suggestion">Fahrten-Vorschläge</string>
<string name="drive_settings">Fahr-Einstellungen</string> <string name="drive_settings">Drive settings</string>
<string name="car_settings">Fahrzeug-Einstellungen</string> <string name="car_settings">Car settings</string>
<string name="combustion">Verbrenner</string> <string name="combustion">Combustion</string>
<string name="electric">Elektro</string> <string name="electric">Electric</string>
<string name="engine_type">Motortyp</string> <string name="engine_type">Engine type</string>
<string name="alternative_routes">Alternative Routen</string> <string name="alternative_routes">Alternative routes</string>
<string name="wait">Warten</string>
<string name="restaurant">Restaurant</string>
<!-- CarHardwareInfoScreen -->
<string name="car_hardware_info">Car Hardware Information</string>
<string name="model_info">Model Information</string>
<string name="no_model_permission">No Model Permission</string>
<string name="no_speed_permission">No Speed Permission</string>
<string name="manufacturer_unavailable">Manufacturer unavailable</string>
<string name="model_unavailable">Model unavailable</string>
<string name="year_unavailable">Year unavailable</string>
<string name="energy_profile">Energy Profile</string>
<string name="no_energy_profile_permission">No Energy Profile Permission</string>
<string name="fuel_types">Fuel Types</string>
<string name="unavailable">Unavailable</string>
<string name="ev_connector_types">EV Connector Types</string>
<string name="car_sensors">Car Sensors</string>
<string name="speed">Speed</string>
<string name="speed_unavailable">Speed unavailable</string>
</resources> </resources>
+6 -26
View File
@@ -50,30 +50,10 @@
<string name="general">Γενικά</string> <string name="general">Γενικά</string>
<string name="traffic">Εμφάνιση κίνησης</string> <string name="traffic">Εμφάνιση κίνησης</string>
<string name="trip_suggestion">Προτάσεις διαδρομής</string> <string name="trip_suggestion">Προτάσεις διαδρομής</string>
<string name="drive_settings">Ρυθμίσεις οδήγησης</string> <string name="drive_settings">Drive settings</string>
<string name="car_settings">Ρυθμίσεις αυτοκινήτου</string> <string name="car_settings">Car settings</string>
<string name="combustion">Κινητήρας εσωτερικής καύσης</string> <string name="combustion">Combustion</string>
<string name="electric">Ηλεκτρικό</string> <string name="electric">Electric</string>
<string name="engine_type">Τύπος κινητήρα</string> <string name="engine_type">Engine type</string>
<string name="alternative_routes">Εναλλακτικές διαδρομές</string> <string name="alternative_routes">Alternative routes</string>
<string name="wait">Περιμένετε</string>
<string name="restaurant">Restaurant</string>
<!-- CarHardwareInfoScreen -->
<string name="car_hardware_info">Car Hardware Information</string>
<string name="model_info">Model Information</string>
<string name="no_model_permission">No Model Permission</string>
<string name="no_speed_permission">No Speed Permission</string>
<string name="manufacturer_unavailable">Manufacturer unavailable</string>
<string name="model_unavailable">Model unavailable</string>
<string name="year_unavailable">Year unavailable</string>
<string name="energy_profile">Energy Profile</string>
<string name="no_energy_profile_permission">No Energy Profile Permission</string>
<string name="fuel_types">Fuel Types</string>
<string name="unavailable">Unavailable</string>
<string name="ev_connector_types">EV Connector Types</string>
<string name="car_sensors">Car Sensors</string>
<string name="speed">Speed</string>
<string name="speed_unavailable">Speed unavailable</string>
</resources> </resources>
+6 -26
View File
@@ -50,30 +50,10 @@
<string name="general">Ogólne</string> <string name="general">Ogólne</string>
<string name="traffic">Pokaż natężenie ruchu</string> <string name="traffic">Pokaż natężenie ruchu</string>
<string name="trip_suggestion">Sugestie dotyczące podróży</string> <string name="trip_suggestion">Sugestie dotyczące podróży</string>
<string name="drive_settings">Ustawienia jazdy</string> <string name="drive_settings">Drive settings</string>
<string name="car_settings">Ustawienia samochodu</string> <string name="car_settings">Car settings</string>
<string name="combustion">Spalinowy</string> <string name="combustion">Combustion</string>
<string name="electric">Elektryczny</string> <string name="electric">Electric</string>
<string name="engine_type">Typ silnika</string> <string name="engine_type">Engine type</string>
<string name="alternative_routes">Alternatywne trasy</string> <string name="alternative_routes">Alternative routes</string>
<string name="wait">Czekaj</string>
<string name="restaurant">Restaurant</string>
<!-- CarHardwareInfoScreen -->
<string name="car_hardware_info">Car Hardware Information</string>
<string name="model_info">Model Information</string>
<string name="no_speed_permission">No Speed Permission</string>
<string name="no_model_permission">No Model Permission</string>
<string name="manufacturer_unavailable">Manufacturer unavailable</string>
<string name="model_unavailable">Model unavailable</string>
<string name="year_unavailable">Year unavailable</string>
<string name="energy_profile">Energy Profile</string>
<string name="no_energy_profile_permission">No Energy Profile Permission</string>
<string name="fuel_types">Fuel Types</string>
<string name="unavailable">Unavailable</string>
<string name="ev_connector_types">EV Connector Types</string>
<string name="car_sensors">Car Sensors</string>
<string name="speed">Speed</string>
<string name="speed_unavailable">Speed unavailable</string>
</resources> </resources>
@@ -59,24 +59,4 @@
<string name="electric">Electric</string> <string name="electric">Electric</string>
<string name="engine_type">Engine type</string> <string name="engine_type">Engine type</string>
<string name="alternative_routes">Alternative routes</string> <string name="alternative_routes">Alternative routes</string>
<string name="wait">Wait</string>
<string name="restaurant">Restaurant</string>
<!-- CarHardwareInfoScreen -->
<string name="car_hardware_info">Car Hardware Information</string>
<string name="model_info">Model Information</string>
<string name="no_model_permission">No Model Permission</string>
<string name="no_speed_permission">No Speed Permission</string>
<string name="manufacturer_unavailable">Manufacturer unavailable</string>
<string name="model_unavailable">Model unavailable</string>
<string name="year_unavailable">Year unavailable</string>
<string name="energy_profile">Energy Profile</string>
<string name="no_energy_profile_permission">No Energy Profile Permission</string>
<string name="fuel_types">Fuel Types</string>
<string name="unavailable">Unavailable</string>
<string name="ev_connector_types">EV Connector Types</string>
<string name="car_sensors">Car Sensors</string>
<string name="speed">Speed</string>
<string name="speed_unavailable">Speed unavailable</string>
</resources> </resources>
File diff suppressed because one or more lines are too long
@@ -7,7 +7,6 @@ import com.kouros.navigation.data.route.Maneuver
import com.kouros.navigation.data.route.Routes import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.data.route.Step import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.utils.location
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
@@ -45,12 +44,12 @@ class RouteCalculatorTest {
waypointIndex: Int = 0, waypointIndex: Int = 0,
): Step { ): Step {
val waypoints = (0 until numWaypoints).map { i -> val waypoints = (0 until numWaypoints).map { i ->
location(11.0 + index * 0.01 + i * 0.001, 48.0) listOf(11.0 + index * 0.01 + i * 0.001, 48.0)
} }
return Step( return Step(
index = index, index = index,
waypointIndex = waypointIndex, waypointIndex = waypointIndex,
maneuver = Maneuver(waypoints = waypoints, location = mock(), leftDistance = mock() ), maneuver = Maneuver(waypoints = waypoints, location = mock()),
duration = duration, duration = duration,
distance = distance, distance = distance,
) )
@@ -101,7 +100,7 @@ class RouteCalculatorTest {
} }
@Test @Test
fun `findStep considers previous step when searching`() { fun `findStep skips all steps before currentStepIndex`() {
val step0 = createStep(index = 0, numWaypoints = 2) val step0 = createStep(index = 0, numWaypoints = 2)
val step1 = createStep(index = 1, numWaypoints = 2) val step1 = createStep(index = 1, numWaypoints = 2)
routeModel.navState = routeModel.navState.copy( routeModel.navState = routeModel.navState.copy(
@@ -109,17 +108,17 @@ class RouteCalculatorTest {
) )
val mockLocation: Location = mock() val mockLocation: Location = mock()
// Distance to step0 waypoints is very small, distance to step1 waypoints is large whenever(mockLocation.distanceTo(any())).thenReturn(200F, 50F)
whenever(mockLocation.distanceTo(any())).thenReturn(5F, 5F, 500F, 500F)
routeCalculator.findStep(mockLocation) routeCalculator.findStep(mockLocation)
assertEquals(0, routeModel.navState.route.currentStepIndex) // step0 is skipped, so distanceTo is only called for step1's 2 waypoints
verify(mockLocation, times(2)).distanceTo(any())
assertEquals(1, routeModel.navState.route.currentStepIndex)
} }
@Test @Test
fun `findStep breaks later with relaxed distance threshold`() { fun `findStep breaks early once nearestDistance drops below NEAREST_LOCATION_DISTANCE`() {
val step0 = createStep(index = 0, numWaypoints = 2) val step0 = createStep(index = 0, numWaypoints = 2)
val step1 = createStep(index = 1, numWaypoints = 2) val step1 = createStep(index = 1, numWaypoints = 2)
val step2 = createStep(index = 2, numWaypoints = 2) val step2 = createStep(index = 2, numWaypoints = 2)
@@ -128,18 +127,16 @@ class RouteCalculatorTest {
) )
val mockLocation: Location = mock() val mockLocation: Location = mock()
// step0/wp0: 500F, step0/wp1: 5F, step1/wp0: 150F, step1/wp1: 160F, step2/wp0: 210F, step2/wp1: 220F // step0/wp0: 500F, step0/wp1: 5F — 5F < NEAREST_LOCATION_DISTANCE (10F) → break
// Here we purposefully exceed the 20 * 10F threshold at the end of step 1 or start of step 2 whenever(mockLocation.distanceTo(any())).thenReturn(500F, 5F)
whenever(mockLocation.distanceTo(any())).thenReturn(500F, 5F, 150F, 160F, 210F, 220F)
routeCalculator.findStep(mockLocation) routeCalculator.findStep(mockLocation)
// It should haveChecked step 0 (2), step 1 (2), and the first point of step 2 (1) where it finally breaks. // step1 and step2 are never evaluated
verify(mockLocation, times(5)).distanceTo(any()) verify(mockLocation, times(2)).distanceTo(any())
assertEquals(0, routeModel.navState.route.currentStepIndex) assertEquals(0, routeModel.navState.route.currentStepIndex)
} }
// ---------------------------------------------------------- // ----------------------------------------------------------
// travelLeftTime // travelLeftTime
// ---------------------------------------------------------- // ----------------------------------------------------------
@@ -8,12 +8,8 @@ import com.kouros.navigation.data.route.Maneuver
import com.kouros.navigation.data.route.Routes import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.data.route.Step import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.location
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.LineString
import org.mockito.kotlin.any import org.mockito.kotlin.any
import org.mockito.kotlin.doNothing import org.mockito.kotlin.doNothing
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
@@ -40,12 +36,12 @@ class RouteModelTest {
waypointIndex: Int = 0, waypointIndex: Int = 0,
): Step { ): Step {
val waypoints = (0 until numWaypoints).map { i -> val waypoints = (0 until numWaypoints).map { i ->
location(11.0 + index * 0.01 + i * 0.001, 48.0) listOf(11.0 + index * 0.01 + i * 0.001, 48.0)
} }
return Step( return Step(
index = index, index = index,
waypointIndex = waypointIndex, waypointIndex = waypointIndex,
maneuver = Maneuver(waypoints = waypoints, location = mock(), leftDistance = mock()), maneuver = Maneuver(waypoints = waypoints, location = mock()),
duration = duration, duration = duration,
distance = distance, distance = distance,
) )
@@ -62,18 +58,6 @@ class RouteModelTest {
return Route(routeEngine = 2, routes = listOf(routes), currentStepIndex = currentStepIndex) return Route(routeEngine = 2, routes = listOf(routes), currentStepIndex = currentStepIndex)
} }
@Test
fun `create Point Collection returns false when route has no legs`() {
val geoJson = routeModel.curRoute.routeGeoJson
val featureCollection = FeatureCollection.fromJson(geoJson)
val geometry = featureCollection.features()!!.first().geometry()
val coordinates = (geometry as LineString)
val first = coordinates.coordinates().first()
val last = coordinates.coordinates().first()
val points = createPointCollection(listOf(
listOf(first.coordinates()[0], first.coordinates()[1]), listOf(last.coordinates()[0], last.coordinates()[1])), "Start")
}
@Test @Test
fun `hasLegs returns true when route has legs`() { fun `hasLegs returns true when route has legs`() {
val step0 = createStep(index = 0, numWaypoints = 2) val step0 = createStep(index = 0, numWaypoints = 2)
-1
View File
@@ -21,7 +21,6 @@ kotlin.code.style=official
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
org.gradle.daemon=true
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
org.gradle.configuration-cache=true org.gradle.configuration-cache=true
+20 -20
View File
@@ -1,21 +1,21 @@
[versions] [versions]
agp = "9.2.1" agp = "9.1.0"
androidGpxParser = "2.3.1" androidGpxParser = "2.3.1"
androidSdkTurf = "6.0.1" androidSdkTurf = "6.0.1"
datastore = "1.2.1" datastore = "1.2.1"
gradle = "9.2.1" gradle = "9.1.0"
koinAndroid = "4.2.1" koinAndroid = "4.2.0"
koinAndroidxCompose = "4.2.1" koinAndroidxCompose = "4.2.0"
koinComposeViewmodel = "4.2.1" koinComposeViewmodel = "4.2.0"
koinCore = "4.2.1" koinCore = "4.2.0"
kotlin = "2.3.21" kotlin = "2.3.20"
coreKtx = "1.18.0" coreKtx = "1.18.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.3.0" junitVersion = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
kotlinxSerializationJson = "1.11.0" kotlinxSerializationJson = "1.10.0"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.10.0"
composeBom = "2026.04.01" composeBom = "2026.03.01"
appcompat = "1.7.1" appcompat = "1.7.1"
material = "1.13.0" material = "1.13.0"
carApp = "1.7.0" carApp = "1.7.0"
@@ -26,21 +26,21 @@ mockitoKotlin = "6.3.0"
rules = "1.7.0" rules = "1.7.0"
runner = "1.7.0" runner = "1.7.0"
material3 = "1.4.0" material3 = "1.4.0"
runtimeLivedata = "1.11.0" runtimeLivedata = "1.10.6"
foundation = "1.11.0" foundation = "1.10.6"
maplibre-compose = "0.12.1" maplibre-compose = "0.12.1"
playServicesLocation = "21.3.0" playServicesLocation = "21.3.0"
runtime = "1.11.0" runtime = "1.10.6"
accompanist = "0.37.3" accompanist = "0.37.3"
uiVersion = "1.11.0" uiVersion = "1.10.6"
uiText = "1.11.0" uiText = "1.10.6"
navigationCompose = "2.9.8" navigationCompose = "2.9.7"
uiToolingPreview = "1.11.0" uiToolingPreview = "1.10.6"
uiTooling = "1.11.0" uiTooling = "1.10.6"
material3WindowSizeClass = "1.4.0" material3WindowSizeClass = "1.4.0"
uiGraphics = "1.11.0" uiGraphics = "1.10.6"
window = "1.5.1" window = "1.5.1"
foundationLayout = "1.11.0" foundationLayout = "1.10.6"
datastorePreferences = "1.2.1" datastorePreferences = "1.2.1"
datastoreCore = "1.2.1" datastoreCore = "1.2.1"
monitor = "1.8.0" monitor = "1.8.0"
@@ -48,7 +48,7 @@ robolectric = "4.16.1"
truth = "1.4.5" truth = "1.4.5"
testCore = "1.7.0" testCore = "1.7.0"
archCoreTesting = "2.2.0" archCoreTesting = "2.2.0"
kotlinxCoroutinesTest = "1.10.2" kotlinxCoroutinesTest = "1.10.1"
[libraries] [libraries]
android-gpx-parser = { module = "com.github.ticofab:android-gpx-parser", version.ref = "androidGpxParser" } android-gpx-parser = { module = "com.github.ticofab:android-gpx-parser", version.ref = "androidGpxParser" }