This commit is contained in:
Dimitris
2026-04-22 17:44:08 +02:00
parent f388ba0fb8
commit 0fa625d785
38 changed files with 658 additions and 190 deletions
+4 -4
View File
@@ -11,14 +11,14 @@ val properties = Properties().apply {
android { android {
namespace = "com.kouros.navigation" namespace = "com.kouros.navigation"
compileSdk = 36 compileSdk = 37
defaultConfig { defaultConfig {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 37
versionCode = 94 versionCode = 97
versionName = "0.2.3.94" versionName = "0.2.3.97"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -0,0 +1,96 @@
package com.kouros.navigation.car
import android.location.Location
import android.location.LocationManager
import com.kouros.navigation.data.overpass.Overpass
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Test
class OverpassTest {
val location = Location(LocationManager.GPS_PROVIDER)
@Test
fun `maxSpeed Schmalkaldener 30 `() {
val curLocation = location(11.582495, 48.186863)
executeSpeedTest( curLocation, "Schmalkaldener Straße", emptyList(), 90, 30)
}
@Test
fun `maxSpeed Ingolstädter 50 `() {
val curLocation = location(11.584384, 48.186338)
executeSpeedTest( curLocation, "Ingolstädter Straße", listOf("B13"), 180, 50)
}
@Test
fun `maxSpeed Isarring 50 `() {
val curLocation = location(11.5989114, 48.1694783)
executeSpeedTest( curLocation, "Isarring", listOf("B2R"), 190, 50)
}
@Test
fun `maxSpeed A94 `() {
val curLocation = location(11.88117, 48.16595)
executeSpeedTest( curLocation, "", listOf("A94", "E552"), 90, 130)
}
@Test
fun `maxSpeed Fendsbach `() {
val curLocation = location(11.94989, 48.21522)
executeSpeedTest( curLocation, "Fendsbach", listOf("St 2331"), 0, 60)
}
@Test
fun `maxSpeed Leopoldstraße `() {
val locations = listOf(
location(11.5854771, 48.1778470),
location(11.5855582, 48.1756081),
location(11.5854672, 48.1753093),
location(11.5850147, 48.1774400)
)
executeSpeedTest( locations[0], "Leopoldstraße", emptyList(), 0, 50)
executeSpeedTest( locations[1], "Leopoldstraße", emptyList(), 180, 30)
executeSpeedTest( locations[2], "Leopoldstraße", emptyList(), 180, 30)
executeSpeedTest( locations[3], "Leopoldstraße", emptyList(), 180, 50)
}
@Test
fun `maxSpeed Egnatia `() {
val locations = listOf(
location(20.645487, 39.552875),
location(20.686672, 39.838547),
)
executeSpeedTest( locations[0], "", listOf("E90", "E92"), 100, 120)
executeSpeedTest( locations[1], "", listOf("E853"), 320, 90)
}
fun executeSpeedTest(
curLocation: Location,
street: String,
roadNumbers: List<String>,
routeBearing: Int,
result: Int
) {
val viewModel = NavigationViewModel(TomTomRepository())
val lineString = "${curLocation.latitude},${curLocation.longitude}"
val elements = Overpass().getSpeedLimit(600F, lineString, street, roadNumbers)
viewModel.speedElements.addAll(elements)
assertNotEquals(0, viewModel.speedElements.size)
val speed = viewModel.calculateSpeedLimit(
curLocation,
routeBearing.toFloat(),
"DEU",
)
assertEquals(result, speed)
}
}
@@ -6,12 +6,14 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kouros.navigation.data.Constants.homeHohenwaldeck import com.kouros.navigation.data.Constants.homeHohenwaldeck
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.overpass.Overpass
import com.kouros.navigation.data.route.ManeuverType import com.kouros.navigation.data.route.ManeuverType
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Test import org.junit.Test
@@ -69,7 +71,7 @@ class RouteModelTest {
val repository = getSettingsRepository(appContext) val repository = getSettingsRepository(appContext)
runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) } runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) }
val routeJsonString = TomTomRepository().fetchUrl( val routeJsonString = TomTomRepository().fetchUrl(
"https://kouros-online.de/tomtom_routing.json", "http://192.168.1.37/tomtom_routing.json",
false false
) )
assertNotEquals("", routeJsonString) assertNotEquals("", routeJsonString)
@@ -201,6 +203,7 @@ class RouteModelTest {
@Test @Test
fun simulate() { fun simulate() {
val viewModel = NavigationViewModel(TomTomRepository())
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) { for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
val curLocation = location(waypoint[0], waypoint[1]) val curLocation = location(waypoint[0], waypoint[1])
@@ -244,9 +247,12 @@ class RouteModelTest {
val curLocation = location(waypoint[0], waypoint[1]) val curLocation = location(waypoint[0], waypoint[1])
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
if (index in 16..43) { if (index in 16..43) {
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(
curLocation,
NavigationViewModel(TomTomRepository())
)
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.leftStepDistance, distance[index-16], 1.0) assertEquals(stepData.leftStepDistance, distance[index - 16], 1.0)
} }
} }
} }
@@ -11,6 +11,12 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.kouros.data.BuildConfig
import com.kouros.navigation.data.Constants.a9
import com.kouros.navigation.data.Constants.a94
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.Constants.ioannina
import com.kouros.navigation.data.Constants.subislawa
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
@@ -92,21 +98,29 @@ class DeviceLocationManager(
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5f) { fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5f) {
if (isListening) return if (isListening) return
val setIndividualLocation = BuildConfig.DEBUG
// Get and deliver last known location first // Get and deliver last known location first
val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (lastLocation != null) { if (lastLocation != null) {
onInitialLocation(lastLocation) if (setIndividualLocation) {
onLocationUpdate(lastLocation) onInitialLocation(homeVogelhart)
onLocationUpdate(homeVogelhart)
} else {
onInitialLocation(lastLocation)
onLocationUpdate(lastLocation)
}
} }
// Start continuous location updates // Start continuous location updates
locationManager.requestLocationUpdates( if (!setIndividualLocation) {
LocationManager.GPS_PROVIDER, locationManager.requestLocationUpdates(
minTimeMs, LocationManager.GPS_PROVIDER,
minDistanceM, minTimeMs,
locationListener minDistanceM,
) locationListener
)
}
isListening = true isListening = true
} }
@@ -112,6 +112,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
var lastRouteDate: LocalDateTime = LocalDateTime.now() var lastRouteDate: LocalDateTime = LocalDateTime.now()
var lastAlert : Long = 0
var navigationManagerStarted = false var navigationManagerStarted = false
/** /**
@@ -462,29 +463,30 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
* Snaps location to route and checks for deviation requiring reroute. * Snaps location to route and checks for deviation requiring reroute.
*/ */
private fun handleNavigationLocation(location: Location) { private fun handleNavigationLocation(location: Location) {
val startTime = System.currentTimeMillis()
routeModel.updateLocation(location, navigationViewModel) routeModel.updateLocation(location, navigationViewModel)
if (routeModel.navState.arrived) return val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
if (guidanceAudio == 1) {
handleGuidanceAudio()
}
val streetName = routeModel.currentStep().street val streetName = routeModel.currentStep().street
val currentDate = LocalDateTime.now(ZoneOffset.UTC) if (checkLocationDeviation(location, snappedLocation, streetName)) {
if (routeModel.navState.arrived) return
if (snapLocation(location, streetName)) { if (guidanceAudio == 1) {
checkTraffic(currentDate, location) handleGuidanceAudio()
updateSpeedCamera(location) }
checkRoute(currentDate, location) val currentDate = LocalDateTime.now(ZoneOffset.UTC)
checkTraffic(currentDate, snappedLocation)
updateSpeedCamera(snappedLocation)
checkRoute(currentDate, snappedLocation)
updateNavigationScreen() updateNavigationScreen()
checkArrival() checkArrival()
} }
val endTime = System.currentTimeMillis() - startTime
Log.d(TAG, "handleNavigationLocation: $endTime")
} }
/** /**
* Updates the surface renderer with snapped location and street name. * Checks if the location deviation is acceptable.
* Checks if maximal route deviation is exceeded and reroutes if needed.
*/ */
private fun snapLocation(location: Location, streetName: String): Boolean { private fun checkLocationDeviation(location: Location, snappedLocation: Location, streetName: String): Boolean {
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snappedLocation) val distance = location.distanceTo(snappedLocation)
when { when {
distance > MAXIMAL_ROUTE_DEVIATION -> { distance > MAXIMAL_ROUTE_DEVIATION -> {
@@ -514,6 +516,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
return return
} }
val currentStep = routeModel.route.currentStep()
val stepData = routeModel.currentStep()
navigationScreen.updateTrip( navigationScreen.updateTrip(
isNavigating = routeModel.isNavigating(), isNavigating = routeModel.isNavigating(),
isRerouting = false, isRerouting = false,
@@ -526,7 +531,8 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
shouldShowNextStep = false, shouldShowNextStep = false,
shouldShowLanes = true, shouldShowLanes = true,
junctionImage = null, junctionImage = null,
backGroundColor = routeModel.backGroundColor() backGroundColor = routeModel.backGroundColor(),
message = stepData.message,
) )
/** /**
@@ -786,18 +792,15 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
val sortedList = updatedCameras.sortedWith(compareBy { it.distance }) val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val camera = sortedList.firstOrNull() ?: return val camera = sortedList.firstOrNull() ?: return
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location) val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
val bearingSpeedCamera = if (camera.tags.direction != null) { val bearingSpeedCamera = try {
try { camera.tags.direction.toFloat()
camera.tags.direction!!.toFloat() } catch (e: Exception) {
} catch (e: Exception) { 0F
0F
}
} else {
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
} }
if (camera.distance < 80) { if (camera.distance < 80 && (System.currentTimeMillis() - lastAlert > 5000)) {
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) { if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed) routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
lastAlert = System.currentTimeMillis()
} }
} }
} }
@@ -8,7 +8,6 @@ import android.location.Location
import android.util.Log import android.util.Log
import androidx.car.app.AppManager import androidx.car.app.AppManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Session
import androidx.car.app.SurfaceCallback import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer import androidx.car.app.SurfaceContainer
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -156,8 +155,6 @@ class SurfaceRenderer(
Log.i(TAG, "Surface available $surfaceContainer") Log.i(TAG, "Surface available $surfaceContainer")
lifecycleOwner = CustomLifecycleOwner() lifecycleOwner = CustomLifecycleOwner()
lifecycleOwner.performRestore(null) lifecycleOwner.performRestore(null)
// technically, we only really need any one of these instead of all 3
// add them to be consistent with the actual lifecycle.
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START) lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
@@ -301,7 +298,7 @@ class SurfaceRenderer(
) { ) {
val cameraDuration = val cameraDuration =
duration( duration(
viewStyle == ViewStyle.PREVIEW, viewStyle,
position!!.bearing, position!!.bearing,
lastBearing, lastBearing,
lastLocationUpdate lastLocationUpdate
@@ -351,13 +348,11 @@ class SurfaceRenderer(
viewStyle = ViewStyle.PAN_VIEW viewStyle = ViewStyle.PAN_VIEW
} }
val newZoom = if (zoomSign < 0) { val newZoom = if (zoomSign < 0) {
cameraPosition.value!!.zoom - 0.2 cameraPosition.value!!.zoom - 1
} else { } else {
cameraPosition.value!!.zoom + 0.2 cameraPosition.value!!.zoom + 1
}
if (viewStyle == ViewStyle.VIEW) {
tilt = calculateTilt(newZoom, tilt)
} }
tilt = calculateTilt(viewStyle, newZoom, tilt)
updateCameraPosition( updateCameraPosition(
cameraPosition.value!!.bearing, cameraPosition.value!!.bearing,
newZoom, newZoom,
@@ -411,6 +406,7 @@ class SurfaceRenderer(
synchronized(this) { synchronized(this) {
routeData.value = routeGeoJson routeData.value = routeGeoJson
viewStyle = ViewStyle.VIEW viewStyle = ViewStyle.VIEW
updateLocation(lastLocation, "")
} }
} }
@@ -479,7 +475,7 @@ class SurfaceRenderer(
} }
viewStyle = ViewStyle.VIEW viewStyle = ViewStyle.VIEW
val zoom = calculateZoom(0.0) val zoom = calculateZoom(0.0)
tilt = calculateTilt(zoom, tilt) tilt = calculateTilt(viewStyle, zoom, tilt)
updateCameraPosition( updateCameraPosition(
tilt = tilt, tilt = tilt,
zoom = zoom, zoom = zoom,
@@ -1,7 +1,6 @@
package com.kouros.navigation.car.map package com.kouros.navigation.car.map
import android.location.Location import android.location.Location
import android.util.Log
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -33,6 +32,7 @@ import com.kouros.navigation.data.NavigationColorLight
import com.kouros.navigation.data.RouteColor import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.utils.GeoUtils.createEndCollection
import com.kouros.navigation.utils.isMetricSystem import com.kouros.navigation.utils.isMetricSystem
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
@@ -49,11 +49,8 @@ import org.maplibre.compose.layers.Anchor
import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.FillLayer
import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.layers.LineLayer
import org.maplibre.compose.layers.SymbolLayer import org.maplibre.compose.layers.SymbolLayer
import org.maplibre.compose.location.LocationPuck
import org.maplibre.compose.location.LocationPuckColors import org.maplibre.compose.location.LocationPuckColors
import org.maplibre.compose.location.LocationPuckSizes import org.maplibre.compose.location.LocationPuckSizes
import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.map.GestureOptions
import org.maplibre.compose.map.MapOptions 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.map.OrnamentOptions
@@ -115,6 +112,7 @@ fun MapLibre(
AmenityLayer(route) AmenityLayer(route)
} else { } else {
RouteLayer(route, traffic!!) RouteLayer(route, traffic!!)
StartEndLayer(route)
//RouteLayerPoint(route ) //RouteLayerPoint(route )
} }
SpeedCameraLayer(speedCameras) SpeedCameraLayer(speedCameras)
@@ -123,6 +121,30 @@ fun MapLibre(
} }
} }
@Composable
fun StartEndLayer(routeData: String?) {
if (!routeData.isNullOrEmpty()) {
val end = createEndCollection(routeData)
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(end))
val img = image(painterResource(R.drawable.sports_score_48px), drawAsSdf = true)
SymbolLayer(
id = "end-layer",
source = routes,
iconColor = const(Color.Black),
iconImage = img,
iconSize =
interpolate(
type = exponential(2.0f),
input = zoom(),
5 to const(2.0f),
10 to const(2.0f),
15 to const(3.0f),
20 to const(4.0f),
),
)
}
}
@Composable @Composable
fun RouteLayer(routeData: String?, trafficData: Map<String, String>) { fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
if (!routeData.isNullOrEmpty()) { if (!routeData.isNullOrEmpty()) {
@@ -308,8 +330,8 @@ fun DrawNavigationImages(
if (speed != null) { if (speed != null) {
CurrentSpeed(width, height, speed, maxSpeed) CurrentSpeed(width, height, speed, maxSpeed)
} }
if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) { if (speed != null && maxSpeed > 0) { // && (speed * 3.6) > maxSpeed) {
MaxSpeed(width, height, maxSpeed) MaxSpeed(width, height, maxSpeed, speed)
} }
//DebugInfo(width, height, lat!!) //DebugInfo(width, height, lat!!)
} }
@@ -325,9 +347,9 @@ fun NavigationImage(
val imageSize = (height / 8) val imageSize = (height / 8)
val navigationColor = if (darkMode) val navigationColor = if (darkMode)
remember { NavigationColorDark }
else
remember { NavigationColorLight } remember { NavigationColorLight }
else
remember { NavigationColorDark }
val textMeasurerStreet = rememberTextMeasurer() val textMeasurerStreet = rememberTextMeasurer()
val street = streetName.toString() val street = streetName.toString()
@@ -397,7 +419,7 @@ private fun CurrentSpeed(
maxSpeed: Int maxSpeed: Int
) { ) {
val radius = 34 val radius = 36
Box( Box(
modifier = Modifier modifier = Modifier
.padding( .padding(
@@ -414,12 +436,12 @@ private fun CurrentSpeed(
val kmh = if (isMetricSystem()) "km/h" else "mph" val kmh = if (isMetricSystem()) "km/h" else "mph"
val styleSpeed = TextStyle( val styleSpeed = TextStyle(
fontSize = 22.sp, fontSize = 24.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = Color.White, color = Color.White,
) )
val styleKm = TextStyle( val styleKm = TextStyle(
fontSize = 12.sp, fontSize = 14.sp,
color = Color.White, color = Color.White,
) )
val textLayoutSpeed = remember(speed, maxSpeed) { val textLayoutSpeed = remember(speed, maxSpeed) {
@@ -464,6 +486,7 @@ private fun MaxSpeed(
width: Int, width: Int,
height: Int, height: Int,
maxSpeed: Int, maxSpeed: Int,
curSpeed: Float,
) { ) {
val radius = 24 val radius = 24
Box( Box(
@@ -484,6 +507,11 @@ private fun MaxSpeed(
val textLayoutSpeed = remember(speed) { val textLayoutSpeed = remember(speed) {
textMeasurerSpeed.measure(speed, styleSpeed) textMeasurerSpeed.measure(speed, styleSpeed)
} }
val signColor = if (curSpeed * 3.6 > maxSpeed) {
Color.Red
} else {
Color.Green
}
Canvas(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle( drawCircle(
center = Offset( center = Offset(
@@ -491,7 +519,7 @@ private fun MaxSpeed(
y = center.y y = center.y
), ),
radius = radius * 1.3.toFloat(), radius = radius * 1.3.toFloat(),
color = Color.Red, color = signColor,
) )
drawCircle( drawCircle(
center = Offset( center = Offset(
@@ -3,6 +3,7 @@ package com.kouros.navigation.car.navigation
import android.text.SpannableString import android.text.SpannableString
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.util.Log
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.car.app.AppManager import androidx.car.app.AppManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
@@ -27,6 +28,7 @@ import androidx.car.app.navigation.model.Trip
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.screen.createCarIcon import com.kouros.navigation.car.screen.createCarIcon
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.route.ManeuverType import com.kouros.navigation.data.route.ManeuverType
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
@@ -265,7 +267,7 @@ class RouteCarModel : RouteModel() {
R.string.exit_action_title, R.string.exit_action_title, R.string.exit_action_title, R.string.exit_action_title,
FLAG_DEFAULT FLAG_DEFAULT
) )
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */5000) return Alert.Builder( /* alertId: */0, title, /* durationMillis: */4000)
.setSubtitle(subtitle) .setSubtitle(subtitle)
.setIcon(icon) .setIcon(icon)
.addAction(dismissAction).setCallback(object : AlertCallback { .addAction(dismissAction).setCallback(object : AlertCallback {
@@ -6,6 +6,7 @@ import android.os.SystemClock
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import com.kouros.data.BuildConfig import com.kouros.data.BuildConfig
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.utils.location
import io.ticofab.androidgpxparser.parser.GPXParser import io.ticofab.androidgpxparser.parser.GPXParser
import io.ticofab.androidgpxparser.parser.domain.Gpx import io.ticofab.androidgpxparser.parser.domain.Gpx
import io.ticofab.androidgpxparser.parser.domain.TrackSegment import io.ticofab.androidgpxparser.parser.domain.TrackSegment
@@ -49,16 +50,16 @@ class Simulation {
simulationJob = lifecycleScope.launch { simulationJob = lifecycleScope.launch {
for ((index, point) in points.withIndex()) { for ((index, point) in points.withIndex()) {
if (index >= 0) { if (index >= 0) {
curBearing = lastLocation.bearingTo(location(point[0], point[1]))
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply { val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = point[1] latitude = point[1]
longitude = point[0] longitude = point[0]
bearing = curBearing bearing = curBearing
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = 5.0f speed = 10.0f
time = System.currentTimeMillis() time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
} }
curBearing = lastLocation.bearingTo(fakeLocation)
// Update your app's state as if a real GPS update occurred // Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation) updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second) // Wait before moving to the next point (e.g., every 1 second)
@@ -120,11 +120,12 @@ class CategoryScreen(
private fun createItem(it: Elements, category: String, index: Int): Row { private fun createItem(it: Elements, category: String, index: Int): Row {
var name = "" var name = ""
if (it.tags.name != null) { name = it.tags.name
name = it.tags.name.toString() if (name.isEmpty()) {
name = it.tags.operator
} }
if (name.isEmpty()) { if (name.isEmpty()) {
name = it.tags.operator.toString() name = "Empty"
} }
val row = Row.Builder() val row = Row.Builder()
.setOnClickListener { .setOnClickListener {
@@ -81,6 +81,8 @@ open class NavigationScreen(
private var junctionImage: CarIcon? = null private var junctionImage: CarIcon? = null
private var backGroundColor = CarColor.BLUE private var backGroundColor = CarColor.BLUE
private var message = ""
private var showAlternativeRoute = false private var showAlternativeRoute = false
val observerRecentPlaces = Observer<List<Place>> { newPlaces -> val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
recentPlaces.addAll(newPlaces) recentPlaces.addAll(newPlaces)
@@ -220,9 +222,10 @@ open class NavigationScreen(
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo( .setNavigationInfo(
MessageInfo.Builder( MessageInfo.Builder(
carContext.getString(R.string.arrived_exclamation_msg) message
//carContext.getString(R.string.arrived_exclamation_msg)
) )
.setText(street) // .setText(street)
.setImage( .setImage(
CarIcon.Builder( CarIcon.Builder(
IconCompat.createWithResource( IconCompat.createWithResource(
@@ -504,7 +507,8 @@ open class NavigationScreen(
shouldShowNextStep: Boolean, shouldShowNextStep: Boolean,
shouldShowLanes: Boolean, shouldShowLanes: Boolean,
junctionImage: CarIcon?, junctionImage: CarIcon?,
backGroundColor: CarColor backGroundColor: CarColor,
message: String
) { ) {
this.isNavigating = isNavigating this.isNavigating = isNavigating
this.isRerouting = isRerouting this.isRerouting = isRerouting
@@ -518,6 +522,7 @@ open class NavigationScreen(
this.shouldShowLanes = shouldShowLanes this.shouldShowLanes = shouldShowLanes
this.junctionImage = junctionImage this.junctionImage = junctionImage
this.backGroundColor = backGroundColor this.backGroundColor = backGroundColor
this.message = message
navigationType = NavigationType.NAVIGATION navigationType = NavigationType.NAVIGATION
invalidate() invalidate()
@@ -72,6 +72,7 @@ class RoutePreviewScreen(
var loading = true var loading = true
var previewReady = false;
var flag = FLAG_DEFAULT var flag = FLAG_DEFAULT
private val backPressedCallback = object : OnBackPressedCallback(false) { private val backPressedCallback = object : OnBackPressedCallback(false) {
@@ -85,6 +86,7 @@ class RoutePreviewScreen(
routeModel.startNavigation(route) routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel) surfaceRenderer.setPreviewRouteData(routeModel)
loading = false loading = false
previewReady = true
if (routeModel.route.routes.size == 1 && showAlternativeRoute) { if (routeModel.route.routes.size == 1 && showAlternativeRoute) {
routeType = RoutePreviewType.SINGLE_ROUTE routeType = RoutePreviewType.SINGLE_ROUTE
showAlternativeRoute = false showAlternativeRoute = false
@@ -167,7 +169,8 @@ class RoutePreviewScreen(
if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) { if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) {
createRouteText(routeModel.route.routes.first()) createRouteText(routeModel.route.routes.first())
} else { } else {
CarText.Builder("Wait") loading = true
CarText.Builder(carContext.getString(R.string.wait))
.build() .build()
} }
val content = if (routeType == RoutePreviewType.MULTI_ROUTE) { val content = if (routeType == RoutePreviewType.MULTI_ROUTE) {
@@ -191,8 +194,10 @@ class RoutePreviewScreen(
}) })
val listContent = MessageTemplate.Builder(message) val listContent = MessageTemplate.Builder(message)
.setHeader(header.build()) .setHeader(header.build())
.addAction(navigateAction)
if (previewReady) {
listContent.addAction(navigateAction)
}
if (showAlternativeRoute) { if (showAlternativeRoute) {
listContent.addAction(selectRouteAction) listContent.addAction(selectRouteAction)
} }
@@ -212,14 +217,16 @@ class RoutePreviewScreen(
) )
if (routeModel.route.routes.size > 1 && routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) { if (routeModel.route.routes.size > 1 && routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) {
template.setActionStrip(createActionStrip { if (previewReady) {
createAction( template.setActionStrip(createActionStrip {
carContext, R.drawable.navigation_48px, createAction(
onClickAction = { carContext, R.drawable.navigation_48px,
onNavigate(routeModel.navState.currentRouteIndex) onClickAction = {
} onNavigate(routeModel.navState.currentRouteIndex)
) }
}) )
})
}
} }
return template.build() return template.build()
} }
@@ -320,7 +327,9 @@ class RoutePreviewScreen(
.setTitle(routeText) .setTitle(routeText)
.setOnClickListener { onRouteSelected(index) } .setOnClickListener { onRouteSelected(index) }
.addText(street) .addText(street)
.addAction(navigateAction) if (previewReady) {
row.addAction(navigateAction)
}
if (route.summary.trafficDelay > 60) { if (route.summary.trafficDelay > 60) {
row.addText(createDelay(route)) row.addText(createDelay(route))
row.setImage(createCarIcon(carContext = carContext, R.drawable.traffic_jam_48px)) row.setImage(createCarIcon(carContext = carContext, R.drawable.traffic_jam_48px))
@@ -346,10 +355,12 @@ class RoutePreviewScreen(
} }
private fun onNavigate(index: Int) { private fun onNavigate(index: Int) {
destination.routeIndex = index if (previewReady) {
destination.route = navigationViewModel.previewRoute.value.toString() destination.routeIndex = index
setResult(destination) destination.route = navigationViewModel.previewRoute.value.toString()
finish() setResult(destination)
finish()
}
} }
private fun onRouteSelected(index: Int) { private fun onRouteSelected(index: Int) {
@@ -4,7 +4,7 @@ import androidx.compose.ui.graphics.Color
val NavigationColorLight = Color(0xFF17A119) val NavigationColorLight = Color(0xFF17A119)
val NavigationColorDark = Color(0xFF4EDE10) val NavigationColorDark = Color(0xFF03411F)
val RouteColor = Color(0xFF195D02) val RouteColor = Color(0xFF195D02)
@@ -74,6 +74,8 @@ data class StepData (
var lane: List<Lane> = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList(), 0, 0)), var lane: List<Lane> = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList(), 0, 0)),
var exitNumber: Int = 0, var exitNumber: Int = 0,
var message: String = "", var message: String = "",
var roadNumbers: List<String> = emptyList(),
) )
@@ -123,7 +125,11 @@ object Constants {
/** The initial location to use as an anchor for searches. */ /** The initial location to use as an anchor for searches. */
val homeVogelhart = location(11.5793748, 48.185749) val homeVogelhart = location(11.5793748, 48.185749)
val homeHohenwaldeck = location( 11.594322, 48.1164817) val homeHohenwaldeck = location( 11.594322, 48.1164817)
val ioannina = location( 20.826237, 39.690174)
val subislawa = location(18.570808, 54.420647)
val a94 = location(11.872097,48.163449)
val a9 = location(11.621556, 48.204402,)
const val NEXT_STEP_THRESHOLD = 500.0 const val NEXT_STEP_THRESHOLD = 500.0
const val MAXIMAL_SNAP_CORRECTION = 50.0 const val MAXIMAL_SNAP_CORRECTION = 50.0
@@ -138,8 +144,12 @@ object Constants {
const val TRAFFIC_UPDATE = 300 const val TRAFFIC_UPDATE = 300
const val SPEED_UPDATE_DISTANCE = 600F
const val INSTRUCTION_DISTANCE = 50 const val INSTRUCTION_DISTANCE = 50
const val SPEED_BEARING_DEVIATION = 60
const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED" const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED"
const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED" const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED"
@@ -1,13 +1,5 @@
package com.kouros.navigation.data.overpass package com.kouros.navigation.data.overpass
import com.google.gson.annotations.SerializedName data class Amenity(
val elements: List<Elements>,
data class Amenity (
@SerializedName("version" ) var version : Double? = null,
@SerializedName("generator" ) var generator : String? = null,
@SerializedName("osm3s" ) var osm3s : Osm3s? = Osm3s(),
@SerializedName("elements" ) var elements : ArrayList<Elements> = arrayListOf()
) )
@@ -0,0 +1,8 @@
package com.kouros.navigation.data.overpass
data class Bounds(
val maxlat: Double,
val maxlon: Double,
val minlat: Double,
val minlon: Double
)
@@ -0,0 +1,7 @@
package com.kouros.navigation.data.overpass
data class ElementSearch(
val element: Elements,
val distance: Double,
val bearing: Float,
)
@@ -1,15 +1,14 @@
package com.kouros.navigation.data.overpass package com.kouros.navigation.data.overpass
import com.google.gson.annotations.SerializedName
data class Elements(
data class Elements ( val bounds: Bounds,
val geometry: List<Geometry>,
@SerializedName("type" ) var type : String = "", val id: Long = 0,
@SerializedName("id" ) var id : Long = 0, val lat: Double= 0.0,
@SerializedName("lat" ) var lat : Double = 0.0, val lon: Double = 0.0,
@SerializedName("lon" ) var lon : Double = 0.0, val tags: Tags,
@SerializedName("tags" ) var tags : Tags = Tags(), val type: String = "",
var distance : Double = 0.0 var distance : Double = 0.0
) )
@@ -0,0 +1,6 @@
package com.kouros.navigation.data.overpass
data class Geometry(
val lat: Double = 0.0,
val lon: Double = 0.0
)
@@ -1,7 +1,9 @@
package com.kouros.navigation.data.overpass package com.kouros.navigation.data.overpass
import android.location.Location import android.location.Location
import android.util.Log
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.kouros.data.BuildConfig
import com.kouros.navigation.utils.GeoUtils.getBoundingBox import com.kouros.navigation.utils.GeoUtils.getBoundingBox
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
import java.net.HttpURLConnection import java.net.HttpURLConnection
@@ -9,12 +11,43 @@ import java.net.URL
class Overpass { class Overpass {
//val overpassUrl = "https://overpass.kumi.systems/api/interpreter" var overpassUrl = if (BuildConfig.DEBUG)
//val overpassUrl = "https://overpass-api.de/api" "http://192.168.1.37/api/interpreter"
val overpassUrl = "https://kouros-online.de/overpass/interpreter" else
"https://kouros-online.de/api/interpreter"
fun getAround(radius: Int, linestring: String): List<Elements> { fun getSpeedLimit(radius: Float, linestring: String, street: String, roadNumbers: List<String>): List<Elements> {
val streetName = if (street.length > 10) {
street.substring(0, 10)
} else {
street
}
val name = if (streetName.isEmpty()) {
""
} else {
"[name~\"^${streetName}\"]"
}
val regex = Regex("""\d+|\D+""")
val search = "way[maxspeed](around:$radius,$linestring)$name[!destination][highway!=\"motorway_link\"]"
var waySearch = search
for ((index, r) in roadNumbers.withIndex()) {
val result = regex.findAll(r).map { it.groupValues.first() }.toList()
if (index > 0) {
waySearch = waySearch.plus(";").plus(search)
}
var refValue = result.first().trim().plus(" ")
for (res in result.subList(1, result.size)) {
refValue = refValue.plus(res)
}
// International reference starts with "E"
waySearch = if (r.first().toString() == "E") {
waySearch.plus("[int_ref~\"${refValue}\"]")
} else {
waySearch.plus("[ref~\"${refValue}\"]")
}
}
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST" httpURLConnection.requestMethod = "POST"
httpURLConnection.setRequestProperty( httpURLConnection.setRequestProperty(
@@ -26,15 +59,14 @@ class Overpass {
val searchQuery = """ val searchQuery = """
|[out:json]; |[out:json];
|( |(
| way[highway](around:$radius,$linestring) | $waySearch;
| ;
|); |);
|out body; |out body geom;
""".trimMargin() """.trimMargin()
//println("way[highway](around:$radius,$linestring)")
return overpassApi(httpURLConnection, searchQuery) return overpassApi(httpURLConnection, searchQuery)
} }
fun getAmenities( fun getAmenities(
type: String, type: String,
category: String, category: String,
@@ -59,7 +91,7 @@ class Overpass {
| ($boundingBox); | ($boundingBox);
|); |);
|(._;>;); |(._;>;);
|out body; |out body geom;
""".trimMargin() """.trimMargin()
return overpassApi(httpURLConnection, searchQuery) return overpassApi(httpURLConnection, searchQuery)
} }
@@ -70,17 +102,22 @@ class Overpass {
outputStreamWriter.write(searchQuery) outputStreamWriter.write(searchQuery)
outputStreamWriter.flush() outputStreamWriter.flush()
// Check if the connection is successful // Check if the connection is successful
httpURLConnection.requestMethod = "POST"
val responseCode = httpURLConnection.responseCode val responseCode = httpURLConnection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) { if (responseCode == HttpURLConnection.HTTP_OK) {
val response = httpURLConnection.inputStream.bufferedReader() val response = httpURLConnection.inputStream.bufferedReader()
.use { it.readText() } // defaults to UTF-8 .use { it.readText() } // defaults to UTF-8
if (response.startsWith("<?xml")) {
return emptyList()
}
val gson = GsonBuilder().serializeNulls().create() val gson = GsonBuilder().serializeNulls().create()
val overpass = gson.fromJson(response, Amenity::class.java) val overpass = gson.fromJson(response, Amenity::class.java)
return overpass.elements return overpass.elements
} else {
Log.e("OverpassApi", responseCode.toString())
} }
} catch (e: Exception) { } catch (e: Exception) {
println("Speed $e") Log.e("OverpassApi", e.toString())
} }
return emptyList() return emptyList()
} }
@@ -4,21 +4,28 @@ import com.google.gson.annotations.SerializedName
data class Tags( data class Tags(
@SerializedName("name") var name: String? = null, val destination: String = "",
@SerializedName("amenity") var amenity: String? = null, val highway: String = "",
@SerializedName("authentication:none") var authenticationNone: String? = null, val lanes: String = "",
@SerializedName("capacity") var capacity: String? = null, val lit: String = "",
@SerializedName("motorcar") var motorcar: String? = null, val maxspeed: String = "0",
@SerializedName("network") var network: String? = null, val name: String = "",
@SerializedName("opening_hours") var openingHours: String? = null, val oneway: String = "",
@SerializedName("operator") var operator: String? = null, val ref: String = "",
@SerializedName("operator:short") var operatorShort: String? = null, @SerializedName("int_ref") val intRef: String = "",
@SerializedName("operator:wikidata") var operatorWikidata: String? = null, val sidewalk: String = "",
@SerializedName("operator:wikipedia") var operatorWikipedia: String? = null, val smoothness: String = "",
@SerializedName("ref") var ref: String? = null, val surface: String = "",
@SerializedName("socket:type2") var socketType2: String? = null, val amenity: String = "",
@SerializedName("socket:type2:output") var socketType2Output: String? = null, val capacity: String = "",
@SerializedName("maxspeed") var maxspeed: String = "0", val motorcar: String = "",
@SerializedName("direction") var direction: String? = null, val network: String = "",
val openingHours: String = "",
val operator: String = "",
val operatorShort: String = "",
val operatorWikidata: String = "",
val operatorWikipedia: String = "",
val socketType2: String = "",
val socketType2Output: String = "",
val direction: String = "",
) )
@@ -12,5 +12,6 @@ data class Step(
val distance: Double = 0.0, val distance: Double = 0.0,
val street : String = "", val street : String = "",
val intersection: List<Intersection> = mutableListOf(), val intersection: List<Intersection> = mutableListOf(),
val countryCode : String = "" val countryCode : String = "",
val roadNumbers : List<String> = emptyList(),
) )
@@ -21,7 +21,7 @@ const val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incident
private const val tomtomFields = private const val tomtomFields =
"{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}" "{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}"
val useLocal = false // BuildConfig.DEBUG val useLocal = BuildConfig.DEBUG
val useLocalTraffic = BuildConfig.DEBUG val useLocalTraffic = BuildConfig.DEBUG
@@ -40,7 +40,7 @@ class TomTomRepository : NavigationRepository() {
} }
if (useLocal) { if (useLocal) {
return fetchUrl( return fetchUrl(
"https://kouros-online.de/tomtom_routing.json", "http://192.168.1.37/tomtom_routing.json",
false false
) )
} }
@@ -104,7 +104,7 @@ class TomTomRepository : NavigationRepository() {
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0) val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
return if (useLocalTraffic) { return if (useLocalTraffic) {
fetchUrl( fetchUrl(
"https://kouros-online.de/tomtom_traffic.json", "http://192.168.1.37/tomtom_traffic.json",
false false
) )
} else { } else {
@@ -99,6 +99,11 @@ class TomTomRoute {
route.guidance.instructions[index].routeOffsetInMeters - stepDistance route.guidance.instructions[index].routeOffsetInMeters - stepDistance
stepDuration = stepDuration =
route.guidance.instructions[index].travelTimeInSeconds - stepDuration route.guidance.instructions[index].travelTimeInSeconds - stepDuration
val roadNumbers = if (lastInstruction.roadNumbers != null) {
lastInstruction.roadNumbers
} else {
emptyList()
}
val step = Step( val step = Step(
index = stepIndex, index = stepIndex,
street = street, street = street,
@@ -106,7 +111,8 @@ class TomTomRoute {
duration = stepDuration, duration = stepDuration,
maneuver = maneuver, maneuver = maneuver,
intersection = intersections, intersection = intersections,
countryCode = lastInstruction.countryCode countryCode = lastInstruction.countryCode,
roadNumbers = roadNumbers
) )
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble() stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble() stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
@@ -1,10 +1,9 @@
package com.kouros.navigation.model package com.kouros.navigation.model
//import com.kouros.navigation.data.Preferences.boxStore
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.location.LocationListener import android.util.Log
import android.location.LocationManager
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -12,15 +11,18 @@ 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.Constants.SPEED_BEARING_DEVIATION
import com.kouros.navigation.data.Constants.SPEED_UPDATE_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.data.Places import com.kouros.navigation.data.Places
import com.kouros.navigation.data.SearchFilter import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.nominatim.Search import com.kouros.navigation.data.nominatim.Search
import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.ElementSearch
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.overpass.Overpass import com.kouros.navigation.data.overpass.Overpass
import com.kouros.navigation.utils.Levenshtein import com.kouros.navigation.utils.countryCodeSpeedLimit
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -31,9 +33,12 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.FeatureCollection
import java.lang.reflect.Modifier
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import kotlin.collections.first
import kotlin.collections.forEach
import kotlin.comparisons.compareBy
import kotlin.math.absoluteValue
/** /**
* ViewModel for navigation-related data operations. * ViewModel for navigation-related data operations.
@@ -86,6 +91,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
MutableLiveData() MutableLiveData()
} }
/** LiveData containing POI elements from Overpass API */
val speedElements = mutableListOf<Elements>()
/** LiveData containing speed camera locations */ /** LiveData containing speed camera locations */
val speedCameras: MutableLiveData<List<Elements>> by lazy { val speedCameras: MutableLiveData<List<Elements>> by lazy {
MutableLiveData() MutableLiveData()
@@ -107,10 +116,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
val gson: com.google.gson.Gson = GsonBuilder().create() val gson: com.google.gson.Gson = GsonBuilder().create()
/** /**
* Retrieves recent places from Preferences as a Flow. * Retrieves recent places from Preferences as a Flow.
*/ */
fun recentPlacesFlow(context: Context, location: Location,): Flow<Place> = callbackFlow { fun recentPlacesFlow(context: Context, location: Location): Flow<Place> = callbackFlow {
for (place in recentPlaces.value!!) { for (place in recentPlaces.value!!) {
trySend(place) trySend(place)
} }
@@ -399,24 +409,93 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
* Queries Overpass API for speed limit on current road using fuzzy matching. * Queries Overpass API for speed limit on current road using fuzzy matching.
* Posts speed limit to maxSpeed LiveData. * Posts speed limit to maxSpeed LiveData.
*/ */
fun getMaxSpeed(location: Location, street: String) { fun getSpeedLimit(
location: Location,
routeBearing: Float,
countryCode: String,
) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val levenshtein = Levenshtein() maxSpeed.postValue(calculateSpeedLimit(location, routeBearing, countryCode))
val lineString = "${location.latitude},${location.longitude}"
val amenities = Overpass().getAround(10, lineString)
amenities.forEach {
if (it.tags.name != null) {
val distance =
levenshtein.distance(it.tags.name!!, street)
if (distance < 5) {
val speed = it.tags.maxspeed.toInt()
maxSpeed.postValue(speed)
}
}
}
} }
} }
/**
* Queries Overpass API for speed limit on current road using fuzzy matching.
*/
fun calculateSpeedLimit(location: Location, routeBearing: Float, countryCode: String): Int {
var speed = 0
var element: Elements?
val search = mutableListOf<ElementSearch>()
speedElements.filter { it.type == "way"}.forEach {
var streetBearingSum = 0F
var streetBearingAvg = 0F
var distance = 0F
var maxDistance = 1000F
for ((geoIndex, geo) in it.geometry.withIndex()) {
val geometryLocation = location(geo.lon, geo.lat)
distance = geometryLocation.distanceTo(location)
if (distance < maxDistance) {
maxDistance = distance
}
if (geoIndex > 0) {
val prevLocation =
location(it.geometry[geoIndex - 1].lon, it.geometry[geoIndex - 1].lat)
val streetBearing = prevLocation.bearingTo(geometryLocation).absoluteValue
streetBearingSum = (streetBearingSum + streetBearing)
streetBearingAvg = streetBearingSum / geoIndex
}
}
val bearing = calculateBearing(it, streetBearingAvg, routeBearing)
if (bearing < SPEED_BEARING_DEVIATION) {
search.add(
ElementSearch(
it,
maxDistance.toDouble(),
streetBearingAvg.absoluteValue
)
)
}
}
val result = search.sortedWith(compareBy<ElementSearch> { it.distance }.thenByDescending { it.bearing })
if (result.isNotEmpty()) {
element = result.first().element
//Log.d("NavigationViewModel", "Distance: ${result.first().distance} Bearing: ${result.first().bearing} RouteBearing $routeBearing")
speed = if (element.tags.maxspeed == "none" && element.tags.highway == "motorway") {
countryCodeSpeedLimit(countryCode)
} else {
element.tags.maxspeed.toInt()
}
}
return speed
}
private fun calculateBearing(element: Elements, streetBearingAvg : Float, routeBearing: Float) : Float {
return if (element.tags.oneway.isNotEmpty()) {
(streetBearingAvg - routeBearing.absoluteValue).absoluteValue
} else {
0F
}
}
/**
* Queries Overpass API for speed limit on current road.
* Posts speed elements to speedElements.
*/
fun updateSpeedLimit(
location: Location,
street: String,
roadNumbers: List<String>
) {
viewModelScope.launch(Dispatchers.IO) {
val lineString = "${location.latitude},${location.longitude}"
val elements = Overpass().getSpeedLimit(SPEED_UPDATE_DISTANCE, lineString, street, roadNumbers)
speedElements.clear()
speedElements.addAll(elements)
}
}
/** /**
* Saves a place as a favorite in Preferences. * Saves a place as a favorite in Preferences.
*/ */
@@ -1,13 +1,12 @@
package com.kouros.navigation.model package com.kouros.navigation.model
import android.location.Location import android.location.Location
import android.util.Log
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import com.kouros.navigation.data.Constants.MAXIMUM_LOCATION_DISTANCE import com.kouros.navigation.data.Constants.MAXIMUM_LOCATION_DISTANCE
import com.kouros.navigation.data.Constants.NEAREST_LOCATION_DISTANCE import com.kouros.navigation.data.Constants.NEAREST_LOCATION_DISTANCE
import com.kouros.navigation.data.Constants.SPEED_UPDATE_DISTANCE
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
class RouteCalculator(var routeModel: RouteModel) { class RouteCalculator(var routeModel: RouteModel) {
@@ -107,14 +106,27 @@ class RouteCalculator(var routeModel: RouteModel) {
return nowUtcMillis + timeToDestinationMillis return nowUtcMillis + timeToDestinationMillis
} }
/**
* Updates the speed limit in the view model.
*/
fun updateSpeedLimit(location: Location, viewModel: NavigationViewModel) { fun updateSpeedLimit(location: Location, viewModel: NavigationViewModel) {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
// speed limit // speed limit
val distance = lastSpeedLocation.distanceTo(location) val distance = lastSpeedLocation.distanceTo(location)
if (distance > 500 || lastSpeedIndex < routeModel.route.currentStepIndex) { if ((distance > SPEED_UPDATE_DISTANCE * 2) || lastSpeedIndex < routeModel.route.currentStepIndex) {
lastSpeedIndex = routeModel.route.currentStepIndex lastSpeedIndex = routeModel.route.currentStepIndex
lastSpeedLocation = location lastSpeedLocation = location
viewModel.getMaxSpeed(location, routeModel.route.currentStep().street) viewModel.updateSpeedLimit(
location,
routeModel.route.currentStep().street,
routeModel.currentStep().roadNumbers
)
} else {
viewModel.getSpeedLimit(
location,
routeModel.navState.routeBearing,
routeModel.currentStep.countryCode
)
} }
} }
} }
@@ -105,7 +105,8 @@ open class RouteModel {
leftDistance = routeCalculator.travelLeftDistance(), leftDistance = routeCalculator.travelLeftDistance(),
lane = currentLanes, lane = currentLanes,
exitNumber = exitNumber, exitNumber = exitNumber,
message = currentStep.maneuver.message message = currentStep.maneuver.message,
roadNumbers = currentStep.roadNumbers
) )
} }
@@ -4,6 +4,7 @@ import android.location.Location
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.LineString
import org.maplibre.geojson.Point import org.maplibre.geojson.Point
import org.maplibre.spatialk.geojson.Feature import org.maplibre.spatialk.geojson.Feature
import org.maplibre.spatialk.geojson.dsl.addFeature import org.maplibre.spatialk.geojson.dsl.addFeature
@@ -19,7 +20,7 @@ import kotlin.math.pow
object GeoUtils { object GeoUtils {
fun snapLocation(location: Location, stepCoordinates: List<Point>) : Location { fun snapLocation(location: Location, stepCoordinates: List<Point>): Location {
val newLocation = Location(location) val newLocation = Location(location)
val oldPoint = Point.fromLngLat(location.longitude, location.latitude) val oldPoint = Point.fromLngLat(location.longitude, location.latitude)
if (stepCoordinates.size > 1) { if (stepCoordinates.size > 1) {
@@ -34,7 +35,7 @@ object GeoUtils {
return newLocation return newLocation
} }
fun decodePolyline(encoded: String, precision: Int = 6): List<List<Double>> { fun decodePolyline(encoded: String, precision: Int = 6): List<List<Double>> {
val factor = 10.0.pow(precision) val factor = 10.0.pow(precision)
var lat = 0 var lat = 0
var lng = 0 var lng = 0
@@ -91,18 +92,20 @@ object GeoUtils {
} }
fun createLineStringCollection(lineCoordinates: List<List<Double>>): String { fun createLineStringCollection(lineCoordinates: List<List<Double>>): String {
// return createPointCollection(lineCoordinates, "Route") // return createPointCollection(lineCoordinates, "Route")
val lineString = buildLineString { val lineString = buildLineString {
lineCoordinates.forEach { lineCoordinates.forEach {
add(org.maplibre.spatialk.geojson.Point( add(
it[0], org.maplibre.spatialk.geojson.Point(
it[1] it[0],
)) it[1]
)
)
} }
} }
val feature = Feature(lineString, null) val feature = Feature(lineString, null)
val featureCollection = org.maplibre.spatialk.geojson.FeatureCollection(feature) val featureCollection = org.maplibre.spatialk.geojson.FeatureCollection(feature)
return featureCollection.toJson() return featureCollection.toJson()
} }
fun createPointCollection(lineCoordinates: List<List<Double>>, category: String): String { fun createPointCollection(lineCoordinates: List<List<Double>>, category: String): String {
@@ -114,9 +117,30 @@ object GeoUtils {
} }
} }
} }
return featureCollection.toJson() return featureCollection.toJson()
} }
fun createStartCollection(geoJson: String): String {
val featureCollection = FeatureCollection.fromJson(geoJson)
val geometry = featureCollection.features()!!.first().geometry()
val coordinates = (geometry as LineString)
val first = coordinates.coordinates().first()
val points = createPointCollection(
listOf(listOf(first.coordinates()[0], first.coordinates()[1])), "End"
)
return points
}
fun createEndCollection(geoJson: String): String {
val featureCollection = FeatureCollection.fromJson(geoJson)
val geometry = featureCollection.features()!!.first().geometry()
val coordinates = (geometry as LineString)
val last = coordinates.coordinates().last()
val points = createPointCollection(
listOf(listOf(last.coordinates()[0], last.coordinates()[1])), "End"
)
return points
}
/** /**
* Calculate the lat and len of a square around a point. * Calculate the lat and len of a square around a point.
@@ -131,6 +155,7 @@ object GeoUtils {
return "$lngMin,$latMin,$lngMax,$latMax" return "$lngMin,$latMin,$lngMax,$latMax"
} }
fun getBoundingBox( fun getBoundingBox(
lat: Double, lat: Double,
lon: Double, lon: Double,
@@ -144,4 +169,18 @@ object GeoUtils {
return "$minLat,$minLon,$maxLat,$maxLon" return "$minLat,$minLon,$maxLat,$maxLon"
} }
fun isLocationInBoundingBox(
bottomLeftLat: Double,
bottomLeftLon: Double,
topRightLat: Double,
topRightLon: Double,
location: Location
): Boolean {
val isInside =
location.latitude in bottomLeftLat..topRightLat
&& location.longitude >= bottomLeftLon
&& location.longitude <= topRightLon
return isInside
}
} }
@@ -25,10 +25,11 @@ class Levenshtein {
* @param limit the maximum result to compute before stopping, terminating calculation early. * @param limit the maximum result to compute before stopping, terminating calculation early.
* @return the computed Levenshtein distance. * @return the computed Levenshtein distance.
*/ */
fun distance(first: CharSequence, second: CharSequence, limit: Int = Int.MAX_VALUE): Int { fun distance(first: CharSequence, second: CharSequence, countryCode: String, limit: Int = Int.MAX_VALUE): Int {
if (countryCode == "GRC") return 0
if (first == second) return 0 if (first == second) return 0
if (first.isEmpty()) return second.length if (first.isEmpty()) return 0
if (second.isEmpty()) return first.length if (second.isEmpty()) return 0
// initial costs is the edit distance from an empty string, which corresponds to the characters to inserts. // initial costs is the edit distance from an empty string, which corresponds to the characters to inserts.
// the array size is : length + 1 (empty string) // the array size is : length + 1 (empty string)
@@ -3,11 +3,10 @@ package com.kouros.navigation.utils
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.util.Log
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TILT import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.osrm.OsrmRepository import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.data.valhalla.ValhallaRepository
@@ -94,15 +93,19 @@ fun calculateZoomFromBoundingBox(centerLocation: Location, previewDistance: Doub
} }
fun calculateTilt(newZoom: Double, tilt: Double): Double = fun calculateTilt(viewStyle: ViewStyle, newZoom: Double, tilt: Double): Double =
if (newZoom < 13) { if (viewStyle == ViewStyle.VIEW) {
0.0 if (newZoom < 13) {
} else { 0.0
if (tilt == 0.0) {
TILT
} else { } else {
tilt if (tilt == 0.0) {
TILT
} else {
tilt
}
} }
} else {
return 0.0
} }
fun bearing(fromLocation: Location, toLocation: Location, oldBearing: Double): Double { fun bearing(fromLocation: Location, toLocation: Location, oldBearing: Double): Double {
@@ -134,13 +137,14 @@ fun Double.round(numFractionDigits: Int): Double {
} }
fun duration( fun duration(
preview: Boolean, viewStyle: ViewStyle,
bearing: Double, bearing: Double,
lastBearing: Double, lastBearing: Double,
lastLocationUpdate: LocalDateTime lastLocationUpdate: LocalDateTime
): Duration { ): Duration {
if (preview) { if (viewStyle == ViewStyle.PREVIEW ||
return 10.milliseconds viewStyle == ViewStyle.PAN_VIEW) {
return 100.milliseconds
} }
val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) { val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) {
2.seconds 2.seconds
@@ -184,3 +188,11 @@ fun formattedDistance(distanceMode: Int, distance: Double): Pair<Double, Int> {
} }
return Pair(currentDistance, displayUnit) return Pair(currentDistance, displayUnit)
} }
fun countryCodeSpeedLimit(countryCode: String) : Int {
return when (countryCode) {
"DEU", "FRA", "AUT", "GRE", "NLD", "ITA", "SLO", "SVK", "CZE" -> 130
"POL", "BEL", "ESP", "PRT", "BGR", "HUN", "FIN" -> 120
else -> 100
}
}
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M390,220L450,220L450,160L390,160L390,220ZM510,220L510,160L570,160L570,220L510,220ZM390,460L390,400L450,400L450,460L390,460ZM630,340L630,280L690,280L690,340L630,340ZM630,460L630,400L690,400L690,460L630,460ZM510,460L510,400L570,400L570,460L510,460ZM630,220L630,160L690,160L690,220L630,220ZM450,280L450,220L510,220L510,280L450,280ZM270,800L270,160L330,160L330,220L390,220L390,280L330,280L330,340L390,340L390,400L330,400L330,800L270,800ZM570,400L570,340L630,340L630,400L570,400ZM450,400L450,340L510,340L510,400L450,400ZM390,340L390,280L450,280L450,340L390,340ZM510,340L510,280L570,280L570,340L510,340ZM570,280L570,220L630,220L630,280L570,280Z"/>
</vector>
@@ -72,4 +72,5 @@
<string name="electric">Electric</string> <string name="electric">Electric</string>
<string name="engine_type">Engine type</string> <string name="engine_type">Engine type</string>
<string name="alternative_routes">Alternative routes</string> <string name="alternative_routes">Alternative routes</string>
<string name="wait">Wait</string>
</resources> </resources>
@@ -56,4 +56,5 @@
<string name="electric">Electric</string> <string name="electric">Electric</string>
<string name="engine_type">Engine type</string> <string name="engine_type">Engine type</string>
<string name="alternative_routes">Alternative routes</string> <string name="alternative_routes">Alternative routes</string>
<string name="wait">Wait</string>
</resources> </resources>
@@ -56,4 +56,5 @@
<string name="electric">Electric</string> <string name="electric">Electric</string>
<string name="engine_type">Engine type</string> <string name="engine_type">Engine type</string>
<string name="alternative_routes">Alternative routes</string> <string name="alternative_routes">Alternative routes</string>
<string name="wait">Wait</string>
</resources> </resources>
@@ -59,4 +59,5 @@
<string name="electric">Electric</string> <string name="electric">Electric</string>
<string name="engine_type">Engine type</string> <string name="engine_type">Engine type</string>
<string name="alternative_routes">Alternative routes</string> <string name="alternative_routes">Alternative routes</string>
<string name="wait">Wait</string>
</resources> </resources>
File diff suppressed because one or more lines are too long
@@ -8,8 +8,11 @@ import com.kouros.navigation.data.route.Maneuver
import com.kouros.navigation.data.route.Routes import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.data.route.Step import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.utils.GeoUtils.createPointCollection
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.LineString
import org.mockito.kotlin.any import org.mockito.kotlin.any
import org.mockito.kotlin.doNothing import org.mockito.kotlin.doNothing
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
@@ -58,6 +61,18 @@ class RouteModelTest {
return Route(routeEngine = 2, routes = listOf(routes), currentStepIndex = currentStepIndex) return Route(routeEngine = 2, routes = listOf(routes), currentStepIndex = currentStepIndex)
} }
@Test
fun `create Point Collection returns false when route has no legs`() {
val geoJson = routeModel.curRoute.routeGeoJson
val featureCollection = FeatureCollection.fromJson(geoJson)
val geometry = featureCollection.features()!!.first().geometry()
val coordinates = (geometry as LineString)
val first = coordinates.coordinates().first()
val last = coordinates.coordinates().first()
val points = createPointCollection(listOf(
listOf(first.coordinates()[0], first.coordinates()[1]), listOf(last.coordinates()[0], last.coordinates()[1])), "Start")
}
@Test @Test
fun `hasLegs returns true when route has legs`() { fun `hasLegs returns true when route has legs`() {
val step0 = createStep(index = 0, numWaypoints = 2) val step0 = createStep(index = 0, numWaypoints = 2)
+2 -2
View File
@@ -1,9 +1,9 @@
[versions] [versions]
agp = "9.1.0" agp = "9.1.1"
androidGpxParser = "2.3.1" androidGpxParser = "2.3.1"
androidSdkTurf = "6.0.1" androidSdkTurf = "6.0.1"
datastore = "1.2.1" datastore = "1.2.1"
gradle = "9.1.0" gradle = "9.1.1"
koinAndroid = "4.2.0" koinAndroid = "4.2.0"
koinAndroidxCompose = "4.2.0" koinAndroidxCompose = "4.2.0"
koinComposeViewmodel = "4.2.0" koinComposeViewmodel = "4.2.0"