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

@@ -14,8 +14,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 11
versionName = "0.1.3.11"
versionCode = 13
versionName = "0.1.3.13"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -2,8 +2,12 @@ package com.kouros.navigation
import android.app.Application
import android.content.Context
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.di.appModule
import com.kouros.navigation.model.ViewModel
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
@@ -26,5 +30,7 @@ class MainApplication : Application() {
private set
var useContacts = false
val navigationViewModel = ViewModel(ValhallaRepository())
}
}

View File

@@ -1,6 +1,8 @@
package com.kouros.navigation.di
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.ViewModel
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModelOf
@@ -8,5 +10,5 @@ import org.koin.dsl.module
val appModule = module {
viewModelOf(::ViewModel)
singleOf(::NavigationRepository)
singleOf(::ValhallaRepository)
}

View File

@@ -49,7 +49,7 @@ class MockLocation (private var locationManager: LocationManager) {
this.longitude = longitude
this.altitude = 0.0
this.accuracy = 1.0f
this.speed = 0f
this.speed = 10f
this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
@@ -72,7 +72,7 @@ class MockLocation (private var locationManager: LocationManager) {
this.longitude = longitude
this.altitude = 0.0
this.accuracy = 1.0f
this.speed = 0f
this.speed = 10f
this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()

View File

@@ -15,18 +15,13 @@ import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.BottomSheetScaffoldState
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@@ -37,19 +32,18 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.kouros.data.R
import com.kouros.navigation.data.Constants
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.MockLocation
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel
@@ -73,7 +67,7 @@ import kotlin.time.Duration.Companion.seconds
class MainActivity : ComponentActivity() {
val routeData = MutableLiveData("")
val viewModel = ViewModel(NavigationRepository())
val routeModel = RouteModel()
var tilt = 50.0
val useMock = true
@@ -109,8 +103,7 @@ class MainActivity : ComponentActivity() {
private var overpass = false
init {
viewModel.route.observe(this, observer)
navigationViewModel.route.observe(this, observer)
}
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
@@ -221,7 +214,7 @@ class MainActivity : ComponentActivity() {
closeSheet: () -> Unit
) {
if (!routeModel.isNavigating()) {
SearchSheet(applicationContext, viewModel, lastLocation) { closeSheet() }
SearchSheet(applicationContext, navigationViewModel, lastLocation) { closeSheet() }
} else {
NavigationSheet(
routeModel, step!!, nextStep!!,
@@ -241,7 +234,7 @@ class MainActivity : ComponentActivity() {
val currentLocation = location(location.position.longitude, location.position.latitude)
with(routeModel) {
if (isNavigating()) {
updateLocation(currentLocation)
updateLocation(currentLocation, navigationViewModel)
stepData.value = currentStep()
if (route.currentManeuverIndex + 1 <= route.maneuvers.size) {
nextStepData.value = nextStep()
@@ -266,7 +259,7 @@ class MainActivity : ComponentActivity() {
)
lastLocation = currentLocation
if (!loadRecentPlaces) {
viewModel.loadRecentPlaces(applicationContext, lastLocation)
navigationViewModel.loadRecentPlaces(applicationContext, lastLocation)
loadRecentPlaces = true
}
}
@@ -320,7 +313,7 @@ class MainActivity : ComponentActivity() {
fun test() {
for ((index, loc) in routeModel.route.waypoints.withIndex()) {
if (index > 300) {
routeModel.updateLocation(location(loc[0], loc[1]))
routeModel.updateLocation(location(loc[0], loc[1]), navigationViewModel)
routeModel.currentStep()
if (routeModel.route.currentManeuverIndex + 1 <= routeModel.route.maneuvers.size) {
nextStepData.value = routeModel.nextStep()

View File

@@ -1,62 +1,28 @@
package com.kouros.navigation.ui
import android.R.attr.x
import android.R.attr.y
import android.content.Context
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.window.layout.WindowMetricsCalculator
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.map.BuildingLayer
import com.kouros.navigation.car.map.DarkMode
import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
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.exponential
import org.maplibre.compose.expressions.dsl.interpolate
import org.maplibre.compose.expressions.dsl.zoom
import org.maplibre.compose.layers.CircleLayer
import org.maplibre.compose.layers.LineLayer
import org.maplibre.compose.location.LocationTrackingEffect
import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
import org.maplibre.compose.sources.GeoJsonData
import org.maplibre.compose.sources.GeoJsonSource
import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.sources.rememberGeoJsonSource
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds

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(
@@ -128,7 +131,7 @@ class RouteCarModel() : RouteModel() {
}
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()
@@ -156,7 +160,7 @@ class RouteCarModel() : RouteModel() {
): Action {
return Action.Builder()
.setOnClickListener { }
.setTitle(createCarText(carContext,titleRes))
.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)
}
}

View File

@@ -8,4 +8,6 @@ val RouteColor = Color(0xFF5582D0)
val SpeedColor = Color(0xFF262525)
val MaxSpeedColor = Color(0xFF262525)
val PlaceColor = Color(0xFF868005)

View File

@@ -27,34 +27,14 @@ import java.net.URL
import kotlinx.serialization.json.Json
class NavigationRepository {
abstract class NavigationRepository {
private val placesUrl = "https://kouros-online.de/maps/placespwd";
private val routeUrl = "https://kouros-online.de/valhalla/route?json="
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
// Road classes from highest to lowest are:
// motorway, trunk, primary, secondary, tertiary, unclassified, residential, service_other.
// exclude_toll
fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String {
SearchFilter
val vLocation = listOf(
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude, search_filter = searchFilter),
Locations(lat = location.latitude, lon = location.longitude, search_filter = searchFilter)
)
val valhallaLocation = ValhallaLocation(
locations = vLocation,
costing = "auto",
units = "km",
id = "my_work_route",
language = "de-DE"
)
val routeLocation = Json.encodeToString(valhallaLocation)
return fetchUrl(routeUrl + routeLocation, true)
}
abstract fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String
fun getRouteDistance(currentLocation: Location, location: Location, searchFilter: SearchFilter): Double {
val route = getRoute(currentLocation, location, searchFilter)
@@ -98,7 +78,7 @@ class NavigationRepository {
return places
}
private fun fetchUrl(url: String, authenticator : Boolean): String {
fun fetchUrl(url: String, authenticator : Boolean): String {
try {
if (authenticator) {
Authenticator.setDefault(object : Authenticator() {

View File

@@ -75,8 +75,8 @@ data class Route(
fun route(route: String) = apply {
if (route.isNotEmpty() && route != "[]") {
val gson = GsonBuilder().serializeNulls().create()
val valhalla = gson.fromJson(route, ValhallaJson::class.java)
trip = valhalla.trip
val routeJson = gson.fromJson(route, ValhallaJson::class.java)
trip = routeJson.trip
}
}

View File

@@ -0,0 +1,13 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Intersections(
@SerializedName("out") var out: Int? = null,
@SerializedName("entry") var entry: ArrayList<Boolean> = arrayListOf(),
@SerializedName("bearings") var bearings: ArrayList<Int> = arrayListOf(),
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(),
@SerializedName("lanes") var lanes: ArrayList<Lane> = arrayListOf(),
)

View File

@@ -0,0 +1,8 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Lane(
@SerializedName("valid" ) var valid: Boolean,
@SerializedName("indications" ) var indications: List<String>,
)

View File

@@ -0,0 +1,14 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Legs (
@SerializedName("steps" ) var steps : ArrayList<Steps> = arrayListOf(),
@SerializedName("weight" ) var weight : Double? = null,
@SerializedName("summary" ) var summary : String? = null,
@SerializedName("duration" ) var duration : Double? = null,
@SerializedName("distance" ) var distance : Double? = null
)

View File

@@ -0,0 +1,14 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Maneuver (
@SerializedName("bearing_after" ) var bearingAfter : Int? = null,
@SerializedName("bearing_before" ) var bearingBefore : Int? = null,
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(),
@SerializedName("modifier" ) var modifier : String? = null,
@SerializedName("type" ) var type : String? = null
)

View File

@@ -0,0 +1,12 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class OsrmJson (
@SerializedName("code" ) var code : String? = null,
@SerializedName("routes" ) var routes : ArrayList<Routes> = arrayListOf(),
@SerializedName("waypoints" ) var waypoints : ArrayList<Waypoints> = arrayListOf()
)

View File

@@ -0,0 +1,18 @@
package com.kouros.navigation.data.osrm
import android.location.Location
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
private const val routeUrl = "https://router.project-osrm.org/route/v1/driving/"
class OsrmRepository : NavigationRepository() {
override fun getRoute(
currentLocation: Location,
location: Location,
searchFilter: SearchFilter
): String {
val routeLocation = "${currentLocation.latitude},${currentLocation.longitude};${location.latitude},${location.longitude}?steps=true"
return fetchUrl(routeUrl + routeLocation, true)
}
}

View File

@@ -0,0 +1,15 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Routes (
@SerializedName("legs" ) var legs : ArrayList<Legs> = arrayListOf(),
@SerializedName("weight_name" ) var weightName : String? = null,
@SerializedName("geometry" ) var geometry : String? = null,
@SerializedName("weight" ) var weight : Double? = null,
@SerializedName("duration" ) var duration : Double? = null,
@SerializedName("distance" ) var distance : Double? = null
)

View File

@@ -0,0 +1,18 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Steps (
@SerializedName("intersections" ) var intersections : ArrayList<Intersections> = arrayListOf(),
@SerializedName("driving_side" ) var drivingSide : String? = null,
@SerializedName("geometry" ) var geometry : String? = null,
@SerializedName("maneuver" ) var maneuver : Maneuver? = Maneuver(),
@SerializedName("name" ) var name : String? = null,
@SerializedName("mode" ) var mode : String? = null,
@SerializedName("weight" ) var weight : Double? = null,
@SerializedName("duration" ) var duration : Double? = null,
@SerializedName("distance" ) var distance : Double? = null
)

View File

@@ -0,0 +1,13 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Waypoints (
@SerializedName("hint" ) var hint : String? = null,
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(),
@SerializedName("name" ) var name : String? = null,
@SerializedName("distance" ) var distance : Double? = null
)

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,8 @@ package com.kouros.navigation.data.overpass
import android.location.Location
import com.google.gson.GsonBuilder
import com.kouros.navigation.utils.GeoUtils.getBoundingBox2
import com.kouros.navigation.utils.GeoUtils.getOverpassBbox
import com.kouros.navigation.utils.NavigationUtils
import kotlinx.serialization.json.Json
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
@@ -12,8 +11,33 @@ import java.net.URL
class Overpass {
val overpassUrl = "https://overpass.kumi.systems/api/interpreter"
fun getAmenities(type: String, category: String, location: Location) : List<Elements> {
val boundingBox = getOverpassBbox(location, 5.0)
fun getAround(radius: Int, linestring: String) : List<Elements> {
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST"
httpURLConnection.setRequestProperty(
"Accept",
"application/json"
)
httpURLConnection.setDoOutput(true);
// define search query
val searchQuery = """
|[out:json];
|(
| way[highway](around:$radius,$linestring)
| ;
|);
|out body;
""".trimMargin()
return overpassApi(httpURLConnection, searchQuery)
}
fun getAmenities(
type: String,
category: String,
location: Location,
radius: Double
): List<Elements> {
val boundingBox = getOverpassBbox(location, radius)
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST"
httpURLConnection.setRequestProperty(
@@ -32,7 +56,10 @@ class Overpass {
|);
|out body;
""".trimMargin()
return overpassApi(httpURLConnection, searchQuery)
}
fun overpassApi(httpURLConnection: HttpURLConnection, searchQuery: String) : List<Elements> {
// Send the JSON we created
val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream)
outputStreamWriter.write(searchQuery)
@@ -44,7 +71,7 @@ class Overpass {
.use { it.readText() } // defaults to UTF-8
val gson = GsonBuilder().serializeNulls().create()
val overpass = gson.fromJson(response, Amenity::class.java)
println("Overpass: $type $response")
//println("Overpass: $response")
return overpass.elements
}
return emptyList()

View File

@@ -0,0 +1,29 @@
package com.kouros.navigation.data.valhalla
import android.location.Location
import com.kouros.navigation.data.Locations
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.ValhallaLocation
import kotlinx.serialization.json.Json
private const val routeUrl = "https://kouros-online.de/valhalla/route?json="
class ValhallaRepository : NavigationRepository() {
override fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String {
SearchFilter
val vLocation = listOf(
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude, search_filter = searchFilter),
Locations(lat = location.latitude, lon = location.longitude, search_filter = searchFilter)
)
val valhallaLocation = ValhallaLocation(
locations = vLocation,
costing = "auto",
units = "km",
id = "my_work_route",
language = "de-DE"
)
val routeLocation = Json.encodeToString(valhallaLocation)
return fetchUrl(routeUrl + routeLocation, true)
}
}

View File

@@ -4,16 +4,16 @@ import android.location.Location
import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Step
import com.kouros.data.R
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.ManeuverType
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.location
import org.maplibre.turf.TurfMeasurement
import org.maplibre.turf.TurfMisc
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
@@ -21,13 +21,18 @@ open class RouteModel() {
data class RouteState(
val route: Route? = null,
val isNavigating: Boolean = false,
var destination: Place = Place(),
val destination: Place = Place(),
val arrived: Boolean = false,
var maneuverType: Int = 0,
val maneuverType: Int = 0,
var currentShapeIndex: Int = 0,
var distanceToStepEnd: Float = 0F,
var beginIndex: Int = 0,
var endIndex: Int = 0
var endIndex: Int = 0,
val travelMessage: String = "",
// max speed for street (maneuver)
val lastSpeedIndex: Int = 0,
val maxSpeed: Int = 0,
)
var routeState = RouteState()
@@ -61,7 +66,8 @@ open class RouteModel() {
)
}
fun updateLocation(location: Location) {
@OptIn(DelicateCoroutinesApi::class)
fun updateLocation(location: Location, viewModel: ViewModel) {
var nearestDistance = 100000.0f
var newShapeIndex = -1
// find nearest waypoint and current shape index
@@ -77,6 +83,24 @@ open class RouteModel() {
// find maneuver
// calculate distance to step end
findManeuver(newShapeIndex)
GlobalScope.launch(Dispatchers.IO) {
updateSpeedLimit(location, viewModel)
}
}
fun updateSpeedLimit(location: Location, viewModel: ViewModel) {
// speed limit for each maneuver index
if (routeState.lastSpeedIndex < route.currentManeuverIndex) {
routeState = routeState.copy(lastSpeedIndex = route.currentManeuverIndex)
val elements = viewModel.getMaxSpeed(location)
elements.forEach {
if (it.tags.name != null && it.tags.maxspeed != null) {
val speed = it.tags.maxspeed!!.toInt()
routeState = routeState.copy(maxSpeed = speed)
}
}
}
}
private fun findManeuver(newShapeIndex: Int) {
@@ -127,7 +151,7 @@ open class RouteModel() {
maneuverType = relevantManeuver.type
}
val maneuverIconPair = maneuverIcon(maneuverType)
routeState.maneuverType = maneuverIconPair.first
routeState = routeState.copy(maneuverType = maneuverIconPair.first)
// Construct and return the final StepData object
return StepData(
streetName,
@@ -149,6 +173,7 @@ open class RouteModel() {
when (distanceLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> {
}
else -> {
if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) {
text = maneuver.streetNames[0]

View File

@@ -251,7 +251,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
fun getAmenities(category: String, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("amenity", category, location)
val amenities = Overpass().getAmenities("amenity", category, location, 5.0)
val distAmenities = mutableListOf<Elements>()
amenities.forEach {
val plLocation =
@@ -267,7 +267,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
fun getSpeedCameras(location: Location) {
viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("highway", "speed_camera", location)
val amenities = Overpass().getAmenities("highway", "speed_camera", location, 5.0)
val distAmenities = mutableListOf<Elements>()
amenities.forEach {
val plLocation =
@@ -281,6 +281,12 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
fun getMaxSpeed(location: Location) : List<Elements> {
val lineString = "${location.latitude},${location.longitude}"
val amenities = Overpass().getAround(10, lineString)
return amenities
}
fun saveFavorite(place: Place) {
place.category = Constants.FAVORITES
savePlace(place)

View File

@@ -74,10 +74,10 @@ fun calculateZoom(speed: Double?): Double {
val speedKmh = (speed * 3.6).toInt()
val zoom = when (speedKmh) {
in 0..10 -> 18.0
in 11..30 -> 17.0
in 31..50 -> 16.0
in 61..70 -> 15.0
else -> 14
in 11..30 -> 17.5
in 31..50 -> 17.0
in 61..70 -> 16.5
else -> 16.0
}
return zoom.toDouble()
}

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500Z"/>
</vector>