Refactoring

This commit is contained in:
Dimitris
2025-12-12 15:32:15 +01:00
parent a02673af36
commit 72872cddeb
21 changed files with 588 additions and 349 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 36
versionCode = 7 versionCode = 8
versionName = "0.1.3.7" versionName = "0.1.3.8"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -15,28 +15,36 @@ import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.BottomSheetScaffold import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.BottomSheetScaffoldState
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices import com.google.android.gms.location.LocationServices
import com.kouros.data.R
import com.kouros.navigation.data.Constants.homeLocation import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
@@ -74,6 +82,10 @@ class MainActivity : ComponentActivity() {
MutableLiveData<StepData>() MutableLiveData<StepData>()
} }
val nextStepData: MutableLiveData<StepData> by lazy {
MutableLiveData<StepData>()
}
var lastLocation = location(0.0, 0.0) var lastLocation = location(0.0, 0.0)
val observer = Observer<String> { newRoute -> val observer = Observer<String> { newRoute ->
@@ -147,8 +159,9 @@ class MainActivity : ComponentActivity() {
fun Content() { fun Content() {
val scaffoldState = rememberBottomSheetScaffoldState() val scaffoldState = rememberBottomSheetScaffoldState()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val sheetPeekHeightState = remember { mutableStateOf(256.dp) }
val locationProvider = rememberDefaultLocationProvider( val locationProvider = rememberDefaultLocationProvider(
updateInterval = 0.5.seconds, updateInterval = 0.5.seconds,
desiredAccuracy = DesiredAccuracy.Highest desiredAccuracy = DesiredAccuracy.Highest
@@ -161,24 +174,27 @@ class MainActivity : ComponentActivity() {
latitude = locationState.value!!.position.latitude latitude = locationState.value!!.position.latitude
} }
val step: StepData? by stepData.observeAsState() val step: StepData? by stepData.observeAsState()
val nextStep: StepData? by nextStepData.observeAsState()
fun openSheet() { fun openSheet() {
scope.launch { scaffoldState.bottomSheetState.expand() } scope.launch { scaffoldState.bottomSheetState.expand() }
} }
fun closeSheet() { fun closeSheet() {
scope.launch { scaffoldState.bottomSheetState.partialExpand() } scope.launch {
scaffoldState.bottomSheetState.partialExpand()
sheetPeekHeightState.value = 128.dp
} }
}
NavigationTheme { NavigationTheme() {
BottomSheetScaffold( BottomSheetScaffold(
snackbarHost = { snackbarHost = {
SnackbarHost(hostState = snackbarHostState) SnackbarHost(hostState = snackbarHostState)
}, },
scaffoldState = scaffoldState, scaffoldState = scaffoldState,
sheetPeekHeight = 128.dp, sheetPeekHeight = sheetPeekHeightState.value,
sheetContent = { sheetContent = {
SheetContent(latitude, step) { closeSheet() } SheetContent(latitude, step, nextStep) { closeSheet() }
}, },
) { innerPadding -> ) { innerPadding ->
Box( Box(
@@ -187,19 +203,31 @@ class MainActivity : ComponentActivity() {
.padding(innerPadding), .padding(innerPadding),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
MapView(applicationContext,userLocationState, step, cameraPosition, routeData, tilt) MapView(
applicationContext,
userLocationState,
step,
cameraPosition,
routeData,
tilt
)
} }
} }
} }
} }
@Composable @Composable
fun SheetContent(locationState: Double, step: StepData?, closeSheet: () -> Unit) { fun SheetContent(
locationState: Double,
step: StepData?,
nextStep: StepData?,
closeSheet: () -> Unit
) {
if (!routeModel.isNavigating()) { if (!routeModel.isNavigating()) {
SearchSheet(applicationContext, viewModel, lastLocation) { closeSheet() } SearchSheet(applicationContext, viewModel, lastLocation) { closeSheet() }
} else { } else {
NavigationSheet( NavigationSheet(
routeModel, step!!, routeModel, step!!, nextStep!!,
{ stopNavigation { closeSheet() } }, { stopNavigation { closeSheet() } },
{ simulateNavigation() } { simulateNavigation() }
) )
@@ -213,11 +241,16 @@ class MainActivity : ComponentActivity() {
&& lastLocation.latitude != location.position.latitude && lastLocation.latitude != location.position.latitude
&& lastLocation.longitude != location.position.longitude && lastLocation.longitude != location.position.longitude
) { ) {
if (routeModel.isNavigating()) {
routeModel.updateLocation(lastLocation)
stepData.value = routeModel.currentStep()
}
val currentLocation = location(location.position.longitude, location.position.latitude) 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 bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
val zoom = calculateZoom(location.speed) val zoom = calculateZoom(location.speed)
cameraPosition.postValue( cameraPosition.postValue(
@@ -252,7 +285,7 @@ class MainActivity : ComponentActivity() {
val appOpsManager = val appOpsManager =
getSystemService(APP_OPS_SERVICE) as AppOpsManager getSystemService(APP_OPS_SERVICE) as AppOpsManager
val mode = val mode =
appOpsManager.unsafeCheckOp( appOpsManager.checkOp(
AppOpsManager.OPSTR_MOCK_LOCATION, AppOpsManager.OPSTR_MOCK_LOCATION,
Process.myUid(), Process.myUid(),
packageName packageName
@@ -275,7 +308,7 @@ class MainActivity : ComponentActivity() {
for ((_, loc) in routeModel.route.waypoints.withIndex()) { for ((_, loc) in routeModel.route.waypoints.withIndex()) {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
mock.setMockLocation(loc[1], loc[0]) mock.setMockLocation(loc[1], loc[0])
delay(1000L) // delay(500L) //
} }
} }
} }

View File

@@ -1,30 +1,43 @@
package com.kouros.navigation.ui package com.kouros.navigation.ui
import android.R.attr.x
import android.R.attr.y
import android.content.Context import android.content.Context
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.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.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment 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.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.window.layout.WindowMetricsCalculator 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.NavigationImage
import com.kouros.navigation.car.map.RouteLayer
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.NavigationUtils
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.LocationTrackingEffect import org.maplibre.compose.location.LocationTrackingEffect
import org.maplibre.compose.location.UserLocationState 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.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -57,32 +70,14 @@ fun MapView(
zoom = 15.0, zoom = 15.0,
) )
) )
val baseStyle = remember {
mutableStateOf(BaseStyle.Uri(Constants.STYLE))
}
DarkMode(applicationContext, baseStyle)
Column { Column {
NavigationInfo(step) NavigationInfo(step)
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
MaplibreMap( MapLibre(applicationContext, cameraState, baseStyle, route, "", position)
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( LocationTrackingEffect(
locationState = userLocationState, locationState = userLocationState,
) { ) {
@@ -97,7 +92,8 @@ fun MapView(
duration = 1.seconds duration = 1.seconds
) )
} }
NavigationImage(paddingValues, width, height / 6, "") NavigationImage(paddingValues, width, height / 6)
} }
} }
} }

View File

@@ -25,6 +25,7 @@ import com.kouros.navigation.utils.round
fun NavigationSheet( fun NavigationSheet(
routeModel: RouteModel, routeModel: RouteModel,
step: StepData, step: StepData,
nextStep: StepData,
stopNavigation: () -> Unit, stopNavigation: () -> Unit,
simulateNavigation: () -> Unit, simulateNavigation: () -> Unit,
) { ) {
@@ -47,7 +48,9 @@ fun NavigationSheet(
modifier = Modifier.size(24.dp, 24.dp), modifier = Modifier.size(24.dp, 24.dp),
) )
} }
}
Spacer(Modifier.size(30.dp)) Spacer(Modifier.size(30.dp))
if (!routeModel.isNavigating()) {
Button(onClick = { Button(onClick = {
simulateNavigation() simulateNavigation()
}) { }) {

View File

@@ -57,7 +57,7 @@ fun PermissionScreen(
errorText = if (rejectedPermissions.none { it in requiredPermissions }) { errorText = if (rejectedPermissions.none { it in requiredPermissions }) {
"" ""
} else { } else {
"${rejectedPermissions.joinToString()} required for the sample" "${rejectedPermissions.joinToString()} required for the app"
} }
} }
val allRequiredPermissionsGranted = val allRequiredPermissionsGranted =
@@ -128,7 +128,7 @@ private fun PermissionScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Text( Text(
text = "Sample requires permission/s:", text = "Navigation requires permission/s:",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
) )

View File

@@ -14,6 +14,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -56,6 +57,7 @@ fun SearchSheet(
if (search.value != null) { if (search.value != null) {
searchResults.addAll(search.value!!) searchResults.addAll(search.value!!)
} }
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
if (searchResults.isNotEmpty()) { if (searchResults.isNotEmpty()) {
val textFieldState = rememberTextFieldState() val textFieldState = rememberTextFieldState()
val items = listOf(searchResults) val items = listOf(searchResults)
@@ -73,6 +75,7 @@ fun SearchSheet(
} }
} }
if (recentPlaces.value != null) { if (recentPlaces.value != null) {
println("Recent Places ${recentPlaces.value}")
val textFieldState = rememberTextFieldState() val textFieldState = rememberTextFieldState()
val items = listOf(recentPlaces) val items = listOf(recentPlaces)
if (items.isNotEmpty()) { if (items.isNotEmpty()) {
@@ -87,6 +90,40 @@ fun SearchSheet(
) )
} }
} }
}
@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) @OptIn(ExperimentalMaterial3Api::class)
@@ -176,6 +213,16 @@ private fun SearchPlaces(
headlineContent = { Text("${place.address.road} ${place.address.postcode}") }, headlineContent = { Text("${place.address.road} ${place.address.postcode}") },
modifier = Modifier modifier = Modifier
.clickable { .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 = val toLocation =
location(place.lon.toDouble(), place.lat.toDouble()) location(place.lon.toDouble(), place.lat.toDouble())
viewModel.loadRoute(context, location, toLocation) viewModel.loadRoute(context, location, toLocation)
@@ -212,7 +259,7 @@ private fun RecentPlaces(
modifier = Modifier.size(24.dp, 24.dp), modifier = Modifier.size(24.dp, 24.dp),
) )
ListItem( ListItem(
headlineContent = { Text("${place.street!!} ${place.postalCode}") }, headlineContent = { Text("${place.name} ${place.postalCode}") },
modifier = Modifier modifier = Modifier
.clickable { .clickable {
val toLocation = location(place.longitude, place.latitude) val toLocation = location(place.longitude, place.latitude)

View File

@@ -162,7 +162,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations()) val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snapedLocation) val distance = location.distanceTo(snapedLocation)
if (distance > MAXIMAL_ROUTE_DEVIATION) { if (distance > MAXIMAL_ROUTE_DEVIATION) {
navigationScreen.calculateNewRoute(routeModel.destination) navigationScreen.calculateNewRoute(routeModel.routeState.destination)
return return
} }
navigationScreen.updateTrip(location) navigationScreen.updateTrip(location)

View File

@@ -5,14 +5,14 @@ import android.graphics.Rect
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay import android.hardware.display.VirtualDisplay
import android.location.Location import android.location.Location
import android.location.LocationManager import android.os.CountDownTimer
import android.os.Handler
import android.util.Log import android.util.Log
import androidx.car.app.AppManager import androidx.car.app.AppManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.SurfaceCallback import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer import androidx.car.app.SurfaceContainer
import androidx.car.app.connection.CarConnection import androidx.car.app.connection.CarConnection
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -27,31 +27,24 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.kouros.navigation.car.map.BuildingLayer import com.kouros.navigation.car.map.DarkMode
import com.kouros.navigation.car.map.DrawImage 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.cameraState
import com.kouros.navigation.car.map.getPaddingValues import com.kouros.navigation.car.map.getPaddingValues
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants 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.data.ObjectBox
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.bearing import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateZoom 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 com.kouros.navigation.utils.previewZoom
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState 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.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import kotlin.math.absoluteValue
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
class SurfaceRenderer( class SurfaceRenderer(
@@ -59,14 +52,14 @@ class SurfaceRenderer(
private var routeModel: RouteCarModel private var routeModel: RouteCarModel
) : DefaultLifecycleObserver { ) : DefaultLifecycleObserver {
var lastLocation = Location(LocationManager.GPS_PROVIDER) var lastLocation = location(0.0, 0.0)
val cameraPosition = MutableLiveData( private val cameraPosition = MutableLiveData(
CameraPosition( CameraPosition(
zoom = 15.0, zoom = 15.0,
target = Position(latitude = 48.1857475, longitude = 11.5793627) target = Position(latitude = 48.1857475, longitude = 11.5793627)
) )
) )
var visibleArea = MutableLiveData( private var visibleArea = MutableLiveData(
Rect(0, 0, 0, 0) Rect(0, 0, 0, 0)
) )
@@ -81,6 +74,7 @@ class SurfaceRenderer(
val previewRouteData = MutableLiveData("") val previewRouteData = MutableLiveData("")
val speed = MutableLiveData(0F)
lateinit var centerLocation: Location lateinit var centerLocation: Location
var preview = false var preview = false
@@ -90,6 +84,8 @@ class SurfaceRenderer(
var tilt = 55.0 var tilt = 55.0
var previewDistance = 0.0 var previewDistance = 0.0
var countDownTimerActive = false
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback { val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
lateinit var lifecycleOwner: CustomLifecycleOwner lateinit var lifecycleOwner: CustomLifecycleOwner
@@ -171,6 +167,7 @@ class SurfaceRenderer(
init { init {
lifecycle.addObserver(this) lifecycle.addObserver(this)
speed.value = 0F
} }
fun onConnectionStateUpdated(connectionState: Int) { fun onConnectionStateUpdated(connectionState: Int) {
@@ -192,26 +189,8 @@ class SurfaceRenderer(
val baseStyle = remember { val baseStyle = remember {
mutableStateOf(BaseStyle.Uri(Constants.STYLE)) mutableStateOf(BaseStyle.Uri(Constants.STYLE))
} }
baseStyle.value = DarkMode(carContext, baseStyle)
(if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri( MapLibre(carContext, cameraState, baseStyle, route, previewRoute, position)
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)
}
ShowPosition(cameraState, position, paddingValues) ShowPosition(cameraState, position, paddingValues)
} }
@@ -221,16 +200,17 @@ class SurfaceRenderer(
position: CameraPosition?, position: CameraPosition?,
paddingValues: PaddingValues paddingValues: PaddingValues
) { ) {
val cameraDuration = duration(position) val cameraDuration = duration(preview, position!!.bearing, lastBearing)
var bearing = position!!.bearing var bearing = position.bearing
var zoom = position.zoom var zoom = position.zoom
var target = position.target var target = position.target
var localTilt = tilt var localTilt = tilt
val currentSpeed: Float? by speed.observeAsState()
if (!preview) { if (!preview) {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
DrawImage(paddingValues, lastLocation, width, height, "") DrawImage(paddingValues, currentSpeed, width, height)
} else { } else {
DrawImage(paddingValues, lastLocation, width, height, "") DrawImage(paddingValues, currentSpeed, width, height)
} }
} else { } else {
bearing = 0.0 bearing = 0.0
@@ -259,18 +239,6 @@ class SurfaceRenderer(
.setSurfaceCallback(mSurfaceCallback) .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. */ /** Handles the map zoom-in and zoom-out events. */
fun handleScale(zoomSign: Int) { fun handleScale(zoomSign: Int) {
synchronized(this) { synchronized(this) {
@@ -313,6 +281,13 @@ class SurfaceRenderer(
) )
lastBearing = cameraPosition.value!!.bearing lastBearing = cameraPosition.value!!.bearing
lastLocation = location lastLocation = location
speed.value = location.speed
if (!countDownTimerActive) {
countDownTimerActive = true
val mainThreadHandler = Handler(carContext.mainLooper)
val lastLocationTimer = lastLocation
checkUpdate(mainThreadHandler, lastLocationTimer)
}
} else { } else {
updateCameraPosition( updateCameraPosition(
0.0, 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) { private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) {
cameraPosition.postValue( cameraPosition.postValue(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
@@ -344,7 +336,7 @@ class SurfaceRenderer(
fun setPreviewRouteData(routeModel: RouteModel) { fun setPreviewRouteData(routeModel: RouteModel) {
previewRouteData.value = routeModel.route.routeGeoJson previewRouteData.value = routeModel.route.routeGeoJson
centerLocation = routeModel.centerLocation centerLocation = routeModel.route.centerLocation
preview = true preview = true
previewDistance = routeModel.route.distance previewDistance = routeModel.route.distance
} }

View File

@@ -1,7 +1,9 @@
package com.kouros.navigation.car.map package com.kouros.navigation.car.map
import android.location.Location import android.location.Location
import android.content.Context
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -12,6 +14,7 @@ import androidx.compose.material3.BadgedBox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -25,9 +28,13 @@ import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.kouros.data.R import com.kouros.data.R
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.NavigationColor
import com.kouros.navigation.data.RouteColor import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor 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.CameraPosition
import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState 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.LocationPuckColors
import org.maplibre.compose.location.LocationPuckSizes import org.maplibre.compose.location.LocationPuckSizes
import org.maplibre.compose.location.UserLocationState 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.GeoJsonData
import org.maplibre.compose.sources.Source import org.maplibre.compose.sources.Source
import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.sources.rememberGeoJsonSource import org.maplibre.compose.sources.rememberGeoJsonSource
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
@@ -65,6 +77,32 @@ fun cameraState(
) )
} }
@Composable
fun MapLibre(
context: Context,
cameraState: CameraState,
baseStyle: MutableState<BaseStyle.Uri>,
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 @Composable
fun RouteLayer(routeData: String?, previewRoute: String?, zoom: Double) { fun RouteLayer(routeData: String?, previewRoute: String?, zoom: Double) {
val width = zoom - 2 val width = zoom - 2
@@ -115,26 +153,20 @@ fun BuildingLayer(tiles: Source) {
} }
@Composable @Composable
fun DrawImage(padding: PaddingValues, location: Location, width: Int, height: Int, street: String) { fun DrawImage(padding: PaddingValues, speed: Float?, width: Int, height: Int) {
NavigationImage(padding, width,height, street) NavigationImage(padding, width,height)
Speed(width, height, location) Speed(width, height, speed)
} }
@Composable @Composable
fun NavigationImage(padding: PaddingValues, width: Int, height: Int, street: String) { fun NavigationImage(padding: PaddingValues, width: Int, height: Int) {
val imageSize = (height/6) val imageSize = (height/6)
val color = remember { NavigationColor } val color = remember { NavigationColor }
BadgedBox( Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) {
modifier = Modifier
.padding(padding),
badge = {
Badge()
}
) {
Canvas(modifier =Modifier Canvas(modifier =Modifier
.size(imageSize.dp, imageSize.dp)) { .size(imageSize.dp, imageSize.dp)) {
scale(scaleX = 1f, scaleY = 0.7f) { scale(scaleX = 1f, scaleY = 0.7f) {
drawCircle(Color.DarkGray.copy(alpha = 0.2f)) drawCircle(Color.DarkGray.copy(alpha = 0.4f))
} }
} }
Icon( Icon(
@@ -143,8 +175,6 @@ fun NavigationImage(padding: PaddingValues, width: Int, height: Int, street: Str
tint = color.copy(alpha = 1f), tint = color.copy(alpha = 1f),
modifier = Modifier.size(imageSize.dp, imageSize.dp), 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( private fun Speed(
width: Int, width: Int,
height: Int, height: Int,
location: Location speed: Float?
) { ) {
val radius = 32 val radius = 32
Box( Box(
@@ -165,7 +195,7 @@ private fun Speed(
) { ) {
val textMeasurerSpeed = rememberTextMeasurer() val textMeasurerSpeed = rememberTextMeasurer()
val textMeasurerKm = rememberTextMeasurer() val textMeasurerKm = rememberTextMeasurer()
val speed = (location.speed * 3.6).toInt().toString() val speed = (speed!! * 3.6).toInt().toString()
val kmh = "km/h" val kmh = "km/h"
val styleSpeed = TextStyle( val styleSpeed = TextStyle(
fontSize = 22.sp, fontSize = 22.sp,
@@ -212,6 +242,23 @@ private fun Speed(
} }
} }
@Composable
fun DarkMode(context: Context, baseStyle: MutableState<BaseStyle.Uri>) {
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 { fun getPaddingValues(width: Int, height: Int, preView: Boolean): PaddingValues {
return if (preView) { return if (preView) {
PaddingValues(start = 150.dp, bottom = 0.dp) PaddingValues(start = 150.dp, bottom = 0.dp)

View File

@@ -49,7 +49,7 @@ class RouteCarModel() : RouteModel() {
.setIcon(createCarIcon(carContext, stepData.icon)) .setIcon(createCarIcon(carContext, stepData.icon))
.build() .build()
) )
.setRoad(destination.street!!) .setRoad(routeState.destination.street!!)
.build() .build()
return step return step
} }

View File

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

View File

@@ -1,12 +1,12 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Header import androidx.car.app.model.Header
import androidx.car.app.model.ItemList import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate import androidx.car.app.model.ListTemplate
import androidx.car.app.model.OnClickListener
import androidx.car.app.model.Row import androidx.car.app.model.Row
import androidx.car.app.model.Template import androidx.car.app.model.Template
import androidx.car.app.model.Toggle import androidx.car.app.model.Toggle
@@ -19,6 +19,7 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
private var buildingToggleState = false private var buildingToggleState = false
init { init {
buildingToggleState = getBooleanKeyValue(carContext, SHOW_THREED_BUILDING) buildingToggleState = getBooleanKeyValue(carContext, SHOW_THREED_BUILDING)
} }
@@ -35,7 +36,12 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
buildingToggleState = !buildingToggleState buildingToggleState = !buildingToggleState
}.setChecked(buildingToggleState).build() }.setChecked(buildingToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.threed_building, buildingToggle)) listBuilder.addItem(buildRowForTemplate(R.string.threed_building, buildingToggle))
listBuilder.addItem(
buildRowForScreenTemplate(
DarkModeSettings(carContext),
R.string.dark_mode
)
)
return ListTemplate.Builder() return ListTemplate.Builder()
.setSingleList(listBuilder.build()) .setSingleList(listBuilder.build())
.setHeader( .setHeader(
@@ -54,4 +60,12 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
.setToggle(toggle) .setToggle(toggle)
.build() .build()
} }
private fun buildRowForScreenTemplate(screen: Screen, title: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.setOnClickListener { screenManager.push(screen) }
.setBrowsable(true)
.build()
}
} }

View File

@@ -111,11 +111,11 @@ class NavigationScreen(
} }
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template { private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
if (routeModel.isArrived()) { if (routeModel.routeState.arrived) {
val timer = object : CountDownTimer(10000, 10000) { val timer = object : CountDownTimer(10000, 10000) {
override fun onTick(millisUntilFinished: Long) {} override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() { override fun onFinish() {
routeModel.arrived = false routeModel.routeState = routeModel.routeState.copy(arrived = false)
invalidate() invalidate()
} }
} }
@@ -135,12 +135,16 @@ class NavigationScreen(
} }
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate { fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
var street = ""
if (routeModel.routeState.destination.street != null) {
street = routeModel.routeState.destination.street!!
}
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo( .setNavigationInfo(
MessageInfo.Builder( MessageInfo.Builder(
carContext.getString(R.string.arrived_exclamation_msg) carContext.getString(R.string.arrived_exclamation_msg)
) )
.setText(routeModel.destination.street!!) .setText(street)
.setImage( .setImage(
CarIcon.Builder( CarIcon.Builder(
IconCompat.createWithResource( IconCompat.createWithResource(
@@ -192,7 +196,7 @@ class NavigationScreen(
} }
fun getRoutingInfo(): RoutingInfo { fun getRoutingInfo(): RoutingInfo {
var currentDistance = routeModel.currentDistance var currentDistance = routeModel.leftStepDistance()
val displayUnit = if (currentDistance > 1000.0) { val displayUnit = if (currentDistance > 1000.0) {
currentDistance /= 1000.0 currentDistance /= 1000.0
Distance.UNIT_KILOMETERS Distance.UNIT_KILOMETERS
@@ -274,7 +278,7 @@ class NavigationScreen(
.setOnClickListener { .setOnClickListener {
val navigateTo = location(recentPlace.longitude, recentPlace.latitude) val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, navigateTo) viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, navigateTo)
routeModel.destination = recentPlace routeModel.routeState.destination = recentPlace
} }
.build() .build()
} }
@@ -398,7 +402,7 @@ class NavigationScreen(
viewModel.saveRecent(place) viewModel.saveRecent(place)
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, location) viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, location)
currentNavigationLocation = location currentNavigationLocation = location
routeModel.destination = place routeModel.routeState.destination = place
invalidate() invalidate()
} }
} }
@@ -416,7 +420,7 @@ class NavigationScreen(
invalidate() invalidate()
val mainThreadHandler = Handler(carContext.mainLooper) val mainThreadHandler = Handler(carContext.mainLooper)
mainThreadHandler.post { mainThreadHandler.post {
object : CountDownTimer(5000, 1000) { object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {} override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() { override fun onFinish() {
calculateNewRoute = false calculateNewRoute = false
@@ -432,14 +436,15 @@ class NavigationScreen(
} }
fun updateTrip(location: Location) { fun updateTrip(location: Location) {
val start = System.currentTimeMillis() with(routeModel) {
routeModel.updateLocation(location) updateLocation(location)
val end = System.currentTimeMillis() if (routeState.maneuverType == Maneuver.TYPE_DESTINATION
println("Time ${end-start}") && leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
if (routeModel.maneuverType == Maneuver.TYPE_DESTINATION ) {
&& routeModel.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE) { stopNavigation()
routeModel.arrived = true routeState = routeState.copy(arrived = true)
routeModel.stopNavigation() surfaceRenderer.routeData.value = ""
}
} }
invalidate() invalidate()
} }

View File

@@ -2,7 +2,7 @@ package com.kouros.navigation.data
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val NavigationColor = Color(0xFF052186) val NavigationColor = Color(0xFF0730B2)
val RouteColor = Color(0xFF5582D0) val RouteColor = Color(0xFF5582D0)

View File

@@ -164,6 +164,8 @@ object Constants {
const val SHOW_THREED_BUILDING = "Show3D" const val SHOW_THREED_BUILDING = "Show3D"
const val DARK_MODE_SETTINGS = "DarkMode"
const val AVOID_MOTORWAY = "AvoidMotorway" const val AVOID_MOTORWAY = "AvoidMotorway"
const val AVOID_TOLLWAY = "AvoidTollway" const val AVOID_TOLLWAY = "AvoidTollway"

View File

@@ -1,13 +1,19 @@
package com.kouros.navigation.data package com.kouros.navigation.data
import android.location.Location
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.kouros.navigation.data.valhalla.Maneuvers import com.kouros.navigation.data.valhalla.Maneuvers
import com.kouros.navigation.data.valhalla.Summary import com.kouros.navigation.data.valhalla.Summary
import com.kouros.navigation.data.valhalla.Trip import com.kouros.navigation.data.valhalla.Trip
import com.kouros.navigation.data.valhalla.ValhallaJson 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.createGeoJson
import com.kouros.navigation.utils.NavigationUtils.decodePolyline 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.geojson.Point
import org.maplibre.turf.TurfMeasurement
import kotlin.math.roundToInt
data class Route( data class Route(
@@ -16,7 +22,7 @@ data class Route (
* *
* @since 1.0.0 * @since 1.0.0
*/ */
var maneuvers: List<Maneuvers>, val maneuvers: List<Maneuvers>,
/** /**
* The distance traveled from origin to destination. * The distance traveled from origin to destination.
@@ -33,8 +39,15 @@ data class Route (
* *
* @since 1.0.0 * @since 1.0.0
*/ */
var waypoints: List<List<Double>>, val waypoints: List<List<Double>>,
/**
* List of [List<Point>] 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<Point>, val pointLocations: List<Point>,
val summary: Summary, val summary: Summary,
@@ -43,25 +56,24 @@ data class Route (
val time: Double, val time: Double,
var routeGeoJson : String, val routeGeoJson: String,
var currentManeuverIndex: Int val currentManeuverIndex : Int,
val centerLocation: Location
) { ) {
class Builder { class Builder {
private lateinit var maneuvers: List<Maneuvers> private lateinit var maneuvers: List<Maneuvers>
private var distance: Double = 0.0 private var distance: Double = 0.0
private var time: Double = 0.0 private var time: Double = 0.0
private lateinit var waypoints: List<List<Double>> private lateinit var waypoints: List<List<Double>>
private lateinit var pointLocations: List<Point> private lateinit var pointLocations: List<Point>
private lateinit var summary: Summary private lateinit var summary: Summary
private lateinit var trip: Trip private lateinit var trip: Trip
private var routeGeoJson = "" 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 != "[]") { if (route.isNotEmpty() && route != "[]") {
@@ -83,35 +95,37 @@ data class Route (
points.add(point) points.add(point)
} }
pointLocations = points pointLocations = points
this.routeGeoJson = createGeoJson(waypoints) routeGeoJson = createGeoJson(waypoints)
centerLocation = createCenterLocation(routeGeoJson)
return Route( return Route(
maneuvers, distance, waypoints, pointLocations, summary, trip, time, routeGeoJson, 0 maneuvers,
distance,
waypoints,
pointLocations,
summary,
trip,
time,
routeGeoJson,
0,
centerLocation
) )
} }
} }
fun maneuverLocations(): List<Point> { fun maneuverLocations(): List<Point> {
val beginShapeIndex = currentManeuver().beginShapeIndex
val endShapeIndex = if (currentManeuver().endShapeIndex >= waypoints.size) {
waypoints.size
} else {
currentManeuver().endShapeIndex + 1
}
//return pointLocations.subList(beginShapeIndex, endShapeIndex)
return pointLocations return pointLocations
} }
fun clear() {
waypoints = mutableListOf()
maneuvers = mutableListOf()
routeGeoJson = ""
}
fun currentManeuver(): Maneuvers { fun currentManeuver(): Maneuvers {
return maneuvers[currentManeuverIndex] return maneuvers[currentManeuverIndex]
} }
fun nextManeuver(): Maneuvers { fun nextManeuver(): Maneuvers {
return maneuvers[currentManeuverIndex+1] val nextIndex = currentManeuverIndex + 1
return if (nextIndex < maneuvers.size) {
maneuvers[nextIndex]
} else {
throw IndexOutOfBoundsException("No next maneuver available.")
}
} }
} }

View File

@@ -22,7 +22,7 @@
"id": "background", "id": "background",
"type": "background", "type": "background",
"layout": {"visibility": "visible"}, "layout": {"visibility": "visible"},
"paint": {"background-color": "rgba(146, 146, 142, 1)"} "paint": {"background-color": "rgba(28, 28, 35, 1)"}
}, },
{ {
"id": "natural_earth", "id": "natural_earth",

View File

@@ -1,139 +1,99 @@
package com.kouros.navigation.model package com.kouros.navigation.model
import android.location.Location import android.location.Location
import android.location.LocationManager
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import com.kouros.data.R 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.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.ManeuverType import com.kouros.navigation.data.ManeuverType
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Route import com.kouros.navigation.data.Route
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.location 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.TurfMeasurement
import org.maplibre.turf.TurfMisc
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt import kotlin.math.roundToInt
open class RouteModel() { 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 route: Route
get() = routeState.route!!
var arrived = false set(value) {
routeState = routeState.copy(route = value)
var maneuverType = 0 }
fun startNavigation(routeString: String) {
/* val newRoute = Route.Builder()
current shapeIndex .route(routeString)
*/
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)
.build() .build()
centerLocation = createCenterLocation() this.routeState = routeState.copy(
navigating = true route = newRoute,
isNavigating = true
)
} }
fun stopNavigation() { fun stopNavigation() {
route.clear() this.routeState = routeState.copy(
navigating = false route = null,
currentShapeIndex = 0 isNavigating = false,
distanceToStepEnd = 0F // destination = Place(),
beginIndex = 0 arrived = false,
maneuverType = 0,
currentShapeIndex = 0,
distanceToStepEnd = 0F,
beginIndex = 0,
endIndex = 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) { fun updateLocation(location: Location) {
var nearestDistance = 100000.0f var nearestDistance = 100000.0f
for (i in route.currentManeuverIndex..<route.maneuvers.size) { var newShapeIndex = -1
val maneuver = route.maneuvers[i] // find nearest waypoint and current shape index
val beginShapeIndex = maneuver.beginShapeIndex // start search at last shape index
val endShapeIndex = maneuver.endShapeIndex for (i in routeState.currentShapeIndex..<route.waypoints.size) {
val distance = calculateDistance(beginShapeIndex, endShapeIndex, location) val waypoint = route.waypoints[i]
val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
if (distance < nearestDistance) { if (distance < nearestDistance) {
nearestDistance = distance nearestDistance = distance
route.currentManeuverIndex = i newShapeIndex = i
calculateCurrentShapeIndex(beginShapeIndex, endShapeIndex, location)
} }
} }
// find maneuver
// calculate distance to step end
findManeuver(newShapeIndex)
} }
/** Calculates the index in a maneuver. */ private fun findManeuver(newShapeIndex: Int) {
private fun calculateCurrentShapeIndex( for (i in route.currentManeuverIndex..<route.maneuvers.size) {
beginShapeIndex: Int, val maneuver = route.maneuvers[i]
endShapeIndex: Int, if (maneuver.beginShapeIndex <= newShapeIndex && maneuver.endShapeIndex >= newShapeIndex) {
location: Location route = route.copy(currentManeuverIndex = i)
) { routeState.apply {
var nearestLocation = 100000.0f currentShapeIndex = newShapeIndex
for (i in currentShapeIndex..endShapeIndex) { beginIndex = maneuver.beginShapeIndex
val waypoint = Location(LocationManager.GPS_PROVIDER) endIndex = maneuver.endShapeIndex
waypoint.longitude = route.waypoints[i][0]
waypoint.latitude = route.waypoints[i][1]
val distance: Float = location.distanceTo(waypoint)
if (distance < nearestLocation) {
nearestLocation = distance
currentShapeIndex = i
beginIndex = beginShapeIndex
endIndex = endShapeIndex
distanceToStepEnd = 0F distanceToStepEnd = 0F
val loc1 = Location(LocationManager.GPS_PROVIDER) // calculate shape distance to step end
val loc2 = Location(LocationManager.GPS_PROVIDER) for (j in newShapeIndex + 1..maneuver.endShapeIndex) {
if (i + 1 < route.waypoints.size) { val loc1 = location(route!!.waypoints[j - 1][0], route.waypoints[j - 1][1])
for (j in i + 1..endShapeIndex) { val loc2 = location(route.waypoints[j][0], route.waypoints[j][1])
loc1.longitude = route.waypoints[j - 1][0]
loc1.latitude = route.waypoints[j - 1][1]
loc2.longitude = route.waypoints[j][0]
loc2.latitude = route.waypoints[j][1]
distanceToStepEnd += loc1.distanceTo(loc2) 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<Int, Int>) = 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 { fun nextStep(): StepData {
val maneuver = route.nextManeuver() val maneuver = route.nextManeuver()
val maneuverType = maneuver.type val maneuverType = maneuver.type
@@ -220,30 +144,24 @@ open class RouteModel() {
when (distanceLeft) { when (distanceLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> { in 0.0..NEXT_STEP_THRESHOLD -> {
} }
else -> { else -> {
if (maneuver.streetNames != null && maneuver.streetNames!!.isNotEmpty()) { if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) {
text = maneuver.streetNames[0] text = maneuver.streetNames[0]
} }
} }
} }
val routing: (Pair<Int, Int>) = maneuverIcon(maneuverType)
return StepData(text, distanceLeft, routing.first, routing.second, arrivalTime(), travelLeftDistance())
}
private fun calculateDistance( val routing: (Pair<Int, Int>) = maneuverIcon(maneuverType)
beginShapeIndex: Int, // Construct and return the final StepData object
endShapeIndex: Int, return StepData(
location: Location text,
): Float { distanceLeft,
var nearestLocation = 100000.0f routing.first,
for (i in beginShapeIndex..endShapeIndex) { routing.second,
val polylineLocation = location(route.waypoints[i][0], route.waypoints[i][1]) arrivalTime(),
val distance: Float = location.distanceTo(polylineLocation) travelLeftDistance()
if (distance < nearestLocation) { )
nearestLocation = distance
}
}
return nearestLocation
} }
fun travelLeftTime(): Double { fun travelLeftTime(): Double {
@@ -252,10 +170,11 @@ open class RouteModel() {
val maneuver = route.maneuvers[i] val maneuver = route.maneuvers[i]
timeLeft += maneuver.time timeLeft += maneuver.time
} }
if (endIndex > 0) { if (routeState.endIndex > 0) {
val maneuver = route.currentManeuver() val maneuver = route.currentManeuver()
val curTime = maneuver.time 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 val time = curTime * percent / 100
timeLeft += time timeLeft += time
} }
@@ -275,10 +194,12 @@ open class RouteModel() {
fun leftStepDistance(): Double { fun leftStepDistance(): Double {
val maneuver = route.currentManeuver() val maneuver = route.currentManeuver()
var leftDistance = maneuver.length var leftDistance = maneuver.length
if (endIndex > 0) { if (routeState.endIndex > 0) {
leftDistance = (distanceToStepEnd / 1000).toDouble() 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. */ /** Returns the left distance in km. */
@@ -288,10 +209,11 @@ open class RouteModel() {
val maneuver = route.maneuvers[i] val maneuver = route.maneuvers[i]
leftDistance += maneuver.length leftDistance += maneuver.length
} }
if (endIndex > 0) { if (routeState.endIndex > 0) {
val maneuver = route.currentManeuver() val maneuver = route.currentManeuver()
val curDistance = maneuver.length 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 val time = curDistance * percent / 100
leftDistance += time leftDistance += time
} }
@@ -360,21 +282,21 @@ open class RouteModel() {
currentTurnIcon = R.drawable.ic_roundabout_ccw currentTurnIcon = R.drawable.ic_roundabout_ccw
} }
} }
maneuverType = type routeState.maneuverType = type
return Pair(type, currentTurnIcon) return Pair(type, currentTurnIcon)
} }
fun isNavigating(): Boolean { fun isNavigating(): Boolean {
return navigating return routeState.isNavigating
} }
fun isArrived(): Boolean { fun isArrived(): Boolean {
return arrived return routeState.arrived
} }
fun hasArrived(type: Int): Boolean { fun hasArrived(type: Int): Boolean {
return type == ManeuverType.DestinationRight.value return type == ManeuverType.DestinationRight.value
|| maneuverType == ManeuverType.Destination.value || routeState.maneuverType == ManeuverType.Destination.value
|| maneuverType == ManeuverType.DestinationLeft.value || routeState.maneuverType == ManeuverType.DestinationLeft.value
} }
} }

View File

@@ -241,7 +241,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
savePlace(place) savePlace(place)
} }
fun savePlace(place: Place) { private fun savePlace(place: Place) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val placeBox = boxStore.boxFor(Place::class) val placeBox = boxStore.boxFor(Place::class)
@@ -258,6 +258,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
val current = LocalDateTime.now(ZoneOffset.UTC) val current = LocalDateTime.now(ZoneOffset.UTC)
place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond() place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond()
placeBox.put(place) placeBox.put(place)
println("Save Recent $place")
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -333,4 +334,21 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
return results.toMutableStateList() return results.toMutableStateList()
} }
fun loadRecentPlace(): SnapshotStateList<Place?> {
val results = listOf<Place>()
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()
}
} }

View File

@@ -9,7 +9,9 @@ import com.kouros.navigation.data.GeoJsonFeature
import com.kouros.navigation.data.GeoJsonFeatureCollection import com.kouros.navigation.data.GeoJsonFeatureCollection
import com.kouros.navigation.data.GeoJsonLineString import com.kouros.navigation.data.GeoJsonLineString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point import org.maplibre.geojson.Point
import org.maplibre.turf.TurfMeasurement
import org.maplibre.turf.TurfMisc import org.maplibre.turf.TurfMisc
import java.lang.Math.toDegrees import java.lang.Math.toDegrees
import java.lang.Math.toRadians import java.lang.Math.toRadians
@@ -19,13 +21,15 @@ import java.time.ZoneOffset
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import kotlin.math.absoluteValue
import kotlin.math.asin import kotlin.math.asin
import kotlin.math.atan2 import kotlin.math.atan2
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.sin import kotlin.math.sin
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
object NavigationUtils { object NavigationUtils {
@@ -52,6 +56,29 @@ object NavigationUtils {
apply() 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<Point>) : Location { fun snapLocation(location: Location, stepCoordinates: List<Point>) : Location {
val newLocation = Location(location) val newLocation = Location(location)
val oldPoint = Point.fromLngLat(location.longitude, location.latitude) val oldPoint = Point.fromLngLat(location.longitude, location.latitude)
@@ -101,6 +128,26 @@ object NavigationUtils {
return coordinates 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<List<Double>>): String { fun createGeoJson(lineCoordinates: List<List<Double>>): String {
val lineString = GeoJsonLineString(type = "LineString", coordinates = lineCoordinates) val lineString = GeoJsonLineString(type = "LineString", coordinates = lineCoordinates)
@@ -211,3 +258,15 @@ fun Double.round(numFractionDigits: Int): Double {
val factor = 10.0.pow(numFractionDigits.toDouble()) val factor = 10.0.pow(numFractionDigits.toDouble())
return (this * factor).roundToInt() / factor 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
}

View File

@@ -1,7 +1,7 @@
[versions] [versions]
agp = "8.13.1" agp = "8.13.2"
androidSdkTurf = "6.0.1" androidSdkTurf = "6.0.1"
gradle = "8.13.1" gradle = "8.13.2"
koinAndroid = "4.1.1" koinAndroid = "4.1.1"
koinAndroidxCompose = "4.1.1" koinAndroidxCompose = "4.1.1"
koinComposeViewmodel = "4.1.1" koinComposeViewmodel = "4.1.1"