Compare commits
12 Commits
45c8bb5ccc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65ff41995d | ||
|
|
5141041b5c | ||
|
|
e9474695bf | ||
|
|
0d51c6121d | ||
|
|
eac5b56bcb | ||
|
|
7db7cba4fb | ||
|
|
e22865bd73 | ||
|
|
e274011080 | ||
|
|
7efa2685be | ||
|
|
fdf2ee9f48 | ||
|
|
1eab4f1aa3 | ||
|
|
9f356bd728 |
200
CLAUDE.md
Normal file
200
CLAUDE.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is an Android navigation app built with Jetpack Compose that supports multiple routing providers (OSRM, Valhalla, TomTom) and includes Android Auto/Automotive OS integration. The app uses MapLibre for rendering, ObjectBox for local persistence, and Koin for dependency injection.
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the app (from repository root)
|
||||||
|
./gradlew :app:assembleDebug
|
||||||
|
|
||||||
|
# Build specific flavor
|
||||||
|
./gradlew :app:assembleDemoDebug
|
||||||
|
./gradlew :app:assembleFullDebug
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
./gradlew test
|
||||||
|
|
||||||
|
# Run tests for specific module
|
||||||
|
./gradlew :common:data:test
|
||||||
|
./gradlew :common:car:test
|
||||||
|
|
||||||
|
# Install on device
|
||||||
|
./gradlew :app:installDebug
|
||||||
|
|
||||||
|
# Clean build
|
||||||
|
./gradlew clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Structure
|
||||||
|
|
||||||
|
The project uses a multi-module architecture:
|
||||||
|
|
||||||
|
- **app/** - Main Android app with Jetpack Compose UI for phone
|
||||||
|
- **common/data/** - Core data layer with routing logic, repositories, and data models (shared by all modules)
|
||||||
|
- **common/car/** - Android Auto/Automotive OS UI implementation
|
||||||
|
- **automotive/** - Placeholder for future native Automotive OS app
|
||||||
|
|
||||||
|
Dependencies flow: `app` → `common:car` → `common:data`
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Routing Providers (Pluggable System)
|
||||||
|
|
||||||
|
The app supports three routing engines that implement the `NavigationRepository` abstract class:
|
||||||
|
|
||||||
|
1. **OsrmRepository** - OSRM routing engine
|
||||||
|
2. **ValhallaRepository** - Valhalla routing engine
|
||||||
|
3. **TomTomRepository** - TomTom routing engine
|
||||||
|
|
||||||
|
Each provider has a corresponding mapper class (`OsrmRoute`, `ValhallaRoute`, `TomTomRoute`) that converts provider-specific JSON responses to the universal `Route` data model.
|
||||||
|
|
||||||
|
**Adding a new routing provider:**
|
||||||
|
1. Create `NewProviderRepository` extending `NavigationRepository` in `common/data/src/main/java/com/kouros/navigation/data/`
|
||||||
|
2. Implement `getRoute()` method
|
||||||
|
3. Create `NewProviderRoute.kt` with `mapToRoute()` function
|
||||||
|
4. Add provider detection logic in `Route.Builder.route()`
|
||||||
|
5. Update `NavigationUtils.getViewModel()` to return appropriate ViewModel
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User Action (search/select destination)
|
||||||
|
↓
|
||||||
|
ViewModel.loadRoute() [LiveData]
|
||||||
|
↓
|
||||||
|
NavigationRepository.getRoute() [Selected provider]
|
||||||
|
↓
|
||||||
|
*Route.mapToRoute() [Convert to universal Route model]
|
||||||
|
↓
|
||||||
|
RouteModel.startNavigation()
|
||||||
|
↓
|
||||||
|
RouteModel.updateLocation() [On each location update]
|
||||||
|
↓
|
||||||
|
UI observes LiveData and displays current step
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Classes
|
||||||
|
|
||||||
|
**Navigation Logic:**
|
||||||
|
- `RouteModel.kt` - Core navigation engine (tracks position, calculates distances, manages steps)
|
||||||
|
- `RouteCarModel.kt` - Extends RouteModel with Android Auto-specific formatting
|
||||||
|
- `ViewModel.kt` - androidx.ViewModel with LiveData for route, traffic, places, etc.
|
||||||
|
|
||||||
|
**Data Models:**
|
||||||
|
- `Route.kt` - Universal route structure used by all providers
|
||||||
|
- `Place.kt` - ObjectBox entity for favorites/recent locations
|
||||||
|
- `StepData.kt` - Display data for current navigation instruction
|
||||||
|
|
||||||
|
**Repositories:**
|
||||||
|
- `NavigationRepository.kt` - Abstract base class for all routing providers
|
||||||
|
- Also handles Nominatim geocoding search and TomTom traffic incidents
|
||||||
|
|
||||||
|
**Android Auto:**
|
||||||
|
- `NavigationCarAppService.kt` - Entry point for Android Auto/Automotive OS
|
||||||
|
- `NavigationSession.kt` - Session management
|
||||||
|
- `NavigationScreen.kt` - Car screen templates with NavigationType state machine
|
||||||
|
- `SurfaceRenderer.kt` - Handles virtual display and map rendering
|
||||||
|
|
||||||
|
### External APIs
|
||||||
|
|
||||||
|
| Service | Purpose | Base URL |
|
||||||
|
|---------|---------|----------|
|
||||||
|
| OSRM | Routing | `https://kouros-online.de/osrm/route/v1/driving/` |
|
||||||
|
| Valhalla | Routing | `https://kouros-online.de/valhalla/route` |
|
||||||
|
| TomTom | Traffic incidents | `https://api.tomtom.com/traffic/services/5/incidentDetails` |
|
||||||
|
| Nominatim | Geocoding search | `https://kouros-online.de/nominatim/` |
|
||||||
|
| Overpass | POI & speed limits | OpenStreetMap Overpass API |
|
||||||
|
|
||||||
|
## Important Constants
|
||||||
|
|
||||||
|
Located in `Constants.kt` (`common/data`):
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
NEXT_STEP_THRESHOLD = 120.0 m // Distance to show next maneuver
|
||||||
|
DESTINATION_ARRIVAL_DISTANCE = 40.0 m // Distance to trigger arrival
|
||||||
|
MAXIMAL_SNAP_CORRECTION = 50.0 m // Max distance to snap to route
|
||||||
|
MAXIMAL_ROUTE_DEVIATION = 80.0 m // Max deviation before reroute
|
||||||
|
```
|
||||||
|
|
||||||
|
SharedPreferences keys:
|
||||||
|
- `ROUTING_ENGINE` - Selected provider (0=Valhalla, 1=OSRM, 2=TomTom)
|
||||||
|
- `DARK_MODE_SETTINGS` - Theme preference
|
||||||
|
- `AVOID_MOTORWAY`, `AVOID_TOLLWAY` - Route preferences
|
||||||
|
|
||||||
|
## Navigation Flow
|
||||||
|
|
||||||
|
1. **Route Loading**: User searches via Nominatim → selects place → ViewModel.loadRoute() calls selected repository
|
||||||
|
2. **Route Parsing**: Provider JSON → mapper converts to universal Route → RouteModel.startNavigation()
|
||||||
|
3. **Location Tracking**: FusedLocationProviderClient provides updates → RouteModel.updateLocation()
|
||||||
|
4. **Step Calculation**: findStep() snaps location to nearest waypoint → updates current step
|
||||||
|
5. **UI Updates**: currentStep() and nextStep() provide display data (instruction, distance, icon, lanes)
|
||||||
|
6. **Arrival**: When distance < DESTINATION_ARRIVAL_DISTANCE, navigation ends
|
||||||
|
|
||||||
|
## Testing Navigation
|
||||||
|
|
||||||
|
The app includes mock location support for testing:
|
||||||
|
|
||||||
|
- Set `useMock = true` in MainActivity
|
||||||
|
- Enable "Mock location app" in Android Developer Options
|
||||||
|
- Choose test mode:
|
||||||
|
- `type = 1` - Simulate movement along entire route
|
||||||
|
- `type = 2` - Test specific step range
|
||||||
|
- `type = 3` - Replay GPX track file
|
||||||
|
|
||||||
|
## ObjectBox Database
|
||||||
|
|
||||||
|
ObjectBox is configured in `common/data/build.gradle.kts` with the kapt plugin. The database stores:
|
||||||
|
|
||||||
|
- Recent destinations (category: "Recent")
|
||||||
|
- Favorite places (category: "Favorites")
|
||||||
|
- Imported contacts (category: "Contacts")
|
||||||
|
|
||||||
|
Queries use ObjectBox query builder pattern with generated `Place_` property accessors.
|
||||||
|
|
||||||
|
## Compose UI Structure
|
||||||
|
|
||||||
|
**Phone App:**
|
||||||
|
- `MainActivity.kt` - Main entry with permission handling and Navigation Compose
|
||||||
|
- `NavigationScreen.kt` - Turn-by-turn navigation display
|
||||||
|
- `SearchSheet.kt` / `NavigationSheet.kt` - Bottom sheet content
|
||||||
|
- `MapView.kt` - MapLibre rendering with camera state management
|
||||||
|
|
||||||
|
**Android Auto:**
|
||||||
|
- Uses CarAppService Screen templates (NavigationTemplate, MessageTemplate, MapWithContentTemplate)
|
||||||
|
- NavigationType enum controls which template to display (VIEW, NAVIGATION, REROUTE, RECENT, ARRIVAL)
|
||||||
|
|
||||||
|
## Build Flavors
|
||||||
|
|
||||||
|
Two product flavors with dimension "version":
|
||||||
|
- **demo** - applicationId: `com.kouros.navigation.demo`
|
||||||
|
- **full** - applicationId: `com.kouros.navigation.full`
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
**Dependency Injection (Koin):**
|
||||||
|
```kotlin
|
||||||
|
single { OsrmRepository() }
|
||||||
|
viewModel { ViewModel(get()) }
|
||||||
|
```
|
||||||
|
|
||||||
|
**LiveData Observation:**
|
||||||
|
```kotlin
|
||||||
|
viewModel.route.observe(this) { routeJson ->
|
||||||
|
routeModel.startNavigation(routeJson, context)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step Finding Algorithm:**
|
||||||
|
RouteModel iterates through all step waypoints, calculates distance to current location, and snaps to the nearest waypoint to determine current step index.
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
- Valhalla route mapping is incomplete (search for TODO comments in ValhallaRoute.kt)
|
||||||
|
- Rerouting logic exists but needs more testing
|
||||||
|
- Speed limit queries via Overpass API could be optimized for performance
|
||||||
|
- TomTom implementation uses local JSON file (R.raw.tomom_routing) instead of live API
|
||||||
@@ -14,8 +14,8 @@ android {
|
|||||||
applicationId = "com.kouros.navigation"
|
applicationId = "com.kouros.navigation"
|
||||||
minSdk = 33
|
minSdk = 33
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 15
|
versionCode = 38
|
||||||
versionName = "0.1.3.15"
|
versionName = "0.2.0.38"
|
||||||
base.archivesName = "navi-$versionName"
|
base.archivesName = "navi-$versionName"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,13 @@ dependencies {
|
|||||||
implementation(libs.androidx.compose.ui.graphics)
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
implementation(libs.androidx.window)
|
implementation(libs.androidx.window)
|
||||||
implementation(libs.androidx.compose.foundation.layout)
|
implementation(libs.androidx.compose.foundation.layout)
|
||||||
|
implementation("com.github.ticofab:android-gpx-parser:2.3.1")
|
||||||
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
implementation("com.github.alorma.compose-settings:ui-tiles:2.25.0")
|
||||||
|
implementation("com.github.alorma.compose-settings:ui-tiles-extended:2.25.0")
|
||||||
|
implementation("com.github.alorma.compose-settings:ui-tiles-expressive:2.25.0")
|
||||||
|
implementation(libs.androidx.foundation.layout)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
<!-- <uses-permission android:name="android.permission.READ_CONTACTS"/>-->
|
<!-- <uses-permission android:name="android.permission.READ_CONTACTS"/>-->
|
||||||
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"
|
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"
|
||||||
tools:ignore="MockLocation" />
|
tools:ignore="MockLocation" />
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
android:theme="@style/Theme.Navigation">
|
android:theme="@style/Theme.Navigation">
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|||||||
@@ -2,25 +2,14 @@ package com.kouros.navigation
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.kouros.navigation.data.Constants.DARK_MODE_SETTINGS
|
|
||||||
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
|
|
||||||
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
|
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
|
||||||
import com.kouros.navigation.data.ObjectBox
|
import com.kouros.navigation.data.ObjectBox
|
||||||
import com.kouros.navigation.data.RouteEngine
|
|
||||||
import com.kouros.navigation.data.osrm.OsrmRepository
|
|
||||||
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
|
||||||
import com.kouros.navigation.di.appModule
|
import com.kouros.navigation.di.appModule
|
||||||
import com.kouros.navigation.model.ViewModel
|
import com.kouros.navigation.model.ViewModel
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
|
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getRouteEngine
|
|
||||||
import com.kouros.navigation.utils.NavigationUtils.setIntKeyValue
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.android.ext.koin.androidLogger
|
import org.koin.android.ext.koin.androidLogger
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
import org.koin.core.logger.Level
|
import org.koin.core.logger.Level
|
||||||
import org.maplibre.compose.expressions.dsl.switch
|
|
||||||
|
|
||||||
class MainApplication : Application() {
|
class MainApplication : Application() {
|
||||||
|
|
||||||
@@ -28,8 +17,7 @@ class MainApplication : Application() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
ObjectBox.init(this);
|
ObjectBox.init(this);
|
||||||
appContext = applicationContext
|
appContext = applicationContext
|
||||||
setIntKeyValue(appContext!!, RouteEngine.VALHALLA.ordinal, ROUTE_ENGINE)
|
navigationViewModel = getViewModel(appContext!!)
|
||||||
navigationViewModel = getRouteEngine(appContext!!)
|
|
||||||
startKoin {
|
startKoin {
|
||||||
androidLogger(Level.DEBUG)
|
androidLogger(Level.DEBUG)
|
||||||
androidContext(this@MainApplication)
|
androidContext(this@MainApplication)
|
||||||
@@ -42,8 +30,6 @@ class MainApplication : Application() {
|
|||||||
var appContext: Context? = null
|
var appContext: Context? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var useContacts = false
|
|
||||||
|
|
||||||
lateinit var navigationViewModel : ViewModel
|
lateinit var navigationViewModel : ViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,14 +2,18 @@ package com.kouros.navigation.di
|
|||||||
|
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
import com.kouros.navigation.data.NavigationRepository
|
||||||
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.valhalla.ValhallaRepository
|
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||||
|
import com.kouros.navigation.model.BaseStyleModel
|
||||||
import com.kouros.navigation.model.ViewModel
|
import com.kouros.navigation.model.ViewModel
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
import org.koin.core.module.dsl.viewModelOf
|
import org.koin.core.module.dsl.viewModelOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
val appModule = module {
|
val appModule = module {
|
||||||
viewModelOf(::ViewModel)
|
viewModelOf(::ViewModel)
|
||||||
singleOf(::ValhallaRepository)
|
singleOf(::ValhallaRepository)
|
||||||
singleOf(::OsrmRepository)
|
singleOf(::OsrmRepository)
|
||||||
}
|
singleOf(::TomTomRepository)
|
||||||
|
}
|
||||||
@@ -48,8 +48,8 @@ class MockLocation (private var locationManager: LocationManager) {
|
|||||||
this.latitude = latitude
|
this.latitude = latitude
|
||||||
this.longitude = longitude
|
this.longitude = longitude
|
||||||
this.altitude = 0.0
|
this.altitude = 0.0
|
||||||
this.accuracy = 1.0f
|
this.accuracy = 0f
|
||||||
this.speed = 10f
|
this.speed = 0f
|
||||||
this.time = System.currentTimeMillis()
|
this.time = System.currentTimeMillis()
|
||||||
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
|
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
|
||||||
|
|
||||||
@@ -71,8 +71,8 @@ class MockLocation (private var locationManager: LocationManager) {
|
|||||||
this.latitude = latitude
|
this.latitude = latitude
|
||||||
this.longitude = longitude
|
this.longitude = longitude
|
||||||
this.altitude = 0.0
|
this.altitude = 0.0
|
||||||
this.accuracy = 1.0f
|
this.accuracy = 0f
|
||||||
this.speed = 10f
|
this.speed = 0f
|
||||||
this.time = System.currentTimeMillis()
|
this.time = System.currentTimeMillis()
|
||||||
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
|
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package com.kouros.navigation.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.alorma.compose.settings.ui.SettingsCheckbox
|
||||||
|
import com.alorma.compose.settings.ui.SettingsGroup
|
||||||
|
import com.alorma.compose.settings.ui.SettingsRadioButton
|
||||||
|
import com.alorma.compose.settings.ui.base.internal.LocalSettingsTileColors
|
||||||
|
import com.alorma.compose.settings.ui.base.internal.SettingsTileDefaults
|
||||||
|
import com.kouros.data.R
|
||||||
|
import com.kouros.navigation.data.Constants.DARK_MODE_SETTINGS
|
||||||
|
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
|
||||||
|
import com.kouros.navigation.utils.NavigationUtils
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun DisplayScreenSettings(context: Context, navigateBack: () -> Unit) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.display_settings),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = navigateBack) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.arrow_back_24px),
|
||||||
|
contentDescription = stringResource(id = R.string.accept_action_title),
|
||||||
|
modifier = Modifier.size(48.dp, 48.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.padding(top = padding.calculateTopPadding()),
|
||||||
|
) {
|
||||||
|
DisplaySettings(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DisplaySettings(context: Context) {
|
||||||
|
Section(title = "Anzeige") {
|
||||||
|
val state = remember {
|
||||||
|
mutableStateOf(
|
||||||
|
NavigationUtils.getBooleanKeyValue(
|
||||||
|
context,
|
||||||
|
SHOW_THREED_BUILDING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SettingsCheckbox(
|
||||||
|
state = state.value,
|
||||||
|
title = { Text(text = stringResource(R.string.threed_building)) },
|
||||||
|
onCheckedChange = {
|
||||||
|
state.value = it
|
||||||
|
NavigationUtils.setBooleanKeyValue(context, it, SHOW_THREED_BUILDING)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Section(title = "Dunkles Design") {
|
||||||
|
val state = remember {
|
||||||
|
mutableIntStateOf(
|
||||||
|
NavigationUtils.getIntKeyValue(
|
||||||
|
context,
|
||||||
|
DARK_MODE_SETTINGS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DarkModeData(context).darkDesign.forEach { sampleItem ->
|
||||||
|
SettingsRadioButton(
|
||||||
|
state = state.intValue == sampleItem.key,
|
||||||
|
title = { Text(text = sampleItem.title) },
|
||||||
|
onClick = {
|
||||||
|
state.intValue = sampleItem.key
|
||||||
|
NavigationUtils.setIntKeyValue(context, state.intValue, DARK_MODE_SETTINGS)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun Section(
|
||||||
|
title: String,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(4.dp),
|
||||||
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
SettingsGroup(
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
verticalArrangement = verticalArrangement,
|
||||||
|
enabled = enabled,
|
||||||
|
title = { Text(text = title) },
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = (LocalSettingsTileColors.current
|
||||||
|
?: SettingsTileDefaults.colors()).containerColor
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
internal class DarkModeData(context: Context) {
|
||||||
|
val darkDesign =
|
||||||
|
listOf(
|
||||||
|
Item(
|
||||||
|
key = 0,
|
||||||
|
title = context.getString(R.string.off_action_title),
|
||||||
|
),
|
||||||
|
Item(
|
||||||
|
key = 1,
|
||||||
|
title = context.getString(R.string.on_action_title),
|
||||||
|
),
|
||||||
|
Item(
|
||||||
|
key = 2,
|
||||||
|
title = context.getString(R.string.use_telephon_settings),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class Item(
|
||||||
|
val key: Int,
|
||||||
|
val title: String,
|
||||||
|
)
|
||||||
@@ -4,6 +4,7 @@ import NavigationSheet
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.AppOpsManager
|
import android.app.AppOpsManager
|
||||||
|
import android.content.Context
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
@@ -14,9 +15,13 @@ import androidx.activity.enableEdgeToEdge
|
|||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.BottomSheetScaffold
|
import androidx.compose.material3.BottomSheetScaffold
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -32,34 +37,47 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
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 androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.google.android.gms.location.FusedLocationProviderClient
|
import com.google.android.gms.location.FusedLocationProviderClient
|
||||||
import com.google.android.gms.location.LocationServices
|
import com.google.android.gms.location.LocationServices
|
||||||
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
|
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
|
||||||
|
import com.kouros.navigation.data.Constants
|
||||||
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
||||||
import com.kouros.navigation.data.Constants.homeLocation
|
import com.kouros.navigation.data.Constants.homeVogelhart
|
||||||
import com.kouros.navigation.data.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
|
import com.kouros.navigation.model.BaseStyleModel
|
||||||
import com.kouros.navigation.model.MockLocation
|
import com.kouros.navigation.model.MockLocation
|
||||||
import com.kouros.navigation.model.RouteModel
|
import com.kouros.navigation.model.RouteModel
|
||||||
import com.kouros.navigation.ui.theme.NavigationTheme
|
import com.kouros.navigation.ui.theme.NavigationTheme
|
||||||
|
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
||||||
import com.kouros.navigation.utils.bearing
|
import com.kouros.navigation.utils.bearing
|
||||||
import com.kouros.navigation.utils.calculateZoom
|
import com.kouros.navigation.utils.calculateZoom
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
|
import io.ticofab.androidgpxparser.parser.GPXParser
|
||||||
|
import io.ticofab.androidgpxparser.parser.domain.Gpx
|
||||||
|
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.joda.time.DateTime
|
||||||
import org.maplibre.compose.camera.CameraPosition
|
import org.maplibre.compose.camera.CameraPosition
|
||||||
import org.maplibre.compose.location.DesiredAccuracy
|
import org.maplibre.compose.location.DesiredAccuracy
|
||||||
import org.maplibre.compose.location.Location
|
import org.maplibre.compose.location.Location
|
||||||
import org.maplibre.compose.location.rememberDefaultLocationProvider
|
import org.maplibre.compose.location.rememberDefaultLocationProvider
|
||||||
import org.maplibre.compose.location.rememberUserLocationState
|
import org.maplibre.compose.location.rememberUserLocationState
|
||||||
|
import org.maplibre.compose.style.BaseStyle
|
||||||
import org.maplibre.spatialk.geojson.Position
|
import org.maplibre.spatialk.geojson.Position
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@@ -69,7 +87,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
val routeModel = RouteModel()
|
val routeModel = RouteModel()
|
||||||
var tilt = 50.0
|
var tilt = 50.0
|
||||||
val useMock = true
|
val useMock = false
|
||||||
|
val type = 3 // simulate 2 test 3 gpx 4 testSingle
|
||||||
|
|
||||||
|
var currentIndex = 0
|
||||||
val stepData: MutableLiveData<StepData> by lazy {
|
val stepData: MutableLiveData<StepData> by lazy {
|
||||||
MutableLiveData<StepData>()
|
MutableLiveData<StepData>()
|
||||||
}
|
}
|
||||||
@@ -80,15 +101,23 @@ class MainActivity : ComponentActivity() {
|
|||||||
val observer = Observer<String> { newRoute ->
|
val observer = Observer<String> { newRoute ->
|
||||||
if (newRoute.isNotEmpty()) {
|
if (newRoute.isNotEmpty()) {
|
||||||
routeModel.startNavigation(newRoute, applicationContext)
|
routeModel.startNavigation(newRoute, applicationContext)
|
||||||
routeData.value = routeModel.route.routeGeoJson
|
routeData.value = routeModel.curRoute.routeGeoJson
|
||||||
simulate()
|
if (useMock) {
|
||||||
//test()
|
when (type) {
|
||||||
|
1 -> simulate()
|
||||||
|
2 -> test()
|
||||||
|
3 -> gpx(
|
||||||
|
context = applicationContext
|
||||||
|
)
|
||||||
|
|
||||||
|
4 -> testSingle()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val cameraPosition = MutableLiveData(
|
val cameraPosition = MutableLiveData(
|
||||||
CameraPosition(
|
CameraPosition(
|
||||||
zoom = 15.0,
|
zoom = 15.0, target = Position(latitude = 48.1857475, longitude = 11.5793627)
|
||||||
target = Position(latitude = 48.1857475, longitude = 11.5793627)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,30 +128,28 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
private var loadRecentPlaces = false
|
private var loadRecentPlaces = false
|
||||||
|
|
||||||
private var overpass = false
|
lateinit var baseStyle: BaseStyle.Json
|
||||||
|
|
||||||
init {
|
|
||||||
navigationViewModel.route.observe(this, observer)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
val darkModeSettings = getIntKeyValue(applicationContext, Constants.DARK_MODE_SETTINGS)
|
||||||
|
baseStyle = BaseStyleModel().readStyle(applicationContext, darkModeSettings, false)
|
||||||
if (useMock) {
|
if (useMock) {
|
||||||
checkMockLocationEnabled()
|
checkMockLocationEnabled()
|
||||||
}
|
}
|
||||||
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
|
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
|
||||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
|
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
|
||||||
fusedLocationClient.lastLocation
|
fusedLocationClient.lastLocation.addOnSuccessListener { _: android.location.Location? ->
|
||||||
.addOnSuccessListener { location : android.location.Location? ->
|
if (useMock) {
|
||||||
if (useMock) {
|
mock = MockLocation(locationManager)
|
||||||
mock = MockLocation(locationManager)
|
mock.setMockLocation(
|
||||||
mock.setMockLocation(
|
homeVogelhart.latitude, homeVogelhart.longitude
|
||||||
location?.latitude ?: homeLocation.latitude,
|
)
|
||||||
location?.longitude ?: homeLocation.longitude
|
navigationViewModel.route.observe(this, observer)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
CheckPermissionScreen()
|
CheckPermissionScreen()
|
||||||
@@ -140,23 +167,35 @@ class MainActivity : ComponentActivity() {
|
|||||||
permissions = permissions,
|
permissions = permissions,
|
||||||
requiredPermissions = listOf(permissions.first()),
|
requiredPermissions = listOf(permissions.first()),
|
||||||
onGranted = {
|
onGranted = {
|
||||||
Content()
|
App()
|
||||||
|
// auto navigate
|
||||||
|
if (useMock) {
|
||||||
|
// navigationViewModel.loadRoute(
|
||||||
|
// applicationContext,
|
||||||
|
// homeVogelhart,
|
||||||
|
// homeHohenwaldeck,
|
||||||
|
// 0F
|
||||||
|
// )
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("AutoboxingStateCreation")
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Content() {
|
fun StartScreen(
|
||||||
|
navController: NavHostController
|
||||||
|
|
||||||
|
) {
|
||||||
|
|
||||||
val scaffoldState = rememberBottomSheetScaffoldState()
|
val scaffoldState = rememberBottomSheetScaffoldState()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val sheetPeekHeightState = remember { mutableStateOf(256.dp) }
|
val sheetPeekHeightState = remember { mutableStateOf(256.dp) }
|
||||||
|
|
||||||
val locationProvider = rememberDefaultLocationProvider(
|
val locationProvider = rememberDefaultLocationProvider(
|
||||||
updateInterval = 0.5.seconds,
|
updateInterval = 0.5.seconds, desiredAccuracy = DesiredAccuracy.Highest
|
||||||
desiredAccuracy = DesiredAccuracy.Highest
|
|
||||||
)
|
)
|
||||||
val userLocationState = rememberUserLocationState(locationProvider)
|
val userLocationState = rememberUserLocationState(locationProvider)
|
||||||
val locationState = locationProvider.location.collectAsState()
|
val locationState = locationProvider.location.collectAsState()
|
||||||
@@ -194,77 +233,108 @@ class MainActivity : ComponentActivity() {
|
|||||||
applicationContext,
|
applicationContext,
|
||||||
userLocationState,
|
userLocationState,
|
||||||
step,
|
step,
|
||||||
|
nextStep,
|
||||||
cameraPosition,
|
cameraPosition,
|
||||||
routeData,
|
routeData,
|
||||||
tilt
|
tilt,
|
||||||
|
baseStyle
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (!routeModel.isNavigating()) {
|
||||||
|
Settings(navController, modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun App() {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
NavHost(navController = navController, startDestination = "startScreen") {
|
||||||
|
composable("startScreen") { StartScreen(navController) }
|
||||||
|
composable("settings") { SettingsScreen(navController) { navController.popBackStack() } }
|
||||||
|
composable("display_settings") { DisplayScreenSettings(applicationContext) { navController.popBackStack() } }
|
||||||
|
composable("nav_settings") { NavigationScreenSettings(applicationContext) { navController.popBackStack() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Settings(navController: NavController, modifier: Modifier = Modifier) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
FloatingActionButton(
|
||||||
|
modifier = Modifier.padding(start = 10.dp, top = 40.dp),
|
||||||
|
onClick = {
|
||||||
|
navController.navigate("settings")
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.menu_24px),
|
||||||
|
contentDescription = stringResource(id = R.string.accept_action_title),
|
||||||
|
modifier = Modifier.size(24.dp, 24.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SheetContent(
|
fun SheetContent(
|
||||||
locationState: Double,
|
locationState: Double, step: StepData?, nextStep: StepData?, closeSheet: () -> Unit
|
||||||
step: StepData?,
|
|
||||||
nextStep: StepData?,
|
|
||||||
closeSheet: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
if (!routeModel.isNavigating()) {
|
if (!routeModel.isNavigating()) {
|
||||||
SearchSheet(applicationContext, navigationViewModel, lastLocation) { closeSheet() }
|
SearchSheet(applicationContext, navigationViewModel, lastLocation) { closeSheet() }
|
||||||
} else {
|
} else {
|
||||||
NavigationSheet(
|
NavigationSheet(
|
||||||
routeModel, step!!, nextStep!!,
|
applicationContext,
|
||||||
|
routeModel,
|
||||||
|
step!!,
|
||||||
|
nextStep!!,
|
||||||
{ stopNavigation { closeSheet() } },
|
{ stopNavigation { closeSheet() } },
|
||||||
{ simulateNavigation() }
|
{ simulateNavigation() })
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// For recomposition!
|
// For recomposition!
|
||||||
Text("$locationState", fontSize = 12.sp)
|
Text("$locationState", fontSize = 12.sp)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLocation(location: Location?) {
|
fun updateLocation(location: Location?) {
|
||||||
if (location != null
|
if (location != null && lastLocation.latitude != location.position.latitude && lastLocation.longitude != location.position.longitude) {
|
||||||
&& lastLocation.latitude != location.position.latitude
|
|
||||||
&& lastLocation.longitude != location.position.longitude
|
|
||||||
) {
|
|
||||||
val currentLocation = location(location.position.longitude, location.position.latitude)
|
val currentLocation = location(location.position.longitude, location.position.latitude)
|
||||||
|
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
|
||||||
with(routeModel) {
|
with(routeModel) {
|
||||||
if (isNavigating()) {
|
if (isNavigating()) {
|
||||||
updateLocation(currentLocation, navigationViewModel)
|
updateLocation(currentLocation, navigationViewModel)
|
||||||
stepData.value = currentStep()
|
stepData.value = currentStep()
|
||||||
if (route.currentStep + 1 <= legs.steps.size) {
|
nextStepData.value = nextStep()
|
||||||
nextStepData.value = nextStep()
|
if (navState.maneuverType in 39..42 && routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE) {
|
||||||
}
|
// stopNavigation()
|
||||||
if (routeState.maneuverType == 39
|
navState.copy(arrived = true)
|
||||||
&& leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
|
||||||
) {
|
|
||||||
// stopNavigation()
|
|
||||||
routeState = routeState.copy(arrived = true)
|
|
||||||
routeData.value = ""
|
routeData.value = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
|
|
||||||
val zoom = calculateZoom(location.speed)
|
val zoom = calculateZoom(location.speed)
|
||||||
cameraPosition.postValue(
|
cameraPosition.postValue(
|
||||||
cameraPosition.value!!.copy(
|
cameraPosition.value!!.copy(
|
||||||
zoom = zoom,
|
zoom = zoom, target = location.position, bearing = bearing
|
||||||
target = location.position,
|
|
||||||
bearing = bearing
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
lastLocation = currentLocation
|
lastLocation = currentLocation
|
||||||
if (!loadRecentPlaces) {
|
if (!loadRecentPlaces) {
|
||||||
navigationViewModel.loadRecentPlaces(applicationContext, lastLocation)
|
navigationViewModel.loadRecentPlaces(applicationContext, lastLocation, 0F)
|
||||||
loadRecentPlaces = true
|
loadRecentPlaces = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopNavigation(closeSheet: () -> Unit) {
|
fun stopNavigation(closeSheet: () -> Unit) {
|
||||||
|
val latitude = routeModel.curRoute.waypoints[0][1]
|
||||||
|
val longitude = routeModel.curRoute.waypoints[0][0]
|
||||||
closeSheet()
|
closeSheet()
|
||||||
routeModel.stopNavigation()
|
routeModel.stopNavigation()
|
||||||
|
if (useMock) {
|
||||||
|
mock.setMockLocation(latitude, longitude)
|
||||||
|
}
|
||||||
routeData.value = ""
|
routeData.value = ""
|
||||||
stepData.value = StepData("", 0.0, 0, 0, 0, 0.0)
|
stepData.value = StepData("", 0.0, 0, 0, 0, 0.0)
|
||||||
}
|
}
|
||||||
@@ -276,14 +346,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
private fun checkMockLocationEnabled() {
|
private fun checkMockLocationEnabled() {
|
||||||
try {
|
try {
|
||||||
// Check if mock location is enabled for this app
|
// Check if mock location is enabled for this app
|
||||||
val appOpsManager =
|
val appOpsManager = getSystemService(APP_OPS_SERVICE) as AppOpsManager
|
||||||
getSystemService(APP_OPS_SERVICE) as AppOpsManager
|
val mode = appOpsManager.checkOp(
|
||||||
val mode =
|
AppOpsManager.OPSTR_MOCK_LOCATION, Process.myUid(), packageName
|
||||||
appOpsManager.checkOp(
|
)
|
||||||
AppOpsManager.OPSTR_MOCK_LOCATION,
|
|
||||||
Process.myUid(),
|
|
||||||
packageName
|
|
||||||
)
|
|
||||||
|
|
||||||
if (mode != AppOpsManager.MODE_ALLOWED) {
|
if (mode != AppOpsManager.MODE_ALLOWED) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
@@ -297,13 +363,14 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun simulate() {
|
fun simulate() {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
for ((index, step) in routeModel.legs.steps.withIndex()) {
|
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
|
||||||
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
if (routeModel.isNavigating()) {
|
||||||
if (routeModel.isNavigating()) {
|
val deviation = 0.0
|
||||||
|
if (index in 0..routeModel.curRoute.waypoints.size) {
|
||||||
mock.setMockLocation(waypoint[1], waypoint[0])
|
mock.setMockLocation(waypoint[1], waypoint[0])
|
||||||
delay(800L) //
|
Thread.sleep(500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,15 +378,67 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun test() {
|
fun test() {
|
||||||
for ((index, step) in routeModel.legs.steps.withIndex()) {
|
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
|
||||||
println("${step.maneuver.waypoints.size}")
|
//if (index in 3..3) {
|
||||||
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
||||||
routeModel.updateLocation(location(waypoint[0], waypoint[1]), navigationViewModel)
|
routeModel.updateLocation(
|
||||||
routeModel.currentStep()
|
location(waypoint[0], waypoint[1]), navigationViewModel
|
||||||
if (index + 1 <= routeModel.legs.steps.size) {
|
)
|
||||||
nextStepData.value = routeModel.nextStep()
|
val step = routeModel.currentStep()
|
||||||
|
val nextStep = routeModel.nextStep()
|
||||||
|
println("Step: ${step.instruction} ${step.leftStepDistance} ${nextStep.currentManeuverType}")
|
||||||
|
}
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testSingle() {
|
||||||
|
testSingleUpdate(48.185976, 11.578463) // Silcherstr. 23-13
|
||||||
|
testSingleUpdate(48.186712, 11.578574) // Silcherstr. 27-33
|
||||||
|
testSingleUpdate(48.186899, 11.580480) // Schmalkadenerstr. 24-28
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testSingleUpdate(latitude: Double, longitude: Double) {
|
||||||
|
if (1 == 1) {
|
||||||
|
mock.setMockLocation(latitude, longitude)
|
||||||
|
} else {
|
||||||
|
routeModel.updateLocation(
|
||||||
|
location(longitude, latitude), navigationViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val step = routeModel.currentStep()
|
||||||
|
val nextStep = routeModel.nextStep()
|
||||||
|
println("Step: ${step.instruction} ${step.leftStepDistance} ${nextStep.currentManeuverType}")
|
||||||
|
Thread.sleep(1_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun gpx(context: Context) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val parser = GPXParser()
|
||||||
|
val input = context.resources.openRawResource(R.raw.vh)
|
||||||
|
val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread
|
||||||
|
parsedGpx?.let {
|
||||||
|
val tracks = parsedGpx.tracks
|
||||||
|
tracks.forEach { tr ->
|
||||||
|
val segments: MutableList<TrackSegment?>? = tr.trackSegments
|
||||||
|
segments!!.forEach { seg ->
|
||||||
|
var lastTime = DateTime.now()
|
||||||
|
seg!!.trackPoints.forEach { p ->
|
||||||
|
val ext = p.extensions
|
||||||
|
val speed: Double?
|
||||||
|
if (ext != null) {
|
||||||
|
speed = ext.speed
|
||||||
|
mock.curSpeed = speed.toFloat()
|
||||||
|
}
|
||||||
|
val duration = p.time.millis - lastTime.millis
|
||||||
|
mock.setMockLocation(p.latitude, p.longitude)
|
||||||
|
if (duration > 0) {
|
||||||
|
delay(duration / 5)
|
||||||
|
}
|
||||||
|
lastTime = p.time
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
println(routeModel.routeState.maneuverType)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,16 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.window.layout.WindowMetricsCalculator
|
import androidx.window.layout.WindowMetricsCalculator
|
||||||
import com.kouros.navigation.car.ViewStyle
|
import com.kouros.navigation.car.ViewStyle
|
||||||
import com.kouros.navigation.car.map.DarkMode
|
|
||||||
import com.kouros.navigation.car.map.MapLibre
|
import com.kouros.navigation.car.map.MapLibre
|
||||||
import com.kouros.navigation.car.map.NavigationImage
|
import com.kouros.navigation.car.map.NavigationImage
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.car.map.rememberBaseStyle
|
||||||
import com.kouros.navigation.data.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
|
import com.kouros.navigation.data.tomtom.TrafficData
|
||||||
import org.maplibre.compose.camera.CameraPosition
|
import org.maplibre.compose.camera.CameraPosition
|
||||||
import org.maplibre.compose.camera.rememberCameraState
|
import org.maplibre.compose.camera.rememberCameraState
|
||||||
import org.maplibre.compose.location.LocationTrackingEffect
|
import org.maplibre.compose.location.LocationTrackingEffect
|
||||||
@@ -32,12 +30,15 @@ fun MapView(
|
|||||||
applicationContext: Context,
|
applicationContext: Context,
|
||||||
userLocationState: UserLocationState,
|
userLocationState: UserLocationState,
|
||||||
step: StepData?,
|
step: StepData?,
|
||||||
|
nextStep: StepData?,
|
||||||
cameraPosition: MutableLiveData<CameraPosition>,
|
cameraPosition: MutableLiveData<CameraPosition>,
|
||||||
routeData: MutableLiveData<String>,
|
routeData: MutableLiveData<String>,
|
||||||
tilt: Double
|
tilt: Double,
|
||||||
|
baseStyle: BaseStyle.Json,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(applicationContext)
|
val metrics =
|
||||||
|
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(applicationContext)
|
||||||
val width = metrics.bounds.width()
|
val width = metrics.bounds.width()
|
||||||
val height = metrics.bounds.height()
|
val height = metrics.bounds.height()
|
||||||
val paddingValues = PaddingValues(start = 0.dp, top = 350.dp)
|
val paddingValues = PaddingValues(start = 0.dp, top = 350.dp)
|
||||||
@@ -55,21 +56,20 @@ fun MapView(
|
|||||||
zoom = 15.0,
|
zoom = 15.0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val baseStyle = remember {
|
|
||||||
mutableStateOf(BaseStyle.Uri(Constants.STYLE))
|
val rememberBaseStyle = rememberBaseStyle( baseStyle)
|
||||||
}
|
|
||||||
DarkMode(applicationContext, baseStyle)
|
|
||||||
Column {
|
Column {
|
||||||
NavigationInfo(step)
|
NavigationInfo(step, nextStep)
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
MapLibre(
|
MapLibre(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
cameraState,
|
cameraState,
|
||||||
baseStyle,
|
rememberBaseStyle,
|
||||||
route,
|
route,
|
||||||
|
emptyMap(),
|
||||||
ViewStyle.VIEW
|
ViewStyle.VIEW
|
||||||
)
|
)
|
||||||
LocationTrackingEffect(
|
LocationTrackingEffect(
|
||||||
locationState = userLocationState,
|
locationState = userLocationState,
|
||||||
) {
|
) {
|
||||||
cameraState.animateTo(
|
cameraState.animateTo(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import com.kouros.navigation.data.StepData
|
|||||||
import com.kouros.navigation.utils.round
|
import com.kouros.navigation.utils.round
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationInfo(step: StepData?) {
|
fun NavigationInfo(step: StepData?, nextStep: StepData?) {
|
||||||
if (step != null && step.instruction.isNotEmpty()) {
|
if (step != null && step.instruction.isNotEmpty()) {
|
||||||
Card(modifier = Modifier.padding(top = 60.dp)) {
|
Card(modifier = Modifier.padding(top = 60.dp)) {
|
||||||
Column() {
|
Column() {
|
||||||
@@ -28,6 +28,10 @@ fun NavigationInfo(step: StepData?) {
|
|||||||
contentDescription = stringResource(id = R.string.accept_action_title),
|
contentDescription = stringResource(id = R.string.accept_action_title),
|
||||||
modifier = Modifier.size(48.dp, 48.dp),
|
modifier = Modifier.size(48.dp, 48.dp),
|
||||||
)
|
)
|
||||||
|
if (step.currentManeuverType == 46
|
||||||
|
|| step.currentManeuverType == 45) {
|
||||||
|
Text(text ="Exit ${step.exitNumber}", fontSize = 20.sp)
|
||||||
|
}
|
||||||
Column {
|
Column {
|
||||||
if (step.leftStepDistance < 1000) {
|
if (step.leftStepDistance < 1000) {
|
||||||
Text(text = "${step.leftStepDistance.toInt()} m", fontSize = 25.sp)
|
Text(text = "${step.leftStepDistance.toInt()} m", fontSize = 25.sp)
|
||||||
@@ -39,11 +43,13 @@ fun NavigationInfo(step: StepData?) {
|
|||||||
}
|
}
|
||||||
Text(text = step.instruction, fontSize = 20.sp)
|
Text(text = step.instruction, fontSize = 20.sp)
|
||||||
}
|
}
|
||||||
Icon(
|
if (nextStep != null && step.icon != nextStep.icon) {
|
||||||
painter = painterResource(step.icon),
|
Icon(
|
||||||
contentDescription = stringResource(id = R.string.accept_action_title),
|
painter = painterResource(nextStep.icon),
|
||||||
modifier = Modifier.size(48.dp, 48.dp),
|
contentDescription = stringResource(id = R.string.accept_action_title),
|
||||||
)
|
modifier = Modifier.size(48.dp, 48.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package com.kouros.navigation.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.alorma.compose.settings.ui.SettingsCheckbox
|
||||||
|
import com.alorma.compose.settings.ui.SettingsRadioButton
|
||||||
|
import com.kouros.data.R
|
||||||
|
import com.kouros.navigation.data.Constants
|
||||||
|
import com.kouros.navigation.data.Constants.DARK_MODE_SETTINGS
|
||||||
|
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
|
||||||
|
import com.kouros.navigation.data.RouteEngine
|
||||||
|
import com.kouros.navigation.utils.NavigationUtils
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun NavigationScreenSettings(context: Context, navigateBack: () -> Unit) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.navigation_settings),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = navigateBack) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.arrow_back_24px),
|
||||||
|
contentDescription = stringResource(id = R.string.accept_action_title),
|
||||||
|
modifier = Modifier.size(48.dp, 48.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.padding(top = padding.calculateTopPadding()),
|
||||||
|
) {
|
||||||
|
NavigationSettings(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NavigationSettings(context: Context) {
|
||||||
|
Section(title = stringResource(id = R.string.options)) {
|
||||||
|
val avoidMotorwayState = remember {
|
||||||
|
mutableStateOf(
|
||||||
|
NavigationUtils.getBooleanKeyValue(
|
||||||
|
context,
|
||||||
|
Constants.AVOID_MOTORWAY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SettingsCheckbox(
|
||||||
|
state = avoidMotorwayState.value,
|
||||||
|
title = { Text(text = stringResource(id = R.string.avoid_highways_row_title)) },
|
||||||
|
onCheckedChange = {
|
||||||
|
avoidMotorwayState.value = it
|
||||||
|
NavigationUtils.setBooleanKeyValue(context, it, Constants.AVOID_MOTORWAY)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
val avoidTollwayState = remember {
|
||||||
|
mutableStateOf(
|
||||||
|
NavigationUtils.getBooleanKeyValue(
|
||||||
|
context,
|
||||||
|
Constants.AVOID_TOLLWAY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SettingsCheckbox(
|
||||||
|
state = avoidTollwayState.value,
|
||||||
|
title = { Text(text = stringResource(id = R.string.avoid_tolls_row_title)) },
|
||||||
|
onCheckedChange = {
|
||||||
|
avoidTollwayState.value = it
|
||||||
|
NavigationUtils.setBooleanKeyValue(context, it, Constants.AVOID_TOLLWAY)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
val carLocationState = remember {
|
||||||
|
mutableStateOf(
|
||||||
|
NavigationUtils.getBooleanKeyValue(
|
||||||
|
context,
|
||||||
|
Constants.CAR_LOCATION
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SettingsCheckbox(
|
||||||
|
state = carLocationState.value,
|
||||||
|
title = { Text(text = stringResource(id = R.string.use_car_location)) },
|
||||||
|
onCheckedChange = {
|
||||||
|
carLocationState.value = it
|
||||||
|
NavigationUtils.setBooleanKeyValue(context, it, Constants.CAR_LOCATION)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(title = stringResource(id = R.string.routing_engine)) {
|
||||||
|
val state = remember {
|
||||||
|
mutableIntStateOf(
|
||||||
|
NavigationUtils.getIntKeyValue(
|
||||||
|
context,
|
||||||
|
ROUTING_ENGINE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
RoutingEngineData.engines.forEach { sampleItem ->
|
||||||
|
SettingsRadioButton(
|
||||||
|
state = state.intValue == sampleItem.key,
|
||||||
|
title = { Text(text = sampleItem.title) },
|
||||||
|
onClick = {
|
||||||
|
state.intValue = sampleItem.key
|
||||||
|
NavigationUtils.setIntKeyValue(context, state.intValue, ROUTING_ENGINE)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object RoutingEngineData {
|
||||||
|
val engines =
|
||||||
|
listOf(
|
||||||
|
Item(
|
||||||
|
key = 0,
|
||||||
|
title = RouteEngine.VALHALLA.toString(),
|
||||||
|
),
|
||||||
|
Item(
|
||||||
|
key = 1,
|
||||||
|
title = RouteEngine.OSRM.toString(),
|
||||||
|
),
|
||||||
|
Item(
|
||||||
|
key = 2,
|
||||||
|
title = RouteEngine.TOMTOM.toString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
|
import android.content.Context
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.VerticalDivider
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
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 androidx.core.graphics.drawable.IconCompat
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
|
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
|
||||||
import com.kouros.navigation.data.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
import com.kouros.navigation.model.RouteModel
|
import com.kouros.navigation.model.RouteModel
|
||||||
import com.kouros.navigation.utils.formatDateTime
|
import com.kouros.navigation.utils.formatDateTime
|
||||||
@@ -23,19 +24,26 @@ import com.kouros.navigation.utils.round
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationSheet(
|
fun NavigationSheet(
|
||||||
|
applicationContext: Context,
|
||||||
routeModel: RouteModel,
|
routeModel: RouteModel,
|
||||||
step: StepData,
|
step: StepData,
|
||||||
nextStep: StepData,
|
nextStep: StepData,
|
||||||
stopNavigation: () -> Unit,
|
stopNavigation: () -> Unit,
|
||||||
simulateNavigation: () -> Unit,
|
simulateNavigation: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val distance = step.leftDistance.round(1)
|
val distance = (step.leftDistance / 1000).round(1)
|
||||||
|
|
||||||
|
if (step.lane.isNotEmpty()) {
|
||||||
|
routeModel.navState.iconMapper.addLanes( step)
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
|
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||||
Text(formatDateTime(step.arrivalTime), fontSize = 22.sp)
|
Text(formatDateTime(step.arrivalTime), fontSize = 22.sp)
|
||||||
Spacer(Modifier.size(30.dp))
|
Spacer(Modifier.size(30.dp))
|
||||||
Text("$distance km", fontSize = 22.sp)
|
Text("$distance km", fontSize = 22.sp)
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
|
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||||
if (routeModel.isNavigating()) {
|
if (routeModel.isNavigating()) {
|
||||||
@@ -48,6 +56,15 @@ fun NavigationSheet(
|
|||||||
modifier = Modifier.size(24.dp, 24.dp),
|
modifier = Modifier.size(24.dp, 24.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Button(onClick = {
|
||||||
|
simulateNavigation()
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_zoom_in_24),
|
||||||
|
"Stop",
|
||||||
|
modifier = Modifier.size(24.dp, 24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.size(30.dp))
|
Spacer(Modifier.size(30.dp))
|
||||||
if (!routeModel.isNavigating()) {
|
if (!routeModel.isNavigating()) {
|
||||||
|
|||||||
@@ -57,38 +57,31 @@ fun SearchSheet(
|
|||||||
if (search.value != null) {
|
if (search.value != null) {
|
||||||
searchResults.addAll(search.value!!)
|
searchResults.addAll(search.value!!)
|
||||||
}
|
}
|
||||||
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
|
|
||||||
if (searchResults.isNotEmpty()) {
|
|
||||||
val textFieldState = rememberTextFieldState()
|
|
||||||
val items = listOf(searchResults)
|
|
||||||
if (items.isNotEmpty()) {
|
|
||||||
SearchBar(
|
|
||||||
textFieldState = textFieldState,
|
|
||||||
searchPlaces = recentPlaces.value!!,
|
|
||||||
searchResults = searchResults,
|
|
||||||
viewModel = viewModel,
|
|
||||||
context = applicationContext,
|
|
||||||
location = location,
|
|
||||||
closeSheet = { closeSheet() }
|
|
||||||
|
|
||||||
)
|
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
|
||||||
}
|
|
||||||
}
|
|
||||||
if (recentPlaces.value != null) {
|
if (recentPlaces.value != null) {
|
||||||
val textFieldState = rememberTextFieldState()
|
|
||||||
val items = listOf(recentPlaces)
|
val items = listOf(recentPlaces)
|
||||||
if (items.isNotEmpty()) {
|
if (items.isNotEmpty()) {
|
||||||
SearchBar(
|
RecentPlaces(recentPlaces.value!!, viewModel, applicationContext, location, closeSheet)
|
||||||
textFieldState = textFieldState,
|
|
||||||
searchPlaces = recentPlaces.value!!,
|
|
||||||
searchResults = searchResults,
|
|
||||||
viewModel = viewModel,
|
|
||||||
context = applicationContext,
|
|
||||||
location = location,
|
|
||||||
closeSheet = { closeSheet() }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// if (searchResults.isNotEmpty()) {
|
||||||
|
val textFieldState = rememberTextFieldState()
|
||||||
|
val items = listOf(searchResults)
|
||||||
|
// if (items.isNotEmpty()) {
|
||||||
|
SearchBar(
|
||||||
|
textFieldState = textFieldState,
|
||||||
|
searchPlaces = emptyList<Place>(),
|
||||||
|
searchResults = searchResults,
|
||||||
|
viewModel = viewModel,
|
||||||
|
context = applicationContext,
|
||||||
|
location = location,
|
||||||
|
closeSheet = { closeSheet() }
|
||||||
|
|
||||||
|
)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +96,7 @@ fun Home(
|
|||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
val places = viewModel.loadRecentPlace()
|
val places = viewModel.loadRecentPlace()
|
||||||
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
|
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
|
||||||
viewModel.loadRoute(applicationContext, location, toLocation)
|
viewModel.loadRoute(applicationContext, location, toLocation, 0F)
|
||||||
closeSheet()
|
closeSheet()
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -138,15 +131,7 @@ fun SearchBar(
|
|||||||
closeSheet: () -> Unit
|
closeSheet: () -> Unit
|
||||||
) {
|
) {
|
||||||
var expanded by rememberSaveable { mutableStateOf(true) }
|
var expanded by rememberSaveable { mutableStateOf(true) }
|
||||||
Box(
|
|
||||||
modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.semantics { isTraversalGroup = true }
|
|
||||||
) {
|
|
||||||
SearchBar(
|
SearchBar(
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopCenter)
|
|
||||||
.semantics { traversalIndex = 0f },
|
|
||||||
inputField = {
|
inputField = {
|
||||||
SearchBarDefaults.InputField(
|
SearchBarDefaults.InputField(
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
@@ -179,7 +164,6 @@ fun SearchBar(
|
|||||||
SearchPlaces(searchResults, viewModel, context, location, closeSheet)
|
SearchPlaces(searchResults, viewModel, context, location, closeSheet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchPlaces(viewModel: ViewModel, location: Location, it: String) {
|
private fun searchPlaces(viewModel: ViewModel, location: Location, it: String) {
|
||||||
@@ -223,7 +207,7 @@ private fun SearchPlaces(
|
|||||||
viewModel.saveRecent(pl)
|
viewModel.saveRecent(pl)
|
||||||
val toLocation =
|
val toLocation =
|
||||||
location(place.lon.toDouble(), place.lat.toDouble())
|
location(place.lon.toDouble(), place.lat.toDouble())
|
||||||
viewModel.loadRoute(context, location, toLocation)
|
viewModel.loadRoute(context, location, toLocation, 0F)
|
||||||
closeSheet()
|
closeSheet()
|
||||||
}
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -261,7 +245,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)
|
viewModel.loadRoute(context, location, toLocation, 0F)
|
||||||
closeSheet()
|
closeSheet()
|
||||||
}
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
86
app/src/main/java/com/kouros/navigation/ui/SettingsScreen.kt
Normal file
86
app/src/main/java/com/kouros/navigation/ui/SettingsScreen.kt
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package com.kouros.navigation.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import com.alorma.compose.settings.ui.SettingsMenuLink
|
||||||
|
import com.kouros.data.R
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(navController: NavHostController, navigateBack: () -> Unit) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.settings_action_title),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = navigateBack) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.arrow_back_24px),
|
||||||
|
contentDescription = stringResource(id = R.string.accept_action_title),
|
||||||
|
modifier = Modifier.size(48.dp, 48.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.padding(top = padding.calculateTopPadding()),
|
||||||
|
) {
|
||||||
|
SettingsMenuLink(
|
||||||
|
title = { Text(text = stringResource(R.string.display_settings)) },
|
||||||
|
modifier = Modifier,
|
||||||
|
enabled = true,
|
||||||
|
onClick = { navController.navigate("display_settings")},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_place_white_24dp),
|
||||||
|
contentDescription = stringResource(id = R.string.display_settings),
|
||||||
|
modifier = Modifier.size(48.dp, 48.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
SettingsMenuLink(
|
||||||
|
title = { Text(text = stringResource(R.string.navigation_settings)) },
|
||||||
|
modifier = Modifier,
|
||||||
|
enabled = true,
|
||||||
|
onClick = { navController.navigate("nav_settings")},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.navigation_24px),
|
||||||
|
contentDescription = stringResource(id = R.string.navigation_settings),
|
||||||
|
modifier = Modifier.size(48.dp, 48.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2,10 +2,75 @@ package com.kouros.navigation.ui.theme
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
val Purple80 = Color(0xFFD0BCFF)
|
//val Purple80 = Color(0xFFD0BCFF)
|
||||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
//val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||||
val Pink80 = Color(0xFFEFB8C8)
|
//val Pink80 = Color(0xFFEFB8C8)
|
||||||
|
//
|
||||||
|
//val Purple40 = Color(0xFF6650a4)
|
||||||
|
//val PurpleGrey40 = Color(0xFF625b71)
|
||||||
|
//val Pink40 = Color(0xFF7D5260)
|
||||||
|
|
||||||
val Purple40 = Color(0xFF6650a4)
|
val md_theme_light_primary = Color(0xFF825500)
|
||||||
val PurpleGrey40 = Color(0xFF625b71)
|
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||||
val Pink40 = Color(0xFF7D5260)
|
val md_theme_light_primaryContainer = Color(0xFFFFDDB3)
|
||||||
|
val md_theme_light_onPrimaryContainer = Color(0xFF291800)
|
||||||
|
val md_theme_light_secondary = Color(0xFF6F5B40)
|
||||||
|
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_secondaryContainer = Color(0xFFFBDEBC)
|
||||||
|
val md_theme_light_onSecondaryContainer = Color(0xFF271904)
|
||||||
|
val md_theme_light_tertiary = Color(0xFF51643F)
|
||||||
|
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_tertiaryContainer = Color(0xFFD4EABB)
|
||||||
|
val md_theme_light_onTertiaryContainer = Color(0xFF102004)
|
||||||
|
val md_theme_light_error = Color(0xFFBA1A1A)
|
||||||
|
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||||
|
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||||
|
val md_theme_light_background = Color(0xFFFFFBFF)
|
||||||
|
val md_theme_light_onBackground = Color(0xFF1F1B16)
|
||||||
|
val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||||
|
val md_theme_light_onSurface = Color(0xFF1F1B16)
|
||||||
|
val md_theme_light_surfaceVariant = Color(0xFFF0E0CF)
|
||||||
|
val md_theme_light_onSurfaceVariant = Color(0xFF4F4539)
|
||||||
|
val md_theme_light_outline = Color(0xFF817567)
|
||||||
|
val md_theme_light_inverseOnSurface = Color(0xFFF9EFE7)
|
||||||
|
val md_theme_light_inverseSurface = Color(0xFF34302A)
|
||||||
|
val md_theme_light_inversePrimary = Color(0xFFFFB951)
|
||||||
|
val md_theme_light_shadow = Color(0xFF000000)
|
||||||
|
val md_theme_light_surfaceTint = Color(0xFF825500)
|
||||||
|
val md_theme_light_outlineVariant = Color(0xFFD3C4B4)
|
||||||
|
val md_theme_light_scrim = Color(0xFF000000)
|
||||||
|
|
||||||
|
val md_theme_dark_primary = Color(0xFFFFB951)
|
||||||
|
val md_theme_dark_onPrimary = Color(0xFF452B00)
|
||||||
|
val md_theme_dark_primaryContainer = Color(0xFF633F00)
|
||||||
|
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3)
|
||||||
|
val md_theme_dark_secondary = Color(0xFFDDC2A1)
|
||||||
|
val md_theme_dark_onSecondary = Color(0xFF3E2D16)
|
||||||
|
val md_theme_dark_secondaryContainer = Color(0xFF56442A)
|
||||||
|
val md_theme_dark_onSecondaryContainer = Color(0xFFFBDEBC)
|
||||||
|
val md_theme_dark_tertiary = Color(0xFFB8CEA1)
|
||||||
|
val md_theme_dark_onTertiary = Color(0xFF243515)
|
||||||
|
val md_theme_dark_tertiaryContainer = Color(0xFF3A4C2A)
|
||||||
|
val md_theme_dark_onTertiaryContainer = Color(0xFFD4EABB)
|
||||||
|
val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||||
|
val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||||
|
val md_theme_dark_onError = Color(0xFF690005)
|
||||||
|
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||||
|
val md_theme_dark_background = Color(0xFF1F1B16)
|
||||||
|
val md_theme_dark_onBackground = Color(0xFFEAE1D9)
|
||||||
|
val md_theme_dark_surface = Color(0xFF1F1B16)
|
||||||
|
val md_theme_dark_onSurface = Color(0xFFEAE1D9)
|
||||||
|
val md_theme_dark_surfaceVariant = Color(0xFF4F4539)
|
||||||
|
val md_theme_dark_onSurfaceVariant = Color(0xFFD3C4B4)
|
||||||
|
val md_theme_dark_outline = Color(0xFF9C8F80)
|
||||||
|
val md_theme_dark_inverseOnSurface = Color(0xFF1F1B16)
|
||||||
|
val md_theme_dark_inverseSurface = Color(0xFFEAE1D9)
|
||||||
|
val md_theme_dark_inversePrimary = Color(0xFF825500)
|
||||||
|
val md_theme_dark_shadow = Color(0xFF000000)
|
||||||
|
val md_theme_dark_surfaceTint = Color(0xFFFFB951)
|
||||||
|
val md_theme_dark_outlineVariant = Color(0xFF4F4539)
|
||||||
|
val md_theme_dark_scrim = Color(0xFF000000)
|
||||||
|
|
||||||
|
|
||||||
|
val seed = Color(0xFF825500)
|
||||||
|
|||||||
29
app/src/main/java/com/kouros/navigation/ui/theme/Shapes.kt
Normal file
29
app/src/main/java/com/kouros/navigation/ui/theme/Shapes.kt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kouros.navigation.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Shapes
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
val shapes = Shapes(
|
||||||
|
extraSmall = RoundedCornerShape(4.dp),
|
||||||
|
small = RoundedCornerShape(8.dp),
|
||||||
|
medium = RoundedCornerShape(16.dp),
|
||||||
|
large = RoundedCornerShape(24.dp),
|
||||||
|
extraLarge = RoundedCornerShape(32.dp)
|
||||||
|
)
|
||||||
@@ -1,57 +1,111 @@
|
|||||||
package com.kouros.navigation.ui.theme
|
package com.kouros.navigation.ui.theme
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val LightColors = lightColorScheme(
|
||||||
primary = Purple80,
|
primary = md_theme_light_primary,
|
||||||
secondary = PurpleGrey80,
|
onPrimary = md_theme_light_onPrimary,
|
||||||
tertiary = Pink80
|
primaryContainer = md_theme_light_primaryContainer,
|
||||||
|
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||||
|
secondary = md_theme_light_secondary,
|
||||||
|
onSecondary = md_theme_light_onSecondary,
|
||||||
|
secondaryContainer = md_theme_light_secondaryContainer,
|
||||||
|
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||||
|
tertiary = md_theme_light_tertiary,
|
||||||
|
onTertiary = md_theme_light_onTertiary,
|
||||||
|
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||||
|
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||||
|
error = md_theme_light_error,
|
||||||
|
errorContainer = md_theme_light_errorContainer,
|
||||||
|
onError = md_theme_light_onError,
|
||||||
|
onErrorContainer = md_theme_light_onErrorContainer,
|
||||||
|
background = md_theme_light_background,
|
||||||
|
onBackground = md_theme_light_onBackground,
|
||||||
|
surface = md_theme_light_surface,
|
||||||
|
onSurface = md_theme_light_onSurface,
|
||||||
|
surfaceVariant = md_theme_light_surfaceVariant,
|
||||||
|
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||||
|
outline = md_theme_light_outline,
|
||||||
|
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||||
|
inverseSurface = md_theme_light_inverseSurface,
|
||||||
|
inversePrimary = md_theme_light_inversePrimary,
|
||||||
|
surfaceTint = md_theme_light_surfaceTint,
|
||||||
|
outlineVariant = md_theme_light_outlineVariant,
|
||||||
|
scrim = md_theme_light_scrim,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme(
|
|
||||||
primary = Purple40,
|
|
||||||
secondary = PurpleGrey40,
|
|
||||||
tertiary = Pink40
|
|
||||||
|
|
||||||
/* Other default colors to override
|
private val DarkColors = darkColorScheme(
|
||||||
background = Color(0xFFFFFBFE),
|
primary = md_theme_dark_primary,
|
||||||
surface = Color(0xFFFFFBFE),
|
onPrimary = md_theme_dark_onPrimary,
|
||||||
onPrimary = Color.White,
|
primaryContainer = md_theme_dark_primaryContainer,
|
||||||
onSecondary = Color.White,
|
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||||
onTertiary = Color.White,
|
secondary = md_theme_dark_secondary,
|
||||||
onBackground = Color(0xFF1C1B1F),
|
onSecondary = md_theme_dark_onSecondary,
|
||||||
onSurface = Color(0xFF1C1B1F),
|
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||||
*/
|
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||||
|
tertiary = md_theme_dark_tertiary,
|
||||||
|
onTertiary = md_theme_dark_onTertiary,
|
||||||
|
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||||
|
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||||
|
error = md_theme_dark_error,
|
||||||
|
errorContainer = md_theme_dark_errorContainer,
|
||||||
|
onError = md_theme_dark_onError,
|
||||||
|
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||||
|
background = md_theme_dark_background,
|
||||||
|
onBackground = md_theme_dark_onBackground,
|
||||||
|
surface = md_theme_dark_surface,
|
||||||
|
onSurface = md_theme_dark_onSurface,
|
||||||
|
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||||
|
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||||
|
outline = md_theme_dark_outline,
|
||||||
|
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||||
|
inverseSurface = md_theme_dark_inverseSurface,
|
||||||
|
inversePrimary = md_theme_dark_inversePrimary,
|
||||||
|
surfaceTint = md_theme_dark_surfaceTint,
|
||||||
|
outlineVariant = md_theme_dark_outlineVariant,
|
||||||
|
scrim = md_theme_dark_scrim,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationTheme(
|
fun NavigationTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
// Dynamic color is available on Android 12+
|
|
||||||
dynamicColor: Boolean = true,
|
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val context = LocalContext.current
|
||||||
dynamicColor -> {
|
val colors = run {
|
||||||
val context = LocalContext.current
|
if (useDarkTheme) dynamicDarkColorScheme(context)
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
else dynamicLightColorScheme(context)
|
||||||
}
|
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
|
||||||
else -> LightColorScheme
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
val view = LocalView.current
|
||||||
colorScheme = colorScheme,
|
if (!view.isInEditMode) {
|
||||||
typography = Typography,
|
SideEffect {
|
||||||
content = content
|
val window = (view.context as Activity).window
|
||||||
)
|
window.statusBarColor = colors.primary.toArgb()
|
||||||
|
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
|
||||||
|
useDarkTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = typography,
|
||||||
|
content = content,
|
||||||
|
shapes = shapes,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,28 +7,35 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
// Set of Material typography styles to start with
|
// Set of Material typography styles to start with
|
||||||
val Typography = Typography(
|
val typography = Typography(
|
||||||
|
headlineSmall = TextStyle(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
lineHeight = 32.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
lineHeight = 32.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
bodyLarge = TextStyle(
|
bodyLarge = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
lineHeight = 24.sp,
|
lineHeight = 24.sp,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.15.sp
|
||||||
)
|
|
||||||
/* Other default text styles to override
|
|
||||||
titleLarge = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontSize = 22.sp,
|
|
||||||
lineHeight = 28.sp,
|
|
||||||
letterSpacing = 0.sp
|
|
||||||
),
|
),
|
||||||
labelSmall = TextStyle(
|
bodyMedium = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 11.sp,
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.25.sp
|
||||||
|
),
|
||||||
|
labelMedium = TextStyle(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 12.sp,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.5.sp
|
||||||
)
|
)
|
||||||
*/
|
)
|
||||||
)
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ buildscript {
|
|||||||
val objectboxVersion by extra("5.0.1") // For KTS build scripts
|
val objectboxVersion by extra("5.0.1") // For KTS build scripts
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Android Gradle Plugin 8.0 or later supported
|
|
||||||
classpath(libs.gradle)
|
classpath(libs.gradle)
|
||||||
classpath("io.objectbox:objectbox-gradle-plugin:$objectboxVersion")
|
classpath("io.objectbox:objectbox-gradle-plugin:$objectboxVersion")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ 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(libs.maplibre.composeMaterial3)
|
//implementation(libs.maplibre.composeMaterial3)
|
||||||
|
|
||||||
implementation(project(":common:data"))
|
implementation(project(":common:data"))
|
||||||
|
|||||||
@@ -32,8 +32,11 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
|
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
|
||||||
|
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED"/>
|
||||||
|
<uses-permission android:name="android.car.permission.READ_CAR_DISPLAY_UNITS"/>
|
||||||
|
|
||||||
<application android:requestLegacyExternalStorage="true">
|
<application android:requestLegacyExternalStorage="true"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="androidx.car.app.minCarApiLevel"
|
android:name="androidx.car.app.minCarApiLevel"
|
||||||
android:value="1" />
|
android:value="1" />
|
||||||
|
|||||||
@@ -38,9 +38,4 @@ class NavigationCarAppService : CarAppService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface LocationCallback {
|
|
||||||
|
|
||||||
fun onLocationChanged(location: Location) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -12,6 +13,13 @@ import androidx.car.app.CarContext
|
|||||||
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.Session
|
||||||
|
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.Speed
|
||||||
import androidx.core.location.LocationListenerCompat
|
import androidx.core.location.LocationListenerCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
@@ -22,17 +30,18 @@ import com.kouros.navigation.car.navigation.RouteCarModel
|
|||||||
import com.kouros.navigation.car.screen.NavigationScreen
|
import com.kouros.navigation.car.screen.NavigationScreen
|
||||||
import com.kouros.navigation.car.screen.RequestPermissionScreen
|
import com.kouros.navigation.car.screen.RequestPermissionScreen
|
||||||
import com.kouros.navigation.car.screen.SearchScreen
|
import com.kouros.navigation.car.screen.SearchScreen
|
||||||
|
import com.kouros.navigation.data.Constants.CAR_LOCATION
|
||||||
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
|
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
|
||||||
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
|
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
|
||||||
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
|
|
||||||
import com.kouros.navigation.data.Constants.TAG
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
import com.kouros.navigation.data.RouteEngine
|
||||||
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.valhalla.ValhallaRepository
|
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||||
import com.kouros.navigation.model.ViewModel
|
import com.kouros.navigation.model.ViewModel
|
||||||
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getRouteEngine
|
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
||||||
|
|
||||||
class NavigationSession : Session(), NavigationScreen.Listener {
|
class NavigationSession : Session(), NavigationScreen.Listener {
|
||||||
|
|
||||||
@@ -45,27 +54,33 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
lateinit var surfaceRenderer: SurfaceRenderer
|
lateinit var surfaceRenderer: SurfaceRenderer
|
||||||
|
|
||||||
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? ->
|
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? ->
|
||||||
updateLocation(location!!)
|
val useCarLocation = getBooleanKeyValue(carContext, CAR_LOCATION)
|
||||||
|
if (!useCarLocation) {
|
||||||
|
updateLocation(location!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
private val mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
||||||
override fun onCreate(owner: LifecycleOwner) {
|
override fun onCreate(owner: LifecycleOwner) {
|
||||||
Log.i(TAG, "In onCreate()")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume(owner: LifecycleOwner) {
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
Log.i(TAG, "In onResume()")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause(owner: LifecycleOwner) {
|
override fun onPause(owner: LifecycleOwner) {
|
||||||
Log.i(TAG, "In onPause()")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
Log.i(TAG, "In onStop()")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy(owner: LifecycleOwner) {
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
|
||||||
|
val useCarLocation = getBooleanKeyValue(carContext, CAR_LOCATION)
|
||||||
|
if (useCarLocation) {
|
||||||
|
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
|
||||||
|
carSensors.removeCarHardwareLocationListener(carLocationListener)
|
||||||
|
}
|
||||||
|
carInfo.removeSpeedListener(carSpeedListener)
|
||||||
Log.i(TAG, "In onDestroy()")
|
Log.i(TAG, "In onDestroy()")
|
||||||
val locationManager =
|
val locationManager =
|
||||||
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
@@ -73,26 +88,67 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var navigationViewModel : ViewModel
|
lateinit var navigationViewModel: ViewModel
|
||||||
|
|
||||||
|
val carLocationListener: OnCarDataAvailableListener<CarHardwareLocation?> =
|
||||||
|
OnCarDataAvailableListener { data ->
|
||||||
|
if (data.location.status == CarValue.STATUS_SUCCESS) {
|
||||||
|
val location = data.location.value
|
||||||
|
if (location != null) {
|
||||||
|
updateLocation(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val carCompassListener: OnCarDataAvailableListener<Compass?> =
|
||||||
|
OnCarDataAvailableListener { data ->
|
||||||
|
if (data.orientations.status == CarValue.STATUS_SUCCESS) {
|
||||||
|
val orientation = data.orientations.value
|
||||||
|
if (orientation != null) {
|
||||||
|
surfaceRenderer.carOrientation = orientation[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val carSpeedListener = OnCarDataAvailableListener<Speed> { data ->
|
||||||
|
if (data.displaySpeedMetersPerSecond.status == CarValue.STATUS_SUCCESS) {
|
||||||
|
val speed = data.displaySpeedMetersPerSecond.value
|
||||||
|
surfaceRenderer.updateCarSpeed(speed!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
init {
|
init {
|
||||||
val lifecycle: Lifecycle = lifecycle
|
val lifecycle: Lifecycle = lifecycle
|
||||||
lifecycle.addObserver(mLifeCycleObserver)
|
lifecycle.addObserver(mLifeCycleObserver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onRoutingEngineStateUpdated(routeEngine : Int) {
|
||||||
|
navigationViewModel = when (routeEngine) {
|
||||||
|
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
|
||||||
|
RouteEngine.OSRM.ordinal -> ViewModel(OsrmRepository())
|
||||||
|
else -> ViewModel(TomTomRepository())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateScreen(intent: Intent): Screen {
|
override fun onCreateScreen(intent: Intent): Screen {
|
||||||
|
|
||||||
navigationViewModel = getRouteEngine(carContext)
|
navigationViewModel = getViewModel(carContext)
|
||||||
|
|
||||||
|
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
|
||||||
|
|
||||||
routeModel = RouteCarModel()
|
routeModel = RouteCarModel()
|
||||||
|
|
||||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel)
|
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel)
|
||||||
|
|
||||||
navigationScreen = NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel)
|
navigationScreen =
|
||||||
|
NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel)
|
||||||
|
|
||||||
if (carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
|
if (carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
== PackageManager.PERMISSION_GRANTED && !useContacts
|
== PackageManager.PERMISSION_GRANTED
|
||||||
|
&& carContext.checkSelfPermission("com.google.android.gms.permission.CAR_SPEED") == PackageManager.PERMISSION_GRANTED
|
||||||
|
&& !useContacts
|
||||||
|| (useContacts && carContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)
|
|| (useContacts && carContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)
|
||||||
== PackageManager.PERMISSION_GRANTED)
|
== PackageManager.PERMISSION_GRANTED)
|
||||||
) {
|
) {
|
||||||
requestLocationUpdates()
|
requestLocationUpdates()
|
||||||
} else {
|
} else {
|
||||||
@@ -111,9 +167,29 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSensors()
|
||||||
|
|
||||||
return navigationScreen
|
return navigationScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addSensors() {
|
||||||
|
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
|
||||||
|
val useCarLocation = getBooleanKeyValue(carContext, CAR_LOCATION)
|
||||||
|
if (useCarLocation) {
|
||||||
|
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
|
||||||
|
carSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL,
|
||||||
|
carContext.mainExecutor,
|
||||||
|
carCompassListener)
|
||||||
|
carSensors.addCarHardwareLocationListener(
|
||||||
|
CarSensors.UPDATE_RATE_FASTEST,
|
||||||
|
carContext.mainExecutor,
|
||||||
|
carLocationListener
|
||||||
|
)
|
||||||
|
}
|
||||||
|
carInfo.addSpeedListener(carContext.mainExecutor, carSpeedListener)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
val screenManager = carContext.getCarService(ScreenManager::class.java)
|
val screenManager = carContext.getCarService(ScreenManager::class.java)
|
||||||
if ((CarContext.ACTION_NAVIGATE == intent.action)) {
|
if ((CarContext.ACTION_NAVIGATE == intent.action)) {
|
||||||
@@ -124,8 +200,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
SearchScreen(
|
SearchScreen(
|
||||||
carContext,
|
carContext,
|
||||||
surfaceRenderer,
|
surfaceRenderer,
|
||||||
location,
|
navigationViewModel
|
||||||
navigationViewModel,
|
|
||||||
// TODO: Uri
|
// TODO: Uri
|
||||||
)
|
)
|
||||||
) { obj: Any? ->
|
) { obj: Any? ->
|
||||||
@@ -151,16 +226,22 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCarConfigurationChanged(newConfiguration: Configuration) {
|
||||||
|
println("Configuration: ${newConfiguration.isNightModeActive}")
|
||||||
|
super.onCarConfigurationChanged(newConfiguration)
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun requestLocationUpdates() {
|
fun requestLocationUpdates() {
|
||||||
val locationManager =
|
val locationManager =
|
||||||
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
||||||
if (location != null) {
|
if (location != null) {
|
||||||
|
navigationViewModel.loadRecentPlace(location = location, surfaceRenderer.carOrientation, carContext)
|
||||||
updateLocation(location)
|
updateLocation(location)
|
||||||
locationManager.requestLocationUpdates(
|
locationManager.requestLocationUpdates(
|
||||||
LocationManager.GPS_PROVIDER,
|
LocationManager.GPS_PROVIDER,
|
||||||
/* minTimeMs= */ 1000,
|
/* minTimeMs= */ 500,
|
||||||
/* minDistanceM= */ 5f,
|
/* minDistanceM= */ 5f,
|
||||||
mLocationListener
|
mLocationListener
|
||||||
)
|
)
|
||||||
@@ -169,17 +250,19 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
|
|
||||||
fun updateLocation(location: Location) {
|
fun updateLocation(location: Location) {
|
||||||
if (routeModel.isNavigating()) {
|
if (routeModel.isNavigating()) {
|
||||||
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
|
|
||||||
val distance = location.distanceTo(snapedLocation)
|
|
||||||
if (distance > MAXIMAL_ROUTE_DEVIATION) {
|
|
||||||
navigationScreen.calculateNewRoute(routeModel.routeState.destination)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
navigationScreen.updateTrip(location)
|
navigationScreen.updateTrip(location)
|
||||||
if (distance < MAXIMAL_SNAP_CORRECTION) {
|
if (!routeModel.navState.arrived) {
|
||||||
surfaceRenderer.updateLocation(snapedLocation)
|
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
|
||||||
} else {
|
val distance = location.distanceTo(snapedLocation)
|
||||||
surfaceRenderer.updateLocation(location)
|
if (distance > MAXIMAL_ROUTE_DEVIATION) {
|
||||||
|
navigationScreen.calculateNewRoute(routeModel.navState.destination)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (distance < MAXIMAL_SNAP_CORRECTION) {
|
||||||
|
surfaceRenderer.updateLocation(snapedLocation)
|
||||||
|
} else {
|
||||||
|
surfaceRenderer.updateLocation(location)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
surfaceRenderer.updateLocation(location)
|
surfaceRenderer.updateLocation(location)
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import android.graphics.Rect
|
|||||||
import android.hardware.display.DisplayManager
|
import android.hardware.display.DisplayManager
|
||||||
import android.hardware.display.VirtualDisplay
|
import android.hardware.display.VirtualDisplay
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.os.CountDownTimer
|
|
||||||
import android.os.Handler
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.car.app.AppManager
|
import androidx.car.app.AppManager
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
@@ -18,8 +16,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
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.mutableStateOf
|
|
||||||
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
|
||||||
@@ -27,16 +23,21 @@ import androidx.lifecycle.LifecycleOwner
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.setViewTreeLifecycleOwner
|
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||||
import com.kouros.navigation.car.map.DarkMode
|
|
||||||
import com.kouros.navigation.car.map.DrawNavigationImages
|
import com.kouros.navigation.car.map.DrawNavigationImages
|
||||||
import com.kouros.navigation.car.map.MapLibre
|
import com.kouros.navigation.car.map.MapLibre
|
||||||
import com.kouros.navigation.car.map.cameraState
|
import com.kouros.navigation.car.map.cameraState
|
||||||
import com.kouros.navigation.car.map.getPaddingValues
|
import com.kouros.navigation.car.map.getPaddingValues
|
||||||
|
import com.kouros.navigation.car.map.rememberBaseStyle
|
||||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
import com.kouros.navigation.data.Constants.homeLocation
|
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
|
||||||
|
import com.kouros.navigation.data.Constants.homeVogelhart
|
||||||
import com.kouros.navigation.data.ObjectBox
|
import com.kouros.navigation.data.ObjectBox
|
||||||
|
import com.kouros.navigation.data.RouteEngine
|
||||||
|
import com.kouros.navigation.data.tomtom.TrafficData
|
||||||
|
import com.kouros.navigation.model.BaseStyleModel
|
||||||
import com.kouros.navigation.model.RouteModel
|
import com.kouros.navigation.model.RouteModel
|
||||||
|
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
||||||
import com.kouros.navigation.utils.bearing
|
import com.kouros.navigation.utils.bearing
|
||||||
import com.kouros.navigation.utils.calculateTilt
|
import com.kouros.navigation.utils.calculateTilt
|
||||||
import com.kouros.navigation.utils.calculateZoom
|
import com.kouros.navigation.utils.calculateZoom
|
||||||
@@ -55,10 +56,12 @@ class SurfaceRenderer(
|
|||||||
) : DefaultLifecycleObserver {
|
) : DefaultLifecycleObserver {
|
||||||
|
|
||||||
var lastLocation = location(0.0, 0.0)
|
var lastLocation = location(0.0, 0.0)
|
||||||
|
|
||||||
|
var carOrientation = 999F
|
||||||
private val cameraPosition = MutableLiveData(
|
private val cameraPosition = MutableLiveData(
|
||||||
CameraPosition(
|
CameraPosition(
|
||||||
zoom = 15.0,
|
zoom = 15.0,
|
||||||
target = Position(latitude = homeLocation.latitude, longitude = homeLocation.longitude)
|
target = Position(latitude = homeVogelhart.latitude, longitude = homeVogelhart.longitude)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
private var visibleArea = MutableLiveData(
|
private var visibleArea = MutableLiveData(
|
||||||
@@ -69,14 +72,21 @@ class SurfaceRenderer(
|
|||||||
var height = 0
|
var height = 0
|
||||||
var lastBearing = 0.0
|
var lastBearing = 0.0
|
||||||
val routeData = MutableLiveData("")
|
val routeData = MutableLiveData("")
|
||||||
|
|
||||||
|
val trafficData = MutableLiveData(emptyMap<String, String>())
|
||||||
val speedCamerasData = MutableLiveData("")
|
val speedCamerasData = MutableLiveData("")
|
||||||
val speed = MutableLiveData(0F)
|
val speed = MutableLiveData(0F)
|
||||||
lateinit var centerLocation: Location
|
val maxSpeed = MutableLiveData(0)
|
||||||
var viewStyle = ViewStyle.VIEW
|
var viewStyle = ViewStyle.VIEW
|
||||||
|
lateinit var centerLocation: Location
|
||||||
var previewDistance = 0.0
|
var previewDistance = 0.0
|
||||||
lateinit var mapView: ComposeView
|
lateinit var mapView: ComposeView
|
||||||
var tilt = 55.0
|
var tilt = 55.0
|
||||||
var countDownTimerActive = false
|
|
||||||
|
val style: MutableLiveData<BaseStyle> by lazy {
|
||||||
|
MutableLiveData()
|
||||||
|
}
|
||||||
|
|
||||||
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
|
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
|
||||||
|
|
||||||
lateinit var lifecycleOwner: CustomLifecycleOwner
|
lateinit var lifecycleOwner: CustomLifecycleOwner
|
||||||
@@ -159,27 +169,35 @@ class SurfaceRenderer(
|
|||||||
init {
|
init {
|
||||||
lifecycle.addObserver(this)
|
lifecycle.addObserver(this)
|
||||||
speed.value = 0F
|
speed.value = 0F
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onConnectionStateUpdated(connectionState: Int) {
|
fun onConnectionStateUpdated(connectionState: Int) {
|
||||||
when (connectionState) {
|
when (connectionState) {
|
||||||
CarConnection.CONNECTION_TYPE_NATIVE -> ObjectBox.init(carContext)
|
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit"
|
||||||
|
CarConnection.CONNECTION_TYPE_PROJECTION -> "Connected to Android Auto"
|
||||||
|
CarConnection.CONNECTION_TYPE_NATIVE -> ObjectBox.init(carContext) // Automotive OS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onBaseStyleStateUpdated(style: BaseStyle) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MapView() {
|
fun MapView() {
|
||||||
|
|
||||||
|
val darkMode = getIntKeyValue(carContext, Constants.DARK_MODE_SETTINGS)
|
||||||
|
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
|
||||||
|
|
||||||
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 speedCameras: String? by speedCamerasData.observeAsState()
|
val speedCameras: String? by speedCamerasData.observeAsState()
|
||||||
val paddingValues = getPaddingValues(height, viewStyle)
|
val paddingValues = getPaddingValues(height, viewStyle)
|
||||||
val cameraState = cameraState(paddingValues, position, tilt)
|
val cameraState = cameraState(paddingValues, position, tilt)
|
||||||
|
val rememberBaseStyle = rememberBaseStyle(baseStyle)
|
||||||
val baseStyle = remember {
|
MapLibre(carContext, cameraState, rememberBaseStyle, route, traffic, viewStyle, speedCameras)
|
||||||
mutableStateOf(BaseStyle.Uri(Constants.STYLE))
|
|
||||||
}
|
|
||||||
DarkMode(carContext, baseStyle)
|
|
||||||
MapLibre(carContext, cameraState, baseStyle, route, viewStyle, speedCameras)
|
|
||||||
ShowPosition(cameraState, position, paddingValues)
|
ShowPosition(cameraState, position, paddingValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,11 +210,12 @@ class SurfaceRenderer(
|
|||||||
val cameraDuration =
|
val cameraDuration =
|
||||||
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
|
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
|
||||||
val currentSpeed: Float? by speed.observeAsState()
|
val currentSpeed: Float? by speed.observeAsState()
|
||||||
if (viewStyle == ViewStyle.VIEW) {
|
val maxSpeed: Int? by maxSpeed.observeAsState()
|
||||||
|
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
||||||
DrawNavigationImages(
|
DrawNavigationImages(
|
||||||
paddingValues,
|
paddingValues,
|
||||||
currentSpeed,
|
currentSpeed,
|
||||||
routeModel.routeState.maxSpeed,
|
maxSpeed!!,
|
||||||
width,
|
width,
|
||||||
height
|
height
|
||||||
)
|
)
|
||||||
@@ -217,6 +236,7 @@ class SurfaceRenderer(
|
|||||||
|
|
||||||
override fun onCreate(owner: LifecycleOwner) {
|
override fun onCreate(owner: LifecycleOwner) {
|
||||||
CarConnection(carContext).type.observe(owner, ::onConnectionStateUpdated)
|
CarConnection(carContext).type.observe(owner, ::onConnectionStateUpdated)
|
||||||
|
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)
|
||||||
@@ -245,11 +265,14 @@ class SurfaceRenderer(
|
|||||||
fun updateLocation(location: Location) {
|
fun updateLocation(location: Location) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
||||||
val bearing = bearing(
|
val bearing = if (carOrientation == 999F)
|
||||||
|
bearing(
|
||||||
lastLocation,
|
lastLocation,
|
||||||
location,
|
location,
|
||||||
cameraPosition.value!!.bearing
|
cameraPosition.value!!.bearing
|
||||||
)
|
) 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 {
|
||||||
@@ -262,57 +285,39 @@ class SurfaceRenderer(
|
|||||||
)
|
)
|
||||||
lastBearing = cameraPosition.value!!.bearing
|
lastBearing = cameraPosition.value!!.bearing
|
||||||
lastLocation = location
|
lastLocation = location
|
||||||
speed.value = location.speed
|
|
||||||
if (!countDownTimerActive) {
|
|
||||||
countDownTimerActive = true
|
|
||||||
val mainThreadHandler = Handler(carContext.mainLooper)
|
|
||||||
val lastLocationTimer = lastLocation
|
|
||||||
checkUpdate(mainThreadHandler, lastLocationTimer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkUpdate(
|
private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) {
|
||||||
mainThreadHandler: Handler,
|
synchronized(this) {
|
||||||
lastLocationTimer: Location
|
cameraPosition.postValue(
|
||||||
) {
|
cameraPosition.value!!.copy(
|
||||||
mainThreadHandler.post {
|
bearing = bearing,
|
||||||
object : CountDownTimer(3000, 1000) {
|
zoom = zoom,
|
||||||
override fun onTick(millisUntilFinished: Long) {}
|
tilt = tilt,
|
||||||
override fun onFinish() {
|
padding = getPaddingValues(height, viewStyle),
|
||||||
countDownTimerActive = false
|
target = target
|
||||||
if (lastLocation.time - lastLocationTimer.time < 1500) {
|
)
|
||||||
speed.postValue(0F)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) {
|
fun setRouteData() {
|
||||||
cameraPosition.postValue(
|
routeData.value = routeModel.curRoute.routeGeoJson
|
||||||
cameraPosition.value!!.copy(
|
viewStyle = ViewStyle.VIEW
|
||||||
bearing = bearing,
|
|
||||||
zoom = zoom,
|
|
||||||
tilt = tilt,
|
|
||||||
padding = getPaddingValues(height, viewStyle),
|
|
||||||
target = target
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setRouteData() {
|
fun setTrafficData(traffic: Map<String, String> ) {
|
||||||
routeData.value = routeModel.route.routeGeoJson
|
trafficData.value = traffic as MutableMap<String, String>?
|
||||||
viewStyle = ViewStyle.VIEW
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPreviewRouteData(routeModel: RouteModel) {
|
fun setPreviewRouteData(routeModel: RouteModel) {
|
||||||
viewStyle = ViewStyle.PREVIEW
|
viewStyle = ViewStyle.PREVIEW
|
||||||
with(routeModel) {
|
with(routeModel) {
|
||||||
routeData.value = route.routeGeoJson
|
routeData.value = curRoute.routeGeoJson
|
||||||
centerLocation = route.centerLocation
|
centerLocation = curRoute.centerLocation
|
||||||
previewDistance = route.summary!!.distance
|
previewDistance = curRoute.summary.distance
|
||||||
}
|
}
|
||||||
updateCameraPosition(
|
updateCameraPosition(
|
||||||
0.0,
|
0.0,
|
||||||
@@ -322,13 +327,26 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setCategories(location: Location, route: String) {
|
fun setCategories(location: Location, route: String) {
|
||||||
viewStyle = ViewStyle.AMENITY_VIEW
|
synchronized(this) {
|
||||||
routeData.value = route
|
viewStyle = ViewStyle.AMENITY_VIEW
|
||||||
updateCameraPosition(
|
routeData.value = route
|
||||||
0.0,
|
updateCameraPosition(
|
||||||
12.0,
|
0.0,
|
||||||
target = Position(location.longitude, location.latitude)
|
14.0,
|
||||||
)
|
target = Position(location.longitude, location.latitude)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCarLocation(location: Location) {
|
||||||
|
val routingEngine = getIntKeyValue(carContext, ROUTING_ENGINE)
|
||||||
|
if (routingEngine == RouteEngine.OSRM.ordinal) {
|
||||||
|
updateLocation(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCarSpeed(newSpeed: Float) {
|
||||||
|
speed.value = newSpeed
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCategoryLocation(location: Location, category: String) {
|
fun setCategoryLocation(location: Location, category: String) {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package com.kouros.navigation.car.map
|
package com.kouros.navigation.car.map
|
||||||
|
|
||||||
import android.location.Location
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.location.Location
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -11,7 +10,8 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -33,17 +33,18 @@ import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
|
|||||||
import com.kouros.navigation.data.NavigationColor
|
import com.kouros.navigation.data.NavigationColor
|
||||||
import com.kouros.navigation.data.RouteColor
|
import com.kouros.navigation.data.RouteColor
|
||||||
import com.kouros.navigation.data.SpeedColor
|
import com.kouros.navigation.data.SpeedColor
|
||||||
|
import com.kouros.navigation.model.RouteModel
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
|
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
|
||||||
import com.kouros.navigation.utils.location
|
|
||||||
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.camera.rememberCameraState
|
import org.maplibre.compose.camera.rememberCameraState
|
||||||
|
import org.maplibre.compose.expressions.ast.Expression
|
||||||
import org.maplibre.compose.expressions.dsl.const
|
import org.maplibre.compose.expressions.dsl.const
|
||||||
import org.maplibre.compose.expressions.dsl.exponential
|
import org.maplibre.compose.expressions.dsl.exponential
|
||||||
import org.maplibre.compose.expressions.dsl.image
|
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.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
|
||||||
@@ -87,8 +88,9 @@ fun cameraState(
|
|||||||
fun MapLibre(
|
fun MapLibre(
|
||||||
context: Context,
|
context: Context,
|
||||||
cameraState: CameraState,
|
cameraState: CameraState,
|
||||||
baseStyle: MutableState<BaseStyle.Uri>,
|
baseStyle: BaseStyle.Json,
|
||||||
route: String?,
|
route: String?,
|
||||||
|
traffic: Map<String, String>?,
|
||||||
viewStyle: ViewStyle,
|
viewStyle: ViewStyle,
|
||||||
speedCameras: String? = ""
|
speedCameras: String? = ""
|
||||||
) {
|
) {
|
||||||
@@ -98,7 +100,7 @@ fun MapLibre(
|
|||||||
OrnamentOptions(isScaleBarEnabled = false)
|
OrnamentOptions(isScaleBarEnabled = false)
|
||||||
),
|
),
|
||||||
cameraState = cameraState,
|
cameraState = cameraState,
|
||||||
baseStyle = baseStyle.value
|
baseStyle = baseStyle
|
||||||
) {
|
) {
|
||||||
getBaseSource(id = "openmaptiles")?.let { tiles ->
|
getBaseSource(id = "openmaptiles")?.let { tiles ->
|
||||||
if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) {
|
if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) {
|
||||||
@@ -107,7 +109,8 @@ fun MapLibre(
|
|||||||
if (viewStyle == ViewStyle.AMENITY_VIEW) {
|
if (viewStyle == ViewStyle.AMENITY_VIEW) {
|
||||||
AmenityLayer(route)
|
AmenityLayer(route)
|
||||||
} else {
|
} else {
|
||||||
RouteLayer(route)
|
RouteLayer(route, traffic!!)
|
||||||
|
//RouteLayerPoint(route )
|
||||||
}
|
}
|
||||||
SpeedCameraLayer(speedCameras)
|
SpeedCameraLayer(speedCameras)
|
||||||
}
|
}
|
||||||
@@ -115,8 +118,9 @@ fun MapLibre(
|
|||||||
//Puck(cameraState, lastLocation)
|
//Puck(cameraState, lastLocation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RouteLayer(routeData: String?) {
|
fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
|
||||||
if (routeData != null && routeData.isNotEmpty()) {
|
if (routeData != null && routeData.isNotEmpty()) {
|
||||||
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
||||||
LineLayer(
|
LineLayer(
|
||||||
@@ -148,23 +152,105 @@ fun RouteLayer(routeData: String?) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
trafficData.forEach {
|
||||||
|
val traffic = rememberGeoJsonSource(GeoJsonData.JsonString(it.value))
|
||||||
|
LineLayer(
|
||||||
|
id = "traffic-${it.key}-casing",
|
||||||
|
source = traffic,
|
||||||
|
color = const(Color.White),
|
||||||
|
width =
|
||||||
|
interpolate(
|
||||||
|
type = exponential(1.2f),
|
||||||
|
input = zoom(),
|
||||||
|
5 to const(0.4.dp),
|
||||||
|
6 to const(0.6.dp),
|
||||||
|
7 to const(1.8.dp),
|
||||||
|
20 to const(20.dp),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
LineLayer(
|
||||||
|
id = "traffic-${it.key}",
|
||||||
|
source = traffic,
|
||||||
|
color = trafficColor(it.key),
|
||||||
|
width =
|
||||||
|
interpolate(
|
||||||
|
type = exponential(1.2f),
|
||||||
|
input = zoom(),
|
||||||
|
5 to const(0.4.dp),
|
||||||
|
6 to const(0.5.dp),
|
||||||
|
7 to const(1.6.dp),
|
||||||
|
20 to const(18.dp),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RouteLayerPoint(routeData: String?) {
|
||||||
|
if (routeData != null && routeData.isNotEmpty()) {
|
||||||
|
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
||||||
|
val img = image(painterResource(R.drawable.ic_favorite_filled_white_24dp), drawAsSdf = true)
|
||||||
|
SymbolLayer(
|
||||||
|
id = "point-layer",
|
||||||
|
source = routes,
|
||||||
|
iconOpacity = const(2.0f),
|
||||||
|
iconColor = const(Color.Red),
|
||||||
|
iconImage = img,
|
||||||
|
iconSize =
|
||||||
|
interpolate(
|
||||||
|
type = exponential(1.2f),
|
||||||
|
input = zoom(),
|
||||||
|
5 to const(0.4f),
|
||||||
|
6 to const(0.6f),
|
||||||
|
7 to const(0.8f),
|
||||||
|
20 to const(1.0f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trafficColor(key: String): Expression<ColorValue> {
|
||||||
|
when (key) {
|
||||||
|
"queuing" -> return const(Color(0xFFD24417))
|
||||||
|
"stationary" -> return const(Color(0xFFFF0000))
|
||||||
|
"heavy" -> return const(Color(0xFF6B0404))
|
||||||
|
"slow" -> return const(Color(0xFFC41F1F))
|
||||||
|
"roadworks" -> return const(Color(0xFF7A631A))
|
||||||
|
}
|
||||||
|
return const(Color.Blue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AmenityLayer(routeData: String?) {
|
fun AmenityLayer(routeData: String?) {
|
||||||
if (routeData != null && routeData.isNotEmpty()) {
|
if (routeData != null && routeData.isNotEmpty()) {
|
||||||
val color = if (routeData.contains(Constants.PHARMACY)) {
|
var color = const(Color.Red)
|
||||||
const(Color.Red)
|
var img = image(painterResource(R.drawable.local_pharmacy_48px), drawAsSdf = true)
|
||||||
} else {
|
if (routeData.contains(Constants.CHARGING_STATION)) {
|
||||||
const(Color.Green)
|
color = const(Color.Green)
|
||||||
|
img = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true)
|
||||||
|
} else if (routeData.contains(Constants.FUEL_STATION)) {
|
||||||
|
color = const(Color.Black)
|
||||||
|
img = image(painterResource(R.drawable.local_gas_station_48px), drawAsSdf = true)
|
||||||
}
|
}
|
||||||
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
||||||
SymbolLayer(
|
SymbolLayer(
|
||||||
id = "amenity-layer",
|
id = "amenity-layer",
|
||||||
source = routes,
|
source = routes,
|
||||||
iconImage = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true),
|
iconImage = img,
|
||||||
iconColor = color,
|
iconColor = color,
|
||||||
iconSize = const(3.0f),
|
iconOpacity = const(2.0f),
|
||||||
|
iconSize =
|
||||||
|
interpolate(
|
||||||
|
type = exponential(1.2f),
|
||||||
|
input = zoom(),
|
||||||
|
5 to const(0.7f),
|
||||||
|
6 to const(1.0f),
|
||||||
|
7 to const(2.0f),
|
||||||
|
20 to const(4f),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,25 +258,26 @@ fun AmenityLayer(routeData: String?) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun SpeedCameraLayer(speedCameras: String?) {
|
fun SpeedCameraLayer(speedCameras: String?) {
|
||||||
if (speedCameras != null && speedCameras.isNotEmpty()) {
|
if (speedCameras != null && speedCameras.isNotEmpty()) {
|
||||||
val color = const(Color.DarkGray)
|
val color = const(Color.Red)
|
||||||
val cameraSource = rememberGeoJsonSource(GeoJsonData.JsonString(speedCameras))
|
val cameraSource = rememberGeoJsonSource(GeoJsonData.JsonString(speedCameras))
|
||||||
SymbolLayer(
|
SymbolLayer(
|
||||||
id = "speed-camera-layer",
|
id = "speed-camera-layer",
|
||||||
source = cameraSource,
|
source = cameraSource,
|
||||||
iconImage = image(painterResource(R.drawable.speed_camera_48px), drawAsSdf = true),
|
iconImage = image(painterResource(R.drawable.speed_camera_24px), drawAsSdf = true),
|
||||||
iconColor = color,
|
iconColor = color,
|
||||||
iconSize =
|
iconSize =
|
||||||
interpolate(
|
interpolate(
|
||||||
type = exponential(1.2f),
|
type = exponential(1.2f),
|
||||||
input = zoom(),
|
input = zoom(),
|
||||||
5 to const(0.4f),
|
5 to const(0.7f),
|
||||||
6 to const(0.7f),
|
6 to const(1.0f),
|
||||||
7 to const(1.75f),
|
7 to const(2.0f),
|
||||||
20 to const(3f),
|
20 to const(4f),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BuildingLayer(tiles: Source) {
|
fun BuildingLayer(tiles: Source) {
|
||||||
Anchor.Replace("building-3d") {
|
Anchor.Replace("building-3d") {
|
||||||
@@ -218,6 +305,7 @@ fun DrawNavigationImages(
|
|||||||
if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) {
|
if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) {
|
||||||
MaxSpeed(width, height, maxSpeed)
|
MaxSpeed(width, height, maxSpeed)
|
||||||
}
|
}
|
||||||
|
//DebugInfo(width, height, routeModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -251,7 +339,7 @@ private fun CurrentSpeed(
|
|||||||
curSpeed: Float,
|
curSpeed: Float,
|
||||||
maxSpeed: Int
|
maxSpeed: Int
|
||||||
) {
|
) {
|
||||||
val radius = 32
|
val radius = 34
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(
|
.padding(
|
||||||
@@ -317,7 +405,7 @@ private fun MaxSpeed(
|
|||||||
height: Int,
|
height: Int,
|
||||||
maxSpeed: Int,
|
maxSpeed: Int,
|
||||||
) {
|
) {
|
||||||
val radius = 20
|
val radius = 24
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(
|
.padding(
|
||||||
@@ -367,25 +455,59 @@ private fun MaxSpeed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DarkMode(context: Context, baseStyle: MutableState<BaseStyle.Uri>) {
|
fun DebugInfo(
|
||||||
val darkMode = getIntKeyValue(context, Constants.DARK_MODE_SETTINGS)
|
width: Int,
|
||||||
if (darkMode == 0) {
|
height: Int,
|
||||||
baseStyle.value = BaseStyle.Uri(Constants.STYLE)
|
routeModel: RouteModel,
|
||||||
|
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
start = 20.dp,
|
||||||
|
top = 0.dp
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
val textMeasurerLocation = rememberTextMeasurer()
|
||||||
|
val location = routeModel.navState.currentLocation.latitude.toString()
|
||||||
|
val styleSpeed = TextStyle(
|
||||||
|
fontSize = 26.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.Black,
|
||||||
|
)
|
||||||
|
val textLayoutLocation = remember(location) {
|
||||||
|
textMeasurerLocation.measure(location, styleSpeed)
|
||||||
|
}
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
drawText(
|
||||||
|
textMeasurer = textMeasurerLocation,
|
||||||
|
text = location,
|
||||||
|
style = styleSpeed,
|
||||||
|
topLeft = Offset(
|
||||||
|
x = center.x - textLayoutLocation.size.width / 2,
|
||||||
|
y = center.y - textLayoutLocation.size.height / 2,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (darkMode == 1) {
|
}
|
||||||
baseStyle.value = BaseStyle.Uri(Constants.STYLE_DARK)
|
|
||||||
}
|
@Composable
|
||||||
if (darkMode == 2) {
|
fun rememberBaseStyle(baseStyle: BaseStyle.Json): BaseStyle.Json {
|
||||||
baseStyle.value =
|
val rememberBaseStyle by remember() {
|
||||||
(if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri(
|
mutableStateOf(baseStyle)
|
||||||
Constants.STYLE
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
return rememberBaseStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
|
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
|
||||||
return when (viewStyle) {
|
return when (viewStyle) {
|
||||||
ViewStyle.VIEW -> PaddingValues(start = 50.dp, top = distanceFromTop(height).dp)
|
ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues(
|
||||||
|
start = 50.dp,
|
||||||
|
top = distanceFromTop(height).dp
|
||||||
|
)
|
||||||
|
|
||||||
ViewStyle.PREVIEW -> PaddingValues(start = 150.dp, bottom = 0.dp)
|
ViewStyle.PREVIEW -> PaddingValues(start = 150.dp, bottom = 0.dp)
|
||||||
else -> PaddingValues(start = 250.dp, bottom = 0.dp)
|
else -> PaddingValues(start = 250.dp, bottom = 0.dp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kouros.navigation.car.navigation
|
package com.kouros.navigation.car.navigation
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
@@ -29,12 +30,20 @@ import androidx.car.app.model.CarIcon
|
|||||||
import androidx.car.app.model.CarText
|
import androidx.car.app.model.CarText
|
||||||
import androidx.car.app.model.DateTimeWithZone
|
import androidx.car.app.model.DateTimeWithZone
|
||||||
import androidx.car.app.model.Distance
|
import androidx.car.app.model.Distance
|
||||||
|
import androidx.car.app.navigation.model.Lane
|
||||||
|
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.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.model.RouteModel
|
import com.kouros.navigation.model.RouteModel
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@@ -46,46 +55,61 @@ class RouteCarModel() : RouteModel() {
|
|||||||
val stepData = currentStep()
|
val stepData = currentStep()
|
||||||
val currentStepCueWithImage: SpannableString =
|
val currentStepCueWithImage: SpannableString =
|
||||||
createString(stepData.instruction)
|
createString(stepData.instruction)
|
||||||
|
|
||||||
|
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
|
||||||
|
.setIcon(createCarIcon(carContext, stepData.icon))
|
||||||
|
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|
||||||
|
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
|
||||||
|
) {
|
||||||
|
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
|
||||||
|
}
|
||||||
val step =
|
val step =
|
||||||
Step.Builder(currentStepCueWithImage)
|
Step.Builder(currentStepCueWithImage)
|
||||||
.setManeuver(
|
.setManeuver(
|
||||||
Maneuver.Builder(stepData.maneuverType)
|
maneuver.build()
|
||||||
.setIcon(createCarIcon(carContext, stepData.icon))
|
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
.setRoad(routeState.destination.street!!)
|
if (navState.destination.street != null) {
|
||||||
.build()
|
step.setRoad(navState.destination.street!!)
|
||||||
return step
|
}
|
||||||
|
if (stepData.lane.isNotEmpty()) {
|
||||||
|
addLanes(carContext, step, stepData)
|
||||||
|
}
|
||||||
|
return step.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the next [Step] with information such as the cue text and images. */
|
/** Returns the next [Step] with information such as the cue text and images. */
|
||||||
fun nextStep(carContext: CarContext): Step? {
|
fun nextStep(carContext: CarContext): Step {
|
||||||
val stepData = nextStep()
|
val stepData = nextStep()
|
||||||
val currentStepCueWithImage: SpannableString =
|
val currentStepCueWithImage: SpannableString =
|
||||||
createString(stepData.instruction)
|
createString(stepData.instruction)
|
||||||
|
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
|
||||||
|
.setIcon(createCarIcon(carContext, stepData.icon))
|
||||||
|
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|
||||||
|
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
|
||||||
|
) {
|
||||||
|
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
|
||||||
|
}
|
||||||
val step =
|
val step =
|
||||||
Step.Builder(currentStepCueWithImage)
|
Step.Builder(currentStepCueWithImage)
|
||||||
.setManeuver(
|
.setManeuver(
|
||||||
Maneuver.Builder(stepData.maneuverType)
|
maneuver.build()
|
||||||
.setIcon(createCarIcon(carContext, stepData.icon))
|
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
return step
|
return step
|
||||||
}
|
}
|
||||||
|
|
||||||
fun travelEstimate(carContext: CarContext): TravelEstimate {
|
fun travelEstimate(carContext: CarContext): TravelEstimate {
|
||||||
val timeLeft = travelLeftTime()
|
val timeLeft = routeCalculator.travelLeftTime()
|
||||||
val timeToDestinationMillis =
|
val timeToDestinationMillis =
|
||||||
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
|
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
|
||||||
val leftDistance = travelLeftDistance()
|
val leftDistance = routeCalculator.travelLeftDistance() / 1000
|
||||||
val displayUnit = if (leftDistance > 1.0) {
|
val displayUnit = if (leftDistance > 1.0) {
|
||||||
Distance.UNIT_KILOMETERS
|
Distance.UNIT_KILOMETERS
|
||||||
} else {
|
} else {
|
||||||
Distance.UNIT_METERS
|
Distance.UNIT_METERS
|
||||||
}
|
}
|
||||||
val arivalTime = DateTimeWithZone.create(
|
val arrivalTime = DateTimeWithZone.create(
|
||||||
arrivalTime(),
|
routeCalculator.arrivalTime(),
|
||||||
TimeZone.getTimeZone("Europe/Berlin")
|
TimeZone.getTimeZone("Europe/Berlin")
|
||||||
)
|
)
|
||||||
val travelBuilder = TravelEstimate.Builder( // The estimated distance to the destination.
|
val travelBuilder = TravelEstimate.Builder( // The estimated distance to the destination.
|
||||||
@@ -93,23 +117,52 @@ class RouteCarModel() : RouteModel() {
|
|||||||
leftDistance,
|
leftDistance,
|
||||||
displayUnit
|
displayUnit
|
||||||
), // Arrival time at the destination with the destination time zone.
|
), // Arrival time at the destination with the destination time zone.
|
||||||
arivalTime
|
arrivalTime
|
||||||
)
|
)
|
||||||
.setRemainingTimeSeconds(
|
.setRemainingTimeSeconds(
|
||||||
TimeUnit.MILLISECONDS.toSeconds(
|
TimeUnit.MILLISECONDS.toSeconds(
|
||||||
timeToDestinationMillis
|
timeToDestinationMillis
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.setRemainingTimeColor(CarColor.YELLOW)
|
.setRemainingTimeColor(CarColor.GREEN)
|
||||||
.setRemainingDistanceColor(CarColor.RED)
|
.setRemainingDistanceColor(CarColor.BLUE)
|
||||||
|
|
||||||
if (routeState.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(routeState.travelMessage))
|
travelBuilder.setTripText(CarText.create(navState.travelMessage))
|
||||||
}
|
}
|
||||||
return travelBuilder.build()
|
return travelBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addLanes(carContext: CarContext, step: Step.Builder, stepData: StepData) {
|
||||||
|
var laneImageAdded = false
|
||||||
|
stepData.lane.forEach {
|
||||||
|
if (it.indications.isNotEmpty() && it.valid) {
|
||||||
|
Collections.sort<String>(it.indications)
|
||||||
|
var direction = ""
|
||||||
|
it.indications.forEach { it2 ->
|
||||||
|
direction = if (direction.isEmpty()) {
|
||||||
|
it2.trim()
|
||||||
|
} else {
|
||||||
|
"${direction}_${it2.trim()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val laneDirection = navState.iconMapper.addLanes(direction, stepData)
|
||||||
|
if (laneDirection != LaneDirection.SHAPE_UNKNOWN) {
|
||||||
|
if (!laneImageAdded) {
|
||||||
|
step.setLanesImage(createCarIcon(navState.iconMapper.createLaneIcon(carContext, stepData)))
|
||||||
|
laneImageAdded = true
|
||||||
|
}
|
||||||
|
val laneType =
|
||||||
|
Lane.Builder()
|
||||||
|
.addDirection(LaneDirection.create(laneDirection, false))
|
||||||
|
.build()
|
||||||
|
step.addLane(laneType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createString(
|
fun createString(
|
||||||
text: String
|
text: String
|
||||||
): SpannableString {
|
): SpannableString {
|
||||||
@@ -125,23 +178,35 @@ class RouteCarModel() : RouteModel() {
|
|||||||
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
|
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String?) {
|
fun createCarIcon(iconCompat: IconCompat): CarIcon {
|
||||||
carContext.getCarService<AppManager?>(AppManager::class.java)
|
return CarIcon.Builder(iconCompat).build()
|
||||||
.showAlert(createAlert(carContext, distance, maxSpeed))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createAlert(carContext: CarContext, distance: Double, maxSpeed: String?): Alert {
|
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String) {
|
||||||
|
carContext.getCarService<AppManager?>(AppManager::class.java)
|
||||||
|
.showAlert(
|
||||||
|
createAlert(
|
||||||
|
carContext,
|
||||||
|
maxSpeed,
|
||||||
|
createCarIcon(carContext, R.drawable.speed_camera_24px)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createAlert(
|
||||||
|
carContext: CarContext,
|
||||||
|
maxSpeed: String?,
|
||||||
|
icon: CarIcon
|
||||||
|
): Alert {
|
||||||
val title = createCarText(carContext, R.string.speed_camera)
|
val title = createCarText(carContext, R.string.speed_camera)
|
||||||
val subtitle = CarText.create(maxSpeed!!)
|
val subtitle = CarText.create(maxSpeed!!)
|
||||||
val icon = CarIcon.ALERT
|
|
||||||
|
|
||||||
val dismissAction: Action = createToastAction(
|
val dismissAction: Action = createToastAction(
|
||||||
carContext,
|
carContext,
|
||||||
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: */10000)
|
|
||||||
.setSubtitle(subtitle)
|
.setSubtitle(subtitle)
|
||||||
.setIcon(icon)
|
.setIcon(icon)
|
||||||
.addAction(dismissAction).setCallback(object : AlertCallback {
|
.addAction(dismissAction).setCallback(object : AlertCallback {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.core.graphics.drawable.IconCompat
|
|||||||
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.ViewStyle
|
import com.kouros.navigation.car.ViewStyle
|
||||||
|
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||||
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
|
||||||
@@ -23,8 +24,7 @@ import com.kouros.navigation.model.ViewModel
|
|||||||
class CategoriesScreen(
|
class CategoriesScreen(
|
||||||
private val carContext: CarContext,
|
private val carContext: CarContext,
|
||||||
private val surfaceRenderer: SurfaceRenderer,
|
private val surfaceRenderer: SurfaceRenderer,
|
||||||
private val location: Location,
|
private val viewModel: ViewModel,
|
||||||
private val viewModel: ViewModel
|
|
||||||
) : Screen(carContext) {
|
) : Screen(carContext) {
|
||||||
|
|
||||||
var categories: List<Category> = listOf(
|
var categories: List<Category> = listOf(
|
||||||
@@ -47,7 +47,6 @@ class CategoriesScreen(
|
|||||||
CategoryScreen(
|
CategoryScreen(
|
||||||
carContext,
|
carContext,
|
||||||
surfaceRenderer,
|
surfaceRenderer,
|
||||||
location,
|
|
||||||
it.id,
|
it.id,
|
||||||
viewModel
|
viewModel
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ import androidx.lifecycle.Observer
|
|||||||
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.NavigationMessage
|
import com.kouros.navigation.car.navigation.NavigationMessage
|
||||||
|
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
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.ViewModel
|
import com.kouros.navigation.model.ViewModel
|
||||||
import com.kouros.navigation.utils.GeoUtils.createPointCollection
|
import com.kouros.navigation.utils.GeoUtils.createPointCollection
|
||||||
@@ -31,9 +32,8 @@ import kotlin.math.min
|
|||||||
class CategoryScreen(
|
class CategoryScreen(
|
||||||
private val carContext: CarContext,
|
private val carContext: CarContext,
|
||||||
private val surfaceRenderer: SurfaceRenderer,
|
private val surfaceRenderer: SurfaceRenderer,
|
||||||
location: Location,
|
|
||||||
private val category: String,
|
private val category: String,
|
||||||
private val viewModel: ViewModel
|
private val viewModel: ViewModel,
|
||||||
) : Screen(carContext) {
|
) : Screen(carContext) {
|
||||||
|
|
||||||
var elements = listOf<Elements>()
|
var elements = listOf<Elements>()
|
||||||
@@ -44,10 +44,10 @@ class CategoryScreen(
|
|||||||
val loc = location(0.0, 0.0)
|
val loc = location(0.0, 0.0)
|
||||||
elements.forEach {
|
elements.forEach {
|
||||||
if (loc.latitude == 0.0) {
|
if (loc.latitude == 0.0) {
|
||||||
loc.longitude = it.lon!!
|
loc.longitude = it.lon
|
||||||
loc.latitude = it.lat!!
|
loc.latitude = it.lat
|
||||||
}
|
}
|
||||||
coordinates.add(listOf(it.lon!!, it.lat!!))
|
coordinates.add(listOf(it.lon, it.lat))
|
||||||
}
|
}
|
||||||
if (elements.isNotEmpty()) {
|
if (elements.isNotEmpty()) {
|
||||||
val route = createPointCollection(coordinates, category)
|
val route = createPointCollection(coordinates, category)
|
||||||
@@ -58,7 +58,7 @@ class CategoryScreen(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
viewModel.elements.observe(this, observer)
|
viewModel.elements.observe(this, observer)
|
||||||
viewModel.getAmenities(category, location)
|
viewModel.getAmenities(category, surfaceRenderer.lastLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ class CategoryScreen(
|
|||||||
}
|
}
|
||||||
val row = Row.Builder()
|
val row = Row.Builder()
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
val location = location(it.lon!!, it.lat!!)
|
val location = location(it.lon, it.lat)
|
||||||
surfaceRenderer.setCategoryLocation(location, category)
|
surfaceRenderer.setCategoryLocation(location, category)
|
||||||
}
|
}
|
||||||
.setTitle(name)
|
.setTitle(name)
|
||||||
@@ -126,6 +126,28 @@ class CategoryScreen(
|
|||||||
} else {
|
} else {
|
||||||
row.addText(carText("${it.tags.openingHours}"))
|
row.addText(carText("${it.tags.openingHours}"))
|
||||||
}
|
}
|
||||||
|
val navigationMessage = NavigationMessage(carContext)
|
||||||
|
row.addAction(
|
||||||
|
Action.Builder()
|
||||||
|
.setOnClickListener {
|
||||||
|
viewModel.loadRoute(
|
||||||
|
carContext,
|
||||||
|
currentLocation = surfaceRenderer.lastLocation,
|
||||||
|
location(it.lon!!, it.lat!!),
|
||||||
|
surfaceRenderer.carOrientation
|
||||||
|
)
|
||||||
|
setResult(
|
||||||
|
Place(
|
||||||
|
name = name,
|
||||||
|
category = Constants.CHARGING_STATION,
|
||||||
|
latitude = it.lat!!,
|
||||||
|
longitude = it.lon!!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
.setIcon(navigationMessage.createCarIcon(R.drawable.navigation_48px))
|
||||||
|
.build())
|
||||||
return row.build()
|
return row.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import androidx.car.app.model.Action.FLAG_DEFAULT
|
|||||||
import androidx.car.app.model.ActionStrip
|
import androidx.car.app.model.ActionStrip
|
||||||
import androidx.car.app.model.CarColor
|
import androidx.car.app.model.CarColor
|
||||||
import androidx.car.app.model.CarIcon
|
import androidx.car.app.model.CarIcon
|
||||||
import androidx.car.app.model.CarText
|
|
||||||
import androidx.car.app.model.Distance
|
import androidx.car.app.model.Distance
|
||||||
import androidx.car.app.model.Header
|
import androidx.car.app.model.Header
|
||||||
import androidx.car.app.model.MessageTemplate
|
import androidx.car.app.model.MessageTemplate
|
||||||
@@ -29,14 +28,14 @@ import com.kouros.navigation.car.ViewStyle
|
|||||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.nominatim.SearchResult
|
import com.kouros.navigation.data.nominatim.SearchResult
|
||||||
import com.kouros.navigation.data.overpass.Elements
|
import com.kouros.navigation.data.overpass.Elements
|
||||||
import com.kouros.navigation.model.ViewModel
|
import com.kouros.navigation.model.ViewModel
|
||||||
import com.kouros.navigation.utils.GeoUtils
|
import com.kouros.navigation.utils.GeoUtils
|
||||||
import com.kouros.navigation.utils.bearing
|
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
class NavigationScreen(
|
class NavigationScreen(
|
||||||
@@ -58,6 +57,7 @@ class NavigationScreen(
|
|||||||
var recentPlace = Place()
|
var recentPlace = Place()
|
||||||
var navigationType = NavigationType.VIEW
|
var navigationType = NavigationType.VIEW
|
||||||
|
|
||||||
|
var lastTrafficDate = LocalDateTime.of(1960, 6, 21, 0, 0)
|
||||||
val observer = Observer<String> { route ->
|
val observer = Observer<String> { route ->
|
||||||
if (route.isNotEmpty()) {
|
if (route.isNotEmpty()) {
|
||||||
navigationType = NavigationType.NAVIGATION
|
navigationType = NavigationType.NAVIGATION
|
||||||
@@ -74,6 +74,10 @@ class NavigationScreen(
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val trafficObserver = Observer<Map<String, String> > { traffic ->
|
||||||
|
surfaceRenderer.setTrafficData(traffic)
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
val placeObserver = Observer<SearchResult> { searchResult ->
|
val placeObserver = Observer<SearchResult> { searchResult ->
|
||||||
val place = Place(
|
val place = Place(
|
||||||
@@ -93,22 +97,19 @@ class NavigationScreen(
|
|||||||
var speedCameras = listOf<Elements>()
|
var speedCameras = listOf<Elements>()
|
||||||
val speedObserver = Observer<List<Elements>> { cameras ->
|
val speedObserver = Observer<List<Elements>> { cameras ->
|
||||||
speedCameras = cameras
|
speedCameras = cameras
|
||||||
|
|
||||||
val coordinates = mutableListOf<List<Double>>()
|
val coordinates = mutableListOf<List<Double>>()
|
||||||
val loc = location(0.0, 0.0)
|
|
||||||
cameras.forEach {
|
cameras.forEach {
|
||||||
val loc =
|
|
||||||
location(longitude = it.lon!!, latitude = it.lat!!)
|
|
||||||
coordinates.add(listOf(it.lon!!, it.lat!!))
|
coordinates.add(listOf(it.lon!!, it.lat!!))
|
||||||
}
|
}
|
||||||
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
|
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
|
||||||
surfaceRenderer.speedCamerasData.value =speedData
|
surfaceRenderer.speedCamerasData.value = speedData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModel.route.observe(this, observer)
|
viewModel.route.observe(this, observer)
|
||||||
|
viewModel.traffic.observe(this, trafficObserver);
|
||||||
viewModel.recentPlace.observe(this, recentObserver)
|
viewModel.recentPlace.observe(this, recentObserver)
|
||||||
viewModel.loadRecentPlace(location = surfaceRenderer.lastLocation)
|
|
||||||
viewModel.placeLocation.observe(this, placeObserver)
|
viewModel.placeLocation.observe(this, placeObserver)
|
||||||
viewModel.speedCameras.observe(this, speedObserver)
|
viewModel.speedCameras.observe(this, speedObserver)
|
||||||
}
|
}
|
||||||
@@ -149,11 +150,11 @@ class NavigationScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
|
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
|
||||||
if (routeModel.routeState.arrived) {
|
if (routeModel.navState.arrived) {
|
||||||
val timer = object : CountDownTimer(8000, 1000) {
|
val timer = object : CountDownTimer(8000, 1000) {
|
||||||
override fun onTick(millisUntilFinished: Long) {}
|
override fun onTick(millisUntilFinished: Long) {}
|
||||||
override fun onFinish() {
|
override fun onFinish() {
|
||||||
routeModel.routeState = routeModel.routeState.copy(arrived = false)
|
routeModel.navState = routeModel.navState.copy(arrived = false)
|
||||||
navigationType = NavigationType.VIEW
|
navigationType = NavigationType.VIEW
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
@@ -172,8 +173,8 @@ class NavigationScreen(
|
|||||||
|
|
||||||
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
|
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
|
||||||
var street = ""
|
var street = ""
|
||||||
if (routeModel.routeState.destination.street != null) {
|
if (routeModel.navState.destination.street != null) {
|
||||||
street = routeModel.routeState.destination.street!!
|
street = routeModel.navState.destination.street!!
|
||||||
}
|
}
|
||||||
return NavigationTemplate.Builder()
|
return NavigationTemplate.Builder()
|
||||||
.setNavigationInfo(
|
.setNavigationInfo(
|
||||||
@@ -232,7 +233,7 @@ class NavigationScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getRoutingInfo(): RoutingInfo {
|
fun getRoutingInfo(): RoutingInfo {
|
||||||
var currentDistance = routeModel.leftStepDistance()
|
var currentDistance = routeModel.routeCalculator.leftStepDistance()
|
||||||
val displayUnit = if (currentDistance > 1000.0) {
|
val displayUnit = if (currentDistance > 1000.0) {
|
||||||
currentDistance /= 1000.0
|
currentDistance /= 1000.0
|
||||||
Distance.UNIT_KILOMETERS
|
Distance.UNIT_KILOMETERS
|
||||||
@@ -240,22 +241,13 @@ class NavigationScreen(
|
|||||||
Distance.UNIT_METERS
|
Distance.UNIT_METERS
|
||||||
}
|
}
|
||||||
val nextStep = routeModel.nextStep(carContext = carContext)
|
val nextStep = routeModel.nextStep(carContext = carContext)
|
||||||
if (nextStep != null) {
|
return RoutingInfo.Builder()
|
||||||
return RoutingInfo.Builder()
|
.setCurrentStep(
|
||||||
.setCurrentStep(
|
routeModel.currentStep(carContext = carContext),
|
||||||
routeModel.currentStep(carContext = carContext),
|
Distance.create(currentDistance, displayUnit)
|
||||||
Distance.create(currentDistance, displayUnit)
|
)
|
||||||
)
|
.setNextStep(nextStep)
|
||||||
.setNextStep(nextStep)
|
.build()
|
||||||
.build()
|
|
||||||
} else {
|
|
||||||
return RoutingInfo.Builder()
|
|
||||||
.setCurrentStep(
|
|
||||||
routeModel.currentStep(carContext = carContext),
|
|
||||||
Distance.create(currentDistance, displayUnit)
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createActionStripBuilder(): ActionStrip.Builder {
|
private fun createActionStripBuilder(): ActionStrip.Builder {
|
||||||
@@ -314,8 +306,13 @@ class NavigationScreen(
|
|||||||
)
|
)
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
|
val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
|
||||||
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, navigateTo)
|
viewModel.loadRoute(
|
||||||
routeModel.routeState = routeModel.routeState.copy(destination = recentPlace)
|
carContext,
|
||||||
|
surfaceRenderer.lastLocation,
|
||||||
|
navigateTo,
|
||||||
|
surfaceRenderer.carOrientation
|
||||||
|
)
|
||||||
|
routeModel.navState = routeModel.navState.copy(destination = recentPlace)
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@@ -352,7 +349,7 @@ class NavigationScreen(
|
|||||||
return Action.Builder()
|
return Action.Builder()
|
||||||
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
|
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
screenManager.push(SettingsScreen(carContext))
|
screenManager.push(SettingsScreen(carContext, viewModel))
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@@ -369,6 +366,7 @@ class NavigationScreen(
|
|||||||
.build()
|
.build()
|
||||||
).setOnClickListener {
|
).setOnClickListener {
|
||||||
surfaceRenderer.handleScale(1)
|
surfaceRenderer.handleScale(1)
|
||||||
|
invalidate()
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@@ -385,6 +383,7 @@ class NavigationScreen(
|
|||||||
.build()
|
.build()
|
||||||
).setOnClickListener {
|
).setOnClickListener {
|
||||||
surfaceRenderer.handleScale(-1)
|
surfaceRenderer.handleScale(-1)
|
||||||
|
invalidate()
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@@ -401,6 +400,7 @@ class NavigationScreen(
|
|||||||
.build()
|
.build()
|
||||||
).setOnClickListener {
|
).setOnClickListener {
|
||||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||||
|
invalidate()
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@@ -408,7 +408,11 @@ class NavigationScreen(
|
|||||||
private fun startSearchScreen() {
|
private fun startSearchScreen() {
|
||||||
screenManager
|
screenManager
|
||||||
.pushForResult(
|
.pushForResult(
|
||||||
SearchScreen(carContext, surfaceRenderer, surfaceRenderer.lastLocation, viewModel)
|
SearchScreen(
|
||||||
|
carContext,
|
||||||
|
surfaceRenderer,
|
||||||
|
viewModel
|
||||||
|
)
|
||||||
) { obj: Any? ->
|
) { obj: Any? ->
|
||||||
if (obj != null) {
|
if (obj != null) {
|
||||||
val place = obj as Place
|
val place = obj as Place
|
||||||
@@ -430,8 +434,13 @@ class NavigationScreen(
|
|||||||
val location = location(place.longitude, place.latitude)
|
val location = location(place.longitude, place.latitude)
|
||||||
viewModel.saveRecent(place)
|
viewModel.saveRecent(place)
|
||||||
currentNavigationLocation = location
|
currentNavigationLocation = location
|
||||||
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, location)
|
viewModel.loadRoute(
|
||||||
routeModel.routeState = routeModel.routeState.copy(destination = place)
|
carContext,
|
||||||
|
surfaceRenderer.lastLocation,
|
||||||
|
location,
|
||||||
|
surfaceRenderer.carOrientation
|
||||||
|
)
|
||||||
|
routeModel.navState = routeModel.navState.copy(destination = place)
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,7 +458,7 @@ class NavigationScreen(
|
|||||||
invalidate()
|
invalidate()
|
||||||
val mainThreadHandler = Handler(carContext.mainLooper)
|
val mainThreadHandler = Handler(carContext.mainLooper)
|
||||||
mainThreadHandler.post {
|
mainThreadHandler.post {
|
||||||
object : CountDownTimer(3000, 1000) {
|
object : CountDownTimer(2000, 1000) {
|
||||||
override fun onTick(millisUntilFinished: Long) {}
|
override fun onTick(millisUntilFinished: Long) {}
|
||||||
override fun onFinish() {
|
override fun onFinish() {
|
||||||
navigationType = NavigationType.NAVIGATION
|
navigationType = NavigationType.NAVIGATION
|
||||||
@@ -461,18 +470,32 @@ class NavigationScreen(
|
|||||||
|
|
||||||
fun reRoute(destination: Place) {
|
fun reRoute(destination: Place) {
|
||||||
val dest = location(destination.longitude, destination.latitude)
|
val dest = location(destination.longitude, destination.latitude)
|
||||||
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, dest)
|
viewModel.loadRoute(
|
||||||
|
carContext,
|
||||||
|
surfaceRenderer.lastLocation,
|
||||||
|
dest,
|
||||||
|
surfaceRenderer.carOrientation
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateTrip(location: Location) {
|
fun updateTrip(location: Location) {
|
||||||
|
val current = LocalDateTime.now(ZoneOffset.UTC)
|
||||||
|
val duration = java.time.Duration.between(current, lastTrafficDate)
|
||||||
|
if (duration.abs().seconds > 360) {
|
||||||
|
lastTrafficDate = current
|
||||||
|
viewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
|
||||||
|
}
|
||||||
updateSpeedCamera(location)
|
updateSpeedCamera(location)
|
||||||
with(routeModel) {
|
with(routeModel) {
|
||||||
updateLocation(location, viewModel)
|
updateLocation(location, viewModel)
|
||||||
if (routeState.maneuverType == Maneuver.TYPE_DESTINATION
|
if ((navState.maneuverType == Maneuver.TYPE_DESTINATION
|
||||||
&& leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|
||||||
|
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|
||||||
|
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT)
|
||||||
|
&& routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
||||||
) {
|
) {
|
||||||
stopNavigation()
|
stopNavigation()
|
||||||
routeState = routeState.copy(arrived = true)
|
navState = navState.copy(arrived = true)
|
||||||
surfaceRenderer.routeData.value = ""
|
surfaceRenderer.routeData.value = ""
|
||||||
navigationType = NavigationType.ARRIVAL
|
navigationType = NavigationType.ARRIVAL
|
||||||
invalidate()
|
invalidate()
|
||||||
@@ -496,20 +519,27 @@ class NavigationScreen(
|
|||||||
val updatedCameras = mutableListOf<Elements>()
|
val updatedCameras = mutableListOf<Elements>()
|
||||||
speedCameras.forEach {
|
speedCameras.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()
|
||||||
updatedCameras.add(it)
|
updatedCameras.add(it)
|
||||||
}
|
}
|
||||||
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
|
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
|
||||||
val camera = sortedList.first()
|
val camera = sortedList.first()
|
||||||
val bearingSpeedCamera = location.bearingTo(location(camera.lon!!, camera.lat!!))
|
|
||||||
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
|
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
|
||||||
|
val bearingSpeedCamera = if (camera.tags.direction != null) {
|
||||||
if (camera.distance < 80
|
try {
|
||||||
&& (bearingSpeedCamera.absoluteValue - bearingRoute.absoluteValue).absoluteValue < 15.0
|
camera.tags.direction!!.toFloat()
|
||||||
) {
|
} catch ( e: Exception) {
|
||||||
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
|
0F
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
|
||||||
|
}
|
||||||
|
if (camera.distance < 80) {
|
||||||
|
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
|
||||||
|
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,20 +12,29 @@ import androidx.car.app.model.Toggle
|
|||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.data.Constants.AVOID_MOTORWAY
|
import com.kouros.navigation.data.Constants.AVOID_MOTORWAY
|
||||||
import com.kouros.navigation.data.Constants.AVOID_TOLLWAY
|
import com.kouros.navigation.data.Constants.AVOID_TOLLWAY
|
||||||
|
import com.kouros.navigation.data.Constants.CAR_LOCATION
|
||||||
|
import com.kouros.navigation.model.ViewModel
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
|
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
|
||||||
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
|
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
|
||||||
|
|
||||||
|
|
||||||
class NavigationSettings(private val carContext: CarContext) : Screen(carContext) {
|
class NavigationSettings(private val carContext: CarContext, private var viewModel: ViewModel) :
|
||||||
|
Screen(carContext) {
|
||||||
|
|
||||||
private var motorWayToggleState = false
|
private var motorWayToggleState = false
|
||||||
|
|
||||||
private var tollWayToggleState = false
|
private var tollWayToggleState = false
|
||||||
|
|
||||||
|
private var carLocationToggleState = false
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
motorWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
|
motorWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
|
||||||
|
|
||||||
tollWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
|
tollWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
|
||||||
|
|
||||||
|
carLocationToggleState = getBooleanKeyValue(carContext, CAR_LOCATION)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
@@ -53,6 +62,29 @@ class NavigationSettings(private val carContext: CarContext) : Screen(carContext
|
|||||||
}.setChecked(tollWayToggleState).build()
|
}.setChecked(tollWayToggleState).build()
|
||||||
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle))
|
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle))
|
||||||
|
|
||||||
|
val carLocationToggle: Toggle =
|
||||||
|
Toggle.Builder { checked: Boolean ->
|
||||||
|
if (checked) {
|
||||||
|
setBooleanKeyValue(carContext, true, CAR_LOCATION)
|
||||||
|
} else {
|
||||||
|
setBooleanKeyValue(carContext, false, CAR_LOCATION)
|
||||||
|
}
|
||||||
|
carLocationToggleState = !carLocationToggleState
|
||||||
|
}.setChecked(carLocationToggleState).build()
|
||||||
|
|
||||||
|
listBuilder.addItem(
|
||||||
|
buildRowForTemplate(
|
||||||
|
R.string.use_car_location,
|
||||||
|
carLocationToggle
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
listBuilder.addItem(
|
||||||
|
buildRowForScreenTemplate(
|
||||||
|
RoutingSettings(carContext, viewModel),
|
||||||
|
R.string.routing_engine
|
||||||
|
)
|
||||||
|
)
|
||||||
return ListTemplate.Builder()
|
return ListTemplate.Builder()
|
||||||
.setSingleList(listBuilder.build())
|
.setSingleList(listBuilder.build())
|
||||||
.setHeader(
|
.setHeader(
|
||||||
@@ -70,4 +102,12 @@ class NavigationSettings(private val carContext: CarContext) : Screen(carContext
|
|||||||
.setToggle(toggle)
|
.setToggle(toggle)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildRowForScreenTemplate(screen: Screen, title: Int): Row {
|
||||||
|
return Row.Builder()
|
||||||
|
.setTitle(carContext.getString(title))
|
||||||
|
.setOnClickListener { screenManager.push(screen) }
|
||||||
|
.setBrowsable(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,6 @@ 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.Screen
|
import androidx.car.app.Screen
|
||||||
import androidx.car.app.constraints.ConstraintManager
|
|
||||||
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.Distance
|
import androidx.car.app.model.Distance
|
||||||
@@ -22,21 +21,16 @@ import androidx.lifecycle.Observer
|
|||||||
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
|
|
||||||
import com.kouros.navigation.data.Constants.CONTACTS
|
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.Constants.categories
|
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.model.ViewModel
|
import com.kouros.navigation.model.ViewModel
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
|
|
||||||
class PlaceListScreen(
|
class PlaceListScreen(
|
||||||
private val carContext: CarContext,
|
private val carContext: CarContext,
|
||||||
private val surfaceRenderer: SurfaceRenderer,
|
private val surfaceRenderer: SurfaceRenderer,
|
||||||
private val location: Location,
|
|
||||||
private val category: String,
|
private val category: String,
|
||||||
private val viewModel: ViewModel
|
private val viewModel: ViewModel
|
||||||
) : Screen(carContext) {
|
) : Screen(carContext) {
|
||||||
@@ -68,13 +62,21 @@ class PlaceListScreen(
|
|||||||
|
|
||||||
fun loadPlaces() {
|
fun loadPlaces() {
|
||||||
if (category == RECENT) {
|
if (category == RECENT) {
|
||||||
viewModel.loadRecentPlaces(carContext, location)
|
viewModel.loadRecentPlaces(
|
||||||
|
carContext,
|
||||||
|
surfaceRenderer.lastLocation,
|
||||||
|
surfaceRenderer.carOrientation
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (category == CONTACTS) {
|
if (category == CONTACTS) {
|
||||||
viewModel.loadContacts(carContext)
|
viewModel.loadContacts(carContext)
|
||||||
}
|
}
|
||||||
if (category == FAVORITES) {
|
if (category == FAVORITES) {
|
||||||
viewModel.loadFavorites(carContext, location)
|
viewModel.loadFavorites(
|
||||||
|
carContext,
|
||||||
|
surfaceRenderer.lastLocation,
|
||||||
|
surfaceRenderer.carOrientation
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,9 +84,14 @@ class PlaceListScreen(
|
|||||||
val itemListBuilder = ItemList.Builder()
|
val itemListBuilder = ItemList.Builder()
|
||||||
.setNoItemsMessage(carContext.getString(R.string.no_places))
|
.setNoItemsMessage(carContext.getString(R.string.no_places))
|
||||||
places.forEach {
|
places.forEach {
|
||||||
|
val street = if (it.street != null) {
|
||||||
|
it.street
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
val row = Row.Builder()
|
val row = Row.Builder()
|
||||||
.setImage(contactIcon(it.avatar, it.category))
|
.setImage(contactIcon(it.avatar, it.category))
|
||||||
.setTitle("${it.street!!} ${it.city}")
|
.setTitle("$street ${it.city}")
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
val place = Place(
|
val place = Place(
|
||||||
0,
|
0,
|
||||||
@@ -117,7 +124,7 @@ class PlaceListScreen(
|
|||||||
setSpan(
|
setSpan(
|
||||||
DistanceSpan.create(
|
DistanceSpan.create(
|
||||||
Distance.create(
|
Distance.create(
|
||||||
it.distance.toDouble(),
|
(it.distance/1000).toDouble(),
|
||||||
Distance.UNIT_KILOMETERS
|
Distance.UNIT_KILOMETERS
|
||||||
)
|
)
|
||||||
), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE
|
), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ class RequestPermissionScreen(
|
|||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
val permissions: MutableList<String?> = ArrayList()
|
val permissions: MutableList<String?> = ArrayList()
|
||||||
permissions.add(permission.ACCESS_FINE_LOCATION)
|
permissions.add(permission.ACCESS_FINE_LOCATION)
|
||||||
|
permissions.add("com.google.android.gms.permission.CAR_SPEED")
|
||||||
//permissions.add(permission.READ_CONTACTS)
|
//permissions.add(permission.READ_CONTACTS)
|
||||||
|
|
||||||
val message = "This app needs access to location in order to show the map around you"
|
val message = "This app needs access to location and to car speed"
|
||||||
|
|
||||||
val listener: OnClickListener = ParkedOnlyOnClickListener.create {
|
val listener: OnClickListener = ParkedOnlyOnClickListener.create {
|
||||||
carContext.requestPermissions(
|
carContext.requestPermissions(
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ import androidx.annotation.DrawableRes
|
|||||||
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.constraints.ConstraintManager
|
||||||
import androidx.car.app.model.Action
|
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_PRIMARY
|
|
||||||
import androidx.car.app.model.ActionStrip
|
import androidx.car.app.model.ActionStrip
|
||||||
import androidx.car.app.model.CarColor
|
|
||||||
import androidx.car.app.model.CarIcon
|
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
|
||||||
@@ -18,19 +17,17 @@ 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.ListTemplate
|
||||||
import androidx.car.app.model.MessageTemplate
|
import androidx.car.app.model.MessageTemplate
|
||||||
|
import androidx.car.app.model.OnClickListener
|
||||||
import androidx.car.app.model.Row
|
import androidx.car.app.model.Row
|
||||||
import androidx.car.app.model.Template
|
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.navigation.model.NavigationTemplate
|
|
||||||
import androidx.car.app.navigation.model.RoutingInfo
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
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.NavigationMessage
|
import com.kouros.navigation.car.navigation.NavigationMessage
|
||||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.model.ViewModel
|
import com.kouros.navigation.model.ViewModel
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
@@ -61,7 +58,12 @@ class RoutePreviewScreen(
|
|||||||
init {
|
init {
|
||||||
viewModel.previewRoute.observe(this, observer)
|
viewModel.previewRoute.observe(this, observer)
|
||||||
val location = location(destination.longitude, destination.latitude)
|
val location = location(destination.longitude, destination.latitude)
|
||||||
viewModel.loadPreviewRoute(carContext, surfaceRenderer.lastLocation, location)
|
viewModel.loadPreviewRoute(
|
||||||
|
carContext,
|
||||||
|
surfaceRenderer.lastLocation,
|
||||||
|
location,
|
||||||
|
surfaceRenderer.carOrientation
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
@@ -74,13 +76,17 @@ class RoutePreviewScreen(
|
|||||||
.setFlags(FLAG_DEFAULT)
|
.setFlags(FLAG_DEFAULT)
|
||||||
.setIcon(navigateActionIcon)
|
.setIcon(navigateActionIcon)
|
||||||
.setOnClickListener { this.onNavigate() }
|
.setOnClickListener { this.onNavigate() }
|
||||||
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
val itemListBuilder = ItemList.Builder()
|
||||||
|
var i = 0
|
||||||
|
routeModel.route.routes.forEach { it ->
|
||||||
|
itemListBuilder.addItem(createRow(i++, navigateAction))
|
||||||
|
}
|
||||||
|
|
||||||
val header = Header.Builder()
|
val header = Header.Builder()
|
||||||
.setStartHeaderAction(Action.BACK)
|
.setStartHeaderAction(Action.BACK)
|
||||||
.setTitle(carContext.getString(R.string.route_preview))
|
.setTitle(carContext.getString(R.string.route_preview))
|
||||||
//.addEndHeaderAction(navigateAction)
|
|
||||||
.addEndHeaderAction(
|
.addEndHeaderAction(
|
||||||
favoriteAction()
|
favoriteAction()
|
||||||
)
|
)
|
||||||
@@ -89,30 +95,40 @@ class RoutePreviewScreen(
|
|||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val message = if (routeModel.isNavigating() && routeModel.route.waypoints!!.isNotEmpty()) {
|
val message =
|
||||||
createRouteText()
|
if (routeModel.isNavigating() && routeModel.curRoute.waypoints!!.isNotEmpty()) {
|
||||||
} else {
|
createRouteText(0)
|
||||||
CarText.Builder("Wait")
|
} else {
|
||||||
.build()
|
CarText.Builder("Wait")
|
||||||
}
|
.build()
|
||||||
val messageTemplate = MessageTemplate.Builder(
|
|
||||||
message
|
|
||||||
)
|
|
||||||
.setHeader(header)
|
|
||||||
.addAction(navigateAction)
|
|
||||||
.setLoading(message.toString() == "Wait")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val timer = object : CountDownTimer(5000, 1000) {
|
|
||||||
override fun onTick(millisUntilFinished: Long) {}
|
|
||||||
override fun onFinish() {
|
|
||||||
//onNavigate()
|
|
||||||
}
|
}
|
||||||
|
if (routeModel.route.routes.size == 1) {
|
||||||
|
val timer = object : CountDownTimer(5000, 1000) {
|
||||||
|
override fun onTick(millisUntilFinished: Long) {}
|
||||||
|
override fun onFinish() {
|
||||||
|
onNavigate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timer.start()
|
||||||
}
|
}
|
||||||
timer.start()
|
|
||||||
|
|
||||||
|
val content = if (routeModel.route.routes.size > 1) {
|
||||||
|
ListTemplate.Builder()
|
||||||
|
.setHeader(header)
|
||||||
|
.setSingleList(itemListBuilder.build())
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
MessageTemplate.Builder(
|
||||||
|
message
|
||||||
|
)
|
||||||
|
.setHeader(header)
|
||||||
|
.addAction(navigateAction)
|
||||||
|
.setLoading(message.toString() == "Wait")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
}
|
||||||
return MapWithContentTemplate.Builder()
|
return MapWithContentTemplate.Builder()
|
||||||
.setContentTemplate(messageTemplate)
|
.setContentTemplate(content)
|
||||||
.setMapController(
|
.setMapController(
|
||||||
MapController.Builder().setMapActionStrip(
|
MapController.Builder().setMapActionStrip(
|
||||||
getMapActionStrip()
|
getMapActionStrip()
|
||||||
@@ -172,9 +188,13 @@ class RoutePreviewScreen(
|
|||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private fun createRouteText(): CarText {
|
private fun createRouteText(index: Int): CarText {
|
||||||
val time = routeModel.route.summary!!.duration
|
val time = routeModel.route.routes[index].summary.duration
|
||||||
val length = BigDecimal(routeModel.route.summary!!.distance).setScale(1, RoundingMode.HALF_EVEN)
|
val length =
|
||||||
|
BigDecimal(routeModel.route.routes[index].summary.distance).setScale(
|
||||||
|
1,
|
||||||
|
RoundingMode.HALF_EVEN
|
||||||
|
)
|
||||||
val firstRoute = SpannableString(" \u00b7 $length km")
|
val firstRoute = SpannableString(" \u00b7 $length km")
|
||||||
firstRoute.setSpan(
|
firstRoute.setSpan(
|
||||||
DurationSpan.create(time.toLong()), 0, 1, 0
|
DurationSpan.create(time.toLong()), 0, 1, 0
|
||||||
@@ -183,14 +203,27 @@ class RoutePreviewScreen(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createRow(index: Int, action: Action): Row {
|
||||||
|
val route = createRouteText(index)
|
||||||
|
val titleText = "$index"
|
||||||
|
return Row.Builder()
|
||||||
|
.setTitle(route)
|
||||||
|
.setOnClickListener(OnClickListener { onRouteSelected(index) })
|
||||||
|
.addText(titleText)
|
||||||
|
.addAction(action)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
private fun onNavigate() {
|
private fun onNavigate() {
|
||||||
setResult(destination)
|
setResult(destination)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRouteSelected(index: Int) {
|
private fun onRouteSelected(index: Int) {
|
||||||
setResult(destination)
|
routeModel.navState = routeModel.navState.copy(currentRouteIndex = index)
|
||||||
finish()
|
surfaceRenderer.setPreviewRouteData(routeModel)
|
||||||
|
//setResult(destination)
|
||||||
|
//finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMapActionStrip(): ActionStrip {
|
fun getMapActionStrip(): ActionStrip {
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.kouros.navigation.car.screen
|
||||||
|
|
||||||
|
import androidx.car.app.CarContext
|
||||||
|
import androidx.car.app.CarToast
|
||||||
|
import androidx.car.app.Screen
|
||||||
|
import androidx.car.app.model.Action
|
||||||
|
import androidx.car.app.model.Header
|
||||||
|
import androidx.car.app.model.ItemList
|
||||||
|
import androidx.car.app.model.ListTemplate
|
||||||
|
import androidx.car.app.model.Row
|
||||||
|
import androidx.car.app.model.SectionedItemList
|
||||||
|
import androidx.car.app.model.Template
|
||||||
|
import androidx.car.app.model.Toggle
|
||||||
|
import com.kouros.data.R
|
||||||
|
import com.kouros.navigation.data.Constants.AVOID_MOTORWAY
|
||||||
|
import com.kouros.navigation.data.Constants.CAR_LOCATION
|
||||||
|
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
|
||||||
|
import com.kouros.navigation.data.RouteEngine
|
||||||
|
import com.kouros.navigation.model.ViewModel
|
||||||
|
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
|
||||||
|
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
||||||
|
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
|
||||||
|
import com.kouros.navigation.utils.NavigationUtils.setIntKeyValue
|
||||||
|
|
||||||
|
class RoutingSettings(private val carContext: CarContext, private var viewModel: ViewModel) : Screen(carContext) {
|
||||||
|
private var routingEngine = RouteEngine.OSRM.ordinal
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
routingEngine = getIntKeyValue(carContext, ROUTING_ENGINE)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGetTemplate(): Template {
|
||||||
|
val templateBuilder = ListTemplate.Builder()
|
||||||
|
val radioList =
|
||||||
|
ItemList.Builder()
|
||||||
|
.addItem(
|
||||||
|
buildRowForTemplate(
|
||||||
|
R.string.valhalla,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addItem(
|
||||||
|
buildRowForTemplate(
|
||||||
|
R.string.osrm,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addItem(
|
||||||
|
buildRowForTemplate(
|
||||||
|
R.string.tomtom,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setOnSelectedListener { index: Int ->
|
||||||
|
this.onSelected(index)
|
||||||
|
}
|
||||||
|
.setSelectedIndex(routingEngine)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return templateBuilder
|
||||||
|
.addSectionedList(SectionedItemList.create(
|
||||||
|
radioList,
|
||||||
|
carContext.getString(R.string.routing_engine)
|
||||||
|
))
|
||||||
|
.setHeader(
|
||||||
|
Header.Builder()
|
||||||
|
.setTitle(carContext.getString(R.string.routing_engine))
|
||||||
|
.setStartHeaderAction(Action.BACK)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSelected(index: Int) {
|
||||||
|
setIntKeyValue(carContext, index, ROUTING_ENGINE)
|
||||||
|
viewModel.routingEngine.value = index
|
||||||
|
CarToast.makeText(
|
||||||
|
carContext,
|
||||||
|
(carContext
|
||||||
|
.getString(R.string.routing_engine)
|
||||||
|
+ ":"
|
||||||
|
+ " " + index), CarToast.LENGTH_LONG
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildRowForTemplate(title: Int): Row {
|
||||||
|
return Row.Builder()
|
||||||
|
.setTitle(carContext.getString(title))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package com.kouros.navigation.car.screen
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.net.Uri
|
|
||||||
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
|
||||||
@@ -17,9 +16,9 @@ import androidx.lifecycle.Observer
|
|||||||
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.ViewStyle
|
import com.kouros.navigation.car.ViewStyle
|
||||||
|
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||||
import com.kouros.navigation.data.Category
|
import com.kouros.navigation.data.Category
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.nominatim.SearchResult
|
import com.kouros.navigation.data.nominatim.SearchResult
|
||||||
import com.kouros.navigation.model.ViewModel
|
import com.kouros.navigation.model.ViewModel
|
||||||
@@ -28,15 +27,14 @@ import com.kouros.navigation.model.ViewModel
|
|||||||
class SearchScreen(
|
class SearchScreen(
|
||||||
carContext: CarContext,
|
carContext: CarContext,
|
||||||
private var surfaceRenderer: SurfaceRenderer,
|
private var surfaceRenderer: SurfaceRenderer,
|
||||||
private var location: Location,
|
private val viewModel: ViewModel,
|
||||||
private val viewModel: ViewModel
|
|
||||||
) : Screen(carContext) {
|
) : Screen(carContext) {
|
||||||
|
|
||||||
var isSearchComplete: Boolean = false
|
var isSearchComplete: Boolean = false
|
||||||
|
|
||||||
var categories: List<Category> = listOf(
|
var categories: List<Category> = listOf(
|
||||||
Category(id = Constants.RECENT, name = carContext.getString(R.string.recent_destinations)),
|
Category(id = Constants.RECENT, name = carContext.getString(R.string.recent_destinations)),
|
||||||
Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)),
|
// Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)),
|
||||||
Category(id = Constants.CATEGORIES, name = carContext.getString(R.string.category_title)),
|
Category(id = Constants.CATEGORIES, name = carContext.getString(R.string.category_title)),
|
||||||
Category(id = Constants.FAVORITES, name = carContext.getString(R.string.favorites))
|
Category(id = Constants.FAVORITES, name = carContext.getString(R.string.favorites))
|
||||||
)
|
)
|
||||||
@@ -73,7 +71,6 @@ class SearchScreen(
|
|||||||
CategoriesScreen(
|
CategoriesScreen(
|
||||||
carContext,
|
carContext,
|
||||||
surfaceRenderer,
|
surfaceRenderer,
|
||||||
location,
|
|
||||||
viewModel
|
viewModel
|
||||||
)
|
)
|
||||||
) { obj: Any? ->
|
) { obj: Any? ->
|
||||||
@@ -89,7 +86,6 @@ class SearchScreen(
|
|||||||
PlaceListScreen(
|
PlaceListScreen(
|
||||||
carContext,
|
carContext,
|
||||||
surfaceRenderer,
|
surfaceRenderer,
|
||||||
location,
|
|
||||||
it.id,
|
it.id,
|
||||||
viewModel
|
viewModel
|
||||||
)
|
)
|
||||||
@@ -119,7 +115,7 @@ class SearchScreen(
|
|||||||
object : SearchCallback {
|
object : SearchCallback {
|
||||||
override fun onSearchSubmitted(searchTerm: String) {
|
override fun onSearchSubmitted(searchTerm: String) {
|
||||||
isSearchComplete = true
|
isSearchComplete = true
|
||||||
viewModel.searchPlaces(searchTerm, location)
|
viewModel.searchPlaces(searchTerm, surfaceRenderer.lastLocation)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setHeaderAction(Action.BACK)
|
.setHeaderAction(Action.BACK)
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ import androidx.car.app.model.ListTemplate
|
|||||||
import androidx.car.app.model.Row
|
import androidx.car.app.model.Row
|
||||||
import androidx.car.app.model.Template
|
import androidx.car.app.model.Template
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
|
import com.kouros.navigation.model.ViewModel
|
||||||
|
|
||||||
/** A screen demonstrating selectable lists. */
|
/** A screen demonstrating selectable lists. */
|
||||||
class SettingsScreen(
|
class SettingsScreen(
|
||||||
carContext: CarContext,
|
carContext: CarContext,
|
||||||
|
private var viewModel: ViewModel,
|
||||||
) : Screen(carContext) {
|
) : Screen(carContext) {
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
@@ -40,7 +42,7 @@ class SettingsScreen(
|
|||||||
)
|
)
|
||||||
listBuilder.addItem(
|
listBuilder.addItem(
|
||||||
buildRowForTemplate(
|
buildRowForTemplate(
|
||||||
NavigationSettings(carContext),
|
NavigationSettings(carContext, viewModel),
|
||||||
R.string.navigation_settings
|
R.string.navigation_settings
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,255 +0,0 @@
|
|||||||
{
|
|
||||||
"trip": {
|
|
||||||
"locations": [
|
|
||||||
{
|
|
||||||
"type": "break",
|
|
||||||
"lat": 48.185749,
|
|
||||||
"lon": 11.579374,
|
|
||||||
"side_of_street": "right",
|
|
||||||
"original_index": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "break",
|
|
||||||
"lat": 48.116481,
|
|
||||||
"lon": 11.594322,
|
|
||||||
"street": "Hohenwaldeckstr. 27",
|
|
||||||
"side_of_street": "left",
|
|
||||||
"original_index": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"legs": [
|
|
||||||
{
|
|
||||||
"maneuvers": [
|
|
||||||
{
|
|
||||||
"type": 2,
|
|
||||||
"instruction": "Auf Vogelhartstraße Richtung Westen fahren.",
|
|
||||||
"verbal_succinct_transition_instruction": "Richtung Westen fahren. Dann Rechts auf Silcherstraße abbiegen.",
|
|
||||||
"verbal_pre_transition_instruction": "Auf Vogelhartstraße Richtung Westen fahren. Dann Rechts auf Silcherstraße abbiegen.",
|
|
||||||
"verbal_post_transition_instruction": "70 Meter weiter der Route folgen.",
|
|
||||||
"street_names": [
|
|
||||||
"Vogelhartstraße"
|
|
||||||
],
|
|
||||||
"bearing_after": 273,
|
|
||||||
"time": 16.965,
|
|
||||||
"length": 0.07,
|
|
||||||
"cost": 34.428,
|
|
||||||
"begin_shape_index": 0,
|
|
||||||
"end_shape_index": 6,
|
|
||||||
"verbal_multi_cue": true,
|
|
||||||
"travel_mode": "drive",
|
|
||||||
"travel_type": "car"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 10,
|
|
||||||
"instruction": "Rechts auf Silcherstraße abbiegen.",
|
|
||||||
"verbal_transition_alert_instruction": "Rechts auf Silcherstraße abbiegen.",
|
|
||||||
"verbal_succinct_transition_instruction": "Rechts abbiegen.",
|
|
||||||
"verbal_pre_transition_instruction": "Rechts auf Silcherstraße abbiegen.",
|
|
||||||
"verbal_post_transition_instruction": "200 Meter weiter der Route folgen.",
|
|
||||||
"street_names": [
|
|
||||||
"Silcherstraße"
|
|
||||||
],
|
|
||||||
"bearing_before": 273,
|
|
||||||
"bearing_after": 5,
|
|
||||||
"time": 43.25,
|
|
||||||
"length": 0.156,
|
|
||||||
"cost": 89.306,
|
|
||||||
"begin_shape_index": 6,
|
|
||||||
"end_shape_index": 13,
|
|
||||||
"travel_mode": "drive",
|
|
||||||
"travel_type": "car"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 10,
|
|
||||||
"instruction": "Rechts auf Schmalkaldener Straße abbiegen.",
|
|
||||||
"verbal_transition_alert_instruction": "Rechts auf Schmalkaldener Straße abbiegen.",
|
|
||||||
"verbal_succinct_transition_instruction": "Rechts abbiegen.",
|
|
||||||
"verbal_pre_transition_instruction": "Rechts auf Schmalkaldener Straße abbiegen.",
|
|
||||||
"verbal_post_transition_instruction": "400 Meter weiter der Route folgen.",
|
|
||||||
"street_names": [
|
|
||||||
"Schmalkaldener Straße"
|
|
||||||
],
|
|
||||||
"bearing_before": 2,
|
|
||||||
"bearing_after": 93,
|
|
||||||
"time": 108.947,
|
|
||||||
"length": 0.43,
|
|
||||||
"cost": 217.43,
|
|
||||||
"begin_shape_index": 13,
|
|
||||||
"end_shape_index": 29,
|
|
||||||
"travel_mode": "drive",
|
|
||||||
"travel_type": "car"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 10,
|
|
||||||
"instruction": "Rechts auf Ingolstädter Straße/B 13 abbiegen.",
|
|
||||||
"verbal_transition_alert_instruction": "Rechts auf Ingolstädter Straße abbiegen.",
|
|
||||||
"verbal_succinct_transition_instruction": "Rechts abbiegen.",
|
|
||||||
"verbal_pre_transition_instruction": "Rechts auf Ingolstädter Straße, B 13 abbiegen.",
|
|
||||||
"verbal_post_transition_instruction": "einen Kilometer weiter der Route folgen.",
|
|
||||||
"street_names": [
|
|
||||||
"B 13"
|
|
||||||
],
|
|
||||||
"begin_street_names": [
|
|
||||||
"Ingolstädter Straße",
|
|
||||||
"B 13"
|
|
||||||
],
|
|
||||||
"bearing_before": 88,
|
|
||||||
"bearing_after": 178,
|
|
||||||
"time": 147.528,
|
|
||||||
"length": 1.064,
|
|
||||||
"cost": 230.646,
|
|
||||||
"begin_shape_index": 29,
|
|
||||||
"end_shape_index": 65,
|
|
||||||
"travel_mode": "drive",
|
|
||||||
"travel_type": "car"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 19,
|
|
||||||
"instruction": "Auf die Auffahrt nach links abbiegen.",
|
|
||||||
"verbal_transition_alert_instruction": "Auf die Auffahrt nach links abbiegen.",
|
|
||||||
"verbal_pre_transition_instruction": "Auf die Auffahrt nach links abbiegen.",
|
|
||||||
"street_names": [
|
|
||||||
"Schenkendorfstraße"
|
|
||||||
],
|
|
||||||
"bearing_before": 188,
|
|
||||||
"bearing_after": 98,
|
|
||||||
"time": 61.597,
|
|
||||||
"length": 0.374,
|
|
||||||
"cost": 117.338,
|
|
||||||
"begin_shape_index": 65,
|
|
||||||
"end_shape_index": 84,
|
|
||||||
"travel_mode": "drive",
|
|
||||||
"travel_type": "car"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 24,
|
|
||||||
"instruction": "Links halten auf B 2R.",
|
|
||||||
"verbal_transition_alert_instruction": "Links halten auf B 2R.",
|
|
||||||
"verbal_pre_transition_instruction": "Links halten auf B 2R.",
|
|
||||||
"verbal_post_transition_instruction": "6 Kilometer weiter der Route folgen.",
|
|
||||||
"street_names": [
|
|
||||||
"B 2R"
|
|
||||||
],
|
|
||||||
"bearing_before": 117,
|
|
||||||
"bearing_after": 118,
|
|
||||||
"time": 509.658,
|
|
||||||
"length": 6.37,
|
|
||||||
"cost": 580.602,
|
|
||||||
"begin_shape_index": 84,
|
|
||||||
"end_shape_index": 240,
|
|
||||||
"travel_mode": "drive",
|
|
||||||
"travel_type": "car"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 20,
|
|
||||||
"instruction": "An der Ausfahrt rechts abfahren.",
|
|
||||||
"verbal_transition_alert_instruction": "An der Ausfahrt rechts abfahren.",
|
|
||||||
"verbal_pre_transition_instruction": "An der Ausfahrt rechts abfahren.",
|
|
||||||
"verbal_post_transition_instruction": "einen Kilometer weiter der Route folgen.",
|
|
||||||
"street_names": [
|
|
||||||
"Ampfingstraße"
|
|
||||||
],
|
|
||||||
"bearing_before": 191,
|
|
||||||
"bearing_after": 206,
|
|
||||||
"time": 133.661,
|
|
||||||
"length": 1.031,
|
|
||||||
"cost": 226.661,
|
|
||||||
"begin_shape_index": 240,
|
|
||||||
"end_shape_index": 280,
|
|
||||||
"travel_mode": "drive",
|
|
||||||
"travel_type": "car"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 10,
|
|
||||||
"instruction": "Rechts auf Anzinger Straße abbiegen.",
|
|
||||||
"verbal_transition_alert_instruction": "Rechts auf Anzinger Straße abbiegen.",
|
|
||||||
"verbal_succinct_transition_instruction": "Rechts abbiegen.",
|
|
||||||
"verbal_pre_transition_instruction": "Rechts auf Anzinger Straße abbiegen.",
|
|
||||||
"verbal_post_transition_instruction": "1.5 Kilometer weiter der Route folgen.",
|
|
||||||
"street_names": [
|
|
||||||
"Anzinger Straße"
|
|
||||||
],
|
|
||||||
"bearing_before": 182,
|
|
||||||
"bearing_after": 277,
|
|
||||||
"time": 211.637,
|
|
||||||
"length": 1.444,
|
|
||||||
"cost": 450.654,
|
|
||||||
"begin_shape_index": 280,
|
|
||||||
"end_shape_index": 334,
|
|
||||||
"travel_mode": "drive",
|
|
||||||
"travel_type": "car"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 15,
|
|
||||||
"instruction": "Links auf Hohenwaldeckstraße abbiegen.",
|
|
||||||
"verbal_transition_alert_instruction": "Links auf Hohenwaldeckstraße abbiegen.",
|
|
||||||
"verbal_succinct_transition_instruction": "Links abbiegen.",
|
|
||||||
"verbal_pre_transition_instruction": "Links auf Hohenwaldeckstraße abbiegen.",
|
|
||||||
"verbal_post_transition_instruction": "200 Meter weiter der Route folgen.",
|
|
||||||
"street_names": [
|
|
||||||
"Hohenwaldeckstraße"
|
|
||||||
],
|
|
||||||
"bearing_before": 249,
|
|
||||||
"bearing_after": 170,
|
|
||||||
"time": 45.365,
|
|
||||||
"length": 0.183,
|
|
||||||
"cost": 84.344,
|
|
||||||
"begin_shape_index": 334,
|
|
||||||
"end_shape_index": 342,
|
|
||||||
"travel_mode": "drive",
|
|
||||||
"travel_type": "car"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 6,
|
|
||||||
"instruction": "Hohenwaldeckstr. 27 befindet sich auf der linken Seite.",
|
|
||||||
"verbal_transition_alert_instruction": "Hohenwaldeckstr. 27 befindet sich auf der linken Seite.",
|
|
||||||
"verbal_pre_transition_instruction": "Hohenwaldeckstr. 27 befindet sich auf der linken Seite.",
|
|
||||||
"bearing_before": 184,
|
|
||||||
"time": 0,
|
|
||||||
"length": 0,
|
|
||||||
"cost": 0,
|
|
||||||
"begin_shape_index": 342,
|
|
||||||
"end_shape_index": 342,
|
|
||||||
"travel_mode": "drive",
|
|
||||||
"travel_type": "car"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"summary": {
|
|
||||||
"level_changes": [
|
|
||||||
[220, -1]
|
|
||||||
],
|
|
||||||
"has_time_restrictions": false,
|
|
||||||
"has_toll": false,
|
|
||||||
"has_highway": false,
|
|
||||||
"has_ferry": false,
|
|
||||||
"min_lat": 48.116486,
|
|
||||||
"min_lon": 11.578422,
|
|
||||||
"max_lat": 48.186957,
|
|
||||||
"max_lon": 11.616382,
|
|
||||||
"time": 1278.611,
|
|
||||||
"length": 11.123,
|
|
||||||
"cost": 2031.412
|
|
||||||
},
|
|
||||||
"shape": "mk_|zA_}vaUA^MzKKhKMrKMrLEbEeLs@kGa@yV}AmIa@cJ]g@AgQc@TgTn@ak@\\}Y`@u~@NiZRss@Ekc@AcGAwE?iB@yN@mH@_L?sAOiVEiGrISbH[|s@kC`KY~Qw@dk@mBdBErH]bIa@pNk@pAGxgA}ErQw@f`@eB`AAhEOjDO~Kg@bh@cCpTcAtEUlBKtFMbk@cBpt@eDfScAlH]hHY`HTdATbBl@rAd@|Bz@xBr@|F`AzD\\l@mHHsCAeDcAkImBiLs@}Ii@wOh@a]vAu[bB{VjCmXjCuUtDoU~A}JjBmIvDmOvDgOfCiJdB}HvAsG|FwSzGaV`IgWdC_K\\cG~Pii@pUcr@dYaz@lEkMdDsPpMm]|Tqj@tQwc@jQsb@nVwm@vEmL`k@suAxHyPzFaKrBaD|AmBxCeDpC}BvDmCnEyBnDuAzFyAhDWdDW|D?~CPjFj@lHrB|QzI~O~Jb]lS`ZbR~NnIhCdBdDtBb`Azk@fhAdr@vN~I~l@|]vr@vVb]jElZw@xG{@jEw@`KiCfLiFrJ_GnPaNjJaJzJoNxMgUtU}g@d]_w@f_@ev@tO_YhRmYbHwIxG_I|NwN~AyAdTiP~b@iZ~J}GxScQ`JoIfGwGtEwFnCqDbEaG~CcFhHgMrGcNxHmR|FaQnCwIpAeEdCiIje@}}AnQil@pGySjCyIvSor@nJ{ZpHwVbQyk@zIkXlHkTrOcc@bPic@rUyk@vKoVnMaWfWed@rT_`@jQcZhBwCzCsEtCcElC}CzCiDrCiCnH{GzLwJlGwDfH_ElGeDhHwClHyBrG{BdK_CzMsBzJgAbIq@jI?nYGbSTfGDvEDnGRfWx@|i@lEvpBbUdRrBpLfAl_@jDr|Ghm@hu@jGrWrBpd@fAxG[raBgUl[{HhM}DzOeGdXiLpCuAzwAij@lEcAbF_AnEc@|DDzE[dAGrIh@|APfe@lLbc@lLpWbIbdAnYnKlBf]|Tzi@rZbl@|ZtSjJpOhG~HvB`AVvBl@hBf@vBl@`AXrJrCtZjHhRvCrKpAjAN|Gb@hLr@dLp@xYbB`CPlDNxBLlOv@n{@jE~F\\lDP|Ov@lSn@rGNrGPlHAxICnJCvJBhFBrQL~E?dC?zFJ[xIcB|_@}Add@kAvi@i@zg@AtGEd_@f@lq@lB`|@jApd@tA`l@XpFn@lHf@bFnAdMjA`HnDpQfEfSvElPfBdOvCrWjP|xANrA|@bH\\pC\\lCNnA~Hhn@dB|MnAtJrAlK`AzH~@hHvBvPj@pEl@zFx@zH^hD~BlQdEbZhF`_@rAbJ|AjK~AtKzBrNt@nFv@lFtB`OdCfQbMb_AlDnUrDvTvF|ZzIxh@jDm@zHgBhF{@lC[`L]jMr@bNj@~_@bB"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"summary": {
|
|
||||||
"has_time_restrictions": false,
|
|
||||||
"has_toll": false,
|
|
||||||
"has_highway": false,
|
|
||||||
"has_ferry": false,
|
|
||||||
"min_lat": 48.116486,
|
|
||||||
"min_lon": 11.578422,
|
|
||||||
"max_lat": 48.186957,
|
|
||||||
"max_lon": 11.616382,
|
|
||||||
"time": 1278.611,
|
|
||||||
"length": 11.123,
|
|
||||||
"cost": 2031.412
|
|
||||||
},
|
|
||||||
"status_message": "Found route between points",
|
|
||||||
"status": 0,
|
|
||||||
"units": "kilometers",
|
|
||||||
"language": "de-DE"
|
|
||||||
},
|
|
||||||
"id": "my_work_route"
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
package com.kouros.navigation.car
|
package com.kouros.navigation.car
|
||||||
|
|
||||||
import android.location.Location
|
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||||
import android.location.LocationManager
|
|
||||||
import com.kouros.navigation.data.Constants.home2Location
|
|
||||||
import com.kouros.navigation.data.Constants.homeLocation
|
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
|
||||||
import com.kouros.navigation.data.SearchFilter
|
|
||||||
import com.kouros.navigation.model.RouteModel
|
import com.kouros.navigation.model.RouteModel
|
||||||
import com.kouros.navigation.model.ViewModel
|
import com.kouros.navigation.model.ViewModel
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -16,23 +11,23 @@ import org.junit.Test
|
|||||||
*/
|
*/
|
||||||
class ViewModelTest {
|
class ViewModelTest {
|
||||||
|
|
||||||
val repo = NavigationRepository()
|
val repo = ValhallaRepository()
|
||||||
val viewModel = ViewModel(repo)
|
val viewModel = ViewModel(repo)
|
||||||
val model = RouteModel()
|
val model = RouteModel()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun routeViewModelTest() {
|
fun routeViewModelTest() {
|
||||||
|
|
||||||
val fromLocation = Location(LocationManager.GPS_PROVIDER)
|
// val fromLocation = Location(LocationManager.GPS_PROVIDER)
|
||||||
fromLocation.isMock = true
|
// fromLocation.isMock = true
|
||||||
fromLocation.latitude = homeLocation.latitude
|
// fromLocation.latitude = homeLocation.latitude
|
||||||
fromLocation.longitude = homeLocation.longitude
|
// fromLocation.longitude = homeLocation.longitude
|
||||||
val toLocation = Location(LocationManager.GPS_PROVIDER)
|
// val toLocation = Location(LocationManager.GPS_PROVIDER)
|
||||||
toLocation.isMock = true
|
// toLocation.isMock = true
|
||||||
toLocation.latitude = home2Location.latitude
|
// toLocation.latitude = home2Location.latitude
|
||||||
toLocation.longitude = home2Location.longitude
|
// toLocation.longitude = home2Location.longitude
|
||||||
|
//
|
||||||
val route = repo.getRoute(fromLocation, toLocation, SearchFilter())
|
// val route = repo.getRoute(fromLocation, toLocation, SearchFilter())
|
||||||
model.startNavigation(route)
|
//model.startNavigation(route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.maplibre.compose)
|
implementation(libs.maplibre.compose)
|
||||||
|
implementation("androidx.compose.material:material-icons-extended:1.7.8")
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ package com.kouros.navigation.data
|
|||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import com.kouros.navigation.data.route.Lane
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
import io.objectbox.annotation.Entity
|
import io.objectbox.annotation.Entity
|
||||||
import io.objectbox.annotation.Id
|
import io.objectbox.annotation.Id
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -57,13 +59,16 @@ data class StepData (
|
|||||||
|
|
||||||
var leftStepDistance: Double,
|
var leftStepDistance: Double,
|
||||||
|
|
||||||
var maneuverType: Int,
|
var currentManeuverType: Int,
|
||||||
|
|
||||||
var icon: Int,
|
var icon: Int,
|
||||||
|
|
||||||
var arrivalTime : Long,
|
var arrivalTime : Long,
|
||||||
|
|
||||||
var leftDistance: Double
|
var leftDistance: Double,
|
||||||
|
|
||||||
|
var lane: List<Lane> = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())),
|
||||||
|
var exitNumber: Int = 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -72,39 +77,15 @@ data class Locations (
|
|||||||
var lat : Double,
|
var lat : Double,
|
||||||
var lon : Double,
|
var lon : Double,
|
||||||
var street : String = "",
|
var street : String = "",
|
||||||
val search_filter: SearchFilter,
|
val search_filter: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SearchFilter(
|
data class SearchFilter(
|
||||||
var max_road_class: String = "",
|
var avoidMotorway: Boolean = false,
|
||||||
var exclude_toll : Boolean = false
|
var avoidTollway : Boolean = false,
|
||||||
) {
|
|
||||||
|
|
||||||
class Builder {
|
)
|
||||||
private var avoidMotorway = false
|
|
||||||
private var avoidTollway = false
|
|
||||||
|
|
||||||
fun avoidMotorway (value: Boolean ) = apply {
|
|
||||||
avoidMotorway = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun avoidTollway (value: Boolean ) = apply {
|
|
||||||
avoidTollway = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun build(): SearchFilter {
|
|
||||||
val filter = SearchFilter()
|
|
||||||
if (avoidMotorway) {
|
|
||||||
filter.max_road_class = "trunk"
|
|
||||||
}
|
|
||||||
if (avoidTollway) {
|
|
||||||
filter.exclude_toll = true
|
|
||||||
}
|
|
||||||
return filter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ValhallaLocation (
|
data class ValhallaLocation (
|
||||||
@@ -115,13 +96,6 @@ data class ValhallaLocation (
|
|||||||
var language: String
|
var language: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BoundingBox (
|
|
||||||
var southernLat : Double,
|
|
||||||
var westernLon: Double,
|
|
||||||
var northerLat : Double,
|
|
||||||
var easternLon : Double
|
|
||||||
)
|
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
|
|
||||||
//const val STYLE: String = "https://kouros-online.de/liberty.json"
|
//const val STYLE: String = "https://kouros-online.de/liberty.json"
|
||||||
@@ -149,17 +123,8 @@ object Constants {
|
|||||||
|
|
||||||
val categories = listOf("Tankstelle", "Apotheke", "Ladestationen")
|
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 homeLocation: Location = Location(LocationManager.GPS_PROVIDER)
|
val homeVogelhart = location(11.5793748, 48.185749)
|
||||||
val home2Location: Location = Location(LocationManager.GPS_PROVIDER)
|
val homeHohenwaldeck = location( 11.594322, 48.1164817)
|
||||||
|
|
||||||
init {
|
|
||||||
// Vogelhartstr. 17
|
|
||||||
homeLocation.latitude = 48.185749
|
|
||||||
homeLocation.longitude = 11.5793748
|
|
||||||
// Hohenwaldeckstr. 27
|
|
||||||
home2Location.latitude = 48.1164817
|
|
||||||
home2Location.longitude = 11.594322
|
|
||||||
}
|
|
||||||
|
|
||||||
const val SHARED_PREF_KEY = "NavigationPrefs"
|
const val SHARED_PREF_KEY = "NavigationPrefs"
|
||||||
|
|
||||||
@@ -171,18 +136,24 @@ object Constants {
|
|||||||
|
|
||||||
const val AVOID_TOLLWAY = "AvoidTollway"
|
const val AVOID_TOLLWAY = "AvoidTollway"
|
||||||
|
|
||||||
const val NEXT_STEP_THRESHOLD = 100.0
|
const val CAR_LOCATION = "CarLocation"
|
||||||
|
const val ROUTING_ENGINE = "RoutingEngine"
|
||||||
|
|
||||||
|
const val NEXT_STEP_THRESHOLD = 120.0
|
||||||
|
|
||||||
const val MAXIMAL_SNAP_CORRECTION = 50.0
|
const val MAXIMAL_SNAP_CORRECTION = 50.0
|
||||||
|
|
||||||
const val MAXIMAL_ROUTE_DEVIATION = 100.0
|
const val MAXIMAL_ROUTE_DEVIATION = 80.0
|
||||||
|
|
||||||
const val DESTINATION_ARRIVAL_DISTANCE = 40.0
|
const val DESTINATION_ARRIVAL_DISTANCE = 40.0
|
||||||
|
|
||||||
val ROUTE_ENGINE = RouteEngine.VALHALLA.name
|
const val NEAREST_LOCATION_DISTANCE = 10F
|
||||||
|
|
||||||
|
const val MAXIMUM_LOCATION_DISTANCE = 100000F
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum class RouteEngine {
|
enum class RouteEngine {
|
||||||
VALHALLA, OSRM, GRAPHHOPPER
|
VALHALLA, OSRM, TOMTOM, GRAPHHOPPER
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,68 +18,63 @@ package com.kouros.navigation.data
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import com.kouros.navigation.data.overpass.Elements
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.model.RouteModel
|
import com.kouros.navigation.model.RouteModel
|
||||||
import org.json.JSONArray
|
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
|
||||||
import java.net.Authenticator
|
import java.net.Authenticator
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.PasswordAuthentication
|
import java.net.PasswordAuthentication
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
|
|
||||||
|
|
||||||
abstract class NavigationRepository {
|
abstract class NavigationRepository {
|
||||||
|
|
||||||
private val placesUrl = "https://kouros-online.de/maps/placespwd";
|
|
||||||
|
|
||||||
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
|
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
|
||||||
|
|
||||||
|
//private val nominatimUrl = "https://kouros-online.de/nominatim/"
|
||||||
|
|
||||||
abstract fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String
|
|
||||||
|
|
||||||
fun getRouteDistance(currentLocation: Location, location: Location, searchFilter: SearchFilter, context: Context): Double {
|
abstract fun getRoute(
|
||||||
val route = getRoute(currentLocation, location, searchFilter)
|
context: Context,
|
||||||
val routeModel = RouteModel()
|
currentLocation: Location,
|
||||||
routeModel.startNavigation(route, context)
|
location: Location,
|
||||||
return routeModel.route.summary!!.distance
|
carOrientation: Float,
|
||||||
|
searchFilter: SearchFilter
|
||||||
|
): String
|
||||||
|
|
||||||
|
abstract fun getTraffic(context: Context, location: Location, carOrientation: Float): String
|
||||||
|
fun getRouteDistance(
|
||||||
|
currentLocation: Location,
|
||||||
|
location: Location,
|
||||||
|
carOrientation: Float,
|
||||||
|
searchFilter: SearchFilter,
|
||||||
|
context: Context
|
||||||
|
): Double {
|
||||||
|
val route = getRoute(context, currentLocation, location, carOrientation, searchFilter)
|
||||||
|
val routeModel = RouteModel()
|
||||||
|
routeModel.startNavigation(route, context)
|
||||||
|
return routeModel.curRoute.summary.distance
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchPlaces(search: String, location: Location) : String {
|
fun searchPlaces(search: String, location: Location): String {
|
||||||
// val bbox = getBoundingBox(location.longitude, location.latitude, 10.0)
|
val box = calculateSquareRadius(location.latitude, location.longitude, 100.0)
|
||||||
// val neLon = bbox["ne"]?.get("lon")
|
val viewbox = "&bounded=1&viewbox=${box}"
|
||||||
// val neLat = bbox["ne"]?.get("lat")
|
return fetchUrl(
|
||||||
// val swLon = bbox["sw"]?.get("lon")
|
"${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox",
|
||||||
// val swLat = bbox["sw"]?.get("lat")
|
true
|
||||||
// val viewbox = "&viewbox=$swLon,$swLat,$neLon,$neLat"
|
)
|
||||||
return fetchUrl("${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true,&countrycodes=de", false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reverseAddress(location: Location) : String {
|
fun reverseAddress(location: Location): String {
|
||||||
return fetchUrl("${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true&countrycodes=de", false)
|
return fetchUrl(
|
||||||
|
"${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true",
|
||||||
|
false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPlaces(): List<Place> {
|
|
||||||
val places: MutableList<Place> = ArrayList()
|
|
||||||
val placesStr = fetchUrl(placesUrl, true)
|
|
||||||
val jArray = JSONArray(placesStr)
|
|
||||||
for (i in 0..<jArray.length()) {
|
|
||||||
val json = jArray.getJSONObject(i)
|
|
||||||
val place = Place(
|
|
||||||
json.getString("id").toLong(),
|
|
||||||
json.getString("name"),
|
|
||||||
category = json.getString("category"),
|
|
||||||
latitude = json.getDouble("latitude"),
|
|
||||||
longitude = json.getDouble("longitude"),
|
|
||||||
postalCode = json.getString("postalCode"),
|
|
||||||
city = json.getString("city"),
|
|
||||||
street = json.getString("street"),
|
|
||||||
)
|
|
||||||
places.add(place)
|
|
||||||
}
|
|
||||||
return places
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchUrl(url: String, authenticator : Boolean): String {
|
|
||||||
|
fun fetchUrl(url: String, authenticator: Boolean): String {
|
||||||
try {
|
try {
|
||||||
if (authenticator) {
|
if (authenticator) {
|
||||||
Authenticator.setDefault(object : Authenticator() {
|
Authenticator.setDefault(object : Authenticator() {
|
||||||
@@ -102,7 +97,7 @@ abstract class NavigationRepository {
|
|||||||
val responseCode = httpURLConnection.responseCode
|
val responseCode = httpURLConnection.responseCode
|
||||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
val response = httpURLConnection.inputStream.bufferedReader()
|
val response = httpURLConnection.inputStream.bufferedReader()
|
||||||
.use { it.readText() } // defaults to UTF-8
|
.use { it.readText() }
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
package com.kouros.navigation.data
|
package com.kouros.navigation.data
|
||||||
|
|
||||||
import android.location.Location
|
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
|
|
||||||
import com.kouros.navigation.data.osrm.OsrmResponse
|
import com.kouros.navigation.data.osrm.OsrmResponse
|
||||||
import com.kouros.navigation.data.osrm.OsrmRoute
|
import com.kouros.navigation.data.osrm.OsrmRoute
|
||||||
import com.kouros.navigation.data.route.Leg
|
import com.kouros.navigation.data.route.Leg
|
||||||
|
import com.kouros.navigation.data.route.Maneuver
|
||||||
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.data.tomtom.TomTomResponse
|
||||||
|
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.GeoUtils.createCenterLocation
|
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getRouteEngine
|
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
@@ -23,34 +21,23 @@ import org.maplibre.geojson.Point
|
|||||||
data class Route(
|
data class Route(
|
||||||
|
|
||||||
val routeEngine: Int,
|
val routeEngine: Int,
|
||||||
val summary: Summary?,
|
val routes: List<com.kouros.navigation.data.route.Routes>,
|
||||||
val legs: List<Leg>?,
|
var currentStepIndex: Int = 0,
|
||||||
val routeGeoJson: String = "",
|
|
||||||
val centerLocation: Location = location(0.0, 0.0),
|
|
||||||
var currentStep: Int = 0,
|
|
||||||
val waypoints: List<List<Double>>?,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class Builder(
|
data class Builder(
|
||||||
|
|
||||||
var routeEngine: Int = 0,
|
var routeEngine: Int = 0,
|
||||||
var summary: Summary? = null,
|
var summary: Summary = Summary(),
|
||||||
var legs: List<Leg>? = null,
|
var routes: List<com.kouros.navigation.data.route.Routes> = emptyList(),
|
||||||
var routeGeoJson: String = "",
|
|
||||||
var centerLocation: Location = location(0.0, 0.0),
|
|
||||||
var waypoints: List<List<Double>>? = null,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun routeType(routeEngine: Int) = apply { this.routeEngine = routeEngine }
|
fun routeType(routeEngine: Int) = apply { this.routeEngine = routeEngine }
|
||||||
fun summary(summary: Summary) = apply { this.summary = summary }
|
fun routes(routes: List<com.kouros.navigation.data.route.Routes>) = apply {
|
||||||
fun legs(legs: List<Leg>) = apply { this.legs = legs }
|
this.routes = routes
|
||||||
fun routeGeoJson(routeGeoJson: String) = apply {
|
|
||||||
this.routeGeoJson = routeGeoJson
|
|
||||||
centerLocation = createCenterLocation(routeGeoJson)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine }
|
fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine }
|
||||||
fun waypoints(waypoints: List<List<Double>>) = apply { this.waypoints = waypoints }
|
|
||||||
fun route(route: String) = apply {
|
fun route(route: String) = apply {
|
||||||
if (route.isNotEmpty() && route != "[]") {
|
if (route.isNotEmpty() && route != "[]") {
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
val gson = GsonBuilder().serializeNulls().create()
|
||||||
@@ -63,12 +50,15 @@ data class Route(
|
|||||||
jsonObject["trip"].toString(),
|
jsonObject["trip"].toString(),
|
||||||
ValhallaResponse::class.java
|
ValhallaResponse::class.java
|
||||||
)
|
)
|
||||||
ValhallaRoute().mapJsonToValhalla(routeJson, this)
|
ValhallaRoute().mapToRoute(routeJson, this)
|
||||||
}
|
}
|
||||||
|
RouteEngine.OSRM.ordinal -> {
|
||||||
else -> {
|
|
||||||
val osrmJson = gson.fromJson(route, OsrmResponse::class.java)
|
val osrmJson = gson.fromJson(route, OsrmResponse::class.java)
|
||||||
OsrmRoute().mapToOsrm(osrmJson, this)
|
OsrmRoute().mapToRoute(osrmJson, this)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val tomtomJson = gson.fromJson(route, TomTomResponse::class.java)
|
||||||
|
TomTomRoute().mapToRoute(tomtomJson, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,17 +67,49 @@ data class Route(
|
|||||||
fun build(): Route {
|
fun build(): Route {
|
||||||
return Route(
|
return Route(
|
||||||
routeEngine = this.routeEngine,
|
routeEngine = this.routeEngine,
|
||||||
summary = this.summary,
|
routes = this.routes,
|
||||||
legs = this.legs,
|
)
|
||||||
waypoints = this.waypoints,
|
}
|
||||||
routeGeoJson = this.routeGeoJson,
|
|
||||||
|
fun buildEmpty(): Route {
|
||||||
|
return Route(
|
||||||
|
routeEngine = 0,
|
||||||
|
routes = emptyList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun legs(): List<Leg> {
|
||||||
|
return if (routes.isNotEmpty()) {
|
||||||
|
routes.first().legs
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isRouteValid(): Boolean {
|
||||||
|
return routes.isNotEmpty() && legs().isNotEmpty()
|
||||||
|
}
|
||||||
|
fun currentStep(): Step {
|
||||||
|
return if (isRouteValid()) {
|
||||||
|
legs().first().steps[currentStepIndex]
|
||||||
|
} else {
|
||||||
|
Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nextStep(steps : Int): Step {
|
||||||
|
val nextIndex = currentStepIndex + steps
|
||||||
|
return if (isRouteValid() && nextIndex < legs().first().steps.size) {
|
||||||
|
legs().first().steps[nextIndex]
|
||||||
|
} else {
|
||||||
|
currentStep()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun maneuverLocations(): List<Point> {
|
fun maneuverLocations(): List<Point> {
|
||||||
val step = currentStep()
|
val waypoints = currentStep().maneuver.waypoints
|
||||||
val waypoints = step.maneuver.waypoints
|
|
||||||
val points = mutableListOf<Point>()
|
val points = mutableListOf<Point>()
|
||||||
for (loc in waypoints) {
|
for (loc in waypoints) {
|
||||||
val point = Point.fromLngLat(loc[0], loc[1])
|
val point = Point.fromLngLat(loc[0], loc[1])
|
||||||
@@ -95,21 +117,4 @@ data class Route(
|
|||||||
}
|
}
|
||||||
return points
|
return points
|
||||||
}
|
}
|
||||||
|
|
||||||
fun currentStep(): Step {
|
|
||||||
if (legs != null) {
|
|
||||||
return legs.first().steps[currentStep]
|
|
||||||
} else {
|
|
||||||
throw IndexOutOfBoundsException("No legs available.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun nextStep(): Step {
|
|
||||||
val nextIndex = currentStep + 1
|
|
||||||
return if (nextIndex < legs!!.first().steps.size) {
|
|
||||||
legs.first().steps[currentStep + 1]
|
|
||||||
} else {
|
|
||||||
throw IndexOutOfBoundsException("No next maneuver available.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,8 @@ import com.google.gson.annotations.SerializedName
|
|||||||
|
|
||||||
data class Intersections(
|
data class Intersections(
|
||||||
|
|
||||||
@SerializedName("out") var out: Int? = null,
|
@SerializedName("in") var inV: Int = 0,
|
||||||
|
@SerializedName("out") var out: Int = 0,
|
||||||
@SerializedName("entry") var entry: ArrayList<Boolean> = arrayListOf(),
|
@SerializedName("entry") var entry: ArrayList<Boolean> = arrayListOf(),
|
||||||
@SerializedName("bearings") var bearings: ArrayList<Int> = arrayListOf(),
|
@SerializedName("bearings") var bearings: ArrayList<Int> = arrayListOf(),
|
||||||
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(),
|
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(),
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import com.google.gson.annotations.SerializedName
|
|||||||
|
|
||||||
data class Legs (
|
data class Legs (
|
||||||
|
|
||||||
@SerializedName("steps" ) var steps : ArrayList<Steps> = arrayListOf(),
|
@SerializedName("steps" ) var steps : List<Steps> = listOf(),
|
||||||
@SerializedName("weight" ) var weight : Double? = null,
|
@SerializedName("weight" ) var weight : Double = 0.0,
|
||||||
@SerializedName("summary" ) var summary : String? = null,
|
@SerializedName("summary" ) var summary : String = "",
|
||||||
@SerializedName("duration" ) var duration : Double? = null,
|
@SerializedName("duration" ) var duration : Double = 0.0,
|
||||||
@SerializedName("distance" ) var distance : Double? = null
|
@SerializedName("distance" ) var distance : Double = 0.0
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -3,12 +3,13 @@ package com.kouros.navigation.data.osrm
|
|||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
|
||||||
data class Maneuver (
|
data class Maneuver(
|
||||||
|
|
||||||
@SerializedName("bearing_after" ) var bearingAfter : Int? = null,
|
@SerializedName("bearing_after") var bearingAfter: Int = 0,
|
||||||
@SerializedName("bearing_before" ) var bearingBefore : Int? = null,
|
@SerializedName("bearing_before") var bearingBefore: Int = 0,
|
||||||
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(),
|
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(),
|
||||||
@SerializedName("modifier" ) var modifier : String? = null,
|
@SerializedName("modifier") var modifier: String = "",
|
||||||
@SerializedName("type" ) var type : String? = null
|
@SerializedName("type") var type: String = "",
|
||||||
|
@SerializedName("exit") var exit: Int = 0,
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.kouros.navigation.data.osrm
|
package com.kouros.navigation.data.osrm
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
import com.kouros.navigation.data.NavigationRepository
|
||||||
import com.kouros.navigation.data.SearchFilter
|
import com.kouros.navigation.data.SearchFilter
|
||||||
@@ -8,11 +9,29 @@ private const val routeUrl = "https://kouros-online.de/osrm/route/v1/driving/"
|
|||||||
|
|
||||||
class OsrmRepository : NavigationRepository() {
|
class OsrmRepository : NavigationRepository() {
|
||||||
override fun getRoute(
|
override fun getRoute(
|
||||||
|
context: Context,
|
||||||
currentLocation: Location,
|
currentLocation: Location,
|
||||||
location: Location,
|
location: Location,
|
||||||
|
carOrientation: Float,
|
||||||
searchFilter: SearchFilter
|
searchFilter: SearchFilter
|
||||||
): String {
|
): String {
|
||||||
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true"
|
|
||||||
return fetchUrl(routeUrl + routeLocation, true)
|
var exclude = ""
|
||||||
|
if (searchFilter.avoidMotorway) {
|
||||||
|
exclude = "&exclude=motorway"
|
||||||
|
}
|
||||||
|
if (searchFilter.avoidTollway) {
|
||||||
|
exclude = "$exclude&exclude=toll"
|
||||||
|
}
|
||||||
|
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true&alternatives=0"
|
||||||
|
return fetchUrl(routeUrl + routeLocation + exclude, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTraffic(
|
||||||
|
context: Context,
|
||||||
|
location: Location,
|
||||||
|
carOrientation: Float
|
||||||
|
): String {
|
||||||
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName
|
|||||||
|
|
||||||
data class OsrmResponse (
|
data class OsrmResponse (
|
||||||
|
|
||||||
@SerializedName("code" ) var code : String? = null,
|
@SerializedName("code" ) var code : String = "",
|
||||||
@SerializedName("routes" ) var routes : ArrayList<Routes> = arrayListOf(),
|
@SerializedName("routes" ) var routes : ArrayList<Routes> = arrayListOf(),
|
||||||
@SerializedName("waypoints" ) var waypoints : ArrayList<Waypoints> = arrayListOf()
|
@SerializedName("waypoints" ) var waypoints : ArrayList<Waypoints> = arrayListOf()
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,88 @@
|
|||||||
package com.kouros.navigation.data.osrm
|
package com.kouros.navigation.data.osrm
|
||||||
|
|
||||||
import com.kouros.navigation.data.Route
|
import com.kouros.navigation.data.Route
|
||||||
|
import com.kouros.navigation.data.RouteEngine
|
||||||
|
import com.kouros.navigation.data.route.Intersection
|
||||||
|
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.Maneuver as RouteManeuver
|
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
|
||||||
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.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
|
||||||
|
|
||||||
class OsrmRoute {
|
class OsrmRoute {
|
||||||
|
|
||||||
fun mapToOsrm(routeJson: OsrmResponse, builder: Route.Builder) {
|
fun mapToRoute(routeJson: OsrmResponse, builder: Route.Builder) {
|
||||||
val waypoints = mutableListOf<List<Double>>()
|
|
||||||
val summary = Summary()
|
val routes = mutableListOf<com.kouros.navigation.data.route.Routes>()
|
||||||
summary.distance = routeJson.routes.first().distance!!
|
|
||||||
summary.duration = routeJson.routes.first().duration!!
|
|
||||||
val steps = mutableListOf<Step>()
|
|
||||||
var stepIndex = 0
|
var stepIndex = 0
|
||||||
routeJson.routes.first().legs.first().steps.forEach {
|
routeJson.routes.forEach { route ->
|
||||||
if (it.maneuver != null) {
|
val legs = mutableListOf<Leg>()
|
||||||
val points = decodePolyline(it.geometry!!, 5)
|
val waypoints = mutableListOf<List<Double>>()
|
||||||
waypoints.addAll(points)
|
val summary = Summary(route.duration, route.distance / 1000)
|
||||||
val maneuver = RouteManeuver(
|
route.legs.forEach { leg ->
|
||||||
bearingBefore = it.maneuver!!.bearingBefore ?: 0,
|
val steps = mutableListOf<Step>()
|
||||||
bearingAfter = it.maneuver!!.bearingAfter ?: 0,
|
leg.steps.forEach { step ->
|
||||||
type = convertType(it.maneuver!!),
|
val intersections = mutableListOf<Intersection>()
|
||||||
waypoints = points
|
val points = decodePolyline(step.geometry, 5)
|
||||||
)
|
waypoints.addAll(points)
|
||||||
val step = Step( index = stepIndex, name = it.name!!, distance = it.distance!!, duration = it.duration!!, maneuver = maneuver)
|
val maneuver = RouteManeuver(
|
||||||
steps.add(step)
|
bearingBefore = step.maneuver.bearingBefore,
|
||||||
stepIndex += 1
|
bearingAfter = step.maneuver.bearingAfter,
|
||||||
|
type = convertType(step.maneuver),
|
||||||
|
waypoints = points,
|
||||||
|
exit = step.maneuver.exit,
|
||||||
|
location = location(
|
||||||
|
step.maneuver.location[0],
|
||||||
|
step.maneuver.location[1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
step.intersections.forEach { it2 ->
|
||||||
|
if (it2.location[0] != 0.0) {
|
||||||
|
val lanes = mutableListOf<Lane>()
|
||||||
|
it2.lanes.forEach { it3 ->
|
||||||
|
if (it3.indications.isNotEmpty() && it3.indications.first() != "none") {
|
||||||
|
val lane = Lane(
|
||||||
|
location(it2.location[0], it2.location[1]),
|
||||||
|
it3.valid,
|
||||||
|
it3.indications
|
||||||
|
)
|
||||||
|
lanes.add(lane)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
intersections.add(Intersection(it2.location, lanes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val step = Step(
|
||||||
|
index = stepIndex,
|
||||||
|
street = step.name,
|
||||||
|
distance = step.distance / 1000,
|
||||||
|
duration = step.duration,
|
||||||
|
maneuver = maneuver,
|
||||||
|
//intersection = intersections
|
||||||
|
)
|
||||||
|
steps.add(step)
|
||||||
|
stepIndex += 1
|
||||||
|
}
|
||||||
|
legs.add(Leg(steps))
|
||||||
}
|
}
|
||||||
|
val routeGeoJson = createLineStringCollection(waypoints)
|
||||||
|
val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
|
||||||
|
val newRoute = com.kouros.navigation.data.route.Routes(
|
||||||
|
legs,
|
||||||
|
summary,
|
||||||
|
routeGeoJson,
|
||||||
|
centerLocation = centerLocation,
|
||||||
|
waypoints = waypoints
|
||||||
|
)
|
||||||
|
routes.add(newRoute)
|
||||||
}
|
}
|
||||||
val leg = Leg(steps)
|
|
||||||
builder
|
builder
|
||||||
.routeType(1)
|
.routeType(RouteEngine.OSRM.ordinal)
|
||||||
.summary(summary)
|
.routes(routes)
|
||||||
.routeGeoJson(createLineStringCollection(waypoints))
|
|
||||||
.legs(listOf(leg))
|
|
||||||
.waypoints(waypoints.toList())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertType(maneuver: Maneuver): Int {
|
fun convertType(maneuver: Maneuver): Int {
|
||||||
@@ -47,38 +91,138 @@ class OsrmRoute {
|
|||||||
ManeuverType.depart.value -> {
|
ManeuverType.depart.value -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART
|
||||||
}
|
}
|
||||||
|
|
||||||
ManeuverType.arrive.value -> {
|
ManeuverType.arrive.value -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION
|
||||||
|
if (maneuver.modifier == "right") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_RIGHT
|
||||||
|
}
|
||||||
|
if (maneuver.modifier == "left") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_LEFT
|
||||||
|
}
|
||||||
|
if (maneuver.modifier == "straight") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_STRAIGHT
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ManeuverType.continue_.value -> {
|
ManeuverType.continue_.value -> {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
|
||||||
}
|
|
||||||
ManeuverType.turn.value -> {
|
|
||||||
if (maneuver.modifier == "right") {
|
if (maneuver.modifier == "right") {
|
||||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
|
||||||
}
|
}
|
||||||
|
if (maneuver.modifier == "left") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.newName.value -> {
|
||||||
|
if (maneuver.modifier == "straight") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
|
||||||
|
}
|
||||||
|
if (maneuver.modifier == "slight right") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
||||||
|
}
|
||||||
|
if (maneuver.modifier == "slight left") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.turn.value,
|
||||||
|
ManeuverType.endOfRoad.value -> {
|
||||||
|
if (maneuver.modifier == "right") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
|
||||||
|
}
|
||||||
|
if (maneuver.modifier == "left") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
|
||||||
|
}
|
||||||
|
if (maneuver.modifier == "straight") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.endOfRoad.value,
|
||||||
|
ManeuverType.onRamp.value
|
||||||
|
-> {
|
||||||
|
if (maneuver.modifier == "left") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
|
||||||
|
}
|
||||||
|
if (maneuver.modifier == "slight right") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
||||||
|
}
|
||||||
|
if (maneuver.modifier == "slight left") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.offRamp.value
|
||||||
|
-> {
|
||||||
|
if (maneuver.modifier == "slight right") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
||||||
|
}
|
||||||
|
if (maneuver.modifier == "right") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
|
||||||
|
}
|
||||||
|
if (maneuver.modifier == "slight left") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
|
||||||
|
}
|
||||||
|
if (maneuver.modifier == "left") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.fork.value
|
||||||
|
-> {
|
||||||
|
if (maneuver.modifier == "slight left") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
|
||||||
|
}
|
||||||
|
if (maneuver.modifier == "slight right") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.merge.value
|
||||||
|
-> {
|
||||||
|
if (maneuver.modifier == "slight left") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
|
||||||
|
}
|
||||||
|
if (maneuver.modifier == "slight right") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ManeuverType.roundAbout.value
|
||||||
|
-> {
|
||||||
|
if (maneuver.modifier == "right") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ManeuverType.exitRoundabout.value
|
||||||
|
-> {
|
||||||
|
if (maneuver.modifier == "right") {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_EXIT_CCW
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newType
|
return newType
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
enum class ManeuverType(val value: String) {
|
|
||||||
turn("turn"),
|
|
||||||
depart("depart"),
|
|
||||||
arrive("arrive"),
|
|
||||||
merge("merge"),
|
|
||||||
onRamp("on ramp"),
|
|
||||||
offRamp("off ramp"),
|
|
||||||
fork("fork"),
|
|
||||||
endOfRoad("end of road"),
|
|
||||||
continue_("continue"),
|
|
||||||
roundAbout("roundabout"),
|
|
||||||
rotary("rotary"),
|
|
||||||
roundaboutTurn("roundabout turn"),
|
|
||||||
notification("notification"),
|
|
||||||
exitRoundabout("exit roundabout"),
|
|
||||||
exitRotary("exit rotary")
|
|
||||||
|
|
||||||
|
enum class ManeuverType(val value: String) {
|
||||||
|
turn("turn"),
|
||||||
|
depart("depart"),
|
||||||
|
arrive("arrive"),
|
||||||
|
merge("merge"),
|
||||||
|
onRamp("on ramp"),
|
||||||
|
offRamp("off ramp"),
|
||||||
|
fork("fork"),
|
||||||
|
endOfRoad("end of road"),
|
||||||
|
continue_("continue"),
|
||||||
|
roundAbout("roundabout"),
|
||||||
|
rotary("rotary"),
|
||||||
|
roundaboutTurn("roundabout turn"),
|
||||||
|
notification("notification"),
|
||||||
|
exitRoundabout("exit roundabout"),
|
||||||
|
exitRotary("exit rotary"),
|
||||||
|
newName("new name"),
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,8 +8,8 @@ data class Routes (
|
|||||||
@SerializedName("legs" ) var legs : ArrayList<Legs> = arrayListOf(),
|
@SerializedName("legs" ) var legs : ArrayList<Legs> = arrayListOf(),
|
||||||
@SerializedName("weight_name" ) var weightName : String? = null,
|
@SerializedName("weight_name" ) var weightName : String? = null,
|
||||||
@SerializedName("geometry" ) var geometry : String? = null,
|
@SerializedName("geometry" ) var geometry : String? = null,
|
||||||
@SerializedName("weight" ) var weight : Double? = null,
|
@SerializedName("weight" ) var weight : Double = 0.0,
|
||||||
@SerializedName("duration" ) var duration : Double? = null,
|
@SerializedName("duration" ) var duration : Double = 0.0,
|
||||||
@SerializedName("distance" ) var distance : Double? = null
|
@SerializedName("distance" ) var distance : Double = 0.0
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -6,13 +6,13 @@ import com.google.gson.annotations.SerializedName
|
|||||||
data class Steps (
|
data class Steps (
|
||||||
|
|
||||||
@SerializedName("intersections" ) var intersections : ArrayList<Intersections> = arrayListOf(),
|
@SerializedName("intersections" ) var intersections : ArrayList<Intersections> = arrayListOf(),
|
||||||
@SerializedName("driving_side" ) var drivingSide : String? = null,
|
@SerializedName("driving_side" ) var drivingSide : String = "",
|
||||||
@SerializedName("geometry" ) var geometry : String? = null,
|
@SerializedName("geometry" ) var geometry : String = "",
|
||||||
@SerializedName("maneuver" ) var maneuver : Maneuver? = Maneuver(),
|
@SerializedName("maneuver" ) val maneuver : Maneuver = Maneuver(),
|
||||||
@SerializedName("name" ) var name : String? = null,
|
@SerializedName("name" ) var name : String = "",
|
||||||
@SerializedName("mode" ) var mode : String? = null,
|
@SerializedName("mode" ) var mode : String = "",
|
||||||
@SerializedName("weight" ) var weight : Double? = null,
|
@SerializedName("weight" ) var weight : Double = 0.0,
|
||||||
@SerializedName("duration" ) var duration : Double? = null,
|
@SerializedName("duration" ) var duration : Double = 0.0,
|
||||||
@SerializedName("distance" ) var distance : Double? = null
|
@SerializedName("distance" ) var distance : Double = 0.0,
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -5,9 +5,9 @@ import com.google.gson.annotations.SerializedName
|
|||||||
|
|
||||||
data class Waypoints (
|
data class Waypoints (
|
||||||
|
|
||||||
@SerializedName("hint" ) var hint : String? = null,
|
@SerializedName("hint" ) var hint : String = "",
|
||||||
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(),
|
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(),
|
||||||
@SerializedName("name" ) var name : String? = null,
|
@SerializedName("name" ) var name : String = "",
|
||||||
@SerializedName("distance" ) var distance : Double? = null
|
@SerializedName("distance" ) var distance : Double = 0.0,
|
||||||
|
|
||||||
)
|
)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -5,10 +5,10 @@ import com.google.gson.annotations.SerializedName
|
|||||||
|
|
||||||
data class Elements (
|
data class Elements (
|
||||||
|
|
||||||
@SerializedName("type" ) var type : String? = null,
|
@SerializedName("type" ) var type : String = "",
|
||||||
@SerializedName("id" ) var id : Long? = null,
|
@SerializedName("id" ) var id : Long = 0,
|
||||||
@SerializedName("lat" ) var lat : Double? = null,
|
@SerializedName("lat" ) var lat : Double = 0.0,
|
||||||
@SerializedName("lon" ) var lon : Double? = null,
|
@SerializedName("lon" ) var lon : Double = 0.0,
|
||||||
@SerializedName("tags" ) var tags : Tags = Tags(),
|
@SerializedName("tags" ) var tags : Tags = Tags(),
|
||||||
var distance : Double = 0.0
|
var distance : Double = 0.0
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ package com.kouros.navigation.data.overpass
|
|||||||
|
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.kouros.navigation.utils.GeoUtils.getOverpassBbox
|
import com.kouros.navigation.utils.GeoUtils.getBoundingBox
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.io.OutputStreamWriter
|
import java.io.OutputStreamWriter
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
@@ -14,7 +13,7 @@ class Overpass {
|
|||||||
val overpassUrl = "https://kouros-online.de/overpass/interpreter"
|
val overpassUrl = "https://kouros-online.de/overpass/interpreter"
|
||||||
|
|
||||||
|
|
||||||
fun getAround(radius: Int, linestring: String) : List<Elements> {
|
fun getAround(radius: Int, linestring: String): List<Elements> {
|
||||||
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
|
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
|
||||||
httpURLConnection.requestMethod = "POST"
|
httpURLConnection.requestMethod = "POST"
|
||||||
httpURLConnection.setRequestProperty(
|
httpURLConnection.setRequestProperty(
|
||||||
@@ -41,7 +40,7 @@ class Overpass {
|
|||||||
location: Location,
|
location: Location,
|
||||||
radius: Double
|
radius: Double
|
||||||
): List<Elements> {
|
): List<Elements> {
|
||||||
val boundingBox = getOverpassBbox(location, radius)
|
val boundingBox = getBoundingBox(location.latitude, location.longitude, radius)
|
||||||
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
|
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
|
||||||
httpURLConnection.requestMethod = "POST"
|
httpURLConnection.requestMethod = "POST"
|
||||||
httpURLConnection.setRequestProperty(
|
httpURLConnection.setRequestProperty(
|
||||||
@@ -58,24 +57,29 @@ class Overpass {
|
|||||||
| node[$type=$category]
|
| node[$type=$category]
|
||||||
| ($boundingBox);
|
| ($boundingBox);
|
||||||
|);
|
|);
|
||||||
|
|(._;>;);
|
||||||
|out body;
|
|out body;
|
||||||
""".trimMargin()
|
""".trimMargin()
|
||||||
return overpassApi(httpURLConnection, searchQuery)
|
return overpassApi(httpURLConnection, searchQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun overpassApi(httpURLConnection: HttpURLConnection, searchQuery: String) : List<Elements> {
|
fun overpassApi(httpURLConnection: HttpURLConnection, searchQuery: String): List<Elements> {
|
||||||
val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream)
|
try {
|
||||||
outputStreamWriter.write(searchQuery)
|
val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream)
|
||||||
outputStreamWriter.flush()
|
outputStreamWriter.write(searchQuery)
|
||||||
// Check if the connection is successful
|
outputStreamWriter.flush()
|
||||||
val responseCode = httpURLConnection.responseCode
|
// Check if the connection is successful
|
||||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
val responseCode = httpURLConnection.responseCode
|
||||||
val response = httpURLConnection.inputStream.bufferedReader()
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
.use { it.readText() } // defaults to UTF-8
|
val response = httpURLConnection.inputStream.bufferedReader()
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
.use { it.readText() } // defaults to UTF-8
|
||||||
val overpass = gson.fromJson(response, Amenity::class.java)
|
val gson = GsonBuilder().serializeNulls().create()
|
||||||
// println("Overpass: $response")
|
val overpass = gson.fromJson(response, Amenity::class.java)
|
||||||
return overpass.elements
|
return overpass.elements
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
}
|
}
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ data class Tags(
|
|||||||
@SerializedName("ref") var ref: String? = null,
|
@SerializedName("ref") var ref: String? = null,
|
||||||
@SerializedName("socket:type2") var socketType2: String? = null,
|
@SerializedName("socket:type2") var socketType2: String? = null,
|
||||||
@SerializedName("socket:type2:output") var socketType2Output: String? = null,
|
@SerializedName("socket:type2:output") var socketType2Output: String? = null,
|
||||||
@SerializedName("maxspeed") var maxspeed: String? = null,
|
@SerializedName("maxspeed") var maxspeed: String = "0",
|
||||||
@SerializedName("direction") var direction: String? = null,
|
@SerializedName("direction") var direction: String? = null,
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.kouros.navigation.data.route
|
||||||
|
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
|
data class Intersection(
|
||||||
|
val location: List<Double> = listOf(0.0, 0.0),
|
||||||
|
val lane : List<Lane> = Collections.emptyList<Lane>(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.kouros.navigation.data.route
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
|
|
||||||
|
data class Lane (
|
||||||
|
val location: Location,
|
||||||
|
val valid: Boolean,
|
||||||
|
var indications: List<String>,
|
||||||
|
)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.kouros.navigation.data.route
|
package com.kouros.navigation.data.route
|
||||||
|
|
||||||
data class Leg(
|
data class Leg(
|
||||||
var steps : List<Step> = arrayListOf()
|
var steps : List<Step> = arrayListOf(),
|
||||||
|
var intersection: List<Intersection> = arrayListOf()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
package com.kouros.navigation.data.route
|
package com.kouros.navigation.data.route
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
|
|
||||||
data class Maneuver(
|
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<List<Double>>,
|
||||||
|
val location: Location,
|
||||||
|
val exit: Int = 0,
|
||||||
|
val street: String = "",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.kouros.navigation.data.route
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
|
|
||||||
|
class Routes(
|
||||||
|
val legs: List<Leg> = arrayListOf(),
|
||||||
|
val summary: Summary,
|
||||||
|
val routeGeoJson: String,
|
||||||
|
val centerLocation: Location = location(0.0, 0.0),
|
||||||
|
val waypoints: List<List<Double>>,
|
||||||
|
)
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
package com.kouros.navigation.data.route
|
package com.kouros.navigation.data.route
|
||||||
|
|
||||||
class Step(
|
import android.location.Location
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
|
|
||||||
|
data class Step(
|
||||||
var index : Int = 0,
|
var index : Int = 0,
|
||||||
var waypointIndex : Int = 0,
|
var waypointIndex : Int = 0,
|
||||||
|
var wayPointLocation : Location = location(0.0,0.0),
|
||||||
val maneuver: Maneuver,
|
val maneuver: Maneuver,
|
||||||
val duration: Double = 0.0,
|
val duration: Double = 0.0,
|
||||||
val distance: Double = 0.0,
|
val distance: Double = 0.0,
|
||||||
val name : String = "",
|
val street : String = "",
|
||||||
|
val intersection: List<Intersection> = mutableListOf(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.kouros.navigation.data.route
|
package com.kouros.navigation.data.route
|
||||||
|
|
||||||
data class Summary(
|
data class Summary(
|
||||||
|
// sec
|
||||||
var duration : Double = 0.0,
|
var duration : Double = 0.0,
|
||||||
|
// km
|
||||||
var distance : Double = 0.0,
|
var distance : Double = 0.0,
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
data class Cause(
|
||||||
|
val mainCauseCode: Int
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
|
||||||
|
data class Events (
|
||||||
|
|
||||||
|
@SerializedName("description" ) var description : String? = null
|
||||||
|
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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()
|
||||||
|
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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()
|
||||||
|
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
data class Guidance(
|
||||||
|
val instructionGroups: List<InstructionGroup>,
|
||||||
|
val instructions: List<Instruction>
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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()
|
||||||
|
|
||||||
|
)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
data class Instruction(
|
||||||
|
val combinedMessage: String,
|
||||||
|
val countryCode: String,
|
||||||
|
val drivingSide: String,
|
||||||
|
val instructionType: String,
|
||||||
|
val junctionType: String,
|
||||||
|
val maneuver: String,
|
||||||
|
val message: String,
|
||||||
|
val point: Point,
|
||||||
|
val pointIndex: Int,
|
||||||
|
val possibleCombineWithNext: Boolean,
|
||||||
|
val roadNumbers: List<String>,
|
||||||
|
val routeOffsetInMeters: Int,
|
||||||
|
val signpostText: String,
|
||||||
|
val street: String? = "",
|
||||||
|
val travelTimeInSeconds: Int,
|
||||||
|
val turnAngleInDecimalDegrees: Int,
|
||||||
|
val exitNumber: String? = "0",
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
data class InstructionGroup(
|
||||||
|
val firstInstructionIndex: Int,
|
||||||
|
val groupLengthInMeters: Int,
|
||||||
|
val groupMessage: String,
|
||||||
|
val lastInstructionIndex: Int
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
data class Lane(
|
||||||
|
val directions: List<String>,
|
||||||
|
val follow: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
data class Leg(
|
||||||
|
val encodedPolyline: String,
|
||||||
|
val encodedPolylinePrecision: Int,
|
||||||
|
val summary: SummaryX
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
data class Point(
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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()
|
||||||
|
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
data class Route(
|
||||||
|
val guidance: Guidance,
|
||||||
|
val legs: List<Leg>,
|
||||||
|
val sections: List<Section>?,
|
||||||
|
val summary: SummaryX
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
|
data class Section(
|
||||||
|
val delayInSeconds: Int,
|
||||||
|
val effectiveSpeedInKmh: Int,
|
||||||
|
val endPointIndex: Int,
|
||||||
|
val eventId: String,
|
||||||
|
val laneSeparators: List<String>,
|
||||||
|
val lanes: List<Lane>? = Collections.emptyList<Lane>(),
|
||||||
|
val magnitudeOfDelay: Int,
|
||||||
|
val sectionType: String,
|
||||||
|
val simpleCategory: String,
|
||||||
|
val startPointIndex: Int,
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
data class SummaryX(
|
||||||
|
val arrivalTime: String,
|
||||||
|
val departureTime: String,
|
||||||
|
val lengthInMeters: Int,
|
||||||
|
val trafficDelayInSeconds: Int,
|
||||||
|
val trafficLengthInMeters: Int,
|
||||||
|
val travelTimeInSeconds: Int
|
||||||
|
)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.location.Location
|
||||||
|
import com.kouros.data.R
|
||||||
|
import com.kouros.navigation.data.NavigationRepository
|
||||||
|
import com.kouros.navigation.data.SearchFilter
|
||||||
|
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
|
||||||
|
|
||||||
|
|
||||||
|
private const val routeUrl = "https://api.tomtom.com/routing/1/calculateRoute/"
|
||||||
|
|
||||||
|
val tomtomApiKey = "678k5v6940cSXXIS5oD92qIrDgW3RBZ3"
|
||||||
|
|
||||||
|
val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incidentDetails"
|
||||||
|
|
||||||
|
|
||||||
|
private val tomtomFields =
|
||||||
|
"{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}"
|
||||||
|
|
||||||
|
const val useAsset = false
|
||||||
|
|
||||||
|
class TomTomRepository : NavigationRepository() {
|
||||||
|
override fun getRoute(
|
||||||
|
context: Context,
|
||||||
|
currentLocation: Location,
|
||||||
|
location: Location,
|
||||||
|
carOrientation: Float,
|
||||||
|
searchFilter: SearchFilter
|
||||||
|
): String {
|
||||||
|
if (useAsset) {
|
||||||
|
val routeJson = context.resources.openRawResource(R.raw.tomom_routing)
|
||||||
|
val routeJsonString = routeJson.bufferedReader().use { it.readText() }
|
||||||
|
return routeJsonString
|
||||||
|
}
|
||||||
|
val url =
|
||||||
|
routeUrl + "${currentLocation.latitude},${currentLocation.longitude}:${location.latitude},${location.longitude}" +
|
||||||
|
"/json?vehicleHeading=90§ionType=traffic&report=effectiveSettings&routeType=eco" +
|
||||||
|
"&traffic=true&avoid=unpavedRoads&travelMode=car" +
|
||||||
|
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
|
||||||
|
"&instructionsType=text&language=en-GB§ionType=lanes" +
|
||||||
|
"&routeRepresentation=encodedPolyline" +
|
||||||
|
"&vehicleEngineType=combustion&key=$tomtomApiKey"
|
||||||
|
return fetchUrl(
|
||||||
|
url,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTraffic(context: Context, location: Location, carOrientation: Float): String {
|
||||||
|
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
|
||||||
|
return if (useAsset) {
|
||||||
|
val trafficJson = context.resources.openRawResource(R.raw.tomtom_traffic)
|
||||||
|
trafficJson.bufferedReader().use { it.readText() }
|
||||||
|
} else {
|
||||||
|
val trafficResult = fetchUrl(
|
||||||
|
"$tomtomTrafficUrl?key=$tomtomApiKey&bbox=$bbox&fields=$tomtomFields&language=en-GB&timeValidityFilter=present",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
trafficResult.replace(
|
||||||
|
"{\"incidents\":",
|
||||||
|
"{\"type\": \"FeatureCollection\", \"features\":"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
data class TomTomResponse(
|
||||||
|
val formatVersion: String,
|
||||||
|
val routes: List<Route>
|
||||||
|
)
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
import com.kouros.navigation.data.Route
|
||||||
|
import com.kouros.navigation.data.RouteEngine
|
||||||
|
import com.kouros.navigation.data.route.Intersection
|
||||||
|
import com.kouros.navigation.data.route.Lane
|
||||||
|
import com.kouros.navigation.data.route.Leg
|
||||||
|
import com.kouros.navigation.data.route.Step
|
||||||
|
import com.kouros.navigation.data.route.Summary
|
||||||
|
import com.kouros.navigation.utils.GeoUtils.createCenterLocation
|
||||||
|
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
|
||||||
|
import com.kouros.navigation.utils.GeoUtils.decodePolyline
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
|
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
|
||||||
|
|
||||||
|
|
||||||
|
class TomTomRoute {
|
||||||
|
|
||||||
|
fun mapToRoute(routeJson: TomTomResponse, builder: Route.Builder) {
|
||||||
|
val routes = mutableListOf<com.kouros.navigation.data.route.Routes>()
|
||||||
|
routeJson.routes.forEach { route ->
|
||||||
|
val waypoints = mutableListOf<List<Double>>()
|
||||||
|
val legs = mutableListOf<Leg>()
|
||||||
|
var stepIndex = 0
|
||||||
|
var points = listOf<List<Double>>()
|
||||||
|
val summary = Summary(
|
||||||
|
route.summary.travelTimeInSeconds.toDouble(),
|
||||||
|
route.summary.lengthInMeters.toDouble()
|
||||||
|
)
|
||||||
|
route.legs.forEach { leg ->
|
||||||
|
points = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision)
|
||||||
|
waypoints.addAll(points)
|
||||||
|
}
|
||||||
|
var stepDistance = 0.0
|
||||||
|
var stepDuration = 0.0
|
||||||
|
val allIntersections = mutableListOf<Intersection>()
|
||||||
|
val steps = mutableListOf<Step>()
|
||||||
|
var lastPointIndex = 0
|
||||||
|
for (index in 1..< route.guidance.instructions.size) {
|
||||||
|
val lastInstruction = route.guidance.instructions[index-1]
|
||||||
|
val instruction = route.guidance.instructions[index]
|
||||||
|
val street = lastInstruction.street ?: ""
|
||||||
|
val maneuverStreet = instruction.street ?: ""
|
||||||
|
val maneuver = RouteManeuver(
|
||||||
|
bearingBefore = 0,
|
||||||
|
bearingAfter = 0,
|
||||||
|
type = convertType(instruction.maneuver),
|
||||||
|
waypoints = points.subList(
|
||||||
|
lastPointIndex,
|
||||||
|
instruction.pointIndex+1,
|
||||||
|
),
|
||||||
|
exit = exitNumber(instruction),
|
||||||
|
location = location(
|
||||||
|
instruction.point.longitude, instruction.point.latitude
|
||||||
|
),
|
||||||
|
street = maneuverStreet
|
||||||
|
)
|
||||||
|
|
||||||
|
lastPointIndex = instruction.pointIndex
|
||||||
|
val intersections = mutableListOf<Intersection>()
|
||||||
|
route.sections?.forEach { section ->
|
||||||
|
val lanes = mutableListOf<Lane>()
|
||||||
|
var startIndex = 0
|
||||||
|
if (section.startPointIndex <= instruction.pointIndex - 3
|
||||||
|
&& instruction.pointIndex <= section.endPointIndex
|
||||||
|
) {
|
||||||
|
section.lanes?.forEach { itLane ->
|
||||||
|
val lane = Lane(
|
||||||
|
location(
|
||||||
|
waypoints[section.startPointIndex][0],
|
||||||
|
waypoints[section.startPointIndex][1]
|
||||||
|
),
|
||||||
|
itLane.directions.first() == itLane.follow,
|
||||||
|
itLane.directions
|
||||||
|
)
|
||||||
|
startIndex = section.startPointIndex
|
||||||
|
lanes.add(lane)
|
||||||
|
}
|
||||||
|
intersections.add(Intersection(waypoints[startIndex], lanes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allIntersections.addAll(intersections)
|
||||||
|
stepDistance = route.guidance.instructions[index].routeOffsetInMeters - stepDistance
|
||||||
|
stepDuration = route.guidance.instructions[index].travelTimeInSeconds - stepDuration
|
||||||
|
val step = Step(
|
||||||
|
index = stepIndex,
|
||||||
|
street = street,
|
||||||
|
distance = stepDistance,
|
||||||
|
duration = stepDuration,
|
||||||
|
maneuver = maneuver,
|
||||||
|
intersection = intersections
|
||||||
|
)
|
||||||
|
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
|
||||||
|
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
|
||||||
|
steps.add(step)
|
||||||
|
stepIndex += 1
|
||||||
|
}
|
||||||
|
legs.add(Leg(steps, allIntersections))
|
||||||
|
val routeGeoJson = createLineStringCollection(waypoints)
|
||||||
|
val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
|
||||||
|
val newRoute = com.kouros.navigation.data.route.Routes(
|
||||||
|
legs,
|
||||||
|
summary,
|
||||||
|
routeGeoJson,
|
||||||
|
centerLocation = centerLocation,
|
||||||
|
waypoints = waypoints
|
||||||
|
)
|
||||||
|
routes.add(newRoute)
|
||||||
|
}
|
||||||
|
builder
|
||||||
|
.routeType(RouteEngine.TOMTOM.ordinal)
|
||||||
|
.routes(routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertType(type: String): Int {
|
||||||
|
var newType = 0
|
||||||
|
when (type) {
|
||||||
|
"DEPART" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART
|
||||||
|
}
|
||||||
|
|
||||||
|
"ARRIVE" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION
|
||||||
|
}
|
||||||
|
|
||||||
|
"ARRIVE_LEFT" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
"ARRIVE_RIGHT" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
"STRAIGHT", "FOLLOW" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
"KEEP_RIGHT" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_KEEP_RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
"BEAR_RIGHT" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
||||||
|
}
|
||||||
|
"BEAR_LEFT" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
"KEEP_LEFT" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_KEEP_LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
"TURN_LEFT" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
"TURN_RIGHT" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
"SHARP_LEFT" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SHARP_LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
"SHARP_RIGHT" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SHARP_RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
"ROUNDABOUT_RIGHT" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CCW
|
||||||
|
}
|
||||||
|
|
||||||
|
"ROUNDABOUT_LEFT" -> {
|
||||||
|
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exitNumber(
|
||||||
|
instruction: Instruction
|
||||||
|
): Int {
|
||||||
|
return if ( instruction.exitNumber == null
|
||||||
|
|| instruction.exitNumber.isEmpty()) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
instruction.exitNumber.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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()
|
||||||
|
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.kouros.navigation.data.tomtom
|
||||||
|
|
||||||
|
data class TrafficData (
|
||||||
|
var traffic : Traffic ,
|
||||||
|
var trafficData: String = ""
|
||||||
|
)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.kouros.navigation.data.valhalla
|
package com.kouros.navigation.data.valhalla
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import com.kouros.navigation.data.Locations
|
import com.kouros.navigation.data.Locations
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
import com.kouros.navigation.data.NavigationRepository
|
||||||
@@ -12,11 +13,29 @@ private const val routeUrl = "https://kouros-online.de/valhalla/route?json="
|
|||||||
|
|
||||||
class ValhallaRepository : NavigationRepository() {
|
class ValhallaRepository : NavigationRepository() {
|
||||||
|
|
||||||
override fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String {
|
override fun getRoute(
|
||||||
SearchFilter
|
context: Context,
|
||||||
|
currentLocation: Location,
|
||||||
|
location: Location,
|
||||||
|
carOrientation: Float,
|
||||||
|
searchFilter: SearchFilter
|
||||||
|
): String {
|
||||||
|
|
||||||
|
var exclude = ""
|
||||||
|
if (searchFilter.avoidMotorway) {
|
||||||
|
exclude = "&max_road_class='trunk'"
|
||||||
|
}
|
||||||
|
if (searchFilter.avoidTollway) {
|
||||||
|
exclude = "$exclude&exclude_toll=true"
|
||||||
|
}
|
||||||
|
|
||||||
val vLocation = listOf(
|
val vLocation = listOf(
|
||||||
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude, search_filter = searchFilter),
|
Locations(
|
||||||
Locations(lat = location.latitude, lon = location.longitude, search_filter = searchFilter)
|
lat = currentLocation.latitude,
|
||||||
|
lon = currentLocation.longitude,
|
||||||
|
search_filter = exclude
|
||||||
|
),
|
||||||
|
Locations(lat = location.latitude, lon = location.longitude, search_filter = exclude)
|
||||||
)
|
)
|
||||||
val valhallaLocation = ValhallaLocation(
|
val valhallaLocation = ValhallaLocation(
|
||||||
locations = vLocation,
|
locations = vLocation,
|
||||||
@@ -28,4 +47,12 @@ class ValhallaRepository : NavigationRepository() {
|
|||||||
val routeLocation = Json.encodeToString(valhallaLocation)
|
val routeLocation = Json.encodeToString(valhallaLocation)
|
||||||
return fetchUrl(routeUrl + routeLocation, true)
|
return fetchUrl(routeUrl + routeLocation, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getTraffic(
|
||||||
|
context: Context,
|
||||||
|
location: Location,
|
||||||
|
carOrientation: Float
|
||||||
|
): String {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,43 +1,98 @@
|
|||||||
package com.kouros.navigation.data.valhalla
|
package com.kouros.navigation.data.valhalla
|
||||||
|
|
||||||
|
import androidx.car.app.navigation.model.Maneuver
|
||||||
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.data.Route
|
import com.kouros.navigation.data.Route
|
||||||
|
import com.kouros.navigation.data.RouteEngine
|
||||||
import com.kouros.navigation.data.route.Leg
|
import com.kouros.navigation.data.route.Leg
|
||||||
import com.kouros.navigation.data.route.Maneuver
|
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
|
||||||
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.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
|
||||||
|
|
||||||
class ValhallaRoute {
|
class ValhallaRoute {
|
||||||
|
|
||||||
fun mapJsonToValhalla(routeJson: ValhallaResponse, builder: Route.Builder) {
|
fun mapToRoute(routeJson: ValhallaResponse, builder: Route.Builder) {
|
||||||
val waypoints = decodePolyline(routeJson.legs[0].shape)
|
val waypoints = decodePolyline(routeJson.legs[0].shape)
|
||||||
val summary = Summary()
|
val summary = Summary(routeJson.summaryValhalla.time, routeJson.summaryValhalla.length)
|
||||||
summary.distance = routeJson.summaryValhalla.length
|
|
||||||
summary.duration = routeJson.summaryValhalla.time
|
|
||||||
val steps = mutableListOf<Step>()
|
val steps = mutableListOf<Step>()
|
||||||
var stepIndex = 0
|
var stepIndex = 0
|
||||||
routeJson.legs[0].maneuvers.forEach {
|
routeJson.legs[0].maneuvers.forEach {
|
||||||
val maneuver = Maneuver(
|
val maneuver = RouteManeuver(
|
||||||
bearingBefore = 0,
|
bearingBefore = 0,
|
||||||
bearingAfter = it.bearingAfter,
|
bearingAfter = it.bearingAfter,
|
||||||
type = it.type,
|
//type = it.type,
|
||||||
waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1)
|
type = convertType(it),
|
||||||
|
waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1),
|
||||||
|
// TODO: calculate from ShapeIndex !
|
||||||
|
location = location(0.0, 0.0)
|
||||||
|
|
||||||
)
|
)
|
||||||
var name = ""
|
var name = ""
|
||||||
if (it.streetNames != null && it.streetNames.isNotEmpty()) {
|
if (it.streetNames != null && it.streetNames.isNotEmpty()) {
|
||||||
name = it.streetNames[0]
|
name = it.streetNames[0]
|
||||||
}
|
}
|
||||||
val step = Step( index = stepIndex, name = name, distance = it.length, duration = it.time, maneuver = maneuver)
|
val step = Step( index = stepIndex, street = name, distance = it.length, duration = it.time, maneuver = maneuver)
|
||||||
steps.add(step)
|
steps.add(step)
|
||||||
stepIndex += 1
|
stepIndex += 1
|
||||||
}
|
}
|
||||||
val leg = Leg(steps)
|
|
||||||
builder
|
builder
|
||||||
.routeType(1)
|
.routeType(RouteEngine.VALHALLA.ordinal)
|
||||||
.summary(summary)
|
// TODO
|
||||||
.routeGeoJson(createLineStringCollection(waypoints))
|
.routes(emptyList())
|
||||||
.legs(listOf(leg))
|
}
|
||||||
.waypoints(waypoints)
|
|
||||||
|
fun convertType(maneuver: Maneuvers): Int {
|
||||||
|
var newType = 0
|
||||||
|
when (maneuver.type) {
|
||||||
|
ManeuverType.None.value -> {
|
||||||
|
newType = Maneuver.TYPE_STRAIGHT
|
||||||
|
}
|
||||||
|
ManeuverType.Destination.value,
|
||||||
|
ManeuverType.DestinationRight.value,
|
||||||
|
ManeuverType.DestinationLeft.value,
|
||||||
|
-> {
|
||||||
|
newType = Maneuver.TYPE_DESTINATION
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.Right.value -> {
|
||||||
|
newType = Maneuver.TYPE_TURN_NORMAL_RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.Left.value -> {
|
||||||
|
newType = Maneuver.TYPE_TURN_NORMAL_LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.RampRight.value -> {
|
||||||
|
newType = Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.RampLeft.value -> {
|
||||||
|
newType = Maneuver.TYPE_OFF_RAMP_SLIGHT_LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.ExitRight.value -> {
|
||||||
|
newType = Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.StayRight.value -> {
|
||||||
|
newType = Maneuver.TYPE_KEEP_RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.StayLeft.value -> {
|
||||||
|
newType = Maneuver.TYPE_KEEP_LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.RoundaboutEnter.value -> {
|
||||||
|
newType = Maneuver.TYPE_ROUNDABOUT_ENTER_CCW
|
||||||
|
}
|
||||||
|
|
||||||
|
ManeuverType.RoundaboutExit.value -> {
|
||||||
|
newType = Maneuver.TYPE_ROUNDABOUT_EXIT_CCW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.kouros.navigation.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import com.kouros.data.R
|
||||||
|
import org.maplibre.compose.style.BaseStyle
|
||||||
|
|
||||||
|
class BaseStyleModel {
|
||||||
|
|
||||||
|
fun isDarkTheme(context: Context): Boolean {
|
||||||
|
return context.resources.configuration.uiMode and
|
||||||
|
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readStyle(context: Context, darkModeSettings: Int, isCarDarkMode: Boolean): BaseStyle.Json {
|
||||||
|
val liberty = when(darkModeSettings) {
|
||||||
|
0 -> context.resources.openRawResource(R.raw.liberty)
|
||||||
|
1 -> context.resources.openRawResource(R.raw.liberty_night)
|
||||||
|
else -> {
|
||||||
|
if (isDarkTheme(context) || isCarDarkMode) {
|
||||||
|
context.resources.openRawResource(R.raw.liberty_night)
|
||||||
|
} else {
|
||||||
|
context.resources.openRawResource(R.raw.liberty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val libertyString = liberty.bufferedReader().use { it.readText() }
|
||||||
|
val baseStyle = BaseStyle.Json(libertyString)
|
||||||
|
return baseStyle
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
package com.kouros.navigation.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.car.app.model.CarIcon
|
||||||
|
import androidx.car.app.navigation.model.LaneDirection
|
||||||
|
import androidx.car.app.navigation.model.Maneuver
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import com.kouros.data.R
|
||||||
|
import com.kouros.navigation.data.StepData
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.collections.forEach
|
||||||
|
|
||||||
|
class IconMapper() {
|
||||||
|
|
||||||
|
fun maneuverIcon(routeManeuverType: Int): Int {
|
||||||
|
var currentTurnIcon = R.drawable.ic_turn_name_change
|
||||||
|
when (routeManeuverType) {
|
||||||
|
Maneuver.TYPE_STRAIGHT -> {
|
||||||
|
currentTurnIcon = R.drawable.ic_turn_name_change
|
||||||
|
}
|
||||||
|
|
||||||
|
Maneuver.TYPE_DESTINATION,
|
||||||
|
Maneuver.TYPE_DESTINATION_RIGHT,
|
||||||
|
Maneuver.TYPE_DESTINATION_LEFT,
|
||||||
|
Maneuver.TYPE_DESTINATION_STRAIGHT
|
||||||
|
-> {
|
||||||
|
currentTurnIcon = R.drawable.ic_turn_destination
|
||||||
|
}
|
||||||
|
|
||||||
|
Maneuver.TYPE_TURN_NORMAL_RIGHT -> {
|
||||||
|
currentTurnIcon = R.drawable.ic_turn_normal_right
|
||||||
|
}
|
||||||
|
|
||||||
|
Maneuver.TYPE_TURN_NORMAL_LEFT -> {
|
||||||
|
currentTurnIcon = R.drawable.ic_turn_normal_left
|
||||||
|
}
|
||||||
|
|
||||||
|
Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT -> {
|
||||||
|
currentTurnIcon = R.drawable.ic_turn_slight_right
|
||||||
|
}
|
||||||
|
|
||||||
|
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> {
|
||||||
|
currentTurnIcon = R.drawable.ic_turn_slight_right
|
||||||
|
}
|
||||||
|
|
||||||
|
Maneuver.TYPE_KEEP_RIGHT -> {
|
||||||
|
currentTurnIcon = R.drawable.ic_turn_name_change
|
||||||
|
}
|
||||||
|
|
||||||
|
Maneuver.TYPE_KEEP_LEFT -> {
|
||||||
|
currentTurnIcon = R.drawable.ic_turn_name_change
|
||||||
|
}
|
||||||
|
|
||||||
|
Maneuver.TYPE_ROUNDABOUT_ENTER_CCW -> {
|
||||||
|
currentTurnIcon = R.drawable.ic_roundabout_ccw
|
||||||
|
}
|
||||||
|
|
||||||
|
Maneuver.TYPE_ROUNDABOUT_EXIT_CCW -> {
|
||||||
|
|
||||||
|
currentTurnIcon = R.drawable.ic_roundabout_ccw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentTurnIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun addLanes(stepData: StepData) : Int {
|
||||||
|
stepData.lane.forEach {
|
||||||
|
if (it.indications.isNotEmpty() && it.valid) {
|
||||||
|
Collections.sort<String>(it.indications)
|
||||||
|
var direction = ""
|
||||||
|
it.indications.forEach { it2 ->
|
||||||
|
direction = if (direction.isEmpty()) {
|
||||||
|
it2.trim()
|
||||||
|
} else {
|
||||||
|
"${direction}_${it2.trim()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val laneDirection = addLanes(direction, stepData)
|
||||||
|
return laneDirection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addLanes(direction: String, stepData: StepData): Int {
|
||||||
|
val laneDirection = when (direction.lowercase(Locale.getDefault())) {
|
||||||
|
"left_straight" -> {
|
||||||
|
when (stepData.currentManeuverType) {
|
||||||
|
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT
|
||||||
|
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
|
||||||
|
else
|
||||||
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"left" -> {
|
||||||
|
when (stepData.currentManeuverType) {
|
||||||
|
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT
|
||||||
|
else
|
||||||
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"straight" -> {
|
||||||
|
when (stepData.currentManeuverType) {
|
||||||
|
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
|
||||||
|
else
|
||||||
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"right" -> {
|
||||||
|
when (stepData.currentManeuverType) {
|
||||||
|
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
|
||||||
|
else
|
||||||
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"right_straight" -> {
|
||||||
|
when (stepData.currentManeuverType) {
|
||||||
|
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
|
||||||
|
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
|
||||||
|
else
|
||||||
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"left_slight", "slight_left" -> {
|
||||||
|
when (stepData.currentManeuverType) {
|
||||||
|
Maneuver.TYPE_TURN_SLIGHT_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT
|
||||||
|
else
|
||||||
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"right_slight", "slight_right" -> {
|
||||||
|
when (stepData.currentManeuverType) {
|
||||||
|
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
|
||||||
|
else
|
||||||
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
LaneDirection.SHAPE_UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return laneDirection
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createCarIcon(iconCompat: IconCompat): CarIcon {
|
||||||
|
return CarIcon.Builder(iconCompat).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createCarIconx(carContext: Context, @DrawableRes iconRes: Int): CarIcon {
|
||||||
|
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createLaneIcon(context: Context, stepData: StepData): IconCompat {
|
||||||
|
val bitmaps = mutableListOf<Bitmap>()
|
||||||
|
stepData.lane.forEach {
|
||||||
|
if (it.indications.isNotEmpty()) {
|
||||||
|
Collections.sort<String>(it.indications)
|
||||||
|
val resource = laneToResource(it.indications, stepData)
|
||||||
|
if (resource.isNotEmpty()) {
|
||||||
|
val id = resourceId(resource);
|
||||||
|
val bitMap = BitmapFactory.decodeResource(context.resources, id)
|
||||||
|
bitmaps.add(bitMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (bitmaps.isEmpty()) {
|
||||||
|
IconCompat.createWithResource(context, R.drawable.ic_close_white_24dp)
|
||||||
|
} else {
|
||||||
|
IconCompat.createWithBitmap(overlay(bitmaps = bitmaps))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun overlay(bitmaps: List<Bitmap>): Bitmap {
|
||||||
|
val matrix = Matrix()
|
||||||
|
if (bitmaps.size == 1) {
|
||||||
|
return bitmaps.first()
|
||||||
|
}
|
||||||
|
val bmOverlay = createBitmap(
|
||||||
|
bitmaps.first().getWidth() * (bitmaps.size * 1.5).toInt(),
|
||||||
|
bitmaps.first().getHeight(),
|
||||||
|
bitmaps.first().getConfig()!!
|
||||||
|
)
|
||||||
|
val canvas = Canvas(bmOverlay)
|
||||||
|
canvas.drawBitmap(bitmaps.first(), matrix, null)
|
||||||
|
var i = 0
|
||||||
|
bitmaps.forEach {
|
||||||
|
if (i > 0) {
|
||||||
|
matrix.setTranslate(i * 45F, 0F)
|
||||||
|
canvas.drawBitmap(it, matrix, null)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return bmOverlay
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun laneToResource(directions: List<String>, stepData: StepData): String {
|
||||||
|
var direction = ""
|
||||||
|
directions.forEach {
|
||||||
|
direction = if (direction.isEmpty()) {
|
||||||
|
it.trim()
|
||||||
|
} else {
|
||||||
|
"${direction}_${it.trim()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
direction = direction.lowercase()
|
||||||
|
return when (direction) {
|
||||||
|
"left_straight" -> {
|
||||||
|
when (stepData.currentManeuverType) {
|
||||||
|
Maneuver.TYPE_TURN_NORMAL_LEFT -> "left_o_straight_x"
|
||||||
|
Maneuver.TYPE_STRAIGHT -> "left_x_straight_o"
|
||||||
|
else
|
||||||
|
-> "left_x_straight_x"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"right_straight" -> {
|
||||||
|
when (stepData.currentManeuverType) {
|
||||||
|
Maneuver.TYPE_TURN_NORMAL_RIGHT -> "right_x_straight_x"
|
||||||
|
Maneuver.TYPE_STRAIGHT -> "right_x_straight_o"
|
||||||
|
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> "right_o_straight_o"
|
||||||
|
else
|
||||||
|
-> "right_x_straight_x"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_RIGHT) "${direction}_o" else "${direction}_x"
|
||||||
|
"left" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_LEFT) "${direction}_o" else "${direction}_x"
|
||||||
|
"straight" -> if (stepData.currentManeuverType == Maneuver.TYPE_STRAIGHT) "${direction}_o" else "${direction}_x"
|
||||||
|
"right_slight", "slight_right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_RIGHT) "${direction}_o" else "${direction}_x"
|
||||||
|
"left_slight", "slight_left" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_LEFT) "${direction}_o" else "${direction}_x"
|
||||||
|
else -> {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resourceId(
|
||||||
|
variableName: String,
|
||||||
|
): Int {
|
||||||
|
return when (variableName) {
|
||||||
|
"left_x" -> R.drawable.left_x
|
||||||
|
"left_o" -> R.drawable.left_o
|
||||||
|
"left_o_right_x" -> R.drawable.left_o_right_x
|
||||||
|
"right_x" -> R.drawable.right_x
|
||||||
|
"right_o" -> R.drawable.right_o
|
||||||
|
"slight_right_x" -> R.drawable.slight_right_x
|
||||||
|
"slight_right_o" -> R.drawable.slight_right_o
|
||||||
|
"slight_left_x" -> R.drawable.left_x
|
||||||
|
"straight_x" -> R.drawable.straight_x
|
||||||
|
"right_o_straight_x" -> R.drawable.right_o_straight_x
|
||||||
|
"right_x_straight_x" -> R.drawable.right_x_straight_x
|
||||||
|
"right_x_straight_o" -> R.drawable.right_x_straight_x
|
||||||
|
"straight_o" -> R.drawable.straight_o
|
||||||
|
"left_o_straight_x" -> R.drawable.left_o_straight_x
|
||||||
|
"left_x_straight_o" -> R.drawable.left_x_straight_o
|
||||||
|
else -> {
|
||||||
|
R.drawable.left_x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package com.kouros.navigation.model
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
|
import androidx.car.app.navigation.model.Step
|
||||||
|
import com.kouros.navigation.data.Constants.MAXIMUM_LOCATION_DISTANCE
|
||||||
|
import com.kouros.navigation.data.Constants.NEAREST_LOCATION_DISTANCE
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class RouteCalculator(var routeModel: RouteModel) {
|
||||||
|
|
||||||
|
var lastSpeedLocation: Location = location(0.0, 0.0)
|
||||||
|
|
||||||
|
var lastSpeedIndex: Int = 0
|
||||||
|
|
||||||
|
fun findStep(location: Location) {
|
||||||
|
var nearestDistance = MAXIMUM_LOCATION_DISTANCE
|
||||||
|
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
|
||||||
|
if (index >= routeModel.navState.route.currentStepIndex) {
|
||||||
|
for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
||||||
|
if (wayIndex >= step.waypointIndex) {
|
||||||
|
val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
|
||||||
|
if (distance < nearestDistance) {
|
||||||
|
nearestDistance = distance
|
||||||
|
routeModel.navState.route.currentStepIndex = step.index
|
||||||
|
step.waypointIndex = wayIndex
|
||||||
|
step.wayPointLocation = location(waypoint[0], waypoint[1])
|
||||||
|
routeModel.navState = routeModel.navState.copy(
|
||||||
|
routeBearing = routeModel.navState.lastLocation.bearingTo(location)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nearestDistance < NEAREST_LOCATION_DISTANCE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun travelLeftTime(): Double {
|
||||||
|
var timeLeft = 0.0
|
||||||
|
// time for next step until end step
|
||||||
|
for (i in routeModel.route.currentStepIndex + 1..<routeModel.curLeg.steps.size) {
|
||||||
|
val step = routeModel.curLeg.steps[i]
|
||||||
|
timeLeft += step.duration
|
||||||
|
}
|
||||||
|
// time for current step
|
||||||
|
val step = routeModel.route.currentStep()
|
||||||
|
val curTime = step.duration
|
||||||
|
val percent =
|
||||||
|
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
|
||||||
|
val time = curTime * percent / 100
|
||||||
|
timeLeft += time
|
||||||
|
return timeLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the current [Step] left distance in m. */
|
||||||
|
fun leftStepDistance(): Double {
|
||||||
|
val step = routeModel.route.currentStep()
|
||||||
|
var leftDistance = 0F
|
||||||
|
for (i in step.waypointIndex..<step.maneuver.waypoints.size - 1) {
|
||||||
|
val loc1 = location(step.maneuver.waypoints[i][0], step.maneuver.waypoints[i][1])
|
||||||
|
val loc2 =
|
||||||
|
location(step.maneuver.waypoints[i + 1][0], step.maneuver.waypoints[i + 1][1])
|
||||||
|
val distance = loc1.distanceTo(loc2)
|
||||||
|
leftDistance += distance
|
||||||
|
}
|
||||||
|
return (leftDistance / 10.0).roundToInt() * 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the left distance in m. */
|
||||||
|
fun travelLeftDistance(): Double {
|
||||||
|
var leftDistance = 0.0
|
||||||
|
for (i in routeModel.route.currentStepIndex + 1..<routeModel.curLeg.steps.size) {
|
||||||
|
val step = routeModel.route.legs()[0].steps[i]
|
||||||
|
leftDistance += step.distance
|
||||||
|
}
|
||||||
|
leftDistance += leftStepDistance()
|
||||||
|
return leftDistance
|
||||||
|
}
|
||||||
|
|
||||||
|
fun arrivalTime(): Long {
|
||||||
|
val timeLeft = travelLeftTime()
|
||||||
|
// Calculate the time to destination from the current time.
|
||||||
|
val nowUtcMillis = System.currentTimeMillis()
|
||||||
|
val timeToDestinationMillis =
|
||||||
|
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
|
||||||
|
return nowUtcMillis + timeToDestinationMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSpeedLimit(location: Location, viewModel: ViewModel) {
|
||||||
|
if (routeModel.isNavigating()) {
|
||||||
|
// speed limit
|
||||||
|
val distance = lastSpeedLocation.distanceTo(location)
|
||||||
|
if (distance > 500 || lastSpeedIndex < routeModel.route.currentStepIndex) {
|
||||||
|
lastSpeedIndex = routeModel.route.currentStepIndex
|
||||||
|
lastSpeedLocation = location
|
||||||
|
viewModel.getMaxSpeed(location, routeModel.route.currentStep().street)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,318 +3,155 @@ package com.kouros.navigation.model
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import androidx.car.app.navigation.model.Maneuver
|
import androidx.car.app.navigation.model.Maneuver
|
||||||
import androidx.car.app.navigation.model.Step
|
|
||||||
import com.kouros.data.R
|
|
||||||
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
|
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
|
||||||
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
|
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
|
||||||
import com.kouros.navigation.data.valhalla.ManeuverType
|
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.Route
|
import com.kouros.navigation.data.Route
|
||||||
import com.kouros.navigation.data.RouteEngine
|
|
||||||
import com.kouros.navigation.data.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
|
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.Routes
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlin.math.absoluteValue
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.invoke
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
open class RouteModel() {
|
open class RouteModel {
|
||||||
data class RouteState(
|
|
||||||
val route: Route? = null,
|
// Immutable Data Class
|
||||||
val isNavigating: Boolean = false,
|
data class NavigationState(
|
||||||
val destination: Place = Place(),
|
val route: Route = Route.Builder().buildEmpty(),
|
||||||
|
val iconMapper : IconMapper = IconMapper(),
|
||||||
|
val navigating: Boolean = false,
|
||||||
val arrived: Boolean = false,
|
val arrived: Boolean = false,
|
||||||
val maneuverType: Int = 0,
|
|
||||||
val travelMessage: String = "",
|
val travelMessage: String = "",
|
||||||
val lastSpeedLocation: Location = location(0.0, 0.0),
|
val maneuverType: Int = 0,
|
||||||
val lastSpeedIndex: Int = 0,
|
val lastLocation: Location = location(0.0, 0.0),
|
||||||
val maxSpeed: Int = 0,
|
val currentLocation: Location = location(0.0, 0.0),
|
||||||
|
val routeBearing: Float = 0F,
|
||||||
|
val currentRouteIndex: Int = 0,
|
||||||
|
val destination: Place = Place()
|
||||||
)
|
)
|
||||||
|
|
||||||
var routeState = RouteState()
|
var navState = NavigationState()
|
||||||
|
|
||||||
var route: Route
|
val route: Route
|
||||||
get() = routeState.route!!
|
get() = navState.route
|
||||||
set(value) {
|
|
||||||
routeState = routeState.copy(route = value)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
val routeCalculator : RouteCalculator = RouteCalculator(this)
|
||||||
|
|
||||||
val legs: Leg
|
val curRoute: Routes
|
||||||
get() = routeState.route!!.legs!!.first()
|
get() = navState.route.routes[navState.currentRouteIndex]
|
||||||
|
|
||||||
|
val curLeg: Leg
|
||||||
|
get() = navState.route.routes[navState.currentRouteIndex].legs.first()
|
||||||
|
|
||||||
fun startNavigation(routeString: String, context: Context) {
|
fun startNavigation(routeString: String, context: Context) {
|
||||||
val routeEngine = getIntKeyValue(context = context, ROUTE_ENGINE)
|
val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
|
||||||
val newRoute = Route.Builder()
|
navState = navState.copy(
|
||||||
.routeEngine(routeEngine)
|
route = Route.Builder()
|
||||||
.route(routeString)
|
.routeEngine(routeEngine)
|
||||||
.build()
|
.route(routeString)
|
||||||
this.routeState = routeState.copy(
|
.build()
|
||||||
route = newRoute,
|
|
||||||
isNavigating = true
|
|
||||||
)
|
)
|
||||||
|
if (hasLegs()) {
|
||||||
|
navState = navState.copy(navigating = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasLegs(): Boolean {
|
||||||
|
return navState.route.routes.isNotEmpty() && navState.route.routes[0].legs.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopNavigation() {
|
fun stopNavigation() {
|
||||||
this.routeState = routeState.copy(
|
navState = navState.copy(
|
||||||
route = null,
|
route = Route.Builder().buildEmpty(),
|
||||||
isNavigating = false,
|
navigating = false,
|
||||||
arrived = false,
|
arrived = false,
|
||||||
maneuverType = 0,
|
maneuverType = Maneuver.TYPE_UNKNOWN
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
fun updateLocation(curLocation: Location, viewModel: ViewModel) {
|
||||||
fun updateLocation(location: Location, viewModel: ViewModel) {
|
navState = navState.copy(currentLocation = curLocation)
|
||||||
findStep(location)
|
routeCalculator.findStep(curLocation)
|
||||||
updateSpeedLimit(location, viewModel)
|
routeCalculator.updateSpeedLimit(curLocation, viewModel)
|
||||||
|
navState = navState.copy(lastLocation = navState.currentLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findStep(location: Location) {
|
private fun currentLanes(): List<Lane> {
|
||||||
var nearestDistance = 100000.0f
|
var lanes = emptyList<Lane>()
|
||||||
for ((index, step) in legs.steps.withIndex()) {
|
if (navState.route.legs().isNotEmpty()) {
|
||||||
if (index >= route.currentStep) {
|
navState.route.legs().first().intersection.forEach {
|
||||||
for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
if (it.lane.isNotEmpty()) {
|
||||||
if (wayIndex >= step.waypointIndex) {
|
val distance =
|
||||||
val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
|
navState.lastLocation.distanceTo(location(it.location[0], it.location[1]))
|
||||||
if (distance < nearestDistance) {
|
val sectionBearing =
|
||||||
nearestDistance = distance
|
navState.lastLocation.bearingTo(location(it.location[0], it.location[1]))
|
||||||
route.currentStep = step.index
|
if (distance < 500 && (navState.routeBearing.absoluteValue - sectionBearing.absoluteValue).absoluteValue < 10) {
|
||||||
step.waypointIndex = wayIndex
|
lanes = it.lane
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nearestDistance == 0F) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nearestDistance == 0F) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//println("Current Index ${route.currentStep} WayPoint: ${route.currentStep().waypointIndex}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateSpeedLimit(location: Location, viewModel: ViewModel) = runBlocking {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
// speed limit
|
|
||||||
val distance = routeState.lastSpeedLocation.distanceTo(location)
|
|
||||||
if (distance > 500 || routeState.lastSpeedIndex < route.currentStep) {
|
|
||||||
routeState = routeState.copy(lastSpeedIndex = route.currentStep)
|
|
||||||
routeState = routeState.copy(lastSpeedLocation = location)
|
|
||||||
val elements = viewModel.getMaxSpeed(location)
|
|
||||||
elements.forEach {
|
|
||||||
if (it.tags.name != null && it.tags.maxspeed != null) {
|
|
||||||
val speed = it.tags.maxspeed!!.toInt()
|
|
||||||
routeState = routeState.copy(maxSpeed = speed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return lanes
|
||||||
}
|
}
|
||||||
|
|
||||||
fun currentStep(): StepData {
|
fun currentStep(): StepData {
|
||||||
val currentStep = route.currentStep()
|
val distanceToNextStep = routeCalculator.leftStepDistance()
|
||||||
// Determine if we should display the current or the next maneuver
|
|
||||||
val distanceToNextStep = leftStepDistance()
|
|
||||||
val isNearNextManeuver = distanceToNextStep in 0.0..NEXT_STEP_THRESHOLD
|
|
||||||
val shouldAdvance =
|
|
||||||
isNearNextManeuver && route.currentStep < (route.legs!!.first().steps.size)
|
|
||||||
|
|
||||||
// Determine the maneuver type and corresponding icon
|
// Determine the maneuver type and corresponding icon
|
||||||
var maneuverType = if (hasArrived(currentStep.maneuver.type)) {
|
val currentStep = navState.route.nextStep(0)
|
||||||
currentStep.maneuver.type
|
|
||||||
} else {
|
|
||||||
ManeuverType.None.value
|
|
||||||
}
|
|
||||||
// Get the single, correct maneuver for this state
|
|
||||||
val relevantManeuver = if (shouldAdvance) {
|
|
||||||
route.nextStep() // This advances the route's state
|
|
||||||
} else {
|
|
||||||
route.currentStep()
|
|
||||||
}
|
|
||||||
// Safely get the street name from the maneuver
|
// Safely get the street name from the maneuver
|
||||||
val streetName = relevantManeuver.name
|
val streetName = currentStep.maneuver.street
|
||||||
if (shouldAdvance) {
|
val curManeuverType = currentStep.maneuver.type
|
||||||
maneuverType = relevantManeuver.maneuver.type
|
val exitNumber = currentStep.maneuver.exit
|
||||||
}
|
val maneuverIcon = navState.iconMapper.maneuverIcon(curManeuverType)
|
||||||
val maneuverIconPair = maneuverIcon(maneuverType)
|
navState = navState.copy(maneuverType = curManeuverType)
|
||||||
routeState = routeState.copy(maneuverType = maneuverIconPair.first)
|
|
||||||
|
val lanes = currentLanes()
|
||||||
|
|
||||||
// Construct and return the final StepData object
|
// Construct and return the final StepData object
|
||||||
return StepData(
|
return StepData(
|
||||||
streetName,
|
streetName,
|
||||||
distanceToNextStep,
|
distanceToNextStep,
|
||||||
maneuverIconPair.first,
|
navState.maneuverType,
|
||||||
maneuverIconPair.second,
|
maneuverIcon,
|
||||||
arrivalTime(),
|
routeCalculator.arrivalTime(),
|
||||||
travelLeftDistance()
|
routeCalculator.travelLeftDistance(),
|
||||||
|
lanes,
|
||||||
|
exitNumber
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun nextStep(): StepData {
|
fun nextStep(): StepData {
|
||||||
val step = route.nextStep()
|
val step = navState.route.nextStep(1)
|
||||||
val maneuverType = step.maneuver.type
|
val maneuverType = step.maneuver.type
|
||||||
val distanceLeft = leftStepDistance()
|
val distanceLeft = routeCalculator.leftStepDistance()
|
||||||
var text = ""
|
var text = ""
|
||||||
when (distanceLeft) {
|
when (distanceLeft) {
|
||||||
in 0.0..NEXT_STEP_THRESHOLD -> {
|
in 0.0..NEXT_STEP_THRESHOLD -> {
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (step.name.isNotEmpty()) {
|
if (step.street.isNotEmpty()) {
|
||||||
text = step.name
|
text = step.street
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val routing: (Pair<Int, Int>) = maneuverIcon(maneuverType)
|
val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType)
|
||||||
// Construct and return the final StepData object
|
// Construct and return the final StepData object
|
||||||
return StepData(
|
return StepData(
|
||||||
text,
|
text,
|
||||||
distanceLeft,
|
distanceLeft,
|
||||||
routing.first,
|
maneuverType,
|
||||||
routing.second,
|
maneuverIcon,
|
||||||
arrivalTime(),
|
routeCalculator.arrivalTime(),
|
||||||
travelLeftDistance()
|
routeCalculator.travelLeftDistance(),
|
||||||
|
listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())),
|
||||||
|
step.maneuver.exit
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun travelLeftTime(): Double {
|
|
||||||
var timeLeft = 0.0
|
|
||||||
// time for next step until end step
|
|
||||||
for (i in route.currentStep + 1..<legs.steps.size) {
|
|
||||||
val step = legs.steps[i]
|
|
||||||
timeLeft += step.duration
|
|
||||||
}
|
|
||||||
// time for current step
|
|
||||||
val step = route.currentStep()
|
|
||||||
val curTime = step.duration
|
|
||||||
val percent =
|
|
||||||
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
|
|
||||||
val time = curTime * percent / 100
|
|
||||||
timeLeft += time
|
|
||||||
return timeLeft
|
|
||||||
}
|
|
||||||
|
|
||||||
fun arrivalTime(): Long {
|
|
||||||
val timeLeft = travelLeftTime()
|
|
||||||
// Calculate the time to destination from the current time.
|
|
||||||
val nowUtcMillis = System.currentTimeMillis()
|
|
||||||
val timeToDestinationMillis =
|
|
||||||
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
|
|
||||||
return nowUtcMillis + timeToDestinationMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the current [Step] left distance in m. */
|
|
||||||
fun leftStepDistance(): Double {
|
|
||||||
val step = route.currentStep()
|
|
||||||
var leftDistance = step.distance
|
|
||||||
val percent =
|
|
||||||
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
|
|
||||||
leftDistance = leftDistance * percent / 100
|
|
||||||
// The remaining distance to the step, rounded to the nearest 10 units.
|
|
||||||
return (leftDistance * 1000 / 10.0).roundToInt() * 10.0
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the left distance in km. */
|
|
||||||
fun travelLeftDistance(): Double {
|
|
||||||
var leftDistance = 0.0
|
|
||||||
for (i in route.currentStep + 1..<legs.steps.size) {
|
|
||||||
val step = route.legs!![0].steps[i]
|
|
||||||
leftDistance += step.distance
|
|
||||||
}
|
|
||||||
|
|
||||||
val step = route.currentStep()
|
|
||||||
val curDistance = step.distance
|
|
||||||
val percent =
|
|
||||||
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
|
|
||||||
val distance = curDistance * percent / 100
|
|
||||||
leftDistance += distance
|
|
||||||
return leftDistance
|
|
||||||
}
|
|
||||||
|
|
||||||
fun maneuverIcon(routeManeuverType: Int): (Pair<Int, Int>) {
|
|
||||||
var type = Maneuver.TYPE_DEPART
|
|
||||||
var currentTurnIcon = R.drawable.ic_turn_name_change
|
|
||||||
when (routeManeuverType) {
|
|
||||||
ManeuverType.None.value -> {
|
|
||||||
type = Maneuver.TYPE_STRAIGHT
|
|
||||||
currentTurnIcon = R.drawable.ic_turn_name_change
|
|
||||||
}
|
|
||||||
|
|
||||||
ManeuverType.Destination.value,
|
|
||||||
ManeuverType.DestinationRight.value,
|
|
||||||
ManeuverType.DestinationLeft.value,
|
|
||||||
-> {
|
|
||||||
type = Maneuver.TYPE_DESTINATION
|
|
||||||
currentTurnIcon = R.drawable.ic_turn_destination
|
|
||||||
}
|
|
||||||
|
|
||||||
ManeuverType.Right.value -> {
|
|
||||||
type = Maneuver.TYPE_TURN_NORMAL_RIGHT
|
|
||||||
currentTurnIcon = R.drawable.ic_turn_normal_right
|
|
||||||
}
|
|
||||||
|
|
||||||
ManeuverType.Left.value -> {
|
|
||||||
type = Maneuver.TYPE_TURN_NORMAL_LEFT
|
|
||||||
currentTurnIcon = R.drawable.ic_turn_normal_left
|
|
||||||
}
|
|
||||||
|
|
||||||
ManeuverType.RampRight.value -> {
|
|
||||||
type = Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT
|
|
||||||
currentTurnIcon = R.drawable.ic_turn_slight_right
|
|
||||||
}
|
|
||||||
|
|
||||||
ManeuverType.RampLeft.value -> {
|
|
||||||
type = Maneuver.TYPE_TURN_NORMAL_LEFT
|
|
||||||
currentTurnIcon = R.drawable.ic_turn_normal_left
|
|
||||||
}
|
|
||||||
|
|
||||||
ManeuverType.ExitRight.value -> {
|
|
||||||
type = Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
|
||||||
currentTurnIcon = R.drawable.ic_turn_slight_right
|
|
||||||
}
|
|
||||||
|
|
||||||
ManeuverType.StayRight.value -> {
|
|
||||||
type = Maneuver.TYPE_KEEP_RIGHT
|
|
||||||
currentTurnIcon = R.drawable.ic_turn_name_change
|
|
||||||
}
|
|
||||||
|
|
||||||
ManeuverType.StayLeft.value -> {
|
|
||||||
type = Maneuver.TYPE_KEEP_LEFT
|
|
||||||
currentTurnIcon = R.drawable.ic_turn_name_change
|
|
||||||
}
|
|
||||||
|
|
||||||
ManeuverType.RoundaboutEnter.value -> {
|
|
||||||
type = Maneuver.TYPE_ROUNDABOUT_ENTER_CCW
|
|
||||||
currentTurnIcon = R.drawable.ic_roundabout_ccw
|
|
||||||
}
|
|
||||||
|
|
||||||
ManeuverType.RoundaboutExit.value -> {
|
|
||||||
type = Maneuver.TYPE_ROUNDABOUT_EXIT_CCW
|
|
||||||
currentTurnIcon = R.drawable.ic_roundabout_ccw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Pair(type, currentTurnIcon)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isNavigating(): Boolean {
|
fun isNavigating(): Boolean {
|
||||||
return routeState.isNavigating
|
return navState.navigating
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun hasArrived(type: Int): Boolean {
|
|
||||||
return type == ManeuverType.DestinationRight.value
|
|
||||||
|| type == ManeuverType.Destination.value
|
|
||||||
|| type == ManeuverType.DestinationLeft.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,11 +18,17 @@ 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.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.data.tomtom.Features
|
||||||
|
import com.kouros.navigation.data.tomtom.Traffic
|
||||||
|
import com.kouros.navigation.data.tomtom.TrafficData
|
||||||
|
import com.kouros.navigation.utils.GeoUtils.createPointCollection
|
||||||
|
import com.kouros.navigation.utils.Levenshtein
|
||||||
import com.kouros.navigation.utils.NavigationUtils
|
import com.kouros.navigation.utils.NavigationUtils
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
import io.objectbox.kotlin.boxFor
|
import io.objectbox.kotlin.boxFor
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.maplibre.geojson.FeatureCollection
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
@@ -32,6 +38,11 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val traffic: MutableLiveData<Map<String, String>> by lazy {
|
||||||
|
MutableLiveData()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val previewRoute: MutableLiveData<String> by lazy {
|
val previewRoute: MutableLiveData<String> by lazy {
|
||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
}
|
}
|
||||||
@@ -54,7 +65,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
val placeLocation: MutableLiveData<SearchResult> by lazy {
|
val placeLocation: MutableLiveData<SearchResult> by lazy {
|
||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
val contactAddress: MutableLiveData<List<Place>> by lazy {
|
val contactAddress: MutableLiveData<List<Place>> by lazy {
|
||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
}
|
}
|
||||||
@@ -67,7 +78,14 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadRecentPlace(location: Location) {
|
val maxSpeed: MutableLiveData<Int> by lazy {
|
||||||
|
MutableLiveData()
|
||||||
|
}
|
||||||
|
val routingEngine: MutableLiveData<Int> by lazy {
|
||||||
|
MutableLiveData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadRecentPlace(location: Location, carOrientation: Float, context: Context) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val placeBox = boxStore.boxFor(Place::class)
|
val placeBox = boxStore.boxFor(Place::class)
|
||||||
@@ -79,12 +97,18 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
query.close()
|
query.close()
|
||||||
for (place in results) {
|
for (place in results) {
|
||||||
val plLocation = location(place.longitude, place.latitude)
|
val plLocation = location(place.longitude, place.latitude)
|
||||||
//val distance = repository.getRouteDistance(location, plLocation, SearchFilter())
|
val distance = repository.getRouteDistance(
|
||||||
//place.distance = distance.toFloat()
|
location,
|
||||||
//if (place.distance == 0F) {
|
plLocation,
|
||||||
recentPlace.postValue(place)
|
carOrientation,
|
||||||
return@launch
|
SearchFilter(),
|
||||||
//}
|
context
|
||||||
|
)
|
||||||
|
place.distance = distance.toFloat()
|
||||||
|
if (place.distance > 1F) {
|
||||||
|
recentPlace.postValue(place)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -92,7 +116,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadRecentPlaces(context: Context, location: Location) {
|
fun loadRecentPlaces(context: Context, location: Location, carOrientation: Float) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val placeBox = boxStore.boxFor(Place::class)
|
val placeBox = boxStore.boxFor(Place::class)
|
||||||
@@ -104,15 +128,16 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
query.close()
|
query.close()
|
||||||
for (place in results) {
|
for (place in results) {
|
||||||
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 =
|
||||||
repository.getRouteDistance(
|
repository.getRouteDistance(
|
||||||
location,
|
location,
|
||||||
plLocation,
|
plLocation,
|
||||||
getSearchFilter(context), context
|
carOrientation,
|
||||||
)
|
getSearchFilter(context), context
|
||||||
place.distance = distance.toFloat()
|
)
|
||||||
}
|
place.distance = distance.toFloat()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
places.postValue(results)
|
places.postValue(results)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -121,7 +146,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFavorites(context: Context, location: Location) {
|
fun loadFavorites(context: Context, location: Location, carOrientation: Float) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val placeBox = boxStore.boxFor(Place::class)
|
val placeBox = boxStore.boxFor(Place::class)
|
||||||
@@ -134,7 +159,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
for (place in results) {
|
for (place in results) {
|
||||||
val plLocation = location(place.longitude, place.latitude)
|
val plLocation = location(place.longitude, place.latitude)
|
||||||
val distance =
|
val distance =
|
||||||
repository.getRouteDistance(location, plLocation, getSearchFilter(context), context)
|
repository.getRouteDistance(
|
||||||
|
location,
|
||||||
|
plLocation,
|
||||||
|
carOrientation,
|
||||||
|
getSearchFilter(context),
|
||||||
|
context
|
||||||
|
)
|
||||||
place.distance = distance.toFloat()
|
place.distance = distance.toFloat()
|
||||||
}
|
}
|
||||||
favorites.postValue(results)
|
favorites.postValue(results)
|
||||||
@@ -144,13 +175,20 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadRoute(context: Context, currentLocation: Location, location: Location) {
|
fun loadRoute(
|
||||||
|
context: Context,
|
||||||
|
currentLocation: Location,
|
||||||
|
location: Location,
|
||||||
|
carOrientation: Float
|
||||||
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
route.postValue(
|
route.postValue(
|
||||||
repository.getRoute(
|
repository.getRoute(
|
||||||
|
context,
|
||||||
currentLocation,
|
currentLocation,
|
||||||
location,
|
location,
|
||||||
|
carOrientation,
|
||||||
getSearchFilter(context)
|
getSearchFilter(context)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -160,13 +198,60 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadPreviewRoute(context: Context, currentLocation: Location, location: Location) {
|
fun loadTraffic(context: Context, currentLocation: Location, carOrientation: Float) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val data = repository.getTraffic(
|
||||||
|
context,
|
||||||
|
currentLocation,
|
||||||
|
carOrientation
|
||||||
|
)
|
||||||
|
val trafficData = rebuildTraffic(data)
|
||||||
|
traffic.postValue(
|
||||||
|
trafficData
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadPreviewRoute(
|
||||||
|
context: Context,
|
||||||
|
currentLocation: Location,
|
||||||
|
location: Location,
|
||||||
|
carOrientation: Float
|
||||||
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
previewRoute.postValue(
|
previewRoute.postValue(
|
||||||
repository.getRoute(
|
repository.getRoute(
|
||||||
|
context,
|
||||||
currentLocation,
|
currentLocation,
|
||||||
location,
|
location,
|
||||||
|
carOrientation,
|
||||||
getSearchFilter(context)
|
getSearchFilter(context)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -224,21 +309,24 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchPlaces(search: String, location: Location) {
|
fun searchPlaces(search: String, location: Location) {
|
||||||
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()
|
if (placesJson.isNotEmpty()) {
|
||||||
val places = gson.fromJson(placesJson, Search::class.java)
|
val gson = GsonBuilder().serializeNulls().create()
|
||||||
val distPlaces = mutableListOf<SearchResult>()
|
val places = gson.fromJson(placesJson, Search::class.java)
|
||||||
places.forEach {
|
val distPlaces = mutableListOf<SearchResult>()
|
||||||
val plLocation =
|
places.forEach {
|
||||||
location(longitude = it.lon.toDouble(), latitude = it.lat.toDouble())
|
val plLocation =
|
||||||
val distance = plLocation.distanceTo(location)
|
location(longitude = it.lon.toDouble(), latitude = it.lat.toDouble())
|
||||||
it.distance = distance
|
val distance = plLocation.distanceTo(location)
|
||||||
distPlaces.add(it)
|
it.distance = distance
|
||||||
|
distPlaces.add(it)
|
||||||
|
}
|
||||||
|
val sortedList = distPlaces.sortedWith(compareBy { it.distance })
|
||||||
|
searchPlaces.postValue(sortedList)
|
||||||
}
|
}
|
||||||
val sortedList = distPlaces.sortedWith(compareBy { it.distance })
|
|
||||||
searchPlaces.postValue(sortedList)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,13 +353,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSpeedCameras(location: Location, radius : Double) {
|
fun getSpeedCameras(location: Location, radius: Double) {
|
||||||
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 =
|
||||||
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()
|
||||||
distAmenities.add(it)
|
distAmenities.add(it)
|
||||||
@@ -281,10 +369,22 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMaxSpeed(location: Location) : List<Elements> {
|
fun getMaxSpeed(location: Location, street: String) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val levenshtein = Levenshtein()
|
||||||
val lineString = "${location.latitude},${location.longitude}"
|
val lineString = "${location.latitude},${location.longitude}"
|
||||||
val amenities = Overpass().getAround(10, lineString)
|
val amenities = Overpass().getAround(10, lineString)
|
||||||
return amenities
|
amenities.forEach {
|
||||||
|
if (it.tags.name != null) {
|
||||||
|
val distance =
|
||||||
|
levenshtein.distance(it.tags.name!!, street)
|
||||||
|
if (distance < 5) {
|
||||||
|
val speed = it.tags.maxspeed.toInt()
|
||||||
|
maxSpeed.postValue(speed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveFavorite(place: Place) {
|
fun saveFavorite(place: Place) {
|
||||||
@@ -293,6 +393,12 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveRecent(place: Place) {
|
fun saveRecent(place: Place) {
|
||||||
|
if (place.category == Constants.FUEL_STATION
|
||||||
|
|| place.category == Constants.CHARGING_STATION
|
||||||
|
|| place.category == Constants.PHARMACY
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
place.category = Constants.RECENT
|
place.category = Constants.RECENT
|
||||||
savePlace(place)
|
savePlace(place)
|
||||||
}
|
}
|
||||||
@@ -351,7 +457,6 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getSearchFilter(context: Context): SearchFilter {
|
fun getSearchFilter(context: Context): SearchFilter {
|
||||||
|
|
||||||
val avoidMotorway = NavigationUtils.getBooleanKeyValue(
|
val avoidMotorway = NavigationUtils.getBooleanKeyValue(
|
||||||
context = context,
|
context = context,
|
||||||
Constants.AVOID_MOTORWAY
|
Constants.AVOID_MOTORWAY
|
||||||
@@ -360,14 +465,15 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
context = context,
|
context = context,
|
||||||
Constants.AVOID_TOLLWAY
|
Constants.AVOID_TOLLWAY
|
||||||
)
|
)
|
||||||
return SearchFilter.Builder()
|
return SearchFilter(avoidMotorway, avoidTollway)
|
||||||
.avoidMotorway(avoidMotorway)
|
|
||||||
.avoidTollway(avoidTollway)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun loadPlaces2(context: Context, location: Location): SnapshotStateList<Place?> {
|
fun loadPlaces2(
|
||||||
|
context: Context,
|
||||||
|
location: Location,
|
||||||
|
carOrientation: Float
|
||||||
|
): SnapshotStateList<Place?> {
|
||||||
val results = listOf<Place>()
|
val results = listOf<Place>()
|
||||||
try {
|
try {
|
||||||
val placeBox = boxStore.boxFor(Place::class)
|
val placeBox = boxStore.boxFor(Place::class)
|
||||||
@@ -380,7 +486,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
for (place in results) {
|
for (place in results) {
|
||||||
val plLocation = location(place.longitude, place.latitude)
|
val plLocation = location(place.longitude, place.latitude)
|
||||||
val distance =
|
val distance =
|
||||||
repository.getRouteDistance(location, plLocation, getSearchFilter(context), context)
|
repository.getRouteDistance(
|
||||||
|
location,
|
||||||
|
plLocation,
|
||||||
|
carOrientation,
|
||||||
|
getSearchFilter(context),
|
||||||
|
context
|
||||||
|
)
|
||||||
place.distance = distance.toFloat()
|
place.distance = distance.toFloat()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.kouros.navigation.utils
|
package com.kouros.navigation.utils
|
||||||
|
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import com.kouros.navigation.data.BoundingBox
|
|
||||||
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
|
||||||
@@ -10,7 +9,6 @@ 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
|
||||||
@@ -33,9 +31,7 @@ object GeoUtils {
|
|||||||
return newLocation
|
return newLocation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decodePolyline(encoded: String, precision: Int = 6): List<List<Double>> {
|
||||||
fun decodePolyline(encoded: String, precision: Int = 6,): List<List<Double>> {
|
|
||||||
//val precisionOptional = if (precisionOptional.isNotEmpty()) precisionOptional[0] else 6
|
|
||||||
val factor = 10.0.pow(precision)
|
val factor = 10.0.pow(precision)
|
||||||
|
|
||||||
var lat = 0
|
var lat = 0
|
||||||
@@ -93,6 +89,7 @@ object GeoUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createLineStringCollection(lineCoordinates: List<List<Double>>): String {
|
fun createLineStringCollection(lineCoordinates: List<List<Double>>): String {
|
||||||
|
// 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(
|
||||||
@@ -118,40 +115,31 @@ object GeoUtils {
|
|||||||
return featureCollection.toJson()
|
return featureCollection.toJson()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOverpassBbox(location: Location, radius: Double): String {
|
|
||||||
|
|
||||||
val bbox = getBoundingBox(location.longitude, location.latitude, radius)
|
/**
|
||||||
val neLon = bbox["ne"]?.get("lon")
|
* Calculate the lat and len of a square around a point.
|
||||||
val neLat = bbox["ne"]?.get("lat")
|
* @return latMin, latMax, lngMin, lngMax
|
||||||
val swLon = bbox["sw"]?.get("lon")
|
*/
|
||||||
val swLat = bbox["sw"]?.get("lat")
|
fun calculateSquareRadius(lat: Double, lng: Double, radius: Double): String {
|
||||||
return "$swLon,$swLat,$neLon,$neLat"
|
val earthRadius = 6371.0 // earth radius in km
|
||||||
}
|
val latMin = lat - toDegrees(radius / earthRadius)
|
||||||
|
val latMax = lat + toDegrees(radius / earthRadius)
|
||||||
|
val lngMin = lng - toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
||||||
|
val lngMax = lng + toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
||||||
|
|
||||||
fun getBoundingBox2(location: Location, radius: Double): BoundingBox {
|
return "$lngMin,$latMin,$lngMax,$latMax"
|
||||||
val bbox = getBoundingBox(location.longitude, location.latitude, radius)
|
|
||||||
val neLon = bbox["ne"]?.get("lon")
|
|
||||||
val neLat = bbox["ne"]?.get("lat")
|
|
||||||
val swLon = bbox["sw"]?.get("lon")
|
|
||||||
val swLat = bbox["sw"]?.get("lat")
|
|
||||||
return BoundingBox(swLat ?: 0.0 , swLon ?: 0.0, neLat ?: 0.0, neLon ?: 0.0)
|
|
||||||
}
|
}
|
||||||
fun getBoundingBox(
|
fun getBoundingBox(
|
||||||
lat: Double,
|
lat: Double,
|
||||||
lon: Double,
|
lon: Double,
|
||||||
radius: Double
|
radius: Double
|
||||||
): Map<String, Map<String, Double>> {
|
): String {
|
||||||
val earthRadius = 6371.0
|
val earthRadius = 6371.0
|
||||||
val maxLat = lat + toDegrees(radius / earthRadius)
|
val maxLat = lat + toDegrees(radius / earthRadius)
|
||||||
val minLat = lat - toDegrees(radius / earthRadius)
|
val minLat = lat - toDegrees(radius / earthRadius)
|
||||||
val maxLon = lon + toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
val maxLon = lon + toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
||||||
val minLon = lon - toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
val minLon = lon - toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
||||||
|
|
||||||
return mapOf(
|
return "$minLat,$minLon,$maxLat,$maxLon"
|
||||||
"nw" to mapOf("lat" to maxLat, "lon" to minLon),
|
|
||||||
"ne" to mapOf("lat" to maxLat, "lon" to maxLon),
|
|
||||||
"sw" to mapOf("lat" to minLat, "lon" to minLon),
|
|
||||||
"se" to mapOf("lat" to minLat, "lon" to maxLon)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.kouros.navigation.utils
|
||||||
|
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Levenshtein distance between two words is the minimum number of single-character edits (insertions, deletions or
|
||||||
|
* substitutions) required to change one string into the other.
|
||||||
|
*
|
||||||
|
* This implementation uses dynamic programming (Wagner–Fischer algorithm).
|
||||||
|
*
|
||||||
|
* [Levenshtein Distance](https://en.wikipedia.org/wiki/Levenshtein_distance)
|
||||||
|
*/
|
||||||
|
class Levenshtein {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Levenshtein distance, or edit distance, between two words is the minimum number of single-character edits
|
||||||
|
* (insertions, deletions or substitutions) required to change one word into the other.
|
||||||
|
*
|
||||||
|
* It is always at least the difference of the sizes of the two strings.
|
||||||
|
* It is at most the length of the longer string.
|
||||||
|
* It is `0` if and only if the strings are equal.
|
||||||
|
*
|
||||||
|
* @param first first string to compare.
|
||||||
|
* @param second second string to compare.
|
||||||
|
* @param limit the maximum result to compute before stopping, terminating calculation early.
|
||||||
|
* @return the computed Levenshtein distance.
|
||||||
|
*/
|
||||||
|
fun distance(first: CharSequence, second: CharSequence, limit: Int = Int.MAX_VALUE): Int {
|
||||||
|
if (first == second) return 0
|
||||||
|
if (first.isEmpty()) return second.length
|
||||||
|
if (second.isEmpty()) return first.length
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
var cost = IntArray(first.length + 1) { it }
|
||||||
|
var newCost = IntArray(first.length + 1)
|
||||||
|
|
||||||
|
for (i in 1..second.length) {
|
||||||
|
// calculate new costs from the previous row.
|
||||||
|
// the first element of the new row is the edit distance (deletes) to match empty string
|
||||||
|
newCost[0] = i
|
||||||
|
var minCost = i
|
||||||
|
|
||||||
|
// fill in the rest of the row
|
||||||
|
for (j in 1..first.length) {
|
||||||
|
// if it's the same char at the same position, no edit cost.
|
||||||
|
val edit = if (first[j - 1] == second[i - 1]) 0 else 1
|
||||||
|
val replace = cost[j - 1] + edit
|
||||||
|
val insert = cost[j] + 1
|
||||||
|
val delete = newCost[j - 1] + 1
|
||||||
|
newCost[j] = minOf(insert, delete, replace)
|
||||||
|
minCost = min(minCost, newCost[j])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minCost >= limit) return limit
|
||||||
|
// flip references of current and previous row
|
||||||
|
val swap = cost
|
||||||
|
cost = newCost
|
||||||
|
newCost = swap
|
||||||
|
}
|
||||||
|
return cost.last()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,11 @@ import android.content.Context
|
|||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
|
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
|
||||||
import com.kouros.navigation.data.Constants.SHARED_PREF_KEY
|
import com.kouros.navigation.data.Constants.SHARED_PREF_KEY
|
||||||
import com.kouros.navigation.data.RouteEngine
|
import com.kouros.navigation.data.RouteEngine
|
||||||
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.valhalla.ValhallaRepository
|
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||||
import com.kouros.navigation.model.ViewModel
|
import com.kouros.navigation.model.ViewModel
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -25,11 +26,12 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
|
|
||||||
object NavigationUtils {
|
object NavigationUtils {
|
||||||
|
|
||||||
fun getRouteEngine(context: Context): ViewModel {
|
fun getViewModel(context: Context): ViewModel {
|
||||||
val routeEngine = getIntKeyValue(context = context, ROUTE_ENGINE)
|
val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
|
||||||
return when (routeEngine) {
|
return when (routeEngine) {
|
||||||
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
|
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
|
||||||
else -> ViewModel(OsrmRepository())
|
RouteEngine.OSRM.ordinal -> ViewModel(OsrmRepository())
|
||||||
|
else -> ViewModel(TomTomRepository())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,16 +94,16 @@ fun calculateZoom(speed: Double?): Double {
|
|||||||
in 61..70 -> 16.5
|
in 61..70 -> 16.5
|
||||||
else -> 16.0
|
else -> 16.0
|
||||||
}
|
}
|
||||||
return zoom.toDouble()
|
return zoom
|
||||||
}
|
}
|
||||||
|
|
||||||
fun previewZoom(previewDistance: Double): Double {
|
fun previewZoom(previewDistance: Double): Double {
|
||||||
when (previewDistance) {
|
when (previewDistance) {
|
||||||
in 0.0..10.0 -> return 13.0
|
in 0.0..10.0 -> return 13.5
|
||||||
in 10.0..20.0 -> return 11.0
|
in 10.0..20.0 -> return 11.5
|
||||||
in 20.0..30.0 -> return 10.0
|
in 20.0..30.0 -> return 10.5
|
||||||
}
|
}
|
||||||
return 9.0
|
return 9.5
|
||||||
}
|
}
|
||||||
|
|
||||||
fun calculateTilt(newZoom: Double, tilt: Double): Double =
|
fun calculateTilt(newZoom: Double, tilt: Double): Double =
|
||||||
|
|||||||
11
common/data/src/main/res/drawable/arrow_back_24px.xml
Normal file
11
common/data/src/main/res/drawable/arrow_back_24px.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<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"
|
||||||
|
android:autoMirrored="true">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:viewportHeight="960"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M340,760L440,600L380,600L380,480L280,640L340,640L340,760ZM240,400L480,400L480,200Q480,200 480,200Q480,200 480,200L240,200Q240,200 240,200Q240,200 240,200L240,400ZM240,760L480,760L480,480L240,480L240,760ZM160,840L160,200Q160,167 183.5,143.5Q207,120 240,120L480,120Q513,120 536.5,143.5Q560,167 560,200L560,480L610,480Q639,480 659.5,500.5Q680,521 680,550L680,735Q680,752 694,766Q708,780 725,780Q743,780 756.5,766Q770,752 770,735L770,360L760,360Q743,360 731.5,348.5Q720,337 720,320L720,240L740,240L740,180L780,180L780,240L820,240L820,180L860,180L860,240L880,240L880,320Q880,337 868.5,348.5Q857,360 840,360L830,360L830,735Q830,777 799.5,808.5Q769,840 725,840Q682,840 651,808.5Q620,777 620,735L620,550Q620,545 617.5,542.5Q615,540 610,540L560,540L560,840L160,840ZM480,760L240,760L240,760L480,760Z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -6,5 +6,5 @@
|
|||||||
android:tint="?attr/colorControlNormal">
|
android:tint="?attr/colorControlNormal">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M337,746L425,606L372,606L372,501L285,641L337,641L337,746ZM220,408L489,408L489,180Q489,180 489,180Q489,180 489,180L220,180Q220,180 220,180Q220,180 220,180L220,408ZM220,780L489,780L489,468L220,468L220,780ZM160,840L160,180Q160,156 178,138Q196,120 220,120L489,120Q513,120 531,138Q549,156 549,180L549,468L614,468Q634.71,468 649.36,482.64Q664,497.29 664,518L664,737Q664,759 681.5,773.5Q699,788 722,788Q745,788 765,773.5Q785,759 785,737L785,350L770,350Q757.25,350 748.63,341.37Q740,332.75 740,320L740,230L760,230L760,180L790,180L790,230L830,230L830,180L860,180L860,230L880,230L880,320Q880,332.75 871.38,341.37Q862.75,350 850,350L835,350L835,736.69Q835,780 801,810Q767,840 721.82,840Q677.66,840 645.83,810Q614,780 614,737L614,518Q614,518 614,518Q614,518 614,518L549,518L549,840L160,840ZM489,780L220,780L220,780L489,780Z"/>
|
android:pathData="M220,408L489,408L489,180Q489,180 489,180Q489,180 489,180L220,180Q220,180 220,180Q220,180 220,180L220,408ZM160,840L160,180Q160,156 178,138Q196,120 220,120L489,120Q513,120 531,138Q549,156 549,180L549,468L614,468Q634.71,468 649.36,482.64Q664,497.29 664,518L664,737Q664,759 681.5,773.5Q699,788 722,788Q745,788 765,773.5Q785,759 785,737L785,350L770,350Q757.25,350 748.63,341.37Q740,332.75 740,320L740,230L760,230L760,180L790,180L790,230L830,230L830,180L860,180L860,230L880,230L880,320Q880,332.75 871.38,341.37Q862.75,350 850,350L835,350L835,736.69Q835,780 801,810Q767,840 721.82,840Q677.66,840 645.83,810Q614,780 614,737L614,518Q614,518 614,518Q614,518 614,518L549,518L549,840L160,840ZM337,746L425,606L372,606L372,501L285,641L337,641L337,746Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
BIN
common/data/src/main/res/drawable/lanes.png
Normal file
BIN
common/data/src/main/res/drawable/lanes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
common/data/src/main/res/drawable/left_o.png
Normal file
BIN
common/data/src/main/res/drawable/left_o.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 914 B |
BIN
common/data/src/main/res/drawable/left_o_right_x.png
Normal file
BIN
common/data/src/main/res/drawable/left_o_right_x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 883 B |
BIN
common/data/src/main/res/drawable/left_o_straight_x.png
Normal file
BIN
common/data/src/main/res/drawable/left_o_straight_x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 881 B |
BIN
common/data/src/main/res/drawable/left_x.png
Normal file
BIN
common/data/src/main/res/drawable/left_x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user