From 0fa625d7857340829d9e2ba4fef8d9b30dc69a18 Mon Sep 17 00:00:00 2001 From: Dimitris Date: Wed, 22 Apr 2026 17:44:08 +0200 Subject: [PATCH] Overpass --- app/build.gradle.kts | 8 +- .../com/kouros/navigation/car/OverpassTest.kt | 96 ++++++++++++++ .../kouros/navigation/car/RouteModelTest.kt | 12 +- .../navigation/car/DeviceLocationManager.kt | 30 +++-- .../navigation/car/NavigationSession.kt | 51 ++++---- .../kouros/navigation/car/SurfaceRenderer.kt | 16 +-- .../com/kouros/navigation/car/map/MapView.kt | 52 ++++++-- .../car/navigation/RouteCarModel.kt | 4 +- .../navigation/car/navigation/Simulation.kt | 5 +- .../navigation/car/screen/CategoryScreen.kt | 7 +- .../navigation/car/screen/NavigationScreen.kt | 11 +- .../car/screen/RoutePreviewScreen.kt | 41 +++--- .../java/com/kouros/navigation/data/Color.kt | 2 +- .../java/com/kouros/navigation/data/Data.kt | 10 ++ .../navigation/data/overpass/Amenity.kt | 12 +- .../kouros/navigation/data/overpass/Bounds.kt | 8 ++ .../navigation/data/overpass/ElementSearch.kt | 7 ++ .../navigation/data/overpass/Elements.kt | 19 ++- .../navigation/data/overpass/Geometry.kt | 6 + .../navigation/data/overpass/Overpass.kt | 59 +++++++-- .../kouros/navigation/data/overpass/Tags.kt | 41 +++--- .../com/kouros/navigation/data/route/Step.kt | 3 +- .../data/tomtom/TomTomRepository.kt | 6 +- .../navigation/data/tomtom/TomTomRoute.kt | 8 +- .../navigation/model/NavigationViewModel.kt | 119 +++++++++++++++--- .../navigation/model/RouteCalculator.kt | 20 ++- .../com/kouros/navigation/model/RouteModel.kt | 3 +- .../com/kouros/navigation/utils/GeoUtils.kt | 57 +++++++-- .../kouros/navigation/utils/Levenshtein.kt | 7 +- .../navigation/utils/NavigationUtils.kt | 36 ++++-- .../main/res/drawable/sports_score_48px.xml | 10 ++ .../data/src/main/res/values-de/strings.xml | 1 + .../data/src/main/res/values-el/strings.xml | 1 + .../data/src/main/res/values-pl/strings.xml | 1 + common/data/src/main/res/values/strings.xml | 1 + .../kouros/navigation/model/OverpassTest.kt | 59 +++++++++ .../kouros/navigation/model/RouteModelTest.kt | 15 +++ gradle/libs.versions.toml | 4 +- 38 files changed, 658 insertions(+), 190 deletions(-) create mode 100644 common/car/src/androidTest/java/com/kouros/navigation/car/OverpassTest.kt create mode 100644 common/data/src/main/java/com/kouros/navigation/data/overpass/Bounds.kt create mode 100644 common/data/src/main/java/com/kouros/navigation/data/overpass/ElementSearch.kt create mode 100644 common/data/src/main/java/com/kouros/navigation/data/overpass/Geometry.kt create mode 100644 common/data/src/main/res/drawable/sports_score_48px.xml create mode 100644 common/data/src/test/java/com/kouros/navigation/model/OverpassTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 906c033..033624e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,14 +11,14 @@ val properties = Properties().apply { android { namespace = "com.kouros.navigation" - compileSdk = 36 + compileSdk = 37 defaultConfig { applicationId = "com.kouros.navigation" minSdk = 33 - targetSdk = 36 - versionCode = 94 - versionName = "0.2.3.94" + targetSdk = 37 + versionCode = 97 + versionName = "0.2.3.97" base.archivesName = "navi-$versionName" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/common/car/src/androidTest/java/com/kouros/navigation/car/OverpassTest.kt b/common/car/src/androidTest/java/com/kouros/navigation/car/OverpassTest.kt new file mode 100644 index 0000000..988c804 --- /dev/null +++ b/common/car/src/androidTest/java/com/kouros/navigation/car/OverpassTest.kt @@ -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, + 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) + } +} \ No newline at end of file diff --git a/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt b/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt index 228687e..a6190a7 100644 --- a/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt +++ b/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt @@ -6,12 +6,14 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import com.kouros.navigation.data.Constants.homeHohenwaldeck 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.tomtom.TomTomRepository import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.RouteModel import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.location +import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.junit.Test @@ -69,7 +71,7 @@ class RouteModelTest { val repository = getSettingsRepository(appContext) runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) } val routeJsonString = TomTomRepository().fetchUrl( - "https://kouros-online.de/tomtom_routing.json", + "http://192.168.1.37/tomtom_routing.json", false ) assertNotEquals("", routeJsonString) @@ -201,6 +203,7 @@ class RouteModelTest { @Test fun simulate() { + val viewModel = NavigationViewModel(TomTomRepository()) for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) { if (routeModel.isNavigating()) { val curLocation = location(waypoint[0], waypoint[1]) @@ -244,9 +247,12 @@ class RouteModelTest { val curLocation = location(waypoint[0], waypoint[1]) if (routeModel.isNavigating()) { if (index in 16..43) { - routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository())) + routeModel.updateLocation( + curLocation, + NavigationViewModel(TomTomRepository()) + ) val stepData = routeModel.currentStep() - assertEquals(stepData.leftStepDistance, distance[index-16], 1.0) + assertEquals(stepData.leftStepDistance, distance[index - 16], 1.0) } } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/DeviceLocationManager.kt b/common/car/src/main/java/com/kouros/navigation/car/DeviceLocationManager.kt index d507d05..7bcab16 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/DeviceLocationManager.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/DeviceLocationManager.kt @@ -11,6 +11,12 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope 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.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -92,21 +98,29 @@ class DeviceLocationManager( @SuppressLint("MissingPermission") fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5f) { if (isListening) return + val setIndividualLocation = BuildConfig.DEBUG // Get and deliver last known location first val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) if (lastLocation != null) { - onInitialLocation(lastLocation) - onLocationUpdate(lastLocation) + if (setIndividualLocation) { + onInitialLocation(homeVogelhart) + onLocationUpdate(homeVogelhart) + } else { + onInitialLocation(lastLocation) + onLocationUpdate(lastLocation) + } } // Start continuous location updates - locationManager.requestLocationUpdates( - LocationManager.GPS_PROVIDER, - minTimeMs, - minDistanceM, - locationListener - ) + if (!setIndividualLocation) { + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + minTimeMs, + minDistanceM, + locationListener + ) + } isListening = true } diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt index 1d6ba08..6f7233e 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt @@ -112,6 +112,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa var lastRouteDate: LocalDateTime = LocalDateTime.now() + var lastAlert : Long = 0 var navigationManagerStarted = false /** @@ -462,29 +463,30 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa * Snaps location to route and checks for deviation requiring reroute. */ private fun handleNavigationLocation(location: Location) { + val startTime = System.currentTimeMillis() routeModel.updateLocation(location, navigationViewModel) - if (routeModel.navState.arrived) return - if (guidanceAudio == 1) { - handleGuidanceAudio() - } + val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations()) val streetName = routeModel.currentStep().street - val currentDate = LocalDateTime.now(ZoneOffset.UTC) - - if (snapLocation(location, streetName)) { - checkTraffic(currentDate, location) - updateSpeedCamera(location) - checkRoute(currentDate, location) + if (checkLocationDeviation(location, snappedLocation, streetName)) { + if (routeModel.navState.arrived) return + if (guidanceAudio == 1) { + handleGuidanceAudio() + } + val currentDate = LocalDateTime.now(ZoneOffset.UTC) + checkTraffic(currentDate, snappedLocation) + updateSpeedCamera(snappedLocation) + checkRoute(currentDate, snappedLocation) updateNavigationScreen() checkArrival() } + val endTime = System.currentTimeMillis() - startTime + Log.d(TAG, "handleNavigationLocation: $endTime") } /** - * Updates the surface renderer with snapped location and street name. - * Checks if maximal route deviation is exceeded and reroutes if needed. + * Checks if the location deviation is acceptable. */ - private fun snapLocation(location: Location, streetName: String): Boolean { - val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations()) + private fun checkLocationDeviation(location: Location, snappedLocation: Location, streetName: String): Boolean { val distance = location.distanceTo(snappedLocation) when { distance > MAXIMAL_ROUTE_DEVIATION -> { @@ -514,6 +516,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa return } + val currentStep = routeModel.route.currentStep() + val stepData = routeModel.currentStep() + navigationScreen.updateTrip( isNavigating = routeModel.isNavigating(), isRerouting = false, @@ -526,7 +531,8 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa shouldShowNextStep = false, shouldShowLanes = true, 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 camera = sortedList.firstOrNull() ?: return val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location) - val bearingSpeedCamera = if (camera.tags.direction != null) { - try { - camera.tags.direction!!.toFloat() - } catch (e: Exception) { - 0F - } - } else { - location.bearingTo(location(camera.lon, camera.lat)).absoluteValue + val bearingSpeedCamera = try { + camera.tags.direction.toFloat() + } catch (e: Exception) { + 0F } - if (camera.distance < 80) { + if (camera.distance < 80 && (System.currentTimeMillis() - lastAlert > 5000)) { if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) { routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed) + lastAlert = System.currentTimeMillis() } } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt index 28448af..cca6693 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt @@ -8,7 +8,6 @@ import android.location.Location import android.util.Log import androidx.car.app.AppManager import androidx.car.app.CarContext -import androidx.car.app.Session import androidx.car.app.SurfaceCallback import androidx.car.app.SurfaceContainer import androidx.compose.foundation.layout.PaddingValues @@ -156,8 +155,6 @@ class SurfaceRenderer( Log.i(TAG, "Surface available $surfaceContainer") lifecycleOwner = CustomLifecycleOwner() 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_START) lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) @@ -301,7 +298,7 @@ class SurfaceRenderer( ) { val cameraDuration = duration( - viewStyle == ViewStyle.PREVIEW, + viewStyle, position!!.bearing, lastBearing, lastLocationUpdate @@ -351,13 +348,11 @@ class SurfaceRenderer( viewStyle = ViewStyle.PAN_VIEW } val newZoom = if (zoomSign < 0) { - cameraPosition.value!!.zoom - 0.2 + cameraPosition.value!!.zoom - 1 } else { - cameraPosition.value!!.zoom + 0.2 - } - if (viewStyle == ViewStyle.VIEW) { - tilt = calculateTilt(newZoom, tilt) + cameraPosition.value!!.zoom + 1 } + tilt = calculateTilt(viewStyle, newZoom, tilt) updateCameraPosition( cameraPosition.value!!.bearing, newZoom, @@ -411,6 +406,7 @@ class SurfaceRenderer( synchronized(this) { routeData.value = routeGeoJson viewStyle = ViewStyle.VIEW + updateLocation(lastLocation, "") } } @@ -479,7 +475,7 @@ class SurfaceRenderer( } viewStyle = ViewStyle.VIEW val zoom = calculateZoom(0.0) - tilt = calculateTilt(zoom, tilt) + tilt = calculateTilt(viewStyle, zoom, tilt) updateCameraPosition( tilt = tilt, zoom = zoom, diff --git a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt index 73aa347..6b33d0b 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt @@ -1,7 +1,6 @@ package com.kouros.navigation.car.map import android.location.Location -import android.util.Log import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Box 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.SpeedColor import com.kouros.navigation.data.ViewStyle +import com.kouros.navigation.utils.GeoUtils.createEndCollection import com.kouros.navigation.utils.isMetricSystem import com.kouros.navigation.utils.location 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.LineLayer import org.maplibre.compose.layers.SymbolLayer -import org.maplibre.compose.location.LocationPuck import org.maplibre.compose.location.LocationPuckColors 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.MaplibreMap import org.maplibre.compose.map.OrnamentOptions @@ -115,6 +112,7 @@ fun MapLibre( AmenityLayer(route) } else { RouteLayer(route, traffic!!) + StartEndLayer(route) //RouteLayerPoint(route ) } 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 fun RouteLayer(routeData: String?, trafficData: Map) { if (!routeData.isNullOrEmpty()) { @@ -308,8 +330,8 @@ fun DrawNavigationImages( if (speed != null) { CurrentSpeed(width, height, speed, maxSpeed) } - if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) { - MaxSpeed(width, height, maxSpeed) + if (speed != null && maxSpeed > 0) { // && (speed * 3.6) > maxSpeed) { + MaxSpeed(width, height, maxSpeed, speed) } //DebugInfo(width, height, lat!!) } @@ -325,9 +347,9 @@ fun NavigationImage( val imageSize = (height / 8) val navigationColor = if (darkMode) - remember { NavigationColorDark } - else remember { NavigationColorLight } + else + remember { NavigationColorDark } val textMeasurerStreet = rememberTextMeasurer() val street = streetName.toString() @@ -397,7 +419,7 @@ private fun CurrentSpeed( maxSpeed: Int ) { - val radius = 34 + val radius = 36 Box( modifier = Modifier .padding( @@ -414,12 +436,12 @@ private fun CurrentSpeed( val kmh = if (isMetricSystem()) "km/h" else "mph" val styleSpeed = TextStyle( - fontSize = 22.sp, + fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color.White, ) val styleKm = TextStyle( - fontSize = 12.sp, + fontSize = 14.sp, color = Color.White, ) val textLayoutSpeed = remember(speed, maxSpeed) { @@ -464,6 +486,7 @@ private fun MaxSpeed( width: Int, height: Int, maxSpeed: Int, + curSpeed: Float, ) { val radius = 24 Box( @@ -484,6 +507,11 @@ private fun MaxSpeed( val textLayoutSpeed = remember(speed) { textMeasurerSpeed.measure(speed, styleSpeed) } + val signColor = if (curSpeed * 3.6 > maxSpeed) { + Color.Red + } else { + Color.Green + } Canvas(modifier = Modifier.fillMaxSize()) { drawCircle( center = Offset( @@ -491,7 +519,7 @@ private fun MaxSpeed( y = center.y ), radius = radius * 1.3.toFloat(), - color = Color.Red, + color = signColor, ) drawCircle( center = Offset( diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt index e00c8f6..c2fdfa4 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt @@ -3,6 +3,7 @@ package com.kouros.navigation.car.navigation import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.Spanned +import android.util.Log import androidx.annotation.StringRes import androidx.car.app.AppManager import androidx.car.app.CarContext @@ -27,6 +28,7 @@ import androidx.car.app.navigation.model.Trip import androidx.core.graphics.drawable.IconCompat import com.kouros.data.R 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.route.ManeuverType import com.kouros.navigation.model.RouteModel @@ -265,7 +267,7 @@ class RouteCarModel : RouteModel() { R.string.exit_action_title, R.string.exit_action_title, FLAG_DEFAULT ) - return Alert.Builder( /* alertId: */0, title, /* durationMillis: */5000) + return Alert.Builder( /* alertId: */0, title, /* durationMillis: */4000) .setSubtitle(subtitle) .setIcon(icon) .addAction(dismissAction).setCallback(object : AlertCallback { diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt index 76179b2..c7a1f88 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt @@ -6,6 +6,7 @@ import android.os.SystemClock import androidx.lifecycle.LifecycleCoroutineScope import com.kouros.data.BuildConfig import com.kouros.navigation.data.tomtom.TomTomRepository +import com.kouros.navigation.utils.location import io.ticofab.androidgpxparser.parser.GPXParser import io.ticofab.androidgpxparser.parser.domain.Gpx import io.ticofab.androidgpxparser.parser.domain.TrackSegment @@ -49,16 +50,16 @@ class Simulation { simulationJob = lifecycleScope.launch { for ((index, point) in points.withIndex()) { if (index >= 0) { + curBearing = lastLocation.bearingTo(location(point[0], point[1])) val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply { latitude = point[1] longitude = point[0] bearing = curBearing speedAccuracyMetersPerSecond = 1.0f // ~1 m/s - speed = 5.0f + speed = 10.0f time = System.currentTimeMillis() elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() } - curBearing = lastLocation.bearingTo(fakeLocation) // Update your app's state as if a real GPS update occurred updateLocation(fakeLocation) // Wait before moving to the next point (e.g., every 1 second) diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt index 7e14e40..8051d83 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt @@ -120,11 +120,12 @@ class CategoryScreen( private fun createItem(it: Elements, category: String, index: Int): Row { var name = "" - if (it.tags.name != null) { - name = it.tags.name.toString() + name = it.tags.name + if (name.isEmpty()) { + name = it.tags.operator } if (name.isEmpty()) { - name = it.tags.operator.toString() + name = "Empty" } val row = Row.Builder() .setOnClickListener { diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt index 43e2ec1..f8f9e3c 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt @@ -81,6 +81,8 @@ open class NavigationScreen( private var junctionImage: CarIcon? = null private var backGroundColor = CarColor.BLUE + private var message = "" + private var showAlternativeRoute = false val observerRecentPlaces = Observer> { newPlaces -> recentPlaces.addAll(newPlaces) @@ -220,9 +222,10 @@ open class NavigationScreen( return NavigationTemplate.Builder() .setNavigationInfo( MessageInfo.Builder( - carContext.getString(R.string.arrived_exclamation_msg) + message + //carContext.getString(R.string.arrived_exclamation_msg) ) - .setText(street) + // .setText(street) .setImage( CarIcon.Builder( IconCompat.createWithResource( @@ -504,7 +507,8 @@ open class NavigationScreen( shouldShowNextStep: Boolean, shouldShowLanes: Boolean, junctionImage: CarIcon?, - backGroundColor: CarColor + backGroundColor: CarColor, + message: String ) { this.isNavigating = isNavigating this.isRerouting = isRerouting @@ -518,6 +522,7 @@ open class NavigationScreen( this.shouldShowLanes = shouldShowLanes this.junctionImage = junctionImage this.backGroundColor = backGroundColor + this.message = message navigationType = NavigationType.NAVIGATION invalidate() diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt index 34d5763..67c35b2 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt @@ -72,6 +72,7 @@ class RoutePreviewScreen( var loading = true + var previewReady = false; var flag = FLAG_DEFAULT private val backPressedCallback = object : OnBackPressedCallback(false) { @@ -85,6 +86,7 @@ class RoutePreviewScreen( routeModel.startNavigation(route) surfaceRenderer.setPreviewRouteData(routeModel) loading = false + previewReady = true if (routeModel.route.routes.size == 1 && showAlternativeRoute) { routeType = RoutePreviewType.SINGLE_ROUTE showAlternativeRoute = false @@ -167,7 +169,8 @@ class RoutePreviewScreen( if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) { createRouteText(routeModel.route.routes.first()) } else { - CarText.Builder("Wait") + loading = true + CarText.Builder(carContext.getString(R.string.wait)) .build() } val content = if (routeType == RoutePreviewType.MULTI_ROUTE) { @@ -191,8 +194,10 @@ class RoutePreviewScreen( }) val listContent = MessageTemplate.Builder(message) .setHeader(header.build()) - .addAction(navigateAction) + if (previewReady) { + listContent.addAction(navigateAction) + } if (showAlternativeRoute) { listContent.addAction(selectRouteAction) } @@ -212,14 +217,16 @@ class RoutePreviewScreen( ) if (routeModel.route.routes.size > 1 && routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) { - template.setActionStrip(createActionStrip { - createAction( - carContext, R.drawable.navigation_48px, - onClickAction = { - onNavigate(routeModel.navState.currentRouteIndex) - } - ) - }) + if (previewReady) { + template.setActionStrip(createActionStrip { + createAction( + carContext, R.drawable.navigation_48px, + onClickAction = { + onNavigate(routeModel.navState.currentRouteIndex) + } + ) + }) + } } return template.build() } @@ -320,7 +327,9 @@ class RoutePreviewScreen( .setTitle(routeText) .setOnClickListener { onRouteSelected(index) } .addText(street) - .addAction(navigateAction) + if (previewReady) { + row.addAction(navigateAction) + } if (route.summary.trafficDelay > 60) { row.addText(createDelay(route)) row.setImage(createCarIcon(carContext = carContext, R.drawable.traffic_jam_48px)) @@ -346,10 +355,12 @@ class RoutePreviewScreen( } private fun onNavigate(index: Int) { - destination.routeIndex = index - destination.route = navigationViewModel.previewRoute.value.toString() - setResult(destination) - finish() + if (previewReady) { + destination.routeIndex = index + destination.route = navigationViewModel.previewRoute.value.toString() + setResult(destination) + finish() + } } private fun onRouteSelected(index: Int) { diff --git a/common/data/src/main/java/com/kouros/navigation/data/Color.kt b/common/data/src/main/java/com/kouros/navigation/data/Color.kt index 06b7b0d..20775ec 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Color.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Color.kt @@ -4,7 +4,7 @@ import androidx.compose.ui.graphics.Color val NavigationColorLight = Color(0xFF17A119) -val NavigationColorDark = Color(0xFF4EDE10) +val NavigationColorDark = Color(0xFF03411F) val RouteColor = Color(0xFF195D02) diff --git a/common/data/src/main/java/com/kouros/navigation/data/Data.kt b/common/data/src/main/java/com/kouros/navigation/data/Data.kt index ec041b3..ca55824 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Data.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Data.kt @@ -74,6 +74,8 @@ data class StepData ( var lane: List = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList(), 0, 0)), var exitNumber: Int = 0, var message: String = "", + var roadNumbers: List = emptyList(), + ) @@ -123,7 +125,11 @@ object Constants { /** The initial location to use as an anchor for searches. */ val homeVogelhart = location(11.5793748, 48.185749) 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 MAXIMAL_SNAP_CORRECTION = 50.0 @@ -138,8 +144,12 @@ object Constants { const val TRAFFIC_UPDATE = 300 + const val SPEED_UPDATE_DISTANCE = 600F + 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 AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED" diff --git a/common/data/src/main/java/com/kouros/navigation/data/overpass/Amenity.kt b/common/data/src/main/java/com/kouros/navigation/data/overpass/Amenity.kt index 725a984..5f54e7b 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/overpass/Amenity.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/overpass/Amenity.kt @@ -1,13 +1,5 @@ package com.kouros.navigation.data.overpass -import com.google.gson.annotations.SerializedName - - -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 = arrayListOf() - +data class Amenity( + val elements: List, ) \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/overpass/Bounds.kt b/common/data/src/main/java/com/kouros/navigation/data/overpass/Bounds.kt new file mode 100644 index 0000000..96ad39f --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/data/overpass/Bounds.kt @@ -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 +) \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/overpass/ElementSearch.kt b/common/data/src/main/java/com/kouros/navigation/data/overpass/ElementSearch.kt new file mode 100644 index 0000000..68ce5c6 --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/data/overpass/ElementSearch.kt @@ -0,0 +1,7 @@ +package com.kouros.navigation.data.overpass + +data class ElementSearch( + val element: Elements, + val distance: Double, + val bearing: Float, +) diff --git a/common/data/src/main/java/com/kouros/navigation/data/overpass/Elements.kt b/common/data/src/main/java/com/kouros/navigation/data/overpass/Elements.kt index 170df49..90e5e2f 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/overpass/Elements.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/overpass/Elements.kt @@ -1,15 +1,14 @@ package com.kouros.navigation.data.overpass -import com.google.gson.annotations.SerializedName - -data class Elements ( - - @SerializedName("type" ) var type : String = "", - @SerializedName("id" ) var id : Long = 0, - @SerializedName("lat" ) var lat : Double = 0.0, - @SerializedName("lon" ) var lon : Double = 0.0, - @SerializedName("tags" ) var tags : Tags = Tags(), - var distance : Double = 0.0 +data class Elements( + val bounds: Bounds, + val geometry: List, + val id: Long = 0, + val lat: Double= 0.0, + val lon: Double = 0.0, + val tags: Tags, + val type: String = "", + var distance : Double = 0.0 ) \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/overpass/Geometry.kt b/common/data/src/main/java/com/kouros/navigation/data/overpass/Geometry.kt new file mode 100644 index 0000000..efbe73e --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/data/overpass/Geometry.kt @@ -0,0 +1,6 @@ +package com.kouros.navigation.data.overpass + +data class Geometry( + val lat: Double = 0.0, + val lon: Double = 0.0 +) \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/overpass/Overpass.kt b/common/data/src/main/java/com/kouros/navigation/data/overpass/Overpass.kt index 7190ea2..0ab43a3 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/overpass/Overpass.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/overpass/Overpass.kt @@ -1,7 +1,9 @@ package com.kouros.navigation.data.overpass import android.location.Location +import android.util.Log import com.google.gson.GsonBuilder +import com.kouros.data.BuildConfig import com.kouros.navigation.utils.GeoUtils.getBoundingBox import java.io.OutputStreamWriter import java.net.HttpURLConnection @@ -9,12 +11,43 @@ import java.net.URL class Overpass { - //val overpassUrl = "https://overpass.kumi.systems/api/interpreter" - //val overpassUrl = "https://overpass-api.de/api" - val overpassUrl = "https://kouros-online.de/overpass/interpreter" + var overpassUrl = if (BuildConfig.DEBUG) + "http://192.168.1.37/api/interpreter" + else + "https://kouros-online.de/api/interpreter" - fun getAround(radius: Int, linestring: String): List { + fun getSpeedLimit(radius: Float, linestring: String, street: String, roadNumbers: List): List { + 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 httpURLConnection.requestMethod = "POST" httpURLConnection.setRequestProperty( @@ -26,15 +59,14 @@ class Overpass { val searchQuery = """ |[out:json]; |( - | way[highway](around:$radius,$linestring) - | ; + | $waySearch; |); - |out body; + |out body geom; """.trimMargin() - //println("way[highway](around:$radius,$linestring)") return overpassApi(httpURLConnection, searchQuery) } + fun getAmenities( type: String, category: String, @@ -59,7 +91,7 @@ class Overpass { | ($boundingBox); |); |(._;>;); - |out body; + |out body geom; """.trimMargin() return overpassApi(httpURLConnection, searchQuery) } @@ -70,17 +102,22 @@ class Overpass { outputStreamWriter.write(searchQuery) outputStreamWriter.flush() // Check if the connection is successful + httpURLConnection.requestMethod = "POST" val responseCode = httpURLConnection.responseCode if (responseCode == HttpURLConnection.HTTP_OK) { val response = httpURLConnection.inputStream.bufferedReader() .use { it.readText() } // defaults to UTF-8 + if (response.startsWith(" = mutableListOf(), - val countryCode : String = "" + val countryCode : String = "", + val roadNumbers : List = emptyList(), ) diff --git a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt index 8178d34..6ef4bef 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt @@ -21,7 +21,7 @@ const val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incident private const val tomtomFields = "{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}" -val useLocal = false // BuildConfig.DEBUG +val useLocal = BuildConfig.DEBUG val useLocalTraffic = BuildConfig.DEBUG @@ -40,7 +40,7 @@ class TomTomRepository : NavigationRepository() { } if (useLocal) { return fetchUrl( - "https://kouros-online.de/tomtom_routing.json", + "http://192.168.1.37/tomtom_routing.json", false ) } @@ -104,7 +104,7 @@ class TomTomRepository : NavigationRepository() { val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0) return if (useLocalTraffic) { fetchUrl( - "https://kouros-online.de/tomtom_traffic.json", + "http://192.168.1.37/tomtom_traffic.json", false ) } else { diff --git a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRoute.kt b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRoute.kt index 6def103..bbef338 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRoute.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRoute.kt @@ -99,6 +99,11 @@ class TomTomRoute { route.guidance.instructions[index].routeOffsetInMeters - stepDistance stepDuration = route.guidance.instructions[index].travelTimeInSeconds - stepDuration + val roadNumbers = if (lastInstruction.roadNumbers != null) { + lastInstruction.roadNumbers + } else { + emptyList() + } val step = Step( index = stepIndex, street = street, @@ -106,7 +111,8 @@ class TomTomRoute { duration = stepDuration, maneuver = maneuver, intersection = intersections, - countryCode = lastInstruction.countryCode + countryCode = lastInstruction.countryCode, + roadNumbers = roadNumbers ) stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble() stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble() diff --git a/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt b/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt index a2803fd..348732c 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt @@ -1,10 +1,9 @@ package com.kouros.navigation.model -//import com.kouros.navigation.data.Preferences.boxStore import android.content.Context import android.location.Location -import android.location.LocationListener -import android.location.LocationManager +import android.util.Log + import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList import androidx.lifecycle.MutableLiveData @@ -12,15 +11,18 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.gson.GsonBuilder 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.Place import com.kouros.navigation.data.Places import com.kouros.navigation.data.SearchFilter import com.kouros.navigation.data.nominatim.Search 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.Overpass -import com.kouros.navigation.utils.Levenshtein +import com.kouros.navigation.utils.countryCodeSpeedLimit import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.location import kotlinx.coroutines.Dispatchers @@ -31,9 +33,12 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.maplibre.geojson.FeatureCollection -import java.lang.reflect.Modifier import java.time.LocalDateTime 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. @@ -86,6 +91,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo MutableLiveData() } + /** LiveData containing POI elements from Overpass API */ + val speedElements = mutableListOf() + + /** LiveData containing speed camera locations */ val speedCameras: MutableLiveData> by lazy { MutableLiveData() @@ -107,10 +116,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo } val gson: com.google.gson.Gson = GsonBuilder().create() + /** * Retrieves recent places from Preferences as a Flow. */ - fun recentPlacesFlow(context: Context, location: Location,): Flow = callbackFlow { + fun recentPlacesFlow(context: Context, location: Location): Flow = callbackFlow { for (place in recentPlaces.value!!) { 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. * Posts speed limit to maxSpeed LiveData. */ - fun getMaxSpeed(location: Location, street: String) { + fun getSpeedLimit( + location: Location, + routeBearing: Float, + countryCode: String, + ) { viewModelScope.launch(Dispatchers.IO) { - val levenshtein = Levenshtein() - 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) - } - } - } + maxSpeed.postValue(calculateSpeedLimit(location, routeBearing, countryCode)) } } + /** + * 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() + + 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 { 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 + ) { + 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. */ diff --git a/common/data/src/main/java/com/kouros/navigation/model/RouteCalculator.kt b/common/data/src/main/java/com/kouros/navigation/model/RouteCalculator.kt index defe253..6de65d9 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/RouteCalculator.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/RouteCalculator.kt @@ -1,13 +1,12 @@ package com.kouros.navigation.model import android.location.Location -import android.util.Log import androidx.car.app.navigation.model.Step import com.kouros.navigation.data.Constants.MAXIMUM_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 java.util.concurrent.TimeUnit -import kotlin.math.roundToInt class RouteCalculator(var routeModel: RouteModel) { @@ -107,14 +106,27 @@ class RouteCalculator(var routeModel: RouteModel) { return nowUtcMillis + timeToDestinationMillis } + /** + * Updates the speed limit in the view model. + */ fun updateSpeedLimit(location: Location, viewModel: NavigationViewModel) { if (routeModel.isNavigating()) { // speed limit 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 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 + ) } } } diff --git a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt index 5a462f4..267b395 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt @@ -105,7 +105,8 @@ open class RouteModel { leftDistance = routeCalculator.travelLeftDistance(), lane = currentLanes, exitNumber = exitNumber, - message = currentStep.maneuver.message + message = currentStep.maneuver.message, + roadNumbers = currentStep.roadNumbers ) } diff --git a/common/data/src/main/java/com/kouros/navigation/utils/GeoUtils.kt b/common/data/src/main/java/com/kouros/navigation/utils/GeoUtils.kt index 49eb74d..0178af6 100644 --- a/common/data/src/main/java/com/kouros/navigation/utils/GeoUtils.kt +++ b/common/data/src/main/java/com/kouros/navigation/utils/GeoUtils.kt @@ -4,6 +4,7 @@ import android.location.Location import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.maplibre.geojson.FeatureCollection +import org.maplibre.geojson.LineString import org.maplibre.geojson.Point import org.maplibre.spatialk.geojson.Feature import org.maplibre.spatialk.geojson.dsl.addFeature @@ -19,7 +20,7 @@ import kotlin.math.pow object GeoUtils { - fun snapLocation(location: Location, stepCoordinates: List) : Location { + fun snapLocation(location: Location, stepCoordinates: List): Location { val newLocation = Location(location) val oldPoint = Point.fromLngLat(location.longitude, location.latitude) if (stepCoordinates.size > 1) { @@ -34,7 +35,7 @@ object GeoUtils { return newLocation } - fun decodePolyline(encoded: String, precision: Int = 6): List> { + fun decodePolyline(encoded: String, precision: Int = 6): List> { val factor = 10.0.pow(precision) var lat = 0 var lng = 0 @@ -91,18 +92,20 @@ object GeoUtils { } fun createLineStringCollection(lineCoordinates: List>): String { - // return createPointCollection(lineCoordinates, "Route") + // return createPointCollection(lineCoordinates, "Route") val lineString = buildLineString { lineCoordinates.forEach { - add(org.maplibre.spatialk.geojson.Point( - it[0], - it[1] - )) + add( + org.maplibre.spatialk.geojson.Point( + it[0], + it[1] + ) + ) } } val feature = Feature(lineString, null) val featureCollection = org.maplibre.spatialk.geojson.FeatureCollection(feature) - return featureCollection.toJson() + return featureCollection.toJson() } fun createPointCollection(lineCoordinates: List>, 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. @@ -131,6 +155,7 @@ object GeoUtils { return "$lngMin,$latMin,$lngMax,$latMax" } + fun getBoundingBox( lat: Double, lon: Double, @@ -144,4 +169,18 @@ object GeoUtils { 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 + } } \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/utils/Levenshtein.kt b/common/data/src/main/java/com/kouros/navigation/utils/Levenshtein.kt index b9751e8..0b8692d 100644 --- a/common/data/src/main/java/com/kouros/navigation/utils/Levenshtein.kt +++ b/common/data/src/main/java/com/kouros/navigation/utils/Levenshtein.kt @@ -25,10 +25,11 @@ class Levenshtein { * @param limit the maximum result to compute before stopping, terminating calculation early. * @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.isEmpty()) return second.length - if (second.isEmpty()) return first.length + if (first.isEmpty()) return 0 + if (second.isEmpty()) return 0 // 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) diff --git a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt index 17be864..f1a7130 100644 --- a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt +++ b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt @@ -3,11 +3,10 @@ package com.kouros.navigation.utils import android.content.Context import android.location.Location import android.location.LocationManager -import android.util.Log 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.RouteEngine +import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.osrm.OsrmRepository import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.valhalla.ValhallaRepository @@ -94,15 +93,19 @@ fun calculateZoomFromBoundingBox(centerLocation: Location, previewDistance: Doub } -fun calculateTilt(newZoom: Double, tilt: Double): Double = - if (newZoom < 13) { - 0.0 - } else { - if (tilt == 0.0) { - TILT +fun calculateTilt(viewStyle: ViewStyle, newZoom: Double, tilt: Double): Double = + if (viewStyle == ViewStyle.VIEW) { + if (newZoom < 13) { + 0.0 } else { - tilt + if (tilt == 0.0) { + TILT + } else { + tilt + } } + } else { + return 0.0 } fun bearing(fromLocation: Location, toLocation: Location, oldBearing: Double): Double { @@ -134,13 +137,14 @@ fun Double.round(numFractionDigits: Int): Double { } fun duration( - preview: Boolean, + viewStyle: ViewStyle, bearing: Double, lastBearing: Double, lastLocationUpdate: LocalDateTime ): Duration { - if (preview) { - return 10.milliseconds + if (viewStyle == ViewStyle.PREVIEW || + viewStyle == ViewStyle.PAN_VIEW) { + return 100.milliseconds } val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) { 2.seconds @@ -184,3 +188,11 @@ fun formattedDistance(distanceMode: Int, distance: Double): Pair { } 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 + } +} diff --git a/common/data/src/main/res/drawable/sports_score_48px.xml b/common/data/src/main/res/drawable/sports_score_48px.xml new file mode 100644 index 0000000..66932e0 --- /dev/null +++ b/common/data/src/main/res/drawable/sports_score_48px.xml @@ -0,0 +1,10 @@ + + + diff --git a/common/data/src/main/res/values-de/strings.xml b/common/data/src/main/res/values-de/strings.xml index 7a3935d..589620b 100644 --- a/common/data/src/main/res/values-de/strings.xml +++ b/common/data/src/main/res/values-de/strings.xml @@ -72,4 +72,5 @@ Electric Engine type Alternative routes + Wait diff --git a/common/data/src/main/res/values-el/strings.xml b/common/data/src/main/res/values-el/strings.xml index f557400..0c5cc40 100644 --- a/common/data/src/main/res/values-el/strings.xml +++ b/common/data/src/main/res/values-el/strings.xml @@ -56,4 +56,5 @@ Electric Engine type Alternative routes + Wait diff --git a/common/data/src/main/res/values-pl/strings.xml b/common/data/src/main/res/values-pl/strings.xml index f6bb504..3d4e890 100644 --- a/common/data/src/main/res/values-pl/strings.xml +++ b/common/data/src/main/res/values-pl/strings.xml @@ -56,4 +56,5 @@ Electric Engine type Alternative routes + Wait diff --git a/common/data/src/main/res/values/strings.xml b/common/data/src/main/res/values/strings.xml index 2810404..3bf6efa 100644 --- a/common/data/src/main/res/values/strings.xml +++ b/common/data/src/main/res/values/strings.xml @@ -59,4 +59,5 @@ Electric Engine type Alternative routes + Wait \ No newline at end of file diff --git a/common/data/src/test/java/com/kouros/navigation/model/OverpassTest.kt b/common/data/src/test/java/com/kouros/navigation/model/OverpassTest.kt new file mode 100644 index 0000000..54fe426 --- /dev/null +++ b/common/data/src/test/java/com/kouros/navigation/model/OverpassTest.kt @@ -0,0 +1,59 @@ +package com.kouros.navigation.model + +import android.location.Location +import com.google.gson.GsonBuilder +import com.kouros.navigation.data.overpass.Amenity +import com.kouros.navigation.data.overpass.Elements +import com.kouros.navigation.data.overpass.Overpass +import com.kouros.navigation.data.tomtom.TomTomRepository +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class OverpassTest { + + val route = + "{\n \"version\": 0.6,\n \"generator\": \"Overpass API 0.7.62.4 2390de5a\",\n \"osm3s\": {\n \"timestamp_osm_base\": \"\",\n \"copyright\": \"The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.\"\n },\n \"elements\": [\n {\n \"type\": \"way\",\n \"id\": 288782966,\n \"bounds\": {\n \"minlat\": 48.1619892,\n \"minlon\": 11.8609409,\n \"maxlat\": 48.1675377,\n \"maxlon\": 11.8855018\n },\n \"geometry\": [\n {\n \"lat\": 48.1619892,\n \"lon\": 11.8609409\n },\n {\n \"lat\": 48.1620566,\n \"lon\": 11.8617319\n },\n {\n \"lat\": 48.1622095,\n \"lon\": 11.8631368\n },\n {\n \"lat\": 48.1623460,\n \"lon\": 11.8644074\n },\n {\n \"lat\": 48.1625048,\n \"lon\": 11.8656775\n },\n {\n \"lat\": 48.1627091,\n \"lon\": 11.8672028\n },\n {\n \"lat\": 48.1629402,\n \"lon\": 11.8686900\n },\n {\n \"lat\": 48.1631729,\n \"lon\": 11.8700333\n },\n {\n \"lat\": 48.1634153,\n \"lon\": 11.8713781\n },\n {\n \"lat\": 48.1637932,\n \"lon\": 11.8731800\n },\n {\n \"lat\": 48.1641213,\n \"lon\": 11.8746189\n },\n {\n \"lat\": 48.1644717,\n \"lon\": 11.8760567\n },\n {\n \"lat\": 48.1648483,\n \"lon\": 11.8774661\n },\n {\n \"lat\": 48.1650472,\n \"lon\": 11.8781814\n },\n {\n \"lat\": 48.1652483,\n \"lon\": 11.8788598\n },\n {\n \"lat\": 48.1656045,\n \"lon\": 11.8800840\n },\n {\n \"lat\": 48.1660008,\n \"lon\": 11.8812864\n },\n {\n \"lat\": 48.1665513,\n \"lon\": 11.8828570\n },\n {\n \"lat\": 48.1671206,\n \"lon\": 11.8844321\n },\n {\n \"lat\": 48.1675377,\n \"lon\": 11.8855018\n }\n ],\n \"tags\": {\n \"bdouble\": \"yes\",\n \"highway\": \"motorway\",\n \"int_ref\": \"E 552\",\n \"lanes\": \"2\",\n \"lit\": \"no\",\n \"maxheight\": \"default\",\n \"maxspeed\": \"none\",\n \"oneway\": \"yes\",\n \"ref\": \"A 94\",\n \"surface\": \"concrete\",\n \"tmc\": \"DE:12905/12906\"\n }\n },\n {\n \"type\": \"way\",\n \"id\": 288782968,\n \"bounds\": {\n \"minlat\": 48.1621875,\n \"minlon\": 11.8620178,\n \"maxlat\": 48.1694156,\n \"maxlon\": 11.8894962\n },\n \"geometry\": [\n {\n \"lat\": 48.1694156,\n \"lon\": 11.8894962\n },\n {\n \"lat\": 48.1691203,\n \"lon\": 11.8888740\n },\n {\n \"lat\": 48.1686990,\n \"lon\": 11.8879594\n },\n {\n \"lat\": 48.1683207,\n \"lon\": 11.8870930\n },\n {\n \"lat\": 48.1678958,\n \"lon\": 11.8860845\n },\n {\n \"lat\": 48.1672082,\n \"lon\": 11.8843454\n },\n {\n \"lat\": 48.1666293,\n \"lon\": 11.8827638\n },\n {\n \"lat\": 48.1660917,\n \"lon\": 11.8811913\n },\n {\n \"lat\": 48.1654436,\n \"lon\": 11.8791649\n },\n {\n \"lat\": 48.1650847,\n \"lon\": 11.8779268\n },\n {\n \"lat\": 48.1647476,\n \"lon\": 11.8766760\n },\n {\n \"lat\": 48.1644025,\n \"lon\": 11.8753593\n },\n {\n \"lat\": 48.1641792,\n \"lon\": 11.8744194\n },\n {\n \"lat\": 48.1639641,\n \"lon\": 11.8734618\n },\n {\n \"lat\": 48.1634961,\n \"lon\": 11.8712468\n },\n {\n \"lat\": 48.1632564,\n \"lon\": 11.8699398\n },\n {\n \"lat\": 48.1630250,\n \"lon\": 11.8686135\n },\n {\n \"lat\": 48.1628050,\n \"lon\": 11.8671495\n },\n {\n \"lat\": 48.1625984,\n \"lon\": 11.8656577\n },\n {\n \"lat\": 48.1624469,\n \"lon\": 11.8643635\n },\n {\n \"lat\": 48.1623072,\n \"lon\": 11.8631342\n },\n {\n \"lat\": 48.1621875,\n \"lon\": 11.8620178\n }\n ],\n \"tags\": {\n \"bdouble\": \"yes\",\n \"highway\": \"motorway\",\n \"int_ref\": \"E 552\",\n \"lanes\": \"2\",\n \"lit\": \"no\",\n \"maxheight\": \"default\",\n \"maxspeed\": \"none\",\n \"oneway\": \"yes\",\n \"ref\": \"A 94\",\n \"surface\": \"concrete\",\n \"tmc\": \"DE:12905/12906\"\n }\n }\n ]\n}" + val speedElements = mutableListOf() + + private lateinit var overpass: Overpass + + @Before + fun setup() { + overpass = Overpass() + val gson = GsonBuilder().serializeNulls().create() + val overpass = gson.fromJson(route, Amenity::class.java) + speedElements.addAll(overpass.elements) + } + + @Test + fun `checkElements `() { + assertEquals(speedElements.size, 2) + } + + @Test + fun `set `() { + val model = NavigationViewModel(TomTomRepository()) + val mockLocation: Location = mock() + whenever(mockLocation.distanceTo(any())).thenReturn(500F, 400F, 300F, 8F) + whenever(mockLocation.bearingTo(any())).thenReturn(50F) + model.speedElements.addAll(speedElements) + val speed = model.calculateSpeedLimit(mockLocation, 90F, "DEU") + verify(mockLocation, times(2)).bearingTo(any()) + assertEquals(speed, 0) + } + + @Test + fun `getSpeedLimit `() { + val mockOverpass: Overpass = mock() + whenever(mockOverpass.getSpeedLimit(100F, any(), any(), any())).thenReturn(speedElements) + overpass.getSpeedLimit(100F, eq(""), eq(""), emptyList()) + + } +} \ No newline at end of file diff --git a/common/data/src/test/java/com/kouros/navigation/model/RouteModelTest.kt b/common/data/src/test/java/com/kouros/navigation/model/RouteModelTest.kt index b2e11b5..1f6d672 100644 --- a/common/data/src/test/java/com/kouros/navigation/model/RouteModelTest.kt +++ b/common/data/src/test/java/com/kouros/navigation/model/RouteModelTest.kt @@ -8,8 +8,11 @@ import com.kouros.navigation.data.route.Maneuver import com.kouros.navigation.data.route.Routes import com.kouros.navigation.data.route.Step import com.kouros.navigation.data.route.Summary +import com.kouros.navigation.utils.GeoUtils.createPointCollection import org.junit.Before import org.junit.Test +import org.maplibre.geojson.FeatureCollection +import org.maplibre.geojson.LineString import org.mockito.kotlin.any import org.mockito.kotlin.doNothing import org.mockito.kotlin.mock @@ -58,6 +61,18 @@ class RouteModelTest { 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 fun `hasLegs returns true when route has legs`() { val step0 = createStep(index = 0, numWaypoints = 2) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 67c6c31..8cfcc4e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -agp = "9.1.0" +agp = "9.1.1" androidGpxParser = "2.3.1" androidSdkTurf = "6.0.1" datastore = "1.2.1" -gradle = "9.1.0" +gradle = "9.1.1" koinAndroid = "4.2.0" koinAndroidxCompose = "4.2.0" koinComposeViewmodel = "4.2.0"