SheetContent Simulation
This commit is contained in:
@@ -99,6 +99,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.androidx.compose.foundation.layout)
|
implementation(libs.androidx.compose.foundation.layout)
|
||||||
|
implementation(libs.androidx.compose.foundation)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@@ -98,4 +98,5 @@ class MockLocation (private var locationManager: LocationManager) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
132
app/src/main/java/com/kouros/navigation/model/Simulation.kt
Normal file
132
app/src/main/java/com/kouros/navigation/model/Simulation.kt
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package com.kouros.navigation.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.kouros.data.R
|
||||||
|
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
|
import io.ticofab.androidgpxparser.parser.GPXParser
|
||||||
|
import io.ticofab.androidgpxparser.parser.domain.Gpx
|
||||||
|
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
import kotlin.collections.forEach
|
||||||
|
|
||||||
|
fun simulate(routeModel: RouteModel, mock: MockLocation) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
var lastLocation = location(0.0, 0.0)
|
||||||
|
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
|
||||||
|
val curLocation = location(waypoint[0], waypoint[1])
|
||||||
|
if (routeModel.isNavigating()) {
|
||||||
|
val deviation = 0.0
|
||||||
|
if (index in 0..routeModel.curRoute.waypoints.size) {
|
||||||
|
val bearing = lastLocation.bearingTo(curLocation)
|
||||||
|
mock.setMockLocation(waypoint[1], waypoint[0], bearing)
|
||||||
|
Thread.sleep(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastLocation = curLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test(applicationContext: Context, routeModel: RouteModel) {
|
||||||
|
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
|
||||||
|
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
||||||
|
routeModel.updateLocation(
|
||||||
|
applicationContext,
|
||||||
|
location(waypoint[0], waypoint[1]), navigationViewModel
|
||||||
|
)
|
||||||
|
val step = routeModel.currentStep()
|
||||||
|
val nextStep = routeModel.nextStep()
|
||||||
|
println("Step: ${step.instruction} ${step.leftStepDistance} ${nextStep.currentManeuverType}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testSingle(applicationContext: Context, routeModel: RouteModel, mock: MockLocation) {
|
||||||
|
testSingleUpdate(
|
||||||
|
applicationContext,
|
||||||
|
48.185976,
|
||||||
|
11.578463,
|
||||||
|
routeModel,
|
||||||
|
mock
|
||||||
|
) // Silcherstr. 23-13
|
||||||
|
testSingleUpdate(
|
||||||
|
applicationContext,
|
||||||
|
48.186712,
|
||||||
|
11.578574,
|
||||||
|
routeModel,
|
||||||
|
mock
|
||||||
|
) // Silcherstr. 27-33
|
||||||
|
testSingleUpdate(
|
||||||
|
applicationContext,
|
||||||
|
48.186899,
|
||||||
|
11.580480,
|
||||||
|
routeModel,
|
||||||
|
mock
|
||||||
|
) // Schmalkadenerstr. 24-28
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testSingleUpdate(
|
||||||
|
applicationContext: Context,
|
||||||
|
latitude: Double,
|
||||||
|
longitude: Double,
|
||||||
|
routeModel: RouteModel,
|
||||||
|
mock: MockLocation
|
||||||
|
) {
|
||||||
|
if (1 == 1) {
|
||||||
|
mock.setMockLocation(latitude, longitude, 0F)
|
||||||
|
} else {
|
||||||
|
routeModel.updateLocation(
|
||||||
|
applicationContext,
|
||||||
|
location(longitude, latitude), navigationViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val step = routeModel.currentStep()
|
||||||
|
val nextStep = routeModel.nextStep()
|
||||||
|
Thread.sleep(1_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun gpx(context: Context, mock: MockLocation) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
var lastLocation = location(0.0, 0.0)
|
||||||
|
val parser = GPXParser()
|
||||||
|
val input = context.resources.openRawResource(R.raw.vh)
|
||||||
|
val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread
|
||||||
|
parsedGpx?.let {
|
||||||
|
val tracks = parsedGpx.tracks
|
||||||
|
tracks.forEach { tr ->
|
||||||
|
val segments: MutableList<TrackSegment?>? = tr.trackSegments
|
||||||
|
segments!!.forEach { seg ->
|
||||||
|
var lastTime = DateTime.now()
|
||||||
|
seg!!.trackPoints.forEach { p ->
|
||||||
|
val curLocation = location(p.longitude, p.latitude)
|
||||||
|
val ext = p.extensions
|
||||||
|
val speed: Double?
|
||||||
|
if (ext != null) {
|
||||||
|
speed = ext.speed
|
||||||
|
mock.curSpeed = speed.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
val duration = p.time.millis - lastTime.millis
|
||||||
|
val bearing = lastLocation.bearingTo(curLocation)
|
||||||
|
println("Bearing $bearing")
|
||||||
|
mock.setMockLocation(p.latitude, p.longitude, bearing)
|
||||||
|
if (duration > 0) {
|
||||||
|
delay(duration / 5)
|
||||||
|
}
|
||||||
|
lastTime = p.time
|
||||||
|
lastLocation = curLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SimulationType {
|
||||||
|
SIMULATE, TEST, GPX, TEST_SINGLE
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
package com.kouros.navigation.ui
|
package com.kouros.navigation.ui
|
||||||
|
|
||||||
import NavigationSheet
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.AppOpsManager
|
import android.app.AppOpsManager
|
||||||
import android.content.Context
|
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
@@ -18,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.BottomSheetScaffold
|
import androidx.compose.material3.BottomSheetScaffold
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
@@ -56,6 +55,11 @@ import com.kouros.navigation.data.StepData
|
|||||||
import com.kouros.navigation.model.BaseStyleModel
|
import com.kouros.navigation.model.BaseStyleModel
|
||||||
import com.kouros.navigation.model.MockLocation
|
import com.kouros.navigation.model.MockLocation
|
||||||
import com.kouros.navigation.model.RouteModel
|
import com.kouros.navigation.model.RouteModel
|
||||||
|
import com.kouros.navigation.model.SimulationType
|
||||||
|
import com.kouros.navigation.model.gpx
|
||||||
|
import com.kouros.navigation.model.simulate
|
||||||
|
import com.kouros.navigation.model.test
|
||||||
|
import com.kouros.navigation.model.testSingle
|
||||||
import com.kouros.navigation.ui.app.AppViewModel
|
import com.kouros.navigation.ui.app.AppViewModel
|
||||||
import com.kouros.navigation.ui.app.appViewModel
|
import com.kouros.navigation.ui.app.appViewModel
|
||||||
import com.kouros.navigation.ui.navigation.AppNavGraph
|
import com.kouros.navigation.ui.navigation.AppNavGraph
|
||||||
@@ -64,14 +68,7 @@ import com.kouros.navigation.utils.GeoUtils.snapLocation
|
|||||||
import com.kouros.navigation.utils.bearing
|
import com.kouros.navigation.utils.bearing
|
||||||
import com.kouros.navigation.utils.getSettingsViewModel
|
import com.kouros.navigation.utils.getSettingsViewModel
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
import io.ticofab.androidgpxparser.parser.GPXParser
|
|
||||||
import io.ticofab.androidgpxparser.parser.domain.Gpx
|
|
||||||
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.joda.time.DateTime
|
|
||||||
import org.maplibre.compose.camera.CameraPosition
|
import org.maplibre.compose.camera.CameraPosition
|
||||||
import org.maplibre.compose.location.DesiredAccuracy
|
import org.maplibre.compose.location.DesiredAccuracy
|
||||||
import org.maplibre.compose.location.Location
|
import org.maplibre.compose.location.Location
|
||||||
@@ -86,7 +83,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
val routeModel = RouteModel()
|
val routeModel = RouteModel()
|
||||||
var tilt = 50.0
|
var tilt = 50.0
|
||||||
val useMock = false
|
val useMock = false
|
||||||
val type = 3 // 1 simulate 2 test 3 gpx 4 testSingle
|
val type = SimulationType.GPX
|
||||||
|
|
||||||
val stepData: MutableLiveData<StepData> by lazy {
|
val stepData: MutableLiveData<StepData> by lazy {
|
||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
@@ -94,6 +91,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
val nextStepData: MutableLiveData<StepData> by lazy {
|
val nextStepData: MutableLiveData<StepData> by lazy {
|
||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastLocation = location(0.0, 0.0)
|
var lastLocation = location(0.0, 0.0)
|
||||||
val observer = Observer<String> { newRoute ->
|
val observer = Observer<String> { newRoute ->
|
||||||
if (newRoute.isNotEmpty()) {
|
if (newRoute.isNotEmpty()) {
|
||||||
@@ -101,13 +99,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
routeData.value = routeModel.curRoute.routeGeoJson
|
routeData.value = routeModel.curRoute.routeGeoJson
|
||||||
if (useMock) {
|
if (useMock) {
|
||||||
when (type) {
|
when (type) {
|
||||||
1 -> simulate()
|
SimulationType.SIMULATE -> simulate(routeModel, mock)
|
||||||
2 -> test()
|
SimulationType.TEST -> test(applicationContext, routeModel)
|
||||||
3 -> gpx(
|
SimulationType.GPX -> gpx(
|
||||||
context = applicationContext
|
context = applicationContext, mock
|
||||||
)
|
)
|
||||||
|
SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock)
|
||||||
4 -> testSingle()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,11 +184,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
val appViewModel: AppViewModel = appViewModel()
|
val appViewModel: AppViewModel = appViewModel()
|
||||||
val darkMode by appViewModel.darkMode.collectAsState()
|
val darkMode by appViewModel.darkMode.collectAsState()
|
||||||
|
|
||||||
val sheetPeekHeight = 250.dp
|
|
||||||
val baseStyle = BaseStyleModel().readStyle(applicationContext, darkMode, darkMode == 1)
|
val baseStyle = BaseStyleModel().readStyle(applicationContext, darkMode, darkMode == 1)
|
||||||
val scaffoldState = rememberBottomSheetScaffoldState()
|
val scaffoldState = rememberBottomSheetScaffoldState()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val sheetPeekHeight = 250.dp
|
||||||
val sheetPeekHeightState = remember { mutableStateOf(sheetPeekHeight) }
|
val sheetPeekHeightState = remember { mutableStateOf(sheetPeekHeight) }
|
||||||
|
|
||||||
val locationProvider = rememberDefaultLocationProvider(
|
val locationProvider = rememberDefaultLocationProvider(
|
||||||
@@ -209,7 +207,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
fun closeSheet() {
|
fun closeSheet() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
scaffoldState.bottomSheetState.partialExpand()
|
scaffoldState.bottomSheetState.partialExpand()
|
||||||
sheetPeekHeightState.value = sheetPeekHeight
|
sheetPeekHeightState.value = 50.dp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavigationTheme(useDarkTheme = darkMode == 1) {
|
NavigationTheme(useDarkTheme = darkMode == 1) {
|
||||||
@@ -217,7 +215,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
SnackbarHost(hostState = snackbarHostState)
|
SnackbarHost(hostState = snackbarHostState)
|
||||||
},
|
},
|
||||||
scaffoldState = scaffoldState,
|
scaffoldState = scaffoldState,
|
||||||
sheetPeekHeight = sheetPeekHeightState.value,
|
sheetPeekHeight = sheetPeekHeightState.value,
|
||||||
sheetContent = {
|
sheetContent = {
|
||||||
SheetContent(latitude, step, nextStep) { closeSheet() }
|
SheetContent(latitude, step, nextStep) { closeSheet() }
|
||||||
@@ -363,7 +361,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun simulateNavigation() {
|
fun simulateNavigation() {
|
||||||
simulate()
|
simulate(
|
||||||
|
routeModel = routeModel,
|
||||||
|
mock = mock
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkMockLocationEnabled() {
|
private fun checkMockLocationEnabled() {
|
||||||
@@ -385,95 +386,5 @@ class MainActivity : ComponentActivity() {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun simulate() {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
var lastLocation = location(0.0, 0.0)
|
|
||||||
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
|
|
||||||
val curLocation = location(waypoint[0], waypoint[1])
|
|
||||||
if (routeModel.isNavigating()) {
|
|
||||||
val deviation = 0.0
|
|
||||||
if (index in 0..routeModel.curRoute.waypoints.size) {
|
|
||||||
val bearing = lastLocation.bearingTo(curLocation)
|
|
||||||
mock.setMockLocation(waypoint[1], waypoint[0], bearing)
|
|
||||||
Thread.sleep(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastLocation = curLocation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun test() {
|
|
||||||
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
|
|
||||||
//if (index in 3..3) {
|
|
||||||
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
|
||||||
routeModel.updateLocation(
|
|
||||||
applicationContext,
|
|
||||||
location(waypoint[0], waypoint[1]), navigationViewModel
|
|
||||||
)
|
|
||||||
val step = routeModel.currentStep()
|
|
||||||
val nextStep = routeModel.nextStep()
|
|
||||||
println("Step: ${step.instruction} ${step.leftStepDistance} ${nextStep.currentManeuverType}")
|
|
||||||
}
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testSingle() {
|
|
||||||
testSingleUpdate(48.185976, 11.578463) // Silcherstr. 23-13
|
|
||||||
testSingleUpdate(48.186712, 11.578574) // Silcherstr. 27-33
|
|
||||||
testSingleUpdate(48.186899, 11.580480) // Schmalkadenerstr. 24-28
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testSingleUpdate(latitude: Double, longitude: Double) {
|
|
||||||
if (1 == 1) {
|
|
||||||
mock.setMockLocation(latitude, longitude, 0F)
|
|
||||||
} else {
|
|
||||||
routeModel.updateLocation(
|
|
||||||
applicationContext,
|
|
||||||
location(longitude, latitude), navigationViewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val step = routeModel.currentStep()
|
|
||||||
val nextStep = routeModel.nextStep()
|
|
||||||
Thread.sleep(1_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun gpx(context: Context) {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
var lastLocation = location(0.0, 0.0)
|
|
||||||
val parser = GPXParser()
|
|
||||||
val input = context.resources.openRawResource(R.raw.vh)
|
|
||||||
val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread
|
|
||||||
parsedGpx?.let {
|
|
||||||
val tracks = parsedGpx.tracks
|
|
||||||
tracks.forEach { tr ->
|
|
||||||
val segments: MutableList<TrackSegment?>? = tr.trackSegments
|
|
||||||
segments!!.forEach { seg ->
|
|
||||||
var lastTime = DateTime.now()
|
|
||||||
seg!!.trackPoints.forEach { p ->
|
|
||||||
val curLocation = location(p.longitude, p.latitude)
|
|
||||||
val ext = p.extensions
|
|
||||||
val speed: Double?
|
|
||||||
if (ext != null) {
|
|
||||||
speed = ext.speed
|
|
||||||
mock.curSpeed = speed.toFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
val duration = p.time.millis - lastTime.millis
|
|
||||||
val bearing = lastLocation.bearingTo(curLocation)
|
|
||||||
println("Bearing $bearing")
|
|
||||||
mock.setMockLocation(p.latitude, p.longitude, bearing)
|
|
||||||
if (duration > 0) {
|
|
||||||
delay(duration / 5)
|
|
||||||
}
|
|
||||||
lastTime = p.time
|
|
||||||
lastLocation = curLocation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
package com.kouros.navigation.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -13,9 +15,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.res.painterResource
|
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.core.graphics.drawable.IconCompat
|
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
|
|
||||||
import com.kouros.navigation.data.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
import com.kouros.navigation.model.RouteModel
|
import com.kouros.navigation.model.RouteModel
|
||||||
import com.kouros.navigation.utils.formatDateTime
|
import com.kouros.navigation.utils.formatDateTime
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ fun SearchSheet(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
) {
|
) {
|
||||||
|
// Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
|
||||||
SearchBar(
|
SearchBar(
|
||||||
textFieldState = textFieldState,
|
textFieldState = textFieldState,
|
||||||
searchPlaces = emptyList(),
|
searchPlaces = emptyList(),
|
||||||
@@ -71,7 +72,7 @@ fun SearchSheet(
|
|||||||
closeSheet = { closeSheet() }
|
closeSheet = { closeSheet() }
|
||||||
|
|
||||||
)
|
)
|
||||||
//Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
|
|
||||||
if (recentPlaces.value != null) {
|
if (recentPlaces.value != null) {
|
||||||
val items = listOf(recentPlaces)
|
val items = listOf(recentPlaces)
|
||||||
if (items.isNotEmpty()) {
|
if (items.isNotEmpty()) {
|
||||||
@@ -142,7 +143,7 @@ fun SearchBar(
|
|||||||
SearchBarDefaults.InputField(
|
SearchBarDefaults.InputField(
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.search_48px),
|
painter = painterResource(id = R.drawable.speed_camera_24px),
|
||||||
"Search",
|
"Search",
|
||||||
modifier = Modifier.size(24.dp, 24.dp),
|
modifier = Modifier.size(24.dp, 24.dp),
|
||||||
)
|
)
|
||||||
@@ -166,8 +167,8 @@ fun SearchBar(
|
|||||||
RecentPlaces(searchPlaces, viewModel, context, location, closeSheet)
|
RecentPlaces(searchPlaces, viewModel, context, location, closeSheet)
|
||||||
}
|
}
|
||||||
if (searchResults.isNotEmpty()) {
|
if (searchResults.isNotEmpty()) {
|
||||||
Text("Search places")
|
Text("Search places")
|
||||||
SearchPlaces(searchResults, viewModel, context, location, closeSheet)
|
SearchPlaces(searchResults, viewModel, context, location, closeSheet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,7 +187,7 @@ private fun SearchPlaces(
|
|||||||
) {
|
) {
|
||||||
val color = remember { PlaceColor }
|
val color = remember { PlaceColor }
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
if (searchResults.isNotEmpty()) {
|
if (searchResults.isNotEmpty()) {
|
||||||
@@ -199,7 +200,7 @@ private fun SearchPlaces(
|
|||||||
modifier = Modifier.size(24.dp, 24.dp),
|
modifier = Modifier.size(24.dp, 24.dp),
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("${place.address.road} ${place.address.postcode}") },
|
headlineContent = { Text(place.displayName) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
val pl = Place(
|
val pl = Place(
|
||||||
@@ -235,7 +236,7 @@ private fun RecentPlaces(
|
|||||||
) {
|
) {
|
||||||
val color = remember { PlaceColor }
|
val color = remember { PlaceColor }
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
items(recentPlaces, key = { it.id }) { place ->
|
items(recentPlaces, key = { it.id }) { place ->
|
||||||
|
|||||||
@@ -1,19 +1,3 @@
|
|||||||
/*
|
|
||||||
* Copyright 2023 Google LLC
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.kouros.navigation.car
|
package com.kouros.navigation.car
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
|||||||
@@ -52,16 +52,30 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import android.Manifest.permission
|
import android.Manifest.permission
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main session for Android Auto/Automotive OS navigation.
|
||||||
|
* Manages the lifecycle of the navigation session, including location updates,
|
||||||
|
* car hardware sensors, routing engine selection, and screen navigation.
|
||||||
|
* Implements NavigationScreen.Listener for handling navigation events.
|
||||||
|
*/
|
||||||
class NavigationSession : Session(), NavigationScreen.Listener {
|
class NavigationSession : Session(), NavigationScreen.Listener {
|
||||||
|
|
||||||
|
// Flag to enable/disable contact access feature
|
||||||
val useContacts = false
|
val useContacts = false
|
||||||
|
|
||||||
|
// Model for managing route state and navigation logic for Android Auto
|
||||||
lateinit var routeModel: RouteCarModel;
|
lateinit var routeModel: RouteCarModel;
|
||||||
|
|
||||||
|
// Main navigation screen displayed to the user
|
||||||
lateinit var navigationScreen: NavigationScreen
|
lateinit var navigationScreen: NavigationScreen
|
||||||
|
|
||||||
|
// Handles map surface rendering on the car display
|
||||||
lateinit var surfaceRenderer: SurfaceRenderer
|
lateinit var surfaceRenderer: SurfaceRenderer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Location listener that receives GPS updates from the device.
|
||||||
|
* Only processes location if car location hardware is not being used.
|
||||||
|
*/
|
||||||
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? ->
|
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? ->
|
||||||
val repository = getSettingsRepository(carContext)
|
val repository = getSettingsRepository(carContext)
|
||||||
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
|
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
|
||||||
@@ -70,6 +84,10 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle observer for managing session lifecycle events.
|
||||||
|
* Cleans up resources when the session is destroyed.
|
||||||
|
*/
|
||||||
private val mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
private val mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
||||||
override fun onCreate(owner: LifecycleOwner) {
|
override fun onCreate(owner: LifecycleOwner) {
|
||||||
}
|
}
|
||||||
@@ -92,9 +110,16 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ViewModel for navigation data and business logic
|
||||||
lateinit var navigationViewModel: NavigationViewModel
|
lateinit var navigationViewModel: NavigationViewModel
|
||||||
|
|
||||||
|
// Store for ViewModels to survive configuration changes
|
||||||
lateinit var viewModelStoreOwner : ViewModelStoreOwner
|
lateinit var viewModelStoreOwner : ViewModelStoreOwner
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for car hardware location updates.
|
||||||
|
* Receives location data from the car's GPS system.
|
||||||
|
*/
|
||||||
val carLocationListener: OnCarDataAvailableListener<CarHardwareLocation?> =
|
val carLocationListener: OnCarDataAvailableListener<CarHardwareLocation?> =
|
||||||
OnCarDataAvailableListener { data ->
|
OnCarDataAvailableListener { data ->
|
||||||
if (data.location.status == CarValue.STATUS_SUCCESS) {
|
if (data.location.status == CarValue.STATUS_SUCCESS) {
|
||||||
@@ -105,6 +130,10 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for car compass/orientation sensor.
|
||||||
|
* Updates the surface renderer with car orientation for map rotation.
|
||||||
|
*/
|
||||||
val carCompassListener: OnCarDataAvailableListener<Compass?> =
|
val carCompassListener: OnCarDataAvailableListener<Compass?> =
|
||||||
OnCarDataAvailableListener { data ->
|
OnCarDataAvailableListener { data ->
|
||||||
if (data.orientations.status == CarValue.STATUS_SUCCESS) {
|
if (data.orientations.status == CarValue.STATUS_SUCCESS) {
|
||||||
@@ -115,17 +144,26 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for car speed sensor updates.
|
||||||
|
* Receives speed in meters per second from car hardware.
|
||||||
|
*/
|
||||||
val carSpeedListener = OnCarDataAvailableListener<Speed> { data ->
|
val carSpeedListener = OnCarDataAvailableListener<Speed> { data ->
|
||||||
if (data.displaySpeedMetersPerSecond.status == CarValue.STATUS_SUCCESS) {
|
if (data.displaySpeedMetersPerSecond.status == CarValue.STATUS_SUCCESS) {
|
||||||
val speed = data.displaySpeedMetersPerSecond.value
|
val speed = data.displaySpeedMetersPerSecond.value
|
||||||
surfaceRenderer.updateCarSpeed(speed!!)
|
surfaceRenderer.updateCarSpeed(speed!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val lifecycle: Lifecycle = lifecycle
|
val lifecycle: Lifecycle = lifecycle
|
||||||
lifecycle.addObserver(mLifeCycleObserver)
|
lifecycle.addObserver(mLifeCycleObserver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when routing engine preference changes.
|
||||||
|
* Creates appropriate repository based on user selection.
|
||||||
|
*/
|
||||||
fun onRoutingEngineStateUpdated(routeEngine : Int) {
|
fun onRoutingEngineStateUpdated(routeEngine : Int) {
|
||||||
navigationViewModel = when (routeEngine) {
|
navigationViewModel = when (routeEngine) {
|
||||||
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
|
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
|
||||||
@@ -134,10 +172,19 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when location permission is granted.
|
||||||
|
* Initializes car hardware sensors if available.
|
||||||
|
*/
|
||||||
fun onPermissionGranted(permission : Boolean) {
|
fun onPermissionGranted(permission : Boolean) {
|
||||||
addSensors(routeModel.navState.carConnection)
|
addSensors(routeModel.navState.carConnection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when car connection state changes.
|
||||||
|
* Handles different connection types: Not Connected, Automotive OS Native, Android Auto Projection.
|
||||||
|
* Requests appropriate car speed permissions based on connection type.
|
||||||
|
*/
|
||||||
fun onConnectionStateUpdated(connectionState: Int) {
|
fun onConnectionStateUpdated(connectionState: Int) {
|
||||||
routeModel.navState = routeModel.navState.copy(carConnection = connectionState)
|
routeModel.navState = routeModel.navState.copy(carConnection = connectionState)
|
||||||
when (connectionState) {
|
when (connectionState) {
|
||||||
@@ -153,8 +200,14 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the initial screen for the session.
|
||||||
|
* Sets up ViewModel store, initializes components, checks permissions,
|
||||||
|
* and returns appropriate starting screen.
|
||||||
|
*/
|
||||||
override fun onCreateScreen(intent: Intent): Screen {
|
override fun onCreateScreen(intent: Intent): Screen {
|
||||||
|
|
||||||
|
// Create ViewModelStoreOwner to manage ViewModels across lifecycle
|
||||||
viewModelStoreOwner = object : ViewModelStoreOwner {
|
viewModelStoreOwner = object : ViewModelStoreOwner {
|
||||||
override val viewModelStore = ViewModelStore()
|
override val viewModelStore = ViewModelStore()
|
||||||
}
|
}
|
||||||
@@ -166,6 +219,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize ViewModel with saved routing engine preference
|
||||||
navigationViewModel = getViewModel(carContext)
|
navigationViewModel = getViewModel(carContext)
|
||||||
|
|
||||||
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
|
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
|
||||||
@@ -174,13 +228,17 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
|
|
||||||
routeModel = RouteCarModel()
|
routeModel = RouteCarModel()
|
||||||
|
|
||||||
|
// Monitor car connection state
|
||||||
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
|
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
|
||||||
|
|
||||||
|
// Initialize surface renderer for map display
|
||||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
|
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
|
||||||
|
|
||||||
|
// Create main navigation screen
|
||||||
navigationScreen =
|
navigationScreen =
|
||||||
NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel)
|
NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel)
|
||||||
|
|
||||||
|
// Check for required permissions before starting
|
||||||
if ( carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
|
if ( carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
== PackageManager.PERMISSION_GRANTED
|
== PackageManager.PERMISSION_GRANTED
|
||||||
&& !useContacts
|
&& !useContacts
|
||||||
@@ -207,6 +265,11 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
return navigationScreen
|
return navigationScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers listeners for car hardware sensors.
|
||||||
|
* Only adds location and compass sensors if useCarLocation setting is enabled.
|
||||||
|
* Speed sensor is added for both native and projection connections.
|
||||||
|
*/
|
||||||
fun addSensors(connectionState: Int) {
|
fun addSensors(connectionState: Int) {
|
||||||
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
|
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
|
||||||
val repository = getSettingsRepository(carContext)
|
val repository = getSettingsRepository(carContext)
|
||||||
@@ -228,6 +291,10 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters all car hardware sensor listeners.
|
||||||
|
* Called when session is being destroyed to prevent memory leaks.
|
||||||
|
*/
|
||||||
fun removeSensors() {
|
fun removeSensors() {
|
||||||
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
|
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
|
||||||
val repository = getSettingsRepository(carContext)
|
val repository = getSettingsRepository(carContext)
|
||||||
@@ -242,6 +309,10 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles new intents, primarily for navigation deep links from other apps.
|
||||||
|
* Supports ACTION_NAVIGATE for starting navigation to a specific location.
|
||||||
|
*/
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
val screenManager = carContext.getCarService(ScreenManager::class.java)
|
val screenManager = carContext.getCarService(ScreenManager::class.java)
|
||||||
if ((CarContext.ACTION_NAVIGATE == intent.action)) {
|
if ((CarContext.ACTION_NAVIGATE == intent.action)) {
|
||||||
@@ -278,11 +349,18 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when car configuration changes (e.g., day/night mode).
|
||||||
|
*/
|
||||||
override fun onCarConfigurationChanged(newConfiguration: Configuration) {
|
override fun onCarConfigurationChanged(newConfiguration: Configuration) {
|
||||||
println("Configuration: ${newConfiguration.isNightModeActive}")
|
println("Configuration: ${newConfiguration.isNightModeActive}")
|
||||||
super.onCarConfigurationChanged(newConfiguration)
|
super.onCarConfigurationChanged(newConfiguration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests GPS location updates from the device.
|
||||||
|
* Updates with last known location and starts listening for updates every 500ms or 5 meters.
|
||||||
|
*/
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun requestLocationUpdates() {
|
fun requestLocationUpdates() {
|
||||||
val locationManager =
|
val locationManager =
|
||||||
@@ -300,6 +378,12 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates navigation state with new location.
|
||||||
|
* Handles route snapping, deviation detection for rerouting, and map updates.
|
||||||
|
* Snaps location to nearest point on route if within threshold.
|
||||||
|
* Triggers reroute calculation if deviated too far from route.
|
||||||
|
*/
|
||||||
fun updateLocation(location: Location) {
|
fun updateLocation(location: Location) {
|
||||||
if (location.hasBearing()) {
|
if (location.hasBearing()) {
|
||||||
routeModel.navState = routeModel.navState.copy(routeBearing = location.bearing)
|
routeModel.navState = routeModel.navState.copy(routeBearing = location.bearing)
|
||||||
@@ -309,10 +393,12 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
if (!routeModel.navState.arrived) {
|
if (!routeModel.navState.arrived) {
|
||||||
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
|
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
|
||||||
val distance = location.distanceTo(snapedLocation)
|
val distance = location.distanceTo(snapedLocation)
|
||||||
|
// Check if user has deviated too far from route
|
||||||
if (distance > MAXIMAL_ROUTE_DEVIATION) {
|
if (distance > MAXIMAL_ROUTE_DEVIATION) {
|
||||||
navigationScreen.calculateNewRoute(routeModel.navState.destination)
|
navigationScreen.calculateNewRoute(routeModel.navState.destination)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Snap to route if close enough, otherwise use raw location
|
||||||
if (distance < MAXIMAL_SNAP_CORRECTION) {
|
if (distance < MAXIMAL_SNAP_CORRECTION) {
|
||||||
surfaceRenderer.updateLocation(snapedLocation)
|
surfaceRenderer.updateLocation(snapedLocation)
|
||||||
} else {
|
} else {
|
||||||
@@ -324,14 +410,20 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops active navigation and clears route state.
|
||||||
|
* Called when user exits navigation or arrives at destination.
|
||||||
|
*/
|
||||||
override fun stopNavigation(context: CarContext) {
|
override fun stopNavigation(context: CarContext) {
|
||||||
routeModel.stopNavigation(context)
|
routeModel.stopNavigation(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
// URI host for deep linking
|
||||||
var uriHost: String = "navigation"
|
var uriHost: String = "navigation"
|
||||||
|
|
||||||
|
// URI scheme for deep linking
|
||||||
var uriScheme: String = "samples"
|
var uriScheme: String = "samples"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,53 +51,100 @@ import org.maplibre.compose.style.BaseStyle
|
|||||||
import org.maplibre.spatialk.geojson.Position
|
import org.maplibre.spatialk.geojson.Position
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles map rendering for Android Auto using a virtual display.
|
||||||
|
* Creates a VirtualDisplay to render Compose UI onto the car's surface.
|
||||||
|
* Manages camera position, zoom, tilt, and navigation state for the map view.
|
||||||
|
*/
|
||||||
class SurfaceRenderer(
|
class SurfaceRenderer(
|
||||||
private var carContext: CarContext, lifecycle: Lifecycle,
|
private var carContext: CarContext, lifecycle: Lifecycle,
|
||||||
private var routeModel: RouteCarModel,
|
private var routeModel: RouteCarModel,
|
||||||
private var viewModelStoreOwner: ViewModelStoreOwner
|
private var viewModelStoreOwner: ViewModelStoreOwner
|
||||||
) : DefaultLifecycleObserver {
|
) : DefaultLifecycleObserver {
|
||||||
|
|
||||||
|
// Last known location for bearing calculations
|
||||||
var lastLocation = location(0.0, 0.0)
|
var lastLocation = location(0.0, 0.0)
|
||||||
|
|
||||||
|
// Car orientation sensor value (999F means no valid orientation)
|
||||||
var carOrientation = 999F
|
var carOrientation = 999F
|
||||||
|
|
||||||
|
// Current camera position state for the map
|
||||||
private val cameraPosition = MutableLiveData(
|
private val cameraPosition = MutableLiveData(
|
||||||
CameraPosition(
|
CameraPosition(
|
||||||
zoom = 15.0,
|
zoom = 15.0,
|
||||||
target = Position(latitude = homeVogelhart.latitude, longitude = homeVogelhart.longitude)
|
target = Position(latitude = homeVogelhart.latitude, longitude = homeVogelhart.longitude)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Visible area of the map surface (can change based on UI elements)
|
||||||
private var visibleArea = MutableLiveData(
|
private var visibleArea = MutableLiveData(
|
||||||
Rect(0, 0, 0, 0)
|
Rect(0, 0, 0, 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Stable area that won't change during scrolling
|
||||||
var stableArea = Rect()
|
var stableArea = Rect()
|
||||||
|
|
||||||
|
// Surface dimensions
|
||||||
var width = 0
|
var width = 0
|
||||||
var height = 0
|
var height = 0
|
||||||
|
|
||||||
|
// Last bearing for smooth transitions
|
||||||
var lastBearing = 0.0
|
var lastBearing = 0.0
|
||||||
|
|
||||||
|
// LiveData for route GeoJSON data
|
||||||
val routeData = MutableLiveData("")
|
val routeData = MutableLiveData("")
|
||||||
|
|
||||||
|
// Traffic incident data (incident ID to GeoJSON mapping)
|
||||||
val trafficData = MutableLiveData(emptyMap<String, String>())
|
val trafficData = MutableLiveData(emptyMap<String, String>())
|
||||||
|
|
||||||
|
// Speed camera locations as GeoJSON
|
||||||
val speedCamerasData = MutableLiveData("")
|
val speedCamerasData = MutableLiveData("")
|
||||||
|
|
||||||
|
// Current speed in km/h
|
||||||
val speed = MutableLiveData(0F)
|
val speed = MutableLiveData(0F)
|
||||||
|
|
||||||
|
// Speed limit for current road
|
||||||
val maxSpeed = MutableLiveData(0)
|
val maxSpeed = MutableLiveData(0)
|
||||||
|
|
||||||
|
// Current view mode (navigation, preview, etc.)
|
||||||
var viewStyle = ViewStyle.VIEW
|
var viewStyle = ViewStyle.VIEW
|
||||||
|
|
||||||
|
// Center location for route preview
|
||||||
lateinit var centerLocation: Location
|
lateinit var centerLocation: Location
|
||||||
|
|
||||||
|
// Route distance for calculating preview zoom
|
||||||
var previewDistance = 0.0
|
var previewDistance = 0.0
|
||||||
|
|
||||||
|
// Compose view for rendering the map
|
||||||
lateinit var mapView: ComposeView
|
lateinit var mapView: ComposeView
|
||||||
|
|
||||||
|
// Camera tilt angle (default 55 degrees for navigation)
|
||||||
var tilt = 55.0
|
var tilt = 55.0
|
||||||
|
|
||||||
|
// Map base style (day/night)
|
||||||
val style: MutableLiveData<BaseStyle> by lazy {
|
val style: MutableLiveData<BaseStyle> by lazy {
|
||||||
MutableLiveData()
|
MutableLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SurfaceCallback implementation for handling the Android Auto surface lifecycle.
|
||||||
|
* Creates and manages the VirtualDisplay and Presentation for rendering Compose content.
|
||||||
|
*/
|
||||||
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
|
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
|
||||||
|
|
||||||
|
// Custom lifecycle owner for the virtual display
|
||||||
lateinit var lifecycleOwner: CustomLifecycleOwner
|
lateinit var lifecycleOwner: CustomLifecycleOwner
|
||||||
|
|
||||||
|
// Virtual display for rendering the map
|
||||||
lateinit var virtualDisplay: VirtualDisplay
|
lateinit var virtualDisplay: VirtualDisplay
|
||||||
|
|
||||||
|
// Presentation that hosts the Compose view
|
||||||
lateinit var presentation: Presentation
|
lateinit var presentation: Presentation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the surface becomes available.
|
||||||
|
* Creates VirtualDisplay, initializes lifecycle, and sets up Compose rendering.
|
||||||
|
*/
|
||||||
override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
|
override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
|
||||||
synchronized(this@SurfaceRenderer) {
|
synchronized(this@SurfaceRenderer) {
|
||||||
Log.i(TAG, "Surface available $surfaceContainer")
|
Log.i(TAG, "Surface available $surfaceContainer")
|
||||||
@@ -135,18 +182,29 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the visible area changes (e.g., due to UI elements appearing).
|
||||||
|
*/
|
||||||
override fun onVisibleAreaChanged(newVisibleArea: Rect) {
|
override fun onVisibleAreaChanged(newVisibleArea: Rect) {
|
||||||
synchronized(this@SurfaceRenderer) {
|
synchronized(this@SurfaceRenderer) {
|
||||||
visibleArea.value = newVisibleArea
|
visibleArea.value = newVisibleArea
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the stable area changes.
|
||||||
|
* Stable area is guaranteed not to change during scroll events.
|
||||||
|
*/
|
||||||
override fun onStableAreaChanged(newStableArea: Rect) {
|
override fun onStableAreaChanged(newStableArea: Rect) {
|
||||||
synchronized(this@SurfaceRenderer) {
|
synchronized(this@SurfaceRenderer) {
|
||||||
stableArea = newStableArea
|
stableArea = newStableArea
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the surface is being destroyed.
|
||||||
|
* Cleans up resources and notifies lifecycle owner.
|
||||||
|
*/
|
||||||
override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
|
override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
|
||||||
synchronized(this@SurfaceRenderer) {
|
synchronized(this@SurfaceRenderer) {
|
||||||
Log.i(TAG, "SurfaceRenderer destroyed")
|
Log.i(TAG, "SurfaceRenderer destroyed")
|
||||||
@@ -159,11 +217,17 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when user scrolls the map (not currently implemented).
|
||||||
|
*/
|
||||||
override fun onScroll(distanceX: Float, distanceY: Float) {
|
override fun onScroll(distanceX: Float, distanceY: Float) {
|
||||||
synchronized(this@SurfaceRenderer) {
|
synchronized(this@SurfaceRenderer) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when user scales (zooms) the map (not currently implemented).
|
||||||
|
*/
|
||||||
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
|
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -179,6 +243,10 @@ class SurfaceRenderer(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable function that renders the map and navigation UI.
|
||||||
|
* Observes various LiveData sources and updates the map accordingly.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun MapView() {
|
fun MapView() {
|
||||||
|
|
||||||
@@ -204,6 +272,10 @@ class SurfaceRenderer(
|
|||||||
ShowPosition(cameraState, position, paddingValues)
|
ShowPosition(cameraState, position, paddingValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable that handles camera animations and navigation overlays.
|
||||||
|
* Displays speed indicator and navigation images during active navigation.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ShowPosition(
|
fun ShowPosition(
|
||||||
cameraState: CameraState,
|
cameraState: CameraState,
|
||||||
@@ -244,7 +316,10 @@ class SurfaceRenderer(
|
|||||||
.setSurfaceCallback(mSurfaceCallback)
|
.setSurfaceCallback(mSurfaceCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Handles the map zoom-in and zoom-out events. */
|
/**
|
||||||
|
* Handles the map zoom-in and zoom-out events.
|
||||||
|
* Switches to PAN_VIEW mode and updates camera zoom level.
|
||||||
|
*/
|
||||||
fun handleScale(zoomSign: Int) {
|
fun handleScale(zoomSign: Int) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (viewStyle == ViewStyle.VIEW) {
|
if (viewStyle == ViewStyle.VIEW) {
|
||||||
@@ -264,6 +339,11 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the camera position based on current location.
|
||||||
|
* Calculates appropriate bearing, zoom, and maintains view style.
|
||||||
|
* Uses car orientation sensor if available, otherwise falls back to location bearing.
|
||||||
|
*/
|
||||||
fun updateLocation(location: Location) {
|
fun updateLocation(location: Location) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
||||||
@@ -296,6 +376,10 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates camera position with new bearing, zoom, and target.
|
||||||
|
* Posts update to LiveData for UI observation.
|
||||||
|
*/
|
||||||
private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) {
|
private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
cameraPosition.postValue(
|
cameraPosition.postValue(
|
||||||
@@ -310,15 +394,25 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets route data for active navigation and switches to VIEW mode.
|
||||||
|
*/
|
||||||
fun setRouteData() {
|
fun setRouteData() {
|
||||||
routeData.value = routeModel.curRoute.routeGeoJson
|
routeData.value = routeModel.curRoute.routeGeoJson
|
||||||
viewStyle = ViewStyle.VIEW
|
viewStyle = ViewStyle.VIEW
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates traffic incident data on the map.
|
||||||
|
*/
|
||||||
fun setTrafficData(traffic: Map<String, String> ) {
|
fun setTrafficData(traffic: Map<String, String> ) {
|
||||||
trafficData.value = traffic as MutableMap<String, String>?
|
trafficData.value = traffic as MutableMap<String, String>?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up route preview mode with overview camera position.
|
||||||
|
* Calculates appropriate zoom based on route distance.
|
||||||
|
*/
|
||||||
fun setPreviewRouteData(routeModel: RouteModel) {
|
fun setPreviewRouteData(routeModel: RouteModel) {
|
||||||
viewStyle = ViewStyle.PREVIEW
|
viewStyle = ViewStyle.PREVIEW
|
||||||
with(routeModel) {
|
with(routeModel) {
|
||||||
@@ -333,6 +427,9 @@ class SurfaceRenderer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a specific location (e.g., amenity/POI) on the map.
|
||||||
|
*/
|
||||||
fun setCategories(location: Location, route: String) {
|
fun setCategories(location: Location, route: String) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
viewStyle = ViewStyle.AMENITY_VIEW
|
viewStyle = ViewStyle.AMENITY_VIEW
|
||||||
@@ -345,6 +442,10 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates car location from the connected car system.
|
||||||
|
* Only updates location when using OSRM routing engine.
|
||||||
|
*/
|
||||||
fun updateCarLocation(location: Location) {
|
fun updateCarLocation(location: Location) {
|
||||||
val repository = getSettingsRepository(carContext)
|
val repository = getSettingsRepository(carContext)
|
||||||
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
||||||
@@ -353,10 +454,16 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates current speed for display.
|
||||||
|
*/
|
||||||
fun updateCarSpeed(newSpeed: Float) {
|
fun updateCarSpeed(newSpeed: Float) {
|
||||||
speed.value = newSpeed
|
speed.value = newSpeed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centers the map on a specific category/POI location.
|
||||||
|
*/
|
||||||
fun setCategoryLocation(location: Location, category: String) {
|
fun setCategoryLocation(location: Location, category: String) {
|
||||||
viewStyle = ViewStyle.AMENITY_VIEW
|
viewStyle = ViewStyle.AMENITY_VIEW
|
||||||
cameraPosition.postValue(
|
cameraPosition.postValue(
|
||||||
@@ -373,7 +480,14 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum representing different map view modes.
|
||||||
|
* - VIEW: Active navigation mode with follow-car camera
|
||||||
|
* - PREVIEW: Route overview before starting navigation
|
||||||
|
* - PAN_VIEW: User-controlled map panning
|
||||||
|
* - AMENITY_VIEW: Displaying POI/amenity locations
|
||||||
|
*/
|
||||||
enum class ViewStyle {
|
enum class ViewStyle {
|
||||||
VIEW, PREVIEW, PAN_VIEW, AMENITY_VIEW
|
VIEW, PREVIEW, PAN_VIEW, AMENITY_VIEW
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ class SearchScreen(
|
|||||||
.setNoItemsMessage("No search results to show")
|
.setNoItemsMessage("No search results to show")
|
||||||
if (!isSearchComplete) {
|
if (!isSearchComplete) {
|
||||||
categories.forEach {
|
categories.forEach {
|
||||||
it.name
|
|
||||||
itemListBuilder.addItem(
|
itemListBuilder.addItem(
|
||||||
Row.Builder()
|
Row.Builder()
|
||||||
.setTitle(it.name)
|
.setTitle(it.name)
|
||||||
|
|||||||
@@ -34,6 +34,6 @@ class OsrmRepository : NavigationRepository() {
|
|||||||
location: Location,
|
location: Location,
|
||||||
carOrientation: Float
|
carOrientation: Float
|
||||||
): String {
|
): String {
|
||||||
TODO("Not yet implemented")
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,6 +53,6 @@ class ValhallaRepository : NavigationRepository() {
|
|||||||
location: Location,
|
location: Location,
|
||||||
carOrientation: Float
|
carOrientation: Float
|
||||||
): String {
|
): String {
|
||||||
TODO("Not yet implemented")
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,10 +250,12 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
currentLocation,
|
currentLocation,
|
||||||
carOrientation
|
carOrientation
|
||||||
)
|
)
|
||||||
val trafficData = rebuildTraffic(data)
|
if (data.isNotEmpty()) {
|
||||||
traffic.postValue(
|
val trafficData = rebuildTraffic(data)
|
||||||
trafficData
|
traffic.postValue(
|
||||||
)
|
trafficData
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user