Categories

This commit is contained in:
Dimitris
2026-03-09 18:55:13 +01:00
parent 8c103a1f96
commit 61ce09f393
38 changed files with 864 additions and 338 deletions

View File

@@ -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"
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
@@ -74,7 +72,7 @@ class NavigationScreen(
/** Starts navigation. */ /** Starts navigation. */
fun startNavigation() fun startNavigation()
/** Updates trip information. */ /** Updates trip information. */
fun updateTrip(trip: Trip) fun updateTrip(trip: Trip)
} }
@@ -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())
}
} }
/** /**

View File

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

View File

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

View File

@@ -83,5 +83,4 @@ class AudioSettings(
private fun onSelected(index: Int) { private fun onSelected(index: Int) {
settingsViewModel.onGuidanceAudioChanged(index) settingsViewModel.onGuidanceAudioChanged(index)
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="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>

View File

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

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M120,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>

View File

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

View File

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

View File

@@ -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>
</resources> <string name="no_categories">Δεν υπάρχουν κατηγορίες</string>
<string name="general">Γενικά</string>
<string name="traffic">Εμφάνιση κίνησης</string>
</resources>

View File

@@ -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>
</resources> <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>

View File

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