Refactoring

This commit is contained in:
Dimitris
2025-12-10 17:08:25 +01:00
parent aeca6ff237
commit a02673af36
92 changed files with 557 additions and 1643 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 6
versionName = "0.1.3.6"
versionCode = 7
versionName = "0.1.3.7"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -93,6 +93,7 @@ dependencies {
implementation(libs.androidx.compose.material3.window.size.class1)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.window)
implementation(libs.androidx.compose.foundation.layout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)

View File

@@ -13,14 +13,10 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
@@ -31,31 +27,23 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.window.layout.WindowMetricsCalculator
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.car.map.BuildingLayer
import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.car.map.RouteLayer
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.MockLocation
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.ui.theme.NavigationTheme
import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.location
@@ -63,18 +51,12 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.LocationTrackingEffect
import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.location.Location
import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds
@@ -88,15 +70,19 @@ class MainActivity : ComponentActivity() {
var tilt = 50.0
val useMock = true
val instruction: MutableLiveData<StepData> by lazy {
val stepData: MutableLiveData<StepData> by lazy {
MutableLiveData<StepData>()
}
var lastLocation = location(0.0, 0.0)
val observer = Observer<String> { newRoute ->
routeModel.startNavigation(newRoute)
routeData.value = routeModel.route.routeGeoJson
if (newRoute.isNotEmpty()) {
routeModel.startNavigation(newRoute)
routeData.value = routeModel.route.routeGeoJson
//mock.setMockLocation(homeLocation.latitude, homeLocation.longitude)
simulate()
}
}
val cameraPosition = MutableLiveData(
CameraPosition(
@@ -129,8 +115,8 @@ class MainActivity : ComponentActivity() {
if (useMock) {
mock = MockLocation(locationManager)
mock.setMockLocation(
Constants.homeLocation.latitude,
Constants.homeLocation.longitude
homeLocation.latitude,
homeLocation.longitude
)
}
enableEdgeToEdge()
@@ -139,6 +125,21 @@ class MainActivity : ComponentActivity() {
}
}
@SuppressLint("MissingPermission")
@Composable
fun CheckPermissionScreen() {
val permissions = listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
)
PermissionScreen(
permissions = permissions,
requiredPermissions = listOf(permissions.first()),
onGranted = {
Content()
},
)
}
@SuppressLint("AutoboxingStateCreation")
@OptIn(ExperimentalMaterial3Api::class)
@@ -146,6 +147,8 @@ class MainActivity : ComponentActivity() {
fun Content() {
val scaffoldState = rememberBottomSheetScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val locationProvider = rememberDefaultLocationProvider(
updateInterval = 0.5.seconds,
desiredAccuracy = DesiredAccuracy.Highest
@@ -157,7 +160,15 @@ class MainActivity : ComponentActivity() {
if (locationState.value != null) {
latitude = locationState.value!!.position.latitude
}
val step: StepData? by instruction.observeAsState()
val step: StepData? by stepData.observeAsState()
fun openSheet() {
scope.launch { scaffoldState.bottomSheetState.expand() }
}
fun closeSheet() {
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
}
NavigationTheme {
BottomSheetScaffold(
@@ -167,7 +178,7 @@ class MainActivity : ComponentActivity() {
scaffoldState = scaffoldState,
sheetPeekHeight = 128.dp,
sheetContent = {
SheetContent(latitude, step)
SheetContent(latitude, step) { closeSheet() }
},
) { innerPadding ->
Box(
@@ -176,117 +187,35 @@ class MainActivity : ComponentActivity() {
.padding(innerPadding),
contentAlignment = Alignment.Center,
) {
Map(userLocationState, step)
MapView(applicationContext,userLocationState, step, cameraPosition, routeData, tilt)
}
}
}
}
@Composable
fun SheetContent(locationState: Double, step: StepData?) {
fun SheetContent(locationState: Double, step: StepData?, closeSheet: () -> Unit) {
if (!routeModel.isNavigating()) {
SearchSheet(applicationContext, viewModel, lastLocation)
SearchSheet(applicationContext, viewModel, lastLocation) { closeSheet() }
} else {
NavigationSheet( routeModel, step, { simulate() })
}
// to recomposite SheetContent !
Text("State $locationState")
}
@Composable
fun NavigationInfo(step: StepData?) {
Card {
Column {
Icon(
painter = painterResource(R.drawable.ic_turn_normal_right),
contentDescription = stringResource(id = R.string.accept_action_title)
)
if (step != null) {
Text(text = step.instruction, fontSize = 25.sp)
}
}
}
}
@Composable
fun Map(userLocationState: UserLocationState, step: StepData?) {
Column {
if (step != null) {
NavigationInfo(step)
}
MapView(userLocationState)
}
}
@Composable
fun MapView(userLocationState: UserLocationState) {
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this)
val width = metrics.bounds.width()
val height = metrics.bounds.height()
val paddingValues = PaddingValues(start = 0.dp, top = 350.dp)
val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState()
val cameraState =
rememberCameraState(
firstPosition =
CameraPosition(
target = Position(
position!!.target.latitude,
position!!.target.longitude
),
zoom = 15.0,
)
NavigationSheet(
routeModel, step!!,
{ stopNavigation { closeSheet() } },
{ simulateNavigation() }
)
Box (contentAlignment = Alignment.Center) {
MaplibreMap(
options = MapOptions(
ornamentOptions =
OrnamentOptions(isScaleBarEnabled = false)
),
cameraState = cameraState,
baseStyle = BaseStyle.Uri(Constants.STYLE),
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!NavigationUtils.getBooleanKeyValue(
context = applicationContext,
Constants.SHOW_THREED_BUILDING
)
) {
BuildingLayer(tiles)
}
RouteLayer(route, "", position!!.zoom)
}
if (userLocationState.location != null) {
///PuckState(cameraState, userLocationState)
}
}
LocationTrackingEffect(
locationState = userLocationState,
) {
cameraState.animateTo(
finalPosition = CameraPosition(
bearing = position!!.bearing,
zoom = position!!.zoom,
target = position!!.target,
tilt = tilt,
padding = paddingValues
),
duration = 1.seconds
)
}
NavigationImage(paddingValues, width, height /6, "")
}
// For recomposition!
Text("$locationState", fontSize = 12.sp)
}
fun updateLocation(location: org.maplibre.compose.location.Location?) {
fun updateLocation(location: Location?) {
if (location != null
&& lastLocation.latitude != location.position.latitude
&& lastLocation.longitude != location.position.longitude) {
&& lastLocation.longitude != location.position.longitude
) {
if (routeModel.isNavigating()) {
routeModel.updateLocation(lastLocation)
instruction.value = routeModel.currentStep()
stepData.value = routeModel.currentStep()
}
val currentLocation = location(location.position.longitude, location.position.latitude)
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
@@ -306,6 +235,17 @@ class MainActivity : ComponentActivity() {
}
}
fun stopNavigation(closeSheet: () -> Unit) {
closeSheet()
routeModel.stopNavigation()
routeData.value = ""
stepData.value = StepData("", 0.0, 0, 0, 0, 0.0)
}
fun simulateNavigation() {
simulate()
}
private fun checkMockLocationEnabled() {
try {
// Check if mock location is enabled for this app
@@ -330,24 +270,9 @@ class MainActivity : ComponentActivity() {
}
}
@SuppressLint("MissingPermission")
@Composable
fun CheckPermissionScreen() {
val permissions = listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
)
PermissionScreen(
permissions = permissions,
requiredPermissions = listOf(permissions.first()),
onGranted = {
Content()
},
)
}
@OptIn(DelicateCoroutinesApi::class)
fun simulate() = GlobalScope.async {
for ((i, loc) in routeModel.route.waypoints.withIndex()) {
for ((_, loc) in routeModel.route.waypoints.withIndex()) {
if (routeModel.isNavigating()) {
mock.setMockLocation(loc[1], loc[0])
delay(1000L) //

View File

@@ -0,0 +1,103 @@
package com.kouros.navigation.ui
import android.content.Context
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.window.layout.WindowMetricsCalculator
import com.kouros.navigation.car.map.BuildingLayer
import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.car.map.RouteLayer
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.NavigationUtils
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.LocationTrackingEffect
import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds
@Composable
fun MapView(
applicationContext: Context,
userLocationState: UserLocationState,
step: StepData?,
cameraPosition: MutableLiveData<CameraPosition>,
routeData: MutableLiveData<String>,
tilt: Double
) {
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(applicationContext)
val width = metrics.bounds.width()
val height = metrics.bounds.height()
val paddingValues = PaddingValues(start = 0.dp, top = 350.dp)
val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState()
val cameraState =
rememberCameraState(
firstPosition =
CameraPosition(
target = Position(
position!!.target.latitude,
position!!.target.longitude
),
zoom = 15.0,
)
)
Column {
NavigationInfo(step)
Box(contentAlignment = Alignment.Center) {
MaplibreMap(
options = MapOptions(
ornamentOptions =
OrnamentOptions(isScaleBarEnabled = false)
),
cameraState = cameraState,
baseStyle = BaseStyle.Uri(Constants.STYLE),
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!NavigationUtils.getBooleanKeyValue(
context = applicationContext,
Constants.SHOW_THREED_BUILDING
)
) {
BuildingLayer(tiles)
}
RouteLayer(route, "", position!!.zoom)
}
if (userLocationState.location != null) {
///PuckState(cameraState, userLocationState)
}
}
LocationTrackingEffect(
locationState = userLocationState,
) {
cameraState.animateTo(
finalPosition = CameraPosition(
bearing = position!!.bearing,
zoom = position!!.zoom,
target = position!!.target,
tilt = tilt,
padding = paddingValues
),
duration = 1.seconds
)
}
NavigationImage(paddingValues, width, height / 6, "")
}
}
}

View File

@@ -0,0 +1,51 @@
package com.kouros.navigation.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.kouros.data.R
import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.round
@Composable
fun NavigationInfo(step: StepData?) {
if (step != null && step.instruction.isNotEmpty()) {
Card(modifier = Modifier.padding(top = 60.dp)) {
Column() {
Row {
Icon(
painter = painterResource(step.icon),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp),
)
Column {
if (step.leftStepDistance < 1000) {
Text(text = "${step.leftStepDistance.toInt()} m", fontSize = 25.sp)
} else {
Text(
text = "${(step.leftStepDistance / 1000).round(1)} km",
fontSize = 25.sp
)
}
Text(text = step.instruction, fontSize = 20.sp)
}
Icon(
painter = painterResource(step.icon),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp),
)
}
}
}
}
}

View File

@@ -1,34 +1,45 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.kouros.android.cars.carappservice.R
import androidx.compose.ui.unit.sp
import com.kouros.data.R
import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel
import kotlinx.coroutines.Deferred
import com.kouros.navigation.utils.formatDateTime
import com.kouros.navigation.utils.round
@Composable
fun NavigationSheet(
routeModel: RouteModel,
step: StepData?,
simulate: () -> Unit
step: StepData,
stopNavigation: () -> Unit,
simulateNavigation: () -> Unit,
) {
val distance = step.leftDistance.round(1)
Column {
//Text("${routeModel.travelLeftTime()}")
if (step != null)
Text("${step.leftDistance / 1000} km")
FlowRow(horizontalArrangement= Arrangement.SpaceEvenly) {
Text(formatDateTime(step.arrivalTime), fontSize = 22.sp)
Spacer(Modifier.size(30.dp))
Text("$distance km", fontSize = 22.sp)
}
HorizontalDivider()
Row() {
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
if (routeModel.isNavigating()) {
Button(onClick = {
routeModel.stopNavigation()
stopNavigation()
}) {
Icon(
painter = painterResource(id = R.drawable.ic_close_white_24dp),
@@ -36,8 +47,9 @@ fun NavigationSheet(
modifier = Modifier.size(24.dp, 24.dp),
)
}
Spacer(Modifier.size(30.dp))
Button(onClick = {
simulate()
simulateNavigation()
}) {
Icon(
painter = painterResource(id = R.drawable.assistant_navigation_48px),

View File

@@ -22,8 +22,6 @@ import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@@ -38,7 +36,7 @@ import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.unit.dp
import com.kouros.android.cars.carappservice.R
import com.kouros.data.R
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.PlaceColor
import com.kouros.navigation.data.nominatim.SearchResult
@@ -46,7 +44,12 @@ import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.location
@Composable
fun SearchSheet(applicationContext: Context, viewModel: ViewModel, location: Location) {
fun SearchSheet(
applicationContext: Context,
viewModel: ViewModel,
location: Location,
closeSheet: () -> Unit
) {
val searchResults = mutableListOf<SearchResult>()
val recentPlaces = viewModel.places.observeAsState()
val search = viewModel.searchPlaces.observeAsState()
@@ -63,7 +66,9 @@ fun SearchSheet(applicationContext: Context, viewModel: ViewModel, location: Loc
searchResults = searchResults,
viewModel = viewModel,
context = applicationContext,
location = location
location = location,
closeSheet = {closeSheet()}
)
}
}
@@ -77,7 +82,8 @@ fun SearchSheet(applicationContext: Context, viewModel: ViewModel, location: Loc
searchResults = searchResults,
viewModel = viewModel,
context = applicationContext,
location = location
location = location,
closeSheet = {closeSheet()}
)
}
}
@@ -93,6 +99,7 @@ fun SearchBar(
viewModel: ViewModel,
context: Context,
location: Location,
closeSheet: () -> Unit
) {
var expanded by rememberSaveable { mutableStateOf(true) }
Box(
@@ -121,19 +128,19 @@ fun SearchBar(
},
expanded = expanded,
onExpandedChange = { expanded = it },
placeholder = { Text("Suchen") }
placeholder = { Text(context.getString(R.string.search_action_title)) }
)
},
expanded = expanded,
onExpandedChange = { expanded = it },
) {
if (searchPlaces.isNotEmpty()) {
Text("Recent places")
RecentPlaces(searchPlaces, viewModel, context, location)
Text(context.getString(R.string.recent_destinations))
RecentPlaces(searchPlaces, viewModel, context, location, closeSheet)
}
if (searchResults.isNotEmpty()) {
Text("Search places")
SearchPlaces(searchResults, viewModel, context, location)
SearchPlaces(searchResults, viewModel, context, location, closeSheet)
}
}
}
@@ -149,6 +156,7 @@ private fun SearchPlaces(
viewModel: ViewModel,
context: Context,
location: Location,
closeSheet: () -> Unit
) {
val color = remember { PlaceColor }
LazyColumn(
@@ -171,6 +179,7 @@ private fun SearchPlaces(
val toLocation =
location(place.lon.toDouble(), place.lat.toDouble())
viewModel.loadRoute(context, location, toLocation)
closeSheet()
}
.fillMaxWidth()
)
@@ -186,7 +195,8 @@ private fun RecentPlaces(
recentPlaces: List<Place>,
viewModel: ViewModel,
context: Context,
location: Location
location: Location,
closeSheet: () -> Unit
) {
val color = remember { PlaceColor }
LazyColumn(
@@ -207,6 +217,7 @@ private fun RecentPlaces(
.clickable {
val toLocation = location(place.longitude, place.latitude)
viewModel.loadRoute(context, location, toLocation)
closeSheet()
}
.fillMaxWidth()
)