diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 435aa85..da2732c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,8 +14,8 @@ android { applicationId = "com.kouros.navigation" minSdk = 33 targetSdk = 36 - versionCode = 7 - versionName = "0.1.3.7" + versionCode = 8 + versionName = "0.1.3.8" base.archivesName = "navi-$versionName" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt index 43ade6d..5f6692b 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt @@ -15,28 +15,36 @@ import androidx.annotation.RequiresPermission import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.Button 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 import androidx.compose.material3.rememberBottomSheetScaffoldState 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.mutableDoubleStateOf +import androidx.compose.runtime.mutableStateOf 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.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices +import com.kouros.data.R import com.kouros.navigation.data.Constants.homeLocation import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.StepData @@ -74,6 +82,10 @@ class MainActivity : ComponentActivity() { MutableLiveData() } + val nextStepData: MutableLiveData by lazy { + MutableLiveData() + } + var lastLocation = location(0.0, 0.0) val observer = Observer { newRoute -> @@ -147,8 +159,9 @@ class MainActivity : ComponentActivity() { fun Content() { val scaffoldState = rememberBottomSheetScaffoldState() val snackbarHostState = remember { SnackbarHostState() } - val scope = rememberCoroutineScope() + val sheetPeekHeightState = remember { mutableStateOf(256.dp) } + val locationProvider = rememberDefaultLocationProvider( updateInterval = 0.5.seconds, desiredAccuracy = DesiredAccuracy.Highest @@ -161,24 +174,27 @@ class MainActivity : ComponentActivity() { latitude = locationState.value!!.position.latitude } val step: StepData? by stepData.observeAsState() + val nextStep: StepData? by nextStepData.observeAsState() fun openSheet() { scope.launch { scaffoldState.bottomSheetState.expand() } } fun closeSheet() { - scope.launch { scaffoldState.bottomSheetState.partialExpand() } + scope.launch { + scaffoldState.bottomSheetState.partialExpand() + sheetPeekHeightState.value = 128.dp + } } - - NavigationTheme { + NavigationTheme() { BottomSheetScaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, scaffoldState = scaffoldState, - sheetPeekHeight = 128.dp, + sheetPeekHeight = sheetPeekHeightState.value, sheetContent = { - SheetContent(latitude, step) { closeSheet() } + SheetContent(latitude, step, nextStep) { closeSheet() } }, ) { innerPadding -> Box( @@ -187,19 +203,31 @@ class MainActivity : ComponentActivity() { .padding(innerPadding), contentAlignment = Alignment.Center, ) { - MapView(applicationContext,userLocationState, step, cameraPosition, routeData, tilt) + MapView( + applicationContext, + userLocationState, + step, + cameraPosition, + routeData, + tilt + ) } } } } @Composable - fun SheetContent(locationState: Double, step: StepData?, closeSheet: () -> Unit) { + fun SheetContent( + locationState: Double, + step: StepData?, + nextStep: StepData?, + closeSheet: () -> Unit + ) { if (!routeModel.isNavigating()) { SearchSheet(applicationContext, viewModel, lastLocation) { closeSheet() } } else { NavigationSheet( - routeModel, step!!, + routeModel, step!!, nextStep!!, { stopNavigation { closeSheet() } }, { simulateNavigation() } ) @@ -213,11 +241,16 @@ class MainActivity : ComponentActivity() { && lastLocation.latitude != location.position.latitude && lastLocation.longitude != location.position.longitude ) { - if (routeModel.isNavigating()) { - routeModel.updateLocation(lastLocation) - stepData.value = routeModel.currentStep() - } val currentLocation = location(location.position.longitude, location.position.latitude) + with(routeModel) { + if (isNavigating()) { + updateLocation(currentLocation) + stepData.value = currentStep() + if (route.currentManeuverIndex + 1 <= route.maneuvers.size) { + nextStepData.value = nextStep() + } + } + } val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing) val zoom = calculateZoom(location.speed) cameraPosition.postValue( @@ -252,7 +285,7 @@ class MainActivity : ComponentActivity() { val appOpsManager = getSystemService(APP_OPS_SERVICE) as AppOpsManager val mode = - appOpsManager.unsafeCheckOp( + appOpsManager.checkOp( AppOpsManager.OPSTR_MOCK_LOCATION, Process.myUid(), packageName @@ -275,7 +308,7 @@ class MainActivity : ComponentActivity() { for ((_, loc) in routeModel.route.waypoints.withIndex()) { if (routeModel.isNavigating()) { mock.setMockLocation(loc[1], loc[0]) - delay(1000L) // + delay(500L) // } } } diff --git a/app/src/main/java/com/kouros/navigation/ui/MapView.kt b/app/src/main/java/com/kouros/navigation/ui/MapView.kt index 67566b8..325e64c 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MapView.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MapView.kt @@ -1,30 +1,43 @@ package com.kouros.navigation.ui +import android.R.attr.x +import android.R.attr.y import android.content.Context import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.DpOffset 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.DarkMode +import com.kouros.navigation.car.map.MapLibre 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.CameraState 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 @@ -57,32 +70,14 @@ fun MapView( zoom = 15.0, ) ) + val baseStyle = remember { + mutableStateOf(BaseStyle.Uri(Constants.STYLE)) + } + DarkMode(applicationContext, baseStyle) 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) - } - } - + MapLibre(applicationContext, cameraState, baseStyle, route, "", position) LocationTrackingEffect( locationState = userLocationState, ) { @@ -97,7 +92,8 @@ fun MapView( duration = 1.seconds ) } - NavigationImage(paddingValues, width, height / 6, "") + NavigationImage(paddingValues, width, height / 6) } } } + diff --git a/app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt b/app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt index e22f4e2..84d8299 100755 --- a/app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt +++ b/app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt @@ -25,12 +25,13 @@ import com.kouros.navigation.utils.round fun NavigationSheet( routeModel: RouteModel, step: StepData, + nextStep: StepData, stopNavigation: () -> Unit, simulateNavigation: () -> Unit, ) { val distance = step.leftDistance.round(1) Column { - FlowRow(horizontalArrangement= Arrangement.SpaceEvenly) { + FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) { Text(formatDateTime(step.arrivalTime), fontSize = 22.sp) Spacer(Modifier.size(30.dp)) Text("$distance km", fontSize = 22.sp) @@ -47,7 +48,9 @@ fun NavigationSheet( modifier = Modifier.size(24.dp, 24.dp), ) } - Spacer(Modifier.size(30.dp)) + } + Spacer(Modifier.size(30.dp)) + if (!routeModel.isNavigating()) { Button(onClick = { simulateNavigation() }) { diff --git a/app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt b/app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt index 2ca8709..f076465 100644 --- a/app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt +++ b/app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt @@ -57,7 +57,7 @@ fun PermissionScreen( errorText = if (rejectedPermissions.none { it in requiredPermissions }) { "" } else { - "${rejectedPermissions.joinToString()} required for the sample" + "${rejectedPermissions.joinToString()} required for the app" } } val allRequiredPermissionsGranted = @@ -128,7 +128,7 @@ private fun PermissionScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = "Sample requires permission/s:", + text = "Navigation requires permission/s:", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(16.dp), ) diff --git a/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt b/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt index b35a883..ef8f912 100644 --- a/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt +++ b/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -56,23 +57,25 @@ fun SearchSheet( if (search.value != null) { searchResults.addAll(search.value!!) } + Home(applicationContext, viewModel, location, closeSheet = { closeSheet() }) if (searchResults.isNotEmpty()) { val textFieldState = rememberTextFieldState() val items = listOf(searchResults) if (items.isNotEmpty()) { SearchBar( textFieldState = textFieldState, - searchPlaces = recentPlaces.value!!, + searchPlaces = recentPlaces.value!!, searchResults = searchResults, viewModel = viewModel, context = applicationContext, location = location, - closeSheet = {closeSheet()} + closeSheet = { closeSheet() } ) } } if (recentPlaces.value != null) { + println("Recent Places ${recentPlaces.value}") val textFieldState = rememberTextFieldState() val items = listOf(recentPlaces) if (items.isNotEmpty()) { @@ -83,10 +86,44 @@ fun SearchSheet( viewModel = viewModel, context = applicationContext, location = location, - closeSheet = {closeSheet()} + closeSheet = { closeSheet() } ) } } + +} + +@Composable +fun Home( + applicationContext: Context, + viewModel: ViewModel, + location: Location, + closeSheet: () -> Unit +) { + Row(horizontalArrangement = Arrangement.SpaceBetween) { + Button(onClick = { + val places = viewModel.loadRecentPlace() + val toLocation = location(places.first()!!.longitude, places.first()!!.latitude) + viewModel.loadRoute(applicationContext, location, toLocation) + closeSheet() + }) { + Icon( + painter = painterResource(id = com.google.android.gms.base.R.drawable.common_full_open_on_phone), + "Home", + modifier = Modifier.size(24.dp, 24.dp), + ) + Text("Home") + } + Button(onClick = { + }) { + Icon( + painter = painterResource(id = R.drawable.ic_favorite_white_24dp), + "Work", + modifier = Modifier.size(24.dp, 24.dp), + ) + Text("Arbeit") + } + } } @OptIn(ExperimentalMaterial3Api::class) @@ -176,6 +213,16 @@ private fun SearchPlaces( headlineContent = { Text("${place.address.road} ${place.address.postcode}") }, modifier = Modifier .clickable { + val pl = Place( + name = place.name, + longitude = place.lon.toDouble(), + latitude = place.lat.toDouble(), + postalCode = place.address.postcode, + city = place.address.city, + street = place.address.road + ) + viewModel.saveRecent(pl) + println("Save $pl") val toLocation = location(place.lon.toDouble(), place.lat.toDouble()) viewModel.loadRoute(context, location, toLocation) @@ -212,7 +259,7 @@ private fun RecentPlaces( modifier = Modifier.size(24.dp, 24.dp), ) ListItem( - headlineContent = { Text("${place.street!!} ${place.postalCode}") }, + headlineContent = { Text("${place.name} ${place.postalCode}") }, modifier = Modifier .clickable { val toLocation = location(place.longitude, place.latitude) diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt index 0a025e0..8954b69 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt @@ -162,7 +162,7 @@ class NavigationSession : Session(), NavigationScreen.Listener { val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations()) val distance = location.distanceTo(snapedLocation) if (distance > MAXIMAL_ROUTE_DEVIATION) { - navigationScreen.calculateNewRoute(routeModel.destination) + navigationScreen.calculateNewRoute(routeModel.routeState.destination) return } navigationScreen.updateTrip(location) diff --git a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt index 9cb9482..f3ba79d 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt @@ -5,14 +5,14 @@ import android.graphics.Rect import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.location.Location -import android.location.LocationManager +import android.os.CountDownTimer +import android.os.Handler import android.util.Log import androidx.car.app.AppManager import androidx.car.app.CarContext import androidx.car.app.SurfaceCallback import androidx.car.app.SurfaceContainer import androidx.car.app.connection.CarConnection -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -27,31 +27,24 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner -import com.kouros.navigation.car.map.BuildingLayer +import com.kouros.navigation.car.map.DarkMode import com.kouros.navigation.car.map.DrawImage -import com.kouros.navigation.car.map.RouteLayer +import com.kouros.navigation.car.map.MapLibre import com.kouros.navigation.car.map.cameraState import com.kouros.navigation.car.map.getPaddingValues import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.data.Constants -import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING import com.kouros.navigation.data.ObjectBox import com.kouros.navigation.model.RouteModel -import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue import com.kouros.navigation.utils.bearing import com.kouros.navigation.utils.calculateZoom +import com.kouros.navigation.utils.duration +import com.kouros.navigation.utils.location import com.kouros.navigation.utils.previewZoom import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraState -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.math.absoluteValue -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds class SurfaceRenderer( @@ -59,14 +52,14 @@ class SurfaceRenderer( private var routeModel: RouteCarModel ) : DefaultLifecycleObserver { - var lastLocation = Location(LocationManager.GPS_PROVIDER) - val cameraPosition = MutableLiveData( + var lastLocation = location(0.0, 0.0) + private val cameraPosition = MutableLiveData( CameraPosition( zoom = 15.0, target = Position(latitude = 48.1857475, longitude = 11.5793627) ) ) - var visibleArea = MutableLiveData( + private var visibleArea = MutableLiveData( Rect(0, 0, 0, 0) ) @@ -81,6 +74,7 @@ class SurfaceRenderer( val previewRouteData = MutableLiveData("") + val speed = MutableLiveData(0F) lateinit var centerLocation: Location var preview = false @@ -90,6 +84,8 @@ class SurfaceRenderer( var tilt = 55.0 var previewDistance = 0.0 + + var countDownTimerActive = false val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback { lateinit var lifecycleOwner: CustomLifecycleOwner @@ -171,6 +167,7 @@ class SurfaceRenderer( init { lifecycle.addObserver(this) + speed.value = 0F } fun onConnectionStateUpdated(connectionState: Int) { @@ -192,26 +189,8 @@ class SurfaceRenderer( val baseStyle = remember { mutableStateOf(BaseStyle.Uri(Constants.STYLE)) } - baseStyle.value = - (if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri( - Constants.STYLE - )) - - MaplibreMap( - options = MapOptions( - ornamentOptions = - OrnamentOptions(isScaleBarEnabled = false)), - cameraState = cameraState, - baseStyle = baseStyle.value - ) { - getBaseSource(id = "openmaptiles")?.let { tiles -> - if (!getBooleanKeyValue(context = carContext, SHOW_THREED_BUILDING)) { - BuildingLayer(tiles) - } - RouteLayer(route, previewRoute, position!!.zoom) - } - //Puck(cameraState, lastLocation) - } + DarkMode(carContext, baseStyle) + MapLibre(carContext, cameraState, baseStyle, route, previewRoute, position) ShowPosition(cameraState, position, paddingValues) } @@ -221,16 +200,17 @@ class SurfaceRenderer( position: CameraPosition?, paddingValues: PaddingValues ) { - val cameraDuration = duration(position) - var bearing = position!!.bearing + val cameraDuration = duration(preview, position!!.bearing, lastBearing) + var bearing = position.bearing var zoom = position.zoom var target = position.target var localTilt = tilt + val currentSpeed: Float? by speed.observeAsState() if (!preview) { if (routeModel.isNavigating()) { - DrawImage(paddingValues, lastLocation, width, height, "") + DrawImage(paddingValues, currentSpeed, width, height) } else { - DrawImage(paddingValues, lastLocation, width, height, "") + DrawImage(paddingValues, currentSpeed, width, height) } } else { bearing = 0.0 @@ -259,18 +239,6 @@ class SurfaceRenderer( .setSurfaceCallback(mSurfaceCallback) } - private fun duration(position: CameraPosition?): Duration { - if (preview) { - return 3.seconds - } - val cameraDuration = if ((lastBearing - position!!.bearing).absoluteValue > 20.0) { - 2.seconds - } else { - 1.seconds - } - return cameraDuration - } - /** Handles the map zoom-in and zoom-out events. */ fun handleScale(zoomSign: Int) { synchronized(this) { @@ -313,6 +281,13 @@ class SurfaceRenderer( ) lastBearing = cameraPosition.value!!.bearing lastLocation = location + speed.value = location.speed + if (!countDownTimerActive) { + countDownTimerActive = true + val mainThreadHandler = Handler(carContext.mainLooper) + val lastLocationTimer = lastLocation + checkUpdate(mainThreadHandler, lastLocationTimer) + } } else { updateCameraPosition( 0.0, @@ -323,6 +298,23 @@ class SurfaceRenderer( } } + private fun checkUpdate( + mainThreadHandler: Handler, + lastLocationTimer: Location + ) { + mainThreadHandler.post { + object : CountDownTimer(5000, 1000) { + override fun onTick(millisUntilFinished: Long) {} + override fun onFinish() { + countDownTimerActive = false + if (lastLocation.time - lastLocationTimer.time < 1500) { + speed.postValue(0F) + } + } + }.start() + } + } + private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) { cameraPosition.postValue( cameraPosition.value!!.copy( @@ -344,7 +336,7 @@ class SurfaceRenderer( fun setPreviewRouteData(routeModel: RouteModel) { previewRouteData.value = routeModel.route.routeGeoJson - centerLocation = routeModel.centerLocation + centerLocation = routeModel.route.centerLocation preview = true previewDistance = routeModel.route.distance } diff --git a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt index b7e6008..d361cfb 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt @@ -1,7 +1,9 @@ package com.kouros.navigation.car.map import android.location.Location +import android.content.Context import androidx.compose.foundation.Canvas +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -12,6 +14,7 @@ import androidx.compose.material3.BadgedBox import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -25,9 +28,13 @@ import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.kouros.data.R +import com.kouros.navigation.data.Constants +import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING import com.kouros.navigation.data.NavigationColor import com.kouros.navigation.data.RouteColor import com.kouros.navigation.data.SpeedColor +import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue +import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.rememberCameraState @@ -39,9 +46,14 @@ import org.maplibre.compose.location.LocationPuck import org.maplibre.compose.location.LocationPuckColors import org.maplibre.compose.location.LocationPuckSizes 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.GeoJsonData import org.maplibre.compose.sources.Source +import org.maplibre.compose.sources.getBaseSource import org.maplibre.compose.sources.rememberGeoJsonSource +import org.maplibre.compose.style.BaseStyle import org.maplibre.spatialk.geojson.Position @@ -65,6 +77,32 @@ fun cameraState( ) } +@Composable +fun MapLibre( + context: Context, + cameraState: CameraState, + baseStyle: MutableState, + route: String?, + previewRoute: String?, + position: CameraPosition? +) { + MaplibreMap( + options = MapOptions( + ornamentOptions = + OrnamentOptions(isScaleBarEnabled = false) + ), + cameraState = cameraState, + baseStyle = baseStyle.value + ) { + getBaseSource(id = "openmaptiles")?.let { tiles -> + if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) { + BuildingLayer(tiles) + } + RouteLayer(route, previewRoute, position!!.zoom) + } + //Puck(cameraState, lastLocation) + } +} @Composable fun RouteLayer(routeData: String?, previewRoute: String?, zoom: Double) { val width = zoom - 2 @@ -115,26 +153,20 @@ fun BuildingLayer(tiles: Source) { } @Composable -fun DrawImage(padding: PaddingValues, location: Location, width: Int, height: Int, street: String) { - NavigationImage(padding, width,height, street) - Speed(width, height, location) +fun DrawImage(padding: PaddingValues, speed: Float?, width: Int, height: Int) { + NavigationImage(padding, width,height) + Speed(width, height, speed) } @Composable -fun NavigationImage(padding: PaddingValues, width: Int, height: Int, street: String) { +fun NavigationImage(padding: PaddingValues, width: Int, height: Int) { val imageSize = (height/6) val color = remember { NavigationColor } - BadgedBox( - modifier = Modifier - .padding(padding), - badge = { - Badge() - } - ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) { Canvas(modifier =Modifier .size(imageSize.dp, imageSize.dp)) { scale(scaleX = 1f, scaleY = 0.7f) { - drawCircle(Color.DarkGray.copy(alpha = 0.2f)) + drawCircle(Color.DarkGray.copy(alpha = 0.4f)) } } Icon( @@ -143,8 +175,6 @@ fun NavigationImage(padding: PaddingValues, width: Int, height: Int, street: Str tint = color.copy(alpha = 1f), modifier = Modifier.size(imageSize.dp, imageSize.dp), ) - if (street.isNotEmpty()) - Text(text = street) } } @@ -152,7 +182,7 @@ fun NavigationImage(padding: PaddingValues, width: Int, height: Int, street: Str private fun Speed( width: Int, height: Int, - location: Location + speed: Float? ) { val radius = 32 Box( @@ -165,7 +195,7 @@ private fun Speed( ) { val textMeasurerSpeed = rememberTextMeasurer() val textMeasurerKm = rememberTextMeasurer() - val speed = (location.speed * 3.6).toInt().toString() + val speed = (speed!! * 3.6).toInt().toString() val kmh = "km/h" val styleSpeed = TextStyle( fontSize = 22.sp, @@ -212,6 +242,23 @@ private fun Speed( } } +@Composable +fun DarkMode(context: Context, baseStyle: MutableState) { + val darkMode = getIntKeyValue(context, Constants.DARK_MODE_SETTINGS) + if (darkMode == 0) { + baseStyle.value = BaseStyle.Uri(Constants.STYLE) + } + if (darkMode == 1) { + baseStyle.value = BaseStyle.Uri(Constants.STYLE_DARK) + } + if (darkMode == 2) { + baseStyle.value = + (if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri( + Constants.STYLE + )) + } +} + fun getPaddingValues(width: Int, height: Int, preView: Boolean): PaddingValues { return if (preView) { PaddingValues(start = 150.dp, bottom = 0.dp) diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt index 30ff4c0..44ad275 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt @@ -49,7 +49,7 @@ class RouteCarModel() : RouteModel() { .setIcon(createCarIcon(carContext, stepData.icon)) .build() ) - .setRoad(destination.street!!) + .setRoad(routeState.destination.street!!) .build() return step } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/DarkModeSettings.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/DarkModeSettings.kt new file mode 100644 index 0000000..2e95394 --- /dev/null +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/DarkModeSettings.kt @@ -0,0 +1,87 @@ +package com.kouros.navigation.car.screen + +import androidx.car.app.CarContext +import androidx.car.app.CarToast +import androidx.car.app.Screen +import androidx.car.app.model.Action +import androidx.car.app.model.Header +import androidx.car.app.model.ItemList +import androidx.car.app.model.ListTemplate +import androidx.car.app.model.Row +import androidx.car.app.model.SectionedItemList +import androidx.car.app.model.Template +import com.kouros.data.R +import com.kouros.navigation.data.Constants.DARK_MODE_SETTINGS +import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING +import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue +import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue +import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue +import com.kouros.navigation.utils.NavigationUtils.setIntKeyValue + +class DarkModeSettings(private val carContext: CarContext) : Screen(carContext) { + + private var darkModeSettings = 0 + + init { + darkModeSettings = getIntKeyValue(carContext, DARK_MODE_SETTINGS) + } + + override fun onGetTemplate(): Template { + val templateBuilder = ListTemplate.Builder() + val radioList = + ItemList.Builder() + .addItem( + buildRowForTemplate( + R.string.off_action_title, + ) + ) + .addItem( + buildRowForTemplate( + R.string.on_action_title, + ) + ) + .addItem( + buildRowForTemplate( + R.string.use_telephon_settings, + ) + ) + .setOnSelectedListener { index: Int -> + this.onSelected(index) + } + .setSelectedIndex(darkModeSettings) + .build() + + return templateBuilder + .addSectionedList(SectionedItemList.create( + radioList, + carContext.getString(R.string.dark_mode) + )) + .setHeader( + Header.Builder() + .setTitle(carContext.getString(R.string.dark_mode)) + .setStartHeaderAction(Action.BACK) + .build() + ) + .build() + } + + + private fun onSelected(index: Int) { + setIntKeyValue(carContext, index, DARK_MODE_SETTINGS) + CarToast.makeText( + carContext, + (carContext + .getString(R.string.display_settings) + + ":" + + " " + index), CarToast.LENGTH_LONG + ) + .show() + } + + private fun buildRowForTemplate(title: Int): Row { + return Row.Builder() + .setTitle(carContext.getString(title)) + .build() + } + +} \ No newline at end of file diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/DisplaySettings.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/DisplaySettings.kt index f8b011d..c6966de 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/DisplaySettings.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/DisplaySettings.kt @@ -1,12 +1,12 @@ package com.kouros.navigation.car.screen import androidx.car.app.CarContext -import androidx.car.app.CarToast import androidx.car.app.Screen import androidx.car.app.model.Action import androidx.car.app.model.Header import androidx.car.app.model.ItemList import androidx.car.app.model.ListTemplate +import androidx.car.app.model.OnClickListener import androidx.car.app.model.Row import androidx.car.app.model.Template import androidx.car.app.model.Toggle @@ -19,6 +19,7 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) { private var buildingToggleState = false + init { buildingToggleState = getBooleanKeyValue(carContext, SHOW_THREED_BUILDING) } @@ -35,7 +36,12 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) { buildingToggleState = !buildingToggleState }.setChecked(buildingToggleState).build() listBuilder.addItem(buildRowForTemplate(R.string.threed_building, buildingToggle)) - + listBuilder.addItem( + buildRowForScreenTemplate( + DarkModeSettings(carContext), + R.string.dark_mode + ) + ) return ListTemplate.Builder() .setSingleList(listBuilder.build()) .setHeader( @@ -54,4 +60,12 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) { .setToggle(toggle) .build() } + + private fun buildRowForScreenTemplate(screen: Screen, title: Int): Row { + return Row.Builder() + .setTitle(carContext.getString(title)) + .setOnClickListener { screenManager.push(screen) } + .setBrowsable(true) + .build() + } } \ No newline at end of file diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt index abfc9b7..ced89eb 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt @@ -111,11 +111,11 @@ class NavigationScreen( } private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template { - if (routeModel.isArrived()) { + if (routeModel.routeState.arrived) { val timer = object : CountDownTimer(10000, 10000) { override fun onTick(millisUntilFinished: Long) {} override fun onFinish() { - routeModel.arrived = false + routeModel.routeState = routeModel.routeState.copy(arrived = false) invalidate() } } @@ -135,12 +135,16 @@ class NavigationScreen( } fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate { + var street = "" + if (routeModel.routeState.destination.street != null) { + street = routeModel.routeState.destination.street!! + } return NavigationTemplate.Builder() .setNavigationInfo( MessageInfo.Builder( carContext.getString(R.string.arrived_exclamation_msg) ) - .setText(routeModel.destination.street!!) + .setText(street) .setImage( CarIcon.Builder( IconCompat.createWithResource( @@ -192,7 +196,7 @@ class NavigationScreen( } fun getRoutingInfo(): RoutingInfo { - var currentDistance = routeModel.currentDistance + var currentDistance = routeModel.leftStepDistance() val displayUnit = if (currentDistance > 1000.0) { currentDistance /= 1000.0 Distance.UNIT_KILOMETERS @@ -274,7 +278,7 @@ class NavigationScreen( .setOnClickListener { val navigateTo = location(recentPlace.longitude, recentPlace.latitude) viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, navigateTo) - routeModel.destination = recentPlace + routeModel.routeState.destination = recentPlace } .build() } @@ -398,7 +402,7 @@ class NavigationScreen( viewModel.saveRecent(place) viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, location) currentNavigationLocation = location - routeModel.destination = place + routeModel.routeState.destination = place invalidate() } } @@ -416,7 +420,7 @@ class NavigationScreen( invalidate() val mainThreadHandler = Handler(carContext.mainLooper) mainThreadHandler.post { - object : CountDownTimer(5000, 1000) { + object : CountDownTimer(3000, 1000) { override fun onTick(millisUntilFinished: Long) {} override fun onFinish() { calculateNewRoute = false @@ -427,19 +431,20 @@ class NavigationScreen( } fun reRoute(destination: Place) { - val dest = location( destination.longitude, destination.latitude) + val dest = location(destination.longitude, destination.latitude) viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, dest) } fun updateTrip(location: Location) { - val start = System.currentTimeMillis() - routeModel.updateLocation(location) - val end = System.currentTimeMillis() - println("Time ${end-start}") - if (routeModel.maneuverType == Maneuver.TYPE_DESTINATION - && routeModel.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE) { - routeModel.arrived = true - routeModel.stopNavigation() + with(routeModel) { + updateLocation(location) + if (routeState.maneuverType == Maneuver.TYPE_DESTINATION + && leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE + ) { + stopNavigation() + routeState = routeState.copy(arrived = true) + surfaceRenderer.routeData.value = "" + } } invalidate() } diff --git a/common/data/src/main/java/com/kouros/navigation/data/Color.kt b/common/data/src/main/java/com/kouros/navigation/data/Color.kt index 82225a2..16ad5dd 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Color.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Color.kt @@ -2,7 +2,7 @@ package com.kouros.navigation.data import androidx.compose.ui.graphics.Color -val NavigationColor = Color(0xFF052186) +val NavigationColor = Color(0xFF0730B2) val RouteColor = Color(0xFF5582D0) diff --git a/common/data/src/main/java/com/kouros/navigation/data/Data.kt b/common/data/src/main/java/com/kouros/navigation/data/Data.kt index e5a5b1e..990687b 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Data.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Data.kt @@ -164,6 +164,8 @@ object Constants { const val SHOW_THREED_BUILDING = "Show3D" + const val DARK_MODE_SETTINGS = "DarkMode" + const val AVOID_MOTORWAY = "AvoidMotorway" const val AVOID_TOLLWAY = "AvoidTollway" diff --git a/common/data/src/main/java/com/kouros/navigation/data/Route.kt b/common/data/src/main/java/com/kouros/navigation/data/Route.kt index 67b8d10..fb1681b 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Route.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Route.kt @@ -1,22 +1,28 @@ package com.kouros.navigation.data +import android.location.Location import com.google.gson.GsonBuilder import com.kouros.navigation.data.valhalla.Maneuvers import com.kouros.navigation.data.valhalla.Summary import com.kouros.navigation.data.valhalla.Trip import com.kouros.navigation.data.valhalla.ValhallaJson +import com.kouros.navigation.utils.NavigationUtils.createCenterLocation import com.kouros.navigation.utils.NavigationUtils.createGeoJson import com.kouros.navigation.utils.NavigationUtils.decodePolyline +import com.kouros.navigation.utils.location +import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.Point +import org.maplibre.turf.TurfMeasurement +import kotlin.math.roundToInt -data class Route ( +data class Route( /** * A Leg is a route between only two waypoints. * * @since 1.0.0 */ - var maneuvers: List, + val maneuvers: List, /** * The distance traveled from origin to destination. @@ -33,9 +39,16 @@ data class Route ( * * @since 1.0.0 */ - var waypoints: List>, + val waypoints: List>, - val pointLocations : List, + /** + * List of [List] objects. Each `Point` is an input coordinate + * snapped to the road and path network. The `waypoint` appear in the list in the order of + * the input coordinates. + * + * @since 1.0.0 + */ + val pointLocations: List, val summary: Summary, @@ -43,27 +56,26 @@ data class Route ( val time: Double, - var routeGeoJson : String, + val routeGeoJson: String, - var currentManeuverIndex: Int + val currentManeuverIndex : Int, + + val centerLocation: Location ) { class Builder { private lateinit var maneuvers: List private var distance: Double = 0.0 - private var time: Double = 0.0 private lateinit var waypoints: List> private lateinit var pointLocations: List - - private lateinit var summary : Summary - - private lateinit var trip : Trip - + private lateinit var summary: Summary + private lateinit var trip: Trip private var routeGeoJson = "" + private var centerLocation = location(0.0, 0.0) - fun route (route: String ) = apply { + fun route(route: String) = apply { if (route.isNotEmpty() && route != "[]") { val gson = GsonBuilder().serializeNulls().create() val valhalla = gson.fromJson(route, ValhallaJson::class.java) @@ -83,35 +95,37 @@ data class Route ( points.add(point) } pointLocations = points - this.routeGeoJson = createGeoJson(waypoints) + routeGeoJson = createGeoJson(waypoints) + centerLocation = createCenterLocation(routeGeoJson) return Route( - maneuvers, distance, waypoints, pointLocations, summary, trip, time, routeGeoJson, 0 + maneuvers, + distance, + waypoints, + pointLocations, + summary, + trip, + time, + routeGeoJson, + 0, + centerLocation ) } } fun maneuverLocations(): List { - val beginShapeIndex = currentManeuver().beginShapeIndex - val endShapeIndex = if (currentManeuver().endShapeIndex >= waypoints.size) { - waypoints.size - } else { - currentManeuver().endShapeIndex + 1 - } - //return pointLocations.subList(beginShapeIndex, endShapeIndex) return pointLocations } - fun clear() { - waypoints = mutableListOf() - maneuvers = mutableListOf() - routeGeoJson = "" - } - - fun currentManeuver() : Maneuvers { + fun currentManeuver(): Maneuvers { return maneuvers[currentManeuverIndex] } - fun nextManeuver() : Maneuvers { - return maneuvers[currentManeuverIndex+1] + fun nextManeuver(): Maneuvers { + val nextIndex = currentManeuverIndex + 1 + return if (nextIndex < maneuvers.size) { + maneuvers[nextIndex] + } else { + throw IndexOutOfBoundsException("No next maneuver available.") + } } } \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/styles/liberty_night.json b/common/data/src/main/java/com/kouros/navigation/data/styles/liberty_night.json index b8c29cc..18263a5 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/styles/liberty_night.json +++ b/common/data/src/main/java/com/kouros/navigation/data/styles/liberty_night.json @@ -22,7 +22,7 @@ "id": "background", "type": "background", "layout": {"visibility": "visible"}, - "paint": {"background-color": "rgba(146, 146, 142, 1)"} + "paint": {"background-color": "rgba(28, 28, 35, 1)"} }, { "id": "natural_earth", diff --git a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt index b6702fc..704f699 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt @@ -1,139 +1,99 @@ package com.kouros.navigation.model import android.location.Location -import android.location.LocationManager import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Step import com.kouros.data.R -import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD import com.kouros.navigation.data.ManeuverType import com.kouros.navigation.data.Place import com.kouros.navigation.data.Route import com.kouros.navigation.data.StepData import com.kouros.navigation.utils.location -import org.maplibre.geojson.FeatureCollection -import org.maplibre.geojson.Point import org.maplibre.turf.TurfMeasurement +import org.maplibre.turf.TurfMisc import java.util.concurrent.TimeUnit import kotlin.math.roundToInt open class RouteModel() { - lateinit var centerLocation: Location + data class RouteState( + val route: Route? = null, + val isNavigating: Boolean = false, + var destination: Place = Place(), + val arrived: Boolean = false, + var maneuverType: Int = 0, + var currentShapeIndex: Int = 0, + var distanceToStepEnd: Float = 0F, + var beginIndex: Int = 0, + var endIndex: Int = 0 + ) - lateinit var destination: Place + var routeState = RouteState() - var navigating = false - - var arrived = false - - var maneuverType = 0 - - /* - current shapeIndex - */ - var currentShapeIndex = 0 - - var distanceToStepEnd = 0F - - var beginIndex = 0 - - var endIndex = 0 - - lateinit var route: Route - - fun startNavigation(valhallaRoute: String) { - route = Route.Builder() - .route(valhallaRoute) + var route: Route + get() = routeState.route!! + set(value) { + routeState = routeState.copy(route = value) + } + fun startNavigation(routeString: String) { + val newRoute = Route.Builder() + .route(routeString) .build() - centerLocation = createCenterLocation() - navigating = true + this.routeState = routeState.copy( + route = newRoute, + isNavigating = true + ) } fun stopNavigation() { - route.clear() - navigating = false - currentShapeIndex = 0 - distanceToStepEnd = 0F - beginIndex = 0 - endIndex = 0 + this.routeState = routeState.copy( + route = null, + isNavigating = false, + // destination = Place(), + arrived = false, + maneuverType = 0, + currentShapeIndex = 0, + distanceToStepEnd = 0F, + beginIndex = 0, + endIndex = 0 + ) } - /** - * Calculates the geographic center of the route's GeoJSON data. - * - * @return A [Location] object representing the center point. - * @throws IllegalStateException if the calculated center does not have valid Point geometry. - */ - private fun createCenterLocation(): Location { - // 1. Create a FeatureCollection from the raw GeoJSON string. - val featureCollection = FeatureCollection.fromJson(route.routeGeoJson) - - // 2. Calculate the center feature of the collection. - val centerFeature = TurfMeasurement.center(featureCollection) - - // 3. Safely access and cast the geometry, throwing an informative error if it fails. - val centerPoint = centerFeature.geometry() as? Point - ?: throw IllegalStateException("Center of GeoJSON is not a valid Point.") - - // 4. Create and return the Location object. - return location(centerPoint.longitude(), centerPoint.latitude()) - } - - /** - * The remaining distance to the step, rounded to the nearest 10 units. - */ - val currentDistance: Double - get() { - // This is a more direct way to round to the nearest multiple of 10. - return (leftStepDistance() / 10.0).roundToInt() * 10.0 - } - - - fun updateLocation(location: Location) { var nearestDistance = 100000.0f - for (i in route.currentManeuverIndex..= newShapeIndex) { + route = route.copy(currentManeuverIndex = i) + routeState.apply { + currentShapeIndex = newShapeIndex + beginIndex = maneuver.beginShapeIndex + endIndex = maneuver.endShapeIndex + distanceToStepEnd = 0F + // calculate shape distance to step end + for (j in newShapeIndex + 1..maneuver.endShapeIndex) { + val loc1 = location(route!!.waypoints[j - 1][0], route.waypoints[j - 1][1]) + val loc2 = location(route.waypoints[j][0], route.waypoints[j][1]) distanceToStepEnd += loc1.distanceTo(loc2) } + break } } } @@ -175,42 +135,6 @@ open class RouteModel() { ) } - - fun currentStepx(): StepData { - val maneuver = route.currentManeuver() - var text = "" - if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) { - text = maneuver.streetNames[0] - } - val distanceStepLeft = leftStepDistance() - when (distanceStepLeft) { - in 0.0..Constants.NEXT_STEP_THRESHOLD -> { - if (route.currentManeuverIndex < route.maneuvers.size) { - val maneuver = route.nextManeuver() - if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) { - text = maneuver.streetNames[0] - } - } - } - } - val type = if (hasArrived(maneuverType)) { - maneuver.type - } else { - ManeuverType.None.value - } - var routing: (Pair) = maneuverIcon(type) - when (distanceStepLeft) { - in 0.0..NEXT_STEP_THRESHOLD -> { - if (route.currentManeuverIndex < route.maneuvers.size) { - val maneuver = route.nextManeuver() - val maneuverType = maneuver.type - routing = maneuverIcon(maneuverType) - } - } - } - return StepData(text, distanceStepLeft, routing.first, routing.second, arrivalTime(), travelLeftDistance()) - } - fun nextStep(): StepData { val maneuver = route.nextManeuver() val maneuverType = maneuver.type @@ -220,30 +144,24 @@ open class RouteModel() { when (distanceLeft) { in 0.0..NEXT_STEP_THRESHOLD -> { } + else -> { - if (maneuver.streetNames != null && maneuver.streetNames!!.isNotEmpty()) { + if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) { text = maneuver.streetNames[0] } } } - val routing: (Pair) = maneuverIcon(maneuverType) - return StepData(text, distanceLeft, routing.first, routing.second, arrivalTime(), travelLeftDistance()) - } - private fun calculateDistance( - beginShapeIndex: Int, - endShapeIndex: Int, - location: Location - ): Float { - var nearestLocation = 100000.0f - for (i in beginShapeIndex..endShapeIndex) { - val polylineLocation = location(route.waypoints[i][0], route.waypoints[i][1]) - val distance: Float = location.distanceTo(polylineLocation) - if (distance < nearestLocation) { - nearestLocation = distance - } - } - return nearestLocation + val routing: (Pair) = maneuverIcon(maneuverType) + // Construct and return the final StepData object + return StepData( + text, + distanceLeft, + routing.first, + routing.second, + arrivalTime(), + travelLeftDistance() + ) } fun travelLeftTime(): Double { @@ -252,10 +170,11 @@ open class RouteModel() { val maneuver = route.maneuvers[i] timeLeft += maneuver.time } - if (endIndex > 0) { + if (routeState.endIndex > 0) { val maneuver = route.currentManeuver() val curTime = maneuver.time - val percent = 100 * (endIndex - currentShapeIndex) / (endIndex - beginIndex) + val percent = + 100 * (routeState.endIndex - routeState.currentShapeIndex) / (routeState.endIndex - routeState.beginIndex) val time = curTime * percent / 100 timeLeft += time } @@ -275,10 +194,12 @@ open class RouteModel() { fun leftStepDistance(): Double { val maneuver = route.currentManeuver() var leftDistance = maneuver.length - if (endIndex > 0) { - leftDistance = (distanceToStepEnd / 1000).toDouble() + if (routeState.endIndex > 0) { + leftDistance = (routeState.distanceToStepEnd / 1000).toDouble() } - return leftDistance * 1000 + // The remaining distance to the step, rounded to the nearest 10 units. + return (leftDistance * 1000 / 10.0).roundToInt() * 10.0 + } /** Returns the left distance in km. */ @@ -288,10 +209,11 @@ open class RouteModel() { val maneuver = route.maneuvers[i] leftDistance += maneuver.length } - if (endIndex > 0) { + if (routeState.endIndex > 0) { val maneuver = route.currentManeuver() val curDistance = maneuver.length - val percent = 100 * (endIndex - currentShapeIndex) / (endIndex - beginIndex) + val percent = + 100 * (routeState.endIndex - routeState.currentShapeIndex) / (routeState.endIndex - routeState.beginIndex) val time = curDistance * percent / 100 leftDistance += time } @@ -360,21 +282,21 @@ open class RouteModel() { currentTurnIcon = R.drawable.ic_roundabout_ccw } } - maneuverType = type + routeState.maneuverType = type return Pair(type, currentTurnIcon) } fun isNavigating(): Boolean { - return navigating + return routeState.isNavigating } fun isArrived(): Boolean { - return arrived + return routeState.arrived } fun hasArrived(type: Int): Boolean { return type == ManeuverType.DestinationRight.value - || maneuverType == ManeuverType.Destination.value - || maneuverType == ManeuverType.DestinationLeft.value + || routeState.maneuverType == ManeuverType.Destination.value + || routeState.maneuverType == ManeuverType.DestinationLeft.value } } \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt b/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt index fb56a33..0fa8188 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt @@ -241,7 +241,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() { savePlace(place) } - fun savePlace(place: Place) { + private fun savePlace(place: Place) { viewModelScope.launch(Dispatchers.IO) { try { val placeBox = boxStore.boxFor(Place::class) @@ -258,6 +258,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() { val current = LocalDateTime.now(ZoneOffset.UTC) place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond() placeBox.put(place) + println("Save Recent $place") } catch (e: Exception) { e.printStackTrace() } @@ -333,4 +334,21 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() { return results.toMutableStateList() } + fun loadRecentPlace(): SnapshotStateList { + val results = listOf() + try { + val placeBox = boxStore.boxFor(Place::class) + val query = placeBox + .query(Place_.name.notEqual("").and(Place_.category.equal(Constants.RECENT))) + .orderDesc(Place_.lastDate) + .build() + val results = query.find() + query.close() + return results.toMutableStateList() + } catch (e: Exception) { + e.printStackTrace() + } + return results.toMutableStateList() + } + } \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt index 1857253..3680f24 100644 --- a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt +++ b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt @@ -9,7 +9,9 @@ import com.kouros.navigation.data.GeoJsonFeature import com.kouros.navigation.data.GeoJsonFeatureCollection import com.kouros.navigation.data.GeoJsonLineString import kotlinx.serialization.json.Json +import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.Point +import org.maplibre.turf.TurfMeasurement import org.maplibre.turf.TurfMisc import java.lang.Math.toDegrees import java.lang.Math.toRadians @@ -19,13 +21,15 @@ import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.format.FormatStyle +import kotlin.math.absoluteValue import kotlin.math.asin import kotlin.math.atan2 import kotlin.math.cos import kotlin.math.pow import kotlin.math.roundToInt import kotlin.math.sin - +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds object NavigationUtils { @@ -52,6 +56,29 @@ object NavigationUtils { apply() } } + + fun getIntKeyValue(context: Context, key: String) : Int { + return context + .getSharedPreferences( + SHARED_PREF_KEY, + Context.MODE_PRIVATE + ) + .getInt(key, 0) + } + + fun setIntKeyValue(context: Context, `val`: Int, key: String) { + context + .getSharedPreferences( + SHARED_PREF_KEY, + Context.MODE_PRIVATE + ) + .edit { + putInt( + key, `val` + ) + apply() + } + } fun snapLocation(location: Location, stepCoordinates: List) : Location { val newLocation = Location(location) val oldPoint = Point.fromLngLat(location.longitude, location.latitude) @@ -101,6 +128,26 @@ object NavigationUtils { return coordinates } + /** + * Calculates the geographic center of the route's GeoJSON data. + * + * @return A [Location] object representing the center point. + * @throws IllegalStateException if the calculated center does not have valid Point geometry. + */ + fun createCenterLocation(routeGeoJson: String): Location { + // 1. Create a FeatureCollection from the raw GeoJSON string. + val featureCollection = FeatureCollection.fromJson(routeGeoJson) + + // 2. Calculate the center feature of the collection. + val centerFeature = TurfMeasurement.center(featureCollection) + + // 3. Safely access and cast the geometry, throwing an informative error if it fails. + val centerPoint = centerFeature.geometry() as? Point + ?: throw IllegalStateException("Center of GeoJSON is not a valid Point.") + + // 4. Create and return the Location object. + return location(centerPoint.longitude(), centerPoint.latitude()) + } fun createGeoJson(lineCoordinates: List>): String { val lineString = GeoJsonLineString(type = "LineString", coordinates = lineCoordinates) @@ -210,4 +257,16 @@ fun formatDateTime(time: Long): String { fun Double.round(numFractionDigits: Int): Double { val factor = 10.0.pow(numFractionDigits.toDouble()) return (this * factor).roundToInt() / factor +} + +fun duration(preview: Boolean, bearing: Double, lastBearing: Double): Duration { + if (preview) { + return 3.seconds + } + val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) { + 2.seconds + } else { + 1.seconds + } + return cameraDuration } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b65dd53..60bd242 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -agp = "8.13.1" +agp = "8.13.2" androidSdkTurf = "6.0.1" -gradle = "8.13.1" +gradle = "8.13.2" koinAndroid = "4.1.1" koinAndroidxCompose = "4.1.1" koinComposeViewmodel = "4.1.1"