LocationPuck

This commit is contained in:
Dimitris
2025-11-15 12:38:40 +01:00
parent d63747e811
commit 1773ec2244
15 changed files with 568 additions and 213 deletions

View File

@@ -18,7 +18,6 @@ package com.kouros.navigation
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.os.Bundle import android.os.Bundle
@@ -34,23 +33,37 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButtonDefaults.Icon
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
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.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.location.LocationListenerCompat import androidx.compose.ui.unit.sp
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.example.places.ui.theme.PlacesTheme import com.example.places.ui.theme.PlacesTheme
@@ -59,10 +72,12 @@ import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.kouros.navigation.data.Category import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.NavigationUtils
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
@@ -85,18 +100,24 @@ import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
val geojson = MutableLiveData("") val routeData = MutableLiveData("")
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
val vieModel = ViewModel(NavigationRepository()) val vieModel = ViewModel(NavigationRepository())
val routeModel = RouteModel() val routeModel = RouteModel()
var tilt = 0.0
val curLocation = Location(LocationManager.GPS_PROVIDER)
val instruction: MutableLiveData<StepData> by lazy {
MutableLiveData<StepData>()
}
val observer = Observer<String> { newRoute -> val observer = Observer<String> { newRoute ->
routeModel.createNavigationRoute(newRoute) routeModel.startNavigation(newRoute)
geojson.value = routeModel.geoJson routeData.value = routeModel.route
homeLocation.latitude = 48.155782
homeLocation.longitude = 11.607921
} }
val cameraPosition = MutableLiveData( val cameraPosition = MutableLiveData(
@@ -108,38 +129,19 @@ class MainActivity : ComponentActivity() {
init { init {
vieModel.route.observe(this, observer) vieModel.route.observe(this, observer)
vieModel.loadRoute(
homeLocation,
Constants.home2Location
)
} }
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? -> fun updateLocation(location: org.maplibre.compose.location.Location?) {
updateLocation(location)
}
@SuppressLint("MissingPermission")
fun requestLocationUpdates() {
val locationManager =
getSystemService(LOCATION_SERVICE) as LocationManager
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
updateLocation(location)
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
/* minTimeMs= */ 100,
/* minDistanceM= */ 0f,
mLocationListener
)
}
fun updateLocation(location: Location?) {
if (location != null) { if (location != null) {
if (routeModel.isNavigating()) {
instruction.value = routeModel.currentStep()
}
val zoom = NavigationUtils().calculateZoom(location.speed)
cameraPosition.postValue( cameraPosition.postValue(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
zoom = 15.0, zoom = zoom,
target = Position(location.longitude, location.latitude), target = location.position
) ),
) )
} }
} }
@@ -164,23 +166,64 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
PlacesTheme { PlacesTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> ModalNavigationDrawer(
Column(modifier = Modifier.padding(innerPadding)) { drawerContent = {
CheckPermission() ModalDrawerSheet {
Text("Drawer title", modifier = Modifier.padding(16.dp))
HorizontalDivider()
NavigationDrawerItem(
label = { Text(text = "Drawer Item") },
selected = false,
onClick = { /*TODO*/ }
)
}
},
gesturesEnabled = false
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = {
Text("Navigate")
},
icon = { Icon(true) },
onClick = {
scope.launch {
snackbarHostState.showSnackbar("Starte Navigation")
}
if (!routeModel.isNavigating()) {
tilt = 60.0
vieModel.loadRoute(
curLocation,
Constants.home2Location
)
} else {
tilt = 0.0
routeModel.stopNavigation()
routeData.value = ""
}
}
)
}
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
CheckPermission()
}
} }
} }
} }
} }
} }
override fun onPause() {
super.onPause()
val locationManager =
getSystemService(LOCATION_SERVICE) as LocationManager
locationManager.removeUpdates(mLocationListener)
}
@SuppressLint("PermissionLaunchedDuringComposition") @SuppressLint("PermissionLaunchedDuringComposition")
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
@@ -224,11 +267,40 @@ class MainActivity : ComponentActivity() {
} }
} }
@Composable
fun NavigationInfo(step: StepData?) {
Card {
Column {
Icon(
painter = painterResource(com.kouros.android.cars.carappservice.R.drawable.ic_turn_normal_right),
contentDescription = stringResource(id = com.kouros.android.cars.carappservice.R.string.accept_action_title)
)
if (step != null) {
Text(text = step.bearing.toString(), fontSize = 25.sp)
Text(text = step.instruction, fontSize = 25.sp)
}
}
}
}
@Composable @Composable
fun Map() { fun Map() {
requestLocationUpdates() val step: StepData? by instruction.observeAsState()
Column {
if (step != null) {
NavigationInfo(step)
}
MapView()
}
}
@Composable
fun MapView() {
val locationProvider = rememberDefaultLocationProvider()
val locationState = rememberUserLocationState(locationProvider)
updateLocation(locationState.location)
val position: CameraPosition? by cameraPosition.observeAsState() val position: CameraPosition? by cameraPosition.observeAsState()
val geoJsonData: String? by geojson.observeAsState() val route: String? by routeData.observeAsState()
val cameraState = val cameraState =
rememberCameraState( rememberCameraState(
firstPosition = firstPosition =
@@ -241,9 +313,10 @@ class MainActivity : ComponentActivity() {
) )
) )
val locationProvider = rememberDefaultLocationProvider() if (locationState.location != null) {
val locationState = rememberUserLocationState(locationProvider) curLocation.latitude = locationState.location?.position!!.latitude
curLocation.longitude = locationState.location?.position!!.longitude
}
MaplibreMap( MaplibreMap(
cameraState = cameraState, cameraState = cameraState,
//baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty"), //baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty"),
@@ -270,7 +343,7 @@ class MainActivity : ComponentActivity() {
) )
getBaseSource(id = "openmaptiles")?.let { tiles -> getBaseSource(id = "openmaptiles")?.let { tiles ->
FillLayer(id = "example", visible = false, source = tiles, sourceLayer = "building") FillLayer(id = "example", visible = false, source = tiles, sourceLayer = "building")
RouteLayer(geoJsonData) RouteLayer(route)
} }
} }
@@ -280,6 +353,7 @@ class MainActivity : ComponentActivity() {
bearing = position!!.bearing, bearing = position!!.bearing,
zoom = position!!.zoom, zoom = position!!.zoom,
target = position!!.target, target = position!!.target,
tilt = tilt
), ),
duration = 3.seconds duration = 3.seconds
) )
@@ -287,21 +361,23 @@ class MainActivity : ComponentActivity() {
} }
@Composable @Composable
fun RouteLayer(geoJsonData: String?) { fun RouteLayer(routeData: String?) {
val routes = if (routeData!!.isNotEmpty()) {
rememberGeoJsonSource(GeoJsonData.JsonString(geoJsonData!!)) val routes =
LineLayer( rememberGeoJsonSource(GeoJsonData.JsonString(routeData!!))
id = "routes-casing", LineLayer(
source = routes, id = "routes-casing",
color = const(Color.White), source = routes,
width = const(6.dp), color = const(Color.White),
) width = const(6.dp),
LineLayer( )
id = "routes", LineLayer(
source = routes, id = "routes",
color = const(Color.Blue), source = routes,
width = const(4.dp), color = const(Color.Blue),
) width = const(4.dp),
)
}
} }
@Composable @Composable

View File

@@ -12,7 +12,6 @@ import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.ScreenManager import androidx.car.app.ScreenManager
import androidx.car.app.Session import androidx.car.app.Session
import androidx.car.app.navigation.model.Maneuver
import androidx.core.location.LocationListenerCompat import androidx.core.location.LocationListenerCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
@@ -25,19 +24,18 @@ 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.TAG import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.ObjectBox import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.model.RouteModel
class NavigationSession : Session() { class NavigationSession : Session() {
val uriScheme = "samples"; val uriScheme = "samples";
val uriHost = "navigation"; val uriHost = "navigation";
lateinit var route: RouteCarModel; lateinit var routeModel: RouteCarModel;
lateinit var navigationScreen: NavigationScreen lateinit var navigationScreen: NavigationScreen
lateinit var surfaceRenderer: SurfaceRenderer lateinit var surfaceRenderer: SurfaceRenderer
var locationIndex = 0 var locationIndex = 0
val test = true val test = false
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? -> var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? ->
updateLocation(location) updateLocation(location)
@@ -75,12 +73,12 @@ class NavigationSession : Session() {
} }
override fun onCreateScreen(intent: Intent): Screen { override fun onCreateScreen(intent: Intent): Screen {
route = RouteCarModel() routeModel = RouteCarModel()
ObjectBox.init(carContext); ObjectBox.init(carContext);
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, route) surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel)
navigationScreen = NavigationScreen(carContext, surfaceRenderer, route) navigationScreen = NavigationScreen(carContext, surfaceRenderer, routeModel)
if (carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) if (carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED == PackageManager.PERMISSION_GRANTED
@@ -152,7 +150,7 @@ class NavigationSession : Session() {
updateLocation(location) updateLocation(location)
locationManager.requestLocationUpdates( locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, LocationManager.GPS_PROVIDER,
/* minTimeMs= */ 100, /* minTimeMs= */ 1000,
/* minDistanceM= */ 0f, /* minDistanceM= */ 0f,
mLocationListener mLocationListener
) )
@@ -169,14 +167,14 @@ class NavigationSession : Session() {
} }
fun test(location: Location?) { fun test(location: Location?) {
if (route.isNavigating() && locationIndex < route.polylineLocations.size) { if (routeModel.isNavigating() && locationIndex < routeModel.polylineLocations.size) {
val loc = route.polylineLocations[locationIndex] val loc = routeModel.polylineLocations[locationIndex]
val curLocation = Location(LocationManager.GPS_PROVIDER) val curLocation = Location(LocationManager.GPS_PROVIDER)
curLocation.longitude = loc[0] curLocation.longitude = loc[0]
curLocation.latitude = loc[1] curLocation.latitude = loc[1]
update(curLocation) update(curLocation)
locationIndex += 1 locationIndex += 1
if (locationIndex > route.polylineLocations.size) { if (locationIndex > routeModel.polylineLocations.size) {
val locationManager = val locationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
locationManager.removeUpdates(mLocationListener) locationManager.removeUpdates(mLocationListener)
@@ -188,12 +186,12 @@ class NavigationSession : Session() {
fun update(location: Location) { fun update(location: Location) {
surfaceRenderer.updateLocation(location) surfaceRenderer.updateLocation(location)
if (route.isNavigating()) { if (routeModel.isNavigating()) {
route.findManeuver(location) routeModel.findManeuver(location)
// if (routingModel.distanceToRoute > 50) { // if (routeModel.distanceToRoute > 50) {
// routingModel.stopNavigating() // routeModel.stopNavigation()
// locationIndex = 0 // locationIndex = 0
// surfaceRenderer.setGeoJson() // surfaceRenderer.setRouteData()
// navigationScreen.reRoute() // navigationScreen.reRoute()
// } // }
navigationScreen.updateTrip() navigationScreen.updateTrip()

View File

@@ -15,6 +15,7 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -27,11 +28,11 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.utils.NavigationUtils.Utils.createGeoJson import com.kouros.navigation.utils.NavigationUtils
import kotlinx.coroutines.flow.onSubscription
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.const
import org.maplibre.compose.layers.CircleLayer
import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.FillLayer
import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.layers.LineLayer
import org.maplibre.compose.location.LocationPuck import org.maplibre.compose.location.LocationPuck
@@ -45,13 +46,13 @@ import org.maplibre.compose.sources.rememberGeoJsonSource
import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import androidx.compose.runtime.collectAsState
class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle, class SurfaceRenderer(
private var routeModel: RouteCarModel) : DefaultLifecycleObserver { carContext: CarContext, lifecycle: Lifecycle,
var mVisibleArea: Rect? = null private var routeModel: RouteCarModel
var mStableArea: Rect? = null ) : DefaultLifecycleObserver {
private val mCarContext: CarContext = carContext private val mCarContext: CarContext = carContext
var lastLocation = Location(LocationManager.GPS_PROVIDER) var lastLocation = Location(LocationManager.GPS_PROVIDER)
val cameraPosition = MutableLiveData( val cameraPosition = MutableLiveData(
@@ -60,15 +61,15 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
target = Position(latitude = 48.1857475, longitude = 11.5793627) target = Position(latitude = 48.1857475, longitude = 11.5793627)
) )
) )
val geojson = MutableLiveData("") val routeData = MutableLiveData("")
val previewGeojson = MutableLiveData("") val previewRouteData = MutableLiveData("")
var preview = false var preview = false
lateinit var mapView: ComposeView lateinit var mapView: ComposeView
val tilt = 60.0 val tilt = 55.0
val padding = PaddingValues(start = 150.dp, top = 250.dp) val padding = PaddingValues(start = 150.dp, top = 250.dp)
val prePadding = PaddingValues(start = 150.dp, bottom = 300.dp) val prePadding = PaddingValues(start = 150.dp, bottom = 300.dp)
@@ -114,15 +115,14 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
presentation.show() presentation.show()
} }
} }
override fun onVisibleAreaChanged(visibleArea: Rect) { override fun onVisibleAreaChanged(visibleArea: Rect) {
synchronized(this@SurfaceRenderer) { synchronized(this@SurfaceRenderer) {
mVisibleArea = visibleArea
} }
} }
override fun onStableAreaChanged(stableArea: Rect) { override fun onStableAreaChanged(stableArea: Rect) {
synchronized(this@SurfaceRenderer) { synchronized(this@SurfaceRenderer) {
mStableArea = stableArea
} }
} }
@@ -155,10 +155,9 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
@Composable @Composable
fun Map() { fun Map() {
val position: CameraPosition? by cameraPosition.observeAsState() val position: CameraPosition? by cameraPosition.observeAsState()
val geoJsonData: String? by geojson.observeAsState() val route: String? by routeData.observeAsState()
val previewGeoJsonData: String? by previewGeojson.observeAsState() val previewRoute: String? by previewRouteData.observeAsState()
val cameraState = val cameraState =
rememberCameraState( rememberCameraState(
firstPosition = firstPosition =
@@ -172,15 +171,17 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
padding = getPaddingValues() padding = getPaddingValues()
) )
) )
val variant = if (isSystemInDarkTheme()) "dark" else "light"
val locationProvider = rememberDefaultLocationProvider() val locationProvider = rememberDefaultLocationProvider()
val locationState = rememberUserLocationState(locationProvider) val locationState = rememberUserLocationState(locationProvider)
MaplibreMap( MaplibreMap(
cameraState = cameraState, cameraState = cameraState,
//baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty"), //baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty"),
baseStyle = BaseStyle.Uri("https://kouros-online.de/liberty"), baseStyle = BaseStyle.Uri("https://kouros-online.de/liberty"),
) { ) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
FillLayer(id = "example", visible = false, source = tiles, sourceLayer = "building")
RouteLayer(route, previewRoute)
}
LocationPuck( LocationPuck(
idPrefix = "user-location", idPrefix = "user-location",
locationState = locationState, locationState = locationState,
@@ -188,18 +189,6 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
accuracyThreshold = 10f, accuracyThreshold = 10f,
colors = LocationPuckColors(accuracyStrokeColor = Color.Green) colors = LocationPuckColors(accuracyStrokeColor = Color.Green)
) )
getBaseSource(id = "openmaptiles")?.let { tiles ->
FillLayer(id = "example", visible = false, source = tiles, sourceLayer = "building")
val coordinates = mutableListOf<List<Double>>()
coordinates.add(listOf(position!!.target.longitude, position!!.target.latitude))
coordinates.add(
listOf(
position!!.target.longitude + 0.00001,
position!!.target.latitude + 0.00001
)
)
RouteLayer(geoJsonData, previewGeoJsonData)
}
} }
LaunchedEffect(position) { LaunchedEffect(position) {
@@ -217,26 +206,26 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
} }
@Composable @Composable
fun RouteLayer(geoJsonData: String?, previewGeoJsonData: String?) { fun RouteLayer(routeData: String?, previewRoute: String?) {
if (geoJsonData!!.isNotEmpty()) { if (routeData!!.isNotEmpty()) {
val routes = val routes =
rememberGeoJsonSource(GeoJsonData.JsonString(geoJsonData!!)) rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
LineLayer( LineLayer(
id = "routes-casing", id = "routes-casing",
source = routes, source = routes,
color = const(Color.White), color = const(Color.White),
width = const(12.dp), width = const(16.dp),
) )
LineLayer( LineLayer(
id = "routes", id = "routes",
source = routes, source = routes,
color = const(Color.Blue), color = const(Color.Blue),
width = const(10.dp), width = const(14.dp),
) )
} }
if (previewGeoJsonData!!.isNotEmpty()) { if (previewRoute!!.isNotEmpty()) {
val routes = val routes =
rememberGeoJsonSource(GeoJsonData.JsonString(previewGeoJsonData!!)) rememberGeoJsonSource(GeoJsonData.JsonString(previewRoute))
LineLayer( LineLayer(
id = "routes-casing-pre", id = "routes-casing-pre",
source = routes, source = routes,
@@ -251,6 +240,7 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
) )
} }
} }
override fun onCreate(owner: LifecycleOwner) { override fun onCreate(owner: LifecycleOwner) {
Log.i(TAG, "SurfaceRenderer created") Log.i(TAG, "SurfaceRenderer created")
mCarContext.getCarService(AppManager::class.java) mCarContext.getCarService(AppManager::class.java)
@@ -276,16 +266,22 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
fun updateLocation(location: Location) { fun updateLocation(location: Location) {
synchronized(this) { synchronized(this) {
var bearing = cameraPosition.value!!.bearing var bearing: Double
if (lastLocation.latitude != location.latitude) { if (routeModel.isNavigating()) {
if (lastLocation.distanceTo(location) > 10) { bearing = routeModel.currentStep().bearing
bearing = lastLocation.bearingTo(location).toDouble() } else {
bearing = cameraPosition.value!!.bearing
if (lastLocation.latitude != location.latitude) {
if (lastLocation.distanceTo(location) > 5) {
bearing = lastLocation.bearingTo(location).toDouble()
}
} }
} }
var zoom = NavigationUtils().calculateZoom(location.speed.toDouble())
if (preview) { if (preview) {
bearing = 0.0 bearing = 0.0
zoom = 11.0
} }
val zoom = calculateZoom(location)
cameraPosition.postValue( cameraPosition.postValue(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
bearing = bearing, bearing = bearing,
@@ -298,30 +294,13 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
} }
} }
private fun calculateZoom(location: Location): Double { fun setRouteData() {
if (preview) { routeData.value = routeModel.route
return 11.0
}
//var zoom = cameraPosition.value!!.zoom
val zoom = when (location.speed.toInt()) {
in 0..10 -> 17.0
in 11..20 -> 16.0
in 21..30 -> 15.0
in 31..40 -> 14.0
in 41..50 -> 13.0
in 51..60 -> 12.0
else -> 11
}
return zoom.toDouble()
}
fun setGeoJson() {
geojson.value = routeModel.geoJson
preview = false preview = false
} }
fun setPreviewGeoJson(geoRoute: String) { fun setPreviewRouteData(route: String) {
previewGeojson.value = geoRoute previewRouteData.value = route
preview = true preview = true
} }
@@ -332,6 +311,7 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
padding padding
} }
} }
companion companion
object { object {
private const val TAG = "MapRenderer" private const val TAG = "MapRenderer"

View File

@@ -15,7 +15,6 @@
*/ */
package com.kouros.navigation.car.navigation package com.kouros.navigation.car.navigation
import android.net.Uri
import android.text.SpannableString import android.text.SpannableString
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
@@ -43,34 +42,27 @@ class RouteCarModel() : RouteModel() {
fun currentStep(carContext: CarContext): Step { fun currentStep(carContext: CarContext): Step {
val maneuver = (maneuvers[maneuverIndex] as JSONObject) val maneuver = (maneuvers[maneuverIndex] as JSONObject)
val maneuverType = maneuver.getInt("type") val maneuverType = maneuver.getInt("type")
val distanceStepLeft = leftStepDistance() * 1000
var text = "" val stepData = currentStep()
var routing: (Pair<Int, CarIcon>) var routing: (Pair<Int, CarIcon>)
routing = if (hasArrived(maneuverType)) { routing = if (hasArrived(maneuverType)) {
routingData(maneuverType, carContext) routingData(maneuverType, carContext)
} else { } else {
routingData(ManeuverType.None.value, carContext) routingData(ManeuverType.None.value, carContext)
} }
when (distanceStepLeft) { when (stepData.leftDistance) {
in 0.0..100.0 -> { in 0.0..100.0 -> {
if (maneuverIndex < maneuvers.length()) { if (maneuverIndex < maneuvers.length()) {
val maneuver = (maneuvers[maneuverIndex + 1] as JSONObject) val maneuver = (maneuvers[maneuverIndex + 1] as JSONObject)
if (maneuver.optJSONArray("street_names") != null) {
text = maneuver.getJSONArray("street_names").get(0) as String
}
val maneuverType = maneuver.getInt("type") val maneuverType = maneuver.getInt("type")
routing = routingData(maneuverType, carContext) routing = routingData(maneuverType, carContext)
} }
} }
else -> {
if (maneuver.optJSONArray("street_names") != null) {
text = maneuver.getJSONArray("street_names").get(0) as String
}
}
} }
val currentStepCueWithImage: SpannableString = val currentStepCueWithImage: SpannableString =
createString(text) createString(stepData.instruction)
val step = val step =
Step.Builder(currentStepCueWithImage) Step.Builder(currentStepCueWithImage)
.setManeuver( .setManeuver(
@@ -88,8 +80,8 @@ class RouteCarModel() : RouteModel() {
val maneuver = (maneuvers[maneuverIndex + 1] as JSONObject) val maneuver = (maneuvers[maneuverIndex + 1] as JSONObject)
val maneuverType = maneuver.getInt("type") val maneuverType = maneuver.getInt("type")
val routing = routingData(maneuverType, carContext) val routing = routingData(maneuverType, carContext)
val distanceLeft = leftStepDistance() * 1000
var text = "" var text = ""
val distanceLeft = leftStepDistance() * 1000
when (distanceLeft) { when (distanceLeft) {
in 0.0..100.0 -> { in 0.0..100.0 -> {

View File

@@ -24,7 +24,6 @@ import androidx.lifecycle.Observer
import com.kouros.android.cars.carappservice.R import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.car.NavigationCarAppService import com.kouros.navigation.car.NavigationCarAppService
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationMessage
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
@@ -41,8 +40,8 @@ class NavigationScreen(
val vieModel = ViewModel(NavigationRepository()) val vieModel = ViewModel(NavigationRepository())
val observer = Observer<String> { route -> val observer = Observer<String> { route ->
if (route.isNotEmpty()) { if (route.isNotEmpty()) {
routeModel.createNavigationRoute(route) routeModel.startNavigation(route)
surfaceRenderer.setGeoJson() surfaceRenderer.setRouteData()
invalidate() invalidate()
} }
} }
@@ -84,8 +83,8 @@ class NavigationScreen(
.build() .build()
) )
.setOnClickListener { .setOnClickListener {
surfaceRenderer.geojson.postValue("") surfaceRenderer.routeData.postValue("")
routeModel.stopNavigating() routeModel.stopNavigation()
invalidate() invalidate()
} }
.build() .build()
@@ -241,7 +240,7 @@ class NavigationScreen(
fun updateTrip() { fun updateTrip() {
if (routeModel.maneuverType == Maneuver.TYPE_DESTINATION && routeModel.leftStepDistance() * 1000 < 25.0) { if (routeModel.maneuverType == Maneuver.TYPE_DESTINATION && routeModel.leftStepDistance() * 1000 < 25.0) {
routeModel.arrived = true routeModel.arrived = true
routeModel.stopNavigating() routeModel.stopNavigation()
} }
invalidate() invalidate()
} }

View File

@@ -67,8 +67,8 @@ class RoutePreviewScreen(
val navigationMessage = NavigationMessage(carContext) val navigationMessage = NavigationMessage(carContext)
val observer = Observer<String> { route -> val observer = Observer<String> { route ->
if (route.isNotEmpty()) { if (route.isNotEmpty()) {
routeModel.createNavigationRoute(route) routeModel.startNavigation(route)
surfaceRenderer.setPreviewGeoJson(routeModel.geoJson) surfaceRenderer.setPreviewRouteData(routeModel.route)
val geocoder = Geocoder(carContext) val geocoder = Geocoder(carContext)
geocoder.getFromLocation(destination.latitude, destination.longitude, 1) { geocoder.getFromLocation(destination.latitude, destination.longitude, 1) {
for (address in it) { for (address in it) {

View File

@@ -47,26 +47,17 @@
<string name="no_action_title" msgid="1452124604210014010">"Nein"</string> <string name="no_action_title" msgid="1452124604210014010">"Nein"</string>
<string name="disable_all_rows" msgid="3003225080532928046">"Alle Zeilen deaktivieren"</string> <string name="disable_all_rows" msgid="3003225080532928046">"Alle Zeilen deaktivieren"</string>
<string name="enable_all_rows" msgid="7274285275711872091">"Alle Zeilen aktivieren"</string> <string name="enable_all_rows" msgid="7274285275711872091">"Alle Zeilen aktivieren"</string>
<string name="bug_reported_toast_msg" msgid="2487119172744644317">"Fehler wurde gemeldet!"</string>
<string name="zoomed_in_toast_msg" msgid="8915301497303842649">"Herangezoomt"</string> <string name="zoomed_in_toast_msg" msgid="8915301497303842649">"Herangezoomt"</string>
<string name="zoomed_out_toast_msg" msgid="6260981223227212493">"Herausgezoomt"</string> <string name="zoomed_out_toast_msg" msgid="6260981223227212493">"Herausgezoomt"</string>
<string name="triggered_toast_msg" msgid="3396166539208366382">"Ausgelöst"</string> <string name="triggered_toast_msg" msgid="3396166539208366382">"Ausgelöst"</string>
<string name="primary_toast_msg" msgid="7153771322662005447">"Primäre Schaltfläche gedrückt"</string>
<string name="search_toast_msg" msgid="7826530065407699347">"Schaltfläche „Suchen“ gedrückt"</string>
<string name="options_toast_msg" msgid="2146223786877557730">"Optionsschaltfläche gedrückt"</string>
<string name="favorite_toast_msg" msgid="522064494016370117">"Favorit!"</string> <string name="favorite_toast_msg" msgid="522064494016370117">"Favorit!"</string>
<string name="not_favorite_toast_msg" msgid="6831181108681007428">"Kein Favorit!"</string> <string name="not_favorite_toast_msg" msgid="6831181108681007428">"Kein Favorit!"</string>
<string name="nav_requested_toast_msg" msgid="6696525973145493908">"Navigation angefragt"</string> <string name="nav_requested_toast_msg" msgid="6696525973145493908">"Navigation angefragt"</string>
<string name="selected_route_toast_msg" msgid="3149189677200086656">"Ausgewählte Route"</string> <string name="selected_route_toast_msg" msgid="3149189677200086656">"Ausgewählte Route"</string>
<string name="visible_routes_toast_msg" msgid="7065558153736024203">"Sichtbare Routen"</string> <string name="visible_routes_toast_msg" msgid="7065558153736024203">"Sichtbare Routen"</string>
<string name="second_item_toast_msg" msgid="7210054709419608215">"Zweiter Eintrag angeklickt"</string>
<string name="third_item_checked_toast_msg" msgid="3022450599567347361">"Dritter Eintrag aktiviert"</string>
<string name="fifth_item_checked_toast_msg" msgid="1627599668504718594">"Fünfter Eintrag aktiviert"</string>
<string name="sixth_item_toast_msg" msgid="6117028866385793707">"Sechster Eintrag angeklickt"</string>
<string name="settings_toast_msg" msgid="7697794473002342727">"Einstellungen angeklickt"</string> <string name="settings_toast_msg" msgid="7697794473002342727">"Einstellungen angeklickt"</string>
<string name="parked_toast_msg" msgid="2532422265890824446">"Aktion „Geparkt“"</string> <string name="parked_toast_msg" msgid="2532422265890824446">"Aktion „Geparkt“"</string>
<string name="more_toast_msg" msgid="5938288138225509885">"„Mehr“ angeklickt"</string> <string name="more_toast_msg" msgid="5938288138225509885">"„Mehr“ angeklickt"</string>
<string name="commute_toast_msg" msgid="4112684360647638688">"Schaltfläche für Arbeitsweg gedrückt"</string>
<string name="grant_location_permission_toast_msg" msgid="268046297444808010">"Standortermittlung erlauben, um aktuellen Standort anzuzeigen"</string> <string name="grant_location_permission_toast_msg" msgid="268046297444808010">"Standortermittlung erlauben, um aktuellen Standort anzuzeigen"</string>
<string name="sign_in_with_google_toast_msg" msgid="5720947549233124775">"Über Google anmelden beginnt hier"</string> <string name="sign_in_with_google_toast_msg" msgid="5720947549233124775">"Über Google anmelden beginnt hier"</string>
<string name="changes_selection_to_index_toast_msg_prefix" msgid="957766225794389167">"Auswahl auf Index geändert"</string> <string name="changes_selection_to_index_toast_msg_prefix" msgid="957766225794389167">"Auswahl auf Index geändert"</string>

View File

@@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<string name="app_name" translatable="false">Showcase</string> <string name="app_name" translatable="false">Navigation</string>
<!-- Action Titles --> <!-- Action Titles -->
<string name="back_caps_action_title">BACK</string> <string name="back_caps_action_title">BACK</string>
@@ -50,27 +50,18 @@
<string name="enable_all_rows">Enable All Rows</string> <string name="enable_all_rows">Enable All Rows</string>
<!-- Toast Messages --> <!-- Toast Messages -->
<string name="bug_reported_toast_msg">Bug reported!</string> <string name="zoomed_in_toast_msg">Zoomed in</string>
<string name="zoomed_in_toast_msg">Zoomed in</string>
<string name="zoomed_out_toast_msg">Zoomed out</string> <string name="zoomed_out_toast_msg">Zoomed out</string>
<string name="triggered_toast_msg">Triggered</string> <string name="triggered_toast_msg">Triggered</string>
<string name="primary_toast_msg">Primary button pressed</string> <string name="favorite_toast_msg">Favorite!</string>
<string name="search_toast_msg">Search button pressed</string>
<string name="options_toast_msg">Options button pressed</string>
<string name="favorite_toast_msg">Favorite!</string>
<string name="not_favorite_toast_msg">Not a favorite!</string> <string name="not_favorite_toast_msg">Not a favorite!</string>
<string name="nav_requested_toast_msg">Navigation Requested</string> <string name="nav_requested_toast_msg">Navigation Requested</string>
<string name="selected_route_toast_msg">Selected route</string> <string name="selected_route_toast_msg">Selected route</string>
<string name="visible_routes_toast_msg">Visible routes</string> <string name="visible_routes_toast_msg">Visible routes</string>
<string name="second_item_toast_msg">Clicked second item</string> <string name="settings_toast_msg">Clicked Settings</string>
<string name="third_item_checked_toast_msg">Third item checked</string>
<string name="fifth_item_checked_toast_msg">Fifth item checked</string>
<string name="sixth_item_toast_msg">Clicked sixth item</string>
<string name="settings_toast_msg">Clicked Settings</string>
<string name="parked_toast_msg">Parked action</string> <string name="parked_toast_msg">Parked action</string>
<string name="more_toast_msg">Clicked More</string> <string name="more_toast_msg">Clicked More</string>
<string name="commute_toast_msg">Commute button pressed</string> <string name="grant_location_permission_toast_msg">Grant location Permission to see current location</string>
<string name="grant_location_permission_toast_msg">Grant location Permission to see current location</string>
<string name="sign_in_with_google_toast_msg">Sign-in with Google starts here</string> <string name="sign_in_with_google_toast_msg">Sign-in with Google starts here</string>
<string name="changes_selection_to_index_toast_msg_prefix">Changed selection to index</string> <string name="changes_selection_to_index_toast_msg_prefix">Changed selection to index</string>
<string name="yes_action_toast_msg">Yes button pressed!</string> <string name="yes_action_toast_msg">Yes button pressed!</string>

View File

@@ -0,0 +1,255 @@
{
"trip": {
"locations": [
{
"type": "break",
"lat": 48.185749,
"lon": 11.579374,
"side_of_street": "right",
"original_index": 0
},
{
"type": "break",
"lat": 48.116481,
"lon": 11.594322,
"street": "Hohenwaldeckstr. 27",
"side_of_street": "left",
"original_index": 1
}
],
"legs": [
{
"maneuvers": [
{
"type": 2,
"instruction": "Auf Vogelhartstraße Richtung Westen fahren.",
"verbal_succinct_transition_instruction": "Richtung Westen fahren. Dann Rechts auf Silcherstraße abbiegen.",
"verbal_pre_transition_instruction": "Auf Vogelhartstraße Richtung Westen fahren. Dann Rechts auf Silcherstraße abbiegen.",
"verbal_post_transition_instruction": "70 Meter weiter der Route folgen.",
"street_names": [
"Vogelhartstraße"
],
"bearing_after": 273,
"time": 16.965,
"length": 0.07,
"cost": 34.428,
"begin_shape_index": 0,
"end_shape_index": 6,
"verbal_multi_cue": true,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Rechts auf Silcherstraße abbiegen.",
"verbal_transition_alert_instruction": "Rechts auf Silcherstraße abbiegen.",
"verbal_succinct_transition_instruction": "Rechts abbiegen.",
"verbal_pre_transition_instruction": "Rechts auf Silcherstraße abbiegen.",
"verbal_post_transition_instruction": "200 Meter weiter der Route folgen.",
"street_names": [
"Silcherstraße"
],
"bearing_before": 273,
"bearing_after": 5,
"time": 43.25,
"length": 0.156,
"cost": 89.306,
"begin_shape_index": 6,
"end_shape_index": 13,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Rechts auf Schmalkaldener Straße abbiegen.",
"verbal_transition_alert_instruction": "Rechts auf Schmalkaldener Straße abbiegen.",
"verbal_succinct_transition_instruction": "Rechts abbiegen.",
"verbal_pre_transition_instruction": "Rechts auf Schmalkaldener Straße abbiegen.",
"verbal_post_transition_instruction": "400 Meter weiter der Route folgen.",
"street_names": [
"Schmalkaldener Straße"
],
"bearing_before": 2,
"bearing_after": 93,
"time": 108.947,
"length": 0.43,
"cost": 217.43,
"begin_shape_index": 13,
"end_shape_index": 29,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Rechts auf Ingolstädter Straße/B 13 abbiegen.",
"verbal_transition_alert_instruction": "Rechts auf Ingolstädter Straße abbiegen.",
"verbal_succinct_transition_instruction": "Rechts abbiegen.",
"verbal_pre_transition_instruction": "Rechts auf Ingolstädter Straße, B 13 abbiegen.",
"verbal_post_transition_instruction": "einen Kilometer weiter der Route folgen.",
"street_names": [
"B 13"
],
"begin_street_names": [
"Ingolstädter Straße",
"B 13"
],
"bearing_before": 88,
"bearing_after": 178,
"time": 147.528,
"length": 1.064,
"cost": 230.646,
"begin_shape_index": 29,
"end_shape_index": 65,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 19,
"instruction": "Auf die Auffahrt nach links abbiegen.",
"verbal_transition_alert_instruction": "Auf die Auffahrt nach links abbiegen.",
"verbal_pre_transition_instruction": "Auf die Auffahrt nach links abbiegen.",
"street_names": [
"Schenkendorfstraße"
],
"bearing_before": 188,
"bearing_after": 98,
"time": 61.597,
"length": 0.374,
"cost": 117.338,
"begin_shape_index": 65,
"end_shape_index": 84,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 24,
"instruction": "Links halten auf B 2R.",
"verbal_transition_alert_instruction": "Links halten auf B 2R.",
"verbal_pre_transition_instruction": "Links halten auf B 2R.",
"verbal_post_transition_instruction": "6 Kilometer weiter der Route folgen.",
"street_names": [
"B 2R"
],
"bearing_before": 117,
"bearing_after": 118,
"time": 509.658,
"length": 6.37,
"cost": 580.602,
"begin_shape_index": 84,
"end_shape_index": 240,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 20,
"instruction": "An der Ausfahrt rechts abfahren.",
"verbal_transition_alert_instruction": "An der Ausfahrt rechts abfahren.",
"verbal_pre_transition_instruction": "An der Ausfahrt rechts abfahren.",
"verbal_post_transition_instruction": "einen Kilometer weiter der Route folgen.",
"street_names": [
"Ampfingstraße"
],
"bearing_before": 191,
"bearing_after": 206,
"time": 133.661,
"length": 1.031,
"cost": 226.661,
"begin_shape_index": 240,
"end_shape_index": 280,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Rechts auf Anzinger Straße abbiegen.",
"verbal_transition_alert_instruction": "Rechts auf Anzinger Straße abbiegen.",
"verbal_succinct_transition_instruction": "Rechts abbiegen.",
"verbal_pre_transition_instruction": "Rechts auf Anzinger Straße abbiegen.",
"verbal_post_transition_instruction": "1.5 Kilometer weiter der Route folgen.",
"street_names": [
"Anzinger Straße"
],
"bearing_before": 182,
"bearing_after": 277,
"time": 211.637,
"length": 1.444,
"cost": 450.654,
"begin_shape_index": 280,
"end_shape_index": 334,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 15,
"instruction": "Links auf Hohenwaldeckstraße abbiegen.",
"verbal_transition_alert_instruction": "Links auf Hohenwaldeckstraße abbiegen.",
"verbal_succinct_transition_instruction": "Links abbiegen.",
"verbal_pre_transition_instruction": "Links auf Hohenwaldeckstraße abbiegen.",
"verbal_post_transition_instruction": "200 Meter weiter der Route folgen.",
"street_names": [
"Hohenwaldeckstraße"
],
"bearing_before": 249,
"bearing_after": 170,
"time": 45.365,
"length": 0.183,
"cost": 84.344,
"begin_shape_index": 334,
"end_shape_index": 342,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 6,
"instruction": "Hohenwaldeckstr. 27 befindet sich auf der linken Seite.",
"verbal_transition_alert_instruction": "Hohenwaldeckstr. 27 befindet sich auf der linken Seite.",
"verbal_pre_transition_instruction": "Hohenwaldeckstr. 27 befindet sich auf der linken Seite.",
"bearing_before": 184,
"time": 0,
"length": 0,
"cost": 0,
"begin_shape_index": 342,
"end_shape_index": 342,
"travel_mode": "drive",
"travel_type": "car"
}
],
"summary": {
"level_changes": [
[220, -1]
],
"has_time_restrictions": false,
"has_toll": false,
"has_highway": false,
"has_ferry": false,
"min_lat": 48.116486,
"min_lon": 11.578422,
"max_lat": 48.186957,
"max_lon": 11.616382,
"time": 1278.611,
"length": 11.123,
"cost": 2031.412
},
"shape": "mk_|zA_}vaUA^MzKKhKMrKMrLEbEeLs@kGa@yV}AmIa@cJ]g@AgQc@TgTn@ak@\\}Y`@u~@NiZRss@Ekc@AcGAwE?iB@yN@mH@_L?sAOiVEiGrISbH[|s@kC`KY~Qw@dk@mBdBErH]bIa@pNk@pAGxgA}ErQw@f`@eB`AAhEOjDO~Kg@bh@cCpTcAtEUlBKtFMbk@cBpt@eDfScAlH]hHY`HTdATbBl@rAd@|Bz@xBr@|F`AzD\\l@mHHsCAeDcAkImBiLs@}Ii@wOh@a]vAu[bB{VjCmXjCuUtDoU~A}JjBmIvDmOvDgOfCiJdB}HvAsG|FwSzGaV`IgWdC_K\\cG~Pii@pUcr@dYaz@lEkMdDsPpMm]|Tqj@tQwc@jQsb@nVwm@vEmL`k@suAxHyPzFaKrBaD|AmBxCeDpC}BvDmCnEyBnDuAzFyAhDWdDW|D?~CPjFj@lHrB|QzI~O~Jb]lS`ZbR~NnIhCdBdDtBb`Azk@fhAdr@vN~I~l@|]vr@vVb]jElZw@xG{@jEw@`KiCfLiFrJ_GnPaNjJaJzJoNxMgUtU}g@d]_w@f_@ev@tO_YhRmYbHwIxG_I|NwN~AyAdTiP~b@iZ~J}GxScQ`JoIfGwGtEwFnCqDbEaG~CcFhHgMrGcNxHmR|FaQnCwIpAeEdCiIje@}}AnQil@pGySjCyIvSor@nJ{ZpHwVbQyk@zIkXlHkTrOcc@bPic@rUyk@vKoVnMaWfWed@rT_`@jQcZhBwCzCsEtCcElC}CzCiDrCiCnH{GzLwJlGwDfH_ElGeDhHwClHyBrG{BdK_CzMsBzJgAbIq@jI?nYGbSTfGDvEDnGRfWx@|i@lEvpBbUdRrBpLfAl_@jDr|Ghm@hu@jGrWrBpd@fAxG[raBgUl[{HhM}DzOeGdXiLpCuAzwAij@lEcAbF_AnEc@|DDzE[dAGrIh@|APfe@lLbc@lLpWbIbdAnYnKlBf]|Tzi@rZbl@|ZtSjJpOhG~HvB`AVvBl@hBf@vBl@`AXrJrCtZjHhRvCrKpAjAN|Gb@hLr@dLp@xYbB`CPlDNxBLlOv@n{@jE~F\\lDP|Ov@lSn@rGNrGPlHAxICnJCvJBhFBrQL~E?dC?zFJ[xIcB|_@}Add@kAvi@i@zg@AtGEd_@f@lq@lB`|@jApd@tA`l@XpFn@lHf@bFnAdMjA`HnDpQfEfSvElPfBdOvCrWjP|xANrA|@bH\\pC\\lCNnA~Hhn@dB|MnAtJrAlK`AzH~@hHvBvPj@pEl@zFx@zH^hD~BlQdEbZhF`_@rAbJ|AjK~AtKzBrNt@nFv@lFtB`OdCfQbMb_AlDnUrDvTvF|ZzIxh@jDm@zHgBhF{@lC[`L]jMr@bNj@~_@bB"
}
],
"summary": {
"has_time_restrictions": false,
"has_toll": false,
"has_highway": false,
"has_ferry": false,
"min_lat": 48.116486,
"min_lon": 11.578422,
"max_lat": 48.186957,
"max_lon": 11.616382,
"time": 1278.611,
"length": 11.123,
"cost": 2031.412
},
"status_message": "Found route between points",
"status": 0,
"units": "kilometers",
"language": "de-DE"
},
"id": "my_work_route"
}

View File

@@ -1,6 +1,12 @@
package com.kouros.navigation.car package com.kouros.navigation.car
import com.kouros.navigation.car.navigation.RoutingModel import android.location.Location
import android.location.LocationManager
import com.kouros.navigation.data.Constants.home2Location
import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
@@ -14,5 +20,18 @@ class ExampleUnitTest {
@Test @Test
fun addition_isCorrect() { fun addition_isCorrect() {
assertEquals(4, 2 + 2) assertEquals(4, 2 + 2)
val model = RouteModel()
val repo = NavigationRepository()
val viewModel = ViewModel(repo)
val fromLocation = Location(LocationManager.GPS_PROVIDER)
fromLocation.latitude = homeLocation.latitude
fromLocation.longitude = homeLocation.longitude
val toLocation = Location(LocationManager.GPS_PROVIDER)
toLocation.latitude = home2Location.latitude
toLocation.longitude = home2Location.longitude
val route = repo.getRoute(fromLocation, toLocation)
model.startNavigation(route)
println(route)
} }
} }

View File

@@ -21,7 +21,6 @@ import android.location.LocationManager
import android.net.Uri import android.net.Uri
import io.objectbox.annotation.Entity import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id import io.objectbox.annotation.Id
import io.objectbox.annotation.Index
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
data class Category( data class Category(
@@ -52,6 +51,12 @@ data class ContactData(
val avatar: Uri? val avatar: Uri?
) )
data class StepData (
var instruction: String,
var leftDistance: Double,
var bearing: Double
)
//val places = mutableListOf<Place>() //val places = mutableListOf<Place>()
/* Place( /* Place(

View File

@@ -33,7 +33,9 @@ class Contacts(private var context: Context) {
while (moveToNext()) { while (moveToNext()) {
val contactId = getLong(getColumnIndex(ContactsContract.Data.CONTACT_ID)) val contactId = getLong(getColumnIndex(ContactsContract.Data.CONTACT_ID))
val name = getString(getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)) val name = getString(getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME))
if (name.contains("Jola") || name.contains("Dominic") if (name.contains("Jola")
|| name.contains("Dominic")
|| name.contains("Martha")
|| name.contains("Μεντή") || name.contains("Μεντή")
|| name.contains("David")) { || name.contains("David")) {
val mimeType: String = getString(getColumnIndex(ContactsContract.Data.MIMETYPE)) val mimeType: String = getString(getColumnIndex(ContactsContract.Data.MIMETYPE))

View File

@@ -3,6 +3,7 @@ package com.kouros.navigation.model
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.NavigationUtils.Utils.createGeoJson import com.kouros.navigation.utils.NavigationUtils.Utils.createGeoJson
import com.kouros.navigation.utils.NavigationUtils.Utils.decodePolyline import com.kouros.navigation.utils.NavigationUtils.Utils.decodePolyline
import org.json.JSONArray import org.json.JSONArray
@@ -10,23 +11,19 @@ import org.json.JSONObject
import kotlin.math.roundToInt import kotlin.math.roundToInt
open class RouteModel { // Source - https://stackoverflow.com/a
// Posted by Dmitrii Bychkov
// Retrieved 2025-11-14, License - CC BY-SA 4.0
open class RouteModel () {
var polylineLocations: List<List<Double>> = emptyList() var polylineLocations: List<List<Double>> = emptyList()
lateinit var maneuvers: JSONArray lateinit var maneuvers: JSONArray
lateinit var locations: JSONArray lateinit var locations: JSONArray
lateinit var summary: JSONObject lateinit var summary: JSONObject
lateinit var destination: Place lateinit var destination: Place
var navigating = false var navigating = false
var arrived = false var arrived = false
var maneuverIndex = 0 var maneuverIndex = 0
var maneuverType = 0 var maneuverType = 0
var currentIndex = 0 var currentIndex = 0
var distanceToStepEnd = 0F var distanceToStepEnd = 0F
@@ -38,13 +35,23 @@ open class RouteModel {
var distanceToRoute = 0F var distanceToRoute = 0F
var geoJson = "" var route = ""
private fun decodeValhallaRoute(route: String) { data class Builder(
if (route.isEmpty() || route == "[]") { var route: String? = null,
var fromLocation: Location? = null,
var toLocation: Location? = null) {
fun route(route: String) = apply { this.route = route }
fun fromLocation(fromLocation: Location) = apply { this.fromLocation = fromLocation }
fun toLocation(toLocation: Location) = apply { this.toLocation = toLocation }
//fun build() = RouteModel(route!!, fromLocation!!, toLocation!!)
}
private fun decodeValhallaRoute(valhallaRoute: String) {
if (valhallaRoute.isEmpty() || valhallaRoute == "[]") {
return; return;
} }
val jObject = JSONObject(route) val jObject = JSONObject(valhallaRoute)
val trip = jObject.getJSONObject("trip") val trip = jObject.getJSONObject("trip")
locations = trip.getJSONArray("locations") locations = trip.getJSONArray("locations")
val legs = trip.getJSONArray("legs") val legs = trip.getJSONArray("legs")
@@ -54,13 +61,13 @@ open class RouteModel {
polylineLocations = decodePolyline(shape) polylineLocations = decodePolyline(shape)
} }
fun createNavigationRoute(route: String) { fun startNavigation(valhallaRoute: String) {
decodeValhallaRoute(route) decodeValhallaRoute(valhallaRoute)
for (i in 0..<maneuvers.length()) { for (i in 0..<maneuvers.length()) {
val maneuver = (maneuvers[i] as JSONObject) val maneuver = (maneuvers[i] as JSONObject)
routingManeuvers.add(maneuver) routingManeuvers.add(maneuver)
} }
geoJson = createGeoJson(polylineLocations) route = createGeoJson(polylineLocations)
navigating = true navigating = true
} }
@@ -88,6 +95,31 @@ open class RouteModel {
distanceToRoute = nearestDistance distanceToRoute = nearestDistance
} }
fun currentStep(): StepData {
var bearing = 0
val maneuver = (maneuvers[maneuverIndex] as JSONObject)
var text = ""
if (maneuver.optJSONArray("street_names") != null) {
text = maneuver.getJSONArray("street_names").get(0) as String
}
if (maneuver.has("bearing_after")) {
bearing = maneuver.getInt("bearing_after")
}
val distanceStepLeft = leftStepDistance() * 1000
when (distanceStepLeft) {
in 0.0..100.0 -> {
if (maneuverIndex < maneuvers.length()) {
val maneuver = (maneuvers[maneuverIndex + 1] as JSONObject)
if (maneuver.optJSONArray("street_names") != null) {
text = maneuver.getJSONArray("street_names").get(0) as String
}
}
}
}
return StepData(text, distanceStepLeft, bearing.toDouble())
}
/** Calculates the index in a maneuver. */ /** Calculates the index in a maneuver. */
private fun calculateCurrentIndex( private fun calculateCurrentIndex(
@@ -159,8 +191,6 @@ open class RouteModel {
val maneuver = routingManeuvers[maneuverIndex] val maneuver = routingManeuvers[maneuverIndex]
var leftDistance = maneuver.getDouble("length") var leftDistance = maneuver.getDouble("length")
if (endIndex > 0) { if (endIndex > 0) {
val percent = 100 * (endIndex - currentIndex) / (endIndex - beginIndex)
//leftDistance = leftDistance * percent / 100
leftDistance = (distanceToStepEnd / 1000).toDouble() leftDistance = (distanceToStepEnd / 1000).toDouble()
} }
return leftDistance return leftDistance
@@ -190,11 +220,11 @@ open class RouteModel {
return arrived return arrived
} }
fun stopNavigating() { fun stopNavigation() {
navigating = false navigating = false
polylineLocations = mutableListOf() polylineLocations = mutableListOf()
routingManeuvers = mutableListOf() routingManeuvers = mutableListOf()
geoJson = "" route = ""
maneuverIndex = 0 maneuverIndex = 0
currentIndex = 0 currentIndex = 0
distanceToStepEnd = 0F distanceToStepEnd = 0F

View File

@@ -105,4 +105,20 @@ class NavigationUtils() {
//return LatLng(toDegrees(asin(sinLat)), toDegrees(fromLng + dLng)) //return LatLng(toDegrees(asin(sinLat)), toDegrees(fromLng + dLng))
} }
} }
fun calculateZoom(speed: Double?): Double {
if (speed == null) {
return 18.0
}
val zoom = when (speed.toInt()) {
in 0..10 -> 17.0
in 11..20 -> 17.0
in 21..30 -> 17.0
in 31..40 -> 16.0
in 41..50 -> 15.0
in 51..60 -> 14.0
else -> 11
}
return zoom.toDouble()
}
} }

View File

@@ -17,6 +17,7 @@ ui = "1.9.4"
material3 = "1.4.0" material3 = "1.4.0"
runtimeLivedata = "1.9.4" runtimeLivedata = "1.9.4"
foundation = "1.9.4" foundation = "1.9.4"
maplibre-composeMaterial3 = "0.12.2"
maplibre-compose = "0.12.1" maplibre-compose = "0.12.1"
playServicesLocation = "21.3.0" playServicesLocation = "21.3.0"
runtime = "1.9.4" runtime = "1.9.4"
@@ -38,7 +39,7 @@ androidx-car-app = { group = "androidx.car.app", name = "app", version.ref = "ca
#objectbox-kotlin = { module = "io.objectbox:objectbox-kotlin", version.ref = "objectboxKotlin" } #objectbox-kotlin = { module = "io.objectbox:objectbox-kotlin", version.ref = "objectboxKotlin" }
ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" } ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" }
maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre-compose" } maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre-compose" }
maplibre-composeMaterial3 = { module = "org.maplibre.compose:maplibre-compose-material3", version = "maplibre-compose" } maplibre-composeMaterial3 = { module = "org.maplibre.compose:maplibre-compose-material3", version = "maplibre-composeMaterial3" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" } androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }