Launcher Icons, NotificationService

This commit is contained in:
Dimitris
2026-03-24 17:04:05 +01:00
parent bc8a53a5d8
commit 5098dad9d6
76 changed files with 930 additions and 478 deletions

View File

@@ -5,3 +5,6 @@
## Simulation ## Simulation
adb shell dumpsys activity service com.kouros.navigation.car.NavigationCarAppService AUTO_DRIVE adb shell dumpsys activity service com.kouros.navigation.car.NavigationCarAppService AUTO_DRIVE
## Signing
./gradlew bundleFull

View File

@@ -1,10 +1,14 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import java.util.Properties
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
} }
val properties = Properties().apply {
load(File("signing.properties").reader())
}
android { android {
namespace = "com.kouros.navigation" namespace = "com.kouros.navigation"
compileSdk = 36 compileSdk = 36
@@ -13,8 +17,8 @@ android {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 36
versionCode = 76 versionCode = 82
versionName = "0.2.0.76" versionName = "0.2.0.82"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -22,23 +26,22 @@ android {
signingConfigs { signingConfigs {
getByName("debug") { getByName("debug") {
keyAlias = "release" keyAlias = "release"
keyPassword = "zeta67#gAe3aN3" keyPassword = properties.getProperty("keyPassword")
storeFile = file("/home/kouros/work/keystore/keystoreRelease") storeFile = file(properties.getProperty("storeFile"))
storePassword = "zeta67#gAe3aN3" storePassword = properties.getProperty("storePassword")
} }
create("release") { create("release") {
keyAlias = "release" keyAlias = "release"
keyPassword = "zeta67#gAe3aN3" keyPassword = properties.getProperty("keyPassword")
storeFile = file("/home/kouros/work/keystore/keystoreRelease") storeFile = file(properties.getProperty("storeFile"))
storePassword = "zeta67#gAe3aN3" storePassword = properties.getProperty("storePassword")
} }
} }
buildTypes { buildTypes {
release { release {
// Enables code-related app optimization. signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false isMinifyEnabled = false
// Enables resource shrinking.
isShrinkResources = false isShrinkResources = false
proguardFiles( proguardFiles(
@@ -48,15 +51,20 @@ android {
} }
} }
// Specifies one flavor dimension. // Specifies one flavor dimension.
flavorDimensions += "version" flavorDimensions += "store"
productFlavors { productFlavors {
create("play") {
dimension = "store"
applicationIdSuffix = ".play"
versionNameSuffix = "-play"
}
create("demo") { create("demo") {
dimension = "version" dimension = "store"
applicationIdSuffix = ".demo" applicationIdSuffix = ".demo"
versionNameSuffix = "-demo" versionNameSuffix = "-demo"
} }
create("full") { create("full") {
dimension = "version" dimension = "store"
applicationIdSuffix = ".full" applicationIdSuffix = ".full"
versionNameSuffix = "-full" versionNameSuffix = "-full"
} }
@@ -66,11 +74,20 @@ android {
targetCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21
} }
buildFeatures { packaging {
compose = true resources {
excludes +=
setOf(
"/META-INF/{AL2.0,LGPL2.1}",
"/META-INF/*.version",
)
}
} }
buildFeatures {
compose = true
buildConfig = true
}
} }
dependencies { dependencies {
@@ -95,7 +112,6 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.window) implementation(libs.androidx.window)
implementation(libs.androidx.compose.foundation.layout) implementation(libs.androidx.compose.foundation.layout)
implementation(libs.android.gpx.parser)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.compose.foundation.layout) implementation(libs.androidx.compose.foundation.layout)
@@ -107,4 +123,3 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,20 +1,12 @@
package com.kouros.navigation.model package com.kouros.navigation.model
import android.content.Context import android.content.Context
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.MainApplication.Companion.navigationViewModel import com.kouros.navigation.MainApplication.Companion.navigationViewModel
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import io.ticofab.androidgpxparser.parser.GPXParser
import io.ticofab.androidgpxparser.parser.domain.Gpx
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.joda.time.DateTime
import kotlin.collections.forEach
var simulationJob: Job? = null var simulationJob: Job? = null
fun simulate(routeModel: RouteModel, mock: MockLocation) { fun simulate(routeModel: RouteModel, mock: MockLocation) {
@@ -91,43 +83,6 @@ fun testSingleUpdate(
Thread.sleep(1_000) Thread.sleep(1_000)
} }
fun gpx(context: Context, mock: MockLocation) {
CoroutineScope(Dispatchers.IO).launch {
var lastLocation = location(0.0, 0.0)
val parser = GPXParser()
val resourceId: Int = context.resources
.getIdentifier("vh", "raw", context.packageName)
val input = context.resources.openRawResource(resourceId)
val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread
parsedGpx?.let {
val tracks = parsedGpx.tracks
tracks.forEach { tr ->
val segments: MutableList<TrackSegment?>? = tr.trackSegments
segments!!.forEach { seg ->
var lastTime = DateTime.now()
seg!!.trackPoints.forEach { p ->
val curLocation = location(p.longitude, p.latitude)
val ext = p.extensions
val speed: Double?
if (ext != null) {
speed = ext.speed
mock.curSpeed = speed.toFloat()
}
val duration = p.time.millis - lastTime.millis
val bearing = lastLocation.bearingTo(curLocation)
mock.setMockLocation(p.latitude, p.longitude, bearing)
if (duration > 0) {
delay(duration / 5)
}
lastTime = p.time
lastLocation = curLocation
}
}
}
}
}
}
enum class SimulationType { enum class SimulationType {
SIMULATE, TEST, GPX, TEST_SINGLE SIMULATE, TEST, GPX, TEST_SINGLE
} }

View File

@@ -50,7 +50,6 @@ import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.MockLocation import com.kouros.navigation.model.MockLocation
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.SimulationType import com.kouros.navigation.model.SimulationType
import com.kouros.navigation.model.gpx
import com.kouros.navigation.model.simulate import com.kouros.navigation.model.simulate
import com.kouros.navigation.model.simulationJob import com.kouros.navigation.model.simulationJob
import com.kouros.navigation.model.test import com.kouros.navigation.model.test
@@ -112,11 +111,10 @@ class MainActivity : ComponentActivity() {
when (type) { when (type) {
SimulationType.SIMULATE -> simulate(routeModel, mock) SimulationType.SIMULATE -> simulate(routeModel, mock)
SimulationType.TEST -> test(applicationContext, routeModel) SimulationType.TEST -> test(applicationContext, routeModel)
SimulationType.GPX -> gpx(
context = applicationContext, mock
)
SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock) SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock)
else -> {}
} }
} }
} }

View File

@@ -13,10 +13,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.window.layout.WindowMetricsCalculator import androidx.window.layout.WindowMetricsCalculator
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.map.MapLibre import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.NavigationImage import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.ui.app.AppViewModel import com.kouros.navigation.ui.app.AppViewModel
import com.kouros.navigation.ui.app.appViewModel import com.kouros.navigation.ui.app.appViewModel
import com.kouros.navigation.ui.navigation.NavigationInfo import com.kouros.navigation.ui.navigation.NavigationInfo

View File

@@ -48,5 +48,11 @@ fun AppNavGraph(mainActivity: MainActivity) {
navController navController
) { navController.popBackStack() } ) { navController.popBackStack() }
} }
composable("car_settings") {
SettingsRoute(
"car_settings",
navController
) { navController.popBackStack() }
}
} }
} }

View File

@@ -0,0 +1,90 @@
package com.kouros.navigation.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.kouros.data.R
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.ui.components.RadioButtonSingleSelection
import com.kouros.navigation.ui.components.SectionTitle
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CarScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) {
val engineType by viewModel.engineType.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
stringResource(id = R.string.car_settings),
)
},
navigationIcon = {
IconButton(onClick = navigateBack) {
Icon(
painter = painterResource(R.drawable.arrow_back_24px),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp),
)
}
},
)
},
)
{ paddingValues ->
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(top = 10.dp)
.verticalScroll(scrollState)
) {
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(10.dp),
) {
SectionTitle(stringResource(R.string.engine_type))
val radioOptions = listOf(
stringResource(R.string.combustion),
stringResource(R.string.electric),
)
RadioButtonSingleSelection(
modifier = Modifier.padding(),
selectedOption = engineType,
radioOptions = radioOptions,
onClick = viewModel::onEngineTypeChanged
)
}
}
}
}
}

View File

@@ -39,4 +39,7 @@ fun SettingsRoute(route: String, navController: NavHostController, function: ()
if (route == "settings_screen") { if (route == "settings_screen") {
SettingsScreen(viewModel, navController, function) SettingsScreen(viewModel, navController, function)
} }
if (route == "car_settings") {
CarScreen (viewModel = viewModel, function)
}
} }

View File

@@ -51,7 +51,14 @@ fun SettingsScreen(
name = "Navigation Settings", name = "Navigation Settings",
description = "", description = "",
icon = R.drawable.navigation_24px icon = R.drawable.navigation_24px
),
Item(
id = "car_settings",
name = "Car Settings",
description = "",
icon = R.drawable.electric_car_24px
) )
) )
Scaffold( Scaffold(

View File

@@ -3,7 +3,7 @@
android:height="108dp" android:height="108dp"
android:viewportWidth="960" android:viewportWidth="960"
android:viewportHeight="960" android:viewportHeight="960"
android:tint="#000000"> android:tint="#1A7416">
<group android:scaleX="0.7888" <group android:scaleX="0.7888"
android:scaleY="0.7888" android:scaleY="0.7888"
android:translateX="101.376" android:translateX="101.376"

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#98DABB</color>
</resources>

View File

@@ -1,170 +1,74 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector
android:width="108dp"
android:height="108dp" android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> xmlns:android="http://schemas.android.com/apk/res/android">
<path <path android:fillColor="#3DDC84"
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/> android:pathData="M0,0h108v108h-108z"/>
<path <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M9,0L9,108" <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:pathData="M19,0L19,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M29,0L29,108" <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:pathData="M39,0L39,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M49,0L49,108" <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:pathData="M59,0L59,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M69,0L69,108" <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:pathData="M79,0L79,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M89,0L89,108" <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:pathData="M99,0L99,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,9L108,9" <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:pathData="M0,19L108,19" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,29L108,29" <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector> </vector>

View File

@@ -3,7 +3,7 @@
android:height="108dp" android:height="108dp"
android:viewportWidth="960" android:viewportWidth="960"
android:viewportHeight="960" android:viewportHeight="960"
android:tint="#000000"> android:tint="#1A7416">
<group android:scaleX="0.7888" <group android:scaleX="0.7888"
android:scaleY="0.7888" android:scaleY="0.7888"
android:translateX="101.376" android:translateX="101.376"

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -29,6 +29,7 @@ android {
} }
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true
} }
} }
@@ -52,6 +53,7 @@ dependencies {
implementation(libs.play.services.location) implementation(libs.play.services.location)
implementation(libs.androidx.datastore.core) implementation(libs.androidx.datastore.core)
implementation(libs.androidx.monitor) implementation(libs.androidx.monitor)
implementation(libs.android.gpx.parser)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.runner) androidTestImplementation(libs.androidx.runner)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -137,7 +137,7 @@ class CarSensorManager(
carCompassListener carCompassListener
) )
carSensors.addCarHardwareLocationListener( carSensors.addCarHardwareLocationListener(
CarSensors.UPDATE_RATE_FASTEST, CarSensors.UPDATE_RATE_NORMAL,
carContext.mainExecutor, carContext.mainExecutor,
carLocationListener carLocationListener
) )

View File

@@ -1,7 +1,7 @@
package com.kouros.navigation.car package com.kouros.navigation.car
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.location.Location import android.net.Uri
import androidx.car.app.CarAppService import androidx.car.app.CarAppService
import androidx.car.app.Session import androidx.car.app.Session
import androidx.car.app.SessionInfo import androidx.car.app.SessionInfo
@@ -10,6 +10,14 @@ import androidx.car.app.validation.HostValidator
class NavigationCarAppService : CarAppService() { class NavigationCarAppService : CarAppService() {
val INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP =
"com.kouros.navigation.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP"
fun createDeepLinkUri(deepLinkAction: String): Uri {
return Uri.fromParts(NavigationSession.uriScheme, NavigationSession.uriHost, deepLinkAction)
}
@SuppressLint("PrivateResource") @SuppressLint("PrivateResource")
override fun createHostValidator(): HostValidator { override fun createHostValidator(): HostValidator {

View File

@@ -0,0 +1,232 @@
package com.kouros.navigation.car
import android.annotation.SuppressLint
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Message
import androidx.car.app.notification.CarAppExtender
import androidx.car.app.notification.CarNotificationManager
import androidx.car.app.notification.CarPendingIntent
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.kouros.data.R
import java.math.RoundingMode
import java.text.DecimalFormat
import java.util.concurrent.TimeUnit
/**
* A simple foreground service that imitates a client routing service posting navigation
* notifications.
*/
class NavigationNotificationService : Service() {
/**
* The number of notifications fired so far.
*
*
* We use this number to post notifications with a repeating list of directions. See [ ][.getDirectionInfo] for details.
*
* Note: Package private for inner class reference
*/
var mNotificationCount: Int = 0
/**
* A handler that posts notifications when given the message request. See [ ] for details.
*
* Note: Package private for inner class reference
*/
val mHandler: Handler =
Handler(Looper.getMainLooper(), HandlerCallback())
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
initNotifications(this)
startForeground(
NAV_NOTIFICATION_ID,
getNavigationNotification(this, mNotificationCount).build()
)
// Start updating the notification continuously.
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_SEND_NOTIFICATION), NAV_NOTIFICATION_DELAY_IN_MILLIS
)
return START_NOT_STICKY
}
override fun onDestroy() {
mHandler.removeMessages(MSG_SEND_NOTIFICATION)
}
override fun onBind(intent: Intent): IBinder? {
return null
}
/**
* A [Handler.Callback] used to process the message queue for the notification service.
*/
internal inner class HandlerCallback : Handler.Callback {
override fun handleMessage(msg: Message): Boolean {
if (msg.what == MSG_SEND_NOTIFICATION) {
val context: Context = this@NavigationNotificationService
CarNotificationManager.from(context).notify(
NAV_NOTIFICATION_ID,
getNavigationNotification(context, mNotificationCount)
)
mNotificationCount++
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_SEND_NOTIFICATION),
NAV_NOTIFICATION_DELAY_IN_MILLIS
)
return true
}
return false
}
}
/**
* A container class that encapsulates the direction information to use in the notifications.
*/
internal class DirectionInfo(
val mTitle: String, val mDistance: String, val mIcon: Int,
val mOnlyAlertOnce: Boolean
)
companion object {
private const val MSG_SEND_NOTIFICATION = 1
private const val NAV_NOTIFICATION_CHANNEL_ID = "nav_channel_00"
private val NAV_NOTIFICATION_CHANNEL_NAME: CharSequence = "Navigation Channel"
private const val NAV_NOTIFICATION_ID = 10101
val NAV_NOTIFICATION_DELAY_IN_MILLIS: Long = TimeUnit.SECONDS.toMillis(1)
/**
* Initializes the notifications, if needed.
*
*
* [NotificationManager.IMPORTANCE_HIGH] is needed to show the alerts on top of the car
* screen. However, the rail widget at the bottom of the screen will show regardless of the
* importance setting.
*/
// Suppressing 'ObsoleteSdkInt' as this code is shared between APKs with different min SDK
// levels
@SuppressLint("ObsoleteSdkInt")
private fun initNotifications(context: Context) {
val navChannel =
NotificationChannelCompat.Builder(
NAV_NOTIFICATION_CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_HIGH
)
.setName(NAV_NOTIFICATION_CHANNEL_NAME).build()
CarNotificationManager.from(context).createNotificationChannel(navChannel)
}
/** Returns the navigation notification that corresponds to the given notification count. */
fun getNavigationNotification(
context: Context, notificationCount: Int
): NotificationCompat.Builder {
val builder =
NotificationCompat.Builder(context, NAV_NOTIFICATION_CHANNEL_ID)
val directionInfo = getDirectionInfo(context, notificationCount)
// Set an intent to open the car app. The app receives this intent when the user taps the
// heads-up notification or the rail widget.
val pendingIntent = CarPendingIntent.getCarApp(
context,
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP.hashCode(),
Intent(
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP
).setComponent(
ComponentName(
context,
NavigationCarAppService()::class.java
)
).setData(
NavigationCarAppService().createDeepLinkUri(
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP
)
),
0
)
return builder // This title, text, and icon will be shown in both phone and car screen. These
// values can
// be overridden in the extender below, to customize notifications in the car
// screen.
.setContentTitle(directionInfo.mTitle)
.setContentText(directionInfo.mDistance)
.setSmallIcon(directionInfo.mIcon) // The notification must be set to 'ongoing' and its category must be set to
// CATEGORY_NAVIGATION in order to show it in the rail widget when the app is
// navigating on
// the background.
// These values cannot be overridden in the extender.
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_NAVIGATION) // If set to true, the notification will only show the alert once in both phone and
// car screen. This value cannot be overridden in the extender.
.setOnlyAlertOnce(directionInfo.mOnlyAlertOnce) // This extender must be set in order to display the notification in the car screen.
// The extender also allows various customizations, such as showing different title
// or icon on the car screen.
.extend(
CarAppExtender.Builder()
.setContentIntent(pendingIntent)
.build()
)
}
/**
* Returns a [DirectionInfo] that corresponds to the given notification count.
*
*
* There are 5 directions, repeating in order. For each direction, the alert will only show
* once, but the distance will update on every count on the rail widget.
*/
private fun getDirectionInfo(context: Context, notificationCount: Int): DirectionInfo {
val formatter = DecimalFormat("#.##")
formatter.setRoundingMode(RoundingMode.DOWN)
val repeatingCount = notificationCount % 35
if (repeatingCount in 0..<10) {
// Distance decreases from 1km to 0.1km
val distance = formatter.format((10 - repeatingCount) * 0.1) + "km"
return DirectionInfo(
context.getString(R.string.stop_action_title),
distance,
R.drawable.arrow_back_24px,
repeatingCount > 0
)
} else if (repeatingCount in 10..<20) {
// Distance decreases from 5km to 0.5km
val distance = formatter.format((20 - repeatingCount) * 0.5) + "km"
return DirectionInfo(
context.getString(R.string.route_preview),
distance,
R.drawable.ic_turn_normal_right, /* onlyAlertOnce= */
repeatingCount > 10
)
} else if (repeatingCount in 20..<25) {
// Distance decreases from 200m to 40m
val distance = formatter.format(((25 - repeatingCount) * 40).toLong()) + "m"
return DirectionInfo(
context.getString(R.string.route_preview),
distance,
R.drawable.navigation_48px, /* onlyAlertOnce= */
repeatingCount > 20
)
} else {
// Distance decreases from 1km to 0.1km
val distance = formatter.format((35 - repeatingCount) * 0.1) + "km"
return DirectionInfo(
context.getString(R.string.charging_station),
distance,
R.drawable.local_gas_station_24,
repeatingCount > 25
)
}
}
}
}

View File

@@ -26,6 +26,7 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.navigation.Simulation import com.kouros.navigation.car.navigation.Simulation
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.car.screen.NavigationType
import com.kouros.navigation.car.screen.RequestPermissionScreen import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.car.screen.checkPermission import com.kouros.navigation.car.screen.checkPermission
@@ -37,6 +38,7 @@ import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.osrm.OsrmRepository import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.data.valhalla.ValhallaRepository
@@ -229,10 +231,11 @@ class NavigationSession : Session(), NavigationListener {
override fun onStopNavigation() { override fun onStopNavigation() {
// Called when the user stops navigation in the car screen // Called when the user stops navigation in the car screen
// Stop turn-by-turn logic and clean up // Stop turn-by-turn logic and clean up
routeModel.stopNavigation() stopNavigation()
autoDriveEnabled = false if (autoDriveEnabled) {
deviceLocationManager.startLocationUpdates() deviceLocationManager.startLocationUpdates()
} }
}
}) })
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner) surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
@@ -387,6 +390,7 @@ class NavigationSession : Session(), NavigationListener {
* Snaps location to route and checks for deviation requiring reroute. * Snaps location to route and checks for deviation requiring reroute.
*/ */
private fun handleNavigationLocation(location: Location) { private fun handleNavigationLocation(location: Location) {
if (guidanceAudio == 1) { if (guidanceAudio == 1) {
handleGuidanceAudio() handleGuidanceAudio()
} }
@@ -420,6 +424,9 @@ class NavigationSession : Session(), NavigationListener {
simulation.stopSimulation() simulation.stopSimulation()
autoDriveEnabled = false autoDriveEnabled = false
} }
surfaceRenderer.routeData.value = ""
surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW
} }
/** /**

View File

@@ -10,12 +10,17 @@ import androidx.car.app.AppManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.SurfaceCallback import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer import androidx.car.app.SurfaceContainer
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -29,9 +34,11 @@ import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.cameraState import com.kouros.navigation.car.map.cameraState
import com.kouros.navigation.car.map.getPaddingValues import com.kouros.navigation.car.map.getPaddingValues
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
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.Constants.homeVogelhart
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
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
@@ -45,9 +52,10 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking 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.expressions.dsl.zoom
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
/** /**
@@ -123,6 +131,8 @@ class SurfaceRenderer(
// Camera tilt angle (default 60 degrees for navigation) // Camera tilt angle (default 60 degrees for navigation)
var tilt = TILT var tilt = TILT
var lastLocationUpdate: LocalDateTime = LocalDateTime.now()
// Map base style (day/night) // Map base style (day/night)
val style: MutableLiveData<BaseStyle> by lazy { val style: MutableLiveData<BaseStyle> by lazy {
MutableLiveData() MutableLiveData()
@@ -238,7 +248,6 @@ class SurfaceRenderer(
init { init {
lifecycle.addObserver(this) lifecycle.addObserver(this)
speed.value = 0F speed.value = 0F
} }
fun onBaseStyleStateUpdated(style: BaseStyle) { fun onBaseStyleStateUpdated(style: BaseStyle) {
@@ -287,7 +296,7 @@ class SurfaceRenderer(
darkMode: Boolean darkMode: Boolean
) { ) {
val cameraDuration = val cameraDuration =
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing) duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing, lastLocationUpdate)
val currentSpeed: Float? by speed.observeAsState() val currentSpeed: Float? by speed.observeAsState()
val maximumSpeed: Int? by maxSpeed.observeAsState() val maximumSpeed: Int? by maxSpeed.observeAsState()
val streetName: String? by street.observeAsState() val streetName: String? by street.observeAsState()
@@ -311,9 +320,10 @@ class SurfaceRenderer(
tilt = tilt, tilt = tilt,
padding = paddingValues padding = paddingValues
), ),
duration = cameraDuration duration = cameraDuration,
) )
} }
lastLocationUpdate = LocalDateTime.now()
} }
override fun onCreate(owner: LifecycleOwner) { override fun onCreate(owner: LifecycleOwner) {
@@ -396,6 +406,15 @@ class SurfaceRenderer(
viewStyle = ViewStyle.VIEW viewStyle = ViewStyle.VIEW
} }
/**
* Activates navigation View
*/
fun activateNavigationView() {
viewStyle = ViewStyle.VIEW
tilt = TILT
updateLocation(lastLocation)
}
/** /**
* Updates camera position with new bearing, zoom, and target. * Updates camera position with new bearing, zoom, and target.
* Posts update to LiveData for UI observation. * Posts update to LiveData for UI observation.
@@ -414,21 +433,6 @@ class SurfaceRenderer(
} }
} }
/**
* Sets route data for active navigation and switches to VIEW mode.
*/
fun clearRouteData() {
updateLocation(lastLocation)
routeData.value = ""
viewStyle = ViewStyle.VIEW
cameraPosition.postValue(
cameraPosition.value!!.copy(
zoom = 16.0
)
)
tilt = TILT
}
/** /**
* Updates traffic incident data on the map. * Updates traffic incident data on the map.
*/ */
@@ -492,9 +496,9 @@ class SurfaceRenderer(
} }
/** /**
* Centers the map on a specific category/POI location. * Centers the map on a specific POI location.
*/ */
fun setCategoryLocation(location: Location, category: String) { fun setCategoryLocation(location: Location) {
viewStyle = ViewStyle.AMENITY_VIEW viewStyle = ViewStyle.AMENITY_VIEW
cameraPosition.postValue( cameraPosition.postValue(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
@@ -502,22 +506,4 @@ class SurfaceRenderer(
) )
) )
} }
companion
object {
private const val TAG = "MapRenderer"
}
}
/**
* Enum representing different map view modes.
* - VIEW: Active navigation mode with follow-car camera
* - PREVIEW: Route overview before starting navigation
* - PAN_VIEW: User-controlled map panning
* - AMENITY_VIEW: Displaying POI/amenity locations
*/
enum class ViewStyle {
VIEW, PREVIEW, PAN_VIEW, AMENITY_VIEW
} }

View File

@@ -26,11 +26,12 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.NavigationColor import com.kouros.navigation.data.NavigationColorDark
import com.kouros.navigation.data.NavigationColorLight
import com.kouros.navigation.data.RouteColor import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.utils.isMetricSystem import com.kouros.navigation.utils.isMetricSystem
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState
@@ -74,9 +75,9 @@ fun cameraState(
latitude = position!!.target.latitude, latitude = position!!.target.latitude,
longitude = position.target.longitude longitude = position.target.longitude
), ),
zoom = 15.0, zoom = position.zoom,
tilt = tilt, tilt = tilt,
padding = padding padding = padding,
) )
) )
} }
@@ -317,7 +318,10 @@ fun NavigationImage(
) { ) {
val imageSize = (height / 8) val imageSize = (height / 8)
val navigationColor = remember { NavigationColor } val navigationColor = if (darkMode)
remember { NavigationColorDark }
else
remember { NavigationColorLight }
val textMeasurerStreet = rememberTextMeasurer() val textMeasurerStreet = rememberTextMeasurer()
val street = streetName.toString() val street = streetName.toString()
@@ -545,7 +549,7 @@ fun DebugInfo(
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues { fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
return when (viewStyle) { return when (viewStyle) {
ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues( ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues(
start = 50.dp, start = 100.dp,
top = distanceFromTop(height).dp top = distanceFromTop(height).dp
) )

View File

@@ -1,80 +0,0 @@
package com.kouros.navigation.car.navigation
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.MediaPlayer
import android.media.MediaPlayer.OnCompletionListener
import android.speech.tts.TextToSpeech
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.annotation.RawRes
import androidx.annotation.StringRes
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.model.Action
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.Row
import androidx.core.graphics.createBitmap
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.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.data.Constants.TAG
import java.io.IOException
import java.util.Locale
class NavigationUtils(private var carContext: CarContext) {
fun createCarIcon(@DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
}
fun buildRowForTemplate(title: Int, resource: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
resource
)
)
.build()
)
.build()
}
fun createNumberIcon(category: String, number: String): IconCompat {
val size = 24
val bitmap = createBitmap(size, size)
val canvas = Canvas(bitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = size * 0.7f
textAlign = Paint.Align.CENTER
isFakeBoldText = true
}
val xPos = size / 2f
val yPos = (size / 2f) - ((paint.descent() + paint.ascent()) / 2f)
val color = when (category) {
CHARGING_STATION -> Color.GREEN
FUEL_STATION -> Color.BLUE
PHARMACY -> Color.RED
else -> Color.WHITE
}
paint.color = color
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
paint.color = Color.WHITE
canvas.drawText(number, xPos, yPos, paint)
return IconCompat.createWithBitmap(bitmap)
}
}

View File

@@ -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
@@ -184,7 +185,6 @@ class RouteCarModel : RouteModel() {
return CarText.create(carContext.getString(stringRes)) return CarText.create(carContext.getString(stringRes))
} }
fun createCarIcon(iconCompat: IconCompat): CarIcon { fun createCarIcon(iconCompat: IconCompat): CarIcon {
return CarIcon.Builder(iconCompat).build() return CarIcon.Builder(iconCompat).build()
} }
@@ -235,4 +235,17 @@ class RouteCarModel : RouteModel() {
.setFlags(flags) .setFlags(flags)
.build() .build()
} }
fun backGroundColor(): CarColor {
return if (isNavigating()) {
when (route.currentStep().countryCode) {
"DEU", "FRA", "AUT", "POL", "BEL", "NLD", "ESP", "PRT", "CZE", "SVK", "BGR", "HUN" -> CarColor.BLUE
else -> {
CarColor.GREEN
}
}
} else {
CarColor.GREEN
}
}
} }

View File

@@ -4,11 +4,17 @@ import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.os.SystemClock import android.os.SystemClock
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import com.kouros.navigation.data.Constants.homeVogelhart import com.kouros.android.cars.carappservice.BuildConfig
import com.kouros.navigation.utils.location import com.kouros.navigation.data.tomtom.TomTomRepository
import io.ticofab.androidgpxparser.parser.GPXParser
import io.ticofab.androidgpxparser.parser.domain.Gpx
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.joda.time.DateTime
class Simulation { class Simulation {
@@ -20,6 +26,20 @@ class Simulation {
updateLocation: (Location) -> Unit updateLocation: (Location) -> Unit
) { ) {
if (routeModel.navState.route.isRouteValid()) { if (routeModel.navState.route.isRouteValid()) {
if (BuildConfig.DEBUG) {
gpxSimulation(routeModel, lifecycleScope, updateLocation)
} else {
currentSimulation(routeModel, lifecycleScope, updateLocation)
}
return
}
}
private fun currentSimulation(
routeModel: RouteCarModel,
lifecycleScope: LifecycleCoroutineScope,
updateLocation: (Location) -> Unit
) {
val points = routeModel.curRoute.waypoints val points = routeModel.curRoute.waypoints
if (points.isEmpty()) return if (points.isEmpty()) return
simulationJob?.cancel() simulationJob?.cancel()
@@ -40,12 +60,71 @@ class Simulation {
// Update your app's state as if a real GPS update occurred // Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation) updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second) // Wait before moving to the next point (e.g., every 1 second)
delay(500) delay(1000)
lastLocation = fakeLocation lastLocation = fakeLocation
} }
routeModel.stopNavigation() routeModel.stopNavigation()
} }
} }
private fun gpxSimulation(
routeModel: RouteCarModel,
lifecycleScope: LifecycleCoroutineScope,
updateLocation: (Location) -> Unit
) {
var route = ""
simulationJob?.cancel()
runBlocking {
simulationJob = launch(Dispatchers.IO) {
route = TomTomRepository().fetchUrl(
"https://kouros-online.de/vh.gpx",
false
)
}
simulationJob?.join()
}
simulationJob?.cancel()
simulationJob =lifecycleScope.launch() {
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)
}
lastTime = p.time
lastLocation = fakeLocation
}
}
}
}
routeModel.stopNavigation()
}
} }
fun stopSimulation() { fun stopSimulation() {

View File

@@ -1,6 +1,5 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import androidx.activity.OnBackPressedCallback
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
@@ -13,18 +12,12 @@ import androidx.car.app.model.Template
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.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Category import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants.CHARGING_STATION import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.screen.observers.CategoryObserver
import com.kouros.navigation.car.screen.observers.CategoryObserverCallback
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.location
class CategoriesScreen( class CategoriesScreen(
private val carContext: CarContext, private val carContext: CarContext,
@@ -100,7 +93,7 @@ fun carIcon(context: CarContext, category: String, index: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(context, resId)).build() return CarIcon.Builder(IconCompat.createWithResource(context, resId)).build()
} else { } else {
return CarIcon.Builder( return CarIcon.Builder(
NavigationUtils(context).createNumberIcon( createNumberIcon(
category, category,
index.toString() index.toString()
) )

View File

@@ -21,7 +21,6 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.screen.observers.CategoryObserver import com.kouros.navigation.car.screen.observers.CategoryObserver
import com.kouros.navigation.car.screen.observers.CategoryObserverCallback import com.kouros.navigation.car.screen.observers.CategoryObserverCallback
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
@@ -130,7 +129,7 @@ class CategoryScreen(
val row = Row.Builder() val row = Row.Builder()
.setOnClickListener { .setOnClickListener {
val location = location(it.lon, it.lat) val location = location(it.lon, it.lat)
surfaceRenderer.setCategoryLocation(location, category) surfaceRenderer.setCategoryLocation(location)
} }
.setTitle(name) .setTitle(name)
.setImage(carIcon(carContext, category, index)) .setImage(carIcon(carContext, category, index))

View File

@@ -9,7 +9,6 @@ import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
import androidx.car.app.model.ActionStrip import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import androidx.car.app.model.Header import androidx.car.app.model.Header
@@ -31,28 +30,25 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.observers.NavigationObserverCallback import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
import com.kouros.navigation.car.screen.observers.NavigationObserverManager import com.kouros.navigation.car.screen.observers.NavigationObserverManager
import com.kouros.navigation.car.screen.settings.SettingsScreen import com.kouros.navigation.car.screen.settings.SettingsScreen
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.GeoUtils import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.formattedDistance import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.maplibre.spatialk.geojson.Position
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
@@ -70,8 +66,6 @@ open class NavigationScreen(
private val navigationViewModel: NavigationViewModel private val navigationViewModel: NavigationViewModel
) : Screen(carContext), NavigationObserverCallback { ) : Screen(carContext), NavigationObserverCallback {
val backGroundColor = CarColor.GREEN
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER) var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
var recentPlaces = mutableListOf<Place>() var recentPlaces = mutableListOf<Place>()
@@ -191,7 +185,7 @@ open class NavigationScreen(
) )
}) })
) )
.setBackgroundColor(backGroundColor) .setBackgroundColor(routeModel.backGroundColor())
.build() .build()
} }
@@ -200,7 +194,7 @@ open class NavigationScreen(
*/ */
private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template { private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setBackgroundColor(backGroundColor) .setBackgroundColor(routeModel.backGroundColor())
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip( .setMapActionStrip(
mapActionStrip( mapActionStrip(
@@ -261,7 +255,7 @@ open class NavigationScreen(
) )
.build() .build()
) )
.setBackgroundColor(backGroundColor) .setBackgroundColor(routeModel.backGroundColor())
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip( .setMapActionStrip(
mapActionStrip( mapActionStrip(
@@ -298,7 +292,7 @@ open class NavigationScreen(
) )
} }
val listBuilder = ItemList.Builder() val listBuilder = ItemList.Builder()
recentPlaces.filter { it.category == Constants.RECENT }.forEach { recentPlaces.filter { it.category == Constants.RECENT && it.distance > 300F }.forEach {
val row = Row.Builder() val row = Row.Builder()
.setTitle(it.name!!) .setTitle(it.name!!)
.addAction( .addAction(
@@ -355,7 +349,7 @@ open class NavigationScreen(
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build()) .setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setBackgroundColor(backGroundColor) .setBackgroundColor(routeModel.backGroundColor())
.build() .build()
} }
@@ -421,12 +415,6 @@ open class NavigationScreen(
* Creates an action to start the settings screen. * Creates an action to start the settings screen.
*/ */
private fun settingsAction(): Action { private fun settingsAction(): Action {
// return Action.Builder()
// .setIcon(createCarIcon(carContext, R.drawable.settings_48px))
// .setOnClickListener {
// screenManager.push(SettingsScreen(carContext, navigationViewModel))
// }
// .build()
return createAction( return createAction(
carContext, R.drawable.settings_48px, carContext, R.drawable.settings_48px,
0, 0,
@@ -514,13 +502,7 @@ open class NavigationScreen(
navigationViewModel.route.value = preview navigationViewModel.route.value = preview
} }
routeModel.navState = routeModel.navState.copy(destination = place) routeModel.navState = routeModel.navState.copy(destination = place)
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.activateNavigationView()
surfaceRenderer.updateCameraPosition(
0.0,
16.0,
Position(surfaceRenderer.lastLocation.longitude, surfaceRenderer.lastLocation.latitude),
TILT
)
invalidate() invalidate()
} }
@@ -530,7 +512,6 @@ open class NavigationScreen(
fun stopNavigation() { fun stopNavigation() {
navigationType = NavigationType.VIEW navigationType = NavigationType.VIEW
listener.stopNavigation() listener.stopNavigation()
surfaceRenderer.routeData.value = ""
lastCameraSearch = 0 lastCameraSearch = 0
invalidate() invalidate()
} }
@@ -573,9 +554,9 @@ open class NavigationScreen(
* Updates navigation state with the current location, checks for arrival, and traffic updates. * Updates navigation state with the current location, checks for arrival, and traffic updates.
*/ */
fun updateTrip(location: Location) { fun updateTrip(location: Location) {
val current = LocalDateTime.now(ZoneOffset.UTC) val currentDate = LocalDateTime.now(ZoneOffset.UTC)
checkRoute(current, location) checkRoute(currentDate, location)
checkTraffic(current, location) checkTraffic(currentDate, location)
updateSpeedCamera(location) updateSpeedCamera(location)
@@ -588,11 +569,11 @@ open class NavigationScreen(
/** /**
* Checks if a new route is needed based on the time since the last update. * Checks if a new route is needed based on the time since the last update.
*/ */
private fun checkRoute(current: LocalDateTime, location: Location) { private fun checkRoute(currentDate: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastRouteDate) val duration = Duration.between(currentDate, lastRouteDate)
val routeUpdate = routeModel.curRoute.summary.duration / 6 val routeUpdate = routeModel.curRoute.summary.duration / 4
if (duration.abs().seconds > routeUpdate) { if (duration.abs().seconds > routeUpdate) {
lastRouteDate = current lastRouteDate = currentDate
val destination = location( val destination = location(
routeModel.navState.destination.longitude, routeModel.navState.destination.longitude,
routeModel.navState.destination.latitude routeModel.navState.destination.latitude

View File

@@ -36,10 +36,9 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.route.Routes import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
@@ -324,7 +323,7 @@ class RoutePreviewScreen(
.addAction(navigateAction) .addAction(navigateAction)
if (route.summary.trafficDelay > 60) { if (route.summary.trafficDelay > 60) {
row.addText(createDelay(route)) row.addText(createDelay(route))
row.setImage(NavigationUtils(carContext).createCarIcon(R.drawable.traffic_jam_48px)) row.setImage(createCarIcon(carContext = carContext, R.drawable.traffic_jam_48px))
} }
return row.build() return row.build()
} }

View File

@@ -1,13 +1,63 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.ActionStrip import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.Row
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.kouros.navigation.car.ViewStyle import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.data.ViewStyle
fun buildRowForTemplate(carContext: CarContext, title: Int, resource: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
resource
)
)
.build()
)
.build()
}
fun createNumberIcon(category: String, number: String): IconCompat {
val size = 24
val bitmap = createBitmap(size, size)
val canvas = Canvas(bitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = size * 0.7f
textAlign = Paint.Align.CENTER
isFakeBoldText = true
}
val xPos = size / 2f
val yPos = (size / 2f) - ((paint.descent() + paint.ascent()) / 2f)
val color = when (category) {
CHARGING_STATION -> Color.GREEN
FUEL_STATION -> Color.BLUE
PHARMACY -> Color.RED
else -> Color.WHITE
}
paint.color = color
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
paint.color = Color.WHITE
canvas.drawText(number, xPos, yPos, paint)
return IconCompat.createWithBitmap(bitmap)
}
fun createActionStrip(executeAction: () -> Action): ActionStrip { fun createActionStrip(executeAction: () -> Action): ActionStrip {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder() val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
@@ -31,7 +81,12 @@ fun createActionStrip(executeAction: () -> Action): ActionStrip {
/** /**
* 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(viewStyle: ViewStyle, zoomPlus: () -> Action, zoomMinus: () -> Action , panAction: () -> Action): ActionStrip { fun mapActionStrip(
viewStyle: ViewStyle,
zoomPlus: () -> Action,
zoomMinus: () -> Action,
panAction: () -> Action
): ActionStrip {
val actionStripBuilder = ActionStrip.Builder() val actionStripBuilder = ActionStrip.Builder()
.addAction(zoomPlus()) .addAction(zoomPlus())
.addAction(zoomMinus()) .addAction(zoomMinus())
@@ -47,7 +102,12 @@ fun mapActionStrip(viewStyle: ViewStyle, zoomPlus: () -> Action, zoomMinus: () -
/** /**
* Creates an action to do something. * Creates an action to do something.
*/ */
fun createAction(carContext: CarContext, @DrawableRes iconRes: Int, flag: Int = FLAG_DEFAULT, onClickAction: () -> Unit): Action { fun createAction(
carContext: CarContext,
@DrawableRes iconRes: Int,
flag: Int = FLAG_DEFAULT,
onClickAction: () -> Unit
): Action {
return Action.Builder() return Action.Builder()
.setIcon(createCarIcon(carContext, iconRes)) .setIcon(createCarIcon(carContext, iconRes))
.setFlags(flag) .setFlags(flag)
@@ -60,3 +120,6 @@ fun createAction(carContext: CarContext, @DrawableRes iconRes: Int, flag: Int =
fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon { fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build() return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
} }

View File

@@ -14,12 +14,12 @@ import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Category import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants.CATEGORIES import com.kouros.navigation.data.Constants.CATEGORIES
import com.kouros.navigation.data.Constants.FAVORITES import com.kouros.navigation.data.Constants.FAVORITES
import com.kouros.navigation.data.Constants.RECENT import com.kouros.navigation.data.Constants.RECENT
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel

View File

@@ -13,7 +13,7 @@ import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.navigation.NavigationUtils import com.kouros.navigation.car.screen.buildRowForTemplate
import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -40,19 +40,22 @@ class AudioSettings(
val radioList = val radioList =
ItemList.Builder() ItemList.Builder()
.addItem( .addItem(
NavigationUtils(carContext).buildRowForTemplate( buildRowForTemplate(
carContext,
R.string.muted, R.string.muted,
R.drawable.volume_off_24px R.drawable.volume_off_24px
) )
) )
.addItem( .addItem(
NavigationUtils(carContext).buildRowForTemplate( buildRowForTemplate(
carContext,
R.string.unmuted, R.string.unmuted,
R.drawable.volume_up_24px, R.drawable.volume_up_24px,
) )
) )
.addItem( .addItem(
NavigationUtils(carContext).buildRowForTemplate( buildRowForTemplate(
carContext,
R.string.alerts_only, R.string.alerts_only,
R.drawable.warning_24px, R.drawable.warning_24px,
) )

View File

@@ -0,0 +1,77 @@
package com.kouros.navigation.car.screen.settings
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.screen.buildRowForTemplate
import com.kouros.navigation.data.EngineType
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class CarSettings(
private val carContext: CarContext,
private var navigationViewModel: NavigationViewModel
) :
Screen(carContext) {
private var engineType = EngineType.COMBUSTION.ordinal
val settingsViewModel = getSettingsViewModel(carContext)
init {
lifecycleScope.launch {
settingsViewModel.engineType.first()
}
}
override fun onGetTemplate(): Template {
engineType = settingsViewModel.engineType.value
val templateBuilder = ListTemplate.Builder()
val radioList =
ItemList.Builder()
.addItem(
buildRowForTemplate(carContext,
R.string.combustion,
R.drawable.ev_station_24px
)
)
.addItem(
buildRowForTemplate(carContext,
R.string.electric,
R.drawable.electric_car_24px
)
)
.setOnSelectedListener { index: Int ->
this.onSelected(index)
}
.setSelectedIndex(engineType)
.build()
return templateBuilder
.addSectionedList(
SectionedItemList.create(
radioList,
carContext.getString(R.string.engine_type)
)
)
.setHeader(
Header.Builder()
.setTitle(carContext.getString(R.string.car_settings))
.setStartHeaderAction(Action.BACK)
.build()
)
.build()
}
private fun onSelected(index: Int) {
settingsViewModel.onEngineTypeChanged(index)
}
}

View File

@@ -11,7 +11,7 @@ import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template import androidx.car.app.model.Template
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.navigation.NavigationUtils import com.kouros.navigation.car.screen.buildRowForTemplate
import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -34,19 +34,19 @@ class DarkModeSettings(private val carContext: CarContext) : Screen(carContext)
val radioList = val radioList =
ItemList.Builder() ItemList.Builder()
.addItem( .addItem(
NavigationUtils(carContext).buildRowForTemplate( buildRowForTemplate(carContext,
R.string.off_action_title, R.string.off_action_title,
R.drawable.light_mode_24px R.drawable.light_mode_24px
) )
) )
.addItem( .addItem(
NavigationUtils(carContext).buildRowForTemplate( buildRowForTemplate(carContext,
R.string.on_action_title, R.string.on_action_title,
R.drawable.dark_mode_24px R.drawable.dark_mode_24px
) )
) )
.addItem( .addItem(
NavigationUtils(carContext).buildRowForTemplate( buildRowForTemplate(carContext,
R.string.use_car_settings, R.string.use_car_settings,
R.drawable.directions_car_24px R.drawable.directions_car_24px
) )

View File

@@ -13,7 +13,7 @@ import androidx.car.app.model.Toggle
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.navigation.NavigationUtils import com.kouros.navigation.car.screen.createCarIcon
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -62,7 +62,7 @@ class NavigationSettings(
buildRowForTemplate( buildRowForTemplate(
R.string.avoid_highways_row_title, R.string.avoid_highways_row_title,
highwayToggle, highwayToggle,
NavigationUtils(carContext).createCarIcon(R.drawable.baseline_add_road_24) createCarIcon(carContext, R.drawable.baseline_add_road_24)
) )
) )
@@ -72,7 +72,7 @@ class NavigationSettings(
settingsViewModel.onAvoidTollway(checked) settingsViewModel.onAvoidTollway(checked)
tollWayToggleState = !tollWayToggleState tollWayToggleState = !tollWayToggleState
}.setChecked(tollWayToggleState).build() }.setChecked(tollWayToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle, NavigationUtils(carContext).createCarIcon(R.drawable.baseline_toll_24))) listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle, createCarIcon(carContext,R.drawable.baseline_toll_24)))
// Ferry // Ferry
val ferryToggle: Toggle = val ferryToggle: Toggle =
@@ -80,7 +80,7 @@ class NavigationSettings(
settingsViewModel.onAvoidFerry(checked) settingsViewModel.onAvoidFerry(checked)
ferryToggleState = !ferryToggleState ferryToggleState = !ferryToggleState
}.setChecked(ferryToggleState).build() }.setChecked(ferryToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_ferries, ferryToggle, NavigationUtils(carContext).createCarIcon(R.drawable.baseline_directions_boat_filled_24))) listBuilder.addItem(buildRowForTemplate(R.string.avoid_ferries, ferryToggle, createCarIcon(carContext, R.drawable.baseline_directions_boat_filled_24)))
// CarLocation // CarLocation
val carLocationToggle: Toggle = val carLocationToggle: Toggle =
@@ -93,7 +93,7 @@ class NavigationSettings(
buildRowForTemplate( buildRowForTemplate(
R.string.use_car_location, R.string.use_car_location,
carLocationToggle, carLocationToggle,
NavigationUtils(carContext).createCarIcon(R.drawable.ic_place_white_24dp) createCarIcon(carContext,R.drawable.ic_place_white_24dp)
) )
) )

View File

@@ -85,7 +85,7 @@ class SettingsScreen(
) )
) )
// Navigation -------------- // Drive settings --------------
listBuilder = ItemList.Builder() listBuilder = ItemList.Builder()
listBuilder.addItem( listBuilder.addItem(
buildRowForTemplate( buildRowForTemplate(
@@ -94,10 +94,17 @@ class SettingsScreen(
) )
) )
listBuilder.addItem(
buildRowForTemplate(
CarSettings(carContext, navigationViewModel),
R.string.car_settings
)
)
templateBuilder.addSectionedList( templateBuilder.addSectionedList(
SectionedItemList.create( SectionedItemList.create(
listBuilder.build(), listBuilder.build(),
carContext.getString(R.string.navigation_settings) carContext.getString(R.string.drive_settings)
) )
) )
@@ -109,6 +116,8 @@ class SettingsScreen(
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)
.build()) .build())
.build() .build()
} }
private fun getTitle(): String { private fun getTitle(): String {

View File

@@ -2,7 +2,9 @@ package com.kouros.navigation.data
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val NavigationColor = Color(0xFF16BBB6) val NavigationColorLight = Color(0xFF066462)
val NavigationColorDark = Color(0xFF10DED9)
val RouteColor = Color(0xFF7B06E1) val RouteColor = Color(0xFF7B06E1)

View File

@@ -77,9 +77,10 @@ data class Locations (
) )
data class SearchFilter( data class SearchFilter(
var avoidMotorway: Boolean = false, val avoidMotorway: Boolean = false,
var avoidTollway : Boolean = false, val avoidTollway : Boolean = false,
var avoidFerry : Boolean = false, val avoidFerry : Boolean = false,
val engineType : Int = 0,
) )
@@ -139,11 +140,27 @@ object Constants {
const val TILT = 60.0 const val TILT = 60.0
} }
/**
* Enum representing different map view modes.
* - VIEW: Active navigation mode with follow-car camera
* - PREVIEW: Route overview before starting navigation
* - PAN_VIEW: User-controlled map panning
* - AMENITY_VIEW: Displaying POI/amenity locations
*/
enum class ViewStyle {
VIEW, PREVIEW, PAN_VIEW, AMENITY_VIEW
}
enum class RouteEngine { enum class RouteEngine {
VALHALLA, OSRM, TOMTOM VALHALLA, OSRM, TOMTOM
} }
enum class EngineType {
COMBUSTION, ELECTRIC
}
enum class NavigationThemeColor(val color: Long) { enum class NavigationThemeColor(val color: Long) {
RED(0xFFD32F2F), RED(0xFFD32F2F),
ORANGE(0xFFF57C00), ORANGE(0xFFF57C00),

View File

@@ -2,6 +2,7 @@ package com.kouros.navigation.data
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.util.Log
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
import java.net.Authenticator import java.net.Authenticator
import java.net.HttpURLConnection import java.net.HttpURLConnection
@@ -66,7 +67,7 @@ abstract class NavigationRepository {
} }
}) })
} }
println(url) Log.d("fetchUrl", url)
val httpURLConnection = URL(url).openConnection() as HttpURLConnection val httpURLConnection = URL(url).openConnection() as HttpURLConnection
httpURLConnection.setRequestProperty( httpURLConnection.setRequestProperty(
"Accept", "Accept",
@@ -81,7 +82,7 @@ abstract class NavigationRepository {
return response return response
} }
} catch (e: Exception) { } catch (e: Exception) {
println("Exception ${e.message}") Log.e("fetchUrl", e.toString())
} }
return "" return ""
} }

View File

@@ -1,5 +1,6 @@
package com.kouros.navigation.data package com.kouros.navigation.data
import android.util.Log
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.kouros.navigation.data.osrm.OsrmResponse import com.kouros.navigation.data.osrm.OsrmResponse
import com.kouros.navigation.data.osrm.OsrmRoute import com.kouros.navigation.data.osrm.OsrmRoute

View File

@@ -8,6 +8,7 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.kouros.navigation.data.EngineType
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -55,6 +56,8 @@ class DataStoreManager(private val context: Context) {
val TRIP_SUGGESTION = booleanPreferencesKey("TripSuggestion") val TRIP_SUGGESTION = booleanPreferencesKey("TripSuggestion")
val ENGINE_TYPE = intPreferencesKey("EngineType")
} }
// Read values // Read values
@@ -136,6 +139,12 @@ class DataStoreManager(private val context: Context) {
preferences[PreferencesKeys.TRIP_SUGGESTION] == true preferences[PreferencesKeys.TRIP_SUGGESTION] == true
} }
val engineTypeFlow: Flow<Int> =
context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.ENGINE_TYPE]
?: EngineType.COMBUSTION.ordinal
}
// Save values // Save values
suspend fun setShow3D(enabled: Boolean) { suspend fun setShow3D(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
@@ -220,4 +229,10 @@ class DataStoreManager(private val context: Context) {
preferences[PreferencesKeys.TRIP_SUGGESTION] = enabled preferences[PreferencesKeys.TRIP_SUGGESTION] = enabled
} }
} }
suspend fun setEngineType(mode: Int) {
context.dataStore.edit { prefs ->
prefs[PreferencesKeys.ENGINE_TYPE] = mode
}
}
} }

View File

@@ -3,6 +3,7 @@ package com.kouros.navigation.data.tomtom
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.EngineType
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
@@ -19,9 +20,9 @@ const val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incident
private const val tomtomFields = private const val tomtomFields =
"{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}" "{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}"
const val useLocal = true const val useLocal = false
const val useLocalTraffic = true const val useLocalTraffic = false
class TomTomRepository : NavigationRepository() { class TomTomRepository : NavigationRepository() {
@@ -38,6 +39,7 @@ class TomTomRepository : NavigationRepository() {
false false
) )
} }
var engineType = "combustion"
var filter = "" var filter = ""
if (searchFilter.avoidMotorway) { if (searchFilter.avoidMotorway) {
filter = "$filter&avoid=motorways" filter = "$filter&avoid=motorways"
@@ -48,6 +50,9 @@ class TomTomRepository : NavigationRepository() {
if (searchFilter.avoidFerry) { if (searchFilter.avoidFerry) {
filter = "$filter&avoid=ferries" filter = "$filter&avoid=ferries"
} }
if (searchFilter.engineType == EngineType.ELECTRIC.ordinal) {
engineType = "electric"
}
val repository = getSettingsRepository(context) val repository = getSettingsRepository(context)
val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() } val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() }
val currentLocale = Locale.getDefault() val currentLocale = Locale.getDefault()
@@ -60,7 +65,7 @@ class TomTomRepository : NavigationRepository() {
"&instructionsType=text&language=$language&sectionType=lanes" + "&instructionsType=text&language=$language&sectionType=lanes" +
"&routeRepresentation=encodedPolyline" + "&routeRepresentation=encodedPolyline" +
"&maxAlternatives=2" + "&maxAlternatives=2" +
"&vehicleEngineType=combustion$filter&key=$tomtomApiKey" "&vehicleEngineType=$engineType$filter&key=$tomtomApiKey"
return fetchUrl( return fetchUrl(
url, url,
false false

View File

@@ -163,7 +163,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
} }
recentPlaces.postValue(pl) recentPlaces.postValue(pl.sortedBy { it.distance })
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -538,7 +538,8 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
val avoidMotorway = runBlocking { repository.avoidMotorwayFlow.first() } val avoidMotorway = runBlocking { repository.avoidMotorwayFlow.first() }
val avoidTollway = runBlocking { repository.avoidTollwayFlow.first() } val avoidTollway = runBlocking { repository.avoidTollwayFlow.first() }
val avoidFerry = runBlocking { repository.avoidFerryFlow.first() } val avoidFerry = runBlocking { repository.avoidFerryFlow.first() }
return SearchFilter(avoidMotorway, avoidTollway, avoidFerry) val engineType = runBlocking { repository.engineTypeFlow.first() }
return SearchFilter(avoidMotorway, avoidTollway, avoidFerry, engineType)
} }
/** /**

View File

@@ -1,6 +1,7 @@
package com.kouros.navigation.model package com.kouros.navigation.model
import android.location.Location import android.location.Location
import android.util.Log
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import com.kouros.navigation.data.Constants.MAXIMUM_LOCATION_DISTANCE import com.kouros.navigation.data.Constants.MAXIMUM_LOCATION_DISTANCE
import com.kouros.navigation.data.Constants.NEAREST_LOCATION_DISTANCE import com.kouros.navigation.data.Constants.NEAREST_LOCATION_DISTANCE

View File

@@ -96,6 +96,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
false false
) )
val engineType = repository.engineTypeFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
0
)
fun onShow3DChanged(enabled: Boolean) { fun onShow3DChanged(enabled: Boolean) {
viewModelScope.launch { repository.setShow3D(enabled) } viewModelScope.launch { repository.setShow3D(enabled) }
} }
@@ -148,4 +154,9 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
fun onTripSuggestion(enabled: Boolean) { fun onTripSuggestion(enabled: Boolean) {
viewModelScope.launch { repository.setTripSuggestion(enabled) } viewModelScope.launch { repository.setTripSuggestion(enabled) }
} }
fun onEngineTypeChanged(mode: Int) {
viewModelScope.launch { repository.setEngineType(mode) }
}
} }

View File

@@ -47,6 +47,9 @@ class SettingsRepository(
val tripSuggestionFlow: Flow<Boolean> = val tripSuggestionFlow: Flow<Boolean> =
dataStoreManager.tripSuggestionFlow dataStoreManager.tripSuggestionFlow
val engineTypeFlow: Flow<Int> =
dataStoreManager.engineTypeFlow
suspend fun setShow3D(enabled: Boolean) { suspend fun setShow3D(enabled: Boolean) {
dataStoreManager.setShow3D(enabled) dataStoreManager.setShow3D(enabled)
} }
@@ -102,4 +105,8 @@ class SettingsRepository(
suspend fun setTripSuggestion(enabled: Boolean) { suspend fun setTripSuggestion(enabled: Boolean) {
dataStoreManager.setTripSuggestion(enabled) dataStoreManager.setTripSuggestion(enabled)
} }
suspend fun setEngineType(mode: Int) {
dataStoreManager.setEngineType(mode)
}
} }

View File

@@ -10,7 +10,6 @@ import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.lang.Math.toDegrees import java.lang.Math.toDegrees
@@ -29,6 +28,8 @@ 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.seconds import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
import kotlin.time.toDuration
object NavigationUtils { object NavigationUtils {
@@ -50,7 +51,7 @@ fun calculateZoom(speed: Double?): Double {
} }
val speedKmh = (speed * 3.6).toInt() val speedKmh = (speed * 3.6).toInt()
val zoom = when (speedKmh) { val zoom = when (speedKmh) {
in 0..10 -> 18.0 in 0..10 -> 17.0
in 11..30 -> 17.5 in 11..30 -> 17.5
in 31..65 -> 17.0 in 31..65 -> 17.0
in 66..70 -> 16.5 in 66..70 -> 16.5
@@ -128,14 +129,20 @@ fun Double.round(numFractionDigits: Int): Double {
return (this * factor).roundToInt() / factor return (this * factor).roundToInt() / factor
} }
fun duration(preview: Boolean, bearing: Double, lastBearing: Double): Duration { fun duration(
preview: Boolean,
bearing: Double,
lastBearing: Double,
lastLocationUpdate: LocalDateTime
): Duration {
if (preview) { if (preview) {
return 3.seconds return 3.seconds
} }
val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) { val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) {
2.seconds 2.seconds
} else { } else {
1.seconds val updateDuration = java.time.Duration.between(LocalDateTime.now(), lastLocationUpdate)
((updateDuration!!.toMillis().absoluteValue * 1.2).toDuration(DurationUnit.MILLISECONDS))
} }
return cameraDuration return cameraDuration
} }

View File

@@ -66,4 +66,9 @@
<string name="general">Allgemein</string> <string name="general">Allgemein</string>
<string name="traffic">Verkehr anzeigen</string> <string name="traffic">Verkehr anzeigen</string>
<string name="trip_suggestion">Fahrten-Vorschläge</string> <string name="trip_suggestion">Fahrten-Vorschläge</string>
<string name="drive_settings">Drive settings</string>
<string name="car_settings">Car settings</string>
<string name="combustion">Combustion</string>
<string name="electric">Electric</string>
<string name="engine_type">Engine type</string>
</resources> </resources>

View File

@@ -50,4 +50,9 @@
<string name="general">Γενικά</string> <string name="general">Γενικά</string>
<string name="traffic">Εμφάνιση κίνησης</string> <string name="traffic">Εμφάνιση κίνησης</string>
<string name="trip_suggestion">Προτάσεις διαδρομής</string> <string name="trip_suggestion">Προτάσεις διαδρομής</string>
<string name="drive_settings">Drive settings</string>
<string name="car_settings">Car settings</string>
<string name="combustion">Combustion</string>
<string name="electric">Electric</string>
<string name="engine_type">Engine type</string>
</resources> </resources>

View File

@@ -50,4 +50,9 @@
<string name="general">Ogólne</string> <string name="general">Ogólne</string>
<string name="traffic">Pokaż natężenie ruchu</string> <string name="traffic">Pokaż natężenie ruchu</string>
<string name="trip_suggestion">Sugestie dotyczące podróży</string> <string name="trip_suggestion">Sugestie dotyczące podróży</string>
<string name="drive_settings">Drive settings</string>
<string name="car_settings">Car settings</string>
<string name="combustion">Combustion</string>
<string name="electric">Electric</string>
<string name="engine_type">Engine type</string>
</resources> </resources>

View File

@@ -53,4 +53,9 @@
<string name="general">General</string> <string name="general">General</string>
<string name="traffic">Show traffic</string> <string name="traffic">Show traffic</string>
<string name="trip_suggestion">Trip suggestions</string> <string name="trip_suggestion">Trip suggestions</string>
<string name="drive_settings">Drive settings</string>
<string name="car_settings">Car settings</string>
<string name="combustion">Combustion</string>
<string name="electric">Electric</string>
<string name="engine_type">Engine type</string>
</resources> </resources>