App DrawItem and Search
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) //
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt
Executable file
53
app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt
Executable 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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
198
app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt
Normal file
198
app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt
Normal 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")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
)
|
|
||||||
@@ -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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
217
app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt
Normal file
217
app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
Reference in New Issue
Block a user