Categories
This commit is contained in:
@@ -13,8 +13,8 @@ android {
|
|||||||
applicationId = "com.kouros.navigation"
|
applicationId = "com.kouros.navigation"
|
||||||
minSdk = 33
|
minSdk = 33
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 61
|
versionCode = 64
|
||||||
versionName = "0.2.0.61"
|
versionName = "0.2.0.64"
|
||||||
base.archivesName = "navi-$versionName"
|
base.archivesName = "navi-$versionName"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,39 +6,43 @@ import android.app.AppOpsManager
|
|||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.speech.tts.TextToSpeech
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
|
import androidx.compose.animation.core.animateIntAsState
|
||||||
|
import androidx.compose.foundation.gestures.detectVerticalDragGestures
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.BottomSheetScaffold
|
import androidx.compose.material3.BottomSheetScaffold
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
@@ -46,7 +50,9 @@ import com.google.android.gms.location.FusedLocationProviderClient
|
|||||||
import com.google.android.gms.location.LocationServices
|
import com.google.android.gms.location.LocationServices
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
|
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
|
||||||
|
import com.kouros.navigation.car.TextToSpeechManager
|
||||||
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
||||||
|
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
|
||||||
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.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
@@ -77,7 +83,6 @@ import org.maplibre.compose.location.Location
|
|||||||
import org.maplibre.compose.location.rememberDefaultLocationProvider
|
import org.maplibre.compose.location.rememberDefaultLocationProvider
|
||||||
import org.maplibre.compose.location.rememberUserLocationState
|
import org.maplibre.compose.location.rememberUserLocationState
|
||||||
import org.maplibre.spatialk.geojson.Position
|
import org.maplibre.spatialk.geojson.Position
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
|
||||||
@@ -94,7 +99,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
val nextStepData: MutableLiveData<StepData> by lazy {
|
val nextStepData: MutableLiveData<StepData> by lazy {
|
||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastStepIndex = -1
|
var lastStepIndex = -1
|
||||||
var lastLocation = location(0.0, 0.0)
|
var lastLocation = location(0.0, 0.0)
|
||||||
val observer = Observer<String> { newRoute ->
|
val observer = Observer<String> { newRoute ->
|
||||||
@@ -108,7 +112,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var textToSpeech: TextToSpeech
|
|
||||||
|
|
||||||
private fun checkMock() {
|
private fun checkMock() {
|
||||||
if (useMock) {
|
if (useMock) {
|
||||||
@@ -118,7 +121,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
SimulationType.GPX -> gpx(
|
SimulationType.GPX -> gpx(
|
||||||
context = applicationContext, mock
|
context = applicationContext, mock
|
||||||
)
|
)
|
||||||
|
|
||||||
SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock)
|
SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,6 +137,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
private lateinit var mock: MockLocation
|
private lateinit var mock: MockLocation
|
||||||
private var loadRecentPlaces = false
|
private var loadRecentPlaces = false
|
||||||
|
|
||||||
|
lateinit var textToSpeechManager: TextToSpeechManager
|
||||||
|
|
||||||
|
var guidanceAudio = 0
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
if (simulationJob != null) {
|
if (simulationJob != null) {
|
||||||
simulationJob?.cancel()
|
simulationJob?.cancel()
|
||||||
@@ -146,11 +152,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
textToSpeech = TextToSpeech(applicationContext) { status ->
|
textToSpeechManager = TextToSpeechManager(applicationContext)
|
||||||
if (status == TextToSpeech.SUCCESS) {
|
val repository = getSettingsRepository(applicationContext)
|
||||||
textToSpeech.language = Locale.getDefault()
|
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
|
||||||
}
|
guidanceAudio = it
|
||||||
}
|
})
|
||||||
|
|
||||||
if (useMock) {
|
if (useMock) {
|
||||||
checkMockLocationEnabled()
|
checkMockLocationEnabled()
|
||||||
@@ -158,12 +164,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
|
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
|
||||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
|
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
|
||||||
fusedLocationClient.lastLocation.addOnSuccessListener { _: android.location.Location? ->
|
fusedLocationClient.lastLocation.addOnSuccessListener { _: android.location.Location? ->
|
||||||
|
navigationViewModel.route.observe(this, observer)
|
||||||
if (useMock) {
|
if (useMock) {
|
||||||
mock = MockLocation(locationManager)
|
mock = MockLocation(locationManager)
|
||||||
mock.setMockLocation(
|
mock.setMockLocation(
|
||||||
homeVogelhart.latitude, homeVogelhart.longitude, 0F
|
homeVogelhart.latitude, homeVogelhart.longitude, 0F
|
||||||
)
|
)
|
||||||
navigationViewModel.route.observe(this, observer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@@ -195,19 +201,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun StartScreen(
|
fun StartScreen(
|
||||||
navController: NavHostController
|
navController: NavHostController) {
|
||||||
) {
|
|
||||||
|
|
||||||
val appViewModel: AppViewModel = appViewModel()
|
val appViewModel: AppViewModel = appViewModel()
|
||||||
val darkMode by appViewModel.darkMode.collectAsState()
|
val darkMode by appViewModel.darkMode.collectAsState()
|
||||||
|
|
||||||
val baseStyle = BaseStyleModel().readStyle(applicationContext, darkMode, darkMode == 1)
|
val baseStyle = BaseStyleModel().readStyle(applicationContext, darkMode, darkMode == 1)
|
||||||
val scaffoldState = rememberBottomSheetScaffoldState()
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val sheetPeekHeight = 180.dp
|
|
||||||
val sheetPeekHeightState = remember { mutableStateOf(sheetPeekHeight) }
|
|
||||||
|
|
||||||
val locationProvider = rememberDefaultLocationProvider(
|
val locationProvider = rememberDefaultLocationProvider(
|
||||||
updateInterval = 0.5.seconds, desiredAccuracy = DesiredAccuracy.Highest
|
updateInterval = 0.5.seconds, desiredAccuracy = DesiredAccuracy.Highest
|
||||||
)
|
)
|
||||||
@@ -218,40 +216,98 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
val step: StepData? by stepData.observeAsState()
|
val step: StepData? by stepData.observeAsState()
|
||||||
val nextStep: StepData? by nextStepData.observeAsState()
|
val nextStep: StepData? by nextStepData.observeAsState()
|
||||||
|
|
||||||
|
var collapsedHeight by remember {
|
||||||
|
mutableIntStateOf(300)
|
||||||
|
}
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
fun closeSheet() {
|
fun closeSheet() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
scaffoldState.bottomSheetState.partialExpand()
|
collapsedHeight = if (routeModel.isNavigating()) {
|
||||||
sheetPeekHeightState.value = 50.dp
|
150
|
||||||
|
} else {
|
||||||
|
300
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val screenHeight = configuration.screenHeightDp
|
||||||
|
var expandedType by remember {
|
||||||
|
mutableStateOf(ExpandedType.COLLAPSED)
|
||||||
|
}
|
||||||
|
|
||||||
|
val height by animateIntAsState(
|
||||||
|
when (expandedType) {
|
||||||
|
ExpandedType.HALF -> screenHeight / 2
|
||||||
|
ExpandedType.FULL -> screenHeight
|
||||||
|
ExpandedType.COLLAPSED -> collapsedHeight
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState()
|
||||||
NavigationTheme(useDarkTheme = darkMode == 1) {
|
NavigationTheme(useDarkTheme = darkMode == 1) {
|
||||||
BottomSheetScaffold(
|
BottomSheetScaffold(
|
||||||
snackbarHost = {
|
scaffoldState = bottomSheetScaffoldState,
|
||||||
SnackbarHost(hostState = snackbarHostState)
|
sheetShape = RoundedCornerShape(
|
||||||
},
|
bottomStart = 0.dp,
|
||||||
scaffoldState = scaffoldState,
|
bottomEnd = 0.dp,
|
||||||
sheetPeekHeight = sheetPeekHeightState.value,
|
topStart = 12.dp,
|
||||||
|
topEnd = 12.dp
|
||||||
|
),
|
||||||
sheetContent = {
|
sheetContent = {
|
||||||
SheetContent(step, nextStep) { closeSheet() }
|
var isUpdated = false
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(height.dp)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectVerticalDragGestures(
|
||||||
|
onVerticalDrag = { change, dragAmount ->
|
||||||
|
change.consume()
|
||||||
|
if (!isUpdated) {
|
||||||
|
expandedType = when {
|
||||||
|
dragAmount < 0 && expandedType == ExpandedType.COLLAPSED -> {
|
||||||
|
ExpandedType.HALF
|
||||||
|
}
|
||||||
|
dragAmount < 0 && expandedType == ExpandedType.HALF -> {
|
||||||
|
ExpandedType.FULL
|
||||||
|
}
|
||||||
|
|
||||||
|
dragAmount > 0 && expandedType == ExpandedType.FULL -> {
|
||||||
|
ExpandedType.HALF
|
||||||
|
}
|
||||||
|
|
||||||
|
dragAmount > 0 && expandedType == ExpandedType.HALF -> {
|
||||||
|
ExpandedType.COLLAPSED
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
ExpandedType.FULL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isUpdated = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd = {
|
||||||
|
isUpdated = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
SheetContent(step, nextStep) { closeSheet() }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) { innerPadding ->
|
sheetPeekHeight = height.dp
|
||||||
Box(
|
) {
|
||||||
modifier = Modifier
|
MapView(
|
||||||
.fillMaxSize()
|
applicationContext,
|
||||||
.padding(innerPadding),
|
userLocationState,
|
||||||
contentAlignment = Alignment.Center,
|
step,
|
||||||
) {
|
nextStep,
|
||||||
MapView(
|
cameraPosition,
|
||||||
applicationContext,
|
routeData,
|
||||||
userLocationState,
|
tilt,
|
||||||
step,
|
baseStyle,
|
||||||
nextStep,
|
)
|
||||||
cameraPosition,
|
|
||||||
routeData,
|
|
||||||
tilt,
|
|
||||||
baseStyle,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!routeModel.isNavigating()) {
|
if (!routeModel.isNavigating()) {
|
||||||
Settings(navController, modifier = Modifier.fillMaxWidth())
|
Settings(navController, modifier = Modifier.fillMaxWidth())
|
||||||
}
|
}
|
||||||
@@ -338,7 +394,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
if (isNavigating()) {
|
if (isNavigating()) {
|
||||||
updateLocation(currentLocation, navigationViewModel)
|
updateLocation(currentLocation, navigationViewModel)
|
||||||
stepData.value = currentStep()
|
stepData.value = currentStep()
|
||||||
//textToSpeech(stepData.value!!.instruction)
|
if (guidanceAudio == 1) {
|
||||||
|
textToSpeech()
|
||||||
|
}
|
||||||
if (navState.nextStep) {
|
if (navState.nextStep) {
|
||||||
nextStepData.value = nextStep()
|
nextStepData.value = nextStep()
|
||||||
}
|
}
|
||||||
@@ -377,19 +435,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
stepData.value = StepData("", "", 0.0, 0, 0, 0, 0.0)
|
stepData.value = StepData("", "", 0.0, 0, 0, 0, 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun textToSpeech(text: String) {
|
fun textToSpeech() {
|
||||||
val currentStep = routeModel.route.currentStep()
|
val currentStep = routeModel.route.currentStep()
|
||||||
val stepData = routeModel.currentStep()
|
val stepData = routeModel.currentStep()
|
||||||
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < 50) {
|
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
|
||||||
lifecycleScope.launch {
|
textToSpeechManager.speak(stepData.message)
|
||||||
try {
|
|
||||||
val cs: CharSequence = stepData.instruction
|
|
||||||
textToSpeech.speak(cs, TextToSpeech.QUEUE_FLUSH, null, "1233455")
|
|
||||||
Log.d("TTS", "speak $cs")
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.d("TTS", "speak error", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastStepIndex = currentStep.index
|
lastStepIndex = currentStep.index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,5 +470,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class ExpandedType {
|
||||||
|
HALF, FULL, COLLAPSED
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import android.content.Context
|
|||||||
import android.location.Location
|
import android.location.Location
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.captionBar
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.layout.size
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -42,6 +45,7 @@ import com.kouros.navigation.data.nominatim.SearchResult
|
|||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchSheet(
|
fun SearchSheet(
|
||||||
applicationContext: Context,
|
applicationContext: Context,
|
||||||
@@ -61,7 +65,6 @@ fun SearchSheet(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
) {
|
) {
|
||||||
// Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
|
|
||||||
SearchBar(
|
SearchBar(
|
||||||
textFieldState = textFieldState,
|
textFieldState = textFieldState,
|
||||||
searchPlaces = emptyList(),
|
searchPlaces = emptyList(),
|
||||||
@@ -72,7 +75,7 @@ fun SearchSheet(
|
|||||||
closeSheet = { closeSheet() }
|
closeSheet = { closeSheet() }
|
||||||
|
|
||||||
)
|
)
|
||||||
|
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
|
||||||
if (recentPlaces.value != null) {
|
if (recentPlaces.value != null) {
|
||||||
val items = listOf(recentPlaces)
|
val items = listOf(recentPlaces)
|
||||||
if (items.isNotEmpty()) {
|
if (items.isNotEmpty()) {
|
||||||
@@ -121,29 +124,33 @@ fun Home(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun searchPlaces(viewModel: NavigationViewModel, location: Location, it: String) {
|
||||||
|
viewModel.searchPlaces(it, location)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchBar(
|
fun SearchBar(
|
||||||
textFieldState: TextFieldState,
|
textFieldState: TextFieldState,
|
||||||
searchPlaces: List<Place>,
|
searchPlaces: List<Place>,
|
||||||
searchResults: List<SearchResult>,
|
searchResults: List<SearchResult>,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
viewModel: NavigationViewModel,
|
viewModel: NavigationViewModel,
|
||||||
context: Context,
|
context: Context,
|
||||||
location: Location,
|
location: Location,
|
||||||
closeSheet: () -> Unit
|
closeSheet: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||||
SearchBar(
|
SearchBar(
|
||||||
|
windowInsets = WindowInsets.captionBar,
|
||||||
colors = SearchBarDefaults.colors(
|
colors = SearchBarDefaults.colors(
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||||
),
|
),
|
||||||
modifier = modifier,
|
|
||||||
inputField = {
|
inputField = {
|
||||||
SearchBarDefaults.InputField(
|
SearchBarDefaults.InputField(
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.speed_camera_24px),
|
painter = painterResource(id = R.drawable.search_48px),
|
||||||
"Search",
|
"Search",
|
||||||
modifier = Modifier.size(24.dp, 24.dp),
|
modifier = Modifier.size(24.dp, 24.dp),
|
||||||
)
|
)
|
||||||
@@ -160,23 +167,19 @@ fun SearchBar(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onExpandedChange = { expanded = it },
|
onExpandedChange = { },
|
||||||
) {
|
) {
|
||||||
if (searchPlaces.isNotEmpty()) {
|
if (searchPlaces.isNotEmpty()) {
|
||||||
Text(context.getString(R.string.recent_destinations))
|
Text(context.getString(R.string.recent_destinations))
|
||||||
RecentPlaces(searchPlaces, viewModel, context, location, closeSheet)
|
RecentPlaces(searchPlaces, viewModel, context, location, closeSheet)
|
||||||
}
|
}
|
||||||
if (searchResults.isNotEmpty()) {
|
if (searchResults.isNotEmpty()) {
|
||||||
Text("Search places")
|
Text("Search places")
|
||||||
SearchPlaces( searchResults, viewModel, context, location, closeSheet)
|
SearchPlaces( searchResults, viewModel, context, location, closeSheet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchPlaces(viewModel: NavigationViewModel, location: Location, it: String) {
|
|
||||||
viewModel.searchPlaces(it, location)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SearchPlaces(
|
private fun SearchPlaces(
|
||||||
searchResults: List<SearchResult>,
|
searchResults: List<SearchResult>,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class RouteModelTest {
|
|||||||
fun checkRoute() {
|
fun checkRoute() {
|
||||||
assertEquals(true, routeModel.isNavigating())
|
assertEquals(true, routeModel.isNavigating())
|
||||||
assertEquals(routeModel.curRoute.summary.distance, 11116.0, 10.0)
|
assertEquals(routeModel.curRoute.summary.distance, 11116.0, 10.0)
|
||||||
assertEquals(routeModel.curRoute.summary.duration, 1148.0, 10.0)
|
assertEquals(routeModel.curRoute.summary.duration, 1483.0, 10.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -66,7 +66,7 @@ class RouteModelTest {
|
|||||||
val stepData = routeModel.currentStep()
|
val stepData = routeModel.currentStep()
|
||||||
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
|
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
|
||||||
assertEquals(stepData.instruction, "Silcherstraße")
|
assertEquals(stepData.instruction, "Silcherstraße")
|
||||||
assertEquals(stepData.leftStepDistance, 30.0, 1.0)
|
assertEquals(stepData.leftStepDistance, 25.0, 1.0)
|
||||||
val nextStepData = routeModel.nextStep()
|
val nextStepData = routeModel.nextStep()
|
||||||
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
|
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
|
||||||
assertEquals(nextStepData.instruction, "Schmalkaldener Straße")
|
assertEquals(nextStepData.instruction, "Schmalkaldener Straße")
|
||||||
@@ -114,7 +114,7 @@ class RouteModelTest {
|
|||||||
val stepData = routeModel.currentStep()
|
val stepData = routeModel.currentStep()
|
||||||
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT)
|
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT)
|
||||||
assertEquals(stepData.instruction, "Schenkendorfstraße")
|
assertEquals(stepData.instruction, "Schenkendorfstraße")
|
||||||
assertEquals(stepData.leftStepDistance, 170.0, 1.0)
|
assertEquals(stepData.leftStepDistance, 170.0, 10.0)
|
||||||
assertEquals(stepData.lane.size, 4)
|
assertEquals(stepData.lane.size, 4)
|
||||||
assertEquals(stepData.lane.first().valid, true)
|
assertEquals(stepData.lane.first().valid, true)
|
||||||
assertEquals(stepData.lane.last().valid, false)
|
assertEquals(stepData.lane.last().valid, false)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import com.kouros.navigation.car.screen.RequestPermissionScreen
|
|||||||
import com.kouros.navigation.car.screen.SearchScreen
|
import com.kouros.navigation.car.screen.SearchScreen
|
||||||
import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION
|
import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION
|
||||||
import com.kouros.navigation.data.Constants.GMS_CAR_SPEED_PERMISSION
|
import com.kouros.navigation.data.Constants.GMS_CAR_SPEED_PERMISSION
|
||||||
|
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
|
||||||
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
|
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
|
||||||
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
|
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
|
||||||
import com.kouros.navigation.data.Constants.TAG
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
@@ -40,6 +41,7 @@ import com.kouros.navigation.model.NavigationViewModel
|
|||||||
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
||||||
import com.kouros.navigation.utils.getSettingsRepository
|
import com.kouros.navigation.utils.getSettingsRepository
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -77,6 +79,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
var autoDriveEnabled = false
|
var autoDriveEnabled = false
|
||||||
|
|
||||||
val simulation = Simulation()
|
val simulation = Simulation()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lifecycle observer for managing session lifecycle events.
|
* Lifecycle observer for managing session lifecycle events.
|
||||||
* Cleans up resources when the session is destroyed.
|
* Cleans up resources when the session is destroyed.
|
||||||
@@ -205,8 +208,13 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
|
navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
|
||||||
override fun onAutoDriveEnabled() {
|
override fun onAutoDriveEnabled() {
|
||||||
// Called when the app should simulate navigation (e.g., for testing)
|
// Called when the app should simulate navigation (e.g., for testing)
|
||||||
// Implement your simulation logic here
|
deviceLocationManager.stopLocationUpdates()
|
||||||
autoDriveEnabled = true
|
autoDriveEnabled = true
|
||||||
|
simulation.startSimulation(
|
||||||
|
routeModel, lifecycle.coroutineScope
|
||||||
|
) { location ->
|
||||||
|
updateLocation(location)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopNavigation() {
|
override fun onStopNavigation() {
|
||||||
@@ -214,6 +222,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
// Stop turn-by-turn logic and clean up
|
// Stop turn-by-turn logic and clean up
|
||||||
routeModel.stopNavigation()
|
routeModel.stopNavigation()
|
||||||
autoDriveEnabled = false
|
autoDriveEnabled = false
|
||||||
|
deviceLocationManager.startLocationUpdates()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
|
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
|
||||||
@@ -240,6 +249,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
textToSpeechManager = TextToSpeechManager(carContext)
|
textToSpeechManager = TextToSpeechManager(carContext)
|
||||||
val repository = getSettingsRepository(carContext)
|
val repository = getSettingsRepository(carContext)
|
||||||
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
|
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
|
||||||
@@ -342,9 +352,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
* Handles route snapping, deviation detection for rerouting, and map updates.
|
* Handles route snapping, deviation detection for rerouting, and map updates.
|
||||||
*/
|
*/
|
||||||
fun updateLocation(location: Location) {
|
fun updateLocation(location: Location) {
|
||||||
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION ) {
|
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
|
||||||
surfaceRenderer.updateCarSpeed(location.speed)
|
surfaceRenderer.updateCarSpeed(location.speed)
|
||||||
}
|
}
|
||||||
updateBearing(location)
|
updateBearing(location)
|
||||||
if (routeModel.isNavigating()) {
|
if (routeModel.isNavigating()) {
|
||||||
handleNavigationLocation(location)
|
handleNavigationLocation(location)
|
||||||
@@ -410,7 +420,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
navigationManager.navigationStarted()
|
navigationManager.navigationStarted()
|
||||||
if (autoDriveEnabled) {
|
if (autoDriveEnabled) {
|
||||||
simulation.startSimulation(
|
simulation.startSimulation(
|
||||||
routeModel, lifecycle.coroutineScope
|
routeModel, lifecycle.coroutineScope
|
||||||
) { location ->
|
) { location ->
|
||||||
updateLocation(location)
|
updateLocation(location)
|
||||||
}
|
}
|
||||||
@@ -418,7 +428,6 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun updateTrip(trip: Trip) {
|
override fun updateTrip(trip: Trip) {
|
||||||
Log.d("Trip", trip.toString())
|
|
||||||
navigationManager.updateTrip(trip)
|
navigationManager.updateTrip(trip)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,10 +438,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
private fun handleGuidanceAudio() {
|
private fun handleGuidanceAudio() {
|
||||||
val currentStep = routeModel.route.currentStep()
|
val currentStep = routeModel.route.currentStep()
|
||||||
val stepData = routeModel.currentStep()
|
val stepData = routeModel.currentStep()
|
||||||
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < 50) {
|
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
|
||||||
if (textToSpeechManager.initialized) {
|
textToSpeechManager.speak(stepData.message)
|
||||||
textToSpeechManager.speak(stepData.message)
|
|
||||||
}
|
|
||||||
lastStepIndex = currentStep.index
|
lastStepIndex = currentStep.index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class SurfaceRenderer(
|
|||||||
// Current camera position state for the map
|
// Current camera position state for the map
|
||||||
private val cameraPosition = MutableLiveData(
|
private val cameraPosition = MutableLiveData(
|
||||||
CameraPosition(
|
CameraPosition(
|
||||||
zoom = 15.0,
|
zoom = 16.0,
|
||||||
target = Position(latitude = homeVogelhart.latitude, longitude = homeVogelhart.longitude)
|
target = Position(latitude = homeVogelhart.latitude, longitude = homeVogelhart.longitude)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,61 +1,81 @@
|
|||||||
package com.kouros.navigation.car
|
package com.kouros.navigation.car
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.AudioFocusRequest
|
import android.media.AudioFocusRequest
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.speech.tts.TextToSpeech
|
import android.speech.tts.TextToSpeech
|
||||||
|
import android.speech.tts.UtteranceProgressListener
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
|
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class TextToSpeechManager(private val carContext: CarContext) {
|
class TextToSpeechManager(private val carContext: Context) {
|
||||||
|
|
||||||
var textToSpeech: TextToSpeech
|
private var textToSpeech: TextToSpeech? = null
|
||||||
|
@Volatile private var initialized = false
|
||||||
|
|
||||||
var initialized = false
|
private val audioManager: AudioManager by lazy {
|
||||||
|
carContext.getSystemService(AudioManager::class.java)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private val audioAttributes = AudioAttributes.Builder()
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||||
|
.setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
|
||||||
|
.setAudioAttributes(audioAttributes)
|
||||||
|
.setOnAudioFocusChangeListener { /* Handle focus changes if needed */ }
|
||||||
|
.build()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
textToSpeech = TextToSpeech(carContext) { status ->
|
textToSpeech = TextToSpeech(carContext) { status ->
|
||||||
if (status == TextToSpeech.SUCCESS) {
|
if (status == TextToSpeech.SUCCESS) {
|
||||||
Log.d("TTS", "Initialization Success")
|
textToSpeech?.apply {
|
||||||
val audioAttributes =
|
setAudioAttributes(audioAttributes)
|
||||||
AudioAttributes.Builder()
|
setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
override fun onStart(utteranceId: String?) {}
|
||||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
|
|
||||||
.build()
|
override fun onDone(utteranceId: String?) {
|
||||||
val request =
|
// Release focus ONLY after speech is finished
|
||||||
AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
|
audioManager.abandonAudioFocusRequest(focusRequest)
|
||||||
.setAudioAttributes(audioAttributes)
|
}
|
||||||
.build()
|
|
||||||
val audioManager: AudioManager =
|
override fun onError(utteranceId: String) {
|
||||||
carContext.getSystemService<AudioManager?>(AudioManager::class.java)!!
|
audioManager.abandonAudioFocusRequest(focusRequest)
|
||||||
// Requesting the audio focus.
|
}
|
||||||
if (audioManager.requestAudioFocus(request) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
})
|
||||||
textToSpeech.setAudioAttributes(audioAttributes)
|
|
||||||
}
|
}
|
||||||
initialized = true
|
initialized = true
|
||||||
|
Log.d("TTS", "Initialization Success")
|
||||||
} else {
|
} else {
|
||||||
Log.d("TTS", "Initialization Failed")
|
Log.e("TTS", "Initialization Failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun speak(text: String) {
|
fun speak(text: String) {
|
||||||
try {
|
if (!initialized) {
|
||||||
val cs: CharSequence = text
|
Log.w("TTS", "Ignore speak: Not initialized yet")
|
||||||
textToSpeech.speak(cs, TextToSpeech.QUEUE_FLUSH, null, "1233455")
|
return
|
||||||
} catch (e: Throwable) {
|
}
|
||||||
Log.d("TTS", "speak error", e)
|
|
||||||
|
// 1. Request focus
|
||||||
|
val result = audioManager.requestAudioFocus(focusRequest)
|
||||||
|
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||||
|
// 2. Speak with a unique ID to trigger the listener
|
||||||
|
val utteranceId = System.currentTimeMillis().toString()
|
||||||
|
textToSpeech?.speak(text, TextToSpeech.QUEUE_FLUSH, null, utteranceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleans up manager.
|
|
||||||
* Should be called when the session is destroyed.
|
|
||||||
*/
|
|
||||||
fun cleanup() {
|
fun cleanup() {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
textToSpeech.shutdown()
|
textToSpeech?.stop()
|
||||||
|
textToSpeech?.shutdown()
|
||||||
|
initialized = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,9 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.scale
|
import androidx.compose.ui.graphics.drawscope.scale
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
@@ -22,6 +24,7 @@ import androidx.compose.ui.text.TextStyle
|
|||||||
import androidx.compose.ui.text.drawText
|
import androidx.compose.ui.text.drawText
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.rememberTextMeasurer
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
|
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
|
||||||
@@ -210,10 +213,10 @@ fun RouteLayerPoint(routeData: String?) {
|
|||||||
fun trafficColor(key: String): Expression<ColorValue> {
|
fun trafficColor(key: String): Expression<ColorValue> {
|
||||||
when (key) {
|
when (key) {
|
||||||
"queuing" -> return const(Color(0xFFC46E53))
|
"queuing" -> return const(Color(0xFFC46E53))
|
||||||
"stationary" -> return const(Color(0xFFFF0000))
|
"slow" -> return const(Color(0xFFC43E3E))
|
||||||
|
"stationary" -> return const(Color(0xFF910A0A))
|
||||||
"heavy" -> return const(Color(0xFF6B0404))
|
"heavy" -> return const(Color(0xFF6B0404))
|
||||||
"slow" -> return const(Color(0xFFBD2525))
|
"roadworks" -> return const(Color(0xFF443506))
|
||||||
"roadworks" -> return const(Color(0xFF725A0F))
|
|
||||||
}
|
}
|
||||||
return const(Color.Blue)
|
return const(Color.Blue)
|
||||||
}
|
}
|
||||||
@@ -222,10 +225,10 @@ fun trafficColor(key: String): Expression<ColorValue> {
|
|||||||
fun AmenityLayer(routeData: String?) {
|
fun AmenityLayer(routeData: String?) {
|
||||||
if (!routeData.isNullOrEmpty()) {
|
if (!routeData.isNullOrEmpty()) {
|
||||||
var color = const(Color.Red)
|
var color = const(Color.Red)
|
||||||
var img = image(painterResource(R.drawable.local_pharmacy_48px), drawAsSdf = true)
|
var img = image(painterResource(R.drawable.local_pharmacy_24px), drawAsSdf = true)
|
||||||
if (routeData.contains(Constants.CHARGING_STATION)) {
|
if (routeData.contains(Constants.CHARGING_STATION)) {
|
||||||
color = const(Color(0xFF054603))
|
color = const(Color(0xFF054603))
|
||||||
img = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true)
|
img = image(painterResource(R.drawable.ev_station_24px), drawAsSdf = true)
|
||||||
} else if (routeData.contains(Constants.FUEL_STATION)) {
|
} else if (routeData.contains(Constants.FUEL_STATION)) {
|
||||||
color = const(Color.Blue)
|
color = const(Color.Blue)
|
||||||
img = image(painterResource(R.drawable.local_gas_station_24), drawAsSdf = true)
|
img = image(painterResource(R.drawable.local_gas_station_24), drawAsSdf = true)
|
||||||
@@ -321,10 +324,11 @@ fun NavigationImage(
|
|||||||
val street = streetName.toString()
|
val street = streetName.toString()
|
||||||
val styleStreet = TextStyle(
|
val styleStreet = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = if (darkMode == 1) Color.Yellow else Color.Red,
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = if (darkMode == 1) Color.White else navigationColor,
|
||||||
)
|
)
|
||||||
val textLayoutStreet = remember(street) {
|
val textLayoutStreet = remember(street) {
|
||||||
textMeasurerStreet.measure(street, styleStreet)
|
textMeasurerStreet.measure(street, styleStreet, overflow = TextOverflow.Ellipsis)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) {
|
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) {
|
||||||
@@ -346,16 +350,26 @@ fun NavigationImage(
|
|||||||
)
|
)
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(imageSize.dp + 100.dp, imageSize.dp + 80.dp)
|
.size((width / 5).dp, (height/4).dp)
|
||||||
) {
|
) {
|
||||||
if (!streetName.isNullOrEmpty()) {
|
if (!streetName.isNullOrEmpty()) {
|
||||||
|
drawRoundRect(
|
||||||
|
topLeft = Offset(
|
||||||
|
x = center.x - textLayoutStreet.size.width / 2 ,
|
||||||
|
y = center.y + textLayoutStreet.size.height,
|
||||||
|
),
|
||||||
|
color = if (darkMode == 1) navigationColor else Color.White,
|
||||||
|
cornerRadius = CornerRadius(x = 10f, y = 10f),
|
||||||
|
)
|
||||||
drawText(
|
drawText(
|
||||||
textMeasurer = textMeasurerStreet,
|
textMeasurer = textMeasurerStreet,
|
||||||
text = streetName,
|
text = streetName,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
style = styleStreet,
|
style = styleStreet,
|
||||||
topLeft = Offset(
|
topLeft = Offset(
|
||||||
x = center.x - textLayoutStreet.size.width / 2,
|
x = center.x - textLayoutStreet.size.width / 2,
|
||||||
y = center.y + textLayoutStreet.size.height,
|
y = center.y + textLayoutStreet.size.height + 10,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.kouros.navigation.car.navigation
|
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.AudioAttributes
|
||||||
import android.media.AudioFocusRequest
|
import android.media.AudioFocusRequest
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
@@ -16,7 +19,12 @@ import androidx.car.app.model.Action
|
|||||||
import androidx.car.app.model.CarIcon
|
import androidx.car.app.model.CarIcon
|
||||||
import androidx.car.app.model.CarText
|
import androidx.car.app.model.CarText
|
||||||
import androidx.car.app.model.Row
|
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.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 com.kouros.navigation.data.Constants.TAG
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -66,4 +74,31 @@ class NavigationUtils(private var carContext: CarContext) {
|
|||||||
)
|
)
|
||||||
.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ 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 2 seconds)
|
// Wait before moving to the next point (e.g., every 2 seconds)
|
||||||
delay(500)
|
delay(1000)
|
||||||
lastLocation = fakeLocation
|
lastLocation = fakeLocation
|
||||||
}
|
}
|
||||||
routeModel.stopNavigation()
|
routeModel.stopNavigation()
|
||||||
|
|||||||
@@ -18,19 +18,32 @@ 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.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,
|
||||||
private val surfaceRenderer: SurfaceRenderer,
|
private val surfaceRenderer: SurfaceRenderer,
|
||||||
private val navigationViewModel: NavigationViewModel,
|
private val navigationViewModel: NavigationViewModel,
|
||||||
) : Screen(carContext) {
|
) : Screen(carContext), CategoryObserverCallback {
|
||||||
|
|
||||||
|
private val categoryObserver = CategoryObserver(this)
|
||||||
|
|
||||||
|
private var category = ""
|
||||||
var categories: List<Category> = listOf(
|
var categories: List<Category> = listOf(
|
||||||
Category(id = FUEL_STATION, name = carContext.getString(R.string.fuel_station)),
|
Category(id = FUEL_STATION, name = carContext.getString(R.string.fuel_station)),
|
||||||
Category(id = PHARMACY, name = carContext.getString(R.string.pharmacy)),
|
Category(id = PHARMACY, name = carContext.getString(R.string.pharmacy)),
|
||||||
Category(id = CHARGING_STATION, name = carContext.getString(R.string.charging_station))
|
Category(id = CHARGING_STATION, name = carContext.getString(R.string.charging_station))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
navigationViewModel.elements.observe(this, categoryObserver)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
val itemListBuilder = ItemList.Builder()
|
val itemListBuilder = ItemList.Builder()
|
||||||
.setNoItemsMessage("No categories to show")
|
.setNoItemsMessage("No categories to show")
|
||||||
@@ -38,22 +51,10 @@ class CategoriesScreen(
|
|||||||
itemListBuilder.addItem(
|
itemListBuilder.addItem(
|
||||||
Row.Builder()
|
Row.Builder()
|
||||||
.setTitle(it.name)
|
.setTitle(it.name)
|
||||||
.setImage(carIcon(carContext,it.id))
|
.setImage(carIcon(carContext,it.id, -1))
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
screenManager
|
category = it.id
|
||||||
.pushForResult(
|
navigationViewModel.getAmenities(it.id, surfaceRenderer.lastLocation)
|
||||||
CategoryScreen(
|
|
||||||
carContext,
|
|
||||||
surfaceRenderer,
|
|
||||||
it.id,
|
|
||||||
navigationViewModel
|
|
||||||
)
|
|
||||||
) { obj: Any? ->
|
|
||||||
if (obj != null) {
|
|
||||||
setResult(obj)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.setBrowsable(true)
|
.setBrowsable(true)
|
||||||
.build()
|
.build()
|
||||||
@@ -72,20 +73,49 @@ class CategoriesScreen(
|
|||||||
.setSingleList(itemListBuilder.build())
|
.setSingleList(itemListBuilder.build())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCategoryElementsReady(
|
||||||
|
elements: List<Elements>,
|
||||||
|
centerLat: Double,
|
||||||
|
centerLon: Double,
|
||||||
|
coordinates: List<List<Double>>
|
||||||
|
) {
|
||||||
|
val loc = location(centerLon, centerLat)
|
||||||
|
val route = createPointCollection(coordinates, category)
|
||||||
|
surfaceRenderer.setCategories(loc, route)
|
||||||
|
screenManager
|
||||||
|
.pushForResult(
|
||||||
|
CategoryScreen(
|
||||||
|
carContext,
|
||||||
|
surfaceRenderer,
|
||||||
|
category,
|
||||||
|
navigationViewModel,
|
||||||
|
elements
|
||||||
|
)
|
||||||
|
) { obj: Any? ->
|
||||||
|
if (obj != null) {
|
||||||
|
setResult(obj)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidateScreen() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun carIcon(context: CarContext, id: String): CarIcon {
|
fun carIcon(context: CarContext, category: String, index: Int): CarIcon {
|
||||||
val resId = when (id) {
|
if (index == -1) {
|
||||||
FUEL_STATION -> R.drawable.local_gas_station_24
|
val resId = when (category) {
|
||||||
PHARMACY -> R.drawable.local_pharmacy_48px
|
CHARGING_STATION -> R.drawable.ev_station_24px
|
||||||
CHARGING_STATION -> R.drawable.ev_station_48px
|
FUEL_STATION -> R.drawable.local_gas_station_24
|
||||||
else -> {}
|
PHARMACY -> R.drawable.local_pharmacy_24px
|
||||||
|
else -> R.drawable.ic_place_white_24dp
|
||||||
|
}
|
||||||
|
return CarIcon.Builder(IconCompat.createWithResource(context, resId)).build()
|
||||||
|
} else {
|
||||||
|
return CarIcon.Builder(NavigationUtils(context).createNumberIcon(category, index.toString())).build()
|
||||||
}
|
}
|
||||||
return CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
context,
|
|
||||||
resId as Int
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,16 @@ import androidx.car.app.model.Row
|
|||||||
import androidx.car.app.model.Template
|
import androidx.car.app.model.Template
|
||||||
import androidx.car.app.navigation.model.MapController
|
import androidx.car.app.navigation.model.MapController
|
||||||
import androidx.car.app.navigation.model.MapWithContentTemplate
|
import androidx.car.app.navigation.model.MapWithContentTemplate
|
||||||
import androidx.lifecycle.Observer
|
import androidx.car.app.versioning.CarAppApiLevels
|
||||||
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.navigation.NavigationUtils
|
||||||
|
import com.kouros.navigation.car.screen.observers.CategoryObserver
|
||||||
|
import com.kouros.navigation.car.screen.observers.CategoryObserverCallback
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
|
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.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.overpass.Elements
|
import com.kouros.navigation.data.overpass.Elements
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
@@ -32,57 +37,39 @@ class CategoryScreen(
|
|||||||
private val surfaceRenderer: SurfaceRenderer,
|
private val surfaceRenderer: SurfaceRenderer,
|
||||||
private val category: String,
|
private val category: String,
|
||||||
private val navigationViewModel: NavigationViewModel,
|
private val navigationViewModel: NavigationViewModel,
|
||||||
|
private var elements: List<Elements>,
|
||||||
) : Screen(carContext) {
|
) : Screen(carContext) {
|
||||||
|
|
||||||
var elements = listOf<Elements>()
|
val maxListItems: Int = 30
|
||||||
|
|
||||||
val observer = Observer<List<Elements>> { newElements ->
|
|
||||||
elements = newElements
|
|
||||||
val coordinates = mutableListOf<List<Double>>()
|
|
||||||
val loc = location(0.0, 0.0)
|
|
||||||
elements.forEach {
|
|
||||||
if (loc.latitude == 0.0) {
|
|
||||||
loc.longitude = it.lon
|
|
||||||
loc.latitude = it.lat
|
|
||||||
}
|
|
||||||
coordinates.add(listOf(it.lon, it.lat))
|
|
||||||
}
|
|
||||||
if (elements.isNotEmpty()) {
|
|
||||||
val route = createPointCollection(coordinates, category)
|
|
||||||
surfaceRenderer.setCategories(loc, route)
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
navigationViewModel.elements.observe(this, observer)
|
|
||||||
navigationViewModel.getAmenities(category, surfaceRenderer.lastLocation)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
val listBuilder = ItemList.Builder()
|
val listBuilder = ItemList.Builder()
|
||||||
var index = 0
|
var index = 0
|
||||||
val listLimit = min(
|
|
||||||
50,
|
// Some hosts may allow more items in the list than others, so create more.
|
||||||
carContext.getCarService(ConstraintManager::class.java)
|
if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
|
||||||
.getContentLimit(
|
val listLimit = min(
|
||||||
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
|
maxListItems,
|
||||||
)
|
carContext.getCarService(ConstraintManager::class.java)
|
||||||
)
|
.getContentLimit(
|
||||||
elements.forEach {
|
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
|
||||||
if (index++ < listLimit) {
|
|
||||||
if (it.tags.operator != null) {
|
|
||||||
listBuilder.addItem(
|
|
||||||
createItem(it, category)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
elements.forEach {
|
||||||
|
if (it.tags.operator != null) {
|
||||||
|
if (index++ < listLimit) {
|
||||||
|
listBuilder.addItem(
|
||||||
|
createItem(it, category, index)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val header = Header.Builder()
|
val header = Header.Builder()
|
||||||
.setStartHeaderAction(Action.BACK)
|
.setStartHeaderAction(Action.BACK)
|
||||||
.setTitle(carContext.getString(R.string.charging_station))
|
.setTitle(getTitle(carContext, category))
|
||||||
.build()
|
.build()
|
||||||
val builder = MapWithContentTemplate.Builder()
|
val builder = MapWithContentTemplate.Builder()
|
||||||
.setContentTemplate(
|
.setContentTemplate(
|
||||||
@@ -99,7 +86,17 @@ class CategoryScreen(
|
|||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createItem(it: Elements, category: String): Row {
|
private fun getTitle(carContext: CarContext, category: String): String {
|
||||||
|
val resId = when (category) {
|
||||||
|
CHARGING_STATION -> R.string.charging_station
|
||||||
|
FUEL_STATION -> R.string.fuel_station
|
||||||
|
PHARMACY -> R.string.pharmacy
|
||||||
|
else -> R.string.no_places
|
||||||
|
}
|
||||||
|
return carContext.getString(resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createItem(it: Elements, category: String, index: Int): Row {
|
||||||
var name = ""
|
var name = ""
|
||||||
if (it.tags.name != null) {
|
if (it.tags.name != null) {
|
||||||
name = it.tags.name.toString()
|
name = it.tags.name.toString()
|
||||||
@@ -113,7 +110,7 @@ class CategoryScreen(
|
|||||||
surfaceRenderer.setCategoryLocation(location, category)
|
surfaceRenderer.setCategoryLocation(location, category)
|
||||||
}
|
}
|
||||||
.setTitle(name)
|
.setTitle(name)
|
||||||
.setImage(carIcon(carContext, category))
|
.setImage(carIcon(carContext, category, index))
|
||||||
if (it.distance < 1000) {
|
if (it.distance < 1000) {
|
||||||
row.addText("${(it.distance).toInt()} m")
|
row.addText("${(it.distance).toInt()} m")
|
||||||
} else {
|
} else {
|
||||||
@@ -127,28 +124,29 @@ class CategoryScreen(
|
|||||||
val navigationUtils = NavigationUtils(carContext)
|
val navigationUtils = NavigationUtils(carContext)
|
||||||
row.addAction(
|
row.addAction(
|
||||||
Action.Builder()
|
Action.Builder()
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
navigationViewModel.loadRoute(
|
navigationViewModel.loadRoute(
|
||||||
carContext,
|
carContext,
|
||||||
currentLocation = surfaceRenderer.lastLocation,
|
currentLocation = surfaceRenderer.lastLocation,
|
||||||
location(it.lon!!, it.lat!!),
|
location(it.lon, it.lat),
|
||||||
surfaceRenderer.carOrientation
|
surfaceRenderer.carOrientation
|
||||||
)
|
|
||||||
setResult(
|
|
||||||
Place(
|
|
||||||
name = name,
|
|
||||||
category = Constants.CHARGING_STATION,
|
|
||||||
latitude = it.lat!!,
|
|
||||||
longitude = it.lon!!
|
|
||||||
)
|
)
|
||||||
)
|
setResult(
|
||||||
finish()
|
Place(
|
||||||
}
|
name = name,
|
||||||
.setIcon(navigationUtils.createCarIcon(R.drawable.navigation_48px))
|
category = Constants.CHARGING_STATION,
|
||||||
.build())
|
latitude = it.lat,
|
||||||
|
longitude = it.lon
|
||||||
|
)
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
.setIcon(navigationUtils.createCarIcon(R.drawable.navigation_48px))
|
||||||
|
.build())
|
||||||
return row.build()
|
return row.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun carText(sText: String): CarText {
|
private fun carText(sText: String): CarText {
|
||||||
val secondText =
|
val secondText =
|
||||||
CarText.Builder(
|
CarText.Builder(
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ 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.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.SettingsViewModel
|
|
||||||
import com.kouros.navigation.repository.SettingsRepository
|
|
||||||
import com.kouros.navigation.utils.GeoUtils
|
import com.kouros.navigation.utils.GeoUtils
|
||||||
import com.kouros.navigation.utils.formattedDistance
|
import com.kouros.navigation.utils.formattedDistance
|
||||||
import com.kouros.navigation.utils.getSettingsRepository
|
import com.kouros.navigation.utils.getSettingsRepository
|
||||||
@@ -102,8 +100,10 @@ class NavigationScreen(
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
settingsViewModel.routingEngine.first()
|
settingsViewModel.routingEngine.first()
|
||||||
settingsViewModel.recentPlaces.first()
|
settingsViewModel.recentPlaces.first()
|
||||||
distanceMode = repository.distanceModeFlow.first()
|
|
||||||
}
|
}
|
||||||
|
repository.distanceModeFlow.asLiveData().observe(this, Observer {
|
||||||
|
distanceMode = it
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,7 +112,7 @@ class NavigationScreen(
|
|||||||
*/
|
*/
|
||||||
override fun onRouteReceived(route: String) {
|
override fun onRouteReceived(route: String) {
|
||||||
if (route.isNotEmpty()) {
|
if (route.isNotEmpty()) {
|
||||||
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
||||||
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
|
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
|
||||||
navigationType = NavigationType.NAVIGATION
|
navigationType = NavigationType.NAVIGATION
|
||||||
routeModel.startNavigation(route)
|
routeModel.startNavigation(route)
|
||||||
@@ -187,9 +187,7 @@ class NavigationScreen(
|
|||||||
* Returns the appropriate template based on the current navigation state.
|
* Returns the appropriate template based on the current navigation state.
|
||||||
*/
|
*/
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
repository.distanceModeFlow.asLiveData().observe(this, Observer {
|
|
||||||
distanceMode = it
|
|
||||||
})
|
|
||||||
val actionStripBuilder = createActionStripBuilder()
|
val actionStripBuilder = createActionStripBuilder()
|
||||||
return when (navigationType) {
|
return when (navigationType) {
|
||||||
NavigationType.NAVIGATION -> navigationTemplate(actionStripBuilder)
|
NavigationType.NAVIGATION -> navigationTemplate(actionStripBuilder)
|
||||||
@@ -330,8 +328,9 @@ class NavigationScreen(
|
|||||||
* Builds and returns RoutingInfo based on the current step and distance.
|
* Builds and returns RoutingInfo based on the current step and distance.
|
||||||
*/
|
*/
|
||||||
fun getRoutingInfo(): RoutingInfo {
|
fun getRoutingInfo(): RoutingInfo {
|
||||||
val distance = formattedDistance(distanceMode, routeModel.routeCalculator.leftStepDistance())
|
val distance =
|
||||||
val routingInfo = RoutingInfo.Builder()
|
formattedDistance(distanceMode, routeModel.routeCalculator.leftStepDistance())
|
||||||
|
val routingInfo = RoutingInfo.Builder()
|
||||||
.setCurrentStep(
|
.setCurrentStep(
|
||||||
routeModel.currentStep(carContext = carContext),
|
routeModel.currentStep(carContext = carContext),
|
||||||
Distance.create(distance.first, distance.second)
|
Distance.create(distance.first, distance.second)
|
||||||
@@ -460,7 +459,7 @@ 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()
|
return Action.Builder()
|
||||||
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
|
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
screenManager.push(SettingsScreen(carContext, navigationViewModel))
|
screenManager.push(SettingsScreen(carContext, navigationViewModel))
|
||||||
@@ -628,7 +627,7 @@ class NavigationScreen(
|
|||||||
}
|
}
|
||||||
updateSpeedCamera(location)
|
updateSpeedCamera(location)
|
||||||
with(routeModel) {
|
with(routeModel) {
|
||||||
updateLocation( location, navigationViewModel)
|
updateLocation(location, navigationViewModel)
|
||||||
checkArrival()
|
checkArrival()
|
||||||
}
|
}
|
||||||
invalidate()
|
invalidate()
|
||||||
@@ -658,14 +657,19 @@ class NavigationScreen(
|
|||||||
* This includes destination name, address, travel estimate, and loading status.
|
* This includes destination name, address, travel estimate, and loading status.
|
||||||
*/
|
*/
|
||||||
private fun updateTrip() {
|
private fun updateTrip() {
|
||||||
val tripBuilder = Trip.Builder()
|
if (routeModel.isNavigating()) {
|
||||||
val destination = Destination.Builder()
|
val tripBuilder = Trip.Builder()
|
||||||
.setName(routeModel.navState.destination.name ?: "")
|
val destination = Destination.Builder()
|
||||||
.setAddress(routeModel.navState.destination.street ?: "")
|
.setName(routeModel.navState.destination.name ?: "")
|
||||||
.build()
|
.setAddress(routeModel.navState.destination.street ?: "")
|
||||||
tripBuilder.addDestination(destination, routeModel.travelEstimate(carContext, distanceMode))
|
.build()
|
||||||
tripBuilder.setLoading(false)
|
tripBuilder.addDestination(
|
||||||
listener.updateTrip(tripBuilder.build())
|
destination,
|
||||||
|
routeModel.travelEstimate(carContext, distanceMode)
|
||||||
|
)
|
||||||
|
tripBuilder.setLoading(false)
|
||||||
|
listener.updateTrip(tripBuilder.build())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -142,22 +142,15 @@ class SearchScreen(
|
|||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
fun doSearch(searchItemListBuilder: ItemList.Builder) {
|
fun doSearch(searchItemListBuilder: ItemList.Builder) {
|
||||||
|
if (searchResult.size == 1) {
|
||||||
|
navigateToPlace(searchResult.first())
|
||||||
|
}
|
||||||
searchResult.forEach {
|
searchResult.forEach {
|
||||||
searchItemListBuilder.addItem(
|
searchItemListBuilder.addItem(
|
||||||
Row.Builder()
|
Row.Builder()
|
||||||
.setTitle("${(it.distance/1000).toInt()} km ${it.displayName} ")
|
.setTitle("${(it.distance/1000).toInt()} km ${it.displayName} ")
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
val place = Place(
|
navigateToPlace(it)
|
||||||
name = it.displayName,
|
|
||||||
latitude = it.lat.toDouble(),
|
|
||||||
longitude = it.lon.toDouble(),
|
|
||||||
street = it.address.road,
|
|
||||||
city = it.address.city,
|
|
||||||
postalCode = it.address.postcode,
|
|
||||||
distance = it.distance
|
|
||||||
)
|
|
||||||
setResult(place)
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
.setBrowsable(false)
|
.setBrowsable(false)
|
||||||
.build()
|
.build()
|
||||||
@@ -165,4 +158,18 @@ class SearchScreen(
|
|||||||
}
|
}
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun navigateToPlace(result: SearchResult) {
|
||||||
|
val place = Place(
|
||||||
|
name = result.displayName,
|
||||||
|
latitude = result.lat.toDouble(),
|
||||||
|
longitude = result.lon.toDouble(),
|
||||||
|
street = result.address.road,
|
||||||
|
city = result.address.city,
|
||||||
|
postalCode = result.address.postcode,
|
||||||
|
distance = result.distance
|
||||||
|
)
|
||||||
|
setResult(place)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.kouros.navigation.car.screen.observers
|
||||||
|
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import com.kouros.navigation.data.overpass.Elements
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback interface for category element updates in CategoryScreen.
|
||||||
|
*/
|
||||||
|
interface CategoryObserverCallback {
|
||||||
|
/**
|
||||||
|
* Called when category elements are ready to display.
|
||||||
|
* @param elements the full list of elements
|
||||||
|
* @param centerLat latitude of the first element (used to center the map)
|
||||||
|
* @param centerLon longitude of the first element (used to center the map)
|
||||||
|
* @param coordinates all element coordinates as [lon, lat] pairs
|
||||||
|
*/
|
||||||
|
fun onCategoryElementsReady(
|
||||||
|
elements: List<Elements>,
|
||||||
|
centerLat: Double,
|
||||||
|
centerLon: Double,
|
||||||
|
coordinates: List<List<Double>>
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Called to request UI invalidation after elements are updated */
|
||||||
|
fun invalidateScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observer for POI/amenity element lists. Extracts coordinates and notifies the screen
|
||||||
|
* when new data is ready to display.
|
||||||
|
*/
|
||||||
|
class CategoryObserver(
|
||||||
|
private val callback: CategoryObserverCallback
|
||||||
|
) : Observer<List<Elements>> {
|
||||||
|
|
||||||
|
override fun onChanged(value: List<Elements>) {
|
||||||
|
if (value.isEmpty()) return
|
||||||
|
|
||||||
|
var centerLat = 0.0
|
||||||
|
var centerLon = 0.0
|
||||||
|
val coordinates = mutableListOf<List<Double>>()
|
||||||
|
|
||||||
|
value.forEach { element ->
|
||||||
|
if (centerLat == 0.0) {
|
||||||
|
centerLat = element.lat
|
||||||
|
centerLon = element.lon
|
||||||
|
}
|
||||||
|
coordinates.add(listOf(element.lon, element.lat))
|
||||||
|
}
|
||||||
|
|
||||||
|
callback.onCategoryElementsReady(value, centerLat, centerLon, coordinates)
|
||||||
|
callback.invalidateScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,5 +83,4 @@ class AudioSettings(
|
|||||||
private fun onSelected(index: Int) {
|
private fun onSelected(index: Int) {
|
||||||
settingsViewModel.onGuidanceAudioChanged(index)
|
settingsViewModel.onGuidanceAudioChanged(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.car.app.model.ListTemplate
|
|||||||
import androidx.car.app.model.Row
|
import androidx.car.app.model.Row
|
||||||
import androidx.car.app.model.Template
|
import androidx.car.app.model.Template
|
||||||
import androidx.car.app.model.Toggle
|
import androidx.car.app.model.Toggle
|
||||||
|
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.screen.settings.DistanceSettings
|
import com.kouros.navigation.car.screen.settings.DistanceSettings
|
||||||
@@ -20,16 +21,19 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
|
|||||||
|
|
||||||
private var buildingToggleState = false
|
private var buildingToggleState = false
|
||||||
|
|
||||||
|
private var showTraffic = false
|
||||||
|
|
||||||
val settingsViewModel = getSettingsViewModel(carContext)
|
val settingsViewModel = getSettingsViewModel(carContext)
|
||||||
|
|
||||||
init {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
settingsViewModel.show3D.first()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
buildingToggleState = settingsViewModel.show3D.value
|
settingsViewModel.traffic.asLiveData().observe(this) {
|
||||||
|
showTraffic = it
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
settingsViewModel.show3D.asLiveData().observe(this) {
|
||||||
|
buildingToggleState = it
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
val listBuilder = ItemList.Builder()
|
val listBuilder = ItemList.Builder()
|
||||||
val buildingToggle: Toggle =
|
val buildingToggle: Toggle =
|
||||||
Toggle.Builder { checked: Boolean ->
|
Toggle.Builder { checked: Boolean ->
|
||||||
@@ -37,6 +41,14 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
|
|||||||
buildingToggleState = !buildingToggleState
|
buildingToggleState = !buildingToggleState
|
||||||
}.setChecked(buildingToggleState).build()
|
}.setChecked(buildingToggleState).build()
|
||||||
listBuilder.addItem(buildRowForTemplate(R.string.threed_building, buildingToggle))
|
listBuilder.addItem(buildRowForTemplate(R.string.threed_building, buildingToggle))
|
||||||
|
|
||||||
|
val trafficToggle: Toggle =
|
||||||
|
Toggle.Builder { checked: Boolean ->
|
||||||
|
settingsViewModel.onTraffic(checked)
|
||||||
|
showTraffic = !showTraffic
|
||||||
|
}.setChecked(showTraffic).build()
|
||||||
|
listBuilder.addItem(buildRowForTemplate(R.string.traffic, trafficToggle))
|
||||||
|
|
||||||
listBuilder.addItem(
|
listBuilder.addItem(
|
||||||
buildRowForScreenTemplate(
|
buildRowForScreenTemplate(
|
||||||
DarkModeSettings(carContext),
|
DarkModeSettings(carContext),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class DistanceSettings(private val carContext: CarContext) : Screen(carContext)
|
|||||||
ItemList.Builder()
|
ItemList.Builder()
|
||||||
.addItem(
|
.addItem(
|
||||||
buildRowForTemplate(
|
buildRowForTemplate(
|
||||||
R.string.automaticaly,
|
R.string.automatically,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addItem(
|
.addItem(
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ class NavigationSettings(
|
|||||||
|
|
||||||
private var tollWayToggleState = false
|
private var tollWayToggleState = false
|
||||||
|
|
||||||
|
private var ferryToggleState = false
|
||||||
|
|
||||||
|
|
||||||
private var carLocationToggleState = false
|
private var carLocationToggleState = false
|
||||||
|
|
||||||
val settingsViewModel = getSettingsViewModel(carContext)
|
val settingsViewModel = getSettingsViewModel(carContext)
|
||||||
@@ -36,6 +39,7 @@ class NavigationSettings(
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
settingsViewModel.avoidTollway.first()
|
settingsViewModel.avoidTollway.first()
|
||||||
settingsViewModel.avoidMotorway.first()
|
settingsViewModel.avoidMotorway.first()
|
||||||
|
settingsViewModel.avoidFerry.first()
|
||||||
settingsViewModel.carLocation.first()
|
settingsViewModel.carLocation.first()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,9 +47,12 @@ class NavigationSettings(
|
|||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
motorWayToggleState = settingsViewModel.avoidMotorway.value
|
motorWayToggleState = settingsViewModel.avoidMotorway.value
|
||||||
tollWayToggleState = settingsViewModel.avoidTollway.value
|
tollWayToggleState = settingsViewModel.avoidTollway.value
|
||||||
|
ferryToggleState = settingsViewModel.avoidFerry.value
|
||||||
carLocationToggleState = settingsViewModel.carLocation.value
|
carLocationToggleState = settingsViewModel.carLocation.value
|
||||||
|
|
||||||
val listBuilder = ItemList.Builder()
|
val listBuilder = ItemList.Builder()
|
||||||
|
|
||||||
|
// Motorway
|
||||||
val highwayToggle: Toggle =
|
val highwayToggle: Toggle =
|
||||||
Toggle.Builder { checked: Boolean ->
|
Toggle.Builder { checked: Boolean ->
|
||||||
settingsViewModel.onAvoidMotorway(checked)
|
settingsViewModel.onAvoidMotorway(checked)
|
||||||
@@ -66,6 +73,15 @@ class NavigationSettings(
|
|||||||
}.setChecked(tollWayToggleState).build()
|
}.setChecked(tollWayToggleState).build()
|
||||||
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle))
|
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle))
|
||||||
|
|
||||||
|
// Ferry
|
||||||
|
val ferryToggle: Toggle =
|
||||||
|
Toggle.Builder { checked: Boolean ->
|
||||||
|
settingsViewModel.onAvoidFerry(checked)
|
||||||
|
ferryToggleState = !ferryToggleState
|
||||||
|
}.setChecked(ferryToggleState).build()
|
||||||
|
listBuilder.addItem(buildRowForTemplate(R.string.avoid_ferries, ferryToggle))
|
||||||
|
|
||||||
|
// CarLocation
|
||||||
val carLocationToggle: Toggle =
|
val carLocationToggle: Toggle =
|
||||||
Toggle.Builder { checked: Boolean ->
|
Toggle.Builder { checked: Boolean ->
|
||||||
settingsViewModel.onCarLocation(checked)
|
settingsViewModel.onCarLocation(checked)
|
||||||
|
|||||||
@@ -3,13 +3,23 @@ package com.kouros.navigation.car.screen.settings
|
|||||||
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
|
||||||
|
import androidx.car.app.model.CarIcon
|
||||||
import androidx.car.app.model.Header
|
import androidx.car.app.model.Header
|
||||||
import androidx.car.app.model.ItemList
|
import androidx.car.app.model.ItemList
|
||||||
import androidx.car.app.model.ListTemplate
|
import androidx.car.app.model.ListTemplate
|
||||||
import androidx.car.app.model.Row
|
import androidx.car.app.model.Row
|
||||||
|
import androidx.car.app.model.SectionedItemList
|
||||||
import androidx.car.app.model.Template
|
import androidx.car.app.model.Template
|
||||||
|
import androidx.car.app.model.Toggle
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
|
import com.kouros.navigation.utils.getSettingsViewModel
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/** A screen demonstrating selectable lists. */
|
/** A screen demonstrating selectable lists. */
|
||||||
class SettingsScreen(
|
class SettingsScreen(
|
||||||
@@ -17,20 +27,66 @@ class SettingsScreen(
|
|||||||
private var navigationViewModel: NavigationViewModel,
|
private var navigationViewModel: NavigationViewModel,
|
||||||
) : Screen(carContext) {
|
) : Screen(carContext) {
|
||||||
|
|
||||||
|
val settingsViewModel = getSettingsViewModel(carContext)
|
||||||
|
|
||||||
|
private var audioToggleState = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
settingsViewModel.guidanceAudio.first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
val listBuilder = ItemList.Builder()
|
settingsViewModel.guidanceAudio.asLiveData().observe(this, Observer {
|
||||||
listBuilder.addItem(
|
audioToggleState = settingsViewModel.guidanceAudio.value == 1
|
||||||
buildRowForTemplate(
|
invalidate()
|
||||||
AudioSettings(carContext),
|
})
|
||||||
R.string.audio_settings
|
|
||||||
)
|
val templateBuilder = ListTemplate.Builder()
|
||||||
)
|
|
||||||
|
val audioToggle: Toggle =
|
||||||
|
Toggle.Builder { checked: Boolean ->
|
||||||
|
if (checked) {
|
||||||
|
settingsViewModel.onGuidanceAudioChanged(1)
|
||||||
|
} else {
|
||||||
|
settingsViewModel.onGuidanceAudioChanged(0)
|
||||||
|
}
|
||||||
|
audioToggleState = !audioToggleState
|
||||||
|
}.setChecked(audioToggleState).build()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var listBuilder = ItemList.Builder()
|
||||||
|
listBuilder.addItem(Row.Builder()
|
||||||
|
.setTitle(getTitle())
|
||||||
|
.setImage(getImage())
|
||||||
|
.setToggle(audioToggle)
|
||||||
|
.build())
|
||||||
|
|
||||||
listBuilder.addItem(
|
listBuilder.addItem(
|
||||||
buildRowForTemplate(
|
buildRowForTemplate(
|
||||||
DisplaySettings(carContext),
|
DisplaySettings(carContext),
|
||||||
R.string.display
|
R.string.display
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
listBuilder.addItem(
|
||||||
|
buildRowForTemplate(
|
||||||
|
AudioSettings(carContext),
|
||||||
|
R.string.audio_settings
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
templateBuilder.addSectionedList(
|
||||||
|
SectionedItemList.create(
|
||||||
|
listBuilder.build(),
|
||||||
|
carContext.getString(R.string.general)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Navigation --------------
|
||||||
|
listBuilder = ItemList.Builder()
|
||||||
listBuilder.addItem(
|
listBuilder.addItem(
|
||||||
buildRowForTemplate(
|
buildRowForTemplate(
|
||||||
NavigationSettings(carContext, navigationViewModel),
|
NavigationSettings(carContext, navigationViewModel),
|
||||||
@@ -38,19 +94,41 @@ class SettingsScreen(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return ListTemplate.Builder()
|
templateBuilder.addSectionedList(
|
||||||
.setSingleList(listBuilder.build())
|
SectionedItemList.create(
|
||||||
.setHeader(
|
listBuilder.build(),
|
||||||
Header.Builder()
|
carContext.getString(R.string.navigation_settings)
|
||||||
.setTitle(
|
|
||||||
(carContext.getString(R.string.settings_action_title))
|
|
||||||
)
|
|
||||||
.setStartHeaderAction(Action.BACK)
|
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return templateBuilder
|
||||||
|
.setHeader( Header.Builder()
|
||||||
|
.setTitle(
|
||||||
|
(carContext.getString(R.string.settings_action_title))
|
||||||
|
)
|
||||||
|
.setStartHeaderAction(Action.BACK)
|
||||||
|
.build())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getTitle(): String {
|
||||||
|
return if (audioToggleState) {
|
||||||
|
carContext.getString(R.string.on_action_title)
|
||||||
|
} else {
|
||||||
|
carContext.getString(R.string.off_action_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getImage(): CarIcon {
|
||||||
|
return if (audioToggleState) {
|
||||||
|
CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.volume_up_24px))
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.volume_off_24px))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildRowForTemplate(screen: Screen, title: Int): Row {
|
private fun buildRowForTemplate(screen: Screen, title: Int): Row {
|
||||||
return Row.Builder()
|
return Row.Builder()
|
||||||
.setTitle(carContext.getString(title))
|
.setTitle(carContext.getString(title))
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package com.kouros.navigation.car.screen.observers
|
||||||
|
|
||||||
|
import com.kouros.navigation.data.overpass.Elements
|
||||||
|
import com.kouros.navigation.data.overpass.Tags
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.kotlin.*
|
||||||
|
|
||||||
|
class CategoryObserverTest {
|
||||||
|
|
||||||
|
private lateinit var mockCallback: CategoryObserverCallback
|
||||||
|
private lateinit var observer: CategoryObserver
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
mockCallback = mock()
|
||||||
|
observer = CategoryObserver(mockCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onChanged with empty list does not invoke callback`() {
|
||||||
|
observer.onChanged(emptyList())
|
||||||
|
|
||||||
|
verify(mockCallback, never()).onCategoryElementsReady(any(), any(), any(), any())
|
||||||
|
verify(mockCallback, never()).invalidateScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onChanged with single element invokes both callbacks`() {
|
||||||
|
val element = createElement(lon = 10.5, lat = 52.3)
|
||||||
|
|
||||||
|
observer.onChanged(listOf(element))
|
||||||
|
|
||||||
|
verify(mockCallback).onCategoryElementsReady(any(), any(), any(), any())
|
||||||
|
verify(mockCallback).invalidateScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onChanged uses first element coordinates as map center`() {
|
||||||
|
val first = createElement(lon = 10.5, lat = 52.3)
|
||||||
|
val second = createElement(lon = 11.0, lat = 53.0)
|
||||||
|
|
||||||
|
observer.onChanged(listOf(first, second))
|
||||||
|
|
||||||
|
argumentCaptor<Double>().apply {
|
||||||
|
verify(mockCallback).onCategoryElementsReady(any(), capture(), capture(), any())
|
||||||
|
val capturedLat = firstValue
|
||||||
|
val capturedLon = secondValue
|
||||||
|
assert(capturedLat == 52.3) { "Expected centerLat=52.3 but was $capturedLat" }
|
||||||
|
assert(capturedLon == 10.5) { "Expected centerLon=10.5 but was $capturedLon" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onChanged passes all elements to callback`() {
|
||||||
|
val elements = listOf(
|
||||||
|
createElement(lon = 10.0, lat = 52.0),
|
||||||
|
createElement(lon = 11.0, lat = 53.0),
|
||||||
|
createElement(lon = 12.0, lat = 54.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
observer.onChanged(elements)
|
||||||
|
|
||||||
|
argumentCaptor<List<Elements>>().apply {
|
||||||
|
verify(mockCallback).onCategoryElementsReady(capture(), any(), any(), any())
|
||||||
|
assert(firstValue.size == 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onChanged builds coordinates list with lon first then lat`() {
|
||||||
|
val element = createElement(lon = 10.5, lat = 52.3)
|
||||||
|
|
||||||
|
observer.onChanged(listOf(element))
|
||||||
|
|
||||||
|
argumentCaptor<List<List<Double>>>().apply {
|
||||||
|
verify(mockCallback).onCategoryElementsReady(any(), any(), any(), capture())
|
||||||
|
val coords = firstValue
|
||||||
|
assert(coords.size == 1)
|
||||||
|
assert(coords[0][0] == 10.5) { "Expected lon=10.5 at index 0 but was ${coords[0][0]}" }
|
||||||
|
assert(coords[0][1] == 52.3) { "Expected lat=52.3 at index 1 but was ${coords[0][1]}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onChanged collects coordinates for all elements`() {
|
||||||
|
val elements = listOf(
|
||||||
|
createElement(lon = 10.0, lat = 52.0),
|
||||||
|
createElement(lon = 11.0, lat = 53.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
observer.onChanged(elements)
|
||||||
|
|
||||||
|
argumentCaptor<List<List<Double>>>().apply {
|
||||||
|
verify(mockCallback).onCategoryElementsReady(any(), any(), any(), capture())
|
||||||
|
assert(firstValue.size == 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createElement(lon: Double, lat: Double): Elements {
|
||||||
|
return Elements(lon = lon, lat = lat, tags = Tags())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,7 +78,7 @@ data class Locations (
|
|||||||
data class SearchFilter(
|
data class SearchFilter(
|
||||||
var avoidMotorway: Boolean = false,
|
var avoidMotorway: Boolean = false,
|
||||||
var avoidTollway : Boolean = false,
|
var avoidTollway : Boolean = false,
|
||||||
|
var avoidFerry : Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -93,10 +93,6 @@ data class ValhallaLocation (
|
|||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
|
|
||||||
//const val STYLE: String = "https://kouros-online.de/liberty.json"
|
|
||||||
|
|
||||||
//const val STYLE_DARK: String = "https://kouros-online.de/liberty_night.json"
|
|
||||||
|
|
||||||
const val TAG: String = "Navigation"
|
const val TAG: String = "Navigation"
|
||||||
|
|
||||||
const val CATEGORIES: String = "Categories"
|
const val CATEGORIES: String = "Categories"
|
||||||
@@ -132,6 +128,8 @@ object Constants {
|
|||||||
const val MAXIMUM_LOCATION_DISTANCE = 100000F
|
const val MAXIMUM_LOCATION_DISTANCE = 100000F
|
||||||
|
|
||||||
const val TRAFFIC_UPDATE = 300
|
const val TRAFFIC_UPDATE = 300
|
||||||
|
|
||||||
|
const val INSTRUCTION_DISTANCE = 50
|
||||||
const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED"
|
const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED"
|
||||||
|
|
||||||
const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED"
|
const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED"
|
||||||
|
|||||||
@@ -55,18 +55,7 @@ abstract class NavigationRepository {
|
|||||||
): Double {
|
): Double {
|
||||||
if (currentLocation.latitude == 0.0)
|
if (currentLocation.latitude == 0.0)
|
||||||
return 0.0
|
return 0.0
|
||||||
val osrm = OsrmRepository()
|
return currentLocation.distanceTo(location).toDouble()
|
||||||
val route = osrm.getRoute(context, currentLocation, location, carOrientation, SearchFilter())
|
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
|
||||||
val osrmJson = gson.fromJson(route, OsrmResponse::class.java)
|
|
||||||
if (osrmJson.routes.isEmpty()) {
|
|
||||||
return 0.0
|
|
||||||
}
|
|
||||||
return osrmJson.routes.first().distance
|
|
||||||
// return osrmJson.destinations.first().distance?.toDouble() ?: 0.0
|
|
||||||
///val routeModel = RouteModel()
|
|
||||||
//routeModel.startNavigation(route, context)
|
|
||||||
//return routeModel.curRoute.summary.distance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchPlaces(search: String, location: Location): String {
|
fun searchPlaces(search: String, location: Location): String {
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class DataStoreManager(private val context: Context) {
|
|||||||
|
|
||||||
val AVOID_TOLLWAY = booleanPreferencesKey("AvoidTollway")
|
val AVOID_TOLLWAY = booleanPreferencesKey("AvoidTollway")
|
||||||
|
|
||||||
|
val AVOID_FERRY = booleanPreferencesKey("AvoidFerry")
|
||||||
|
|
||||||
val CAR_LOCATION = booleanPreferencesKey("CarLocation")
|
val CAR_LOCATION = booleanPreferencesKey("CarLocation")
|
||||||
|
|
||||||
val ROUTING_ENGINE = intPreferencesKey("RoutingEngine")
|
val ROUTING_ENGINE = intPreferencesKey("RoutingEngine")
|
||||||
@@ -49,6 +51,8 @@ class DataStoreManager(private val context: Context) {
|
|||||||
|
|
||||||
val GUIDANCE_AUDIO = intPreferencesKey("GuidanceAudio")
|
val GUIDANCE_AUDIO = intPreferencesKey("GuidanceAudio")
|
||||||
|
|
||||||
|
val TRAFFIC = booleanPreferencesKey("Traffic")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read values
|
// Read values
|
||||||
@@ -73,6 +77,11 @@ class DataStoreManager(private val context: Context) {
|
|||||||
preferences[PreferencesKeys.AVOID_TOLLWAY] == true
|
preferences[PreferencesKeys.AVOID_TOLLWAY] == true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val avoidFerryFlow: Flow<Boolean> =
|
||||||
|
context.dataStore.data.map { preferences ->
|
||||||
|
preferences[PreferencesKeys.AVOID_FERRY] == true
|
||||||
|
}
|
||||||
|
|
||||||
val useCarLocationFlow: Flow<Boolean> =
|
val useCarLocationFlow: Flow<Boolean> =
|
||||||
context.dataStore.data.map { preferences ->
|
context.dataStore.data.map { preferences ->
|
||||||
preferences[PreferencesKeys.CAR_LOCATION] == true
|
preferences[PreferencesKeys.CAR_LOCATION] == true
|
||||||
@@ -115,6 +124,11 @@ class DataStoreManager(private val context: Context) {
|
|||||||
?: 0
|
?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val trafficFlow: Flow<Boolean> =
|
||||||
|
context.dataStore.data.map { preferences ->
|
||||||
|
preferences[PreferencesKeys.TRAFFIC] == true
|
||||||
|
}
|
||||||
|
|
||||||
// Save values
|
// Save values
|
||||||
suspend fun setShow3D(enabled: Boolean) {
|
suspend fun setShow3D(enabled: Boolean) {
|
||||||
context.dataStore.edit { preferences ->
|
context.dataStore.edit { preferences ->
|
||||||
@@ -140,6 +154,12 @@ class DataStoreManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun setAvoidFerry(enabled: Boolean) {
|
||||||
|
context.dataStore.edit { preferences ->
|
||||||
|
preferences[PreferencesKeys.AVOID_FERRY] = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setCarLocation(enabled: Boolean) {
|
suspend fun setCarLocation(enabled: Boolean) {
|
||||||
context.dataStore.edit { preferences ->
|
context.dataStore.edit { preferences ->
|
||||||
preferences[PreferencesKeys.CAR_LOCATION] = enabled
|
preferences[PreferencesKeys.CAR_LOCATION] = enabled
|
||||||
@@ -182,4 +202,9 @@ class DataStoreManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun setTraffic(enabled: Boolean) {
|
||||||
|
context.dataStore.edit { preferences ->
|
||||||
|
preferences[PreferencesKeys.TRAFFIC] = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ class OsrmRepository : NavigationRepository() {
|
|||||||
if (searchFilter.avoidTollway) {
|
if (searchFilter.avoidTollway) {
|
||||||
exclude = "$exclude&exclude=toll"
|
exclude = "$exclude&exclude=toll"
|
||||||
}
|
}
|
||||||
|
if (searchFilter.avoidFerry) {
|
||||||
|
exclude = "$exclude&exclude=ferry"
|
||||||
|
}
|
||||||
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true&alternatives=false"
|
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true&alternatives=false"
|
||||||
return fetchUrl(routeUrl + routeLocation + exclude, true)
|
return fetchUrl(routeUrl + routeLocation + exclude, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ class TomTomRepository : NavigationRepository() {
|
|||||||
if (searchFilter.avoidTollway) {
|
if (searchFilter.avoidTollway) {
|
||||||
filter = "$filter&avoid=tollRoads"
|
filter = "$filter&avoid=tollRoads"
|
||||||
}
|
}
|
||||||
|
if (searchFilter.avoidFerry) {
|
||||||
|
filter = "$filter&avoid=ferries"
|
||||||
|
}
|
||||||
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()
|
||||||
@@ -65,6 +68,10 @@ class TomTomRepository : NavigationRepository() {
|
|||||||
override fun getTraffic(context: Context, location: Location, carOrientation: Float): String {
|
override fun getTraffic(context: Context, location: Location, carOrientation: Float): String {
|
||||||
val repository = getSettingsRepository(context)
|
val repository = getSettingsRepository(context)
|
||||||
val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() }
|
val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() }
|
||||||
|
val showTraffic = runBlocking { repository.trafficFlow.first() }
|
||||||
|
if (!showTraffic) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
|
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
|
||||||
return if (useAssetTraffic) {
|
return if (useAssetTraffic) {
|
||||||
val trafficJson = context.resources.openRawResource(R.raw.tomtom_traffic)
|
val trafficJson = context.resources.openRawResource(R.raw.tomtom_traffic)
|
||||||
|
|||||||
@@ -119,17 +119,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
val recentPlaces = settingsRepository.recentPlacesFlow.first()
|
val recentPlaces = settingsRepository.recentPlacesFlow.first()
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
val gson = GsonBuilder().serializeNulls().create()
|
||||||
val places = gson.fromJson(recentPlaces, Places::class.java)
|
val places = gson.fromJson(recentPlaces, Places::class.java)
|
||||||
val place = places.places.minByOrNull { it.lastDate.dec() }
|
for (place in places.places.sortedBy { it.lastDate }) {
|
||||||
if (place != null) {
|
|
||||||
val plLocation = location(place.longitude, place.latitude)
|
val plLocation = location(place.longitude, place.latitude)
|
||||||
val distance = repository.getRouteDistance(
|
val distance = plLocation.distanceTo(location)
|
||||||
location,
|
place.distance = distance
|
||||||
plLocation,
|
if (place.distance > 200F) {
|
||||||
carOrientation,
|
|
||||||
context
|
|
||||||
)
|
|
||||||
place.distance = distance.toFloat()
|
|
||||||
if (place.distance > 1F) {
|
|
||||||
recentPlace.postValue(place)
|
recentPlace.postValue(place)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@@ -152,7 +146,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
val gson = GsonBuilder().serializeNulls().create()
|
val gson = GsonBuilder().serializeNulls().create()
|
||||||
val recentPlaces = gson.fromJson(rp, Places::class.java)
|
val recentPlaces = gson.fromJson(rp, Places::class.java)
|
||||||
val pl = mutableListOf<Place>()
|
val pl = mutableListOf<Place>()
|
||||||
var id : Long = 0
|
var id: Long = 0
|
||||||
if (rp.isNotEmpty()) {
|
if (rp.isNotEmpty()) {
|
||||||
for (place in recentPlaces.places) {
|
for (place in recentPlaces.places) {
|
||||||
if (place.category.equals(Constants.RECENT)) {
|
if (place.category.equals(Constants.RECENT)) {
|
||||||
@@ -509,7 +503,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
val gson = GsonBuilder().serializeNulls().create()
|
val gson = GsonBuilder().serializeNulls().create()
|
||||||
val settingsRepository = getSettingsRepository(context)
|
val settingsRepository = getSettingsRepository(context)
|
||||||
val rp = settingsRepository.recentPlacesFlow.first()
|
val rp = settingsRepository.recentPlacesFlow.first()
|
||||||
var id : Long = 0
|
var id: Long = 0
|
||||||
if (rp.isNotEmpty()) {
|
if (rp.isNotEmpty()) {
|
||||||
val recentPlaces =
|
val recentPlaces =
|
||||||
gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate }
|
gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate }
|
||||||
@@ -579,9 +573,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
*/
|
*/
|
||||||
fun getSearchFilter(context: Context): SearchFilter {
|
fun getSearchFilter(context: Context): SearchFilter {
|
||||||
val repository = getSettingsRepository(context)
|
val repository = getSettingsRepository(context)
|
||||||
val avoidMotorway = runBlocking { repository.avoidMotorwayFlow.first() }
|
val avoidMotorway = runBlocking { repository.avoidMotorwayFlow.first() }
|
||||||
val avoidTollway = runBlocking { repository.avoidTollwayFlow.first() }
|
val avoidTollway = runBlocking { repository.avoidTollwayFlow.first() }
|
||||||
return SearchFilter(avoidMotorway, avoidTollway)
|
val avoidFerry = runBlocking { repository.avoidFerryFlow.first() }
|
||||||
|
return SearchFilter(avoidMotorway, avoidTollway, avoidFerry)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -591,7 +586,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
fun loadRecentPlace(context: Context): SnapshotStateList<Place?> {
|
fun loadRecentPlace(context: Context): SnapshotStateList<Place?> {
|
||||||
val pl = mutableListOf<Place>()
|
val pl = mutableListOf<Place>()
|
||||||
val settingsRepository = getSettingsRepository(context)
|
val settingsRepository = getSettingsRepository(context)
|
||||||
val rp = runBlocking { settingsRepository.recentPlacesFlow.first()}
|
val rp = runBlocking { settingsRepository.recentPlacesFlow.first() }
|
||||||
if (rp.isNotEmpty()) {
|
if (rp.isNotEmpty()) {
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
val gson = GsonBuilder().serializeNulls().create()
|
||||||
val recentPlaces = gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate }
|
val recentPlaces = gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate }
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val avoidFerry = repository.avoidFerryFlow.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(5_000),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
val carLocation = repository.carLocationFlow.stateIn(
|
val carLocation = repository.carLocationFlow.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(5_000),
|
SharingStarted.WhileSubscribed(5_000),
|
||||||
@@ -78,6 +84,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
|
|||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val traffic = repository.trafficFlow.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(5_000),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
fun onShow3DChanged(enabled: Boolean) {
|
fun onShow3DChanged(enabled: Boolean) {
|
||||||
viewModelScope.launch { repository.setShow3D(enabled) }
|
viewModelScope.launch { repository.setShow3D(enabled) }
|
||||||
}
|
}
|
||||||
@@ -94,6 +106,11 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
|
|||||||
viewModelScope.launch { repository.setAvoidTollway(enabled) }
|
viewModelScope.launch { repository.setAvoidTollway(enabled) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onAvoidFerry(enabled: Boolean) {
|
||||||
|
viewModelScope.launch { repository.setAvoidFerry(enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun onCarLocation(enabled: Boolean) {
|
fun onCarLocation(enabled: Boolean) {
|
||||||
viewModelScope.launch { repository.setCarLocation(enabled) }
|
viewModelScope.launch { repository.setCarLocation(enabled) }
|
||||||
}
|
}
|
||||||
@@ -118,4 +135,7 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
|
|||||||
viewModelScope.launch { repository.setGuidanceAudio(mode) }
|
viewModelScope.launch { repository.setGuidanceAudio(mode) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onTraffic(enabled: Boolean) {
|
||||||
|
viewModelScope.launch { repository.setTraffic(enabled) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ class SettingsRepository(
|
|||||||
val avoidTollwayFlow: Flow<Boolean> =
|
val avoidTollwayFlow: Flow<Boolean> =
|
||||||
dataStoreManager.avoidTollwayFlow
|
dataStoreManager.avoidTollwayFlow
|
||||||
|
|
||||||
|
val avoidFerryFlow: Flow<Boolean> =
|
||||||
|
dataStoreManager.avoidFerryFlow
|
||||||
|
|
||||||
val carLocationFlow: Flow<Boolean> =
|
val carLocationFlow: Flow<Boolean> =
|
||||||
dataStoreManager.useCarLocationFlow
|
dataStoreManager.useCarLocationFlow
|
||||||
|
|
||||||
@@ -29,7 +32,6 @@ class SettingsRepository(
|
|||||||
val tomTomApiKeyFlow: Flow<String> =
|
val tomTomApiKeyFlow: Flow<String> =
|
||||||
dataStoreManager.tomTomApiKeyFlow
|
dataStoreManager.tomTomApiKeyFlow
|
||||||
|
|
||||||
|
|
||||||
val recentPlacesFlow: Flow<String> =
|
val recentPlacesFlow: Flow<String> =
|
||||||
dataStoreManager.recentPlacesFlow
|
dataStoreManager.recentPlacesFlow
|
||||||
|
|
||||||
@@ -39,6 +41,8 @@ class SettingsRepository(
|
|||||||
val guidanceAudioFlow: Flow<Int> =
|
val guidanceAudioFlow: Flow<Int> =
|
||||||
dataStoreManager.guidanceAudioFlow
|
dataStoreManager.guidanceAudioFlow
|
||||||
|
|
||||||
|
val trafficFlow: Flow<Boolean> =
|
||||||
|
dataStoreManager.trafficFlow
|
||||||
|
|
||||||
suspend fun setShow3D(enabled: Boolean) {
|
suspend fun setShow3D(enabled: Boolean) {
|
||||||
dataStoreManager.setShow3D(enabled)
|
dataStoreManager.setShow3D(enabled)
|
||||||
@@ -56,6 +60,10 @@ class SettingsRepository(
|
|||||||
dataStoreManager.setAvoidTollway(enabled)
|
dataStoreManager.setAvoidTollway(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun setAvoidFerry(enabled: Boolean) {
|
||||||
|
dataStoreManager.setAvoidFerry(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setCarLocation(enabled: Boolean) {
|
suspend fun setCarLocation(enabled: Boolean) {
|
||||||
dataStoreManager.setCarLocation(enabled)
|
dataStoreManager.setCarLocation(enabled)
|
||||||
}
|
}
|
||||||
@@ -84,4 +92,7 @@ class SettingsRepository(
|
|||||||
dataStoreManager.setGuidanceAudio(mode)
|
dataStoreManager.setGuidanceAudio(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun setTraffic(enabled: Boolean) {
|
||||||
|
dataStoreManager.setTraffic(enabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
common/data/src/main/res/drawable/ev_station_24px.xml
Normal file
10
common/data/src/main/res/drawable/ev_station_24px.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M240,400L480,400L480,200Q480,200 480,200Q480,200 480,200L240,200Q240,200 240,200Q240,200 240,200L240,400ZM160,840L160,200Q160,167 183.5,143.5Q207,120 240,120L480,120Q513,120 536.5,143.5Q560,167 560,200L560,480L610,480Q639,480 659.5,500.5Q680,521 680,550L680,735Q680,752 694,766Q708,780 725,780Q743,780 756.5,766Q770,752 770,735L770,360L760,360Q743,360 731.5,348.5Q720,337 720,320L720,240L740,240L740,180L780,180L780,240L820,240L820,180L860,180L860,240L880,240L880,320Q880,337 868.5,348.5Q857,360 840,360L830,360L830,735Q830,777 799.5,808.5Q769,840 725,840Q682,840 651,808.5Q620,777 620,735L620,550Q620,545 617.5,542.5Q615,540 610,540L560,540L560,840L160,840ZM340,760L440,600L380,600L380,480L280,640L340,640L340,760Z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="48dp"
|
|
||||||
android:height="48dp"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:viewportHeight="960"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M220,408L489,408L489,180Q489,180 489,180Q489,180 489,180L220,180Q220,180 220,180Q220,180 220,180L220,408ZM160,840L160,180Q160,156 178,138Q196,120 220,120L489,120Q513,120 531,138Q549,156 549,180L549,468L614,468Q634.71,468 649.36,482.64Q664,497.29 664,518L664,737Q664,759 681.5,773.5Q699,788 722,788Q745,788 765,773.5Q785,759 785,737L785,350L770,350Q757.25,350 748.63,341.37Q740,332.75 740,320L740,230L760,230L760,180L790,180L790,230L830,230L830,180L860,180L860,230L880,230L880,320Q880,332.75 871.38,341.37Q862.75,350 850,350L835,350L835,736.69Q835,780 801,810Q767,840 721.82,840Q677.66,840 645.83,810Q614,780 614,737L614,518Q614,518 614,518Q614,518 614,518L549,518L549,840L160,840ZM337,746L425,606L372,606L372,501L285,641L337,641L337,746Z"/>
|
|
||||||
</vector>
|
|
||||||
10
common/data/src/main/res/drawable/local_pharmacy_24px.xml
Normal file
10
common/data/src/main/res/drawable/local_pharmacy_24px.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M120,840L120,760L200,520L120,280L120,200L628,200L686,40L780,74L734,200L840,200L840,280L760,520L840,760L840,840L120,840ZM440,680L520,680L520,560L640,560L640,480L520,480L520,360L440,360L440,480L320,480L320,560L440,560L440,680Z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="48dp"
|
|
||||||
android:height="48dp"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:viewportHeight="960"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M120,840L120,780L207,525L120,270L120,210L647,210L709,40L777,67L725,210L840,210L840,270L752,525L840,780L840,840L120,840ZM452,679L512,679L512,555L636,555L636,495L512,495L512,371L452,371L452,495L328,495L328,555L452,555L452,679ZM182,780L778,780L690,525L778,270L182,270L270,525L182,780ZM480,525L480,525L480,525L480,525L480,525L480,525Z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
<string name="drive_now">Losfahren</string>
|
<string name="drive_now">Losfahren</string>
|
||||||
<string name="avoid_tolls_row_title" msgid="5194057244144831024">"Mautstraßen vermeiden"</string>
|
<string name="avoid_tolls_row_title" msgid="5194057244144831024">"Mautstraßen vermeiden"</string>
|
||||||
<string name="avoid_highways_row_title" msgid="4711913426200490304">"Autobahnen vermeiden"</string>
|
<string name="avoid_highways_row_title" msgid="4711913426200490304">"Autobahnen vermeiden"</string>
|
||||||
|
<string name="avoid_ferries">Fähren vermeiden</string>
|
||||||
<string name="recent_destinations">Letzte Ziele</string>
|
<string name="recent_destinations">Letzte Ziele</string>
|
||||||
<string name="contacts">Kontakte</string>
|
<string name="contacts">Kontakte</string>
|
||||||
<string name="route_preview">Route Vorschau</string>
|
<string name="route_preview">Route Vorschau</string>
|
||||||
@@ -46,20 +47,22 @@
|
|||||||
<string name="fuel_station">Tankstelle</string>
|
<string name="fuel_station">Tankstelle</string>
|
||||||
<string name="pharmacy">Apotheke</string>
|
<string name="pharmacy">Apotheke</string>
|
||||||
<string name="charging_station">Ladestation</string>
|
<string name="charging_station">Ladestation</string>
|
||||||
<string name="speed_camera">Speed camera</string>
|
<string name="speed_camera">Blitzer</string>
|
||||||
<string name="use_car_location">Auto GPS verwenden</string>
|
<string name="use_car_location">Auto GPS verwenden</string>
|
||||||
<string name="tomtom">TomTom\t</string>
|
|
||||||
<string name="options">Optionen</string>
|
<string name="options">Optionen</string>
|
||||||
<string name="tomtom_api_key">TomTom ApiKey</string>
|
<string name="tomtom_api_key">TomTom ApiKey</string>
|
||||||
<string name="use_car_settings">Verwende Auto Einstellungen</string>
|
<string name="use_car_settings">Verwende Auto Einstellungen</string>
|
||||||
<string name="exit_number">Ausfahrt nummer</string>
|
<string name="exit_number">Ausfahrt nummer</string>
|
||||||
<string name="navigation_icon_description">Navigations Icon</string>
|
<string name="navigation_icon_description">Navigations Icon</string>
|
||||||
<string name="distance_units">Entfernungseinheiten</string>
|
<string name="distance_units">Entfernungseinheiten</string>
|
||||||
<string name="automaticaly">Automatisch</string>
|
<string name="automatically">Automatisch</string>
|
||||||
<string name="kilometer">Kilometer</string>
|
<string name="kilometer">Kilometer</string>
|
||||||
<string name="miles">Meilen</string>
|
<string name="miles">Meilen</string>
|
||||||
<string name="audio_settings">Töne</string>
|
<string name="audio_settings">Töne</string>
|
||||||
<string name="muted">Stummgeschaltet</string>
|
<string name="muted">Stummgeschaltet</string>
|
||||||
<string name="unmuted">Ton an</string>
|
<string name="unmuted">Ton an</string>
|
||||||
<string name="alerts_only">Nur Alarme</string>
|
<string name="alerts_only">Nur Alarme</string>
|
||||||
|
<string name="no_categories">Keine Kategorien</string>
|
||||||
|
<string name="general">Allgemein</string>
|
||||||
|
<string name="traffic">Verkehr anzeigen</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<string name="stop_action_title">Διακοπή</string>
|
<string name="stop_action_title">Διακοπή</string>
|
||||||
<string name="avoid_highways_row_title">Αποφυγή αυτοκινητοδρόμων</string>
|
<string name="avoid_highways_row_title">Αποφυγή αυτοκινητοδρόμων</string>
|
||||||
<string name="avoid_tolls_row_title">Αποφυγή διοδίων</string>
|
<string name="avoid_tolls_row_title">Αποφυγή διοδίων</string>
|
||||||
|
<string name="avoid_ferries">Αποφυγή φέρι μποτ</string>
|
||||||
<string name="no_places">Δεν βρέθηκαν τοποθεσίες</string>
|
<string name="no_places">Δεν βρέθηκαν τοποθεσίες</string>
|
||||||
<string name="recent_destinations">Πρόσφατοι προορισμοί</string>
|
<string name="recent_destinations">Πρόσφατοι προορισμοί</string>
|
||||||
<string name="contacts">Επαφές</string>
|
<string name="contacts">Επαφές</string>
|
||||||
@@ -38,11 +39,14 @@
|
|||||||
<string name="exit_number">Αριθμός εξόδου</string>
|
<string name="exit_number">Αριθμός εξόδου</string>
|
||||||
<string name="navigation_icon_description">Εικονίδιο πλοήγησης</string>
|
<string name="navigation_icon_description">Εικονίδιο πλοήγησης</string>
|
||||||
<string name="distance_units">Μονάδες απόστασης</string>
|
<string name="distance_units">Μονάδες απόστασης</string>
|
||||||
<string name="automaticaly">Αυτόματα</string>
|
<string name="automatically">Αυτόματα</string>
|
||||||
<string name="kilometer">Χιλιόμετρα</string>
|
<string name="kilometer">Χιλιόμετρα</string>
|
||||||
<string name="miles">Μίλια</string>
|
<string name="miles">Μίλια</string>
|
||||||
<string name="audio_settings">Φωνητική καθοδήγηση</string>
|
<string name="audio_settings">Φωνητική καθοδήγηση</string>
|
||||||
<string name="muted">Σίγαση</string>
|
<string name="muted">Σίγαση</string>
|
||||||
<string name="unmuted">Ήχος ενεργός</string>
|
<string name="unmuted">Ήχος ενεργός</string>
|
||||||
<string name="alerts_only">Μόνο ειδοποιήσεις</string>
|
<string name="alerts_only">Μόνο ειδοποιήσεις</string>
|
||||||
|
<string name="no_categories">Δεν υπάρχουν κατηγορίες</string>
|
||||||
|
<string name="general">Γενικά</string>
|
||||||
|
<string name="traffic">Εμφάνιση κίνησης</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
<string name="stop_action_title">Zatrzymaj</string>
|
<string name="stop_action_title">Zatrzymaj</string>
|
||||||
<string name="avoid_highways_row_title">Unikaj autostrad</string>
|
<string name="avoid_highways_row_title">Unikaj autostrad</string>
|
||||||
<string name="avoid_tolls_row_title">Unikaj opłat drogowych</string>
|
<string name="avoid_tolls_row_title">Unikaj opłat drogowych</string>
|
||||||
|
<string name="avoid_ferries">Unikaj promów</string>
|
||||||
<string name="no_places">Brak miejsc</string>
|
<string name="no_places">Brak miejsc</string>
|
||||||
<string name="recent_destinations">Ostatnie cele</string>
|
<string name="recent_destinations">Ostatnie cele</string>
|
||||||
<string name="contacts">Kontakty</string>
|
<string name="contacts">Kontakty</string>
|
||||||
@@ -38,11 +39,14 @@
|
|||||||
<string name="exit_number">Numer zjazdu</string>
|
<string name="exit_number">Numer zjazdu</string>
|
||||||
<string name="navigation_icon_description">Ikona nawigacji</string>
|
<string name="navigation_icon_description">Ikona nawigacji</string>
|
||||||
<string name="distance_units">Jednostki odległości</string>
|
<string name="distance_units">Jednostki odległości</string>
|
||||||
<string name="automaticaly">Automatycznie</string>
|
<string name="automatically">Automatycznie</string>
|
||||||
<string name="kilometer">Kilometry</string>
|
<string name="kilometer">Kilometry</string>
|
||||||
<string name="miles">Mile</string>
|
<string name="miles">Mile</string>
|
||||||
<string name="audio_settings">Wskazówki głosowe</string>
|
<string name="audio_settings">Wskazówki głosowe</string>
|
||||||
<string name="muted">Wyciszony</string>
|
<string name="muted">Wyciszony</string>
|
||||||
<string name="unmuted">Dźwięk włączony</string>
|
<string name="unmuted">Dźwięk włączony</string>
|
||||||
<string name="alerts_only">Tylko ostrzeżenia</string>
|
<string name="alerts_only">Tylko ostrzeżenia</string>
|
||||||
|
<string name="no_categories">Brak kategorii do wyświetlenia</string>
|
||||||
|
<string name="general">Ogólne</string>
|
||||||
|
<string name="traffic">Pokaż natężenie ruchu</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
<string name="stop_action_title">Stop</string>
|
<string name="stop_action_title">Stop</string>
|
||||||
<string name="avoid_highways_row_title">Avoid highways</string>
|
<string name="avoid_highways_row_title">Avoid highways</string>
|
||||||
<string name="avoid_tolls_row_title">Avoid tolls rows</string>
|
<string name="avoid_tolls_row_title">Avoid tolls rows</string>
|
||||||
|
<string name="avoid_ferries">Avoid ferries</string>
|
||||||
<string name="no_places">No places</string>
|
<string name="no_places">No places</string>
|
||||||
<string name="recent_destinations">Recent destinations</string>
|
<string name="recent_destinations">Recent destinations</string>
|
||||||
<string name="contacts">Contacts</string>
|
<string name="contacts">Contacts</string>
|
||||||
@@ -34,18 +35,21 @@
|
|||||||
<string name="osrm" translatable="false">Osrm</string>
|
<string name="osrm" translatable="false">Osrm</string>
|
||||||
<string name="routing_engine" translatable="false">Routing engine</string>
|
<string name="routing_engine" translatable="false">Routing engine</string>
|
||||||
<string name="use_car_location">Use car location</string>
|
<string name="use_car_location">Use car location</string>
|
||||||
<string name="tomtom" translatable="false">TomTom\t</string>
|
<string name="tomtom" translatable="false">TomTom</string>
|
||||||
<string name="options">Options</string>
|
<string name="options">Options</string>
|
||||||
<string name="tomtom_api_key">TomTom ApiKey</string>
|
<string name="tomtom_api_key">TomTom ApiKey</string>
|
||||||
<string name="use_car_settings">Use car settings</string>
|
<string name="use_car_settings">Use car settings</string>
|
||||||
<string name="exit_number">Exit number</string>
|
<string name="exit_number">Exit number</string>
|
||||||
<string name="navigation_icon_description">Navigation icon</string>
|
<string name="navigation_icon_description">Navigation icon</string>
|
||||||
<string name="distance_units">Distance units</string>
|
<string name="distance_units">Distance units</string>
|
||||||
<string name="automaticaly">Automaticaly</string>
|
<string name="automatically">Automatically</string>
|
||||||
<string name="kilometer">Kilometer</string>
|
<string name="kilometer">Kilometer</string>
|
||||||
<string name="miles">Miles</string>
|
<string name="miles">Miles</string>
|
||||||
<string name="audio_settings">Guidance audio</string>
|
<string name="audio_settings">Guidance audio</string>
|
||||||
<string name="muted">Muted</string>
|
<string name="muted">Muted</string>
|
||||||
<string name="unmuted">Unmuted</string>
|
<string name="unmuted">Unmuted</string>
|
||||||
<string name="alerts_only">Alerts only</string>
|
<string name="alerts_only">Alerts only</string>
|
||||||
|
<string name="no_categories">No categories to show</string>
|
||||||
|
<string name="general">General</string>
|
||||||
|
<string name="traffic">Show traffic</string>
|
||||||
</resources>
|
</resources>
|
||||||
27
gradle.properties
Normal file
27
gradle.properties
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. For more details, visit
|
||||||
|
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
|
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.configuration-cache=true
|
||||||
|
org.gradle.configuration-cache.problems=warn
|
||||||
Reference in New Issue
Block a user