App DrawItem and Search

This commit is contained in:
Dimitris
2025-12-09 15:52:27 +01:00
parent f70ca6e8fe
commit aeca6ff237
25 changed files with 868 additions and 654 deletions

View File

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

View File

@@ -6,6 +6,7 @@ import android.os.SystemClock
class MockLocation (private var locationManager: LocationManager) { class MockLocation (private var locationManager: LocationManager) {
var curSpeed = 0F
fun setMockLocation(latitude: Double, longitude: Double) { fun setMockLocation(latitude: Double, longitude: Double) {
try { try {
// Set mock location for all providers // Set mock location for all providers
@@ -48,7 +49,7 @@ class MockLocation (private var locationManager: LocationManager) {
this.longitude = longitude this.longitude = longitude
this.altitude = 0.0 this.altitude = 0.0
this.accuracy = 1.0f this.accuracy = 1.0f
this.speed = 0F this.speed = 0f
this.time = System.currentTimeMillis() this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
@@ -56,7 +57,6 @@ class MockLocation (private var locationManager: LocationManager) {
this.verticalAccuracyMeters = 0.0f this.verticalAccuracyMeters = 0.0f
this.speedAccuracyMetersPerSecond = 0.0f this.speedAccuracyMetersPerSecond = 0.0f
} }
// Set the mock location // Set the mock location
locationManager.setTestProviderLocation(provider, mockLocation) locationManager.setTestProviderLocation(provider, mockLocation)
@@ -72,6 +72,7 @@ class MockLocation (private var locationManager: LocationManager) {
this.longitude = longitude this.longitude = longitude
this.altitude = 0.0 this.altitude = 0.0
this.accuracy = 1.0f this.accuracy = 1.0f
this.speed = 0f
this.time = System.currentTimeMillis() this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
@@ -79,7 +80,6 @@ class MockLocation (private var locationManager: LocationManager) {
this.verticalAccuracyMeters = 0.0f this.verticalAccuracyMeters = 0.0f
this.speedAccuracyMetersPerSecond = 0.0f this.speedAccuracyMetersPerSecond = 0.0f
} }
locationManager.setTestProviderLocation(provider, mockLocation) locationManager.setTestProviderLocation(provider, mockLocation)
} catch (ex: Exception) { } catch (ex: Exception) {
ex.printStackTrace() ex.printStackTrace()

View File

@@ -1,10 +1,9 @@
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.pm.PackageManager
import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.os.Bundle import android.os.Bundle
import android.os.Process import android.os.Process
@@ -12,100 +11,93 @@ import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.google.accompanist.permissions.ExperimentalPermissionsApi import androidx.window.layout.WindowMetricsCalculator
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices import com.google.android.gms.location.LocationServices
import com.kouros.android.cars.carappservice.R import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.MainApplication import com.kouros.navigation.car.map.BuildingLayer
import com.kouros.navigation.car.BuildingLayer import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.car.PuckState import com.kouros.navigation.car.map.RouteLayer
import com.kouros.navigation.car.RouteLayer
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.nominatim.SearchResult
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.ViewModel import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.ui.theme.NavigationTheme import com.kouros.navigation.ui.theme.NavigationTheme
import com.kouros.navigation.utils.NavigationUtils import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateZoom import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.location
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.DesiredAccuracy import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.LocationTrackingEffect import org.maplibre.compose.location.LocationTrackingEffect
import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.location.rememberDefaultLocationProvider import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
import org.maplibre.compose.sources.getBaseSource import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val LOCATION_PERMISSION_REQUEST_CODE = 1001
private val CONTACTS_PERMISSION_REQUEST_CODE = 1002
val routeData = MutableLiveData("") val routeData = MutableLiveData("")
val vieModel = ViewModel(NavigationRepository()) val viewModel = ViewModel(NavigationRepository())
val routeModel = RouteModel() val routeModel = RouteModel()
var tilt = 50.0 var tilt = 50.0
val useMock = true
val instruction: MutableLiveData<StepData> by lazy { val instruction: MutableLiveData<StepData> by lazy {
MutableLiveData<StepData>() MutableLiveData<StepData>()
} }
var lastLocation = Location(LocationManager.GPS_PROVIDER) var lastLocation = location(0.0, 0.0)
val observer = Observer<String> { newRoute -> val observer = Observer<String> { newRoute ->
routeModel.startNavigation(newRoute) routeModel.startNavigation(newRoute)
routeData.value = routeModel.route.routeGeoJson routeData.value = routeModel.route.routeGeoJson
println("Start simulating $newRoute")
simulate()
} }
val cameraPosition = MutableLiveData( val cameraPosition = MutableLiveData(
CameraPosition( CameraPosition(
zoom = 15.0, zoom = 15.0,
@@ -113,153 +105,93 @@ class MainActivity : ComponentActivity() {
) )
) )
private lateinit var locationManager: LocationManager private lateinit var locationManager: LocationManager
private lateinit var fusedLocationClient: FusedLocationProviderClient private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var mock: MockLocation private lateinit var mock: MockLocation
private var loadRecentPlaces = false
init { init {
vieModel.route.observe(this, observer) viewModel.route.observe(this, observer)
} }
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
checkLocationPermissions() if (useMock) {
checkMockLocationEnabled()
if (MainApplication.useContacts) {
checkContactsPermissions()
} }
// checkMockLocationEnabled()
enableEdgeToEdge()
setContent {
if ((checkPermissionForLocation() && !MainApplication.useContacts)
|| (checkPermissionForLocation() && MainApplication.useContacts && checkPermissionForContact())) {
Content()
} else {
}
}
}
@Composable
fun Content() {
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
mock = MockLocation(locationManager) if (useMock) {
mock.setMockLocation( mock = MockLocation(locationManager)
Constants.homeLocation.latitude, mock.setMockLocation(
Constants.homeLocation.longitude Constants.homeLocation.latitude,
) Constants.homeLocation.longitude
)
}
enableEdgeToEdge()
setContent {
CheckPermissionScreen()
}
}
val scope = rememberCoroutineScope()
@SuppressLint("AutoboxingStateCreation")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Content() {
val scaffoldState = rememberBottomSheetScaffoldState()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
var simulationText by remember { mutableStateOf("Start Simulation") } val locationProvider = rememberDefaultLocationProvider(
updateInterval = 0.5.seconds,
desiredAccuracy = DesiredAccuracy.Highest
)
val userLocationState = rememberUserLocationState(locationProvider)
val locationState = locationProvider.location.collectAsState()
updateLocation(locationState.value)
var latitude by remember { mutableDoubleStateOf(0.0) }
if (locationState.value != null) {
latitude = locationState.value!!.position.latitude
}
val step: StepData? by instruction.observeAsState()
NavigationTheme { NavigationTheme {
ModalNavigationDrawer( BottomSheetScaffold(
drawerContent = { snackbarHost = {
ModalDrawerSheet { SnackbarHost(hostState = snackbarHostState)
Text("Drawer title", modifier = Modifier.Companion.padding(16.dp))
HorizontalDivider()
NavigationDrawerItem(
label = { Text(text = "Drawer Item") },
selected = false,
onClick = { /*TODO*/ }
)
}
}, },
gesturesEnabled = false scaffoldState = scaffoldState,
) { sheetPeekHeight = 128.dp,
Scaffold( sheetContent = {
modifier = Modifier.fillMaxSize(), SheetContent(latitude, step)
snackbarHost = { },
SnackbarHost(hostState = snackbarHostState) ) { innerPadding ->
}, Box(
floatingActionButton = { modifier = Modifier
ExtendedFloatingActionButton( .fillMaxSize()
text = { .padding(innerPadding),
Text(simulationText) contentAlignment = Alignment.Center,
}, ) {
icon = { SegmentedButtonDefaults.Icon(true) }, Map(userLocationState, step)
onClick = {
scope.launch {
snackbarHostState.showSnackbar("Starte Navigation")
}
if (!routeModel.isNavigating()) {
tilt = 60.0
vieModel.loadRoute(
applicationContext,
lastLocation,
Constants.home2Location
)
simulationText = "Stop Simulation"
} else {
tilt = 0.0
routeModel.stopNavigation()
routeData.value = ""
println("stopNavigation")
simulationText = "Start Simulation"
}
}
)
}
) { innerPadding ->
Column(modifier = Modifier.Companion.padding(innerPadding)) {
//CheckPermission()
Map()
}
} }
} }
} }
} }
@SuppressLint("PermissionLaunchedDuringComposition")
@OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
fun CheckPermission() { fun SheetContent(locationState: Double, step: StepData?) {
var rationaleState by remember { if (!routeModel.isNavigating()) {
mutableStateOf<RationaleState?>(null) SearchSheet(applicationContext, viewModel, lastLocation)
} } else {
NavigationSheet( routeModel, step, { simulate() })
@OptIn(ExperimentalPermissionsApi::class)
val fineLocationPermissionState = rememberMultiplePermissionsState(
listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
//Manifest.permission.READ_CONTACTS,
),
)
if (fineLocationPermissionState.allPermissionsGranted) {
Map()
}
// Show rationale dialog when needed
rationaleState?.run { PermissionRationaleDialog(rationaleState = this) }
PermissionRequestButton(
isGranted = fineLocationPermissionState.allPermissionsGranted,
title = "Precise location access",
) {
if (fineLocationPermissionState.shouldShowRationale) {
rationaleState = RationaleState(
"Request Precise Location",
"In order to use this feature please grant access by accepting " + "the location permission dialog." + "\n\nWould you like to continue?",
) { proceed ->
if (proceed) {
fineLocationPermissionState.launchMultiplePermissionRequest()
}
rationaleState = null
}
} else {
fineLocationPermissionState.launchMultiplePermissionRequest()
}
} }
// to recomposite SheetContent !
Text("State $locationState")
} }
@Composable @Composable
fun NavigationInfo(step: StepData?) { fun NavigationInfo(step: StepData?) {
Card { Card {
@@ -269,7 +201,6 @@ class MainActivity : ComponentActivity() {
contentDescription = stringResource(id = R.string.accept_action_title) contentDescription = stringResource(id = R.string.accept_action_title)
) )
if (step != null) { if (step != null) {
Text(text = step.bearing.toString(), fontSize = 25.sp)
Text(text = step.instruction, fontSize = 25.sp) Text(text = step.instruction, fontSize = 25.sp)
} }
} }
@@ -277,32 +208,24 @@ class MainActivity : ComponentActivity() {
} }
@Composable @Composable
fun Map() { fun Map(userLocationState: UserLocationState, step: StepData?) {
val step: StepData? by instruction.observeAsState()
Column { Column {
//SimpleSearchBar()
if (step != null) { if (step != null) {
NavigationInfo(step) NavigationInfo(step)
} }
MapView() MapView(userLocationState)
} }
} }
@Composable @Composable
fun MapView() { fun MapView(userLocationState: UserLocationState) {
val locationProvider = rememberDefaultLocationProvider(
updateInterval = 0.5.seconds,
desiredAccuracy = DesiredAccuracy.Highest
)
val userLocationState = rememberUserLocationState(locationProvider)
val locationState = locationProvider.location.collectAsState()
updateLocation(locationState.value)
if (locationState.value != null && lastLocation.latitude == 0.0) {
lastLocation.latitude = locationState.value?.position!!.latitude
lastLocation.longitude = locationState.value?.position!!.longitude
}
val position: CameraPosition? by cameraPosition.observeAsState()
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this)
val width = metrics.bounds.width()
val height = metrics.bounds.height()
val paddingValues = PaddingValues(start = 0.dp, top = 350.dp)
val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState() val route: String? by routeData.observeAsState()
val cameraState = val cameraState =
rememberCameraState( rememberCameraState(
@@ -315,124 +238,71 @@ class MainActivity : ComponentActivity() {
zoom = 15.0, zoom = 15.0,
) )
) )
MaplibreMap( Box (contentAlignment = Alignment.Center) {
cameraState = cameraState, MaplibreMap(
baseStyle = BaseStyle.Uri(Constants.STYLE), options = MapOptions(
) { ornamentOptions =
getBaseSource(id = "openmaptiles")?.let { tiles -> OrnamentOptions(isScaleBarEnabled = false)
if (!NavigationUtils.getBooleanKeyValue(
context = applicationContext,
Constants.SHOW_THREED_BUILDING
)
) {
BuildingLayer(tiles)
}
RouteLayer(route, "")
}
val location = Location(LocationManager.GPS_PROVIDER)
if (userLocationState.location != null) {
location.longitude = userLocationState.location!!.position.longitude
location.latitude = userLocationState.location!!.position.latitude
PuckState(cameraState, userLocationState)
}
}
LocationTrackingEffect(
locationState = userLocationState,
) {
cameraState.animateTo(
finalPosition = CameraPosition(
bearing = position!!.bearing,
zoom = position!!.zoom,
target = position!!.target,
tilt = tilt,
padding = PaddingValues(start = 0.dp, top = 350.dp)
), ),
duration = 1.seconds cameraState = cameraState,
) baseStyle = BaseStyle.Uri(Constants.STYLE),
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!NavigationUtils.getBooleanKeyValue(
context = applicationContext,
Constants.SHOW_THREED_BUILDING
)
) {
BuildingLayer(tiles)
}
RouteLayer(route, "", position!!.zoom)
}
if (userLocationState.location != null) {
///PuckState(cameraState, userLocationState)
}
}
LocationTrackingEffect(
locationState = userLocationState,
) {
cameraState.animateTo(
finalPosition = CameraPosition(
bearing = position!!.bearing,
zoom = position!!.zoom,
target = position!!.target,
tilt = tilt,
padding = paddingValues
),
duration = 1.seconds
)
}
NavigationImage(paddingValues, width, height /6, "")
} }
} }
fun updateLocation(location: org.maplibre.compose.location.Location?) { fun updateLocation(location: org.maplibre.compose.location.Location?) {
if (1 == 1) if (location != null
return && lastLocation.latitude != location.position.latitude
if (location != null) { && lastLocation.longitude != location.position.longitude) {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
routeModel.updateLocation(lastLocation) routeModel.updateLocation(lastLocation)
instruction.value = routeModel.currentStep() instruction.value = routeModel.currentStep()
} }
val currentLocation = location(location.position.longitude, location.position.latitude)
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
val zoom = calculateZoom(location.speed) val zoom = calculateZoom(location.speed)
cameraPosition.postValue( cameraPosition.postValue(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
zoom = zoom, zoom = zoom,
target = location.position target = location.position,
bearing = bearing
), ),
) )
} lastLocation = currentLocation
} if (!loadRecentPlaces) {
viewModel.loadRecentPlaces(applicationContext, lastLocation)
private fun checkLocationPermissions() { loadRecentPlaces = true
val permissions = mutableListOf( }
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
if (MainApplication.useContacts) {
permissions.add(Manifest.permission.READ_CONTACTS)
}
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(
this,
permissionsToRequest.toTypedArray(),
LOCATION_PERMISSION_REQUEST_CODE
)
}
}
fun checkPermissionForLocation(): Boolean {
val permissions = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
if (MainApplication.useContacts) {
permissions.add(Manifest.permission.READ_CONTACTS)
}
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
return permissionsToRequest.isEmpty()
}
fun checkPermissionForContact(): Boolean {
val permissions = arrayOf(
Manifest.permission.READ_CONTACTS,
)
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
return permissionsToRequest.isEmpty()
}
private fun checkContactsPermissions() {
val permissions = arrayOf(
Manifest.permission.READ_CONTACTS,
)
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(
this,
permissionsToRequest.toTypedArray(),
CONTACTS_PERMISSION_REQUEST_CODE
)
} }
} }
@@ -460,18 +330,27 @@ class MainActivity : ComponentActivity() {
} }
} }
@SuppressLint("MissingPermission")
@Composable
fun CheckPermissionScreen() {
val permissions = listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
)
PermissionScreen(
permissions = permissions,
requiredPermissions = listOf(permissions.first()),
onGranted = {
Content()
},
)
}
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
fun simulate() = GlobalScope.async { fun simulate() = GlobalScope.async {
for ((i, loc) in routeModel.route.waypoints.withIndex()) { for ((i, loc) in routeModel.route.waypoints.withIndex()) {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
lastLocation.longitude = loc[0] mock.setMockLocation(loc[1], loc[0])
lastLocation.latitude = loc[1] delay(1000L) //
if (i == 20) {
mock.setMockLocation(loc[1] , loc[0])
} else {
mock.setMockLocation(loc[1], loc[0])
}
delay(500L) //
} }
} }
} }

View File

@@ -0,0 +1,53 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel
import kotlinx.coroutines.Deferred
@Composable
fun NavigationSheet(
routeModel: RouteModel,
step: StepData?,
simulate: () -> Unit
) {
Column {
//Text("${routeModel.travelLeftTime()}")
if (step != null)
Text("${step.leftDistance / 1000} km")
HorizontalDivider()
Row() {
if (routeModel.isNavigating()) {
Button(onClick = {
routeModel.stopNavigation()
}) {
Icon(
painter = painterResource(id = R.drawable.ic_close_white_24dp),
"Stop",
modifier = Modifier.size(24.dp, 24.dp),
)
}
Button(onClick = {
simulate()
}) {
Icon(
painter = painterResource(id = R.drawable.assistant_navigation_48px),
"Simulate",
modifier = Modifier.size(24.dp, 24.dp),
)
}
}
}
}
}

View File

@@ -0,0 +1,198 @@
package com.kouros.navigation.ui
import android.content.Intent
import android.provider.Settings
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import androidx.core.net.toUri
/**
* [PermissionScreen] that takes a list of permissions and only calls [onGranted] when
* all the [requiredPermissions] are granted.
*
* By default it assumes that all [permissions] are required.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PermissionScreen(
modifier: Modifier = Modifier,
permissions: List<String>,
requiredPermissions: List<String> = permissions,
description: String? = null,
contentAlignment: Alignment = Alignment.TopStart,
onGranted: @Composable BoxScope.(List<String>) -> Unit,
) {
val context = LocalContext.current
var errorText by remember {
mutableStateOf("")
}
val permissionState = rememberMultiplePermissionsState(permissions = permissions) { map ->
val rejectedPermissions = map.filterValues { !it }.keys
errorText = if (rejectedPermissions.none { it in requiredPermissions }) {
""
} else {
"${rejectedPermissions.joinToString()} required for the sample"
}
}
val allRequiredPermissionsGranted =
permissionState.revokedPermissions.none { it.permission in requiredPermissions }
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
contentAlignment = if (allRequiredPermissionsGranted) {
contentAlignment
} else {
Alignment.Center
},
) {
if (allRequiredPermissionsGranted) {
onGranted(
permissionState.permissions
.filter { it.status.isGranted }
.map { it.permission },
)
} else {
PermissionScreen(
permissionState,
description,
errorText,
)
FloatingActionButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
onClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
data = "package:${context.packageName}".toUri()
}
context.startActivity(intent)
},
) {
Text("App settings")
}
}
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun PermissionScreen(
state: MultiplePermissionsState,
description: String?,
errorText: String,
) {
var showRationale by remember(state) {
mutableStateOf(false)
}
val permissions = remember(state.revokedPermissions) {
state.revokedPermissions.joinToString("\n") {
" - " + it.permission.removePrefix("android.permission.")
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.animateContentSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Sample requires permission/s:",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(16.dp),
)
Text(
text = permissions,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp),
)
if (description != null) {
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp),
)
}
Button(
onClick = {
if (state.shouldShowRationale) {
showRationale = true
} else {
state.launchMultiplePermissionRequest()
}
},
) {
Text(text = "Grant permissions")
}
if (errorText.isNotBlank()) {
Text(
text = errorText,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(16.dp),
)
}
}
if (showRationale) {
AlertDialog(
onDismissRequest = {
showRationale = false
},
title = {
Text(text = "Permissions required by the navigation app")
},
text = {
Text(text = "The navigation app requires the following permissions to work:\n $permissions")
},
confirmButton = {
TextButton(
onClick = {
showRationale = false
state.launchMultiplePermissionRequest()
},
) {
Text("Continue")
}
},
dismissButton = {
TextButton(
onClick = {
showRationale = false
},
) {
Text("Dismiss")
}
},
)
}
}

View File

@@ -1,130 +0,0 @@
package com.kouros.navigation.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.rememberMultiplePermissionsState
/**
* Simple screen that manages the location permission state
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun Permissions(text: String, rationale: String, locationState: PermissionState) {
Permissions(
text = text,
rationale = rationale,
locationState = rememberMultiplePermissionsState(
permissions = listOf(
locationState.permission
)
)
)
}
/**
* Simple screen that manages the location permission state
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun Permissions(text: String, rationale: String, locationState: MultiplePermissionsState) {
var showRationale by remember(locationState) {
mutableStateOf(false)
}
if (showRationale) {
PermissionRationaleDialog(rationaleState = RationaleState(
title = "Permission Access",
rationale = rationale,
onRationaleReply = { proceed ->
if (proceed) {
locationState.launchMultiplePermissionRequest()
}
showRationale = false
}
))
}
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
PermissionRequestButton(isGranted = false, title = text) {
if (locationState.shouldShowRationale) {
showRationale = true
} else {
locationState.launchMultiplePermissionRequest()
}
}
}
}
/**
* A button that shows the title or the request permission action.
*/
@Composable
fun PermissionRequestButton(isGranted: Boolean, title: String, onClick: () -> Unit) {
if (isGranted) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Icon(Icons.Outlined.CheckCircle, title, modifier = Modifier.size(48.dp))
Spacer(Modifier.size(10.dp))
Text(text = title, modifier = Modifier.background(Color.Transparent))
}
} else {
Button(onClick = onClick) {
Text("Request $title")
}
}
}
/**
* Simple AlertDialog that displays the given rationale state
*/
@Composable
fun PermissionRationaleDialog(rationaleState: RationaleState) {
AlertDialog(onDismissRequest = { rationaleState.onRationaleReply(false) }, title = {
Text(text = rationaleState.title)
}, text = {
Text(text = rationaleState.rationale)
}, confirmButton = {
TextButton(onClick = {
rationaleState.onRationaleReply(true)
}) {
Text("Continue")
}
}, dismissButton = {
TextButton(onClick = {
rationaleState.onRationaleReply(false)
}) {
Text("Dismiss")
}
})
}
data class RationaleState(
val title: String,
val rationale: String,
val onRationaleReply: (proceed: Boolean) -> Unit,
)

View File

@@ -1,79 +0,0 @@
package com.kouros.navigation.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SimpleSearchBar(
textFieldState: TextFieldState,
onSearch: (String) -> Unit,
searchResults: List<String>,
modifier: Modifier = Modifier
) {
// Controls expansion state of the search bar
var expanded by rememberSaveable { mutableStateOf(false) }
Box(
modifier
.fillMaxSize()
.semantics { isTraversalGroup = true }
) {
SearchBar(
modifier = Modifier
.align(Alignment.TopCenter)
.semantics { traversalIndex = 0f },
inputField = {
SearchBarDefaults.InputField(
query = textFieldState.text.toString(),
onQueryChange = { textFieldState.edit { replace(0, length, it) } },
onSearch = {
onSearch(textFieldState.text.toString())
expanded = false
},
expanded = expanded,
onExpandedChange = { expanded = it },
placeholder = { Text("Search") }
)
},
expanded = expanded,
onExpandedChange = { expanded = it },
) {
// Display search results in a scrollable column
Column(Modifier.verticalScroll(rememberScrollState())) {
searchResults.forEach { result ->
ListItem(
headlineContent = { Text(result) },
modifier = Modifier
.clickable {
textFieldState.edit { replace(0, length, result) }
expanded = false
}
.fillMaxWidth()
)
}
}
}
}
}

View File

@@ -0,0 +1,217 @@
package com.kouros.navigation.ui
import android.content.Context
import android.location.Location
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
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.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.unit.dp
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.PlaceColor
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.location
@Composable
fun SearchSheet(applicationContext: Context, viewModel: ViewModel, location: Location) {
val searchResults = mutableListOf<SearchResult>()
val recentPlaces = viewModel.places.observeAsState()
val search = viewModel.searchPlaces.observeAsState()
if (search.value != null) {
searchResults.addAll(search.value!!)
}
if (searchResults.isNotEmpty()) {
val textFieldState = rememberTextFieldState()
val items = listOf(searchResults)
if (items.isNotEmpty()) {
SearchBar(
textFieldState = textFieldState,
searchPlaces = recentPlaces.value!!,
searchResults = searchResults,
viewModel = viewModel,
context = applicationContext,
location = location
)
}
}
if (recentPlaces.value != null) {
val textFieldState = rememberTextFieldState()
val items = listOf(recentPlaces)
if (items.isNotEmpty()) {
SearchBar(
textFieldState = textFieldState,
searchPlaces = recentPlaces.value!!,
searchResults = searchResults,
viewModel = viewModel,
context = applicationContext,
location = location
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBar(
textFieldState: TextFieldState,
searchPlaces: List<Place>,
searchResults: List<SearchResult>,
modifier: Modifier = Modifier,
viewModel: ViewModel,
context: Context,
location: Location,
) {
var expanded by rememberSaveable { mutableStateOf(true) }
Box(
modifier
.fillMaxSize()
.semantics { isTraversalGroup = true }
) {
SearchBar(
modifier = Modifier
.align(Alignment.TopCenter)
.semantics { traversalIndex = 0f },
inputField = {
SearchBarDefaults.InputField(
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_search_black36dp),
"Search",
modifier = Modifier.size(24.dp, 24.dp),
)
},
query = textFieldState.text.toString(),
onQueryChange = { textFieldState.edit { replace(0, length, it) } },
onSearch = {
searchPlaces(viewModel, location, it)
expanded = false
},
expanded = expanded,
onExpandedChange = { expanded = it },
placeholder = { Text("Suchen") }
)
},
expanded = expanded,
onExpandedChange = { expanded = it },
) {
if (searchPlaces.isNotEmpty()) {
Text("Recent places")
RecentPlaces(searchPlaces, viewModel, context, location)
}
if (searchResults.isNotEmpty()) {
Text("Search places")
SearchPlaces(searchResults, viewModel, context, location)
}
}
}
}
private fun searchPlaces(viewModel: ViewModel, location: Location, it: String) {
viewModel.searchPlaces(it, location)
}
@Composable
private fun SearchPlaces(
searchResults: List<SearchResult>,
viewModel: ViewModel,
context: Context,
location: Location,
) {
val color = remember { PlaceColor }
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (searchResults.isNotEmpty()) {
items(searchResults, key = { it.placeId }) { place ->
Row {
Icon(
painter = painterResource(id = R.drawable.ic_place_white_24dp),
"Navigation",
tint = color.copy(alpha = 1f),
modifier = Modifier.size(24.dp, 24.dp),
)
ListItem(
headlineContent = { Text("${place.address.road} ${place.address.postcode}") },
modifier = Modifier
.clickable {
val toLocation =
location(place.lon.toDouble(), place.lat.toDouble())
viewModel.loadRoute(context, location, toLocation)
}
.fillMaxWidth()
)
HorizontalDivider(color = Color.Gray)
}
}
}
}
}
@Composable
private fun RecentPlaces(
recentPlaces: List<Place>,
viewModel: ViewModel,
context: Context,
location: Location
) {
val color = remember { PlaceColor }
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
items(recentPlaces, key = { it.id }) { place ->
Row {
Icon(
painter = painterResource(id = R.drawable.ic_place_white_24dp),
"Navigation",
tint = color.copy(alpha = 1f),
modifier = Modifier.size(24.dp, 24.dp),
)
ListItem(
headlineContent = { Text("${place.street!!} ${place.postalCode}") },
modifier = Modifier
.clickable {
val toLocation = location(place.longitude, place.latitude)
viewModel.loadRoute(context, location, toLocation)
}
.fillMaxWidth()
)
HorizontalDivider(color = Color.Gray)
}
}
}
}

View File

@@ -40,7 +40,7 @@ fun NavigationTheme(
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val colorScheme = when { val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { dynamicColor -> {
val context = LocalContext.current val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
} }

View File

@@ -151,8 +151,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
updateLocation(location) updateLocation(location)
locationManager.requestLocationUpdates( locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, LocationManager.GPS_PROVIDER,
/* minTimeMs= */ 500, /* minTimeMs= */ 1000,
/* minDistanceM= */ 0f, /* minDistanceM= */ 5f,
mLocationListener mLocationListener
) )
} }

View File

@@ -18,6 +18,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -25,6 +27,11 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.kouros.navigation.car.map.BuildingLayer
import com.kouros.navigation.car.map.DrawImage
import com.kouros.navigation.car.map.RouteLayer
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.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
@@ -33,9 +40,12 @@ import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.bearing import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateZoom import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.previewZoom
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
import org.maplibre.compose.sources.getBaseSource import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
@@ -77,7 +87,7 @@ class SurfaceRenderer(
lateinit var mapView: ComposeView lateinit var mapView: ComposeView
var panView = false var panView = false
val tilt = 55.0 var tilt = 55.0
var previewDistance = 0.0 var previewDistance = 0.0
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback { val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
@@ -164,11 +174,12 @@ class SurfaceRenderer(
} }
fun onConnectionStateUpdated(connectionState: Int) { fun onConnectionStateUpdated(connectionState: Int) {
when(connectionState) { when (connectionState) {
CarConnection.CONNECTION_TYPE_NATIVE -> ObjectBox.init(carContext) CarConnection.CONNECTION_TYPE_NATIVE -> ObjectBox.init(carContext)
} }
} }
@Composable @Composable
fun MapView() { fun MapView() {
val stateWidth = visibleArea.observeAsState() val stateWidth = visibleArea.observeAsState()
@@ -178,21 +189,29 @@ class SurfaceRenderer(
val paddingValues = getPaddingValues(width - stateWidth.value!!.width(), height, preview) val paddingValues = getPaddingValues(width - stateWidth.value!!.width(), height, preview)
val cameraState = cameraState(paddingValues, position, tilt) val cameraState = cameraState(paddingValues, position, tilt)
val baseStyle =if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri( val baseStyle = remember {
Constants.STYLE mutableStateOf(BaseStyle.Uri(Constants.STYLE))
)
MaplibreMap(
cameraState = cameraState,
baseStyle = baseStyle,
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!getBooleanKeyValue(context = carContext, SHOW_THREED_BUILDING)) {
BuildingLayer(tiles)
}
RouteLayer(route, previewRoute)
}
//Puck(cameraState, lastLocation)
} }
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)
}
ShowPosition(cameraState, position, paddingValues) ShowPosition(cameraState, position, paddingValues)
} }
@@ -215,7 +234,7 @@ class SurfaceRenderer(
} }
} else { } else {
bearing = 0.0 bearing = 0.0
zoom = previewZoom() zoom = previewZoom(previewDistance)
target = Position(centerLocation.longitude, centerLocation.latitude) target = Position(centerLocation.longitude, centerLocation.latitude)
localTilt = 0.0 localTilt = 0.0
} }
@@ -261,11 +280,19 @@ class SurfaceRenderer(
} else { } else {
cameraPosition.value!!.zoom + 1.0 cameraPosition.value!!.zoom + 1.0
} }
cameraPosition.postValue( tilt = if (newZoom < 13) {
cameraPosition.value!!.copy( 0.0
zoom = newZoom, } else {
target = cameraPosition.value!!.target if (tilt == 0.0) {
) 55.0
} else {
tilt
}
}
updateCameraPosition(
cameraPosition.value!!.bearing,
newZoom,
cameraPosition.value!!.target,
) )
} }
} }
@@ -273,7 +300,7 @@ class SurfaceRenderer(
fun updateLocation(location: Location) { fun updateLocation(location: Location) {
synchronized(this) { synchronized(this) {
if (!preview) { if (!preview) {
val bearing = bearing(lastLocation, location) val bearing = bearing(lastLocation, location, cameraPosition.value!!.bearing)
val zoom = if (!panView) { val zoom = if (!panView) {
calculateZoom(location.speed.toDouble()) calculateZoom(location.speed.toDouble())
} else { } else {
@@ -287,10 +314,9 @@ class SurfaceRenderer(
lastBearing = cameraPosition.value!!.bearing lastBearing = cameraPosition.value!!.bearing
lastLocation = location lastLocation = location
} else { } else {
val zoom = previewZoom()
updateCameraPosition( updateCameraPosition(
0.0, 0.0,
zoom, previewZoom(previewDistance),
Position(centerLocation.longitude, centerLocation.latitude) Position(centerLocation.longitude, centerLocation.latitude)
) )
} }
@@ -302,7 +328,7 @@ class SurfaceRenderer(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
bearing = bearing, bearing = bearing,
zoom = zoom, zoom = zoom,
tilt = 0.0, tilt = tilt,
padding = getPaddingValues(width - visibleArea.value!!.width(), height, preview), padding = getPaddingValues(width - visibleArea.value!!.width(), height, preview),
target = target target = target
) )
@@ -323,28 +349,6 @@ class SurfaceRenderer(
previewDistance = routeModel.route.distance previewDistance = routeModel.route.distance
} }
private fun previewZoom(): Double {
when (previewDistance) {
in 0.0..10.0 -> {
return 13.0
}
in 10.0..20.0 -> {
return 11.0
}
in 20.0..30.0 -> {
return 10.0
}
}
return 9.0
}
fun setPreViewDistance(): Double {
return previewDistance
}
companion companion
object { object {
private const val TAG = "MapRenderer" private const val TAG = "MapRenderer"

View File

@@ -1,4 +1,4 @@
package com.kouros.navigation.car package com.kouros.navigation.car.map
import android.location.Location import android.location.Location
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable

View File

@@ -1,4 +1,4 @@
package com.kouros.navigation.car package com.kouros.navigation.car.map
import android.location.Location import android.location.Location
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
@@ -17,9 +17,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
@@ -67,7 +66,8 @@ fun cameraState(
} }
@Composable @Composable
fun RouteLayer(routeData: String?, previewRoute: String?) { fun RouteLayer(routeData: String?, previewRoute: String?, zoom: Double) {
val width = zoom - 2
if (routeData!!.isNotEmpty()) { if (routeData!!.isNotEmpty()) {
val routes = val routes =
rememberGeoJsonSource(GeoJsonData.JsonString(routeData)) rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
@@ -75,13 +75,13 @@ fun RouteLayer(routeData: String?, previewRoute: String?) {
id = "routes-casing", id = "routes-casing",
source = routes, source = routes,
color = const(Color.White), color = const(Color.White),
width = const(16.dp), width = const((width+2).dp),
) )
LineLayer( LineLayer(
id = "routes", id = "routes",
source = routes, source = routes,
color = const(RouteColor), color = const(RouteColor),
width = const(14.dp), width = const(width.dp),
) )
} }
if (previewRoute!!.isNotEmpty()) { if (previewRoute!!.isNotEmpty()) {
@@ -122,10 +122,7 @@ fun DrawImage(padding: PaddingValues, location: Location, width: Int, height: In
@Composable @Composable
fun NavigationImage(padding: PaddingValues, width: Int, height: Int, street: String) { fun NavigationImage(padding: PaddingValues, width: Int, height: Int, street: String) {
val imageSize = (height/6) val imageSize = (height/6)
println("Image Size: $imageSize")
val vector = ImageVector.vectorResource(id = R.drawable.assistant_navigation_48px)
val color = remember { NavigationColor } val color = remember { NavigationColor }
BadgedBox( BadgedBox(
modifier = Modifier modifier = Modifier
@@ -134,19 +131,18 @@ fun NavigationImage(padding: PaddingValues, width: Int, height: Int, street: Str
Badge() Badge()
} }
) { ) {
Canvas(modifier =Modifier
.size(imageSize.dp, imageSize.dp)) {
scale(scaleX = 1f, scaleY = 0.7f) {
drawCircle(Color.DarkGray.copy(alpha = 0.2f))
}
}
Icon( Icon(
painter = painterResource(id = R.drawable.navigation), painter = painterResource(id = R.drawable.navigation),
"Navigation", "Navigation",
tint = color, tint = color.copy(alpha = 1f),
modifier = Modifier.size(imageSize.dp, imageSize.dp), modifier = Modifier.size(imageSize.dp, imageSize.dp),
) )
// Icon(
// modifier = Modifier.size(72.dp, 72.dp),
// imageVector = vector,
// contentDescription = "Navigation",
// tint = color
// )
if (street.isNotEmpty()) if (street.isNotEmpty())
Text(text = street) Text(text = street)
} }

View File

@@ -81,7 +81,7 @@ class RouteCarModel() : RouteModel() {
val maneuverType = maneuver.type val maneuverType = maneuver.type
val routing = routingData(maneuverType, carContext) val routing = routingData(maneuverType, carContext)
var text = "" var text = ""
val distanceLeft = leftStepDistance() * 1000 val distanceLeft = leftStepDistance()
when (distanceLeft) { when (distanceLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> { in 0.0..NEXT_STEP_THRESHOLD -> {
@@ -178,7 +178,7 @@ class RouteCarModel() : RouteModel() {
|| maneuverType == ManeuverType.DestinationLeft.value || maneuverType == ManeuverType.DestinationLeft.value
} }
fun travelEstimate(carContext: CarContext): TravelEstimate { fun travelEstimate(): TravelEstimate {
val timeLeft = travelLeftTime() val timeLeft = travelLeftTime()
// Calculate the time to destination from the current time. // Calculate the time to destination from the current time.
val nowUtcMillis = System.currentTimeMillis() val nowUtcMillis = System.currentTimeMillis()

View File

@@ -29,6 +29,7 @@ import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.car.NavigationCarAppService import com.kouros.navigation.car.NavigationCarAppService
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
@@ -102,7 +103,7 @@ class NavigationScreen(
.setNavigationInfo( .setNavigationInfo(
getRoutingInfo() getRoutingInfo()
) )
.setDestinationTravelEstimate(routeModel.travelEstimate(carContext)) .setDestinationTravelEstimate(routeModel.travelEstimate())
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build()) .setMapActionStrip(mapActionStripBuilder().build())
.setBackgroundColor(CarColor.GREEN) .setBackgroundColor(CarColor.GREEN)
@@ -182,27 +183,11 @@ class NavigationScreen(
return builder.build() return builder.build()
} }
fun navigationRerouteTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate { fun navigationRerouteTemplate(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo( .setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
MessageInfo.Builder(
carContext.getString(R.string.new_route)
)
.setText(routeModel.destination.street.toString())
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.navigation_48px
)
)
.build()
)
.build()
)
.setBackgroundColor(CarColor.SECONDARY)
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build()) .setBackgroundColor(CarColor.GREEN)
.build() .build()
} }
@@ -447,8 +432,12 @@ class NavigationScreen(
} }
fun updateTrip(location: Location) { fun updateTrip(location: Location) {
val start = System.currentTimeMillis()
routeModel.updateLocation(location) routeModel.updateLocation(location)
if (routeModel.maneuverType == Maneuver.TYPE_DESTINATION && routeModel.leftStepDistance() * 1000 < 25.0) { val end = System.currentTimeMillis()
println("Time ${end-start}")
if (routeModel.maneuverType == Maneuver.TYPE_DESTINATION
&& routeModel.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE) {
routeModel.arrived = true routeModel.arrived = true
routeModel.stopNavigation() routeModel.stopNavigation()
} }

View File

@@ -2,11 +2,8 @@ package com.kouros.navigation.car.screen
import android.location.Location import android.location.Location
import android.net.Uri import android.net.Uri
import android.os.Build
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
@@ -25,7 +22,6 @@ import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
@@ -66,7 +62,7 @@ class PlaceListScreen(
fun loadPlaces() { fun loadPlaces() {
if (category == Constants.RECENT) { if (category == Constants.RECENT) {
viewModel.loadPlaces(carContext, location) viewModel.loadRecentPlaces(carContext, location)
} }
if (category == Constants.CONTACTS) { if (category == Constants.CONTACTS) {
viewModel.loadContacts(carContext, location) viewModel.loadContacts(carContext, location)

View File

@@ -2,14 +2,17 @@ package com.kouros.navigation.car.screen
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.location.Location import android.location.Location
import android.net.Uri
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.CarIcon
import androidx.car.app.model.ItemList import androidx.car.app.model.ItemList
import androidx.car.app.model.Row import androidx.car.app.model.Row
import androidx.car.app.model.SearchTemplate import androidx.car.app.model.SearchTemplate
import androidx.car.app.model.SearchTemplate.SearchCallback import androidx.car.app.model.SearchTemplate.SearchCallback
import androidx.car.app.model.Template import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.kouros.android.cars.carappservice.R import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
@@ -61,6 +64,7 @@ class SearchScreen(
itemListBuilder.addItem( itemListBuilder.addItem(
Row.Builder() Row.Builder()
.setTitle(it.name) .setTitle(it.name)
.setImage(categoryIcon(it.id))
.setOnClickListener { .setOnClickListener {
screenManager screenManager
.pushForResult( .pushForResult(
@@ -104,6 +108,25 @@ class SearchScreen(
.build() .build()
} }
fun categoryIcon(category: String?): CarIcon {
val resId : Int = when (category) {
Constants.RECENT -> {
R.drawable.ic_place_white_24dp
}
Constants.FAVORITES -> {
R.drawable.ic_favorite_white_24dp
}
else -> {
R.drawable.navigation
}
}
return CarIcon.Builder(
IconCompat.createWithResource(
carContext, resId
)
).build()
}
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
fun doSearch(searchItemListBuilder: ItemList.Builder) { fun doSearch(searchItemListBuilder: ItemList.Builder) {
searchResult.forEach { searchResult.forEach {

View File

@@ -2,8 +2,10 @@ package com.kouros.navigation.data
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val NavigationColor = Color(0xFF368605) val NavigationColor = Color(0xFF052186)
val RouteColor = Color(0xFF5582D0) val RouteColor = Color(0xFF5582D0)
val SpeedColor = Color(0xFF262525) val SpeedColor = Color(0xFF262525)
val PlaceColor = Color(0xFF868005)

View File

@@ -62,7 +62,6 @@ data class ContactData(
data class StepData ( data class StepData (
var instruction: String, var instruction: String,
var leftDistance: Double, var leftDistance: Double,
var bearing: Double
) )
@@ -138,8 +137,6 @@ object Constants {
const val STYLE: String = "https://kouros-online.de/liberty.json" const val STYLE: String = "https://kouros-online.de/liberty.json"
const val STYLE_DARK: String = "https://kouros-online.de/liberty_night.json" const val STYLE_DARK: String = "https://kouros-online.de/liberty_night.json"
//const val STYLE: String = "https://tiles.openfreemap.org/styles/liberty" //const val STYLE: String = "https://tiles.openfreemap.org/styles/liberty"
const val TAG: String = "Navigation" const val TAG: String = "Navigation"
const val CONTACTS: String = "Contacts" const val CONTACTS: String = "Contacts"
@@ -175,6 +172,8 @@ object Constants {
const val MAXIMAL_ROUTE_DEVIATION = 100.0 const val MAXIMAL_ROUTE_DEVIATION = 100.0
const val DESTINATION_ARRIVAL_DISTANCE = 20.0
} }

View File

@@ -39,11 +39,11 @@ class NavigationRepository {
// motorway, trunk, primary, secondary, tertiary, unclassified, residential, service_other. // motorway, trunk, primary, secondary, tertiary, unclassified, residential, service_other.
// exclude_toll // exclude_toll
fun getRoute(currentLocation: Location, location: Location, SearchFilter: SearchFilter): String { fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String {
SearchFilter SearchFilter
val vLocation = listOf( val vLocation = listOf(
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude, search_filter = SearchFilter), Locations(lat = currentLocation.latitude, lon = currentLocation.longitude, search_filter = searchFilter),
Locations(lat = location.latitude, lon = location.longitude, search_filter = SearchFilter) Locations(lat = location.latitude, lon = location.longitude, search_filter = searchFilter)
) )
val valhallaLocation = ValhallaLocation( val valhallaLocation = ValhallaLocation(
locations = vLocation, locations = vLocation,

View File

@@ -47,9 +47,9 @@
"source": "openmaptiles", "source": "openmaptiles",
"source-layer": "park", "source-layer": "park",
"paint": { "paint": {
"fill-color": "#d8e8c8", "fill-color": "rgba(78, 100, 56, 1)",
"fill-opacity": 0.7, "fill-opacity": 0.7,
"fill-outline-color": "rgba(95, 208, 100, 1)" "fill-outline-color": "rgba(39, 78, 40, 1)"
} }
}, },
{ {
@@ -101,7 +101,7 @@
"filter": ["==", ["get", "class"], "grass"], "filter": ["==", ["get", "class"], "grass"],
"paint": { "paint": {
"fill-antialias": false, "fill-antialias": false,
"fill-color": "rgba(176, 213, 154, 1)", "fill-color": "rgba(21, 32, 15, 1)",
"fill-opacity": 0.3 "fill-opacity": 0.3
} }
}, },
@@ -153,7 +153,7 @@
"source": "openmaptiles", "source": "openmaptiles",
"source-layer": "landuse", "source-layer": "landuse",
"filter": ["==", ["get", "class"], "cemetery"], "filter": ["==", ["get", "class"], "cemetery"],
"paint": {"fill-color": "hsl(75,37%,81%)"} "paint": {"fill-color": "rgba(65, 74, 92, 1)"}
}, },
{ {
"id": "landuse_hospital", "id": "landuse_hospital",
@@ -169,7 +169,7 @@
"source": "openmaptiles", "source": "openmaptiles",
"source-layer": "landuse", "source-layer": "landuse",
"filter": ["==", ["get", "class"], "school"], "filter": ["==", ["get", "class"], "school"],
"paint": {"fill-color": "rgb(236,238,204)"} "paint": {"fill-color": "rgba(65, 74, 92, 1)"}
}, },
{ {
"id": "waterway_tunnel", "id": "waterway_tunnel",
@@ -905,7 +905,6 @@
], ],
"layout": {"line-cap": "round", "line-join": "round"}, "layout": {"line-cap": "round", "line-join": "round"},
"paint": { "paint": {
"line-color": "#cfcdca",
"line-width": [ "line-width": [
"interpolate", "interpolate",
["exponential", 1.2], ["exponential", 1.2],
@@ -916,7 +915,8 @@
4, 4,
20, 20,
11 11
] ],
"line-color": "rgba(65, 74, 92, 1)"
} }
}, },
{ {
@@ -975,7 +975,6 @@
], ],
"layout": {"line-cap": "round", "line-join": "round"}, "layout": {"line-cap": "round", "line-join": "round"},
"paint": { "paint": {
"line-color": "#cfcdca",
"line-opacity": ["interpolate", ["linear"], ["zoom"], 12, 0, 12.5, 1], "line-opacity": ["interpolate", ["linear"], ["zoom"], 12, 0, 12.5, 1],
"line-width": [ "line-width": [
"interpolate", "interpolate",
@@ -989,7 +988,8 @@
4, 4,
20, 20,
20 20
] ],
"line-color": "rgba(65, 74, 92, 1)"
} }
}, },
{ {
@@ -1095,7 +1095,7 @@
], ],
"layout": {"line-join": "round"}, "layout": {"line-join": "round"},
"paint": { "paint": {
"line-color": "hsl(0,0%,100%)", "line-color": "rgba(65, 74, 92, 1)",
"line-dasharray": [1, 0.7], "line-dasharray": [1, 0.7],
"line-width": [ "line-width": [
"interpolate", "interpolate",
@@ -1219,7 +1219,6 @@
], ],
"layout": {"line-cap": "round", "line-join": "round"}, "layout": {"line-cap": "round", "line-join": "round"},
"paint": { "paint": {
"line-color": "rgba(195, 190, 190, 1)",
"line-width": [ "line-width": [
"interpolate", "interpolate",
["exponential", 1.2], ["exponential", 1.2],
@@ -1230,7 +1229,8 @@
2.5, 2.5,
20, 20,
18 18
] ],
"line-color": "rgba(65, 74, 92, 1)"
} }
}, },
{ {

View File

@@ -30,8 +30,6 @@ open class RouteModel() {
var distanceToStepEnd = 0F var distanceToStepEnd = 0F
var bearing = 0F
var beginIndex = 0 var beginIndex = 0
var endIndex = 0 var endIndex = 0
@@ -57,7 +55,7 @@ open class RouteModel() {
val currentDistance: Double val currentDistance: Double
/** Returns the current [Step] with information such as the cue text and images. */ /** Returns the current [Step] with information such as the cue text and images. */
get() { get() {
return ((leftStepDistance() * 1000).roundToInt().toDouble() / 10.0).roundToInt() * 10.0 return ((leftStepDistance()).roundToInt().toDouble() / 10.0).roundToInt() * 10.0
} }
fun updateLocation(location: Location) { fun updateLocation(location: Location) {
@@ -86,14 +84,7 @@ open class RouteModel() {
route.pointLocations[currentShapeIndex].longitude(), route.pointLocations[currentShapeIndex].longitude(),
route.pointLocations[currentShapeIndex].latitude() route.pointLocations[currentShapeIndex].latitude()
) )
// if (currentShapeIndex < route.pointLocations.size) { val distanceStepLeft = leftStepDistance()
// val nextLocation = location(
// route.pointLocations[currentShapeIndex + 1].latitude(),
// route.pointLocations[currentShapeIndex + 1].longitude()
// )
// bearing = curLocation.bearingTo(nextLocation)
// }
val distanceStepLeft = leftStepDistance() * 1000
when (distanceStepLeft) { when (distanceStepLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> { in 0.0..NEXT_STEP_THRESHOLD -> {
if (route.currentManeuverIndex < route.maneuvers.size) { if (route.currentManeuverIndex < route.maneuvers.size) {
@@ -104,7 +95,7 @@ open class RouteModel() {
} }
} }
} }
return StepData(text, distanceStepLeft, bearing.toDouble()) return StepData(text, distanceStepLeft)
} }
/** Calculates the index in a maneuver. */ /** Calculates the index in a maneuver. */
@@ -174,16 +165,17 @@ open class RouteModel() {
return timeLeft return timeLeft
} }
/** Returns the current [Step] left distance in km. */ /** Returns the current [Step] left distance in m. */
fun leftStepDistance(): Double { fun leftStepDistance(): Double {
val maneuver = route.currentManeuver() val maneuver = route.currentManeuver()
var leftDistance = maneuver.length var leftDistance = maneuver.length
if (endIndex > 0) { if (endIndex > 0) {
leftDistance = (distanceToStepEnd / 1000).toDouble() leftDistance = distanceToStepEnd.toDouble()
} }
return leftDistance return leftDistance * 1000
} }
/** Returns the left distance in km. */
fun travelLeftDistance(): Double { fun travelLeftDistance(): Double {
var leftDistance = 0.0 var leftDistance = 0.0
for (i in route.currentManeuverIndex + 1..<route.maneuvers.size) { for (i in route.currentManeuverIndex + 1..<route.maneuvers.size) {

View File

@@ -3,11 +3,14 @@ package com.kouros.navigation.model
import android.content.Context import android.content.Context
import android.location.Geocoder import android.location.Geocoder
import android.location.Location import android.location.Location
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Locations
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.ObjectBox.boxStore import com.kouros.navigation.data.ObjectBox.boxStore
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
@@ -64,20 +67,21 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
val results = query.find() val results = query.find()
query.close() query.close()
for (place in results) { for (place in results) {
val plLocation = location(place.longitude,place.latitude) val plLocation = location(place.longitude, place.latitude)
// val distance = repository.getRouteDistance(location, plLocation) //val distance = repository.getRouteDistance(location, plLocation, SearchFilter())
//place.distance = distance.toFloat() //place.distance = distance.toFloat()
if (place.distance == 0F) { //if (place.distance == 0F) {
recentPlace.postValue(place) recentPlace.postValue(place)
return@launch return@launch
} //}
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
} }
fun loadPlaces(context: Context, location: Location) {
fun loadRecentPlaces(context: Context, location: Location) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val placeBox = boxStore.boxFor(Place::class) val placeBox = boxStore.boxFor(Place::class)
@@ -89,7 +93,8 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
query.close() query.close()
for (place in results) { for (place in results) {
val plLocation = location(place.longitude, place.latitude) val plLocation = location(place.longitude, place.latitude)
val distance = repository.getRouteDistance(location, plLocation, getSearchFilter(context)) val distance =
repository.getRouteDistance(location, plLocation, getSearchFilter(context))
place.distance = distance.toFloat() place.distance = distance.toFloat()
} }
places.postValue(results) places.postValue(results)
@@ -110,8 +115,9 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
val results = query.find() val results = query.find()
query.close() query.close()
for (place in results) { for (place in results) {
val plLocation = location(place.longitude, place.latitude ) val plLocation = location(place.longitude, place.latitude)
val distance = repository.getRouteDistance(location, plLocation, getSearchFilter(context)) val distance =
repository.getRouteDistance(location, plLocation, getSearchFilter(context))
place.distance = distance.toFloat() place.distance = distance.toFloat()
} }
favorites.postValue(results) favorites.postValue(results)
@@ -124,7 +130,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
fun loadRoute(context: Context, currentLocation: Location, location: Location) { fun loadRoute(context: Context, currentLocation: Location, location: Location) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
route.postValue(repository.getRoute(currentLocation, location, getSearchFilter(context))) route.postValue(
repository.getRoute(
currentLocation,
location,
getSearchFilter(context)
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -134,7 +146,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
fun loadPreviewRoute(context: Context, currentLocation: Location, location: Location) { fun loadPreviewRoute(context: Context, currentLocation: Location, location: Location) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
previewRoute.postValue(repository.getRoute(currentLocation, location, getSearchFilter(context))) previewRoute.postValue(
repository.getRoute(
currentLocation,
location,
getSearchFilter(context)
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -155,9 +173,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
) { ) {
for (adr in it) { for (adr in it) {
if (addressLines.size > 1) { if (addressLines.size > 1) {
val plLocation = location( adr.longitude, adr.latitude) val plLocation = location(adr.longitude, adr.latitude)
val distance = val distance =
repository.getRouteDistance(currentLocation, plLocation, getSearchFilter(context)) repository.getRouteDistance(
currentLocation,
plLocation,
getSearchFilter(context)
)
contactList.add( contactList.add(
Place( Place(
id = address.contactId, id = address.contactId,
@@ -201,7 +223,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
} }
} }
fun reverseAddress(location: Location ): String { fun reverseAddress(location: Location): String {
val address = repository.reverseAddress(location) val address = repository.reverseAddress(location)
val gson = GsonBuilder().serializeNulls().create() val gson = GsonBuilder().serializeNulls().create()
val place = gson.fromJson(address, SearchResult::class.java) val place = gson.fromJson(address, SearchResult::class.java)
@@ -213,16 +235,20 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
place.category = Constants.FAVORITES place.category = Constants.FAVORITES
savePlace(place) savePlace(place)
} }
fun saveRecent(place: Place) { fun saveRecent(place: Place) {
place.category = Constants.RECENT place.category = Constants.RECENT
savePlace(place) savePlace(place)
} }
fun savePlace(place: Place) { fun savePlace(place: Place) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val placeBox = boxStore.boxFor(Place::class) val placeBox = boxStore.boxFor(Place::class)
val query = placeBox val query = placeBox
.query(Place_.name.equal(place.name!!).and(Place_.category.equal(place.category!!))) .query(
Place_.name.equal(place.name!!).and(Place_.category.equal(place.category!!))
)
.build() .build()
val results = query.find() val results = query.find()
query.close() query.close()
@@ -247,12 +273,15 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
place.category = Constants.RECENT place.category = Constants.RECENT
deletePlace(place) deletePlace(place)
} }
fun deletePlace(place: Place) { fun deletePlace(place: Place) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val placeBox = boxStore.boxFor(Place::class) val placeBox = boxStore.boxFor(Place::class)
val query = placeBox val query = placeBox
.query(Place_.name.equal(place.name!!).and(Place_.category.equal(place.category!!))) .query(
Place_.name.equal(place.name!!).and(Place_.category.equal(place.category!!))
)
.build() .build()
val results = query.find() val results = query.find()
query.close() query.close()
@@ -267,11 +296,11 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
fun getSearchFilter(context: Context): SearchFilter { fun getSearchFilter(context: Context): SearchFilter {
val avoidMotorway = NavigationUtils.getBooleanKeyValue( val avoidMotorway = NavigationUtils.getBooleanKeyValue(
context = context, context = context,
Constants.AVOID_MOTORWAY Constants.AVOID_MOTORWAY
) )
val avoidTollway = NavigationUtils.getBooleanKeyValue( val avoidTollway = NavigationUtils.getBooleanKeyValue(
context = context, context = context,
Constants.AVOID_TOLLWAY Constants.AVOID_TOLLWAY
) )
@@ -280,5 +309,28 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
.avoidTollway(avoidTollway) .avoidTollway(avoidTollway)
.build() .build()
} }
}
fun loadPlaces2(context: Context, location: Location): 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()
for (place in results) {
val plLocation = location(place.longitude, place.latitude)
val distance =
repository.getRouteDistance(location, plLocation, getSearchFilter(context))
place.distance = distance.toFloat()
}
} catch (e: Exception) {
e.printStackTrace()
}
return results.toMutableStateList()
}
}

View File

@@ -11,7 +11,6 @@ import com.kouros.navigation.data.GeoJsonLineString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.maplibre.geojson.Point import org.maplibre.geojson.Point
import org.maplibre.turf.TurfMisc import org.maplibre.turf.TurfMisc
import org.maplibre.turf.TurfMeasurement
import java.lang.Math.toDegrees import java.lang.Math.toDegrees
import java.lang.Math.toRadians import java.lang.Math.toRadians
import kotlin.math.asin import kotlin.math.asin
@@ -151,20 +150,38 @@ object NavigationUtils {
fun calculateZoom(speed: Double?): Double { fun calculateZoom(speed: Double?): Double {
if (speed == null) { if (speed == null) {
return 18.0 return 17.0
} }
val zoom = when (speed.toInt()) { val speedKmh = (speed * 3.6).toInt()
val zoom = when (speedKmh) {
in 0..10 -> 17.0 in 0..10 -> 17.0
in 11..20 -> 17.0 in 11..30 -> 16.0
in 21..30 -> 17.0 in 31..50 -> 16.0
in 31..40 -> 16.0 in 51..60 -> 15.0
in 41..50 -> 15.0 else -> 15
in 51..60 -> 14.0
else -> 14
} }
return zoom.toDouble() return zoom.toDouble()
} }
fun bearing(fromLocation: Location, toLocation: Location ) : Double {
fun previewZoom(previewDistance: Double): Double {
when (previewDistance) {
in 0.0..10.0 -> {
return 13.0
}
in 10.0..20.0 -> {
return 11.0
}
in 20.0..30.0 -> {
return 10.0
}
}
return 9.0
}
fun bearing(fromLocation: Location, toLocation: Location, oldBearing: Double) : Double {
val distance = fromLocation.distanceTo(toLocation)
if (distance < 1.0) {
return oldBearing
}
val bearing = fromLocation.bearingTo(toLocation).toInt().toDouble() val bearing = fromLocation.bearingTo(toLocation).toInt().toDouble()
return bearing return bearing
} }

View File

@@ -35,6 +35,8 @@ navigationCompose = "2.9.6"
uiToolingPreview = "1.9.5" uiToolingPreview = "1.9.5"
uiTooling = "1.9.5" uiTooling = "1.9.5"
material3WindowSizeClass = "1.4.0" material3WindowSizeClass = "1.4.0"
uiGraphics = "1.10.0"
window = "1.5.1"
[libraries] [libraries]
@@ -73,6 +75,8 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
androidx-compose-material3-window-size-class1 = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "material3WindowSizeClass" } androidx-compose-material3-window-size-class1 = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "material3WindowSizeClass" }
androidx-app-automotive = { module = "androidx.car.app:app-automotive", version.ref = "androidx-car" } androidx-app-automotive = { module = "androidx.car.app:app-automotive", version.ref = "androidx-car" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "uiGraphics" }
androidx-window = { group = "androidx.window", name = "window", version.ref = "window" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }