Refactoring
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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<StepData>()
|
||||
}
|
||||
|
||||
val nextStepData: MutableLiveData<StepData> by lazy {
|
||||
MutableLiveData<StepData>()
|
||||
}
|
||||
|
||||
var lastLocation = location(0.0, 0.0)
|
||||
|
||||
val observer = Observer<String> { 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) //
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
if (!routeModel.isNavigating()) {
|
||||
Button(onClick = {
|
||||
simulateNavigation()
|
||||
}) {
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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,6 +57,7 @@ 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)
|
||||
@@ -67,12 +69,13 @@ fun SearchSheet(
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<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
|
||||
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<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 {
|
||||
return if (preView) {
|
||||
PaddingValues(start = 150.dp, bottom = 0.dp)
|
||||
|
||||
@@ -49,7 +49,7 @@ class RouteCarModel() : RouteModel() {
|
||||
.setIcon(createCarIcon(carContext, stepData.icon))
|
||||
.build()
|
||||
)
|
||||
.setRoad(destination.street!!)
|
||||
.setRoad(routeState.destination.street!!)
|
||||
.build()
|
||||
return step
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<Maneuvers>,
|
||||
val maneuvers: List<Maneuvers>,
|
||||
|
||||
/**
|
||||
* The distance traveled from origin to destination.
|
||||
@@ -33,9 +39,16 @@ data class Route (
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
var waypoints: List<List<Double>>,
|
||||
val waypoints: List<List<Double>>,
|
||||
|
||||
val pointLocations : List<Point>,
|
||||
/**
|
||||
* 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 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<Maneuvers>
|
||||
private var distance: Double = 0.0
|
||||
|
||||
private var time: Double = 0.0
|
||||
private lateinit var waypoints: List<List<Double>>
|
||||
private lateinit var pointLocations: List<Point>
|
||||
|
||||
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<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
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
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..<route.maneuvers.size) {
|
||||
val maneuver = route.maneuvers[i]
|
||||
val beginShapeIndex = maneuver.beginShapeIndex
|
||||
val endShapeIndex = maneuver.endShapeIndex
|
||||
val distance = calculateDistance(beginShapeIndex, endShapeIndex, location)
|
||||
var newShapeIndex = -1
|
||||
// find nearest waypoint and current shape index
|
||||
// start search at last shape index
|
||||
for (i in routeState.currentShapeIndex..<route.waypoints.size) {
|
||||
val waypoint = route.waypoints[i]
|
||||
val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance
|
||||
route.currentManeuverIndex = i
|
||||
calculateCurrentShapeIndex(beginShapeIndex, endShapeIndex, location)
|
||||
newShapeIndex = i
|
||||
}
|
||||
}
|
||||
// find maneuver
|
||||
// calculate distance to step end
|
||||
findManeuver(newShapeIndex)
|
||||
}
|
||||
|
||||
/** Calculates the index in a maneuver. */
|
||||
private fun calculateCurrentShapeIndex(
|
||||
beginShapeIndex: Int,
|
||||
endShapeIndex: Int,
|
||||
location: Location
|
||||
) {
|
||||
var nearestLocation = 100000.0f
|
||||
for (i in currentShapeIndex..endShapeIndex) {
|
||||
val waypoint = Location(LocationManager.GPS_PROVIDER)
|
||||
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
|
||||
private fun findManeuver(newShapeIndex: Int) {
|
||||
for (i in route.currentManeuverIndex..<route.maneuvers.size) {
|
||||
val maneuver = route.maneuvers[i]
|
||||
if (maneuver.beginShapeIndex <= newShapeIndex && maneuver.endShapeIndex >= newShapeIndex) {
|
||||
route = route.copy(currentManeuverIndex = i)
|
||||
routeState.apply {
|
||||
currentShapeIndex = newShapeIndex
|
||||
beginIndex = maneuver.beginShapeIndex
|
||||
endIndex = maneuver.endShapeIndex
|
||||
distanceToStepEnd = 0F
|
||||
val loc1 = Location(LocationManager.GPS_PROVIDER)
|
||||
val loc2 = Location(LocationManager.GPS_PROVIDER)
|
||||
if (i + 1 < route.waypoints.size) {
|
||||
for (j in i + 1..endShapeIndex) {
|
||||
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]
|
||||
// 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<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 {
|
||||
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<Int, Int>) = 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<Int, Int>) = 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
|
||||
}
|
||||
}
|
||||
@@ -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<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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Point>) : 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<List<Double>>): String {
|
||||
|
||||
val lineString = GeoJsonLineString(type = "LineString", coordinates = lineCoordinates)
|
||||
@@ -211,3 +258,15 @@ 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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user