TomTom Routing

This commit is contained in:
Dimitris
2026-02-07 12:56:45 +01:00
parent eac5b56bcb
commit 0d51c6121d
50 changed files with 8923 additions and 5084 deletions

200
CLAUDE.md Normal file
View 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

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 36
versionCode = 32 versionCode = 36
versionName = "0.1.3.32" versionName = "0.2.0.36"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -95,6 +95,12 @@ dependencies {
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("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)

View File

@@ -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()

View File

@@ -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,
)

View File

@@ -15,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
@@ -33,17 +37,23 @@ 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.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
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.homeHohenwaldeck
import com.kouros.navigation.data.Constants.homeVogelhart 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.BaseStyleModel
@@ -78,6 +88,7 @@ class MainActivity : ComponentActivity() {
val routeModel = RouteModel() val routeModel = RouteModel()
var tilt = 50.0 var tilt = 50.0
val useMock = false val useMock = false
val type = 1 // simulate 2 test 3 gpx
var currentIndex = 0 var currentIndex = 0
val stepData: MutableLiveData<StepData> by lazy { val stepData: MutableLiveData<StepData> by lazy {
@@ -92,9 +103,13 @@ class MainActivity : ComponentActivity() {
routeModel.startNavigation(newRoute, applicationContext) routeModel.startNavigation(newRoute, applicationContext)
routeData.value = routeModel.curRoute.routeGeoJson routeData.value = routeModel.curRoute.routeGeoJson
if (useMock) { if (useMock) {
simulate() when (type) {
//test() 1 -> simulate()
///gpx(applicationContext) 2 -> test()
3 -> gpx(
context = applicationContext
)
}
} }
} }
} }
@@ -114,9 +129,6 @@ class MainActivity : ComponentActivity() {
lateinit var baseStyle: BaseStyle.Json lateinit var baseStyle: BaseStyle.Json
init {
}
@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?) {
@@ -156,24 +168,28 @@ class MainActivity : ComponentActivity() {
permissions = permissions, permissions = permissions,
requiredPermissions = listOf(permissions.first()), requiredPermissions = listOf(permissions.first()),
onGranted = { onGranted = {
Content() App()
// auto navigate // auto navigate
if (useMock) { if (useMock) {
navigationViewModel.loadRoute( // navigationViewModel.loadRoute(
applicationContext, // applicationContext,
homeVogelhart, // homeVogelhart,
homeHohenwaldeck, // homeHohenwaldeck,
0F // 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()
@@ -226,6 +242,42 @@ class MainActivity : ComponentActivity() {
baseStyle 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),
)
} }
} }
} }
@@ -265,7 +317,7 @@ class MainActivity : ComponentActivity() {
if (isNavigating()) { if (isNavigating()) {
updateLocation(currentLocation, navigationViewModel) updateLocation(currentLocation, navigationViewModel)
stepData.value = currentStep() stepData.value = currentStep()
if (route.currentStep + 1 <= curLeg.steps.size) { if (route.currentStepIndex + 1 <= curLeg.steps.size) {
nextStepData.value = nextStep() nextStepData.value = nextStep()
} }
if (maneuverType in 39..42 if (maneuverType in 39..42
@@ -302,7 +354,7 @@ class MainActivity : ComponentActivity() {
mock.setMockLocation(latitude, longitude) 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)
} }
fun simulateNavigation() { fun simulateNavigation() {
@@ -338,8 +390,8 @@ class MainActivity : ComponentActivity() {
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) { for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
val deviation = 0.0 val deviation = 0.0
if (index in 0..routeModel.curRoute.waypoints.size) { if (index in 300..routeModel.curRoute.waypoints.size) {
mock.setMockLocation(waypoint[1] + deviation, waypoint[0]) mock.setMockLocation(waypoint[1], waypoint[0])
delay(500L) // delay(500L) //
} }
} }
@@ -349,13 +401,14 @@ class MainActivity : ComponentActivity() {
fun test() { fun test() {
for ((index, step) in routeModel.curLeg.steps.withIndex()) { for ((index, step) in routeModel.curLeg.steps.withIndex()) {
if (index in 3..3) { //if (index in 3..3) {
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) { for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
routeModel.updateLocation( routeModel.updateLocation(
location(waypoint[0], waypoint[1]), location(waypoint[0], waypoint[1]),
navigationViewModel navigationViewModel
) )
val step = routeModel.currentStep() val step = routeModel.currentStep()
println("Step: ${step}")
if (step.leftStepDistance == 70.0) { if (step.leftStepDistance == 70.0) {
println("") println("")
} }
@@ -363,7 +416,7 @@ class MainActivity : ComponentActivity() {
//nextStepData.value = routeModel.nextStep() //nextStepData.value = routeModel.nextStep()
} }
} }
} //}
} }
} }
@@ -377,7 +430,7 @@ class MainActivity : ComponentActivity() {
fun gpx(context: Context) { fun gpx(context: Context) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val parser = GPXParser() val parser = GPXParser()
val input = context.resources.openRawResource(R.raw.hv) val input = context.resources.openRawResource(R.raw.vh)
val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread
parsedGpx?.let { parsedGpx?.let {
val tracks = parsedGpx.tracks val tracks = parsedGpx.tracks

View File

@@ -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(),
),
)
}

View File

@@ -13,6 +13,7 @@ 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.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
@@ -30,21 +31,19 @@ fun NavigationSheet(
stopNavigation: () -> Unit, stopNavigation: () -> Unit,
simulateNavigation: () -> Unit, simulateNavigation: () -> Unit,
) { ) {
val distance = step.leftDistance.round(1) val distance = (step.leftDistance / 1000).round(1)
step.lane.forEach { if (step.lane.isNotEmpty()) {
if (it.indications.isNotEmpty()) { routeModel.addLanes( step)
routeModel.createLaneIcon(applicationContext, 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()) {

View 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),
)
}
)
}
}
}

View File

@@ -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)

View 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)
)

View File

@@ -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,
)
}
} }

View File

@@ -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
) )
*/ )
)

View File

@@ -213,7 +213,7 @@ class SurfaceRenderer(
DrawNavigationImages( DrawNavigationImages(
paddingValues, paddingValues,
currentSpeed, currentSpeed,
routeModel.maxSpeed, routeModel,
width, width,
height height
) )

View File

@@ -2,7 +2,6 @@ package com.kouros.navigation.car.map
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import androidx.car.app.connection.CarConnection
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -32,10 +31,9 @@ import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING 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.ObjectBox
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.data.tomtom.TrafficData import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState
@@ -92,7 +90,7 @@ fun MapLibre(
cameraState: CameraState, cameraState: CameraState,
baseStyle: BaseStyle.Json, baseStyle: BaseStyle.Json,
route: String?, route: String?,
traffic: Map<String, String> ?, traffic: Map<String, String>?,
viewStyle: ViewStyle, viewStyle: ViewStyle,
speedCameras: String? = "" speedCameras: String? = ""
) { ) {
@@ -112,6 +110,7 @@ fun MapLibre(
AmenityLayer(route) AmenityLayer(route)
} else { } else {
RouteLayer(route, traffic!!) RouteLayer(route, traffic!!)
//RouteLayerPoint(route )
} }
SpeedCameraLayer(speedCameras) SpeedCameraLayer(speedCameras)
} }
@@ -164,9 +163,9 @@ fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
type = exponential(1.2f), type = exponential(1.2f),
input = zoom(), input = zoom(),
5 to const(0.4.dp), 5 to const(0.4.dp),
6 to const(0.8.dp), 6 to const(0.6.dp),
7 to const(2.0.dp), 7 to const(1.8.dp),
20 to const(24.dp), 20 to const(20.dp),
), ),
) )
LineLayer( LineLayer(
@@ -178,18 +177,44 @@ fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
type = exponential(1.2f), type = exponential(1.2f),
input = zoom(), input = zoom(),
5 to const(0.4.dp), 5 to const(0.4.dp),
6 to const(0.7.dp), 6 to const(0.5.dp),
7 to const(1.75.dp), 7 to const(1.6.dp),
20 to const(22.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> { fun trafficColor(key: String): Expression<ColorValue> {
when (key) { when (key) {
"queuing" -> return const(Color(0xFFD24417)) "queuing" -> return const(Color(0xFFD24417))
"stationary" -> return const(Color(0xFFFF0000)) "stationary" -> return const(Color(0xFFFF0000))
"heavy" -> return const(Color(0xFF6B0404)) "heavy" -> return const(Color(0xFF6B0404))
"slow" -> return const(Color(0xFFC41F1F)) "slow" -> return const(Color(0xFFC41F1F))
@@ -206,7 +231,7 @@ fun AmenityLayer(routeData: String?) {
if (routeData.contains(Constants.CHARGING_STATION)) { if (routeData.contains(Constants.CHARGING_STATION)) {
color = const(Color.Green) color = const(Color.Green)
img = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true) img = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true)
} else if (routeData.contains(Constants.FUEL_STATION)){ } else if (routeData.contains(Constants.FUEL_STATION)) {
color = const(Color.Black) color = const(Color.Black)
img = image(painterResource(R.drawable.local_gas_station_48px), drawAsSdf = true) img = image(painterResource(R.drawable.local_gas_station_48px), drawAsSdf = true)
} }
@@ -269,17 +294,18 @@ fun BuildingLayer(tiles: Source) {
fun DrawNavigationImages( fun DrawNavigationImages(
padding: PaddingValues, padding: PaddingValues,
speed: Float?, speed: Float?,
maxSpeed: Int, routeModel: RouteModel,
width: Int, width: Int,
height: Int height: Int
) { ) {
NavigationImage(padding, width, height) NavigationImage(padding, width, height)
if (speed != null) { if (speed != null) {
CurrentSpeed(width, height, speed, maxSpeed) CurrentSpeed(width, height, speed, routeModel.maxSpeed)
} }
if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) { if (speed != null && routeModel.maxSpeed > 0 && (speed * 3.6) > routeModel.maxSpeed) {
MaxSpeed(width, height, maxSpeed) MaxSpeed(width, height, routeModel.maxSpeed)
} }
//DebugInfo(width, height, routeModel)
} }
@Composable @Composable
@@ -429,14 +455,52 @@ private fun MaxSpeed(
} }
@Composable @Composable
fun rememberBaseStyle( baseStyle : BaseStyle.Json): BaseStyle.Json { fun DebugInfo(
width: Int,
height: Int,
routeModel: RouteModel,
) {
Box(
modifier = Modifier
.padding(
start = 20.dp,
top = 0.dp
),
contentAlignment = Alignment.CenterStart
) {
val textMeasurerLocation = rememberTextMeasurer()
val location = routeModel.location.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,
)
)
}
}
}
@Composable
fun rememberBaseStyle(baseStyle: BaseStyle.Json): BaseStyle.Json {
val rememberBaseStyle by remember() { val rememberBaseStyle by remember() {
mutableStateOf(baseStyle) mutableStateOf(baseStyle)
} }
return rememberBaseStyle return rememberBaseStyle
} }
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues { fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
return when (viewStyle) { return when (viewStyle) {
ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues( ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues(

View File

@@ -16,7 +16,6 @@
package com.kouros.navigation.car.navigation package com.kouros.navigation.car.navigation
import android.text.SpannableString import android.text.SpannableString
import android.text.Spanned
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.car.app.AppManager import androidx.car.app.AppManager
@@ -27,7 +26,6 @@ import androidx.car.app.model.Alert
import androidx.car.app.model.AlertCallback import androidx.car.app.model.AlertCallback
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.CarIconSpan
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
@@ -42,8 +40,9 @@ import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import org.maplibre.compose.expressions.dsl.step import com.kouros.navigation.utils.location
import java.util.Collections 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
@@ -54,18 +53,19 @@ class RouteCarModel() : RouteModel() {
fun currentStep(carContext: CarContext): Step { fun currentStep(carContext: CarContext): Step {
val stepData = currentStep() val stepData = currentStep()
val currentStepCueWithImage: SpannableString = val currentStepCueWithImage: SpannableString =
createString(stepData.instruction) createString(stepData.instruction)
val maneuver = Maneuver.Builder(stepData.currentManeuverType) val maneuver = Maneuver.Builder(stepData.currentManeuverType)
.setIcon(createCarIcon(carContext, stepData.icon)) .setIcon(createCarIcon(carContext, stepData.icon))
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW) { || stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
) {
maneuver.setRoundaboutExitNumber(stepData.exitNumber) maneuver.setRoundaboutExitNumber(stepData.exitNumber)
} }
val step = val step =
Step.Builder(currentStepCueWithImage) Step.Builder(currentStepCueWithImage)
.setManeuver( .setManeuver(
maneuver.build() maneuver.build()
) )
if (destination.street != null) { if (destination.street != null) {
step.setRoad(destination.street!!) step.setRoad(destination.street!!)
@@ -81,16 +81,17 @@ class RouteCarModel() : RouteModel() {
val stepData = nextStep() val stepData = nextStep()
val currentStepCueWithImage: SpannableString = val currentStepCueWithImage: SpannableString =
createString(stepData.instruction) createString(stepData.instruction)
val maneuver = Maneuver.Builder(stepData.currentManeuverType) val maneuver = Maneuver.Builder(stepData.currentManeuverType)
.setIcon(createCarIcon(carContext, stepData.icon)) .setIcon(createCarIcon(carContext, stepData.icon))
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW) { || stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
) {
maneuver.setRoundaboutExitNumber(stepData.exitNumber) maneuver.setRoundaboutExitNumber(stepData.exitNumber)
} }
val step = val step =
Step.Builder(currentStepCueWithImage) Step.Builder(currentStepCueWithImage)
.setManeuver( .setManeuver(
maneuver.build() maneuver.build()
) )
.build() .build()
return step return step
@@ -100,13 +101,13 @@ class RouteCarModel() : RouteModel() {
val timeLeft = travelLeftTime() val timeLeft = travelLeftTime()
val timeToDestinationMillis = val timeToDestinationMillis =
TimeUnit.SECONDS.toMillis(timeLeft.toLong()) TimeUnit.SECONDS.toMillis(timeLeft.toLong())
val leftDistance = travelLeftDistance() val leftDistance = 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(), arrivalTime(),
TimeZone.getTimeZone("Europe/Berlin") TimeZone.getTimeZone("Europe/Berlin")
) )
@@ -115,7 +116,7 @@ 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(
@@ -132,161 +133,99 @@ class RouteCarModel() : RouteModel() {
return travelBuilder.build() return travelBuilder.build()
} }
fun addLanes(carContext: CarContext, step: Step.Builder, stepData: StepData) { fun addLanes(carContext: CarContext, step: Step.Builder, stepData: StepData) {
var laneImageAdded = false var laneImageAdded = false
stepData.lane.forEach { stepData.lane.forEach {
if (it.indications.isNotEmpty() && it.valid) { if (it.indications.isNotEmpty() && it.valid) {
Collections.sort<String>(it.indications) Collections.sort<String>(it.indications)
var direction = "" var direction = ""
it.indications.forEach { it2 -> it.indications.forEach { it2 ->
direction = if (direction.isEmpty()) { direction = if (direction.isEmpty()) {
it2.trim() it2.trim()
} else { } else {
"${direction}_${it2.trim()}" "${direction}_${it2.trim()}"
}
} }
val laneDirection = when (direction) { }
"left_straight" -> { val laneDirection = addLanes(direction, stepData)
when (stepData.currentManeuverType) { if (laneDirection != LaneDirection.SHAPE_UNKNOWN) {
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT if (!laneImageAdded) {
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT step.setLanesImage(createCarIcon(createLaneIcon(carContext, stepData)))
else laneImageAdded = true
-> 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" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_SLIGHT_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"right_slight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_SLIGHT_RIGHT-> LaneDirection.SHAPE_NORMAL_RIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
else -> {
LaneDirection.SHAPE_UNKNOWN
}
}
if (laneDirection != LaneDirection.SHAPE_UNKNOWN) {
if (!laneImageAdded) {
step.setLanesImage(createCarIcon(createLaneIcon(carContext, stepData)))
laneImageAdded = true
}
val laneType =
Lane.Builder()
.addDirection(LaneDirection.create(laneDirection, false))
.build()
step.addLane(laneType)
} }
val laneType =
Lane.Builder()
.addDirection(LaneDirection.create(laneDirection, false))
.build()
step.addLane(laneType)
} }
} }
} }
}
private fun createStringWithIcon(
carContext: CarContext, fun createString(
text: String, text: String
@DrawableRes iconRes: Int ): SpannableString {
): SpannableString { val spannableString = SpannableString(text)
val start = 0 return spannableString
val end = text.length }
val span = CarIconSpan.create(createCarIcon(carContext, iconRes), CarIconSpan.ALIGN_CENTER)
val spannableString = SpannableString(text) fun createCarText(carContext: CarContext, @StringRes stringRes: Int): CarText {
spannableString.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) return CarText.create(carContext.getString(stringRes))
return spannableString }
}
fun createString( fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon {
text: String return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
): SpannableString { }
val spannableString = SpannableString(text)
return spannableString // fun createCarIcon(iconCompat: IconCompat): CarIcon {
} // return CarIcon.Builder(iconCompat).build()
// }
fun createCarText(carContext: CarContext, @StringRes stringRes: Int): CarText {
return CarText.create(carContext.getString(stringRes)) fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String) {
} carContext.getCarService<AppManager?>(AppManager::class.java)
.showAlert(
fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon { createAlert(
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build() carContext,
} maxSpeed,
createCarIcon(carContext, R.drawable.speed_camera_24px)
fun createCarIcon(iconCompat: IconCompat): CarIcon { )
return CarIcon.Builder(iconCompat).build() )
} }
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String) { fun createAlert(
carContext.getCarService<AppManager?>(AppManager::class.java) carContext: CarContext,
.showAlert(createAlert(carContext, distance, maxSpeed, createCarIcon(carContext, R.drawable.speed_camera_24px))) maxSpeed: String?,
} icon: CarIcon
): Alert {
fun createAlert( val title = createCarText(carContext, R.string.speed_camera)
carContext: CarContext, val subtitle = CarText.create(maxSpeed!!)
distance: Double,
maxSpeed: String?, val dismissAction: Action = createToastAction(
icon: CarIcon carContext,
): Alert { R.string.exit_action_title, R.string.exit_action_title,
val title = createCarText(carContext, R.string.speed_camera) FLAG_DEFAULT
val subtitle = CarText.create(maxSpeed!!) )
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */5000)
val dismissAction: Action = createToastAction( .setSubtitle(subtitle)
carContext, .setIcon(icon)
R.string.exit_action_title, R.string.exit_action_title, .addAction(dismissAction).setCallback(object : AlertCallback {
FLAG_DEFAULT override fun onCancel(reason: Int) {
) }
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */5000)
.setSubtitle(subtitle) override fun onDismiss() {
.setIcon(icon) }
.addAction(dismissAction).setCallback(object : AlertCallback { }).build()
override fun onCancel(reason: Int) { }
}
private fun createToastAction(
override fun onDismiss() { carContext: CarContext,
} @StringRes titleRes: Int, @StringRes toastStringRes: Int,
}).build() flags: Int
} ): Action {
return Action.Builder()
private fun createToastAction( .setOnClickListener { }
carContext: CarContext, .setTitle(createCarText(carContext, titleRes))
@StringRes titleRes: Int, @StringRes toastStringRes: Int, .setFlags(flags)
flags: Int .build()
): Action { }
return Action.Builder()
.setOnClickListener { }
.setTitle(createCarText(carContext, titleRes))
.setFlags(flags)
.build()
}
} }

View File

@@ -489,7 +489,6 @@ class NavigationScreen(
lastTrafficDate = current lastTrafficDate = current
viewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation) viewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
} }
//updateTraffic(location)
updateSpeedCamera(location) updateSpeedCamera(location)
with(routeModel) { with(routeModel) {
updateLocation(location, viewModel) updateLocation(location, viewModel)
@@ -524,7 +523,7 @@ 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)
@@ -533,7 +532,11 @@ class NavigationScreen(
val camera = sortedList.first() val camera = sortedList.first()
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location) val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
val bearingSpeedCamera = if (camera.tags.direction != null) { val bearingSpeedCamera = if (camera.tags.direction != null) {
camera.tags.direction!!.toFloat() try {
camera.tags.direction!!.toFloat()
} catch ( e: Exception) {
0F
}
} else { } else {
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
} }

View File

@@ -34,7 +34,7 @@ class SearchScreen(
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))
) )

View File

@@ -1,11 +1,5 @@
package com.kouros.navigation.car package com.kouros.navigation.car
import android.location.Location
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.data.valhalla.ValhallaRepository import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
@@ -24,16 +18,16 @@ class ViewModelTest {
@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)
} }
} }

View File

@@ -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)

View File

@@ -29,11 +29,11 @@ import java.net.URL
abstract class NavigationRepository { abstract class NavigationRepository {
//private val nominatimUrl = "https://nominatim.openstreetmap.org/" private val nominatimUrl = "https://nominatim.openstreetmap.org/"
private val nominatimUrl = "https://kouros-online.de/nominatim/" //private val nominatimUrl = "https://kouros-online.de/nominatim/"
private val tomtomApiKey = "678k5v6940cSXXIS5oD92qIrDgW3RBZ3" val tomtomApiKey = "678k5v6940cSXXIS5oD92qIrDgW3RBZ3"
private val tomtomUrl = "https://api.tomtom.com/traffic/services/5/incidentDetails" private val tomtomUrl = "https://api.tomtom.com/traffic/services/5/incidentDetails"
@@ -55,18 +55,19 @@ abstract class NavigationRepository {
searchFilter: SearchFilter, searchFilter: SearchFilter,
context: Context context: Context
): Double { ): Double {
val route = getRoute(context, currentLocation, location, carOrientation, searchFilter) //val route = getRoute(context, currentLocation, location, carOrientation, searchFilter)
val routeModel = RouteModel() //val routeModel = RouteModel()
routeModel.startNavigation(route, context) //routeModel.startNavigation(route, context)
return routeModel.curRoute.summary.distance // return routeModel.curRoute.summary.distance
return 0.0
} }
fun searchPlaces(search: String, location: Location): String { fun searchPlaces(search: String, location: Location): String {
val box = calculateSquareRadius(location.latitude, location.longitude, 20.0) val box = calculateSquareRadius(location.latitude, location.longitude, 100.0)
val viewbox = "&bounded=1&viewbox=${box}" val viewbox = "&bounded=1&viewbox=${box}"
return fetchUrl( return fetchUrl(
"${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox", "${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox",
false true
) )
} }

View File

@@ -22,7 +22,7 @@ data class Route(
val routeEngine: Int, val routeEngine: Int,
val routes: List<com.kouros.navigation.data.route.Routes>, val routes: List<com.kouros.navigation.data.route.Routes>,
var currentStep: Int = 0, var currentStepIndex: Int = 0,
) { ) {
data class Builder( data class Builder(
@@ -51,15 +51,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 -> { RouteEngine.OSRM.ordinal -> {
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 -> { else -> {
val tomtomJson = gson.fromJson(route, TomTomResponse::class.java) val tomtomJson = gson.fromJson(route, TomTomResponse::class.java)
TomTomRoute().mapToOsrm(tomtomJson, this) TomTomRoute().mapToRoute(tomtomJson, this)
} }
} }
} }
@@ -91,21 +91,23 @@ data class Route(
} }
} }
fun isRouteValid(): Boolean {
return routes.isNotEmpty() && legs().isNotEmpty()
}
fun currentStep(): Step { fun currentStep(): Step {
return if (isRouteValid()) {
return if (routes.isNotEmpty() && legs().isNotEmpty()) { legs().first().steps[currentStepIndex]
legs().first().steps[currentStep]
} else { } else {
Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0))) Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0)))
} }
} }
fun nextStep(): Step { fun nextStep(steps : Int): Step {
val nextIndex = currentStep + 1 val nextIndex = currentStepIndex + steps
return if (nextIndex < legs().first().steps.size) { return if (isRouteValid() && nextIndex < legs().first().steps.size) {
legs().first().steps[nextIndex] legs().first().steps[nextIndex]
} else { } else {
throw IndexOutOfBoundsException("No next maneuver available.") currentStep()
} }
} }

View File

@@ -5,7 +5,7 @@ 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 = 0.0, @SerializedName("weight" ) var weight : Double = 0.0,
@SerializedName("summary" ) var summary : String = "", @SerializedName("summary" ) var summary : String = "",
@SerializedName("duration" ) var duration : Double = 0.0, @SerializedName("duration" ) var duration : Double = 0.0,

View File

@@ -1,6 +1,7 @@
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.Intersection
import com.kouros.navigation.data.route.Lane import com.kouros.navigation.data.route.Lane
import com.kouros.navigation.data.route.Leg import com.kouros.navigation.data.route.Leg
@@ -14,7 +15,7 @@ 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 routes = mutableListOf<com.kouros.navigation.data.route.Routes>() val routes = mutableListOf<com.kouros.navigation.data.route.Routes>()
var stepIndex = 0 var stepIndex = 0
@@ -61,7 +62,7 @@ class OsrmRoute {
distance = step.distance / 1000, distance = step.distance / 1000,
duration = step.duration, duration = step.duration,
maneuver = maneuver, maneuver = maneuver,
intersection = intersections //intersection = intersections
) )
steps.add(step) steps.add(step)
stepIndex += 1 stepIndex += 1
@@ -80,7 +81,7 @@ class OsrmRoute {
routes.add(newRoute) routes.add(newRoute)
} }
builder builder
.routeType(1) .routeType(RouteEngine.OSRM.ordinal)
.routes(routes) .routes(routes)
} }

View File

@@ -3,6 +3,6 @@ package com.kouros.navigation.data.route
import java.util.Collections import java.util.Collections
data class Intersection( data class Intersection(
val location: ArrayList<Double> = arrayListOf(0.0, 0.0), val location: List<Double> = listOf(0.0, 0.0),
val lane : List<Lane> = Collections.emptyList<Lane>(), val lane : List<Lane> = Collections.emptyList<Lane>(),
) )

View File

@@ -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()
) )

View File

@@ -0,0 +1,5 @@
package com.kouros.navigation.data.tomtom
data class Cause(
val mainCauseCode: Int
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class Guidance(
val instructionGroups: List<InstructionGroup>,
val instructions: List<Instruction>
)

View File

@@ -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",
)

View File

@@ -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
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class Lane(
val directions: List<String>,
val follow: String
)

View File

@@ -0,0 +1,7 @@
package com.kouros.navigation.data.tomtom
data class Leg(
val encodedPolyline: String,
val encodedPolylinePrecision: Int,
val summary: SummaryX
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class Point(
val latitude: Double,
val longitude: Double
)

View File

@@ -1,5 +0,0 @@
package com.kouros.navigation.data.tomtom
data class Report(
val effectiveSettings: List<EffectiveSetting>
)

View File

@@ -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
)

View File

@@ -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,
)

View File

@@ -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
)

View File

@@ -0,0 +1,36 @@
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
private const val routeUrl = "https://api.tomtom.com/routing/1/calculateRoute/"
class TomTomRepository : NavigationRepository() {
override fun getRoute(
context: Context,
currentLocation: Location,
location: Location,
carOrientation: Float,
searchFilter: SearchFilter
): String {
//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&sectionType=traffic&report=effectiveSettings&routeType=eco" +
"&traffic=true&avoid=unpavedRoads&travelMode=car" +
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
"&instructionsType=text&language=en-GB&sectionType=lanes" +
"&routeRepresentation=encodedPolyline" +
"&vehicleEngineType=combustion&key=$tomtomApiKey"
return fetchUrl(
url,
false
)
}
}

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class TomTomResponse(
val formatVersion: String,
val routes: List<Route>
)

View File

@@ -1,63 +1,120 @@
package com.kouros.navigation.data.tomtom package com.kouros.navigation.data.tomtom
import com.kouros.navigation.data.Route import com.kouros.navigation.data.Route
import com.kouros.navigation.data.osrm.OsrmResponse import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRoute.ManeuverType 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
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.decodePolyline import com.kouros.navigation.utils.GeoUtils.decodePolyline
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import com.kouros.navigation.data.route.Maneuver as RouteManeuver import com.kouros.navigation.data.route.Maneuver as RouteManeuver
/**
curl -X GET "https://api.tomtom.com/routing/1/calculateRoute/\
48.1856548,11.57928:48.1183,11.59485/json?\
vehicleHeading=90&sectionType=traffic\
&report=effectiveSettings&routeType=eco\
&traffic=true&avoid=unpavedRoadimport com.kouros.navigation.data.route.Maneuver as RouteManeuvers&travelMode=car\
&vehicleMaxSpeed=120&vehicleCommercial=false\
&instructionsType=text&language=en-GB&sectionType=lanes\
&routeRepresentation=encodedPolyline\
&vehicleEngineType=combustion&key=678k5v6940cSXXIS5oD92qIrDgW3RBZ3"
*/
class TomTomRoute { class TomTomRoute {
fun mapToOsrm(routeJson: TomTomResponse, builder: Route.Builder) { fun mapToRoute(routeJson: TomTomResponse, builder: Route.Builder) {
val routes = mutableListOf<com.kouros.navigation.data.route.Routes>()
routeJson.routes.forEach { route -> routeJson.routes.forEach { route ->
val legs = mutableListOf<Leg>()
val waypoints = mutableListOf<List<Double>>() val waypoints = mutableListOf<List<Double>>()
val legs = mutableListOf<Leg>()
var stepIndex = 0
var points = listOf<List<Double>>() var points = listOf<List<Double>>()
val summary = Summary( val summary = Summary(
route.summary.travelTimeInSeconds.toDouble(), route.summary.travelTimeInSeconds.toDouble(),
route.summary.lengthInMeters.toDouble() / 1000 route.summary.lengthInMeters.toDouble()
) )
route.legs.forEach { leg -> route.legs.forEach { leg ->
points = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision) points = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision)
waypoints.addAll(points) waypoints.addAll(points)
} }
route.guidance.instructions.forEach { instruction -> var stepDistance = 0.0
instruction.exitNumber var stepDuration = 0.0
// val maneuver = RouteManeuver( val allIntersections = mutableListOf<Intersection>()
// // bearingBefore = step.maneuver.bearingBefore, val steps = mutableListOf<Step>()
// //bearingAfter = step.maneuver.bearingAfter, for (index in 0..< route.guidance.instructions.size) {
// type = convertType(instruction.maneuver), val instruction = route.guidance.instructions[index]
// waypoints = points.subList(section.startPointIndex, section.endPointIndex + 1), val nextPointIndex = nextPointIndex(index, route)
// exit = instruction.exitNumber.toInt(), val maneuver = RouteManeuver(
// location = location( bearingBefore = 0,
// instruction.point.longitude, instruction.point.latitude bearingAfter = 0,
// ) type = convertType(instruction.maneuver),
// ) waypoints = points.subList(
instruction.pointIndex,
route.guidance.instructions[nextPointIndex].pointIndex
),
exit = exitNumber(instruction),
location = location(
instruction.point.longitude, instruction.point.latitude
),
)
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[nextPointIndex].routeOffsetInMeters - stepDistance
stepDuration = route.guidance.instructions[nextPointIndex].travelTimeInSeconds - stepDuration
val name = instruction.street
val step = Step(
index = stepIndex,
name = name,
distance = stepDistance,
duration = stepDuration,
maneuver = maneuver,
intersection = intersections
)
stepDistance = route.guidance.instructions[nextPointIndex].routeOffsetInMeters.toDouble()
stepDuration = route.guidance.instructions[nextPointIndex].travelTimeInSeconds.toDouble()
steps.add(step)
stepIndex += 1
} }
route.sections.forEach { section -> 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)
} }
println(routeJson) builder
.routeType(RouteEngine.TOMTOM.ordinal)
.routes(routes)
}
private fun nextPointIndex(index: Int, route: com.kouros.navigation.data.tomtom.Route): Int {
val nextPointIndex = if (index < route.guidance.instructions.size - 1) {
index + 1
} else {
index + 0
}
return nextPointIndex
} }
fun convertType(type: String): Int { fun convertType(type: String): Int {
@@ -66,7 +123,73 @@ class TomTomRoute {
"DEPART" -> { "DEPART" -> {
newType = androidx.car.app.navigation.model.Maneuver.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 return newType
} }
}
private fun exitNumber(
instruction: Instruction
): Int {
return if ( instruction.exitNumber == null
|| instruction.exitNumber.isEmpty()) {
0
} else {
instruction.exitNumber.toInt()
}
} }

View File

@@ -3,6 +3,7 @@ package com.kouros.navigation.data.valhalla
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import com.kouros.data.R 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 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
@@ -13,7 +14,7 @@ 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(routeJson.summaryValhalla.time, routeJson.summaryValhalla.length) val summary = Summary(routeJson.summaryValhalla.time, routeJson.summaryValhalla.length)
val steps = mutableListOf<Step>() val steps = mutableListOf<Step>()
@@ -38,12 +39,9 @@ class ValhallaRoute {
stepIndex += 1 stepIndex += 1
} }
builder builder
.routeType(1) .routeType(RouteEngine.VALHALLA.ordinal)
// TODO // TODO
.routes(emptyList()) .routes(emptyList())
//.summary(summary)
//.routeGeoJson(createLineStringCollection(waypoints))
//.waypoints(waypoints)
} }
fun convertType(maneuver: Maneuvers): Int { fun convertType(maneuver: Maneuvers): Int {

View File

@@ -6,6 +6,10 @@ import android.graphics.BitmapFactory
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Matrix import android.graphics.Matrix
import android.location.Location import android.location.Location
import androidx.annotation.DrawableRes
import androidx.car.app.CarContext
import androidx.car.app.model.CarIcon
import androidx.car.app.navigation.model.LaneDirection
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
@@ -15,6 +19,7 @@ import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.Constants.ROUTING_ENGINE import com.kouros.navigation.data.Constants.ROUTING_ENGINE
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.Intersection import com.kouros.navigation.data.route.Intersection
import com.kouros.navigation.data.route.Lane import com.kouros.navigation.data.route.Lane
@@ -30,6 +35,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.Collections import java.util.Collections
import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -45,9 +51,10 @@ open class RouteModel() {
var lastSpeedLocation: Location = location(0.0, 0.0) var lastSpeedLocation: Location = location(0.0, 0.0)
var lastSpeedIndex: Int = 0 var lastSpeedIndex: Int = 0
var maxSpeed: Int = 0 var maxSpeed: Int = 0
var location: Location = location(0.0, 0.0) var location: Location = location(0.0, 0.0)
var lastLocation: Location = location(0.0, 0.0) var lastLocation: Location = location(0.0, 0.0)
var bearing: Float = 0F var routeBearing: Float = 0F
var currentRouteIndex = 0 var currentRouteIndex = 0
val curRoute: Routes val curRoute: Routes
@@ -62,7 +69,13 @@ open class RouteModel() {
.routeEngine(routeEngine) .routeEngine(routeEngine)
.route(routeString) .route(routeString)
.build() .build()
navigating = true if (hasLegs()) {
navigating = true
}
}
private fun hasLegs(): Boolean {
return route.routes.isNotEmpty() && route.routes[0].legs.isNotEmpty()
} }
fun stopNavigation() { fun stopNavigation() {
@@ -76,24 +89,24 @@ open class RouteModel() {
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
fun updateLocation(curLocation: Location, viewModel: ViewModel) { fun updateLocation(curLocation: Location, viewModel: ViewModel) {
location = curLocation location = curLocation
findStep(location) findStep(curLocation)
updateSpeedLimit(location, viewModel) updateSpeedLimit(curLocation, viewModel)
lastLocation = location
} }
private fun findStep(location: Location) { private fun findStep(location: Location) {
var nearestDistance = 100000.0f var nearestDistance = 100000f
for ((index, step) in curLeg.steps.withIndex()) { for ((index, step) in curLeg.steps.withIndex()) {
if (index >= route.currentStep) { if (index >= route.currentStepIndex) {
for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) { for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) {
if (wayIndex >= step.waypointIndex) { if (wayIndex >= step.waypointIndex) {
val distance = location.distanceTo(location(waypoint[0], waypoint[1])) val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
if (distance < nearestDistance) { if (distance < nearestDistance) {
nearestDistance = distance nearestDistance = distance
route.currentStep = step.index route.currentStepIndex = step.index
step.waypointIndex = wayIndex step.waypointIndex = wayIndex
step.wayPointLocation = location(waypoint[0], waypoint[1]) step.wayPointLocation = location(waypoint[0], waypoint[1])
lastLocation = location routeBearing = lastLocation.bearingTo(location)
bearing = lastLocation.bearingTo(location)
} }
} }
if (nearestDistance == 0F) { if (nearestDistance == 0F) {
@@ -108,6 +121,21 @@ open class RouteModel() {
} }
private fun currentLanes(location: Location): List<Lane> { private fun currentLanes(location: Location): List<Lane> {
var lanes = emptyList<Lane>()
if (route.legs().isNotEmpty()) {
route.legs().first().intersection.forEach { it ->
if (it.lane.isNotEmpty()) {
val distance = lastLocation.distanceTo(location(it.location[0], it.location[1]))
val sectionBearing =
lastLocation.bearingTo(location(it.location[0], it.location[1]))
if (distance < 500 && (routeBearing.absoluteValue - sectionBearing.absoluteValue).absoluteValue < 10) {
lanes = it.lane
}
}
}
}
return lanes
var inter = Intersection() var inter = Intersection()
var nearestDistance = 100000.0f var nearestDistance = 100000.0f
route.currentStep().intersection.forEach { route.currentStep().intersection.forEach {
@@ -117,7 +145,9 @@ open class RouteModel() {
if (distance < nearestDistance) { if (distance < nearestDistance) {
nearestDistance = distance nearestDistance = distance
if (distance <= NEXT_STEP_THRESHOLD * 3) { if (distance <= NEXT_STEP_THRESHOLD * 3) {
if ((interBearing.absoluteValue - route.currentStep().maneuver.bearingAfter.absoluteValue).absoluteValue < 20) { if (route.routeEngine == RouteEngine.TOMTOM.ordinal
|| (interBearing.absoluteValue - route.currentStep().maneuver.bearingAfter.absoluteValue).absoluteValue < 20
) {
inter = it inter = it
} }
} }
@@ -129,22 +159,24 @@ open class RouteModel() {
fun updateSpeedLimit(location: Location, viewModel: ViewModel) = runBlocking { fun updateSpeedLimit(location: Location, viewModel: ViewModel) = runBlocking {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val instruction = currentStep().instruction if (isNavigating()) {
val levenshtein = Levenshtein() val instruction = currentStep().instruction
// speed limit val levenshtein = Levenshtein()
val distance = lastSpeedLocation.distanceTo(location) // speed limit
if (distance > 500 || lastSpeedIndex < route.currentStep) { val distance = lastSpeedLocation.distanceTo(location)
lastSpeedIndex = route.currentStep if (distance > 500 || lastSpeedIndex < route.currentStepIndex) {
val elements = viewModel.getMaxSpeed(location) lastSpeedIndex = route.currentStepIndex
elements.forEach { val elements = viewModel.getMaxSpeed(location)
if (it.tags.name != null) { elements.forEach {
if (isNavigating()) { if (it.tags.name != null) {
val distance = if (isNavigating()) {
levenshtein.distance(it.tags.name!!, instruction) val distance =
if (distance < 5) { levenshtein.distance(it.tags.name!!, instruction)
val speed = it.tags.maxspeed.toInt() if (distance < 5) {
maxSpeed = speed val speed = it.tags.maxspeed.toInt()
lastSpeedLocation = location maxSpeed = speed
lastSpeedLocation = location
}
} }
} }
} }
@@ -154,32 +186,13 @@ open class RouteModel() {
} }
fun currentStep(): StepData { fun currentStep(): StepData {
val currentStep = route.currentStep()
// Determine if we should display the current or the next maneuver
val distanceToNextStep = leftStepDistance() 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 curManeuverType = if (hasArrived(currentStep.maneuver.type)) { val currentStep = route.nextStep(1) // This advances the route's state
currentStep.maneuver.type
} else {
Maneuver.TYPE_STRAIGHT
}
// Get the single, correct maneuver for this state
val relevantStep = 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 = relevantStep.name val streetName = currentStep.name
var exitNumber = currentStep.maneuver.exit val curManeuverType = currentStep.maneuver.type
if (shouldAdvance) { val exitNumber = currentStep.maneuver.exit
curManeuverType = relevantStep.maneuver.type
exitNumber = relevantStep.maneuver.exit
}
val maneuverIcon = maneuverIcon(curManeuverType) val maneuverIcon = maneuverIcon(curManeuverType)
maneuverType = curManeuverType maneuverType = curManeuverType
@@ -198,9 +211,8 @@ open class RouteModel() {
) )
} }
fun nextStep(): StepData { fun nextStep(): StepData {
val step = route.nextStep() val step = route.nextStep(2)
val maneuverType = step.maneuver.type val maneuverType = step.maneuver.type
val distanceLeft = leftStepDistance() val distanceLeft = leftStepDistance()
var text = "" var text = ""
@@ -226,14 +238,13 @@ open class RouteModel() {
travelLeftDistance(), travelLeftDistance(),
listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())), listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())),
step.maneuver.exit step.maneuver.exit
) )
} }
fun travelLeftTime(): Double { fun travelLeftTime(): Double {
var timeLeft = 0.0 var timeLeft = 0.0
// time for next step until end step // time for next step until end step
for (i in route.currentStep + 1..<curLeg.steps.size) { for (i in route.currentStepIndex + 1..<curLeg.steps.size) {
val step = curLeg.steps[i] val step = curLeg.steps[i]
timeLeft += step.duration timeLeft += step.duration
} }
@@ -270,14 +281,14 @@ open class RouteModel() {
return (leftDistance / 10.0).roundToInt() * 10.0 return (leftDistance / 10.0).roundToInt() * 10.0
} }
/** Returns the left distance in km. */ /** Returns the left distance in m. */
fun travelLeftDistance(): Double { fun travelLeftDistance(): Double {
var leftDistance = 0.0 var leftDistance = 0.0
for (i in route.currentStep + 1..<curLeg.steps.size) { for (i in route.currentStepIndex + 1..<curLeg.steps.size) {
val step = route.legs()[0].steps[i] val step = route.legs()[0].steps[i]
leftDistance += step.distance leftDistance += step.distance
} }
leftDistance += leftStepDistance() / 1000 leftDistance += leftStepDistance()
return leftDistance return leftDistance
} }
@@ -343,6 +354,100 @@ open class RouteModel() {
|| type == ManeuverType.DestinationLeft.value || type == ManeuverType.DestinationLeft.value
} }
fun addLanes(stepData: StepData) {
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)
println(laneDirection)
// TODO:
}
}
}
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", "slight_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 { fun createLaneIcon(context: Context, stepData: StepData): IconCompat {
val bitmaps = mutableListOf<Bitmap>() val bitmaps = mutableListOf<Bitmap>()
@@ -396,6 +501,7 @@ open class RouteModel() {
"${direction}_${it.trim()}" "${direction}_${it.trim()}"
} }
} }
direction = direction.lowercase()
return when (direction) { return when (direction) {
"left_straight" -> { "left_straight" -> {
when (stepData.currentManeuverType) { when (stepData.currentManeuverType) {
@@ -419,8 +525,8 @@ open class RouteModel() {
"right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_RIGHT) "${direction}_o" else "${direction}_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" "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" "straight" -> if (stepData.currentManeuverType == Maneuver.TYPE_STRAIGHT) "${direction}_o" else "${direction}_x"
"right_slight" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_RIGHT) "${direction}_o" else "${direction}_x" "right_slight", "slight_right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_RIGHT) "${direction}_o" else "${direction}_x"
"left_slight" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_LEFT) "${direction}_o" else "${direction}_x" "left_slight", "slight_left" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_LEFT) "${direction}_o" else "${direction}_x"
else -> { else -> {
"" ""
} }
@@ -438,6 +544,7 @@ open class RouteModel() {
"right_o" -> R.drawable.right_o "right_o" -> R.drawable.right_o
"slight_right_x" -> R.drawable.slight_right_x "slight_right_x" -> R.drawable.slight_right_x
"slight_right_o" -> R.drawable.slight_right_o "slight_right_o" -> R.drawable.slight_right_o
"slight_left_x" -> R.drawable.left_x
"straight_x" -> R.drawable.straight_x "straight_x" -> R.drawable.straight_x
"right_o_straight_x" -> R.drawable.right_o_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_x" -> R.drawable.right_x_straight_x
@@ -446,7 +553,7 @@ open class RouteModel() {
"left_o_straight_x" -> R.drawable.left_o_straight_x "left_o_straight_x" -> R.drawable.left_o_straight_x
"left_x_straight_o" -> R.drawable.left_x_straight_o "left_x_straight_o" -> R.drawable.left_x_straight_o
else -> { else -> {
R.drawable.ic_close_white_24dp R.drawable.left_x
} }
} }
} }

View File

@@ -280,18 +280,20 @@ 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)
} }
} }

View File

@@ -90,6 +90,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(

View 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>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M120,720L120,640L840,640L840,720L120,720ZM120,520L120,440L840,440L840,520L120,520ZM120,320L120,240L840,240L840,320L120,320Z"/>
</vector>

View File

@@ -24,7 +24,7 @@
}, },
{ {
"key": "departAt", "key": "departAt",
"value": "2026-01-29T08:43:35.397Z" "value": "2026-02-06T09:09:59.054Z"
}, },
{ {
"key": "guidanceVersion", "key": "guidanceVersion",
@@ -44,7 +44,7 @@
}, },
{ {
"key": "locations", "key": "locations",
"value": "48.18565,11.57928:48.11830,11.59485" "value": "48.18575,11.57939:48.11654,11.59449"
}, },
{ {
"key": "maxAlternatives", "key": "maxAlternatives",
@@ -60,11 +60,11 @@
}, },
{ {
"key": "sectionType", "key": "sectionType",
"value": "lanes" "value": "traffic"
}, },
{ {
"key": "sectionType", "key": "sectionType",
"value": "traffic" "value": "lanes"
}, },
{ {
"key": "traffic", "key": "traffic",
@@ -119,46 +119,28 @@
"routes": [ "routes": [
{ {
"summary": { "summary": {
"lengthInMeters": 10879, "lengthInMeters": 11116,
"travelTimeInSeconds": 1170, "travelTimeInSeconds": 1148,
"trafficDelayInSeconds": 76, "trafficDelayInSeconds": 0,
"trafficLengthInMeters": 1727, "trafficLengthInMeters": 0,
"departureTime": "2026-01-29T09:43:35+01:00", "departureTime": "2026-02-06T10:09:59+01:00",
"arrivalTime": "2026-01-29T10:03:05+01:00" "arrivalTime": "2026-02-06T10:29:07+01:00"
}, },
"legs": [ "legs": [
{ {
"summary": { "summary": {
"lengthInMeters": 10879, "lengthInMeters": 11116,
"travelTimeInSeconds": 1170, "travelTimeInSeconds": 1148,
"trafficDelayInSeconds": 76, "trafficDelayInSeconds": 0,
"trafficLengthInMeters": 1727, "trafficLengthInMeters": 0,
"departureTime": "2026-01-29T09:43:35+01:00", "departureTime": "2026-02-06T10:09:59+01:00",
"arrivalTime": "2026-01-29T10:03:05+01:00" "arrivalTime": "2026-02-06T10:29:07+01:00"
}, },
"encodedPolyline": "sfbeHmqteAEjDQEy@GQ?wDQFkEH{G?M?sA@kB?_FAeC?o@?[@_@\\Ab@CVAz@CJA@?dBGhAGjAExAGlBEvBKdCKTAfCKLAv@ELA|AGnAGt@ClCKjDQpBIf@BXDPDPBF@ZB?S@SAYAUKi@Go@Cc@BgBBs@Bg@JyAJiAXqBDWNs@TaA\\mAFa@Po@`BwF?YL]FSFSl@iB^kALc@L]Ro@f@yAf@{AFQNe@dAoCdBgEx@qBTi@BITe@L[L_@^}@HSXu@pB}El@eAb@e@f@[h@QZCRAL?j@HRFh@Vf@XLJhAn@lAv@TLtAz@r@`@bAn@pAv@f@X~@j@z@h@vBpA`@VHDFFJFf@X`CzApAh@f@L`@Fz@@f@AXEVEVEhA[h@Yn@e@PQFEJKRWV[PW`@w@t@}AHQN]~BiFP]`AoBh@aADGTa@t@aAt@{@PQJKJGFG@Cd@]XSxDmCf@a@n@o@TY\\g@LQHMJSLUP[f@iAPg@b@yAFODMNi@FS|@qCVaAHUHUn@wBHYh@eBpAkEjBiGfAeDj@yADMFQd@sAf@kAJUh@qAf@eAt@sAn@iALSN[p@kAVc@JOLSj@w@z@}@x@q@pAu@p@_@j@Sl@MLCRCb@E`@?^?L@`ABz@?N@~AFdADJ@rAH`DVpCVrAJd@BfHp@zGl@pAJ|ALnGp@jEh@fBJpAFF?P@N@ZCtC]r@GJCFCLCD?TEVEXGhAYzAg@NGv@]`@QJEvB_AXMVK\\Qb@Qn@QJCNAZC^ENA`@FnBb@xEtA^H^JnCl@z@r@`@Pr@TtBlA~C`Bn@\\xAl@PF`@LrAVlCh@bBLl@BlBJdG\\RDjCHn@?pB?xB?R@`@@GxAC^?ZInBIfCAvC?r@@dD@n@@b@@^D`C?TDxAFbBHdB@VHp@RjAJb@NNH`@VlBFf@PzARhBFd@@LRbBFh@\\nC@FNhAb@lEj@tDPpABTBRZlBTdBXjBn@xEBLDTRpAR~@Fb@", "encodedPolyline": "sfbeHarteAE~DQEy@GQ?wDQFkEH{G?M?sA@kB?_FAeC?o@?[@_@\\Ab@CVAz@CJA@?dBGhAGjAExAGlBEvBKdCKTAfCKLAv@ELA|AGnAGt@ClCKjDQpBIf@BXDPDPBF@ZB?S@SAYAUKi@Go@Cc@BgBBs@Bg@JyAJiAXqBDWNs@TaA\\mAFa@Po@`BwF?YL]FSFSl@iB^kALc@L]Ro@f@yAf@{AFQNe@dAoCdBgEx@qBTi@BITe@L[L_@^}@HSXu@pB}El@eAb@e@f@[h@QZCRAL?j@HRFh@Vf@XLJhAn@lAv@TLtAz@r@`@bAn@pAv@f@X~@j@z@h@vBpA`@VHDFFJFf@X`CzApAh@f@L`@Fz@@f@AXEVEVEhA[h@Yn@e@PQFEJKRWV[PW`@w@t@}AHQN]~BiFP]`AoBh@aADGTa@t@aAt@{@PQJKJGFG@Cd@]XSxDmCf@a@n@o@TY\\g@LQHMJSLUP[f@iAPg@b@yAFODMNi@FS|@qCVaAHUHUn@wBHYh@eBpAkEjBiGfAeDj@yADMFQd@sAf@kAJUh@qAf@eAt@sAn@iALSN[p@kAVc@JOLSj@w@z@}@x@q@pAu@p@_@j@Sl@MLCRCb@E`@?^?L@`ABz@?N@~AFdADJ@rAH`DVpCVrAJd@BfHp@zGl@pAJ|ALnGp@jEh@fBJpAFF?P@N@ZCtC]r@GJCFCLCD?TEVEXGhAYzAg@NGv@]`@QJEvB_AXMVK\\Qb@Qn@QJCNAZC^ENA`@FnBb@xEtA^H^JnCl@z@r@`@Pr@TtBlA~C`Bn@\\xAl@PF`@LrAVlCh@bBLl@BlBJdG\\RDjCHn@?pB?xB?R@`@@GxAC^?ZInBIfCAvC?r@@dD@n@@b@@^D`C?TDxAFbBHdB@VHp@RjAJb@NNH`@VlBFf@PzARhBFd@@LRbBFh@\\nC@FNhAb@lEj@tDPpABTBRZlBTdBXjBn@xEBLDTRpAR~@l@jDj@Qv@IrEP",
"encodedPolylinePrecision": 5 "encodedPolylinePrecision": 5
} }
], ],
"sections": [ "sections": [
{
"startPointIndex": 83,
"endPointIndex": 147,
"sectionType": "TRAFFIC",
"simpleCategory": "JAM",
"effectiveSpeedInKmh": 35,
"delayInSeconds": 76,
"magnitudeOfDelay": 1,
"tec": {
"causes": [
{
"mainCauseCode": 1
}
],
"effectCode": 4
},
"eventId": "TTL41048054144049000"
},
{ {
"lanes": [ "lanes": [
{ {
@@ -349,9 +331,8 @@
"travelTimeInSeconds": 0, "travelTimeInSeconds": 0,
"point": { "point": {
"latitude": 48.18554, "latitude": 48.18554,
"longitude": 11.57927 "longitude": 11.57937
}, },
"exitNumber": "",
"pointIndex": 0, "pointIndex": 0,
"instructionType": "LOCATION_DEPARTURE", "instructionType": "LOCATION_DEPARTURE",
"street": "Vogelhartstraße", "street": "Vogelhartstraße",
@@ -362,8 +343,8 @@
"message": "Leave from Vogelhartstraße" "message": "Leave from Vogelhartstraße"
}, },
{ {
"routeOffsetInMeters": 64, "routeOffsetInMeters": 72,
"travelTimeInSeconds": 14, "travelTimeInSeconds": 16,
"point": { "point": {
"latitude": 48.18557, "latitude": 48.18557,
"longitude": 11.57841 "longitude": 11.57841
@@ -380,8 +361,8 @@
"message": "Turn right onto Silcherstraße" "message": "Turn right onto Silcherstraße"
}, },
{ {
"routeOffsetInMeters": 218, "routeOffsetInMeters": 226,
"travelTimeInSeconds": 57, "travelTimeInSeconds": 60,
"point": { "point": {
"latitude": 48.18696, "latitude": 48.18696,
"longitude": 11.57857 "longitude": 11.57857
@@ -398,8 +379,8 @@
"message": "Turn right onto Schmalkaldener Straße" "message": "Turn right onto Schmalkaldener Straße"
}, },
{ {
"routeOffsetInMeters": 650, "routeOffsetInMeters": 658,
"travelTimeInSeconds": 131, "travelTimeInSeconds": 134,
"point": { "point": {
"latitude": 48.18686, "latitude": 48.18686,
"longitude": 11.58437 "longitude": 11.58437
@@ -419,8 +400,8 @@
"message": "Turn right onto Ingolstädter Straße/B13" "message": "Turn right onto Ingolstädter Straße/B13"
}, },
{ {
"routeOffsetInMeters": 1713, "routeOffsetInMeters": 1720,
"travelTimeInSeconds": 266, "travelTimeInSeconds": 267,
"point": { "point": {
"latitude": 48.17733, "latitude": 48.17733,
"longitude": 11.58503 "longitude": 11.58503
@@ -441,8 +422,8 @@
"combinedMessage": "Turn left onto Schenkendorfstraße/B2R then keep left at Schenkendorfstraße/B2R toward Messe / ICM" "combinedMessage": "Turn left onto Schenkendorfstraße/B2R then keep left at Schenkendorfstraße/B2R toward Messe / ICM"
}, },
{ {
"routeOffsetInMeters": 2067, "routeOffsetInMeters": 2075,
"travelTimeInSeconds": 309, "travelTimeInSeconds": 307,
"point": { "point": {
"latitude": 48.17678, "latitude": 48.17678,
"longitude": 11.58957 "longitude": 11.58957
@@ -464,8 +445,8 @@
"combinedMessage": "Keep left at Schenkendorfstraße/B2R toward Messe / ICM then keep left at Schenkendorfstraße/B2R toward Passau" "combinedMessage": "Keep left at Schenkendorfstraße/B2R toward Messe / ICM then keep left at Schenkendorfstraße/B2R toward Passau"
}, },
{ {
"routeOffsetInMeters": 2419, "routeOffsetInMeters": 2426,
"travelTimeInSeconds": 332, "travelTimeInSeconds": 329,
"point": { "point": {
"latitude": 48.17518, "latitude": 48.17518,
"longitude": 11.59363 "longitude": 11.59363
@@ -486,8 +467,8 @@
"message": "Keep left at Schenkendorfstraße/B2R toward Passau" "message": "Keep left at Schenkendorfstraße/B2R toward Passau"
}, },
{ {
"routeOffsetInMeters": 2774, "routeOffsetInMeters": 2781,
"travelTimeInSeconds": 357, "travelTimeInSeconds": 353,
"point": { "point": {
"latitude": 48.17329, "latitude": 48.17329,
"longitude": 11.59747 "longitude": 11.59747
@@ -506,8 +487,8 @@
"message": "Follow Isarring/B2R toward München-Ost" "message": "Follow Isarring/B2R toward München-Ost"
}, },
{ {
"routeOffsetInMeters": 8425, "routeOffsetInMeters": 8433,
"travelTimeInSeconds": 806, "travelTimeInSeconds": 734,
"point": { "point": {
"latitude": 48.13017, "latitude": 48.13017,
"longitude": 11.61541 "longitude": 11.61541
@@ -524,8 +505,8 @@
"message": "Bear right at Ampfingstraße" "message": "Bear right at Ampfingstraße"
}, },
{ {
"routeOffsetInMeters": 9487, "routeOffsetInMeters": 9495,
"travelTimeInSeconds": 953, "travelTimeInSeconds": 884,
"point": { "point": {
"latitude": 48.12089, "latitude": 48.12089,
"longitude": 11.61285 "longitude": 11.61285
@@ -543,8 +524,8 @@
"combinedMessage": "Turn right onto Anzinger Straße then keep straight on at Sankt-Martin-Straße" "combinedMessage": "Turn right onto Anzinger Straße then keep straight on at Sankt-Martin-Straße"
}, },
{ {
"routeOffsetInMeters": 9983, "routeOffsetInMeters": 9991,
"travelTimeInSeconds": 1044, "travelTimeInSeconds": 974,
"point": { "point": {
"latitude": 48.12087, "latitude": 48.12087,
"longitude": 11.60621 "longitude": 11.60621
@@ -561,20 +542,39 @@
"message": "Keep straight on at Sankt-Martin-Straße" "message": "Keep straight on at Sankt-Martin-Straße"
}, },
{ {
"routeOffsetInMeters": 10879, "routeOffsetInMeters": 10941,
"travelTimeInSeconds": 1170, "travelTimeInSeconds": 1103,
"point": { "point": {
"latitude": 48.1183, "latitude": 48.11811,
"longitude": 11.59485 "longitude": 11.59417
}, },
"pointIndex": 335, "pointIndex": 335,
"instructionType": "TURN",
"street": "Hohenwaldeckstraße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": -90,
"possibleCombineWithNext": true,
"drivingSide": "RIGHT",
"maneuver": "TURN_LEFT",
"message": "Turn left onto Hohenwaldeckstraße",
"combinedMessage": "Turn left onto Hohenwaldeckstraße then you have arrived at Hohenwaldeckstraße. Your destination is on the left"
},
{
"routeOffsetInMeters": 11116,
"travelTimeInSeconds": 1148,
"point": {
"latitude": 48.11655,
"longitude": 11.59422
},
"pointIndex": 338,
"instructionType": "LOCATION_ARRIVAL", "instructionType": "LOCATION_ARRIVAL",
"street": "Sankt-Martin-Straße", "street": "Hohenwaldeckstraße",
"countryCode": "DEU", "countryCode": "DEU",
"possibleCombineWithNext": false, "possibleCombineWithNext": false,
"drivingSide": "RIGHT", "drivingSide": "RIGHT",
"maneuver": "ARRIVE", "maneuver": "ARRIVE_LEFT",
"message": "You have arrived at Sankt-Martin-Straße" "message": "You have arrived at Hohenwaldeckstraße. Your destination is on the left"
} }
], ],
"instructionGroups": [ "instructionGroups": [
@@ -582,19 +582,25 @@
"firstInstructionIndex": 0, "firstInstructionIndex": 0,
"lastInstructionIndex": 3, "lastInstructionIndex": 3,
"groupMessage": "Leave from Vogelhartstraße. Take the Ingolstädter Straße/B13", "groupMessage": "Leave from Vogelhartstraße. Take the Ingolstädter Straße/B13",
"groupLengthInMeters": 1713 "groupLengthInMeters": 1720
}, },
{ {
"firstInstructionIndex": 4, "firstInstructionIndex": 4,
"lastInstructionIndex": 7, "lastInstructionIndex": 7,
"groupMessage": "Take the Schenkendorfstraße, Isarring/B2R toward Messe / ICM, Passau, München-Ost", "groupMessage": "Take the Schenkendorfstraße, Isarring/B2R toward Messe / ICM, Passau, München-Ost",
"groupLengthInMeters": 6712 "groupLengthInMeters": 6713
}, },
{ {
"firstInstructionIndex": 8, "firstInstructionIndex": 8,
"lastInstructionIndex": 11, "lastInstructionIndex": 10,
"groupMessage": "Take the Ampfingstraße, Anzinger Straße. Continue to your destination at Sankt-Martin-Straße", "groupMessage": "Take the Ampfingstraße, Anzinger Straße, Sankt-Martin-Straße",
"groupLengthInMeters": 2454 "groupLengthInMeters": 2508
},
{
"firstInstructionIndex": 11,
"lastInstructionIndex": 12,
"groupMessage": "Get to your destination at Hohenwaldeckstraße",
"groupLengthInMeters": 175
} }
] ]
} }

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@
<string name="reject_action_title" msgid="6730366705938402668">"Ablehnen"</string> <string name="reject_action_title" msgid="6730366705938402668">"Ablehnen"</string>
<string name="ok_action_title" msgid="7128494973966098611">"OK"</string> <string name="ok_action_title" msgid="7128494973966098611">"OK"</string>
<string name="favorites" msgid="522064494016370117">"Favoriten"</string> <string name="favorites" msgid="522064494016370117">"Favoriten"</string>
<string name="display_settings" msgid="5726880972110281095">"Einstellungen für die Anzeige"</string> <string name="display_settings" msgid="5726880972110281095">"Anzeige"</string>
<string name="arrived_exclamation_msg" msgid="9132265698563096988">"Angekommen!"</string> <string name="arrived_exclamation_msg" msgid="9132265698563096988">"Angekommen!"</string>
<string name="category_title" msgid="4783851267093259949">"Kategorien"</string> <string name="category_title" msgid="4783851267093259949">"Kategorien"</string>
<string name="no_places" msgid="7246005909846715898">"Keine Orte"</string> <string name="no_places" msgid="7246005909846715898">"Keine Orte"</string>
@@ -42,12 +42,13 @@
<string name="contacts">Kontakte</string> <string name="contacts">Kontakte</string>
<string name="route_preview">Route Vorschau</string> <string name="route_preview">Route Vorschau</string>
<string name="display">Anzeige</string> <string name="display">Anzeige</string>
<string name="navigation_settings">Navigations Einstellungen</string> <string name="navigation_settings">Navigation</string>
<string name="fuel_station">Tankstelle</string> <string name="fuel_station">Tankstelle</string>
<string name="pharmacy">Apotheke</string> <string name="pharmacy">Apotheke</string>
<string name="charging_station">Ladestation</string> <string name="charging_station">Ladestation</string>
<string name="speed_camera">Speed camera</string> <string name="speed_camera">Speed camera</string>
<string name="use_car_location">Auto GPS verwenden</string> <string name="use_car_location">Auto GPS verwenden</string>
<string name="tomtom">TomTom\t</string> <string name="tomtom">TomTom\t</string>
<string name="options">Optionen</string>
</resources> </resources>

View File

@@ -10,7 +10,7 @@
<string name="off_action_title">Off</string> <string name="off_action_title">Off</string>
<string name="use_telephon_settings">Use telephon settings</string> <string name="use_telephon_settings">Use telephon settings</string>
<string name="dark_mode">Dark mode</string> <string name="dark_mode">Dark mode</string>
<string name="display_settings">Display settings</string> <string name="display_settings">Display</string>
<string name="threed_building">3D building</string> <string name="threed_building">3D building</string>
<string name="arrived_exclamation_msg">Arrived!</string> <string name="arrived_exclamation_msg">Arrived!</string>
<string name="drive_now">Drive now</string> <string name="drive_now">Drive now</string>
@@ -24,7 +24,7 @@
<string name="recent_Item_deleted">Recent item deleted</string> <string name="recent_Item_deleted">Recent item deleted</string>
<string name="route_preview">Route preview</string> <string name="route_preview">Route preview</string>
<string name="display">Display</string> <string name="display">Display</string>
<string name="navigation_settings">Navigation settings</string> <string name="navigation_settings">Navigation</string>
<string name="settings_action_title">Settings</string> <string name="settings_action_title">Settings</string>
<string name="accept_action_title">Accept</string> <string name="accept_action_title">Accept</string>
<string name="reject_action_title">Reject</string> <string name="reject_action_title">Reject</string>
@@ -35,4 +35,5 @@
<string name="routing_engine" translatable="false">Routing engine</string> <string name="routing_engine" translatable="false">Routing engine</string>
<string name="use_car_location">Use car location</string> <string name="use_car_location">Use car location</string>
<string name="tomtom">TomTom\t</string> <string name="tomtom">TomTom\t</string>
<string name="options">Options</string>
</resources> </resources>

View File

@@ -38,6 +38,7 @@ material3WindowSizeClass = "1.4.0"
uiGraphics = "1.10.0" uiGraphics = "1.10.0"
window = "1.5.1" window = "1.5.1"
foundationLayout = "1.10.0" foundationLayout = "1.10.0"
foundationLayoutVersion = "1.10.1"
[libraries] [libraries]
@@ -79,6 +80,7 @@ androidx-app-automotive = { module = "androidx.car.app:app-automotive", version.
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "uiGraphics" } androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "uiGraphics" }
androidx-window = { group = "androidx.window", name = "window", version.ref = "window" } androidx-window = { group = "androidx.window", name = "window", version.ref = "window" }
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" } androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
androidx-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayoutVersion" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }