Compare commits
| 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 | ||
|
|
24173412e8 | ||
|
|
52f8dec2e6 | ||
|
|
6838ad09c4 |
@@ -1,10 +1,10 @@
|
|||||||
# CLAUDE.md
|
# 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
|
## 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
|
## Build Commands
|
||||||
|
|
||||||
@@ -12,14 +12,15 @@ This is an Android navigation app built with Jetpack Compose that supports multi
|
|||||||
# Build the app (from repository root)
|
# Build the app (from repository root)
|
||||||
./gradlew :app:assembleDebug
|
./gradlew :app:assembleDebug
|
||||||
|
|
||||||
# Build specific flavor
|
# Build a specific flavor
|
||||||
|
./gradlew :app:assemblePlayDebug
|
||||||
./gradlew :app:assembleDemoDebug
|
./gradlew :app:assembleDemoDebug
|
||||||
./gradlew :app:assembleFullDebug
|
./gradlew :app:assembleFullDebug
|
||||||
|
|
||||||
# Run tests
|
# Run unit tests
|
||||||
./gradlew test
|
./gradlew test
|
||||||
|
|
||||||
# Run tests for specific module
|
# Run tests for a specific module
|
||||||
./gradlew :common:data:test
|
./gradlew :common:data:test
|
||||||
./gradlew :common:car:test
|
./gradlew :common:car:test
|
||||||
|
|
||||||
@@ -32,12 +33,12 @@ This is an Android navigation app built with Jetpack Compose that supports multi
|
|||||||
|
|
||||||
## Module Structure
|
## Module Structure
|
||||||
|
|
||||||
The project uses a multi-module architecture:
|
The project uses a multi-module architecture (see `settings.gradle.kts`):
|
||||||
|
|
||||||
- **app/** - Main Android app with Jetpack Compose UI for phone
|
- **app/** - Main Android app with Jetpack Compose UI for phone (`com.kouros.navigation`)
|
||||||
- **common/data/** - Core data layer with routing logic, repositories, and data models (shared by all modules)
|
- **common/data/** - Core data layer with routing logic, repositories, view models, persistence (`com.kouros.data`)
|
||||||
- **common/car/** - Android Auto/Automotive OS UI implementation
|
- **common/car/** - Android Auto/Automotive OS UI implementation
|
||||||
- **automotive/** - Placeholder for future native Automotive OS app
|
- **automotive/** - Placeholder for future native Automotive OS app (no Kotlin sources yet)
|
||||||
|
|
||||||
Dependencies flow: `app` → `common:car` → `common:data`
|
Dependencies flow: `app` → `common:car` → `common:data`
|
||||||
|
|
||||||
@@ -45,156 +46,212 @@ Dependencies flow: `app` → `common:car` → `common:data`
|
|||||||
|
|
||||||
### Routing Providers (Pluggable System)
|
### Routing Providers (Pluggable System)
|
||||||
|
|
||||||
The app supports three routing engines that implement the `NavigationRepository` abstract class:
|
The app supports three routing engines that extend the `NavigationRepository` abstract class (`common/data/.../data/NavigationRepository.kt`):
|
||||||
|
|
||||||
1. **OsrmRepository** - OSRM routing engine
|
1. **ValhallaRepository** - Valhalla routing engine (ordinal 0)
|
||||||
2. **ValhallaRepository** - Valhalla routing engine
|
2. **OsrmRepository** - OSRM routing engine (ordinal 1)
|
||||||
3. **TomTomRepository** - TomTom routing engine
|
3. **TomTomRepository** - TomTom routing engine (ordinal 2, default)
|
||||||
|
|
||||||
Each provider has a corresponding mapper class (`OsrmRoute`, `ValhallaRoute`, `TomTomRoute`) that converts provider-specific JSON responses to the universal `Route` data model.
|
Selection is driven by `RouteEngine` enum order (see `Data.kt`). `NavigationRepository` also exposes shared HTTP helpers (`fetchUrl`, `searchPlaces`, `reverseAddress`) that use Nominatim for geocoding and `ApplicationConfig`-backed HTTP basic auth for protected endpoints.
|
||||||
|
|
||||||
|
Each provider has a corresponding mapper (`OsrmRoute`, `ValhallaRoute`, `TomTomRoute`) that converts provider-specific JSON into the universal `Route` model. The universal model is decomposed into a dedicated `data/route/` package: `Routes`, `Leg`, `Step`, `Maneuver`, `Intersection`, `Lane`, `Summary`.
|
||||||
|
|
||||||
**Adding a new routing provider:**
|
**Adding a new routing provider:**
|
||||||
1. Create `NewProviderRepository` extending `NavigationRepository` in `common/data/src/main/java/com/kouros/navigation/data/`
|
1. Create `NewProviderRepository` extending `NavigationRepository` under `common/data/src/main/java/com/kouros/navigation/data/<provider>/`
|
||||||
2. Implement `getRoute()` method
|
2. Implement `getRoute()` and `getTraffic()`
|
||||||
3. Create `NewProviderRoute.kt` with `mapToRoute()` function
|
3. Create `NewProviderRoute.kt` with a `mapToRoute(response, builder)` function that populates a `Route.Builder`
|
||||||
4. Add provider detection logic in `Route.Builder.route()`
|
4. Add a `RouteEngine` enum entry and provider branch in `Route.Builder.route()` (`data/Route.kt`)
|
||||||
5. Update `NavigationUtils.getViewModel()` to return appropriate ViewModel
|
5. Update `NavigationUtils.getViewModel()` (`utils/NavigationUtils.kt`) to return a `NavigationViewModel` wired to the new repository
|
||||||
|
|
||||||
### Data Flow
|
### Data Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
User Action (search/select destination)
|
User action (search / select destination)
|
||||||
↓
|
↓
|
||||||
ViewModel.loadRoute() [LiveData]
|
NavigationViewModel.loadRoute() [LiveData / Flow]
|
||||||
↓
|
↓
|
||||||
NavigationRepository.getRoute() [Selected provider]
|
NavigationRepository.getRoute() [selected provider]
|
||||||
↓
|
↓
|
||||||
*Route.mapToRoute() [Convert to universal Route model]
|
*Route.mapToRoute() [convert to universal Route]
|
||||||
↓
|
↓
|
||||||
RouteModel.startNavigation()
|
RouteModel.startNavigation()
|
||||||
↓
|
↓
|
||||||
RouteModel.updateLocation() [On each location update]
|
RouteCalculator.findStep() [on each location update]
|
||||||
↓
|
↓
|
||||||
UI observes LiveData and displays current step
|
NavigationState updated → UI observes and renders current step
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Classes
|
### Key Classes
|
||||||
|
|
||||||
**Navigation Logic:**
|
**Navigation logic** (`common/data/.../model/`):
|
||||||
- `RouteModel.kt` - Core navigation engine (tracks position, calculates distances, manages steps)
|
- `RouteModel.kt` - Core navigation engine; tracks position, manages step progression, owns `NavigationState`
|
||||||
- `RouteCarModel.kt` - Extends RouteModel with Android Auto-specific formatting
|
- `RouteCalculator.kt` - Step-finding algorithm: snaps current location to the nearest waypoint, computes leftover distance, handles snap correction and reroute thresholds
|
||||||
- `ViewModel.kt` - androidx.ViewModel with LiveData for route, traffic, places, etc.
|
- `RouteCarModel.kt` (`common/car/.../navigation/`) - Extends RouteModel with Android Auto-specific formatting
|
||||||
|
- `NavigationViewModel.kt` - androidx ViewModel exposing route, traffic, places (Nominatim), amenities (Overpass), and fuel prices (Tankerkönig) as LiveData
|
||||||
|
- `SettingsViewModel.kt` - State holder for DataStore-backed settings (dark mode, 3D, routing engine, avoid preferences, etc.)
|
||||||
|
- `BaseStyleModel.kt` - Map style state
|
||||||
|
|
||||||
**Data Models:**
|
**Data models** (`common/data/.../data/`):
|
||||||
- `Route.kt` - Universal route structure used by all providers
|
- `Route.kt` - Universal route wrapper with `Route.Builder` and provider dispatch
|
||||||
- `Place.kt` - ObjectBox entity for favorites/recent locations
|
- `data/route/*` - Decomposed route components (`Routes`, `Leg`, `Step`, `Maneuver`, `Intersection`, `Lane`, `Summary`)
|
||||||
- `StepData.kt` - Display data for current navigation instruction
|
- `NavigationState.kt` - Immutable navigation state (route, flags, location, bearing, maneuver, destination)
|
||||||
|
- `Data.kt` - Shared types (`Place`, `StepData`, `SearchFilter`, `Locations`, `ValhallaLocation`) plus `object Constants` and the `RouteEngine`, `DarkMode`, `EngineType`, `ViewStyle`, `NavigationThemeColor` enums
|
||||||
|
- `ApplicationConfig.kt` - Loads `USER` / `PASSWORD` from `BuildConfig` for HTTP basic auth on protected endpoints
|
||||||
|
|
||||||
**Repositories:**
|
**Persistence** (`common/data/.../data/datastore/` and `common/data/.../repository/`):
|
||||||
- `NavigationRepository.kt` - Abstract base class for all routing providers
|
- `DataStoreManager.kt` - Single source of truth for preference keys; exposes `Flow<T>` reads and `suspend` writes for each setting
|
||||||
- Also handles Nominatim geocoding search and TomTom traffic incidents
|
- `SettingsRepository.kt` - Higher-level wrapper over `DataStoreManager`
|
||||||
|
|
||||||
**Android Auto:**
|
**Repositories** (`common/data/.../data/`):
|
||||||
- `NavigationCarAppService.kt` - Entry point for Android Auto/Automotive OS
|
- `NavigationRepository.kt` - Abstract base class for routing providers; also handles Nominatim geocoding (search and reverse)
|
||||||
- `NavigationSession.kt` - Session management
|
- `osrm/`, `valhalla/`, `tomtom/` - Provider implementations and JSON DTOs
|
||||||
- `NavigationScreen.kt` - Car screen templates with NavigationType state machine
|
- `fuel/FuelPrices.kt` - Tankerkönig fuel-price client (returns `List<Station>`)
|
||||||
- `SurfaceRenderer.kt` - Handles virtual display and map rendering
|
- `overpass/` - Overpass API client for POIs and speed limits
|
||||||
|
|
||||||
|
**Android Auto / Automotive** (`common/car/`):
|
||||||
|
- `NavigationCarAppService.kt` - CarAppService entry point
|
||||||
|
- `CarSession.kt` (abstract) and concrete `NavigationSession.kt`, `ClusterSession.kt`
|
||||||
|
- `screen/NavigationScreen.kt` - Main navigation template; `NavigationType` enum drives template selection (VIEW, NAVIGATION, REROUTE, RECENT, ARRIVAL)
|
||||||
|
- `screen/SearchScreen.kt`, `RoutePreviewScreen.kt`, `PlaceListScreen.kt`, `CategoriesScreen.kt`, `CategoryScreen.kt`, `StopOverScreen.kt`, `RequestPermissionScreen.kt`
|
||||||
|
- `screen/settings/*` - Per-setting screens (RoutingSettings, NavigationSettings, DisplaySettings, DarkModeSettings, DistanceSettings, AudioSettings, CarSettings, PasswordSettings)
|
||||||
|
- `screen/observers/*` - Observer pattern that bridges LiveData to car screens. `NavigationObserverManager` orchestrates `RouteObserver`, `TrafficObserver`, `TrafficMessageObserver`, `PlaceSearchObserver`, `CategoryObserver`, `MaxSpeedObserver`, `SpeedCameraObserver`
|
||||||
|
- Supporting managers: `SurfaceRenderer.kt` (virtual display + map), `DeviceLocationManager.kt`, `CarSensorManager.kt`, `NavigationNotificationManager.kt` + `NavigationNotificationService.kt`, `TextToSpeechManager.kt`, `CustomLifecycleOwner.kt`
|
||||||
|
- `map/MapView.kt`, `map/LocationPuck.kt` - MapLibre rendering for the car surface
|
||||||
|
|
||||||
### External APIs
|
### External APIs
|
||||||
|
|
||||||
| Service | Purpose | Base URL |
|
| Service | Purpose | URL |
|
||||||
|---------|---------|----------|
|
|---------------|--------------------------|------------------------------------------------------------------------------|
|
||||||
| OSRM | Routing | `https://kouros-online.de/osrm/route/v1/driving/` |
|
| OSRM | Routing | `https://router.project-osrm.org/route/v1/driving/` |
|
||||||
| Valhalla | Routing | `https://kouros-online.de/valhalla/route` |
|
| Valhalla | Routing | `https://kouros-online.de/valhalla/route?json=` (HTTP basic auth) |
|
||||||
|
| TomTom | Routing | `https://api.tomtom.com/routing/1/calculateRoute/` |
|
||||||
| TomTom | Traffic incidents | `https://api.tomtom.com/traffic/services/5/incidentDetails` |
|
| TomTom | Traffic incidents | `https://api.tomtom.com/traffic/services/5/incidentDetails` |
|
||||||
| Nominatim | Geocoding search | `https://kouros-online.de/nominatim/` |
|
| Nominatim | Geocoding (search/reverse) | `https://nominatim.openstreetmap.org/` |
|
||||||
| Overpass | POI & speed limits | OpenStreetMap Overpass API |
|
| Overpass | POIs & speed limits | OpenStreetMap Overpass API (DEBUG builds use `https://kouros-online.de/api/interpreter`) |
|
||||||
|
| Tankerkönig | Fuel prices | `https://creativecommons.tankerkoenig.de/json/list.php?` (API key in `fuel/FuelPrices.kt`) |
|
||||||
|
|
||||||
|
In DEBUG builds, `TomTomRepository` and `FuelPrices` can be pointed at a local fixture (see `useLocal` flags) — TomTom can fall back to `R.raw.tomom_routing`, fuel falls back to a local JSON URL.
|
||||||
|
|
||||||
## Important Constants
|
## Important Constants
|
||||||
|
|
||||||
Located in `Constants.kt` (`common/data`):
|
Defined in `object Constants` inside `common/data/.../data/Data.kt`:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
NEXT_STEP_THRESHOLD = 120.0 m // Distance to show next maneuver
|
NEXT_STEP_THRESHOLD = 500.0 // Distance (m) to show next maneuver
|
||||||
DESTINATION_ARRIVAL_DISTANCE = 40.0 m // Distance to trigger arrival
|
DESTINATION_ARRIVAL_DISTANCE = 10.0 // Distance (m) to trigger arrival
|
||||||
MAXIMAL_SNAP_CORRECTION = 50.0 m // Max distance to snap to route
|
MAXIMAL_SNAP_CORRECTION = 50.0 // Max distance (m) to snap to route
|
||||||
MAXIMAL_ROUTE_DEVIATION = 80.0 m // Max deviation before reroute
|
MAXIMAL_ROUTE_DEVIATION = 100.0 // Max deviation (m) before reroute
|
||||||
|
NEAREST_LOCATION_DISTANCE = 10F
|
||||||
|
SPEED_UPDATE_DISTANCE = 600F
|
||||||
|
INSTRUCTION_DISTANCE = 50
|
||||||
|
SPEED_BEARING_DEVIATION = 60
|
||||||
|
TILT = 60.0 // Map tilt in degrees during navigation
|
||||||
|
TANKER_KOENIG_DELAY = 300_000 // ms between fuel price refreshes
|
||||||
```
|
```
|
||||||
|
|
||||||
SharedPreferences keys:
|
DataStore keys (see `DataStoreManager.PreferencesKeys`):
|
||||||
- `ROUTING_ENGINE` - Selected provider (0=Valhalla, 1=OSRM, 2=TomTom)
|
- `RoutingEngine` (Int) — `0=Valhalla`, `1=OSRM`, `2=TomTom` (default 2)
|
||||||
- `DARK_MODE_SETTINGS` - Theme preference
|
- `DarkMode` (Int), `Show3D` (Bool), `CarLocation` (Bool)
|
||||||
- `AVOID_MOTORWAY`, `AVOID_TOLLWAY` - Route preferences
|
- `AvoidMotorway`, `AvoidTollway`, `AvoidFerry` (Bool)
|
||||||
|
- `LastRoute`, `RecentPlaces`, `FuelPrices`, `LastFuelPrices`
|
||||||
|
- `TomTomApiKey`, `DistanceMode`, `GuidanceAudio`, `Traffic`, `TripSuggestion`, `EngineType`, `AlternativeRoutes`
|
||||||
|
|
||||||
## Navigation Flow
|
## Navigation Flow
|
||||||
|
|
||||||
1. **Route Loading**: User searches via Nominatim → selects place → ViewModel.loadRoute() calls selected repository
|
1. **Route loading** — User searches via Nominatim → selects place → `NavigationViewModel.loadRoute()` calls the selected repository
|
||||||
2. **Route Parsing**: Provider JSON → mapper converts to universal Route → RouteModel.startNavigation()
|
2. **Route parsing** — Provider JSON → `*Route.mapToRoute()` populates `Route.Builder` → universal `Route` → `RouteModel.startNavigation()`
|
||||||
3. **Location Tracking**: FusedLocationProviderClient provides updates → RouteModel.updateLocation()
|
3. **Location tracking** — `FusedLocationProviderClient` updates → `RouteModel.updateLocation()`
|
||||||
4. **Step Calculation**: findStep() snaps location to nearest waypoint → updates current step
|
4. **Step calculation** — `RouteCalculator.findStep()` snaps location to the nearest waypoint, returns `StepMatch`, and updates the current step index
|
||||||
5. **UI Updates**: currentStep() and nextStep() provide display data (instruction, distance, icon, lanes)
|
5. **UI updates** — `NavigationState` flows out via LiveData/Compose state; phone and car UIs render the current/next step (instruction, distance, icon, lanes)
|
||||||
6. **Arrival**: When distance < DESTINATION_ARRIVAL_DISTANCE, navigation ends
|
6. **Arrival** — When distance < `DESTINATION_ARRIVAL_DISTANCE`, navigation ends
|
||||||
|
|
||||||
## Testing Navigation
|
## Testing Navigation
|
||||||
|
|
||||||
The app includes mock location support for testing:
|
The phone app supports mock locations for testing:
|
||||||
|
|
||||||
- Set `useMock = true` in MainActivity
|
- Set `useMock = true` in `MainActivity`
|
||||||
- Enable "Mock location app" in Android Developer Options
|
- Enable "Mock location app" in Android Developer Options
|
||||||
- Choose test mode:
|
- Choose mode in `model/Simulation.kt` / `model/MockLocation.kt`:
|
||||||
- `type = 1` - Simulate movement along entire route
|
- `type = 1` — Simulate movement along the entire route
|
||||||
- `type = 2` - Test specific step range
|
- `type = 2` — Test a specific step range
|
||||||
- `type = 3` - Replay GPX track file
|
- `type = 3` — Replay a GPX track file
|
||||||
|
|
||||||
## ObjectBox Database
|
The car module has its own `navigation/Simulation.kt` for car-side simulation.
|
||||||
|
|
||||||
ObjectBox is configured in `common/data/build.gradle.kts` with the kapt plugin. The database stores:
|
### Unit tests
|
||||||
|
|
||||||
- Recent destinations (category: "Recent")
|
- `common/data/src/test/.../model/RouteCalculatorTest.kt`
|
||||||
- Favorite places (category: "Favorites")
|
- `common/data/src/test/.../model/RouteModelTest.kt`
|
||||||
- Imported contacts (category: "Contacts")
|
- `common/data/src/test/.../model/IconMapperTest.kt`
|
||||||
|
- `common/data/src/test/.../model/OverpassTest.kt`
|
||||||
|
- `common/data/src/test/.../utils/GeoUtilsTest.kt`
|
||||||
|
- `common/car/src/test/.../screen/NavigationScreenTest.kt`
|
||||||
|
- `common/car/src/test/.../screen/observers/CategoryObserverTest.kt`, `ObserversTest.kt`
|
||||||
|
|
||||||
Queries use ObjectBox query builder pattern with generated `Place_` property accessors.
|
## Persistence
|
||||||
|
|
||||||
|
All app preferences are stored via Androidx **DataStore Preferences** (`navigation_settings` data store). There is no Room/ObjectBox/SQL database — the only persisted entities are settings and serialized lists (recent places, last route, last fuel prices) kept as strings.
|
||||||
|
|
||||||
|
`DataStoreManager` exposes a `Flow<T>` per setting plus a matching `suspend set*` writer. `SettingsRepository` wraps it for higher-level access. `SettingsViewModel` and `AppViewModel` consume those flows for UI state.
|
||||||
|
|
||||||
## Compose UI Structure
|
## Compose UI Structure
|
||||||
|
|
||||||
**Phone App:**
|
**Phone app** (`app/src/main/java/com/kouros/navigation/`):
|
||||||
- `MainActivity.kt` - Main entry with permission handling and Navigation Compose
|
- `ui/MainActivity.kt` - Entry point with permission handling and Navigation Compose host
|
||||||
- `NavigationScreen.kt` - Turn-by-turn navigation display
|
- `ui/MapView.kt` - MapLibre rendering with camera state management
|
||||||
- `SearchSheet.kt` / `NavigationSheet.kt` - Bottom sheet content
|
- `ui/SheetLayout.kt` - Bottom sheet scaffold
|
||||||
- `MapView.kt` - MapLibre rendering with camera state management
|
- `ui/PermissionScreen.kt`
|
||||||
|
- `ui/navigation/AppNavGraph.kt` - Compose navigation graph
|
||||||
|
- `ui/navigation/NavigationScreen.kt`, `NavigationSheet.kt` - Turn-by-turn UI
|
||||||
|
- `ui/search/SearchScreen.kt`, `SearchSheet.kt`
|
||||||
|
- `ui/settings/SettingsScreen.kt`, `SettingsRoute.kt`, `DisplayScreen.kt`, `NavigationScreen.kt`, `CarScreen.kt`, `Settings.kt`
|
||||||
|
- `ui/components/SettingItem.kt`, `SettingSwitch.kt`, `RadioButtonSingleSelection.kt`, `SectionTitle.kt`
|
||||||
|
- `ui/app/AppViewModel.kt`, `AppViewModelProvider.kt`
|
||||||
|
- `ui/theme/{Color,Type,Shapes,Theme}.kt`
|
||||||
|
- `model/Simulation.kt`, `model/MockLocation.kt` - Mock location for testing
|
||||||
|
- `di/appModule.kt` - Koin module
|
||||||
|
- `MainApplication.kt` - Application class
|
||||||
|
|
||||||
**Android Auto:**
|
**Android Auto** uses CarAppService templates (NavigationTemplate, MessageTemplate, MapWithContentTemplate). `NavigationType` (in `screen/NavigationScreen.kt`) controls which template to render. UI state is synchronized with `NavigationViewModel` through the observers in `screen/observers/`.
|
||||||
- Uses CarAppService Screen templates (NavigationTemplate, MessageTemplate, MapWithContentTemplate)
|
|
||||||
- NavigationType enum controls which template to display (VIEW, NAVIGATION, REROUTE, RECENT, ARRIVAL)
|
|
||||||
|
|
||||||
## Build Flavors
|
## Build Flavors
|
||||||
|
|
||||||
Two product flavors with dimension "version":
|
`app/build.gradle.kts` defines three product flavors under the `store` dimension:
|
||||||
- **demo** - applicationId: `com.kouros.navigation.demo`
|
- **play** - applicationIdSuffix `.play`
|
||||||
- **full** - applicationId: `com.kouros.navigation.full`
|
- **demo** - applicationIdSuffix `.demo`
|
||||||
|
- **full** - applicationIdSuffix `.full`
|
||||||
|
|
||||||
|
Base namespace is `com.kouros.navigation`; current versionName is `0.3.0.109`. Java/Kotlin target is 21 for the app module, 11 for `common:data`. compileSdk/targetSdk = 37, minSdk = 33.
|
||||||
|
|
||||||
|
`signing.properties` (root, gitignored) provides the keystore credentials for both debug and release builds. `local.properties` provides `USER` / `PASSWORD` build config fields consumed by `ApplicationConfig`.
|
||||||
|
|
||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
**Dependency Injection (Koin):**
|
**Dependency injection (Koin):**
|
||||||
```kotlin
|
```kotlin
|
||||||
single { OsrmRepository() }
|
single { OsrmRepository() }
|
||||||
viewModel { ViewModel(get()) }
|
viewModel { NavigationViewModel(get()) }
|
||||||
```
|
```
|
||||||
|
|
||||||
**LiveData Observation:**
|
**LiveData observation:**
|
||||||
```kotlin
|
```kotlin
|
||||||
viewModel.route.observe(this) { routeJson ->
|
viewModel.route.observe(this) { routeJson ->
|
||||||
routeModel.startNavigation(routeJson, context)
|
routeModel.startNavigation(routeJson, context)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Step Finding Algorithm:**
|
**Step-finding algorithm:**
|
||||||
RouteModel iterates through all step waypoints, calculates distance to current location, and snaps to the nearest waypoint to determine current step index.
|
`RouteCalculator` iterates the current step's waypoints, calculates distance from the current location to each, and snaps to the nearest waypoint. Snap is rejected if the closest point exceeds `MAXIMAL_SNAP_CORRECTION`; deviation greater than `MAXIMAL_ROUTE_DEVIATION` triggers a reroute.
|
||||||
|
|
||||||
|
**Settings flow:**
|
||||||
|
```kotlin
|
||||||
|
val darkMode by viewModel.darkMode.collectAsStateWithLifecycle()
|
||||||
|
```
|
||||||
|
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
- Valhalla route mapping is incomplete (search for TODO comments in ValhallaRoute.kt)
|
- Valhalla route mapping is incomplete in places (search for TODO comments in `data/valhalla/ValhallaRoute.kt`)
|
||||||
- Rerouting logic exists but needs more testing
|
- Rerouting logic exists but needs more testing
|
||||||
- Speed limit queries via Overpass API could be optimized for performance
|
- Speed-limit queries via Overpass API could be optimized for performance
|
||||||
- TomTom implementation uses local JSON file (R.raw.tomom_routing) instead of live API
|
- TomTom and Tankerkönig clients have DEBUG-only `useLocal` shortcuts that hit fixture URLs (`http://192.168.1.37/...` and `R.raw.tomom_routing`) — these need to be off for any real-network testing
|
||||||
|
- The `automotive` module is wired into Gradle but has no Kotlin source yet
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ val properties = Properties().apply {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.kouros.navigation"
|
namespace = "com.kouros.navigation"
|
||||||
compileSdk = 36
|
compileSdk = 37
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.kouros.navigation"
|
applicationId = "com.kouros.navigation"
|
||||||
minSdk = 33
|
minSdk = 33
|
||||||
targetSdk = 36
|
targetSdk = 37
|
||||||
versionCode = 87
|
versionCode = 131
|
||||||
versionName = "0.2.0.87"
|
versionName = "0.4.0.131"
|
||||||
base.archivesName = "navi-$versionName"
|
base.archivesName = "navi-$versionName"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ android {
|
|||||||
release {
|
release {
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
isShrinkResources = false
|
//isShrinkResources = false
|
||||||
|
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
@@ -101,9 +101,10 @@ dependencies {
|
|||||||
implementation(libs.maplibre.compose)
|
implementation(libs.maplibre.compose)
|
||||||
|
|
||||||
implementation(libs.accompanist.permissions)
|
implementation(libs.accompanist.permissions)
|
||||||
|
|
||||||
implementation(project(":common:car"))
|
implementation(project(":common:car"))
|
||||||
implementation(project(":common:data"))
|
implementation(project(":common:data"))
|
||||||
|
implementation(libs.androidx.car.app)
|
||||||
|
implementation(libs.androidx.app.projected)
|
||||||
implementation(libs.play.services.location)
|
implementation(libs.play.services.location)
|
||||||
implementation(libs.androidx.compose.runtime)
|
implementation(libs.androidx.compose.runtime)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ fun test(applicationContext: Context, routeModel: RouteModel) {
|
|||||||
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
|
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
|
||||||
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
||||||
routeModel.updateLocation(
|
routeModel.updateLocation(
|
||||||
location(waypoint[0], waypoint[1]), navigationViewModel
|
waypoint, navigationViewModel
|
||||||
)
|
)
|
||||||
val step = routeModel.currentStep()
|
val step = routeModel.currentStep()
|
||||||
val nextStep = routeModel.nextStep()
|
val nextStep = routeModel.nextStep()
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
package com.kouros.navigation.ui
|
package com.kouros.navigation.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
@@ -45,7 +40,6 @@ import com.kouros.navigation.MainApplication.Companion.navigationViewModel
|
|||||||
import com.kouros.navigation.car.TextToSpeechManager
|
import com.kouros.navigation.car.TextToSpeechManager
|
||||||
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
||||||
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
|
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
|
||||||
import com.kouros.navigation.data.Constants.TAG
|
|
||||||
import com.kouros.navigation.data.Constants.TILT
|
import com.kouros.navigation.data.Constants.TILT
|
||||||
import com.kouros.navigation.data.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
import com.kouros.navigation.model.BaseStyleModel
|
import com.kouros.navigation.model.BaseStyleModel
|
||||||
@@ -59,7 +53,7 @@ import com.kouros.navigation.ui.navigation.NavigationSheet
|
|||||||
import com.kouros.navigation.ui.search.SearchSheet
|
import com.kouros.navigation.ui.search.SearchSheet
|
||||||
import com.kouros.navigation.ui.theme.NavigationTheme
|
import com.kouros.navigation.ui.theme.NavigationTheme
|
||||||
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||||
import com.kouros.navigation.utils.bearing
|
import com.kouros.navigation.utils.bearingPositive
|
||||||
import com.kouros.navigation.utils.getSettingsRepository
|
import com.kouros.navigation.utils.getSettingsRepository
|
||||||
import com.kouros.navigation.utils.getSettingsViewModel
|
import com.kouros.navigation.utils.getSettingsViewModel
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
@@ -278,7 +272,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
val bearing = if (currentLocation.hasBearing()) {
|
val bearing = if (currentLocation.hasBearing()) {
|
||||||
currentLocation.bearing.toDouble()
|
currentLocation.bearing.toDouble()
|
||||||
} else {
|
} else {
|
||||||
bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
|
bearingPositive(lastLocation, currentLocation, cameraPosition.value!!.bearing)
|
||||||
}
|
}
|
||||||
|
|
||||||
with(routeModel) {
|
with(routeModel) {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ fun MapView(
|
|||||||
duration = 1.seconds
|
duration = 1.seconds
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
NavigationImage(paddingValues, width, height / 6, "", dark)
|
NavigationImage(paddingValues, width, height / 6, "", dark, tilt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,9 +179,9 @@ fun Categories(
|
|||||||
modifier = Modifier.horizontalScroll(scrollState)
|
modifier = Modifier.horizontalScroll(scrollState)
|
||||||
) {
|
) {
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
val places = viewModel.loadRecentPlace(applicationContext)
|
val places = viewModel.loadRecentPlaces(applicationContext)
|
||||||
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
|
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
|
||||||
viewModel.loadRoute(applicationContext, location, toLocation, 0F)
|
viewModel.loadRoute(applicationContext, location, listOf(toLocation), 0F)
|
||||||
closeSheet()
|
closeSheet()
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -243,12 +243,12 @@ private fun SearchPlaces(
|
|||||||
latitude = place.lat.toDouble(),
|
latitude = place.lat.toDouble(),
|
||||||
postalCode = place.address.postcode,
|
postalCode = place.address.postcode,
|
||||||
city = place.address.city,
|
city = place.address.city,
|
||||||
street = place.address.road
|
street = place.address.road,
|
||||||
)
|
)
|
||||||
viewModel.saveRecent(context, pl)
|
viewModel.saveRecent(context, pl)
|
||||||
val toLocation =
|
val toLocation =
|
||||||
location(place.lon.toDouble(), place.lat.toDouble())
|
location(place.lon.toDouble(), place.lat.toDouble())
|
||||||
viewModel.loadRoute(context, location, toLocation, 0F)
|
viewModel.loadRoute(context, location, listOf(toLocation), 0F)
|
||||||
closeSheet()
|
closeSheet()
|
||||||
}
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
@@ -116,9 +116,9 @@ fun Home(
|
|||||||
) {
|
) {
|
||||||
Row(horizontalArrangement = Arrangement.SpaceBetween) {
|
Row(horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
val places = viewModel.loadRecentPlace(applicationContext)
|
val places = viewModel.loadRecentPlaces(applicationContext)
|
||||||
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
|
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
|
||||||
viewModel.loadRoute(applicationContext, location, toLocation, 0F)
|
viewModel.loadRoute(applicationContext, location, listOf(toLocation), 0F)
|
||||||
closeSheet()
|
closeSheet()
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -168,7 +168,7 @@ private fun RecentPlaces(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
val toLocation = location(place.longitude, place.latitude)
|
val toLocation = location(place.longitude, place.latitude)
|
||||||
viewModel.loadRoute(context, location, toLocation, 0F)
|
viewModel.loadRoute(context, location, listOf(toLocation), 0F)
|
||||||
closeSheet()
|
closeSheet()
|
||||||
}
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ plugins {
|
|||||||
android {
|
android {
|
||||||
namespace = "com.kouros.navigation"
|
namespace = "com.kouros.navigation"
|
||||||
compileSdk {
|
compileSdk {
|
||||||
version = release(36)
|
version = release(37)
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -37,7 +37,6 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.app.automotive)
|
implementation(libs.androidx.app.automotive)
|
||||||
implementation(libs.androidx.car.app)
|
|
||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.androidx.runtime.livedata)
|
implementation(libs.androidx.runtime.livedata)
|
||||||
implementation(project(":common:car"))
|
implementation(project(":common:car"))
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960"
|
android:viewportHeight="960"
|
||||||
android:tint="#1A7416">
|
android:tint="#1A7416">
|
||||||
<group android:scaleX="0.7888"
|
<group android:scaleX="0.58"
|
||||||
android:scaleY="0.7888"
|
android:scaleY="0.58"
|
||||||
android:translateX="101.376"
|
android:translateX="201.6"
|
||||||
android:translateY="101.376">
|
android:translateY="201.6">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M319,680L480,607L641,680L656,665L480,240L304,665L319,680ZM480,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,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
android:pathData="M319,680L480,607L641,680L656,665L480,240L304,665L319,680ZM480,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,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.8 KiB |
@@ -6,7 +6,7 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.kouros.android.cars.carappservice"
|
namespace = "com.kouros.android.cars.carappservice"
|
||||||
compileSdk = 36
|
compileSdk = 37
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 33
|
minSdk = 33
|
||||||
@@ -42,7 +42,6 @@ dependencies {
|
|||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.ui)
|
implementation(libs.androidx.ui)
|
||||||
implementation(libs.maplibre.compose)
|
implementation(libs.maplibre.compose)
|
||||||
implementation(libs.androidx.app.projected)
|
|
||||||
implementation(project(":common:data"))
|
implementation(project(":common:data"))
|
||||||
implementation(libs.androidx.runtime.livedata)
|
implementation(libs.androidx.runtime.livedata)
|
||||||
implementation(libs.androidx.compose.foundation)
|
implementation(libs.androidx.compose.foundation)
|
||||||
|
|||||||
@@ -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,26 +2,24 @@ package com.kouros.navigation.car
|
|||||||
|
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import androidx.car.app.navigation.model.Maneuver
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.kouros.data.R
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.kouros.navigation.data.Constants.homeHohenwaldeck
|
import com.kouros.navigation.data.Constants.homeHohenwaldeck
|
||||||
import com.kouros.navigation.data.RouteEngine
|
import com.kouros.navigation.data.RouteEngine
|
||||||
|
import com.kouros.navigation.data.route.ManeuverType
|
||||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import com.kouros.navigation.model.RouteModel
|
import com.kouros.navigation.model.RouteModel
|
||||||
|
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||||
import com.kouros.navigation.utils.getSettingsRepository
|
import com.kouros.navigation.utils.getSettingsRepository
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
import org.junit.Before
|
|
||||||
import kotlin.collections.forEach
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
@@ -33,13 +31,386 @@ class RouteModelTest {
|
|||||||
val routeModel = RouteModel()
|
val routeModel = RouteModel()
|
||||||
val location = Location(LocationManager.GPS_PROVIDER)
|
val location = Location(LocationManager.GPS_PROVIDER)
|
||||||
|
|
||||||
|
val leftDistance = listOf(
|
||||||
|
64.0,
|
||||||
|
0.0,
|
||||||
|
143.7469482421875,
|
||||||
|
111.36382293701172,
|
||||||
|
101.35636901855469,
|
||||||
|
0.0,
|
||||||
|
356.0272216796875,
|
||||||
|
250.2962646484375,
|
||||||
|
245.09136962890625,
|
||||||
|
213.86209106445312,
|
||||||
|
173.69473266601562,
|
||||||
|
90.41661834716797,
|
||||||
|
40.58605194091797,
|
||||||
|
22.74074363708496,
|
||||||
|
12.33098030090332,
|
||||||
|
0.0,
|
||||||
|
1046.3043212890625,
|
||||||
|
1026.2342529296875,
|
||||||
|
1012.8703002929688,
|
||||||
|
979.4789428710938,
|
||||||
|
972.7660522460938,
|
||||||
|
971.6541137695312,
|
||||||
|
914.8672485351562,
|
||||||
|
873.6181030273438,
|
||||||
|
831.3056030273438,
|
||||||
|
781.1799926757812,
|
||||||
|
719.982666015625,
|
||||||
|
653.1173095703125,
|
||||||
|
578.4839477539062,
|
||||||
|
566.2300415039062,
|
||||||
|
490.4866943359375,
|
||||||
|
482.66766357421875,
|
||||||
|
451.45355224609375,
|
||||||
|
443.6345520019531,
|
||||||
|
391.28887939453125,
|
||||||
|
346.71197509765625,
|
||||||
|
316.65283203125,
|
||||||
|
237.5792236328125,
|
||||||
|
141.71861267089844,
|
||||||
|
78.22917175292969,
|
||||||
|
55.94073486328125,
|
||||||
|
41.31438064575195,
|
||||||
|
31.061260223388672,
|
||||||
|
20.943891525268555,
|
||||||
|
16.43439483642578,
|
||||||
|
0.0,
|
||||||
|
346.5630798339844,
|
||||||
|
339.0434875488281,
|
||||||
|
329.3117370605469,
|
||||||
|
321.055908203125,
|
||||||
|
304.0730285644531,
|
||||||
|
285.6786193847656,
|
||||||
|
272.10870361328125,
|
||||||
|
233.37289428710938,
|
||||||
|
213.9094696044922,
|
||||||
|
198.87030029296875,
|
||||||
|
164.74563598632812,
|
||||||
|
136.4317626953125,
|
||||||
|
91.64434051513672,
|
||||||
|
82.11692810058594,
|
||||||
|
60.832786560058594,
|
||||||
|
33.411705017089844,
|
||||||
|
0.0,
|
||||||
|
338.5975341796875,
|
||||||
|
318.1346435546875,
|
||||||
|
211.02232360839844,
|
||||||
|
201.35411071777344,
|
||||||
|
187.75144958496094,
|
||||||
|
179.0858154296875,
|
||||||
|
170.42019653320312,
|
||||||
|
123.4335708618164,
|
||||||
|
90.03872680664062,
|
||||||
|
74.55345916748047,
|
||||||
|
60.95069122314453,
|
||||||
|
39.9212532043457,
|
||||||
|
0.0,
|
||||||
|
314.19586181640625,
|
||||||
|
306.1593017578125,
|
||||||
|
289.4617004394531,
|
||||||
|
223.2646484375,
|
||||||
|
129.7374725341797,
|
||||||
|
76.47386932373047,
|
||||||
|
56.635841369628906,
|
||||||
|
52.30287170410156,
|
||||||
|
33.61341857910156,
|
||||||
|
20.613277435302734,
|
||||||
|
0.0,
|
||||||
|
5621.87744140625,
|
||||||
|
5612.591796875,
|
||||||
|
5587.8486328125,
|
||||||
|
5483.76806640625,
|
||||||
|
5447.275390625,
|
||||||
|
5422.7744140625,
|
||||||
|
5398.21875,
|
||||||
|
5373.927734375,
|
||||||
|
5358.28955078125,
|
||||||
|
5347.1455078125,
|
||||||
|
5339.36181640625,
|
||||||
|
5314.6181640625,
|
||||||
|
5303.107421875,
|
||||||
|
5278.109375,
|
||||||
|
5253.859375,
|
||||||
|
5244.88720703125,
|
||||||
|
5200.0400390625,
|
||||||
|
5151.9326171875,
|
||||||
|
5138.63916015625,
|
||||||
|
5085.87548828125,
|
||||||
|
5054.32080078125,
|
||||||
|
5012.51220703125,
|
||||||
|
4962.390625,
|
||||||
|
4938.140625,
|
||||||
|
4898.97607421875,
|
||||||
|
4862.1416015625,
|
||||||
|
4788.78564453125,
|
||||||
|
4767.880859375,
|
||||||
|
4761.89013671875,
|
||||||
|
4756.5390625,
|
||||||
|
4749.23388671875,
|
||||||
|
4724.98388671875,
|
||||||
|
4645.017578125,
|
||||||
|
4596.826171875,
|
||||||
|
4573.98583984375,
|
||||||
|
4554.85009765625,
|
||||||
|
4521.48388671875,
|
||||||
|
4499.232421875,
|
||||||
|
4484.6064453125,
|
||||||
|
4471.07763671875,
|
||||||
|
4457.54931640625,
|
||||||
|
4415.10986328125,
|
||||||
|
4389.8359375,
|
||||||
|
4359.6376953125,
|
||||||
|
4347.59716796875,
|
||||||
|
4342.62109375,
|
||||||
|
4334.59375,
|
||||||
|
4320.33447265625,
|
||||||
|
4303.408203125,
|
||||||
|
4289.998046875,
|
||||||
|
4261.86962890625,
|
||||||
|
4215.78515625,
|
||||||
|
4207.08251953125,
|
||||||
|
4192.81201171875,
|
||||||
|
4080.384521484375,
|
||||||
|
4065.3955078125,
|
||||||
|
4009.8798828125,
|
||||||
|
3975.99853515625,
|
||||||
|
3971.5283203125,
|
||||||
|
3953.9345703125,
|
||||||
|
3915.152587890625,
|
||||||
|
3877.74365234375,
|
||||||
|
3865.702880859375,
|
||||||
|
3857.67578125,
|
||||||
|
3850.37060546875,
|
||||||
|
3845.019287109375,
|
||||||
|
3843.161865234375,
|
||||||
|
3819.26904296875,
|
||||||
|
3803.01171875,
|
||||||
|
3686.892578125,
|
||||||
|
3661.30908203125,
|
||||||
|
3629.2001953125,
|
||||||
|
3613.607177734375,
|
||||||
|
3591.255859375,
|
||||||
|
3580.988525390625,
|
||||||
|
3573.37060546875,
|
||||||
|
3563.377685546875,
|
||||||
|
3552.08349609375,
|
||||||
|
3537.639404296875,
|
||||||
|
3502.25146484375,
|
||||||
|
3484.319580078125,
|
||||||
|
3445.313720703125,
|
||||||
|
3437.883544921875,
|
||||||
|
3431.69873046875,
|
||||||
|
3413.72021484375,
|
||||||
|
3405.05224609375,
|
||||||
|
3340.725830078125,
|
||||||
|
3312.782470703125,
|
||||||
|
3302.888671875,
|
||||||
|
3292.994873046875,
|
||||||
|
3240.986328125,
|
||||||
|
3229.830322265625,
|
||||||
|
3185.277099609375,
|
||||||
|
3096.74755859375,
|
||||||
|
2981.00146484375,
|
||||||
|
2907.40869140625,
|
||||||
|
2865.943115234375,
|
||||||
|
2859.75830078125,
|
||||||
|
2851.719482421875,
|
||||||
|
2813.998779296875,
|
||||||
|
2778.027587890625,
|
||||||
|
2767.468505859375,
|
||||||
|
2729.052001953125,
|
||||||
|
2694.80712890625,
|
||||||
|
2651.47265625,
|
||||||
|
2613.131591796875,
|
||||||
|
2602.36376953125,
|
||||||
|
2588.665771484375,
|
||||||
|
2549.015380859375,
|
||||||
|
2530.110107421875,
|
||||||
|
2521.1689453125,
|
||||||
|
2510.401123046875,
|
||||||
|
2478.26953125,
|
||||||
|
2437.71337890625,
|
||||||
|
2400.48681640625,
|
||||||
|
2350.667236328125,
|
||||||
|
2320.4267578125,
|
||||||
|
2294.857666015625,
|
||||||
|
2268.75830078125,
|
||||||
|
2260.833740234375,
|
||||||
|
2249.615234375,
|
||||||
|
2229.476318359375,
|
||||||
|
2210.573486328125,
|
||||||
|
2192.78271484375,
|
||||||
|
2184.963623046875,
|
||||||
|
2148.23974609375,
|
||||||
|
2114.8818359375,
|
||||||
|
2105.955078125,
|
||||||
|
2052.49951171875,
|
||||||
|
2013.5179443359375,
|
||||||
|
2006.804931640625,
|
||||||
|
1959.955810546875,
|
||||||
|
1869.44775390625,
|
||||||
|
1787.787109375,
|
||||||
|
1740.873046875,
|
||||||
|
1719.6939697265625,
|
||||||
|
1554.079833984375,
|
||||||
|
1395.2606201171875,
|
||||||
|
1349.453369140625,
|
||||||
|
1296.93359375,
|
||||||
|
1144.57080078125,
|
||||||
|
1030.08203125,
|
||||||
|
972.0895385742188,
|
||||||
|
926.4032592773438,
|
||||||
|
921.95556640625,
|
||||||
|
911.9205322265625,
|
||||||
|
902.9940185546875,
|
||||||
|
887.3560180664062,
|
||||||
|
803.21728515625,
|
||||||
|
774.1542358398438,
|
||||||
|
767.318603515625,
|
||||||
|
762.62841796875,
|
||||||
|
754.703857421875,
|
||||||
|
751.3680419921875,
|
||||||
|
738.9346923828125,
|
||||||
|
725.406005859375,
|
||||||
|
710.6475219726562,
|
||||||
|
668.3836669921875,
|
||||||
|
615.11279296875,
|
||||||
|
605.7323608398438,
|
||||||
|
572.6571044921875,
|
||||||
|
552.6024780273438,
|
||||||
|
545.567138671875,
|
||||||
|
474.7272644042969,
|
||||||
|
459.36187744140625,
|
||||||
|
445.29119873046875,
|
||||||
|
427.3172607421875,
|
||||||
|
406.21124267578125,
|
||||||
|
378.6969909667969,
|
||||||
|
371.8613586425781,
|
||||||
|
362.934814453125,
|
||||||
|
347.29681396484375,
|
||||||
|
329.3663330078125,
|
||||||
|
320.4398193359375,
|
||||||
|
301.3039855957031,
|
||||||
|
237.61085510253906,
|
||||||
|
112.25553131103516,
|
||||||
|
94.07954406738281,
|
||||||
|
75.73666381835938,
|
||||||
|
0.0,
|
||||||
|
1023.4342651367188,
|
||||||
|
1003.37939453125,
|
||||||
|
973.33203125,
|
||||||
|
901.591796875,
|
||||||
|
805.4495849609375,
|
||||||
|
776.5214233398438,
|
||||||
|
723.6364135742188,
|
||||||
|
713.1954956054688,
|
||||||
|
693.5875854492188,
|
||||||
|
646.0399169921875,
|
||||||
|
565.5601196289062,
|
||||||
|
509.7200622558594,
|
||||||
|
484.1024169921875,
|
||||||
|
422.78350830078125,
|
||||||
|
276.693603515625,
|
||||||
|
265.35223388671875,
|
||||||
|
187.42837524414062,
|
||||||
|
160.74212646484375,
|
||||||
|
97.36228942871094,
|
||||||
|
29.534751892089844,
|
||||||
|
18.390586853027344,
|
||||||
|
0.0,
|
||||||
|
462.20318603515625,
|
||||||
|
450.08526611328125,
|
||||||
|
439.66217041015625,
|
||||||
|
397.6007080078125,
|
||||||
|
346.669921875,
|
||||||
|
290.0765380859375,
|
||||||
|
270.7193603515625,
|
||||||
|
208.91537475585938,
|
||||||
|
191.0126495361328,
|
||||||
|
177.56549072265625,
|
||||||
|
165.6016082763672,
|
||||||
|
117.09382629394531,
|
||||||
|
108.90425109863281,
|
||||||
|
75.23577117919922,
|
||||||
|
37.74562072753906,
|
||||||
|
0.0,
|
||||||
|
940.9969482421875,
|
||||||
|
921.5716552734375,
|
||||||
|
891.173583984375,
|
||||||
|
876.2035522460938,
|
||||||
|
865.4982299804688,
|
||||||
|
851.6742553710938,
|
||||||
|
808.6068725585938,
|
||||||
|
793.0664672851562,
|
||||||
|
757.3865356445312,
|
||||||
|
716.390380859375,
|
||||||
|
701.561767578125,
|
||||||
|
696.2328491210938,
|
||||||
|
657.3816528320312,
|
||||||
|
641.1264038085938,
|
||||||
|
584.9859619140625,
|
||||||
|
581.8070678710938,
|
||||||
|
552.8590087890625,
|
||||||
|
473.6039733886719,
|
||||||
|
401.5708923339844,
|
||||||
|
369.44659423828125,
|
||||||
|
360.9601745605469,
|
||||||
|
353.1898498535156,
|
||||||
|
309.3813781738281,
|
||||||
|
269.4887390136719,
|
||||||
|
226.76416015625,
|
||||||
|
141.3342742919922,
|
||||||
|
135.6678466796875,
|
||||||
|
126.8246078491211,
|
||||||
|
94.33628845214844,
|
||||||
|
68.04391479492188,
|
||||||
|
0.0,
|
||||||
|
149.6364288330078,
|
||||||
|
118.28071594238281,
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
|
val distance = listOf(
|
||||||
|
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
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
val repository = getSettingsRepository(appContext)
|
val repository = getSettingsRepository(appContext)
|
||||||
runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) }
|
runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) }
|
||||||
val routeJsonString = TomTomRepository().fetchUrl(
|
val routeJsonString = TomTomRepository().fetchUrl(
|
||||||
"https://kouros-online.de/tomtom_routing.json",
|
"http://192.168.1.37/tomtom_routing.json",
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
assertNotEquals("", routeJsonString)
|
assertNotEquals("", routeJsonString)
|
||||||
@@ -50,8 +421,8 @@ class RouteModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun checkRoute() {
|
fun checkRoute() {
|
||||||
assertEquals(true, routeModel.isNavigating())
|
assertEquals(true, routeModel.isNavigating())
|
||||||
assertEquals(routeModel.curRoute.summary.distance, 11116.0, 10.0)
|
assertEquals(routeModel.curRoute.summary.distance, 11108.0, 10.0)
|
||||||
assertEquals(routeModel.curRoute.summary.duration, 1581.0, 10.0)
|
assertEquals(routeModel.curRoute.summary.duration, 1094.0, 10.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -60,27 +431,38 @@ class RouteModelTest {
|
|||||||
location.longitude = 11.579034
|
location.longitude = 11.579034
|
||||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||||
val stepData = routeModel.currentStep()
|
val stepData = routeModel.currentStep()
|
||||||
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
|
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
|
||||||
assertEquals(stepData.instruction, "Silcherstraße")
|
assertEquals(stepData.instruction, "Silcherstraße")
|
||||||
assertEquals(stepData.leftStepDistance, 20.0, 5.0)
|
assertEquals(stepData.leftStepDistance, 46.0, 5.0)
|
||||||
val nextStepData = routeModel.nextStep()
|
val nextStepData = routeModel.nextStep()
|
||||||
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
|
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
|
||||||
assertEquals(nextStepData.instruction, "Schmalkaldener Straße")
|
assertEquals(nextStepData.instruction, "Schmalkaldener Straße")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkArrival() {
|
||||||
|
location.latitude = 48.116829
|
||||||
|
location.longitude = 11.594309
|
||||||
|
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||||
|
val stepData = routeModel.currentStep()
|
||||||
|
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_DESTINATION_LEFT.value)
|
||||||
|
assertEquals(stepData.instruction, "Hohenwaldeckstraße")
|
||||||
|
assertEquals(stepData.leftDistance, 0.0, 5.0)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun checkSchmalkadener20() {
|
fun checkSchmalkadener20() {
|
||||||
location.latitude = 48.187057
|
location.latitude = 48.186943
|
||||||
location.longitude = 11.576652
|
location.longitude = 11.579195
|
||||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||||
val stepData = routeModel.currentStep()
|
val stepData = routeModel.currentStep()
|
||||||
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
|
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
|
||||||
assertEquals(stepData.instruction, "Schmalkaldener Straße")
|
assertEquals(stepData.instruction, "Ingolstädter Straße")
|
||||||
assertEquals(stepData.leftStepDistance, 0.0, 1.0)
|
assertEquals(stepData.leftStepDistance, 326.0, 1.0)
|
||||||
val nextStepData = routeModel.nextStep()
|
val nextStepData = routeModel.nextStep()
|
||||||
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
|
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
|
||||||
assertEquals(nextStepData.instruction, "Ingolstädter Straße")
|
assertEquals(nextStepData.instruction, "Schenkendorfstraße")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -90,15 +472,15 @@ class RouteModelTest {
|
|||||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||||
val stepData = routeModel.currentStep()
|
val stepData = routeModel.currentStep()
|
||||||
if (routeModel.navState.nextStep) {
|
if (routeModel.navState.nextStep) {
|
||||||
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_STRAIGHT)
|
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_STRAIGHT.value)
|
||||||
assertEquals(stepData.instruction, "Ingolstädter Straße")
|
assertEquals(stepData.instruction, "Ingolstädter Straße")
|
||||||
val nextStepData = routeModel.nextStep()
|
val nextStepData = routeModel.nextStep()
|
||||||
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT)
|
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
|
||||||
assertEquals(nextStepData.instruction, "Schenkendorfstraße")
|
assertEquals(nextStepData.instruction, "Schenkendorfstraße")
|
||||||
} else {
|
} else {
|
||||||
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT)
|
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
|
||||||
}
|
}
|
||||||
assertEquals(stepData.leftStepDistance, 301.0, 1.0)
|
assertEquals(stepData.leftStepDistance, 327.0, 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -108,14 +490,14 @@ class RouteModelTest {
|
|||||||
location.bearing = 180.0F
|
location.bearing = 180.0F
|
||||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||||
val stepData = routeModel.currentStep()
|
val stepData = routeModel.currentStep()
|
||||||
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT)
|
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
|
||||||
assertEquals(stepData.instruction, "Schenkendorfstraße")
|
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.size, 4)
|
||||||
assertEquals(stepData.lane.first().valid, true)
|
assertEquals(stepData.lane.first().valid, true)
|
||||||
assertEquals(stepData.lane.last().valid, false)
|
assertEquals(stepData.lane.last().valid, false)
|
||||||
val nextStepData = routeModel.nextStep()
|
val nextStepData = routeModel.nextStep()
|
||||||
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_KEEP_LEFT)
|
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_KEEP_LEFT.value)
|
||||||
assertEquals(nextStepData.instruction, "Schenkendorfstraße")
|
assertEquals(nextStepData.instruction, "Schenkendorfstraße")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +507,7 @@ class RouteModelTest {
|
|||||||
location.longitude = homeHohenwaldeck.longitude
|
location.longitude = homeHohenwaldeck.longitude
|
||||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||||
val stepData = routeModel.nextStep()
|
val stepData = routeModel.nextStep()
|
||||||
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_DESTINATION_LEFT)
|
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_DESTINATION_LEFT.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -143,11 +525,11 @@ class RouteModelTest {
|
|||||||
if (index in 61..61) {
|
if (index in 61..61) {
|
||||||
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
|
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
|
||||||
val stepData = routeModel.currentStep()
|
val stepData = routeModel.currentStep()
|
||||||
assertEquals(stepData.lane.size, 3)
|
assertEquals(stepData.lane.size, 2)
|
||||||
assertEquals(stepData.lane.first().valid, true)
|
assertEquals(stepData.lane.first().valid, true)
|
||||||
assertEquals(stepData.lane.first().indications.first(), "STRAIGHT")
|
assertEquals(stepData.lane.first().indications.first(), "STRAIGHT")
|
||||||
}
|
}
|
||||||
if (index in 74..74) {
|
if (index in 74..75) {
|
||||||
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
|
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
|
||||||
val stepData = routeModel.currentStep()
|
val stepData = routeModel.currentStep()
|
||||||
assertEquals(stepData.lane.size, 3)
|
assertEquals(stepData.lane.size, 3)
|
||||||
@@ -175,29 +557,24 @@ class RouteModelTest {
|
|||||||
if (routeModel.isNavigating()) {
|
if (routeModel.isNavigating()) {
|
||||||
val curLocation = location(waypoint[0], waypoint[1])
|
val curLocation = location(waypoint[0], waypoint[1])
|
||||||
if (index in 0..routeModel.curRoute.waypoints.size) {
|
if (index in 0..routeModel.curRoute.waypoints.size) {
|
||||||
//runBlocking { delay(1000) }
|
|
||||||
val start = System.currentTimeMillis()
|
|
||||||
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
|
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
|
||||||
val stepData = routeModel.currentStep()
|
val stepData = routeModel.currentStep()
|
||||||
//println("${stepData.instruction} ${System.currentTimeMillis() - start}")
|
assertEquals(stepData.leftStepDistance, leftDistance[index], 1.0)
|
||||||
if (stepData.lane.isNotEmpty()) {
|
|
||||||
println(stepData.street)
|
|
||||||
stepData.lane.forEach {
|
|
||||||
println("${it.indications} ${it.valid}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// val nextData = routeModel.nextStep()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `leftStepDistance Inglolstädter `() {
|
fun `leftStepDistance Ingolstädter `() {
|
||||||
val location: Location = location(11.584578, 48.183653)
|
var location = location(11.584352, 48.186771)
|
||||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||||
val step = routeModel.currentStep()
|
var step = routeModel.currentStep()
|
||||||
assertEquals(step.leftStepDistance, 645.0, 1.0)
|
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
|
@Test
|
||||||
@@ -205,6 +582,66 @@ class RouteModelTest {
|
|||||||
val location: Location = location(11.578911, 48.185565)
|
val location: Location = location(11.578911, 48.185565)
|
||||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||||
val step = routeModel.currentStep()
|
val step = routeModel.currentStep()
|
||||||
assertEquals(step.leftStepDistance, 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())
|
||||||
|
)
|
||||||
|
val stepData = routeModel.currentStep()
|
||||||
|
assertEquals(stepData.leftStepDistance, distance[index - 16], 1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
val end = System.currentTimeMillis() - start
|
||||||
|
println("Time $end")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check leftStepDistance Vogelhart Gpx `() {
|
||||||
|
routeModel.navState = routeModel.navState.copy(routeBearing = 270F)
|
||||||
|
var location = location(11.57927955, 48.18554854)
|
||||||
|
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||||
|
var stepData = routeModel.currentStep()
|
||||||
|
assertEquals(stepData.leftStepDistance, 62.0, 1.0)
|
||||||
|
|
||||||
|
location = location(11.57919950, 48.18556317)
|
||||||
|
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||||
|
stepData = routeModel.currentStep()
|
||||||
|
assertEquals(stepData.leftStepDistance, 58.0, 1.0)
|
||||||
|
|
||||||
|
location = location(11.57906832, 48.18556405)
|
||||||
|
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||||
|
stepData = routeModel.currentStep()
|
||||||
|
assertEquals(stepData.leftStepDistance, 48.0, 1.0)
|
||||||
|
|
||||||
|
routeModel.navState = routeModel.navState.copy(routeBearing = 270F)
|
||||||
|
location = location(11.57875945, 48.18558161)
|
||||||
|
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||||
|
//assertEquals(routeModel.currentStep().leftStepDistance, 0.0, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check deviation `() {
|
||||||
|
val navigationViewModel = NavigationViewModel(TomTomRepository())
|
||||||
|
// Schmalkaldener Straße
|
||||||
|
val firstLocation = location(11.579903, 48.186906)
|
||||||
|
routeModel.updateLocation(firstLocation, navigationViewModel)
|
||||||
|
// Frankfurter Ring
|
||||||
|
val location = location(11.579794, 48.187700)
|
||||||
|
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
|
||||||
|
val distance = snappedLocation.distanceTo(firstLocation)
|
||||||
|
assertEquals(distance.toDouble(), 12.0, 1.0)
|
||||||
|
assertEquals(snappedLocation.latitude, 48.18691500607897, 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,12 +132,12 @@ class CarSensorManager(
|
|||||||
|
|
||||||
val carSensors = carHardwareManager.carSensors
|
val carSensors = carHardwareManager.carSensors
|
||||||
carSensors.addCompassListener(
|
carSensors.addCompassListener(
|
||||||
CarSensors.UPDATE_RATE_NORMAL,
|
CarSensors.UPDATE_RATE_FASTEST,
|
||||||
carContext.mainExecutor,
|
carContext.mainExecutor,
|
||||||
carCompassListener
|
carCompassListener
|
||||||
)
|
)
|
||||||
carSensors.addCarHardwareLocationListener(
|
carSensors.addCarHardwareLocationListener(
|
||||||
CarSensors.UPDATE_RATE_UI,
|
CarSensors.UPDATE_RATE_FASTEST,
|
||||||
carContext.mainExecutor,
|
carContext.mainExecutor,
|
||||||
carLocationListener
|
carLocationListener
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.kouros.navigation.car
|
||||||
|
|
||||||
|
import androidx.car.app.Session
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
abstract class CarSession : Session() {
|
||||||
|
|
||||||
|
abstract fun invalidateNavigationScreen()
|
||||||
|
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ package com.kouros.navigation.car
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.location.Location
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.CarToast
|
import androidx.car.app.CarToast
|
||||||
@@ -27,6 +28,9 @@ import androidx.car.app.model.CarIcon
|
|||||||
import androidx.car.app.model.OnClickListener
|
import androidx.car.app.model.OnClickListener
|
||||||
import androidx.car.app.navigation.model.Trip
|
import androidx.car.app.navigation.model.Trip
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.ViewModelStore
|
import androidx.lifecycle.ViewModelStore
|
||||||
import androidx.lifecycle.ViewModelStoreOwner
|
import androidx.lifecycle.ViewModelStoreOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -34,46 +38,74 @@ import com.kouros.data.R
|
|||||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||||
import com.kouros.navigation.car.screen.NavigationListener
|
import com.kouros.navigation.car.screen.NavigationListener
|
||||||
import com.kouros.navigation.car.screen.NavigationScreen
|
import com.kouros.navigation.car.screen.NavigationScreen
|
||||||
|
import com.kouros.navigation.data.Constants
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
|
import com.kouros.navigation.data.datastore.DataStoreManager.PreferencesKeys.CAR_LOCATION
|
||||||
|
import com.kouros.navigation.data.datastore.dataStore
|
||||||
|
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||||
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/** Session class for the Navigation sample app. */
|
/** Session class for the Navigation sample app. */
|
||||||
internal class ClusterSession : Session(), NavigationListener {
|
internal class ClusterSession : CarSession(), NavigationListener {
|
||||||
var mNavigationScreen: NavigationScreen? = null
|
lateinit var mNavigationScreen: NavigationScreen
|
||||||
|
|
||||||
var mNavigationCarSurface: SurfaceRenderer? = null
|
lateinit var surfaceRenderer: SurfaceRenderer
|
||||||
|
|
||||||
var mSettingsAction: Action? = null
|
|
||||||
|
|
||||||
var routeModel = RouteCarModel()
|
var routeModel = RouteCarModel()
|
||||||
|
|
||||||
lateinit var viewModelStoreOwner: ViewModelStoreOwner
|
lateinit var viewModelStoreOwner: ViewModelStoreOwner
|
||||||
|
|
||||||
|
lateinit var navigationViewModel: NavigationViewModel
|
||||||
|
|
||||||
|
lateinit var deviceLocationManager: DeviceLocationManager
|
||||||
|
|
||||||
|
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
||||||
|
override fun onPause(owner: LifecycleOwner) {
|
||||||
|
Log.d(Constants.TAG, "NavigationSession paused")
|
||||||
|
super.onPause(owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
|
Log.d(Constants.TAG, "NavigationSession resumed")
|
||||||
|
super.onResume(owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
if (::deviceLocationManager.isInitialized) {
|
||||||
|
deviceLocationManager.stopLocationUpdates()
|
||||||
|
}
|
||||||
|
carContext
|
||||||
|
.stopService(
|
||||||
|
Intent(
|
||||||
|
carContext,
|
||||||
|
NavigationNotificationService::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
lifecycle.addObserver(lifecycleObserver)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateScreen(intent: Intent): Screen {
|
override fun onCreateScreen(intent: Intent): Screen {
|
||||||
Log.i(TAG, "In onCreateScreen()")
|
Log.i(TAG, "In onCreateScreen()")
|
||||||
|
|
||||||
setupViewModelStore()
|
setupViewModelStore()
|
||||||
mSettingsAction =
|
|
||||||
Action.Builder()
|
|
||||||
.setIcon(
|
|
||||||
CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
carContext, R.drawable.alt_route_48px
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.setOnClickListener(
|
|
||||||
OnClickListener {})
|
|
||||||
.build()
|
|
||||||
|
|
||||||
mNavigationCarSurface = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner)
|
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
|
||||||
|
navigationViewModel = NavigationViewModel(TomTomRepository())
|
||||||
// mNavigationScreen =
|
mNavigationScreen =
|
||||||
// new NavigationScreen(getCarContext(), mSettingsAction, this, mNavigationCarSurface);
|
NavigationScreen(carContext, surfaceRenderer, this, navigationViewModel)
|
||||||
val action = intent.action
|
val action = intent.action
|
||||||
if (CarContext.ACTION_NAVIGATE == action) {
|
if (CarContext.ACTION_NAVIGATE == action) {
|
||||||
|
Log.i(TAG, "In onCreateScreen() Navigation intent")
|
||||||
CarToast.makeText(
|
CarToast.makeText(
|
||||||
carContext,
|
carContext,
|
||||||
"Navigation intent: " + intent.dataString,
|
"Navigation intent: " + intent.dataString,
|
||||||
@@ -81,8 +113,28 @@ internal class ClusterSession : Session(), NavigationListener {
|
|||||||
)
|
)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
initializeManagers()
|
||||||
|
return mNavigationScreen
|
||||||
|
}
|
||||||
|
|
||||||
return mNavigationScreen!!
|
/**
|
||||||
|
* Initializes managers for rendering, sensors, and location.
|
||||||
|
*/
|
||||||
|
private fun initializeManagers() {
|
||||||
|
deviceLocationManager = DeviceLocationManager(
|
||||||
|
carContext = carContext,
|
||||||
|
lifecycleOwner = this,
|
||||||
|
shouldUseCarLocationFlow = flowOf(false),
|
||||||
|
onLocationUpdate = ::updateLocation,
|
||||||
|
onInitialLocation = { location ->
|
||||||
|
|
||||||
|
})
|
||||||
|
deviceLocationManager.startLocationUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun updateLocation(location: Location) {
|
||||||
|
surfaceRenderer.updateLocation(location, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCarConfigurationChanged(newConfiguration: Configuration) {
|
override fun onCarConfigurationChanged(newConfiguration: Configuration) {
|
||||||
@@ -104,6 +156,10 @@ internal class ClusterSession : Session(), NavigationListener {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun recalcRoute(destination: Place) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG: String = ClusterSession::class.java.getSimpleName()
|
val TAG: String = ClusterSession::class.java.getSimpleName()
|
||||||
}
|
}
|
||||||
@@ -121,4 +177,8 @@ internal class ClusterSession : Session(), NavigationListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun invalidateNavigationScreen() {
|
||||||
|
mNavigationScreen.invalidate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.kouros.navigation.car
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
|
import android.location.LocationListener
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.core.location.LocationListenerCompat
|
import androidx.core.location.LocationListenerCompat
|
||||||
@@ -10,7 +11,12 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.kouros.data.BuildConfig
|
||||||
|
import com.kouros.navigation.data.Constants.homeVogelhart
|
||||||
|
import com.kouros.navigation.data.Constants.widenmayer
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -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.
|
* Starts requesting location updates from device GPS.
|
||||||
* Provides initial location via callback and then starts continuous updates.
|
* Provides initial location via callback and then starts continuous updates.
|
||||||
@@ -69,14 +95,16 @@ class DeviceLocationManager(
|
|||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5f) {
|
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5f) {
|
||||||
if (isListening) return
|
if (isListening) return
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
onInitialLocation(homeVogelhart)
|
||||||
|
onLocationUpdate(homeVogelhart)
|
||||||
|
} else {
|
||||||
// Get and deliver last known location first
|
// Get and deliver last known location first
|
||||||
val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
||||||
if (lastLocation != null) {
|
if (lastLocation != null) {
|
||||||
onInitialLocation(lastLocation)
|
onInitialLocation(lastLocation)
|
||||||
onLocationUpdate(lastLocation)
|
onLocationUpdate(lastLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start continuous location updates
|
// Start continuous location updates
|
||||||
locationManager.requestLocationUpdates(
|
locationManager.requestLocationUpdates(
|
||||||
LocationManager.GPS_PROVIDER,
|
LocationManager.GPS_PROVIDER,
|
||||||
@@ -84,6 +112,7 @@ class DeviceLocationManager(
|
|||||||
minDistanceM,
|
minDistanceM,
|
||||||
locationListener
|
locationListener
|
||||||
)
|
)
|
||||||
|
}
|
||||||
isListening = true
|
isListening = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
package com.kouros.navigation.car
|
package com.kouros.navigation.car
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.location.Location
|
|
||||||
import android.os.Message
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.hardware.CarHardwareManager
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.kouros.navigation.data.Constants.TAG
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class NotificationManager(
|
class NavigationNotificationManager(
|
||||||
private val carContext: CarContext,
|
private val carContext: CarContext,
|
||||||
private val lifecycleOwner: LifecycleOwner,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var notificationServiceStarted = false
|
private var notificationServiceStarted = false
|
||||||
@@ -24,17 +17,8 @@ class NotificationManager(
|
|||||||
private var serviceStarted = false
|
private var serviceStarted = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycleOwner.lifecycleScope.launch {
|
|
||||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.DESTROYED) {
|
|
||||||
if (notificationServiceStarted) {
|
|
||||||
stopNotificationService()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startNotificationService() {
|
fun startNotificationService() {
|
||||||
val intent = Intent(carContext, NavigationNotificationService::class.java)
|
val intent = Intent(carContext, NavigationNotificationService::class.java)
|
||||||
@@ -111,7 +111,7 @@ class NavigationNotificationService : Service() {
|
|||||||
* Initializes the notifications, if needed.
|
* 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
|
* screen. However, the rail widget at the bottom of the screen will show regardless of the
|
||||||
* importance setting.
|
* importance setting.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,18 +4,16 @@ import android.Manifest.permission
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.CarToast
|
import androidx.car.app.CarToast
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
import androidx.car.app.ScreenManager
|
import androidx.car.app.ScreenManager
|
||||||
import androidx.car.app.Session
|
|
||||||
import androidx.car.app.connection.CarConnection
|
import androidx.car.app.connection.CarConnection
|
||||||
import androidx.car.app.model.Distance
|
|
||||||
import androidx.car.app.navigation.NavigationManager
|
import androidx.car.app.navigation.NavigationManager
|
||||||
import androidx.car.app.navigation.NavigationManagerCallback
|
import androidx.car.app.navigation.NavigationManagerCallback
|
||||||
import androidx.car.app.navigation.model.Destination
|
|
||||||
import androidx.car.app.navigation.model.Step
|
|
||||||
import androidx.car.app.navigation.model.Trip
|
import androidx.car.app.navigation.model.Trip
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleObserver
|
||||||
@@ -53,10 +51,12 @@ import com.kouros.navigation.data.tomtom.TomTomRepository
|
|||||||
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import com.kouros.navigation.model.RouteModel
|
import com.kouros.navigation.model.RouteModel
|
||||||
|
import com.kouros.navigation.model.SettingsViewModel
|
||||||
|
import com.kouros.navigation.repository.SettingsRepository
|
||||||
import com.kouros.navigation.utils.GeoUtils
|
import com.kouros.navigation.utils.GeoUtils
|
||||||
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
||||||
import com.kouros.navigation.utils.formattedDistance
|
import com.kouros.navigation.utils.bearingPositive
|
||||||
import com.kouros.navigation.utils.getSettingsRepository
|
import com.kouros.navigation.utils.getSettingsRepository
|
||||||
import com.kouros.navigation.utils.getSettingsViewModel
|
import com.kouros.navigation.utils.getSettingsViewModel
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
@@ -74,7 +74,7 @@ import kotlin.math.absoluteValue
|
|||||||
* car hardware sensors, routing engine selection, and screen navigation.
|
* car hardware sensors, routing engine selection, and screen navigation.
|
||||||
* Implements NavigationScreen.Listener for handling navigation events.
|
* Implements NavigationScreen.Listener for handling navigation events.
|
||||||
*/
|
*/
|
||||||
class NavigationSession : Session(), NavigationListener, NavigationObserverCallback {
|
class NavigationSession : CarSession(), NavigationListener, NavigationObserverCallback {
|
||||||
|
|
||||||
// Flag to enable/disable contact access feature
|
// Flag to enable/disable contact access feature
|
||||||
val useContacts = false
|
val useContacts = false
|
||||||
@@ -100,9 +100,6 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
|
|
||||||
lateinit var textToSpeechManager: TextToSpeechManager
|
lateinit var textToSpeechManager: TextToSpeechManager
|
||||||
|
|
||||||
lateinit var notificationManager: NotificationManager
|
|
||||||
|
|
||||||
|
|
||||||
var autoDriveEnabled = false
|
var autoDriveEnabled = false
|
||||||
|
|
||||||
val simulation = Simulation()
|
val simulation = Simulation()
|
||||||
@@ -116,11 +113,11 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
|
|
||||||
var speedCameras = listOf<Elements>()
|
var speedCameras = listOf<Elements>()
|
||||||
|
|
||||||
var lastRouteDate: LocalDateTime = LocalDateTime.now()
|
private var lastRouteCheckLocation = location(0.0, 0.0)
|
||||||
|
|
||||||
var navigationManagerStarted = false
|
var navigationManagerStarted = false
|
||||||
|
|
||||||
var notificationActive = false
|
var updateLocationIndex = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lifecycle observer for managing session lifecycle events.
|
* Lifecycle observer for managing session lifecycle events.
|
||||||
@@ -128,16 +125,6 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
*/
|
*/
|
||||||
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
||||||
|
|
||||||
override fun onPause(owner: LifecycleOwner) {
|
|
||||||
Log.d(TAG, "NavigationSession paused")
|
|
||||||
super.onPause(owner)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume(owner: LifecycleOwner) {
|
|
||||||
Log.d(TAG, "NavigationSession resumed")
|
|
||||||
super.onResume(owner)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy(owner: LifecycleOwner) {
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
if (::navigationManager.isInitialized) {
|
if (::navigationManager.isInitialized) {
|
||||||
navigationManager.clearNavigationManagerCallback()
|
navigationManager.clearNavigationManagerCallback()
|
||||||
@@ -151,13 +138,6 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
if (::textToSpeechManager.isInitialized) {
|
if (::textToSpeechManager.isInitialized) {
|
||||||
textToSpeechManager.cleanup()
|
textToSpeechManager.cleanup()
|
||||||
}
|
}
|
||||||
carContext
|
|
||||||
.stopService(
|
|
||||||
Intent(
|
|
||||||
carContext,
|
|
||||||
NavigationNotificationService::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Log.i(TAG, "NavigationSession destroyed")
|
Log.i(TAG, "NavigationSession destroyed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,23 +155,12 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
|
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
|
||||||
lateinit var observerManager: NavigationObserverManager
|
lateinit var observerManager: NavigationObserverManager
|
||||||
|
|
||||||
val repository = getSettingsRepository(carContext)
|
lateinit var repository: SettingsRepository
|
||||||
|
|
||||||
val settingsViewModel = getSettingsViewModel(carContext)
|
lateinit var settingsViewModel: SettingsViewModel
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycle.addObserver(lifecycleObserver)
|
lifecycle.addObserver(lifecycleObserver)
|
||||||
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
|
||||||
routingEngine = it
|
|
||||||
})
|
|
||||||
|
|
||||||
repository.trafficFlow.asLiveData().observe(this, Observer {
|
|
||||||
showTraffic = it
|
|
||||||
})
|
|
||||||
repository.distanceModeFlow.asLiveData().observe(this, Observer {
|
|
||||||
distanceMode = it
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,6 +168,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
* Creates appropriate repository based on user selection.
|
* Creates appropriate repository based on user selection.
|
||||||
*/
|
*/
|
||||||
fun onRoutingEngineStateUpdated(routeEngine: Int) {
|
fun onRoutingEngineStateUpdated(routeEngine: Int) {
|
||||||
|
if (!::navigationViewModel.isInitialized || routeEngine != routingEngine) {
|
||||||
navigationViewModel = when (routeEngine) {
|
navigationViewModel = when (routeEngine) {
|
||||||
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
|
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
|
||||||
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
|
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
|
||||||
@@ -207,6 +177,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
observerManager = NavigationObserverManager(navigationViewModel, this)
|
observerManager = NavigationObserverManager(navigationViewModel, this)
|
||||||
observerManager.attachAllObservers(this)
|
observerManager.attachAllObservers(this)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when location permission is granted.
|
* Called when location permission is granted.
|
||||||
@@ -241,10 +212,11 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the initial screen for the session.
|
* Creates the initial screen for the session.
|
||||||
* Sets up ViewModel store, initializes components, checks permissions,
|
* Sets up ViewModel store, initializes settings, components, checks permissions,
|
||||||
* and returns appropriate starting screen.
|
* and returns appropriate starting screen.
|
||||||
*/
|
*/
|
||||||
override fun onCreateScreen(intent: Intent): Screen {
|
override fun onCreateScreen(intent: Intent): Screen {
|
||||||
|
initializeSettings()
|
||||||
setupViewModelStore()
|
setupViewModelStore()
|
||||||
initializeViewModels()
|
initializeViewModels()
|
||||||
initializeManagers()
|
initializeManagers()
|
||||||
@@ -252,6 +224,26 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
return checkPermissionsAndGetScreen()
|
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.
|
* Sets up ViewModelStoreOwner and manages its lifecycle.
|
||||||
*/
|
*/
|
||||||
@@ -280,6 +272,10 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
routeModel = RouteCarModel()
|
routeModel = RouteCarModel()
|
||||||
|
|
||||||
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
|
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
|
||||||
|
|
||||||
|
navigationViewModel.initialSnapLocation.observe(this, Observer {
|
||||||
|
surfaceRenderer.updateLocation(it, "")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -306,7 +302,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner)
|
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
|
||||||
|
|
||||||
carSensorManager = CarSensorManager(
|
carSensorManager = CarSensorManager(
|
||||||
carContext = carContext,
|
carContext = carContext,
|
||||||
@@ -322,6 +318,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(),
|
shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(),
|
||||||
onLocationUpdate = ::updateLocation,
|
onLocationUpdate = ::updateLocation,
|
||||||
onInitialLocation = { location ->
|
onInitialLocation = { location ->
|
||||||
|
navigationViewModel.loadCurrentLocation(location)
|
||||||
navigationViewModel.loadRecentPlaces(
|
navigationViewModel.loadRecentPlaces(
|
||||||
carContext,
|
carContext,
|
||||||
location,
|
location,
|
||||||
@@ -330,13 +327,10 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
textToSpeechManager = TextToSpeechManager(carContext)
|
textToSpeechManager = TextToSpeechManager(carContext)
|
||||||
val repository = getSettingsRepository(carContext)
|
|
||||||
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
|
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
|
||||||
guidanceAudio = it
|
guidanceAudio = it
|
||||||
})
|
})
|
||||||
notificationManager = NotificationManager(carContext, this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -378,6 +372,10 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
screenManager.push(navigationScreen)
|
screenManager.push(navigationScreen)
|
||||||
return RequestPermissionScreen(
|
return RequestPermissionScreen(
|
||||||
carContext,
|
carContext,
|
||||||
|
listOf(
|
||||||
|
permission.ACCESS_COARSE_LOCATION,
|
||||||
|
permission.ACCESS_FINE_LOCATION,
|
||||||
|
),
|
||||||
permissionCheckCallback = { screenManager.pop() }
|
permissionCheckCallback = { screenManager.pop() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -407,7 +405,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
screenManager.popToRoot()
|
screenManager.popToRoot()
|
||||||
screenManager.pushForResult(
|
screenManager.pushForResult(
|
||||||
SearchScreen(carContext, surfaceRenderer, navigationViewModel, mutableListOf())
|
SearchScreen(carContext, surfaceRenderer, navigationViewModel, mutableListOf())
|
||||||
) { result ->
|
) { _ ->
|
||||||
// Handle search result if needed
|
// Handle search result if needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,6 +446,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
|
checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
|
||||||
surfaceRenderer.updateLocation(location, streetName)
|
surfaceRenderer.updateLocation(location, streetName)
|
||||||
}
|
}
|
||||||
|
updateLocationIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -460,38 +459,30 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles location updates during active navigation.
|
* Checks if the location deviation is acceptable.
|
||||||
* Snaps location to route and checks for deviation requiring reroute.
|
|
||||||
*/
|
*/
|
||||||
private fun handleNavigationLocation(location: Location) {
|
private fun checkLocationDeviation(
|
||||||
routeModel.updateLocation(location, navigationViewModel)
|
location: Location,
|
||||||
if (routeModel.navState.arrived) return
|
snappedLocation: Location,
|
||||||
if (guidanceAudio == 1) {
|
streetName: String
|
||||||
handleGuidanceAudio()
|
): 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)
|
|
||||||
|
|
||||||
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) {
|
|
||||||
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
|
|
||||||
val distance = location.distanceTo(snappedLocation)
|
val distance = location.distanceTo(snappedLocation)
|
||||||
|
Log.d(TAG, "Distance: $distance $maximalRouteDeviation $speed")
|
||||||
when {
|
when {
|
||||||
distance > MAXIMAL_ROUTE_DEVIATION -> {
|
distance > maximalRouteDeviation -> {
|
||||||
stopNavigation()
|
stopNavigation()
|
||||||
navigationScreen.calculateNewRoute(routeModel.navState.destination)
|
navigationScreen.calculateNewRoute(routeModel.navState.destination, distance)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
distance < MAXIMAL_SNAP_CORRECTION -> {
|
distance < MAXIMAL_SNAP_CORRECTION -> {
|
||||||
@@ -502,6 +493,32 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
surfaceRenderer.updateLocation(location, streetName)
|
surfaceRenderer.updateLocation(location, streetName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -509,59 +526,41 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
*/
|
*/
|
||||||
fun updateNavigationScreen() {
|
fun updateNavigationScreen() {
|
||||||
if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty()
|
if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty()
|
||||||
&& routeModel.navState.destination.street.isEmpty()) {
|
&& routeModel.navState.destination.street.isEmpty()
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val travelEstimateTrip = routeModel.travelEstimateTrip(carContext, distanceMode)
|
|
||||||
val travelEstimateStep = routeModel.travelEstimateStep(carContext, distanceMode)
|
|
||||||
val steps = mutableListOf<Step>()
|
|
||||||
val destination = Destination.Builder()
|
|
||||||
.setName(routeModel.navState.destination.name)
|
|
||||||
.setAddress(routeModel.navState.destination.street)
|
|
||||||
.build()
|
|
||||||
val distance =
|
|
||||||
formattedDistance(0, routeModel.routeCalculator.leftStepDistance())
|
|
||||||
steps.add(routeModel.currentStep(carContext))
|
|
||||||
if (routeModel.navState.nextStep) {
|
|
||||||
steps.add(routeModel.nextStep(carContext = carContext))
|
|
||||||
|
|
||||||
}
|
val stepData = routeModel.currentStep()
|
||||||
|
|
||||||
navigationScreen.updateTrip(
|
navigationScreen.updateTrip(
|
||||||
isNavigating = routeModel.isNavigating(),
|
isNavigating = routeModel.isNavigating(),
|
||||||
isRerouting = false,
|
isRerouting = false,
|
||||||
hasArrived = routeModel.isArrival(),
|
hasArrived = routeModel.isManeuverArrival(),
|
||||||
destinationTravelEstimate = travelEstimateTrip,
|
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext),
|
||||||
stepTravelEstimate = travelEstimateStep,
|
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext),
|
||||||
destinations = mutableListOf(destination),
|
destinations = mutableListOf(routeModel.getDestination()),
|
||||||
steps = steps,
|
steps = routeModel.getSteps(carContext),
|
||||||
stepRemainingDistance = Distance.create(distance.first, distance.second),
|
stepRemainingDistance = routeModel.getDistance(),
|
||||||
shouldShowNextStep = false,
|
shouldShowNextStep = false,
|
||||||
shouldShowLanes = true,
|
shouldShowLanes = true,
|
||||||
junctionImage = null,
|
junctionImage = null,
|
||||||
backGroundColor = routeModel.backGroundColor()
|
backGroundColor = routeModel.backGroundColor(),
|
||||||
|
message = stepData.message,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the trip information and notifies the listener with a new Trip object.
|
* Updates the trip information and notifies the listener with a new Trip object.
|
||||||
* This includes destination name, address, travel estimate, and loading status.
|
* This includes destination name, address, travel estimate, and loading status.
|
||||||
*/
|
*/
|
||||||
|
updateTrip(routeModel.getTrip(carContext))
|
||||||
val tripBuilder = Trip.Builder()
|
|
||||||
tripBuilder.addDestination(
|
|
||||||
destination,
|
|
||||||
travelEstimateTrip
|
|
||||||
)
|
|
||||||
tripBuilder.setLoading(false)
|
|
||||||
tripBuilder.setCurrentRoad(destination.name.toString())
|
|
||||||
tripBuilder.addStep(steps.first(), travelEstimateStep)
|
|
||||||
updateTrip(tripBuilder.build())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for arrival
|
* Checks for arrival
|
||||||
*/
|
*/
|
||||||
fun checkArrival() {
|
fun checkArrival() {
|
||||||
if (routeModel.isArrival()
|
if (routeModel.isManeuverArrival()
|
||||||
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
||||||
) {
|
) {
|
||||||
stopNavigation()
|
stopNavigation()
|
||||||
@@ -578,6 +577,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
* Called when user starts navigation
|
* Called when user starts navigation
|
||||||
*/
|
*/
|
||||||
override fun startNavigation() {
|
override fun startNavigation() {
|
||||||
|
surfaceRenderer.navigation = true
|
||||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||||
navigationManager.navigationStarted()
|
navigationManager.navigationStarted()
|
||||||
navigationManagerStarted = true
|
navigationManagerStarted = true
|
||||||
@@ -588,8 +588,6 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
updateLocation(location)
|
updateLocation(location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (notificationActive)
|
|
||||||
notificationManager.startNotificationService()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -597,6 +595,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
* Called when user exits navigation or arrives at destination.
|
* Called when user exits navigation or arrives at destination.
|
||||||
*/
|
*/
|
||||||
override fun stopNavigation() {
|
override fun stopNavigation() {
|
||||||
|
surfaceRenderer.navigation = false
|
||||||
routeModel.stopNavigation()
|
routeModel.stopNavigation()
|
||||||
navigationManager.navigationEnded()
|
navigationManager.navigationEnded()
|
||||||
if (autoDriveEnabled) {
|
if (autoDriveEnabled) {
|
||||||
@@ -607,8 +606,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
lastCameraSearch = 0
|
lastCameraSearch = 0
|
||||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||||
navigationScreen.navigationType = NavigationType.VIEW
|
navigationScreen.navigationType = NavigationType.VIEW
|
||||||
if (notificationActive)
|
navigationScreen.invalidate()
|
||||||
notificationManager.stopNotificationService()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateTrip(trip: Trip) {
|
override fun updateTrip(trip: Trip) {
|
||||||
@@ -617,6 +615,19 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.cameraPosition.value!!.bearing.toFloat()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle guidance audio
|
* Handle guidance audio
|
||||||
* Called when user wants to hear the step-by-step instructions
|
* Called when user wants to hear the step-by-step instructions
|
||||||
@@ -627,9 +638,6 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
|
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
|
||||||
textToSpeechManager.speak(stepData.message)
|
textToSpeechManager.speak(stepData.message)
|
||||||
lastStepIndex = currentStep.index
|
lastStepIndex = currentStep.index
|
||||||
if (notificationActive) {
|
|
||||||
notificationManager.sendMessage(stepData.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,9 +678,11 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
val newRouteModel = RouteModel()
|
val newRouteModel = RouteModel()
|
||||||
newRouteModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
|
newRouteModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
|
||||||
newRouteModel.startNavigation(route)
|
newRouteModel.startNavigation(route)
|
||||||
routeModel.curRoute.summary.trafficDelay = newRouteModel.curRoute.summary.trafficDelay
|
if ((routeModel.curRoute.summary.trafficDelay - newRouteModel.curRoute.summary.trafficDelay).absoluteValue > 300) {
|
||||||
|
routeModel.startNavigation(route)
|
||||||
updateNavigationScreen()
|
updateNavigationScreen()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun isNavigating(): Boolean = routeModel.isNavigating()
|
override fun isNavigating(): Boolean = routeModel.isNavigating()
|
||||||
@@ -704,8 +714,10 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
cameras.forEach {
|
cameras.forEach {
|
||||||
coordinates.add(listOf(it.lon, it.lat))
|
coordinates.add(listOf(it.lon, it.lat))
|
||||||
}
|
}
|
||||||
|
synchronized(this) {
|
||||||
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
|
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
|
||||||
surfaceRenderer.speedCamerasData.value = speedData
|
surfaceRenderer.speedCameraData.value = speedData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -719,13 +731,30 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
navigationScreen.invalidate()
|
navigationScreen.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTrafficMessageReceived(trafficMessage: String) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a route to the specified place and sets it as the destination.
|
* Loads a route to the specified place and sets it as the destination.
|
||||||
*/
|
*/
|
||||||
override fun navigateToPlace(place: Place) {
|
override fun navigateToPlace(place: Place) {
|
||||||
|
var prevDestination = Place()
|
||||||
|
if (surfaceRenderer.navigation) {
|
||||||
|
prevDestination = routeModel.navState.destination
|
||||||
|
stopNavigation()
|
||||||
|
}
|
||||||
val preview = place.route
|
val preview = place.route
|
||||||
navigationViewModel.previewRoute.value = ""
|
navigationViewModel.previewRoute.value = ""
|
||||||
val location = location(place.longitude, place.latitude)
|
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)
|
navigationViewModel.saveRecent(carContext, place)
|
||||||
routeModel.navState = routeModel.navState.copy(destination = place)
|
routeModel.navState = routeModel.navState.copy(destination = place)
|
||||||
if (preview.isEmpty()) {
|
if (preview.isEmpty()) {
|
||||||
@@ -749,7 +778,11 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
val duration = Duration.between(current, lastTrafficDate)
|
val duration = Duration.between(current, lastTrafficDate)
|
||||||
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
|
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
|
||||||
lastTrafficDate = current
|
lastTrafficDate = current
|
||||||
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
|
navigationViewModel.loadTraffic(
|
||||||
|
carContext,
|
||||||
|
location,
|
||||||
|
surfaceRenderer.carOrientation
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,12 +790,13 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
* Periodically requests speed camera information near the current location.
|
* Periodically requests speed camera information near the current location.
|
||||||
*/
|
*/
|
||||||
private fun updateSpeedCamera(location: Location) {
|
private fun updateSpeedCamera(location: Location) {
|
||||||
if (lastCameraSearch++ % 100 == 0) {
|
if (lastCameraSearch % 200 == 0) {
|
||||||
navigationViewModel.getSpeedCameras(location, 5.0)
|
navigationViewModel.getSpeedCameras(location, 5.0)
|
||||||
}
|
}
|
||||||
if (speedCameras.isNotEmpty()) {
|
if (speedCameras.isNotEmpty() && lastCameraSearch % 15 == 0) {
|
||||||
updateDistance(location)
|
updateDistance(location)
|
||||||
}
|
}
|
||||||
|
lastCameraSearch++
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -771,6 +805,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
private fun updateDistance(
|
private fun updateDistance(
|
||||||
location: Location,
|
location: Location,
|
||||||
) {
|
) {
|
||||||
|
synchronized(this) {
|
||||||
val updatedCameras = mutableListOf<Elements>()
|
val updatedCameras = mutableListOf<Elements>()
|
||||||
speedCameras.forEach {
|
speedCameras.forEach {
|
||||||
val plLocation =
|
val plLocation =
|
||||||
@@ -781,31 +816,35 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
}
|
}
|
||||||
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
|
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
|
||||||
val camera = sortedList.firstOrNull() ?: return
|
val camera = sortedList.firstOrNull() ?: return
|
||||||
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
|
val bearingRoute = surfaceRenderer.lastLocation.bearingPositive(location)
|
||||||
val bearingSpeedCamera = if (camera.tags.direction != null) {
|
val bearingSpeedCamera = try {
|
||||||
try {
|
camera.tags.direction.toFloat()
|
||||||
camera.tags.direction!!.toFloat()
|
} catch (_: Exception) {
|
||||||
} catch (e: Exception) {
|
|
||||||
0F
|
0F
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
|
|
||||||
}
|
|
||||||
if (camera.distance < 80) {
|
if (camera.distance < 80) {
|
||||||
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
|
if ((bearingSpeedCamera - bearingRoute).absoluteValue < 15.0) {
|
||||||
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
|
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
|
||||||
|
val cameras = mutableListOf<Elements>()
|
||||||
|
speedCameras.forEach {
|
||||||
|
if (!(camera.lon == it.lon && camera.lat == it.lat)) {
|
||||||
|
cameras.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
speedCameras = cameras
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a new route is needed based on the time since the last update.
|
* Checks if a new route is needed based on the time since the last update.
|
||||||
*/
|
*/
|
||||||
private fun checkRoute(currentDate: LocalDateTime, location: Location) {
|
private fun checkRoute(location: Location) {
|
||||||
val duration = Duration.between(currentDate, lastRouteDate)
|
val distance = location.distanceTo(lastRouteCheckLocation)
|
||||||
val routeUpdate = routeModel.curRoute.summary.duration / 4
|
if (distance > checkDistance(routeModel)) {
|
||||||
if (duration.abs().seconds > routeUpdate) {
|
if (lastRouteCheckLocation.latitude != 0.0) {
|
||||||
lastRouteDate = currentDate
|
|
||||||
val destination = location(
|
val destination = location(
|
||||||
routeModel.navState.destination.longitude,
|
routeModel.navState.destination.longitude,
|
||||||
routeModel.navState.destination.latitude
|
routeModel.navState.destination.latitude
|
||||||
@@ -813,10 +852,26 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
navigationViewModel.loadRoute(
|
navigationViewModel.loadRoute(
|
||||||
carContext,
|
carContext,
|
||||||
location,
|
location,
|
||||||
destination,
|
listOf(destination),
|
||||||
surfaceRenderer.carOrientation
|
surfaceRenderer.carOrientation
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
lastRouteCheckLocation = location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkDistance(routeModel: RouteModel): Double {
|
||||||
|
val factor = when (routeModel.curRoute.summary.distance) {
|
||||||
|
in 0.0..20000.0 -> 4
|
||||||
|
in 20000.0..100000.0 -> 5
|
||||||
|
in 10000.0..200000.0 -> 6
|
||||||
|
else -> 7
|
||||||
|
}
|
||||||
|
return (routeModel.curRoute.summary.distance / factor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidateNavigationScreen() {
|
||||||
|
navigationScreen.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -10,17 +10,12 @@ import androidx.car.app.AppManager
|
|||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.SurfaceCallback
|
import androidx.car.app.SurfaceCallback
|
||||||
import androidx.car.app.SurfaceContainer
|
import androidx.car.app.SurfaceContainer
|
||||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@@ -36,26 +31,25 @@ import com.kouros.navigation.car.map.getPaddingValues
|
|||||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||||
import com.kouros.navigation.data.Constants.TAG
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
import com.kouros.navigation.data.Constants.TILT
|
import com.kouros.navigation.data.Constants.TILT
|
||||||
import com.kouros.navigation.data.Constants.homeVogelhart
|
import com.kouros.navigation.data.DarkMode
|
||||||
import com.kouros.navigation.data.RouteEngine
|
|
||||||
import com.kouros.navigation.data.ViewStyle
|
import com.kouros.navigation.data.ViewStyle
|
||||||
import com.kouros.navigation.model.BaseStyleModel
|
import com.kouros.navigation.model.BaseStyleModel
|
||||||
import com.kouros.navigation.utils.bearing
|
import com.kouros.navigation.utils.bearingPositive
|
||||||
import com.kouros.navigation.utils.calculateTilt
|
import com.kouros.navigation.utils.calculateTilt
|
||||||
import com.kouros.navigation.utils.calculateZoom
|
import com.kouros.navigation.utils.calculateZoom
|
||||||
import com.kouros.navigation.utils.duration
|
import com.kouros.navigation.utils.duration
|
||||||
import com.kouros.navigation.utils.getSettingsRepository
|
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
import com.kouros.navigation.utils.previewZoom
|
import com.kouros.navigation.utils.previewZoom
|
||||||
import com.kouros.navigation.utils.settingsViewModel
|
import com.kouros.navigation.utils.settingsViewModel
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.maplibre.compose.camera.CameraPosition
|
import org.maplibre.compose.camera.CameraPosition
|
||||||
import org.maplibre.compose.camera.CameraState
|
import org.maplibre.compose.camera.CameraState
|
||||||
import org.maplibre.compose.style.BaseStyle
|
import org.maplibre.compose.style.BaseStyle
|
||||||
import org.maplibre.spatialk.geojson.Position
|
import org.maplibre.spatialk.geojson.Position
|
||||||
import java.time.Duration
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,9 +59,9 @@ import java.time.LocalDateTime
|
|||||||
*/
|
*/
|
||||||
class SurfaceRenderer(
|
class SurfaceRenderer(
|
||||||
private var carContext: CarContext,
|
private var carContext: CarContext,
|
||||||
private var lifecycle: Lifecycle,
|
lifecycle: Lifecycle,
|
||||||
//private var routeModel: RouteCarModel,
|
private var viewModelStoreOwner: ViewModelStoreOwner,
|
||||||
private var viewModelStoreOwner: ViewModelStoreOwner
|
private var navigationSession: CarSession
|
||||||
) : DefaultLifecycleObserver {
|
) : DefaultLifecycleObserver {
|
||||||
|
|
||||||
// Last known location for bearing calculations
|
// Last known location for bearing calculations
|
||||||
@@ -80,10 +74,6 @@ class SurfaceRenderer(
|
|||||||
val cameraPosition = MutableLiveData(
|
val cameraPosition = MutableLiveData(
|
||||||
CameraPosition(
|
CameraPosition(
|
||||||
zoom = 16.0,
|
zoom = 16.0,
|
||||||
target = Position(
|
|
||||||
latitude = homeVogelhart.latitude,
|
|
||||||
longitude = homeVogelhart.longitude
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,7 +99,7 @@ class SurfaceRenderer(
|
|||||||
val trafficData = MutableLiveData(emptyMap<String, String>())
|
val trafficData = MutableLiveData(emptyMap<String, String>())
|
||||||
|
|
||||||
// Speed camera locations as GeoJSON
|
// Speed camera locations as GeoJSON
|
||||||
val speedCamerasData = MutableLiveData("")
|
val speedCameraData = MutableLiveData("")
|
||||||
|
|
||||||
// Current speed in km/h
|
// Current speed in km/h
|
||||||
val speed = MutableLiveData(0F)
|
val speed = MutableLiveData(0F)
|
||||||
@@ -123,6 +113,9 @@ class SurfaceRenderer(
|
|||||||
// Current view mode (navigation, preview, etc.)
|
// Current view mode (navigation, preview, etc.)
|
||||||
var viewStyle = ViewStyle.VIEW
|
var viewStyle = ViewStyle.VIEW
|
||||||
|
|
||||||
|
// Flag to indicate if in navigation mode
|
||||||
|
var navigation = false
|
||||||
|
|
||||||
// Center location for route preview
|
// Center location for route preview
|
||||||
lateinit var centerLocation: Location
|
lateinit var centerLocation: Location
|
||||||
|
|
||||||
@@ -166,15 +159,13 @@ class SurfaceRenderer(
|
|||||||
Log.i(TAG, "Surface available $surfaceContainer")
|
Log.i(TAG, "Surface available $surfaceContainer")
|
||||||
lifecycleOwner = CustomLifecycleOwner()
|
lifecycleOwner = CustomLifecycleOwner()
|
||||||
lifecycleOwner.performRestore(null)
|
lifecycleOwner.performRestore(null)
|
||||||
// technically, we only really need any one of these instead of all 3
|
|
||||||
// i add them to be consistent with the actual lifecycle.
|
|
||||||
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||||
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||||
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||||
|
|
||||||
virtualDisplay = carContext.getSystemService(DisplayManager::class.java)
|
virtualDisplay = carContext.getSystemService(DisplayManager::class.java)
|
||||||
.createVirtualDisplay(
|
.createVirtualDisplay(
|
||||||
"Maps",
|
"Navigation",
|
||||||
surfaceContainer.width,
|
surfaceContainer.width,
|
||||||
surfaceContainer.height,
|
surfaceContainer.height,
|
||||||
surfaceContainer.dpi,
|
surfaceContainer.dpi,
|
||||||
@@ -234,10 +225,44 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when user scrolls the map (not currently implemented).
|
* Called when user scrolls the map .
|
||||||
*/
|
*/
|
||||||
override fun onScroll(distanceX: Float, distanceY: Float) {
|
override fun onScroll(distanceX: Float, distanceY: Float) {
|
||||||
synchronized(this@SurfaceRenderer) {
|
synchronized(this@SurfaceRenderer) {
|
||||||
|
val currentCamera = cameraPosition.value ?: return@synchronized
|
||||||
|
val zoom = currentCamera.zoom
|
||||||
|
val bearing = currentCamera.bearing
|
||||||
|
|
||||||
|
// MapLibre typically uses 512px tiles.
|
||||||
|
// At zoom level z, the world (360 degrees) is 512 * 2^z pixels wide.
|
||||||
|
val pixelsPerDegreeLon = (512.0 * 2.0.pow(zoom)) / 360.0
|
||||||
|
|
||||||
|
// Latitude correction: In Mercator, the vertical scale is stretched by 1/cos(lat).
|
||||||
|
val latRad = lastLocation.latitude * PI / 180.0
|
||||||
|
val pixelsPerDegreeLat = pixelsPerDegreeLon / cos(latRad)
|
||||||
|
|
||||||
|
// Rotation compensation (bearing is degrees clockwise from North)
|
||||||
|
val bearingRad = bearing * PI / 180.0
|
||||||
|
val cosB = cos(bearingRad)
|
||||||
|
val sinB = sin(bearingRad)
|
||||||
|
|
||||||
|
// Rotate screen-space scroll to map-space scroll
|
||||||
|
val rotatedDx = distanceX * cosB - distanceY * sinB
|
||||||
|
val rotatedDy = distanceX * sinB + distanceY * cosB
|
||||||
|
|
||||||
|
viewStyle = ViewStyle.PAN_VIEW
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
bearing = bearing,
|
||||||
|
zoom = zoom,
|
||||||
|
target = pos
|
||||||
|
)
|
||||||
|
navigationSession.invalidateNavigationScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +270,8 @@ class SurfaceRenderer(
|
|||||||
* Called when user scales (zooms) the map (not currently implemented).
|
* Called when user scales (zooms) the map (not currently implemented).
|
||||||
*/
|
*/
|
||||||
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
|
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
|
||||||
|
synchronized(this@SurfaceRenderer) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,10 +280,6 @@ class SurfaceRenderer(
|
|||||||
speed.value = 0F
|
speed.value = 0F
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBaseStyleStateUpdated(style: BaseStyle) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable function that renders the map and navigation UI.
|
* Composable function that renders the map and navigation UI.
|
||||||
* Observes various LiveData sources and updates the map accordingly.
|
* Observes various LiveData sources and updates the map accordingly.
|
||||||
@@ -271,11 +293,12 @@ class SurfaceRenderer(
|
|||||||
val position: CameraPosition? by cameraPosition.observeAsState()
|
val position: CameraPosition? by cameraPosition.observeAsState()
|
||||||
val route: String? by routeData.observeAsState()
|
val route: String? by routeData.observeAsState()
|
||||||
val traffic: Map<String, String>? by trafficData.observeAsState()
|
val traffic: Map<String, String>? by trafficData.observeAsState()
|
||||||
val speedCameras: String? by speedCamerasData.observeAsState()
|
val speedCamera: String? by speedCameraData.observeAsState()
|
||||||
val paddingValues = getPaddingValues(height, viewStyle)
|
val paddingValues = getPaddingValues(height, viewStyle)
|
||||||
val cameraState = cameraState(paddingValues, position, tilt)
|
val cameraState = cameraState(paddingValues, position, tilt)
|
||||||
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
|
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
|
||||||
val dark = darkMode == 1 || darkMode == 2 && carContext.isDarkMode
|
val dark = darkMode == DarkMode.DARK.ordinal
|
||||||
|
|| (darkMode == DarkMode.USE_CAR.ordinal && carContext.isDarkMode)
|
||||||
|
|
||||||
MapLibre(
|
MapLibre(
|
||||||
cameraState,
|
cameraState,
|
||||||
@@ -283,7 +306,7 @@ class SurfaceRenderer(
|
|||||||
route,
|
route,
|
||||||
traffic,
|
traffic,
|
||||||
viewStyle,
|
viewStyle,
|
||||||
speedCameras,
|
speedCamera,
|
||||||
showBuildings
|
showBuildings
|
||||||
)
|
)
|
||||||
ShowPosition(cameraState, position, paddingValues, dark)
|
ShowPosition(cameraState, position, paddingValues, dark)
|
||||||
@@ -302,7 +325,7 @@ class SurfaceRenderer(
|
|||||||
) {
|
) {
|
||||||
val cameraDuration =
|
val cameraDuration =
|
||||||
duration(
|
duration(
|
||||||
viewStyle == ViewStyle.PREVIEW,
|
viewStyle,
|
||||||
position!!.bearing,
|
position!!.bearing,
|
||||||
lastBearing,
|
lastBearing,
|
||||||
lastLocationUpdate
|
lastLocationUpdate
|
||||||
@@ -318,7 +341,8 @@ class SurfaceRenderer(
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
streetName,
|
streetName,
|
||||||
darkMode
|
darkMode,
|
||||||
|
tilt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LaunchedEffect(position, viewStyle) {
|
LaunchedEffect(position, viewStyle) {
|
||||||
@@ -337,7 +361,6 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(owner: LifecycleOwner) {
|
override fun onCreate(owner: LifecycleOwner) {
|
||||||
style.observe(owner, ::onBaseStyleStateUpdated)
|
|
||||||
Log.i(TAG, "SurfaceRenderer created")
|
Log.i(TAG, "SurfaceRenderer created")
|
||||||
carContext.getCarService(AppManager::class.java)
|
carContext.getCarService(AppManager::class.java)
|
||||||
.setSurfaceCallback(mSurfaceCallback)
|
.setSurfaceCallback(mSurfaceCallback)
|
||||||
@@ -353,13 +376,11 @@ class SurfaceRenderer(
|
|||||||
viewStyle = ViewStyle.PAN_VIEW
|
viewStyle = ViewStyle.PAN_VIEW
|
||||||
}
|
}
|
||||||
val newZoom = if (zoomSign < 0) {
|
val newZoom = if (zoomSign < 0) {
|
||||||
cameraPosition.value!!.zoom - 1.0
|
cameraPosition.value!!.zoom - 1
|
||||||
} else {
|
} else {
|
||||||
cameraPosition.value!!.zoom + 1.0
|
cameraPosition.value!!.zoom + 1
|
||||||
}
|
|
||||||
if (viewStyle == ViewStyle.VIEW) {
|
|
||||||
tilt = calculateTilt(newZoom, tilt)
|
|
||||||
}
|
}
|
||||||
|
tilt = calculateTilt(viewStyle, newZoom, tilt)
|
||||||
updateCameraPosition(
|
updateCameraPosition(
|
||||||
cameraPosition.value!!.bearing,
|
cameraPosition.value!!.bearing,
|
||||||
newZoom,
|
newZoom,
|
||||||
@@ -377,19 +398,23 @@ class SurfaceRenderer(
|
|||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
street.value = streetName
|
street.value = streetName
|
||||||
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
||||||
val bearing = if (carOrientation == 999F) {
|
//val bearing =
|
||||||
if (location.hasBearing()) {
|
// location.bearing.toDouble()
|
||||||
|
//carOrientation = bearing.toFloat()
|
||||||
|
// val bearing = if (carOrientation == 999F) {
|
||||||
|
val bearing = if (location.hasBearing()) {
|
||||||
location.bearing.toDouble()
|
location.bearing.toDouble()
|
||||||
} else {
|
} else {
|
||||||
bearing(
|
bearingPositive(
|
||||||
lastLocation,
|
lastLocation,
|
||||||
location,
|
location,
|
||||||
cameraPosition.value!!.bearing
|
cameraPosition.value!!.bearing
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
carOrientation = bearing.toFloat()
|
||||||
carOrientation.toDouble()
|
// } else {
|
||||||
}
|
// carOrientation.toDouble()
|
||||||
|
// }
|
||||||
val zoom = if (viewStyle == ViewStyle.VIEW) {
|
val zoom = if (viewStyle == ViewStyle.VIEW) {
|
||||||
calculateZoom(location.speed.toDouble())
|
calculateZoom(location.speed.toDouble())
|
||||||
} else {
|
} else {
|
||||||
@@ -410,8 +435,11 @@ class SurfaceRenderer(
|
|||||||
* Sets route data for active navigation and switches to VIEW mode.
|
* Sets route data for active navigation and switches to VIEW mode.
|
||||||
*/
|
*/
|
||||||
fun setRouteData(routeGeoJson: String) {
|
fun setRouteData(routeGeoJson: String) {
|
||||||
|
synchronized(this) {
|
||||||
routeData.value = routeGeoJson
|
routeData.value = routeGeoJson
|
||||||
viewStyle = ViewStyle.VIEW
|
viewStyle = ViewStyle.VIEW
|
||||||
|
updateLocation(lastLocation, "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -427,7 +455,8 @@ class SurfaceRenderer(
|
|||||||
* Updates camera position with new bearing, zoom, and target.
|
* Updates camera position with new bearing, zoom, and target.
|
||||||
* Posts update to LiveData for UI observation.
|
* Posts update to LiveData for UI observation.
|
||||||
*/
|
*/
|
||||||
fun updateCameraPosition(bearing: Double = 0.0, zoom: Double, target: Position, tilt: Double) {
|
fun updateCameraPosition(bearing: Double = 0.0, zoom: Double = cameraPosition.value!!.zoom ,
|
||||||
|
target: Position, tilt: Double = 0.0) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
cameraPosition.postValue(
|
cameraPosition.postValue(
|
||||||
cameraPosition.value!!.copy(
|
cameraPosition.value!!.copy(
|
||||||
@@ -457,7 +486,7 @@ class SurfaceRenderer(
|
|||||||
with(routeModel) {
|
with(routeModel) {
|
||||||
routeData.value = curRoute.routeGeoJson
|
routeData.value = curRoute.routeGeoJson
|
||||||
centerLocation = curRoute.centerLocation
|
centerLocation = curRoute.centerLocation
|
||||||
previewDistance = curRoute.summary.distance
|
previewDistance = curLeg.summary.distance
|
||||||
}
|
}
|
||||||
tilt = 0.0
|
tilt = 0.0
|
||||||
updateCameraPosition(
|
updateCameraPosition(
|
||||||
@@ -473,10 +502,12 @@ class SurfaceRenderer(
|
|||||||
* Calculates appropriate zoom
|
* Calculates appropriate zoom
|
||||||
*/
|
*/
|
||||||
fun setStandardView() {
|
fun setStandardView() {
|
||||||
viewStyle = ViewStyle.VIEW
|
if (!navigation) {
|
||||||
setRouteData("")
|
setRouteData("")
|
||||||
|
}
|
||||||
|
viewStyle = ViewStyle.VIEW
|
||||||
val zoom = calculateZoom(0.0)
|
val zoom = calculateZoom(0.0)
|
||||||
tilt = calculateTilt(zoom, tilt)
|
tilt = calculateTilt(viewStyle, zoom, tilt)
|
||||||
updateCameraPosition(
|
updateCameraPosition(
|
||||||
tilt = tilt,
|
tilt = tilt,
|
||||||
zoom = zoom,
|
zoom = zoom,
|
||||||
@@ -500,18 +531,6 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates car location from the connected car system.
|
|
||||||
* Only updates location when using OSRM routing engine.
|
|
||||||
*/
|
|
||||||
fun updateCarLocation(location: Location, streetName: String) {
|
|
||||||
val repository = getSettingsRepository(carContext)
|
|
||||||
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
|
||||||
if (routingEngine == RouteEngine.OSRM.ordinal) {
|
|
||||||
updateLocation(location, streetName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates current speed for display.
|
* Updates current speed for display.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ class TextToSpeechManager(private val carContext: Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
initialized = true
|
initialized = true
|
||||||
Log.d("TTS", "Initialization Success")
|
|
||||||
} else {
|
} else {
|
||||||
Log.e("TTS", "Initialization Failed")
|
Log.e("TTS", "Initialization Failed")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,15 +23,25 @@ import androidx.compose.ui.text.drawText
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.rememberTextMeasurer
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
|
import com.kouros.navigation.data.HeavyColor
|
||||||
|
import com.kouros.navigation.data.LaneColor
|
||||||
|
import com.kouros.navigation.data.NavigationCircle
|
||||||
import com.kouros.navigation.data.NavigationColorDark
|
import com.kouros.navigation.data.NavigationColorDark
|
||||||
import com.kouros.navigation.data.NavigationColorLight
|
import com.kouros.navigation.data.NavigationColorLight
|
||||||
|
import com.kouros.navigation.data.PharmacyColor
|
||||||
|
import com.kouros.navigation.data.QueuingColor
|
||||||
|
import com.kouros.navigation.data.RoadworksColor
|
||||||
import com.kouros.navigation.data.RouteColor
|
import com.kouros.navigation.data.RouteColor
|
||||||
|
import com.kouros.navigation.data.SlowColor
|
||||||
import com.kouros.navigation.data.SpeedColor
|
import com.kouros.navigation.data.SpeedColor
|
||||||
|
import com.kouros.navigation.data.StationaryColor
|
||||||
import com.kouros.navigation.data.ViewStyle
|
import com.kouros.navigation.data.ViewStyle
|
||||||
|
import com.kouros.navigation.utils.GeoUtils.createPointCollection
|
||||||
import com.kouros.navigation.utils.isMetricSystem
|
import com.kouros.navigation.utils.isMetricSystem
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
import org.maplibre.compose.camera.CameraPosition
|
import org.maplibre.compose.camera.CameraPosition
|
||||||
@@ -44,14 +54,14 @@ import org.maplibre.compose.expressions.dsl.image
|
|||||||
import org.maplibre.compose.expressions.dsl.interpolate
|
import org.maplibre.compose.expressions.dsl.interpolate
|
||||||
import org.maplibre.compose.expressions.dsl.zoom
|
import org.maplibre.compose.expressions.dsl.zoom
|
||||||
import org.maplibre.compose.expressions.value.ColorValue
|
import org.maplibre.compose.expressions.value.ColorValue
|
||||||
|
import org.maplibre.compose.expressions.value.DpValue
|
||||||
|
import org.maplibre.compose.expressions.value.ImageValue
|
||||||
import org.maplibre.compose.layers.Anchor
|
import org.maplibre.compose.layers.Anchor
|
||||||
import org.maplibre.compose.layers.FillLayer
|
import org.maplibre.compose.layers.FillLayer
|
||||||
import org.maplibre.compose.layers.LineLayer
|
import org.maplibre.compose.layers.LineLayer
|
||||||
import org.maplibre.compose.layers.SymbolLayer
|
import org.maplibre.compose.layers.SymbolLayer
|
||||||
import org.maplibre.compose.location.LocationPuck
|
|
||||||
import org.maplibre.compose.location.LocationPuckColors
|
import org.maplibre.compose.location.LocationPuckColors
|
||||||
import org.maplibre.compose.location.LocationPuckSizes
|
import org.maplibre.compose.location.LocationPuckSizes
|
||||||
import org.maplibre.compose.location.UserLocationState
|
|
||||||
import org.maplibre.compose.map.MapOptions
|
import org.maplibre.compose.map.MapOptions
|
||||||
import org.maplibre.compose.map.MaplibreMap
|
import org.maplibre.compose.map.MaplibreMap
|
||||||
import org.maplibre.compose.map.OrnamentOptions
|
import org.maplibre.compose.map.OrnamentOptions
|
||||||
@@ -60,6 +70,8 @@ import org.maplibre.compose.sources.Source
|
|||||||
import org.maplibre.compose.sources.getBaseSource
|
import org.maplibre.compose.sources.getBaseSource
|
||||||
import org.maplibre.compose.sources.rememberGeoJsonSource
|
import org.maplibre.compose.sources.rememberGeoJsonSource
|
||||||
import org.maplibre.compose.style.BaseStyle
|
import org.maplibre.compose.style.BaseStyle
|
||||||
|
import org.maplibre.geojson.FeatureCollection
|
||||||
|
import org.maplibre.geojson.LineString
|
||||||
import org.maplibre.spatialk.geojson.Position
|
import org.maplibre.spatialk.geojson.Position
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@@ -100,101 +112,190 @@ fun MapLibre(
|
|||||||
OrnamentOptions(isScaleBarEnabled = false)
|
OrnamentOptions(isScaleBarEnabled = false)
|
||||||
),
|
),
|
||||||
cameraState = cameraState,
|
cameraState = cameraState,
|
||||||
baseStyle = baseStyle
|
baseStyle = baseStyle,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
getBaseSource(id = "openmaptiles")?.let { tiles ->
|
getBaseSource(id = "openmaptiles")?.let { tiles ->
|
||||||
if (!showBuildings) {
|
if (!showBuildings) {
|
||||||
BuildingLayer(tiles)
|
BuildingLayer(tiles)
|
||||||
}
|
}
|
||||||
if (viewStyle == ViewStyle.AMENITY_VIEW) {
|
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)
|
Puck(cameraState, lastLocation)
|
||||||
AmenityLayer(route)
|
AmenityLayer(route)
|
||||||
} else {
|
} else {
|
||||||
RouteLayer(route, traffic!!)
|
TrafficLayer(traffic!!)
|
||||||
|
RouteLayer(route)
|
||||||
|
StartEndLayer(route)
|
||||||
|
// change also createLineStringCollection in GeoUtils
|
||||||
|
// Uncomment StartEndLayer
|
||||||
// RouteLayerPoint(route )
|
// RouteLayerPoint(route )
|
||||||
}
|
}
|
||||||
SpeedCameraLayer(speedCameras)
|
SpeedCameraLayer(speedCameras)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
|
fun RouteLayer(routeData: String?) {
|
||||||
if (!routeData.isNullOrEmpty()) {
|
if (!routeData.isNullOrEmpty()) {
|
||||||
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
||||||
LineLayer(
|
LineLayer(
|
||||||
id = "routes-casing",
|
id = "routes-casing",
|
||||||
source = routes,
|
source = routes,
|
||||||
color = const(Color.White),
|
color = const(Color.Green),
|
||||||
width =
|
width = routeLineWidth(base = 1.dp, isCasing = true),
|
||||||
interpolate(
|
|
||||||
type = exponential(1.2f),
|
|
||||||
input = zoom(),
|
|
||||||
5 to const(0.4.dp),
|
|
||||||
6 to const(0.8.dp),
|
|
||||||
7 to const(2.0.dp),
|
|
||||||
20 to const(24.dp),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
LineLayer(
|
LineLayer(
|
||||||
id = "routes",
|
id = "routes",
|
||||||
source = routes,
|
source = routes,
|
||||||
color = const(RouteColor),
|
color = const(RouteColor),
|
||||||
width =
|
width = routeLineWidth(base = 1.dp, isCasing = false),
|
||||||
interpolate(
|
|
||||||
type = exponential(1.2f),
|
|
||||||
input = zoom(),
|
|
||||||
5 to const(0.4.dp),
|
|
||||||
6 to const(0.7.dp),
|
|
||||||
7 to const(1.75.dp),
|
|
||||||
20 to const(22.dp),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TrafficLayer(trafficData: Map<String, String>) {
|
||||||
trafficData.forEach {
|
trafficData.forEach {
|
||||||
|
if (it.key == "closed") {
|
||||||
|
//ClosedLayer(it)
|
||||||
|
} else {
|
||||||
val traffic = rememberGeoJsonSource(GeoJsonData.JsonString(it.value))
|
val traffic = rememberGeoJsonSource(GeoJsonData.JsonString(it.value))
|
||||||
LineLayer(
|
LineLayer(
|
||||||
id = "traffic-${it.key}-casing",
|
id = "traffic-${it.key}-casing",
|
||||||
source = traffic,
|
source = traffic,
|
||||||
color = const(Color.White),
|
color = const(Color.White),
|
||||||
width =
|
width = routeLineWidth(base = 2.dp, isCasing = true),
|
||||||
interpolate(
|
|
||||||
type = exponential(1.2f),
|
|
||||||
input = zoom(),
|
|
||||||
5 to const(0.4.dp),
|
|
||||||
6 to const(0.6.dp),
|
|
||||||
7 to const(1.8.dp),
|
|
||||||
20 to const(20.dp),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
LineLayer(
|
LineLayer(
|
||||||
id = "traffic-${it.key}",
|
id = "traffic-${it.key}",
|
||||||
source = traffic,
|
source = traffic,
|
||||||
color = trafficColor(it.key),
|
color = trafficColor(it.key),
|
||||||
width =
|
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(
|
interpolate(
|
||||||
type = exponential(1.2f),
|
type = exponential(2.0f),
|
||||||
input = zoom(),
|
input = zoom(),
|
||||||
5 to const(0.4.dp),
|
5 to const(2.0f),
|
||||||
6 to const(0.5.dp),
|
10 to const(2.0f),
|
||||||
7 to const(1.6.dp),
|
15 to const(3.0f),
|
||||||
20 to const(18.dp),
|
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
|
@Composable
|
||||||
fun RouteLayerPoint(routeData: String?) {
|
fun RouteLayerPoint(routeData: String?) {
|
||||||
if (!routeData.isNullOrEmpty()) {
|
if (!routeData.isNullOrEmpty()) {
|
||||||
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
||||||
val img = image(painterResource(R.drawable.ic_favorite_filled_white_24dp), drawAsSdf = true)
|
val img = image(painterResource(R.drawable.settings_48px), drawAsSdf = true)
|
||||||
SymbolLayer(
|
SymbolLayer(
|
||||||
id = "point-layer",
|
id = "route-point-layer",
|
||||||
source = routes,
|
source = routes,
|
||||||
iconOpacity = const(2.0f),
|
iconOpacity = const(2.0f),
|
||||||
iconColor = const(Color.Red),
|
iconColor = const(Color.Red),
|
||||||
@@ -203,23 +304,34 @@ fun RouteLayerPoint(routeData: String?) {
|
|||||||
interpolate(
|
interpolate(
|
||||||
type = exponential(1.2f),
|
type = exponential(1.2f),
|
||||||
input = zoom(),
|
input = zoom(),
|
||||||
5 to const(0.4f),
|
5 to const(0.8f),
|
||||||
6 to const(0.6f),
|
6 to const(1.0f),
|
||||||
7 to const(0.8f),
|
7 to const(1.2f),
|
||||||
20 to const(1.0f),
|
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> {
|
fun trafficColor(key: String): Expression<ColorValue> {
|
||||||
when (key) {
|
when (key) {
|
||||||
"queuing" -> return const(Color(0xFFC46E53))
|
"queuing" -> return const(QueuingColor)
|
||||||
"slow" -> return const(Color(0xFFC43E3E))
|
"slow" -> return const(SlowColor)
|
||||||
"stationary" -> return const(Color(0xFF910A0A))
|
"stationary" -> return const(StationaryColor)
|
||||||
"heavy" -> return const(Color(0xFF6B0404))
|
"heavy" -> return const(HeavyColor)
|
||||||
"roadworks" -> return const(Color(0xFF443506))
|
"roadworks" -> return const(RoadworksColor)
|
||||||
|
"lane" -> return const(LaneColor)
|
||||||
}
|
}
|
||||||
return const(Color.Blue)
|
return const(Color.Blue)
|
||||||
}
|
}
|
||||||
@@ -230,11 +342,14 @@ fun AmenityLayer(routeData: String?) {
|
|||||||
var color = const(Color.Red)
|
var color = const(Color.Red)
|
||||||
var img = image(painterResource(R.drawable.local_pharmacy_24px), drawAsSdf = true)
|
var img = image(painterResource(R.drawable.local_pharmacy_24px), drawAsSdf = true)
|
||||||
if (routeData.contains(Constants.CHARGING_STATION)) {
|
if (routeData.contains(Constants.CHARGING_STATION)) {
|
||||||
color = const(Color(0xFF054603))
|
color = const(PharmacyColor)
|
||||||
img = image(painterResource(R.drawable.ev_station_24px), drawAsSdf = true)
|
img = image(painterResource(R.drawable.ev_station_24px), drawAsSdf = true)
|
||||||
} else if (routeData.contains(Constants.FUEL_STATION)) {
|
} else if (routeData.contains(Constants.FUEL_STATION)) {
|
||||||
color = const(Color.Blue)
|
color = const(Color.Blue)
|
||||||
img = image(painterResource(R.drawable.local_gas_station_24), drawAsSdf = true)
|
img = image(painterResource(R.drawable.local_gas_station_24), drawAsSdf = true)
|
||||||
|
} else if (routeData.contains(Constants.RESTAURANT)) {
|
||||||
|
color = const(Color.Magenta)
|
||||||
|
img = image(painterResource(R.drawable.restaurant_24px), drawAsSdf = true)
|
||||||
}
|
}
|
||||||
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
||||||
SymbolLayer(
|
SymbolLayer(
|
||||||
@@ -300,13 +415,14 @@ fun DrawNavigationImages(
|
|||||||
height: Int,
|
height: Int,
|
||||||
streetName: String?,
|
streetName: String?,
|
||||||
darkMode: Boolean,
|
darkMode: Boolean,
|
||||||
|
tilt: Double,
|
||||||
) {
|
) {
|
||||||
NavigationImage(padding, width, height, streetName, darkMode)
|
NavigationImage(padding, width, height, streetName, darkMode, tilt)
|
||||||
if (speed != null) {
|
if (speed != null) {
|
||||||
CurrentSpeed(width, height, speed, maxSpeed)
|
CurrentSpeed(width, height, speed, maxSpeed)
|
||||||
}
|
}
|
||||||
if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) {
|
if (speed != null && maxSpeed > 0) {
|
||||||
MaxSpeed(width, height, maxSpeed)
|
MaxSpeed(width, height, maxSpeed, speed)
|
||||||
}
|
}
|
||||||
//DebugInfo(width, height, lat!!)
|
//DebugInfo(width, height, lat!!)
|
||||||
}
|
}
|
||||||
@@ -317,19 +433,20 @@ fun NavigationImage(
|
|||||||
width: Int,
|
width: Int,
|
||||||
height: Int,
|
height: Int,
|
||||||
streetName: String?,
|
streetName: String?,
|
||||||
darkMode: Boolean
|
darkMode: Boolean,
|
||||||
|
tilt: Double
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val imageSize = (height / 8)
|
val imageSize = (height / 8)
|
||||||
val navigationColor = if (darkMode)
|
val navigationColor = if (darkMode)
|
||||||
remember { NavigationColorDark }
|
|
||||||
else
|
|
||||||
remember { NavigationColorLight }
|
remember { NavigationColorLight }
|
||||||
|
else
|
||||||
|
remember { NavigationColorDark }
|
||||||
|
|
||||||
val textMeasurerStreet = rememberTextMeasurer()
|
val textMeasurerStreet = rememberTextMeasurer()
|
||||||
val street = streetName.toString()
|
val street = streetName.toString()
|
||||||
val styleStreet = TextStyle(
|
val styleStreet = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 18.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = if (darkMode) Color.White else navigationColor,
|
color = if (darkMode) Color.White else navigationColor,
|
||||||
)
|
)
|
||||||
@@ -337,13 +454,18 @@ fun NavigationImage(
|
|||||||
textMeasurerStreet.measure(street, styleStreet, overflow = TextOverflow.Ellipsis)
|
textMeasurerStreet.measure(street, styleStreet, overflow = TextOverflow.Ellipsis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val scaleY = if (tilt == 0.0) {
|
||||||
|
1F
|
||||||
|
} else {
|
||||||
|
0.7F
|
||||||
|
}
|
||||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) {
|
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) {
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(imageSize.dp, imageSize.dp)
|
.size(imageSize.dp, imageSize.dp)
|
||||||
) {
|
) {
|
||||||
scale(scaleX = 1f, scaleY = 0.7f) {
|
scale(scaleX = 1f, scaleY = scaleY) {
|
||||||
drawCircle(navigationColor.copy(alpha = 0.3f))
|
drawCircle(NavigationCircle.copy(alpha = 0.4f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
@@ -352,7 +474,7 @@ fun NavigationImage(
|
|||||||
tint = navigationColor.copy(alpha = 0.7f),
|
tint = navigationColor.copy(alpha = 0.7f),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(imageSize.dp, imageSize.dp)
|
.size(imageSize.dp, imageSize.dp)
|
||||||
.scale(scaleX = 1f, scaleY = 0.7f),
|
.scale(scaleX = 1f, scaleY = scaleY),
|
||||||
)
|
)
|
||||||
|
|
||||||
Canvas(
|
Canvas(
|
||||||
@@ -367,7 +489,7 @@ fun NavigationImage(
|
|||||||
x = topLeftX,
|
x = topLeftX,
|
||||||
y = topLeftY,
|
y = topLeftY,
|
||||||
),
|
),
|
||||||
color = if (darkMode) navigationColor else Color.White,
|
color = if (darkMode) NavigationColorLight else Color.White,
|
||||||
cornerRadius = CornerRadius(x = 10f, y = 10f),
|
cornerRadius = CornerRadius(x = 10f, y = 10f),
|
||||||
)
|
)
|
||||||
drawText(
|
drawText(
|
||||||
@@ -394,7 +516,7 @@ private fun CurrentSpeed(
|
|||||||
maxSpeed: Int
|
maxSpeed: Int
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val radius = 34
|
val radius = 36
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(
|
.padding(
|
||||||
@@ -406,17 +528,18 @@ private fun CurrentSpeed(
|
|||||||
val textMeasurerSpeed = rememberTextMeasurer()
|
val textMeasurerSpeed = rememberTextMeasurer()
|
||||||
val textMeasurerKm = 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 kmh = if (isMetricSystem()) "km/h" else "mph"
|
||||||
|
|
||||||
val styleSpeed = TextStyle(
|
val styleSpeed = TextStyle(
|
||||||
fontSize = 22.sp,
|
fontSize = 24.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
)
|
)
|
||||||
val styleKm = TextStyle(
|
val styleKm = TextStyle(
|
||||||
fontSize = 12.sp,
|
fontSize = 14.sp,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
)
|
)
|
||||||
val textLayoutSpeed = remember(speed, maxSpeed) {
|
val textLayoutSpeed = remember(speed, maxSpeed) {
|
||||||
@@ -461,6 +584,7 @@ private fun MaxSpeed(
|
|||||||
width: Int,
|
width: Int,
|
||||||
height: Int,
|
height: Int,
|
||||||
maxSpeed: Int,
|
maxSpeed: Int,
|
||||||
|
curSpeed: Float,
|
||||||
) {
|
) {
|
||||||
val radius = 24
|
val radius = 24
|
||||||
Box(
|
Box(
|
||||||
@@ -481,6 +605,11 @@ private fun MaxSpeed(
|
|||||||
val textLayoutSpeed = remember(speed) {
|
val textLayoutSpeed = remember(speed) {
|
||||||
textMeasurerSpeed.measure(speed, styleSpeed)
|
textMeasurerSpeed.measure(speed, styleSpeed)
|
||||||
}
|
}
|
||||||
|
val signColor = if (curSpeed * 3.6 > (maxSpeed + 3)) {
|
||||||
|
Color.Red
|
||||||
|
} else {
|
||||||
|
Color.Green
|
||||||
|
}
|
||||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
drawCircle(
|
drawCircle(
|
||||||
center = Offset(
|
center = Offset(
|
||||||
@@ -488,7 +617,7 @@ private fun MaxSpeed(
|
|||||||
y = center.y
|
y = center.y
|
||||||
),
|
),
|
||||||
radius = radius * 1.3.toFloat(),
|
radius = radius * 1.3.toFloat(),
|
||||||
color = Color.Red,
|
color = signColor,
|
||||||
)
|
)
|
||||||
drawCircle(
|
drawCircle(
|
||||||
center = Offset(
|
center = Offset(
|
||||||
@@ -576,7 +705,7 @@ fun Puck(cameraState: CameraState, location: Location) {
|
|||||||
locationState = location,
|
locationState = location,
|
||||||
cameraState = cameraState,
|
cameraState = cameraState,
|
||||||
accuracyThreshold = 10f,
|
accuracyThreshold = 10f,
|
||||||
oldLocationThreshold = 2.seconds,
|
oldLocationThreshold = 1.seconds,
|
||||||
showBearing = false,
|
showBearing = false,
|
||||||
sizes = LocationPuckSizes(dotRadius = 10.dp),
|
sizes = LocationPuckSizes(dotRadius = 10.dp),
|
||||||
colors = LocationPuckColors(
|
colors = LocationPuckColors(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.kouros.navigation.car.navigation
|
|||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
|
import android.util.Log
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.car.app.AppManager
|
import androidx.car.app.AppManager
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
@@ -17,17 +18,19 @@ import androidx.car.app.model.DateTimeWithZone
|
|||||||
import androidx.car.app.model.Distance
|
import androidx.car.app.model.Distance
|
||||||
import androidx.car.app.model.DurationSpan
|
import androidx.car.app.model.DurationSpan
|
||||||
import androidx.car.app.model.ForegroundCarColorSpan
|
import androidx.car.app.model.ForegroundCarColorSpan
|
||||||
|
import androidx.car.app.navigation.model.Destination
|
||||||
import androidx.car.app.navigation.model.Lane
|
import androidx.car.app.navigation.model.Lane
|
||||||
import androidx.car.app.navigation.model.LaneDirection
|
import androidx.car.app.navigation.model.LaneDirection
|
||||||
import androidx.car.app.navigation.model.Maneuver
|
import androidx.car.app.navigation.model.Maneuver
|
||||||
import androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
|
|
||||||
import androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|
|
||||||
import androidx.car.app.navigation.model.Step
|
import androidx.car.app.navigation.model.Step
|
||||||
import androidx.car.app.navigation.model.TravelEstimate
|
import androidx.car.app.navigation.model.TravelEstimate
|
||||||
|
import androidx.car.app.navigation.model.Trip
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.car.screen.createCarIcon
|
import com.kouros.navigation.car.screen.createCarIcon
|
||||||
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
import com.kouros.navigation.data.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
|
import com.kouros.navigation.data.route.ManeuverType
|
||||||
import com.kouros.navigation.model.RouteModel
|
import com.kouros.navigation.model.RouteModel
|
||||||
import com.kouros.navigation.utils.formattedDistance
|
import com.kouros.navigation.utils.formattedDistance
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -47,17 +50,15 @@ class RouteCarModel : RouteModel() {
|
|||||||
|
|
||||||
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
|
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
|
||||||
.setIcon(createCarIcon(carContext, stepData.icon))
|
.setIcon(createCarIcon(carContext, stepData.icon))
|
||||||
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|
if (stepData.currentManeuverType == ManeuverType.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW.ordinal
|
||||||
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
|
|| stepData.currentManeuverType == ManeuverType.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW.ordinal
|
||||||
) {
|
) {
|
||||||
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
|
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
|
||||||
}
|
}
|
||||||
val step =
|
val step =
|
||||||
Step.Builder(currentStepCueWithImage)
|
Step.Builder(currentStepCueWithImage)
|
||||||
|
|
||||||
if (navState.destination.street != null) {
|
step.setRoad(navState.destination.street)
|
||||||
step.setRoad(navState.destination.street!!)
|
|
||||||
}
|
|
||||||
if (stepData.lane.isNotEmpty()) {
|
if (stepData.lane.isNotEmpty()) {
|
||||||
val lanesAdded = addLanes(carContext, step, stepData)
|
val lanesAdded = addLanes(carContext, step, stepData)
|
||||||
if (lanesAdded) {
|
if (lanesAdded) {
|
||||||
@@ -77,8 +78,8 @@ class RouteCarModel : RouteModel() {
|
|||||||
createString(stepData.instruction)
|
createString(stepData.instruction)
|
||||||
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
|
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
|
||||||
.setIcon(createCarIcon(carContext, stepData.icon))
|
.setIcon(createCarIcon(carContext, stepData.icon))
|
||||||
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|
if (stepData.currentManeuverType == ManeuverType.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW.ordinal
|
||||||
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
|
|| stepData.currentManeuverType == ManeuverType.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW.ordinal
|
||||||
) {
|
) {
|
||||||
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
|
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
|
||||||
}
|
}
|
||||||
@@ -101,7 +102,6 @@ class RouteCarModel : RouteModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun travelEstimate(carContext: CarContext, timeLeft: Double, distanceMode: Int): TravelEstimate {
|
fun travelEstimate(carContext: CarContext, timeLeft: Double, distanceMode: Int): TravelEstimate {
|
||||||
|
|
||||||
val timeToDestinationMillis =
|
val timeToDestinationMillis =
|
||||||
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
|
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
|
||||||
val distance = formattedDistance(distanceMode, routeCalculator.travelLeftDistance())
|
val distance = formattedDistance(distanceMode, routeCalculator.travelLeftDistance())
|
||||||
@@ -126,16 +126,61 @@ class RouteCarModel : RouteModel() {
|
|||||||
.setRemainingTimeColor(CarColor.GREEN)
|
.setRemainingTimeColor(CarColor.GREEN)
|
||||||
.setRemainingDistanceColor(CarColor.BLUE)
|
.setRemainingDistanceColor(CarColor.BLUE)
|
||||||
if (traffic > 0) {
|
if (traffic > 0) {
|
||||||
travelBuilder.setTripText(createDelay(traffic))
|
|
||||||
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.traffic_jam_48px))
|
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.traffic_jam_48px))
|
||||||
|
travelBuilder.setTripText(createDelay(traffic))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (navState.travelMessage.isNotEmpty()) {
|
if (navState.travelMessage.isNotEmpty()) {
|
||||||
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px))
|
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px))
|
||||||
travelBuilder.setTripText(CarText.create(navState.travelMessage))
|
travelBuilder.setTripText(CarText.create(navState.travelMessage))
|
||||||
}
|
}
|
||||||
return travelBuilder.build()
|
return travelBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSteps(carContext: CarContext): MutableList<Step> {
|
||||||
|
val steps = mutableListOf<Step>()
|
||||||
|
steps.add(currentStep(carContext))
|
||||||
|
if (navState.nextStep) {
|
||||||
|
steps.add(nextStep(carContext = carContext))
|
||||||
|
}
|
||||||
|
return steps
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDistance(): Distance {
|
||||||
|
val distance =
|
||||||
|
formattedDistance(0, routeCalculator.leftStepDistance())
|
||||||
|
return Distance.create(distance.first, distance.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTravelEstimateTrip(carContext: CarContext): TravelEstimate {
|
||||||
|
return travelEstimateTrip(carContext, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTravelEstimateStep(carContext: CarContext): TravelEstimate {
|
||||||
|
return travelEstimateStep(carContext, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDestination(): Destination {
|
||||||
|
return Destination.Builder()
|
||||||
|
.setName(navState.destination.name)
|
||||||
|
.setAddress(navState.destination.street)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a trip builder object
|
||||||
|
*/
|
||||||
|
fun getTrip(carContext: CarContext): Trip {
|
||||||
|
val tripBuilder = Trip.Builder()
|
||||||
|
tripBuilder.addDestination(
|
||||||
|
getDestination(),
|
||||||
|
getTravelEstimateTrip(carContext)
|
||||||
|
)
|
||||||
|
tripBuilder.setLoading(false)
|
||||||
|
tripBuilder.setCurrentRoad( getDestination().name.toString())
|
||||||
|
tripBuilder.addStep(getSteps(carContext).first(), getTravelEstimateStep(carContext))
|
||||||
|
return tripBuilder.build()
|
||||||
|
}
|
||||||
|
|
||||||
private fun createDelay(delay: Int): CarText {
|
private fun createDelay(delay: Int): CarText {
|
||||||
val delayBuilder = SpannableStringBuilder()
|
val delayBuilder = SpannableStringBuilder()
|
||||||
delayBuilder.append(
|
delayBuilder.append(
|
||||||
@@ -221,7 +266,7 @@ class RouteCarModel : RouteModel() {
|
|||||||
R.string.exit_action_title, R.string.exit_action_title,
|
R.string.exit_action_title, R.string.exit_action_title,
|
||||||
FLAG_DEFAULT
|
FLAG_DEFAULT
|
||||||
)
|
)
|
||||||
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */5000)
|
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */4000)
|
||||||
.setSubtitle(subtitle)
|
.setSubtitle(subtitle)
|
||||||
.setIcon(icon)
|
.setIcon(icon)
|
||||||
.addAction(dismissAction).setCallback(object : AlertCallback {
|
.addAction(dismissAction).setCallback(object : AlertCallback {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.os.SystemClock
|
|||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import com.kouros.data.BuildConfig
|
import com.kouros.data.BuildConfig
|
||||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
import io.ticofab.androidgpxparser.parser.GPXParser
|
import io.ticofab.androidgpxparser.parser.GPXParser
|
||||||
import io.ticofab.androidgpxparser.parser.domain.Gpx
|
import io.ticofab.androidgpxparser.parser.domain.Gpx
|
||||||
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
|
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
|
||||||
@@ -27,8 +28,8 @@ class Simulation {
|
|||||||
) {
|
) {
|
||||||
if (routeModel.navState.route.isRouteValid()) {
|
if (routeModel.navState.route.isRouteValid()) {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
gpxSimulation(routeModel, lifecycleScope, updateLocation)
|
//gpxSimulation(routeModel, lifecycleScope, updateLocation)
|
||||||
//currentSimulation(routeModel, lifecycleScope, updateLocation)
|
currentSimulation(routeModel, lifecycleScope, updateLocation)
|
||||||
} else {
|
} else {
|
||||||
currentSimulation(routeModel, lifecycleScope, updateLocation)
|
currentSimulation(routeModel, lifecycleScope, updateLocation)
|
||||||
}
|
}
|
||||||
@@ -45,20 +46,20 @@ class Simulation {
|
|||||||
if (points.isEmpty()) return
|
if (points.isEmpty()) return
|
||||||
simulationJob?.cancel()
|
simulationJob?.cancel()
|
||||||
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
|
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
|
||||||
var curBearing = 0f
|
var curBearing: Float
|
||||||
simulationJob = lifecycleScope.launch {
|
simulationJob = lifecycleScope.launch {
|
||||||
for ((index, point) in points.withIndex()) {
|
for ((index, point) in points.withIndex()) {
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
|
curBearing = lastLocation.bearingTo(location(point[0], point[1]))
|
||||||
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
|
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
|
||||||
latitude = point[1]
|
latitude = point[1]
|
||||||
longitude = point[0]
|
longitude = point[0]
|
||||||
bearing = curBearing
|
bearing = curBearing
|
||||||
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
|
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
|
||||||
speed = 13.0f // ~50 km/h
|
speed = 10.0f
|
||||||
time = System.currentTimeMillis()
|
time = System.currentTimeMillis()
|
||||||
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
|
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
|
||||||
}
|
}
|
||||||
curBearing = lastLocation.bearingTo(fakeLocation)
|
|
||||||
// Update your app's state as if a real GPS update occurred
|
// Update your app's state as if a real GPS update occurred
|
||||||
updateLocation(fakeLocation)
|
updateLocation(fakeLocation)
|
||||||
// Wait before moving to the next point (e.g., every 1 second)
|
// Wait before moving to the next point (e.g., every 1 second)
|
||||||
@@ -80,7 +81,7 @@ class Simulation {
|
|||||||
runBlocking {
|
runBlocking {
|
||||||
simulationJob = launch(Dispatchers.IO) {
|
simulationJob = launch(Dispatchers.IO) {
|
||||||
route = TomTomRepository().fetchUrl(
|
route = TomTomRepository().fetchUrl(
|
||||||
"https://kouros-online.de/vh.gpx",
|
"https://kouros-online.de/VH.gpx",
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -89,7 +90,7 @@ class Simulation {
|
|||||||
simulationJob?.cancel()
|
simulationJob?.cancel()
|
||||||
simulationJob =lifecycleScope.launch() {
|
simulationJob =lifecycleScope.launch() {
|
||||||
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
|
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
|
||||||
var curBearing = 0f
|
val curBearing = 0f
|
||||||
val parser = GPXParser()
|
val parser = GPXParser()
|
||||||
val parsedGpx: Gpx? =
|
val parsedGpx: Gpx? =
|
||||||
parser.parse(route.byteInputStream())
|
parser.parse(route.byteInputStream())
|
||||||
@@ -117,10 +118,7 @@ class Simulation {
|
|||||||
// Update your app's state as if a real GPS update occurred
|
// Update your app's state as if a real GPS update occurred
|
||||||
updateLocation(fakeLocation)
|
updateLocation(fakeLocation)
|
||||||
// Wait before moving to the next point (e.g., every 1 second)
|
// Wait before moving to the next point (e.g., every 1 second)
|
||||||
if (duration > 100) {
|
delay(200)
|
||||||
// delay(duration / 4)
|
|
||||||
}
|
|
||||||
delay(500)
|
|
||||||
lastTime = p.time
|
lastTime = p.time
|
||||||
lastLocation = fakeLocation
|
lastLocation = fakeLocation
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.CarContext
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
import androidx.car.app.model.Action
|
import androidx.car.app.model.Action
|
||||||
|
import androidx.car.app.model.CarColor
|
||||||
import androidx.car.app.model.CarIcon
|
import androidx.car.app.model.CarIcon
|
||||||
|
import androidx.car.app.model.GridItem
|
||||||
|
import androidx.car.app.model.GridTemplate
|
||||||
import androidx.car.app.model.Header
|
import androidx.car.app.model.Header
|
||||||
import androidx.car.app.model.ItemList
|
import androidx.car.app.model.ItemList
|
||||||
import androidx.car.app.model.ListTemplate
|
|
||||||
import androidx.car.app.model.Row
|
|
||||||
import androidx.car.app.model.Template
|
import androidx.car.app.model.Template
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.car.SurfaceRenderer
|
import com.kouros.navigation.car.SurfaceRenderer
|
||||||
import com.kouros.navigation.data.Category
|
import com.kouros.navigation.data.Category
|
||||||
import com.kouros.navigation.data.Constants.CHARGING_STATION
|
import com.kouros.navigation.data.Constants.CHARGING_STATION
|
||||||
import com.kouros.navigation.data.Constants.FUEL_STATION
|
import com.kouros.navigation.data.Constants.FUEL_STATION
|
||||||
import com.kouros.navigation.data.Constants.PHARMACY
|
import com.kouros.navigation.data.Constants.PHARMACY
|
||||||
|
import com.kouros.navigation.data.Constants.RESTAURANT
|
||||||
import com.kouros.navigation.data.ViewStyle
|
import com.kouros.navigation.data.ViewStyle
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
|
|
||||||
@@ -30,22 +34,18 @@ class CategoriesScreen(
|
|||||||
var categories: List<Category> = listOf(
|
var categories: List<Category> = listOf(
|
||||||
Category(id = FUEL_STATION, name = carContext.getString(R.string.fuel_station)),
|
Category(id = FUEL_STATION, name = carContext.getString(R.string.fuel_station)),
|
||||||
Category(id = PHARMACY, name = carContext.getString(R.string.pharmacy)),
|
Category(id = PHARMACY, name = carContext.getString(R.string.pharmacy)),
|
||||||
Category(id = CHARGING_STATION, name = carContext.getString(R.string.charging_station))
|
Category(id = CHARGING_STATION, name = carContext.getString(R.string.charging_station)),
|
||||||
|
Category(id = RESTAURANT, name = carContext.getString(R.string.restaurant))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
init {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
val itemListBuilder = ItemList.Builder()
|
val itemListBuilder = ItemList.Builder()
|
||||||
.setNoItemsMessage("No categories to show")
|
.setNoItemsMessage("No categories to show")
|
||||||
categories.forEach {
|
categories.forEach {
|
||||||
itemListBuilder.addItem(
|
itemListBuilder.addItem(
|
||||||
Row.Builder()
|
GridItem.Builder()
|
||||||
.setTitle(it.name)
|
|
||||||
.setImage(carIcon(carContext, it.id, -1))
|
.setImage(carIcon(carContext, it.id, -1))
|
||||||
|
.setTitle(it.name)
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
category = it.id
|
category = it.id
|
||||||
screenManager
|
screenManager
|
||||||
@@ -63,34 +63,68 @@ class CategoriesScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setBrowsable(true)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
surfaceRenderer.viewStyle = ViewStyle.AMENITY_VIEW
|
surfaceRenderer.viewStyle = ViewStyle.AMENITY_VIEW
|
||||||
|
|
||||||
val header = Header.Builder()
|
return GridTemplate.Builder()
|
||||||
|
.setHeader(
|
||||||
|
Header.Builder()
|
||||||
.setStartHeaderAction(Action.BACK)
|
.setStartHeaderAction(Action.BACK)
|
||||||
.setTitle(carContext.getString(R.string.category_title))
|
.setTitle(carContext.getString(R.string.category_title))
|
||||||
.build()
|
.build()
|
||||||
|
)
|
||||||
return ListTemplate.Builder()
|
|
||||||
.setHeader(header)
|
|
||||||
.setSingleList(itemListBuilder.build())
|
.setSingleList(itemListBuilder.build())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun carIcon(context: CarContext, category: String, index: Int): CarIcon {
|
fun carIcon(context: CarContext, category: String, index: Int): CarIcon {
|
||||||
|
|
||||||
|
val customCarColor =
|
||||||
|
CarColor.createCustom(android.graphics.Color.MAGENTA, android.graphics.Color.MAGENTA)
|
||||||
|
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
val resId = when (category) {
|
val icon = when (category) {
|
||||||
CHARGING_STATION -> R.drawable.ev_station_24px
|
CHARGING_STATION -> CarIcon.Builder(
|
||||||
FUEL_STATION -> R.drawable.local_gas_station_24
|
IconCompat.createWithResource(
|
||||||
PHARMACY -> R.drawable.local_pharmacy_24px
|
context,
|
||||||
else -> R.drawable.ic_place_white_24dp
|
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 {
|
} else {
|
||||||
return CarIcon.Builder(
|
return CarIcon.Builder(
|
||||||
createNumberIcon(
|
createNumberIcon(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.kouros.navigation.car.screen
|
package com.kouros.navigation.car.screen
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
@@ -19,6 +20,9 @@ import androidx.car.app.navigation.model.MapWithContentTemplate
|
|||||||
import androidx.car.app.versioning.CarAppApiLevels
|
import androidx.car.app.versioning.CarAppApiLevels
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.car.SurfaceRenderer
|
import com.kouros.navigation.car.SurfaceRenderer
|
||||||
import com.kouros.navigation.car.screen.observers.CategoryObserver
|
import com.kouros.navigation.car.screen.observers.CategoryObserver
|
||||||
@@ -27,12 +31,18 @@ import com.kouros.navigation.data.Constants
|
|||||||
import com.kouros.navigation.data.Constants.CHARGING_STATION
|
import com.kouros.navigation.data.Constants.CHARGING_STATION
|
||||||
import com.kouros.navigation.data.Constants.FUEL_STATION
|
import com.kouros.navigation.data.Constants.FUEL_STATION
|
||||||
import com.kouros.navigation.data.Constants.PHARMACY
|
import com.kouros.navigation.data.Constants.PHARMACY
|
||||||
|
import com.kouros.navigation.data.Constants.RESTAURANT
|
||||||
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.overpass.Elements
|
import com.kouros.navigation.data.overpass.Elements
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import com.kouros.navigation.utils.GeoUtils.createPointCollection
|
import com.kouros.navigation.utils.GeoUtils.createPointCollection
|
||||||
|
import com.kouros.navigation.utils.getSettingsRepository
|
||||||
|
import com.kouros.navigation.utils.getSettingsViewModel
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
import com.kouros.navigation.utils.round
|
import com.kouros.navigation.utils.round
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class CategoryScreen(
|
class CategoryScreen(
|
||||||
@@ -43,9 +53,14 @@ class CategoryScreen(
|
|||||||
|
|
||||||
) : Screen(carContext), CategoryObserverCallback {
|
) : Screen(carContext), CategoryObserverCallback {
|
||||||
|
|
||||||
|
val repository = getSettingsRepository(carContext)
|
||||||
|
|
||||||
|
val settingsViewModel = getSettingsViewModel(carContext)
|
||||||
|
|
||||||
val maxListItems: Int = 30
|
val maxListItems: Int = 30
|
||||||
|
|
||||||
var elements: List<Elements> = emptyList()
|
var elements: List<Elements> = emptyList()
|
||||||
|
|
||||||
private val categoryObserver = CategoryObserver(this)
|
private val categoryObserver = CategoryObserver(this)
|
||||||
|
|
||||||
private var loading = true
|
private var loading = true
|
||||||
@@ -56,8 +71,12 @@ class CategoryScreen(
|
|||||||
navigationViewModel.elements.value = emptyList()
|
navigationViewModel.elements.value = emptyList()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
repository.lastFuelPricesFlow.asLiveData().observe(this, Observer {
|
||||||
|
navigationViewModel.getAmenities(carContext, category, surfaceRenderer.lastLocation, it)
|
||||||
|
})
|
||||||
|
|
||||||
navigationViewModel.elements.observe(this, categoryObserver)
|
navigationViewModel.elements.observe(this, categoryObserver)
|
||||||
navigationViewModel.getAmenities(category, surfaceRenderer.lastLocation)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
@@ -74,7 +93,6 @@ class CategoryScreen(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
elements.forEach {
|
elements.forEach {
|
||||||
if (it.tags.operator != null) {
|
|
||||||
if (index++ < listLimit) {
|
if (index++ < listLimit) {
|
||||||
listBuilder.addItem(
|
listBuilder.addItem(
|
||||||
createItem(it, category, index)
|
createItem(it, category, index)
|
||||||
@@ -82,7 +100,6 @@ class CategoryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val header = Header.Builder()
|
val header = Header.Builder()
|
||||||
.setStartHeaderAction(Action.BACK)
|
.setStartHeaderAction(Action.BACK)
|
||||||
@@ -113,18 +130,19 @@ class CategoryScreen(
|
|||||||
CHARGING_STATION -> R.string.charging_station
|
CHARGING_STATION -> R.string.charging_station
|
||||||
FUEL_STATION -> R.string.fuel_station
|
FUEL_STATION -> R.string.fuel_station
|
||||||
PHARMACY -> R.string.pharmacy
|
PHARMACY -> R.string.pharmacy
|
||||||
else -> R.string.no_places
|
else -> R.string.restaurant
|
||||||
}
|
}
|
||||||
return carContext.getString(resId)
|
return carContext.getString(resId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createItem(it: Elements, category: String, index: Int): Row {
|
private fun createItem(it: Elements, category: String, index: Int): Row {
|
||||||
var name = ""
|
var name = ""
|
||||||
if (it.tags.name != null) {
|
name = it.tags.name
|
||||||
name = it.tags.name.toString()
|
if (name.isEmpty()) {
|
||||||
|
name = it.tags.operator
|
||||||
}
|
}
|
||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
name = it.tags.operator.toString()
|
name = "Empty"
|
||||||
}
|
}
|
||||||
val row = Row.Builder()
|
val row = Row.Builder()
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
@@ -133,23 +151,23 @@ class CategoryScreen(
|
|||||||
}
|
}
|
||||||
.setTitle(name)
|
.setTitle(name)
|
||||||
.setImage(carIcon(carContext, category, index))
|
.setImage(carIcon(carContext, category, index))
|
||||||
|
when (category) {
|
||||||
|
CHARGING_STATION -> row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}")
|
||||||
|
FUEL_STATION -> row.addText(carText("${it.e5} / ${it.e10} € ${it.tags.openingHours}"))
|
||||||
|
RESTAURANT, PHARMACY ->row.addText(carText(it.tags.openingHours))
|
||||||
|
}
|
||||||
if (it.distance < 1000) {
|
if (it.distance < 1000) {
|
||||||
row.addText("${(it.distance).toInt()} m")
|
row.addText("${(it.distance).toInt()} m")
|
||||||
} else {
|
} else {
|
||||||
row.addText("${(it.distance / 1000).round(1)} km")
|
row.addText("${(it.distance / 1000).round(1)} km")
|
||||||
}
|
}
|
||||||
if (category == CHARGING_STATION) {
|
|
||||||
if (it.tags.socketType2 != null)
|
|
||||||
row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}")
|
|
||||||
} else {
|
|
||||||
row.addText(carText("${it.tags.openingHours}"))
|
|
||||||
}
|
|
||||||
row.addAction(
|
row.addAction(
|
||||||
createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT, {
|
createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT, {
|
||||||
navigationViewModel.loadRoute(
|
navigationViewModel.loadRoute(
|
||||||
carContext,
|
carContext,
|
||||||
currentLocation = surfaceRenderer.lastLocation,
|
currentLocation = surfaceRenderer.lastLocation,
|
||||||
location(it.lon, it.lat),
|
listOf(location(it.lon, it.lat)),
|
||||||
surfaceRenderer.carOrientation
|
surfaceRenderer.carOrientation
|
||||||
)
|
)
|
||||||
setResult(
|
setResult(
|
||||||
|
|||||||
@@ -16,4 +16,7 @@ interface NavigationListener {
|
|||||||
fun updateTrip(trip: Trip)
|
fun updateTrip(trip: Trip)
|
||||||
|
|
||||||
fun navigateToPlace(place: Place)
|
fun navigateToPlace(place: Place)
|
||||||
|
|
||||||
|
fun recalcRoute(destination: Place)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import com.kouros.navigation.data.ViewStyle
|
|||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import com.kouros.navigation.utils.getSettingsRepository
|
import com.kouros.navigation.utils.getSettingsRepository
|
||||||
import com.kouros.navigation.utils.getSettingsViewModel
|
import com.kouros.navigation.utils.getSettingsViewModel
|
||||||
import com.kouros.navigation.utils.location
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -52,6 +51,7 @@ open class NavigationScreen(
|
|||||||
private val navigationViewModel: NavigationViewModel
|
private val navigationViewModel: NavigationViewModel
|
||||||
) : Screen(carContext) {
|
) : Screen(carContext) {
|
||||||
|
|
||||||
|
var deviation = 0F
|
||||||
var recentPlaces = mutableListOf<Place>()
|
var recentPlaces = mutableListOf<Place>()
|
||||||
|
|
||||||
var recentPlace: Place = Place()
|
var recentPlace: Place = Place()
|
||||||
@@ -79,6 +79,10 @@ open class NavigationScreen(
|
|||||||
private lateinit var steps: MutableList<Step>
|
private lateinit var steps: MutableList<Step>
|
||||||
private var junctionImage: CarIcon? = null
|
private var junctionImage: CarIcon? = null
|
||||||
private var backGroundColor = CarColor.BLUE
|
private var backGroundColor = CarColor.BLUE
|
||||||
|
|
||||||
|
private var message = ""
|
||||||
|
|
||||||
|
private var showAlternativeRoute = false
|
||||||
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
|
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
|
||||||
recentPlaces.addAll(newPlaces)
|
recentPlaces.addAll(newPlaces)
|
||||||
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
|
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
|
||||||
@@ -97,6 +101,9 @@ open class NavigationScreen(
|
|||||||
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
|
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
|
||||||
tripSuggestion = it
|
tripSuggestion = it
|
||||||
})
|
})
|
||||||
|
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
|
||||||
|
showAlternativeRoute = it
|
||||||
|
})
|
||||||
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
arrivalTimer?.cancel()
|
arrivalTimer?.cancel()
|
||||||
@@ -117,26 +124,26 @@ open class NavigationScreen(
|
|||||||
)
|
)
|
||||||
}, { settingsAction() })
|
}, { settingsAction() })
|
||||||
return when (navigationType) {
|
return when (navigationType) {
|
||||||
NavigationType.NAVIGATION -> navigationTemplate(actionStripBuilder)
|
NavigationType.NAVIGATION -> navigation(actionStripBuilder)
|
||||||
NavigationType.RECENT -> navigationRecentPlacesTemplate()
|
NavigationType.RECENT -> navigationRecentPlaces()
|
||||||
NavigationType.REROUTE -> navigationRerouteTemplate(actionStripBuilder)
|
NavigationType.REROUTE -> navigationReroute(actionStripBuilder)
|
||||||
NavigationType.ARRIVAL -> navigationEndTemplate(actionStripBuilder)
|
NavigationType.ARRIVAL -> navigationEnd(actionStripBuilder)
|
||||||
else -> navigationViewTemplate(actionStripBuilder)
|
else -> navigationView(actionStripBuilder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and returns a NavigationTemplate for the active navigation state.
|
* Creates and returns a NavigationTemplate for the active navigation state.
|
||||||
*/
|
*/
|
||||||
private fun navigationTemplate(actionStripBuilder: ActionStrip.Builder): Template {
|
private fun navigation(actionStripBuilder: ActionStrip.Builder): Template {
|
||||||
actionStripBuilder.addAction(
|
// actionStripBuilder.addAction(
|
||||||
createAction(
|
// createAction(
|
||||||
carContext,
|
// carContext,
|
||||||
R.drawable.ic_close_white_24dp,
|
// R.drawable.ic_close_white_24dp,
|
||||||
0,
|
// 0
|
||||||
{ stopNavigation() })
|
// ) { stopNavigation() }
|
||||||
)
|
// )
|
||||||
return NavigationTemplate.Builder()
|
val navigationTemplate = NavigationTemplate.Builder()
|
||||||
.setNavigationInfo(
|
.setNavigationInfo(
|
||||||
getRoutingInfo()
|
getRoutingInfo()
|
||||||
)
|
)
|
||||||
@@ -144,50 +151,54 @@ open class NavigationScreen(
|
|||||||
.setActionStrip(actionStripBuilder.build())
|
.setActionStrip(actionStripBuilder.build())
|
||||||
.setMapActionStrip(
|
.setMapActionStrip(
|
||||||
mapActionStrip(
|
mapActionStrip(
|
||||||
|
carContext,
|
||||||
surfaceRenderer.viewStyle,
|
surfaceRenderer.viewStyle,
|
||||||
{ zoomPlus() }, { zoomMinus() }, {
|
{ zoomPlus() }, { zoomMinus() }, {
|
||||||
createAction(
|
Action.Builder()
|
||||||
carContext = carContext, R.drawable.ic_pan_24,
|
.setIcon(createCarIcon(carContext, R.drawable.ic_recenter_24))
|
||||||
0,
|
.setFlags(0)
|
||||||
onClickAction = {
|
.setOnClickListener {
|
||||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
surfaceRenderer.setStandardView()
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
)
|
.build()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.setBackgroundColor(backGroundColor)
|
.setBackgroundColor(backGroundColor)
|
||||||
.build()
|
.build()
|
||||||
|
return navigationTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and returns a template for the default view state.
|
* Creates and returns a template for the default view state.
|
||||||
*/
|
*/
|
||||||
private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template {
|
private fun navigationView(actionStripBuilder: ActionStrip.Builder): Template {
|
||||||
return NavigationTemplate.Builder()
|
val mapActionStrip = mapActionStrip(
|
||||||
.setBackgroundColor(backGroundColor)
|
carContext,
|
||||||
.setActionStrip(actionStripBuilder.build())
|
|
||||||
.setMapActionStrip(
|
|
||||||
mapActionStrip(
|
|
||||||
surfaceRenderer.viewStyle,
|
surfaceRenderer.viewStyle,
|
||||||
{ zoomPlus() }, { zoomMinus() }, {
|
{ zoomPlus() }, { zoomMinus() }, {
|
||||||
createAction(
|
createAction(
|
||||||
carContext = carContext, R.drawable.ic_pan_24,
|
carContext = carContext, R.drawable.ic_recenter_24,
|
||||||
0,
|
0,
|
||||||
onClickAction = {
|
onClickAction = {
|
||||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
surfaceRenderer.setStandardView()
|
||||||
invalidate()
|
invalidate()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
)
|
return NavigationTemplate.Builder()
|
||||||
.build()
|
.setBackgroundColor(backGroundColor)
|
||||||
|
.setActionStrip(actionStripBuilder.build())
|
||||||
|
.setMapActionStrip(mapActionStrip)
|
||||||
|
.setPanModeListener { isInPanMode: Boolean ->
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and returns a template for the arrival.
|
* Creates and returns a template for the arrival.
|
||||||
*/
|
*/
|
||||||
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
|
private fun navigationEnd(actionStripBuilder: ActionStrip.Builder): Template {
|
||||||
arrivalTimer?.cancel()
|
arrivalTimer?.cancel()
|
||||||
arrivalTimer = object : CountDownTimer(8000, 1000) {
|
arrivalTimer = object : CountDownTimer(8000, 1000) {
|
||||||
override fun onTick(millisUntilFinished: Long) {}
|
override fun onTick(millisUntilFinished: Long) {}
|
||||||
@@ -198,23 +209,19 @@ open class NavigationScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
arrivalTimer?.start()
|
arrivalTimer?.start()
|
||||||
return navigationArrivedTemplate(actionStripBuilder)
|
return navigationArrived(actionStripBuilder)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and returns a NavigationTemplate specifically for when the destination is reached.
|
* Creates and returns a NavigationTemplate specifically for when the destination is reached.
|
||||||
*/
|
*/
|
||||||
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
|
fun navigationArrived(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
|
||||||
var street = ""
|
|
||||||
if (destinations.first().address != null) {
|
|
||||||
street = destinations.first().address.toString()
|
|
||||||
}
|
|
||||||
return NavigationTemplate.Builder()
|
return NavigationTemplate.Builder()
|
||||||
.setNavigationInfo(
|
.setNavigationInfo(
|
||||||
MessageInfo.Builder(
|
MessageInfo.Builder(
|
||||||
carContext.getString(R.string.arrived_exclamation_msg)
|
carContext.getString(R.string.arrived_exclamation_msg)
|
||||||
)
|
)
|
||||||
.setText(street)
|
.setText(message)
|
||||||
.setImage(
|
.setImage(
|
||||||
CarIcon.Builder(
|
CarIcon.Builder(
|
||||||
IconCompat.createWithResource(
|
IconCompat.createWithResource(
|
||||||
@@ -226,14 +233,15 @@ open class NavigationScreen(
|
|||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
// .setBackgroundColor(routeModel.backGroundColor())
|
.setBackgroundColor(backGroundColor)
|
||||||
.setActionStrip(actionStripBuilder.build())
|
.setActionStrip(actionStripBuilder.build())
|
||||||
.setMapActionStrip(
|
.setMapActionStrip(
|
||||||
mapActionStrip(
|
mapActionStrip(
|
||||||
|
carContext,
|
||||||
surfaceRenderer.viewStyle,
|
surfaceRenderer.viewStyle,
|
||||||
{ zoomPlus() }, { zoomMinus() }, {
|
{ zoomPlus() }, { zoomMinus() }, {
|
||||||
createAction(
|
createAction(
|
||||||
carContext = carContext, R.drawable.ic_pan_24,
|
carContext = carContext, R.drawable.ic_recenter_24,
|
||||||
0,
|
0,
|
||||||
onClickAction = {
|
onClickAction = {
|
||||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||||
@@ -247,10 +255,10 @@ open class NavigationScreen(
|
|||||||
/**
|
/**
|
||||||
* Creates and returns a template showing recent places or destinations.
|
* Creates and returns a template showing recent places or destinations.
|
||||||
*/
|
*/
|
||||||
fun navigationRecentPlacesTemplate(): Template {
|
fun navigationRecentPlaces(): Template {
|
||||||
if (!tripSuggestion || recentPlaces.isEmpty()) {
|
if (!tripSuggestion || recentPlaces.isEmpty()) {
|
||||||
navigationType = NavigationType.VIEW
|
navigationType = NavigationType.VIEW
|
||||||
return navigationViewTemplate(
|
return navigationView(
|
||||||
createActionStripBuilder(
|
createActionStripBuilder(
|
||||||
{
|
{
|
||||||
createAction(
|
createAction(
|
||||||
@@ -291,6 +299,7 @@ open class NavigationScreen(
|
|||||||
.setContentTemplate(contentTemplate)
|
.setContentTemplate(contentTemplate)
|
||||||
.setActionStrip(
|
.setActionStrip(
|
||||||
mapActionStrip(
|
mapActionStrip(
|
||||||
|
carContext,
|
||||||
ViewStyle.VIEW,
|
ViewStyle.VIEW,
|
||||||
{ settingsAction() },
|
{ settingsAction() },
|
||||||
{
|
{
|
||||||
@@ -302,7 +311,7 @@ open class NavigationScreen(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
createAction(
|
createAction(
|
||||||
carContext = carContext, R.drawable.ic_zoom_out_24,
|
carContext = carContext, R.drawable.ic_recenter_24,
|
||||||
FLAG_IS_PERSISTENT,
|
FLAG_IS_PERSISTENT,
|
||||||
onClickAction = {
|
onClickAction = {
|
||||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||||
@@ -316,7 +325,7 @@ open class NavigationScreen(
|
|||||||
/**
|
/**
|
||||||
* Creates and returns a template for when the route is being recalculated.
|
* Creates and returns a template for when the route is being recalculated.
|
||||||
*/
|
*/
|
||||||
fun navigationRerouteTemplate(actionStripBuilder: ActionStrip.Builder): Template {
|
fun navigationReroute(actionStripBuilder: ActionStrip.Builder): Template {
|
||||||
return NavigationTemplate.Builder()
|
return NavigationTemplate.Builder()
|
||||||
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
|
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
|
||||||
.setActionStrip(actionStripBuilder.build())
|
.setActionStrip(actionStripBuilder.build())
|
||||||
@@ -356,6 +365,7 @@ open class NavigationScreen(
|
|||||||
surfaceRenderer,
|
surfaceRenderer,
|
||||||
place,
|
place,
|
||||||
navigationViewModel,
|
navigationViewModel,
|
||||||
|
showAlternativeRoute
|
||||||
)
|
)
|
||||||
) { obj: Any? ->
|
) { obj: Any? ->
|
||||||
if (obj != null) {
|
if (obj != null) {
|
||||||
@@ -461,36 +471,24 @@ open class NavigationScreen(
|
|||||||
/**
|
/**
|
||||||
* Initiates recalculation for a new route to the destination.
|
* Initiates recalculation for a new route to the destination.
|
||||||
*/
|
*/
|
||||||
fun calculateNewRoute(destination: Place) {
|
fun calculateNewRoute(destination: Place, distance: Float) {
|
||||||
|
deviation = distance
|
||||||
navigationType = NavigationType.REROUTE
|
navigationType = NavigationType.REROUTE
|
||||||
invalidate()
|
invalidate()
|
||||||
val mainThreadHandler = Handler(carContext.mainLooper)
|
val mainThreadHandler = Handler(carContext.mainLooper)
|
||||||
mainThreadHandler.post {
|
mainThreadHandler.post {
|
||||||
reRouteTimer?.cancel()
|
reRouteTimer?.cancel()
|
||||||
reRouteTimer = object : CountDownTimer(2000, 1000) {
|
reRouteTimer = object : CountDownTimer(3000, 1000) {
|
||||||
override fun onTick(millisUntilFinished: Long) {}
|
override fun onTick(millisUntilFinished: Long) {}
|
||||||
override fun onFinish() {
|
override fun onFinish() {
|
||||||
navigationType = NavigationType.NAVIGATION
|
navigationType = NavigationType.NAVIGATION
|
||||||
reRoute(destination)
|
listener.recalcRoute(destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reRouteTimer?.start()
|
reRouteTimer?.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-requests a route for the specified place.
|
|
||||||
*/
|
|
||||||
fun reRoute(place: Place) {
|
|
||||||
val destination = location(place.longitude, place.latitude)
|
|
||||||
navigationViewModel.loadRoute(
|
|
||||||
carContext,
|
|
||||||
surfaceRenderer.lastLocation,
|
|
||||||
destination,
|
|
||||||
surfaceRenderer.carOrientation
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates navigation state with the current location, checks for arrival, and traffic updates.
|
* Updates navigation state with the current location, checks for arrival, and traffic updates.
|
||||||
*/
|
*/
|
||||||
@@ -506,7 +504,8 @@ open class NavigationScreen(
|
|||||||
shouldShowNextStep: Boolean,
|
shouldShowNextStep: Boolean,
|
||||||
shouldShowLanes: Boolean,
|
shouldShowLanes: Boolean,
|
||||||
junctionImage: CarIcon?,
|
junctionImage: CarIcon?,
|
||||||
backGroundColor: CarColor
|
backGroundColor: CarColor,
|
||||||
|
message: String
|
||||||
) {
|
) {
|
||||||
this.isNavigating = isNavigating
|
this.isNavigating = isNavigating
|
||||||
this.isRerouting = isRerouting
|
this.isRerouting = isRerouting
|
||||||
@@ -520,7 +519,7 @@ open class NavigationScreen(
|
|||||||
this.shouldShowLanes = shouldShowLanes
|
this.shouldShowLanes = shouldShowLanes
|
||||||
this.junctionImage = junctionImage
|
this.junctionImage = junctionImage
|
||||||
this.backGroundColor = backGroundColor
|
this.backGroundColor = backGroundColor
|
||||||
|
this.message = message
|
||||||
navigationType = NavigationType.NAVIGATION
|
navigationType = NavigationType.NAVIGATION
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.text.Spannable
|
|||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.CarToast
|
import androidx.car.app.CarToast
|
||||||
import androidx.car.app.OnScreenResultListener
|
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
import androidx.car.app.model.Action
|
import androidx.car.app.model.Action
|
||||||
import androidx.car.app.model.CarIcon
|
import androidx.car.app.model.CarIcon
|
||||||
@@ -28,9 +27,9 @@ import com.kouros.navigation.data.Constants.CONTACTS
|
|||||||
import com.kouros.navigation.data.Constants.FAVORITES
|
import com.kouros.navigation.data.Constants.FAVORITES
|
||||||
import com.kouros.navigation.data.Constants.RECENT
|
import com.kouros.navigation.data.Constants.RECENT
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.ViewStyle
|
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import com.kouros.navigation.utils.getSettingsRepository
|
import com.kouros.navigation.utils.getSettingsRepository
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
|
|
||||||
class PlaceListScreen(
|
class PlaceListScreen(
|
||||||
private val carContext: CarContext,
|
private val carContext: CarContext,
|
||||||
@@ -48,14 +47,25 @@ class PlaceListScreen(
|
|||||||
|
|
||||||
private var routingEngine = 0
|
private var routingEngine = 0
|
||||||
|
|
||||||
|
private var showAlternativeRoute = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
||||||
routingEngine = it
|
routingEngine = it
|
||||||
})
|
})
|
||||||
|
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
|
||||||
|
showAlternativeRoute = it
|
||||||
|
})
|
||||||
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
navigationViewModel.recentPlaces.value = emptyList()
|
navigationViewModel.recentPlaces.value = emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
|
recentPlaces.forEach {
|
||||||
|
it.distance = location(it.longitude, it.latitude).distanceTo(surfaceRenderer.lastLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,42 +75,16 @@ class PlaceListScreen(
|
|||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
val itemListBuilder = ItemList.Builder()
|
val itemListBuilder = ItemList.Builder()
|
||||||
.setNoItemsMessage(carContext.getString(R.string.no_places))
|
.setNoItemsMessage(carContext.getString(R.string.no_places))
|
||||||
recentPlaces.filter { it.category == category }.forEach {
|
recentPlaces.filter { it.category == category && it.distance > 500F }.forEach {
|
||||||
val street = if (it.street != null) {
|
val street = it.street.ifEmpty {
|
||||||
it.street
|
it.name
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val row = Row.Builder()
|
val row = Row.Builder()
|
||||||
.setImage(contactIcon(null, it.category))
|
.setImage(contactIcon(null, it.category))
|
||||||
.setTitle("$street ${it.city}")
|
.setTitle("$street ${it.city}")
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
place = Place(
|
clickOnPlace(it)
|
||||||
0,
|
|
||||||
it.name,
|
|
||||||
it.category,
|
|
||||||
it.latitude,
|
|
||||||
it.longitude,
|
|
||||||
it.postalCode,
|
|
||||||
it.city,
|
|
||||||
it.street,
|
|
||||||
// avatar = null
|
|
||||||
)
|
|
||||||
screenManager
|
|
||||||
.pushForResult(
|
|
||||||
RoutePreviewScreen(
|
|
||||||
carContext,
|
|
||||||
RoutePreviewType.MULTI_ROUTE,
|
|
||||||
surfaceRenderer,
|
|
||||||
place,
|
|
||||||
navigationViewModel,
|
|
||||||
)
|
|
||||||
) { obj: Any? ->
|
|
||||||
if (obj != null) {
|
|
||||||
setResult(obj)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (category != CONTACTS) {
|
if (category != CONTACTS) {
|
||||||
row.addText(SpannableString(" ").apply {
|
row.addText(SpannableString(" ").apply {
|
||||||
@@ -138,6 +122,59 @@ class PlaceListScreen(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Action to navigate to a specific place.
|
||||||
|
*/
|
||||||
|
private fun clickOnPlace(itPlace: Place) {
|
||||||
|
if (surfaceRenderer.navigation) {
|
||||||
|
startStopOverScreen(itPlace)
|
||||||
|
} else {
|
||||||
|
starPreviewScreen(itPlace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts preview screen for a specific place.
|
||||||
|
*/
|
||||||
|
private fun starPreviewScreen(place: Place) {
|
||||||
|
screenManager
|
||||||
|
.pushForResult(
|
||||||
|
RoutePreviewScreen(
|
||||||
|
carContext,
|
||||||
|
if (showAlternativeRoute) RoutePreviewType.MULTI_ROUTE else RoutePreviewType.SINGLE_ROUTE,
|
||||||
|
surfaceRenderer,
|
||||||
|
place,
|
||||||
|
navigationViewModel,
|
||||||
|
showAlternativeRoute
|
||||||
|
)
|
||||||
|
) { obj: Any? ->
|
||||||
|
if (obj != null) {
|
||||||
|
setResult(obj)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts preview screen for a specific place.
|
||||||
|
*/
|
||||||
|
private fun startStopOverScreen(place: Place) {
|
||||||
|
screenManager
|
||||||
|
.pushForResult(
|
||||||
|
StopOverScreen(
|
||||||
|
carContext,
|
||||||
|
surfaceRenderer,
|
||||||
|
navigationViewModel,
|
||||||
|
place,
|
||||||
|
)
|
||||||
|
) { obj: Any? ->
|
||||||
|
if (obj != null) {
|
||||||
|
setResult(obj)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an Action to delete a place.
|
* Creates an Action to delete a place.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ import androidx.car.app.model.Template
|
|||||||
/** Screen for asking the user to grant location permission. */
|
/** Screen for asking the user to grant location permission. */
|
||||||
class RequestPermissionScreen(
|
class RequestPermissionScreen(
|
||||||
carContext: CarContext,
|
carContext: CarContext,
|
||||||
var permissionCheckCallback: PermissionCheckCallback,
|
val permissions: List<String?> = ArrayList(),
|
||||||
//var mContactsPermissionCheckCallback: LocationPermissionCheckCallback,
|
var permissionCheckCallback: PermissionCheckCallback
|
||||||
val permissions: MutableList<String?> = ArrayList()
|
|
||||||
) : Screen(carContext) {
|
) : Screen(carContext) {
|
||||||
|
|
||||||
/** Callback called when the permission is granted. */
|
/** Callback called when the permission is granted. */
|
||||||
@@ -29,7 +28,7 @@ class RequestPermissionScreen(
|
|||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
|
|
||||||
var message = ""
|
var message = "This app needs access to location"
|
||||||
if (permissions.contains(permission.ACCESS_FINE_LOCATION))
|
if (permissions.contains(permission.ACCESS_FINE_LOCATION))
|
||||||
message = "This app needs access to location and to car speed"
|
message = "This app needs access to location and to car speed"
|
||||||
if (permissions.contains("android.car.permission.CAR_SPEED"))
|
if (permissions.contains("android.car.permission.CAR_SPEED"))
|
||||||
@@ -83,7 +82,7 @@ fun checkPermission(carContext: CarContext, permission: String) : Boolean {
|
|||||||
screenManager.pop()
|
screenManager.pop()
|
||||||
return@RequestPermissionScreen
|
return@RequestPermissionScreen
|
||||||
},
|
},
|
||||||
permissions
|
permissions = permissions
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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_DEFAULT
|
||||||
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
|
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
|
||||||
import androidx.car.app.model.CarColor
|
import androidx.car.app.model.CarColor
|
||||||
import androidx.car.app.model.CarIcon
|
|
||||||
import androidx.car.app.model.CarText
|
import androidx.car.app.model.CarText
|
||||||
import androidx.car.app.model.DurationSpan
|
import androidx.car.app.model.DurationSpan
|
||||||
import androidx.car.app.model.ForegroundCarColorSpan
|
import androidx.car.app.model.ForegroundCarColorSpan
|
||||||
@@ -26,7 +25,6 @@ import androidx.car.app.model.Template
|
|||||||
import androidx.car.app.navigation.model.MapController
|
import androidx.car.app.navigation.model.MapController
|
||||||
import androidx.car.app.navigation.model.MapWithContentTemplate
|
import androidx.car.app.navigation.model.MapWithContentTemplate
|
||||||
import androidx.car.app.versioning.CarAppApiLevels
|
import androidx.car.app.versioning.CarAppApiLevels
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
@@ -35,6 +33,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.car.SurfaceRenderer
|
import com.kouros.navigation.car.SurfaceRenderer
|
||||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||||
|
import com.kouros.navigation.data.Constants.FAVORITES
|
||||||
import com.kouros.navigation.data.Constants.TAG
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.ViewStyle
|
import com.kouros.navigation.data.ViewStyle
|
||||||
@@ -55,9 +54,10 @@ class RoutePreviewScreen(
|
|||||||
private var surfaceRenderer: SurfaceRenderer,
|
private var surfaceRenderer: SurfaceRenderer,
|
||||||
private var destination: Place,
|
private var destination: Place,
|
||||||
private val navigationViewModel: NavigationViewModel,
|
private val navigationViewModel: NavigationViewModel,
|
||||||
|
private var showAlternativeRoute: Boolean
|
||||||
) :
|
) :
|
||||||
Screen(carContext) {
|
Screen(carContext) {
|
||||||
private var isFavorite = false
|
private var isFavorite = destination.favorite
|
||||||
|
|
||||||
val maxListItems: Int = 3
|
val maxListItems: Int = 3
|
||||||
|
|
||||||
@@ -71,6 +71,9 @@ class RoutePreviewScreen(
|
|||||||
|
|
||||||
var loading = true
|
var loading = true
|
||||||
|
|
||||||
|
var previewReady = false;
|
||||||
|
var flag = FLAG_DEFAULT
|
||||||
|
|
||||||
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
}
|
}
|
||||||
@@ -82,9 +85,15 @@ class RoutePreviewScreen(
|
|||||||
routeModel.startNavigation(route)
|
routeModel.startNavigation(route)
|
||||||
surfaceRenderer.setPreviewRouteData(routeModel)
|
surfaceRenderer.setPreviewRouteData(routeModel)
|
||||||
loading = false
|
loading = false
|
||||||
|
previewReady = true
|
||||||
|
if (routeModel.route.routes.size == 1 && showAlternativeRoute) {
|
||||||
|
routeType = RoutePreviewType.SINGLE_ROUTE
|
||||||
|
showAlternativeRoute = false
|
||||||
|
}
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val trafficObserver = Observer<Map<String, String>> { traffic ->
|
val trafficObserver = Observer<Map<String, String>> { traffic ->
|
||||||
if (traffic.isNotEmpty()) {
|
if (traffic.isNotEmpty()) {
|
||||||
navigationViewModel.traffic.value = emptyMap()
|
navigationViewModel.traffic.value = emptyMap()
|
||||||
@@ -108,7 +117,10 @@ class RoutePreviewScreen(
|
|||||||
})
|
})
|
||||||
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
||||||
routingEngine = it
|
routingEngine = it
|
||||||
|
})
|
||||||
|
|
||||||
|
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
|
||||||
|
showAlternativeRoute = it
|
||||||
})
|
})
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
navigationViewModel.loadPreviewRoute(
|
navigationViewModel.loadPreviewRoute(
|
||||||
@@ -117,7 +129,6 @@ class RoutePreviewScreen(
|
|||||||
location(destination.longitude, destination.latitude),
|
location(destination.longitude, destination.latitude),
|
||||||
surfaceRenderer.carOrientation
|
surfaceRenderer.carOrientation
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,28 +150,23 @@ class RoutePreviewScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val street = if (destination.street.isEmpty()) {
|
val street = destination.street.ifEmpty {
|
||||||
carContext.getString((R.string.route_preview))
|
carContext.getString((R.string.route_preview))
|
||||||
} else {
|
|
||||||
destination.street
|
|
||||||
}
|
}
|
||||||
val header = Header.Builder()
|
val header = Header.Builder()
|
||||||
.setStartHeaderAction(Action.BACK)
|
.setStartHeaderAction(Action.BACK)
|
||||||
.setTitle(street)
|
.setTitle(street)
|
||||||
|
|
||||||
if (routeType == RoutePreviewType.SINGLE_ROUTE) {
|
if (routeType == RoutePreviewType.SINGLE_ROUTE) {
|
||||||
header.addEndHeaderAction(
|
header.addEndHeaderAction(
|
||||||
favoriteAction()
|
favoriteAction()
|
||||||
)
|
)
|
||||||
header.addEndHeaderAction(
|
|
||||||
deleteFavoriteAction()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
val message =
|
val message =
|
||||||
if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) {
|
if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) {
|
||||||
createRouteText(routeModel.route.routes.first())
|
createRouteText(routeModel.route.routes.first())
|
||||||
} else {
|
} else {
|
||||||
CarText.Builder("Wait")
|
loading = true
|
||||||
|
CarText.Builder(carContext.getString(R.string.wait))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
val content = if (routeType == RoutePreviewType.MULTI_ROUTE) {
|
val content = if (routeType == RoutePreviewType.MULTI_ROUTE) {
|
||||||
@@ -174,18 +180,8 @@ class RoutePreviewScreen(
|
|||||||
.build()
|
.build()
|
||||||
listContent.build()
|
listContent.build()
|
||||||
} else {
|
} else {
|
||||||
val navigateActionIcon: CarIcon = CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
carContext, R.drawable.navigation_48px
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
val selectRouteIcon: CarIcon = CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
carContext, R.drawable.alt_route_48px
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
val navigateAction =
|
val navigateAction =
|
||||||
createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT,{
|
createAction(carContext, R.drawable.navigation_48px, flag,{
|
||||||
onNavigate(routeModel.navState.currentRouteIndex)
|
onNavigate(routeModel.navState.currentRouteIndex)
|
||||||
})
|
})
|
||||||
val selectRouteAction = createAction(carContext, R.drawable.alt_route_48px, FLAG_IS_PERSISTENT, {
|
val selectRouteAction = createAction(carContext, R.drawable.alt_route_48px, FLAG_IS_PERSISTENT, {
|
||||||
@@ -194,8 +190,13 @@ class RoutePreviewScreen(
|
|||||||
})
|
})
|
||||||
val listContent = MessageTemplate.Builder(message)
|
val listContent = MessageTemplate.Builder(message)
|
||||||
.setHeader(header.build())
|
.setHeader(header.build())
|
||||||
.addAction(navigateAction)
|
|
||||||
.addAction(selectRouteAction)
|
if (previewReady) {
|
||||||
|
listContent.addAction(navigateAction)
|
||||||
|
}
|
||||||
|
if (showAlternativeRoute) {
|
||||||
|
listContent.addAction(selectRouteAction)
|
||||||
|
}
|
||||||
if (loading) {
|
if (loading) {
|
||||||
listContent.setLoading(true)
|
listContent.setLoading(true)
|
||||||
}
|
}
|
||||||
@@ -206,12 +207,13 @@ class RoutePreviewScreen(
|
|||||||
.setContentTemplate(content)
|
.setContentTemplate(content)
|
||||||
.setMapController(
|
.setMapController(
|
||||||
MapController.Builder().setMapActionStrip(
|
MapController.Builder().setMapActionStrip(
|
||||||
mapActionStrip(ViewStyle.PREVIEW, {zoomPlus()}, { zoomMinus()}, {
|
mapActionStrip(carContext, ViewStyle.PREVIEW, {zoomPlus()}, { zoomMinus()}, {
|
||||||
zoomMinus()
|
zoomMinus()
|
||||||
} )).build()
|
} )).build()
|
||||||
|
|
||||||
)
|
)
|
||||||
if (routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) {
|
if (routeModel.route.routes.size > 1 && routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) {
|
||||||
|
if (previewReady) {
|
||||||
template.setActionStrip(createActionStrip {
|
template.setActionStrip(createActionStrip {
|
||||||
createAction(
|
createAction(
|
||||||
carContext, R.drawable.navigation_48px,
|
carContext, R.drawable.navigation_48px,
|
||||||
@@ -221,6 +223,7 @@ class RoutePreviewScreen(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return template.build()
|
return template.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +232,7 @@ class RoutePreviewScreen(
|
|||||||
carContext, R.drawable.ic_zoom_in_24,
|
carContext, R.drawable.ic_zoom_in_24,
|
||||||
FLAG_IS_PERSISTENT,
|
FLAG_IS_PERSISTENT,
|
||||||
onClickAction = {
|
onClickAction = {
|
||||||
|
flag = FLAG_IS_PERSISTENT
|
||||||
surfaceRenderer.handleScale(1)
|
surfaceRenderer.handleScale(1)
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
@@ -243,6 +247,7 @@ class RoutePreviewScreen(
|
|||||||
carContext, R.drawable.ic_zoom_out_24,
|
carContext, R.drawable.ic_zoom_out_24,
|
||||||
FLAG_IS_PERSISTENT,
|
FLAG_IS_PERSISTENT,
|
||||||
onClickAction = {
|
onClickAction = {
|
||||||
|
flag = FLAG_IS_PERSISTENT
|
||||||
surfaceRenderer.handleScale(-1)
|
surfaceRenderer.handleScale(-1)
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
@@ -256,34 +261,17 @@ class RoutePreviewScreen(
|
|||||||
else
|
else
|
||||||
R.drawable.ic_favorite_white_24dp
|
R.drawable.ic_favorite_white_24dp
|
||||||
, FLAG_IS_PERSISTENT,
|
, FLAG_IS_PERSISTENT,
|
||||||
) {
|
onClickAction = {
|
||||||
isFavorite = !isFavorite
|
isFavorite = !isFavorite
|
||||||
CarToast.makeText(
|
destination.favorite = isFavorite
|
||||||
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) {
|
if (isFavorite) {
|
||||||
|
navigationViewModel.saveFavorite(carContext, destination)
|
||||||
|
} else {
|
||||||
navigationViewModel.deleteFavorite(carContext, destination)
|
navigationViewModel.deleteFavorite(carContext, destination)
|
||||||
}
|
}
|
||||||
isFavorite = !isFavorite
|
invalidate()
|
||||||
finish()
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
|
|
||||||
private fun createRouteText(route: Routes): CarText {
|
private fun createRouteText(route: Routes): CarText {
|
||||||
val time = route.summary.duration
|
val time = route.summary.duration
|
||||||
@@ -301,7 +289,7 @@ class RoutePreviewScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createRow(route: Routes, index: Int): Row {
|
private fun createRow(route: Routes, index: Int): Row {
|
||||||
val navigateAction = createAction(carContext, R.drawable.navigation_48px ) {
|
val navigateAction = createAction(carContext, R.drawable.navigation_48px, flag ) {
|
||||||
this.onNavigate(index)
|
this.onNavigate(index)
|
||||||
}
|
}
|
||||||
val routeText = createRouteText(route)
|
val routeText = createRouteText(route)
|
||||||
@@ -318,7 +306,9 @@ class RoutePreviewScreen(
|
|||||||
.setTitle(routeText)
|
.setTitle(routeText)
|
||||||
.setOnClickListener { onRouteSelected(index) }
|
.setOnClickListener { onRouteSelected(index) }
|
||||||
.addText(street)
|
.addText(street)
|
||||||
.addAction(navigateAction)
|
if (previewReady) {
|
||||||
|
row.addAction(navigateAction)
|
||||||
|
}
|
||||||
if (route.summary.trafficDelay > 60) {
|
if (route.summary.trafficDelay > 60) {
|
||||||
row.addText(createDelay(route))
|
row.addText(createDelay(route))
|
||||||
row.setImage(createCarIcon(carContext = carContext, R.drawable.traffic_jam_48px))
|
row.setImage(createCarIcon(carContext = carContext, R.drawable.traffic_jam_48px))
|
||||||
@@ -344,11 +334,13 @@ class RoutePreviewScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onNavigate(index: Int) {
|
private fun onNavigate(index: Int) {
|
||||||
|
if (previewReady) {
|
||||||
destination.routeIndex = index
|
destination.routeIndex = index
|
||||||
destination.route = navigationViewModel.previewRoute.value.toString()
|
destination.route = navigationViewModel.previewRoute.value.toString()
|
||||||
setResult(destination)
|
setResult(destination)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onRouteSelected(index: Int) {
|
private fun onRouteSelected(index: Int) {
|
||||||
routeModel.navState = routeModel.navState.copy(currentRouteIndex = index)
|
routeModel.navState = routeModel.navState.copy(currentRouteIndex = index)
|
||||||
@@ -364,7 +356,6 @@ class RoutePreviewScreen(
|
|||||||
surfaceRenderer.carOrientation
|
surfaceRenderer.carOrientation
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class RoutePreviewType {
|
enum class RoutePreviewType {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.car.app.model.CarIcon
|
|||||||
import androidx.car.app.model.Row
|
import androidx.car.app.model.Row
|
||||||
import androidx.core.graphics.createBitmap
|
import androidx.core.graphics.createBitmap
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.data.Constants.CHARGING_STATION
|
import com.kouros.navigation.data.Constants.CHARGING_STATION
|
||||||
import com.kouros.navigation.data.Constants.FUEL_STATION
|
import com.kouros.navigation.data.Constants.FUEL_STATION
|
||||||
import com.kouros.navigation.data.Constants.PHARMACY
|
import com.kouros.navigation.data.Constants.PHARMACY
|
||||||
@@ -49,7 +50,7 @@ fun createNumberIcon(category: String, number: String): IconCompat {
|
|||||||
CHARGING_STATION -> Color.GREEN
|
CHARGING_STATION -> Color.GREEN
|
||||||
FUEL_STATION -> Color.BLUE
|
FUEL_STATION -> Color.BLUE
|
||||||
PHARMACY -> Color.RED
|
PHARMACY -> Color.RED
|
||||||
else -> Color.WHITE
|
else -> Color.MAGENTA
|
||||||
}
|
}
|
||||||
paint.color = color
|
paint.color = color
|
||||||
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
|
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
|
||||||
@@ -82,20 +83,28 @@ fun createActionStripBuilder(action1: () -> Action, action2: () -> Action): Acti
|
|||||||
* Creates an ActionStrip builder for map-related actions like zoom and pan.
|
* Creates an ActionStrip builder for map-related actions like zoom and pan.
|
||||||
*/
|
*/
|
||||||
fun mapActionStrip(
|
fun mapActionStrip(
|
||||||
|
carContext: CarContext,
|
||||||
viewStyle: ViewStyle,
|
viewStyle: ViewStyle,
|
||||||
zoomPlus: () -> Action,
|
zoomPlus: () -> Action,
|
||||||
zoomMinus: () -> Action,
|
zoomMinus: () -> Action,
|
||||||
panAction: () -> Action
|
recenterAction: () -> Action
|
||||||
): ActionStrip {
|
): ActionStrip {
|
||||||
val actionStripBuilder = ActionStrip.Builder()
|
val actionStripBuilder = ActionStrip.Builder()
|
||||||
.addAction(zoomPlus())
|
.addAction(zoomPlus())
|
||||||
.addAction(zoomMinus())
|
.addAction(zoomMinus())
|
||||||
|
actionStripBuilder.addAction(
|
||||||
|
Action.Builder(Action.PAN)
|
||||||
|
.setIcon(createCarIcon(carContext, R.drawable.ic_pan_24))
|
||||||
|
.setFlags(0)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
if (viewStyle == ViewStyle.PAN_VIEW) {
|
if (viewStyle == ViewStyle.PAN_VIEW) {
|
||||||
actionStripBuilder
|
actionStripBuilder
|
||||||
.addAction(
|
.addAction(
|
||||||
panAction()
|
recenterAction()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return actionStripBuilder.build()
|
return actionStripBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +115,7 @@ fun createAction(
|
|||||||
carContext: CarContext,
|
carContext: CarContext,
|
||||||
@DrawableRes iconRes: Int,
|
@DrawableRes iconRes: Int,
|
||||||
flag: Int = FLAG_DEFAULT,
|
flag: Int = FLAG_DEFAULT,
|
||||||
onClickAction: () -> Unit
|
onClickAction: () -> Unit,
|
||||||
): Action {
|
): Action {
|
||||||
return Action.Builder()
|
return Action.Builder()
|
||||||
.setIcon(createCarIcon(carContext, iconRes))
|
.setIcon(createCarIcon(carContext, iconRes))
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import com.kouros.navigation.data.Constants.CATEGORIES
|
|||||||
import com.kouros.navigation.data.Constants.FAVORITES
|
import com.kouros.navigation.data.Constants.FAVORITES
|
||||||
import com.kouros.navigation.data.Constants.RECENT
|
import com.kouros.navigation.data.Constants.RECENT
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.ViewStyle
|
|
||||||
import com.kouros.navigation.data.nominatim.SearchResult
|
import com.kouros.navigation.data.nominatim.SearchResult
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
|
|
||||||
@@ -86,22 +85,7 @@ class SearchScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
screenManager
|
startPlaceListScreen(it)
|
||||||
.pushForResult(
|
|
||||||
PlaceListScreen(
|
|
||||||
carContext,
|
|
||||||
surfaceRenderer,
|
|
||||||
it.id,
|
|
||||||
navigationViewModel,
|
|
||||||
recentPlaces
|
|
||||||
)
|
|
||||||
) { obj: Any? ->
|
|
||||||
surfaceRenderer.setStandardView()
|
|
||||||
if (obj != null) {
|
|
||||||
setResult(obj)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setBrowsable(true)
|
.setBrowsable(true)
|
||||||
@@ -131,6 +115,24 @@ class SearchScreen(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startPlaceListScreen(it: Category) {
|
||||||
|
screenManager
|
||||||
|
.pushForResult(
|
||||||
|
PlaceListScreen(
|
||||||
|
carContext,
|
||||||
|
surfaceRenderer,
|
||||||
|
it.id,
|
||||||
|
navigationViewModel,
|
||||||
|
recentPlaces
|
||||||
|
)
|
||||||
|
) { obj: Any? ->
|
||||||
|
surfaceRenderer.setStandardView()
|
||||||
|
if (obj != null) {
|
||||||
|
setResult(obj)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
fun categoryIcon(category: String?): CarIcon {
|
fun categoryIcon(category: String?): CarIcon {
|
||||||
val resId: Int = when (category) {
|
val resId: Int = when (category) {
|
||||||
RECENT -> {
|
RECENT -> {
|
||||||
@@ -182,7 +184,28 @@ class SearchScreen(
|
|||||||
distance = result.distance
|
distance = result.distance
|
||||||
)
|
)
|
||||||
recentPlaces.add(place)
|
recentPlaces.add(place)
|
||||||
|
startPreviewScreen(place)
|
||||||
setResult(place)
|
setResult(place)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startPreviewScreen(it: Place) {
|
||||||
|
screenManager
|
||||||
|
.pushForResult(
|
||||||
|
RoutePreviewScreen(
|
||||||
|
carContext,
|
||||||
|
RoutePreviewType.SINGLE_ROUTE,
|
||||||
|
surfaceRenderer,
|
||||||
|
destination = it,
|
||||||
|
navigationViewModel,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
) { obj: Any? ->
|
||||||
|
surfaceRenderer.setStandardView()
|
||||||
|
if (obj != null) {
|
||||||
|
setResult(obj)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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.Action.FLAG_DEFAULT
|
||||||
|
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
|
||||||
|
import androidx.car.app.model.CarText
|
||||||
|
import androidx.car.app.model.Header
|
||||||
|
import androidx.car.app.model.MessageTemplate
|
||||||
|
import androidx.car.app.model.Template
|
||||||
|
import androidx.car.app.navigation.model.MapController
|
||||||
|
import androidx.car.app.navigation.model.MapWithContentTemplate
|
||||||
|
import com.kouros.data.R
|
||||||
|
import com.kouros.navigation.car.SurfaceRenderer
|
||||||
|
import com.kouros.navigation.data.Place
|
||||||
|
import com.kouros.navigation.data.ViewStyle
|
||||||
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
|
|
||||||
|
class StopOverScreen(
|
||||||
|
private val carContext: CarContext,
|
||||||
|
private val surfaceRenderer: SurfaceRenderer,
|
||||||
|
private val navigationViewModel: NavigationViewModel,
|
||||||
|
private val place: Place,
|
||||||
|
) : Screen(carContext) {
|
||||||
|
override fun onGetTemplate(): MapWithContentTemplate {
|
||||||
|
val cancelAction =
|
||||||
|
createAction(carContext, R.drawable.ic_close_white_24dp, FLAG_IS_PERSISTENT,{
|
||||||
|
finish()
|
||||||
|
})
|
||||||
|
val header = Header.Builder()
|
||||||
|
.setStartHeaderAction(Action.BACK)
|
||||||
|
.addEndHeaderAction(cancelAction)
|
||||||
|
.setTitle(place.street)
|
||||||
|
|
||||||
|
val message = CarText.Builder("Neue Fahrt oder Zwischenstopp einfügen")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val navigateAction = Action.Builder()
|
||||||
|
.setIcon(createCarIcon(carContext, R.drawable.navigation_48px))
|
||||||
|
.setFlags(FLAG_DEFAULT)
|
||||||
|
.setOnClickListener {
|
||||||
|
setResult(place)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val selectRouteAction = Action.Builder()
|
||||||
|
.setIcon(createCarIcon(carContext, R.drawable.alt_route_48px))
|
||||||
|
.setFlags(FLAG_IS_PERSISTENT)
|
||||||
|
.setOnClickListener {
|
||||||
|
place.stopOver = true
|
||||||
|
setResult(place)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val listContent = MessageTemplate.Builder(message)
|
||||||
|
.setHeader(header.build())
|
||||||
|
.addAction(navigateAction)
|
||||||
|
.addAction(selectRouteAction)
|
||||||
|
|
||||||
|
val template = MapWithContentTemplate.Builder()
|
||||||
|
.setContentTemplate(listContent.build())
|
||||||
|
|
||||||
|
return template.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,4 +29,6 @@ interface NavigationObserverCallback {
|
|||||||
/** Called to request UI invalidation/refresh */
|
/** Called to request UI invalidation/refresh */
|
||||||
fun invalidateScreen()
|
fun invalidateScreen()
|
||||||
|
|
||||||
|
fun onTrafficMessageReceived(trafficMessage: String)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class NavigationObserverManager(
|
|||||||
|
|
||||||
val routeObserver = RouteObserver(callback)
|
val routeObserver = RouteObserver(callback)
|
||||||
val trafficObserver = TrafficObserver(callback)
|
val trafficObserver = TrafficObserver(callback)
|
||||||
|
|
||||||
|
val trafficMessageObserver = TrafficMessageObserver(callback)
|
||||||
val placeSearchObserver = PlaceSearchObserver(callback)
|
val placeSearchObserver = PlaceSearchObserver(callback)
|
||||||
val speedCameraObserver = SpeedCameraObserver(callback)
|
val speedCameraObserver = SpeedCameraObserver(callback)
|
||||||
val maxSpeedObserver = MaxSpeedObserver(callback)
|
val maxSpeedObserver = MaxSpeedObserver(callback)
|
||||||
@@ -21,6 +23,7 @@ class NavigationObserverManager(
|
|||||||
fun attachAllObservers(session: NavigationSession) {
|
fun attachAllObservers(session: NavigationSession) {
|
||||||
viewModel.route.observe(session, routeObserver)
|
viewModel.route.observe(session, routeObserver)
|
||||||
viewModel.traffic.observe(session, trafficObserver)
|
viewModel.traffic.observe(session, trafficObserver)
|
||||||
|
viewModel.trafficMessage.observe(session, trafficMessageObserver)
|
||||||
viewModel.placeLocation.observe(session, placeSearchObserver)
|
viewModel.placeLocation.observe(session, placeSearchObserver)
|
||||||
viewModel.speedCameras.observe(session, speedCameraObserver)
|
viewModel.speedCameras.observe(session, speedCameraObserver)
|
||||||
viewModel.maxSpeed.observe(session, maxSpeedObserver)
|
viewModel.maxSpeed.observe(session, maxSpeedObserver)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,8 @@ class NavigationSettings(
|
|||||||
|
|
||||||
private var carLocationToggleState = false
|
private var carLocationToggleState = false
|
||||||
|
|
||||||
|
private var alternativeRoutesToggleState = false
|
||||||
|
|
||||||
val settingsViewModel = getSettingsViewModel(carContext)
|
val settingsViewModel = getSettingsViewModel(carContext)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -41,6 +43,7 @@ class NavigationSettings(
|
|||||||
settingsViewModel.avoidMotorway.first()
|
settingsViewModel.avoidMotorway.first()
|
||||||
settingsViewModel.avoidFerry.first()
|
settingsViewModel.avoidFerry.first()
|
||||||
settingsViewModel.carLocation.first()
|
settingsViewModel.carLocation.first()
|
||||||
|
settingsViewModel.alternativeRoutes.first()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +52,7 @@ class NavigationSettings(
|
|||||||
tollWayToggleState = settingsViewModel.avoidTollway.value
|
tollWayToggleState = settingsViewModel.avoidTollway.value
|
||||||
ferryToggleState = settingsViewModel.avoidFerry.value
|
ferryToggleState = settingsViewModel.avoidFerry.value
|
||||||
carLocationToggleState = settingsViewModel.carLocation.value
|
carLocationToggleState = settingsViewModel.carLocation.value
|
||||||
|
alternativeRoutesToggleState = settingsViewModel.alternativeRoutes.value
|
||||||
|
|
||||||
val listBuilder = ItemList.Builder()
|
val listBuilder = ItemList.Builder()
|
||||||
|
|
||||||
@@ -89,6 +93,14 @@ class NavigationSettings(
|
|||||||
carLocationToggleState = !carLocationToggleState
|
carLocationToggleState = !carLocationToggleState
|
||||||
}.setChecked(carLocationToggleState).build()
|
}.setChecked(carLocationToggleState).build()
|
||||||
|
|
||||||
|
// Alternative routes
|
||||||
|
val alternativeRoutesToggle: Toggle =
|
||||||
|
Toggle.Builder { checked: Boolean ->
|
||||||
|
settingsViewModel.onAlternativeRoutes(checked)
|
||||||
|
alternativeRoutesToggleState = !alternativeRoutesToggleState
|
||||||
|
}.setChecked(alternativeRoutesToggleState).build()
|
||||||
|
|
||||||
|
|
||||||
listBuilder.addItem(
|
listBuilder.addItem(
|
||||||
buildRowForTemplate(
|
buildRowForTemplate(
|
||||||
R.string.use_car_location,
|
R.string.use_car_location,
|
||||||
@@ -97,6 +109,14 @@ class NavigationSettings(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
listBuilder.addItem(
|
||||||
|
buildRowForTemplate(
|
||||||
|
R.string.alternative_routes,
|
||||||
|
alternativeRoutesToggle,
|
||||||
|
createCarIcon(carContext,R.drawable.alt_route_48px)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
listBuilder.addItem(
|
listBuilder.addItem(
|
||||||
buildRowForScreenTemplate(
|
buildRowForScreenTemplate(
|
||||||
RoutingSettings(carContext, navigationViewModel),
|
RoutingSettings(carContext, navigationViewModel),
|
||||||
@@ -109,6 +129,7 @@ class NavigationSettings(
|
|||||||
R.string.tomtom_api_key
|
R.string.tomtom_api_key
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return ListTemplate.Builder()
|
return ListTemplate.Builder()
|
||||||
.setSingleList(listBuilder.build())
|
.setSingleList(listBuilder.build())
|
||||||
.setHeader(
|
.setHeader(
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.kouros.navigation.car.screen.settings
|
package com.kouros.navigation.car.screen.settings
|
||||||
|
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
|
import androidx.car.app.annotations.ExperimentalCarApi
|
||||||
import androidx.car.app.model.Action
|
import androidx.car.app.model.Action
|
||||||
import androidx.car.app.model.CarIcon
|
import androidx.car.app.model.CarIcon
|
||||||
import androidx.car.app.model.Header
|
import androidx.car.app.model.Header
|
||||||
@@ -16,6 +18,7 @@ import androidx.lifecycle.Observer
|
|||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
|
import com.kouros.navigation.car.screen.CarHardwareInfoScreen
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import com.kouros.navigation.utils.getSettingsViewModel
|
import com.kouros.navigation.utils.getSettingsViewModel
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
@@ -37,6 +40,7 @@ class SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCarApi::class)
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
settingsViewModel.guidanceAudio.asLiveData().observe(this, Observer {
|
settingsViewModel.guidanceAudio.asLiveData().observe(this, Observer {
|
||||||
audioToggleState = settingsViewModel.guidanceAudio.value == 1
|
audioToggleState = settingsViewModel.guidanceAudio.value == 1
|
||||||
@@ -94,6 +98,13 @@ class SettingsScreen(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
listBuilder.addItem(
|
||||||
|
buildRowForTemplate(
|
||||||
|
CarHardwareInfoScreen(carContext),
|
||||||
|
R.string.model_info
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
listBuilder.addItem(
|
listBuilder.addItem(
|
||||||
buildRowForTemplate(
|
buildRowForTemplate(
|
||||||
CarSettings(carContext, navigationViewModel),
|
CarSettings(carContext, navigationViewModel),
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.kouros.navigation.car.screen
|
|||||||
|
|
||||||
import androidx.car.app.testing.ScreenController
|
import androidx.car.app.testing.ScreenController
|
||||||
import androidx.car.app.testing.TestCarContext
|
import androidx.car.app.testing.TestCarContext
|
||||||
import androidx.car.app.navigation.model.NavigationTemplate
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import com.kouros.navigation.car.SurfaceRenderer
|
import com.kouros.navigation.car.SurfaceRenderer
|
||||||
@@ -63,7 +62,6 @@ class NavigationScreenTest {
|
|||||||
navigationScreen = NavigationScreen(
|
navigationScreen = NavigationScreen(
|
||||||
testCarContext,
|
testCarContext,
|
||||||
mockSurfaceRenderer,
|
mockSurfaceRenderer,
|
||||||
mockRouteModel,
|
|
||||||
mockListener,
|
mockListener,
|
||||||
mockViewModel
|
mockViewModel
|
||||||
)
|
)
|
||||||
@@ -109,7 +107,7 @@ class NavigationScreenTest {
|
|||||||
navigationScreen.navigationType = NavigationType.NAVIGATION
|
navigationScreen.navigationType = NavigationType.NAVIGATION
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
navigationScreen.calculateNewRoute(Place())
|
navigationScreen.calculateNewRoute(Place(), distance)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.REROUTE)
|
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.REROUTE)
|
||||||
@@ -120,14 +118,14 @@ class NavigationScreenTest {
|
|||||||
// Arrange
|
// Arrange
|
||||||
navigationScreen.navigationType = NavigationType.NAVIGATION
|
navigationScreen.navigationType = NavigationType.NAVIGATION
|
||||||
|
|
||||||
`when`(mockRouteModel.isArrival()).thenReturn(true)
|
`when`(mockRouteModel.isManeuverArrival()).thenReturn(true)
|
||||||
`when`(mockRouteModel.routeCalculator).thenReturn(mockRouteCalculator)
|
`when`(mockRouteModel.routeCalculator).thenReturn(mockRouteCalculator)
|
||||||
`when`(mockRouteCalculator.leftStepDistance()).thenReturn(19.0)
|
`when`(mockRouteCalculator.leftStepDistance()).thenReturn(9.0)
|
||||||
`when`(mockRouteModel.navState).thenReturn(NavigationState())
|
`when`(mockRouteModel.navState).thenReturn(NavigationState())
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
navigationScreen.checkArrival()
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.ARRIVAL)
|
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.NAVIGATION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ class CategoryObserverTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createElement(lon: Double, lat: Double): Elements {
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ class ObserversTest {
|
|||||||
return Elements(
|
return Elements(
|
||||||
lon = lon,
|
lon = lon,
|
||||||
lat = lat,
|
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 {
|
plugins {
|
||||||
alias(libs.plugins.android.library)
|
alias(libs.plugins.android.library)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
@@ -8,12 +12,24 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.kouros.data"
|
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 {
|
defaultConfig {
|
||||||
minSdk = 33
|
minSdk = 33
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
consumerProguardFiles("consumer-rules.pro")
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
|
|
||||||
|
buildConfigField("String", "USER", "\"${properties.getProperty("USER") ?: ""}\"")
|
||||||
|
buildConfigField("String", "PASSWORD", "\"${properties.getProperty("PASSWORD") ?: ""}\"")
|
||||||
|
buildConfigField("String", "TANKER_KOENIG_API_KEY", "\"${properties.getProperty("TANKER_KOENIG_API_KEY") ?: ""}\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,24 @@ package com.kouros.navigation.data
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
val NavigationColorLight = Color(0xFF066462)
|
val NavigationColorLight = Color(0xFF17A119)
|
||||||
|
val NavigationColorDark = Color(0xFF2B007A)
|
||||||
|
|
||||||
val NavigationColorDark = Color(0xFF10DED9)
|
val NavigationCircle = Color(0xFFFFEB3B)
|
||||||
|
|
||||||
val RouteColor = Color(0xFF7B06E1)
|
val RouteColor = Color(0xFF5201B4)
|
||||||
|
|
||||||
val SpeedColor = Color(0xFF262525)
|
val SpeedColor = Color(0xFF262525)
|
||||||
|
|
||||||
val MaxSpeedColor = Color(0xFFB71515)
|
val MaxSpeedColor = Color(0xFFB71515)
|
||||||
|
|
||||||
val PlaceColor = Color(0xFF868005)
|
val PlaceColor = Color(0xFF868005)
|
||||||
|
|
||||||
|
val QueuingColor = Color(0xFFC46E53)
|
||||||
|
val SlowColor = Color(0xFFC43E3E)
|
||||||
|
val StationaryColor = Color(0xFF910A0A)
|
||||||
|
val HeavyColor = Color(0xFF6B0404)
|
||||||
|
val RoadworksColor = Color(0xFFDAA707)
|
||||||
|
val LaneColor = Color(0xFFEFBAC1)
|
||||||
|
|
||||||
|
val PharmacyColor = Color(0xFF054603)
|
||||||
@@ -16,16 +16,20 @@
|
|||||||
|
|
||||||
package com.kouros.navigation.data
|
package com.kouros.navigation.data
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import com.google.gson.annotations.Expose
|
||||||
import com.kouros.navigation.data.route.Lane
|
import com.kouros.navigation.data.route.Lane
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
|
||||||
data class Category(
|
data class Category(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class StepMatch(val stepIndex: Int, val waypointIndex: Int, val location: Location)
|
||||||
|
|
||||||
data class Places(
|
data class Places(
|
||||||
val places: List<Place>,
|
val places: List<Place>,
|
||||||
@@ -41,11 +45,19 @@ data class Place(
|
|||||||
var postalCode: String = "",
|
var postalCode: String = "",
|
||||||
var city: String = "",
|
var city: String = "",
|
||||||
var street: String = "",
|
var street: String = "",
|
||||||
|
var navigations: Int = 0,
|
||||||
|
@Transient
|
||||||
var distance: Float = 0F,
|
var distance: Float = 0F,
|
||||||
//var avatar: Uri? = null,
|
//var avatar: Uri? = null,
|
||||||
var lastDate: Long = 0,
|
var lastDate: Long = 0,
|
||||||
|
@Transient
|
||||||
var routeIndex: Int = 0,
|
var routeIndex: Int = 0,
|
||||||
|
@Transient
|
||||||
var route: String = "",
|
var route: String = "",
|
||||||
|
@Transient
|
||||||
|
var stopOver: Boolean = false,
|
||||||
|
@Transient
|
||||||
|
var favorite: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ContactData(
|
data class ContactData(
|
||||||
@@ -66,6 +78,8 @@ data class StepData (
|
|||||||
var lane: List<Lane> = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList(), 0, 0)),
|
var lane: List<Lane> = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList(), 0, 0)),
|
||||||
var exitNumber: Int = 0,
|
var exitNumber: Int = 0,
|
||||||
var message: String = "",
|
var message: String = "",
|
||||||
|
var roadNumbers: List<String> = emptyList(),
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -112,33 +126,51 @@ object Constants {
|
|||||||
|
|
||||||
const val CHARGING_STATION: String ="charging_station"
|
const val CHARGING_STATION: String ="charging_station"
|
||||||
|
|
||||||
|
const val RESTAURANT: String ="restaurant"
|
||||||
|
|
||||||
val categories = listOf("Tankstelle", "Apotheke", "Ladestationen")
|
|
||||||
/** The initial location to use as an anchor for searches. */
|
/** The initial location to use as an anchor for searches. */
|
||||||
|
|
||||||
|
val widenmayer = location( 11.595721, 48.146113)
|
||||||
val homeVogelhart = location(11.5793748, 48.185749)
|
val homeVogelhart = location(11.5793748, 48.185749)
|
||||||
val homeHohenwaldeck = location( 11.594322, 48.1164817)
|
val homeHohenwaldeck = location( 11.594322, 48.1164817)
|
||||||
|
val ioannina = location( 20.826237, 39.690174)
|
||||||
|
val subislawa = location(18.570808, 54.420647)
|
||||||
|
val a94 = location(11.947101,48.213251)
|
||||||
|
val a9 = location(11.621556, 48.204402,)
|
||||||
|
|
||||||
|
val a22 = location(10.845749, 45.602507)
|
||||||
|
val isarring = location( 11.609696, 48.155116)
|
||||||
const val NEXT_STEP_THRESHOLD = 500.0
|
const val NEXT_STEP_THRESHOLD = 500.0
|
||||||
|
|
||||||
const val MAXIMAL_SNAP_CORRECTION = 50.0
|
const val MAXIMAL_SNAP_CORRECTION = 50.0
|
||||||
|
|
||||||
const val MAXIMAL_ROUTE_DEVIATION = 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 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_UPDATE = 300
|
||||||
|
|
||||||
|
const val TRAFFIC_MESSAGE_UPDATE = 10
|
||||||
|
|
||||||
|
const val SPEED_UPDATE_DISTANCE = 600F
|
||||||
|
|
||||||
const val INSTRUCTION_DISTANCE = 50
|
const val INSTRUCTION_DISTANCE = 50
|
||||||
|
|
||||||
|
const val SPEED_BEARING_DEVIATION = 60
|
||||||
|
|
||||||
const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED"
|
const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED"
|
||||||
|
|
||||||
const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED"
|
const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED"
|
||||||
|
|
||||||
const val TILT = 60.0
|
const val TILT = 60.0
|
||||||
|
|
||||||
|
const val TANKER_KOENIG_DELAY = 300000
|
||||||
|
|
||||||
|
const val LAST_CHECK_ROUTE_DISTANCE = 5000
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,6 +189,10 @@ enum class RouteEngine {
|
|||||||
VALHALLA, OSRM, TOMTOM
|
VALHALLA, OSRM, TOMTOM
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class DarkMode {
|
||||||
|
LIGHT, DARK, USE_CAR
|
||||||
|
}
|
||||||
|
|
||||||
enum class EngineType {
|
enum class EngineType {
|
||||||
COMBUSTION, ELECTRIC
|
COMBUSTION, ELECTRIC
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import java.net.URL
|
|||||||
|
|
||||||
abstract class NavigationRepository {
|
abstract class NavigationRepository {
|
||||||
|
|
||||||
|
private val config by lazy { ApplicationConfig.load() }
|
||||||
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
|
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
|
||||||
|
|
||||||
//private val nominatimUrl = "https://kouros-online.de/nominatim/"
|
//private val nominatimUrl = "https://kouros-online.de/nominatim/"
|
||||||
@@ -20,7 +21,7 @@ abstract class NavigationRepository {
|
|||||||
abstract fun getRoute(
|
abstract fun getRoute(
|
||||||
context: Context,
|
context: Context,
|
||||||
currentLocation: Location,
|
currentLocation: Location,
|
||||||
destination: Location,
|
location: List<Location>,
|
||||||
carOrientation: Float,
|
carOrientation: Float,
|
||||||
searchFilter: SearchFilter
|
searchFilter: SearchFilter
|
||||||
): String
|
): String
|
||||||
@@ -61,12 +62,16 @@ abstract class NavigationRepository {
|
|||||||
try {
|
try {
|
||||||
if (authenticator) {
|
if (authenticator) {
|
||||||
Authenticator.setDefault(object : Authenticator() {
|
Authenticator.setDefault(object : Authenticator() {
|
||||||
override fun getPasswordAuthentication(): PasswordAuthentication {
|
override fun getPasswordAuthentication(): PasswordAuthentication? {
|
||||||
return PasswordAuthentication(
|
return if (config.user.isEmpty() || config.password.isEmpty()) {
|
||||||
"kouros",
|
null
|
||||||
"eo7sbjyWpmjSVFyELgbfrryqJ6ddNeq9".toCharArray()
|
} else {
|
||||||
|
PasswordAuthentication(
|
||||||
|
config.user,
|
||||||
|
config.password.toCharArray()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Log.d("NavigationRepository", url)
|
Log.d("NavigationRepository", url)
|
||||||
@@ -75,7 +80,7 @@ abstract class NavigationRepository {
|
|||||||
"Accept",
|
"Accept",
|
||||||
"application/json"
|
"application/json"
|
||||||
) // The format of response we want to get from the server
|
) // The format of response we want to get from the server
|
||||||
httpURLConnection.setRequestProperty("User-Agent", "email=nominatim@kouros-online.de")
|
httpURLConnection.setRequestProperty("User-Agent", "email=online@kouros-online.de")
|
||||||
httpURLConnection.requestMethod = "GET"
|
httpURLConnection.requestMethod = "GET"
|
||||||
val responseCode = httpURLConnection.responseCode
|
val responseCode = httpURLConnection.responseCode
|
||||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.kouros.navigation.data.tomtom.TomTomRoute
|
|||||||
import com.kouros.navigation.data.valhalla.ValhallaResponse
|
import com.kouros.navigation.data.valhalla.ValhallaResponse
|
||||||
import com.kouros.navigation.data.valhalla.ValhallaRoute
|
import com.kouros.navigation.data.valhalla.ValhallaRoute
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
|
import kotlinx.coroutines.selects.whileSelect
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
@@ -102,17 +103,23 @@ data class Route(
|
|||||||
return if (isRouteValid()) {
|
return if (isRouteValid()) {
|
||||||
legs().first().steps[currentStepIndex]
|
legs().first().steps[currentStepIndex]
|
||||||
} else {
|
} else {
|
||||||
Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0)))
|
Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0), leftDistance = emptyList()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maneuver locations to snap location
|
||||||
|
*/
|
||||||
fun maneuverLocations(): List<Point> {
|
fun maneuverLocations(): List<Point> {
|
||||||
|
val wayPointIndex = currentStep().waypointIndex
|
||||||
val waypoints = currentStep().maneuver.waypoints
|
val waypoints = currentStep().maneuver.waypoints
|
||||||
val points = mutableListOf<Point>()
|
val points = mutableListOf<Point>()
|
||||||
for (loc in waypoints) {
|
for ((index,loc) in waypoints.withIndex()) {
|
||||||
val point = Point.fromLngLat(loc[0], loc[1])
|
if (index >= wayPointIndex && points.size < 20) {
|
||||||
|
val point = Point.fromLngLat(loc.longitude, loc.latitude)
|
||||||
points.add(point)
|
points.add(point)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return points
|
return points
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.Preferences
|
|||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.intPreferencesKey
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.longPreferencesKey
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.kouros.navigation.data.EngineType
|
import com.kouros.navigation.data.EngineType
|
||||||
@@ -16,17 +17,16 @@ import kotlinx.coroutines.flow.map
|
|||||||
private const val DATASTORE_NAME = "navigation_settings"
|
private const val DATASTORE_NAME = "navigation_settings"
|
||||||
|
|
||||||
|
|
||||||
|
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Central manager for app settings using DataStore
|
* Central manager for app settings using DataStore
|
||||||
*/
|
*/
|
||||||
class DataStoreManager(private val context: Context) {
|
class DataStoreManager(private val context: Context) {
|
||||||
|
|
||||||
companion object {
|
|
||||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys
|
// Keys
|
||||||
object PreferencesKeys {
|
companion object PreferencesKeys {
|
||||||
|
|
||||||
val SHOW_3D = booleanPreferencesKey("Show3D")
|
val SHOW_3D = booleanPreferencesKey("Show3D")
|
||||||
|
|
||||||
@@ -58,181 +58,223 @@ class DataStoreManager(private val context: Context) {
|
|||||||
|
|
||||||
val ENGINE_TYPE = intPreferencesKey("EngineType")
|
val ENGINE_TYPE = intPreferencesKey("EngineType")
|
||||||
|
|
||||||
|
val ALTERNATIVE_ROUTES = booleanPreferencesKey("AlternativeRoutes")
|
||||||
|
|
||||||
|
val LAST_FUEL_PRICES = longPreferencesKey("LastFuelPrices")
|
||||||
|
|
||||||
|
val FUEL_PRICES = stringPreferencesKey("FuelPrices")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read values
|
// Read values
|
||||||
|
|
||||||
val show3DFlow: Flow<Boolean> =
|
val show3DFlow: Flow<Boolean> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.SHOW_3D] == true
|
preferences[SHOW_3D] == true
|
||||||
}
|
}
|
||||||
val darkModeFlow: Flow<Int> =
|
val darkModeFlow: Flow<Int> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.DARK_MODE]
|
preferences[DARK_MODE]
|
||||||
?: 0
|
?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
val avoidMotorwayFlow: Flow<Boolean> =
|
val avoidMotorwayFlow: Flow<Boolean> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.AVOID_MOTORWAY] == true
|
preferences[AVOID_MOTORWAY] == true
|
||||||
}
|
}
|
||||||
|
|
||||||
val avoidTollwayFlow: Flow<Boolean> =
|
val avoidTollwayFlow: Flow<Boolean> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.AVOID_TOLLWAY] == true
|
preferences[AVOID_TOLLWAY] == true
|
||||||
}
|
}
|
||||||
|
|
||||||
val avoidFerryFlow: Flow<Boolean> =
|
val avoidFerryFlow: Flow<Boolean> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.AVOID_FERRY] == true
|
preferences[AVOID_FERRY] == true
|
||||||
}
|
}
|
||||||
|
|
||||||
val useCarLocationFlow: Flow<Boolean> =
|
val useCarLocationFlow: Flow<Boolean> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.CAR_LOCATION] == true
|
preferences[CAR_LOCATION] == true
|
||||||
}
|
}
|
||||||
|
|
||||||
val routingEngineFlow: Flow<Int> =
|
val routingEngineFlow: Flow<Int> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.ROUTING_ENGINE]
|
preferences[ROUTING_ENGINE]
|
||||||
?: 2
|
?: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
val lastRouteFlow: Flow<String> =
|
val lastRouteFlow: Flow<String> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.LAST_ROUTE]
|
preferences[LAST_ROUTE]
|
||||||
?: ""
|
?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
val tomTomApiKeyFlow: Flow<String> =
|
val tomTomApiKeyFlow: Flow<String> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.TOMTOM_APIKEY]
|
preferences[TOMTOM_APIKEY]
|
||||||
?: ""
|
?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
val recentPlacesFlow: Flow<String> =
|
val recentPlacesFlow: Flow<String> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.RECENT_PLACES]
|
preferences[RECENT_PLACES]
|
||||||
?: ""
|
?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val distanceModeFlow: Flow<Int> =
|
val distanceModeFlow: Flow<Int> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.DISTANCE_MODE]
|
preferences[DISTANCE_MODE]
|
||||||
?: 0
|
?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
val guidanceAudioFlow: Flow<Int> =
|
val guidanceAudioFlow: Flow<Int> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.GUIDANCE_AUDIO]
|
preferences[GUIDANCE_AUDIO]
|
||||||
?: 0
|
?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
val trafficFlow: Flow<Boolean> =
|
val trafficFlow: Flow<Boolean> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.TRAFFIC] == true
|
preferences[TRAFFIC] == true
|
||||||
}
|
}
|
||||||
|
|
||||||
val tripSuggestionFlow: Flow<Boolean> =
|
val tripSuggestionFlow: Flow<Boolean> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.TRIP_SUGGESTION] == true
|
preferences[TRIP_SUGGESTION] == true
|
||||||
}
|
}
|
||||||
|
|
||||||
val engineTypeFlow: Flow<Int> =
|
val engineTypeFlow: Flow<Int> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.ENGINE_TYPE]
|
preferences[ENGINE_TYPE]
|
||||||
?: EngineType.COMBUSTION.ordinal
|
?: EngineType.COMBUSTION.ordinal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val alternativeRoutesFlow: Flow<Boolean> =
|
||||||
|
context.dataStore.data.map { preferences ->
|
||||||
|
preferences[ALTERNATIVE_ROUTES] == true
|
||||||
|
}
|
||||||
|
|
||||||
|
val lastFuelPricesFlow: Flow<Long> =
|
||||||
|
context.dataStore.data.map { preferences ->
|
||||||
|
preferences[LAST_FUEL_PRICES]
|
||||||
|
?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val fuelPricesFlow: Flow<String> =
|
||||||
|
context.dataStore.data.map { preferences ->
|
||||||
|
preferences[FUEL_PRICES]
|
||||||
|
?: ""
|
||||||
|
}
|
||||||
|
|
||||||
// Save values
|
// Save values
|
||||||
suspend fun setShow3D(enabled: Boolean) {
|
suspend fun setShow3D(enabled: Boolean) {
|
||||||
context.dataStore.edit { preferences ->
|
context.dataStore.edit { preferences ->
|
||||||
preferences[PreferencesKeys.SHOW_3D] = enabled
|
preferences[SHOW_3D] = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setDarkMode(mode: Int) {
|
suspend fun setDarkMode(mode: Int) {
|
||||||
context.dataStore.edit { prefs ->
|
context.dataStore.edit { prefs ->
|
||||||
prefs[PreferencesKeys.DARK_MODE] = mode
|
prefs[DARK_MODE] = mode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setAvoidMotorway(enabled: Boolean) {
|
suspend fun setAvoidMotorway(enabled: Boolean) {
|
||||||
context.dataStore.edit { preferences ->
|
context.dataStore.edit { preferences ->
|
||||||
preferences[PreferencesKeys.AVOID_MOTORWAY] = enabled
|
preferences[AVOID_MOTORWAY] = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setAvoidTollway(enabled: Boolean) {
|
suspend fun setAvoidTollway(enabled: Boolean) {
|
||||||
context.dataStore.edit { preferences ->
|
context.dataStore.edit { preferences ->
|
||||||
preferences[PreferencesKeys.AVOID_TOLLWAY] = enabled
|
preferences[AVOID_TOLLWAY] = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setAvoidFerry(enabled: Boolean) {
|
suspend fun setAvoidFerry(enabled: Boolean) {
|
||||||
context.dataStore.edit { preferences ->
|
context.dataStore.edit { preferences ->
|
||||||
preferences[PreferencesKeys.AVOID_FERRY] = enabled
|
preferences[AVOID_FERRY] = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setCarLocation(enabled: Boolean) {
|
suspend fun setCarLocation(enabled: Boolean) {
|
||||||
context.dataStore.edit { preferences ->
|
context.dataStore.edit { preferences ->
|
||||||
preferences[PreferencesKeys.CAR_LOCATION] = enabled
|
preferences[CAR_LOCATION] = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setRoutingEngine(mode: Int) {
|
suspend fun setRoutingEngine(mode: Int) {
|
||||||
context.dataStore.edit { prefs ->
|
context.dataStore.edit { prefs ->
|
||||||
prefs[PreferencesKeys.ROUTING_ENGINE] = mode
|
prefs[ROUTING_ENGINE] = mode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setLastRoute(route: String) {
|
suspend fun setLastRoute(route: String) {
|
||||||
context.dataStore.edit { prefs ->
|
context.dataStore.edit { prefs ->
|
||||||
prefs[PreferencesKeys.LAST_ROUTE] = route
|
prefs[LAST_ROUTE] = route
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setTomtomApiKey(apiKey: String) {
|
suspend fun setTomtomApiKey(apiKey: String) {
|
||||||
context.dataStore.edit { prefs ->
|
context.dataStore.edit { prefs ->
|
||||||
prefs[PreferencesKeys.TOMTOM_APIKEY] = apiKey
|
prefs[TOMTOM_APIKEY] = apiKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setRecentPlaces(apiKey: String) {
|
suspend fun setRecentPlaces(apiKey: String) {
|
||||||
context.dataStore.edit { prefs ->
|
context.dataStore.edit { prefs ->
|
||||||
prefs[PreferencesKeys.RECENT_PLACES] = apiKey
|
prefs[RECENT_PLACES] = apiKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setDistanceMode(mode: Int) {
|
suspend fun setDistanceMode(mode: Int) {
|
||||||
context.dataStore.edit { prefs ->
|
context.dataStore.edit { prefs ->
|
||||||
prefs[PreferencesKeys.DISTANCE_MODE] = mode
|
prefs[DISTANCE_MODE] = mode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setGuidanceAudio(mode: Int) {
|
suspend fun setGuidanceAudio(mode: Int) {
|
||||||
context.dataStore.edit { prefs ->
|
context.dataStore.edit { prefs ->
|
||||||
prefs[PreferencesKeys.GUIDANCE_AUDIO] = mode
|
prefs[GUIDANCE_AUDIO] = mode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setTraffic(enabled: Boolean) {
|
suspend fun setTraffic(enabled: Boolean) {
|
||||||
context.dataStore.edit { preferences ->
|
context.dataStore.edit { preferences ->
|
||||||
preferences[PreferencesKeys.TRAFFIC] = enabled
|
preferences[TRAFFIC] = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setTripSuggestion(enabled: Boolean) {
|
suspend fun setTripSuggestion(enabled: Boolean) {
|
||||||
context.dataStore.edit { preferences ->
|
context.dataStore.edit { preferences ->
|
||||||
preferences[PreferencesKeys.TRIP_SUGGESTION] = enabled
|
preferences[TRIP_SUGGESTION] = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setEngineType(mode: Int) {
|
suspend fun setEngineType(mode: Int) {
|
||||||
context.dataStore.edit { prefs ->
|
context.dataStore.edit { prefs ->
|
||||||
prefs[PreferencesKeys.ENGINE_TYPE] = mode
|
prefs[ENGINE_TYPE] = mode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun setAlternativeRoutes(enabled: Boolean) {
|
||||||
|
context.dataStore.edit { preferences ->
|
||||||
|
preferences[ALTERNATIVE_ROUTES] = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
@@ -13,7 +13,7 @@ class OsrmRepository : NavigationRepository() {
|
|||||||
override fun getRoute(
|
override fun getRoute(
|
||||||
context: Context,
|
context: Context,
|
||||||
currentLocation: Location,
|
currentLocation: Location,
|
||||||
location: Location,
|
location: List<Location>,
|
||||||
carOrientation: Float,
|
carOrientation: Float,
|
||||||
searchFilter: SearchFilter
|
searchFilter: SearchFilter
|
||||||
): String {
|
): String {
|
||||||
@@ -28,7 +28,7 @@ class OsrmRepository : NavigationRepository() {
|
|||||||
if (searchFilter.avoidFerry) {
|
if (searchFilter.avoidFerry) {
|
||||||
exclude = "$exclude&exclude=ferry"
|
exclude = "$exclude&exclude=ferry"
|
||||||
}
|
}
|
||||||
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true&alternatives=false"
|
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.first().longitude},${location.first().latitude}?steps=true&alternatives=false"
|
||||||
return fetchUrl(routeUrl + routeLocation + exclude, true)
|
return fetchUrl(routeUrl + routeLocation + exclude, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.kouros.navigation.utils.GeoUtils.createCenterLocation
|
|||||||
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
|
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
|
||||||
import com.kouros.navigation.utils.GeoUtils.decodePolyline
|
import com.kouros.navigation.utils.GeoUtils.decodePolyline
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
class OsrmRoute {
|
class OsrmRoute {
|
||||||
|
|
||||||
@@ -27,18 +28,33 @@ class OsrmRoute {
|
|||||||
val steps = mutableListOf<Step>()
|
val steps = mutableListOf<Step>()
|
||||||
leg.steps.forEach { step ->
|
leg.steps.forEach { step ->
|
||||||
val intersections = mutableListOf<Intersection>()
|
val intersections = mutableListOf<Intersection>()
|
||||||
|
val leftDistance = mutableListOf<Float>()
|
||||||
|
var lastLocation = location(0.0,0.0)
|
||||||
val points = decodePolyline(step.geometry, 5)
|
val points = decodePolyline(step.geometry, 5)
|
||||||
waypoints.addAll(points)
|
waypoints.addAll(points)
|
||||||
|
// calculate left step distance for each point
|
||||||
|
var leftStepDistance = step.distance
|
||||||
|
leftDistance.add(leftStepDistance.toFloat())
|
||||||
|
points.forEach {
|
||||||
|
if (lastLocation.latitude != 0.0) {
|
||||||
|
val curLocation = location(it[0], it[1])
|
||||||
|
val dist = curLocation.distanceTo(lastLocation).absoluteValue
|
||||||
|
leftStepDistance -= dist
|
||||||
|
leftDistance.add(leftStepDistance.toFloat())
|
||||||
|
}
|
||||||
|
lastLocation = location(it[0], it[1])
|
||||||
|
}
|
||||||
val maneuver = RouteManeuver(
|
val maneuver = RouteManeuver(
|
||||||
bearingBefore = step.maneuver.bearingBefore,
|
bearingBefore = step.maneuver.bearingBefore,
|
||||||
bearingAfter = step.maneuver.bearingAfter,
|
bearingAfter = step.maneuver.bearingAfter,
|
||||||
type = convertType(step.maneuver),
|
type = convertType(step.maneuver),
|
||||||
waypoints = points,
|
waypoints = points.map { location(it[0], it[1]) },
|
||||||
exit = step.maneuver.exit,
|
exit = step.maneuver.exit,
|
||||||
location = location(
|
location = location(
|
||||||
step.maneuver.location[0],
|
step.maneuver.location[0],
|
||||||
step.maneuver.location[1]
|
step.maneuver.location[1]
|
||||||
)
|
),
|
||||||
|
leftDistance = leftDistance
|
||||||
)
|
)
|
||||||
step.intersections.forEach { it2 ->
|
step.intersections.forEach { it2 ->
|
||||||
if (it2.location[0] != 0.0) {
|
if (it2.location[0] != 0.0) {
|
||||||
@@ -69,7 +85,7 @@ class OsrmRoute {
|
|||||||
steps.add(step)
|
steps.add(step)
|
||||||
stepIndex += 1
|
stepIndex += 1
|
||||||
}
|
}
|
||||||
legs.add(Leg(steps))
|
legs.add(Leg(steps, summary))
|
||||||
}
|
}
|
||||||
val routeGeoJson = createLineStringCollection(waypoints)
|
val routeGeoJson = createLineStringCollection(waypoints)
|
||||||
val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
|
val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
package com.kouros.navigation.data.overpass
|
package com.kouros.navigation.data.overpass
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
|
|
||||||
data class Amenity(
|
data class Amenity(
|
||||||
|
val elements: List<Elements>,
|
||||||
@SerializedName("version" ) var version : Double? = null,
|
|
||||||
@SerializedName("generator" ) var generator : String? = null,
|
|
||||||
@SerializedName("osm3s" ) var osm3s : Osm3s? = Osm3s(),
|
|
||||||
@SerializedName("elements" ) var elements : ArrayList<Elements> = arrayListOf()
|
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -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
|
package com.kouros.navigation.data.overpass
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
|
|
||||||
data class Elements(
|
data class Elements(
|
||||||
|
val bounds: Bounds,
|
||||||
@SerializedName("type" ) var type : String = "",
|
val geometry: List<Geometry> = emptyList(),
|
||||||
@SerializedName("id" ) var id : Long = 0,
|
val id: Long = 0,
|
||||||
@SerializedName("lat" ) var lat : Double = 0.0,
|
val lat: Double= 0.0,
|
||||||
@SerializedName("lon" ) var lon : Double = 0.0,
|
val lon: Double = 0.0,
|
||||||
@SerializedName("tags" ) var tags : Tags = Tags(),
|
val tags: Tags,
|
||||||
var distance : Double = 0.0
|
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
|
package com.kouros.navigation.data.overpass
|
||||||
|
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
|
import android.util.Log
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.kouros.data.BuildConfig
|
||||||
import com.kouros.navigation.utils.GeoUtils.getBoundingBox
|
import com.kouros.navigation.utils.GeoUtils.getBoundingBox
|
||||||
import java.io.OutputStreamWriter
|
import java.io.OutputStreamWriter
|
||||||
|
import java.net.Authenticator
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.PasswordAuthentication
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
class Overpass {
|
class Overpass {
|
||||||
|
|
||||||
//val overpassUrl = "https://overpass.kumi.systems/api/interpreter"
|
private val config by lazy { com.kouros.navigation.data.ApplicationConfig.load() }
|
||||||
//val overpassUrl = "https://overpass-api.de/api"
|
private val gson = GsonBuilder().serializeNulls().create()
|
||||||
val overpassUrl = "https://kouros-online.de/overpass/interpreter"
|
|
||||||
|
|
||||||
|
var overpassUrl = if (BuildConfig.DEBUG)
|
||||||
|
"http://192.168.1.37/api/interpreter"
|
||||||
|
else
|
||||||
|
"https://kouros-online.de/api/interpreter"
|
||||||
|
|
||||||
fun getAround(radius: Int, linestring: String): List<Elements> {
|
val destination = "[!destination][highway!=\"motorway_link\"]"
|
||||||
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
|
fun getSpeedLimit(radius: Float, linestring: String, street: String, roadNumbers: List<String>): List<Elements> {
|
||||||
httpURLConnection.requestMethod = "POST"
|
|
||||||
httpURLConnection.setRequestProperty(
|
val wayAround = "way[maxspeed](around:$radius,$linestring)"
|
||||||
"Accept",
|
val searchClauses = mutableListOf<String>()
|
||||||
"application/json"
|
|
||||||
)
|
// 1. Search by street name (fuzzy match with first 10 characters)
|
||||||
httpURLConnection.setDoOutput(true);
|
val streetPrefix = street.take(10)
|
||||||
// define search query
|
if (streetPrefix.isNotEmpty()) {
|
||||||
val searchQuery = """
|
searchClauses.add("$wayAround[name~\"^$streetPrefix\"]")
|
||||||
|[out:json];
|
|
||||||
|(
|
|
||||||
| way[highway](around:$radius,$linestring)
|
|
||||||
| ;
|
|
||||||
|);
|
|
||||||
|out body;
|
|
||||||
""".trimMargin()
|
|
||||||
//println("way[highway](around:$radius,$linestring)")
|
|
||||||
return overpassApi(httpURLConnection, searchQuery)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchQuery = """
|
||||||
|
|[out:json][timeout:10];
|
||||||
|
|(
|
||||||
|
| ${searchClauses.joinToString(";")};
|
||||||
|
|);
|
||||||
|
|out body geom;
|
||||||
|
""".trimMargin()
|
||||||
|
|
||||||
|
//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(
|
fun getAmenities(
|
||||||
type: String,
|
type: String,
|
||||||
category: String,
|
category: String,
|
||||||
@@ -42,16 +98,7 @@ class Overpass {
|
|||||||
radius: Double
|
radius: Double
|
||||||
): List<Elements> {
|
): List<Elements> {
|
||||||
val boundingBox = getBoundingBox(location.latitude, location.longitude, radius)
|
val boundingBox = getBoundingBox(location.latitude, location.longitude, radius)
|
||||||
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
|
|
||||||
httpURLConnection.requestMethod = "POST"
|
|
||||||
// node["highway"="speed_camera"]
|
|
||||||
// node[amenity=$category]
|
|
||||||
httpURLConnection.setDoOutput(true);
|
|
||||||
httpURLConnection.setRequestProperty(
|
|
||||||
"Accept",
|
|
||||||
"application/json"
|
|
||||||
)
|
|
||||||
// define search query
|
|
||||||
val searchQuery = """
|
val searchQuery = """
|
||||||
|[out:json];
|
|[out:json];
|
||||||
|(
|
|(
|
||||||
@@ -59,29 +106,57 @@ class Overpass {
|
|||||||
| ($boundingBox);
|
| ($boundingBox);
|
||||||
|);
|
|);
|
||||||
|(._;>;);
|
|(._;>;);
|
||||||
|out body;
|
|out body geom;
|
||||||
""".trimMargin()
|
""".trimMargin()
|
||||||
return overpassApi(httpURLConnection, searchQuery)
|
|
||||||
|
val connection = (URL(overpassUrl).openConnection() as HttpURLConnection).apply {
|
||||||
|
requestMethod = "POST"
|
||||||
|
setRequestProperty("Accept", "application/json")
|
||||||
|
doOutput = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun overpassApi(httpURLConnection: HttpURLConnection, searchQuery: String): List<Elements> {
|
return overpassApi(connection, searchQuery)
|
||||||
try {
|
}
|
||||||
val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream)
|
|
||||||
outputStreamWriter.write(searchQuery)
|
private fun overpassApi(connection: HttpURLConnection, searchQuery: String): List<Elements> {
|
||||||
outputStreamWriter.flush()
|
|
||||||
// Check if the connection is successful
|
return try {
|
||||||
val responseCode = httpURLConnection.responseCode
|
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) {
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
val response = httpURLConnection.inputStream.bufferedReader()
|
val response = connection.inputStream.bufferedReader().use { it.readText() }
|
||||||
.use { it.readText() } // defaults to UTF-8
|
if (response.startsWith("<?xml")) {
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
Log.w("OverpassApi", "Received XML instead of JSON")
|
||||||
val overpass = gson.fromJson(response, Amenity::class.java)
|
|
||||||
return overpass.elements
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("Speed $e")
|
|
||||||
}
|
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
gson.fromJson(response, Amenity::class.java).elements
|
||||||
|
} else {
|
||||||
|
Log.e("OverpassApi", "Error code: $responseCode")
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("OverpassApi", "Exception in Overpass API call", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,21 +4,29 @@ import com.google.gson.annotations.SerializedName
|
|||||||
|
|
||||||
|
|
||||||
data class Tags(
|
data class Tags(
|
||||||
@SerializedName("name") var name: String? = null,
|
val destination: String = "",
|
||||||
@SerializedName("amenity") var amenity: String? = null,
|
val highway: String = "",
|
||||||
@SerializedName("authentication:none") var authenticationNone: String? = null,
|
val lanes: String = "",
|
||||||
@SerializedName("capacity") var capacity: String? = null,
|
val lit: String = "",
|
||||||
@SerializedName("motorcar") var motorcar: String? = null,
|
val maxspeed: String = "0",
|
||||||
@SerializedName("network") var network: String? = null,
|
@SerializedName("maxspeed:variable") val maxSpeedVariable: String = "",
|
||||||
@SerializedName("opening_hours") var openingHours: String? = null,
|
val name: String = "",
|
||||||
@SerializedName("operator") var operator: String? = null,
|
val oneway: String = "",
|
||||||
@SerializedName("operator:short") var operatorShort: String? = null,
|
val ref: String = "",
|
||||||
@SerializedName("operator:wikidata") var operatorWikidata: String? = null,
|
@SerializedName("int_ref") val intRef: String = "",
|
||||||
@SerializedName("operator:wikipedia") var operatorWikipedia: String? = null,
|
val sidewalk: String = "",
|
||||||
@SerializedName("ref") var ref: String? = null,
|
val smoothness: String = "",
|
||||||
@SerializedName("socket:type2") var socketType2: String? = null,
|
val surface: String = "",
|
||||||
@SerializedName("socket:type2:output") var socketType2Output: String? = null,
|
val amenity: String = "",
|
||||||
@SerializedName("maxspeed") var maxspeed: String = "0",
|
val capacity: String = "",
|
||||||
@SerializedName("direction") var direction: String? = null,
|
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 = "",
|
||||||
)
|
)
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
package com.kouros.navigation.data.route
|
package com.kouros.navigation.data.route
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
|
|
||||||
data class Leg(
|
data class Leg(
|
||||||
var steps : List<Step> = arrayListOf(),
|
var steps : List<Step> = arrayListOf(),
|
||||||
|
val summary: Summary,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,10 +6,65 @@ data class Maneuver(
|
|||||||
val bearingBefore: Int = 0,
|
val bearingBefore: Int = 0,
|
||||||
val bearingAfter: Int = 0,
|
val bearingAfter: Int = 0,
|
||||||
val type: Int = 0,
|
val type: Int = 0,
|
||||||
val waypoints: List<List<Double>>,
|
val waypoints: List<Location>,
|
||||||
val location: Location,
|
val location: Location,
|
||||||
val exit: Int = 0,
|
val exit: Int = 0,
|
||||||
val street: String = "",
|
val street: String = "",
|
||||||
val message: String = "",
|
val message: String = "",
|
||||||
val pointIndex: Int = 0,
|
val pointIndex: Int = 0,
|
||||||
|
val leftDistance: List<Float>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enum class ManeuverType(val value: Int) {
|
||||||
|
TYPE_UNKNOWN(0),
|
||||||
|
TYPE_DEPART(1),
|
||||||
|
TYPE_NAME_CHANGE(2),
|
||||||
|
TYPE_KEEP_LEFT(3),
|
||||||
|
TYPE_KEEP_RIGHT(4),
|
||||||
|
TYPE_TURN_SLIGHT_LEFT(5),
|
||||||
|
TYPE_TURN_SLIGHT_RIGHT(6),
|
||||||
|
TYPE_TURN_NORMAL_LEFT(7),
|
||||||
|
TYPE_TURN_NORMAL_RIGHT(8),
|
||||||
|
TYPE_TURN_SHARP_LEFT(9),
|
||||||
|
TYPE_TURN_SHARP_RIGHT(10),
|
||||||
|
TYPE_U_TURN_LEFT(11),
|
||||||
|
TYPE_U_TURN_RIGHT(12),
|
||||||
|
TYPE_ON_RAMP_SLIGHT_LEFT(13),
|
||||||
|
TYPE_ON_RAMP_SLIGHT_RIGHT(14),
|
||||||
|
TYPE_ON_RAMP_NORMAL_LEFT(15),
|
||||||
|
TYPE_ON_RAMP_NORMAL_RIGHT(16),
|
||||||
|
TYPE_ON_RAMP_SHARP_LEFT(17),
|
||||||
|
TYPE_ON_RAMP_SHARP_RIGHT(18),
|
||||||
|
TYPE_ON_RAMP_U_TURN_LEFT(19),
|
||||||
|
TYPE_ON_RAMP_U_TURN_RIGHT(20),
|
||||||
|
TYPE_OFF_RAMP_SLIGHT_LEFT(21),
|
||||||
|
TYPE_OFF_RAMP_SLIGHT_RIGHT(22),
|
||||||
|
TYPE_OFF_RAMP_NORMAL_LEFT(23),
|
||||||
|
TYPE_OFF_RAMP_NORMAL_RIGHT(24),
|
||||||
|
TYPE_FORK_LEFT(25),
|
||||||
|
TYPE_FORK_RIGHT(26),
|
||||||
|
TYPE_MERGE_LEFT(27),
|
||||||
|
TYPE_MERGE_RIGHT(28),
|
||||||
|
TYPE_MERGE_SIDE_UNSPECIFIED (29),
|
||||||
|
TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW (32),
|
||||||
|
TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE(33),
|
||||||
|
TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW(34),
|
||||||
|
TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE(35),
|
||||||
|
TYPE_STRAIGHT(36),
|
||||||
|
TYPE_FERRY_BOAT (37),
|
||||||
|
TYPE_FERRY_TRAIN (38),
|
||||||
|
TYPE_DESTINATION (39),
|
||||||
|
TYPE_DESTINATION_STRAIGHT (40),
|
||||||
|
TYPE_DESTINATION_LEFT(41),
|
||||||
|
TYPE_DESTINATION_RIGHT(42),
|
||||||
|
TYPE_ROUNDABOUT_ENTER_CW(43),
|
||||||
|
TYPE_ROUNDABOUT_EXIT_CW(44),
|
||||||
|
TYPE_ROUNDABOUT_ENTER_CCW(45),
|
||||||
|
TYPE_ROUNDABOUT_EXIT_CCW(46),
|
||||||
|
TYPE_FERRY_BOAT_LEFT(47),
|
||||||
|
TYPE_FERRY_BOAT_RIGHT(48),
|
||||||
|
TYPE_FERRY_TRAIN_LEFT(49),
|
||||||
|
TYPE_FERRY_TRAIN_RIGHT(50),
|
||||||
|
TYPE_WAYPOINT_RIGHT(51),
|
||||||
|
TYPE_WAYPOINT_LEFT(52),
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ data class Step(
|
|||||||
val distance: Double = 0.0,
|
val distance: Double = 0.0,
|
||||||
val street : String = "",
|
val street : String = "",
|
||||||
val intersection: List<Intersection> = mutableListOf(),
|
val intersection: List<Intersection> = mutableListOf(),
|
||||||
val countryCode : String = ""
|
val countryCode : String = "",
|
||||||
|
val roadNumbers : List<String> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.kouros.navigation.data.tomtom
|
|
||||||
|
|
||||||
data class Cause(
|
|
||||||
val mainCauseCode: Int
|
|
||||||
)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.kouros.navigation.data.tomtom
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
|
|
||||||
data class Events (
|
|
||||||
|
|
||||||
@SerializedName("description" ) var description : String? = null
|
|
||||||
|
|
||||||
)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.kouros.navigation.data.tomtom
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
|
|
||||||
data class Features (
|
|
||||||
|
|
||||||
@SerializedName("type" ) var type : String? = null,
|
|
||||||
@SerializedName("properties" ) var properties : Properties? = Properties(),
|
|
||||||
@SerializedName("geometry" ) var geometry : Geometry? = Geometry()
|
|
||||||
|
|
||||||
)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.kouros.navigation.data.tomtom
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
|
|
||||||
data class Geometry (
|
|
||||||
|
|
||||||
@SerializedName("type" ) var type : String? = null,
|
|
||||||
@SerializedName("coordinates" ) var coordinates : List<List<Double>> = arrayListOf()
|
|
||||||
|
|
||||||
)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.kouros.navigation.data.tomtom
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
|
|
||||||
data class Incidents (
|
|
||||||
|
|
||||||
@SerializedName("type" ) var type : String? = null,
|
|
||||||
@SerializedName("properties" ) var properties : Properties? = Properties(),
|
|
||||||
@SerializedName("geometry" ) var geometry : Geometry? = Geometry()
|
|
||||||
|
|
||||||
)
|
|
||||||
@@ -11,7 +11,7 @@ data class Instruction(
|
|||||||
val point: Point,
|
val point: Point,
|
||||||
val pointIndex: Int,
|
val pointIndex: Int,
|
||||||
val possibleCombineWithNext: Boolean,
|
val possibleCombineWithNext: Boolean,
|
||||||
val roadNumbers: List<String>,
|
val roadNumbers: List<String> = emptyList(),
|
||||||
val routeOffsetInMeters: Int,
|
val routeOffsetInMeters: Int,
|
||||||
val signpostText: String,
|
val signpostText: String,
|
||||||
val street: String? = "",
|
val street: String? = "",
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.kouros.navigation.data.tomtom
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
|
|
||||||
data class Properties (
|
|
||||||
|
|
||||||
@SerializedName("iconCategory" ) var iconCategory : Int? = null,
|
|
||||||
@SerializedName("events" ) var events : ArrayList<Events> = arrayListOf()
|
|
||||||
|
|
||||||
)
|
|
||||||
@@ -25,18 +25,24 @@ val useLocal = BuildConfig.DEBUG
|
|||||||
|
|
||||||
val useLocalTraffic = BuildConfig.DEBUG
|
val useLocalTraffic = BuildConfig.DEBUG
|
||||||
|
|
||||||
|
|
||||||
class TomTomRepository : NavigationRepository() {
|
class TomTomRepository : NavigationRepository() {
|
||||||
|
|
||||||
override fun getRoute(
|
override fun getRoute(
|
||||||
context: Context,
|
context: Context,
|
||||||
currentLocation: Location,
|
currentLocation: Location,
|
||||||
location: Location,
|
location: List<Location>,
|
||||||
carOrientation: Float,
|
carOrientation: Float,
|
||||||
searchFilter: SearchFilter
|
searchFilter: SearchFilter
|
||||||
): String {
|
): String {
|
||||||
|
val vehicleHeading = if (carOrientation !in 0.0..360.0) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
carOrientation.toInt()
|
||||||
|
}
|
||||||
if (useLocal) {
|
if (useLocal) {
|
||||||
return fetchUrl(
|
return fetchUrl(
|
||||||
"https://kouros-online.de/tomtom_routing.json",
|
"http://192.168.1.37/tomtom_routing.json",
|
||||||
|
//"http://192.168.1.37/verona.json",
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -55,18 +61,35 @@ class TomTomRepository : NavigationRepository() {
|
|||||||
engineType = "electric"
|
engineType = "electric"
|
||||||
}
|
}
|
||||||
val repository = getSettingsRepository(context)
|
val repository = getSettingsRepository(context)
|
||||||
val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() }
|
val tomtomApiKey = runBlocking {
|
||||||
|
repository.tomTomApiKeyFlow.first()
|
||||||
|
}
|
||||||
|
val alternativeRoutes = runBlocking {
|
||||||
|
repository.alternativeRoutesFlow.first()
|
||||||
|
}
|
||||||
|
val altRoutes = if (alternativeRoutes) {
|
||||||
|
"&maxAlternatives=2"
|
||||||
|
} else {
|
||||||
|
"&maxAlternatives=0"
|
||||||
|
}
|
||||||
val currentLocale = Locale.getDefault()
|
val currentLocale = Locale.getDefault()
|
||||||
val language = currentLocale.language + "-" + currentLocale.country
|
val language = currentLocale.language + "-" + currentLocale.country
|
||||||
|
var loc = ""
|
||||||
|
location.forEach {
|
||||||
|
loc += if (loc.isEmpty()) {
|
||||||
|
"${it.latitude},${it.longitude}"
|
||||||
|
} else {
|
||||||
|
":${it.latitude},${it.longitude}"
|
||||||
|
}
|
||||||
|
}
|
||||||
val url =
|
val url =
|
||||||
routeUrl + "${currentLocation.latitude},${currentLocation.longitude}:${location.latitude},${location.longitude}" +
|
routeUrl + "${currentLocation.latitude},${currentLocation.longitude}:$loc" +
|
||||||
"/json?sectionType=traffic&report=effectiveSettings&routeType=eco" +
|
"/json?sectionType=traffic&report=effectiveSettings&routeType=eco" +
|
||||||
"&traffic=true&avoid=unpavedRoads&travelMode=car" +
|
"&traffic=true&avoid=unpavedRoads&travelMode=car" +
|
||||||
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
|
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
|
||||||
"&instructionsType=text&language=$language§ionType=lanes" +
|
"&instructionsType=text&language=$language§ionType=lanes" +
|
||||||
"&routeRepresentation=encodedPolyline" +
|
"&routeRepresentation=encodedPolyline$altRoutes" +
|
||||||
"&maxAlternatives=2" +
|
"&vehicleHeading=$vehicleHeading&vehicleEngineType=$engineType$filter&key=$tomtomApiKey"
|
||||||
"&vehicleEngineType=$engineType$filter&key=$tomtomApiKey"
|
|
||||||
return fetchUrl(
|
return fetchUrl(
|
||||||
url,
|
url,
|
||||||
false
|
false
|
||||||
@@ -80,10 +103,10 @@ class TomTomRepository : NavigationRepository() {
|
|||||||
if (!showTraffic) {
|
if (!showTraffic) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
|
val bbox = calculateSquareRadius(location.latitude, location.longitude, 10.0)
|
||||||
return if (useLocalTraffic) {
|
return if (useLocalTraffic) {
|
||||||
fetchUrl(
|
fetchUrl(
|
||||||
"https://kouros-online.de/tomtom_traffic.json",
|
"http://192.168.1.37/tomtom_traffic.json",
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,28 +1,33 @@
|
|||||||
package com.kouros.navigation.data.tomtom
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
|
||||||
import com.kouros.navigation.data.Route
|
import com.kouros.navigation.data.Route
|
||||||
import com.kouros.navigation.data.RouteEngine
|
import com.kouros.navigation.data.RouteEngine
|
||||||
import com.kouros.navigation.data.route.Intersection
|
import com.kouros.navigation.data.route.Intersection
|
||||||
import com.kouros.navigation.data.route.Lane
|
import com.kouros.navigation.data.route.Lane
|
||||||
import com.kouros.navigation.data.route.Leg
|
import com.kouros.navigation.data.route.Leg
|
||||||
|
import com.kouros.navigation.data.route.ManeuverType
|
||||||
|
import com.kouros.navigation.data.route.Routes
|
||||||
import com.kouros.navigation.data.route.Step
|
import com.kouros.navigation.data.route.Step
|
||||||
import com.kouros.navigation.data.route.Summary
|
import com.kouros.navigation.data.route.Summary
|
||||||
import com.kouros.navigation.utils.GeoUtils.createCenterLocation
|
import com.kouros.navigation.utils.GeoUtils.createCenterLocation
|
||||||
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
|
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
|
||||||
import com.kouros.navigation.utils.GeoUtils.decodePolyline
|
import com.kouros.navigation.utils.GeoUtils.decodePolyline
|
||||||
|
import com.kouros.navigation.utils.isNumeric
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
|
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
|
||||||
|
|
||||||
|
|
||||||
class TomTomRoute {
|
class TomTomRoute {
|
||||||
|
|
||||||
fun mapToRoute(routeJson: TomTomResponse, builder: Route.Builder) {
|
fun mapToRoute(routeJson: TomTomResponse, builder: Route.Builder) {
|
||||||
val routes = mutableListOf<com.kouros.navigation.data.route.Routes>()
|
val routes = mutableListOf<Routes>()
|
||||||
routeJson.routes.forEach { route ->
|
routeJson.routes.forEach { route ->
|
||||||
val waypoints = mutableListOf<List<Double>>()
|
val waypoints = mutableListOf<List<Double>>()
|
||||||
|
val points = mutableListOf<List<Double>>()
|
||||||
val legs = mutableListOf<Leg>()
|
val legs = mutableListOf<Leg>()
|
||||||
var stepIndex = 0
|
var stepIndex = 0
|
||||||
var points = listOf<List<Double>>()
|
|
||||||
val summary = Summary(
|
val summary = Summary(
|
||||||
route.summary.travelTimeInSeconds.toDouble(),
|
route.summary.travelTimeInSeconds.toDouble(),
|
||||||
route.summary.lengthInMeters.toDouble(),
|
route.summary.lengthInMeters.toDouble(),
|
||||||
@@ -30,35 +35,61 @@ class TomTomRoute {
|
|||||||
route.summary.trafficLengthInMeters.toDouble()
|
route.summary.trafficLengthInMeters.toDouble()
|
||||||
)
|
)
|
||||||
route.legs.forEach { leg ->
|
route.legs.forEach { leg ->
|
||||||
points = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision)
|
val p = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision)
|
||||||
waypoints.addAll(points)
|
points.addAll(p)
|
||||||
|
waypoints.addAll(p)
|
||||||
}
|
}
|
||||||
|
route.legs.forEach { leg ->
|
||||||
var stepDistance = 0.0
|
var stepDistance = 0.0
|
||||||
var stepDuration = 0.0
|
var stepDuration = 0.0
|
||||||
val steps = mutableListOf<Step>()
|
val steps = mutableListOf<Step>()
|
||||||
|
val summary = Summary(
|
||||||
|
leg.summary.travelTimeInSeconds.toDouble(),
|
||||||
|
leg.summary.lengthInMeters.toDouble(),
|
||||||
|
leg.summary.trafficDelayInSeconds.toDouble(),
|
||||||
|
leg.summary.trafficLengthInMeters.toDouble()
|
||||||
|
)
|
||||||
var lastPointIndex = 0
|
var lastPointIndex = 0
|
||||||
for (index in 1..<route.guidance.instructions.size) {
|
for (index in 1..<route.guidance.instructions.size) {
|
||||||
val lastInstruction = route.guidance.instructions[index - 1]
|
val lastInstruction = route.guidance.instructions[index - 1]
|
||||||
val instruction = route.guidance.instructions[index]
|
val instruction = route.guidance.instructions[index]
|
||||||
val street = lastInstruction.street ?: ""
|
val street = lastInstruction.street ?: ""
|
||||||
val maneuverStreet = instruction.street ?: ""
|
val maneuverStreet = instruction.street ?: ""
|
||||||
|
val leftDistance = mutableListOf<Float>()
|
||||||
|
var lastLocation = location(0.0, 0.0)
|
||||||
|
stepDistance =
|
||||||
|
route.guidance.instructions[index].routeOffsetInMeters - stepDistance
|
||||||
|
stepDuration =
|
||||||
|
route.guidance.instructions[index].travelTimeInSeconds - stepDuration
|
||||||
|
// calculate left step distance for each point
|
||||||
|
var leftStepDistance = stepDistance
|
||||||
|
leftDistance.add(leftStepDistance.toFloat())
|
||||||
|
val subPoints = points.subList(
|
||||||
|
lastPointIndex,
|
||||||
|
instruction.pointIndex + 1)
|
||||||
|
subPoints.forEach {
|
||||||
|
if (lastLocation.latitude != 0.0) {
|
||||||
|
val curLocation = location(it[0], it[1])
|
||||||
|
val dist = curLocation.distanceTo(lastLocation).absoluteValue
|
||||||
|
leftStepDistance -= dist
|
||||||
|
leftDistance.add(leftStepDistance.toFloat())
|
||||||
|
}
|
||||||
|
lastLocation = location(it[0], it[1])
|
||||||
|
}
|
||||||
val maneuver = RouteManeuver(
|
val maneuver = RouteManeuver(
|
||||||
bearingBefore = 0,
|
bearingBefore = 0,
|
||||||
bearingAfter = 0,
|
bearingAfter = 0,
|
||||||
type = convertType(instruction.maneuver),
|
type = convertType(instruction.maneuver),
|
||||||
waypoints = points.subList(
|
waypoints = subPoints.map { location(it[0], it[1]) },
|
||||||
lastPointIndex,
|
|
||||||
instruction.pointIndex + 1,
|
|
||||||
),
|
|
||||||
exit = exitNumber(instruction),
|
exit = exitNumber(instruction),
|
||||||
location = location(
|
location = location(
|
||||||
instruction.point.longitude, instruction.point.latitude
|
instruction.point.longitude, instruction.point.latitude
|
||||||
),
|
),
|
||||||
street = maneuverStreet,
|
street = maneuverStreet,
|
||||||
message = instruction.message,
|
message = instruction.message,
|
||||||
pointIndex = instruction.pointIndex
|
pointIndex = instruction.pointIndex,
|
||||||
|
leftDistance = leftDistance
|
||||||
)
|
)
|
||||||
|
|
||||||
lastPointIndex = instruction.pointIndex
|
lastPointIndex = instruction.pointIndex
|
||||||
val intersections = mutableListOf<Intersection>()
|
val intersections = mutableListOf<Intersection>()
|
||||||
route.sections?.forEach { section ->
|
route.sections?.forEach { section ->
|
||||||
@@ -67,7 +98,6 @@ class TomTomRoute {
|
|||||||
) {
|
) {
|
||||||
val lanes = mutableListOf<Lane>()
|
val lanes = mutableListOf<Lane>()
|
||||||
var startIndex = 0
|
var startIndex = 0
|
||||||
var lastLane: Lane? = null
|
|
||||||
section.lanes?.forEach { itLane ->
|
section.lanes?.forEach { itLane ->
|
||||||
val lane = Lane(
|
val lane = Lane(
|
||||||
location = location(
|
location = location(
|
||||||
@@ -80,21 +110,17 @@ class TomTomRoute {
|
|||||||
endIndex = section.endPointIndex
|
endIndex = section.endPointIndex
|
||||||
)
|
)
|
||||||
startIndex = section.startPointIndex
|
startIndex = section.startPointIndex
|
||||||
if (lastLane == null
|
|
||||||
|| (!(lastLane.valid && lane.valid
|
|
||||||
&& lastLane.indications == lane.indications))
|
|
||||||
) {
|
|
||||||
lanes.add(lane)
|
lanes.add(lane)
|
||||||
}
|
}
|
||||||
lastLane = lane
|
|
||||||
}
|
|
||||||
intersections.add(Intersection(waypoints[startIndex], lanes))
|
intersections.add(Intersection(waypoints[startIndex], lanes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stepDistance =
|
|
||||||
route.guidance.instructions[index].routeOffsetInMeters - stepDistance
|
val roadNumbers = if (lastInstruction.roadNumbers != null) {
|
||||||
stepDuration =
|
lastInstruction.roadNumbers
|
||||||
route.guidance.instructions[index].travelTimeInSeconds - stepDuration
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
val step = Step(
|
val step = Step(
|
||||||
index = stepIndex,
|
index = stepIndex,
|
||||||
street = street,
|
street = street,
|
||||||
@@ -102,17 +128,19 @@ class TomTomRoute {
|
|||||||
duration = stepDuration,
|
duration = stepDuration,
|
||||||
maneuver = maneuver,
|
maneuver = maneuver,
|
||||||
intersection = intersections,
|
intersection = intersections,
|
||||||
countryCode = lastInstruction.countryCode
|
countryCode = lastInstruction.countryCode,
|
||||||
|
roadNumbers = roadNumbers
|
||||||
)
|
)
|
||||||
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
|
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
|
||||||
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
|
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
|
||||||
steps.add(step)
|
steps.add(step)
|
||||||
stepIndex += 1
|
stepIndex += 1
|
||||||
}
|
}
|
||||||
legs.add(Leg(steps))
|
legs.add(Leg(steps, summary))
|
||||||
|
}
|
||||||
val routeGeoJson = createLineStringCollection(waypoints)
|
val routeGeoJson = createLineStringCollection(waypoints)
|
||||||
val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
|
val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
|
||||||
val newRoute = com.kouros.navigation.data.route.Routes(
|
val newRoute = Routes(
|
||||||
legs,
|
legs,
|
||||||
summary,
|
summary,
|
||||||
routeGeoJson,
|
routeGeoJson,
|
||||||
@@ -130,75 +158,80 @@ class TomTomRoute {
|
|||||||
var newType = 0
|
var newType = 0
|
||||||
when (type) {
|
when (type) {
|
||||||
"DEPART" -> {
|
"DEPART" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART
|
newType = ManeuverType.TYPE_DEPART.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"ARRIVE" -> {
|
"ARRIVE" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION
|
newType = ManeuverType.TYPE_DESTINATION.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
"ARRIVE_LEFT" -> {
|
"ARRIVE_LEFT" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_LEFT
|
newType = ManeuverType.TYPE_DESTINATION_LEFT.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"ARRIVE_RIGHT" -> {
|
"ARRIVE_RIGHT" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_RIGHT
|
newType = ManeuverType.TYPE_DESTINATION_RIGHT.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"STRAIGHT", "FOLLOW" -> {
|
"STRAIGHT", "FOLLOW" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
|
newType = ManeuverType.TYPE_STRAIGHT.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"KEEP_RIGHT" -> {
|
"KEEP_RIGHT" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_KEEP_RIGHT
|
newType = ManeuverType.TYPE_KEEP_RIGHT.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"BEAR_RIGHT" -> {
|
"BEAR_RIGHT" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
newType = ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"BEAR_LEFT" -> {
|
"BEAR_LEFT" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
|
newType = ManeuverType.TYPE_TURN_SLIGHT_LEFT.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"KEEP_LEFT" -> {
|
"KEEP_LEFT" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_KEEP_LEFT
|
newType = ManeuverType.TYPE_KEEP_LEFT.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"TURN_LEFT" -> {
|
"TURN_LEFT" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
|
newType = ManeuverType.TYPE_TURN_NORMAL_LEFT.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"TURN_RIGHT" -> {
|
"TURN_RIGHT" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
|
newType = ManeuverType.TYPE_TURN_NORMAL_RIGHT.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"SHARP_LEFT" -> {
|
"SHARP_LEFT" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SHARP_LEFT
|
newType = ManeuverType.TYPE_TURN_SHARP_LEFT.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"SHARP_RIGHT" -> {
|
"SHARP_RIGHT" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SHARP_RIGHT
|
newType = ManeuverType.TYPE_TURN_SHARP_RIGHT.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"ROUNDABOUT_RIGHT", "ROUNDABOUT_CROSS" -> {
|
"ROUNDABOUT_RIGHT", "ROUNDABOUT_CROSS" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CCW
|
newType = ManeuverType.TYPE_ROUNDABOUT_ENTER_CCW.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"ROUNDABOUT_LEFT" -> {
|
"ROUNDABOUT_LEFT" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CW
|
newType = ManeuverType.TYPE_ROUNDABOUT_ENTER_CW.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"MAKE_UTURN" -> {
|
"MAKE_UTURN", "TRY_MAKE_UTURN" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_U_TURN_LEFT
|
newType = ManeuverType.TYPE_U_TURN_LEFT.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"ENTER_MOTORWAY" -> {
|
"ENTER_MOTORWAY" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_MERGE_LEFT
|
newType = ManeuverType.TYPE_MERGE_LEFT.value
|
||||||
}
|
}
|
||||||
|
|
||||||
"TAKE_EXIT" -> {
|
"TAKE_EXIT" -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
newType = ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value
|
||||||
|
}
|
||||||
|
|
||||||
|
"WAYPOINT_RIGHT" -> {
|
||||||
|
newType = ManeuverType.TYPE_WAYPOINT_RIGHT.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newType
|
return newType
|
||||||
@@ -212,6 +245,10 @@ private fun exitNumber(
|
|||||||
) {
|
) {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
|
if (isNumeric(instruction.exitNumber)) {
|
||||||
instruction.exitNumber.toInt()
|
instruction.exitNumber.toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.kouros.navigation.data.tomtom
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
|
|
||||||
data class Traffic (
|
|
||||||
|
|
||||||
//@SerializedName("incidents" ) var incidents : ArrayList<Incidents> = arrayListOf()
|
|
||||||
@SerializedName("type" ) var type : String = "",
|
|
||||||
@SerializedName("features" ) var features : ArrayList<Features> = arrayListOf()
|
|
||||||
|
|
||||||
)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.kouros.navigation.data.tomtom
|
|
||||||
|
|
||||||
data class TrafficData (
|
|
||||||
var traffic : Traffic ,
|
|
||||||
var trafficData: String = ""
|
|
||||||
)
|
|
||||||
@@ -16,7 +16,7 @@ class ValhallaRepository : NavigationRepository() {
|
|||||||
override fun getRoute(
|
override fun getRoute(
|
||||||
context: Context,
|
context: Context,
|
||||||
currentLocation: Location,
|
currentLocation: Location,
|
||||||
location: Location,
|
location: List<Location>,
|
||||||
carOrientation: Float,
|
carOrientation: Float,
|
||||||
searchFilter: SearchFilter
|
searchFilter: SearchFilter
|
||||||
): String {
|
): String {
|
||||||
@@ -35,7 +35,7 @@ class ValhallaRepository : NavigationRepository() {
|
|||||||
lon = currentLocation.longitude,
|
lon = currentLocation.longitude,
|
||||||
searchFilter = exclude
|
searchFilter = exclude
|
||||||
),
|
),
|
||||||
Locations(lat = location.latitude, lon = location.longitude, searchFilter = exclude)
|
Locations(lat = location.first().latitude, lon = location.first().longitude, searchFilter = exclude)
|
||||||
)
|
)
|
||||||
val valhallaLocation = ValhallaLocation(
|
val valhallaLocation = ValhallaLocation(
|
||||||
locations = vLocation,
|
locations = vLocation,
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ class ValhallaRoute {
|
|||||||
bearingAfter = it.bearingAfter,
|
bearingAfter = it.bearingAfter,
|
||||||
//type = it.type,
|
//type = it.type,
|
||||||
type = convertType(it),
|
type = convertType(it),
|
||||||
waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1),
|
waypoints = waypoints.subList(it.beginShapeIndex, it.endShapeIndex + 1).map { location(it[0], it[1]) },
|
||||||
// TODO: calculate from ShapeIndex !
|
// TODO: calculate from ShapeIndex !
|
||||||
location = location(0.0, 0.0)
|
location = location(0.0, 0.0),
|
||||||
|
leftDistance = emptyList()
|
||||||
|
|
||||||
)
|
)
|
||||||
var name = ""
|
var name = ""
|
||||||
|
|||||||
@@ -6,15 +6,12 @@ import android.graphics.BitmapFactory
|
|||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.car.app.model.CarIcon
|
|
||||||
import androidx.car.app.navigation.model.LaneDirection
|
import androidx.car.app.navigation.model.LaneDirection
|
||||||
import androidx.car.app.navigation.model.Maneuver
|
|
||||||
import androidx.core.graphics.createBitmap
|
import androidx.core.graphics.createBitmap
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.data.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
import java.util.Collections
|
import com.kouros.navigation.data.route.ManeuverType
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class IconMapper {
|
class IconMapper {
|
||||||
@@ -22,62 +19,66 @@ class IconMapper {
|
|||||||
fun maneuverIcon(routeManeuverType: Int): Int {
|
fun maneuverIcon(routeManeuverType: Int): Int {
|
||||||
var currentTurnIcon = R.drawable.ic_turn_name_change
|
var currentTurnIcon = R.drawable.ic_turn_name_change
|
||||||
when (routeManeuverType) {
|
when (routeManeuverType) {
|
||||||
Maneuver.TYPE_STRAIGHT -> {
|
ManeuverType.TYPE_STRAIGHT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_name_change
|
currentTurnIcon = R.drawable.ic_turn_name_change
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_DESTINATION,
|
ManeuverType.TYPE_DESTINATION.value,
|
||||||
Maneuver.TYPE_DESTINATION_RIGHT,
|
ManeuverType.TYPE_DESTINATION_RIGHT.value,
|
||||||
Maneuver.TYPE_DESTINATION_LEFT,
|
ManeuverType.TYPE_DESTINATION_LEFT.value,
|
||||||
Maneuver.TYPE_DESTINATION_STRAIGHT
|
ManeuverType.TYPE_DESTINATION_STRAIGHT.value
|
||||||
-> {
|
-> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_destination
|
currentTurnIcon = R.drawable.ic_turn_destination
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_TURN_NORMAL_RIGHT -> {
|
ManeuverType.TYPE_TURN_NORMAL_RIGHT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_normal_right
|
currentTurnIcon = R.drawable.ic_turn_normal_right
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_TURN_NORMAL_LEFT -> {
|
ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_normal_left
|
currentTurnIcon = R.drawable.ic_turn_normal_left
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT -> {
|
ManeuverType.TYPE_OFF_RAMP_SLIGHT_RIGHT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_slight_right
|
currentTurnIcon = R.drawable.ic_turn_slight_right
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> {
|
ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_slight_right
|
currentTurnIcon = R.drawable.ic_turn_slight_right
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_KEEP_RIGHT -> {
|
ManeuverType.TYPE_KEEP_RIGHT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_name_change
|
currentTurnIcon = R.drawable.ic_turn_name_change
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_KEEP_LEFT -> {
|
ManeuverType.TYPE_KEEP_LEFT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_name_change
|
currentTurnIcon = R.drawable.ic_turn_name_change
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_ROUNDABOUT_ENTER_CCW -> {
|
ManeuverType.TYPE_ROUNDABOUT_ENTER_CCW.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_roundabout_ccw
|
currentTurnIcon = R.drawable.ic_roundabout_ccw
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_ROUNDABOUT_EXIT_CCW -> {
|
ManeuverType.TYPE_ROUNDABOUT_EXIT_CCW.value -> {
|
||||||
|
|
||||||
currentTurnIcon = R.drawable.ic_roundabout_ccw
|
currentTurnIcon = R.drawable.ic_roundabout_ccw
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_U_TURN_LEFT -> {
|
ManeuverType.TYPE_U_TURN_LEFT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_u_turn_left
|
currentTurnIcon = R.drawable.ic_turn_u_turn_left
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_U_TURN_RIGHT -> {
|
ManeuverType.TYPE_U_TURN_RIGHT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_u_turn_right
|
currentTurnIcon = R.drawable.ic_turn_u_turn_right
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_MERGE_LEFT -> {
|
ManeuverType.TYPE_MERGE_LEFT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_merge_symmetrical
|
currentTurnIcon = R.drawable.ic_turn_merge_symmetrical
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ManeuverType.TYPE_WAYPOINT_RIGHT.value -> {
|
||||||
|
currentTurnIcon = R.drawable.ic_turn_destination
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return currentTurnIcon
|
return currentTurnIcon
|
||||||
}
|
}
|
||||||
@@ -86,8 +87,8 @@ class IconMapper {
|
|||||||
val laneDirection = when (direction.lowercase(Locale.getDefault())) {
|
val laneDirection = when (direction.lowercase(Locale.getDefault())) {
|
||||||
"left_straight" -> {
|
"left_straight" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT
|
ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> LaneDirection.SHAPE_NORMAL_LEFT
|
||||||
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
|
ManeuverType.TYPE_STRAIGHT.value -> LaneDirection.SHAPE_STRAIGHT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -95,7 +96,7 @@ class IconMapper {
|
|||||||
|
|
||||||
"left" -> {
|
"left" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT
|
ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> LaneDirection.SHAPE_NORMAL_LEFT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -103,9 +104,9 @@ class IconMapper {
|
|||||||
|
|
||||||
"straight" -> {
|
"straight" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
|
ManeuverType.TYPE_STRAIGHT.value -> LaneDirection.SHAPE_STRAIGHT
|
||||||
Maneuver.TYPE_KEEP_LEFT -> LaneDirection.SHAPE_STRAIGHT
|
ManeuverType.TYPE_KEEP_LEFT.value -> LaneDirection.SHAPE_STRAIGHT
|
||||||
Maneuver.TYPE_KEEP_RIGHT -> LaneDirection.SHAPE_STRAIGHT
|
ManeuverType.TYPE_KEEP_RIGHT.value -> LaneDirection.SHAPE_STRAIGHT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -113,7 +114,7 @@ class IconMapper {
|
|||||||
|
|
||||||
"right" -> {
|
"right" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
|
ManeuverType.TYPE_TURN_NORMAL_RIGHT.value -> LaneDirection.SHAPE_NORMAL_RIGHT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -121,8 +122,8 @@ class IconMapper {
|
|||||||
|
|
||||||
"right_straight" -> {
|
"right_straight" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
|
ManeuverType.TYPE_TURN_NORMAL_RIGHT.value -> LaneDirection.SHAPE_NORMAL_RIGHT
|
||||||
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
|
ManeuverType.TYPE_STRAIGHT.value -> LaneDirection.SHAPE_STRAIGHT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -130,8 +131,8 @@ class IconMapper {
|
|||||||
|
|
||||||
"left_slight", "slight_left" -> {
|
"left_slight", "slight_left" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT
|
ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> LaneDirection.SHAPE_SLIGHT_LEFT
|
||||||
Maneuver.TYPE_KEEP_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT
|
ManeuverType.TYPE_KEEP_LEFT.value -> LaneDirection.SHAPE_SLIGHT_LEFT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -139,8 +140,8 @@ class IconMapper {
|
|||||||
|
|
||||||
"right_slight", "slight_right" -> {
|
"right_slight", "slight_right" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
|
ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value -> LaneDirection.SHAPE_NORMAL_RIGHT
|
||||||
Maneuver.TYPE_KEEP_RIGHT -> LaneDirection.SHAPE_SLIGHT_RIGHT
|
ManeuverType.TYPE_KEEP_RIGHT.value -> LaneDirection.SHAPE_SLIGHT_RIGHT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -212,8 +213,8 @@ class IconMapper {
|
|||||||
return when (direction) {
|
return when (direction) {
|
||||||
"left_straight" -> {
|
"left_straight" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_LEFT -> "left_o_straight_x"
|
ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> "left_o_straight_x"
|
||||||
Maneuver.TYPE_STRAIGHT -> "left_x_straight_o"
|
ManeuverType.TYPE_STRAIGHT.value -> "left_x_straight_o"
|
||||||
else
|
else
|
||||||
-> "left_x_straight_x"
|
-> "left_x_straight_x"
|
||||||
}
|
}
|
||||||
@@ -221,29 +222,29 @@ class IconMapper {
|
|||||||
|
|
||||||
"right_straight" -> {
|
"right_straight" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_RIGHT -> "right_x_straight_x"
|
ManeuverType.TYPE_TURN_NORMAL_RIGHT.value -> "right_x_straight_x"
|
||||||
Maneuver.TYPE_STRAIGHT -> "right_x_straight_o"
|
ManeuverType.TYPE_STRAIGHT.value -> "right_x_straight_o"
|
||||||
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> "right_o_straight_o"
|
ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value -> "right_o_straight_o"
|
||||||
else
|
else
|
||||||
-> "right_x_straight_x"
|
-> "right_x_straight_x"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_RIGHT) "${direction}_o" else "${direction}_x"
|
"right" -> if (stepData.currentManeuverType == ManeuverType.TYPE_TURN_NORMAL_RIGHT.value) "${direction}_o" else "${direction}_x"
|
||||||
"left" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_LEFT) "${direction}_o" else "${direction}_x"
|
"left" -> if (stepData.currentManeuverType == ManeuverType.TYPE_TURN_NORMAL_LEFT.value) "${direction}_o" else "${direction}_x"
|
||||||
"straight" -> if (stepData.currentManeuverType == Maneuver.TYPE_STRAIGHT
|
"straight" -> if (stepData.currentManeuverType == ManeuverType.TYPE_STRAIGHT.value
|
||||||
|| stepData.currentManeuverType == Maneuver.TYPE_KEEP_LEFT
|
|| stepData.currentManeuverType == ManeuverType.TYPE_KEEP_LEFT.value
|
||||||
|| stepData.currentManeuverType == Maneuver.TYPE_KEEP_RIGHT
|
|| stepData.currentManeuverType == ManeuverType.TYPE_KEEP_RIGHT.value
|
||||||
) "${direction}_o" else "${direction}_x"
|
) "${direction}_o" else "${direction}_x"
|
||||||
|
|
||||||
"right_slight", "slight_right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
"right_slight", "slight_right" -> if (stepData.currentManeuverType == ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value
|
||||||
|| stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_RIGHT
|
|| stepData.currentManeuverType == ManeuverType.TYPE_TURN_NORMAL_RIGHT.value
|
||||||
|| stepData.currentManeuverType == Maneuver.TYPE_KEEP_RIGHT
|
|| stepData.currentManeuverType == ManeuverType.TYPE_KEEP_RIGHT.value
|
||||||
) "slight_right_o" else "slight_right_x"
|
) "slight_right_o" else "slight_right_x"
|
||||||
|
|
||||||
"left_slight", "slight_left" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_LEFT
|
"left_slight", "slight_left" -> if (stepData.currentManeuverType == ManeuverType.TYPE_TURN_SLIGHT_LEFT.value
|
||||||
|| stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_LEFT
|
|| stepData.currentManeuverType == ManeuverType.TYPE_TURN_NORMAL_LEFT.value
|
||||||
|| stepData.currentManeuverType == Maneuver.TYPE_KEEP_LEFT
|
|| stepData.currentManeuverType == ManeuverType.TYPE_KEEP_LEFT.value
|
||||||
) "slight_left_o" else "slight_left_x"
|
) "slight_left_o" else "slight_left_x"
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
|||||||
@@ -1,33 +1,59 @@
|
|||||||
package com.kouros.navigation.model
|
package com.kouros.navigation.model
|
||||||
|
|
||||||
//import com.kouros.navigation.data.Preferences.boxStore
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
|
import com.kouros.navigation.data.Constants.FAVORITES
|
||||||
|
import com.kouros.navigation.data.Constants.SPEED_BEARING_DEVIATION
|
||||||
|
import com.kouros.navigation.data.Constants.SPEED_UPDATE_DISTANCE
|
||||||
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
|
import com.kouros.navigation.data.Constants.TANKER_KOENIG_DELAY
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
import com.kouros.navigation.data.NavigationRepository
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.Places
|
import com.kouros.navigation.data.Places
|
||||||
import com.kouros.navigation.data.SearchFilter
|
import com.kouros.navigation.data.SearchFilter
|
||||||
|
import com.kouros.navigation.data.fuel.FuelPrices
|
||||||
|
import com.kouros.navigation.data.fuel.Station
|
||||||
|
import com.kouros.navigation.data.fuel.Stations
|
||||||
import com.kouros.navigation.data.nominatim.Search
|
import com.kouros.navigation.data.nominatim.Search
|
||||||
import com.kouros.navigation.data.nominatim.SearchResult
|
import com.kouros.navigation.data.nominatim.SearchResult
|
||||||
|
import com.kouros.navigation.data.overpass.ElementSearch
|
||||||
import com.kouros.navigation.data.overpass.Elements
|
import com.kouros.navigation.data.overpass.Elements
|
||||||
import com.kouros.navigation.data.overpass.Overpass
|
import com.kouros.navigation.data.overpass.Overpass
|
||||||
import com.kouros.navigation.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.getSettingsRepository
|
||||||
|
import com.kouros.navigation.utils.isNumeric
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.maplibre.geojson.FeatureCollection
|
import org.maplibre.geojson.FeatureCollection
|
||||||
|
import org.maplibre.geojson.Point
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
import kotlin.collections.first
|
||||||
|
import kotlin.collections.forEach
|
||||||
|
import kotlin.comparisons.compareBy
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel for navigation-related data operations.
|
* ViewModel for navigation-related data operations.
|
||||||
@@ -35,6 +61,8 @@ import java.time.ZoneOffset
|
|||||||
*/
|
*/
|
||||||
class NavigationViewModel(private val repository: NavigationRepository) : ViewModel() {
|
class NavigationViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||||
|
|
||||||
|
private val overpass = Overpass()
|
||||||
|
|
||||||
/** LiveData containing the calculated route JSON string */
|
/** LiveData containing the calculated route JSON string */
|
||||||
val route: MutableLiveData<String> by lazy {
|
val route: MutableLiveData<String> by lazy {
|
||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
@@ -45,6 +73,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** LiveData containing categorized traffic messages map */
|
||||||
|
val trafficMessage: MutableLiveData<String> by lazy {
|
||||||
|
MutableLiveData()
|
||||||
|
}
|
||||||
|
|
||||||
/** LiveData containing a preview route JSON string for route preview screens */
|
/** LiveData containing a preview route JSON string for route preview screens */
|
||||||
val previewRoute: MutableLiveData<String> by lazy {
|
val previewRoute: MutableLiveData<String> by lazy {
|
||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
@@ -80,6 +113,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** LiveData containing POI elements from Overpass API */
|
||||||
|
val speedElements = mutableListOf<Elements>()
|
||||||
|
|
||||||
|
|
||||||
/** LiveData containing speed camera locations */
|
/** LiveData containing speed camera locations */
|
||||||
val speedCameras: MutableLiveData<List<Elements>> by lazy {
|
val speedCameras: MutableLiveData<List<Elements>> by lazy {
|
||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
@@ -100,6 +137,22 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
* Loads all recent places from Preferences and calculates distances.
|
||||||
@@ -110,14 +163,17 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
try {
|
try {
|
||||||
val settingsRepository = getSettingsRepository(context)
|
val settingsRepository = getSettingsRepository(context)
|
||||||
val rp = settingsRepository.recentPlacesFlow.first()
|
val rp = settingsRepository.recentPlacesFlow.first()
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
|
||||||
val places = gson.fromJson(rp, Places::class.java)
|
val places = gson.fromJson(rp, Places::class.java)
|
||||||
val pl = mutableListOf<Place>()
|
val pl = mutableListOf<Place>()
|
||||||
var id: Long = 0
|
var id: Long = 0
|
||||||
if (rp.isNotEmpty()) {
|
if (rp.isNotEmpty()) {
|
||||||
for (place in places.places) {
|
for (place in places.places) {
|
||||||
if (place.category.equals(Constants.RECENT)
|
if (place.category == Constants.RECENT
|
||||||
|| place.category.equals(Constants.FAVORITES)) {
|
|| place.category == FAVORITES
|
||||||
|
) {
|
||||||
|
if (place.category == FAVORITES) {
|
||||||
|
place.favorite = true
|
||||||
|
}
|
||||||
val plLocation = location(place.longitude, place.latitude)
|
val plLocation = location(place.longitude, place.latitude)
|
||||||
if (place.latitude != 0.0) {
|
if (place.latitude != 0.0) {
|
||||||
val distance =
|
val distance =
|
||||||
@@ -133,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) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
@@ -147,7 +205,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
fun loadRoute(
|
fun loadRoute(
|
||||||
context: Context,
|
context: Context,
|
||||||
currentLocation: Location,
|
currentLocation: Location,
|
||||||
destination: Location,
|
destination: List<Location>,
|
||||||
carOrientation: Float
|
carOrientation: Float
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
@@ -171,7 +229,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
* Fetches traffic incident data and categorizes by severity.
|
* Fetches traffic incident data and categorizes by severity.
|
||||||
* Posts categorized traffic map to traffic LiveData.
|
* Posts categorized traffic map to traffic LiveData.
|
||||||
*/
|
*/
|
||||||
fun loadTraffic(context: Context, currentLocation: Location, carOrientation: Float) {
|
fun loadTraffic(
|
||||||
|
context: Context,
|
||||||
|
currentLocation: Location,
|
||||||
|
carOrientation: Float,
|
||||||
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val data = repository.getTraffic(
|
val data = repository.getTraffic(
|
||||||
@@ -181,43 +243,18 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
)
|
)
|
||||||
if (data.isNotEmpty()) {
|
if (data.isNotEmpty()) {
|
||||||
val trafficData = rebuildTraffic(data)
|
val trafficData = rebuildTraffic(data)
|
||||||
|
if (trafficData.isNotEmpty()) {
|
||||||
traffic.postValue(
|
traffic.postValue(
|
||||||
trafficData
|
trafficData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Categorizes traffic incidents by type (queuing, stationary, slow, heavy, roadworks).
|
|
||||||
* @param data Raw traffic GeoJSON string
|
|
||||||
* @return Map of incident type to GeoJSON FeatureCollection
|
|
||||||
*/
|
|
||||||
private fun rebuildTraffic(data: String): Map<String, String> {
|
|
||||||
val featureCollection = FeatureCollection.fromJson(data)
|
|
||||||
val incidents = mutableMapOf<String, String>()
|
|
||||||
val queuing = featureCollection.features()!!
|
|
||||||
.filter { it.properties()!!.get("events").toString().contains("Queuing traffic") }
|
|
||||||
incidents["queuing"] = FeatureCollection.fromFeatures(queuing).toJson()
|
|
||||||
val stationary = featureCollection.features()!!
|
|
||||||
.filter { it.properties()!!.get("events").toString().contains("Stationary traffic") }
|
|
||||||
incidents["stationary"] = FeatureCollection.fromFeatures(stationary).toJson()
|
|
||||||
val slow = featureCollection.features()!!
|
|
||||||
.filter { it.properties()!!.get("events").toString().contains("Slow traffic") }
|
|
||||||
incidents["slow"] = FeatureCollection.fromFeatures(slow).toJson()
|
|
||||||
val heavy = featureCollection.features()!!
|
|
||||||
.filter { it.properties()!!.get("events").toString().contains("Heavy traffic") }
|
|
||||||
incidents["heavy"] = FeatureCollection.fromFeatures(heavy).toJson()
|
|
||||||
val roadworks = featureCollection.features()!!
|
|
||||||
.filter { it.properties()!!.get("events").toString().contains("Roadworks") }
|
|
||||||
incidents["roadworks"] = FeatureCollection.fromFeatures(roadworks).toJson()
|
|
||||||
|
|
||||||
return incidents
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates a preview route for route preview screen.
|
* Calculates a preview route for route preview screen.
|
||||||
* Posts the route JSON to previewRoute LiveData.
|
* Posts the route JSON to previewRoute LiveData.
|
||||||
@@ -234,7 +271,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
repository.getRoute(
|
repository.getRoute(
|
||||||
context,
|
context,
|
||||||
currentLocation,
|
currentLocation,
|
||||||
location,
|
listOf(location),
|
||||||
carOrientation,
|
carOrientation,
|
||||||
getSearchFilter(context)
|
getSearchFilter(context)
|
||||||
)
|
)
|
||||||
@@ -246,6 +283,39 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
return previewRoute.value
|
return previewRoute.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorizes traffic incidents by type (queuing, stationary, slow, heavy, roadworks).
|
||||||
|
* @param data Raw traffic GeoJSON string
|
||||||
|
* @return Map of incident type to GeoJSON FeatureCollection
|
||||||
|
*/
|
||||||
|
private fun rebuildTraffic(data: String): Map<String, String> {
|
||||||
|
val featureCollection = FeatureCollection.fromJson(data)
|
||||||
|
val incidents = mutableMapOf<String, String>()
|
||||||
|
val queuing = featureCollection.features()!!
|
||||||
|
.filter { it.properties()!!.get("events").toString().contains("Queuing traffic") }
|
||||||
|
incidents["queuing"] = buildLineStrings(queuing)
|
||||||
|
val stationary = featureCollection.features()!!
|
||||||
|
.filter { it.properties()!!.get("events").toString().contains("Stationary traffic") }
|
||||||
|
incidents["stationary"] = buildLineStrings(stationary)
|
||||||
|
val slow = featureCollection.features()!!
|
||||||
|
.filter { it.properties()!!.get("events").toString().contains("Slow traffic") }
|
||||||
|
incidents["slow"] = buildLineStrings(slow)
|
||||||
|
val heavy = featureCollection.features()!!
|
||||||
|
.filter { it.properties()!!.get("events").toString().contains("Heavy traffic") }
|
||||||
|
incidents["heavy"] = buildLineStrings(heavy)
|
||||||
|
val roadworks = featureCollection.features()!!
|
||||||
|
.filter { it.properties()!!.get("events").toString().contains("Roadworks") }
|
||||||
|
incidents["roadworks"] = buildLineStrings(roadworks)
|
||||||
|
val laneClosed = featureCollection.features()!!
|
||||||
|
.filter { it.properties()!!.get("events").toString().contains("Lane closed") }
|
||||||
|
incidents["lane"] = buildLineStrings(laneClosed)
|
||||||
|
val closed = featureCollection.features()!!
|
||||||
|
.filter { it.properties()!!.get("events").toString().contains("Closed") }
|
||||||
|
incidents["closed"] = buildPointCollection(closed)
|
||||||
|
|
||||||
|
return incidents
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads device contacts with addresses and converts to Place objects.
|
* Loads device contacts with addresses and converts to Place objects.
|
||||||
@@ -286,7 +356,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
var sortedList: List<SearchResult>
|
var sortedList: List<SearchResult>
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val placesJson = repository.searchPlaces(search, location)
|
val placesJson = repository.searchPlaces(search, location)
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
|
||||||
val places = gson.fromJson(placesJson, Search::class.java)
|
val places = gson.fromJson(placesJson, Search::class.java)
|
||||||
val distPlaces = mutableListOf<SearchResult>()
|
val distPlaces = mutableListOf<SearchResult>()
|
||||||
places.forEach {
|
places.forEach {
|
||||||
@@ -311,7 +380,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val placesJson = repository.searchPlaces(search, location)
|
val placesJson = repository.searchPlaces(search, location)
|
||||||
if (placesJson.isNotEmpty()) {
|
if (placesJson.isNotEmpty()) {
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
|
||||||
val places = gson.fromJson(placesJson, Search::class.java)
|
val places = gson.fromJson(placesJson, Search::class.java)
|
||||||
val distPlaces = mutableListOf<SearchResult>()
|
val distPlaces = mutableListOf<SearchResult>()
|
||||||
places.forEach {
|
places.forEach {
|
||||||
@@ -342,15 +410,25 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
* Queries Overpass API for nearby amenities of a specific category.
|
* Queries Overpass API for nearby amenities of a specific category.
|
||||||
* Posts sorted results to elements LiveData.
|
* Posts sorted results to elements LiveData.
|
||||||
*/
|
*/
|
||||||
fun getAmenities(category: String, location: Location) {
|
fun getAmenities(
|
||||||
|
carContext: Context,
|
||||||
|
category: String,
|
||||||
|
location: Location,
|
||||||
|
lastFuelUpdate: Long = 0
|
||||||
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
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>()
|
val distAmenities = mutableListOf<Elements>()
|
||||||
amenities.forEach {
|
amenities.forEach {
|
||||||
val plLocation =
|
val plLocation =
|
||||||
location(longitude = it.lon, latitude = it.lat)
|
location(longitude = it.lon, latitude = it.lat)
|
||||||
val distance = plLocation.distanceTo(location)
|
val distance = plLocation.distanceTo(location)
|
||||||
it.distance = distance.toDouble()
|
it.distance = distance.toDouble()
|
||||||
|
if (category == Constants.FUEL_STATION) {
|
||||||
|
addFuelPrice(it, fuelPrices, location)
|
||||||
|
}
|
||||||
distAmenities.add(it)
|
distAmenities.add(it)
|
||||||
}
|
}
|
||||||
val sortedList = distAmenities.sortedWith(compareBy { it.distance })
|
val sortedList = distAmenities.sortedWith(compareBy { it.distance })
|
||||||
@@ -358,13 +436,51 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun fuelStations(
|
||||||
|
category: String,
|
||||||
|
lastFuelUpdate: Long,
|
||||||
|
location: Location,
|
||||||
|
repository: SettingsRepository
|
||||||
|
): List<Station> {
|
||||||
|
var fuelPrices = emptyList<Station>()
|
||||||
|
if (category == Constants.FUEL_STATION) {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if ((now - lastFuelUpdate) > TANKER_KOENIG_DELAY) {
|
||||||
|
fuelPrices = FuelPrices().getFuelPrices(location, 5)
|
||||||
|
repository.setLastFuelPrices(System.currentTimeMillis())
|
||||||
|
repository.setFuelPrices(gson.toJson(Stations(fuelPrices)))
|
||||||
|
} else {
|
||||||
|
val fuels = repository.fuelPricesFlow.first()
|
||||||
|
fuelPrices =
|
||||||
|
gson.fromJson(fuels, Stations::class.java).stations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fuelPrices
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addFuelPrice(fuel: Elements, fuelPrices: List<Station>, location: Location) {
|
||||||
|
fuelPrices.forEach {
|
||||||
|
if (fuel.tags.name.equals(it.brand, ignoreCase = true)) {
|
||||||
|
val plLocation =
|
||||||
|
location(longitude = it.lng, latitude = it.lat)
|
||||||
|
val distance = plLocation.distanceTo(location)
|
||||||
|
if ((distance - fuel.distance).absoluteValue < 30) {
|
||||||
|
fuel.e10 = it.e10
|
||||||
|
fuel.e5 = it.e5
|
||||||
|
fuel.diesel = it.diesel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries Overpass API for speed cameras within a radius.
|
* Queries Overpass API for speed cameras within a radius.
|
||||||
* Posts sorted results to speedCameras LiveData.
|
* Posts sorted results to speedCameras LiveData.
|
||||||
*/
|
*/
|
||||||
fun getSpeedCameras(location: Location, radius: Double) {
|
fun getSpeedCameras(location: Location, radius: Double) {
|
||||||
|
synchronized(this) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius)
|
val amenities = overpass.getAmenities("highway", "speed_camera", location, radius)
|
||||||
val distAmenities = mutableListOf<Elements>()
|
val distAmenities = mutableListOf<Elements>()
|
||||||
amenities.forEach {
|
amenities.forEach {
|
||||||
val plLocation =
|
val plLocation =
|
||||||
@@ -377,25 +493,111 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
speedCameras.postValue(sortedList)
|
speedCameras.postValue(sortedList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries Overpass API for speed limit on current road using fuzzy matching.
|
* Queries Overpass API for speed limit on current road using fuzzy matching.
|
||||||
* Posts speed limit to maxSpeed LiveData.
|
* Posts speed limit to maxSpeed LiveData.
|
||||||
*/
|
*/
|
||||||
fun getMaxSpeed(location: Location, street: String) {
|
fun getSpeedLimit(
|
||||||
|
location: Location,
|
||||||
|
routeBearing: Float,
|
||||||
|
countryCode: String,
|
||||||
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val levenshtein = Levenshtein()
|
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 lineString = "${location.latitude},${location.longitude}"
|
||||||
val amenities = Overpass().getAround(10, lineString)
|
val elements =
|
||||||
amenities.forEach {
|
overpass.getSpeedLimit(SPEED_UPDATE_DISTANCE, lineString, street, roadNumbers)
|
||||||
if (it.tags.name != null) {
|
speedElements.clear()
|
||||||
val distance =
|
speedElements.addAll(elements)
|
||||||
levenshtein.distance(it.tags.name!!, street)
|
|
||||||
if (distance < 5) {
|
|
||||||
val speed = it.tags.maxspeed.toInt()
|
|
||||||
maxSpeed.postValue(speed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,7 +633,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val places = mutableListOf<Place>()
|
val places = mutableListOf<Place>()
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
|
||||||
val settingsRepository = getSettingsRepository(context)
|
val settingsRepository = getSettingsRepository(context)
|
||||||
val rp = settingsRepository.recentPlacesFlow.first()
|
val rp = settingsRepository.recentPlacesFlow.first()
|
||||||
var id: Long = 0
|
var id: Long = 0
|
||||||
@@ -441,6 +642,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
for (curPlace in recentPlaces) {
|
for (curPlace in recentPlaces) {
|
||||||
if (curPlace.name != place.name || curPlace.category != place.category) {
|
if (curPlace.name != place.name || curPlace.category != place.category) {
|
||||||
curPlace.id = id
|
curPlace.id = id
|
||||||
|
curPlace.route = ""
|
||||||
places.add(curPlace)
|
places.add(curPlace)
|
||||||
id += 1
|
id += 1
|
||||||
}
|
}
|
||||||
@@ -448,7 +650,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
}
|
}
|
||||||
val current = LocalDateTime.now(ZoneOffset.UTC)
|
val current = LocalDateTime.now(ZoneOffset.UTC)
|
||||||
place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond()
|
place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond()
|
||||||
|
place.route = ""
|
||||||
|
place.navigations += 1
|
||||||
places.add(place)
|
places.add(place)
|
||||||
|
recentPlaces.postValue(places)
|
||||||
settingsRepository.setRecentPlaces(gson.toJson(Places(places)))
|
settingsRepository.setRecentPlaces(gson.toJson(Places(places)))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -470,7 +675,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
fun deletePlace(context: Context, place: Place) {
|
fun deletePlace(context: Context, place: Place) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
|
||||||
val settingsRepository = getSettingsRepository(context)
|
val settingsRepository = getSettingsRepository(context)
|
||||||
val rp = settingsRepository.recentPlacesFlow.first()
|
val rp = settingsRepository.recentPlacesFlow.first()
|
||||||
val places = mutableListOf<Place>()
|
val places = mutableListOf<Place>()
|
||||||
@@ -479,11 +683,12 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate }
|
gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate }
|
||||||
for (curPlace in rPlaces) {
|
for (curPlace in rPlaces) {
|
||||||
if (curPlace.name != place.name || curPlace.category != place.category) {
|
if (curPlace.name != place.name || curPlace.category != place.category) {
|
||||||
|
curPlace.route = ""
|
||||||
places.add(curPlace)
|
places.add(curPlace)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
settingsRepository.setRecentPlaces(gson.toJson(Places(places)))
|
settingsRepository.setRecentPlaces(gson.toJson(Places(places)))
|
||||||
recentPlaces.value = places
|
recentPlaces.postValue(places)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -508,12 +713,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
* Loads recent places as Compose SnapshotStateList.
|
* Loads recent places as Compose SnapshotStateList.
|
||||||
* @return SnapshotStateList of recent places
|
* @return SnapshotStateList of recent places
|
||||||
*/
|
*/
|
||||||
fun loadRecentPlace(context: Context): SnapshotStateList<Place?> {
|
fun loadRecentPlaces(context: Context): SnapshotStateList<Place?> {
|
||||||
val pl = mutableListOf<Place>()
|
val pl = mutableListOf<Place>()
|
||||||
val settingsRepository = getSettingsRepository(context)
|
val settingsRepository = getSettingsRepository(context)
|
||||||
val rp = runBlocking { settingsRepository.recentPlacesFlow.first() }
|
val rp = runBlocking { settingsRepository.recentPlacesFlow.first() }
|
||||||
if (rp.isNotEmpty()) {
|
if (rp.isNotEmpty()) {
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
|
||||||
val recentPlaces = gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate }
|
val recentPlaces = gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate }
|
||||||
for (place in recentPlaces) {
|
for (place in recentPlaces) {
|
||||||
if (place.category == Constants.RECENT) {
|
if (place.category == Constants.RECENT) {
|
||||||
@@ -523,4 +727,31 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
}
|
}
|
||||||
return pl.toMutableStateList()
|
return pl.toMutableStateList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads current location nearest street
|
||||||
|
* and snap to location to street
|
||||||
|
*/
|
||||||
|
fun loadCurrentLocation(location: Location) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
synchronized(this) {
|
||||||
|
val elements = overpass.getStreet(location)
|
||||||
|
if (elements.isNotEmpty()) {
|
||||||
|
val points = mutableListOf<Point>()
|
||||||
|
elements.first().geometry.forEach {
|
||||||
|
points.add(Point.fromLngLat(it.lon, it.lat))
|
||||||
|
}
|
||||||
|
val snappedLocation = snapLocation(location, points)
|
||||||
|
if (points.size > 1) {
|
||||||
|
val bearing = location(
|
||||||
|
points.first().longitude(),
|
||||||
|
points.first().latitude()
|
||||||
|
).bearingPositive(location(points[1].longitude(), points[1].latitude()))
|
||||||
|
snappedLocation.bearing = bearing
|
||||||
|
}
|
||||||
|
initialSnapLocation.postValue(snappedLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,38 +5,61 @@ import android.util.Log
|
|||||||
import androidx.car.app.navigation.model.Step
|
import androidx.car.app.navigation.model.Step
|
||||||
import com.kouros.navigation.data.Constants.MAXIMUM_LOCATION_DISTANCE
|
import com.kouros.navigation.data.Constants.MAXIMUM_LOCATION_DISTANCE
|
||||||
import com.kouros.navigation.data.Constants.NEAREST_LOCATION_DISTANCE
|
import com.kouros.navigation.data.Constants.NEAREST_LOCATION_DISTANCE
|
||||||
|
import com.kouros.navigation.data.Constants.SPEED_UPDATE_DISTANCE
|
||||||
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
|
import com.kouros.navigation.data.StepMatch
|
||||||
|
import com.kouros.navigation.data.osrm.Waypoints
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
class RouteCalculator(var routeModel: RouteModel) {
|
class RouteCalculator(var routeModel: RouteModel) {
|
||||||
|
|
||||||
|
var bestMatch: StepMatch? = null
|
||||||
var lastSpeedLocation: Location = location(0.0, 0.0)
|
var lastSpeedLocation: Location = location(0.0, 0.0)
|
||||||
|
var lastLocalSpeedLocation: Location = location(0.0, 0.0)
|
||||||
|
|
||||||
var lastSpeedIndex: Int = 0
|
var lastSpeedIndex: Int = 0
|
||||||
|
|
||||||
fun findStep(location: Location) {
|
fun findStep(location: Location) {
|
||||||
var nearestDistance = MAXIMUM_LOCATION_DISTANCE
|
var nearestDistance = MAXIMUM_LOCATION_DISTANCE
|
||||||
|
var count = 0
|
||||||
|
var lastDistance = 0F
|
||||||
|
var increaseDistance = 0
|
||||||
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
|
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
|
||||||
|
count++
|
||||||
|
var distance = 0F
|
||||||
if (index >= routeModel.navState.route.currentStepIndex) {
|
if (index >= routeModel.navState.route.currentStepIndex) {
|
||||||
for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
||||||
|
count++
|
||||||
if (wayIndex >= step.waypointIndex) {
|
if (wayIndex >= step.waypointIndex) {
|
||||||
val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
|
distance = location.distanceTo(waypoint)
|
||||||
if (distance < nearestDistance ) {
|
if (distance < nearestDistance ) {
|
||||||
nearestDistance = distance
|
nearestDistance = distance
|
||||||
routeModel.navState.route.currentStepIndex = step.index
|
routeModel.navState.route.currentStepIndex = step.index
|
||||||
step.waypointIndex = wayIndex
|
step.waypointIndex = wayIndex
|
||||||
step.wayPointLocation = 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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun stopSearch(nearestDistance: Float, distance: Float): Boolean {
|
||||||
|
return nearestDistance < NEAREST_LOCATION_DISTANCE && distance > NEAREST_LOCATION_DISTANCE * 10
|
||||||
|
}
|
||||||
|
|
||||||
fun travelLeftTime(): Double {
|
fun travelLeftTime(): Double {
|
||||||
var timeLeft = 0.0
|
var timeLeft = 0.0
|
||||||
// time for next step until end step
|
// time for next step until end step
|
||||||
@@ -70,19 +93,15 @@ class RouteCalculator(var routeModel: RouteModel) {
|
|||||||
fun leftStepDistance(): Double {
|
fun leftStepDistance(): Double {
|
||||||
val step = routeModel.route.currentStep()
|
val step = routeModel.route.currentStep()
|
||||||
var leftDistance = 0F
|
var leftDistance = 0F
|
||||||
for (i in step.waypointIndex..<step.maneuver.waypoints.size - 1) {
|
if (step.waypointIndex < step.maneuver.waypoints.size - 1) {
|
||||||
val loc1 = location(step.maneuver.waypoints[i][0], step.maneuver.waypoints[i][1])
|
leftDistance = step.maneuver.leftDistance[step.waypointIndex]
|
||||||
val loc2 =
|
val waypointLocation = step.maneuver.waypoints[step.waypointIndex]
|
||||||
location(step.maneuver.waypoints[i + 1][0], step.maneuver.waypoints[i + 1][1])
|
if (routeModel.navState.lastLocation.latitude != 0.0) {
|
||||||
val locationDistance = loc1.distanceTo(routeModel.navState.lastLocation)
|
val locationDistance = waypointLocation.distanceTo(routeModel.navState.lastLocation)
|
||||||
val distance = loc1.distanceTo(loc2)
|
leftDistance -= locationDistance
|
||||||
leftDistance += if (locationDistance < distance) {
|
|
||||||
locationDistance
|
|
||||||
} else {
|
|
||||||
distance
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return leftDistance.toDouble()
|
return leftDistance.absoluteValue.toDouble()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the left distance in m. */
|
/** Returns the left distance in m. */
|
||||||
@@ -107,14 +126,31 @@ class RouteCalculator(var routeModel: RouteModel) {
|
|||||||
return nowUtcMillis + timeToDestinationMillis
|
return nowUtcMillis + timeToDestinationMillis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the speed limit in the view model.
|
||||||
|
*/
|
||||||
fun updateSpeedLimit(location: Location, viewModel: NavigationViewModel) {
|
fun updateSpeedLimit(location: Location, viewModel: NavigationViewModel) {
|
||||||
if (routeModel.isNavigating()) {
|
if (routeModel.isNavigating()) {
|
||||||
// speed limit
|
// speed limit
|
||||||
val distance = lastSpeedLocation.distanceTo(location)
|
val distance = lastSpeedLocation.distanceTo(location)
|
||||||
if (distance > 500 || lastSpeedIndex < routeModel.route.currentStepIndex) {
|
if ((distance > SPEED_UPDATE_DISTANCE * 2) || lastSpeedIndex < routeModel.route.currentStepIndex) {
|
||||||
lastSpeedIndex = routeModel.route.currentStepIndex
|
lastSpeedIndex = routeModel.route.currentStepIndex
|
||||||
lastSpeedLocation = location
|
lastSpeedLocation = location
|
||||||
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
|
package com.kouros.navigation.model
|
||||||
|
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
|
import android.util.Log
|
||||||
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE
|
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE
|
||||||
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION
|
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION
|
||||||
import androidx.car.app.navigation.model.Maneuver
|
import androidx.car.app.navigation.model.Maneuver
|
||||||
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
|
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
|
||||||
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
import com.kouros.navigation.data.NavigationState
|
import com.kouros.navigation.data.NavigationState
|
||||||
import com.kouros.navigation.data.Route
|
import com.kouros.navigation.data.Route
|
||||||
import com.kouros.navigation.data.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
@@ -76,48 +78,9 @@ open class RouteModel {
|
|||||||
navState = navState.copy(lastLocation = navState.currentLocation)
|
navState = navState.copy(lastLocation = navState.currentLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun nextStep(): StepData {
|
/*
|
||||||
val distanceToNextStep = routeCalculator.leftStepDistance()
|
* Returns the current step
|
||||||
val nextStep = navState.route.nextStep(1)
|
*/
|
||||||
var streetName = nextStep.street
|
|
||||||
var maneuverType = currentStep.maneuver.type
|
|
||||||
if (distanceToNextStep < NEXT_STEP_THRESHOLD) {
|
|
||||||
streetName = nextStep.maneuver.street
|
|
||||||
maneuverType = nextStep.maneuver.type
|
|
||||||
}
|
|
||||||
|
|
||||||
val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType)
|
|
||||||
// Construct and return the final StepData object
|
|
||||||
return StepData(
|
|
||||||
instruction = streetName,
|
|
||||||
street = "",
|
|
||||||
leftStepDistance = distanceToNextStep,
|
|
||||||
currentManeuverType = maneuverType,
|
|
||||||
icon = maneuverIcon,
|
|
||||||
arrivalTime = routeCalculator.arrivalTime(),
|
|
||||||
leftDistance = routeCalculator.travelLeftDistance(),
|
|
||||||
exitNumber = nextStep.maneuver.exit,
|
|
||||||
message = nextStep.maneuver.message
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun currentLanes(): List<Lane> {
|
|
||||||
var lanes = emptyList<Lane>()
|
|
||||||
if (navState.route.legs().isNotEmpty()) {
|
|
||||||
currentStep.intersection.forEach {
|
|
||||||
if (it.lane.isNotEmpty()) {
|
|
||||||
val distance =
|
|
||||||
navState.lastLocation.distanceTo(location(it.location[0], it.location[1]))
|
|
||||||
if (distance < NEXT_STEP_THRESHOLD) {
|
|
||||||
lanes = it.lane
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lanes
|
|
||||||
}
|
|
||||||
|
|
||||||
fun currentStep(): StepData {
|
fun currentStep(): StepData {
|
||||||
val distanceToNextStep = routeCalculator.leftStepDistance()
|
val distanceToNextStep = routeCalculator.leftStepDistance()
|
||||||
// Determine the maneuver type and corresponding icon
|
// Determine the maneuver type and corresponding icon
|
||||||
@@ -144,10 +107,59 @@ open class RouteModel {
|
|||||||
leftDistance = routeCalculator.travelLeftDistance(),
|
leftDistance = routeCalculator.travelLeftDistance(),
|
||||||
lane = currentLanes,
|
lane = currentLanes,
|
||||||
exitNumber = exitNumber,
|
exitNumber = exitNumber,
|
||||||
message = currentStep.maneuver.message
|
message = currentStep.maneuver.message,
|
||||||
|
roadNumbers = currentStep.roadNumbers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns the next step
|
||||||
|
*/
|
||||||
|
fun nextStep(): StepData {
|
||||||
|
val distanceToNextStep = routeCalculator.leftStepDistance()
|
||||||
|
val nextStep = navState.route.nextStep(1)
|
||||||
|
var streetName = nextStep.street
|
||||||
|
var maneuverType = currentStep.maneuver.type
|
||||||
|
if (distanceToNextStep < NEXT_STEP_THRESHOLD) {
|
||||||
|
streetName = nextStep.maneuver.street
|
||||||
|
maneuverType = nextStep.maneuver.type
|
||||||
|
}
|
||||||
|
|
||||||
|
val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType)
|
||||||
|
// Construct and return the final StepData object
|
||||||
|
return StepData(
|
||||||
|
instruction = streetName,
|
||||||
|
street = "",
|
||||||
|
leftStepDistance = distanceToNextStep,
|
||||||
|
currentManeuverType = maneuverType,
|
||||||
|
icon = maneuverIcon,
|
||||||
|
arrivalTime = routeCalculator.arrivalTime(),
|
||||||
|
leftDistance = routeCalculator.travelLeftDistance(),
|
||||||
|
exitNumber = nextStep.maneuver.exit,
|
||||||
|
message = nextStep.maneuver.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns the current lanes
|
||||||
|
*/
|
||||||
|
private fun currentLanes(): List<Lane> {
|
||||||
|
var lanes = emptyList<Lane>()
|
||||||
|
if (navState.route.legs().isNotEmpty()) {
|
||||||
|
currentStep.intersection.forEach {
|
||||||
|
if (it.lane.isNotEmpty()) {
|
||||||
|
val distance =
|
||||||
|
navState.lastLocation.distanceTo(location(it.location[0], it.location[1]))
|
||||||
|
if (distance < NEXT_STEP_THRESHOLD) {
|
||||||
|
lanes = it.lane
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lanes
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for navigating
|
* Checks for navigating
|
||||||
*/
|
*/
|
||||||
@@ -158,7 +170,7 @@ open class RouteModel {
|
|||||||
/**
|
/**
|
||||||
* Checks for arrival
|
* Checks for arrival
|
||||||
*/
|
*/
|
||||||
fun isArrival(): Boolean {
|
fun isManeuverArrival(): Boolean {
|
||||||
return navState.maneuverType == Maneuver.TYPE_DESTINATION
|
return navState.maneuverType == Maneuver.TYPE_DESTINATION
|
||||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|
||||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package com.kouros.navigation.model
|
package com.kouros.navigation.model
|
||||||
|
|
||||||
import androidx.datastore.preferences.core.edit
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.kouros.navigation.data.datastore.DataStoreManager.Companion.dataStore
|
|
||||||
import com.kouros.navigation.data.datastore.DataStoreManager.PreferencesKeys
|
|
||||||
import com.kouros.navigation.repository.SettingsRepository
|
import com.kouros.navigation.repository.SettingsRepository
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -102,6 +99,24 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
|
|||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val alternativeRoutes = repository.alternativeRoutesFlow.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(5_000),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
val lastFuelPrices = repository.lastFuelPricesFlow.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(5_000),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
val fuelPrices = repository.fuelPricesFlow.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(5_000),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
fun onShow3DChanged(enabled: Boolean) {
|
fun onShow3DChanged(enabled: Boolean) {
|
||||||
viewModelScope.launch { repository.setShow3D(enabled) }
|
viewModelScope.launch { repository.setShow3D(enabled) }
|
||||||
}
|
}
|
||||||
@@ -159,4 +174,15 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
|
|||||||
viewModelScope.launch { repository.setEngineType(mode) }
|
viewModelScope.launch { repository.setEngineType(mode) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.kouros.navigation.repository
|
package com.kouros.navigation.repository
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.kouros.navigation.data.datastore.DataStoreManager
|
import com.kouros.navigation.data.datastore.DataStoreManager
|
||||||
|
import com.kouros.navigation.data.fuel.FuelPrices
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class SettingsRepository(
|
class SettingsRepository(
|
||||||
@@ -50,6 +52,16 @@ class SettingsRepository(
|
|||||||
val engineTypeFlow: Flow<Int> =
|
val engineTypeFlow: Flow<Int> =
|
||||||
dataStoreManager.engineTypeFlow
|
dataStoreManager.engineTypeFlow
|
||||||
|
|
||||||
|
val alternativeRoutesFlow: Flow<Boolean> =
|
||||||
|
dataStoreManager.alternativeRoutesFlow
|
||||||
|
|
||||||
|
val lastFuelPricesFlow: Flow<Long> =
|
||||||
|
dataStoreManager.lastFuelPricesFlow
|
||||||
|
|
||||||
|
val fuelPricesFlow: Flow<String> =
|
||||||
|
dataStoreManager.fuelPricesFlow
|
||||||
|
|
||||||
|
|
||||||
suspend fun setShow3D(enabled: Boolean) {
|
suspend fun setShow3D(enabled: Boolean) {
|
||||||
dataStoreManager.setShow3D(enabled)
|
dataStoreManager.setShow3D(enabled)
|
||||||
}
|
}
|
||||||
@@ -109,4 +121,16 @@ class SettingsRepository(
|
|||||||
suspend fun setEngineType(mode: Int) {
|
suspend fun setEngineType(mode: Int) {
|
||||||
dataStoreManager.setEngineType(mode)
|
dataStoreManager.setEngineType(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,21 +4,28 @@ import android.location.Location
|
|||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import org.maplibre.geojson.FeatureCollection
|
import org.maplibre.geojson.FeatureCollection
|
||||||
|
import org.maplibre.geojson.LineString
|
||||||
import org.maplibre.geojson.Point
|
import org.maplibre.geojson.Point
|
||||||
import org.maplibre.spatialk.geojson.Feature
|
import org.maplibre.spatialk.geojson.Feature
|
||||||
import org.maplibre.spatialk.geojson.dsl.addFeature
|
import org.maplibre.spatialk.geojson.dsl.addFeature
|
||||||
import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection
|
import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection
|
||||||
import org.maplibre.spatialk.geojson.dsl.buildLineString
|
import org.maplibre.spatialk.geojson.dsl.buildLineString
|
||||||
|
import org.maplibre.spatialk.geojson.dsl.buildMultiPoint
|
||||||
import org.maplibre.spatialk.geojson.toJson
|
import org.maplibre.spatialk.geojson.toJson
|
||||||
import org.maplibre.turf.TurfMeasurement
|
import org.maplibre.turf.TurfMeasurement
|
||||||
import org.maplibre.turf.TurfMisc
|
import org.maplibre.turf.TurfMisc
|
||||||
import java.lang.Math.toDegrees
|
import java.lang.Math.toDegrees
|
||||||
import java.lang.Math.toRadians
|
import java.lang.Math.toRadians
|
||||||
|
import kotlin.math.asin
|
||||||
|
import kotlin.math.atan2
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
object GeoUtils {
|
object GeoUtils {
|
||||||
|
|
||||||
|
const val EARTH_RADIUS = 6371.0 // in km
|
||||||
|
|
||||||
fun snapLocation(location: Location, stepCoordinates: List<Point>): Location {
|
fun snapLocation(location: Location, stepCoordinates: List<Point>): Location {
|
||||||
val newLocation = Location(location)
|
val newLocation = Location(location)
|
||||||
val oldPoint = Point.fromLngLat(location.longitude, location.latitude)
|
val oldPoint = Point.fromLngLat(location.longitude, location.latitude)
|
||||||
@@ -94,10 +101,12 @@ object GeoUtils {
|
|||||||
//return createPointCollection(lineCoordinates, "Route")
|
//return createPointCollection(lineCoordinates, "Route")
|
||||||
val lineString = buildLineString {
|
val lineString = buildLineString {
|
||||||
lineCoordinates.forEach {
|
lineCoordinates.forEach {
|
||||||
add(org.maplibre.spatialk.geojson.Point(
|
add(
|
||||||
|
org.maplibre.spatialk.geojson.Point(
|
||||||
it[0],
|
it[0],
|
||||||
it[1]
|
it[1]
|
||||||
))
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val feature = Feature(lineString, null)
|
val feature = Feature(lineString, null)
|
||||||
@@ -117,31 +126,150 @@ object GeoUtils {
|
|||||||
return featureCollection.toJson()
|
return featureCollection.toJson()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createStartCollection(geoJson: String): String {
|
||||||
|
val featureCollection = FeatureCollection.fromJson(geoJson)
|
||||||
|
val geometry = featureCollection.features()!!.first().geometry()
|
||||||
|
val coordinates = (geometry as LineString)
|
||||||
|
val first = coordinates.coordinates().first()
|
||||||
|
val points = createPointCollection(
|
||||||
|
listOf(listOf(first.coordinates()[0], first.coordinates()[1])), "End"
|
||||||
|
)
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createPointCollection(geoJson: String): String {
|
||||||
|
val featureCollection = FeatureCollection.fromJson(geoJson)
|
||||||
|
val geometry = featureCollection.features()!!.first().geometry()
|
||||||
|
val coordinates = (geometry as LineString)
|
||||||
|
val last = coordinates.coordinates().last()
|
||||||
|
val points = createPointCollection(
|
||||||
|
listOf(listOf(last.coordinates()[0], last.coordinates()[1])), "End"
|
||||||
|
)
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the lat and len of a square around a point.
|
* Calculate the lat and len of a square around a point.
|
||||||
* @return latMin, latMax, lngMin, lngMax
|
* @return latMin, latMax, lngMin, lngMax
|
||||||
*/
|
*/
|
||||||
fun calculateSquareRadius(lat: Double, lng: Double, radius: Double): String {
|
fun calculateSquareRadius(lat: Double, lng: Double, radius: Double): String {
|
||||||
val earthRadius = 6371.0 // earth radius in km
|
val latMin = lat - toDegrees(radius / EARTH_RADIUS)
|
||||||
val latMin = lat - toDegrees(radius / earthRadius)
|
val latMax = lat + toDegrees(radius / EARTH_RADIUS)
|
||||||
val latMax = lat + toDegrees(radius / earthRadius)
|
val lngMin = lng - toDegrees(radius / EARTH_RADIUS / cos(toRadians(lat)))
|
||||||
val lngMin = lng - toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
val lngMax = lng + toDegrees(radius / EARTH_RADIUS / cos(toRadians(lat)))
|
||||||
val lngMax = lng + toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
|
||||||
|
|
||||||
return "$lngMin,$latMin,$lngMax,$latMax"
|
return "$lngMin,$latMin,$lngMax,$latMax"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBoundingBox(
|
fun getBoundingBox(
|
||||||
lat: Double,
|
lat: Double,
|
||||||
lon: Double,
|
lon: Double,
|
||||||
radius: Double
|
radius: Double
|
||||||
): String {
|
): String {
|
||||||
val earthRadius = 6371.0
|
val maxLat = lat + toDegrees(radius / EARTH_RADIUS)
|
||||||
val maxLat = lat + toDegrees(radius / earthRadius)
|
val minLat = lat - toDegrees(radius / EARTH_RADIUS)
|
||||||
val minLat = lat - toDegrees(radius / earthRadius)
|
val maxLon = lon + toDegrees(radius / EARTH_RADIUS / cos(toRadians(lat)))
|
||||||
val maxLon = lon + toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
val minLon = lon - toDegrees(radius / EARTH_RADIUS / cos(toRadians(lat)))
|
||||||
val minLon = lon - toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
|
||||||
|
|
||||||
return "$minLat,$minLon,$maxLat,$maxLon"
|
return "$minLat,$minLon,$maxLat,$maxLon"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isLocationInBoundingBox(
|
||||||
|
bottomLeftLat: Double,
|
||||||
|
bottomLeftLon: Double,
|
||||||
|
topRightLat: Double,
|
||||||
|
topRightLon: Double,
|
||||||
|
location: Location
|
||||||
|
): Boolean {
|
||||||
|
val isInside =
|
||||||
|
location.latitude in bottomLeftLat..topRightLat
|
||||||
|
&& location.longitude >= bottomLeftLon
|
||||||
|
&& location.longitude <= topRightLon
|
||||||
|
return isInside
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildLineStrings(features: List<org.maplibre.geojson.Feature>): String {
|
||||||
|
return buildFeatureCollection {
|
||||||
|
features.forEach {
|
||||||
|
val movedCoordinates = arrayListOf<org.maplibre.spatialk.geojson.Point>()
|
||||||
|
val coordinates = it.geometry() as LineString
|
||||||
|
val geo = coordinates.coordinates()
|
||||||
|
var bearing = 0.0F
|
||||||
|
for (index in 0..<geo.size) {
|
||||||
|
val current = location(geo[index].longitude(), geo[index].latitude())
|
||||||
|
bearing = if (index < geo.size - 1) {
|
||||||
|
val next =
|
||||||
|
location(geo[index + 1].longitude(), geo[index + 1].latitude())
|
||||||
|
current.bearingPositive(next)
|
||||||
|
} else {
|
||||||
|
bearing
|
||||||
|
}
|
||||||
|
val point = movePoint(
|
||||||
|
current.latitude,
|
||||||
|
current.longitude,
|
||||||
|
bearingDegrees = bearing + 90.0
|
||||||
|
)
|
||||||
|
movedCoordinates.add(point)
|
||||||
|
}
|
||||||
|
val lineString = buildLineString {
|
||||||
|
movedCoordinates.forEach { ft ->
|
||||||
|
add(
|
||||||
|
ft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addFeature {
|
||||||
|
geometry = lineString
|
||||||
|
properties = buildJsonObject { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.toJson()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildPointCollection(features: List<org.maplibre.geojson.Feature>): String {
|
||||||
|
return buildFeatureCollection {
|
||||||
|
features.forEach {
|
||||||
|
val movedCoordinates = arrayListOf<org.maplibre.spatialk.geojson.Point>()
|
||||||
|
val coordinates = it.geometry() as LineString
|
||||||
|
val geo = coordinates.coordinates()
|
||||||
|
var point = org.maplibre.spatialk.geojson.Point(geo.first().longitude(), geo.first().latitude())
|
||||||
|
movedCoordinates.add(point)
|
||||||
|
point = org.maplibre.spatialk.geojson.Point(geo.last().longitude(), geo.last().latitude())
|
||||||
|
movedCoordinates.add(point)
|
||||||
|
val multiPoint = buildMultiPoint {
|
||||||
|
movedCoordinates.forEach { ft ->
|
||||||
|
add(
|
||||||
|
ft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addFeature {
|
||||||
|
geometry = multiPoint
|
||||||
|
properties = buildJsonObject { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.toJson()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun movePoint(
|
||||||
|
latitude: Double,
|
||||||
|
longitude: Double,
|
||||||
|
distanceMeters: Double = 5.0,
|
||||||
|
bearingDegrees: Double = 0.0
|
||||||
|
): org.maplibre.spatialk.geojson.Point {
|
||||||
|
val radius = EARTH_RADIUS * 1000 // meters
|
||||||
|
val bearingRadians = toRadians(bearingDegrees)
|
||||||
|
val lat1 = toRadians(latitude)
|
||||||
|
val lon1 = toRadians(longitude)
|
||||||
|
|
||||||
|
val lat2 = asin(
|
||||||
|
sin(lat1) * cos(distanceMeters / radius) +
|
||||||
|
cos(lat1) * sin(distanceMeters / radius) * cos(bearingRadians)
|
||||||
|
)
|
||||||
|
val lon2 = lon1 + atan2(
|
||||||
|
sin(bearingRadians) * sin(distanceMeters / radius) * cos(lat1),
|
||||||
|
cos(distanceMeters / radius) - sin(lat1) * sin(lat2)
|
||||||
|
)
|
||||||
|
return org.maplibre.spatialk.geojson.Point(toDegrees(lon2), toDegrees(lat2))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -25,10 +25,11 @@ class Levenshtein {
|
|||||||
* @param limit the maximum result to compute before stopping, terminating calculation early.
|
* @param limit the maximum result to compute before stopping, terminating calculation early.
|
||||||
* @return the computed Levenshtein distance.
|
* @return the computed Levenshtein distance.
|
||||||
*/
|
*/
|
||||||
fun distance(first: CharSequence, second: CharSequence, 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 == second) return 0
|
||||||
if (first.isEmpty()) return second.length
|
if (first.isEmpty()) return 0
|
||||||
if (second.isEmpty()) return first.length
|
if (second.isEmpty()) return 0
|
||||||
|
|
||||||
// initial costs is the edit distance from an empty string, which corresponds to the characters to inserts.
|
// initial costs is the edit distance from an empty string, which corresponds to the characters to inserts.
|
||||||
// the array size is : length + 1 (empty string)
|
// the array size is : length + 1 (empty string)
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ package com.kouros.navigation.utils
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.util.Log
|
|
||||||
import androidx.car.app.model.Distance
|
import androidx.car.app.model.Distance
|
||||||
import com.kouros.navigation.data.Constants.TAG
|
|
||||||
import com.kouros.navigation.data.Constants.TILT
|
import com.kouros.navigation.data.Constants.TILT
|
||||||
import com.kouros.navigation.data.RouteEngine
|
import com.kouros.navigation.data.RouteEngine
|
||||||
|
import com.kouros.navigation.data.ViewStyle
|
||||||
import com.kouros.navigation.data.osrm.OsrmRepository
|
import com.kouros.navigation.data.osrm.OsrmRepository
|
||||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||||
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||||
@@ -28,7 +27,9 @@ import kotlin.math.cos
|
|||||||
import kotlin.math.ln
|
import kotlin.math.ln
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.ranges.contains
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
import kotlin.time.toDuration
|
import kotlin.time.toDuration
|
||||||
@@ -48,18 +49,20 @@ object NavigationUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun calculateZoom(speed: Double?): Double {
|
fun calculateZoom(speed: Double?): Double {
|
||||||
|
val zoom = 17.0
|
||||||
if (speed == null) {
|
if (speed == null) {
|
||||||
return 17.0
|
return zoom
|
||||||
}
|
}
|
||||||
val speedKmh = (speed * 3.6).toInt()
|
val speedKmh = (speed * 3.6).toInt()
|
||||||
val zoom = when (speedKmh) {
|
return when (speedKmh) {
|
||||||
in 0..10 -> 17.0
|
in 0..10 -> zoom + 1.0
|
||||||
in 11..30 -> 17.5
|
in 11..30 -> zoom + 0.5
|
||||||
in 31..65 -> 17.0
|
in 31..65 -> zoom
|
||||||
in 66..70 -> 16.5
|
in 66..70 -> zoom - 0.5
|
||||||
else -> 16.0
|
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 {
|
fun previewZoom(centerLocation: Location, previewDistance: Double): Double {
|
||||||
@@ -92,7 +95,8 @@ fun calculateZoomFromBoundingBox(centerLocation: Location, previewDistance: Doub
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun calculateTilt(newZoom: Double, tilt: Double): Double =
|
fun calculateTilt(viewStyle: ViewStyle, newZoom: Double, tilt: Double): Double =
|
||||||
|
if (viewStyle == ViewStyle.VIEW) {
|
||||||
if (newZoom < 13) {
|
if (newZoom < 13) {
|
||||||
0.0
|
0.0
|
||||||
} else {
|
} else {
|
||||||
@@ -102,14 +106,16 @@ fun calculateTilt(newZoom: Double, tilt: Double): Double =
|
|||||||
tilt
|
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)
|
val distance = fromLocation.distanceTo(toLocation)
|
||||||
if (distance < 1.0) {
|
if (distance < 1.0) {
|
||||||
return oldBearing
|
return oldBearing
|
||||||
}
|
}
|
||||||
val bearing = fromLocation.bearingTo(toLocation).toInt().toDouble()
|
return fromLocation.bearingPositive(toLocation).toInt().toDouble()
|
||||||
return bearing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun location(longitude: Double, latitude: Double): Location {
|
fun location(longitude: Double, latitude: Double): Location {
|
||||||
@@ -119,6 +125,10 @@ fun location(longitude: Double, latitude: Double): Location {
|
|||||||
return location
|
return location
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Location.bearingPositive(locationTo: Location): Float {
|
||||||
|
return (this.bearingTo (locationTo) + 360) % 360
|
||||||
|
}
|
||||||
|
|
||||||
fun formatDateTime(time: Long): String {
|
fun formatDateTime(time: Long): String {
|
||||||
val dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
val dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||||
val dateTime = LocalDateTime.ofEpochSecond(time / 1000, 0, ZoneOffset.UTC)
|
val dateTime = LocalDateTime.ofEpochSecond(time / 1000, 0, ZoneOffset.UTC)
|
||||||
@@ -131,23 +141,29 @@ fun Double.round(numFractionDigits: Int): Double {
|
|||||||
return (this * factor).roundToInt() / factor
|
return (this * factor).roundToInt() / factor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isNumeric(toCheck: String): Boolean {
|
||||||
|
val regex = "-?[0-9]+(\\.[0-9]+)?".toRegex()
|
||||||
|
return toCheck.matches(regex)
|
||||||
|
}
|
||||||
|
|
||||||
fun duration(
|
fun duration(
|
||||||
preview: Boolean,
|
viewStyle: ViewStyle,
|
||||||
bearing: Double,
|
bearing: Double,
|
||||||
lastBearing: Double,
|
lastBearing: Double,
|
||||||
lastLocationUpdate: LocalDateTime
|
lastLocationUpdate: LocalDateTime
|
||||||
): Duration {
|
): Duration {
|
||||||
if (preview) {
|
if (viewStyle == ViewStyle.PREVIEW ||
|
||||||
return 3.seconds
|
viewStyle == ViewStyle.AMENITY_VIEW) {
|
||||||
|
return 100.milliseconds
|
||||||
}
|
}
|
||||||
val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) {
|
val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) {
|
||||||
2.seconds
|
2.seconds
|
||||||
} else {
|
} else {
|
||||||
val updateDuration = java.time.Duration.between(LocalDateTime.now(), lastLocationUpdate)
|
val updateDuration = java.time.Duration.between(LocalDateTime.now(), lastLocationUpdate)
|
||||||
if (updateDuration.toMillis().absoluteValue < 1000) {
|
if (updateDuration.toMillis().absoluteValue !in 1000..2000) {
|
||||||
2.seconds
|
2.seconds
|
||||||
} else {
|
} else {
|
||||||
((updateDuration!!.toMillis().absoluteValue * 1.2).toDuration(DurationUnit.MILLISECONDS))
|
((updateDuration!!.toMillis().absoluteValue * 1.8).toDuration(DurationUnit.MILLISECONDS))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cameraDuration
|
return cameraDuration
|
||||||
@@ -182,3 +198,11 @@ fun formattedDistance(distanceMode: Int, distance: Double): Pair<Double, Int> {
|
|||||||
}
|
}
|
||||||
return Pair(currentDistance, displayUnit)
|
return Pair(currentDistance, displayUnit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun countryCodeSpeedLimit(countryCode: String) : Int {
|
||||||
|
return when (countryCode) {
|
||||||
|
"DEU", "FRA", "AUT", "GRE", "NLD", "ITA", "SLO", "SVK", "CZE" -> 130
|
||||||
|
"POL", "BEL", "ESP", "PRT", "BGR", "HUN", "FIN" -> 120
|
||||||
|
else -> 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||