Compare commits

...
21 Commits
Author SHA1 Message Date
Dimitris b99ebfd36f Service 2026-04-04 13:16:15 +02:00
Dimitris 69b27d3b6c Service 2026-04-04 09:39:54 +02:00
Dimitris 8b886c36b1 Service 2026-04-03 13:23:26 +02:00
Dimitris 2ce079a7c1 Service 2026-04-03 12:30:39 +02:00
Dimitris 8af2d3ad0b Service 2026-04-03 10:00:00 +02:00
Dimitris 1d67b3cc06 Arrival Issue 2026-04-03 09:59:32 +02:00
Dimitris a4227c80d3 NavigationService 2026-04-03 09:57:57 +02:00
Dimitris 757c4c8d8d NavigationService 2026-04-03 09:57:52 +02:00
Dimitris 24173412e8 Automotive, CarSession 2026-04-03 09:54:43 +02:00
Dimitris 52f8dec2e6 Stopover and Legs, ManeuverType 2026-04-01 16:24:51 +02:00
Dimitris 6838ad09c4 Stopover and Legs 2026-03-31 17:06:55 +02:00
Dimitris 60b842d883 Nominatim 2026-03-30 10:00:16 +02:00
Dimitris 9def7a5c64 Nominatim 2026-03-30 09:58:13 +02:00
Dimitris bd8a497fbe Diverse 2026-03-30 09:18:04 +02:00
Dimitris 8ca450cd10 Camerat Duration 3 seconds 2026-03-28 13:43:36 +01:00
Dimitris 94d6d6d311 Overlay icons with paint 2026-03-28 13:37:05 +01:00
Dimitris d81d33df43 Arrival Issue 2026-03-28 12:41:12 +01:00
Dimitris 5317a14fb3 Arrival Issue 2026-03-28 11:17:51 +01:00
Dimitris 90010d91b7 Notification 2026-03-27 13:40:14 +01:00
Dimitris 2348d3b633 Duration Map 2026-03-27 07:17:53 +01:00
Dimitris 263b5b576d Navigation Screen to Session, Remove NavigationService 2026-03-26 17:16:04 +01:00
82 changed files with 3037 additions and 1158 deletions
+4 -3
View File
@@ -17,8 +17,8 @@ android {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 36
versionCode = 83 versionCode = 91
versionName = "0.2.0.83" versionName = "0.2.3.91"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -101,9 +101,10 @@ dependencies {
implementation(libs.maplibre.compose) implementation(libs.maplibre.compose)
implementation(libs.accompanist.permissions) implementation(libs.accompanist.permissions)
implementation(project(":common:car")) implementation(project(":common:car"))
implementation(project(":common:data")) implementation(project(":common:data"))
implementation(libs.androidx.car.app)
implementation(libs.androidx.app.projected)
implementation(libs.play.services.location) implementation(libs.play.services.location)
implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
+6 -1
View File
@@ -41,7 +41,12 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service <service
android:name="com.kouros.navigation.car.navigation.NavigationService" android:name=".car.NavigationNotificationService"
android:foregroundServiceType="location"
android:exported="true">
</service>
<service
android:name=".car.navigation.NavigationService"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="location" android:foregroundServiceType="location"
android:exported="true"> android:exported="true">
@@ -179,9 +179,9 @@ fun Categories(
modifier = Modifier.horizontalScroll(scrollState) modifier = Modifier.horizontalScroll(scrollState)
) { ) {
Button(onClick = { Button(onClick = {
val places = viewModel.loadRecentPlace(applicationContext) val places = viewModel.loadRecentPlaces(applicationContext)
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude) val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
viewModel.loadRoute(applicationContext, location, toLocation, 0F) viewModel.loadRoute(applicationContext, location, listOf(toLocation), 0F)
closeSheet() closeSheet()
}) { }) {
Icon( Icon(
@@ -248,7 +248,7 @@ private fun SearchPlaces(
viewModel.saveRecent(context, pl) viewModel.saveRecent(context, pl)
val toLocation = val toLocation =
location(place.lon.toDouble(), place.lat.toDouble()) location(place.lon.toDouble(), place.lat.toDouble())
viewModel.loadRoute(context, location, toLocation, 0F) viewModel.loadRoute(context, location, listOf(toLocation), 0F)
closeSheet() closeSheet()
} }
.fillMaxWidth() .fillMaxWidth()
@@ -116,9 +116,9 @@ fun Home(
) { ) {
Row(horizontalArrangement = Arrangement.SpaceBetween) { Row(horizontalArrangement = Arrangement.SpaceBetween) {
Button(onClick = { Button(onClick = {
val places = viewModel.loadRecentPlace(applicationContext) val places = viewModel.loadRecentPlaces(applicationContext)
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude) val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
viewModel.loadRoute(applicationContext, location, toLocation, 0F) viewModel.loadRoute(applicationContext, location, listOf(toLocation), 0F)
closeSheet() closeSheet()
}) { }) {
Icon( Icon(
@@ -168,7 +168,7 @@ private fun RecentPlaces(
modifier = Modifier modifier = Modifier
.clickable { .clickable {
val toLocation = location(place.longitude, place.latitude) val toLocation = location(place.longitude, place.latitude)
viewModel.loadRoute(context, location, toLocation, 0F) viewModel.loadRoute(context, location, listOf(toLocation), 0F)
closeSheet() closeSheet()
} }
.fillMaxWidth() .fillMaxWidth()
-1
View File
@@ -37,7 +37,6 @@ android {
dependencies { dependencies {
implementation(libs.androidx.app.automotive) implementation(libs.androidx.app.automotive)
implementation(libs.androidx.car.app)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.runtime.livedata) implementation(libs.androidx.runtime.livedata)
implementation(project(":common:car")) implementation(project(":common:car"))
+2 -2
View File
@@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.car.permission.CAR_SPEED"/> <uses-permission android:name="android.car.permission.CAR_SPEED"/>
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" /> <uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-feature <uses-feature
android:name="android.hardware.type.automotive" android:name="android.hardware.type.automotive"
@@ -71,8 +72,7 @@
android:value="true" /> android:value="true" />
</activity> </activity>
<service <service
android:name="com.kouros.navigation.car.navigation.NavigationService" android:name=".car.NavigationNotificationService"
android:enabled="true"
android:foregroundServiceType="location" android:foregroundServiceType="location"
android:exported="true"> android:exported="true">
</service> </service>
@@ -4,10 +4,10 @@
android:viewportWidth="960" android:viewportWidth="960"
android:viewportHeight="960" android:viewportHeight="960"
android:tint="#1A7416"> android:tint="#1A7416">
<group android:scaleX="0.7888" <group android:scaleX="0.58"
android:scaleY="0.7888" android:scaleY="0.58"
android:translateX="101.376" android:translateX="201.6"
android:translateY="101.376"> android:translateY="201.6">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M319,680L480,607L641,680L656,665L480,240L304,665L319,680ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/> android:pathData="M319,680L480,607L641,680L656,665L480,240L304,665L319,680ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

-1
View File
@@ -42,7 +42,6 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.ui) implementation(libs.androidx.ui)
implementation(libs.maplibre.compose) implementation(libs.maplibre.compose)
implementation(libs.androidx.app.projected)
implementation(project(":common:data")) implementation(project(":common:data"))
implementation(libs.androidx.runtime.livedata) implementation(libs.androidx.runtime.livedata)
implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.foundation)
@@ -2,12 +2,11 @@ package com.kouros.navigation.car
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import androidx.car.app.navigation.model.Maneuver
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kouros.data.R
import com.kouros.navigation.data.Constants.homeHohenwaldeck import com.kouros.navigation.data.Constants.homeHohenwaldeck
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.route.ManeuverType
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
@@ -33,6 +32,37 @@ class RouteModelTest {
val routeModel = RouteModel() val routeModel = RouteModel()
val location = Location(LocationManager.GPS_PROVIDER) val location = Location(LocationManager.GPS_PROVIDER)
val distance = listOf(
1025.5,
989.8,
963.5,
923.7,
915.8,
914.6,
871.0,
822.7,
769.7,
713.8,
644.8,
577.6,
501.7,
489.7,
452.5,
437.4,
398.0,
390.1,
341.3,
266.6,
219.5,
140.7,
77.4,
55.1,
40.0,
30.0,
19.0,
4.0
)
@Before @Before
fun setup() { fun setup() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
@@ -50,8 +80,8 @@ class RouteModelTest {
@Test @Test
fun checkRoute() { fun checkRoute() {
assertEquals(true, routeModel.isNavigating()) assertEquals(true, routeModel.isNavigating())
assertEquals(routeModel.curRoute.summary.distance, 11116.0, 10.0) assertEquals(routeModel.curRoute.summary.distance, 11108.0, 10.0)
assertEquals(routeModel.curRoute.summary.duration, 1581.0, 10.0) assertEquals(routeModel.curRoute.summary.duration, 1094.0, 10.0)
} }
@Test @Test
@@ -60,11 +90,11 @@ class RouteModelTest {
location.longitude = 11.579034 location.longitude = 11.579034
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(stepData.instruction, "Silcherstraße") assertEquals(stepData.instruction, "Silcherstraße")
assertEquals(stepData.leftStepDistance, 20.0, 5.0) assertEquals(stepData.leftStepDistance, 20.0, 5.0)
val nextStepData = routeModel.nextStep() val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(nextStepData.instruction, "Schmalkaldener Straße") assertEquals(nextStepData.instruction, "Schmalkaldener Straße")
} }
@@ -75,11 +105,11 @@ class RouteModelTest {
location.longitude = 11.576652 location.longitude = 11.576652
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(stepData.instruction, "Schmalkaldener Straße") assertEquals(stepData.instruction, "Schmalkaldener Straße")
assertEquals(stepData.leftStepDistance, 0.0, 1.0) assertEquals(stepData.leftStepDistance, 0.0, 1.0)
val nextStepData = routeModel.nextStep() val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(nextStepData.instruction, "Ingolstädter Straße") assertEquals(nextStepData.instruction, "Ingolstädter Straße")
} }
@@ -90,13 +120,13 @@ class RouteModelTest {
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
if (routeModel.navState.nextStep) { if (routeModel.navState.nextStep) {
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_STRAIGHT) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_STRAIGHT.value)
assertEquals(stepData.instruction, "Ingolstädter Straße") assertEquals(stepData.instruction, "Ingolstädter Straße")
val nextStepData = routeModel.nextStep() val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT) assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
assertEquals(nextStepData.instruction, "Schenkendorfstraße") assertEquals(nextStepData.instruction, "Schenkendorfstraße")
} else { } else {
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
} }
assertEquals(stepData.leftStepDistance, 301.0, 1.0) assertEquals(stepData.leftStepDistance, 301.0, 1.0)
} }
@@ -108,14 +138,14 @@ class RouteModelTest {
location.bearing = 180.0F location.bearing = 180.0F
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
assertEquals(stepData.instruction, "Schenkendorfstraße") assertEquals(stepData.instruction, "Schenkendorfstraße")
assertEquals(stepData.leftStepDistance, 170.0, 10.0) assertEquals(stepData.leftStepDistance, 170.0, 10.0)
assertEquals(stepData.lane.size, 4) assertEquals(stepData.lane.size, 4)
assertEquals(stepData.lane.first().valid, true) assertEquals(stepData.lane.first().valid, true)
assertEquals(stepData.lane.last().valid, false) assertEquals(stepData.lane.last().valid, false)
val nextStepData = routeModel.nextStep() val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_KEEP_LEFT) assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_KEEP_LEFT.value)
assertEquals(nextStepData.instruction, "Schenkendorfstraße") assertEquals(nextStepData.instruction, "Schenkendorfstraße")
} }
@@ -125,7 +155,7 @@ class RouteModelTest {
location.longitude = homeHohenwaldeck.longitude location.longitude = homeHohenwaldeck.longitude
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.nextStep() val stepData = routeModel.nextStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_DESTINATION_LEFT) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_DESTINATION_LEFT.value)
} }
@Test @Test
@@ -143,11 +173,11 @@ class RouteModelTest {
if (index in 61..61) { if (index in 61..61) {
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.lane.size, 3) assertEquals(stepData.lane.size, 2)
assertEquals(stepData.lane.first().valid, true) assertEquals(stepData.lane.first().valid, true)
assertEquals(stepData.lane.first().indications.first(), "STRAIGHT") assertEquals(stepData.lane.first().indications.first(), "STRAIGHT")
} }
if (index in 74..74) { if (index in 74..75) {
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.lane.size, 3) assertEquals(stepData.lane.size, 3)
@@ -181,9 +211,9 @@ class RouteModelTest {
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
//println("${stepData.instruction} ${System.currentTimeMillis() - start}") //println("${stepData.instruction} ${System.currentTimeMillis() - start}")
if (stepData.lane.isNotEmpty()) { if (stepData.lane.isNotEmpty()) {
println(stepData.street) // println(stepData.street)
stepData.lane.forEach { stepData.lane.forEach {
println("${it.indications} ${it.valid}") // println("${it.indications} ${it.valid}")
} }
} }
// val nextData = routeModel.nextStep() // val nextData = routeModel.nextStep()
@@ -207,4 +237,19 @@ class RouteModelTest {
val step = routeModel.currentStep() val step = routeModel.currentStep()
assertEquals(step.leftStepDistance, 26.0, 1.0) assertEquals(step.leftStepDistance, 26.0, 1.0)
} }
@Test
fun leftStepDistance() {
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
val curLocation = location(waypoint[0], waypoint[1])
if (routeModel.isNavigating()) {
if (index in 16..43) {
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep()
assertEquals(stepData.leftStepDistance, distance[index-16], 1.0)
}
}
}
}
} }
@@ -0,0 +1,10 @@
package com.kouros.navigation.car
import androidx.car.app.Session
import kotlinx.coroutines.flow.Flow
abstract class CarSession : Session() {
abstract fun invalidateNavigationScreen()
}
@@ -17,6 +17,7 @@ package com.kouros.navigation.car
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.location.Location
import android.util.Log import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
@@ -27,6 +28,9 @@ import androidx.car.app.model.CarIcon
import androidx.car.app.model.OnClickListener import androidx.car.app.model.OnClickListener
import androidx.car.app.navigation.model.Trip import androidx.car.app.navigation.model.Trip
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -34,45 +38,74 @@ import com.kouros.data.R
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.NavigationListener import com.kouros.navigation.car.screen.NavigationListener
import com.kouros.navigation.car.screen.NavigationScreen import com.kouros.navigation.car.screen.NavigationScreen
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.datastore.DataStoreManager.PreferencesKeys.CAR_LOCATION
import com.kouros.navigation.data.datastore.dataStore
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.model.NavigationViewModel
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/** Session class for the Navigation sample app. */ /** Session class for the Navigation sample app. */
internal class ClusterSession : Session(), NavigationListener { internal class ClusterSession : CarSession(), NavigationListener {
var mNavigationScreen: NavigationScreen? = null lateinit var mNavigationScreen: NavigationScreen
var mNavigationCarSurface: SurfaceRenderer? = null lateinit var surfaceRenderer: SurfaceRenderer
var mSettingsAction: Action? = null
var routeModel = RouteCarModel() var routeModel = RouteCarModel()
lateinit var viewModelStoreOwner: ViewModelStoreOwner lateinit var viewModelStoreOwner: ViewModelStoreOwner
lateinit var navigationViewModel: NavigationViewModel
lateinit var deviceLocationManager: DeviceLocationManager
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
Log.d(Constants.TAG, "NavigationSession paused")
super.onPause(owner)
}
override fun onResume(owner: LifecycleOwner) {
Log.d(Constants.TAG, "NavigationSession resumed")
super.onResume(owner)
}
override fun onDestroy(owner: LifecycleOwner) {
if (::deviceLocationManager.isInitialized) {
deviceLocationManager.stopLocationUpdates()
}
carContext
.stopService(
Intent(
carContext,
NavigationNotificationService::class.java
)
)
}
}
init {
lifecycle.addObserver(lifecycleObserver)
}
override fun onCreateScreen(intent: Intent): Screen { override fun onCreateScreen(intent: Intent): Screen {
Log.i(TAG, "In onCreateScreen()") Log.i(TAG, "In onCreateScreen()")
setupViewModelStore() setupViewModelStore()
mSettingsAction =
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext, R.drawable.alt_route_48px
)
)
.build()
)
.setOnClickListener(
OnClickListener {})
.build()
mNavigationCarSurface = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner) surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
navigationViewModel = NavigationViewModel(TomTomRepository())
// mNavigationScreen = mNavigationScreen =
// new NavigationScreen(getCarContext(), mSettingsAction, this, mNavigationCarSurface); NavigationScreen(carContext, surfaceRenderer, this, navigationViewModel)
val action = intent.action val action = intent.action
if (CarContext.ACTION_NAVIGATE == action) { if (CarContext.ACTION_NAVIGATE == action) {
Log.i(TAG, "In onCreateScreen() Navigation intent")
CarToast.makeText( CarToast.makeText(
carContext, carContext,
"Navigation intent: " + intent.dataString, "Navigation intent: " + intent.dataString,
@@ -80,8 +113,29 @@ internal class ClusterSession : Session(), NavigationListener {
) )
.show() .show()
} }
initializeManagers()
return mNavigationScreen
}
return mNavigationScreen!! /**
* Initializes managers for rendering, sensors, and location.
*/
private fun initializeManagers() {
deviceLocationManager = DeviceLocationManager(
carContext = carContext,
lifecycleOwner = this,
shouldUseCarLocationFlow = flowOf(false),
onLocationUpdate = ::updateLocation,
onInitialLocation = { location ->
})
deviceLocationManager.startLocationUpdates()
}
fun updateLocation(location: Location) {
Log.d(TAG, "updateLocation $location")
surfaceRenderer.updateLocation(location, "")
} }
override fun onCarConfigurationChanged(newConfiguration: Configuration) { override fun onCarConfigurationChanged(newConfiguration: Configuration) {
@@ -99,6 +153,14 @@ internal class ClusterSession : Session(), NavigationListener {
override fun updateTrip(trip: Trip) { override fun updateTrip(trip: Trip) {
} }
override fun navigateToPlace(place: Place) {
}
override fun recalcRoute(destination: Place) {
}
companion object { companion object {
val TAG: String = ClusterSession::class.java.getSimpleName() val TAG: String = ClusterSession::class.java.getSimpleName()
} }
@@ -116,4 +178,8 @@ internal class ClusterSession : Session(), NavigationListener {
} }
} }
} }
override fun invalidateNavigationScreen() {
mNavigationScreen.invalidate()
}
} }
@@ -67,7 +67,7 @@ class DeviceLocationManager(
* @param minDistanceM Minimum distance between updates in meters (default: 5m) * @param minDistanceM Minimum distance between updates in meters (default: 5m)
*/ */
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun startLocationUpdates(minTimeMs: Long = 500, minDistanceM: Float = 5f) { fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5f) {
if (isListening) return if (isListening) return
// Get and deliver last known location first // Get and deliver last known location first
@@ -0,0 +1,97 @@
package com.kouros.navigation.car
import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import android.location.LocationManager
import androidx.car.app.CarContext
import androidx.core.location.LocationListenerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kouros.navigation.car.navigation.NavigationService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/**
* Manages device GPS location updates for navigation.
* Coordinates with car hardware sensors to avoid duplicate location sources.
*
* @param carContext The car context for accessing system services
* @param serviceOwner Owner of the lifecycle for coroutine management
* @param shouldUseCarLocationFlow Flow indicating whether car location hardware should be used
* @param onLocationUpdate Callback invoked when location updates are received
* @param onInitialLocation Callback invoked with the last known location when starting
*/
class DeviceLocationManagerService(
private val carContext: CarContext,
private val onLocationUpdate: (Location) -> Unit,
private val onInitialLocation: (Location) -> Unit
) {
private val locationManager: LocationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private var shouldUseDeviceLocation = true
private var isListening = false
/**
* Location listener that receives GPS updates from the device.
* Only processes location if car location hardware is not being used.
*/
private val locationListener: LocationListenerCompat = LocationListenerCompat { location ->
if (shouldUseDeviceLocation) {
onLocationUpdate(location)
}
}
init {
}
/**
* Starts requesting location updates from device GPS.
* Provides initial location via callback and then starts continuous updates.
*
* @param minTimeMs Minimum time interval between updates in milliseconds (default: 500ms)
* @param minDistanceM Minimum distance between updates in meters (default: 5m)
*/
@SuppressLint("MissingPermission")
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5.0f) {
if (isListening) return
// Get and deliver last known location first
val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (lastLocation != null) {
onInitialLocation(lastLocation)
onLocationUpdate(lastLocation)
}
// Start continuous location updates
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
minTimeMs,
minDistanceM,
locationListener
)
isListening = true
}
/**
* Stops receiving location updates from device GPS.
* Should be called when the session is destroyed to prevent memory leaks.
*/
fun stopLocationUpdates() {
if (!isListening) return
locationManager.removeUpdates(locationListener)
isListening = false
}
/**
* Checks if location updates are currently active.
*/
fun isListeningForUpdates(): Boolean = isListening
}
@@ -14,7 +14,7 @@ import com.kouros.navigation.data.Constants.TAG
class NavigationCarAppService : CarAppService() { class NavigationCarAppService : CarAppService() {
val INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP = val intentActionNavNotificationOpenApp =
"com.kouros.navigation.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP" "com.kouros.navigation.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP"
val channelId: String = "NavigationSessionChannel" val channelId: String = "NavigationSessionChannel"
@@ -26,17 +26,17 @@ class NavigationCarAppService : CarAppService() {
@SuppressLint("PrivateResource") @SuppressLint("PrivateResource")
override fun createHostValidator(): HostValidator { override fun createHostValidator(): HostValidator {
return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
} }
override fun onCreateSession(sessionInfo: SessionInfo): Session { override fun onCreateSession(sessionInfo: SessionInfo): Session {
Log.d(TAG, "Display Type: ${sessionInfo.displayType}")
if (sessionInfo.displayType == SessionInfo.DISPLAY_TYPE_CLUSTER) { if (sessionInfo.displayType == SessionInfo.DISPLAY_TYPE_CLUSTER) {
return ClusterSession() return ClusterSession()
} else { } else {
createNotificationChannel() createNotificationChannel()
return NavigationSession() //return NavigationSession()
return NavigationServiceSession()
} }
} }
@@ -9,6 +9,7 @@ import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.Looper import android.os.Looper
import android.os.Message import android.os.Message
import android.util.Log
import androidx.car.app.notification.CarAppExtender import androidx.car.app.notification.CarAppExtender
import androidx.car.app.notification.CarNotificationManager import androidx.car.app.notification.CarNotificationManager
import androidx.car.app.notification.CarPendingIntent import androidx.car.app.notification.CarPendingIntent
@@ -16,6 +17,7 @@ import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.Constants.TAG
import java.math.RoundingMode import java.math.RoundingMode
import java.text.DecimalFormat import java.text.DecimalFormat
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -44,17 +46,12 @@ class NavigationNotificationService : Service() {
Handler(Looper.getMainLooper(), HandlerCallback()) Handler(Looper.getMainLooper(), HandlerCallback())
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val message = intent.getStringExtra("EXTRA_MESSAGE") ?: "Navigating..."
initNotifications(this) initNotifications(this)
startForeground( val notification = getNavigationNotification(this, message)
NAV_NOTIFICATION_ID, // This updates the existing notification if the service is already running
getNavigationNotification(this, mNotificationCount).build() CarNotificationManager.from(this).notify(NAV_NOTIFICATION_ID, notification)
) startForeground(NAV_NOTIFICATION_ID, notification.build())
// Start updating the notification continuously.
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_SEND_NOTIFICATION), NAV_NOTIFICATION_DELAY_IN_MILLIS
)
return START_NOT_STICKY return START_NOT_STICKY
} }
@@ -71,11 +68,12 @@ class NavigationNotificationService : Service() {
*/ */
internal inner class HandlerCallback : Handler.Callback { internal inner class HandlerCallback : Handler.Callback {
override fun handleMessage(msg: Message): Boolean { override fun handleMessage(msg: Message): Boolean {
Log.d(TAG, "Notification handleMessage: $msg")
if (msg.what == MSG_SEND_NOTIFICATION) { if (msg.what == MSG_SEND_NOTIFICATION) {
val context: Context = this@NavigationNotificationService val context: Context = this@NavigationNotificationService
CarNotificationManager.from(context).notify( CarNotificationManager.from(context).notify(
NAV_NOTIFICATION_ID, NAV_NOTIFICATION_ID,
getNavigationNotification(context, mNotificationCount) getNavigationNotification(context, "Nachricht")
) )
mNotificationCount++ mNotificationCount++
mHandler.sendMessageDelayed( mHandler.sendMessageDelayed(
@@ -96,6 +94,12 @@ class NavigationNotificationService : Service() {
val mOnlyAlertOnce: Boolean val mOnlyAlertOnce: Boolean
) )
fun startForeground(message: String) {
startForeground(
NAV_NOTIFICATION_ID,
getNavigationNotification(this, message).build()
)
}
companion object { companion object {
private const val MSG_SEND_NOTIFICATION = 1 private const val MSG_SEND_NOTIFICATION = 1
private const val NAV_NOTIFICATION_CHANNEL_ID = "nav_channel_00" private const val NAV_NOTIFICATION_CHANNEL_ID = "nav_channel_00"
@@ -124,21 +128,40 @@ class NavigationNotificationService : Service() {
CarNotificationManager.from(context).createNotificationChannel(navChannel) CarNotificationManager.from(context).createNotificationChannel(navChannel)
} }
/**
* Returns a [DirectionInfo] that corresponds to the given notification count.
*
*
* There are 5 directions, repeating in order. For each direction, the alert will only show
* once, but the distance will update on every count on the rail widget.
*/
private fun getDirectionInfo(context: Context, message: String): DirectionInfo {
val formatter = DecimalFormat("#.##")
formatter.setRoundingMode(RoundingMode.DOWN)
val distance = formatter.format((10) * 0.1) + "km"
return DirectionInfo(
message,
distance,
R.drawable.navigation_48px,
false
)
}
/** Returns the navigation notification that corresponds to the given notification count. */ /** Returns the navigation notification that corresponds to the given notification count. */
fun getNavigationNotification( fun getNavigationNotification(
context: Context, notificationCount: Int context: Context
): NotificationCompat.Builder { ): NotificationCompat.Builder {
val builder = val builder =
NotificationCompat.Builder(context, NAV_NOTIFICATION_CHANNEL_ID) NotificationCompat.Builder(context, NAV_NOTIFICATION_CHANNEL_ID)
val directionInfo = getDirectionInfo(context, notificationCount) val directionInfo = getDirectionInfo(context, "Test")
// Set an intent to open the car app. The app receives this intent when the user taps the // Set an intent to open the car app. The app receives this intent when the user taps the
// heads-up notification or the rail widget. // heads-up notification or the rail widget.
val pendingIntent = CarPendingIntent.getCarApp( val pendingIntent = CarPendingIntent.getCarApp(
context, context,
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP.hashCode(), NavigationCarAppService().intentActionNavNotificationOpenApp.hashCode(),
Intent( Intent(
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP NavigationCarAppService().intentActionNavNotificationOpenApp
).setComponent( ).setComponent(
ComponentName( ComponentName(
context, context,
@@ -146,7 +169,7 @@ class NavigationNotificationService : Service() {
) )
).setData( ).setData(
NavigationCarAppService().createDeepLinkUri( NavigationCarAppService().createDeepLinkUri(
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP NavigationCarAppService().intentActionNavNotificationOpenApp
) )
), ),
0 0
@@ -179,54 +202,59 @@ class NavigationNotificationService : Service() {
) )
} }
/** fun getNavigationNotification(
* Returns a [DirectionInfo] that corresponds to the given notification count. context: Context, message: String
* ): NotificationCompat.Builder {
* val builder =
* There are 5 directions, repeating in order. For each direction, the alert will only show NotificationCompat.Builder(context, NAV_NOTIFICATION_CHANNEL_ID)
* once, but the distance will update on every count on the rail widget. val directionInfo = getDirectionInfo(context, message)
*/
private fun getDirectionInfo(context: Context, notificationCount: Int): DirectionInfo { // Set an intent to open the car app. The app receives this intent when the user taps the
val formatter = DecimalFormat("#.##") // heads-up notification or the rail widget.
formatter.setRoundingMode(RoundingMode.DOWN) val pendingIntent = CarPendingIntent.getCarApp(
val repeatingCount = notificationCount % 35 context,
if (repeatingCount in 0..<10) { NavigationCarAppService().intentActionNavNotificationOpenApp.hashCode(),
// Distance decreases from 1km to 0.1km Intent(
val distance = formatter.format((10 - repeatingCount) * 0.1) + "km" NavigationCarAppService().intentActionNavNotificationOpenApp
return DirectionInfo( ).setComponent(
context.getString(R.string.stop_action_title), ComponentName(
distance, context,
R.drawable.arrow_back_24px, NavigationCarAppService()::class.java
repeatingCount > 0 )
).setData(
NavigationCarAppService().createDeepLinkUri(
NavigationCarAppService().intentActionNavNotificationOpenApp
)
),
0
)
return builder
// This title, text, and icon will be shown in both phone and car screen. These
// values can
// be overridden in the extender below, to customize notifications in the car
// screen.
.setContentTitle(directionInfo.mTitle)
.setContentText(directionInfo.mDistance)
.setSmallIcon(directionInfo.mIcon) // The notification must be set to 'ongoing' and its category must be set to
// CATEGORY_NAVIGATION in order to show it in the rail widget when the app is
// navigating on
// the background.
// These values cannot be overridden in the extender.
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_NAVIGATION) // If set to true, the notification will only show the alert once in both phone and
// car screen. This value cannot be overridden in the extender.
.setOnlyAlertOnce(directionInfo.mOnlyAlertOnce) // This extender must be set in order to display the notification in the car screen.
// The extender also allows various customizations, such as showing different title
// or icon on the car screen.
.extend(
CarAppExtender.Builder()
.setContentIntent(pendingIntent)
.build()
) )
} else if (repeatingCount in 10..<20) {
// Distance decreases from 5km to 0.5km
val distance = formatter.format((20 - repeatingCount) * 0.5) + "km"
return DirectionInfo(
context.getString(R.string.route_preview),
distance,
R.drawable.ic_turn_normal_right, /* onlyAlertOnce= */
repeatingCount > 10
)
} else if (repeatingCount in 20..<25) {
// Distance decreases from 200m to 40m
val distance = formatter.format(((25 - repeatingCount) * 40).toLong()) + "m"
return DirectionInfo(
context.getString(R.string.route_preview),
distance,
R.drawable.navigation_48px, /* onlyAlertOnce= */
repeatingCount > 20
)
} else {
// Distance decreases from 1km to 0.1km
val distance = formatter.format((35 - repeatingCount) * 0.1) + "km"
return DirectionInfo(
context.getString(R.string.charging_station),
distance,
R.drawable.local_gas_station_24,
repeatingCount > 25
)
}
} }
} }
} }
@@ -0,0 +1,724 @@
package com.kouros.navigation.car
import android.Manifest.permission
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.location.Location
import android.os.IBinder
import android.util.Log
import androidx.annotation.RequiresPermission
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.ScreenManager
import androidx.car.app.connection.CarConnection
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import com.kouros.navigation.car.navigation.NavigationService
import com.kouros.navigation.car.screen.NavigationListener
import com.kouros.navigation.car.screen.NavigationScreen
import com.kouros.navigation.car.screen.NavigationType
import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.car.screen.checkPermission
import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
import com.kouros.navigation.car.screen.observers.NavigationObserverManager
import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.GMS_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
import com.kouros.navigation.data.Constants.homeHohenwaldeck
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.absoluteValue
/**
* Main session for Android Auto/Automotive OS navigation.
* Manages the lifecycle of the navigation session, including location updates,
* car hardware sensors, routing engine selection, and screen navigation.
* Implements NavigationScreen.Listener for handling navigation events.
*/
class NavigationServiceSession : CarSession(), NavigationListener, NavigationObserverCallback {
// Flag to enable/disable contact access feature
val useContacts = false
var route = ""
// Main navigation screen displayed to the user
lateinit var navigationScreen: NavigationScreen
// Handles map surface rendering on the car display
lateinit var surfaceRenderer: SurfaceRenderer
// Manages car hardware sensors (location, compass, speed)
lateinit var carSensorManager: CarSensorManager
var initialLocation = true;
lateinit var textToSpeechManager: TextToSpeechManager
lateinit var notificationManager: NotificationManager
private var routingEngine = 0
private var showTraffic = false;
private var distanceMode = 0
var lastCameraSearch = 0
var speedCameras = listOf<Elements>()
var recentPlaces = mutableListOf<Place>()
var lastRouteDate: LocalDateTime = LocalDateTime.now()
var destination = Place()
var notificationActive = false
var navigationService: NavigationService? = null
val serviceListener: NavigationService.Listener = object : NavigationService.Listener {
override fun navigationStateChanged(
isNavigating: Boolean,
isRerouting: Boolean,
hasArrived: Boolean,
destinations: MutableList<Destination>,
steps: MutableList<Step>,
destinationTravelEstimate: TravelEstimate,
stepTravelEstimate: TravelEstimate,
stepRemainingDistance: Distance,
shouldShowNextStep: Boolean,
shouldShowLanes: Boolean,
junctionImage: CarIcon?,
backGroundColor: CarColor
) {
navigationScreen.updateTrip(
isNavigating = isNavigating,
isRerouting = isRerouting,
hasArrived = hasArrived,
destinationTravelEstimate = destinationTravelEstimate,
stepTravelEstimate = stepTravelEstimate,
destinations = destinations,
steps = steps,
stepRemainingDistance = stepRemainingDistance,
shouldShowNextStep = shouldShowNextStep,
shouldShowLanes = shouldShowLanes,
junctionImage = junctionImage,
backGroundColor = backGroundColor
)
}
override fun updateServiceLocation(location: Location) {
if (initialLocation) {
navigationViewModel.loadRecentPlaces(
carContext,
location,
surfaceRenderer.carOrientation,
)
initialLocation = false
}
updateLocation(location)
}
}
// Monitors the state of the connection to the Navigation service.
val serviceConnection: ServiceConnection = object : ServiceConnection {
@RequiresPermission(allOf = [permission.ACCESS_FINE_LOCATION, permission.ACCESS_COARSE_LOCATION])
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.d("NavigationService", "In onServiceConnected() Session component:$service")
val binder: NavigationService.LocalBinder = service as NavigationService.LocalBinder
navigationService = binder.service
navigationService!!.setCarContext(carContext, serviceListener)
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.d("NavigationService", "In onServiceDisconnected() Session component: $name")
// Unhook map models here
navigationService!!.clearCarContext()
navigationService = null
}
}
/**
* Lifecycle observer for managing session lifecycle events.
* Cleans up resources when the session is destroyed.
*/
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
Log.i(TAG, "In onStart() Session")
carContext
.bindService(
Intent(carContext, NavigationService::class.java),
serviceConnection,
Context.BIND_AUTO_CREATE
)
}
override fun onPause(owner: LifecycleOwner) {
Log.d(TAG, "NavigationSession paused")
super.onPause(owner)
}
override fun onResume(owner: LifecycleOwner) {
Log.d(TAG, "NavigationSession resumed")
super.onResume(owner)
}
override fun onStop(owner: LifecycleOwner) {
Log.i(TAG, "In onStop()")
carContext.unbindService(serviceConnection)
navigationService = null
}
override fun onDestroy(owner: LifecycleOwner) {
if (::carSensorManager.isInitialized) {
carSensorManager.cleanup()
}
if (::textToSpeechManager.isInitialized) {
textToSpeechManager.cleanup()
}
carContext
.stopService(
Intent(
carContext,
NavigationNotificationService::class.java
)
)
Log.i(TAG, "NavigationSession destroyed")
}
}
// ViewModel for navigation data and business logic
lateinit var navigationViewModel: NavigationViewModel
// Store for ViewModels to survive configuration changes
lateinit var viewModelStoreOwner: ViewModelStoreOwner
var lastStepIndex = -1
var guidanceAudio = 0
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
lateinit var observerManager: NavigationObserverManager
lateinit var repository: SettingsRepository
lateinit var settingsViewModel: SettingsViewModel
var carConnection: Int = 0
init {
lifecycle.addObserver(lifecycleObserver)
}
/**
* Called when routing engine preference changes.
* Creates appropriate repository based on user selection.
*/
fun onRoutingEngineStateUpdated(routeEngine: Int) {
Log.d(TAG, "onRoutingEngineStateUpdated $routeEngine")
if (!::navigationViewModel.isInitialized || routeEngine != routingEngine) {
navigationViewModel = when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
else -> NavigationViewModel(TomTomRepository())
}
observerManager = NavigationObserverManager(navigationViewModel, this)
observerManager.attachAllObservers(this)
}
}
/**
* Called when location permission is granted.
* Initializes car hardware sensors if available.
*/
fun onPermissionGranted(permission: Boolean) {
if (::carSensorManager.isInitialized && permission) {
carSensorManager.updateConnectionState(carConnection)
}
}
/**
* Called when car connection state changes.
* Handles different connection types: Not Connected, Automotive OS Native, Android Auto Projection.
* Requests appropriate car speed permissions based on connection type.
*/
fun onConnectionStateUpdated(connectionState: Int) {
carConnection = connectionState
when (connectionState) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit
CarConnection.CONNECTION_TYPE_NATIVE -> {
navigationViewModel.permissionGranted.value =
checkPermission(carContext, AUTOMOTIVE_CAR_SPEED_PERMISSION)
}
CarConnection.CONNECTION_TYPE_PROJECTION -> {
navigationViewModel.permissionGranted.value =
checkPermission(carContext, GMS_CAR_SPEED_PERMISSION)
}
}
}
/**
* Creates the initial screen for the session.
* Sets up ViewModel store, initializes settings, components, checks permissions,
* and returns appropriate starting screen.
*/
override fun onCreateScreen(intent: Intent): Screen {
initializeSettings()
setupViewModelStore()
initializeManagers()
initializeViewModels()
initializeScreen()
return checkPermissionsAndGetScreen()
}
/*
* Initializes the settings repository and ViewModel.
*/
private fun initializeSettings() {
repository = getSettingsRepository(carContext)
settingsViewModel = getSettingsViewModel(carContext)
repository.routingEngineFlow.asLiveData().observe(this, Observer {
onRoutingEngineStateUpdated(it)
routingEngine = it
})
repository.trafficFlow.asLiveData().observe(this, Observer {
showTraffic = it
})
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
}
/**
* Sets up ViewModelStoreOwner and manages its lifecycle.
*/
private fun setupViewModelStore() {
viewModelStoreOwner = object : ViewModelStoreOwner {
override val viewModelStore = ViewModelStore()
}
lifecycleScope.launch {
try {
awaitCancellation()
} finally {
viewModelStoreOwner.viewModelStore.clear()
}
}
}
/**
* Initializes ViewModels and observes their state changes.
*/
private fun initializeViewModels() {
navigationViewModel = getViewModel(carContext)
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
navigationViewModel.permissionGranted.observe(this, ::onPermissionGranted)
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
}
/**
* Initializes managers for rendering, sensors, and location.
*/
private fun initializeManagers() {
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
carSensorManager = CarSensorManager(
carContext = carContext,
lifecycleOwner = this,
onLocationUpdate = ::updateLocation,
onCompassUpdate = { orientation -> surfaceRenderer.carOrientation = orientation },
onSpeedUpdate = { speed -> surfaceRenderer.updateCarSpeed(speed) }
)
textToSpeechManager = TextToSpeechManager(carContext)
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
guidanceAudio = it
})
notificationManager = NotificationManager(carContext, this)
}
/**
* Creates the main navigation screen.
*/
private fun initializeScreen() {
navigationScreen = NavigationScreen(
carContext,
surfaceRenderer,
this,
navigationViewModel
)
}
/**
* Checks required permissions and returns appropriate screen.
* Shows permission request screen if needed, otherwise starts location updates.
*/
private fun checkPermissionsAndGetScreen(): Screen {
val hasLocationPermission =
carContext.checkSelfPermission(permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
val hasContactsPermission = !useContacts ||
carContext.checkSelfPermission(permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
return if (hasLocationPermission && hasContactsPermission) {
navigationScreen
} else {
showPermissionScreen()
}
}
/**
* Shows the permission request screen.
*/
private fun showPermissionScreen(): Screen {
val screenManager = carContext.getCarService(ScreenManager::class.java)
screenManager.push(navigationScreen)
return RequestPermissionScreen(
carContext,
listOf(
permission.ACCESS_COARSE_LOCATION,
permission.ACCESS_FINE_LOCATION,
),
permissionCheckCallback = { screenManager.pop() }
)
}
/**
* Handles new intents, primarily for navigation deep links from other apps.
* Supports ACTION_NAVIGATE for starting navigation to a specific location.
*/
override fun onNewIntent(intent: Intent) {
val screenManager = carContext.getCarService(ScreenManager::class.java)
// Handle Android Auto ACTION_NAVIGATE intent
if (CarContext.ACTION_NAVIGATE == intent.action) {
handleNavigateIntent(screenManager)
return
}
// Handle custom deep links
handleDeepLink(intent, screenManager)
}
/**
* Handles ACTION_NAVIGATE intent by showing search screen.
*/
private fun handleNavigateIntent(screenManager: ScreenManager) {
screenManager.popToRoot()
screenManager.pushForResult(
SearchScreen(carContext, surfaceRenderer, navigationViewModel, recentPlaces)
) { result ->
// Handle search result if needed
}
}
/**
* Handles custom deep link URIs.
*/
private fun handleDeepLink(intent: Intent, screenManager: ScreenManager) {
val uri = intent.data ?: return
if (uri.scheme != uriScheme || uri.schemeSpecificPart != uriHost) return
when (uri.fragment) {
"DEEP_LINK_ACTION" -> {
if (screenManager.getTop() !is NavigationScreen) {
screenManager.popToRoot()
}
}
}
}
/**
* Updates navigation state with new location.
* Handles route snapping, deviation detection for rerouting, and map updates.
*/
fun updateLocation(location: Location) {
if (carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
surfaceRenderer.updateCarSpeed(location.speed)
}
updateBearing(location)
checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
surfaceRenderer.updateLocation(location, "")
}
/**
* Updates route bearing if location has bearing information.
*/
private fun updateBearing(location: Location) {
if (location.hasBearing()) {
//routeModel.navState = routeModel.navState.copy(routeBearing = location.bearing)
}
}
/**
* Start navigation process.
* Called when user starts navigation
*/
override fun startNavigation() {
Log.d(TAG, "startNavigation")
navigationService!!.startNavigation(route, destination)
if (notificationActive)
notificationManager.startNotificationService()
}
/**
* Stops active navigation and clears route state.
* Called when user exits navigation or arrives at destination.
*/
override fun stopNavigation() {
Log.d(TAG, "stopNavigation")
navigationService!!.stopNavigation()
surfaceRenderer.navigation = false
surfaceRenderer.routeData.value = ""
lastCameraSearch = 0
surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW
if (notificationActive)
notificationManager.stopNotificationService()
Log.d(TAG, "end stopNavigation")
}
override fun updateTrip(trip: Trip) {
}
/**
* Recalculates a route for the specified place.
*/
override fun recalcRoute(destination: Place) {
val destination = location(destination.longitude, destination.latitude)
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
listOf(destination),
surfaceRenderer.carOrientation
)
}
/**
* Handles the received route string.
* Starts navigation and invalidates the screen.
*/
override fun onRouteReceived(route: String) {
Log.d(TAG, "onRouteReceived")
if (route.isNotEmpty()) {
prepareRoute(route)
}
}
override fun isNavigating(): Boolean {
return navigationService!!.isNavigating()
}
/**
* Prepare route and start navigation
*/
private fun prepareRoute(route: String) {
this.route = route
startNavigation()
surfaceRenderer.setRouteData(navigationService!!.routeModel.curRoute.routeGeoJson)
}
/**
* Handles received traffic data and updates the surface renderer.
*/
override fun onTrafficReceived(traffic: Map<String, String>) {
if (traffic.isNotEmpty()) {
surfaceRenderer.setTrafficData(traffic)
}
}
/**
* Handles the received place search result.
* Navigates to the specified place.
*/
override fun onPlaceSearchResultReceived(place: Place) {
navigateToPlace(place)
}
/**
* Handles received speed camera data.
* Updates the surface renderer with the camera locations.
*/
override fun onSpeedCamerasReceived(cameras: List<Elements>) {
speedCameras = cameras
val coordinates = mutableListOf<List<Double>>()
cameras.forEach {
coordinates.add(listOf(it.lon, it.lat))
}
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCameraData.value = speedData
}
/**
* Handles received maximum speed data and updates the surface renderer.
*/
override fun onMaxSpeedReceived(speed: Int) {
surfaceRenderer.maxSpeed.value = speed
}
override fun onRecentPlacesReceived(places: List<Place>) {
Log.d(TAG, "onRecentPlacesReceived ${places.size}")
recentPlaces = places.toMutableList()
navigationScreen.recentPlaces = places.toMutableList()
navigationScreen.invalidate()
}
override fun invalidateScreen() {
navigationScreen.invalidate()
}
/**
* Loads a route to the specified place and sets it as the destination.
*/
override fun navigateToPlace(place: Place) {
Log.d(TAG, "navigateToPlace ${place.street}")
var prevDestination = Place()
if (surfaceRenderer.navigation) {
prevDestination = place
stopNavigation()
}
val preview = place.route
navigationViewModel.previewRoute.value = ""
val location = if (place.stopOver && prevDestination.latitude != 0.0) {
listOf(
location(place.longitude, place.latitude),
location(prevDestination.longitude, prevDestination.latitude)
)
} else {
listOf(location(place.longitude, place.latitude))
}
navigationViewModel.saveRecent(carContext, place)
destination = place
// routeModel.navState = routeModel.navState.copy(destination = place)
if (preview.isEmpty()) {
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
} else {
//routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
onRouteReceived(preview)
}
surfaceRenderer.activateNavigationView()
}
/**
* Checks if traffic data needs to be updated based on the time since the last update.
*/
fun checkTraffic(current: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastTrafficDate)
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
lastTrafficDate = current
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
}
/**
* Periodically requests speed camera information near the current location.
*/
private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
navigationViewModel.getSpeedCameras(location, 5.0)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)
}
}
/**
* Updates distances to nearby speed cameras and checks for proximity alerts.
*/
private fun updateDistance(
location: Location,
) {
val updatedCameras = mutableListOf<Elements>()
speedCameras.forEach {
val plLocation =
location(longitude = it.lon, latitude = it.lat)
val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble()
updatedCameras.add(it)
}
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val camera = sortedList.firstOrNull() ?: return
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
val bearingSpeedCamera = if (camera.tags.direction != null) {
try {
camera.tags.direction!!.toFloat()
} catch (e: Exception) {
0F
}
} else {
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
}
if (camera.distance < 80) {
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
// routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
}
}
}
override fun invalidateNavigationScreen() {
navigationScreen.invalidate()
}
companion object {
// URI host for deep linking
var uriHost: String = "navigation"
// URI scheme for deep linking
var uriScheme: String = "samples"
}
}
@@ -1,27 +1,20 @@
package com.kouros.navigation.car package com.kouros.navigation.car
import android.Manifest.permission import android.Manifest.permission
import android.content.ComponentName
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.Location import android.location.Location
import android.os.IBinder
import android.util.Log import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.ScreenManager import androidx.car.app.ScreenManager
import androidx.car.app.Session
import androidx.car.app.connection.CarConnection import androidx.car.app.connection.CarConnection
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import androidx.car.app.navigation.NavigationManager import androidx.car.app.navigation.NavigationManager
import androidx.car.app.navigation.NavigationManagerCallback import androidx.car.app.navigation.NavigationManagerCallback
import androidx.car.app.navigation.model.Destination import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.car.app.navigation.model.Trip import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
@@ -40,25 +33,40 @@ import com.kouros.navigation.car.screen.NavigationType
import com.kouros.navigation.car.screen.RequestPermissionScreen import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.car.screen.checkPermission import com.kouros.navigation.car.screen.checkPermission
import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
import com.kouros.navigation.car.screen.observers.NavigationObserverManager
import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.GMS_CAR_SPEED_PERMISSION import com.kouros.navigation.data.Constants.GMS_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.osrm.OsrmRepository import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.GeoUtils.snapLocation import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import kotlin.math.absoluteValue
/** /**
@@ -67,7 +75,7 @@ import java.time.ZoneOffset
* car hardware sensors, routing engine selection, and screen navigation. * car hardware sensors, routing engine selection, and screen navigation.
* Implements NavigationScreen.Listener for handling navigation events. * Implements NavigationScreen.Listener for handling navigation events.
*/ */
class NavigationSession : Session(), NavigationListener { class NavigationSession : CarSession(), NavigationListener, NavigationObserverCallback {
// Flag to enable/disable contact access feature // Flag to enable/disable contact access feature
val useContacts = false val useContacts = false
@@ -75,6 +83,8 @@ class NavigationSession : Session(), NavigationListener {
// Model for managing route state and navigation logic for Android Auto // Model for managing route state and navigation logic for Android Auto
lateinit var routeModel: RouteCarModel lateinit var routeModel: RouteCarModel
var route = ""
// Main navigation screen displayed to the user // Main navigation screen displayed to the user
lateinit var navigationScreen: NavigationScreen lateinit var navigationScreen: NavigationScreen
@@ -91,18 +101,44 @@ class NavigationSession : Session(), NavigationListener {
lateinit var textToSpeechManager: TextToSpeechManager lateinit var textToSpeechManager: TextToSpeechManager
lateinit var notificationManager: NotificationManager
var autoDriveEnabled = false var autoDriveEnabled = false
val simulation = Simulation() val simulation = Simulation()
private var routingEngine = 0
private var showTraffic = false;
private var distanceMode = 0
var lastCameraSearch = 0
var speedCameras = listOf<Elements>()
var lastRouteDate: LocalDateTime = LocalDateTime.now()
var navigationManagerStarted = false var navigationManagerStarted = false
var notificationActive = false
/** /**
* Lifecycle observer for managing session lifecycle events. * Lifecycle observer for managing session lifecycle events.
* Cleans up resources when the session is destroyed. * Cleans up resources when the session is destroyed.
*/ */
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver { private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
Log.d(TAG, "NavigationSession paused")
super.onPause(owner)
}
override fun onResume(owner: LifecycleOwner) {
Log.d(TAG, "NavigationSession resumed")
super.onResume(owner)
}
override fun onDestroy(owner: LifecycleOwner) { override fun onDestroy(owner: LifecycleOwner) {
if (::navigationManager.isInitialized) { if (::navigationManager.isInitialized) {
navigationManager.clearNavigationManagerCallback() navigationManager.clearNavigationManagerCallback()
@@ -116,6 +152,13 @@ class NavigationSession : Session(), NavigationListener {
if (::textToSpeechManager.isInitialized) { if (::textToSpeechManager.isInitialized) {
textToSpeechManager.cleanup() textToSpeechManager.cleanup()
} }
carContext
.stopService(
Intent(
carContext,
NavigationNotificationService::class.java
)
)
Log.i(TAG, "NavigationSession destroyed") Log.i(TAG, "NavigationSession destroyed")
} }
} }
@@ -130,6 +173,13 @@ class NavigationSession : Session(), NavigationListener {
var guidanceAudio = 0 var guidanceAudio = 0
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
lateinit var observerManager: NavigationObserverManager
lateinit var repository: SettingsRepository
lateinit var settingsViewModel: SettingsViewModel
init { init {
lifecycle.addObserver(lifecycleObserver) lifecycle.addObserver(lifecycleObserver)
} }
@@ -139,10 +189,14 @@ class NavigationSession : Session(), NavigationListener {
* Creates appropriate repository based on user selection. * Creates appropriate repository based on user selection.
*/ */
fun onRoutingEngineStateUpdated(routeEngine: Int) { fun onRoutingEngineStateUpdated(routeEngine: Int) {
navigationViewModel = when (routeEngine) { if (!::navigationViewModel.isInitialized || routeEngine != routingEngine) {
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository()) navigationViewModel = when (routeEngine) {
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository()) RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
else -> NavigationViewModel(TomTomRepository()) RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
else -> NavigationViewModel(TomTomRepository())
}
observerManager = NavigationObserverManager(navigationViewModel, this)
observerManager.attachAllObservers(this)
} }
} }
@@ -166,21 +220,24 @@ class NavigationSession : Session(), NavigationListener {
when (connectionState) { when (connectionState) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit
CarConnection.CONNECTION_TYPE_NATIVE -> { CarConnection.CONNECTION_TYPE_NATIVE -> {
navigationViewModel.permissionGranted.value = checkPermission(carContext,AUTOMOTIVE_CAR_SPEED_PERMISSION) navigationViewModel.permissionGranted.value =
checkPermission(carContext, AUTOMOTIVE_CAR_SPEED_PERMISSION)
} }
CarConnection.CONNECTION_TYPE_PROJECTION -> { CarConnection.CONNECTION_TYPE_PROJECTION -> {
navigationViewModel.permissionGranted.value = checkPermission(carContext, GMS_CAR_SPEED_PERMISSION) navigationViewModel.permissionGranted.value =
checkPermission(carContext, GMS_CAR_SPEED_PERMISSION)
} }
} }
} }
/** /**
* Creates the initial screen for the session. * Creates the initial screen for the session.
* Sets up ViewModel store, initializes components, checks permissions, * Sets up ViewModel store, initializes settings, components, checks permissions,
* and returns appropriate starting screen. * and returns appropriate starting screen.
*/ */
override fun onCreateScreen(intent: Intent): Screen { override fun onCreateScreen(intent: Intent): Screen {
initializeSettings()
setupViewModelStore() setupViewModelStore()
initializeViewModels() initializeViewModels()
initializeManagers() initializeManagers()
@@ -188,6 +245,26 @@ class NavigationSession : Session(), NavigationListener {
return checkPermissionsAndGetScreen() return checkPermissionsAndGetScreen()
} }
/*
* Initializes the settings repository and ViewModel.
*/
private fun initializeSettings() {
repository = getSettingsRepository(carContext)
settingsViewModel = getSettingsViewModel(carContext)
repository.routingEngineFlow.asLiveData().observe(this, Observer {
onRoutingEngineStateUpdated(it)
routingEngine = it
})
repository.trafficFlow.asLiveData().observe(this, Observer {
showTraffic = it
})
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
}
/** /**
* Sets up ViewModelStoreOwner and manages its lifecycle. * Sets up ViewModelStoreOwner and manages its lifecycle.
*/ */
@@ -230,7 +307,7 @@ class NavigationSession : Session(), NavigationListener {
autoDriveEnabled = true autoDriveEnabled = true
startNavigation() startNavigation()
CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG) CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
.show() .show()
} }
override fun onStopNavigation() { override fun onStopNavigation() {
@@ -242,7 +319,7 @@ class NavigationSession : Session(), NavigationListener {
} }
} }
}) })
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner) surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
carSensorManager = CarSensorManager( carSensorManager = CarSensorManager(
carContext = carContext, carContext = carContext,
@@ -268,10 +345,10 @@ class NavigationSession : Session(), NavigationListener {
textToSpeechManager = TextToSpeechManager(carContext) textToSpeechManager = TextToSpeechManager(carContext)
val repository = getSettingsRepository(carContext)
repository.guidanceAudioFlow.asLiveData().observe(this, Observer { repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
guidanceAudio = it guidanceAudio = it
}) })
notificationManager = NotificationManager(carContext, this)
} }
/** /**
@@ -281,7 +358,6 @@ class NavigationSession : Session(), NavigationListener {
navigationScreen = NavigationScreen( navigationScreen = NavigationScreen(
carContext, carContext,
surfaceRenderer, surfaceRenderer,
routeModel,
this, this,
navigationViewModel navigationViewModel
) )
@@ -314,6 +390,10 @@ class NavigationSession : Session(), NavigationListener {
screenManager.push(navigationScreen) screenManager.push(navigationScreen)
return RequestPermissionScreen( return RequestPermissionScreen(
carContext, carContext,
listOf(
permission.ACCESS_COARSE_LOCATION,
permission.ACCESS_FINE_LOCATION,
),
permissionCheckCallback = { screenManager.pop() } permissionCheckCallback = { screenManager.pop() }
) )
} }
@@ -369,6 +449,11 @@ class NavigationSession : Session(), NavigationListener {
* Handles route snapping, deviation detection for rerouting, and map updates. * Handles route snapping, deviation detection for rerouting, and map updates.
*/ */
fun updateLocation(location: Location) { fun updateLocation(location: Location) {
val streetName = if (routeModel.isNavigating()) {
routeModel.currentStep().street
} else {
""
}
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) { if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
surfaceRenderer.updateCarSpeed(location.speed) surfaceRenderer.updateCarSpeed(location.speed)
} }
@@ -376,8 +461,8 @@ class NavigationSession : Session(), NavigationListener {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
handleNavigationLocation(location) handleNavigationLocation(location)
} else { } else {
navigationScreen.checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location) checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
surfaceRenderer.updateLocation(location) surfaceRenderer.updateLocation(location, streetName)
} }
} }
@@ -395,43 +480,116 @@ class NavigationSession : Session(), NavigationListener {
* Snaps location to route and checks for deviation requiring reroute. * Snaps location to route and checks for deviation requiring reroute.
*/ */
private fun handleNavigationLocation(location: Location) { private fun handleNavigationLocation(location: Location) {
routeModel.updateLocation(location, navigationViewModel)
if (routeModel.navState.arrived) return
if (guidanceAudio == 1) { if (guidanceAudio == 1) {
handleGuidanceAudio() handleGuidanceAudio()
} }
navigationScreen.updateTrip(location) val streetName = routeModel.currentStep().street
if (routeModel.navState.arrived) return val currentDate = LocalDateTime.now(ZoneOffset.UTC)
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snappedLocation)
when {
distance > MAXIMAL_ROUTE_DEVIATION -> {
navigationScreen.calculateNewRoute(routeModel.navState.destination)
}
distance < MAXIMAL_SNAP_CORRECTION -> { if (snapLocation(location, streetName)) {
surfaceRenderer.updateLocation(snappedLocation) checkTraffic(currentDate, location)
} updateSpeedCamera(location)
checkRoute(currentDate, location)
else -> { updateNavigationScreen()
surfaceRenderer.updateLocation(location) checkArrival()
}
} }
} }
/** /**
* Stops active navigation and clears route state. * Updates the surface renderer with snapped location and street name.
* Called when user exits navigation or arrives at destination. * Checks if maximal route deviation is exceeded and reroutes if needed.
*/ */
override fun stopNavigation() { private fun snapLocation(location: Location, streetName: String): Boolean {
routeModel.stopNavigation() val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
navigationManager.navigationEnded() val distance = location.distanceTo(snappedLocation)
if (autoDriveEnabled) { when {
simulation.stopSimulation() distance > MAXIMAL_ROUTE_DEVIATION -> {
autoDriveEnabled = false stopNavigation()
navigationScreen.calculateNewRoute(routeModel.navState.destination)
return false
}
distance < MAXIMAL_SNAP_CORRECTION -> {
surfaceRenderer.updateLocation(snappedLocation, streetName)
}
else -> {
surfaceRenderer.updateLocation(location, streetName)
}
}
return true
}
/**
* Updates the navigation screen with new trip information.
*/
fun updateNavigationScreen() {
if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty()
&& routeModel.navState.destination.street.isEmpty()
) {
return
}
val travelEstimateTrip = routeModel.travelEstimateTrip(carContext, distanceMode)
val travelEstimateStep = routeModel.travelEstimateStep(carContext, distanceMode)
val steps = mutableListOf<Step>()
val destination = Destination.Builder()
.setName(routeModel.navState.destination.name)
.setAddress(routeModel.navState.destination.street)
.build()
val distance =
formattedDistance(0, routeModel.routeCalculator.leftStepDistance())
steps.add(routeModel.currentStep(carContext))
if (routeModel.navState.nextStep) {
steps.add(routeModel.nextStep(carContext = carContext))
}
navigationScreen.updateTrip(
isNavigating = routeModel.isNavigating(),
isRerouting = false,
hasArrived = routeModel.isArrival(),
destinationTravelEstimate = travelEstimateTrip,
stepTravelEstimate = travelEstimateStep,
destinations = mutableListOf(destination),
steps = steps,
stepRemainingDistance = Distance.create(distance.first, distance.second),
shouldShowNextStep = false,
shouldShowLanes = true,
junctionImage = null,
backGroundColor = routeModel.backGroundColor()
)
/**
* Updates the trip information and notifies the listener with a new Trip object.
* This includes destination name, address, travel estimate, and loading status.
*/
val tripBuilder = Trip.Builder()
tripBuilder.addDestination(
destination,
travelEstimateTrip
)
tripBuilder.setLoading(false)
tripBuilder.setCurrentRoad(destination.name.toString())
tripBuilder.addStep(steps.first(), travelEstimateStep)
updateTrip(tripBuilder.build())
}
/**
* Checks for arrival
*/
fun checkArrival() {
if (routeModel.isArrival()
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
stopNavigation()
settingsViewModel.onLastRouteChanged("")
routeModel.navState = routeModel.navState.copy(arrived = true)
surfaceRenderer.routeData.value = ""
navigationScreen.navigationType = NavigationType.ARRIVAL
invalidateScreen()
} }
surfaceRenderer.routeData.value = ""
surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW
} }
/** /**
@@ -439,6 +597,8 @@ class NavigationSession : Session(), NavigationListener {
* Called when user starts navigation * Called when user starts navigation
*/ */
override fun startNavigation() { override fun startNavigation() {
Log.d(TAG, "startNavigation")
surfaceRenderer.navigation = true
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationManager.navigationStarted() navigationManager.navigationStarted()
navigationManagerStarted = true navigationManagerStarted = true
@@ -449,6 +609,29 @@ class NavigationSession : Session(), NavigationListener {
updateLocation(location) updateLocation(location)
} }
} }
if (notificationActive)
notificationManager.startNotificationService()
}
/**
* Stops active navigation and clears route state.
* Called when user exits navigation or arrives at destination.
*/
override fun stopNavigation() {
Log.d(TAG, "stopNavigation")
surfaceRenderer.navigation = false
routeModel.stopNavigation()
navigationManager.navigationEnded()
if (autoDriveEnabled) {
simulation.stopSimulation()
autoDriveEnabled = false
}
surfaceRenderer.routeData.value = ""
lastCameraSearch = 0
surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW
if (notificationActive)
notificationManager.stopNotificationService()
} }
override fun updateTrip(trip: Trip) { override fun updateTrip(trip: Trip) {
@@ -457,6 +640,19 @@ class NavigationSession : Session(), NavigationListener {
} }
} }
/**
* Recalculates a route for the specified place.
*/
override fun recalcRoute(destination: Place) {
val destination = location(destination.longitude, destination.latitude)
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
listOf(destination),
surfaceRenderer.carOrientation
)
}
/** /**
* Handle guidance audio * Handle guidance audio
* Called when user wants to hear the step-by-step instructions * Called when user wants to hear the step-by-step instructions
@@ -467,9 +663,220 @@ class NavigationSession : Session(), NavigationListener {
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) { if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
textToSpeechManager.speak(stepData.message) textToSpeechManager.speak(stepData.message)
lastStepIndex = currentStep.index lastStepIndex = currentStep.index
if (notificationActive) {
notificationManager.sendMessage(stepData.message)
}
} }
} }
/**
* Handles the received route string.
* Starts navigation and invalidates the screen.
*/
override fun onRouteReceived(route: String) {
Log.d(TAG, "onRouteReceived")
if (route.isNotEmpty()) {
this.route = route
if (routeModel.isNavigating()) {
updateRoute(route)
} else {
prepareRoute(route)
}
updateNavigationScreen()
}
}
/**
* Prepare route and start navigation
*/
private fun prepareRoute(route: String) {
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
routeModel.startNavigation(route)
if (routeModel.hasLegs()) {
settingsViewModel.onLastRouteChanged(route)
}
surfaceRenderer.setRouteData(routeModel.curRoute.routeGeoJson)
startNavigation()
updateNavigationScreen()
}
/**
* Update route and traffic data
*/
private fun updateRoute(route: String) {
val newRouteModel = RouteModel()
newRouteModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
newRouteModel.startNavigation(route)
routeModel.curRoute.summary.trafficDelay = newRouteModel.curRoute.summary.trafficDelay
updateNavigationScreen()
}
override fun isNavigating(): Boolean = routeModel.isNavigating()
/**
* Handles received traffic data and updates the surface renderer.
*/
override fun onTrafficReceived(traffic: Map<String, String>) {
if (traffic.isNotEmpty()) {
surfaceRenderer.setTrafficData(traffic)
}
}
/**
* Handles the received place search result.
* Navigates to the specified place.
*/
override fun onPlaceSearchResultReceived(place: Place) {
navigateToPlace(place)
}
/**
* Handles received speed camera data.
* Updates the surface renderer with the camera locations.
*/
override fun onSpeedCamerasReceived(cameras: List<Elements>) {
speedCameras = cameras
val coordinates = mutableListOf<List<Double>>()
cameras.forEach {
coordinates.add(listOf(it.lon, it.lat))
}
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCameraData.value = speedData
}
/**
* Handles received maximum speed data and updates the surface renderer.
*/
override fun onMaxSpeedReceived(speed: Int) {
surfaceRenderer.maxSpeed.value = speed
}
override fun onRecentPlacesReceived(places: List<Place>) {
}
override fun invalidateScreen() {
navigationScreen.invalidate()
}
/**
* Loads a route to the specified place and sets it as the destination.
*/
override fun navigateToPlace(place: Place) {
Log.d(TAG, "navigateToPlace ${place.street}")
var prevDestination = Place()
if (surfaceRenderer.navigation) {
prevDestination = routeModel.navState.destination
stopNavigation()
}
val preview = place.route
navigationViewModel.previewRoute.value = ""
val location = if (place.stopOver && prevDestination.latitude != 0.0) {
listOf(
location(place.longitude, place.latitude),
location(prevDestination.longitude, prevDestination.latitude)
)
} else {
listOf(location(place.longitude, place.latitude))
}
navigationViewModel.saveRecent(carContext, place)
routeModel.navState = routeModel.navState.copy(destination = place)
if (preview.isEmpty()) {
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
} else {
routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
onRouteReceived(preview)
}
surfaceRenderer.activateNavigationView()
}
/**
* Checks if traffic data needs to be updated based on the time since the last update.
*/
fun checkTraffic(current: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastTrafficDate)
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
lastTrafficDate = current
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
}
/**
* Periodically requests speed camera information near the current location.
*/
private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
navigationViewModel.getSpeedCameras(location, 5.0)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)
}
}
/**
* Updates distances to nearby speed cameras and checks for proximity alerts.
*/
private fun updateDistance(
location: Location,
) {
val updatedCameras = mutableListOf<Elements>()
speedCameras.forEach {
val plLocation =
location(longitude = it.lon, latitude = it.lat)
val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble()
updatedCameras.add(it)
}
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val camera = sortedList.firstOrNull() ?: return
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
val bearingSpeedCamera = if (camera.tags.direction != null) {
try {
camera.tags.direction!!.toFloat()
} catch (e: Exception) {
0F
}
} else {
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
}
if (camera.distance < 80) {
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
}
}
}
/**
* Checks if a new route is needed based on the time since the last update.
*/
private fun checkRoute(currentDate: LocalDateTime, location: Location) {
val duration = Duration.between(currentDate, lastRouteDate)
val routeUpdate = routeModel.curRoute.summary.duration / 4
if (duration.abs().seconds > routeUpdate) {
lastRouteDate = currentDate
val destination = location(
routeModel.navState.destination.longitude,
routeModel.navState.destination.latitude
)
navigationViewModel.loadRoute(
carContext,
location,
listOf(destination),
surfaceRenderer.carOrientation
)
}
}
override fun invalidateNavigationScreen() {
navigationScreen.invalidate()
}
companion object { companion object {
// URI host for deep linking // URI host for deep linking
var uriHost: String = "navigation" var uriHost: String = "navigation"
@@ -0,0 +1,62 @@
package com.kouros.navigation.car
import android.content.Intent
import android.location.Location
import android.os.Message
import android.util.Log
import androidx.car.app.CarContext
import androidx.car.app.hardware.CarHardwareManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kouros.navigation.data.Constants.TAG
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class NotificationManager(
private val carContext: CarContext,
private val lifecycleOwner: LifecycleOwner,
) {
private var notificationServiceStarted = false
private var serviceStarted = false
init {
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
}
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.DESTROYED) {
if (notificationServiceStarted) {
stopNotificationService()
}
}
}
}
fun startNotificationService() {
val intent = Intent(carContext, NavigationNotificationService::class.java)
carContext.startForegroundService(intent)
notificationServiceStarted = true
}
fun stopNotificationService() {
carContext
.stopService(
Intent(
carContext,
NavigationNotificationService::class.java
)
)
notificationServiceStarted = false
}
fun sendMessage(message: String) {
val intent = Intent(carContext, NavigationNotificationService::class.java).apply {
putExtra("EXTRA_MESSAGE", message)
}
carContext.startForegroundService(intent)
}
}
@@ -8,19 +8,15 @@ import android.location.Location
import android.util.Log import android.util.Log
import androidx.car.app.AppManager import androidx.car.app.AppManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Session
import androidx.car.app.SurfaceCallback import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer import androidx.car.app.SurfaceContainer
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -36,25 +32,20 @@ import com.kouros.navigation.car.map.getPaddingValues
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TILT import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.Constants.homeVogelhart import com.kouros.navigation.data.DarkMode
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.BaseStyleModel import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.utils.bearing import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateTilt import com.kouros.navigation.utils.calculateTilt
import com.kouros.navigation.utils.calculateZoom import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.duration import com.kouros.navigation.utils.duration
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import com.kouros.navigation.utils.previewZoom import com.kouros.navigation.utils.previewZoom
import com.kouros.navigation.utils.settingsViewModel import com.kouros.navigation.utils.settingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -65,9 +56,9 @@ import java.time.LocalDateTime
*/ */
class SurfaceRenderer( class SurfaceRenderer(
private var carContext: CarContext, private var carContext: CarContext,
private var lifecycle: Lifecycle, lifecycle: Lifecycle,
private var routeModel: RouteCarModel, private var viewModelStoreOwner: ViewModelStoreOwner,
private var viewModelStoreOwner: ViewModelStoreOwner private var navigationSession: CarSession
) : DefaultLifecycleObserver { ) : DefaultLifecycleObserver {
// Last known location for bearing calculations // Last known location for bearing calculations
@@ -80,7 +71,6 @@ class SurfaceRenderer(
val cameraPosition = MutableLiveData( val cameraPosition = MutableLiveData(
CameraPosition( CameraPosition(
zoom = 16.0, zoom = 16.0,
target = Position(latitude = homeVogelhart.latitude, longitude = homeVogelhart.longitude)
) )
) )
@@ -106,7 +96,7 @@ class SurfaceRenderer(
val trafficData = MutableLiveData(emptyMap<String, String>()) val trafficData = MutableLiveData(emptyMap<String, String>())
// Speed camera locations as GeoJSON // Speed camera locations as GeoJSON
val speedCamerasData = MutableLiveData("") val speedCameraData = MutableLiveData("")
// Current speed in km/h // Current speed in km/h
val speed = MutableLiveData(0F) val speed = MutableLiveData(0F)
@@ -120,6 +110,9 @@ class SurfaceRenderer(
// Current view mode (navigation, preview, etc.) // Current view mode (navigation, preview, etc.)
var viewStyle = ViewStyle.VIEW var viewStyle = ViewStyle.VIEW
// Flag to indicate if in navigation mode
var navigation = false
// Center location for route preview // Center location for route preview
lateinit var centerLocation: Location lateinit var centerLocation: Location
@@ -164,7 +157,7 @@ class SurfaceRenderer(
lifecycleOwner = CustomLifecycleOwner() lifecycleOwner = CustomLifecycleOwner()
lifecycleOwner.performRestore(null) lifecycleOwner.performRestore(null)
// technically, we only really need any one of these instead of all 3 // technically, we only really need any one of these instead of all 3
// i add them to be consistent with the actual lifecycle. // add them to be consistent with the actual lifecycle.
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START) lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
@@ -186,7 +179,7 @@ class SurfaceRenderer(
this.setViewTreeLifecycleOwner(lifecycleOwner) this.setViewTreeLifecycleOwner(lifecycleOwner)
this.setViewTreeSavedStateRegistryOwner(lifecycleOwner) this.setViewTreeSavedStateRegistryOwner(lifecycleOwner)
setContent { setContent {
MapView() MapView()
} }
} }
presentation = Presentation(carContext, virtualDisplay.display) presentation = Presentation(carContext, virtualDisplay.display)
@@ -231,10 +224,20 @@ class SurfaceRenderer(
} }
/** /**
* Called when user scrolls the map (not currently implemented). * Called when user scrolls the map .
*/ */
override fun onScroll(distanceX: Float, distanceY: Float) { override fun onScroll(distanceX: Float, distanceY: Float) {
synchronized(this@SurfaceRenderer) { synchronized(this@SurfaceRenderer) {
viewStyle = ViewStyle.PAN_VIEW
if (distanceX != 0.0F) {
lastLocation.longitude += (distanceX / 1000) / cameraPosition.value!!.zoom
}
if (distanceY != 0.0F) {
lastLocation.latitude += (distanceY / 1000) / cameraPosition.value!!.zoom
}
val pos = Position(lastLocation.longitude, lastLocation.latitude)
updateCameraPosition( target = pos)
navigationSession.invalidateNavigationScreen()
} }
} }
@@ -242,7 +245,9 @@ class SurfaceRenderer(
* Called when user scales (zooms) the map (not currently implemented). * Called when user scales (zooms) the map (not currently implemented).
*/ */
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) { override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
synchronized(this@SurfaceRenderer) {
Log.d(TAG, "onScale")
}
} }
} }
@@ -251,27 +256,25 @@ class SurfaceRenderer(
speed.value = 0F speed.value = 0F
} }
fun onBaseStyleStateUpdated(style: BaseStyle) {
}
/** /**
* Composable function that renders the map and navigation UI. * Composable function that renders the map and navigation UI.
* Observes various LiveData sources and updates the map accordingly. * Observes various LiveData sources and updates the map accordingly.
*/ */
@Composable @Composable
fun MapView() { fun MapView() {
val darkMode =
val darkMode = settingsViewModel(carContext, viewModelStoreOwner).darkMode.collectAsState().value settingsViewModel(carContext, viewModelStoreOwner).darkMode.collectAsState().value
val showBuildings = settingsViewModel(carContext, viewModelStoreOwner).show3D.collectAsState().value val showBuildings =
settingsViewModel(carContext, viewModelStoreOwner).show3D.collectAsState().value
val position: CameraPosition? by cameraPosition.observeAsState() val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState() val route: String? by routeData.observeAsState()
val traffic: Map<String, String> ? by trafficData.observeAsState() val traffic: Map<String, String>? by trafficData.observeAsState()
val speedCameras: String? by speedCamerasData.observeAsState() val speedCamera: String? by speedCameraData.observeAsState()
val paddingValues = getPaddingValues(height, viewStyle) val paddingValues = getPaddingValues(height, viewStyle)
val cameraState = cameraState(paddingValues, position, tilt) val cameraState = cameraState(paddingValues, position, tilt)
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode) val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
val dark = darkMode == 1 || darkMode == 2 && carContext.isDarkMode val dark = darkMode == DarkMode.DARK.ordinal
|| (darkMode == DarkMode.USE_CAR.ordinal && carContext.isDarkMode)
MapLibre( MapLibre(
cameraState, cameraState,
@@ -279,7 +282,7 @@ class SurfaceRenderer(
route, route,
traffic, traffic,
viewStyle, viewStyle,
speedCameras, speedCamera,
showBuildings showBuildings
) )
ShowPosition(cameraState, position, paddingValues, dark) ShowPosition(cameraState, position, paddingValues, dark)
@@ -297,7 +300,12 @@ class SurfaceRenderer(
darkMode: Boolean darkMode: Boolean
) { ) {
val cameraDuration = val cameraDuration =
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing, lastLocationUpdate) duration(
viewStyle == ViewStyle.PREVIEW,
position!!.bearing,
lastBearing,
lastLocationUpdate
)
val currentSpeed: Float? by speed.observeAsState() val currentSpeed: Float? by speed.observeAsState()
val maximumSpeed: Int? by maxSpeed.observeAsState() val maximumSpeed: Int? by maxSpeed.observeAsState()
val streetName: String? by street.observeAsState() val streetName: String? by street.observeAsState()
@@ -328,7 +336,6 @@ class SurfaceRenderer(
} }
override fun onCreate(owner: LifecycleOwner) { override fun onCreate(owner: LifecycleOwner) {
style.observe(owner, :: onBaseStyleStateUpdated)
Log.i(TAG, "SurfaceRenderer created") Log.i(TAG, "SurfaceRenderer created")
carContext.getCarService(AppManager::class.java) carContext.getCarService(AppManager::class.java)
.setSurfaceCallback(mSurfaceCallback) .setSurfaceCallback(mSurfaceCallback)
@@ -344,11 +351,13 @@ class SurfaceRenderer(
viewStyle = ViewStyle.PAN_VIEW viewStyle = ViewStyle.PAN_VIEW
} }
val newZoom = if (zoomSign < 0) { val newZoom = if (zoomSign < 0) {
cameraPosition.value!!.zoom - 1.0 cameraPosition.value!!.zoom - 0.2
} else { } else {
cameraPosition.value!!.zoom + 1.0 cameraPosition.value!!.zoom + 0.2
}
if (viewStyle == ViewStyle.VIEW) {
tilt = calculateTilt(newZoom, tilt)
} }
tilt = calculateTilt(newZoom, tilt)
updateCameraPosition( updateCameraPosition(
cameraPosition.value!!.bearing, cameraPosition.value!!.bearing,
newZoom, newZoom,
@@ -362,13 +371,10 @@ class SurfaceRenderer(
* Calculates appropriate bearing, zoom, and maintains view style. * Calculates appropriate bearing, zoom, and maintains view style.
* Uses car orientation sensor if available, otherwise falls back to location bearing. * Uses car orientation sensor if available, otherwise falls back to location bearing.
*/ */
fun updateLocation(location: Location) { fun updateLocation(location: Location, streetName: String) {
Log.d(TAG, "updateLocation Surface $location $streetName")
synchronized(this) { synchronized(this) {
if (routeModel.isNavigating()) { street.value = streetName
street.value = routeModel.currentStep().street
} else {
street.value = ""
}
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) { if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
val bearing = if (carOrientation == 999F) { val bearing = if (carOrientation == 999F) {
if (location.hasBearing()) { if (location.hasBearing()) {
@@ -402,8 +408,8 @@ class SurfaceRenderer(
/** /**
* Sets route data for active navigation and switches to VIEW mode. * Sets route data for active navigation and switches to VIEW mode.
*/ */
fun setRouteData() { fun setRouteData(routeGeoJson: String) {
routeData.value = routeModel.curRoute.routeGeoJson routeData.value = routeGeoJson
viewStyle = ViewStyle.VIEW viewStyle = ViewStyle.VIEW
} }
@@ -413,14 +419,15 @@ class SurfaceRenderer(
fun activateNavigationView() { fun activateNavigationView() {
viewStyle = ViewStyle.VIEW viewStyle = ViewStyle.VIEW
tilt = TILT tilt = TILT
updateLocation(lastLocation) updateLocation(lastLocation, "")
} }
/** /**
* Updates camera position with new bearing, zoom, and target. * Updates camera position with new bearing, zoom, and target.
* Posts update to LiveData for UI observation. * Posts update to LiveData for UI observation.
*/ */
fun updateCameraPosition(bearing: Double, zoom: Double, target: Position, tilt: Double) { fun updateCameraPosition(bearing: Double = 0.0, zoom: Double = cameraPosition.value!!.zoom ,
target: Position, tilt: Double = 0.0) {
synchronized(this) { synchronized(this) {
cameraPosition.postValue( cameraPosition.postValue(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
@@ -437,7 +444,7 @@ class SurfaceRenderer(
/** /**
* Updates traffic incident data on the map. * Updates traffic incident data on the map.
*/ */
fun setTrafficData(traffic: Map<String, String> ) { fun setTrafficData(traffic: Map<String, String>) {
trafficData.value = traffic as MutableMap<String, String>? trafficData.value = traffic as MutableMap<String, String>?
} }
@@ -450,7 +457,7 @@ class SurfaceRenderer(
with(routeModel) { with(routeModel) {
routeData.value = curRoute.routeGeoJson routeData.value = curRoute.routeGeoJson
centerLocation = curRoute.centerLocation centerLocation = curRoute.centerLocation
previewDistance = curRoute.summary.distance previewDistance = curLeg.summary.distance
} }
tilt = 0.0 tilt = 0.0
updateCameraPosition( updateCameraPosition(
@@ -459,7 +466,24 @@ class SurfaceRenderer(
Position(centerLocation.longitude, centerLocation.latitude), 0.0 Position(centerLocation.longitude, centerLocation.latitude), 0.0
) )
}
/**
* Sets up standard view mode with camera position.
* Calculates appropriate zoom
*/
fun setStandardView() {
if (!navigation) {
setRouteData("")
}
viewStyle = ViewStyle.VIEW
val zoom = calculateZoom(0.0)
tilt = calculateTilt(zoom, tilt)
updateCameraPosition(
tilt = tilt,
zoom = zoom,
target = Position(lastLocation.longitude, lastLocation.latitude)
)
} }
/** /**
@@ -469,26 +493,15 @@ class SurfaceRenderer(
synchronized(this) { synchronized(this) {
viewStyle = ViewStyle.AMENITY_VIEW viewStyle = ViewStyle.AMENITY_VIEW
routeData.value = route routeData.value = route
tilt = 0.0
updateCameraPosition( updateCameraPosition(
0.0, zoom = 14.0,
14.0, target = Position(location.longitude, location.latitude),
target = Position(location.longitude, location.latitude), tilt tilt = tilt
) )
} }
} }
/**
* Updates car location from the connected car system.
* Only updates location when using OSRM routing engine.
*/
fun updateCarLocation(location: Location) {
val repository = getSettingsRepository(carContext)
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
if (routingEngine == RouteEngine.OSRM.ordinal) {
updateLocation(location)
}
}
/** /**
* Updates current speed for display. * Updates current speed for display.
*/ */
@@ -1,6 +1,7 @@
package com.kouros.navigation.car.map package com.kouros.navigation.car.map
import android.location.Location import android.location.Location
import android.util.Log
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -33,6 +34,7 @@ import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.utils.isMetricSystem import com.kouros.navigation.utils.isMetricSystem
import com.kouros.navigation.utils.location
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
@@ -51,6 +53,7 @@ import org.maplibre.compose.location.LocationPuck
import org.maplibre.compose.location.LocationPuckColors import org.maplibre.compose.location.LocationPuckColors
import org.maplibre.compose.location.LocationPuckSizes import org.maplibre.compose.location.LocationPuckSizes
import org.maplibre.compose.location.UserLocationState import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.map.GestureOptions
import org.maplibre.compose.map.MapOptions import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions import org.maplibre.compose.map.OrnamentOptions
@@ -60,6 +63,7 @@ import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.sources.rememberGeoJsonSource import org.maplibre.compose.sources.rememberGeoJsonSource
import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds
@Composable @Composable
@@ -98,13 +102,16 @@ fun MapLibre(
OrnamentOptions(isScaleBarEnabled = false) OrnamentOptions(isScaleBarEnabled = false)
), ),
cameraState = cameraState, cameraState = cameraState,
baseStyle = baseStyle baseStyle = baseStyle,
) { ) {
getBaseSource(id = "openmaptiles")?.let { tiles -> getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!showBuildings) { if (!showBuildings) {
BuildingLayer(tiles) BuildingLayer(tiles)
} }
if (viewStyle == ViewStyle.AMENITY_VIEW) { if (viewStyle == ViewStyle.AMENITY_VIEW) {
val lastLocation = location(cameraState.position.target.longitude, cameraState.position.target.latitude)
Puck(cameraState, lastLocation)
AmenityLayer(route) AmenityLayer(route)
} else { } else {
RouteLayer(route, traffic!!) RouteLayer(route, traffic!!)
@@ -112,8 +119,7 @@ fun MapLibre(
} }
SpeedCameraLayer(speedCameras) SpeedCameraLayer(speedCameras)
} }
//val lastLocation = location(cameraState.position.target.longitude, cameraState.position.target.latitude)
//Puck(cameraState, lastLocation)
} }
} }
@@ -143,10 +149,10 @@ fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
interpolate( interpolate(
type = exponential(1.2f), type = exponential(1.2f),
input = zoom(), input = zoom(),
5 to const(0.4.dp), 5 to const(0.7.dp),
6 to const(0.7.dp), 6 to const(1.0.dp),
7 to const(1.75.dp), 7 to const(2.4.dp),
20 to const(22.dp), 20 to const(26.dp),
), ),
) )
} }
@@ -364,7 +370,7 @@ fun NavigationImage(
x = topLeftX , x = topLeftX ,
y = topLeftY, y = topLeftY,
), ),
color = if (darkMode) navigationColor else Color.White, color = if (darkMode) NavigationColorLight else Color.White,
cornerRadius = CornerRadius(x = 10f, y = 10f), cornerRadius = CornerRadius(x = 10f, y = 10f),
) )
drawText( drawText(
@@ -573,22 +579,7 @@ fun Puck(cameraState: CameraState, location: Location) {
locationState = location, locationState = location,
cameraState = cameraState, cameraState = cameraState,
accuracyThreshold = 10f, accuracyThreshold = 10f,
showBearing = false, oldLocationThreshold = 2.seconds,
sizes = LocationPuckSizes(dotRadius = 10.dp),
colors = LocationPuckColors(
dotFillColorCurrentLocation = Color.Cyan,
accuracyStrokeColor = Color.Green
)
)
}
@Composable
fun PuckState(cameraState: CameraState, userLocationState: UserLocationState) {
LocationPuck(
idPrefix = "user-location1",
locationState = userLocationState,
cameraState = cameraState,
accuracyThreshold = 10f,
showBearing = false, showBearing = false,
sizes = LocationPuckSizes(dotRadius = 10.dp), sizes = LocationPuckSizes(dotRadius = 10.dp),
colors = LocationPuckColors( colors = LocationPuckColors(
@@ -0,0 +1,349 @@
package com.kouros.navigation.car.navigation
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.location.Location
import android.location.LocationManager
import android.os.Binder
import android.os.IBinder
import android.text.TextUtils
import android.util.Log
import androidx.annotation.RequiresPermission
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.model.Distance.UNIT_METERS
import androidx.car.app.navigation.NavigationManager
import androidx.car.app.navigation.NavigationManagerCallback
import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import com.kouros.navigation.car.DeviceLocationManagerService
import com.kouros.navigation.car.screen.NavigationType
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Place
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlin.collections.copy
import kotlin.compareTo
class NavigationService : Service() {
val TAG: String = "NavigationService"
val DEEP_LINK_ACTION: String = ("com.kouros.navigation.car.navigation"
+ ".NavigationDeepLinkAction")
val channelId: String = "NavigationServiceChannel"
/** The identifier for the navigation notification displayed for the foreground service. */
val NAV_NOTIFICATION_ID: Int = 87356325
/** The identifier for the non-navigation notifications, such as a traffic accident warning. */
val NOTIFICATION_ID: Int = 71653346
// Constants for location broadcast
val PACKAGE_NAME: String =
"androidx.car.app.sample.navigation.common.nav.navigationservice"
val EXTRA_STARTED_FROM_NOTIFICATION: String = PACKAGE_NAME + ".started_from_notification"
val CANCEL_ACTION: String = "CANCEL"
private var notificationManager: NotificationManager? = null
private var carContext: CarContext? = null
var autoDriveEnabled = false
val simulation = Simulation()
private lateinit var listener: Listener
// Model for managing route state and navigation logic for Android Auto
var routeModel = RouteCarModel()
// Manages device GPS location updates
lateinit var deviceLocationManager: DeviceLocationManagerService
var currentLocation = location(0.0, 0.0)
lateinit var navigationViewModel: NavigationViewModel
private lateinit var navigationManager: NavigationManager
private var navigationManagerInitialized = false
var binder: IBinder = LocalBinder()
/** A listener for the navigation state changes. */
interface Listener {
/** Callback called when the navigation state changes. */
fun navigationStateChanged(
isNavigating: Boolean,
isRerouting: Boolean,
hasArrived: Boolean,
destinations: MutableList<Destination>,
steps: MutableList<Step>,
destinationTravelEstimate: TravelEstimate,
stepTravelEstimate: TravelEstimate,
stepRemainingDistance: Distance,
shouldShowNextStep: Boolean,
shouldShowLanes: Boolean,
junctionImage: CarIcon?,
backGroundColor: CarColor
)
fun updateServiceLocation(location: Location)
}
/**
* Class used for the client Binder. Since this service runs in the same process as its clients,
* we don't need to deal with IPC.
*/
inner class LocalBinder : Binder() {
val service: NavigationService
get() = this@NavigationService
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
override fun onCreate() {
Log.i(TAG, "In onCreate()");
createNotificationChannel();
}
override fun onBind(p0: Intent?): IBinder {
Log.d(TAG, "in onBind")
return binder
}
override fun onUnbind(intent: Intent): Boolean {
Log.d(TAG, "in UnBind")
if (!routeModel.isNavigating()) {
Log.d(TAG, "Stopping location updates")
if (::deviceLocationManager.isInitialized) {
deviceLocationManager.stopLocationUpdates()
}
}
return true
}
override fun onDestroy() {
if (::deviceLocationManager.isInitialized) {
deviceLocationManager.stopLocationUpdates()
}
Log.i(TAG, "In onDestroy()");
}
private fun createNotificationChannel() {
val serviceChannel = NotificationChannel(
"CHANNEL_ID",
"Location Service Channel",
NotificationManager.IMPORTANCE_HIGH
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
}
/** Sets the [CarContext] to use while the service is connected. */
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
fun setCarContext(
carContext: CarContext,
listener: Listener
) {
Log.d(TAG, "in setCarContext")
this.carContext = carContext
navigationViewModel = getViewModel(carContext)
this.listener = listener
deviceLocationManager = DeviceLocationManagerService(
carContext = carContext,
onLocationUpdate = ::updateLocation,
onInitialLocation = { location ->
updateLocation(location)
}
)
deviceLocationManager.startLocationUpdates()
navigationManagerInitialized = true
navigationManager =
carContext.getCarService(NavigationManager::class.java)
navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
override fun onAutoDriveEnabled() {
Log.d(TAG, "onAutoDriveEnabled")
// Called when the app should simulate navigation (e.g., for testing)
deviceLocationManager.stopLocationUpdates()
autoDriveEnabled = true
simulation()
CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
.show()
}
private fun simulation() {
simulation.gpxSimulation {
listener.updateServiceLocation(it)
}
}
override fun onStopNavigation() {
// Called when the user stops navigation in the car screen
// Stop turn-by-turn logic and clean up
stopNavigation()
if (autoDriveEnabled) {
deviceLocationManager.startLocationUpdates()
}
}
})
// Uncomment if navigating
// mNavigationManager.navigationStarted();
}
/** Clears the currently used {@link CarContext}. */
fun clearCarContext() {
Log.i(TAG, "clearContext");
carContext = null;
navigationManager.clearNavigationManagerCallback();
}
/** Starts navigation. */
fun startNavigation(route: String, destination: Place) {
Log.i(TAG, "Starting Navigation")
startService(Intent(applicationContext, NavigationService::class.java))
routeModel.navState = routeModel.navState.copy(destination = destination)
routeModel.navState = routeModel.navState.copy(routingEngine = 2)
routeModel.startNavigation(route)
if (routeModel.isNavigating()) {
routeModel.updateLocation(currentLocation, navigationViewModel)
listener.navigationStateChanged(
isNavigating = true,
isRerouting = false,
hasArrived = false,
destinations = mutableListOf(routeModel.getDestination()),
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext!!),
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext!!),
steps = routeModel.getSteps(carContext!!),
stepRemainingDistance = routeModel.getDistance(),
shouldShowNextStep = false,
shouldShowLanes = false,
junctionImage = null,
backGroundColor = routeModel.backGroundColor()
)
}
}
/** Starts navigation. */
fun stopNavigation() {
if (autoDriveEnabled) {
autoDriveEnabled = false
}
if (navigationManagerInitialized)
navigationManager.navigationEnded()
listener.navigationStateChanged(
isNavigating = false,
isRerouting = false,
hasArrived = false,
destinations = emptyList<Destination>().toMutableList(),
steps = emptyList<Step>().toMutableList(),
destinationTravelEstimate = routeModel.travelEstimate(carContext!!, 0.0, 0),
stepTravelEstimate = routeModel.travelEstimate(carContext!!, 0.0, 0),
stepRemainingDistance = Distance.create(0.0, UNIT_METERS),
shouldShowNextStep = false,
shouldShowLanes = false,
junctionImage = null,
backGroundColor = CarColor.BLUE
)
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
fun updateLocation(location: Location) {
Log.d(TAG, "updateLocation")
currentLocation = location
if (routeModel.isNavigating()) {
routeModel.updateLocation(location, navigationViewModel)
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
listener.updateServiceLocation(snappedLocation)
checkArrival()
updateNavigationScreen( 0)
} else {
listener.updateServiceLocation(location)
}
}
fun isNavigating(): Boolean {
return routeModel.isNavigating()
}
fun updateNavigationScreen(distanceMode: Int) {
if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty()
&& routeModel.navState.destination.street.isEmpty()
) {
return
}
listener.navigationStateChanged(
isNavigating = routeModel.isNavigating(),
isRerouting = false,
hasArrived = routeModel.isArrival(),
destinations = mutableListOf(routeModel.getDestination()),
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext!!),
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext!!),
steps = routeModel.getSteps(carContext!!),
stepRemainingDistance = routeModel.getDistance(),
shouldShowNextStep = false,
shouldShowLanes = false,
junctionImage = null,
backGroundColor = routeModel.backGroundColor()
)
/**
* Updates the trip information and notifies the listener with a new Trip object.
* This includes destination name, address, travel estimate, and loading status.
*/
val tripBuilder = Trip.Builder()
tripBuilder.addDestination(
routeModel.getDestination(),
routeModel.getTravelEstimateTrip(carContext!!)
)
tripBuilder.setLoading(false)
tripBuilder.setCurrentRoad(routeModel.getDestination().name.toString())
tripBuilder.addStep(routeModel.getSteps(carContext!!).first(), routeModel.getTravelEstimateStep(carContext!!))
navigationManager.updateTrip(tripBuilder.build())
}
/**
* Checks for arrival
*/
fun checkArrival() {
if (routeModel.isArrival()
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
stopNavigation()
routeModel.navState = routeModel.navState.copy(arrived = true)
}
}
}
@@ -3,6 +3,7 @@ package com.kouros.navigation.car.navigation
import android.text.SpannableString import android.text.SpannableString
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.util.Log
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.car.app.AppManager import androidx.car.app.AppManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
@@ -17,17 +18,17 @@ import androidx.car.app.model.DateTimeWithZone
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import androidx.car.app.model.DurationSpan import androidx.car.app.model.DurationSpan
import androidx.car.app.model.ForegroundCarColorSpan import androidx.car.app.model.ForegroundCarColorSpan
import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Lane import androidx.car.app.navigation.model.Lane
import androidx.car.app.navigation.model.LaneDirection import androidx.car.app.navigation.model.LaneDirection
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
import androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate import androidx.car.app.navigation.model.TravelEstimate
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.screen.createCarIcon import com.kouros.navigation.car.screen.createCarIcon
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.route.ManeuverType
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.formattedDistance import com.kouros.navigation.utils.formattedDistance
import java.time.Duration import java.time.Duration
@@ -47,17 +48,15 @@ class RouteCarModel : RouteModel() {
val maneuver = Maneuver.Builder(stepData.currentManeuverType) val maneuver = Maneuver.Builder(stepData.currentManeuverType)
.setIcon(createCarIcon(carContext, stepData.icon)) .setIcon(createCarIcon(carContext, stepData.icon))
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW if (stepData.currentManeuverType == ManeuverType.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW.ordinal
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW || stepData.currentManeuverType == ManeuverType.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW.ordinal
) { ) {
maneuver.setRoundaboutExitNumber(stepData.exitNumber) maneuver.setRoundaboutExitNumber(stepData.exitNumber)
} }
val step = val step =
Step.Builder(currentStepCueWithImage) Step.Builder(currentStepCueWithImage)
if (navState.destination.street != null) { step.setRoad(navState.destination.street)
step.setRoad(navState.destination.street!!)
}
if (stepData.lane.isNotEmpty()) { if (stepData.lane.isNotEmpty()) {
val lanesAdded = addLanes(carContext, step, stepData) val lanesAdded = addLanes(carContext, step, stepData)
if (lanesAdded) { if (lanesAdded) {
@@ -77,8 +76,8 @@ class RouteCarModel : RouteModel() {
createString(stepData.instruction) createString(stepData.instruction)
val maneuver = Maneuver.Builder(stepData.currentManeuverType) val maneuver = Maneuver.Builder(stepData.currentManeuverType)
.setIcon(createCarIcon(carContext, stepData.icon)) .setIcon(createCarIcon(carContext, stepData.icon))
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW if (stepData.currentManeuverType == ManeuverType.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW.ordinal
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW || stepData.currentManeuverType == ManeuverType.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW.ordinal
) { ) {
maneuver.setRoundaboutExitNumber(stepData.exitNumber) maneuver.setRoundaboutExitNumber(stepData.exitNumber)
} }
@@ -101,7 +100,6 @@ class RouteCarModel : RouteModel() {
} }
fun travelEstimate(carContext: CarContext, timeLeft: Double, distanceMode: Int): TravelEstimate { fun travelEstimate(carContext: CarContext, timeLeft: Double, distanceMode: Int): TravelEstimate {
val timeToDestinationMillis = val timeToDestinationMillis =
TimeUnit.SECONDS.toMillis(timeLeft.toLong()) TimeUnit.SECONDS.toMillis(timeLeft.toLong())
val distance = formattedDistance(distanceMode, routeCalculator.travelLeftDistance()) val distance = formattedDistance(distanceMode, routeCalculator.travelLeftDistance())
@@ -136,6 +134,37 @@ class RouteCarModel : RouteModel() {
} }
return travelBuilder.build() return travelBuilder.build()
} }
fun getSteps(carContext: CarContext): MutableList<Step> {
val steps = mutableListOf<Step>()
steps.add(currentStep(carContext))
if (navState.nextStep) {
steps.add(nextStep(carContext = carContext))
}
return steps
}
fun getDistance(): Distance {
val distance =
formattedDistance(0, routeCalculator.leftStepDistance())
return Distance.create(distance.first, distance.second)
}
fun getTravelEstimateTrip(carContext: CarContext): TravelEstimate {
return travelEstimateTrip(carContext, 0)
}
fun getTravelEstimateStep(carContext: CarContext): TravelEstimate {
return travelEstimateStep(carContext, 0)
}
fun getDestination(): Destination {
return Destination.Builder()
.setName(navState.destination.name)
.setAddress(navState.destination.street)
.build()
}
private fun createDelay(delay: Int): CarText { private fun createDelay(delay: Int): CarText {
val delayBuilder = SpannableStringBuilder() val delayBuilder = SpannableStringBuilder()
delayBuilder.append( delayBuilder.append(
@@ -3,8 +3,10 @@ package com.kouros.navigation.car.navigation
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.os.SystemClock import android.os.SystemClock
import android.util.Log
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import com.kouros.data.BuildConfig import com.kouros.data.BuildConfig
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import io.ticofab.androidgpxparser.parser.GPXParser import io.ticofab.androidgpxparser.parser.GPXParser
import io.ticofab.androidgpxparser.parser.domain.Gpx import io.ticofab.androidgpxparser.parser.domain.Gpx
@@ -28,6 +30,8 @@ class Simulation {
if (routeModel.navState.route.isRouteValid()) { if (routeModel.navState.route.isRouteValid()) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
gpxSimulation(routeModel, lifecycleScope, updateLocation) gpxSimulation(routeModel, lifecycleScope, updateLocation)
//gpxSimulation(updateLocation)
//currentSimulation(routeModel, lifecycleScope, updateLocation)
} else { } else {
currentSimulation(routeModel, lifecycleScope, updateLocation) currentSimulation(routeModel, lifecycleScope, updateLocation)
} }
@@ -46,24 +50,26 @@ class Simulation {
var lastLocation = Location(LocationManager.FUSED_PROVIDER) var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f var curBearing = 0f
simulationJob = lifecycleScope.launch { simulationJob = lifecycleScope.launch {
for (point in points) { for ((index, point) in points.withIndex()) {
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply { if (index >= 0) {
latitude = point[1] val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
longitude = point[0] latitude = point[1]
bearing = curBearing longitude = point[0]
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s bearing = curBearing
speed = 13.0f // ~50 km/h speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
time = System.currentTimeMillis() speed = 5.0f
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
}
curBearing = lastLocation.bearingTo(fakeLocation)
// Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second)
delay(1000)
lastLocation = fakeLocation
} }
curBearing = lastLocation.bearingTo(fakeLocation)
// Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second)
delay(1000)
lastLocation = fakeLocation
} }
routeModel.stopNavigation() // routeModel.stopNavigation()
} }
} }
@@ -84,7 +90,7 @@ class Simulation {
simulationJob?.join() simulationJob?.join()
} }
simulationJob?.cancel() simulationJob?.cancel()
simulationJob =lifecycleScope.launch() { simulationJob = lifecycleScope.launch() {
var lastLocation = Location(LocationManager.FUSED_PROVIDER) var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f var curBearing = 0f
val parser = GPXParser() val parser = GPXParser()
@@ -115,8 +121,9 @@ class Simulation {
updateLocation(fakeLocation) updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second) // Wait before moving to the next point (e.g., every 1 second)
if (duration > 100) { if (duration > 100) {
delay(duration / 4) // delay(duration / 4)
} }
delay(500)
lastTime = p.time lastTime = p.time
lastLocation = fakeLocation lastLocation = fakeLocation
} }
@@ -130,4 +137,62 @@ class Simulation {
fun stopSimulation() { fun stopSimulation() {
simulationJob?.cancel() simulationJob?.cancel()
} }
fun gpxSimulation(
updateLocation: (Location) -> Unit
) {
Runnable {
var route = ""
simulationJob?.cancel()
runBlocking {
simulationJob = launch(Dispatchers.IO) {
route = TomTomRepository().fetchUrl(
"https://kouros-online.de/vh.gpx",
false
)
}
simulationJob?.join()
}
simulationJob?.cancel()
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f
val parser = GPXParser()
val parsedGpx: Gpx? =
parser.parse(route.byteInputStream())
parsedGpx?.let {
val tracks = parsedGpx.tracks
tracks.forEach { tr ->
val segments: MutableList<TrackSegment?>? = tr.trackSegments
segments!!.forEach { seg ->
var lastTime = DateTime.now()
seg!!.trackPoints.forEach { p ->
val ext = p.extensions
var curSpeed = 0F
if (ext != null) {
curSpeed = ext.speed.toFloat()
}
val duration = p.time.millis - lastTime.millis
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = p.latitude
longitude = p.longitude
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = curSpeed
time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
}
// Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second)
if (duration > 100) {
// delay(duration / 4)
}
Thread.sleep(2000)
lastTime = p.time
lastLocation = fakeLocation
}
}
}
}
}.run()
}
} }
@@ -149,7 +149,7 @@ class CategoryScreen(
navigationViewModel.loadRoute( navigationViewModel.loadRoute(
carContext, carContext,
currentLocation = surfaceRenderer.lastLocation, currentLocation = surfaceRenderer.lastLocation,
location(it.lon, it.lat), listOf(location(it.lon, it.lat)),
surfaceRenderer.carOrientation surfaceRenderer.carOrientation
) )
setResult( setResult(
@@ -197,7 +197,7 @@ class CategoryScreen(
@DrawableRes iconRes: Int, @DrawableRes iconRes: Int,
scale: Int scale: Int
): Action { ): Action {
return createAction(carContext, iconRes, FLAG_IS_PERSISTENT, { return createAction(carContext, iconRes, FLAG_IS_PERSISTENT, {
surfaceRenderer.handleScale(scale) surfaceRenderer.handleScale(scale)
}) })
} }
@@ -1,7 +1,7 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import androidx.car.app.navigation.model.Trip import androidx.car.app.navigation.model.Trip
import com.kouros.navigation.data.Place
/** A listener for navigation start and stop signals. */ /** A listener for navigation start and stop signals. */
@@ -14,4 +14,9 @@ interface NavigationListener {
/** Updates trip information. */ /** Updates trip information. */
fun updateTrip(trip: Trip) fun updateTrip(trip: Trip)
fun navigateToPlace(place: Place)
fun recalcRoute(destination: Place)
} }
@@ -1,14 +1,15 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import android.location.Location
import android.location.LocationManager
import android.os.CountDownTimer import android.os.CountDownTimer
import android.os.Handler import android.os.Handler
import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
import androidx.car.app.model.ActionStrip import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import androidx.car.app.model.Header import androidx.car.app.model.Header
@@ -20,8 +21,10 @@ import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.navigation.model.MessageInfo import androidx.car.app.navigation.model.MessageInfo
import androidx.car.app.navigation.model.NavigationTemplate import androidx.car.app.navigation.model.NavigationTemplate
import androidx.car.app.navigation.model.PanModeListener
import androidx.car.app.navigation.model.RoutingInfo import androidx.car.app.navigation.model.RoutingInfo
import androidx.car.app.navigation.model.Trip import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@@ -30,29 +33,16 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
import com.kouros.navigation.car.screen.observers.NavigationObserverManager
import com.kouros.navigation.car.screen.settings.SettingsScreen import com.kouros.navigation.car.screen.settings.SettingsScreen
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.absoluteValue
/** /**
* Main screen for car navigation. * Main screen for car navigation.
@@ -61,42 +51,40 @@ import kotlin.math.absoluteValue
open class NavigationScreen( open class NavigationScreen(
carContext: CarContext, carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer, private var surfaceRenderer: SurfaceRenderer,
private var routeModel: RouteCarModel,
private var listener: NavigationListener, private var listener: NavigationListener,
private val navigationViewModel: NavigationViewModel private val navigationViewModel: NavigationViewModel
) : Screen(carContext), NavigationObserverCallback { ) : Screen(carContext) {
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
var recentPlaces = mutableListOf<Place>() var recentPlaces = mutableListOf<Place>()
var recentPlace: Place = Place() var recentPlace: Place = Place()
var navigationType = NavigationType.VIEW var navigationType = NavigationType.VIEW
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
var lastRouteDate: LocalDateTime = LocalDateTime.now()
var lastCameraSearch = 0
var speedCameras = listOf<Elements>()
val observerManager = NavigationObserverManager(navigationViewModel, this)
val repository = getSettingsRepository(carContext) val repository = getSettingsRepository(carContext)
val settingsViewModel = getSettingsViewModel(carContext) val settingsViewModel = getSettingsViewModel(carContext)
private var distanceMode = 0
private var tripSuggestion = false private var tripSuggestion = false
private var tripSuggestionCalled = false private var tripSuggestionCalled = false
private var routingEngine = 0
private var showTraffic = false;
private var arrivalTimer: CountDownTimer? = null private var arrivalTimer: CountDownTimer? = null
private var reRouteTimer: CountDownTimer? = null private var reRouteTimer: CountDownTimer? = null
private var isNavigating = false
private var isRerouting = false
private var hasArrived = false
private lateinit var destinations: MutableList<Destination>
private lateinit var stepRemainingDistance: Distance
private lateinit var destinationTravelEstimate: TravelEstimate
private lateinit var stepTravelEstimate: TravelEstimate
private var shouldShowNextStep = false
private var shouldShowLanes = false
private lateinit var steps: MutableList<Step>
private var junctionImage: CarIcon? = null
private var backGroundColor = CarColor.BLUE
private var showAlternativeRoute = false
val observerRecentPlaces = Observer<List<Place>> { newPlaces -> val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
Log.d(TAG, "NavigationScreen 4")
recentPlaces.addAll(newPlaces) recentPlaces.addAll(newPlaces)
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) { if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
tripSuggestionCalled = true tripSuggestionCalled = true
@@ -106,24 +94,18 @@ open class NavigationScreen(
} }
init { init {
observerManager.attachAllObservers(this)
lifecycleScope.launch { lifecycleScope.launch {
settingsViewModel.tripSuggestion.first() settingsViewModel.tripSuggestion.first()
settingsViewModel.routingEngine.first()
} }
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
repository.trafficFlow.asLiveData().observe(this, Observer {
showTraffic = it
})
repository.tripSuggestionFlow.asLiveData().observe(this, Observer { repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
Log.d(TAG, "NavigationScreen 3")
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces) navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
tripSuggestion = it tripSuggestion = it
}) })
repository.routingEngineFlow.asLiveData().observe(this, Observer {
routingEngine = it repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
showAlternativeRoute = it
}) })
lifecycle.addObserver(object : DefaultLifecycleObserver { lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
@@ -137,6 +119,7 @@ open class NavigationScreen(
* Returns the appropriate template based on the current navigation state. * Returns the appropriate template based on the current navigation state.
*/ */
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
Log.d(TAG, "NavigationScreen 2")
val actionStripBuilder = createActionStripBuilder({ val actionStripBuilder = createActionStripBuilder({
createAction( createAction(
carContext, carContext,
@@ -145,18 +128,18 @@ open class NavigationScreen(
) )
}, { settingsAction() }) }, { settingsAction() })
return when (navigationType) { return when (navigationType) {
NavigationType.NAVIGATION -> navigationTemplate(actionStripBuilder) NavigationType.NAVIGATION -> navigation(actionStripBuilder)
NavigationType.RECENT -> navigationRecentPlacesTemplate() NavigationType.RECENT -> navigationRecentPlaces()
NavigationType.REROUTE -> navigationRerouteTemplate(actionStripBuilder) NavigationType.REROUTE -> navigationReroute(actionStripBuilder)
NavigationType.ARRIVAL -> navigationEndTemplate(actionStripBuilder) NavigationType.ARRIVAL -> navigationEnd(actionStripBuilder)
else -> navigationViewTemplate(actionStripBuilder) else -> navigationView(actionStripBuilder)
} }
} }
/** /**
* Creates and returns a NavigationTemplate for the active navigation state. * Creates and returns a NavigationTemplate for the active navigation state.
*/ */
private fun navigationTemplate(actionStripBuilder: ActionStrip.Builder): Template { private fun navigation(actionStripBuilder: ActionStrip.Builder): Template {
actionStripBuilder.addAction( actionStripBuilder.addAction(
createAction( createAction(
carContext, carContext,
@@ -164,79 +147,81 @@ open class NavigationScreen(
0, 0,
{ stopNavigation() }) { stopNavigation() })
) )
updateTrip()
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo( .setNavigationInfo(
getRoutingInfo() getRoutingInfo()
) )
.setDestinationTravelEstimate(routeModel.travelEstimateTrip(carContext, distanceMode)) .setDestinationTravelEstimate(destinationTravelEstimate)
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip( .setMapActionStrip(
mapActionStrip( mapActionStrip(
carContext,
surfaceRenderer.viewStyle, surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, { { zoomPlus() }, { zoomMinus() }, {
createAction( Action.Builder()
carContext = carContext, R.drawable.ic_pan_24, .setIcon(createCarIcon(carContext, R.drawable.ic_recenter_24))
0, .setFlags(0)
onClickAction = { .setOnClickListener {
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.setStandardView()
invalidate() invalidate()
} }
) .build()
}) })
) )
.setBackgroundColor(routeModel.backGroundColor()) .setBackgroundColor(backGroundColor)
.build() .build()
} }
/** /**
* Creates and returns a template for the default view state. * Creates and returns a template for the default view state.
*/ */
private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template { private fun navigationView(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder() val mapActionStrip = mapActionStrip(
.setBackgroundColor(routeModel.backGroundColor()) carContext,
.setActionStrip(actionStripBuilder.build()) surfaceRenderer.viewStyle,
.setMapActionStrip( { zoomPlus() }, { zoomMinus() }, {
mapActionStrip( createAction(
surfaceRenderer.viewStyle, carContext = carContext, R.drawable.ic_recenter_24,
{ zoomPlus() }, { zoomMinus() }, { 0,
createAction( onClickAction = {
carContext = carContext, R.drawable.ic_pan_24, surfaceRenderer.viewStyle = ViewStyle.VIEW
0, invalidate()
onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate()
})
}) })
) })
return NavigationTemplate.Builder()
.setBackgroundColor(backGroundColor)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStrip)
.setPanModeListener { isInPanMode: Boolean ->
Log.d(TAG, "PanMode $isInPanMode")
}
.build() .build()
} }
/** /**
* Creates and returns a template for the arrival. * Creates and returns a template for the arrival.
*/ */
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template { private fun navigationEnd(actionStripBuilder: ActionStrip.Builder): Template {
arrivalTimer?.cancel() arrivalTimer?.cancel()
arrivalTimer = object : CountDownTimer(8000, 1000) { arrivalTimer = object : CountDownTimer(8000, 1000) {
override fun onTick(millisUntilFinished: Long) {} override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() { override fun onFinish() {
routeModel.navState = routeModel.navState.copy(arrived = false) // routeModel.navState = routeModel.navState.copy(arrived = false)
navigationType = NavigationType.VIEW navigationType = NavigationType.VIEW
invalidate() invalidate()
} }
} }
arrivalTimer?.start() arrivalTimer?.start()
return navigationArrivedTemplate(actionStripBuilder) return navigationArrived(actionStripBuilder)
} }
/** /**
* Creates and returns a NavigationTemplate specifically for when the destination is reached. * Creates and returns a NavigationTemplate specifically for when the destination is reached.
*/ */
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate { fun navigationArrived(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
var street = "" var street = ""
if (routeModel.navState.destination.street != null) { if (destinations.first().address != null) {
street = routeModel.navState.destination.street!! street = destinations.first().address.toString()
} }
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo( .setNavigationInfo(
@@ -255,14 +240,15 @@ open class NavigationScreen(
) )
.build() .build()
) )
.setBackgroundColor(routeModel.backGroundColor()) .setBackgroundColor(backGroundColor)
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip( .setMapActionStrip(
mapActionStrip( mapActionStrip(
carContext,
surfaceRenderer.viewStyle, surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, { { zoomPlus() }, { zoomMinus() }, {
createAction( createAction(
carContext = carContext, R.drawable.ic_pan_24, carContext = carContext, R.drawable.ic_recenter_24,
0, 0,
onClickAction = { onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.viewStyle = ViewStyle.VIEW
@@ -276,10 +262,10 @@ open class NavigationScreen(
/** /**
* Creates and returns a template showing recent places or destinations. * Creates and returns a template showing recent places or destinations.
*/ */
fun navigationRecentPlacesTemplate(): Template { fun navigationRecentPlaces(): Template {
if (!tripSuggestion || recentPlaces.isEmpty()) { if (!tripSuggestion || recentPlaces.isEmpty()) {
navigationType = NavigationType.VIEW navigationType = NavigationType.VIEW
return navigationViewTemplate( return navigationView(
createActionStripBuilder( createActionStripBuilder(
{ {
createAction( createAction(
@@ -292,14 +278,14 @@ open class NavigationScreen(
) )
} }
val listBuilder = ItemList.Builder() val listBuilder = ItemList.Builder()
recentPlaces.filter { it.category == Constants.RECENT && it.distance > 300F }.forEach { recentPlaces.filter { it.category == Constants.RECENT && it.distance > 300F }.forEach {
val row = Row.Builder() val row = Row.Builder()
.setTitle(it.name!!) .setTitle(it.name)
.addAction( .addAction(
createNavigateAction(it) createNavigateAction(it)
) )
.setOnClickListener { .setOnClickListener {
navigateToPlace(it) listener.navigateToPlace(it)
} }
listBuilder.addItem( listBuilder.addItem(
row.build() row.build()
@@ -320,6 +306,7 @@ open class NavigationScreen(
.setContentTemplate(contentTemplate) .setContentTemplate(contentTemplate)
.setActionStrip( .setActionStrip(
mapActionStrip( mapActionStrip(
carContext,
ViewStyle.VIEW, ViewStyle.VIEW,
{ settingsAction() }, { settingsAction() },
{ {
@@ -331,7 +318,7 @@ open class NavigationScreen(
}, },
{ {
createAction( createAction(
carContext = carContext, R.drawable.ic_zoom_out_24, carContext = carContext, R.drawable.ic_recenter_24,
FLAG_IS_PERSISTENT, FLAG_IS_PERSISTENT,
onClickAction = { onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.viewStyle = ViewStyle.VIEW
@@ -345,11 +332,11 @@ open class NavigationScreen(
/** /**
* Creates and returns a template for when the route is being recalculated. * Creates and returns a template for when the route is being recalculated.
*/ */
fun navigationRerouteTemplate(actionStripBuilder: ActionStrip.Builder): Template { fun navigationReroute(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build()) .setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setBackgroundColor(routeModel.backGroundColor()) .setBackgroundColor(backGroundColor)
.build() .build()
} }
@@ -357,16 +344,15 @@ open class NavigationScreen(
* Builds and returns RoutingInfo based on the current step and distance. * Builds and returns RoutingInfo based on the current step and distance.
*/ */
fun getRoutingInfo(): RoutingInfo { fun getRoutingInfo(): RoutingInfo {
val distance =
formattedDistance(distanceMode, routeModel.routeCalculator.leftStepDistance())
val routingInfo = RoutingInfo.Builder() val routingInfo = RoutingInfo.Builder()
.setCurrentStep( if (steps.isNotEmpty()) {
routeModel.currentStep(carContext = carContext), routingInfo.setCurrentStep(
Distance.create(distance.first, distance.second) steps.first(),
stepRemainingDistance
) )
if (routeModel.navState.nextStep) { }
val nextStep = routeModel.nextStep(carContext = carContext) if (shouldShowNextStep && steps.size > 1) {
routingInfo.setNextStep(nextStep) routingInfo.setNextStep(steps[1])
} }
return routingInfo.build() return routingInfo.build()
} }
@@ -388,10 +374,11 @@ open class NavigationScreen(
surfaceRenderer, surfaceRenderer,
place, place,
navigationViewModel, navigationViewModel,
showAlternativeRoute
) )
) { obj: Any? -> ) { obj: Any? ->
if (obj != null) { if (obj != null) {
navigateToPlace(place) listener.navigateToPlace(place)
} }
} }
} }
@@ -471,48 +458,22 @@ open class NavigationScreen(
if (place.longitude == 0.0) { if (place.longitude == 0.0) {
navigationViewModel.findAddress( navigationViewModel.findAddress(
"${obj.city} ${obj.street}},", "${obj.city} ${obj.street}},",
currentNavigationLocation surfaceRenderer.lastLocation
) )
// result see observer // result see observer
} else { } else {
navigateToPlace(place) listener.navigateToPlace(place)
} }
} }
} }
} }
/**
* Loads a route to the specified place and sets it as the destination.
*/
fun navigateToPlace(place: Place) {
val preview = navigationViewModel.previewRoute.value
navigationViewModel.previewRoute.value = ""
val location = location(place.longitude, place.latitude)
navigationViewModel.saveRecent(carContext, place)
currentNavigationLocation = location
if (preview.isNullOrEmpty()) {
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
} else {
routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
navigationViewModel.route.value = preview
}
routeModel.navState = routeModel.navState.copy(destination = place)
surfaceRenderer.activateNavigationView()
invalidate()
}
/** /**
* Stops navigation, resets state, and notifies listeners. * Stops navigation, resets state, and notifies listeners.
*/ */
fun stopNavigation() { fun stopNavigation() {
navigationType = NavigationType.VIEW navigationType = NavigationType.VIEW
listener.stopNavigation() listener.stopNavigation()
lastCameraSearch = 0
invalidate() invalidate()
} }
@@ -520,7 +481,6 @@ open class NavigationScreen(
* Initiates recalculation for a new route to the destination. * Initiates recalculation for a new route to the destination.
*/ */
fun calculateNewRoute(destination: Place) { fun calculateNewRoute(destination: Place) {
stopNavigation()
navigationType = NavigationType.REROUTE navigationType = NavigationType.REROUTE
invalidate() invalidate()
val mainThreadHandler = Handler(carContext.mainLooper) val mainThreadHandler = Handler(carContext.mainLooper)
@@ -530,247 +490,46 @@ open class NavigationScreen(
override fun onTick(millisUntilFinished: Long) {} override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() { override fun onFinish() {
navigationType = NavigationType.NAVIGATION navigationType = NavigationType.NAVIGATION
reRoute(destination) listener.recalcRoute(destination)
} }
} }
reRouteTimer?.start() reRouteTimer?.start()
} }
} }
/**
* Re-requests a route for the specified place.
*/
fun reRoute(place: Place) {
val destination = location(place.longitude, place.latitude)
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
destination,
surfaceRenderer.carOrientation
)
}
/** /**
* Updates navigation state with the current location, checks for arrival, and traffic updates. * Updates navigation state with the current location, checks for arrival, and traffic updates.
*/ */
fun updateTrip(location: Location) { fun updateTrip(
val currentDate = LocalDateTime.now(ZoneOffset.UTC) isNavigating: Boolean,
checkRoute(currentDate, location) isRerouting: Boolean,
checkTraffic(currentDate, location) hasArrived: Boolean,
destinations: MutableList<Destination>,
updateSpeedCamera(location) steps: MutableList<Step>,
destinationTravelEstimate: TravelEstimate,
routeModel.updateLocation(location, navigationViewModel) stepTravelEstimate: TravelEstimate,
checkArrival() stepRemainingDistance: Distance,
shouldShowNextStep: Boolean,
invalidate() shouldShowLanes: Boolean,
} junctionImage: CarIcon?,
backGroundColor: CarColor
/**
* Checks if a new route is needed based on the time since the last update.
*/
private fun checkRoute(currentDate: LocalDateTime, location: Location) {
val duration = Duration.between(currentDate, lastRouteDate)
val routeUpdate = routeModel.curRoute.summary.duration / 4
if (duration.abs().seconds > routeUpdate) {
lastRouteDate = currentDate
val destination = location(
routeModel.navState.destination.longitude,
routeModel.navState.destination.latitude
)
navigationViewModel.loadRoute(
carContext,
location,
destination,
surfaceRenderer.carOrientation
)
}
}
/**
* Checks if traffic data needs to be updated based on the time since the last update.
*/
fun checkTraffic(current: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastTrafficDate)
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
lastTrafficDate = current
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
}
/**
* Checks for arrival
*/
fun checkArrival() {
if (routeModel.isArrival()
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
listener.stopNavigation()
settingsViewModel.onLastRouteChanged("")
routeModel.navState = routeModel.navState.copy(arrived = true)
surfaceRenderer.routeData.value = ""
navigationType = NavigationType.ARRIVAL
invalidate()
}
}
/**
* Updates the trip information and notifies the listener with a new Trip object.
* This includes destination name, address, travel estimate, and loading status.
*/
private fun updateTrip() {
if (routeModel.isNavigating() && !routeModel.navState.destination.name.isNullOrEmpty()) {
val tripBuilder = Trip.Builder()
val destination = Destination.Builder()
.setName(routeModel.navState.destination.name ?: "")
.setAddress(routeModel.navState.destination.street ?: "")
.build()
tripBuilder.addDestination(
destination,
routeModel.travelEstimateTrip(carContext, distanceMode)
)
tripBuilder.setLoading(false)
tripBuilder.setCurrentRoad(routeModel.currentStep.street)
tripBuilder.addStep(routeModel.currentStep(carContext), routeModel.travelEstimateStep(carContext, distanceMode ))
listener.updateTrip(tripBuilder.build())
}
}
/**
* Periodically requests speed camera information near the current location.
*/
private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
navigationViewModel.getSpeedCameras(location, 5.0)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)
}
}
/**
* Updates distances to nearby speed cameras and checks for proximity alerts.
*/
private fun updateDistance(
location: Location,
) { ) {
val updatedCameras = mutableListOf<Elements>() this.isNavigating = isNavigating
speedCameras.forEach { this.isRerouting = isRerouting
val plLocation = this.hasArrived = hasArrived
location(longitude = it.lon, latitude = it.lat) this.destinations = destinations
val distance = plLocation.distanceTo(location) this.steps = steps
it.distance = distance.toDouble() this.stepRemainingDistance = stepRemainingDistance
updatedCameras.add(it) this.destinationTravelEstimate = destinationTravelEstimate
} this.stepTravelEstimate = stepTravelEstimate
val sortedList = updatedCameras.sortedWith(compareBy { it.distance }) this.shouldShowNextStep = shouldShowNextStep
val camera = sortedList.firstOrNull() ?: return this.shouldShowLanes = shouldShowLanes
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location) this.junctionImage = junctionImage
val bearingSpeedCamera = if (camera.tags.direction != null) { this.backGroundColor = backGroundColor
try {
camera.tags.direction!!.toFloat()
} catch (e: Exception) {
0F
}
} else {
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
}
if (camera.distance < 80) {
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
}
}
}
/**
* Handles the received route string.
* Starts navigation and invalidates the screen.
*/
override fun onRouteReceived(route: String) {
if (route.isNotEmpty()) {
if (routeModel.isNavigating()) {
updateRoute(route)
} else {
prepareRoute(route)
}
invalidate()
}
}
/**
* Prepare route and start navigation
*/
private fun prepareRoute(route: String) {
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
navigationType = NavigationType.NAVIGATION navigationType = NavigationType.NAVIGATION
routeModel.startNavigation(route)
if (routeModel.hasLegs()) {
settingsViewModel.onLastRouteChanged(route)
}
surfaceRenderer.setRouteData()
listener.startNavigation()
}
/**
* Update route and traffic data
*/
private fun updateRoute(route: String) {
val newRouteModel = RouteModel()
newRouteModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
navigationType = NavigationType.NAVIGATION
newRouteModel.startNavigation(route)
routeModel.curRoute.summary.trafficDelay = newRouteModel.curRoute.summary.trafficDelay
}
/**
* Checks if navigation is currently active.
*/
override fun isNavigating(): Boolean = routeModel.isNavigating()
/**
* Handles received traffic data and updates the surface renderer.
*/
override fun onTrafficReceived(traffic: Map<String, String>) {
if (traffic.isNotEmpty()) {
surfaceRenderer.setTrafficData(traffic)
}
}
/**
* Handles the received place search result.
* Navigates to the specified place.
*/
override fun onPlaceSearchResultReceived(place: Place) {
navigateToPlace(place)
}
/**
* Handles received speed camera data.
* Updates the surface renderer with the camera locations.
*/
override fun onSpeedCamerasReceived(cameras: List<Elements>) {
speedCameras = cameras
val coordinates = mutableListOf<List<Double>>()
cameras.forEach {
coordinates.add(listOf(it.lon, it.lat))
}
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCamerasData.value = speedData
}
/**
* Handles received maximum speed data and updates the surface renderer.
*/
override fun onMaxSpeedReceived(speed: Int) {
surfaceRenderer.maxSpeed.value = speed
}
/**
* Invalidates the screen.
*/
override fun invalidateScreen() {
invalidate() invalidate()
} }
} }
/** /**
@@ -5,7 +5,6 @@ import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.OnScreenResultListener
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
@@ -47,10 +46,15 @@ class PlaceListScreen(
private var routingEngine = 0 private var routingEngine = 0
private var showAlternativeRoute = false
init { init {
repository.routingEngineFlow.asLiveData().observe(this, Observer { repository.routingEngineFlow.asLiveData().observe(this, Observer {
routingEngine = it routingEngine = it
}) })
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
showAlternativeRoute = it
})
lifecycle.addObserver(object : DefaultLifecycleObserver { lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
navigationViewModel.recentPlaces.value = emptyList() navigationViewModel.recentPlaces.value = emptyList()
@@ -65,41 +69,12 @@ class PlaceListScreen(
val itemListBuilder = ItemList.Builder() val itemListBuilder = ItemList.Builder()
.setNoItemsMessage(carContext.getString(R.string.no_places)) .setNoItemsMessage(carContext.getString(R.string.no_places))
recentPlaces.filter { it.category == category }.forEach { recentPlaces.filter { it.category == category }.forEach {
val street = if (it.street != null) { val street = it.street
it.street
} else {
""
}
val row = Row.Builder() val row = Row.Builder()
.setImage(contactIcon(null, it.category)) .setImage(contactIcon(null, it.category))
.setTitle("$street ${it.city}") .setTitle("$street ${it.city}")
.setOnClickListener { .setOnClickListener {
place = Place( clickOnPlace(it)
0,
it.name,
it.category,
it.latitude,
it.longitude,
it.postalCode,
it.city,
it.street,
// avatar = null
)
screenManager
.pushForResult(
RoutePreviewScreen(
carContext,
RoutePreviewType.MULTI_ROUTE,
surfaceRenderer,
place,
navigationViewModel,
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
} }
if (category != CONTACTS) { if (category != CONTACTS) {
row.addText(SpannableString(" ").apply { row.addText(SpannableString(" ").apply {
@@ -137,6 +112,70 @@ class PlaceListScreen(
.build() .build()
} }
/**
* Creates an Action to navigate to a specific place.
*/
private fun clickOnPlace(it: Place) {
place = Place(
0,
it.name,
it.category,
it.latitude,
it.longitude,
it.postalCode,
it.city,
it.street,
// avatar = null
)
if (surfaceRenderer.navigation) {
startStopOverScreen(place)
} else {
starPreviewScreen(place)
}
}
/**
* Starts preview screen for a specific place.
*/
private fun starPreviewScreen(place: Place) {
screenManager
.pushForResult(
RoutePreviewScreen(
carContext,
if (showAlternativeRoute) RoutePreviewType.MULTI_ROUTE else RoutePreviewType.SINGLE_ROUTE,
surfaceRenderer,
place,
navigationViewModel,
showAlternativeRoute
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
}
/**
* Starts preview screen for a specific place.
*/
private fun startStopOverScreen(place: Place) {
screenManager
.pushForResult(
StopOverScreen(
carContext,
surfaceRenderer,
navigationViewModel,
place,
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
}
/** /**
* Creates an Action to delete a place. * Creates an Action to delete a place.
*/ */
@@ -16,9 +16,8 @@ import androidx.car.app.model.Template
/** Screen for asking the user to grant location permission. */ /** Screen for asking the user to grant location permission. */
class RequestPermissionScreen( class RequestPermissionScreen(
carContext: CarContext, carContext: CarContext,
var permissionCheckCallback: PermissionCheckCallback, val permissions: List<String?> = ArrayList(),
//var mContactsPermissionCheckCallback: LocationPermissionCheckCallback, var permissionCheckCallback: PermissionCheckCallback
val permissions: MutableList<String?> = ArrayList()
) : Screen(carContext) { ) : Screen(carContext) {
/** Callback called when the permission is granted. */ /** Callback called when the permission is granted. */
@@ -29,7 +28,7 @@ class RequestPermissionScreen(
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
var message = "" var message = "This app needs access to location"
if (permissions.contains(permission.ACCESS_FINE_LOCATION)) if (permissions.contains(permission.ACCESS_FINE_LOCATION))
message = "This app needs access to location and to car speed" message = "This app needs access to location and to car speed"
if (permissions.contains("android.car.permission.CAR_SPEED")) if (permissions.contains("android.car.permission.CAR_SPEED"))
@@ -83,7 +82,7 @@ fun checkPermission(carContext: CarContext, permission: String) : Boolean {
screenManager.pop() screenManager.pop()
return@RequestPermissionScreen return@RequestPermissionScreen
}, },
permissions permissions = permissions
) )
) )
} else { } else {
@@ -5,7 +5,6 @@ import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.util.Log import android.util.Log
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.annotation.DrawableRes
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
@@ -13,7 +12,6 @@ import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText import androidx.car.app.model.CarText
@@ -37,6 +35,7 @@ import androidx.lifecycle.lifecycleScope
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.route.Routes import com.kouros.navigation.data.route.Routes
@@ -56,6 +55,7 @@ class RoutePreviewScreen(
private var surfaceRenderer: SurfaceRenderer, private var surfaceRenderer: SurfaceRenderer,
private var destination: Place, private var destination: Place,
private val navigationViewModel: NavigationViewModel, private val navigationViewModel: NavigationViewModel,
private var showAlternativeRoute: Boolean
) : ) :
Screen(carContext) { Screen(carContext) {
private var isFavorite = false private var isFavorite = false
@@ -72,10 +72,11 @@ class RoutePreviewScreen(
var loading = true var loading = true
var flag = FLAG_DEFAULT
private val backPressedCallback = object : OnBackPressedCallback(false) { private val backPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
invalidate() }
}
} }
val observer = Observer<String> { route -> val observer = Observer<String> { route ->
@@ -84,6 +85,10 @@ class RoutePreviewScreen(
routeModel.startNavigation(route) routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel) surfaceRenderer.setPreviewRouteData(routeModel)
loading = false loading = false
if (routeModel.route.routes.size == 1 && showAlternativeRoute) {
routeType = RoutePreviewType.SINGLE_ROUTE
showAlternativeRoute = false
}
invalidate() invalidate()
} }
} }
@@ -112,6 +117,10 @@ class RoutePreviewScreen(
routingEngine = it routingEngine = it
}) })
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
showAlternativeRoute = it
})
lifecycleScope.launch { lifecycleScope.launch {
navigationViewModel.loadPreviewRoute( navigationViewModel.loadPreviewRoute(
carContext, carContext,
@@ -141,15 +150,12 @@ class RoutePreviewScreen(
} }
} }
val street = if (destination.street.isNullOrEmpty()) { val street = destination.street.ifEmpty {
carContext.getString((R.string.route_preview)) carContext.getString((R.string.route_preview))
} else {
destination.street.toString()
} }
val header = Header.Builder() val header = Header.Builder()
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)
.setTitle(street) .setTitle(street)
if (routeType == RoutePreviewType.SINGLE_ROUTE) { if (routeType == RoutePreviewType.SINGLE_ROUTE) {
header.addEndHeaderAction( header.addEndHeaderAction(
favoriteAction() favoriteAction()
@@ -176,18 +182,8 @@ class RoutePreviewScreen(
.build() .build()
listContent.build() listContent.build()
} else { } else {
val navigateActionIcon: CarIcon = CarIcon.Builder(
IconCompat.createWithResource(
carContext, R.drawable.navigation_48px
)
).build()
val selectRouteIcon: CarIcon = CarIcon.Builder(
IconCompat.createWithResource(
carContext, R.drawable.alt_route_48px
)
).build()
val navigateAction = val navigateAction =
createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT,{ createAction(carContext, R.drawable.navigation_48px, flag,{
onNavigate(routeModel.navState.currentRouteIndex) onNavigate(routeModel.navState.currentRouteIndex)
}) })
val selectRouteAction = createAction(carContext, R.drawable.alt_route_48px, FLAG_IS_PERSISTENT, { val selectRouteAction = createAction(carContext, R.drawable.alt_route_48px, FLAG_IS_PERSISTENT, {
@@ -197,7 +193,10 @@ class RoutePreviewScreen(
val listContent = MessageTemplate.Builder(message) val listContent = MessageTemplate.Builder(message)
.setHeader(header.build()) .setHeader(header.build())
.addAction(navigateAction) .addAction(navigateAction)
.addAction(selectRouteAction)
if (showAlternativeRoute) {
listContent.addAction(selectRouteAction)
}
if (loading) { if (loading) {
listContent.setLoading(true) listContent.setLoading(true)
} }
@@ -208,12 +207,12 @@ class RoutePreviewScreen(
.setContentTemplate(content) .setContentTemplate(content)
.setMapController( .setMapController(
MapController.Builder().setMapActionStrip( MapController.Builder().setMapActionStrip(
mapActionStrip(ViewStyle.PREVIEW, {zoomPlus()}, { zoomMinus()}, { mapActionStrip(carContext, ViewStyle.PREVIEW, {zoomPlus()}, { zoomMinus()}, {
zoomMinus() zoomMinus()
} )).build() } )).build()
) )
if (routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) { if (routeModel.route.routes.size > 1 && routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) {
template.setActionStrip(createActionStrip { template.setActionStrip(createActionStrip {
createAction( createAction(
carContext, R.drawable.navigation_48px, carContext, R.drawable.navigation_48px,
@@ -231,6 +230,7 @@ class RoutePreviewScreen(
carContext, R.drawable.ic_zoom_in_24, carContext, R.drawable.ic_zoom_in_24,
FLAG_IS_PERSISTENT, FLAG_IS_PERSISTENT,
onClickAction = { onClickAction = {
flag = FLAG_IS_PERSISTENT
surfaceRenderer.handleScale(1) surfaceRenderer.handleScale(1)
invalidate() invalidate()
} }
@@ -245,6 +245,7 @@ class RoutePreviewScreen(
carContext, R.drawable.ic_zoom_out_24, carContext, R.drawable.ic_zoom_out_24,
FLAG_IS_PERSISTENT, FLAG_IS_PERSISTENT,
onClickAction = { onClickAction = {
flag = FLAG_IS_PERSISTENT
surfaceRenderer.handleScale(-1) surfaceRenderer.handleScale(-1)
invalidate() invalidate()
} }
@@ -303,7 +304,7 @@ class RoutePreviewScreen(
} }
private fun createRow(route: Routes, index: Int): Row { private fun createRow(route: Routes, index: Int): Row {
val navigateAction = createAction(carContext, R.drawable.navigation_48px ) { val navigateAction = createAction(carContext, R.drawable.navigation_48px, flag ) {
this.onNavigate(index) this.onNavigate(index)
} }
val routeText = createRouteText(route) val routeText = createRouteText(route)
@@ -347,6 +348,7 @@ class RoutePreviewScreen(
private fun onNavigate(index: Int) { private fun onNavigate(index: Int) {
destination.routeIndex = index destination.routeIndex = index
destination.route = navigationViewModel.previewRoute.value.toString()
setResult(destination) setResult(destination)
finish() finish()
} }
@@ -12,6 +12,7 @@ import androidx.car.app.model.CarIcon
import androidx.car.app.model.Row import androidx.car.app.model.Row
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R
import com.kouros.navigation.data.Constants.CHARGING_STATION import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY import com.kouros.navigation.data.Constants.PHARMACY
@@ -82,20 +83,28 @@ fun createActionStripBuilder(action1: () -> Action, action2: () -> Action): Acti
* Creates an ActionStrip builder for map-related actions like zoom and pan. * Creates an ActionStrip builder for map-related actions like zoom and pan.
*/ */
fun mapActionStrip( fun mapActionStrip(
carContext: CarContext,
viewStyle: ViewStyle, viewStyle: ViewStyle,
zoomPlus: () -> Action, zoomPlus: () -> Action,
zoomMinus: () -> Action, zoomMinus: () -> Action,
panAction: () -> Action recenterAction: () -> Action
): ActionStrip { ): ActionStrip {
val actionStripBuilder = ActionStrip.Builder() val actionStripBuilder = ActionStrip.Builder()
.addAction(zoomPlus()) .addAction(zoomPlus())
.addAction(zoomMinus()) .addAction(zoomMinus())
actionStripBuilder.addAction(
Action.Builder(Action.PAN)
.setIcon(createCarIcon(carContext, R.drawable.ic_pan_24))
.setFlags(0)
.build()
)
if (viewStyle == ViewStyle.PAN_VIEW) { if (viewStyle == ViewStyle.PAN_VIEW) {
actionStripBuilder actionStripBuilder
.addAction( .addAction(
panAction() recenterAction()
) )
} }
return actionStripBuilder.build() return actionStripBuilder.build()
} }
@@ -106,7 +115,7 @@ fun createAction(
carContext: CarContext, carContext: CarContext,
@DrawableRes iconRes: Int, @DrawableRes iconRes: Int,
flag: Int = FLAG_DEFAULT, flag: Int = FLAG_DEFAULT,
onClickAction: () -> Unit onClickAction: () -> Unit,
): Action { ): Action {
return Action.Builder() return Action.Builder()
.setIcon(createCarIcon(carContext, iconRes)) .setIcon(createCarIcon(carContext, iconRes))
@@ -79,7 +79,7 @@ class SearchScreen(
navigationViewModel navigationViewModel
) )
) { obj: Any? -> ) { obj: Any? ->
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.setStandardView()
if (obj != null) { if (obj != null) {
setResult(obj) setResult(obj)
finish() finish()
@@ -96,7 +96,7 @@ class SearchScreen(
recentPlaces recentPlaces
) )
) { obj: Any? -> ) { obj: Any? ->
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.setStandardView()
if (obj != null) { if (obj != null) {
setResult(obj) setResult(obj)
finish() finish()
@@ -0,0 +1,68 @@
package com.kouros.navigation.car.screen
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
import androidx.car.app.model.CarText
import androidx.car.app.model.Header
import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.Template
import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.NavigationViewModel
class StopOverScreen(
private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer,
private val navigationViewModel: NavigationViewModel,
private val place: Place,
) : Screen(carContext) {
override fun onGetTemplate(): MapWithContentTemplate {
val cancelAction =
createAction(carContext, R.drawable.ic_close_white_24dp, FLAG_IS_PERSISTENT,{
finish()
})
val header = Header.Builder()
.setStartHeaderAction(Action.BACK)
.addEndHeaderAction(cancelAction)
.setTitle(place.street)
val message = CarText.Builder("Neue Fahrt oder Zwischenstopp einfügen")
.build()
val navigateAction = Action.Builder()
.setIcon(createCarIcon(carContext, R.drawable.navigation_48px))
.setFlags(FLAG_DEFAULT)
.setOnClickListener {
setResult(place)
finish()
}
.build()
val selectRouteAction = Action.Builder()
.setIcon(createCarIcon(carContext, R.drawable.alt_route_48px))
.setFlags(FLAG_IS_PERSISTENT)
.setOnClickListener {
place.stopOver = true
setResult(place)
finish()
}
.build()
val listContent = MessageTemplate.Builder(message)
.setHeader(header.build())
.addAction(navigateAction)
.addAction(selectRouteAction)
val template = MapWithContentTemplate.Builder()
.setContentTemplate(listContent.build())
return template.build()
}
}
@@ -1,7 +1,6 @@
package com.kouros.navigation.car.screen.observers package com.kouros.navigation.car.screen.observers
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
/** /**
@@ -26,6 +25,8 @@ interface NavigationObserverCallback {
/** Called when max speed is updated */ /** Called when max speed is updated */
fun onMaxSpeedReceived(speed: Int) fun onMaxSpeedReceived(speed: Int)
fun onRecentPlacesReceived(places: List<Place>)
/** Called to request UI invalidation/refresh */ /** Called to request UI invalidation/refresh */
fun invalidateScreen() fun invalidateScreen()
@@ -1,5 +1,7 @@
package com.kouros.navigation.car.screen.observers package com.kouros.navigation.car.screen.observers
import com.kouros.navigation.car.CarSession
import com.kouros.navigation.car.NavigationSession
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
/** /**
@@ -17,16 +19,18 @@ class NavigationObserverManager(
val speedCameraObserver = SpeedCameraObserver(callback) val speedCameraObserver = SpeedCameraObserver(callback)
val maxSpeedObserver = MaxSpeedObserver(callback) val maxSpeedObserver = MaxSpeedObserver(callback)
/** val recentPlacesObserver = RecentPlacesObserver(callback)
* Attaches all observers to the ViewModel.
* Call this from NavigationScreen's init block or lifecycle method.
*/
fun attachAllObservers(screen: androidx.car.app.Screen) { fun attachAllObservers(session: CarSession) {
viewModel.route.observe(screen, routeObserver) viewModel.route.observe(session, routeObserver)
viewModel.traffic.observe(screen, trafficObserver) viewModel.traffic.observe(session, trafficObserver)
viewModel.placeLocation.observe(screen, placeSearchObserver) viewModel.placeLocation.observe(session, placeSearchObserver)
viewModel.speedCameras.observe(screen, speedCameraObserver) viewModel.speedCameras.observe(session, speedCameraObserver)
viewModel.maxSpeed.observe(screen, maxSpeedObserver) viewModel.maxSpeed.observe(session, maxSpeedObserver)
viewModel.recentPlaces.observe(session, recentPlacesObserver)
} }
/** /**
@@ -0,0 +1,18 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
import com.kouros.navigation.data.Place
/**
* Observer for route updates. Triggers navigation start when a non-empty route is received.
*/
class RecentPlacesObserver(
private val callback: NavigationObserverCallback
) : Observer<List<Place>> {
override fun onChanged(value: List<Place>) {
if (value.isNotEmpty()) {
callback.onRecentPlacesReceived(value)
}
}
}
@@ -33,6 +33,8 @@ class NavigationSettings(
private var carLocationToggleState = false private var carLocationToggleState = false
private var alternativeRoutesToggleState = false
val settingsViewModel = getSettingsViewModel(carContext) val settingsViewModel = getSettingsViewModel(carContext)
init { init {
@@ -41,6 +43,7 @@ class NavigationSettings(
settingsViewModel.avoidMotorway.first() settingsViewModel.avoidMotorway.first()
settingsViewModel.avoidFerry.first() settingsViewModel.avoidFerry.first()
settingsViewModel.carLocation.first() settingsViewModel.carLocation.first()
settingsViewModel.alternativeRoutes.first()
} }
} }
@@ -49,6 +52,7 @@ class NavigationSettings(
tollWayToggleState = settingsViewModel.avoidTollway.value tollWayToggleState = settingsViewModel.avoidTollway.value
ferryToggleState = settingsViewModel.avoidFerry.value ferryToggleState = settingsViewModel.avoidFerry.value
carLocationToggleState = settingsViewModel.carLocation.value carLocationToggleState = settingsViewModel.carLocation.value
alternativeRoutesToggleState = settingsViewModel.alternativeRoutes.value
val listBuilder = ItemList.Builder() val listBuilder = ItemList.Builder()
@@ -89,6 +93,14 @@ class NavigationSettings(
carLocationToggleState = !carLocationToggleState carLocationToggleState = !carLocationToggleState
}.setChecked(carLocationToggleState).build() }.setChecked(carLocationToggleState).build()
// Alternative routes
val alternativeRoutesToggle: Toggle =
Toggle.Builder { checked: Boolean ->
settingsViewModel.onAlternativeRoutes(checked)
alternativeRoutesToggleState = !alternativeRoutesToggleState
}.setChecked(alternativeRoutesToggleState).build()
listBuilder.addItem( listBuilder.addItem(
buildRowForTemplate( buildRowForTemplate(
R.string.use_car_location, R.string.use_car_location,
@@ -97,6 +109,14 @@ class NavigationSettings(
) )
) )
listBuilder.addItem(
buildRowForTemplate(
R.string.alternative_routes,
alternativeRoutesToggle,
createCarIcon(carContext,R.drawable.alt_route_48px)
)
)
listBuilder.addItem( listBuilder.addItem(
buildRowForScreenTemplate( buildRowForScreenTemplate(
RoutingSettings(carContext, navigationViewModel), RoutingSettings(carContext, navigationViewModel),
@@ -109,6 +129,7 @@ class NavigationSettings(
R.string.tomtom_api_key R.string.tomtom_api_key
) )
) )
return ListTemplate.Builder() return ListTemplate.Builder()
.setSingleList(listBuilder.build()) .setSingleList(listBuilder.build())
.setHeader( .setHeader(
@@ -2,11 +2,11 @@ package com.kouros.navigation.data
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val NavigationColorLight = Color(0xFF066462) val NavigationColorLight = Color(0xFF17A119)
val NavigationColorDark = Color(0xFF10DED9) val NavigationColorDark = Color(0xFF4EDE10)
val RouteColor = Color(0xFF7B06E1) val RouteColor = Color(0xFF195D02)
val SpeedColor = Color(0xFF262525) val SpeedColor = Color(0xFF262525)
@@ -17,9 +17,11 @@
package com.kouros.navigation.data package com.kouros.navigation.data
import android.net.Uri import android.net.Uri
import com.google.gson.annotations.Expose
import com.kouros.navigation.data.route.Lane import com.kouros.navigation.data.route.Lane
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
data class Category( data class Category(
val id: String, val id: String,
@@ -27,6 +29,7 @@ data class Category(
) )
data class Places( data class Places(
val places: List<Place>, val places: List<Place>,
) )
@@ -34,17 +37,23 @@ data class Places(
@Serializable @Serializable
data class Place( data class Place(
var id: Long = 0, var id: Long = 0,
var name: String? = null, var name: String = "",
var category: String? = null, var category: String = "",
var latitude: Double = 0.0, var latitude: Double = 0.0,
var longitude: Double = 0.0, var longitude: Double = 0.0,
var postalCode: String? = null, var postalCode: String = "",
var city: String? = null, var city: String = "",
var street: String? = null, var street: String = "",
@Transient
var distance: Float = 0F, var distance: Float = 0F,
//var avatar: Uri? = null, //var avatar: Uri? = null,
var lastDate: Long = 0, var lastDate: Long = 0,
var routeIndex: Int = 0 @Transient
var routeIndex: Int = 0,
@Transient
var route: String = "",
@Transient
var stopOver: Boolean = false,
) )
data class ContactData( data class ContactData(
@@ -111,8 +120,6 @@ object Constants {
const val CHARGING_STATION: String ="charging_station" const val CHARGING_STATION: String ="charging_station"
val categories = listOf("Tankstelle", "Apotheke", "Ladestationen")
/** The initial location to use as an anchor for searches. */ /** The initial location to use as an anchor for searches. */
val homeVogelhart = location(11.5793748, 48.185749) val homeVogelhart = location(11.5793748, 48.185749)
val homeHohenwaldeck = location( 11.594322, 48.1164817) val homeHohenwaldeck = location( 11.594322, 48.1164817)
@@ -156,6 +163,10 @@ enum class RouteEngine {
VALHALLA, OSRM, TOMTOM VALHALLA, OSRM, TOMTOM
} }
enum class DarkMode {
LIGHT, DARK, USE_CAR
}
enum class EngineType { enum class EngineType {
COMBUSTION, ELECTRIC COMBUSTION, ELECTRIC
} }
@@ -20,7 +20,7 @@ abstract class NavigationRepository {
abstract fun getRoute( abstract fun getRoute(
context: Context, context: Context,
currentLocation: Location, currentLocation: Location,
destination: Location, location: List<Location>,
carOrientation: Float, carOrientation: Float,
searchFilter: SearchFilter searchFilter: SearchFilter
): String ): String
@@ -28,9 +28,7 @@ abstract class NavigationRepository {
abstract fun getTraffic(context: Context, location: Location, carOrientation: Float): String abstract fun getTraffic(context: Context, location: Location, carOrientation: Float): String
fun getRouteDistance( fun getRouteDistance(
currentLocation: Location, currentLocation: Location,
location: Location, location: Location
carOrientation: Float,
context: Context
): Double { ): Double {
if (currentLocation.latitude == 0.0) if (currentLocation.latitude == 0.0)
return 0.0 return 0.0
@@ -38,12 +36,16 @@ abstract class NavigationRepository {
} }
fun searchPlaces(search: String, location: Location): String { fun searchPlaces(search: String, location: Location): String {
val box = calculateSquareRadius(location.latitude, location.longitude, 800.0) val box = calculateSquareRadius(location.latitude, location.longitude, 50.0)
val viewbox = "&bounded=1&viewbox=${box}" val viewbox = "&bounded=1&viewbox=${box}"
return fetchUrl( var result = fetchUrl(
"${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox", "${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox",
true false
) )
if (result == "[]") {
result = fetchUrl("${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true", false)
}
return result
} }
fun reverseAddress(location: Location): String { fun reverseAddress(location: Location): String {
@@ -67,13 +69,13 @@ abstract class NavigationRepository {
} }
}) })
} }
Log.d("fetchUrl", url) Log.d("NavigationRepository", url)
val httpURLConnection = URL(url).openConnection() as HttpURLConnection val httpURLConnection = URL(url).openConnection() as HttpURLConnection
httpURLConnection.setRequestProperty( httpURLConnection.setRequestProperty(
"Accept", "Accept",
"application/json" "application/json"
) // The format of response we want to get from the server ) // The format of response we want to get from the server
httpURLConnection.setRequestProperty("User-Agent", "email=nominatim@kouros-online.de"); httpURLConnection.setRequestProperty("User-Agent", "email=nominatim@kouros-online.de")
httpURLConnection.requestMethod = "GET" httpURLConnection.requestMethod = "GET"
val responseCode = httpURLConnection.responseCode val responseCode = httpURLConnection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) { if (responseCode == HttpURLConnection.HTTP_OK) {
@@ -16,17 +16,16 @@ import kotlinx.coroutines.flow.map
private const val DATASTORE_NAME = "navigation_settings" private const val DATASTORE_NAME = "navigation_settings"
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME)
/** /**
* Central manager for app settings using DataStore * Central manager for app settings using DataStore
*/ */
class DataStoreManager(private val context: Context) { class DataStoreManager(private val context: Context) {
companion object {
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME)
}
// Keys // Keys
object PreferencesKeys { companion object PreferencesKeys {
val SHOW_3D = booleanPreferencesKey("Show3D") val SHOW_3D = booleanPreferencesKey("Show3D")
@@ -58,181 +57,195 @@ class DataStoreManager(private val context: Context) {
val ENGINE_TYPE = intPreferencesKey("EngineType") val ENGINE_TYPE = intPreferencesKey("EngineType")
val ALTERNATIVE_ROUTES = booleanPreferencesKey("AlternativeRoutes")
} }
// Read values // Read values
val show3DFlow: Flow<Boolean> = val show3DFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.SHOW_3D] == true preferences[SHOW_3D] == true
} }
val darkModeFlow: Flow<Int> = val darkModeFlow: Flow<Int> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.DARK_MODE] preferences[DARK_MODE]
?: 0 ?: 0
} }
val avoidMotorwayFlow: Flow<Boolean> = val avoidMotorwayFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.AVOID_MOTORWAY] == true preferences[AVOID_MOTORWAY] == true
} }
val avoidTollwayFlow: Flow<Boolean> = val avoidTollwayFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.AVOID_TOLLWAY] == true preferences[AVOID_TOLLWAY] == true
} }
val avoidFerryFlow: Flow<Boolean> = val avoidFerryFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.AVOID_FERRY] == true preferences[AVOID_FERRY] == true
} }
val useCarLocationFlow: Flow<Boolean> = val useCarLocationFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.CAR_LOCATION] == true preferences[CAR_LOCATION] == true
} }
val routingEngineFlow: Flow<Int> = val routingEngineFlow: Flow<Int> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.ROUTING_ENGINE] preferences[ROUTING_ENGINE]
?: 2 ?: 2
} }
val lastRouteFlow: Flow<String> = val lastRouteFlow: Flow<String> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.LAST_ROUTE] preferences[LAST_ROUTE]
?: "" ?: ""
} }
val tomTomApiKeyFlow: Flow<String> = val tomTomApiKeyFlow: Flow<String> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.TOMTOM_APIKEY] preferences[TOMTOM_APIKEY]
?: "" ?: ""
} }
val recentPlacesFlow: Flow<String> = val recentPlacesFlow: Flow<String> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.RECENT_PLACES] preferences[RECENT_PLACES]
?: "" ?: ""
} }
val distanceModeFlow: Flow<Int> = val distanceModeFlow: Flow<Int> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.DISTANCE_MODE] preferences[DISTANCE_MODE]
?: 0 ?: 0
} }
val guidanceAudioFlow: Flow<Int> = val guidanceAudioFlow: Flow<Int> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.GUIDANCE_AUDIO] preferences[GUIDANCE_AUDIO]
?: 0 ?: 0
} }
val trafficFlow: Flow<Boolean> = val trafficFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.TRAFFIC] == true preferences[TRAFFIC] == true
} }
val tripSuggestionFlow: Flow<Boolean> = val tripSuggestionFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.TRIP_SUGGESTION] == true preferences[TRIP_SUGGESTION] == true
} }
val engineTypeFlow: Flow<Int> = val engineTypeFlow: Flow<Int> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.ENGINE_TYPE] preferences[ENGINE_TYPE]
?: EngineType.COMBUSTION.ordinal ?: EngineType.COMBUSTION.ordinal
} }
val alternativeRoutesFlow: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[ALTERNATIVE_ROUTES] == true
}
// Save values // Save values
suspend fun setShow3D(enabled: Boolean) { suspend fun setShow3D(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.SHOW_3D] = enabled preferences[SHOW_3D] = enabled
} }
} }
suspend fun setDarkMode(mode: Int) { suspend fun setDarkMode(mode: Int) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.DARK_MODE] = mode prefs[DARK_MODE] = mode
} }
} }
suspend fun setAvoidMotorway(enabled: Boolean) { suspend fun setAvoidMotorway(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.AVOID_MOTORWAY] = enabled preferences[AVOID_MOTORWAY] = enabled
} }
} }
suspend fun setAvoidTollway(enabled: Boolean) { suspend fun setAvoidTollway(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.AVOID_TOLLWAY] = enabled preferences[AVOID_TOLLWAY] = enabled
} }
} }
suspend fun setAvoidFerry(enabled: Boolean) { suspend fun setAvoidFerry(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.AVOID_FERRY] = enabled preferences[AVOID_FERRY] = enabled
} }
} }
suspend fun setCarLocation(enabled: Boolean) { suspend fun setCarLocation(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.CAR_LOCATION] = enabled preferences[CAR_LOCATION] = enabled
} }
} }
suspend fun setRoutingEngine(mode: Int) { suspend fun setRoutingEngine(mode: Int) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.ROUTING_ENGINE] = mode prefs[ROUTING_ENGINE] = mode
} }
} }
suspend fun setLastRoute(route: String) { suspend fun setLastRoute(route: String) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.LAST_ROUTE] = route prefs[LAST_ROUTE] = route
} }
} }
suspend fun setTomtomApiKey(apiKey: String) { suspend fun setTomtomApiKey(apiKey: String) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.TOMTOM_APIKEY] = apiKey prefs[TOMTOM_APIKEY] = apiKey
} }
} }
suspend fun setRecentPlaces(apiKey: String) { suspend fun setRecentPlaces(apiKey: String) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.RECENT_PLACES] = apiKey prefs[RECENT_PLACES] = apiKey
} }
} }
suspend fun setDistanceMode(mode: Int) { suspend fun setDistanceMode(mode: Int) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.DISTANCE_MODE] = mode prefs[DISTANCE_MODE] = mode
} }
} }
suspend fun setGuidanceAudio(mode: Int) { suspend fun setGuidanceAudio(mode: Int) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.GUIDANCE_AUDIO] = mode prefs[GUIDANCE_AUDIO] = mode
} }
} }
suspend fun setTraffic(enabled: Boolean) { suspend fun setTraffic(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.TRAFFIC] = enabled preferences[TRAFFIC] = enabled
} }
} }
suspend fun setTripSuggestion(enabled: Boolean) { suspend fun setTripSuggestion(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.TRIP_SUGGESTION] = enabled preferences[TRIP_SUGGESTION] = enabled
} }
} }
suspend fun setEngineType(mode: Int) { suspend fun setEngineType(mode: Int) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.ENGINE_TYPE] = mode prefs[ENGINE_TYPE] = mode
} }
} }
suspend fun setAlternativeRoutes(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[ALTERNATIVE_ROUTES] = enabled
}
}
} }
@@ -13,7 +13,7 @@ class OsrmRepository : NavigationRepository() {
override fun getRoute( override fun getRoute(
context: Context, context: Context,
currentLocation: Location, currentLocation: Location,
location: Location, location: List<Location>,
carOrientation: Float, carOrientation: Float,
searchFilter: SearchFilter searchFilter: SearchFilter
): String { ): String {
@@ -28,7 +28,7 @@ class OsrmRepository : NavigationRepository() {
if (searchFilter.avoidFerry) { if (searchFilter.avoidFerry) {
exclude = "$exclude&exclude=ferry" exclude = "$exclude&exclude=ferry"
} }
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true&alternatives=false" val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.first().longitude},${location.first().latitude}?steps=true&alternatives=false"
return fetchUrl(routeUrl + routeLocation + exclude, true) return fetchUrl(routeUrl + routeLocation + exclude, true)
} }
@@ -69,7 +69,7 @@ class OsrmRoute {
steps.add(step) steps.add(step)
stepIndex += 1 stepIndex += 1
} }
legs.add(Leg(steps)) legs.add(Leg(steps, summary))
} }
val routeGeoJson = createLineStringCollection(waypoints) val routeGeoJson = createLineStringCollection(waypoints)
val centerLocation = createCenterLocation(createLineStringCollection(waypoints)) val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
@@ -1,5 +1,9 @@
package com.kouros.navigation.data.route package com.kouros.navigation.data.route
import android.location.Location
import com.kouros.navigation.utils.location
data class Leg( data class Leg(
var steps : List<Step> = arrayListOf(), var steps : List<Step> = arrayListOf(),
val summary: Summary,
) )
@@ -3,13 +3,67 @@ package com.kouros.navigation.data.route
import android.location.Location import android.location.Location
data class Maneuver( data class Maneuver(
val bearingBefore : Int = 0, val bearingBefore: Int = 0,
val bearingAfter : Int = 0, val bearingAfter: Int = 0,
val type: Int = 0, val type: Int = 0,
val waypoints: List<List<Double>>, val waypoints: List<List<Double>>,
val location: Location, val location: Location,
val exit: Int = 0, val exit: Int = 0,
val street: String = "", val street: String = "",
val message: String = "", val message: String = "",
val pointIndex : Int = 0, val pointIndex: Int = 0,
) )
enum class ManeuverType(val value: Int) {
TYPE_UNKNOWN(0),
TYPE_DEPART(1),
TYPE_NAME_CHANGE(2),
TYPE_KEEP_LEFT(3),
TYPE_KEEP_RIGHT(4),
TYPE_TURN_SLIGHT_LEFT(5),
TYPE_TURN_SLIGHT_RIGHT(6),
TYPE_TURN_NORMAL_LEFT(7),
TYPE_TURN_NORMAL_RIGHT(8),
TYPE_TURN_SHARP_LEFT(9),
TYPE_TURN_SHARP_RIGHT(10),
TYPE_U_TURN_LEFT(11),
TYPE_U_TURN_RIGHT(12),
TYPE_ON_RAMP_SLIGHT_LEFT(13),
TYPE_ON_RAMP_SLIGHT_RIGHT(14),
TYPE_ON_RAMP_NORMAL_LEFT(15),
TYPE_ON_RAMP_NORMAL_RIGHT(16),
TYPE_ON_RAMP_SHARP_LEFT(17),
TYPE_ON_RAMP_SHARP_RIGHT(18),
TYPE_ON_RAMP_U_TURN_LEFT(19),
TYPE_ON_RAMP_U_TURN_RIGHT(20),
TYPE_OFF_RAMP_SLIGHT_LEFT(21),
TYPE_OFF_RAMP_SLIGHT_RIGHT(22),
TYPE_OFF_RAMP_NORMAL_LEFT(23),
TYPE_OFF_RAMP_NORMAL_RIGHT(24),
TYPE_FORK_LEFT(25),
TYPE_FORK_RIGHT(26),
TYPE_MERGE_LEFT(27),
TYPE_MERGE_RIGHT(28),
TYPE_MERGE_SIDE_UNSPECIFIED (29),
TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW (32),
TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE(33),
TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW(34),
TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE(35),
TYPE_STRAIGHT(36),
TYPE_FERRY_BOAT (37),
TYPE_FERRY_TRAIN (38),
TYPE_DESTINATION (39),
TYPE_DESTINATION_STRAIGHT (40),
TYPE_DESTINATION_LEFT(41),
TYPE_DESTINATION_RIGHT(42),
TYPE_ROUNDABOUT_ENTER_CW(43),
TYPE_ROUNDABOUT_EXIT_CW(44),
TYPE_ROUNDABOUT_ENTER_CCW(45),
TYPE_ROUNDABOUT_EXIT_CCW(46),
TYPE_FERRY_BOAT_LEFT(47),
TYPE_FERRY_BOAT_RIGHT(48),
TYPE_FERRY_TRAIN_LEFT(49),
TYPE_FERRY_TRAIN_RIGHT(50),
TYPE_WAYPOINT_RIGHT(51),
TYPE_WAYPOINT_LEFT(52),
}
@@ -1,5 +0,0 @@
package com.kouros.navigation.data.tomtom
data class Cause(
val mainCauseCode: Int
)
@@ -1,10 +0,0 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Events (
@SerializedName("description" ) var description : String? = null
)
@@ -1,12 +0,0 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Features (
@SerializedName("type" ) var type : String? = null,
@SerializedName("properties" ) var properties : Properties? = Properties(),
@SerializedName("geometry" ) var geometry : Geometry? = Geometry()
)
@@ -1,11 +0,0 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Geometry (
@SerializedName("type" ) var type : String? = null,
@SerializedName("coordinates" ) var coordinates : List<List<Double>> = arrayListOf()
)
@@ -1,12 +0,0 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Incidents (
@SerializedName("type" ) var type : String? = null,
@SerializedName("properties" ) var properties : Properties? = Properties(),
@SerializedName("geometry" ) var geometry : Geometry? = Geometry()
)
@@ -1,11 +0,0 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Properties (
@SerializedName("iconCategory" ) var iconCategory : Int? = null,
@SerializedName("events" ) var events : ArrayList<Events> = arrayListOf()
)
@@ -30,7 +30,7 @@ class TomTomRepository : NavigationRepository() {
override fun getRoute( override fun getRoute(
context: Context, context: Context,
currentLocation: Location, currentLocation: Location,
location: Location, location: List<Location>,
carOrientation: Float, carOrientation: Float,
searchFilter: SearchFilter searchFilter: SearchFilter
): String { ): String {
@@ -55,17 +55,34 @@ class TomTomRepository : NavigationRepository() {
engineType = "electric" engineType = "electric"
} }
val repository = getSettingsRepository(context) val repository = getSettingsRepository(context)
val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() } val tomtomApiKey = runBlocking {
repository.tomTomApiKeyFlow.first()
}
val alternativeRoutes = runBlocking {
repository.alternativeRoutesFlow.first()
}
val altRoutes = if (alternativeRoutes) {
"&maxAlternatives=2"
} else {
"&maxAlternatives=0"
}
val currentLocale = Locale.getDefault() val currentLocale = Locale.getDefault()
val language = currentLocale.language + "-" + currentLocale.country val language = currentLocale.language + "-" + currentLocale.country
var loc = ""
location.forEach {
loc += if (loc.isEmpty()) {
"${it.latitude},${it.longitude}"
} else {
":${it.latitude},${it.longitude}"
}
}
val url = val url =
routeUrl + "${currentLocation.latitude},${currentLocation.longitude}:${location.latitude},${location.longitude}" + routeUrl + "${currentLocation.latitude},${currentLocation.longitude}:$loc" +
"/json?sectionType=traffic&report=effectiveSettings&routeType=eco" + "/json?sectionType=traffic&report=effectiveSettings&routeType=eco" +
"&traffic=true&avoid=unpavedRoads&travelMode=car" + "&traffic=true&avoid=unpavedRoads&travelMode=car" +
"&vehicleMaxSpeed=120&vehicleCommercial=false" + "&vehicleMaxSpeed=120&vehicleCommercial=false" +
"&instructionsType=text&language=$language&sectionType=lanes" + "&instructionsType=text&language=$language&sectionType=lanes" +
"&routeRepresentation=encodedPolyline" + "&routeRepresentation=encodedPolyline$altRoutes" +
"&maxAlternatives=2" +
"&vehicleEngineType=$engineType$filter&key=$tomtomApiKey" "&vehicleEngineType=$engineType$filter&key=$tomtomApiKey"
return fetchUrl( return fetchUrl(
url, url,
@@ -1,10 +1,13 @@
package com.kouros.navigation.data.tomtom package com.kouros.navigation.data.tomtom
import com.kouros.navigation.data.Route import com.kouros.navigation.data.Route
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.route.Intersection import com.kouros.navigation.data.route.Intersection
import com.kouros.navigation.data.route.Lane import com.kouros.navigation.data.route.Lane
import com.kouros.navigation.data.route.Leg import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.ManeuverType
import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.data.route.Step import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.utils.GeoUtils.createCenterLocation import com.kouros.navigation.utils.GeoUtils.createCenterLocation
@@ -17,12 +20,12 @@ import com.kouros.navigation.data.route.Maneuver as RouteManeuver
class TomTomRoute { class TomTomRoute {
fun mapToRoute(routeJson: TomTomResponse, builder: Route.Builder) { fun mapToRoute(routeJson: TomTomResponse, builder: Route.Builder) {
val routes = mutableListOf<com.kouros.navigation.data.route.Routes>() val routes = mutableListOf<Routes>()
routeJson.routes.forEach { route -> routeJson.routes.forEach { route ->
val waypoints = mutableListOf<List<Double>>() val waypoints = mutableListOf<List<Double>>()
val points = mutableListOf<List<Double>>()
val legs = mutableListOf<Leg>() val legs = mutableListOf<Leg>()
var stepIndex = 0 var stepIndex = 0
var points = listOf<List<Double>>()
val summary = Summary( val summary = Summary(
route.summary.travelTimeInSeconds.toDouble(), route.summary.travelTimeInSeconds.toDouble(),
route.summary.lengthInMeters.toDouble(), route.summary.lengthInMeters.toDouble(),
@@ -30,89 +33,91 @@ class TomTomRoute {
route.summary.trafficLengthInMeters.toDouble() route.summary.trafficLengthInMeters.toDouble()
) )
route.legs.forEach { leg -> route.legs.forEach { leg ->
points = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision) val p = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision)
waypoints.addAll(points) points.addAll(p)
waypoints.addAll(p)
} }
var stepDistance = 0.0 route.legs.forEach { leg ->
var stepDuration = 0.0 var stepDistance = 0.0
val steps = mutableListOf<Step>() var stepDuration = 0.0
var lastPointIndex = 0 val steps = mutableListOf<Step>()
for (index in 1..<route.guidance.instructions.size) { val summary = Summary(
val lastInstruction = route.guidance.instructions[index - 1] leg.summary.travelTimeInSeconds.toDouble(),
val instruction = route.guidance.instructions[index] leg.summary.lengthInMeters.toDouble(),
val street = lastInstruction.street ?: "" leg.summary.trafficDelayInSeconds.toDouble(),
val maneuverStreet = instruction.street ?: "" leg.summary.trafficLengthInMeters.toDouble()
val maneuver = RouteManeuver(
bearingBefore = 0,
bearingAfter = 0,
type = convertType(instruction.maneuver),
waypoints = points.subList(
lastPointIndex,
instruction.pointIndex + 1,
),
exit = exitNumber(instruction),
location = location(
instruction.point.longitude, instruction.point.latitude
),
street = maneuverStreet,
message = instruction.message,
pointIndex = instruction.pointIndex
) )
var lastPointIndex = 0
for (index in 1..<route.guidance.instructions.size) {
val lastInstruction = route.guidance.instructions[index - 1]
val instruction = route.guidance.instructions[index]
val street = lastInstruction.street ?: ""
val maneuverStreet = instruction.street ?: ""
val maneuver = RouteManeuver(
bearingBefore = 0,
bearingAfter = 0,
type = convertType(instruction.maneuver),
waypoints = points.subList(
lastPointIndex,
instruction.pointIndex + 1,
),
exit = exitNumber(instruction),
location = location(
instruction.point.longitude, instruction.point.latitude
),
street = maneuverStreet,
message = instruction.message,
pointIndex = instruction.pointIndex
)
lastPointIndex = instruction.pointIndex lastPointIndex = instruction.pointIndex
val intersections = mutableListOf<Intersection>() val intersections = mutableListOf<Intersection>()
route.sections?.forEach { section -> route.sections?.forEach { section ->
if (section.sectionType == "LANES" && section.startPointIndex <= lastPointIndex if (section.sectionType == "LANES" && section.startPointIndex <= lastPointIndex
&& section.endPointIndex >= lastPointIndex && section.endPointIndex >= lastPointIndex
) { ) {
val lanes = mutableListOf<Lane>() val lanes = mutableListOf<Lane>()
var startIndex = 0 var startIndex = 0
var lastLane: Lane? = null section.lanes?.forEach { itLane ->
section.lanes?.forEach { itLane -> val lane = Lane(
val lane = Lane( location = location(
location = location( waypoints[section.startPointIndex][0],
waypoints[section.startPointIndex][0], waypoints[section.startPointIndex][1]
waypoints[section.startPointIndex][1] ),
), valid = itLane.directions.first() == itLane.follow,
valid = itLane.directions.first() == itLane.follow, indications = itLane.directions,
indications = itLane.directions, startIndex = startIndex,
startIndex = startIndex, endIndex = section.endPointIndex
endIndex = section.endPointIndex )
) startIndex = section.startPointIndex
startIndex = section.startPointIndex
if (lastLane == null
|| (!(lastLane.valid && lane.valid
&& lastLane.indications == lane.indications))
) {
lanes.add(lane) lanes.add(lane)
} }
lastLane = lane intersections.add(Intersection(waypoints[startIndex], lanes))
} }
intersections.add(Intersection(waypoints[startIndex], lanes))
} }
stepDistance =
route.guidance.instructions[index].routeOffsetInMeters - stepDistance
stepDuration =
route.guidance.instructions[index].travelTimeInSeconds - stepDuration
val step = Step(
index = stepIndex,
street = street,
distance = stepDistance,
duration = stepDuration,
maneuver = maneuver,
intersection = intersections,
countryCode = lastInstruction.countryCode
)
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
steps.add(step)
stepIndex += 1
} }
stepDistance = legs.add(Leg(steps, summary))
route.guidance.instructions[index].routeOffsetInMeters - stepDistance
stepDuration =
route.guidance.instructions[index].travelTimeInSeconds - stepDuration
val step = Step(
index = stepIndex,
street = street,
distance = stepDistance,
duration = stepDuration,
maneuver = maneuver,
intersection = intersections,
countryCode = lastInstruction.countryCode
)
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
steps.add(step)
stepIndex += 1
} }
legs.add(Leg(steps))
val routeGeoJson = createLineStringCollection(waypoints) val routeGeoJson = createLineStringCollection(waypoints)
val centerLocation = createCenterLocation(createLineStringCollection(waypoints)) val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
val newRoute = com.kouros.navigation.data.route.Routes( val newRoute = Routes(
legs, legs,
summary, summary,
routeGeoJson, routeGeoJson,
@@ -130,75 +135,79 @@ class TomTomRoute {
var newType = 0 var newType = 0
when (type) { when (type) {
"DEPART" -> { "DEPART" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART newType = ManeuverType.TYPE_DEPART.value
} }
"ARRIVE" -> { "ARRIVE" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION newType = ManeuverType.TYPE_DESTINATION.value
} }
"ARRIVE_LEFT" -> { "ARRIVE_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_LEFT newType = ManeuverType.TYPE_DESTINATION_LEFT.value
} }
"ARRIVE_RIGHT" -> { "ARRIVE_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_RIGHT newType = ManeuverType.TYPE_DESTINATION_RIGHT.value
} }
"STRAIGHT", "FOLLOW" -> { "STRAIGHT", "FOLLOW" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT newType = ManeuverType.TYPE_STRAIGHT.value
} }
"KEEP_RIGHT" -> { "KEEP_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_KEEP_RIGHT newType = ManeuverType.TYPE_KEEP_RIGHT.value
} }
"BEAR_RIGHT" -> { "BEAR_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT newType = ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value
} }
"BEAR_LEFT" -> { "BEAR_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT newType = ManeuverType.TYPE_TURN_SLIGHT_LEFT.value
} }
"KEEP_LEFT" -> { "KEEP_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_KEEP_LEFT newType = ManeuverType.TYPE_KEEP_LEFT.value
} }
"TURN_LEFT" -> { "TURN_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT newType = ManeuverType.TYPE_TURN_NORMAL_LEFT.value
} }
"TURN_RIGHT" -> { "TURN_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT newType = ManeuverType.TYPE_TURN_NORMAL_RIGHT.value
} }
"SHARP_LEFT" -> { "SHARP_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SHARP_LEFT newType = ManeuverType.TYPE_TURN_SHARP_LEFT.value
} }
"SHARP_RIGHT" -> { "SHARP_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SHARP_RIGHT newType = ManeuverType.TYPE_TURN_SHARP_RIGHT.value
} }
"ROUNDABOUT_RIGHT", "ROUNDABOUT_CROSS" -> { "ROUNDABOUT_RIGHT", "ROUNDABOUT_CROSS" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CCW newType = ManeuverType.TYPE_ROUNDABOUT_ENTER_CCW.value
} }
"ROUNDABOUT_LEFT" -> { "ROUNDABOUT_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CW newType = ManeuverType.TYPE_ROUNDABOUT_ENTER_CW.value
} }
"MAKE_UTURN" -> { "MAKE_UTURN", "TRY_MAKE_UTURN" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_U_TURN_LEFT newType = ManeuverType.TYPE_U_TURN_LEFT.value
} }
"ENTER_MOTORWAY" -> { "ENTER_MOTORWAY" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_MERGE_LEFT newType = ManeuverType.TYPE_MERGE_LEFT.value
} }
"TAKE_EXIT" -> { "TAKE_EXIT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT newType = ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value
}
"WAYPOINT_RIGHT" -> {
newType = ManeuverType.TYPE_WAYPOINT_RIGHT.value
} }
} }
return newType return newType
@@ -1,12 +0,0 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Traffic (
//@SerializedName("incidents" ) var incidents : ArrayList<Incidents> = arrayListOf()
@SerializedName("type" ) var type : String = "",
@SerializedName("features" ) var features : ArrayList<Features> = arrayListOf()
)
@@ -1,6 +0,0 @@
package com.kouros.navigation.data.tomtom
data class TrafficData (
var traffic : Traffic ,
var trafficData: String = ""
)
@@ -16,7 +16,7 @@ class ValhallaRepository : NavigationRepository() {
override fun getRoute( override fun getRoute(
context: Context, context: Context,
currentLocation: Location, currentLocation: Location,
location: Location, location: List<Location>,
carOrientation: Float, carOrientation: Float,
searchFilter: SearchFilter searchFilter: SearchFilter
): String { ): String {
@@ -35,7 +35,7 @@ class ValhallaRepository : NavigationRepository() {
lon = currentLocation.longitude, lon = currentLocation.longitude,
searchFilter = exclude searchFilter = exclude
), ),
Locations(lat = location.latitude, lon = location.longitude, searchFilter = exclude) Locations(lat = location.first().latitude, lon = location.first().longitude, searchFilter = exclude)
) )
val valhallaLocation = ValhallaLocation( val valhallaLocation = ValhallaLocation(
locations = vLocation, locations = vLocation,
@@ -5,15 +5,13 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Matrix import android.graphics.Matrix
import androidx.annotation.DrawableRes import android.graphics.Paint
import androidx.car.app.model.CarIcon
import androidx.car.app.navigation.model.LaneDirection import androidx.car.app.navigation.model.LaneDirection
import androidx.car.app.navigation.model.Maneuver
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import java.util.Collections import com.kouros.navigation.data.route.ManeuverType
import java.util.Locale import java.util.Locale
class IconMapper { class IconMapper {
@@ -21,62 +19,66 @@ class IconMapper {
fun maneuverIcon(routeManeuverType: Int): Int { fun maneuverIcon(routeManeuverType: Int): Int {
var currentTurnIcon = R.drawable.ic_turn_name_change var currentTurnIcon = R.drawable.ic_turn_name_change
when (routeManeuverType) { when (routeManeuverType) {
Maneuver.TYPE_STRAIGHT -> { ManeuverType.TYPE_STRAIGHT.value -> {
currentTurnIcon = R.drawable.ic_turn_name_change currentTurnIcon = R.drawable.ic_turn_name_change
} }
Maneuver.TYPE_DESTINATION, ManeuverType.TYPE_DESTINATION.value,
Maneuver.TYPE_DESTINATION_RIGHT, ManeuverType.TYPE_DESTINATION_RIGHT.value,
Maneuver.TYPE_DESTINATION_LEFT, ManeuverType.TYPE_DESTINATION_LEFT.value,
Maneuver.TYPE_DESTINATION_STRAIGHT ManeuverType.TYPE_DESTINATION_STRAIGHT.value
-> { -> {
currentTurnIcon = R.drawable.ic_turn_destination currentTurnIcon = R.drawable.ic_turn_destination
} }
Maneuver.TYPE_TURN_NORMAL_RIGHT -> { ManeuverType.TYPE_TURN_NORMAL_RIGHT.value -> {
currentTurnIcon = R.drawable.ic_turn_normal_right currentTurnIcon = R.drawable.ic_turn_normal_right
} }
Maneuver.TYPE_TURN_NORMAL_LEFT -> { ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> {
currentTurnIcon = R.drawable.ic_turn_normal_left currentTurnIcon = R.drawable.ic_turn_normal_left
} }
Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT -> { ManeuverType.TYPE_OFF_RAMP_SLIGHT_RIGHT.value -> {
currentTurnIcon = R.drawable.ic_turn_slight_right currentTurnIcon = R.drawable.ic_turn_slight_right
} }
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> { ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value -> {
currentTurnIcon = R.drawable.ic_turn_slight_right currentTurnIcon = R.drawable.ic_turn_slight_right
} }
Maneuver.TYPE_KEEP_RIGHT -> { ManeuverType.TYPE_KEEP_RIGHT.value -> {
currentTurnIcon = R.drawable.ic_turn_name_change currentTurnIcon = R.drawable.ic_turn_name_change
} }
Maneuver.TYPE_KEEP_LEFT -> { ManeuverType.TYPE_KEEP_LEFT.value -> {
currentTurnIcon = R.drawable.ic_turn_name_change currentTurnIcon = R.drawable.ic_turn_name_change
} }
Maneuver.TYPE_ROUNDABOUT_ENTER_CCW -> { ManeuverType.TYPE_ROUNDABOUT_ENTER_CCW.value -> {
currentTurnIcon = R.drawable.ic_roundabout_ccw currentTurnIcon = R.drawable.ic_roundabout_ccw
} }
Maneuver.TYPE_ROUNDABOUT_EXIT_CCW -> { ManeuverType.TYPE_ROUNDABOUT_EXIT_CCW.value -> {
currentTurnIcon = R.drawable.ic_roundabout_ccw currentTurnIcon = R.drawable.ic_roundabout_ccw
} }
Maneuver.TYPE_U_TURN_LEFT -> { ManeuverType.TYPE_U_TURN_LEFT.value -> {
currentTurnIcon = R.drawable.ic_turn_u_turn_left currentTurnIcon = R.drawable.ic_turn_u_turn_left
} }
Maneuver.TYPE_U_TURN_RIGHT -> { ManeuverType.TYPE_U_TURN_RIGHT.value -> {
currentTurnIcon = R.drawable.ic_turn_u_turn_right currentTurnIcon = R.drawable.ic_turn_u_turn_right
} }
Maneuver.TYPE_MERGE_LEFT -> { ManeuverType.TYPE_MERGE_LEFT.value -> {
currentTurnIcon = R.drawable.ic_turn_merge_symmetrical currentTurnIcon = R.drawable.ic_turn_merge_symmetrical
} }
ManeuverType.TYPE_WAYPOINT_RIGHT.value -> {
currentTurnIcon = R.drawable.ic_turn_destination
}
} }
return currentTurnIcon return currentTurnIcon
} }
@@ -85,8 +87,8 @@ class IconMapper {
val laneDirection = when (direction.lowercase(Locale.getDefault())) { val laneDirection = when (direction.lowercase(Locale.getDefault())) {
"left_straight" -> { "left_straight" -> {
when (stepData.currentManeuverType) { when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> LaneDirection.SHAPE_NORMAL_LEFT
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT ManeuverType.TYPE_STRAIGHT.value -> LaneDirection.SHAPE_STRAIGHT
else else
-> LaneDirection.SHAPE_UNKNOWN -> LaneDirection.SHAPE_UNKNOWN
} }
@@ -94,7 +96,7 @@ class IconMapper {
"left" -> { "left" -> {
when (stepData.currentManeuverType) { when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> LaneDirection.SHAPE_NORMAL_LEFT
else else
-> LaneDirection.SHAPE_UNKNOWN -> LaneDirection.SHAPE_UNKNOWN
} }
@@ -102,9 +104,9 @@ class IconMapper {
"straight" -> { "straight" -> {
when (stepData.currentManeuverType) { when (stepData.currentManeuverType) {
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT ManeuverType.TYPE_STRAIGHT.value -> LaneDirection.SHAPE_STRAIGHT
Maneuver.TYPE_KEEP_LEFT -> LaneDirection.SHAPE_STRAIGHT ManeuverType.TYPE_KEEP_LEFT.value -> LaneDirection.SHAPE_STRAIGHT
Maneuver.TYPE_KEEP_RIGHT -> LaneDirection.SHAPE_STRAIGHT ManeuverType.TYPE_KEEP_RIGHT.value -> LaneDirection.SHAPE_STRAIGHT
else else
-> LaneDirection.SHAPE_UNKNOWN -> LaneDirection.SHAPE_UNKNOWN
} }
@@ -112,7 +114,7 @@ class IconMapper {
"right" -> { "right" -> {
when (stepData.currentManeuverType) { when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT ManeuverType.TYPE_TURN_NORMAL_RIGHT.value -> LaneDirection.SHAPE_NORMAL_RIGHT
else else
-> LaneDirection.SHAPE_UNKNOWN -> LaneDirection.SHAPE_UNKNOWN
} }
@@ -120,8 +122,8 @@ class IconMapper {
"right_straight" -> { "right_straight" -> {
when (stepData.currentManeuverType) { when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT ManeuverType.TYPE_TURN_NORMAL_RIGHT.value -> LaneDirection.SHAPE_NORMAL_RIGHT
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT ManeuverType.TYPE_STRAIGHT.value -> LaneDirection.SHAPE_STRAIGHT
else else
-> LaneDirection.SHAPE_UNKNOWN -> LaneDirection.SHAPE_UNKNOWN
} }
@@ -129,8 +131,8 @@ class IconMapper {
"left_slight", "slight_left" -> { "left_slight", "slight_left" -> {
when (stepData.currentManeuverType) { when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> LaneDirection.SHAPE_SLIGHT_LEFT
Maneuver.TYPE_KEEP_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT ManeuverType.TYPE_KEEP_LEFT.value -> LaneDirection.SHAPE_SLIGHT_LEFT
else else
-> LaneDirection.SHAPE_UNKNOWN -> LaneDirection.SHAPE_UNKNOWN
} }
@@ -138,8 +140,8 @@ class IconMapper {
"right_slight", "slight_right" -> { "right_slight", "slight_right" -> {
when (stepData.currentManeuverType) { when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value -> LaneDirection.SHAPE_NORMAL_RIGHT
Maneuver.TYPE_KEEP_RIGHT -> LaneDirection.SHAPE_SLIGHT_RIGHT ManeuverType.TYPE_KEEP_RIGHT.value -> LaneDirection.SHAPE_SLIGHT_RIGHT
else else
-> LaneDirection.SHAPE_UNKNOWN -> LaneDirection.SHAPE_UNKNOWN
} }
@@ -181,13 +183,17 @@ class IconMapper {
bitmaps.first().height, bitmaps.first().height,
bitmaps.first().config!! bitmaps.first().config!!
) )
val paint = Paint().apply {
color = android.graphics.Color.YELLOW
}
val canvas = Canvas(bmOverlay) val canvas = Canvas(bmOverlay)
canvas.drawBitmap(bitmaps.first(), matrix, null) canvas.drawBitmap(bitmaps.first(), matrix, paint)
var i = 0 var i = 0
bitmaps.forEach { bitmap -> bitmaps.forEach { bitmap ->
if (i > 0) { if (i > 0) {
matrix.setTranslate(i * 45F, 0F) matrix.setTranslate(i * 45F, 0F)
canvas.drawBitmap(bitmap, matrix, null) canvas.drawBitmap(bitmap, matrix, paint)
} }
i++ i++
} }
@@ -207,8 +213,8 @@ class IconMapper {
return when (direction) { return when (direction) {
"left_straight" -> { "left_straight" -> {
when (stepData.currentManeuverType) { when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_LEFT -> "left_o_straight_x" ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> "left_o_straight_x"
Maneuver.TYPE_STRAIGHT -> "left_x_straight_o" ManeuverType.TYPE_STRAIGHT.value -> "left_x_straight_o"
else else
-> "left_x_straight_x" -> "left_x_straight_x"
} }
@@ -216,29 +222,29 @@ class IconMapper {
"right_straight" -> { "right_straight" -> {
when (stepData.currentManeuverType) { when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_RIGHT -> "right_x_straight_x" ManeuverType.TYPE_TURN_NORMAL_RIGHT.value -> "right_x_straight_x"
Maneuver.TYPE_STRAIGHT -> "right_x_straight_o" ManeuverType.TYPE_STRAIGHT.value -> "right_x_straight_o"
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> "right_o_straight_o" ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value -> "right_o_straight_o"
else else
-> "right_x_straight_x" -> "right_x_straight_x"
} }
} }
"right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_RIGHT) "${direction}_o" else "${direction}_x" "right" -> if (stepData.currentManeuverType == ManeuverType.TYPE_TURN_NORMAL_RIGHT.value) "${direction}_o" else "${direction}_x"
"left" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_LEFT) "${direction}_o" else "${direction}_x" "left" -> if (stepData.currentManeuverType == ManeuverType.TYPE_TURN_NORMAL_LEFT.value) "${direction}_o" else "${direction}_x"
"straight" -> if (stepData.currentManeuverType == Maneuver.TYPE_STRAIGHT "straight" -> if (stepData.currentManeuverType == ManeuverType.TYPE_STRAIGHT.value
|| stepData.currentManeuverType == Maneuver.TYPE_KEEP_LEFT || stepData.currentManeuverType == ManeuverType.TYPE_KEEP_LEFT.value
|| stepData.currentManeuverType == Maneuver.TYPE_KEEP_RIGHT || stepData.currentManeuverType == ManeuverType.TYPE_KEEP_RIGHT.value
) "${direction}_o" else "${direction}_x" ) "${direction}_o" else "${direction}_x"
"right_slight", "slight_right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_RIGHT "right_slight", "slight_right" -> if (stepData.currentManeuverType == ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value
|| stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_RIGHT || stepData.currentManeuverType == ManeuverType.TYPE_TURN_NORMAL_RIGHT.value
|| stepData.currentManeuverType == Maneuver.TYPE_KEEP_RIGHT || stepData.currentManeuverType == ManeuverType.TYPE_KEEP_RIGHT.value
) "slight_right_o" else "slight_right_x" ) "slight_right_o" else "slight_right_x"
"left_slight", "slight_left" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_LEFT "left_slight", "slight_left" -> if (stepData.currentManeuverType == ManeuverType.TYPE_TURN_SLIGHT_LEFT.value
|| stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_LEFT || stepData.currentManeuverType == ManeuverType.TYPE_TURN_NORMAL_LEFT.value
|| stepData.currentManeuverType == Maneuver.TYPE_KEEP_LEFT || stepData.currentManeuverType == ManeuverType.TYPE_KEEP_LEFT.value
) "slight_left_o" else "slight_left_x" ) "slight_left_o" else "slight_left_x"
else -> { else -> {
@@ -3,15 +3,15 @@ package com.kouros.navigation.model
//import com.kouros.navigation.data.Preferences.boxStore //import com.kouros.navigation.data.Preferences.boxStore
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.util.Log
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Places import com.kouros.navigation.data.Places
@@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.FeatureCollection
import java.lang.reflect.Modifier
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
@@ -102,32 +103,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
MutableLiveData() MutableLiveData()
} }
val gson: com.google.gson.Gson = GsonBuilder().create()
/**
* Loads the most recent place from Preferences and calculates its distance.
* Posts the result to recentPlace LiveData if distance > 1km.
*/
fun loadRecentPlace(location: Location, carOrientation: Float, context: Context) {
viewModelScope.launch(Dispatchers.IO) {
try {
val settingsRepository = getSettingsRepository(context)
val recentPlaces = settingsRepository.recentPlacesFlow.first()
val gson = GsonBuilder().serializeNulls().create()
val places = gson.fromJson(recentPlaces, Places::class.java)
for (place in places.places.sortedBy { it.lastDate }) {
val plLocation = location(place.longitude, place.latitude)
val distance = plLocation.distanceTo(location)
place.distance = distance
if (place.distance > 200F) {
recentPlace.postValue(place)
return@launch
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
/** /**
* Loads all recent places from Preferences and calculates distances. * Loads all recent places from Preferences and calculates distances.
@@ -138,22 +114,20 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
try { try {
val settingsRepository = getSettingsRepository(context) val settingsRepository = getSettingsRepository(context)
val rp = settingsRepository.recentPlacesFlow.first() val rp = settingsRepository.recentPlacesFlow.first()
val gson = GsonBuilder().serializeNulls().create()
val places = gson.fromJson(rp, Places::class.java) val places = gson.fromJson(rp, Places::class.java)
val pl = mutableListOf<Place>() val pl = mutableListOf<Place>()
var id: Long = 0 var id: Long = 0
if (rp.isNotEmpty()) { if (rp.isNotEmpty()) {
for (place in places.places) { for (place in places.places) {
if (place.category.equals(Constants.RECENT) if (place.category == Constants.RECENT
|| place.category.equals(Constants.FAVORITES)) { || place.category == Constants.FAVORITES
) {
val plLocation = location(place.longitude, place.latitude) val plLocation = location(place.longitude, place.latitude)
if (place.latitude != 0.0) { if (place.latitude != 0.0) {
val distance = val distance =
repository.getRouteDistance( repository.getRouteDistance(
location, location,
plLocation, plLocation
carOrientation,
context
) )
place.distance = distance.toFloat() place.distance = distance.toFloat()
place.id = id place.id = id
@@ -177,7 +151,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
fun loadRoute( fun loadRoute(
context: Context, context: Context,
currentLocation: Location, currentLocation: Location,
destination: Location, destination: List<Location>,
carOrientation: Float carOrientation: Float
) { ) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@@ -211,9 +185,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
) )
if (data.isNotEmpty()) { if (data.isNotEmpty()) {
val trafficData = rebuildTraffic(data) val trafficData = rebuildTraffic(data)
traffic.postValue( if (trafficData.isNotEmpty()) {
trafficData traffic.postValue(
) trafficData
)
}
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -264,7 +240,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
repository.getRoute( repository.getRoute(
context, context,
currentLocation, currentLocation,
location, listOf(location),
carOrientation, carOrientation,
getSearchFilter(context) getSearchFilter(context)
) )
@@ -316,7 +292,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
var sortedList: List<SearchResult> var sortedList: List<SearchResult>
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val placesJson = repository.searchPlaces(search, location) val placesJson = repository.searchPlaces(search, location)
val gson = GsonBuilder().serializeNulls().create()
val places = gson.fromJson(placesJson, Search::class.java) val places = gson.fromJson(placesJson, Search::class.java)
val distPlaces = mutableListOf<SearchResult>() val distPlaces = mutableListOf<SearchResult>()
places.forEach { places.forEach {
@@ -341,7 +316,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val placesJson = repository.searchPlaces(search, location) val placesJson = repository.searchPlaces(search, location)
if (placesJson.isNotEmpty()) { if (placesJson.isNotEmpty()) {
val gson = GsonBuilder().serializeNulls().create()
val places = gson.fromJson(placesJson, Search::class.java) val places = gson.fromJson(placesJson, Search::class.java)
val distPlaces = mutableListOf<SearchResult>() val distPlaces = mutableListOf<SearchResult>()
places.forEach { places.forEach {
@@ -461,7 +435,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val places = mutableListOf<Place>() val places = mutableListOf<Place>()
val gson = GsonBuilder().serializeNulls().create()
val settingsRepository = getSettingsRepository(context) val settingsRepository = getSettingsRepository(context)
val rp = settingsRepository.recentPlacesFlow.first() val rp = settingsRepository.recentPlacesFlow.first()
var id: Long = 0 var id: Long = 0
@@ -471,6 +444,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
for (curPlace in recentPlaces) { for (curPlace in recentPlaces) {
if (curPlace.name != place.name || curPlace.category != place.category) { if (curPlace.name != place.name || curPlace.category != place.category) {
curPlace.id = id curPlace.id = id
curPlace.route = ""
places.add(curPlace) places.add(curPlace)
id += 1 id += 1
} }
@@ -478,6 +452,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
val current = LocalDateTime.now(ZoneOffset.UTC) val current = LocalDateTime.now(ZoneOffset.UTC)
place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond() place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond()
place.route = ""
places.add(place) places.add(place)
settingsRepository.setRecentPlaces(gson.toJson(Places(places))) settingsRepository.setRecentPlaces(gson.toJson(Places(places)))
} catch (e: Exception) { } catch (e: Exception) {
@@ -494,21 +469,12 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
deletePlace(context, place) deletePlace(context, place)
} }
/**
* Deletes a place from recent destinations in Preferences.
*/
fun deleteRecent(context: Context, place: Place) {
place.category = Constants.RECENT
deletePlace(context, place)
}
/** /**
* Deletes a place from Preferences matching name and category. * Deletes a place from Preferences matching name and category.
*/ */
fun deletePlace(context: Context, place: Place) { fun deletePlace(context: Context, place: Place) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val gson = GsonBuilder().serializeNulls().create()
val settingsRepository = getSettingsRepository(context) val settingsRepository = getSettingsRepository(context)
val rp = settingsRepository.recentPlacesFlow.first() val rp = settingsRepository.recentPlacesFlow.first()
val places = mutableListOf<Place>() val places = mutableListOf<Place>()
@@ -517,6 +483,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate } gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate }
for (curPlace in rPlaces) { for (curPlace in rPlaces) {
if (curPlace.name != place.name || curPlace.category != place.category) { if (curPlace.name != place.name || curPlace.category != place.category) {
curPlace.route = ""
places.add(curPlace) places.add(curPlace)
} }
} }
@@ -546,12 +513,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
* Loads recent places as Compose SnapshotStateList. * Loads recent places as Compose SnapshotStateList.
* @return SnapshotStateList of recent places * @return SnapshotStateList of recent places
*/ */
fun loadRecentPlace(context: Context): SnapshotStateList<Place?> { fun loadRecentPlaces(context: Context): SnapshotStateList<Place?> {
val pl = mutableListOf<Place>() val pl = mutableListOf<Place>()
val settingsRepository = getSettingsRepository(context) val settingsRepository = getSettingsRepository(context)
val rp = runBlocking { settingsRepository.recentPlacesFlow.first() } val rp = runBlocking { settingsRepository.recentPlacesFlow.first() }
if (rp.isNotEmpty()) { if (rp.isNotEmpty()) {
val gson = GsonBuilder().serializeNulls().create()
val recentPlaces = gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate } val recentPlaces = gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate }
for (place in recentPlaces) { for (place in recentPlaces) {
if (place.category == Constants.RECENT) { if (place.category == Constants.RECENT) {
@@ -76,48 +76,9 @@ open class RouteModel {
navState = navState.copy(lastLocation = navState.currentLocation) navState = navState.copy(lastLocation = navState.currentLocation)
} }
fun nextStep(): StepData { /*
val distanceToNextStep = routeCalculator.leftStepDistance() * Returns the current step
val nextStep = navState.route.nextStep(1) */
var streetName = nextStep.street
var maneuverType = currentStep.maneuver.type
if (distanceToNextStep < NEXT_STEP_THRESHOLD) {
streetName = nextStep.maneuver.street
maneuverType = nextStep.maneuver.type
}
val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType)
// Construct and return the final StepData object
return StepData(
instruction = streetName,
street = "",
leftStepDistance = distanceToNextStep,
currentManeuverType = maneuverType,
icon = maneuverIcon,
arrivalTime = routeCalculator.arrivalTime(),
leftDistance = routeCalculator.travelLeftDistance(),
exitNumber = nextStep.maneuver.exit,
message = nextStep.maneuver.message
)
}
private fun currentLanes(): List<Lane> {
var lanes = emptyList<Lane>()
if (navState.route.legs().isNotEmpty()) {
currentStep.intersection.forEach {
if (it.lane.isNotEmpty()) {
val distance =
navState.lastLocation.distanceTo(location(it.location[0], it.location[1]))
if (distance < NEXT_STEP_THRESHOLD) {
lanes = it.lane
return@forEach
}
}
}
}
return lanes
}
fun currentStep(): StepData { fun currentStep(): StepData {
val distanceToNextStep = routeCalculator.leftStepDistance() val distanceToNextStep = routeCalculator.leftStepDistance()
// Determine the maneuver type and corresponding icon // Determine the maneuver type and corresponding icon
@@ -148,6 +109,54 @@ open class RouteModel {
) )
} }
/*
* Returns the next step
*/
fun nextStep(): StepData {
val distanceToNextStep = routeCalculator.leftStepDistance()
val nextStep = navState.route.nextStep(1)
var streetName = nextStep.street
var maneuverType = currentStep.maneuver.type
if (distanceToNextStep < NEXT_STEP_THRESHOLD) {
streetName = nextStep.maneuver.street
maneuverType = nextStep.maneuver.type
}
val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType)
// Construct and return the final StepData object
return StepData(
instruction = streetName,
street = "",
leftStepDistance = distanceToNextStep,
currentManeuverType = maneuverType,
icon = maneuverIcon,
arrivalTime = routeCalculator.arrivalTime(),
leftDistance = routeCalculator.travelLeftDistance(),
exitNumber = nextStep.maneuver.exit,
message = nextStep.maneuver.message
)
}
/*
* Returns the current lanes
*/
private fun currentLanes(): List<Lane> {
var lanes = emptyList<Lane>()
if (navState.route.legs().isNotEmpty()) {
currentStep.intersection.forEach {
if (it.lane.isNotEmpty()) {
val distance =
navState.lastLocation.distanceTo(location(it.location[0], it.location[1]))
if (distance < NEXT_STEP_THRESHOLD) {
lanes = it.lane
return@forEach
}
}
}
}
return lanes
}
/** /**
* Checks for navigating * Checks for navigating
*/ */
@@ -1,10 +1,7 @@
package com.kouros.navigation.model package com.kouros.navigation.model
import androidx.datastore.preferences.core.edit
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.kouros.navigation.data.datastore.DataStoreManager.Companion.dataStore
import com.kouros.navigation.data.datastore.DataStoreManager.PreferencesKeys
import com.kouros.navigation.repository.SettingsRepository import com.kouros.navigation.repository.SettingsRepository
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
@@ -102,6 +99,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
0 0
) )
val alternativeRoutes = repository.alternativeRoutesFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
false
)
fun onShow3DChanged(enabled: Boolean) { fun onShow3DChanged(enabled: Boolean) {
viewModelScope.launch { repository.setShow3D(enabled) } viewModelScope.launch { repository.setShow3D(enabled) }
} }
@@ -159,4 +162,7 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
viewModelScope.launch { repository.setEngineType(mode) } viewModelScope.launch { repository.setEngineType(mode) }
} }
fun onAlternativeRoutes(enabled: Boolean) {
viewModelScope.launch { repository.setAlternativeRoutes(enabled) }
}
} }
@@ -1,5 +1,6 @@
package com.kouros.navigation.repository package com.kouros.navigation.repository
import android.util.Log
import com.kouros.navigation.data.datastore.DataStoreManager import com.kouros.navigation.data.datastore.DataStoreManager
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -50,6 +51,9 @@ class SettingsRepository(
val engineTypeFlow: Flow<Int> = val engineTypeFlow: Flow<Int> =
dataStoreManager.engineTypeFlow dataStoreManager.engineTypeFlow
val alternativeRoutesFlow: Flow<Boolean> =
dataStoreManager.alternativeRoutesFlow
suspend fun setShow3D(enabled: Boolean) { suspend fun setShow3D(enabled: Boolean) {
dataStoreManager.setShow3D(enabled) dataStoreManager.setShow3D(enabled)
} }
@@ -109,4 +113,8 @@ class SettingsRepository(
suspend fun setEngineType(mode: Int) { suspend fun setEngineType(mode: Int) {
dataStoreManager.setEngineType(mode) dataStoreManager.setEngineType(mode)
} }
suspend fun setAlternativeRoutes(enabled: Boolean) {
dataStoreManager.setAlternativeRoutes(enabled)
}
} }
@@ -3,7 +3,9 @@ package com.kouros.navigation.utils
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.util.Log
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TILT import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRepository import com.kouros.navigation.data.osrm.OsrmRepository
@@ -27,6 +29,7 @@ import kotlin.math.ln
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
import kotlin.time.toDuration import kotlin.time.toDuration
@@ -136,14 +139,17 @@ fun duration(
lastLocationUpdate: LocalDateTime lastLocationUpdate: LocalDateTime
): Duration { ): Duration {
if (preview) { if (preview) {
return 3.seconds return 10.milliseconds
} }
val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) { val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) {
2.seconds 2.seconds
} else { } else {
1.seconds val updateDuration = java.time.Duration.between(LocalDateTime.now(), lastLocationUpdate)
//val updateDuration = java.time.Duration.between(LocalDateTime.now(), lastLocationUpdate) if (updateDuration.toMillis().absoluteValue < 1000) {
//((updateDuration!!.toMillis().absoluteValue * 1.2).toDuration(DurationUnit.MILLISECONDS)) 2.seconds
} else {
((updateDuration!!.toMillis().absoluteValue * 1.8).toDuration(DurationUnit.MILLISECONDS))
}
} }
return cameraDuration return cameraDuration
} }
@@ -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="M240,600L240,640Q240,657 228.5,668.5Q217,680 200,680L160,680Q143,680 131.5,668.5Q120,657 120,640L120,320L204,80Q210,62 225.5,51Q241,40 260,40L700,40Q719,40 734.5,51Q750,62 756,80L840,320L840,640Q840,657 828.5,668.5Q817,680 800,680L760,680Q743,680 731.5,668.5Q720,657 720,640L720,600L240,600ZM232,240L728,240L686,120L274,120L232,240ZM200,320L200,320L200,520L200,520L200,320ZM300,480Q325,480 342.5,462.5Q360,445 360,420Q360,395 342.5,377.5Q325,360 300,360Q275,360 257.5,377.5Q240,395 240,420Q240,445 257.5,462.5Q275,480 300,480ZM660,480Q685,480 702.5,462.5Q720,445 720,420Q720,395 702.5,377.5Q685,360 660,360Q635,360 617.5,377.5Q600,395 600,420Q600,445 617.5,462.5Q635,480 660,480ZM520,920L280,800L440,800L440,720L680,840L520,840L520,920ZM200,520L760,520L760,320L200,320L200,520Z"/>
</vector>
@@ -0,0 +1,25 @@
<!--
Copyright 2021 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
http://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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.49,2 2,6.49 2,12s4.49,10 10,10 10,-4.49 10,-10S17.51,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM15,12c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3 1.34,-3 3,-3 3,1.34 3,3z"/>
</vector>
@@ -71,4 +71,5 @@
<string name="combustion">Combustion</string> <string name="combustion">Combustion</string>
<string name="electric">Electric</string> <string name="electric">Electric</string>
<string name="engine_type">Engine type</string> <string name="engine_type">Engine type</string>
<string name="alternative_routes">Alternative routes</string>
</resources> </resources>
@@ -55,4 +55,5 @@
<string name="combustion">Combustion</string> <string name="combustion">Combustion</string>
<string name="electric">Electric</string> <string name="electric">Electric</string>
<string name="engine_type">Engine type</string> <string name="engine_type">Engine type</string>
<string name="alternative_routes">Alternative routes</string>
</resources> </resources>
@@ -55,4 +55,5 @@
<string name="combustion">Combustion</string> <string name="combustion">Combustion</string>
<string name="electric">Electric</string> <string name="electric">Electric</string>
<string name="engine_type">Engine type</string> <string name="engine_type">Engine type</string>
<string name="alternative_routes">Alternative routes</string>
</resources> </resources>
@@ -58,4 +58,5 @@
<string name="combustion">Combustion</string> <string name="combustion">Combustion</string>
<string name="electric">Electric</string> <string name="electric">Electric</string>
<string name="engine_type">Engine type</string> <string name="engine_type">Engine type</string>
<string name="alternative_routes">Alternative routes</string>
</resources> </resources>
@@ -55,8 +55,8 @@ class RouteCalculatorTest {
) )
} }
private fun setupRoute(steps: List<Step>, currentStepIndex: Int = 0): Route { private fun setupRoute(steps: List<Step>, currentStepIndex: Int = 0, summary: Summary): Route {
val leg = Leg(steps = steps) val leg = Leg(steps = steps, summary)
val routes = Routes( val routes = Routes(
legs = listOf(leg), legs = listOf(leg),
summary = Summary(), summary = Summary(),
@@ -74,7 +74,7 @@ class RouteCalculatorTest {
fun `findStep updates currentStepIndex to step containing the nearest waypoint`() { fun `findStep updates currentStepIndex to step containing the nearest waypoint`() {
val step0 = createStep(index = 0, numWaypoints = 2) val step0 = createStep(index = 0, numWaypoints = 2)
val step1 = createStep(index = 1, numWaypoints = 2) val step1 = createStep(index = 1, numWaypoints = 2)
routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0, step1))) routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0, step1), summary = Summary()))
val mockLocation: Location = mock() val mockLocation: Location = mock()
// step0/wp0: 500F, step0/wp1: 400F, step1/wp0: 300F, step1/wp1: 8F // step0/wp0: 500F, step0/wp1: 400F, step1/wp0: 300F, step1/wp1: 8F
@@ -88,7 +88,7 @@ class RouteCalculatorTest {
@Test @Test
fun `findStep updates waypointIndex to the nearest waypoint within the step`() { fun `findStep updates waypointIndex to the nearest waypoint within the step`() {
val step0 = createStep(index = 0, numWaypoints = 3) val step0 = createStep(index = 0, numWaypoints = 3)
routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0))) routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0), summary = Summary()))
val mockLocation: Location = mock() val mockLocation: Location = mock()
// wp0: 100F, wp1: 30F (nearest), wp2: 80F // wp0: 100F, wp1: 30F (nearest), wp2: 80F
@@ -104,7 +104,7 @@ class RouteCalculatorTest {
val step0 = createStep(index = 0, numWaypoints = 2) val step0 = createStep(index = 0, numWaypoints = 2)
val step1 = createStep(index = 1, numWaypoints = 2) val step1 = createStep(index = 1, numWaypoints = 2)
routeModel.navState = routeModel.navState.copy( routeModel.navState = routeModel.navState.copy(
route = setupRoute(listOf(step0, step1), currentStepIndex = 1) route = setupRoute(listOf(step0, step1), currentStepIndex = 1, summary = Summary())
) )
val mockLocation: Location = mock() val mockLocation: Location = mock()
@@ -123,7 +123,7 @@ class RouteCalculatorTest {
val step1 = createStep(index = 1, numWaypoints = 2) val step1 = createStep(index = 1, numWaypoints = 2)
val step2 = createStep(index = 2, numWaypoints = 2) val step2 = createStep(index = 2, numWaypoints = 2)
routeModel.navState = routeModel.navState.copy( routeModel.navState = routeModel.navState.copy(
route = setupRoute(listOf(step0, step1, step2)) route = setupRoute(listOf(step0, step1, step2), summary = Summary())
) )
val mockLocation: Location = mock() val mockLocation: Location = mock()
@@ -147,7 +147,7 @@ class RouteCalculatorTest {
val step0 = createStep(index = 0, numWaypoints = 2, duration = 60.0, waypointIndex = 0) val step0 = createStep(index = 0, numWaypoints = 2, duration = 60.0, waypointIndex = 0)
val step1 = createStep(index = 1, numWaypoints = 2, duration = 120.0) val step1 = createStep(index = 1, numWaypoints = 2, duration = 120.0)
val step2 = createStep(index = 2, numWaypoints = 2, duration = 90.0) val step2 = createStep(index = 2, numWaypoints = 2, duration = 90.0)
routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0, step1, step2))) routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0, step1, step2), summary = Summary()))
val result = routeCalculator.travelLeftTime() val result = routeCalculator.travelLeftTime()
@@ -160,7 +160,7 @@ class RouteCalculatorTest {
// waypointIndex=2, waypoints=4 → percent = 100*(4-2)/4 = 50 → time = 80*50/100 = 40s // waypointIndex=2, waypoints=4 → percent = 100*(4-2)/4 = 50 → time = 80*50/100 = 40s
val step0 = createStep(index = 0, numWaypoints = 4, duration = 80.0, waypointIndex = 2) val step0 = createStep(index = 0, numWaypoints = 4, duration = 80.0, waypointIndex = 2)
val step1 = createStep(index = 1, numWaypoints = 2, duration = 40.0) val step1 = createStep(index = 1, numWaypoints = 2, duration = 40.0)
routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0, step1))) routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0, step1), summary = Summary()))
val result = routeCalculator.travelLeftTime() val result = routeCalculator.travelLeftTime()
@@ -172,7 +172,7 @@ class RouteCalculatorTest {
fun `travelLeftTime returns only future steps when at last step`() { fun `travelLeftTime returns only future steps when at last step`() {
val step0 = createStep(index = 0, numWaypoints = 2, duration = 60.0, waypointIndex = 1) val step0 = createStep(index = 0, numWaypoints = 2, duration = 60.0, waypointIndex = 1)
routeModel.navState = routeModel.navState.copy( routeModel.navState = routeModel.navState.copy(
route = setupRoute(listOf(step0), currentStepIndex = 0) route = setupRoute(listOf(step0), currentStepIndex = 0, summary = Summary())
) )
val result = routeCalculator.travelLeftTime() val result = routeCalculator.travelLeftTime()
@@ -189,7 +189,7 @@ class RouteCalculatorTest {
fun `leftStepDistance returns 0 when waypointIndex is at the last position`() { fun `leftStepDistance returns 0 when waypointIndex is at the last position`() {
// Loop range: waypointIndex..<waypoints.size-1 = 2..<2, which is empty // Loop range: waypointIndex..<waypoints.size-1 = 2..<2, which is empty
val step0 = createStep(index = 0, numWaypoints = 3, waypointIndex = 2) val step0 = createStep(index = 0, numWaypoints = 3, waypointIndex = 2)
routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0))) routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0), summary = Summary()))
val result = routeCalculator.leftStepDistance() val result = routeCalculator.leftStepDistance()
@@ -206,7 +206,7 @@ class RouteCalculatorTest {
val step0 = createStep(index = 0, numWaypoints = 2, distance = 100.0, waypointIndex = 1) val step0 = createStep(index = 0, numWaypoints = 2, distance = 100.0, waypointIndex = 1)
val step1 = createStep(index = 1, numWaypoints = 2, distance = 200.0) val step1 = createStep(index = 1, numWaypoints = 2, distance = 200.0)
val step2 = createStep(index = 2, numWaypoints = 2, distance = 150.0) val step2 = createStep(index = 2, numWaypoints = 2, distance = 150.0)
routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0, step1, step2))) routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0, step1, step2), summary = Summary()))
val result = routeCalculator.travelLeftDistance() val result = routeCalculator.travelLeftDistance()
@@ -217,7 +217,7 @@ class RouteCalculatorTest {
@Test @Test
fun `travelLeftDistance returns 0 when on last step at last waypoint`() { fun `travelLeftDistance returns 0 when on last step at last waypoint`() {
val step0 = createStep(index = 0, numWaypoints = 2, distance = 200.0, waypointIndex = 1) val step0 = createStep(index = 0, numWaypoints = 2, distance = 200.0, waypointIndex = 1)
routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0))) routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0), summary = Summary()))
val result = routeCalculator.travelLeftDistance() val result = routeCalculator.travelLeftDistance()
@@ -232,7 +232,8 @@ class RouteCalculatorTest {
fun `arrivalTime returns a timestamp roughly travelLeftTime seconds in the future`() { fun `arrivalTime returns a timestamp roughly travelLeftTime seconds in the future`() {
// step0: 2 waypoints at wp0 → 100% of 3600s duration // step0: 2 waypoints at wp0 → 100% of 3600s duration
val step0 = createStep(index = 0, numWaypoints = 2, duration = 3600.0, waypointIndex = 0) val step0 = createStep(index = 0, numWaypoints = 2, duration = 3600.0, waypointIndex = 0)
routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0))) val summary = Summary(duration = 3600.0)
routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0), summary = summary))
val before = System.currentTimeMillis() val before = System.currentTimeMillis()
val result = routeCalculator.arrivalTime() val result = routeCalculator.arrivalTime()
@@ -48,7 +48,7 @@ class RouteModelTest {
} }
private fun setupRoute(steps: List<Step>, currentStepIndex: Int = 0): Route { private fun setupRoute(steps: List<Step>, currentStepIndex: Int = 0): Route {
val leg = Leg(steps = steps) val leg = Leg(steps = steps, summary = Summary())
val routes = Routes( val routes = Routes(
legs = listOf(leg), legs = listOf(leg),
summary = Summary(), summary = Summary(),
+14 -10
View File
@@ -15,7 +15,7 @@ junitVersion = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
kotlinxSerializationJson = "1.10.0" kotlinxSerializationJson = "1.10.0"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.10.0"
composeBom = "2026.03.00" composeBom = "2026.03.01"
appcompat = "1.7.1" appcompat = "1.7.1"
material = "1.13.0" material = "1.13.0"
carApp = "1.7.0" carApp = "1.7.0"
@@ -26,27 +26,29 @@ mockitoKotlin = "6.3.0"
rules = "1.7.0" rules = "1.7.0"
runner = "1.7.0" runner = "1.7.0"
material3 = "1.4.0" material3 = "1.4.0"
runtimeLivedata = "1.10.5" runtimeLivedata = "1.10.6"
foundation = "1.10.5" foundation = "1.10.6"
maplibre-compose = "0.12.1" maplibre-compose = "0.12.1"
playServicesLocation = "21.3.0" playServicesLocation = "21.3.0"
runtime = "1.10.5" runtime = "1.10.6"
accompanist = "0.37.3" accompanist = "0.37.3"
uiVersion = "1.10.5" uiVersion = "1.10.6"
uiText = "1.10.5" uiText = "1.10.6"
navigationCompose = "2.9.7" navigationCompose = "2.9.7"
uiToolingPreview = "1.10.5" uiToolingPreview = "1.10.6"
uiTooling = "1.10.5" uiTooling = "1.10.6"
material3WindowSizeClass = "1.4.0" material3WindowSizeClass = "1.4.0"
uiGraphics = "1.10.5" uiGraphics = "1.10.6"
window = "1.5.1" window = "1.5.1"
foundationLayout = "1.10.5" foundationLayout = "1.10.6"
datastorePreferences = "1.2.1" datastorePreferences = "1.2.1"
datastoreCore = "1.2.1" datastoreCore = "1.2.1"
monitor = "1.8.0" monitor = "1.8.0"
robolectric = "4.16.1" robolectric = "4.16.1"
truth = "1.4.5" truth = "1.4.5"
testCore = "1.7.0" testCore = "1.7.0"
archCoreTesting = "2.2.0"
kotlinxCoroutinesTest = "1.10.1"
[libraries] [libraries]
android-gpx-parser = { module = "com.github.ticofab:android-gpx-parser", version.ref = "androidGpxParser" } android-gpx-parser = { module = "com.github.ticofab:android-gpx-parser", version.ref = "androidGpxParser" }
@@ -98,6 +100,8 @@ androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "m
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
google-truth = { module = "com.google.truth:truth", version.ref = "truth" } google-truth = { module = "com.google.truth:truth", version.ref = "truth" }
androidx-test-core = { module = "androidx.test:core", version.ref = "testCore" } androidx-test-core = { module = "androidx.test:core", version.ref = "testCore" }
androidx-arch-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "archCoreTesting" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }