Compare commits
35
Commits
b99ebfd36f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e33eba0d51 | ||
|
|
a3b22c386b | ||
|
|
df1ceb5c44 | ||
|
|
21d9688b99 | ||
|
|
02107256c7 | ||
|
|
9b4e730dcf | ||
|
|
357a9af4f1 | ||
|
|
ffa0d47e7b | ||
|
|
29e58f6a24 | ||
|
|
72b3185280 | ||
|
|
8b612d5b80 | ||
|
|
25ba7d7bf6 | ||
|
|
202c2dfcea | ||
|
|
07cc382d3e | ||
|
|
4cf8517782 | ||
|
|
60cd6abc35 | ||
|
|
3926bbf1f9 | ||
|
|
615141d816 | ||
|
|
ae4d3f321e | ||
|
|
42b3eabe47 | ||
|
|
17e68de017 | ||
|
|
0fa625d785 | ||
|
|
f388ba0fb8 | ||
|
|
3fbba92b1d | ||
|
|
b1d56d536d | ||
|
|
dd2bdc2768 | ||
|
|
6a1897efd2 | ||
|
|
6c64bbecfd | ||
|
|
c6bad779d3 | ||
|
|
3d7749c6c7 | ||
|
|
5105d4eb9a | ||
|
|
f4cac4361b | ||
|
|
eb37c26fab | ||
|
|
b360f4bb5c | ||
|
|
4874d35d45 |
@@ -1,10 +1,10 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
This file provides guidance to Claude Code when working with code in this repository.
|
||||
|
||||
## 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, ObjectBox for local persistence, and Koin for dependency injection.
|
||||
This is an Android navigation app built with Jetpack Compose that supports multiple routing providers (OSRM, Valhalla, TomTom) and includes Android Auto/Automotive OS integration. The app uses MapLibre for rendering, Androidx DataStore for local persistence, and Koin for dependency injection.
|
||||
|
||||
## Build Commands
|
||||
|
||||
@@ -12,14 +12,15 @@ This is an Android navigation app built with Jetpack Compose that supports multi
|
||||
# Build the app (from repository root)
|
||||
./gradlew :app:assembleDebug
|
||||
|
||||
# Build specific flavor
|
||||
# Build a specific flavor
|
||||
./gradlew :app:assemblePlayDebug
|
||||
./gradlew :app:assembleDemoDebug
|
||||
./gradlew :app:assembleFullDebug
|
||||
|
||||
# Run tests
|
||||
# Run unit tests
|
||||
./gradlew test
|
||||
|
||||
# Run tests for specific module
|
||||
# Run tests for a specific module
|
||||
./gradlew :common:data:test
|
||||
./gradlew :common:car:test
|
||||
|
||||
@@ -32,12 +33,12 @@ This is an Android navigation app built with Jetpack Compose that supports multi
|
||||
|
||||
## Module Structure
|
||||
|
||||
The project uses a multi-module architecture:
|
||||
The project uses a multi-module architecture (see `settings.gradle.kts`):
|
||||
|
||||
- **app/** - Main Android app with Jetpack Compose UI for phone
|
||||
- **common/data/** - Core data layer with routing logic, repositories, and data models (shared by all modules)
|
||||
- **app/** - Main Android app with Jetpack Compose UI for phone (`com.kouros.navigation`)
|
||||
- **common/data/** - Core data layer with routing logic, repositories, view models, persistence (`com.kouros.data`)
|
||||
- **common/car/** - Android Auto/Automotive OS UI implementation
|
||||
- **automotive/** - Placeholder for future native Automotive OS app
|
||||
- **automotive/** - Placeholder for future native Automotive OS app (no Kotlin sources yet)
|
||||
|
||||
Dependencies flow: `app` → `common:car` → `common:data`
|
||||
|
||||
@@ -45,156 +46,212 @@ Dependencies flow: `app` → `common:car` → `common:data`
|
||||
|
||||
### Routing Providers (Pluggable System)
|
||||
|
||||
The app supports three routing engines that implement the `NavigationRepository` abstract class:
|
||||
The app supports three routing engines that extend the `NavigationRepository` abstract class (`common/data/.../data/NavigationRepository.kt`):
|
||||
|
||||
1. **OsrmRepository** - OSRM routing engine
|
||||
2. **ValhallaRepository** - Valhalla routing engine
|
||||
3. **TomTomRepository** - TomTom routing engine
|
||||
1. **ValhallaRepository** - Valhalla routing engine (ordinal 0)
|
||||
2. **OsrmRepository** - OSRM routing engine (ordinal 1)
|
||||
3. **TomTomRepository** - TomTom routing engine (ordinal 2, default)
|
||||
|
||||
Each provider has a corresponding mapper class (`OsrmRoute`, `ValhallaRoute`, `TomTomRoute`) that converts provider-specific JSON responses to the universal `Route` data model.
|
||||
Selection is driven by `RouteEngine` enum order (see `Data.kt`). `NavigationRepository` also exposes shared HTTP helpers (`fetchUrl`, `searchPlaces`, `reverseAddress`) that use Nominatim for geocoding and `ApplicationConfig`-backed HTTP basic auth for protected endpoints.
|
||||
|
||||
Each provider has a corresponding mapper (`OsrmRoute`, `ValhallaRoute`, `TomTomRoute`) that converts provider-specific JSON into the universal `Route` model. The universal model is decomposed into a dedicated `data/route/` package: `Routes`, `Leg`, `Step`, `Maneuver`, `Intersection`, `Lane`, `Summary`.
|
||||
|
||||
**Adding a new routing provider:**
|
||||
1. Create `NewProviderRepository` extending `NavigationRepository` in `common/data/src/main/java/com/kouros/navigation/data/`
|
||||
2. Implement `getRoute()` method
|
||||
3. Create `NewProviderRoute.kt` with `mapToRoute()` function
|
||||
4. Add provider detection logic in `Route.Builder.route()`
|
||||
5. Update `NavigationUtils.getViewModel()` to return appropriate ViewModel
|
||||
1. Create `NewProviderRepository` extending `NavigationRepository` under `common/data/src/main/java/com/kouros/navigation/data/<provider>/`
|
||||
2. Implement `getRoute()` and `getTraffic()`
|
||||
3. Create `NewProviderRoute.kt` with a `mapToRoute(response, builder)` function that populates a `Route.Builder`
|
||||
4. Add a `RouteEngine` enum entry and provider branch in `Route.Builder.route()` (`data/Route.kt`)
|
||||
5. Update `NavigationUtils.getViewModel()` (`utils/NavigationUtils.kt`) to return a `NavigationViewModel` wired to the new repository
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User Action (search/select destination)
|
||||
User action (search / select destination)
|
||||
↓
|
||||
ViewModel.loadRoute() [LiveData]
|
||||
NavigationViewModel.loadRoute() [LiveData / Flow]
|
||||
↓
|
||||
NavigationRepository.getRoute() [Selected provider]
|
||||
NavigationRepository.getRoute() [selected provider]
|
||||
↓
|
||||
*Route.mapToRoute() [Convert to universal Route model]
|
||||
*Route.mapToRoute() [convert to universal Route]
|
||||
↓
|
||||
RouteModel.startNavigation()
|
||||
↓
|
||||
RouteModel.updateLocation() [On each location update]
|
||||
RouteCalculator.findStep() [on each location update]
|
||||
↓
|
||||
UI observes LiveData and displays current step
|
||||
NavigationState updated → UI observes and renders current step
|
||||
```
|
||||
|
||||
### Key Classes
|
||||
|
||||
**Navigation Logic:**
|
||||
- `RouteModel.kt` - Core navigation engine (tracks position, calculates distances, manages steps)
|
||||
- `RouteCarModel.kt` - Extends RouteModel with Android Auto-specific formatting
|
||||
- `ViewModel.kt` - androidx.ViewModel with LiveData for route, traffic, places, etc.
|
||||
**Navigation logic** (`common/data/.../model/`):
|
||||
- `RouteModel.kt` - Core navigation engine; tracks position, manages step progression, owns `NavigationState`
|
||||
- `RouteCalculator.kt` - Step-finding algorithm: snaps current location to the nearest waypoint, computes leftover distance, handles snap correction and reroute thresholds
|
||||
- `RouteCarModel.kt` (`common/car/.../navigation/`) - Extends RouteModel with Android Auto-specific formatting
|
||||
- `NavigationViewModel.kt` - androidx ViewModel exposing route, traffic, places (Nominatim), amenities (Overpass), and fuel prices (Tankerkönig) as LiveData
|
||||
- `SettingsViewModel.kt` - State holder for DataStore-backed settings (dark mode, 3D, routing engine, avoid preferences, etc.)
|
||||
- `BaseStyleModel.kt` - Map style state
|
||||
|
||||
**Data Models:**
|
||||
- `Route.kt` - Universal route structure used by all providers
|
||||
- `Place.kt` - ObjectBox entity for favorites/recent locations
|
||||
- `StepData.kt` - Display data for current navigation instruction
|
||||
**Data models** (`common/data/.../data/`):
|
||||
- `Route.kt` - Universal route wrapper with `Route.Builder` and provider dispatch
|
||||
- `data/route/*` - Decomposed route components (`Routes`, `Leg`, `Step`, `Maneuver`, `Intersection`, `Lane`, `Summary`)
|
||||
- `NavigationState.kt` - Immutable navigation state (route, flags, location, bearing, maneuver, destination)
|
||||
- `Data.kt` - Shared types (`Place`, `StepData`, `SearchFilter`, `Locations`, `ValhallaLocation`) plus `object Constants` and the `RouteEngine`, `DarkMode`, `EngineType`, `ViewStyle`, `NavigationThemeColor` enums
|
||||
- `ApplicationConfig.kt` - Loads `USER` / `PASSWORD` from `BuildConfig` for HTTP basic auth on protected endpoints
|
||||
|
||||
**Repositories:**
|
||||
- `NavigationRepository.kt` - Abstract base class for all routing providers
|
||||
- Also handles Nominatim geocoding search and TomTom traffic incidents
|
||||
**Persistence** (`common/data/.../data/datastore/` and `common/data/.../repository/`):
|
||||
- `DataStoreManager.kt` - Single source of truth for preference keys; exposes `Flow<T>` reads and `suspend` writes for each setting
|
||||
- `SettingsRepository.kt` - Higher-level wrapper over `DataStoreManager`
|
||||
|
||||
**Android Auto:**
|
||||
- `NavigationCarAppService.kt` - Entry point for Android Auto/Automotive OS
|
||||
- `NavigationSession.kt` - Session management
|
||||
- `NavigationScreen.kt` - Car screen templates with NavigationType state machine
|
||||
- `SurfaceRenderer.kt` - Handles virtual display and map rendering
|
||||
**Repositories** (`common/data/.../data/`):
|
||||
- `NavigationRepository.kt` - Abstract base class for routing providers; also handles Nominatim geocoding (search and reverse)
|
||||
- `osrm/`, `valhalla/`, `tomtom/` - Provider implementations and JSON DTOs
|
||||
- `fuel/FuelPrices.kt` - Tankerkönig fuel-price client (returns `List<Station>`)
|
||||
- `overpass/` - Overpass API client for POIs and speed limits
|
||||
|
||||
**Android Auto / Automotive** (`common/car/`):
|
||||
- `NavigationCarAppService.kt` - CarAppService entry point
|
||||
- `CarSession.kt` (abstract) and concrete `NavigationSession.kt`, `ClusterSession.kt`
|
||||
- `screen/NavigationScreen.kt` - Main navigation template; `NavigationType` enum drives template selection (VIEW, NAVIGATION, REROUTE, RECENT, ARRIVAL)
|
||||
- `screen/SearchScreen.kt`, `RoutePreviewScreen.kt`, `PlaceListScreen.kt`, `CategoriesScreen.kt`, `CategoryScreen.kt`, `StopOverScreen.kt`, `RequestPermissionScreen.kt`
|
||||
- `screen/settings/*` - Per-setting screens (RoutingSettings, NavigationSettings, DisplaySettings, DarkModeSettings, DistanceSettings, AudioSettings, CarSettings, PasswordSettings)
|
||||
- `screen/observers/*` - Observer pattern that bridges LiveData to car screens. `NavigationObserverManager` orchestrates `RouteObserver`, `TrafficObserver`, `TrafficMessageObserver`, `PlaceSearchObserver`, `CategoryObserver`, `MaxSpeedObserver`, `SpeedCameraObserver`
|
||||
- Supporting managers: `SurfaceRenderer.kt` (virtual display + map), `DeviceLocationManager.kt`, `CarSensorManager.kt`, `NavigationNotificationManager.kt` + `NavigationNotificationService.kt`, `TextToSpeechManager.kt`, `CustomLifecycleOwner.kt`
|
||||
- `map/MapView.kt`, `map/LocationPuck.kt` - MapLibre rendering for the car surface
|
||||
|
||||
### External APIs
|
||||
|
||||
| Service | Purpose | Base URL |
|
||||
|---------|---------|----------|
|
||||
| OSRM | Routing | `https://kouros-online.de/osrm/route/v1/driving/` |
|
||||
| Valhalla | Routing | `https://kouros-online.de/valhalla/route` |
|
||||
| TomTom | Traffic incidents | `https://api.tomtom.com/traffic/services/5/incidentDetails` |
|
||||
| Nominatim | Geocoding search | `https://kouros-online.de/nominatim/` |
|
||||
| Overpass | POI & speed limits | OpenStreetMap Overpass API |
|
||||
| Service | Purpose | URL |
|
||||
|---------------|--------------------------|------------------------------------------------------------------------------|
|
||||
| OSRM | Routing | `https://router.project-osrm.org/route/v1/driving/` |
|
||||
| Valhalla | Routing | `https://kouros-online.de/valhalla/route?json=` (HTTP basic auth) |
|
||||
| TomTom | Routing | `https://api.tomtom.com/routing/1/calculateRoute/` |
|
||||
| TomTom | Traffic incidents | `https://api.tomtom.com/traffic/services/5/incidentDetails` |
|
||||
| Nominatim | Geocoding (search/reverse) | `https://nominatim.openstreetmap.org/` |
|
||||
| Overpass | POIs & speed limits | OpenStreetMap Overpass API (DEBUG builds use `https://kouros-online.de/api/interpreter`) |
|
||||
| Tankerkönig | Fuel prices | `https://creativecommons.tankerkoenig.de/json/list.php?` (API key in `fuel/FuelPrices.kt`) |
|
||||
|
||||
In DEBUG builds, `TomTomRepository` and `FuelPrices` can be pointed at a local fixture (see `useLocal` flags) — TomTom can fall back to `R.raw.tomom_routing`, fuel falls back to a local JSON URL.
|
||||
|
||||
## Important Constants
|
||||
|
||||
Located in `Constants.kt` (`common/data`):
|
||||
Defined in `object Constants` inside `common/data/.../data/Data.kt`:
|
||||
|
||||
```kotlin
|
||||
NEXT_STEP_THRESHOLD = 120.0 m // Distance to show next maneuver
|
||||
DESTINATION_ARRIVAL_DISTANCE = 40.0 m // Distance to trigger arrival
|
||||
MAXIMAL_SNAP_CORRECTION = 50.0 m // Max distance to snap to route
|
||||
MAXIMAL_ROUTE_DEVIATION = 80.0 m // Max deviation before reroute
|
||||
NEXT_STEP_THRESHOLD = 500.0 // Distance (m) to show next maneuver
|
||||
DESTINATION_ARRIVAL_DISTANCE = 10.0 // Distance (m) to trigger arrival
|
||||
MAXIMAL_SNAP_CORRECTION = 50.0 // Max distance (m) to snap to route
|
||||
MAXIMAL_ROUTE_DEVIATION = 100.0 // Max deviation (m) before reroute
|
||||
NEAREST_LOCATION_DISTANCE = 10F
|
||||
SPEED_UPDATE_DISTANCE = 600F
|
||||
INSTRUCTION_DISTANCE = 50
|
||||
SPEED_BEARING_DEVIATION = 60
|
||||
TILT = 60.0 // Map tilt in degrees during navigation
|
||||
TANKER_KOENIG_DELAY = 300_000 // ms between fuel price refreshes
|
||||
```
|
||||
|
||||
SharedPreferences keys:
|
||||
- `ROUTING_ENGINE` - Selected provider (0=Valhalla, 1=OSRM, 2=TomTom)
|
||||
- `DARK_MODE_SETTINGS` - Theme preference
|
||||
- `AVOID_MOTORWAY`, `AVOID_TOLLWAY` - Route preferences
|
||||
DataStore keys (see `DataStoreManager.PreferencesKeys`):
|
||||
- `RoutingEngine` (Int) — `0=Valhalla`, `1=OSRM`, `2=TomTom` (default 2)
|
||||
- `DarkMode` (Int), `Show3D` (Bool), `CarLocation` (Bool)
|
||||
- `AvoidMotorway`, `AvoidTollway`, `AvoidFerry` (Bool)
|
||||
- `LastRoute`, `RecentPlaces`, `FuelPrices`, `LastFuelPrices`
|
||||
- `TomTomApiKey`, `DistanceMode`, `GuidanceAudio`, `Traffic`, `TripSuggestion`, `EngineType`, `AlternativeRoutes`
|
||||
|
||||
## Navigation Flow
|
||||
|
||||
1. **Route Loading**: User searches via Nominatim → selects place → ViewModel.loadRoute() calls selected repository
|
||||
2. **Route Parsing**: Provider JSON → mapper converts to universal Route → RouteModel.startNavigation()
|
||||
3. **Location Tracking**: FusedLocationProviderClient provides updates → RouteModel.updateLocation()
|
||||
4. **Step Calculation**: findStep() snaps location to nearest waypoint → updates current step
|
||||
5. **UI Updates**: currentStep() and nextStep() provide display data (instruction, distance, icon, lanes)
|
||||
6. **Arrival**: When distance < DESTINATION_ARRIVAL_DISTANCE, navigation ends
|
||||
1. **Route loading** — User searches via Nominatim → selects place → `NavigationViewModel.loadRoute()` calls the selected repository
|
||||
2. **Route parsing** — Provider JSON → `*Route.mapToRoute()` populates `Route.Builder` → universal `Route` → `RouteModel.startNavigation()`
|
||||
3. **Location tracking** — `FusedLocationProviderClient` updates → `RouteModel.updateLocation()`
|
||||
4. **Step calculation** — `RouteCalculator.findStep()` snaps location to the nearest waypoint, returns `StepMatch`, and updates the current step index
|
||||
5. **UI updates** — `NavigationState` flows out via LiveData/Compose state; phone and car UIs render the current/next step (instruction, distance, icon, lanes)
|
||||
6. **Arrival** — When distance < `DESTINATION_ARRIVAL_DISTANCE`, navigation ends
|
||||
|
||||
## Testing Navigation
|
||||
|
||||
The app includes mock location support for testing:
|
||||
The phone app supports mock locations for testing:
|
||||
|
||||
- Set `useMock = true` in MainActivity
|
||||
- Set `useMock = true` in `MainActivity`
|
||||
- Enable "Mock location app" in Android Developer Options
|
||||
- Choose test mode:
|
||||
- `type = 1` - Simulate movement along entire route
|
||||
- `type = 2` - Test specific step range
|
||||
- `type = 3` - Replay GPX track file
|
||||
- Choose mode in `model/Simulation.kt` / `model/MockLocation.kt`:
|
||||
- `type = 1` — Simulate movement along the entire route
|
||||
- `type = 2` — Test a specific step range
|
||||
- `type = 3` — Replay a GPX track file
|
||||
|
||||
## ObjectBox Database
|
||||
The car module has its own `navigation/Simulation.kt` for car-side simulation.
|
||||
|
||||
ObjectBox is configured in `common/data/build.gradle.kts` with the kapt plugin. The database stores:
|
||||
### Unit tests
|
||||
|
||||
- Recent destinations (category: "Recent")
|
||||
- Favorite places (category: "Favorites")
|
||||
- Imported contacts (category: "Contacts")
|
||||
- `common/data/src/test/.../model/RouteCalculatorTest.kt`
|
||||
- `common/data/src/test/.../model/RouteModelTest.kt`
|
||||
- `common/data/src/test/.../model/IconMapperTest.kt`
|
||||
- `common/data/src/test/.../model/OverpassTest.kt`
|
||||
- `common/data/src/test/.../utils/GeoUtilsTest.kt`
|
||||
- `common/car/src/test/.../screen/NavigationScreenTest.kt`
|
||||
- `common/car/src/test/.../screen/observers/CategoryObserverTest.kt`, `ObserversTest.kt`
|
||||
|
||||
Queries use ObjectBox query builder pattern with generated `Place_` property accessors.
|
||||
## Persistence
|
||||
|
||||
All app preferences are stored via Androidx **DataStore Preferences** (`navigation_settings` data store). There is no Room/ObjectBox/SQL database — the only persisted entities are settings and serialized lists (recent places, last route, last fuel prices) kept as strings.
|
||||
|
||||
`DataStoreManager` exposes a `Flow<T>` per setting plus a matching `suspend set*` writer. `SettingsRepository` wraps it for higher-level access. `SettingsViewModel` and `AppViewModel` consume those flows for UI state.
|
||||
|
||||
## Compose UI Structure
|
||||
|
||||
**Phone App:**
|
||||
- `MainActivity.kt` - Main entry with permission handling and Navigation Compose
|
||||
- `NavigationScreen.kt` - Turn-by-turn navigation display
|
||||
- `SearchSheet.kt` / `NavigationSheet.kt` - Bottom sheet content
|
||||
- `MapView.kt` - MapLibre rendering with camera state management
|
||||
**Phone app** (`app/src/main/java/com/kouros/navigation/`):
|
||||
- `ui/MainActivity.kt` - Entry point with permission handling and Navigation Compose host
|
||||
- `ui/MapView.kt` - MapLibre rendering with camera state management
|
||||
- `ui/SheetLayout.kt` - Bottom sheet scaffold
|
||||
- `ui/PermissionScreen.kt`
|
||||
- `ui/navigation/AppNavGraph.kt` - Compose navigation graph
|
||||
- `ui/navigation/NavigationScreen.kt`, `NavigationSheet.kt` - Turn-by-turn UI
|
||||
- `ui/search/SearchScreen.kt`, `SearchSheet.kt`
|
||||
- `ui/settings/SettingsScreen.kt`, `SettingsRoute.kt`, `DisplayScreen.kt`, `NavigationScreen.kt`, `CarScreen.kt`, `Settings.kt`
|
||||
- `ui/components/SettingItem.kt`, `SettingSwitch.kt`, `RadioButtonSingleSelection.kt`, `SectionTitle.kt`
|
||||
- `ui/app/AppViewModel.kt`, `AppViewModelProvider.kt`
|
||||
- `ui/theme/{Color,Type,Shapes,Theme}.kt`
|
||||
- `model/Simulation.kt`, `model/MockLocation.kt` - Mock location for testing
|
||||
- `di/appModule.kt` - Koin module
|
||||
- `MainApplication.kt` - Application class
|
||||
|
||||
**Android Auto:**
|
||||
- Uses CarAppService Screen templates (NavigationTemplate, MessageTemplate, MapWithContentTemplate)
|
||||
- NavigationType enum controls which template to display (VIEW, NAVIGATION, REROUTE, RECENT, ARRIVAL)
|
||||
**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/`.
|
||||
|
||||
## Build Flavors
|
||||
|
||||
Two product flavors with dimension "version":
|
||||
- **demo** - applicationId: `com.kouros.navigation.demo`
|
||||
- **full** - applicationId: `com.kouros.navigation.full`
|
||||
`app/build.gradle.kts` defines three product flavors under the `store` dimension:
|
||||
- **play** - applicationIdSuffix `.play`
|
||||
- **demo** - applicationIdSuffix `.demo`
|
||||
- **full** - applicationIdSuffix `.full`
|
||||
|
||||
Base namespace is `com.kouros.navigation`; current versionName is `0.3.0.109`. Java/Kotlin target is 21 for the app module, 11 for `common:data`. compileSdk/targetSdk = 37, minSdk = 33.
|
||||
|
||||
`signing.properties` (root, gitignored) provides the keystore credentials for both debug and release builds. `local.properties` provides `USER` / `PASSWORD` build config fields consumed by `ApplicationConfig`.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Dependency Injection (Koin):**
|
||||
**Dependency injection (Koin):**
|
||||
```kotlin
|
||||
single { OsrmRepository() }
|
||||
viewModel { ViewModel(get()) }
|
||||
viewModel { NavigationViewModel(get()) }
|
||||
```
|
||||
|
||||
**LiveData Observation:**
|
||||
**LiveData observation:**
|
||||
```kotlin
|
||||
viewModel.route.observe(this) { routeJson ->
|
||||
routeModel.startNavigation(routeJson, context)
|
||||
}
|
||||
```
|
||||
|
||||
**Step Finding Algorithm:**
|
||||
RouteModel iterates through all step waypoints, calculates distance to current location, and snaps to the nearest waypoint to determine current step index.
|
||||
**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.
|
||||
|
||||
**Settings flow:**
|
||||
```kotlin
|
||||
val darkMode by viewModel.darkMode.collectAsStateWithLifecycle()
|
||||
```
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Valhalla route mapping is incomplete (search for TODO comments in ValhallaRoute.kt)
|
||||
- Valhalla route mapping is incomplete in places (search for TODO comments in `data/valhalla/ValhallaRoute.kt`)
|
||||
- Rerouting logic exists but needs more testing
|
||||
- Speed limit queries via Overpass API could be optimized for performance
|
||||
- TomTom implementation uses local JSON file (R.raw.tomom_routing) instead of live API
|
||||
- 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
|
||||
- The `automotive` module is wired into Gradle but has no Kotlin source yet
|
||||
|
||||
@@ -11,14 +11,14 @@ val properties = Properties().apply {
|
||||
|
||||
android {
|
||||
namespace = "com.kouros.navigation"
|
||||
compileSdk = 36
|
||||
compileSdk = 37
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.kouros.navigation"
|
||||
minSdk = 33
|
||||
targetSdk = 36
|
||||
versionCode = 91
|
||||
versionName = "0.2.3.91"
|
||||
targetSdk = 37
|
||||
versionCode = 131
|
||||
versionName = "0.4.0.131"
|
||||
base.archivesName = "navi-$versionName"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -42,7 +42,7 @@ android {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
isMinifyEnabled = false
|
||||
isShrinkResources = false
|
||||
//isShrinkResources = false
|
||||
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
|
||||
@@ -45,12 +45,6 @@
|
||||
android:foregroundServiceType="location"
|
||||
android:exported="true">
|
||||
</service>
|
||||
<service
|
||||
android:name=".car.navigation.NavigationService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="location"
|
||||
android:exported="true">
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -31,7 +31,7 @@ fun test(applicationContext: Context, routeModel: RouteModel) {
|
||||
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
|
||||
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
||||
routeModel.updateLocation(
|
||||
location(waypoint[0], waypoint[1]), navigationViewModel
|
||||
waypoint, navigationViewModel
|
||||
)
|
||||
val step = routeModel.currentStep()
|
||||
val nextStep = routeModel.nextStep()
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
package com.kouros.navigation.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.location.LocationManager
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
@@ -45,7 +40,6 @@ import com.kouros.navigation.MainApplication.Companion.navigationViewModel
|
||||
import com.kouros.navigation.car.TextToSpeechManager
|
||||
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_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.StepData
|
||||
import com.kouros.navigation.model.BaseStyleModel
|
||||
@@ -59,7 +53,7 @@ import com.kouros.navigation.ui.navigation.NavigationSheet
|
||||
import com.kouros.navigation.ui.search.SearchSheet
|
||||
import com.kouros.navigation.ui.theme.NavigationTheme
|
||||
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||
import com.kouros.navigation.utils.bearing
|
||||
import com.kouros.navigation.utils.bearingPositive
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
import com.kouros.navigation.utils.getSettingsViewModel
|
||||
import com.kouros.navigation.utils.location
|
||||
@@ -278,7 +272,7 @@ class MainActivity : ComponentActivity() {
|
||||
val bearing = if (currentLocation.hasBearing()) {
|
||||
currentLocation.bearing.toDouble()
|
||||
} else {
|
||||
bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
|
||||
bearingPositive(lastLocation, currentLocation, cameraPosition.value!!.bearing)
|
||||
}
|
||||
|
||||
with(routeModel) {
|
||||
|
||||
@@ -92,7 +92,7 @@ fun MapView(
|
||||
duration = 1.seconds
|
||||
)
|
||||
}
|
||||
NavigationImage(paddingValues, width, height / 6, "", dark)
|
||||
NavigationImage(paddingValues, width, height / 6, "", dark, tilt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ private fun SearchPlaces(
|
||||
latitude = place.lat.toDouble(),
|
||||
postalCode = place.address.postcode,
|
||||
city = place.address.city,
|
||||
street = place.address.road
|
||||
street = place.address.road,
|
||||
)
|
||||
viewModel.saveRecent(context, pl)
|
||||
val toLocation =
|
||||
|
||||
@@ -7,7 +7,7 @@ plugins {
|
||||
android {
|
||||
namespace = "com.kouros.navigation"
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
version = release(37)
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
|
||||
@@ -6,7 +6,7 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "com.kouros.android.cars.carappservice"
|
||||
compileSdk = 36
|
||||
compileSdk = 37
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 33
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
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,25 +2,24 @@ package com.kouros.navigation.car
|
||||
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.kouros.navigation.data.Constants.homeHohenwaldeck
|
||||
import com.kouros.navigation.data.RouteEngine
|
||||
import com.kouros.navigation.data.route.ManeuverType
|
||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
import com.kouros.navigation.utils.location
|
||||
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.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import kotlin.collections.forEach
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
@@ -32,36 +31,378 @@ class RouteModelTest {
|
||||
val routeModel = RouteModel()
|
||||
val location = Location(LocationManager.GPS_PROVIDER)
|
||||
|
||||
val distance = listOf(
|
||||
1025.5,
|
||||
989.8,
|
||||
963.5,
|
||||
923.7,
|
||||
915.8,
|
||||
914.6,
|
||||
871.0,
|
||||
822.7,
|
||||
769.7,
|
||||
713.8,
|
||||
644.8,
|
||||
577.6,
|
||||
501.7,
|
||||
489.7,
|
||||
452.5,
|
||||
437.4,
|
||||
398.0,
|
||||
390.1,
|
||||
341.3,
|
||||
266.6,
|
||||
219.5,
|
||||
140.7,
|
||||
77.4,
|
||||
55.1,
|
||||
40.0,
|
||||
30.0,
|
||||
19.0,
|
||||
4.0
|
||||
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(
|
||||
1046.5,
|
||||
1026.5,
|
||||
1012.0,
|
||||
979.4,
|
||||
972.7,
|
||||
971.6,
|
||||
914.8,
|
||||
873.6,
|
||||
831.3,
|
||||
781.1,
|
||||
719.9,
|
||||
653.1,
|
||||
578.4,
|
||||
566.2,
|
||||
490.4,
|
||||
482.6,
|
||||
451.4,
|
||||
443.6,
|
||||
391.2,
|
||||
346.7,
|
||||
316.6,
|
||||
237.5,
|
||||
141.7,
|
||||
78.22,
|
||||
55.94,
|
||||
41.31,
|
||||
31.0,
|
||||
20.9,
|
||||
)
|
||||
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
@@ -69,7 +410,7 @@ class RouteModelTest {
|
||||
val repository = getSettingsRepository(appContext)
|
||||
runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) }
|
||||
val routeJsonString = TomTomRepository().fetchUrl(
|
||||
"https://kouros-online.de/tomtom_routing.json",
|
||||
"http://192.168.1.37/tomtom_routing.json",
|
||||
false
|
||||
)
|
||||
assertNotEquals("", routeJsonString)
|
||||
@@ -92,25 +433,36 @@ class RouteModelTest {
|
||||
val stepData = routeModel.currentStep()
|
||||
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
|
||||
assertEquals(stepData.instruction, "Silcherstraße")
|
||||
assertEquals(stepData.leftStepDistance, 20.0, 5.0)
|
||||
assertEquals(stepData.leftStepDistance, 46.0, 5.0)
|
||||
val nextStepData = routeModel.nextStep()
|
||||
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
|
||||
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
|
||||
fun checkSchmalkadener20() {
|
||||
location.latitude = 48.187057
|
||||
location.longitude = 11.576652
|
||||
location.latitude = 48.186943
|
||||
location.longitude = 11.579195
|
||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||
val stepData = routeModel.currentStep()
|
||||
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
|
||||
assertEquals(stepData.instruction, "Schmalkaldener Straße")
|
||||
assertEquals(stepData.leftStepDistance, 0.0, 1.0)
|
||||
assertEquals(stepData.instruction, "Ingolstädter Straße")
|
||||
assertEquals(stepData.leftStepDistance, 326.0, 1.0)
|
||||
val nextStepData = routeModel.nextStep()
|
||||
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
|
||||
assertEquals(nextStepData.instruction, "Ingolstädter Straße")
|
||||
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
|
||||
assertEquals(nextStepData.instruction, "Schenkendorfstraße")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -128,7 +480,7 @@ class RouteModelTest {
|
||||
} else {
|
||||
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
|
||||
}
|
||||
assertEquals(stepData.leftStepDistance, 301.0, 1.0)
|
||||
assertEquals(stepData.leftStepDistance, 327.0, 1.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -140,7 +492,7 @@ class RouteModelTest {
|
||||
val stepData = routeModel.currentStep()
|
||||
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
|
||||
assertEquals(stepData.instruction, "Schenkendorfstraße")
|
||||
assertEquals(stepData.leftStepDistance, 170.0, 10.0)
|
||||
assertEquals(stepData.leftStepDistance, 212.0, 10.0)
|
||||
assertEquals(stepData.lane.size, 4)
|
||||
assertEquals(stepData.lane.first().valid, true)
|
||||
assertEquals(stepData.lane.last().valid, false)
|
||||
@@ -205,29 +557,24 @@ class RouteModelTest {
|
||||
if (routeModel.isNavigating()) {
|
||||
val curLocation = location(waypoint[0], waypoint[1])
|
||||
if (index in 0..routeModel.curRoute.waypoints.size) {
|
||||
//runBlocking { delay(1000) }
|
||||
val start = System.currentTimeMillis()
|
||||
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
|
||||
val stepData = routeModel.currentStep()
|
||||
//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()
|
||||
assertEquals(stepData.leftStepDistance, leftDistance[index], 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `leftStepDistance Inglolstädter `() {
|
||||
val location: Location = location(11.584578, 48.183653)
|
||||
fun `leftStepDistance Ingolstädter `() {
|
||||
var location = location(11.584352, 48.186771)
|
||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||
val step = routeModel.currentStep()
|
||||
assertEquals(step.leftStepDistance, 645.0, 1.0)
|
||||
var step = routeModel.currentStep()
|
||||
assertEquals(step.leftStepDistance, 1039.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
|
||||
@@ -235,21 +582,66 @@ class RouteModelTest {
|
||||
val location: Location = location(11.578911, 48.185565)
|
||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||
val step = routeModel.currentStep()
|
||||
assertEquals(step.leftStepDistance, 26.0, 1.0)
|
||||
assertEquals(step.leftStepDistance, 37.0, 1.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leftStepDistance() {
|
||||
val start = System.currentTimeMillis()
|
||||
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
|
||||
val curLocation = location(waypoint[0], waypoint[1])
|
||||
if (routeModel.isNavigating()) {
|
||||
if (index in 16..43) {
|
||||
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
|
||||
routeModel.updateLocation(
|
||||
curLocation,
|
||||
NavigationViewModel(TomTomRepository())
|
||||
)
|
||||
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
|
||||
carSensors.addCompassListener(
|
||||
CarSensors.UPDATE_RATE_NORMAL,
|
||||
CarSensors.UPDATE_RATE_FASTEST,
|
||||
carContext.mainExecutor,
|
||||
carCompassListener
|
||||
)
|
||||
carSensors.addCarHardwareLocationListener(
|
||||
CarSensors.UPDATE_RATE_UI,
|
||||
CarSensors.UPDATE_RATE_FASTEST,
|
||||
carContext.mainExecutor,
|
||||
carLocationListener
|
||||
)
|
||||
|
||||
@@ -134,7 +134,6 @@ internal class ClusterSession : CarSession(), NavigationListener {
|
||||
|
||||
|
||||
fun updateLocation(location: Location) {
|
||||
Log.d(TAG, "updateLocation $location")
|
||||
surfaceRenderer.updateLocation(location, "")
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.kouros.navigation.car
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.core.location.LocationListenerCompat
|
||||
@@ -10,7 +11,12 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.callbackFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -59,6 +65,26 @@ 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.
|
||||
* Provides initial location via callback and then starts continuous updates.
|
||||
@@ -69,21 +95,24 @@ class DeviceLocationManager(
|
||||
@SuppressLint("MissingPermission")
|
||||
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5f) {
|
||||
if (isListening) return
|
||||
|
||||
// Get and deliver last known location first
|
||||
val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
||||
if (lastLocation != null) {
|
||||
onInitialLocation(lastLocation)
|
||||
onLocationUpdate(lastLocation)
|
||||
if (BuildConfig.DEBUG) {
|
||||
onInitialLocation(homeVogelhart)
|
||||
onLocationUpdate(homeVogelhart)
|
||||
} else {
|
||||
// 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
|
||||
)
|
||||
}
|
||||
|
||||
// Start continuous location updates
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
minTimeMs,
|
||||
minDistanceM,
|
||||
locationListener
|
||||
)
|
||||
isListening = true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
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,8 +35,7 @@ class NavigationCarAppService : CarAppService() {
|
||||
return ClusterSession()
|
||||
} else {
|
||||
createNotificationChannel()
|
||||
//return NavigationSession()
|
||||
return NavigationServiceSession()
|
||||
return NavigationSession()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-17
@@ -1,22 +1,15 @@
|
||||
package com.kouros.navigation.car
|
||||
|
||||
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.hardware.CarHardwareManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kouros.navigation.data.Constants.TAG
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NotificationManager(
|
||||
class NavigationNotificationManager(
|
||||
private val carContext: CarContext,
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
) {
|
||||
|
||||
private var notificationServiceStarted = false
|
||||
@@ -24,16 +17,7 @@ class NotificationManager(
|
||||
private var serviceStarted = false
|
||||
|
||||
init {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
|
||||
}
|
||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.DESTROYED) {
|
||||
if (notificationServiceStarted) {
|
||||
stopNotificationService()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startNotificationService() {
|
||||
@@ -111,7 +111,7 @@ class NavigationNotificationService : Service() {
|
||||
* Initializes the notifications, if needed.
|
||||
*
|
||||
*
|
||||
* [NotificationManager.IMPORTANCE_HIGH] is needed to show the alerts on top of the car
|
||||
* [NavigationNotificationManager.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
|
||||
* importance setting.
|
||||
*/
|
||||
|
||||
@@ -1,724 +0,0 @@
|
||||
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,17 +4,16 @@ import android.Manifest.permission
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.ScreenManager
|
||||
import androidx.car.app.connection.CarConnection
|
||||
import androidx.car.app.model.Distance
|
||||
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.Trip
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
@@ -57,7 +56,7 @@ import com.kouros.navigation.repository.SettingsRepository
|
||||
import com.kouros.navigation.utils.GeoUtils
|
||||
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.bearingPositive
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
import com.kouros.navigation.utils.getSettingsViewModel
|
||||
import com.kouros.navigation.utils.location
|
||||
@@ -101,9 +100,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
|
||||
lateinit var textToSpeechManager: TextToSpeechManager
|
||||
|
||||
lateinit var notificationManager: NotificationManager
|
||||
|
||||
|
||||
var autoDriveEnabled = false
|
||||
|
||||
val simulation = Simulation()
|
||||
@@ -117,11 +113,11 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
|
||||
var speedCameras = listOf<Elements>()
|
||||
|
||||
var lastRouteDate: LocalDateTime = LocalDateTime.now()
|
||||
private var lastRouteCheckLocation = location(0.0, 0.0)
|
||||
|
||||
var navigationManagerStarted = false
|
||||
|
||||
var notificationActive = false
|
||||
var updateLocationIndex = 0
|
||||
|
||||
/**
|
||||
* Lifecycle observer for managing session lifecycle events.
|
||||
@@ -129,16 +125,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
*/
|
||||
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) {
|
||||
if (::navigationManager.isInitialized) {
|
||||
navigationManager.clearNavigationManagerCallback()
|
||||
@@ -152,13 +138,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
if (::textToSpeechManager.isInitialized) {
|
||||
textToSpeechManager.cleanup()
|
||||
}
|
||||
carContext
|
||||
.stopService(
|
||||
Intent(
|
||||
carContext,
|
||||
NavigationNotificationService::class.java
|
||||
)
|
||||
)
|
||||
Log.i(TAG, "NavigationSession destroyed")
|
||||
}
|
||||
}
|
||||
@@ -293,6 +272,10 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
routeModel = RouteCarModel()
|
||||
|
||||
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
|
||||
|
||||
navigationViewModel.initialSnapLocation.observe(this, Observer {
|
||||
surfaceRenderer.updateLocation(it, "")
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,6 +318,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(),
|
||||
onLocationUpdate = ::updateLocation,
|
||||
onInitialLocation = { location ->
|
||||
navigationViewModel.loadCurrentLocation(location)
|
||||
navigationViewModel.loadRecentPlaces(
|
||||
carContext,
|
||||
location,
|
||||
@@ -343,12 +327,10 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
textToSpeechManager = TextToSpeechManager(carContext)
|
||||
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
|
||||
guidanceAudio = it
|
||||
})
|
||||
notificationManager = NotificationManager(carContext, this)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -423,7 +405,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
screenManager.popToRoot()
|
||||
screenManager.pushForResult(
|
||||
SearchScreen(carContext, surfaceRenderer, navigationViewModel, mutableListOf())
|
||||
) { result ->
|
||||
) { _ ->
|
||||
// Handle search result if needed
|
||||
}
|
||||
}
|
||||
@@ -464,6 +446,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
|
||||
surfaceRenderer.updateLocation(location, streetName)
|
||||
}
|
||||
updateLocationIndex++
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -476,38 +459,29 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles location updates during active navigation.
|
||||
* Snaps location to route and checks for deviation requiring reroute.
|
||||
* Checks if the location deviation is acceptable.
|
||||
*/
|
||||
private fun handleNavigationLocation(location: Location) {
|
||||
routeModel.updateLocation(location, navigationViewModel)
|
||||
if (routeModel.navState.arrived) return
|
||||
if (guidanceAudio == 1) {
|
||||
handleGuidanceAudio()
|
||||
private fun checkLocationDeviation(
|
||||
location: Location,
|
||||
snappedLocation: Location,
|
||||
streetName: String
|
||||
): Boolean {
|
||||
var maximalRouteDeviation = MAXIMAL_ROUTE_DEVIATION
|
||||
val speed = surfaceRenderer.speed.value
|
||||
if (speed != null) {
|
||||
when (speed) {
|
||||
in 0.0..10.0 -> maximalRouteDeviation = MAXIMAL_ROUTE_DEVIATION
|
||||
in 10.0..20.0 -> maximalRouteDeviation = MAXIMAL_ROUTE_DEVIATION + 200
|
||||
in 20.0..30.0 -> maximalRouteDeviation = MAXIMAL_ROUTE_DEVIATION + 400
|
||||
in 30.0..100.0 -> maximalRouteDeviation = MAXIMAL_ROUTE_DEVIATION + 500
|
||||
}
|
||||
}
|
||||
val streetName = routeModel.currentStep().street
|
||||
val currentDate = LocalDateTime.now(ZoneOffset.UTC)
|
||||
|
||||
if (snapLocation(location, streetName)) {
|
||||
checkTraffic(currentDate, location)
|
||||
updateSpeedCamera(location)
|
||||
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)
|
||||
Log.d(TAG, "Distance: $distance $maximalRouteDeviation $speed")
|
||||
when {
|
||||
distance > MAXIMAL_ROUTE_DEVIATION -> {
|
||||
distance > maximalRouteDeviation -> {
|
||||
stopNavigation()
|
||||
navigationScreen.calculateNewRoute(routeModel.navState.destination)
|
||||
navigationScreen.calculateNewRoute(routeModel.navState.destination, distance)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -522,6 +496,31 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
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.
|
||||
*/
|
||||
@@ -531,56 +530,37 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
) {
|
||||
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(
|
||||
isNavigating = routeModel.isNavigating(),
|
||||
isRerouting = false,
|
||||
hasArrived = routeModel.isArrival(),
|
||||
destinationTravelEstimate = travelEstimateTrip,
|
||||
stepTravelEstimate = travelEstimateStep,
|
||||
destinations = mutableListOf(destination),
|
||||
steps = steps,
|
||||
stepRemainingDistance = Distance.create(distance.first, distance.second),
|
||||
hasArrived = routeModel.isManeuverArrival(),
|
||||
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext),
|
||||
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext),
|
||||
destinations = mutableListOf(routeModel.getDestination()),
|
||||
steps = routeModel.getSteps(carContext),
|
||||
stepRemainingDistance = routeModel.getDistance(),
|
||||
shouldShowNextStep = false,
|
||||
shouldShowLanes = true,
|
||||
junctionImage = null,
|
||||
backGroundColor = routeModel.backGroundColor()
|
||||
backGroundColor = routeModel.backGroundColor(),
|
||||
message = stepData.message,
|
||||
)
|
||||
|
||||
/**
|
||||
* 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(
|
||||
destination,
|
||||
travelEstimateTrip
|
||||
)
|
||||
tripBuilder.setLoading(false)
|
||||
tripBuilder.setCurrentRoad(destination.name.toString())
|
||||
tripBuilder.addStep(steps.first(), travelEstimateStep)
|
||||
updateTrip(tripBuilder.build())
|
||||
updateTrip(routeModel.getTrip(carContext))
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for arrival
|
||||
*/
|
||||
fun checkArrival() {
|
||||
if (routeModel.isArrival()
|
||||
if (routeModel.isManeuverArrival()
|
||||
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
||||
) {
|
||||
stopNavigation()
|
||||
@@ -597,7 +577,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
* Called when user starts navigation
|
||||
*/
|
||||
override fun startNavigation() {
|
||||
Log.d(TAG, "startNavigation")
|
||||
surfaceRenderer.navigation = true
|
||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||
navigationManager.navigationStarted()
|
||||
@@ -609,8 +588,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
updateLocation(location)
|
||||
}
|
||||
}
|
||||
if (notificationActive)
|
||||
notificationManager.startNotificationService()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -618,7 +595,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
* Called when user exits navigation or arrives at destination.
|
||||
*/
|
||||
override fun stopNavigation() {
|
||||
Log.d(TAG, "stopNavigation")
|
||||
surfaceRenderer.navigation = false
|
||||
routeModel.stopNavigation()
|
||||
navigationManager.navigationEnded()
|
||||
@@ -630,8 +606,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
lastCameraSearch = 0
|
||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||
navigationScreen.navigationType = NavigationType.VIEW
|
||||
if (notificationActive)
|
||||
notificationManager.stopNotificationService()
|
||||
navigationScreen.invalidate()
|
||||
}
|
||||
|
||||
override fun updateTrip(trip: Trip) {
|
||||
@@ -649,7 +624,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
listOf(destination),
|
||||
surfaceRenderer.carOrientation
|
||||
surfaceRenderer.cameraPosition.value!!.bearing.toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -663,9 +638,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
|
||||
textToSpeechManager.speak(stepData.message)
|
||||
lastStepIndex = currentStep.index
|
||||
if (notificationActive) {
|
||||
notificationManager.sendMessage(stepData.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,7 +646,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
* Starts navigation and invalidates the screen.
|
||||
*/
|
||||
override fun onRouteReceived(route: String) {
|
||||
Log.d(TAG, "onRouteReceived")
|
||||
if (route.isNotEmpty()) {
|
||||
this.route = route
|
||||
if (routeModel.isNavigating()) {
|
||||
@@ -707,8 +678,10 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
val newRouteModel = RouteModel()
|
||||
newRouteModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
|
||||
newRouteModel.startNavigation(route)
|
||||
routeModel.curRoute.summary.trafficDelay = newRouteModel.curRoute.summary.trafficDelay
|
||||
updateNavigationScreen()
|
||||
if ((routeModel.curRoute.summary.trafficDelay - newRouteModel.curRoute.summary.trafficDelay).absoluteValue > 300) {
|
||||
routeModel.startNavigation(route)
|
||||
updateNavigationScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -741,8 +714,10 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
cameras.forEach {
|
||||
coordinates.add(listOf(it.lon, it.lat))
|
||||
}
|
||||
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
|
||||
surfaceRenderer.speedCameraData.value = speedData
|
||||
synchronized(this) {
|
||||
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
|
||||
surfaceRenderer.speedCameraData.value = speedData
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -752,18 +727,18 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
surfaceRenderer.maxSpeed.value = speed
|
||||
}
|
||||
|
||||
override fun onRecentPlacesReceived(places: List<Place>) {
|
||||
}
|
||||
|
||||
override fun invalidateScreen() {
|
||||
navigationScreen.invalidate()
|
||||
}
|
||||
|
||||
override fun onTrafficMessageReceived(trafficMessage: String) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = routeModel.navState.destination
|
||||
@@ -803,7 +778,11 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
val duration = Duration.between(current, lastTrafficDate)
|
||||
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
|
||||
lastTrafficDate = current
|
||||
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
|
||||
navigationViewModel.loadTraffic(
|
||||
carContext,
|
||||
location,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -811,12 +790,13 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
* Periodically requests speed camera information near the current location.
|
||||
*/
|
||||
private fun updateSpeedCamera(location: Location) {
|
||||
if (lastCameraSearch++ % 100 == 0) {
|
||||
if (lastCameraSearch % 200 == 0) {
|
||||
navigationViewModel.getSpeedCameras(location, 5.0)
|
||||
}
|
||||
if (speedCameras.isNotEmpty()) {
|
||||
if (speedCameras.isNotEmpty() && lastCameraSearch % 15 == 0) {
|
||||
updateDistance(location)
|
||||
}
|
||||
lastCameraSearch++
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -825,54 +805,71 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
||||
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) {
|
||||
synchronized(this) {
|
||||
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.bearingPositive(location)
|
||||
val bearingSpeedCamera = try {
|
||||
camera.tags.direction.toFloat()
|
||||
} catch (_: 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)
|
||||
if (camera.distance < 80) {
|
||||
if ((bearingSpeedCamera - bearingRoute).absoluteValue < 15.0) {
|
||||
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.
|
||||
*/
|
||||
private fun checkRoute(currentDate: LocalDateTime, location: Location) {
|
||||
val duration = Duration.between(currentDate, lastRouteDate)
|
||||
val routeUpdate = routeModel.curRoute.summary.duration / 4
|
||||
if (duration.abs().seconds > routeUpdate) {
|
||||
lastRouteDate = currentDate
|
||||
val destination = location(
|
||||
routeModel.navState.destination.longitude,
|
||||
routeModel.navState.destination.latitude
|
||||
)
|
||||
navigationViewModel.loadRoute(
|
||||
carContext,
|
||||
location,
|
||||
listOf(destination),
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
private fun checkRoute(location: Location) {
|
||||
val distance = location.distanceTo(lastRouteCheckLocation)
|
||||
if (distance > checkDistance(routeModel)) {
|
||||
if (lastRouteCheckLocation.latitude != 0.0) {
|
||||
val destination = location(
|
||||
routeModel.navState.destination.longitude,
|
||||
routeModel.navState.destination.latitude
|
||||
)
|
||||
navigationViewModel.loadRoute(
|
||||
carContext,
|
||||
location,
|
||||
listOf(destination),
|
||||
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() {
|
||||
navigationScreen.invalidate()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.location.Location
|
||||
import android.util.Log
|
||||
import androidx.car.app.AppManager
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Session
|
||||
import androidx.car.app.SurfaceCallback
|
||||
import androidx.car.app.SurfaceContainer
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -35,7 +34,7 @@ import com.kouros.navigation.data.Constants.TILT
|
||||
import com.kouros.navigation.data.DarkMode
|
||||
import com.kouros.navigation.data.ViewStyle
|
||||
import com.kouros.navigation.model.BaseStyleModel
|
||||
import com.kouros.navigation.utils.bearing
|
||||
import com.kouros.navigation.utils.bearingPositive
|
||||
import com.kouros.navigation.utils.calculateTilt
|
||||
import com.kouros.navigation.utils.calculateZoom
|
||||
import com.kouros.navigation.utils.duration
|
||||
@@ -47,6 +46,10 @@ import org.maplibre.compose.camera.CameraState
|
||||
import org.maplibre.compose.style.BaseStyle
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
|
||||
|
||||
/**
|
||||
@@ -156,15 +159,13 @@ class SurfaceRenderer(
|
||||
Log.i(TAG, "Surface available $surfaceContainer")
|
||||
lifecycleOwner = CustomLifecycleOwner()
|
||||
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_START)
|
||||
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
|
||||
virtualDisplay = carContext.getSystemService(DisplayManager::class.java)
|
||||
.createVirtualDisplay(
|
||||
"Maps",
|
||||
"Navigation",
|
||||
surfaceContainer.width,
|
||||
surfaceContainer.height,
|
||||
surfaceContainer.dpi,
|
||||
@@ -228,15 +229,39 @@ class SurfaceRenderer(
|
||||
*/
|
||||
override fun onScroll(distanceX: Float, distanceY: Float) {
|
||||
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
|
||||
if (distanceX != 0.0F) {
|
||||
lastLocation.longitude += (distanceX / 1000) / cameraPosition.value!!.zoom
|
||||
}
|
||||
if (distanceY != 0.0F) {
|
||||
lastLocation.latitude += (distanceY / 1000) / cameraPosition.value!!.zoom
|
||||
}
|
||||
|
||||
// Update location based on rotated scroll distances and calculated factors
|
||||
lastLocation.longitude += (rotatedDx / pixelsPerDegreeLon)
|
||||
lastLocation.latitude -= (rotatedDy / pixelsPerDegreeLat)
|
||||
|
||||
val pos = Position(lastLocation.longitude, lastLocation.latitude)
|
||||
updateCameraPosition( target = pos)
|
||||
updateCameraPosition(
|
||||
bearing = bearing,
|
||||
zoom = zoom,
|
||||
target = pos
|
||||
)
|
||||
navigationSession.invalidateNavigationScreen()
|
||||
}
|
||||
}
|
||||
@@ -246,7 +271,6 @@ class SurfaceRenderer(
|
||||
*/
|
||||
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
|
||||
synchronized(this@SurfaceRenderer) {
|
||||
Log.d(TAG, "onScale")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,7 +325,7 @@ class SurfaceRenderer(
|
||||
) {
|
||||
val cameraDuration =
|
||||
duration(
|
||||
viewStyle == ViewStyle.PREVIEW,
|
||||
viewStyle,
|
||||
position!!.bearing,
|
||||
lastBearing,
|
||||
lastLocationUpdate
|
||||
@@ -317,7 +341,8 @@ class SurfaceRenderer(
|
||||
width,
|
||||
height,
|
||||
streetName,
|
||||
darkMode
|
||||
darkMode,
|
||||
tilt
|
||||
)
|
||||
}
|
||||
LaunchedEffect(position, viewStyle) {
|
||||
@@ -351,13 +376,11 @@ class SurfaceRenderer(
|
||||
viewStyle = ViewStyle.PAN_VIEW
|
||||
}
|
||||
val newZoom = if (zoomSign < 0) {
|
||||
cameraPosition.value!!.zoom - 0.2
|
||||
cameraPosition.value!!.zoom - 1
|
||||
} else {
|
||||
cameraPosition.value!!.zoom + 0.2
|
||||
}
|
||||
if (viewStyle == ViewStyle.VIEW) {
|
||||
tilt = calculateTilt(newZoom, tilt)
|
||||
cameraPosition.value!!.zoom + 1
|
||||
}
|
||||
tilt = calculateTilt(viewStyle, newZoom, tilt)
|
||||
updateCameraPosition(
|
||||
cameraPosition.value!!.bearing,
|
||||
newZoom,
|
||||
@@ -372,23 +395,26 @@ class SurfaceRenderer(
|
||||
* Uses car orientation sensor if available, otherwise falls back to location bearing.
|
||||
*/
|
||||
fun updateLocation(location: Location, streetName: String) {
|
||||
Log.d(TAG, "updateLocation Surface $location $streetName")
|
||||
synchronized(this) {
|
||||
street.value = streetName
|
||||
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
||||
val bearing = if (carOrientation == 999F) {
|
||||
if (location.hasBearing()) {
|
||||
//val bearing =
|
||||
// location.bearing.toDouble()
|
||||
//carOrientation = bearing.toFloat()
|
||||
// val bearing = if (carOrientation == 999F) {
|
||||
val bearing = if (location.hasBearing()) {
|
||||
location.bearing.toDouble()
|
||||
} else {
|
||||
bearing(
|
||||
bearingPositive(
|
||||
lastLocation,
|
||||
location,
|
||||
cameraPosition.value!!.bearing
|
||||
)
|
||||
}
|
||||
} else {
|
||||
carOrientation.toDouble()
|
||||
}
|
||||
carOrientation = bearing.toFloat()
|
||||
// } else {
|
||||
// carOrientation.toDouble()
|
||||
// }
|
||||
val zoom = if (viewStyle == ViewStyle.VIEW) {
|
||||
calculateZoom(location.speed.toDouble())
|
||||
} else {
|
||||
@@ -409,8 +435,11 @@ class SurfaceRenderer(
|
||||
* Sets route data for active navigation and switches to VIEW mode.
|
||||
*/
|
||||
fun setRouteData(routeGeoJson: String) {
|
||||
routeData.value = routeGeoJson
|
||||
viewStyle = ViewStyle.VIEW
|
||||
synchronized(this) {
|
||||
routeData.value = routeGeoJson
|
||||
viewStyle = ViewStyle.VIEW
|
||||
updateLocation(lastLocation, "")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -478,7 +507,7 @@ class SurfaceRenderer(
|
||||
}
|
||||
viewStyle = ViewStyle.VIEW
|
||||
val zoom = calculateZoom(0.0)
|
||||
tilt = calculateTilt(zoom, tilt)
|
||||
tilt = calculateTilt(viewStyle, zoom, tilt)
|
||||
updateCameraPosition(
|
||||
tilt = tilt,
|
||||
zoom = zoom,
|
||||
|
||||
@@ -49,7 +49,6 @@ class TextToSpeechManager(private val carContext: Context) {
|
||||
})
|
||||
}
|
||||
initialized = true
|
||||
Log.d("TTS", "Initialization Success")
|
||||
} else {
|
||||
Log.e("TTS", "Initialization Failed")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.kouros.navigation.car.map
|
||||
|
||||
import android.location.Location
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -24,15 +23,25 @@ import androidx.compose.ui.text.drawText
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.kouros.data.R
|
||||
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.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.SlowColor
|
||||
import com.kouros.navigation.data.SpeedColor
|
||||
import com.kouros.navigation.data.StationaryColor
|
||||
import com.kouros.navigation.data.ViewStyle
|
||||
import com.kouros.navigation.utils.GeoUtils.createPointCollection
|
||||
import com.kouros.navigation.utils.isMetricSystem
|
||||
import com.kouros.navigation.utils.location
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
@@ -45,15 +54,14 @@ import org.maplibre.compose.expressions.dsl.image
|
||||
import org.maplibre.compose.expressions.dsl.interpolate
|
||||
import org.maplibre.compose.expressions.dsl.zoom
|
||||
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.FillLayer
|
||||
import org.maplibre.compose.layers.LineLayer
|
||||
import org.maplibre.compose.layers.SymbolLayer
|
||||
import org.maplibre.compose.location.LocationPuck
|
||||
import org.maplibre.compose.location.LocationPuckColors
|
||||
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.MaplibreMap
|
||||
import org.maplibre.compose.map.OrnamentOptions
|
||||
@@ -62,6 +70,8 @@ import org.maplibre.compose.sources.Source
|
||||
import org.maplibre.compose.sources.getBaseSource
|
||||
import org.maplibre.compose.sources.rememberGeoJsonSource
|
||||
import org.maplibre.compose.style.BaseStyle
|
||||
import org.maplibre.geojson.FeatureCollection
|
||||
import org.maplibre.geojson.LineString
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -104,100 +114,188 @@ fun MapLibre(
|
||||
cameraState = cameraState,
|
||||
baseStyle = baseStyle,
|
||||
|
||||
) {
|
||||
) {
|
||||
getBaseSource(id = "openmaptiles")?.let { tiles ->
|
||||
if (!showBuildings) {
|
||||
BuildingLayer(tiles)
|
||||
}
|
||||
if (viewStyle == ViewStyle.AMENITY_VIEW) {
|
||||
val lastLocation = location(cameraState.position.target.longitude, cameraState.position.target.latitude)
|
||||
val lastLocation = location(
|
||||
cameraState.position.target.longitude,
|
||||
cameraState.position.target.latitude
|
||||
)
|
||||
Puck(cameraState, lastLocation)
|
||||
AmenityLayer(route)
|
||||
} else {
|
||||
RouteLayer(route, traffic!!)
|
||||
//RouteLayerPoint(route )
|
||||
TrafficLayer(traffic!!)
|
||||
RouteLayer(route)
|
||||
StartEndLayer(route)
|
||||
// change also createLineStringCollection in GeoUtils
|
||||
// Uncomment StartEndLayer
|
||||
// RouteLayerPoint(route )
|
||||
}
|
||||
SpeedCameraLayer(speedCameras)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
|
||||
fun RouteLayer(routeData: String?) {
|
||||
if (!routeData.isNullOrEmpty()) {
|
||||
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
||||
LineLayer(
|
||||
id = "routes-casing",
|
||||
source = routes,
|
||||
color = const(Color.White),
|
||||
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),
|
||||
),
|
||||
color = const(Color.Green),
|
||||
width = routeLineWidth(base = 1.dp, isCasing = true),
|
||||
)
|
||||
LineLayer(
|
||||
id = "routes",
|
||||
source = routes,
|
||||
color = const(RouteColor),
|
||||
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),
|
||||
),
|
||||
width = routeLineWidth(base = 1.dp, isCasing = false),
|
||||
)
|
||||
}
|
||||
trafficData.forEach {
|
||||
val traffic = rememberGeoJsonSource(GeoJsonData.JsonString(it.value))
|
||||
LineLayer(
|
||||
id = "traffic-${it.key}-casing",
|
||||
source = traffic,
|
||||
color = const(Color.White),
|
||||
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(
|
||||
id = "traffic-${it.key}",
|
||||
source = traffic,
|
||||
color = trafficColor(it.key),
|
||||
width =
|
||||
interpolate(
|
||||
type = exponential(1.2f),
|
||||
input = zoom(),
|
||||
5 to const(0.4.dp),
|
||||
6 to const(0.5.dp),
|
||||
7 to const(1.6.dp),
|
||||
20 to const(18.dp),
|
||||
),
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TrafficLayer(trafficData: Map<String, String>) {
|
||||
trafficData.forEach {
|
||||
if (it.key == "closed") {
|
||||
//ClosedLayer(it)
|
||||
} else {
|
||||
val traffic = rememberGeoJsonSource(GeoJsonData.JsonString(it.value))
|
||||
LineLayer(
|
||||
id = "traffic-${it.key}-casing",
|
||||
source = traffic,
|
||||
color = const(Color.White),
|
||||
width = routeLineWidth(base = 2.dp, isCasing = true),
|
||||
)
|
||||
LineLayer(
|
||||
id = "traffic-${it.key}",
|
||||
source = traffic,
|
||||
color = trafficColor(it.key),
|
||||
width = routeLineWidth(base = 1.dp, isCasing = false),
|
||||
)
|
||||
}
|
||||
// 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(
|
||||
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 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
|
||||
fun RouteLayerPoint(routeData: String?) {
|
||||
if (!routeData.isNullOrEmpty()) {
|
||||
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
||||
val img = image(painterResource(R.drawable.ic_favorite_filled_white_24dp), drawAsSdf = true)
|
||||
val img = image(painterResource(R.drawable.settings_48px), drawAsSdf = true)
|
||||
SymbolLayer(
|
||||
id = "point-layer",
|
||||
id = "route-point-layer",
|
||||
source = routes,
|
||||
iconOpacity = const(2.0f),
|
||||
iconColor = const(Color.Red),
|
||||
@@ -206,23 +304,34 @@ fun RouteLayerPoint(routeData: String?) {
|
||||
interpolate(
|
||||
type = exponential(1.2f),
|
||||
input = zoom(),
|
||||
5 to const(0.4f),
|
||||
6 to const(0.6f),
|
||||
7 to const(0.8f),
|
||||
20 to const(1.0f),
|
||||
5 to const(0.8f),
|
||||
6 to const(1.0f),
|
||||
7 to const(1.2f),
|
||||
20 to const(1.4f),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@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> {
|
||||
when (key) {
|
||||
"queuing" -> return const(Color(0xFFC46E53))
|
||||
"slow" -> return const(Color(0xFFC43E3E))
|
||||
"stationary" -> return const(Color(0xFF910A0A))
|
||||
"heavy" -> return const(Color(0xFF6B0404))
|
||||
"roadworks" -> return const(Color(0xFF443506))
|
||||
"queuing" -> return const(QueuingColor)
|
||||
"slow" -> return const(SlowColor)
|
||||
"stationary" -> return const(StationaryColor)
|
||||
"heavy" -> return const(HeavyColor)
|
||||
"roadworks" -> return const(RoadworksColor)
|
||||
"lane" -> return const(LaneColor)
|
||||
}
|
||||
return const(Color.Blue)
|
||||
}
|
||||
@@ -233,11 +342,14 @@ fun AmenityLayer(routeData: String?) {
|
||||
var color = const(Color.Red)
|
||||
var img = image(painterResource(R.drawable.local_pharmacy_24px), drawAsSdf = true)
|
||||
if (routeData.contains(Constants.CHARGING_STATION)) {
|
||||
color = const(Color(0xFF054603))
|
||||
color = const(PharmacyColor)
|
||||
img = image(painterResource(R.drawable.ev_station_24px), drawAsSdf = true)
|
||||
} else if (routeData.contains(Constants.FUEL_STATION)) {
|
||||
color = const(Color.Blue)
|
||||
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))
|
||||
SymbolLayer(
|
||||
@@ -303,13 +415,14 @@ fun DrawNavigationImages(
|
||||
height: Int,
|
||||
streetName: String?,
|
||||
darkMode: Boolean,
|
||||
tilt: Double,
|
||||
) {
|
||||
NavigationImage(padding, width, height, streetName, darkMode)
|
||||
NavigationImage(padding, width, height, streetName, darkMode, tilt)
|
||||
if (speed != null) {
|
||||
CurrentSpeed(width, height, speed, maxSpeed)
|
||||
}
|
||||
if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) {
|
||||
MaxSpeed(width, height, maxSpeed)
|
||||
if (speed != null && maxSpeed > 0) {
|
||||
MaxSpeed(width, height, maxSpeed, speed)
|
||||
}
|
||||
//DebugInfo(width, height, lat!!)
|
||||
}
|
||||
@@ -320,19 +433,20 @@ fun NavigationImage(
|
||||
width: Int,
|
||||
height: Int,
|
||||
streetName: String?,
|
||||
darkMode: Boolean
|
||||
darkMode: Boolean,
|
||||
tilt: Double
|
||||
) {
|
||||
|
||||
val imageSize = (height / 8)
|
||||
val navigationColor = if (darkMode)
|
||||
remember { NavigationColorDark }
|
||||
else
|
||||
remember { NavigationColorLight }
|
||||
else
|
||||
remember { NavigationColorDark }
|
||||
|
||||
val textMeasurerStreet = rememberTextMeasurer()
|
||||
val street = streetName.toString()
|
||||
val styleStreet = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (darkMode) Color.White else navigationColor,
|
||||
)
|
||||
@@ -340,13 +454,18 @@ fun NavigationImage(
|
||||
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)) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.size(imageSize.dp, imageSize.dp)
|
||||
) {
|
||||
scale(scaleX = 1f, scaleY = 0.7f) {
|
||||
drawCircle(navigationColor.copy(alpha = 0.3f))
|
||||
scale(scaleX = 1f, scaleY = scaleY) {
|
||||
drawCircle(NavigationCircle.copy(alpha = 0.4f))
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
@@ -355,19 +474,19 @@ fun NavigationImage(
|
||||
tint = navigationColor.copy(alpha = 0.7f),
|
||||
modifier = Modifier
|
||||
.size(imageSize.dp, imageSize.dp)
|
||||
.scale(scaleX = 1f, scaleY = 0.7f),
|
||||
.scale(scaleX = 1f, scaleY = scaleY),
|
||||
)
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.size(textLayoutStreet.size.width.dp, textLayoutStreet.size.height.dp * 6 )
|
||||
.size(textLayoutStreet.size.width.dp, textLayoutStreet.size.height.dp * 6)
|
||||
) {
|
||||
if (street.isNotEmpty()) {
|
||||
val topLeftX = center.x - textLayoutStreet.size.width / 2
|
||||
val topLeftY = center.y + textLayoutStreet.size.height
|
||||
drawRoundRect(
|
||||
topLeft = Offset(
|
||||
x = topLeftX ,
|
||||
x = topLeftX,
|
||||
y = topLeftY,
|
||||
),
|
||||
color = if (darkMode) NavigationColorLight else Color.White,
|
||||
@@ -397,7 +516,7 @@ private fun CurrentSpeed(
|
||||
maxSpeed: Int
|
||||
) {
|
||||
|
||||
val radius = 34
|
||||
val radius = 36
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
@@ -409,17 +528,18 @@ private fun CurrentSpeed(
|
||||
val textMeasurerSpeed = rememberTextMeasurer()
|
||||
val textMeasurerKm = rememberTextMeasurer()
|
||||
|
||||
val speed = if (isMetricSystem()) (curSpeed * 3.6).toInt().toString() else (curSpeed * 3.6 * 0.6214).toInt().toString()
|
||||
val speed = if (isMetricSystem()) (curSpeed * 3.6).toInt()
|
||||
.toString() else (curSpeed * 3.6 * 0.6214).toInt().toString()
|
||||
|
||||
val kmh = if (isMetricSystem()) "km/h" else "mph"
|
||||
|
||||
val styleSpeed = TextStyle(
|
||||
fontSize = 22.sp,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
)
|
||||
val styleKm = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
fontSize = 14.sp,
|
||||
color = Color.White,
|
||||
)
|
||||
val textLayoutSpeed = remember(speed, maxSpeed) {
|
||||
@@ -464,6 +584,7 @@ private fun MaxSpeed(
|
||||
width: Int,
|
||||
height: Int,
|
||||
maxSpeed: Int,
|
||||
curSpeed: Float,
|
||||
) {
|
||||
val radius = 24
|
||||
Box(
|
||||
@@ -484,6 +605,11 @@ private fun MaxSpeed(
|
||||
val textLayoutSpeed = remember(speed) {
|
||||
textMeasurerSpeed.measure(speed, styleSpeed)
|
||||
}
|
||||
val signColor = if (curSpeed * 3.6 > (maxSpeed + 3)) {
|
||||
Color.Red
|
||||
} else {
|
||||
Color.Green
|
||||
}
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawCircle(
|
||||
center = Offset(
|
||||
@@ -491,7 +617,7 @@ private fun MaxSpeed(
|
||||
y = center.y
|
||||
),
|
||||
radius = radius * 1.3.toFloat(),
|
||||
color = Color.Red,
|
||||
color = signColor,
|
||||
)
|
||||
drawCircle(
|
||||
center = Offset(
|
||||
@@ -579,7 +705,7 @@ fun Puck(cameraState: CameraState, location: Location) {
|
||||
locationState = location,
|
||||
cameraState = cameraState,
|
||||
accuracyThreshold = 10f,
|
||||
oldLocationThreshold = 2.seconds,
|
||||
oldLocationThreshold = 1.seconds,
|
||||
showBearing = false,
|
||||
sizes = LocationPuckSizes(dotRadius = 10.dp),
|
||||
colors = LocationPuckColors(
|
||||
|
||||
@@ -1,349 +0,0 @@
|
||||
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,9 +24,11 @@ import androidx.car.app.navigation.model.LaneDirection
|
||||
import androidx.car.app.navigation.model.Maneuver
|
||||
import androidx.car.app.navigation.model.Step
|
||||
import androidx.car.app.navigation.model.TravelEstimate
|
||||
import androidx.car.app.navigation.model.Trip
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.kouros.data.R
|
||||
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.route.ManeuverType
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
@@ -124,10 +126,9 @@ class RouteCarModel : RouteModel() {
|
||||
.setRemainingTimeColor(CarColor.GREEN)
|
||||
.setRemainingDistanceColor(CarColor.BLUE)
|
||||
if (traffic > 0) {
|
||||
travelBuilder.setTripText(createDelay(traffic))
|
||||
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.traffic_jam_48px))
|
||||
travelBuilder.setTripText(createDelay(traffic))
|
||||
}
|
||||
|
||||
if (navState.travelMessage.isNotEmpty()) {
|
||||
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px))
|
||||
travelBuilder.setTripText(CarText.create(navState.travelMessage))
|
||||
@@ -165,6 +166,21 @@ class RouteCarModel : RouteModel() {
|
||||
.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 {
|
||||
val delayBuilder = SpannableStringBuilder()
|
||||
delayBuilder.append(
|
||||
@@ -250,7 +266,7 @@ class RouteCarModel : RouteModel() {
|
||||
R.string.exit_action_title, R.string.exit_action_title,
|
||||
FLAG_DEFAULT
|
||||
)
|
||||
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */5000)
|
||||
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */4000)
|
||||
.setSubtitle(subtitle)
|
||||
.setIcon(icon)
|
||||
.addAction(dismissAction).setCallback(object : AlertCallback {
|
||||
|
||||
@@ -3,11 +3,10 @@ package com.kouros.navigation.car.navigation
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.kouros.data.BuildConfig
|
||||
import com.kouros.navigation.data.Constants.TAG
|
||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||
import com.kouros.navigation.utils.location
|
||||
import io.ticofab.androidgpxparser.parser.GPXParser
|
||||
import io.ticofab.androidgpxparser.parser.domain.Gpx
|
||||
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
|
||||
@@ -29,9 +28,8 @@ class Simulation {
|
||||
) {
|
||||
if (routeModel.navState.route.isRouteValid()) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
gpxSimulation(routeModel, lifecycleScope, updateLocation)
|
||||
//gpxSimulation(updateLocation)
|
||||
//currentSimulation(routeModel, lifecycleScope, updateLocation)
|
||||
//gpxSimulation(routeModel, lifecycleScope, updateLocation)
|
||||
currentSimulation(routeModel, lifecycleScope, updateLocation)
|
||||
} else {
|
||||
currentSimulation(routeModel, lifecycleScope, updateLocation)
|
||||
}
|
||||
@@ -48,20 +46,20 @@ class Simulation {
|
||||
if (points.isEmpty()) return
|
||||
simulationJob?.cancel()
|
||||
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
|
||||
var curBearing = 0f
|
||||
var curBearing: Float
|
||||
simulationJob = lifecycleScope.launch {
|
||||
for ((index, point) in points.withIndex()) {
|
||||
if (index >= 0) {
|
||||
curBearing = lastLocation.bearingTo(location(point[0], point[1]))
|
||||
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
|
||||
latitude = point[1]
|
||||
longitude = point[0]
|
||||
bearing = curBearing
|
||||
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
|
||||
speed = 5.0f
|
||||
speed = 10.0f
|
||||
time = System.currentTimeMillis()
|
||||
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
|
||||
}
|
||||
curBearing = lastLocation.bearingTo(fakeLocation)
|
||||
// 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)
|
||||
@@ -69,7 +67,7 @@ class Simulation {
|
||||
lastLocation = fakeLocation
|
||||
}
|
||||
}
|
||||
// routeModel.stopNavigation()
|
||||
// routeModel.stopNavigation()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,16 +81,16 @@ class Simulation {
|
||||
runBlocking {
|
||||
simulationJob = launch(Dispatchers.IO) {
|
||||
route = TomTomRepository().fetchUrl(
|
||||
"https://kouros-online.de/vh.gpx",
|
||||
"https://kouros-online.de/VH.gpx",
|
||||
false
|
||||
)
|
||||
}
|
||||
simulationJob?.join()
|
||||
}
|
||||
simulationJob?.cancel()
|
||||
simulationJob = lifecycleScope.launch() {
|
||||
simulationJob =lifecycleScope.launch() {
|
||||
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
|
||||
var curBearing = 0f
|
||||
val curBearing = 0f
|
||||
val parser = GPXParser()
|
||||
val parsedGpx: Gpx? =
|
||||
parser.parse(route.byteInputStream())
|
||||
@@ -120,10 +118,7 @@ class Simulation {
|
||||
// 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)
|
||||
}
|
||||
delay(500)
|
||||
delay(200)
|
||||
lastTime = p.time
|
||||
lastLocation = fakeLocation
|
||||
}
|
||||
@@ -137,62 +132,4 @@ class Simulation {
|
||||
fun stopSimulation() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
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,19 +3,23 @@ package com.kouros.navigation.car.screen
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.CarColor
|
||||
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.ItemList
|
||||
import androidx.car.app.model.ListTemplate
|
||||
import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.core.graphics.toColorInt
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.data.Category
|
||||
import com.kouros.navigation.data.Constants.CHARGING_STATION
|
||||
import com.kouros.navigation.data.Constants.FUEL_STATION
|
||||
import com.kouros.navigation.data.Constants.PHARMACY
|
||||
import com.kouros.navigation.data.Constants.RESTAURANT
|
||||
import com.kouros.navigation.data.ViewStyle
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
|
||||
@@ -30,22 +34,18 @@ class CategoriesScreen(
|
||||
var categories: List<Category> = listOf(
|
||||
Category(id = FUEL_STATION, name = carContext.getString(R.string.fuel_station)),
|
||||
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 {
|
||||
val itemListBuilder = ItemList.Builder()
|
||||
.setNoItemsMessage("No categories to show")
|
||||
categories.forEach {
|
||||
itemListBuilder.addItem(
|
||||
Row.Builder()
|
||||
.setTitle(it.name)
|
||||
GridItem.Builder()
|
||||
.setImage(carIcon(carContext, it.id, -1))
|
||||
.setTitle(it.name)
|
||||
.setOnClickListener {
|
||||
category = it.id
|
||||
screenManager
|
||||
@@ -55,7 +55,7 @@ class CategoriesScreen(
|
||||
surfaceRenderer,
|
||||
category,
|
||||
navigationViewModel,
|
||||
)
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
setResult(obj)
|
||||
@@ -63,34 +63,68 @@ class CategoriesScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
.setBrowsable(true)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
surfaceRenderer.viewStyle = ViewStyle.AMENITY_VIEW
|
||||
|
||||
val header = Header.Builder()
|
||||
.setStartHeaderAction(Action.BACK)
|
||||
.setTitle(carContext.getString(R.string.category_title))
|
||||
.build()
|
||||
|
||||
return ListTemplate.Builder()
|
||||
.setHeader(header)
|
||||
return GridTemplate.Builder()
|
||||
.setHeader(
|
||||
Header.Builder()
|
||||
.setStartHeaderAction(Action.BACK)
|
||||
.setTitle(carContext.getString(R.string.category_title))
|
||||
.build()
|
||||
)
|
||||
.setSingleList(itemListBuilder.build())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun carIcon(context: CarContext, category: String, index: Int): CarIcon {
|
||||
|
||||
val customCarColor =
|
||||
CarColor.createCustom(android.graphics.Color.MAGENTA, android.graphics.Color.MAGENTA)
|
||||
|
||||
if (index == -1) {
|
||||
val resId = when (category) {
|
||||
CHARGING_STATION -> R.drawable.ev_station_24px
|
||||
FUEL_STATION -> R.drawable.local_gas_station_24
|
||||
PHARMACY -> R.drawable.local_pharmacy_24px
|
||||
else -> R.drawable.ic_place_white_24dp
|
||||
val icon = when (category) {
|
||||
CHARGING_STATION -> CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
context,
|
||||
R.drawable.ev_station_24px
|
||||
)
|
||||
).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 CarIcon.Builder(IconCompat.createWithResource(context, resId)).build()
|
||||
return icon.build()
|
||||
} else {
|
||||
return CarIcon.Builder(
|
||||
createNumberIcon(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.kouros.navigation.car.screen
|
||||
|
||||
import android.util.Log
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
@@ -19,6 +20,9 @@ import androidx.car.app.navigation.model.MapWithContentTemplate
|
||||
import androidx.car.app.versioning.CarAppApiLevels
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.car.screen.observers.CategoryObserver
|
||||
@@ -27,12 +31,18 @@ import com.kouros.navigation.data.Constants
|
||||
import com.kouros.navigation.data.Constants.CHARGING_STATION
|
||||
import com.kouros.navigation.data.Constants.FUEL_STATION
|
||||
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.overpass.Elements
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
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.round
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.min
|
||||
|
||||
class CategoryScreen(
|
||||
@@ -43,9 +53,14 @@ class CategoryScreen(
|
||||
|
||||
) : Screen(carContext), CategoryObserverCallback {
|
||||
|
||||
val repository = getSettingsRepository(carContext)
|
||||
|
||||
val settingsViewModel = getSettingsViewModel(carContext)
|
||||
|
||||
val maxListItems: Int = 30
|
||||
|
||||
var elements: List<Elements> = emptyList()
|
||||
|
||||
private val categoryObserver = CategoryObserver(this)
|
||||
|
||||
private var loading = true
|
||||
@@ -56,8 +71,12 @@ class CategoryScreen(
|
||||
navigationViewModel.elements.value = emptyList()
|
||||
}
|
||||
})
|
||||
|
||||
repository.lastFuelPricesFlow.asLiveData().observe(this, Observer {
|
||||
navigationViewModel.getAmenities(carContext, category, surfaceRenderer.lastLocation, it)
|
||||
})
|
||||
|
||||
navigationViewModel.elements.observe(this, categoryObserver)
|
||||
navigationViewModel.getAmenities(category, surfaceRenderer.lastLocation)
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -74,12 +93,10 @@ class CategoryScreen(
|
||||
)
|
||||
)
|
||||
elements.forEach {
|
||||
if (it.tags.operator != null) {
|
||||
if (index++ < listLimit) {
|
||||
listBuilder.addItem(
|
||||
createItem(it, category, index)
|
||||
)
|
||||
}
|
||||
if (index++ < listLimit) {
|
||||
listBuilder.addItem(
|
||||
createItem(it, category, index)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,18 +130,19 @@ class CategoryScreen(
|
||||
CHARGING_STATION -> R.string.charging_station
|
||||
FUEL_STATION -> R.string.fuel_station
|
||||
PHARMACY -> R.string.pharmacy
|
||||
else -> R.string.no_places
|
||||
else -> R.string.restaurant
|
||||
}
|
||||
return carContext.getString(resId)
|
||||
}
|
||||
|
||||
private fun createItem(it: Elements, category: String, index: Int): Row {
|
||||
var name = ""
|
||||
if (it.tags.name != null) {
|
||||
name = it.tags.name.toString()
|
||||
name = it.tags.name
|
||||
if (name.isEmpty()) {
|
||||
name = it.tags.operator
|
||||
}
|
||||
if (name.isEmpty()) {
|
||||
name = it.tags.operator.toString()
|
||||
name = "Empty"
|
||||
}
|
||||
val row = Row.Builder()
|
||||
.setOnClickListener {
|
||||
@@ -133,17 +151,17 @@ class CategoryScreen(
|
||||
}
|
||||
.setTitle(name)
|
||||
.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) {
|
||||
row.addText("${(it.distance).toInt()} m")
|
||||
} else {
|
||||
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(
|
||||
createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT, {
|
||||
navigationViewModel.loadRoute(
|
||||
|
||||
@@ -2,9 +2,7 @@ package com.kouros.navigation.car.screen
|
||||
|
||||
import android.os.CountDownTimer
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
|
||||
@@ -21,7 +19,6 @@ import androidx.car.app.navigation.model.Destination
|
||||
import androidx.car.app.navigation.model.MapWithContentTemplate
|
||||
import androidx.car.app.navigation.model.MessageInfo
|
||||
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.Step
|
||||
import androidx.car.app.navigation.model.TravelEstimate
|
||||
@@ -35,7 +32,6 @@ import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.car.screen.settings.SettingsScreen
|
||||
import com.kouros.navigation.data.Constants
|
||||
import com.kouros.navigation.data.Constants.TAG
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.data.ViewStyle
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
@@ -55,7 +51,9 @@ open class NavigationScreen(
|
||||
private val navigationViewModel: NavigationViewModel
|
||||
) : Screen(carContext) {
|
||||
|
||||
var deviation = 0F
|
||||
var recentPlaces = mutableListOf<Place>()
|
||||
|
||||
var recentPlace: Place = Place()
|
||||
var navigationType = NavigationType.VIEW
|
||||
|
||||
@@ -82,9 +80,10 @@ open class NavigationScreen(
|
||||
private var junctionImage: CarIcon? = null
|
||||
private var backGroundColor = CarColor.BLUE
|
||||
|
||||
private var message = ""
|
||||
|
||||
private var showAlternativeRoute = false
|
||||
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
|
||||
Log.d(TAG, "NavigationScreen 4")
|
||||
recentPlaces.addAll(newPlaces)
|
||||
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
|
||||
tripSuggestionCalled = true
|
||||
@@ -99,11 +98,9 @@ open class NavigationScreen(
|
||||
}
|
||||
|
||||
repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
|
||||
Log.d(TAG, "NavigationScreen 3")
|
||||
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
|
||||
tripSuggestion = it
|
||||
})
|
||||
|
||||
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
|
||||
showAlternativeRoute = it
|
||||
})
|
||||
@@ -119,7 +116,6 @@ open class NavigationScreen(
|
||||
* Returns the appropriate template based on the current navigation state.
|
||||
*/
|
||||
override fun onGetTemplate(): Template {
|
||||
Log.d(TAG, "NavigationScreen 2")
|
||||
val actionStripBuilder = createActionStripBuilder({
|
||||
createAction(
|
||||
carContext,
|
||||
@@ -140,14 +136,14 @@ open class NavigationScreen(
|
||||
* Creates and returns a NavigationTemplate for the active navigation state.
|
||||
*/
|
||||
private fun navigation(actionStripBuilder: ActionStrip.Builder): Template {
|
||||
actionStripBuilder.addAction(
|
||||
createAction(
|
||||
carContext,
|
||||
R.drawable.ic_close_white_24dp,
|
||||
0,
|
||||
{ stopNavigation() })
|
||||
)
|
||||
return NavigationTemplate.Builder()
|
||||
// actionStripBuilder.addAction(
|
||||
// createAction(
|
||||
// carContext,
|
||||
// R.drawable.ic_close_white_24dp,
|
||||
// 0
|
||||
// ) { stopNavigation() }
|
||||
// )
|
||||
val navigationTemplate = NavigationTemplate.Builder()
|
||||
.setNavigationInfo(
|
||||
getRoutingInfo()
|
||||
)
|
||||
@@ -170,6 +166,7 @@ open class NavigationScreen(
|
||||
)
|
||||
.setBackgroundColor(backGroundColor)
|
||||
.build()
|
||||
return navigationTemplate
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,7 +181,7 @@ open class NavigationScreen(
|
||||
carContext = carContext, R.drawable.ic_recenter_24,
|
||||
0,
|
||||
onClickAction = {
|
||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||
surfaceRenderer.setStandardView()
|
||||
invalidate()
|
||||
})
|
||||
})
|
||||
@@ -193,7 +190,7 @@ open class NavigationScreen(
|
||||
.setActionStrip(actionStripBuilder.build())
|
||||
.setMapActionStrip(mapActionStrip)
|
||||
.setPanModeListener { isInPanMode: Boolean ->
|
||||
Log.d(TAG, "PanMode $isInPanMode")
|
||||
|
||||
}
|
||||
.build()
|
||||
}
|
||||
@@ -219,16 +216,12 @@ open class NavigationScreen(
|
||||
* Creates and returns a NavigationTemplate specifically for when the destination is reached.
|
||||
*/
|
||||
fun navigationArrived(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
|
||||
var street = ""
|
||||
if (destinations.first().address != null) {
|
||||
street = destinations.first().address.toString()
|
||||
}
|
||||
return NavigationTemplate.Builder()
|
||||
.setNavigationInfo(
|
||||
MessageInfo.Builder(
|
||||
carContext.getString(R.string.arrived_exclamation_msg)
|
||||
)
|
||||
.setText(street)
|
||||
.setText(message)
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
@@ -345,12 +338,10 @@ open class NavigationScreen(
|
||||
*/
|
||||
fun getRoutingInfo(): RoutingInfo {
|
||||
val routingInfo = RoutingInfo.Builder()
|
||||
if (steps.isNotEmpty()) {
|
||||
routingInfo.setCurrentStep(
|
||||
.setCurrentStep(
|
||||
steps.first(),
|
||||
stepRemainingDistance
|
||||
)
|
||||
}
|
||||
if (shouldShowNextStep && steps.size > 1) {
|
||||
routingInfo.setNextStep(steps[1])
|
||||
}
|
||||
@@ -480,13 +471,14 @@ open class NavigationScreen(
|
||||
/**
|
||||
* Initiates recalculation for a new route to the destination.
|
||||
*/
|
||||
fun calculateNewRoute(destination: Place) {
|
||||
fun calculateNewRoute(destination: Place, distance: Float) {
|
||||
deviation = distance
|
||||
navigationType = NavigationType.REROUTE
|
||||
invalidate()
|
||||
val mainThreadHandler = Handler(carContext.mainLooper)
|
||||
mainThreadHandler.post {
|
||||
reRouteTimer?.cancel()
|
||||
reRouteTimer = object : CountDownTimer(2000, 1000) {
|
||||
reRouteTimer = object : CountDownTimer(3000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
override fun onFinish() {
|
||||
navigationType = NavigationType.NAVIGATION
|
||||
@@ -497,7 +489,6 @@ open class NavigationScreen(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates navigation state with the current location, checks for arrival, and traffic updates.
|
||||
*/
|
||||
@@ -513,7 +504,8 @@ open class NavigationScreen(
|
||||
shouldShowNextStep: Boolean,
|
||||
shouldShowLanes: Boolean,
|
||||
junctionImage: CarIcon?,
|
||||
backGroundColor: CarColor
|
||||
backGroundColor: CarColor,
|
||||
message: String
|
||||
) {
|
||||
this.isNavigating = isNavigating
|
||||
this.isRerouting = isRerouting
|
||||
@@ -527,6 +519,7 @@ open class NavigationScreen(
|
||||
this.shouldShowLanes = shouldShowLanes
|
||||
this.junctionImage = junctionImage
|
||||
this.backGroundColor = backGroundColor
|
||||
this.message = message
|
||||
navigationType = NavigationType.NAVIGATION
|
||||
invalidate()
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.kouros.navigation.data.Constants.RECENT
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
import com.kouros.navigation.utils.location
|
||||
|
||||
class PlaceListScreen(
|
||||
private val carContext: CarContext,
|
||||
@@ -59,6 +60,12 @@ class PlaceListScreen(
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
navigationViewModel.recentPlaces.value = emptyList()
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
recentPlaces.forEach {
|
||||
it.distance = location(it.longitude, it.latitude).distanceTo(surfaceRenderer.lastLocation)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,13 +75,16 @@ class PlaceListScreen(
|
||||
override fun onGetTemplate(): Template {
|
||||
val itemListBuilder = ItemList.Builder()
|
||||
.setNoItemsMessage(carContext.getString(R.string.no_places))
|
||||
recentPlaces.filter { it.category == category }.forEach {
|
||||
val street = it.street
|
||||
recentPlaces.filter { it.category == category && it.distance > 500F }.forEach {
|
||||
val street = it.street.ifEmpty {
|
||||
it.name
|
||||
}
|
||||
|
||||
val row = Row.Builder()
|
||||
.setImage(contactIcon(null, it.category))
|
||||
.setTitle("$street ${it.city}")
|
||||
.setOnClickListener {
|
||||
clickOnPlace(it)
|
||||
clickOnPlace(it)
|
||||
}
|
||||
if (category != CONTACTS) {
|
||||
row.addText(SpannableString(" ").apply {
|
||||
@@ -115,22 +125,11 @@ class PlaceListScreen(
|
||||
/**
|
||||
* Creates an Action to navigate to a specific 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
|
||||
)
|
||||
private fun clickOnPlace(itPlace: Place) {
|
||||
if (surfaceRenderer.navigation) {
|
||||
startStopOverScreen(place)
|
||||
startStopOverScreen(itPlace)
|
||||
} else {
|
||||
starPreviewScreen(place)
|
||||
starPreviewScreen(itPlace)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +141,7 @@ class PlaceListScreen(
|
||||
.pushForResult(
|
||||
RoutePreviewScreen(
|
||||
carContext,
|
||||
if (showAlternativeRoute) RoutePreviewType.MULTI_ROUTE else RoutePreviewType.SINGLE_ROUTE,
|
||||
if (showAlternativeRoute) RoutePreviewType.MULTI_ROUTE else RoutePreviewType.SINGLE_ROUTE,
|
||||
surfaceRenderer,
|
||||
place,
|
||||
navigationViewModel,
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.Action.FLAG_DEFAULT
|
||||
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.CarText
|
||||
import androidx.car.app.model.DurationSpan
|
||||
import androidx.car.app.model.ForegroundCarColorSpan
|
||||
@@ -26,7 +25,6 @@ import androidx.car.app.model.Template
|
||||
import androidx.car.app.navigation.model.MapController
|
||||
import androidx.car.app.navigation.model.MapWithContentTemplate
|
||||
import androidx.car.app.versioning.CarAppApiLevels
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
@@ -35,6 +33,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
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.Place
|
||||
import com.kouros.navigation.data.ViewStyle
|
||||
@@ -58,7 +57,7 @@ class RoutePreviewScreen(
|
||||
private var showAlternativeRoute: Boolean
|
||||
) :
|
||||
Screen(carContext) {
|
||||
private var isFavorite = false
|
||||
private var isFavorite = destination.favorite
|
||||
|
||||
val maxListItems: Int = 3
|
||||
|
||||
@@ -72,6 +71,7 @@ class RoutePreviewScreen(
|
||||
|
||||
var loading = true
|
||||
|
||||
var previewReady = false;
|
||||
var flag = FLAG_DEFAULT
|
||||
|
||||
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
||||
@@ -85,6 +85,7 @@ class RoutePreviewScreen(
|
||||
routeModel.startNavigation(route)
|
||||
surfaceRenderer.setPreviewRouteData(routeModel)
|
||||
loading = false
|
||||
previewReady = true
|
||||
if (routeModel.route.routes.size == 1 && showAlternativeRoute) {
|
||||
routeType = RoutePreviewType.SINGLE_ROUTE
|
||||
showAlternativeRoute = false
|
||||
@@ -92,6 +93,7 @@ class RoutePreviewScreen(
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
val trafficObserver = Observer<Map<String, String>> { traffic ->
|
||||
if (traffic.isNotEmpty()) {
|
||||
navigationViewModel.traffic.value = emptyMap()
|
||||
@@ -115,7 +117,6 @@ class RoutePreviewScreen(
|
||||
})
|
||||
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
||||
routingEngine = it
|
||||
|
||||
})
|
||||
|
||||
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
|
||||
@@ -128,7 +129,6 @@ class RoutePreviewScreen(
|
||||
location(destination.longitude, destination.latitude),
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,15 +160,13 @@ class RoutePreviewScreen(
|
||||
header.addEndHeaderAction(
|
||||
favoriteAction()
|
||||
)
|
||||
header.addEndHeaderAction(
|
||||
deleteFavoriteAction()
|
||||
)
|
||||
}
|
||||
val message =
|
||||
if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) {
|
||||
createRouteText(routeModel.route.routes.first())
|
||||
} else {
|
||||
CarText.Builder("Wait")
|
||||
loading = true
|
||||
CarText.Builder(carContext.getString(R.string.wait))
|
||||
.build()
|
||||
}
|
||||
val content = if (routeType == RoutePreviewType.MULTI_ROUTE) {
|
||||
@@ -192,8 +190,10 @@ class RoutePreviewScreen(
|
||||
})
|
||||
val listContent = MessageTemplate.Builder(message)
|
||||
.setHeader(header.build())
|
||||
.addAction(navigateAction)
|
||||
|
||||
if (previewReady) {
|
||||
listContent.addAction(navigateAction)
|
||||
}
|
||||
if (showAlternativeRoute) {
|
||||
listContent.addAction(selectRouteAction)
|
||||
}
|
||||
@@ -213,14 +213,16 @@ class RoutePreviewScreen(
|
||||
|
||||
)
|
||||
if (routeModel.route.routes.size > 1 && routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) {
|
||||
template.setActionStrip(createActionStrip {
|
||||
createAction(
|
||||
carContext, R.drawable.navigation_48px,
|
||||
onClickAction = {
|
||||
onNavigate(routeModel.navState.currentRouteIndex)
|
||||
}
|
||||
)
|
||||
})
|
||||
if (previewReady) {
|
||||
template.setActionStrip(createActionStrip {
|
||||
createAction(
|
||||
carContext, R.drawable.navigation_48px,
|
||||
onClickAction = {
|
||||
onNavigate(routeModel.navState.currentRouteIndex)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
return template.build()
|
||||
}
|
||||
@@ -259,34 +261,17 @@ class RoutePreviewScreen(
|
||||
else
|
||||
R.drawable.ic_favorite_white_24dp
|
||||
, FLAG_IS_PERSISTENT,
|
||||
) {
|
||||
isFavorite = !isFavorite
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
if (isFavorite)
|
||||
carContext
|
||||
.getString(R.string.favorites)
|
||||
else
|
||||
carContext.getString(
|
||||
R.string.favorites
|
||||
),
|
||||
CarToast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
navigationViewModel.saveFavorite(carContext, destination)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
|
||||
private fun deleteFavoriteAction(): Action =
|
||||
createAction(carContext, R.drawable.heart_minus_48px, FLAG_IS_PERSISTENT,{
|
||||
if (isFavorite) {
|
||||
navigationViewModel.deleteFavorite(carContext, destination)
|
||||
onClickAction = {
|
||||
isFavorite = !isFavorite
|
||||
destination.favorite = isFavorite
|
||||
if (isFavorite) {
|
||||
navigationViewModel.saveFavorite(carContext, destination)
|
||||
} else {
|
||||
navigationViewModel.deleteFavorite(carContext, destination)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
isFavorite = !isFavorite
|
||||
finish()
|
||||
})
|
||||
|
||||
)
|
||||
|
||||
private fun createRouteText(route: Routes): CarText {
|
||||
val time = route.summary.duration
|
||||
@@ -321,7 +306,9 @@ class RoutePreviewScreen(
|
||||
.setTitle(routeText)
|
||||
.setOnClickListener { onRouteSelected(index) }
|
||||
.addText(street)
|
||||
.addAction(navigateAction)
|
||||
if (previewReady) {
|
||||
row.addAction(navigateAction)
|
||||
}
|
||||
if (route.summary.trafficDelay > 60) {
|
||||
row.addText(createDelay(route))
|
||||
row.setImage(createCarIcon(carContext = carContext, R.drawable.traffic_jam_48px))
|
||||
@@ -347,10 +334,12 @@ class RoutePreviewScreen(
|
||||
}
|
||||
|
||||
private fun onNavigate(index: Int) {
|
||||
destination.routeIndex = index
|
||||
destination.route = navigationViewModel.previewRoute.value.toString()
|
||||
setResult(destination)
|
||||
finish()
|
||||
if (previewReady) {
|
||||
destination.routeIndex = index
|
||||
destination.route = navigationViewModel.previewRoute.value.toString()
|
||||
setResult(destination)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRouteSelected(index: Int) {
|
||||
@@ -367,7 +356,6 @@ class RoutePreviewScreen(
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum class RoutePreviewType {
|
||||
|
||||
@@ -50,7 +50,7 @@ fun createNumberIcon(category: String, number: String): IconCompat {
|
||||
CHARGING_STATION -> Color.GREEN
|
||||
FUEL_STATION -> Color.BLUE
|
||||
PHARMACY -> Color.RED
|
||||
else -> Color.WHITE
|
||||
else -> Color.MAGENTA
|
||||
}
|
||||
paint.color = color
|
||||
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
|
||||
|
||||
@@ -19,7 +19,6 @@ import com.kouros.navigation.data.Constants.CATEGORIES
|
||||
import com.kouros.navigation.data.Constants.FAVORITES
|
||||
import com.kouros.navigation.data.Constants.RECENT
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.data.ViewStyle
|
||||
import com.kouros.navigation.data.nominatim.SearchResult
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
|
||||
@@ -86,22 +85,7 @@ class SearchScreen(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
screenManager
|
||||
.pushForResult(
|
||||
PlaceListScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
it.id,
|
||||
navigationViewModel,
|
||||
recentPlaces
|
||||
)
|
||||
) { obj: Any? ->
|
||||
surfaceRenderer.setStandardView()
|
||||
if (obj != null) {
|
||||
setResult(obj)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
startPlaceListScreen(it)
|
||||
}
|
||||
}
|
||||
.setBrowsable(true)
|
||||
@@ -131,6 +115,24 @@ class SearchScreen(
|
||||
.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 {
|
||||
val resId: Int = when (category) {
|
||||
RECENT -> {
|
||||
@@ -182,7 +184,28 @@ class SearchScreen(
|
||||
distance = result.distance
|
||||
)
|
||||
recentPlaces.add(place)
|
||||
startPreviewScreen(place)
|
||||
setResult(place)
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
package com.kouros.navigation.car.screen.observers
|
||||
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.data.nominatim.SearchResult
|
||||
import com.kouros.navigation.data.overpass.Elements
|
||||
|
||||
/**
|
||||
@@ -25,9 +26,9 @@ interface NavigationObserverCallback {
|
||||
/** Called when max speed is updated */
|
||||
fun onMaxSpeedReceived(speed: Int)
|
||||
|
||||
fun onRecentPlacesReceived(places: List<Place>)
|
||||
|
||||
/** Called to request UI invalidation/refresh */
|
||||
fun invalidateScreen()
|
||||
|
||||
fun onTrafficMessageReceived(trafficMessage: String)
|
||||
|
||||
}
|
||||
|
||||
+4
-8
@@ -1,6 +1,5 @@
|
||||
package com.kouros.navigation.car.screen.observers
|
||||
|
||||
import com.kouros.navigation.car.CarSession
|
||||
import com.kouros.navigation.car.NavigationSession
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
|
||||
@@ -15,22 +14,19 @@ class NavigationObserverManager(
|
||||
|
||||
val routeObserver = RouteObserver(callback)
|
||||
val trafficObserver = TrafficObserver(callback)
|
||||
|
||||
val trafficMessageObserver = TrafficMessageObserver(callback)
|
||||
val placeSearchObserver = PlaceSearchObserver(callback)
|
||||
val speedCameraObserver = SpeedCameraObserver(callback)
|
||||
val maxSpeedObserver = MaxSpeedObserver(callback)
|
||||
|
||||
val recentPlacesObserver = RecentPlacesObserver(callback)
|
||||
|
||||
|
||||
|
||||
fun attachAllObservers(session: CarSession) {
|
||||
fun attachAllObservers(session: NavigationSession) {
|
||||
viewModel.route.observe(session, routeObserver)
|
||||
viewModel.traffic.observe(session, trafficObserver)
|
||||
viewModel.trafficMessage.observe(session, trafficMessageObserver)
|
||||
viewModel.placeLocation.observe(session, placeSearchObserver)
|
||||
viewModel.speedCameras.observe(session, speedCameraObserver)
|
||||
viewModel.maxSpeed.observe(session, maxSpeedObserver)
|
||||
viewModel.recentPlaces.observe(session, recentPlacesObserver)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
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,7 +1,9 @@
|
||||
package com.kouros.navigation.car.screen.settings
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.annotations.ExperimentalCarApi
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.Header
|
||||
@@ -16,6 +18,7 @@ import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.screen.CarHardwareInfoScreen
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.utils.getSettingsViewModel
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -37,6 +40,7 @@ class SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCarApi::class)
|
||||
override fun onGetTemplate(): Template {
|
||||
settingsViewModel.guidanceAudio.asLiveData().observe(this, Observer {
|
||||
audioToggleState = settingsViewModel.guidanceAudio.value == 1
|
||||
@@ -94,6 +98,13 @@ class SettingsScreen(
|
||||
)
|
||||
)
|
||||
|
||||
listBuilder.addItem(
|
||||
buildRowForTemplate(
|
||||
CarHardwareInfoScreen(carContext),
|
||||
R.string.model_info
|
||||
)
|
||||
)
|
||||
|
||||
listBuilder.addItem(
|
||||
buildRowForTemplate(
|
||||
CarSettings(carContext, navigationViewModel),
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.kouros.navigation.car.screen
|
||||
|
||||
import androidx.car.app.testing.ScreenController
|
||||
import androidx.car.app.testing.TestCarContext
|
||||
import androidx.car.app.navigation.model.NavigationTemplate
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
@@ -63,7 +62,6 @@ class NavigationScreenTest {
|
||||
navigationScreen = NavigationScreen(
|
||||
testCarContext,
|
||||
mockSurfaceRenderer,
|
||||
mockRouteModel,
|
||||
mockListener,
|
||||
mockViewModel
|
||||
)
|
||||
@@ -109,7 +107,7 @@ class NavigationScreenTest {
|
||||
navigationScreen.navigationType = NavigationType.NAVIGATION
|
||||
|
||||
// Act
|
||||
navigationScreen.calculateNewRoute(Place())
|
||||
navigationScreen.calculateNewRoute(Place(), distance)
|
||||
|
||||
// Assert
|
||||
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.REROUTE)
|
||||
@@ -120,14 +118,14 @@ class NavigationScreenTest {
|
||||
// Arrange
|
||||
navigationScreen.navigationType = NavigationType.NAVIGATION
|
||||
|
||||
`when`(mockRouteModel.isArrival()).thenReturn(true)
|
||||
`when`(mockRouteModel.isManeuverArrival()).thenReturn(true)
|
||||
`when`(mockRouteModel.routeCalculator).thenReturn(mockRouteCalculator)
|
||||
`when`(mockRouteCalculator.leftStepDistance()).thenReturn(19.0)
|
||||
`when`(mockRouteCalculator.leftStepDistance()).thenReturn(9.0)
|
||||
`when`(mockRouteModel.navState).thenReturn(NavigationState())
|
||||
|
||||
// Act
|
||||
navigationScreen.checkArrival()
|
||||
|
||||
// Assert
|
||||
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.ARRIVAL)
|
||||
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.NAVIGATION)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -98,6 +98,9 @@ class CategoryObserverTest {
|
||||
}
|
||||
|
||||
private fun createElement(lon: Double, lat: Double): Elements {
|
||||
return Elements(lon = lon, lat = lat, tags = Tags())
|
||||
return Elements(
|
||||
lon = lon, lat = lat,
|
||||
tags = Tags(maxspeed = "0", direction = ""),
|
||||
bounds = com.kouros.navigation.data.overpass.Bounds(0.0, 0.0, 0.0, 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -123,7 +123,8 @@ class ObserversTest {
|
||||
return Elements(
|
||||
lon = lon,
|
||||
lat = lat,
|
||||
tags = Tags(maxspeed = maxSpeed, direction = null)
|
||||
tags = Tags(maxspeed = maxSpeed, direction = ""),
|
||||
bounds = com.kouros.navigation.data.overpass.Bounds(0.0, 0.0, 0.0, 0.0),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import com.android.build.gradle.internal.tasks.AarMetadataReader.Companion.load
|
||||
import java.util.Properties
|
||||
import kotlin.apply
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
@@ -8,12 +12,24 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "com.kouros.data"
|
||||
compileSdk = 36
|
||||
compileSdk = 37
|
||||
|
||||
|
||||
val properties = Properties().apply {
|
||||
val localPropertiesFile = project.rootProject.file("local.properties")
|
||||
if (localPropertiesFile.exists()) {
|
||||
load(localPropertiesFile.inputStream())
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 33
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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,13 +3,23 @@ package com.kouros.navigation.data
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val NavigationColorLight = Color(0xFF17A119)
|
||||
val NavigationColorDark = Color(0xFF2B007A)
|
||||
|
||||
val NavigationColorDark = Color(0xFF4EDE10)
|
||||
val NavigationCircle = Color(0xFFFFEB3B)
|
||||
|
||||
val RouteColor = Color(0xFF195D02)
|
||||
val RouteColor = Color(0xFF5201B4)
|
||||
|
||||
val SpeedColor = Color(0xFF262525)
|
||||
|
||||
val MaxSpeedColor = Color(0xFFB71515)
|
||||
|
||||
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,6 +16,7 @@
|
||||
|
||||
package com.kouros.navigation.data
|
||||
|
||||
import android.location.Location
|
||||
import android.net.Uri
|
||||
import com.google.gson.annotations.Expose
|
||||
import com.kouros.navigation.data.route.Lane
|
||||
@@ -28,7 +29,7 @@ data class Category(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
|
||||
data class StepMatch(val stepIndex: Int, val waypointIndex: Int, val location: Location)
|
||||
|
||||
data class Places(
|
||||
val places: List<Place>,
|
||||
@@ -44,6 +45,7 @@ data class Place(
|
||||
var postalCode: String = "",
|
||||
var city: String = "",
|
||||
var street: String = "",
|
||||
var navigations: Int = 0,
|
||||
@Transient
|
||||
var distance: Float = 0F,
|
||||
//var avatar: Uri? = null,
|
||||
@@ -54,6 +56,8 @@ data class Place(
|
||||
var route: String = "",
|
||||
@Transient
|
||||
var stopOver: Boolean = false,
|
||||
@Transient
|
||||
var favorite: Boolean = false
|
||||
)
|
||||
|
||||
data class ContactData(
|
||||
@@ -74,6 +78,8 @@ data class StepData (
|
||||
var lane: List<Lane> = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList(), 0, 0)),
|
||||
var exitNumber: Int = 0,
|
||||
var message: String = "",
|
||||
var roadNumbers: List<String> = emptyList(),
|
||||
|
||||
)
|
||||
|
||||
|
||||
@@ -120,31 +126,51 @@ object Constants {
|
||||
|
||||
const val CHARGING_STATION: String ="charging_station"
|
||||
|
||||
const val RESTAURANT: String ="restaurant"
|
||||
|
||||
/** 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 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 MAXIMAL_SNAP_CORRECTION = 50.0
|
||||
|
||||
const val MAXIMAL_ROUTE_DEVIATION = 80.0
|
||||
const val MAXIMAL_ROUTE_DEVIATION = 100.0
|
||||
|
||||
const val DESTINATION_ARRIVAL_DISTANCE = 20.0
|
||||
const val DESTINATION_ARRIVAL_DISTANCE = 10.0
|
||||
|
||||
const val NEAREST_LOCATION_DISTANCE = 10F
|
||||
|
||||
const val MAXIMUM_LOCATION_DISTANCE = 100000F
|
||||
const val MAXIMUM_LOCATION_DISTANCE = Float.MAX_VALUE
|
||||
|
||||
const val TRAFFIC_UPDATE = 300
|
||||
|
||||
const val TRAFFIC_MESSAGE_UPDATE = 10
|
||||
|
||||
const val SPEED_UPDATE_DISTANCE = 600F
|
||||
|
||||
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 AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED"
|
||||
|
||||
const val TILT = 60.0
|
||||
|
||||
const val TANKER_KOENIG_DELAY = 300000
|
||||
|
||||
const val LAST_CHECK_ROUTE_DISTANCE = 5000
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.net.URL
|
||||
|
||||
abstract class NavigationRepository {
|
||||
|
||||
private val config by lazy { ApplicationConfig.load() }
|
||||
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
|
||||
|
||||
//private val nominatimUrl = "https://kouros-online.de/nominatim/"
|
||||
@@ -61,11 +62,15 @@ abstract class NavigationRepository {
|
||||
try {
|
||||
if (authenticator) {
|
||||
Authenticator.setDefault(object : Authenticator() {
|
||||
override fun getPasswordAuthentication(): PasswordAuthentication {
|
||||
return PasswordAuthentication(
|
||||
"kouros",
|
||||
"eo7sbjyWpmjSVFyELgbfrryqJ6ddNeq9".toCharArray()
|
||||
)
|
||||
override fun getPasswordAuthentication(): PasswordAuthentication? {
|
||||
return if (config.user.isEmpty() || config.password.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
PasswordAuthentication(
|
||||
config.user,
|
||||
config.password.toCharArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -75,7 +80,7 @@ abstract class NavigationRepository {
|
||||
"Accept",
|
||||
"application/json"
|
||||
) // The format of response we want to get from the server
|
||||
httpURLConnection.setRequestProperty("User-Agent", "email=nominatim@kouros-online.de")
|
||||
httpURLConnection.setRequestProperty("User-Agent", "email=online@kouros-online.de")
|
||||
httpURLConnection.requestMethod = "GET"
|
||||
val responseCode = httpURLConnection.responseCode
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.kouros.navigation.data.tomtom.TomTomRoute
|
||||
import com.kouros.navigation.data.valhalla.ValhallaResponse
|
||||
import com.kouros.navigation.data.valhalla.ValhallaRoute
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlinx.coroutines.selects.whileSelect
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
@@ -102,16 +103,22 @@ data class Route(
|
||||
return if (isRouteValid()) {
|
||||
legs().first().steps[currentStepIndex]
|
||||
} else {
|
||||
Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0)))
|
||||
Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0), leftDistance = emptyList()))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneuver locations to snap location
|
||||
*/
|
||||
fun maneuverLocations(): List<Point> {
|
||||
val wayPointIndex = currentStep().waypointIndex
|
||||
val waypoints = currentStep().maneuver.waypoints
|
||||
val points = mutableListOf<Point>()
|
||||
for (loc in waypoints) {
|
||||
val point = Point.fromLngLat(loc[0], loc[1])
|
||||
points.add(point)
|
||||
for ((index,loc) in waypoints.withIndex()) {
|
||||
if (index >= wayPointIndex && points.size < 20) {
|
||||
val point = Point.fromLngLat(loc.longitude, loc.latitude)
|
||||
points.add(point)
|
||||
}
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.kouros.navigation.data.EngineType
|
||||
@@ -59,6 +60,10 @@ class DataStoreManager(private val context: Context) {
|
||||
|
||||
val ALTERNATIVE_ROUTES = booleanPreferencesKey("AlternativeRoutes")
|
||||
|
||||
val LAST_FUEL_PRICES = longPreferencesKey("LastFuelPrices")
|
||||
|
||||
val FUEL_PRICES = stringPreferencesKey("FuelPrices")
|
||||
|
||||
}
|
||||
|
||||
// Read values
|
||||
@@ -151,6 +156,18 @@ class DataStoreManager(private val context: Context) {
|
||||
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
|
||||
suspend fun setShow3D(enabled: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
@@ -248,4 +265,16 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,63 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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,6 +12,7 @@ import com.kouros.navigation.utils.GeoUtils.createCenterLocation
|
||||
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
|
||||
import com.kouros.navigation.utils.GeoUtils.decodePolyline
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
class OsrmRoute {
|
||||
|
||||
@@ -27,18 +28,33 @@ class OsrmRoute {
|
||||
val steps = mutableListOf<Step>()
|
||||
leg.steps.forEach { step ->
|
||||
val intersections = mutableListOf<Intersection>()
|
||||
val leftDistance = mutableListOf<Float>()
|
||||
var lastLocation = location(0.0,0.0)
|
||||
val points = decodePolyline(step.geometry, 5)
|
||||
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(
|
||||
bearingBefore = step.maneuver.bearingBefore,
|
||||
bearingAfter = step.maneuver.bearingAfter,
|
||||
type = convertType(step.maneuver),
|
||||
waypoints = points,
|
||||
waypoints = points.map { location(it[0], it[1]) },
|
||||
exit = step.maneuver.exit,
|
||||
location = location(
|
||||
step.maneuver.location[0],
|
||||
step.maneuver.location[1]
|
||||
)
|
||||
),
|
||||
leftDistance = leftDistance
|
||||
)
|
||||
step.intersections.forEach { it2 ->
|
||||
if (it2.location[0] != 0.0) {
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
package com.kouros.navigation.data.overpass
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
data class Amenity (
|
||||
|
||||
@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()
|
||||
|
||||
data class Amenity(
|
||||
val elements: List<Elements>,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kouros.navigation.data.overpass
|
||||
|
||||
data class Bounds(
|
||||
val maxlat: Double,
|
||||
val maxlon: Double,
|
||||
val minlat: Double,
|
||||
val minlon: Double
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.kouros.navigation.data.overpass
|
||||
|
||||
data class ElementSearch(
|
||||
val element: Elements,
|
||||
val distance: Double,
|
||||
val bearing: Float,
|
||||
)
|
||||
@@ -1,15 +1,17 @@
|
||||
package com.kouros.navigation.data.overpass
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
data class Elements (
|
||||
|
||||
@SerializedName("type" ) var type : String = "",
|
||||
@SerializedName("id" ) var id : Long = 0,
|
||||
@SerializedName("lat" ) var lat : Double = 0.0,
|
||||
@SerializedName("lon" ) var lon : Double = 0.0,
|
||||
@SerializedName("tags" ) var tags : Tags = Tags(),
|
||||
var distance : Double = 0.0
|
||||
data class Elements(
|
||||
val bounds: Bounds,
|
||||
val geometry: List<Geometry> = emptyList(),
|
||||
val id: Long = 0,
|
||||
val lat: Double= 0.0,
|
||||
val lon: Double = 0.0,
|
||||
val tags: Tags,
|
||||
val type: String = "",
|
||||
var distance : Double = 0.0,
|
||||
var e5 : Double = 0.0,
|
||||
var e10: Double = 0.0,
|
||||
var diesel: Double = 0.0
|
||||
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.kouros.navigation.data.overpass
|
||||
|
||||
data class Geometry(
|
||||
val lat: Double = 0.0,
|
||||
val lon: Double = 0.0
|
||||
)
|
||||
@@ -1,40 +1,96 @@
|
||||
package com.kouros.navigation.data.overpass
|
||||
|
||||
import android.location.Location
|
||||
import android.util.Log
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.kouros.data.BuildConfig
|
||||
import com.kouros.navigation.utils.GeoUtils.getBoundingBox
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.Authenticator
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.PasswordAuthentication
|
||||
import java.net.URL
|
||||
|
||||
class Overpass {
|
||||
|
||||
//val overpassUrl = "https://overpass.kumi.systems/api/interpreter"
|
||||
//val overpassUrl = "https://overpass-api.de/api"
|
||||
val overpassUrl = "https://kouros-online.de/overpass/interpreter"
|
||||
private val config by lazy { com.kouros.navigation.data.ApplicationConfig.load() }
|
||||
private val gson = GsonBuilder().serializeNulls().create()
|
||||
|
||||
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 = """
|
||||
|[out:json];
|
||||
|(
|
||||
| way[highway](around:$radius,$linestring)
|
||||
| ;
|
||||
|);
|
||||
|out body;
|
||||
|[out:json][timeout:10];
|
||||
|(
|
||||
| ${searchClauses.joinToString(";")};
|
||||
|);
|
||||
|out body geom;
|
||||
""".trimMargin()
|
||||
//println("way[highway](around:$radius,$linestring)")
|
||||
return overpassApi(httpURLConnection, searchQuery)
|
||||
|
||||
//Log.d("OverpassApi", "Overpass Query: $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(
|
||||
type: String,
|
||||
category: String,
|
||||
@@ -42,46 +98,65 @@ class Overpass {
|
||||
radius: Double
|
||||
): List<Elements> {
|
||||
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 = """
|
||||
|[out:json];
|
||||
|(
|
||||
| node[$type=$category]
|
||||
| ($boundingBox);
|
||||
|);
|
||||
|(._;>;);
|
||||
|out body;
|
||||
|[out:json];
|
||||
|(
|
||||
| node[$type=$category]
|
||||
| ($boundingBox);
|
||||
|);
|
||||
|(._;>;);
|
||||
|out body geom;
|
||||
""".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)
|
||||
outputStreamWriter.write(searchQuery)
|
||||
outputStreamWriter.flush()
|
||||
// Check if the connection is successful
|
||||
val responseCode = httpURLConnection.responseCode
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
val response = httpURLConnection.inputStream.bufferedReader()
|
||||
.use { it.readText() } // defaults to UTF-8
|
||||
val gson = GsonBuilder().serializeNulls().create()
|
||||
val overpass = gson.fromJson(response, Amenity::class.java)
|
||||
return overpass.elements
|
||||
private fun overpassApi(connection: HttpURLConnection, searchQuery: String): List<Elements> {
|
||||
|
||||
return try {
|
||||
Authenticator.setDefault(object : Authenticator() {
|
||||
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) {
|
||||
val response = connection.inputStream.bufferedReader().use { it.readText() }
|
||||
if (response.startsWith("<?xml")) {
|
||||
Log.w("OverpassApi", "Received XML instead of JSON")
|
||||
return emptyList()
|
||||
}
|
||||
gson.fromJson(response, Amenity::class.java).elements
|
||||
} else {
|
||||
Log.e("OverpassApi", "Error code: $responseCode")
|
||||
emptyList()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Speed $e")
|
||||
Log.e("OverpassApi", "Exception in Overpass API call", e)
|
||||
emptyList()
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,29 @@ import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
data class Tags(
|
||||
@SerializedName("name") var name: String? = null,
|
||||
@SerializedName("amenity") var amenity: String? = null,
|
||||
@SerializedName("authentication:none") var authenticationNone: String? = null,
|
||||
@SerializedName("capacity") var capacity: String? = null,
|
||||
@SerializedName("motorcar") var motorcar: String? = null,
|
||||
@SerializedName("network") var network: String? = null,
|
||||
@SerializedName("opening_hours") var openingHours: String? = null,
|
||||
@SerializedName("operator") var operator: String? = null,
|
||||
@SerializedName("operator:short") var operatorShort: String? = null,
|
||||
@SerializedName("operator:wikidata") var operatorWikidata: String? = null,
|
||||
@SerializedName("operator:wikipedia") var operatorWikipedia: String? = null,
|
||||
@SerializedName("ref") var ref: String? = null,
|
||||
@SerializedName("socket:type2") var socketType2: String? = null,
|
||||
@SerializedName("socket:type2:output") var socketType2Output: String? = null,
|
||||
@SerializedName("maxspeed") var maxspeed: String = "0",
|
||||
@SerializedName("direction") var direction: String? = null,
|
||||
|
||||
val destination: String = "",
|
||||
val highway: String = "",
|
||||
val lanes: String = "",
|
||||
val lit: String = "",
|
||||
val maxspeed: String = "0",
|
||||
@SerializedName("maxspeed:variable") val maxSpeedVariable: String = "",
|
||||
val name: String = "",
|
||||
val oneway: String = "",
|
||||
val ref: String = "",
|
||||
@SerializedName("int_ref") val intRef: String = "",
|
||||
val sidewalk: String = "",
|
||||
val smoothness: String = "",
|
||||
val surface: String = "",
|
||||
val amenity: String = "",
|
||||
val capacity: String = "",
|
||||
val motorcar: String = "",
|
||||
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,12 +6,13 @@ data class Maneuver(
|
||||
val bearingBefore: Int = 0,
|
||||
val bearingAfter: Int = 0,
|
||||
val type: Int = 0,
|
||||
val waypoints: List<List<Double>>,
|
||||
val waypoints: List<Location>,
|
||||
val location: Location,
|
||||
val exit: Int = 0,
|
||||
val street: String = "",
|
||||
val message: String = "",
|
||||
val pointIndex: Int = 0,
|
||||
val leftDistance: List<Float>
|
||||
)
|
||||
|
||||
enum class ManeuverType(val value: Int) {
|
||||
|
||||
@@ -12,5 +12,6 @@ data class Step(
|
||||
val distance: Double = 0.0,
|
||||
val street : String = "",
|
||||
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 pointIndex: Int,
|
||||
val possibleCombineWithNext: Boolean,
|
||||
val roadNumbers: List<String>,
|
||||
val roadNumbers: List<String> = emptyList(),
|
||||
val routeOffsetInMeters: Int,
|
||||
val signpostText: String,
|
||||
val street: String? = "",
|
||||
|
||||
@@ -25,8 +25,8 @@ val useLocal = BuildConfig.DEBUG
|
||||
|
||||
val useLocalTraffic = BuildConfig.DEBUG
|
||||
|
||||
|
||||
class TomTomRepository : NavigationRepository() {
|
||||
|
||||
override fun getRoute(
|
||||
context: Context,
|
||||
currentLocation: Location,
|
||||
@@ -34,9 +34,15 @@ class TomTomRepository : NavigationRepository() {
|
||||
carOrientation: Float,
|
||||
searchFilter: SearchFilter
|
||||
): String {
|
||||
val vehicleHeading = if (carOrientation !in 0.0..360.0) {
|
||||
0
|
||||
} else {
|
||||
carOrientation.toInt()
|
||||
}
|
||||
if (useLocal) {
|
||||
return fetchUrl(
|
||||
"https://kouros-online.de/tomtom_routing.json",
|
||||
"http://192.168.1.37/tomtom_routing.json",
|
||||
//"http://192.168.1.37/verona.json",
|
||||
false
|
||||
)
|
||||
}
|
||||
@@ -83,7 +89,7 @@ class TomTomRepository : NavigationRepository() {
|
||||
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
|
||||
"&instructionsType=text&language=$language§ionType=lanes" +
|
||||
"&routeRepresentation=encodedPolyline$altRoutes" +
|
||||
"&vehicleEngineType=$engineType$filter&key=$tomtomApiKey"
|
||||
"&vehicleHeading=$vehicleHeading&vehicleEngineType=$engineType$filter&key=$tomtomApiKey"
|
||||
return fetchUrl(
|
||||
url,
|
||||
false
|
||||
@@ -97,10 +103,10 @@ class TomTomRepository : NavigationRepository() {
|
||||
if (!showTraffic) {
|
||||
return ""
|
||||
}
|
||||
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
|
||||
val bbox = calculateSquareRadius(location.latitude, location.longitude, 10.0)
|
||||
return if (useLocalTraffic) {
|
||||
fetchUrl(
|
||||
"https://kouros-online.de/tomtom_traffic.json",
|
||||
"http://192.168.1.37/tomtom_traffic.json",
|
||||
false
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -13,7 +13,9 @@ import com.kouros.navigation.data.route.Summary
|
||||
import com.kouros.navigation.utils.GeoUtils.createCenterLocation
|
||||
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
|
||||
import com.kouros.navigation.utils.GeoUtils.decodePolyline
|
||||
import com.kouros.navigation.utils.isNumeric
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlin.math.absoluteValue
|
||||
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
|
||||
|
||||
|
||||
@@ -53,23 +55,41 @@ class TomTomRoute {
|
||||
val instruction = route.guidance.instructions[index]
|
||||
val street = lastInstruction.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(
|
||||
bearingBefore = 0,
|
||||
bearingAfter = 0,
|
||||
type = convertType(instruction.maneuver),
|
||||
waypoints = points.subList(
|
||||
lastPointIndex,
|
||||
instruction.pointIndex + 1,
|
||||
),
|
||||
waypoints = subPoints.map { location(it[0], it[1]) },
|
||||
exit = exitNumber(instruction),
|
||||
location = location(
|
||||
instruction.point.longitude, instruction.point.latitude
|
||||
),
|
||||
street = maneuverStreet,
|
||||
message = instruction.message,
|
||||
pointIndex = instruction.pointIndex
|
||||
pointIndex = instruction.pointIndex,
|
||||
leftDistance = leftDistance
|
||||
)
|
||||
|
||||
lastPointIndex = instruction.pointIndex
|
||||
val intersections = mutableListOf<Intersection>()
|
||||
route.sections?.forEach { section ->
|
||||
@@ -95,10 +115,12 @@ class TomTomRoute {
|
||||
intersections.add(Intersection(waypoints[startIndex], lanes))
|
||||
}
|
||||
}
|
||||
stepDistance =
|
||||
route.guidance.instructions[index].routeOffsetInMeters - stepDistance
|
||||
stepDuration =
|
||||
route.guidance.instructions[index].travelTimeInSeconds - stepDuration
|
||||
|
||||
val roadNumbers = if (lastInstruction.roadNumbers != null) {
|
||||
lastInstruction.roadNumbers
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val step = Step(
|
||||
index = stepIndex,
|
||||
street = street,
|
||||
@@ -106,7 +128,8 @@ class TomTomRoute {
|
||||
duration = stepDuration,
|
||||
maneuver = maneuver,
|
||||
intersection = intersections,
|
||||
countryCode = lastInstruction.countryCode
|
||||
countryCode = lastInstruction.countryCode,
|
||||
roadNumbers = roadNumbers
|
||||
)
|
||||
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
|
||||
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
|
||||
@@ -206,6 +229,7 @@ class TomTomRoute {
|
||||
"TAKE_EXIT" -> {
|
||||
newType = ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value
|
||||
}
|
||||
|
||||
"WAYPOINT_RIGHT" -> {
|
||||
newType = ManeuverType.TYPE_WAYPOINT_RIGHT.value
|
||||
}
|
||||
@@ -221,6 +245,10 @@ private fun exitNumber(
|
||||
) {
|
||||
0
|
||||
} else {
|
||||
instruction.exitNumber.toInt()
|
||||
if (isNumeric(instruction.exitNumber)) {
|
||||
instruction.exitNumber.toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,10 @@ class ValhallaRoute {
|
||||
bearingAfter = it.bearingAfter,
|
||||
//type = it.type,
|
||||
type = convertType(it),
|
||||
waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1),
|
||||
waypoints = waypoints.subList(it.beginShapeIndex, it.endShapeIndex + 1).map { location(it[0], it[1]) },
|
||||
// TODO: calculate from ShapeIndex !
|
||||
location = location(0.0, 0.0)
|
||||
location = location(0.0, 0.0),
|
||||
leftDistance = emptyList()
|
||||
|
||||
)
|
||||
var name = ""
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.kouros.navigation.model
|
||||
|
||||
//import com.kouros.navigation.data.Preferences.boxStore
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.util.Log
|
||||
@@ -9,28 +8,52 @@ import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
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.TANKER_KOENIG_DELAY
|
||||
import com.kouros.navigation.data.NavigationRepository
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.data.Places
|
||||
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.SearchResult
|
||||
import com.kouros.navigation.data.overpass.ElementSearch
|
||||
import com.kouros.navigation.data.overpass.Elements
|
||||
import com.kouros.navigation.data.overpass.Overpass
|
||||
import com.kouros.navigation.utils.Levenshtein
|
||||
import com.kouros.navigation.repository.SettingsRepository
|
||||
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.isNumeric
|
||||
import com.kouros.navigation.utils.location
|
||||
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.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.maplibre.geojson.FeatureCollection
|
||||
import java.lang.reflect.Modifier
|
||||
import org.maplibre.geojson.Point
|
||||
import java.time.LocalDateTime
|
||||
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.
|
||||
@@ -38,6 +61,8 @@ import java.time.ZoneOffset
|
||||
*/
|
||||
class NavigationViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
|
||||
private val overpass = Overpass()
|
||||
|
||||
/** LiveData containing the calculated route JSON string */
|
||||
val route: MutableLiveData<String> by lazy {
|
||||
MutableLiveData()
|
||||
@@ -48,6 +73,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
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 */
|
||||
val previewRoute: MutableLiveData<String> by lazy {
|
||||
MutableLiveData()
|
||||
@@ -83,6 +113,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
MutableLiveData()
|
||||
}
|
||||
|
||||
/** LiveData containing POI elements from Overpass API */
|
||||
val speedElements = mutableListOf<Elements>()
|
||||
|
||||
|
||||
/** LiveData containing speed camera locations */
|
||||
val speedCameras: MutableLiveData<List<Elements>> by lazy {
|
||||
MutableLiveData()
|
||||
@@ -103,7 +137,22 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
MutableLiveData()
|
||||
}
|
||||
|
||||
val gson: com.google.gson.Gson = GsonBuilder().create()
|
||||
val initialSnapLocation: MutableLiveData<Location> by lazy {
|
||||
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.
|
||||
@@ -120,8 +169,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
if (rp.isNotEmpty()) {
|
||||
for (place in places.places) {
|
||||
if (place.category == Constants.RECENT
|
||||
|| place.category == Constants.FAVORITES
|
||||
|| place.category == FAVORITES
|
||||
) {
|
||||
if (place.category == FAVORITES) {
|
||||
place.favorite = true
|
||||
}
|
||||
val plLocation = location(place.longitude, place.latitude)
|
||||
if (place.latitude != 0.0) {
|
||||
val distance =
|
||||
@@ -137,7 +189,9 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
}
|
||||
}
|
||||
}
|
||||
recentPlaces.postValue(pl.sortedBy { it.distance })
|
||||
val sortedList = pl.sortedWith(compareByDescending<Place> { it.navigations }
|
||||
.thenByDescending { it.distance })
|
||||
recentPlaces.postValue(sortedList)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
@@ -175,7 +229,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
* Fetches traffic incident data and categorizes by severity.
|
||||
* Posts categorized traffic map to traffic LiveData.
|
||||
*/
|
||||
fun loadTraffic(context: Context, currentLocation: Location, carOrientation: Float) {
|
||||
fun loadTraffic(
|
||||
context: Context,
|
||||
currentLocation: Location,
|
||||
carOrientation: Float,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val data = repository.getTraffic(
|
||||
@@ -197,33 +255,6 @@ 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.
|
||||
* Posts the route JSON to previewRoute LiveData.
|
||||
@@ -252,6 +283,39 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
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.
|
||||
@@ -346,15 +410,25 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
* Queries Overpass API for nearby amenities of a specific category.
|
||||
* Posts sorted results to elements LiveData.
|
||||
*/
|
||||
fun getAmenities(category: String, location: Location) {
|
||||
fun getAmenities(
|
||||
carContext: Context,
|
||||
category: String,
|
||||
location: Location,
|
||||
lastFuelUpdate: Long = 0
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val amenities = Overpass().getAmenities("amenity", category, location, 5.0)
|
||||
val repository = getSettingsRepository(carContext)
|
||||
val amenities = overpass.getAmenities("amenity", category, location, 5.0)
|
||||
val fuelPrices = fuelStations(category, lastFuelUpdate, location, repository)
|
||||
val distAmenities = mutableListOf<Elements>()
|
||||
amenities.forEach {
|
||||
val plLocation =
|
||||
location(longitude = it.lon, latitude = it.lat)
|
||||
val distance = plLocation.distanceTo(location)
|
||||
it.distance = distance.toDouble()
|
||||
if (category == Constants.FUEL_STATION) {
|
||||
addFuelPrice(it, fuelPrices, location)
|
||||
}
|
||||
distAmenities.add(it)
|
||||
}
|
||||
val sortedList = distAmenities.sortedWith(compareBy { it.distance })
|
||||
@@ -362,23 +436,62 @@ 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.
|
||||
* Posts sorted results to speedCameras LiveData.
|
||||
*/
|
||||
fun getSpeedCameras(location: Location, radius: Double) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius)
|
||||
val distAmenities = mutableListOf<Elements>()
|
||||
amenities.forEach {
|
||||
val plLocation =
|
||||
location(longitude = it.lon, latitude = it.lat)
|
||||
val distance = plLocation.distanceTo(location)
|
||||
it.distance = distance.toDouble()
|
||||
distAmenities.add(it)
|
||||
synchronized(this) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val amenities = overpass.getAmenities("highway", "speed_camera", location, radius)
|
||||
val distAmenities = mutableListOf<Elements>()
|
||||
amenities.forEach {
|
||||
val plLocation =
|
||||
location(longitude = it.lon, latitude = it.lat)
|
||||
val distance = plLocation.distanceTo(location)
|
||||
it.distance = distance.toDouble()
|
||||
distAmenities.add(it)
|
||||
}
|
||||
val sortedList = distAmenities.sortedWith(compareBy { it.distance })
|
||||
speedCameras.postValue(sortedList)
|
||||
}
|
||||
val sortedList = distAmenities.sortedWith(compareBy { it.distance })
|
||||
speedCameras.postValue(sortedList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,22 +499,107 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
* Queries Overpass API for speed limit on current road using fuzzy matching.
|
||||
* Posts speed limit to maxSpeed LiveData.
|
||||
*/
|
||||
fun getMaxSpeed(location: Location, street: String) {
|
||||
fun getSpeedLimit(
|
||||
location: Location,
|
||||
routeBearing: Float,
|
||||
countryCode: String,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val levenshtein = Levenshtein()
|
||||
val lineString = "${location.latitude},${location.longitude}"
|
||||
val amenities = Overpass().getAround(10, lineString)
|
||||
amenities.forEach {
|
||||
if (it.tags.name != null) {
|
||||
val distance =
|
||||
levenshtein.distance(it.tags.name!!, street)
|
||||
if (distance < 5) {
|
||||
val speed = it.tags.maxspeed.toInt()
|
||||
maxSpeed.postValue(speed)
|
||||
synchronized(this) {
|
||||
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 elements =
|
||||
overpass.getSpeedLimit(SPEED_UPDATE_DISTANCE, lineString, street, roadNumbers)
|
||||
speedElements.clear()
|
||||
speedElements.addAll(elements)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -453,7 +651,9 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
val current = LocalDateTime.now(ZoneOffset.UTC)
|
||||
place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond()
|
||||
place.route = ""
|
||||
place.navigations += 1
|
||||
places.add(place)
|
||||
recentPlaces.postValue(places)
|
||||
settingsRepository.setRecentPlaces(gson.toJson(Places(places)))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -488,7 +688,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
}
|
||||
}
|
||||
settingsRepository.setRecentPlaces(gson.toJson(Places(places)))
|
||||
recentPlaces.value = places
|
||||
recentPlaces.postValue(places)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -527,4 +727,31 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
}
|
||||
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,38 +5,61 @@ import android.util.Log
|
||||
import androidx.car.app.navigation.model.Step
|
||||
import com.kouros.navigation.data.Constants.MAXIMUM_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 java.util.concurrent.TimeUnit
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
class RouteCalculator(var routeModel: RouteModel) {
|
||||
|
||||
var bestMatch: StepMatch? = null
|
||||
var lastSpeedLocation: Location = location(0.0, 0.0)
|
||||
var lastLocalSpeedLocation: Location = location(0.0, 0.0)
|
||||
|
||||
var lastSpeedIndex: Int = 0
|
||||
|
||||
fun findStep(location: Location) {
|
||||
var nearestDistance = MAXIMUM_LOCATION_DISTANCE
|
||||
var count = 0
|
||||
var lastDistance = 0F
|
||||
var increaseDistance = 0
|
||||
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
|
||||
count++
|
||||
var distance = 0F
|
||||
if (index >= routeModel.navState.route.currentStepIndex) {
|
||||
for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
||||
count++
|
||||
if (wayIndex >= step.waypointIndex) {
|
||||
val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
|
||||
if (distance < nearestDistance) {
|
||||
distance = location.distanceTo(waypoint)
|
||||
if (distance < nearestDistance ) {
|
||||
nearestDistance = distance
|
||||
routeModel.navState.route.currentStepIndex = step.index
|
||||
step.waypointIndex = wayIndex
|
||||
step.wayPointLocation = location(waypoint[0], waypoint[1])
|
||||
step.wayPointLocation = waypoint
|
||||
}
|
||||
}
|
||||
if (stopSearch(nearestDistance, distance)) {
|
||||
break
|
||||
}
|
||||
if (distance > lastDistance) {
|
||||
increaseDistance++
|
||||
}
|
||||
lastDistance = distance
|
||||
}
|
||||
}
|
||||
if (nearestDistance < NEAREST_LOCATION_DISTANCE) {
|
||||
if (stopSearch(nearestDistance, distance)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopSearch(nearestDistance: Float, distance: Float): Boolean {
|
||||
return nearestDistance < NEAREST_LOCATION_DISTANCE && distance > NEAREST_LOCATION_DISTANCE * 10
|
||||
}
|
||||
|
||||
fun travelLeftTime(): Double {
|
||||
var timeLeft = 0.0
|
||||
// time for next step until end step
|
||||
@@ -70,19 +93,15 @@ class RouteCalculator(var routeModel: RouteModel) {
|
||||
fun leftStepDistance(): Double {
|
||||
val step = routeModel.route.currentStep()
|
||||
var leftDistance = 0F
|
||||
for (i in step.waypointIndex..<step.maneuver.waypoints.size - 1) {
|
||||
val loc1 = location(step.maneuver.waypoints[i][0], step.maneuver.waypoints[i][1])
|
||||
val loc2 =
|
||||
location(step.maneuver.waypoints[i + 1][0], step.maneuver.waypoints[i + 1][1])
|
||||
val locationDistance = loc1.distanceTo(routeModel.navState.lastLocation)
|
||||
val distance = loc1.distanceTo(loc2)
|
||||
leftDistance += if (locationDistance < distance) {
|
||||
locationDistance
|
||||
} else {
|
||||
distance
|
||||
if (step.waypointIndex < step.maneuver.waypoints.size - 1) {
|
||||
leftDistance = step.maneuver.leftDistance[step.waypointIndex]
|
||||
val waypointLocation = step.maneuver.waypoints[step.waypointIndex]
|
||||
if (routeModel.navState.lastLocation.latitude != 0.0) {
|
||||
val locationDistance = waypointLocation.distanceTo(routeModel.navState.lastLocation)
|
||||
leftDistance -= locationDistance
|
||||
}
|
||||
}
|
||||
return leftDistance.toDouble()
|
||||
return leftDistance.absoluteValue.toDouble()
|
||||
}
|
||||
|
||||
/** Returns the left distance in m. */
|
||||
@@ -107,14 +126,31 @@ class RouteCalculator(var routeModel: RouteModel) {
|
||||
return nowUtcMillis + timeToDestinationMillis
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the speed limit in the view model.
|
||||
*/
|
||||
fun updateSpeedLimit(location: Location, viewModel: NavigationViewModel) {
|
||||
if (routeModel.isNavigating()) {
|
||||
// speed limit
|
||||
val distance = lastSpeedLocation.distanceTo(location)
|
||||
if (distance > 500 || lastSpeedIndex < routeModel.route.currentStepIndex) {
|
||||
if ((distance > SPEED_UPDATE_DISTANCE * 2) || lastSpeedIndex < routeModel.route.currentStepIndex) {
|
||||
lastSpeedIndex = routeModel.route.currentStepIndex
|
||||
lastSpeedLocation = location
|
||||
viewModel.getMaxSpeed(location, routeModel.route.currentStep().street)
|
||||
// Force the local re-match on the next GPS fix once new elements arrive.
|
||||
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,10 +1,12 @@
|
||||
package com.kouros.navigation.model
|
||||
|
||||
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_PROJECTION
|
||||
import androidx.car.app.navigation.model.Maneuver
|
||||
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.Route
|
||||
import com.kouros.navigation.data.StepData
|
||||
@@ -105,7 +107,8 @@ open class RouteModel {
|
||||
leftDistance = routeCalculator.travelLeftDistance(),
|
||||
lane = currentLanes,
|
||||
exitNumber = exitNumber,
|
||||
message = currentStep.maneuver.message
|
||||
message = currentStep.maneuver.message,
|
||||
roadNumbers = currentStep.roadNumbers
|
||||
)
|
||||
}
|
||||
|
||||
@@ -167,7 +170,7 @@ open class RouteModel {
|
||||
/**
|
||||
* Checks for arrival
|
||||
*/
|
||||
fun isArrival(): Boolean {
|
||||
fun isManeuverArrival(): Boolean {
|
||||
return navState.maneuverType == Maneuver.TYPE_DESTINATION
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|
||||
|
||||
@@ -105,6 +105,18 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
|
||||
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) {
|
||||
viewModelScope.launch { repository.setShow3D(enabled) }
|
||||
}
|
||||
@@ -165,4 +177,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
|
||||
fun onAlternativeRoutes(enabled: Boolean) {
|
||||
viewModelScope.launch { repository.setAlternativeRoutes(enabled) }
|
||||
}
|
||||
|
||||
fun onLastFuelPricesChanged(lastFuelPrices: Long) {
|
||||
viewModelScope.launch { repository.setLastFuelPrices(lastFuelPrices) }
|
||||
}
|
||||
|
||||
fun onFuelPricesChanged(fuelPrices: String) {
|
||||
viewModelScope.launch { repository.setFuelPrices(fuelPrices) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.kouros.navigation.repository
|
||||
|
||||
import android.util.Log
|
||||
import com.kouros.navigation.data.datastore.DataStoreManager
|
||||
import com.kouros.navigation.data.fuel.FuelPrices
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class SettingsRepository(
|
||||
@@ -54,6 +55,13 @@ class SettingsRepository(
|
||||
val alternativeRoutesFlow: Flow<Boolean> =
|
||||
dataStoreManager.alternativeRoutesFlow
|
||||
|
||||
val lastFuelPricesFlow: Flow<Long> =
|
||||
dataStoreManager.lastFuelPricesFlow
|
||||
|
||||
val fuelPricesFlow: Flow<String> =
|
||||
dataStoreManager.fuelPricesFlow
|
||||
|
||||
|
||||
suspend fun setShow3D(enabled: Boolean) {
|
||||
dataStoreManager.setShow3D(enabled)
|
||||
}
|
||||
@@ -117,4 +125,12 @@ class SettingsRepository(
|
||||
suspend fun setAlternativeRoutes(enabled: Boolean) {
|
||||
dataStoreManager.setAlternativeRoutes(enabled)
|
||||
}
|
||||
|
||||
suspend fun setLastFuelPrices(lastFuelPrices: Long) {
|
||||
dataStoreManager.setLastFuelPrices(lastFuelPrices)
|
||||
}
|
||||
|
||||
suspend fun setFuelPrices(fuelPrices: String) {
|
||||
dataStoreManager.setFuelPrices(fuelPrices)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,22 +4,29 @@ import android.location.Location
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import org.maplibre.geojson.FeatureCollection
|
||||
import org.maplibre.geojson.LineString
|
||||
import org.maplibre.geojson.Point
|
||||
import org.maplibre.spatialk.geojson.Feature
|
||||
import org.maplibre.spatialk.geojson.dsl.addFeature
|
||||
import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection
|
||||
import org.maplibre.spatialk.geojson.dsl.buildLineString
|
||||
import org.maplibre.spatialk.geojson.dsl.buildMultiPoint
|
||||
import org.maplibre.spatialk.geojson.toJson
|
||||
import org.maplibre.turf.TurfMeasurement
|
||||
import org.maplibre.turf.TurfMisc
|
||||
import java.lang.Math.toDegrees
|
||||
import java.lang.Math.toRadians
|
||||
import kotlin.math.asin
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
|
||||
object GeoUtils {
|
||||
|
||||
fun snapLocation(location: Location, stepCoordinates: List<Point>) : Location {
|
||||
const val EARTH_RADIUS = 6371.0 // in km
|
||||
|
||||
fun snapLocation(location: Location, stepCoordinates: List<Point>): Location {
|
||||
val newLocation = Location(location)
|
||||
val oldPoint = Point.fromLngLat(location.longitude, location.latitude)
|
||||
if (stepCoordinates.size > 1) {
|
||||
@@ -34,7 +41,7 @@ object GeoUtils {
|
||||
return newLocation
|
||||
}
|
||||
|
||||
fun decodePolyline(encoded: String, precision: Int = 6): List<List<Double>> {
|
||||
fun decodePolyline(encoded: String, precision: Int = 6): List<List<Double>> {
|
||||
val factor = 10.0.pow(precision)
|
||||
var lat = 0
|
||||
var lng = 0
|
||||
@@ -91,18 +98,20 @@ object GeoUtils {
|
||||
}
|
||||
|
||||
fun createLineStringCollection(lineCoordinates: List<List<Double>>): String {
|
||||
// return createPointCollection(lineCoordinates, "Route")
|
||||
//return createPointCollection(lineCoordinates, "Route")
|
||||
val lineString = buildLineString {
|
||||
lineCoordinates.forEach {
|
||||
add(org.maplibre.spatialk.geojson.Point(
|
||||
it[0],
|
||||
it[1]
|
||||
))
|
||||
add(
|
||||
org.maplibre.spatialk.geojson.Point(
|
||||
it[0],
|
||||
it[1]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
val feature = Feature(lineString, null)
|
||||
val featureCollection = org.maplibre.spatialk.geojson.FeatureCollection(feature)
|
||||
return featureCollection.toJson()
|
||||
return featureCollection.toJson()
|
||||
}
|
||||
|
||||
fun createPointCollection(lineCoordinates: List<List<Double>>, category: String): String {
|
||||
@@ -114,34 +123,153 @@ 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.
|
||||
* @return latMin, latMax, lngMin, lngMax
|
||||
*/
|
||||
fun calculateSquareRadius(lat: Double, lng: Double, radius: Double): String {
|
||||
val earthRadius = 6371.0 // earth radius in km
|
||||
val latMin = lat - toDegrees(radius / earthRadius)
|
||||
val latMax = lat + toDegrees(radius / earthRadius)
|
||||
val lngMin = lng - toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
||||
val lngMax = lng + toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
||||
val latMin = lat - toDegrees(radius / EARTH_RADIUS)
|
||||
val latMax = lat + toDegrees(radius / EARTH_RADIUS)
|
||||
val lngMin = lng - toDegrees(radius / EARTH_RADIUS / cos(toRadians(lat)))
|
||||
val lngMax = lng + toDegrees(radius / EARTH_RADIUS / cos(toRadians(lat)))
|
||||
|
||||
return "$lngMin,$latMin,$lngMax,$latMax"
|
||||
}
|
||||
|
||||
fun getBoundingBox(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
radius: Double
|
||||
): String {
|
||||
val earthRadius = 6371.0
|
||||
val maxLat = lat + toDegrees(radius / earthRadius)
|
||||
val minLat = lat - toDegrees(radius / earthRadius)
|
||||
val maxLon = lon + toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
||||
val minLon = lon - toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
||||
val maxLat = lat + toDegrees(radius / EARTH_RADIUS)
|
||||
val minLat = lat - toDegrees(radius / EARTH_RADIUS)
|
||||
val maxLon = lon + toDegrees(radius / EARTH_RADIUS / cos(toRadians(lat)))
|
||||
val minLon = lon - toDegrees(radius / EARTH_RADIUS / cos(toRadians(lat)))
|
||||
|
||||
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,10 +25,11 @@ class Levenshtein {
|
||||
* @param limit the maximum result to compute before stopping, terminating calculation early.
|
||||
* @return the computed Levenshtein distance.
|
||||
*/
|
||||
fun distance(first: CharSequence, second: CharSequence, limit: Int = Int.MAX_VALUE): Int {
|
||||
fun distance(first: CharSequence, second: CharSequence, countryCode: String, limit: Int = Int.MAX_VALUE): Int {
|
||||
if (countryCode == "GRC") return 0
|
||||
if (first == second) return 0
|
||||
if (first.isEmpty()) return second.length
|
||||
if (second.isEmpty()) return first.length
|
||||
if (first.isEmpty()) return 0
|
||||
if (second.isEmpty()) return 0
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -3,11 +3,10 @@ package com.kouros.navigation.utils
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.util.Log
|
||||
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.RouteEngine
|
||||
import com.kouros.navigation.data.ViewStyle
|
||||
import com.kouros.navigation.data.osrm.OsrmRepository
|
||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||
@@ -28,6 +27,7 @@ import kotlin.math.cos
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.ranges.contains
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -49,18 +49,20 @@ object NavigationUtils {
|
||||
}
|
||||
|
||||
fun calculateZoom(speed: Double?): Double {
|
||||
val zoom = 17.0
|
||||
if (speed == null) {
|
||||
return 17.0
|
||||
return zoom
|
||||
}
|
||||
val speedKmh = (speed * 3.6).toInt()
|
||||
val zoom = when (speedKmh) {
|
||||
in 0..10 -> 17.0
|
||||
in 11..30 -> 17.5
|
||||
in 31..65 -> 17.0
|
||||
in 66..70 -> 16.5
|
||||
else -> 16.0
|
||||
return when (speedKmh) {
|
||||
in 0..10 -> zoom + 1.0
|
||||
in 11..30 -> zoom + 0.5
|
||||
in 31..65 -> zoom
|
||||
in 66..70 -> zoom - 0.5
|
||||
in 71..90 -> zoom - 1.0
|
||||
in 91..100 -> zoom - 2.0
|
||||
else -> zoom - 3.0
|
||||
}
|
||||
return zoom
|
||||
}
|
||||
|
||||
fun previewZoom(centerLocation: Location, previewDistance: Double): Double {
|
||||
@@ -93,24 +95,27 @@ fun calculateZoomFromBoundingBox(centerLocation: Location, previewDistance: Doub
|
||||
}
|
||||
|
||||
|
||||
fun calculateTilt(newZoom: Double, tilt: Double): Double =
|
||||
if (newZoom < 13) {
|
||||
0.0
|
||||
} else {
|
||||
if (tilt == 0.0) {
|
||||
TILT
|
||||
fun calculateTilt(viewStyle: ViewStyle, newZoom: Double, tilt: Double): Double =
|
||||
if (viewStyle == ViewStyle.VIEW) {
|
||||
if (newZoom < 13) {
|
||||
0.0
|
||||
} else {
|
||||
tilt
|
||||
if (tilt == 0.0) {
|
||||
TILT
|
||||
} else {
|
||||
tilt
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
fun bearing(fromLocation: Location, toLocation: Location, oldBearing: Double): Double {
|
||||
fun bearingPositive(fromLocation: Location, toLocation: Location, oldBearing: Double): Double {
|
||||
val distance = fromLocation.distanceTo(toLocation)
|
||||
if (distance < 1.0) {
|
||||
return oldBearing
|
||||
}
|
||||
val bearing = fromLocation.bearingTo(toLocation).toInt().toDouble()
|
||||
return bearing
|
||||
return fromLocation.bearingPositive(toLocation).toInt().toDouble()
|
||||
}
|
||||
|
||||
fun location(longitude: Double, latitude: Double): Location {
|
||||
@@ -120,6 +125,10 @@ fun location(longitude: Double, latitude: Double): Location {
|
||||
return location
|
||||
}
|
||||
|
||||
fun Location.bearingPositive(locationTo: Location): Float {
|
||||
return (this.bearingTo (locationTo) + 360) % 360
|
||||
}
|
||||
|
||||
fun formatDateTime(time: Long): String {
|
||||
val dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
val dateTime = LocalDateTime.ofEpochSecond(time / 1000, 0, ZoneOffset.UTC)
|
||||
@@ -132,20 +141,26 @@ fun Double.round(numFractionDigits: Int): Double {
|
||||
return (this * factor).roundToInt() / factor
|
||||
}
|
||||
|
||||
fun isNumeric(toCheck: String): Boolean {
|
||||
val regex = "-?[0-9]+(\\.[0-9]+)?".toRegex()
|
||||
return toCheck.matches(regex)
|
||||
}
|
||||
|
||||
fun duration(
|
||||
preview: Boolean,
|
||||
viewStyle: ViewStyle,
|
||||
bearing: Double,
|
||||
lastBearing: Double,
|
||||
lastLocationUpdate: LocalDateTime
|
||||
): Duration {
|
||||
if (preview) {
|
||||
return 10.milliseconds
|
||||
if (viewStyle == ViewStyle.PREVIEW ||
|
||||
viewStyle == ViewStyle.AMENITY_VIEW) {
|
||||
return 100.milliseconds
|
||||
}
|
||||
val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) {
|
||||
2.seconds
|
||||
} else {
|
||||
val updateDuration = java.time.Duration.between(LocalDateTime.now(), lastLocationUpdate)
|
||||
if (updateDuration.toMillis().absoluteValue < 1000) {
|
||||
if (updateDuration.toMillis().absoluteValue !in 1000..2000) {
|
||||
2.seconds
|
||||
} else {
|
||||
((updateDuration!!.toMillis().absoluteValue * 1.8).toDuration(DurationUnit.MILLISECONDS))
|
||||
@@ -183,3 +198,11 @@ fun formattedDistance(distanceMode: Int, distance: Double): Pair<Double, Int> {
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
@@ -66,10 +66,30 @@
|
||||
<string name="general">Allgemein</string>
|
||||
<string name="traffic">Verkehr anzeigen</string>
|
||||
<string name="trip_suggestion">Fahrten-Vorschläge</string>
|
||||
<string name="drive_settings">Drive settings</string>
|
||||
<string name="car_settings">Car settings</string>
|
||||
<string name="combustion">Combustion</string>
|
||||
<string name="electric">Electric</string>
|
||||
<string name="engine_type">Engine type</string>
|
||||
<string name="alternative_routes">Alternative routes</string>
|
||||
<string name="drive_settings">Fahr-Einstellungen</string>
|
||||
<string name="car_settings">Fahrzeug-Einstellungen</string>
|
||||
<string name="combustion">Verbrenner</string>
|
||||
<string name="electric">Elektro</string>
|
||||
<string name="engine_type">Motortyp</string>
|
||||
<string name="alternative_routes">Alternative Routen</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>
|
||||
|
||||
@@ -50,10 +50,30 @@
|
||||
<string name="general">Γενικά</string>
|
||||
<string name="traffic">Εμφάνιση κίνησης</string>
|
||||
<string name="trip_suggestion">Προτάσεις διαδρομής</string>
|
||||
<string name="drive_settings">Drive settings</string>
|
||||
<string name="car_settings">Car settings</string>
|
||||
<string name="combustion">Combustion</string>
|
||||
<string name="electric">Electric</string>
|
||||
<string name="engine_type">Engine type</string>
|
||||
<string name="alternative_routes">Alternative routes</string>
|
||||
<string name="drive_settings">Ρυθμίσεις οδήγησης</string>
|
||||
<string name="car_settings">Ρυθμίσεις αυτοκινήτου</string>
|
||||
<string name="combustion">Κινητήρας εσωτερικής καύσης</string>
|
||||
<string name="electric">Ηλεκτρικό</string>
|
||||
<string name="engine_type">Τύπος κινητήρα</string>
|
||||
<string name="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>
|
||||
|
||||
@@ -50,10 +50,30 @@
|
||||
<string name="general">Ogólne</string>
|
||||
<string name="traffic">Pokaż natężenie ruchu</string>
|
||||
<string name="trip_suggestion">Sugestie dotyczące podróży</string>
|
||||
<string name="drive_settings">Drive settings</string>
|
||||
<string name="car_settings">Car settings</string>
|
||||
<string name="combustion">Combustion</string>
|
||||
<string name="electric">Electric</string>
|
||||
<string name="engine_type">Engine type</string>
|
||||
<string name="alternative_routes">Alternative routes</string>
|
||||
<string name="drive_settings">Ustawienia jazdy</string>
|
||||
<string name="car_settings">Ustawienia samochodu</string>
|
||||
<string name="combustion">Spalinowy</string>
|
||||
<string name="electric">Elektryczny</string>
|
||||
<string name="engine_type">Typ silnika</string>
|
||||
<string name="alternative_routes">Alternatywne trasy</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>
|
||||
|
||||
@@ -59,4 +59,24 @@
|
||||
<string name="electric">Electric</string>
|
||||
<string name="engine_type">Engine type</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>
|
||||
File diff suppressed because one or more lines are too long
@@ -7,6 +7,7 @@ import com.kouros.navigation.data.route.Maneuver
|
||||
import com.kouros.navigation.data.route.Routes
|
||||
import com.kouros.navigation.data.route.Step
|
||||
import com.kouros.navigation.data.route.Summary
|
||||
import com.kouros.navigation.utils.location
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
@@ -44,12 +45,12 @@ class RouteCalculatorTest {
|
||||
waypointIndex: Int = 0,
|
||||
): Step {
|
||||
val waypoints = (0 until numWaypoints).map { i ->
|
||||
listOf(11.0 + index * 0.01 + i * 0.001, 48.0)
|
||||
location(11.0 + index * 0.01 + i * 0.001, 48.0)
|
||||
}
|
||||
return Step(
|
||||
index = index,
|
||||
waypointIndex = waypointIndex,
|
||||
maneuver = Maneuver(waypoints = waypoints, location = mock()),
|
||||
maneuver = Maneuver(waypoints = waypoints, location = mock(), leftDistance = mock() ),
|
||||
duration = duration,
|
||||
distance = distance,
|
||||
)
|
||||
@@ -100,7 +101,7 @@ class RouteCalculatorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `findStep skips all steps before currentStepIndex`() {
|
||||
fun `findStep considers previous step when searching`() {
|
||||
val step0 = createStep(index = 0, numWaypoints = 2)
|
||||
val step1 = createStep(index = 1, numWaypoints = 2)
|
||||
routeModel.navState = routeModel.navState.copy(
|
||||
@@ -108,17 +109,17 @@ class RouteCalculatorTest {
|
||||
)
|
||||
|
||||
val mockLocation: Location = mock()
|
||||
whenever(mockLocation.distanceTo(any())).thenReturn(200F, 50F)
|
||||
// Distance to step0 waypoints is very small, distance to step1 waypoints is large
|
||||
whenever(mockLocation.distanceTo(any())).thenReturn(5F, 5F, 500F, 500F)
|
||||
|
||||
routeCalculator.findStep(mockLocation)
|
||||
|
||||
// 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)
|
||||
assertEquals(0, routeModel.navState.route.currentStepIndex)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `findStep breaks early once nearestDistance drops below NEAREST_LOCATION_DISTANCE`() {
|
||||
fun `findStep breaks later with relaxed distance threshold`() {
|
||||
val step0 = createStep(index = 0, numWaypoints = 2)
|
||||
val step1 = createStep(index = 1, numWaypoints = 2)
|
||||
val step2 = createStep(index = 2, numWaypoints = 2)
|
||||
@@ -127,16 +128,18 @@ class RouteCalculatorTest {
|
||||
)
|
||||
|
||||
val mockLocation: Location = mock()
|
||||
// step0/wp0: 500F, step0/wp1: 5F — 5F < NEAREST_LOCATION_DISTANCE (10F) → break
|
||||
whenever(mockLocation.distanceTo(any())).thenReturn(500F, 5F)
|
||||
// step0/wp0: 500F, step0/wp1: 5F, step1/wp0: 150F, step1/wp1: 160F, step2/wp0: 210F, step2/wp1: 220F
|
||||
// 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, 150F, 160F, 210F, 220F)
|
||||
|
||||
routeCalculator.findStep(mockLocation)
|
||||
|
||||
// step1 and step2 are never evaluated
|
||||
verify(mockLocation, times(2)).distanceTo(any())
|
||||
// It should have’Checked step 0 (2), step 1 (2), and the first point of step 2 (1) where it finally breaks.
|
||||
verify(mockLocation, times(5)).distanceTo(any())
|
||||
assertEquals(0, routeModel.navState.route.currentStepIndex)
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// travelLeftTime
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@@ -8,8 +8,12 @@ import com.kouros.navigation.data.route.Maneuver
|
||||
import com.kouros.navigation.data.route.Routes
|
||||
import com.kouros.navigation.data.route.Step
|
||||
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.Test
|
||||
import org.maplibre.geojson.FeatureCollection
|
||||
import org.maplibre.geojson.LineString
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doNothing
|
||||
import org.mockito.kotlin.mock
|
||||
@@ -36,12 +40,12 @@ class RouteModelTest {
|
||||
waypointIndex: Int = 0,
|
||||
): Step {
|
||||
val waypoints = (0 until numWaypoints).map { i ->
|
||||
listOf(11.0 + index * 0.01 + i * 0.001, 48.0)
|
||||
location(11.0 + index * 0.01 + i * 0.001, 48.0)
|
||||
}
|
||||
return Step(
|
||||
index = index,
|
||||
waypointIndex = waypointIndex,
|
||||
maneuver = Maneuver(waypoints = waypoints, location = mock()),
|
||||
maneuver = Maneuver(waypoints = waypoints, location = mock(), leftDistance = mock()),
|
||||
duration = duration,
|
||||
distance = distance,
|
||||
)
|
||||
@@ -58,6 +62,18 @@ class RouteModelTest {
|
||||
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
|
||||
fun `hasLegs returns true when route has legs`() {
|
||||
val step0 = createStep(index = 0, numWaypoints = 2)
|
||||
|
||||
@@ -21,6 +21,7 @@ kotlin.code.style=official
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
|
||||
org.gradle.daemon=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=true
|
||||
|
||||
+20
-20
@@ -1,21 +1,21 @@
|
||||
[versions]
|
||||
agp = "9.1.0"
|
||||
agp = "9.2.1"
|
||||
androidGpxParser = "2.3.1"
|
||||
androidSdkTurf = "6.0.1"
|
||||
datastore = "1.2.1"
|
||||
gradle = "9.1.0"
|
||||
koinAndroid = "4.2.0"
|
||||
koinAndroidxCompose = "4.2.0"
|
||||
koinComposeViewmodel = "4.2.0"
|
||||
koinCore = "4.2.0"
|
||||
kotlin = "2.3.20"
|
||||
gradle = "9.2.1"
|
||||
koinAndroid = "4.2.1"
|
||||
koinAndroidxCompose = "4.2.1"
|
||||
koinComposeViewmodel = "4.2.1"
|
||||
koinCore = "4.2.1"
|
||||
kotlin = "2.3.21"
|
||||
coreKtx = "1.18.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
kotlinxSerializationJson = "1.10.0"
|
||||
kotlinxSerializationJson = "1.11.0"
|
||||
lifecycleRuntimeKtx = "2.10.0"
|
||||
composeBom = "2026.03.01"
|
||||
composeBom = "2026.04.01"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.13.0"
|
||||
carApp = "1.7.0"
|
||||
@@ -26,21 +26,21 @@ mockitoKotlin = "6.3.0"
|
||||
rules = "1.7.0"
|
||||
runner = "1.7.0"
|
||||
material3 = "1.4.0"
|
||||
runtimeLivedata = "1.10.6"
|
||||
foundation = "1.10.6"
|
||||
runtimeLivedata = "1.11.0"
|
||||
foundation = "1.11.0"
|
||||
maplibre-compose = "0.12.1"
|
||||
playServicesLocation = "21.3.0"
|
||||
runtime = "1.10.6"
|
||||
runtime = "1.11.0"
|
||||
accompanist = "0.37.3"
|
||||
uiVersion = "1.10.6"
|
||||
uiText = "1.10.6"
|
||||
navigationCompose = "2.9.7"
|
||||
uiToolingPreview = "1.10.6"
|
||||
uiTooling = "1.10.6"
|
||||
uiVersion = "1.11.0"
|
||||
uiText = "1.11.0"
|
||||
navigationCompose = "2.9.8"
|
||||
uiToolingPreview = "1.11.0"
|
||||
uiTooling = "1.11.0"
|
||||
material3WindowSizeClass = "1.4.0"
|
||||
uiGraphics = "1.10.6"
|
||||
uiGraphics = "1.11.0"
|
||||
window = "1.5.1"
|
||||
foundationLayout = "1.10.6"
|
||||
foundationLayout = "1.11.0"
|
||||
datastorePreferences = "1.2.1"
|
||||
datastoreCore = "1.2.1"
|
||||
monitor = "1.8.0"
|
||||
@@ -48,7 +48,7 @@ robolectric = "4.16.1"
|
||||
truth = "1.4.5"
|
||||
testCore = "1.7.0"
|
||||
archCoreTesting = "2.2.0"
|
||||
kotlinxCoroutinesTest = "1.10.1"
|
||||
kotlinxCoroutinesTest = "1.10.2"
|
||||
|
||||
[libraries]
|
||||
android-gpx-parser = { module = "com.github.ticofab:android-gpx-parser", version.ref = "androidGpxParser" }
|
||||
|
||||
Reference in New Issue
Block a user