Speed limit

This commit is contained in:
Dimitris
2025-12-22 11:08:53 +01:00
parent 9e453dc955
commit e5c74f6fa2
36 changed files with 1421 additions and 167 deletions

View File

@@ -25,7 +25,9 @@ import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.GeoUtils.snapLocation
class NavigationSession : Session(), NavigationScreen.Listener {
@@ -67,6 +69,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
}
val navigationViewModel = ViewModel(ValhallaRepository())
init {
val lifecycle: Lifecycle = lifecycle
@@ -78,7 +81,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel)
navigationScreen = NavigationScreen(carContext, surfaceRenderer, routeModel, this)
navigationScreen = NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel)
if (carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED && !useContacts
@@ -116,6 +119,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
carContext,
surfaceRenderer,
location,
navigationViewModel,
// TODO: Uri
)
) { obj: Any? ->

View File

@@ -28,7 +28,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.kouros.navigation.car.map.DarkMode
import com.kouros.navigation.car.map.DrawImage
import com.kouros.navigation.car.map.DrawNavigationImages
import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.cameraState
import com.kouros.navigation.car.map.getPaddingValues
@@ -191,7 +191,7 @@ class SurfaceRenderer(
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
val currentSpeed: Float? by speed.observeAsState()
if (viewStyle == ViewStyle.VIEW) {
DrawImage(paddingValues, currentSpeed, width, height)
DrawNavigationImages(paddingValues, currentSpeed, routeModel.routeState.maxSpeed, width, height)
}
LaunchedEffect(position, viewStyle) {
cameraState.animateTo(

View File

@@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -33,13 +34,10 @@ import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.expressions.dsl.const
import org.maplibre.compose.expressions.dsl.contains
import org.maplibre.compose.expressions.dsl.exponential
import org.maplibre.compose.expressions.dsl.image
import org.maplibre.compose.expressions.dsl.interpolate
@@ -60,15 +58,7 @@ import org.maplibre.compose.sources.Source
import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.sources.rememberGeoJsonSource
import org.maplibre.compose.style.BaseStyle
import org.maplibre.geojson.FeatureCollection
import org.maplibre.spatialk.geojson.BoundingBox.Companion.serializer
import org.maplibre.spatialk.geojson.Feature
import org.maplibre.spatialk.geojson.GeoJson
import org.maplibre.spatialk.geojson.Geometry
import org.maplibre.spatialk.geojson.Position
import org.maplibre.spatialk.geojson.dsl.FeatureBuilder
import org.maplibre.spatialk.geojson.dsl.FeatureCollectionBuilder
import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection
@Composable
@@ -187,9 +177,12 @@ fun BuildingLayer(tiles: Source) {
}
@Composable
fun DrawImage(padding: PaddingValues, speed: Float?, width: Int, height: Int) {
fun DrawNavigationImages(padding: PaddingValues, speed: Float?, maxSpeed: Int, width: Int, height: Int) {
NavigationImage(padding, width, height)
Speed(width, height, speed)
CurrentSpeed(width, height, speed)
if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) {
MaxSpeed(width, height, maxSpeed)
}
}
@Composable
@@ -215,7 +208,7 @@ fun NavigationImage(padding: PaddingValues, width: Int, height: Int) {
}
@Composable
private fun Speed(
private fun CurrentSpeed(
width: Int,
height: Int,
speed: Float?
@@ -235,6 +228,7 @@ private fun Speed(
val kmh = "km/h"
val styleSpeed = TextStyle(
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
)
val styleKm = TextStyle(
@@ -278,6 +272,61 @@ private fun Speed(
}
}
@Composable
private fun MaxSpeed(
width: Int,
height: Int,
maxSpeed: Int,
) {
val radius = 20
Box(
modifier = Modifier
.padding(
start = width.dp - 350.dp,
top = height.dp - 80.dp
),
contentAlignment = Alignment.Center
) {
val textMeasurerSpeed = rememberTextMeasurer()
val speed = maxSpeed.toString()
val styleSpeed = TextStyle(
fontSize = 26.sp,
fontWeight = FontWeight.Bold,
color = Color.Black,
)
val textLayoutSpeed = remember(speed) {
textMeasurerSpeed.measure(speed, styleSpeed)
}
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(
center = Offset(
x = center.x,
y = center.y
),
radius = radius * 1.3.toFloat(),
color = Color.Red,
)
drawCircle(
center = Offset(
x = center.x,
y = center.y
),
radius = radius.toFloat(),
color = Color.White,
)
drawText(
textMeasurer = textMeasurerSpeed,
text = speed,
style = styleSpeed,
topLeft = Offset(
x = center.x - textLayoutSpeed.size.width / 2,
y = center.y - textLayoutSpeed.size.height / 2,
)
)
}
}
}
@Composable
fun DarkMode(context: Context, baseStyle: MutableState<BaseStyle.Uri>) {
val darkMode = getIntKeyValue(context, Constants.DARK_MODE_SETTINGS)

View File

@@ -74,7 +74,7 @@ class RouteCarModel() : RouteModel() {
return step
}
fun travelEstimate(): TravelEstimate {
fun travelEstimate(carContext: CarContext): TravelEstimate {
val timeLeft = travelLeftTime()
val timeToDestinationMillis =
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
@@ -88,7 +88,7 @@ class RouteCarModel() : RouteModel() {
arrivalTime(),
TimeZone.getTimeZone("Europe/Berlin")
)
return TravelEstimate.Builder( // The estimated distance to the destination.
val travelBuilder = TravelEstimate.Builder( // The estimated distance to the destination.
Distance.create(
leftDistance,
displayUnit
@@ -102,9 +102,12 @@ class RouteCarModel() : RouteModel() {
)
.setRemainingTimeColor(CarColor.YELLOW)
.setRemainingDistanceColor(CarColor.RED)
//.setTripText(createCarText(carContext,R.string.navigate))
//.setTripIcon(createCarIcon(carContext, R.drawable.navigation_48px))
.build()
if (routeState.travelMessage.isNotEmpty()) {
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px))
travelBuilder.setTripText(CarText.create(routeState.travelMessage))
}
return travelBuilder.build()
}
fun createString(
@@ -123,12 +126,12 @@ class RouteCarModel() : RouteModel() {
}
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String?) {
carContext.getCarService<AppManager?>(AppManager::class.java)
.showAlert(createAlert(carContext, distance, maxSpeed))
carContext.getCarService<AppManager?>(AppManager::class.java)
.showAlert(createAlert(carContext, distance, maxSpeed))
}
fun createAlert(carContext: CarContext, distance: Double, maxSpeed: String?): Alert {
val title = createCarText(carContext,R.string.speed_camera)
val title = createCarText(carContext, R.string.speed_camera)
val subtitle = CarText.create(maxSpeed!!)
val icon = CarIcon.ALERT
@@ -144,6 +147,7 @@ class RouteCarModel() : RouteModel() {
.addAction(dismissAction).setCallback(object : AlertCallback {
override fun onCancel(reason: Int) {
}
override fun onDismiss() {
}
}).build()
@@ -155,8 +159,8 @@ class RouteCarModel() : RouteModel() {
flags: Int
): Action {
return Action.Builder()
.setOnClickListener { }
.setTitle(createCarText(carContext,titleRes))
.setOnClickListener { }
.setTitle(createCarText(carContext, titleRes))
.setFlags(flags)
.build()
}

View File

@@ -18,11 +18,13 @@ import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.model.ViewModel
class CategoriesScreen(
private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer,
private val location: Location,
private val viewModel: ViewModel
) : Screen(carContext) {
var categories: List<Category> = listOf(
@@ -46,7 +48,8 @@ class CategoriesScreen(
carContext,
surfaceRenderer,
location,
it.id
it.id,
viewModel
)
) { obj: Any? ->
if (obj != null) {

View File

@@ -33,9 +33,9 @@ class CategoryScreen(
private val surfaceRenderer: SurfaceRenderer,
location: Location,
private val category: String,
private val viewModel: ViewModel
) : Screen(carContext) {
val viewModel = ViewModel(NavigationRepository())
var elements = listOf<Elements>()
val observer = Observer<List<Elements>> { newElements ->
@@ -113,7 +113,6 @@ class CategoryScreen(
.setOnClickListener {
val location = location(it.lon!!, it.lat!!)
surfaceRenderer.setCategoryLocation(location, category)
println(it)
}
.setTitle(name)
.setImage(carIcon(carContext, category))

View File

@@ -11,6 +11,7 @@ import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.Distance
import androidx.car.app.model.Header
import androidx.car.app.model.MessageTemplate
@@ -39,7 +40,8 @@ class NavigationScreen(
carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer,
private var routeModel: RouteCarModel,
private var listener: Listener
private var listener: Listener,
private val viewModel: ViewModel
) :
Screen(carContext) {
@@ -52,7 +54,7 @@ class NavigationScreen(
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
var recentPlace = Place()
var navigationType = NavigationType.VIEW
val viewModel = ViewModel(NavigationRepository())
val observer = Observer<String> { route ->
if (route.isNotEmpty()) {
navigationType = NavigationType.NAVIGATION
@@ -88,7 +90,6 @@ class NavigationScreen(
var speedCameras = listOf<Elements>()
val speedObserver = Observer<List<Elements>> { cameras ->
speedCameras = cameras
println("Speed cameras ${speedCameras.size}")
}
init {
@@ -118,7 +119,7 @@ class NavigationScreen(
.setNavigationInfo(
getRoutingInfo()
)
.setDestinationTravelEstimate(routeModel.travelEstimate())
.setDestinationTravelEstimate(routeModel.travelEstimate(carContext))
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build())
.setBackgroundColor(CarColor.GREEN)
@@ -301,7 +302,7 @@ class NavigationScreen(
.setOnClickListener {
val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, navigateTo)
routeModel.routeState.destination = recentPlace
routeModel.routeState = routeModel.routeState.copy(destination = recentPlace)
}
.build()
}
@@ -394,7 +395,7 @@ class NavigationScreen(
private fun startSearchScreen() {
screenManager
.pushForResult(
SearchScreen(carContext, surfaceRenderer, surfaceRenderer.lastLocation)
SearchScreen(carContext, surfaceRenderer, surfaceRenderer.lastLocation, viewModel)
) { obj: Any? ->
if (obj != null) {
val place = obj as Place
@@ -417,7 +418,7 @@ class NavigationScreen(
viewModel.saveRecent(place)
currentNavigationLocation = location
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, location)
routeModel.routeState.destination = place
routeModel.routeState = routeModel.routeState.copy(destination = place)
invalidate()
}
@@ -430,8 +431,8 @@ class NavigationScreen(
}
fun calculateNewRoute(destination: Place) {
navigationType = NavigationType.REROUTE
stopNavigation()
navigationType = NavigationType.REROUTE
invalidate()
val mainThreadHandler = Handler(carContext.mainLooper)
mainThreadHandler.post {
@@ -451,14 +452,9 @@ class NavigationScreen(
}
fun updateTrip(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
viewModel.getSpeedCameras(location)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)
}
updateSpeedCamera(location)
with(routeModel) {
updateLocation(location)
updateLocation(location, viewModel)
if (routeState.maneuverType == Maneuver.TYPE_DESTINATION
&& leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
@@ -472,6 +468,15 @@ class NavigationScreen(
invalidate()
}
private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
viewModel.getSpeedCameras(location)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)
}
}
private fun updateDistance(
location: Location,
) {
@@ -485,10 +490,9 @@ class NavigationScreen(
}
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val camera = sortedList.first()
if (camera.distance < 100) {
if (camera.distance < 80) {
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
}
}
}

View File

@@ -37,10 +37,10 @@ class PlaceListScreen(
private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer,
private val location: Location,
private val category: String
private val category: String,
private val viewModel: ViewModel
) : Screen(carContext) {
val viewModel = ViewModel(NavigationRepository())
var places = listOf<Place>()
val observer = Observer<List<Place>> { newPlaces ->
@@ -102,7 +102,8 @@ class PlaceListScreen(
RoutePreviewScreen(
carContext,
surfaceRenderer,
place
place,
viewModel
)
) { obj: Any? ->
if (obj != null) {

View File

@@ -41,13 +41,12 @@ import java.math.RoundingMode
class RoutePreviewScreen(
carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer,
private var destination: Place
private var destination: Place,
private val viewModel: ViewModel
) :
Screen(carContext) {
private var isFavorite = false
val vieModel = ViewModel(NavigationRepository())
val routeModel = RouteCarModel()
val navigationMessage = NavigationMessage(carContext)
@@ -60,9 +59,9 @@ class RoutePreviewScreen(
}
init {
vieModel.previewRoute.observe(this, observer)
viewModel.previewRoute.observe(this, observer)
val location = location(destination.longitude, destination.latitude)
vieModel.loadPreviewRoute(carContext, surfaceRenderer.lastLocation, location)
viewModel.loadPreviewRoute(carContext, surfaceRenderer.lastLocation, location)
}
override fun onGetTemplate(): Template {
@@ -149,7 +148,7 @@ class RoutePreviewScreen(
CarToast.LENGTH_SHORT
)
.show()
vieModel.saveFavorite(destination)
viewModel.saveFavorite(destination)
invalidate()
}
.build()
@@ -157,7 +156,7 @@ class RoutePreviewScreen(
private fun deleteFavoriteAction(): Action = Action.Builder()
.setOnClickListener {
if (isFavorite) {
vieModel.deleteFavorite(destination)
viewModel.deleteFavorite(destination)
}
isFavorite = !isFavorite
finish()

View File

@@ -28,7 +28,8 @@ import com.kouros.navigation.model.ViewModel
class SearchScreen(
carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer,
private var location: Location
private var location: Location,
private val viewModel: ViewModel
) : Screen(carContext) {
var isSearchComplete: Boolean = false
@@ -47,8 +48,6 @@ class SearchScreen(
invalidate()
}
val viewModel = ViewModel(NavigationRepository())
init {
viewModel.searchPlaces.observe(this, observer)
}
@@ -75,6 +74,7 @@ class SearchScreen(
carContext,
surfaceRenderer,
location,
viewModel
)
) { obj: Any? ->
surfaceRenderer.viewStyle = ViewStyle.VIEW
@@ -90,7 +90,8 @@ class SearchScreen(
carContext,
surfaceRenderer,
location,
it.id
it.id,
viewModel
)
) { obj: Any? ->
if (obj != null) {

View File

@@ -34,6 +34,5 @@ class ViewModelTest {
val route = repo.getRoute(fromLocation, toLocation, SearchFilter())
model.startNavigation(route)
println(route)
}
}