MapView, Navigation to RecentPlace

This commit is contained in:
Dimitris
2025-12-01 19:45:17 +01:00
parent da209a4354
commit cddb193260
16 changed files with 346 additions and 154 deletions

View File

@@ -1,8 +1,10 @@
package com.kouros.navigation.car
import android.location.Location
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Badge
@@ -11,13 +13,12 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
@@ -26,6 +27,7 @@ import androidx.compose.ui.unit.sp
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.data.NavigationColor
import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState
@@ -134,43 +136,78 @@ fun NavigationImage(height: Int, street: String) {
}
) {
Icon(
modifier = Modifier.size(72.dp, 72.dp),
imageVector = vector,
contentDescription = "Navigation",
tint = color
)
if (street.isNotEmpty())
Text(text=street)
Text(text = street)
}
}
@Composable
private fun Speed(
width: Int,
height: Int,
location: Location
) {
val textMeasurer = rememberTextMeasurer()
val radius = 32
Box(
modifier = Modifier
.size(30.dp, 30.dp)
.padding(
start = width.dp - percent(width, 20).dp,
top = height.dp - 60.dp
start = width.dp- 300.dp,
top = height.dp- 80.dp
),
contentAlignment = Alignment.Center
) {
val textMeasurerSpeed = rememberTextMeasurer()
val textMeasurerKm = rememberTextMeasurer()
val speed = (location.speed * 3.6).toInt().toString()
val kmh = "km/h"
val styleSpeed = TextStyle(
fontSize = 22.sp,
color = Color.White,
)
val styleKm = TextStyle(
fontSize = 12.sp,
color = Color.White,
)
val textLayoutSpeed = remember(speed) {
textMeasurerSpeed.measure(speed, styleSpeed)
}
val textLayoutKm = remember(kmh) {
textMeasurerSpeed.measure(kmh, styleKm)
}
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(
center = Offset(
x = center.x,
y = center.y
),
radius = radius.toFloat(),
color = SpeedColor,
)
.drawWithCache {
val measuredText =
textMeasurer.measure(
AnnotatedString("${(location.speed * 3.6).toInt()}"),
style = TextStyle(color = Color.White, fontSize = 22.sp)
)
onDrawBehind {
drawCircle(
Color.Black, radius = 30.dp.toPx(), center = Offset(15f, 12f)
)
drawText(measuredText)
}
}
)
drawText(
textMeasurer = textMeasurerSpeed,
text = speed,
style = styleSpeed,
topLeft = Offset(
x = center.x - textLayoutSpeed.size.width / 2,
y = center.y - textLayoutSpeed.size.height / 2 - 5,
)
)
drawText(
textMeasurer = textMeasurerKm,
text = "km/h",
style = styleKm,
topLeft = Offset(
x = center.x - textLayoutKm.size.width / 2,
y = center.y - textLayoutKm.size.height / 2 + 15,
)
)
}
}
}
fun getPaddingValues(height: Int, preView: Boolean): PaddingValues {

View File

@@ -87,7 +87,6 @@ class NavigationSession : Session(), NavigationScreen.Listener {
lifecycle.addObserver(mLifeCycleObserver)
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onCreateScreen(intent: Intent): Screen {
routeModel = RouteCarModel()

View File

@@ -54,7 +54,7 @@ class SurfaceRenderer(
)
)
var visibleArea = MutableLiveData(
Rect()
Rect(0,0,0,0)
)
var stableArea = Rect()
@@ -76,6 +76,7 @@ class SurfaceRenderer(
var panView = false
val tilt = 55.0
var previewDistance = 0.0
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
lateinit var lifecycleOwner: CustomLifecycleOwner
@@ -84,7 +85,6 @@ class SurfaceRenderer(
lateinit var presentation: Presentation
@RequiresApi(Build.VERSION_CODES.M)
override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
synchronized(this@SurfaceRenderer) {
Log.i(TAG, "Surface available $surfaceContainer")
@@ -134,7 +134,6 @@ class SurfaceRenderer(
}
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
synchronized(this@SurfaceRenderer) {
Log.i(TAG, "SurfaceRenderer destroyed")
@@ -168,7 +167,7 @@ class SurfaceRenderer(
val previewRoute: String? by previewRouteData.observeAsState()
val cameraState = cameraState(width, height, position, tilt, preview)
val baseStyle =BaseStyle.Uri(Constants.STYLE)
val baseStyle = BaseStyle.Uri(Constants.STYLE)
// if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri(
// Constants.STYLE
// )
@@ -257,7 +256,7 @@ class SurfaceRenderer(
}
}
fun updateLocation(location: Location) {
fun updateLocation(location: Location) {
synchronized(this) {
if (!preview) {
var bearing = cameraPosition.value!!.bearing
@@ -307,25 +306,29 @@ class SurfaceRenderer(
previewRouteData.value = routeModel.route.routeGeoJson
centerLocation = routeModel.centerLocation
preview = true
previewDistance = routeModel.route.distance
}
private fun previewZoom(): Double {
if (routeModel.isNavigating()) {
when (routeModel.route.distance) {
in 0.0..10.0 -> {
return 14.0
}
in 10.0..20.0 -> {
return 12.0
}
in 20.0..30.0 -> {
return 11.0
}
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 10.0
return 9.0
}
fun setPreViewDistance(): Double {
return previewDistance
}
companion
object {
private const val TAG = "MapRenderer"

View File

@@ -6,7 +6,6 @@ import android.location.Location
import android.location.LocationManager
import android.os.CountDownTimer
import android.os.Handler
import android.os.Looper
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
@@ -14,8 +13,12 @@ import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.model.Header
import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.Template
import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.navigation.model.MessageInfo
import androidx.car.app.navigation.model.NavigationTemplate
import androidx.car.app.navigation.model.RoutingInfo
@@ -31,6 +34,7 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.location
class NavigationScreen(
carContext: CarContext,
@@ -43,46 +47,45 @@ class NavigationScreen(
/** A listener for navigation start and stop signals. */
interface Listener {
/** Stops navigation. */
/** Stops navigation. */
fun stopNavigation()
}
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
lateinit var recentPlace: Place
var recentPlaceFound = false
var recentPlaceActive = true
var calculateNewRoute = false
val vieModel = ViewModel(NavigationRepository())
val viewModel = ViewModel(NavigationRepository())
val observer = Observer<String> { route ->
if (route.isNotEmpty()) {
routeModel.startNavigation(route)
surfaceRenderer.setRouteData()
recentPlaceActive = false
invalidate()
}
}
val recentObserver = Observer<Place> { lastPlace ->
if (!routeModel.isNavigating()) {
recentPlace = lastPlace
recentPlaceFound = true
invalidate()
}
}
init {
vieModel.route.observe(this, observer)
viewModel.route.observe(this, observer)
viewModel.recentPlace.observe(this, recentObserver)
viewModel.loadRecentPlace(location = surfaceRenderer.lastLocation)
}
override fun onGetTemplate(): Template {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
actionStripBuilder.addAction(
Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.ic_search_black36dp))
.setOnClickListener {
startSearchScreen()
}
//.setFlags(Action.FLAG_IS_PERSISTENT)
.build()
)
actionStripBuilder.addAction(
Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_24px))
.setOnClickListener {
screenManager.push(SettingsScreen(carContext))
}
//.setFlags(Action.FLAG_IS_PERSISTENT)
.build()
)
val actionStripBuilder = createActionStripBuilder()
return if (routeModel.isNavigating()) {
if (calculateNewRoute) {
getNavigationLoadingTemplate(actionStripBuilder)
@@ -96,21 +99,7 @@ class NavigationScreen(
private fun getNavigationTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
actionStripBuilder.addAction(
Action.Builder()
.setTitle(carContext.getString(R.string.stop_action_title))
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_close_white_24dp
)
)
.build()
)
.setOnClickListener {
stopNavigation()
}
.build()
stopAction()
)
return NavigationTemplate.Builder()
.setNavigationInfo(
@@ -123,7 +112,7 @@ class NavigationScreen(
.build()
}
private fun getNavigationEndTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
private fun getNavigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
if (routeModel.isArrived()) {
val timer = object : CountDownTimer(10000, 10000) {
override fun onTick(millisUntilFinished: Long) {}
@@ -155,14 +144,43 @@ class NavigationScreen(
.setMapActionStrip(mapActionStripBuilder().build())
.build()
} else {
return NavigationTemplate.Builder()
.setBackgroundColor(CarColor.SECONDARY)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build())
.build()
return if (recentPlaceFound && recentPlaceActive) {
return getRecentPlaceTemplate()
} else {
NavigationTemplate.Builder()
.setBackgroundColor(CarColor.SECONDARY)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build())
.build()
}
}
}
fun getRecentPlaceTemplate(): Template {
val messageTemplate = MessageTemplate.Builder(
recentPlace.name + "\n"
+ recentPlace.city
)
.setHeader(
Header.Builder()
.setTitle(carContext.getString(R.string.drive_now))
.build()
)
.addAction(navigateAction())
.addAction(closeAction())
.build()
val builder = MapWithContentTemplate.Builder()
.setContentTemplate(messageTemplate)
.setActionStrip(
mapActionStripBuilder()
.addAction(settingsAction())
.addAction(searchAction())
.build()
)
return builder.build()
}
fun getNavigationLoadingTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
return NavigationTemplate.Builder()
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
@@ -198,58 +216,151 @@ class NavigationScreen(
}
}
private fun createActionStripBuilder(): ActionStrip.Builder {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
actionStripBuilder.addAction(
searchAction()
)
actionStripBuilder.addAction(
settingsAction()
)
return actionStripBuilder
}
private fun mapActionStripBuilder(): ActionStrip.Builder {
val actionStripBuilder = ActionStrip.Builder()
.addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_zoom_in_24
)
)
.build()
).setOnClickListener {
surfaceRenderer.handleScale(1)
}
.build()
)
.addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_zoom_out_24
)
)
.build()
).setOnClickListener {
surfaceRenderer.handleScale(-1)
}
.build())
.addAction(zoomPlus())
.addAction(zoomMinus())
if (surfaceRenderer.panView) {
actionStripBuilder.addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_pan_24
)
)
.build()
).setOnClickListener {
surfaceRenderer.panView = false
}
.build()
)
actionStripBuilder
.addAction(
panAction()
)
}
return actionStripBuilder
}
private fun stopAction(): Action {
return Action.Builder()
.setTitle(carContext.getString(R.string.stop_action_title))
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_close_white_24dp
)
)
.build()
)
.setOnClickListener {
stopNavigation()
}
.build()
}
private fun navigateAction(): Action {
return Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.assistant_navigation_48px
)
)
.build()
)
.setOnClickListener {
val navigateTo = location(recentPlace.latitude, recentPlace.longitude)
viewModel.loadRoute(surfaceRenderer.lastLocation, navigateTo)
routeModel.destination = recentPlace
}
.build()
}
private fun closeAction(): Action {
return Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_close_white_24dp
)
)
.build()
)
.setOnClickListener {
recentPlaceActive = false
invalidate()
}
.build()
}
private fun searchAction(): Action {
return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.ic_search_black36dp))
.setOnClickListener {
startSearchScreen()
}
.build()
}
private fun settingsAction(): Action {
return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_24px))
.setOnClickListener {
screenManager.push(SettingsScreen(carContext))
}
.build()
}
private fun zoomPlus(): Action {
return Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_zoom_in_24
)
)
.build()
).setOnClickListener {
surfaceRenderer.handleScale(1)
}
.build()
}
private fun zoomMinus(): Action {
return Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_zoom_out_24
)
)
.build()
).setOnClickListener {
surfaceRenderer.handleScale(-1)
}
.build()
}
private fun panAction(): Action {
return Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_pan_24
)
)
.build()
).setOnClickListener {
surfaceRenderer.panView = false
}
.build()
}
private fun getSuggestion(title: Int, subtitle: Int, icon: CarIcon): Suggestion {
return Suggestion.Builder()
.setIdentifier("0")
@@ -282,8 +393,8 @@ class NavigationScreen(
val location = Location(LocationManager.GPS_PROVIDER)
location.latitude = place.latitude
location.longitude = place.longitude
vieModel.saveRecent(place)
vieModel.loadRoute(surfaceRenderer.lastLocation, location)
viewModel.saveRecent(place)
viewModel.loadRoute(surfaceRenderer.lastLocation, location)
currentNavigationLocation = location
routeModel.destination = place
invalidate()
@@ -303,7 +414,7 @@ class NavigationScreen(
val mainThreadhandler = Handler(carContext.mainLooper)
mainThreadhandler.post {
object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) { }
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
calculateNewRoute = false
stopNavigation()
@@ -314,7 +425,7 @@ class NavigationScreen(
fun reRoute() {
NavigationMessage(carContext).createAlert()
vieModel.loadRoute(surfaceRenderer.lastLocation, currentNavigationLocation)
viewModel.loadRoute(surfaceRenderer.lastLocation, currentNavigationLocation)
}
fun updateTrip() {

View File

@@ -2,9 +2,11 @@ package com.kouros.navigation.car.screen
import android.location.Location
import android.net.Uri
import android.os.Build
import android.text.Spannable
import android.text.SpannableString
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen

View File

@@ -15,7 +15,6 @@
*/
package com.kouros.navigation.car.screen
import android.location.Geocoder
import android.location.Location
import android.location.LocationManager
import android.os.CountDownTimer
@@ -60,7 +59,6 @@ class RoutePreviewScreen(
private var mItemLimit = 0
private var street = ""
val vieModel = ViewModel(NavigationRepository())
val routeModel = RouteCarModel()
@@ -70,14 +68,7 @@ class RoutePreviewScreen(
if (route.isNotEmpty()) {
routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel)
val geocoder = Geocoder(carContext)
// nominatim ->
geocoder.getFromLocation(destination.latitude, destination.longitude, 1) {
for (address in it) {
street = address.getAddressLine(0)
}
invalidate()
}
invalidate()
}
}
@@ -192,7 +183,7 @@ class RoutePreviewScreen(
return Row.Builder()
.setTitle(route)
.setOnClickListener { onRouteSelected(index) }
.addText(street)
.addText( "${destination.street!!} ${destination.postalCode} ${destination.city}")
.addAction(action)
.build()
}

View File

@@ -37,7 +37,6 @@ class SearchScreen(
lateinit var searchResult: List<SearchResult>
val observer = Observer<List<SearchResult>> { newSearch ->
println(newSearch)
searchResult = newSearch
invalidate()
}

View File

@@ -364,4 +364,5 @@
<string name="route_preview">Routen</string>
<string name="recent_destinations">Letzte Ziele</string>
<string name="contacts">Kontakte</string>
<string name="drive_now">Jetzt losfahren</string>
</resources>

View File

@@ -491,4 +491,5 @@
<string name="route_preview">Routes</string>
<string name="recent_destinations">Recent</string>
<string name="contacts">Contacts</string>
<string name="drive_now">Drive now</string>
</resources>