Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b99ebfd36f | ||
|
|
69b27d3b6c | ||
|
|
8b886c36b1 | ||
|
|
2ce079a7c1 | ||
|
|
8af2d3ad0b | ||
|
|
1d67b3cc06 | ||
|
|
a4227c80d3 | ||
|
|
757c4c8d8d | ||
|
|
24173412e8 | ||
|
|
52f8dec2e6 | ||
|
|
6838ad09c4 |
@@ -17,8 +17,8 @@ android {
|
|||||||
applicationId = "com.kouros.navigation"
|
applicationId = "com.kouros.navigation"
|
||||||
minSdk = 33
|
minSdk = 33
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 87
|
versionCode = 91
|
||||||
versionName = "0.2.0.87"
|
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)
|
||||||
|
|||||||
@@ -45,6 +45,12 @@
|
|||||||
android:foregroundServiceType="location"
|
android:foregroundServiceType="location"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
</service>
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".car.navigation.NavigationService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:foregroundServiceType="location"
|
||||||
|
android:exported="true">
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.app.automotive)
|
implementation(libs.androidx.app.automotive)
|
||||||
implementation(libs.androidx.car.app)
|
|
||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.androidx.runtime.livedata)
|
implementation(libs.androidx.runtime.livedata)
|
||||||
implementation(project(":common:car"))
|
implementation(project(":common:car"))
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960"
|
android:viewportHeight="960"
|
||||||
android:tint="#1A7416">
|
android:tint="#1A7416">
|
||||||
<group android:scaleX="0.7888"
|
<group android:scaleX="0.58"
|
||||||
android:scaleY="0.7888"
|
android:scaleY="0.58"
|
||||||
android:translateX="101.376"
|
android:translateX="201.6"
|
||||||
android:translateY="101.376">
|
android:translateY="201.6">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M319,680L480,607L641,680L656,665L480,240L304,665L319,680ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
android:pathData="M319,680L480,607L641,680L656,665L480,240L304,665L319,680ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.8 KiB |
@@ -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,46 +38,74 @@ import com.kouros.data.R
|
|||||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||||
import com.kouros.navigation.car.screen.NavigationListener
|
import com.kouros.navigation.car.screen.NavigationListener
|
||||||
import com.kouros.navigation.car.screen.NavigationScreen
|
import com.kouros.navigation.car.screen.NavigationScreen
|
||||||
|
import com.kouros.navigation.data.Constants
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
|
import com.kouros.navigation.data.datastore.DataStoreManager.PreferencesKeys.CAR_LOCATION
|
||||||
|
import com.kouros.navigation.data.datastore.dataStore
|
||||||
|
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||||
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/** Session class for the Navigation sample app. */
|
/** Session class for the Navigation sample app. */
|
||||||
internal class ClusterSession : Session(), NavigationListener {
|
internal class ClusterSession : CarSession(), NavigationListener {
|
||||||
var mNavigationScreen: NavigationScreen? = null
|
lateinit var mNavigationScreen: NavigationScreen
|
||||||
|
|
||||||
var mNavigationCarSurface: SurfaceRenderer? = null
|
lateinit var surfaceRenderer: SurfaceRenderer
|
||||||
|
|
||||||
var mSettingsAction: Action? = null
|
|
||||||
|
|
||||||
var routeModel = RouteCarModel()
|
var routeModel = RouteCarModel()
|
||||||
|
|
||||||
lateinit var viewModelStoreOwner: ViewModelStoreOwner
|
lateinit var viewModelStoreOwner: ViewModelStoreOwner
|
||||||
|
|
||||||
|
lateinit var navigationViewModel: NavigationViewModel
|
||||||
|
|
||||||
|
lateinit var deviceLocationManager: DeviceLocationManager
|
||||||
|
|
||||||
|
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
||||||
|
override fun onPause(owner: LifecycleOwner) {
|
||||||
|
Log.d(Constants.TAG, "NavigationSession paused")
|
||||||
|
super.onPause(owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
|
Log.d(Constants.TAG, "NavigationSession resumed")
|
||||||
|
super.onResume(owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
if (::deviceLocationManager.isInitialized) {
|
||||||
|
deviceLocationManager.stopLocationUpdates()
|
||||||
|
}
|
||||||
|
carContext
|
||||||
|
.stopService(
|
||||||
|
Intent(
|
||||||
|
carContext,
|
||||||
|
NavigationNotificationService::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
lifecycle.addObserver(lifecycleObserver)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateScreen(intent: Intent): Screen {
|
override fun onCreateScreen(intent: Intent): Screen {
|
||||||
Log.i(TAG, "In onCreateScreen()")
|
Log.i(TAG, "In onCreateScreen()")
|
||||||
|
|
||||||
setupViewModelStore()
|
setupViewModelStore()
|
||||||
mSettingsAction =
|
|
||||||
Action.Builder()
|
|
||||||
.setIcon(
|
|
||||||
CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
carContext, R.drawable.alt_route_48px
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.setOnClickListener(
|
|
||||||
OnClickListener {})
|
|
||||||
.build()
|
|
||||||
|
|
||||||
mNavigationCarSurface = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner)
|
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
|
||||||
|
navigationViewModel = NavigationViewModel(TomTomRepository())
|
||||||
// mNavigationScreen =
|
mNavigationScreen =
|
||||||
// new NavigationScreen(getCarContext(), mSettingsAction, this, mNavigationCarSurface);
|
NavigationScreen(carContext, surfaceRenderer, this, navigationViewModel)
|
||||||
val action = intent.action
|
val action = intent.action
|
||||||
if (CarContext.ACTION_NAVIGATE == action) {
|
if (CarContext.ACTION_NAVIGATE == action) {
|
||||||
|
Log.i(TAG, "In onCreateScreen() Navigation intent")
|
||||||
CarToast.makeText(
|
CarToast.makeText(
|
||||||
carContext,
|
carContext,
|
||||||
"Navigation intent: " + intent.dataString,
|
"Navigation intent: " + intent.dataString,
|
||||||
@@ -81,8 +113,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) {
|
||||||
@@ -104,6 +157,10 @@ internal class ClusterSession : Session(), NavigationListener {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun recalcRoute(destination: Place) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG: String = ClusterSession::class.java.getSimpleName()
|
val TAG: String = ClusterSession::class.java.getSimpleName()
|
||||||
}
|
}
|
||||||
@@ -121,4 +178,8 @@ internal class ClusterSession : Session(), NavigationListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun invalidateNavigationScreen() {
|
||||||
|
mNavigationScreen.invalidate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -35,7 +35,8 @@ class NavigationCarAppService : CarAppService() {
|
|||||||
return ClusterSession()
|
return ClusterSession()
|
||||||
} else {
|
} else {
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
return NavigationSession()
|
//return NavigationSession()
|
||||||
|
return NavigationServiceSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import androidx.car.app.CarContext
|
|||||||
import androidx.car.app.CarToast
|
import androidx.car.app.CarToast
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
import androidx.car.app.ScreenManager
|
import androidx.car.app.ScreenManager
|
||||||
import androidx.car.app.Session
|
|
||||||
import androidx.car.app.connection.CarConnection
|
import androidx.car.app.connection.CarConnection
|
||||||
import androidx.car.app.model.Distance
|
import androidx.car.app.model.Distance
|
||||||
import androidx.car.app.navigation.NavigationManager
|
import androidx.car.app.navigation.NavigationManager
|
||||||
@@ -53,6 +52,8 @@ import com.kouros.navigation.data.tomtom.TomTomRepository
|
|||||||
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import com.kouros.navigation.model.RouteModel
|
import com.kouros.navigation.model.RouteModel
|
||||||
|
import com.kouros.navigation.model.SettingsViewModel
|
||||||
|
import com.kouros.navigation.repository.SettingsRepository
|
||||||
import com.kouros.navigation.utils.GeoUtils
|
import com.kouros.navigation.utils.GeoUtils
|
||||||
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
||||||
@@ -74,7 +75,7 @@ import kotlin.math.absoluteValue
|
|||||||
* car hardware sensors, routing engine selection, and screen navigation.
|
* car hardware sensors, routing engine selection, and screen navigation.
|
||||||
* Implements NavigationScreen.Listener for handling navigation events.
|
* Implements NavigationScreen.Listener for handling navigation events.
|
||||||
*/
|
*/
|
||||||
class NavigationSession : Session(), NavigationListener, NavigationObserverCallback {
|
class NavigationSession : CarSession(), NavigationListener, NavigationObserverCallback {
|
||||||
|
|
||||||
// Flag to enable/disable contact access feature
|
// Flag to enable/disable contact access feature
|
||||||
val useContacts = false
|
val useContacts = false
|
||||||
@@ -175,23 +176,12 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
|
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
|
||||||
lateinit var observerManager: NavigationObserverManager
|
lateinit var observerManager: NavigationObserverManager
|
||||||
|
|
||||||
val repository = getSettingsRepository(carContext)
|
lateinit var repository: SettingsRepository
|
||||||
|
|
||||||
val settingsViewModel = getSettingsViewModel(carContext)
|
lateinit var settingsViewModel: SettingsViewModel
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycle.addObserver(lifecycleObserver)
|
lifecycle.addObserver(lifecycleObserver)
|
||||||
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
|
||||||
routingEngine = it
|
|
||||||
})
|
|
||||||
|
|
||||||
repository.trafficFlow.asLiveData().observe(this, Observer {
|
|
||||||
showTraffic = it
|
|
||||||
})
|
|
||||||
repository.distanceModeFlow.asLiveData().observe(this, Observer {
|
|
||||||
distanceMode = it
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,6 +189,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
* Creates appropriate repository based on user selection.
|
* Creates appropriate repository based on user selection.
|
||||||
*/
|
*/
|
||||||
fun onRoutingEngineStateUpdated(routeEngine: Int) {
|
fun onRoutingEngineStateUpdated(routeEngine: Int) {
|
||||||
|
if (!::navigationViewModel.isInitialized || routeEngine != routingEngine) {
|
||||||
navigationViewModel = when (routeEngine) {
|
navigationViewModel = when (routeEngine) {
|
||||||
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
|
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
|
||||||
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
|
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
|
||||||
@@ -207,6 +198,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
observerManager = NavigationObserverManager(navigationViewModel, this)
|
observerManager = NavigationObserverManager(navigationViewModel, this)
|
||||||
observerManager.attachAllObservers(this)
|
observerManager.attachAllObservers(this)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when location permission is granted.
|
* Called when location permission is granted.
|
||||||
@@ -241,10 +233,11 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the initial screen for the session.
|
* Creates the initial screen for the session.
|
||||||
* Sets up ViewModel store, initializes components, checks permissions,
|
* Sets up ViewModel store, initializes settings, components, checks permissions,
|
||||||
* and returns appropriate starting screen.
|
* and returns appropriate starting screen.
|
||||||
*/
|
*/
|
||||||
override fun onCreateScreen(intent: Intent): Screen {
|
override fun onCreateScreen(intent: Intent): Screen {
|
||||||
|
initializeSettings()
|
||||||
setupViewModelStore()
|
setupViewModelStore()
|
||||||
initializeViewModels()
|
initializeViewModels()
|
||||||
initializeManagers()
|
initializeManagers()
|
||||||
@@ -252,6 +245,26 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
return checkPermissionsAndGetScreen()
|
return checkPermissionsAndGetScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initializes the settings repository and ViewModel.
|
||||||
|
*/
|
||||||
|
private fun initializeSettings() {
|
||||||
|
repository = getSettingsRepository(carContext)
|
||||||
|
settingsViewModel = getSettingsViewModel(carContext)
|
||||||
|
|
||||||
|
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
||||||
|
onRoutingEngineStateUpdated(it)
|
||||||
|
routingEngine = it
|
||||||
|
})
|
||||||
|
|
||||||
|
repository.trafficFlow.asLiveData().observe(this, Observer {
|
||||||
|
showTraffic = it
|
||||||
|
})
|
||||||
|
repository.distanceModeFlow.asLiveData().observe(this, Observer {
|
||||||
|
distanceMode = it
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up ViewModelStoreOwner and manages its lifecycle.
|
* Sets up ViewModelStoreOwner and manages its lifecycle.
|
||||||
*/
|
*/
|
||||||
@@ -306,7 +319,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner)
|
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
|
||||||
|
|
||||||
carSensorManager = CarSensorManager(
|
carSensorManager = CarSensorManager(
|
||||||
carContext = carContext,
|
carContext = carContext,
|
||||||
@@ -332,7 +345,6 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
|
|
||||||
|
|
||||||
textToSpeechManager = TextToSpeechManager(carContext)
|
textToSpeechManager = TextToSpeechManager(carContext)
|
||||||
val repository = getSettingsRepository(carContext)
|
|
||||||
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
|
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
|
||||||
guidanceAudio = it
|
guidanceAudio = it
|
||||||
})
|
})
|
||||||
@@ -378,6 +390,10 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
screenManager.push(navigationScreen)
|
screenManager.push(navigationScreen)
|
||||||
return RequestPermissionScreen(
|
return RequestPermissionScreen(
|
||||||
carContext,
|
carContext,
|
||||||
|
listOf(
|
||||||
|
permission.ACCESS_COARSE_LOCATION,
|
||||||
|
permission.ACCESS_FINE_LOCATION,
|
||||||
|
),
|
||||||
permissionCheckCallback = { screenManager.pop() }
|
permissionCheckCallback = { screenManager.pop() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -472,26 +488,27 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
val streetName = routeModel.currentStep().street
|
val streetName = routeModel.currentStep().street
|
||||||
val currentDate = LocalDateTime.now(ZoneOffset.UTC)
|
val currentDate = LocalDateTime.now(ZoneOffset.UTC)
|
||||||
|
|
||||||
snapLocation(location, streetName)
|
if (snapLocation(location, streetName)) {
|
||||||
|
|
||||||
checkTraffic(currentDate, location)
|
checkTraffic(currentDate, location)
|
||||||
updateSpeedCamera(location)
|
updateSpeedCamera(location)
|
||||||
checkRoute(currentDate, location)
|
checkRoute(currentDate, location)
|
||||||
updateNavigationScreen()
|
updateNavigationScreen()
|
||||||
checkArrival()
|
checkArrival()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the surface renderer with snapped location and street name.
|
* Updates the surface renderer with snapped location and street name.
|
||||||
* Checks if maximal route deviation is exceeded and reroutes if needed.
|
* Checks if maximal route deviation is exceeded and reroutes if needed.
|
||||||
*/
|
*/
|
||||||
private fun snapLocation(location: Location, streetName: String) {
|
private fun snapLocation(location: Location, streetName: String): Boolean {
|
||||||
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
|
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
|
||||||
val distance = location.distanceTo(snappedLocation)
|
val distance = location.distanceTo(snappedLocation)
|
||||||
when {
|
when {
|
||||||
distance > MAXIMAL_ROUTE_DEVIATION -> {
|
distance > MAXIMAL_ROUTE_DEVIATION -> {
|
||||||
stopNavigation()
|
stopNavigation()
|
||||||
navigationScreen.calculateNewRoute(routeModel.navState.destination)
|
navigationScreen.calculateNewRoute(routeModel.navState.destination)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
distance < MAXIMAL_SNAP_CORRECTION -> {
|
distance < MAXIMAL_SNAP_CORRECTION -> {
|
||||||
@@ -502,6 +519,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
surfaceRenderer.updateLocation(location, streetName)
|
surfaceRenderer.updateLocation(location, streetName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -509,7 +527,8 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
*/
|
*/
|
||||||
fun updateNavigationScreen() {
|
fun updateNavigationScreen() {
|
||||||
if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty()
|
if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty()
|
||||||
&& routeModel.navState.destination.street.isEmpty()) {
|
&& routeModel.navState.destination.street.isEmpty()
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val travelEstimateTrip = routeModel.travelEstimateTrip(carContext, distanceMode)
|
val travelEstimateTrip = routeModel.travelEstimateTrip(carContext, distanceMode)
|
||||||
@@ -578,6 +597,8 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
* 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
|
||||||
@@ -597,6 +618,8 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
* Called when user exits navigation or arrives at destination.
|
* Called when user exits navigation or arrives at destination.
|
||||||
*/
|
*/
|
||||||
override fun stopNavigation() {
|
override fun stopNavigation() {
|
||||||
|
Log.d(TAG, "stopNavigation")
|
||||||
|
surfaceRenderer.navigation = false
|
||||||
routeModel.stopNavigation()
|
routeModel.stopNavigation()
|
||||||
navigationManager.navigationEnded()
|
navigationManager.navigationEnded()
|
||||||
if (autoDriveEnabled) {
|
if (autoDriveEnabled) {
|
||||||
@@ -617,6 +640,19 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recalculates a route for the specified place.
|
||||||
|
*/
|
||||||
|
override fun recalcRoute(destination: Place) {
|
||||||
|
val destination = location(destination.longitude, destination.latitude)
|
||||||
|
navigationViewModel.loadRoute(
|
||||||
|
carContext,
|
||||||
|
surfaceRenderer.lastLocation,
|
||||||
|
listOf(destination),
|
||||||
|
surfaceRenderer.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
|
||||||
@@ -638,6 +674,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
* Starts navigation and invalidates the screen.
|
* Starts navigation and invalidates the screen.
|
||||||
*/
|
*/
|
||||||
override fun onRouteReceived(route: String) {
|
override fun onRouteReceived(route: String) {
|
||||||
|
Log.d(TAG, "onRouteReceived")
|
||||||
if (route.isNotEmpty()) {
|
if (route.isNotEmpty()) {
|
||||||
this.route = route
|
this.route = route
|
||||||
if (routeModel.isNavigating()) {
|
if (routeModel.isNavigating()) {
|
||||||
@@ -705,7 +742,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
coordinates.add(listOf(it.lon, it.lat))
|
coordinates.add(listOf(it.lon, it.lat))
|
||||||
}
|
}
|
||||||
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
|
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
|
||||||
surfaceRenderer.speedCamerasData.value = speedData
|
surfaceRenderer.speedCameraData.value = speedData
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -715,6 +752,9 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
surfaceRenderer.maxSpeed.value = speed
|
surfaceRenderer.maxSpeed.value = speed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRecentPlacesReceived(places: List<Place>) {
|
||||||
|
}
|
||||||
|
|
||||||
override fun invalidateScreen() {
|
override fun invalidateScreen() {
|
||||||
navigationScreen.invalidate()
|
navigationScreen.invalidate()
|
||||||
}
|
}
|
||||||
@@ -723,9 +763,23 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
* Loads a route to the specified place and sets it as the destination.
|
* Loads a route to the specified place and sets it as the destination.
|
||||||
*/
|
*/
|
||||||
override fun navigateToPlace(place: Place) {
|
override fun navigateToPlace(place: Place) {
|
||||||
|
Log.d(TAG, "navigateToPlace ${place.street}")
|
||||||
|
var prevDestination = Place()
|
||||||
|
if (surfaceRenderer.navigation) {
|
||||||
|
prevDestination = routeModel.navState.destination
|
||||||
|
stopNavigation()
|
||||||
|
}
|
||||||
val preview = place.route
|
val preview = place.route
|
||||||
navigationViewModel.previewRoute.value = ""
|
navigationViewModel.previewRoute.value = ""
|
||||||
val location = location(place.longitude, place.latitude)
|
val location = if (place.stopOver && prevDestination.latitude != 0.0) {
|
||||||
|
listOf(
|
||||||
|
location(place.longitude, place.latitude),
|
||||||
|
location(prevDestination.longitude, prevDestination.latitude)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(location(place.longitude, place.latitude))
|
||||||
|
}
|
||||||
|
|
||||||
navigationViewModel.saveRecent(carContext, place)
|
navigationViewModel.saveRecent(carContext, place)
|
||||||
routeModel.navState = routeModel.navState.copy(destination = place)
|
routeModel.navState = routeModel.navState.copy(destination = place)
|
||||||
if (preview.isEmpty()) {
|
if (preview.isEmpty()) {
|
||||||
@@ -813,12 +867,16 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
|||||||
navigationViewModel.loadRoute(
|
navigationViewModel.loadRoute(
|
||||||
carContext,
|
carContext,
|
||||||
location,
|
location,
|
||||||
destination,
|
listOf(destination),
|
||||||
surfaceRenderer.carOrientation
|
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"
|
||||||
|
|||||||
@@ -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,10 +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
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,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)
|
||||||
@@ -123,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
|
||||||
|
|
||||||
@@ -167,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)
|
||||||
@@ -234,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,10 +256,6 @@ class SurfaceRenderer(
|
|||||||
speed.value = 0F
|
speed.value = 0F
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBaseStyleStateUpdated(style: BaseStyle) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable function that renders the map and navigation UI.
|
* Composable function that renders the map and navigation UI.
|
||||||
* Observes various LiveData sources and updates the map accordingly.
|
* Observes various LiveData sources and updates the map accordingly.
|
||||||
@@ -271,11 +269,12 @@ class SurfaceRenderer(
|
|||||||
val position: CameraPosition? by cameraPosition.observeAsState()
|
val position: CameraPosition? by cameraPosition.observeAsState()
|
||||||
val route: String? by routeData.observeAsState()
|
val route: String? by routeData.observeAsState()
|
||||||
val traffic: Map<String, String>? by trafficData.observeAsState()
|
val traffic: Map<String, String>? by trafficData.observeAsState()
|
||||||
val speedCameras: String? by speedCamerasData.observeAsState()
|
val speedCamera: String? by speedCameraData.observeAsState()
|
||||||
val paddingValues = getPaddingValues(height, viewStyle)
|
val paddingValues = getPaddingValues(height, viewStyle)
|
||||||
val cameraState = cameraState(paddingValues, position, tilt)
|
val cameraState = cameraState(paddingValues, position, tilt)
|
||||||
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
|
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
|
||||||
val dark = darkMode == 1 || darkMode == 2 && carContext.isDarkMode
|
val dark = darkMode == DarkMode.DARK.ordinal
|
||||||
|
|| (darkMode == DarkMode.USE_CAR.ordinal && carContext.isDarkMode)
|
||||||
|
|
||||||
MapLibre(
|
MapLibre(
|
||||||
cameraState,
|
cameraState,
|
||||||
@@ -283,7 +282,7 @@ class SurfaceRenderer(
|
|||||||
route,
|
route,
|
||||||
traffic,
|
traffic,
|
||||||
viewStyle,
|
viewStyle,
|
||||||
speedCameras,
|
speedCamera,
|
||||||
showBuildings
|
showBuildings
|
||||||
)
|
)
|
||||||
ShowPosition(cameraState, position, paddingValues, dark)
|
ShowPosition(cameraState, position, paddingValues, dark)
|
||||||
@@ -337,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)
|
||||||
@@ -353,9 +351,9 @@ 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) {
|
if (viewStyle == ViewStyle.VIEW) {
|
||||||
tilt = calculateTilt(newZoom, tilt)
|
tilt = calculateTilt(newZoom, tilt)
|
||||||
@@ -374,6 +372,7 @@ class SurfaceRenderer(
|
|||||||
* 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, streetName: String) {
|
fun updateLocation(location: Location, streetName: String) {
|
||||||
|
Log.d(TAG, "updateLocation Surface $location $streetName")
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
street.value = streetName
|
street.value = streetName
|
||||||
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
||||||
@@ -427,7 +426,8 @@ class SurfaceRenderer(
|
|||||||
* Updates camera position with new bearing, zoom, and target.
|
* Updates camera position with new bearing, zoom, and target.
|
||||||
* Posts update to LiveData for UI observation.
|
* Posts update to LiveData for UI observation.
|
||||||
*/
|
*/
|
||||||
fun updateCameraPosition(bearing: Double = 0.0, zoom: Double, target: Position, tilt: Double) {
|
fun updateCameraPosition(bearing: Double = 0.0, zoom: Double = cameraPosition.value!!.zoom ,
|
||||||
|
target: Position, tilt: Double = 0.0) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
cameraPosition.postValue(
|
cameraPosition.postValue(
|
||||||
cameraPosition.value!!.copy(
|
cameraPosition.value!!.copy(
|
||||||
@@ -457,7 +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(
|
||||||
@@ -473,8 +473,10 @@ class SurfaceRenderer(
|
|||||||
* Calculates appropriate zoom
|
* Calculates appropriate zoom
|
||||||
*/
|
*/
|
||||||
fun setStandardView() {
|
fun setStandardView() {
|
||||||
viewStyle = ViewStyle.VIEW
|
if (!navigation) {
|
||||||
setRouteData("")
|
setRouteData("")
|
||||||
|
}
|
||||||
|
viewStyle = ViewStyle.VIEW
|
||||||
val zoom = calculateZoom(0.0)
|
val zoom = calculateZoom(0.0)
|
||||||
tilt = calculateTilt(zoom, tilt)
|
tilt = calculateTilt(zoom, tilt)
|
||||||
updateCameraPosition(
|
updateCameraPosition(
|
||||||
@@ -500,18 +502,6 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates car location from the connected car system.
|
|
||||||
* Only updates location when using OSRM routing engine.
|
|
||||||
*/
|
|
||||||
fun updateCarLocation(location: Location, streetName: String) {
|
|
||||||
val repository = getSettingsRepository(carContext)
|
|
||||||
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
|
||||||
if (routingEngine == RouteEngine.OSRM.ordinal) {
|
|
||||||
updateLocation(location, streetName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates current speed for display.
|
* Updates current speed for display.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -52,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
|
||||||
@@ -100,7 +102,8 @@ 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) {
|
||||||
@@ -146,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),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -367,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(
|
||||||
|
|||||||
@@ -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,7 @@ 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)
|
//currentSimulation(routeModel, lifecycleScope, updateLocation)
|
||||||
} else {
|
} else {
|
||||||
currentSimulation(routeModel, lifecycleScope, updateLocation)
|
currentSimulation(routeModel, lifecycleScope, updateLocation)
|
||||||
@@ -54,7 +57,7 @@ class Simulation {
|
|||||||
longitude = point[0]
|
longitude = point[0]
|
||||||
bearing = curBearing
|
bearing = curBearing
|
||||||
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
|
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
|
||||||
speed = 13.0f // ~50 km/h
|
speed = 5.0f
|
||||||
time = System.currentTimeMillis()
|
time = System.currentTimeMillis()
|
||||||
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
|
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
|
||||||
}
|
}
|
||||||
@@ -87,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()
|
||||||
@@ -134,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(
|
||||||
|
|||||||
@@ -16,4 +16,7 @@ interface NavigationListener {
|
|||||||
fun updateTrip(trip: Trip)
|
fun updateTrip(trip: Trip)
|
||||||
|
|
||||||
fun navigateToPlace(place: Place)
|
fun navigateToPlace(place: Place)
|
||||||
|
|
||||||
|
fun recalcRoute(destination: Place)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.kouros.navigation.car.screen
|
|||||||
|
|
||||||
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
|
||||||
@@ -19,6 +21,7 @@ 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.Step
|
import androidx.car.app.navigation.model.Step
|
||||||
import androidx.car.app.navigation.model.TravelEstimate
|
import androidx.car.app.navigation.model.TravelEstimate
|
||||||
@@ -32,12 +35,12 @@ import com.kouros.data.R
|
|||||||
import com.kouros.navigation.car.SurfaceRenderer
|
import com.kouros.navigation.car.SurfaceRenderer
|
||||||
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.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.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import com.kouros.navigation.utils.getSettingsRepository
|
import com.kouros.navigation.utils.getSettingsRepository
|
||||||
import com.kouros.navigation.utils.getSettingsViewModel
|
import com.kouros.navigation.utils.getSettingsViewModel
|
||||||
import com.kouros.navigation.utils.location
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -53,7 +56,6 @@ open class NavigationScreen(
|
|||||||
) : Screen(carContext) {
|
) : Screen(carContext) {
|
||||||
|
|
||||||
var recentPlaces = mutableListOf<Place>()
|
var recentPlaces = mutableListOf<Place>()
|
||||||
|
|
||||||
var recentPlace: Place = Place()
|
var recentPlace: Place = Place()
|
||||||
var navigationType = NavigationType.VIEW
|
var navigationType = NavigationType.VIEW
|
||||||
|
|
||||||
@@ -79,7 +81,10 @@ open class NavigationScreen(
|
|||||||
private lateinit var steps: MutableList<Step>
|
private lateinit var steps: MutableList<Step>
|
||||||
private var junctionImage: CarIcon? = null
|
private var junctionImage: CarIcon? = null
|
||||||
private var backGroundColor = CarColor.BLUE
|
private var backGroundColor = CarColor.BLUE
|
||||||
|
|
||||||
|
private var 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
|
||||||
@@ -94,9 +99,14 @@ open class NavigationScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
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.alternativeRoutesFlow.asLiveData().observe(this, Observer {
|
||||||
|
showAlternativeRoute = it
|
||||||
|
})
|
||||||
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
arrivalTimer?.cancel()
|
arrivalTimer?.cancel()
|
||||||
@@ -109,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,
|
||||||
@@ -117,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,
|
||||||
@@ -144,16 +155,17 @@ open class NavigationScreen(
|
|||||||
.setActionStrip(actionStripBuilder.build())
|
.setActionStrip(actionStripBuilder.build())
|
||||||
.setMapActionStrip(
|
.setMapActionStrip(
|
||||||
mapActionStrip(
|
mapActionStrip(
|
||||||
|
carContext,
|
||||||
surfaceRenderer.viewStyle,
|
surfaceRenderer.viewStyle,
|
||||||
{ zoomPlus() }, { zoomMinus() }, {
|
{ zoomPlus() }, { zoomMinus() }, {
|
||||||
createAction(
|
Action.Builder()
|
||||||
carContext = carContext, R.drawable.ic_pan_24,
|
.setIcon(createCarIcon(carContext, R.drawable.ic_recenter_24))
|
||||||
0,
|
.setFlags(0)
|
||||||
onClickAction = {
|
.setOnClickListener {
|
||||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
surfaceRenderer.setStandardView()
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
)
|
.build()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.setBackgroundColor(backGroundColor)
|
.setBackgroundColor(backGroundColor)
|
||||||
@@ -163,31 +175,33 @@ open class NavigationScreen(
|
|||||||
/**
|
/**
|
||||||
* Creates and returns a template for the default view state.
|
* Creates and returns a template for the default view state.
|
||||||
*/
|
*/
|
||||||
private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template {
|
private fun navigationView(actionStripBuilder: ActionStrip.Builder): Template {
|
||||||
return NavigationTemplate.Builder()
|
val mapActionStrip = mapActionStrip(
|
||||||
.setBackgroundColor(backGroundColor)
|
carContext,
|
||||||
.setActionStrip(actionStripBuilder.build())
|
|
||||||
.setMapActionStrip(
|
|
||||||
mapActionStrip(
|
|
||||||
surfaceRenderer.viewStyle,
|
surfaceRenderer.viewStyle,
|
||||||
{ zoomPlus() }, { zoomMinus() }, {
|
{ zoomPlus() }, { zoomMinus() }, {
|
||||||
createAction(
|
createAction(
|
||||||
carContext = carContext, R.drawable.ic_pan_24,
|
carContext = carContext, R.drawable.ic_recenter_24,
|
||||||
0,
|
0,
|
||||||
onClickAction = {
|
onClickAction = {
|
||||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||||
invalidate()
|
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) {}
|
||||||
@@ -198,13 +212,13 @@ open class NavigationScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
arrivalTimer?.start()
|
arrivalTimer?.start()
|
||||||
return navigationArrivedTemplate(actionStripBuilder)
|
return navigationArrived(actionStripBuilder)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and returns a NavigationTemplate specifically for when the destination is reached.
|
* Creates and returns a NavigationTemplate specifically for when the destination is reached.
|
||||||
*/
|
*/
|
||||||
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
|
fun navigationArrived(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
|
||||||
var street = ""
|
var street = ""
|
||||||
if (destinations.first().address != null) {
|
if (destinations.first().address != null) {
|
||||||
street = destinations.first().address.toString()
|
street = destinations.first().address.toString()
|
||||||
@@ -226,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
|
||||||
@@ -247,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(
|
||||||
@@ -291,6 +306,7 @@ open class NavigationScreen(
|
|||||||
.setContentTemplate(contentTemplate)
|
.setContentTemplate(contentTemplate)
|
||||||
.setActionStrip(
|
.setActionStrip(
|
||||||
mapActionStrip(
|
mapActionStrip(
|
||||||
|
carContext,
|
||||||
ViewStyle.VIEW,
|
ViewStyle.VIEW,
|
||||||
{ settingsAction() },
|
{ settingsAction() },
|
||||||
{
|
{
|
||||||
@@ -302,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
|
||||||
@@ -316,7 +332,7 @@ open class NavigationScreen(
|
|||||||
/**
|
/**
|
||||||
* Creates and returns a template for when the route is being recalculated.
|
* Creates and returns a template for when the route is being recalculated.
|
||||||
*/
|
*/
|
||||||
fun navigationRerouteTemplate(actionStripBuilder: ActionStrip.Builder): Template {
|
fun navigationReroute(actionStripBuilder: ActionStrip.Builder): Template {
|
||||||
return NavigationTemplate.Builder()
|
return NavigationTemplate.Builder()
|
||||||
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
|
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
|
||||||
.setActionStrip(actionStripBuilder.build())
|
.setActionStrip(actionStripBuilder.build())
|
||||||
@@ -329,10 +345,12 @@ open class NavigationScreen(
|
|||||||
*/
|
*/
|
||||||
fun getRoutingInfo(): RoutingInfo {
|
fun getRoutingInfo(): RoutingInfo {
|
||||||
val routingInfo = RoutingInfo.Builder()
|
val routingInfo = RoutingInfo.Builder()
|
||||||
.setCurrentStep(
|
if (steps.isNotEmpty()) {
|
||||||
|
routingInfo.setCurrentStep(
|
||||||
steps.first(),
|
steps.first(),
|
||||||
stepRemainingDistance
|
stepRemainingDistance
|
||||||
)
|
)
|
||||||
|
}
|
||||||
if (shouldShowNextStep && steps.size > 1) {
|
if (shouldShowNextStep && steps.size > 1) {
|
||||||
routingInfo.setNextStep(steps[1])
|
routingInfo.setNextStep(steps[1])
|
||||||
}
|
}
|
||||||
@@ -356,6 +374,7 @@ open class NavigationScreen(
|
|||||||
surfaceRenderer,
|
surfaceRenderer,
|
||||||
place,
|
place,
|
||||||
navigationViewModel,
|
navigationViewModel,
|
||||||
|
showAlternativeRoute
|
||||||
)
|
)
|
||||||
) { obj: Any? ->
|
) { obj: Any? ->
|
||||||
if (obj != null) {
|
if (obj != null) {
|
||||||
@@ -471,25 +490,13 @@ 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.
|
||||||
@@ -520,7 +527,6 @@ open class NavigationScreen(
|
|||||||
this.shouldShowLanes = shouldShowLanes
|
this.shouldShowLanes = shouldShowLanes
|
||||||
this.junctionImage = junctionImage
|
this.junctionImage = junctionImage
|
||||||
this.backGroundColor = backGroundColor
|
this.backGroundColor = backGroundColor
|
||||||
|
|
||||||
navigationType = NavigationType.NAVIGATION
|
navigationType = NavigationType.NAVIGATION
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.text.Spannable
|
|||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.CarToast
|
import androidx.car.app.CarToast
|
||||||
import androidx.car.app.OnScreenResultListener
|
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
import androidx.car.app.model.Action
|
import androidx.car.app.model.Action
|
||||||
import androidx.car.app.model.CarIcon
|
import androidx.car.app.model.CarIcon
|
||||||
@@ -28,7 +27,6 @@ import com.kouros.navigation.data.Constants.CONTACTS
|
|||||||
import com.kouros.navigation.data.Constants.FAVORITES
|
import com.kouros.navigation.data.Constants.FAVORITES
|
||||||
import com.kouros.navigation.data.Constants.RECENT
|
import com.kouros.navigation.data.Constants.RECENT
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.ViewStyle
|
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import com.kouros.navigation.utils.getSettingsRepository
|
import com.kouros.navigation.utils.getSettingsRepository
|
||||||
|
|
||||||
@@ -48,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()
|
||||||
@@ -66,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 {
|
||||||
@@ -138,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 {
|
||||||
|
|||||||
@@ -55,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
|
||||||
@@ -71,6 +72,8 @@ 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() {
|
||||||
}
|
}
|
||||||
@@ -82,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,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,
|
||||||
@@ -139,15 +150,12 @@ class RoutePreviewScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val street = if (destination.street.isEmpty()) {
|
val street = destination.street.ifEmpty {
|
||||||
carContext.getString((R.string.route_preview))
|
carContext.getString((R.string.route_preview))
|
||||||
} else {
|
|
||||||
destination.street
|
|
||||||
}
|
}
|
||||||
val header = Header.Builder()
|
val header = Header.Builder()
|
||||||
.setStartHeaderAction(Action.BACK)
|
.setStartHeaderAction(Action.BACK)
|
||||||
.setTitle(street)
|
.setTitle(street)
|
||||||
|
|
||||||
if (routeType == RoutePreviewType.SINGLE_ROUTE) {
|
if (routeType == RoutePreviewType.SINGLE_ROUTE) {
|
||||||
header.addEndHeaderAction(
|
header.addEndHeaderAction(
|
||||||
favoriteAction()
|
favoriteAction()
|
||||||
@@ -174,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, {
|
||||||
@@ -195,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)
|
||||||
}
|
}
|
||||||
@@ -206,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,
|
||||||
@@ -229,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()
|
||||||
}
|
}
|
||||||
@@ -243,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()
|
||||||
}
|
}
|
||||||
@@ -301,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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,6 @@
|
|||||||
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.car.NavigationSession
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
|
|
||||||
@@ -18,12 +19,18 @@ class NavigationObserverManager(
|
|||||||
val speedCameraObserver = SpeedCameraObserver(callback)
|
val speedCameraObserver = SpeedCameraObserver(callback)
|
||||||
val maxSpeedObserver = MaxSpeedObserver(callback)
|
val maxSpeedObserver = MaxSpeedObserver(callback)
|
||||||
|
|
||||||
fun attachAllObservers(session: NavigationSession) {
|
val recentPlacesObserver = RecentPlacesObserver(callback)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun attachAllObservers(session: CarSession) {
|
||||||
viewModel.route.observe(session, routeObserver)
|
viewModel.route.observe(session, routeObserver)
|
||||||
viewModel.traffic.observe(session, trafficObserver)
|
viewModel.traffic.observe(session, trafficObserver)
|
||||||
viewModel.placeLocation.observe(session, placeSearchObserver)
|
viewModel.placeLocation.observe(session, placeSearchObserver)
|
||||||
viewModel.speedCameras.observe(session, speedCameraObserver)
|
viewModel.speedCameras.observe(session, speedCameraObserver)
|
||||||
viewModel.maxSpeed.observe(session, maxSpeedObserver)
|
viewModel.maxSpeed.observe(session, maxSpeedObserver)
|
||||||
|
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>,
|
||||||
)
|
)
|
||||||
@@ -41,11 +44,16 @@ data class Place(
|
|||||||
var postalCode: String = "",
|
var postalCode: String = "",
|
||||||
var city: String = "",
|
var city: String = "",
|
||||||
var street: String = "",
|
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,
|
||||||
|
@Transient
|
||||||
var routeIndex: Int = 0,
|
var routeIndex: Int = 0,
|
||||||
|
@Transient
|
||||||
var route: String = "",
|
var route: String = "",
|
||||||
|
@Transient
|
||||||
|
var stopOver: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ContactData(
|
data class ContactData(
|
||||||
@@ -112,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)
|
||||||
@@ -157,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
|
||||||
|
|||||||
@@ -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§ionType=lanes" +
|
"&instructionsType=text&language=$language§ionType=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,12 +33,20 @@ class TomTomRoute {
|
|||||||
route.summary.trafficLengthInMeters.toDouble()
|
route.summary.trafficLengthInMeters.toDouble()
|
||||||
)
|
)
|
||||||
route.legs.forEach { leg ->
|
route.legs.forEach { leg ->
|
||||||
points = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision)
|
val p = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision)
|
||||||
waypoints.addAll(points)
|
points.addAll(p)
|
||||||
|
waypoints.addAll(p)
|
||||||
}
|
}
|
||||||
|
route.legs.forEach { leg ->
|
||||||
var stepDistance = 0.0
|
var stepDistance = 0.0
|
||||||
var stepDuration = 0.0
|
var stepDuration = 0.0
|
||||||
val steps = mutableListOf<Step>()
|
val steps = mutableListOf<Step>()
|
||||||
|
val summary = Summary(
|
||||||
|
leg.summary.travelTimeInSeconds.toDouble(),
|
||||||
|
leg.summary.lengthInMeters.toDouble(),
|
||||||
|
leg.summary.trafficDelayInSeconds.toDouble(),
|
||||||
|
leg.summary.trafficLengthInMeters.toDouble()
|
||||||
|
)
|
||||||
var lastPointIndex = 0
|
var lastPointIndex = 0
|
||||||
for (index in 1..<route.guidance.instructions.size) {
|
for (index in 1..<route.guidance.instructions.size) {
|
||||||
val lastInstruction = route.guidance.instructions[index - 1]
|
val lastInstruction = route.guidance.instructions[index - 1]
|
||||||
@@ -67,7 +78,6 @@ class TomTomRoute {
|
|||||||
) {
|
) {
|
||||||
val lanes = mutableListOf<Lane>()
|
val lanes = mutableListOf<Lane>()
|
||||||
var startIndex = 0
|
var startIndex = 0
|
||||||
var lastLane: Lane? = null
|
|
||||||
section.lanes?.forEach { itLane ->
|
section.lanes?.forEach { itLane ->
|
||||||
val lane = Lane(
|
val lane = Lane(
|
||||||
location = location(
|
location = location(
|
||||||
@@ -80,14 +90,8 @@ class TomTomRoute {
|
|||||||
endIndex = section.endPointIndex
|
endIndex = section.endPointIndex
|
||||||
)
|
)
|
||||||
startIndex = section.startPointIndex
|
startIndex = section.startPointIndex
|
||||||
if (lastLane == null
|
|
||||||
|| (!(lastLane.valid && lane.valid
|
|
||||||
&& lastLane.indications == lane.indications))
|
|
||||||
) {
|
|
||||||
lanes.add(lane)
|
lanes.add(lane)
|
||||||
}
|
}
|
||||||
lastLane = lane
|
|
||||||
}
|
|
||||||
intersections.add(Intersection(waypoints[startIndex], lanes))
|
intersections.add(Intersection(waypoints[startIndex], lanes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,10 +113,11 @@ class TomTomRoute {
|
|||||||
steps.add(step)
|
steps.add(step)
|
||||||
stepIndex += 1
|
stepIndex += 1
|
||||||
}
|
}
|
||||||
legs.add(Leg(steps))
|
legs.add(Leg(steps, summary))
|
||||||
|
}
|
||||||
val routeGeoJson = createLineStringCollection(waypoints)
|
val routeGeoJson = createLineStringCollection(waypoints)
|
||||||
val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
|
val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
|
||||||
val newRoute = com.kouros.navigation.data.route.Routes(
|
val newRoute = Routes(
|
||||||
legs,
|
legs,
|
||||||
summary,
|
summary,
|
||||||
routeGeoJson,
|
routeGeoJson,
|
||||||
@@ -130,75 +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,
|
||||||
|
|||||||
@@ -6,15 +6,12 @@ import android.graphics.BitmapFactory
|
|||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.car.app.model.CarIcon
|
|
||||||
import androidx.car.app.navigation.model.LaneDirection
|
import androidx.car.app.navigation.model.LaneDirection
|
||||||
import androidx.car.app.navigation.model.Maneuver
|
|
||||||
import androidx.core.graphics.createBitmap
|
import androidx.core.graphics.createBitmap
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.data.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
import java.util.Collections
|
import com.kouros.navigation.data.route.ManeuverType
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class IconMapper {
|
class IconMapper {
|
||||||
@@ -22,62 +19,66 @@ class IconMapper {
|
|||||||
fun maneuverIcon(routeManeuverType: Int): Int {
|
fun maneuverIcon(routeManeuverType: Int): Int {
|
||||||
var currentTurnIcon = R.drawable.ic_turn_name_change
|
var currentTurnIcon = R.drawable.ic_turn_name_change
|
||||||
when (routeManeuverType) {
|
when (routeManeuverType) {
|
||||||
Maneuver.TYPE_STRAIGHT -> {
|
ManeuverType.TYPE_STRAIGHT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_name_change
|
currentTurnIcon = R.drawable.ic_turn_name_change
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_DESTINATION,
|
ManeuverType.TYPE_DESTINATION.value,
|
||||||
Maneuver.TYPE_DESTINATION_RIGHT,
|
ManeuverType.TYPE_DESTINATION_RIGHT.value,
|
||||||
Maneuver.TYPE_DESTINATION_LEFT,
|
ManeuverType.TYPE_DESTINATION_LEFT.value,
|
||||||
Maneuver.TYPE_DESTINATION_STRAIGHT
|
ManeuverType.TYPE_DESTINATION_STRAIGHT.value
|
||||||
-> {
|
-> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_destination
|
currentTurnIcon = R.drawable.ic_turn_destination
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_TURN_NORMAL_RIGHT -> {
|
ManeuverType.TYPE_TURN_NORMAL_RIGHT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_normal_right
|
currentTurnIcon = R.drawable.ic_turn_normal_right
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_TURN_NORMAL_LEFT -> {
|
ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_normal_left
|
currentTurnIcon = R.drawable.ic_turn_normal_left
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT -> {
|
ManeuverType.TYPE_OFF_RAMP_SLIGHT_RIGHT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_slight_right
|
currentTurnIcon = R.drawable.ic_turn_slight_right
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> {
|
ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_slight_right
|
currentTurnIcon = R.drawable.ic_turn_slight_right
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_KEEP_RIGHT -> {
|
ManeuverType.TYPE_KEEP_RIGHT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_name_change
|
currentTurnIcon = R.drawable.ic_turn_name_change
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_KEEP_LEFT -> {
|
ManeuverType.TYPE_KEEP_LEFT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_name_change
|
currentTurnIcon = R.drawable.ic_turn_name_change
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_ROUNDABOUT_ENTER_CCW -> {
|
ManeuverType.TYPE_ROUNDABOUT_ENTER_CCW.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_roundabout_ccw
|
currentTurnIcon = R.drawable.ic_roundabout_ccw
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_ROUNDABOUT_EXIT_CCW -> {
|
ManeuverType.TYPE_ROUNDABOUT_EXIT_CCW.value -> {
|
||||||
|
|
||||||
currentTurnIcon = R.drawable.ic_roundabout_ccw
|
currentTurnIcon = R.drawable.ic_roundabout_ccw
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_U_TURN_LEFT -> {
|
ManeuverType.TYPE_U_TURN_LEFT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_u_turn_left
|
currentTurnIcon = R.drawable.ic_turn_u_turn_left
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_U_TURN_RIGHT -> {
|
ManeuverType.TYPE_U_TURN_RIGHT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_u_turn_right
|
currentTurnIcon = R.drawable.ic_turn_u_turn_right
|
||||||
}
|
}
|
||||||
|
|
||||||
Maneuver.TYPE_MERGE_LEFT -> {
|
ManeuverType.TYPE_MERGE_LEFT.value -> {
|
||||||
currentTurnIcon = R.drawable.ic_turn_merge_symmetrical
|
currentTurnIcon = R.drawable.ic_turn_merge_symmetrical
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ManeuverType.TYPE_WAYPOINT_RIGHT.value -> {
|
||||||
|
currentTurnIcon = R.drawable.ic_turn_destination
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return currentTurnIcon
|
return currentTurnIcon
|
||||||
}
|
}
|
||||||
@@ -86,8 +87,8 @@ class IconMapper {
|
|||||||
val laneDirection = when (direction.lowercase(Locale.getDefault())) {
|
val laneDirection = when (direction.lowercase(Locale.getDefault())) {
|
||||||
"left_straight" -> {
|
"left_straight" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT
|
ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> LaneDirection.SHAPE_NORMAL_LEFT
|
||||||
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
|
ManeuverType.TYPE_STRAIGHT.value -> LaneDirection.SHAPE_STRAIGHT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -95,7 +96,7 @@ class IconMapper {
|
|||||||
|
|
||||||
"left" -> {
|
"left" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT
|
ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> LaneDirection.SHAPE_NORMAL_LEFT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -103,9 +104,9 @@ class IconMapper {
|
|||||||
|
|
||||||
"straight" -> {
|
"straight" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
|
ManeuverType.TYPE_STRAIGHT.value -> LaneDirection.SHAPE_STRAIGHT
|
||||||
Maneuver.TYPE_KEEP_LEFT -> LaneDirection.SHAPE_STRAIGHT
|
ManeuverType.TYPE_KEEP_LEFT.value -> LaneDirection.SHAPE_STRAIGHT
|
||||||
Maneuver.TYPE_KEEP_RIGHT -> LaneDirection.SHAPE_STRAIGHT
|
ManeuverType.TYPE_KEEP_RIGHT.value -> LaneDirection.SHAPE_STRAIGHT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -113,7 +114,7 @@ class IconMapper {
|
|||||||
|
|
||||||
"right" -> {
|
"right" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
|
ManeuverType.TYPE_TURN_NORMAL_RIGHT.value -> LaneDirection.SHAPE_NORMAL_RIGHT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -121,8 +122,8 @@ class IconMapper {
|
|||||||
|
|
||||||
"right_straight" -> {
|
"right_straight" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
|
ManeuverType.TYPE_TURN_NORMAL_RIGHT.value -> LaneDirection.SHAPE_NORMAL_RIGHT
|
||||||
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
|
ManeuverType.TYPE_STRAIGHT.value -> LaneDirection.SHAPE_STRAIGHT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -130,8 +131,8 @@ class IconMapper {
|
|||||||
|
|
||||||
"left_slight", "slight_left" -> {
|
"left_slight", "slight_left" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT
|
ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> LaneDirection.SHAPE_SLIGHT_LEFT
|
||||||
Maneuver.TYPE_KEEP_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT
|
ManeuverType.TYPE_KEEP_LEFT.value -> LaneDirection.SHAPE_SLIGHT_LEFT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -139,8 +140,8 @@ class IconMapper {
|
|||||||
|
|
||||||
"right_slight", "slight_right" -> {
|
"right_slight", "slight_right" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
|
ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value -> LaneDirection.SHAPE_NORMAL_RIGHT
|
||||||
Maneuver.TYPE_KEEP_RIGHT -> LaneDirection.SHAPE_SLIGHT_RIGHT
|
ManeuverType.TYPE_KEEP_RIGHT.value -> LaneDirection.SHAPE_SLIGHT_RIGHT
|
||||||
else
|
else
|
||||||
-> LaneDirection.SHAPE_UNKNOWN
|
-> LaneDirection.SHAPE_UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -212,8 +213,8 @@ class IconMapper {
|
|||||||
return when (direction) {
|
return when (direction) {
|
||||||
"left_straight" -> {
|
"left_straight" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_LEFT -> "left_o_straight_x"
|
ManeuverType.TYPE_TURN_NORMAL_LEFT.value -> "left_o_straight_x"
|
||||||
Maneuver.TYPE_STRAIGHT -> "left_x_straight_o"
|
ManeuverType.TYPE_STRAIGHT.value -> "left_x_straight_o"
|
||||||
else
|
else
|
||||||
-> "left_x_straight_x"
|
-> "left_x_straight_x"
|
||||||
}
|
}
|
||||||
@@ -221,29 +222,29 @@ class IconMapper {
|
|||||||
|
|
||||||
"right_straight" -> {
|
"right_straight" -> {
|
||||||
when (stepData.currentManeuverType) {
|
when (stepData.currentManeuverType) {
|
||||||
Maneuver.TYPE_TURN_NORMAL_RIGHT -> "right_x_straight_x"
|
ManeuverType.TYPE_TURN_NORMAL_RIGHT.value -> "right_x_straight_x"
|
||||||
Maneuver.TYPE_STRAIGHT -> "right_x_straight_o"
|
ManeuverType.TYPE_STRAIGHT.value -> "right_x_straight_o"
|
||||||
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> "right_o_straight_o"
|
ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value -> "right_o_straight_o"
|
||||||
else
|
else
|
||||||
-> "right_x_straight_x"
|
-> "right_x_straight_x"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_RIGHT) "${direction}_o" else "${direction}_x"
|
"right" -> if (stepData.currentManeuverType == ManeuverType.TYPE_TURN_NORMAL_RIGHT.value) "${direction}_o" else "${direction}_x"
|
||||||
"left" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_LEFT) "${direction}_o" else "${direction}_x"
|
"left" -> if (stepData.currentManeuverType == ManeuverType.TYPE_TURN_NORMAL_LEFT.value) "${direction}_o" else "${direction}_x"
|
||||||
"straight" -> if (stepData.currentManeuverType == Maneuver.TYPE_STRAIGHT
|
"straight" -> if (stepData.currentManeuverType == ManeuverType.TYPE_STRAIGHT.value
|
||||||
|| stepData.currentManeuverType == Maneuver.TYPE_KEEP_LEFT
|
|| stepData.currentManeuverType == ManeuverType.TYPE_KEEP_LEFT.value
|
||||||
|| stepData.currentManeuverType == Maneuver.TYPE_KEEP_RIGHT
|
|| stepData.currentManeuverType == ManeuverType.TYPE_KEEP_RIGHT.value
|
||||||
) "${direction}_o" else "${direction}_x"
|
) "${direction}_o" else "${direction}_x"
|
||||||
|
|
||||||
"right_slight", "slight_right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
"right_slight", "slight_right" -> if (stepData.currentManeuverType == ManeuverType.TYPE_TURN_SLIGHT_RIGHT.value
|
||||||
|| stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_RIGHT
|
|| stepData.currentManeuverType == ManeuverType.TYPE_TURN_NORMAL_RIGHT.value
|
||||||
|| stepData.currentManeuverType == Maneuver.TYPE_KEEP_RIGHT
|
|| stepData.currentManeuverType == ManeuverType.TYPE_KEEP_RIGHT.value
|
||||||
) "slight_right_o" else "slight_right_x"
|
) "slight_right_o" else "slight_right_x"
|
||||||
|
|
||||||
"left_slight", "slight_left" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_LEFT
|
"left_slight", "slight_left" -> if (stepData.currentManeuverType == ManeuverType.TYPE_TURN_SLIGHT_LEFT.value
|
||||||
|| stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_LEFT
|
|| stepData.currentManeuverType == ManeuverType.TYPE_TURN_NORMAL_LEFT.value
|
||||||
|| stepData.currentManeuverType == Maneuver.TYPE_KEEP_LEFT
|
|| stepData.currentManeuverType == ManeuverType.TYPE_KEEP_LEFT.value
|
||||||
) "slight_left_o" else "slight_left_x"
|
) "slight_left_o" else "slight_left_x"
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@@ -10,6 +11,7 @@ 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
|
||||||
@@ -26,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
|
||||||
|
|
||||||
@@ -100,6 +103,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val gson: com.google.gson.Gson = GsonBuilder().create()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads all recent places from Preferences and calculates distances.
|
* Loads all recent places from Preferences and calculates distances.
|
||||||
@@ -110,14 +114,14 @@ 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 =
|
||||||
@@ -147,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) {
|
||||||
@@ -181,10 +185,12 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
)
|
)
|
||||||
if (data.isNotEmpty()) {
|
if (data.isNotEmpty()) {
|
||||||
val trafficData = rebuildTraffic(data)
|
val trafficData = rebuildTraffic(data)
|
||||||
|
if (trafficData.isNotEmpty()) {
|
||||||
traffic.postValue(
|
traffic.postValue(
|
||||||
trafficData
|
trafficData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
@@ -234,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)
|
||||||
)
|
)
|
||||||
@@ -286,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 {
|
||||||
@@ -311,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 {
|
||||||
@@ -431,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
|
||||||
@@ -441,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
|
||||||
}
|
}
|
||||||
@@ -448,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) {
|
||||||
@@ -470,7 +475,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
fun deletePlace(context: Context, place: Place) {
|
fun deletePlace(context: Context, place: Place) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
|
||||||
val settingsRepository = getSettingsRepository(context)
|
val settingsRepository = getSettingsRepository(context)
|
||||||
val rp = settingsRepository.recentPlacesFlow.first()
|
val rp = settingsRepository.recentPlacesFlow.first()
|
||||||
val places = mutableListOf<Place>()
|
val places = mutableListOf<Place>()
|
||||||
@@ -479,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,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
|
||||||
@@ -138,7 +139,7 @@ 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
|
||||||
@@ -147,7 +148,7 @@ fun duration(
|
|||||||
if (updateDuration.toMillis().absoluteValue < 1000) {
|
if (updateDuration.toMillis().absoluteValue < 1000) {
|
||||||
2.seconds
|
2.seconds
|
||||||
} else {
|
} else {
|
||||||
((updateDuration!!.toMillis().absoluteValue * 1.2).toDuration(DurationUnit.MILLISECONDS))
|
((updateDuration!!.toMillis().absoluteValue * 1.8).toDuration(DurationUnit.MILLISECONDS))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cameraDuration
|
return cameraDuration
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||