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.annotation.SuppressLint
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationManager
import android.os.Bundle
@@ -34,23 +33,37 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
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.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
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.unit.dp
import androidx.core.location.LocationListenerCompat
import androidx.compose.ui.unit.sp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
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.Constants
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.StepData
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.NavigationUtils
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState
@@ -85,18 +100,24 @@ import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds
val geojson = MutableLiveData("")
val routeData = MutableLiveData("")
class MainActivity : ComponentActivity() {
val vieModel = ViewModel(NavigationRepository())
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 ->
routeModel.createNavigationRoute(newRoute)
geojson.value = routeModel.geoJson
homeLocation.latitude = 48.155782
homeLocation.longitude = 11.607921
routeModel.startNavigation(newRoute)
routeData.value = routeModel.route
}
val cameraPosition = MutableLiveData(
@@ -108,38 +129,19 @@ class MainActivity : ComponentActivity() {
init {
vieModel.route.observe(this, observer)
vieModel.loadRoute(
homeLocation,
Constants.home2Location
)
}
var mLocationListener: LocationListenerCompat = LocationListenerCompat { 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?) {
fun updateLocation(location: org.maplibre.compose.location.Location?) {
if (location != null) {
if (routeModel.isNavigating()) {
instruction.value = routeModel.currentStep()
}
val zoom = NavigationUtils().calculateZoom(location.speed)
cameraPosition.postValue(
cameraPosition.value!!.copy(
zoom = 15.0,
target = Position(location.longitude, location.latitude),
)
zoom = zoom,
target = location.position
),
)
}
}
@@ -164,8 +166,55 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
PlacesTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
ModalNavigationDrawer(
drawerContent = {
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()
}
@@ -173,12 +222,6 @@ class MainActivity : ComponentActivity() {
}
}
}
override fun onPause() {
super.onPause()
val locationManager =
getSystemService(LOCATION_SERVICE) as LocationManager
locationManager.removeUpdates(mLocationListener)
}
@SuppressLint("PermissionLaunchedDuringComposition")
@@ -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
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 geoJsonData: String? by geojson.observeAsState()
val route: String? by routeData.observeAsState()
val cameraState =
rememberCameraState(
firstPosition =
@@ -241,9 +313,10 @@ class MainActivity : ComponentActivity() {
)
)
val locationProvider = rememberDefaultLocationProvider()
val locationState = rememberUserLocationState(locationProvider)
if (locationState.location != null) {
curLocation.latitude = locationState.location?.position!!.latitude
curLocation.longitude = locationState.location?.position!!.longitude
}
MaplibreMap(
cameraState = cameraState,
//baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty"),
@@ -270,7 +343,7 @@ class MainActivity : ComponentActivity() {
)
getBaseSource(id = "openmaptiles")?.let { tiles ->
FillLayer(id = "example", visible = false, source = tiles, sourceLayer = "building")
RouteLayer(geoJsonData)
RouteLayer(route)
}
}
@@ -280,6 +353,7 @@ class MainActivity : ComponentActivity() {
bearing = position!!.bearing,
zoom = position!!.zoom,
target = position!!.target,
tilt = tilt
),
duration = 3.seconds
)
@@ -287,9 +361,10 @@ class MainActivity : ComponentActivity() {
}
@Composable
fun RouteLayer(geoJsonData: String?) {
fun RouteLayer(routeData: String?) {
if (routeData!!.isNotEmpty()) {
val routes =
rememberGeoJsonSource(GeoJsonData.JsonString(geoJsonData!!))
rememberGeoJsonSource(GeoJsonData.JsonString(routeData!!))
LineLayer(
id = "routes-casing",
source = routes,
@@ -303,6 +378,7 @@ class MainActivity : ComponentActivity() {
width = const(4.dp),
)
}
}
@Composable
fun PlaceList(viewModel: ViewModel = koinViewModel()) {

View File

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

View File

@@ -15,6 +15,7 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.graphics.Color
@@ -27,11 +28,11 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
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.rememberCameraState
import org.maplibre.compose.expressions.dsl.const
import org.maplibre.compose.layers.CircleLayer
import org.maplibre.compose.layers.FillLayer
import org.maplibre.compose.layers.LineLayer
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.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds
import androidx.compose.runtime.collectAsState
class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
private var routeModel: RouteCarModel) : DefaultLifecycleObserver {
var mVisibleArea: Rect? = null
var mStableArea: Rect? = null
class SurfaceRenderer(
carContext: CarContext, lifecycle: Lifecycle,
private var routeModel: RouteCarModel
) : DefaultLifecycleObserver {
private val mCarContext: CarContext = carContext
var lastLocation = Location(LocationManager.GPS_PROVIDER)
val cameraPosition = MutableLiveData(
@@ -60,15 +61,15 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
target = Position(latitude = 48.1857475, longitude = 11.5793627)
)
)
val geojson = MutableLiveData("")
val routeData = MutableLiveData("")
val previewGeojson = MutableLiveData("")
val previewRouteData = MutableLiveData("")
var preview = false
lateinit var mapView: ComposeView
val tilt = 60.0
val tilt = 55.0
val padding = PaddingValues(start = 150.dp, top = 250.dp)
val prePadding = PaddingValues(start = 150.dp, bottom = 300.dp)
@@ -114,15 +115,14 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
presentation.show()
}
}
override fun onVisibleAreaChanged(visibleArea: Rect) {
synchronized(this@SurfaceRenderer) {
mVisibleArea = visibleArea
}
}
override fun onStableAreaChanged(stableArea: Rect) {
synchronized(this@SurfaceRenderer) {
mStableArea = stableArea
}
}
@@ -155,10 +155,9 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
@Composable
fun Map() {
val position: CameraPosition? by cameraPosition.observeAsState()
val geoJsonData: String? by geojson.observeAsState()
val previewGeoJsonData: String? by previewGeojson.observeAsState()
val route: String? by routeData.observeAsState()
val previewRoute: String? by previewRouteData.observeAsState()
val cameraState =
rememberCameraState(
firstPosition =
@@ -172,15 +171,17 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
padding = getPaddingValues()
)
)
val variant = if (isSystemInDarkTheme()) "dark" else "light"
val locationProvider = rememberDefaultLocationProvider()
val locationState = rememberUserLocationState(locationProvider)
MaplibreMap(
cameraState = cameraState,
//baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/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(
idPrefix = "user-location",
locationState = locationState,
@@ -188,18 +189,6 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
accuracyThreshold = 10f,
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) {
@@ -217,26 +206,26 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
}
@Composable
fun RouteLayer(geoJsonData: String?, previewGeoJsonData: String?) {
if (geoJsonData!!.isNotEmpty()) {
fun RouteLayer(routeData: String?, previewRoute: String?) {
if (routeData!!.isNotEmpty()) {
val routes =
rememberGeoJsonSource(GeoJsonData.JsonString(geoJsonData!!))
rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
LineLayer(
id = "routes-casing",
source = routes,
color = const(Color.White),
width = const(12.dp),
width = const(16.dp),
)
LineLayer(
id = "routes",
source = routes,
color = const(Color.Blue),
width = const(10.dp),
width = const(14.dp),
)
}
if (previewGeoJsonData!!.isNotEmpty()) {
if (previewRoute!!.isNotEmpty()) {
val routes =
rememberGeoJsonSource(GeoJsonData.JsonString(previewGeoJsonData!!))
rememberGeoJsonSource(GeoJsonData.JsonString(previewRoute))
LineLayer(
id = "routes-casing-pre",
source = routes,
@@ -251,6 +240,7 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
)
}
}
override fun onCreate(owner: LifecycleOwner) {
Log.i(TAG, "SurfaceRenderer created")
mCarContext.getCarService(AppManager::class.java)
@@ -276,16 +266,22 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
fun updateLocation(location: Location) {
synchronized(this) {
var bearing = cameraPosition.value!!.bearing
var bearing: Double
if (routeModel.isNavigating()) {
bearing = routeModel.currentStep().bearing
} else {
bearing = cameraPosition.value!!.bearing
if (lastLocation.latitude != location.latitude) {
if (lastLocation.distanceTo(location) > 10) {
if (lastLocation.distanceTo(location) > 5) {
bearing = lastLocation.bearingTo(location).toDouble()
}
}
}
var zoom = NavigationUtils().calculateZoom(location.speed.toDouble())
if (preview) {
bearing = 0.0
zoom = 11.0
}
val zoom = calculateZoom(location)
cameraPosition.postValue(
cameraPosition.value!!.copy(
bearing = bearing,
@@ -298,30 +294,13 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
}
}
private fun calculateZoom(location: Location): Double {
if (preview) {
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
fun setRouteData() {
routeData.value = routeModel.route
preview = false
}
fun setPreviewGeoJson(geoRoute: String) {
previewGeojson.value = geoRoute
fun setPreviewRouteData(route: String) {
previewRouteData.value = route
preview = true
}
@@ -332,6 +311,7 @@ class SurfaceRenderer(carContext: CarContext, lifecycle: Lifecycle,
padding
}
}
companion
object {
private const val TAG = "MapRenderer"

View File

@@ -15,7 +15,6 @@
*/
package com.kouros.navigation.car.navigation
import android.net.Uri
import android.text.SpannableString
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
@@ -43,34 +42,27 @@ class RouteCarModel() : RouteModel() {
fun currentStep(carContext: CarContext): Step {
val maneuver = (maneuvers[maneuverIndex] as JSONObject)
val maneuverType = maneuver.getInt("type")
val distanceStepLeft = leftStepDistance() * 1000
var text = ""
val stepData = currentStep()
var routing: (Pair<Int, CarIcon>)
routing = if (hasArrived(maneuverType)) {
routingData(maneuverType, carContext)
} else {
routingData(ManeuverType.None.value, carContext)
}
when (distanceStepLeft) {
when (stepData.leftDistance) {
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
}
val maneuverType = maneuver.getInt("type")
routing = routingData(maneuverType, carContext)
}
}
else -> {
if (maneuver.optJSONArray("street_names") != null) {
text = maneuver.getJSONArray("street_names").get(0) as String
}
}
}
val currentStepCueWithImage: SpannableString =
createString(text)
createString(stepData.instruction)
val step =
Step.Builder(currentStepCueWithImage)
.setManeuver(
@@ -88,8 +80,8 @@ class RouteCarModel() : RouteModel() {
val maneuver = (maneuvers[maneuverIndex + 1] as JSONObject)
val maneuverType = maneuver.getInt("type")
val routing = routingData(maneuverType, carContext)
val distanceLeft = leftStepDistance() * 1000
var text = ""
val distanceLeft = leftStepDistance() * 1000
when (distanceLeft) {
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.navigation.car.NavigationCarAppService
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationMessage
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place
@@ -41,8 +40,8 @@ class NavigationScreen(
val vieModel = ViewModel(NavigationRepository())
val observer = Observer<String> { route ->
if (route.isNotEmpty()) {
routeModel.createNavigationRoute(route)
surfaceRenderer.setGeoJson()
routeModel.startNavigation(route)
surfaceRenderer.setRouteData()
invalidate()
}
}
@@ -84,8 +83,8 @@ class NavigationScreen(
.build()
)
.setOnClickListener {
surfaceRenderer.geojson.postValue("")
routeModel.stopNavigating()
surfaceRenderer.routeData.postValue("")
routeModel.stopNavigation()
invalidate()
}
.build()
@@ -241,7 +240,7 @@ class NavigationScreen(
fun updateTrip() {
if (routeModel.maneuverType == Maneuver.TYPE_DESTINATION && routeModel.leftStepDistance() * 1000 < 25.0) {
routeModel.arrived = true
routeModel.stopNavigating()
routeModel.stopNavigation()
}
invalidate()
}

View File

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

View File

@@ -47,26 +47,17 @@
<string name="no_action_title" msgid="1452124604210014010">"Nein"</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="bug_reported_toast_msg" msgid="2487119172744644317">"Fehler wurde gemeldet!"</string>
<string name="zoomed_in_toast_msg" msgid="8915301497303842649">"Herangezoomt"</string>
<string name="zoomed_out_toast_msg" msgid="6260981223227212493">"Herausgezoomt"</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="not_favorite_toast_msg" msgid="6831181108681007428">"Kein Favorit!"</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="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="parked_toast_msg" msgid="2532422265890824446">"Aktion „Geparkt“"</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="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>

View File

@@ -15,7 +15,7 @@
limitations under the License.
-->
<resources>
<string name="app_name" translatable="false">Showcase</string>
<string name="app_name" translatable="false">Navigation</string>
<!-- Action Titles -->
<string name="back_caps_action_title">BACK</string>
@@ -50,26 +50,17 @@
<string name="enable_all_rows">Enable All Rows</string>
<!-- Toast Messages -->
<string name="bug_reported_toast_msg">Bug reported!</string>
<string name="zoomed_in_toast_msg">Zoomed in</string>
<string name="zoomed_out_toast_msg">Zoomed out</string>
<string name="triggered_toast_msg">Triggered</string>
<string name="primary_toast_msg">Primary button pressed</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="nav_requested_toast_msg">Navigation Requested</string>
<string name="selected_route_toast_msg">Selected route</string>
<string name="visible_routes_toast_msg">Visible routes</string>
<string name="second_item_toast_msg">Clicked second item</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="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="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>

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
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.Test
@@ -14,5 +20,18 @@ class ExampleUnitTest {
@Test
fun addition_isCorrect() {
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 io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import io.objectbox.annotation.Index
import kotlinx.serialization.Serializable
data class Category(
@@ -52,6 +51,12 @@ data class ContactData(
val avatar: Uri?
)
data class StepData (
var instruction: String,
var leftDistance: Double,
var bearing: Double
)
//val places = mutableListOf<Place>()
/* Place(

View File

@@ -33,7 +33,9 @@ class Contacts(private var context: Context) {
while (moveToNext()) {
val contactId = getLong(getColumnIndex(ContactsContract.Data.CONTACT_ID))
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("David")) {
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.LocationManager
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.decodePolyline
import org.json.JSONArray
@@ -10,23 +11,19 @@ import org.json.JSONObject
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()
lateinit var maneuvers: JSONArray
lateinit var locations: JSONArray
lateinit var summary: JSONObject
lateinit var destination: Place
var navigating = false
var arrived = false
var maneuverIndex = 0
var maneuverType = 0
var currentIndex = 0
var distanceToStepEnd = 0F
@@ -38,13 +35,23 @@ open class RouteModel {
var distanceToRoute = 0F
var geoJson = ""
var route = ""
private fun decodeValhallaRoute(route: String) {
if (route.isEmpty() || route == "[]") {
data class Builder(
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;
}
val jObject = JSONObject(route)
val jObject = JSONObject(valhallaRoute)
val trip = jObject.getJSONObject("trip")
locations = trip.getJSONArray("locations")
val legs = trip.getJSONArray("legs")
@@ -54,13 +61,13 @@ open class RouteModel {
polylineLocations = decodePolyline(shape)
}
fun createNavigationRoute(route: String) {
decodeValhallaRoute(route)
fun startNavigation(valhallaRoute: String) {
decodeValhallaRoute(valhallaRoute)
for (i in 0..<maneuvers.length()) {
val maneuver = (maneuvers[i] as JSONObject)
routingManeuvers.add(maneuver)
}
geoJson = createGeoJson(polylineLocations)
route = createGeoJson(polylineLocations)
navigating = true
}
@@ -88,6 +95,31 @@ open class RouteModel {
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. */
private fun calculateCurrentIndex(
@@ -159,8 +191,6 @@ open class RouteModel {
val maneuver = routingManeuvers[maneuverIndex]
var leftDistance = maneuver.getDouble("length")
if (endIndex > 0) {
val percent = 100 * (endIndex - currentIndex) / (endIndex - beginIndex)
//leftDistance = leftDistance * percent / 100
leftDistance = (distanceToStepEnd / 1000).toDouble()
}
return leftDistance
@@ -190,11 +220,11 @@ open class RouteModel {
return arrived
}
fun stopNavigating() {
fun stopNavigation() {
navigating = false
polylineLocations = mutableListOf()
routingManeuvers = mutableListOf()
geoJson = ""
route = ""
maneuverIndex = 0
currentIndex = 0
distanceToStepEnd = 0F

View File

@@ -105,4 +105,20 @@ class NavigationUtils() {
//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"
runtimeLivedata = "1.9.4"
foundation = "1.9.4"
maplibre-composeMaterial3 = "0.12.2"
maplibre-compose = "0.12.1"
playServicesLocation = "21.3.0"
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" }
ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" }
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-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }