From 17e68de0174db355a21745325a9af8314b454861 Mon Sep 17 00:00:00 2001 From: Dimitris Date: Fri, 24 Apr 2026 12:24:53 +0200 Subject: [PATCH] Pan Mode --- app/build.gradle.kts | 4 +- .../com/kouros/navigation/ui/MainActivity.kt | 10 +- .../com/kouros/navigation/car/OverpassTest.kt | 4 +- .../navigation/car/DeviceLocationManager.kt | 7 +- .../navigation/car/NavigationSession.kt | 77 ++++----- .../kouros/navigation/car/SurfaceRenderer.kt | 44 +++-- .../com/kouros/navigation/car/map/MapView.kt | 4 +- .../navigation/car/navigation/Simulation.kt | 4 +- .../navigation/car/screen/NavigationScreen.kt | 11 +- .../java/com/kouros/navigation/data/Data.kt | 4 +- .../navigation/data/overpass/Overpass.kt | 151 +++++++++--------- .../navigation/data/tomtom/Instruction.kt | 2 +- .../data/tomtom/TomTomRepository.kt | 5 +- .../navigation/model/NavigationViewModel.kt | 71 ++++---- .../com/kouros/navigation/model/RouteModel.kt | 2 + .../com/kouros/navigation/utils/GeoUtils.kt | 2 + .../navigation/utils/NavigationUtils.kt | 9 +- gradle/libs.versions.toml | 36 ++--- 18 files changed, 242 insertions(+), 205 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 033624e..208b52c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,8 +17,8 @@ android { applicationId = "com.kouros.navigation" minSdk = 33 targetSdk = 37 - versionCode = 97 - versionName = "0.2.3.97" + versionCode = 98 + versionName = "0.2.3.98" base.archivesName = "navi-$versionName" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt index a88cbdc..181b476 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt @@ -1,13 +1,8 @@ package com.kouros.navigation.ui import android.Manifest -import android.content.ComponentName -import android.content.Intent -import android.content.ServiceConnection import android.location.LocationManager import android.os.Bundle -import android.os.IBinder -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -45,7 +40,6 @@ import com.kouros.navigation.MainApplication.Companion.navigationViewModel import com.kouros.navigation.car.TextToSpeechManager import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE -import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.TILT import com.kouros.navigation.data.StepData import com.kouros.navigation.model.BaseStyleModel @@ -59,7 +53,7 @@ import com.kouros.navigation.ui.navigation.NavigationSheet import com.kouros.navigation.ui.search.SearchSheet import com.kouros.navigation.ui.theme.NavigationTheme import com.kouros.navigation.utils.GeoUtils.snapLocation -import com.kouros.navigation.utils.bearing +import com.kouros.navigation.utils.bearingPositive import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.location @@ -278,7 +272,7 @@ class MainActivity : ComponentActivity() { val bearing = if (currentLocation.hasBearing()) { currentLocation.bearing.toDouble() } else { - bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing) + bearingPositive(lastLocation, currentLocation, cameraPosition.value!!.bearing) } with(routeModel) { 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 index 988c804..cf986aa 100644 --- a/common/car/src/androidTest/java/com/kouros/navigation/car/OverpassTest.kt +++ b/common/car/src/androidTest/java/com/kouros/navigation/car/OverpassTest.kt @@ -14,8 +14,6 @@ import org.junit.Test class OverpassTest { - val location = Location(LocationManager.GPS_PROVIDER) - @Test fun `maxSpeed Schmalkaldener 30 `() { val curLocation = location(11.582495, 48.186863) @@ -31,7 +29,7 @@ class OverpassTest { @Test fun `maxSpeed Isarring 50 `() { val curLocation = location(11.5989114, 48.1694783) - executeSpeedTest( curLocation, "Isarring", listOf("B2R"), 190, 50) + executeSpeedTest( curLocation, "Isarring", listOf("B2R"), 190, 60) } @Test 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 7bcab16..8582158 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 @@ -12,10 +12,13 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.kouros.data.BuildConfig +import com.kouros.navigation.data.Constants.a22 import com.kouros.navigation.data.Constants.a9 import com.kouros.navigation.data.Constants.a94 +import com.kouros.navigation.data.Constants.homeHohenwaldeck import com.kouros.navigation.data.Constants.homeVogelhart import com.kouros.navigation.data.Constants.ioannina +import com.kouros.navigation.data.Constants.isarring import com.kouros.navigation.data.Constants.subislawa import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -104,8 +107,8 @@ class DeviceLocationManager( val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) if (lastLocation != null) { if (setIndividualLocation) { - onInitialLocation(homeVogelhart) - onLocationUpdate(homeVogelhart) + onInitialLocation(a22) + onLocationUpdate(a22) } else { onInitialLocation(lastLocation) onLocationUpdate(lastLocation) 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 6f7233e..4bb031c 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 @@ -54,6 +54,7 @@ import com.kouros.navigation.repository.SettingsRepository import com.kouros.navigation.utils.GeoUtils import com.kouros.navigation.utils.GeoUtils.snapLocation import com.kouros.navigation.utils.NavigationUtils.getViewModel +import com.kouros.navigation.utils.bearingPositive import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.location @@ -464,23 +465,25 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa */ private fun handleNavigationLocation(location: Location) { val startTime = System.currentTimeMillis() - routeModel.updateLocation(location, navigationViewModel) - val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations()) + routeModel.updateLocation(location, navigationViewModel) // 30 ms + val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations()) // 160 ms val streetName = routeModel.currentStep().street - if (checkLocationDeviation(location, snappedLocation, streetName)) { + if (checkLocationDeviation(location, snappedLocation, streetName)) { // 1 ms if (routeModel.navState.arrived) return if (guidanceAudio == 1) { handleGuidanceAudio() } val currentDate = LocalDateTime.now(ZoneOffset.UTC) - checkTraffic(currentDate, snappedLocation) - updateSpeedCamera(snappedLocation) - checkRoute(currentDate, snappedLocation) - updateNavigationScreen() + checkTraffic(currentDate, snappedLocation) // 0 ms + updateSpeedCamera(snappedLocation) // 0 ms + checkRoute(currentDate, snappedLocation) // 0 ms + updateNavigationScreen() // 660 ms + val endTime = System.currentTimeMillis() - startTime +// Log.d(TAG, "updateNavigationScreen: $endTime") checkArrival() + } - val endTime = System.currentTimeMillis() - startTime - Log.d(TAG, "handleNavigationLocation: $endTime") + } /** @@ -498,9 +501,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa distance < MAXIMAL_SNAP_CORRECTION -> { surfaceRenderer.updateLocation(snappedLocation, streetName) } - else -> { surfaceRenderer.updateLocation(location, streetName) + } } return true @@ -516,15 +519,14 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa return } - val currentStep = routeModel.route.currentStep() - val stepData = routeModel.currentStep() + val stepData = routeModel.currentStep() // 121 ms - navigationScreen.updateTrip( + navigationScreen.updateTrip( // 277 ms isNavigating = routeModel.isNavigating(), isRerouting = false, hasArrived = routeModel.isArrival(), - destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext), - stepTravelEstimate = routeModel.getTravelEstimateStep(carContext), + destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext), // 60 ms + stepTravelEstimate = routeModel.getTravelEstimateStep(carContext), // 60 ms destinations = mutableListOf(routeModel.getDestination()), steps = routeModel.getSteps(carContext), stepRemainingDistance = routeModel.getDistance(), @@ -539,7 +541,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa * Updates the trip information and notifies the listener with a new Trip object. * This includes destination name, address, travel estimate, and loading status. */ - updateTrip(routeModel.getTrip(carContext)) + updateTrip(routeModel.getTrip(carContext)) // 230 ms } /** @@ -594,6 +596,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa lastCameraSearch = 0 surfaceRenderer.viewStyle = ViewStyle.VIEW navigationScreen.navigationType = NavigationType.VIEW + navigationScreen.invalidate() } override fun updateTrip(trip: Trip) { @@ -781,26 +784,28 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa private fun updateDistance( location: Location, ) { - val updatedCameras = mutableListOf() - speedCameras.forEach { - val plLocation = - location(longitude = it.lon, latitude = it.lat) - val distance = plLocation.distanceTo(location) - it.distance = distance.toDouble() - updatedCameras.add(it) - } - val sortedList = updatedCameras.sortedWith(compareBy { it.distance }) - val camera = sortedList.firstOrNull() ?: return - val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location) - val bearingSpeedCamera = try { - camera.tags.direction.toFloat() - } catch (e: Exception) { - 0F - } - 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() + synchronized(this) { + val updatedCameras = mutableListOf() + speedCameras.forEach { + val plLocation = + location(longitude = it.lon, latitude = it.lat) + val distance = plLocation.distanceTo(location) + it.distance = distance.toDouble() + updatedCameras.add(it) + } + val sortedList = updatedCameras.sortedWith(compareBy { it.distance }) + val camera = sortedList.firstOrNull() ?: return + val bearingRoute = surfaceRenderer.lastLocation.bearingPositive(location) + val bearingSpeedCamera = try { + camera.tags.direction.toFloat() + } catch (e: Exception) { + 0F + } + if (camera.distance < 80 && (System.currentTimeMillis() - lastAlert > 10000)) { + if ((bearingSpeedCamera - bearingRoute).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 cca6693..fca44a0 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 @@ -34,7 +34,7 @@ import com.kouros.navigation.data.Constants.TILT import com.kouros.navigation.data.DarkMode import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.model.BaseStyleModel -import com.kouros.navigation.utils.bearing +import com.kouros.navigation.utils.bearingPositive import com.kouros.navigation.utils.calculateTilt import com.kouros.navigation.utils.calculateZoom import com.kouros.navigation.utils.duration @@ -46,6 +46,7 @@ import org.maplibre.compose.camera.CameraState import org.maplibre.compose.style.BaseStyle import org.maplibre.spatialk.geojson.Position import java.time.LocalDateTime +import kotlin.math.* /** @@ -225,15 +226,40 @@ class SurfaceRenderer( */ override fun onScroll(distanceX: Float, distanceY: Float) { synchronized(this@SurfaceRenderer) { + val currentCamera = cameraPosition.value ?: return@synchronized + val zoom = currentCamera.zoom + val bearing = currentCamera.bearing + + // MapLibre typically uses 512px tiles. + // At zoom level z, the world (360 degrees) is 512 * 2^z pixels wide. + val pixelsPerDegreeLon = (512.0 * 2.0.pow(zoom)) / 360.0 + + // Latitude correction: In Mercator, the vertical scale is stretched by 1/cos(lat). + val latRad = lastLocation.latitude * PI / 180.0 + val pixelsPerDegreeLat = pixelsPerDegreeLon / cos(latRad) + + // Rotation compensation (bearing is degrees clockwise from North) + val bearingRad = bearing * PI / 180.0 + val cosB = cos(bearingRad) + val sinB = sin(bearingRad) + + // Rotate screen-space scroll to map-space scroll + val rotatedDx = distanceX * cosB - distanceY * sinB + val rotatedDy = distanceX * sinB + distanceY * cosB + viewStyle = ViewStyle.PAN_VIEW - if (distanceX != 0.0F) { - lastLocation.longitude += (distanceX / 1000) / cameraPosition.value!!.zoom - } - if (distanceY != 0.0F) { - lastLocation.latitude += (distanceY / 1000) / cameraPosition.value!!.zoom - } + + // Update location based on rotated scroll distances and calculated factors + lastLocation.longitude += (rotatedDx / pixelsPerDegreeLon) + lastLocation.latitude -= (rotatedDy / pixelsPerDegreeLat) + val pos = Position(lastLocation.longitude, lastLocation.latitude) - updateCameraPosition( target = pos) + updateCameraPosition( + bearing = bearing, + zoom = zoom, + target = pos, + tilt = tilt + ) navigationSession.invalidateNavigationScreen() } } @@ -374,7 +400,7 @@ class SurfaceRenderer( if (location.hasBearing()) { location.bearing.toDouble() } else { - bearing( + bearingPositive( lastLocation, location, cameraPosition.value!!.bearing 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 6b33d0b..0156b34 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 @@ -330,7 +330,7 @@ fun DrawNavigationImages( if (speed != null) { CurrentSpeed(width, height, speed, maxSpeed) } - if (speed != null && maxSpeed > 0) { // && (speed * 3.6) > maxSpeed) { + if (speed != null && maxSpeed > 0) { MaxSpeed(width, height, maxSpeed, speed) } //DebugInfo(width, height, lat!!) @@ -507,7 +507,7 @@ private fun MaxSpeed( val textLayoutSpeed = remember(speed) { textMeasurerSpeed.measure(speed, styleSpeed) } - val signColor = if (curSpeed * 3.6 > maxSpeed) { + val signColor = if (curSpeed * 3.6 > (maxSpeed + 3)) { Color.Red } else { Color.Green 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 c7a1f88..c3c7e6e 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 @@ -56,14 +56,14 @@ class Simulation { longitude = point[0] bearing = curBearing speedAccuracyMetersPerSecond = 1.0f // ~1 m/s - speed = 10.0f + speed = 9.0f time = System.currentTimeMillis() elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() } // 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) - delay(1000) + delay(500) lastLocation = fakeLocation } } 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 f8f9e3c..6f010af 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 @@ -100,7 +100,6 @@ open class NavigationScreen( repository.tripSuggestionFlow.asLiveData().observe(this, Observer { navigationViewModel.recentPlaces.observe(this, observerRecentPlaces) - //navigationViewModel.recentPlacesFlow(carContext, surfaceRenderer.lastLocation ).asLiveData().observe(this, ::test) tripSuggestion = it }) repository.alternativeRoutesFlow.asLiveData().observe(this, Observer { @@ -142,10 +141,10 @@ open class NavigationScreen( createAction( carContext, R.drawable.ic_close_white_24dp, - 0, - { stopNavigation() }) + 0 + ) { stopNavigation() } ) - return NavigationTemplate.Builder() + val navigationTemplate = NavigationTemplate.Builder() .setNavigationInfo( getRoutingInfo() ) @@ -167,6 +166,7 @@ open class NavigationScreen( ) .setBackgroundColor(backGroundColor) .build() + return navigationTemplate } /** @@ -490,8 +490,6 @@ open class NavigationScreen( } } - - /** * Updates navigation state with the current location, checks for arrival, and traffic updates. */ @@ -523,7 +521,6 @@ open class NavigationScreen( this.junctionImage = junctionImage this.backGroundColor = backGroundColor this.message = message - navigationType = NavigationType.NAVIGATION invalidate() } 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 ca55824..1c28f88 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 @@ -128,8 +128,10 @@ object Constants { 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,) + + val a22 = location(10.845749, 45.602507) + val isarring = location( 11.609696, 48.155116) const val NEXT_STEP_THRESHOLD = 500.0 const val MAXIMAL_SNAP_CORRECTION = 50.0 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 0ab43a3..69141b3 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 @@ -11,59 +11,59 @@ import java.net.URL class Overpass { + private val gson = GsonBuilder().serializeNulls().create() + var overpassUrl = if (BuildConfig.DEBUG) "http://192.168.1.37/api/interpreter" else "https://kouros-online.de/api/interpreter" - + val destination = "[!destination][highway!=\"motorway_link\"]" 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 wayAround = "way[maxspeed](around:$radius,$linestring)" + val searchClauses = mutableListOf() + + // 1. Search by street name (fuzzy match with first 10 characters) + val streetPrefix = street.take(10) + if (streetPrefix.isNotEmpty()) { + searchClauses.add("$wayAround[name~\"^$streetPrefix\"]") } - 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}\"]") + // 2. Search by road numbers (ref or int_ref) + val partRegex = Regex("""\d+|\D+""") + roadNumbers.forEach { number -> + val parts = partRegex.findAll(number).map { it.value.trim() }.filter { it.isNotEmpty() }.toList() + if (parts.isNotEmpty()) { + // Construct ref value: e.g., "A1" -> "A 1", "E30" -> "E 30" + val refValue = if (parts.size > 1) "${parts[0]} ${parts.drop(1).joinToString("")}" else parts[0] + val tag = if (number.startsWith("E", ignoreCase = true)) "int_ref" else "ref" + searchClauses.add("$wayAround$destination[$tag~\"$refValue\"]") } } - val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection - httpURLConnection.requestMethod = "POST" - httpURLConnection.setRequestProperty( - "Accept", - "application/json" - ) - httpURLConnection.setDoOutput(true); - // define search query + + // 3. Fallback to searching everything if no specific filters were added + if (searchClauses.isEmpty()) { + searchClauses.add(wayAround) + } + val searchQuery = """ - |[out:json]; - |( - | $waySearch; - |); - |out body geom; + |[out:json]; + |( + | ${searchClauses.joinToString(";")}; + |); + |out body geom; """.trimMargin() - return overpassApi(httpURLConnection, searchQuery) + + Log.d("OverpassApi", "Overpass Query: $searchQuery") + + val connection = (URL(overpassUrl).openConnection() as HttpURLConnection).apply { + requestMethod = "POST" + setRequestProperty("Accept", "application/json") + doOutput = true + } + + return overpassApi(connection, searchQuery) } @@ -74,51 +74,50 @@ class Overpass { radius: Double ): List { val boundingBox = getBoundingBox(location.latitude, location.longitude, radius) - val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection - httpURLConnection.requestMethod = "POST" - // node["highway"="speed_camera"] - // node[amenity=$category] - httpURLConnection.setDoOutput(true); - httpURLConnection.setRequestProperty( - "Accept", - "application/json" - ) - // define search query + val searchQuery = """ - |[out:json]; - |( - | node[$type=$category] - | ($boundingBox); - |); - |(._;>;); - |out body geom; + |[out:json]; + |( + | node[$type=$category] + | ($boundingBox); + |); + |(._;>;); + |out body geom; """.trimMargin() - return overpassApi(httpURLConnection, searchQuery) + + val connection = (URL(overpassUrl).openConnection() as HttpURLConnection).apply { + requestMethod = "POST" + setRequestProperty("Accept", "application/json") + doOutput = true + } + + return overpassApi(connection, searchQuery) } - fun overpassApi(httpURLConnection: HttpURLConnection, searchQuery: String): List { - try { - val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream) - outputStreamWriter.write(searchQuery) - outputStreamWriter.flush() - // Check if the connection is successful - httpURLConnection.requestMethod = "POST" - val responseCode = httpURLConnection.responseCode + private fun overpassApi(connection: HttpURLConnection, searchQuery: String): List { + return try { + connection.outputStream.use { os -> + OutputStreamWriter(os).use { writer -> + writer.write(searchQuery) + writer.flush() + } + } + + val responseCode = connection.responseCode if (responseCode == HttpURLConnection.HTTP_OK) { - val response = httpURLConnection.inputStream.bufferedReader() - .use { it.readText() } // defaults to UTF-8 + val response = connection.inputStream.bufferedReader().use { it.readText() } if (response.startsWith(", + val roadNumbers: List = emptyList(), val routeOffsetInMeters: Int, val signpostText: String, val street: String? = "", 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 6ef4bef..8ed5478 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 @@ -23,7 +23,7 @@ private const val tomtomFields = val useLocal = BuildConfig.DEBUG -val useLocalTraffic = BuildConfig.DEBUG +val useLocalTraffic = BuildConfig.DEBUG class TomTomRepository : NavigationRepository() { override fun getRoute( @@ -40,7 +40,8 @@ class TomTomRepository : NavigationRepository() { } if (useLocal) { return fetchUrl( - "http://192.168.1.37/tomtom_routing.json", + //"http://192.168.1.37/tomtom_routing.json", + "http://192.168.1.37/verona.json", false ) } 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 348732c..e6da9a0 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 @@ -2,7 +2,6 @@ package com.kouros.navigation.model import android.content.Context import android.location.Location -import android.util.Log import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList @@ -22,6 +21,7 @@ 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.bearingPositive import com.kouros.navigation.utils.countryCodeSpeedLimit import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.location @@ -390,18 +390,20 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo * Posts sorted results to speedCameras LiveData. */ fun getSpeedCameras(location: Location, radius: Double) { - viewModelScope.launch(Dispatchers.IO) { - val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius) - val distAmenities = mutableListOf() - amenities.forEach { - val plLocation = - location(longitude = it.lon, latitude = it.lat) - val distance = plLocation.distanceTo(location) - it.distance = distance.toDouble() - distAmenities.add(it) + synchronized(this) { + viewModelScope.launch(Dispatchers.IO) { + val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius) + val distAmenities = mutableListOf() + amenities.forEach { + val plLocation = + location(longitude = it.lon, latitude = it.lat) + val distance = plLocation.distanceTo(location) + it.distance = distance.toDouble() + distAmenities.add(it) + } + val sortedList = distAmenities.sortedWith(compareBy { it.distance }) + speedCameras.postValue(sortedList) } - val sortedList = distAmenities.sortedWith(compareBy { it.distance }) - speedCameras.postValue(sortedList) } } @@ -415,7 +417,9 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo countryCode: String, ) { viewModelScope.launch(Dispatchers.IO) { - maxSpeed.postValue(calculateSpeedLimit(location, routeBearing, countryCode)) + synchronized(this) { + maxSpeed.postValue(calculateSpeedLimit(location, routeBearing, countryCode)) + } } } @@ -428,31 +432,30 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo val search = mutableListOf() speedElements.filter { it.type == "way"}.forEach { - var streetBearingSum = 0F - var streetBearingAvg = 0F var distance = 0F var maxDistance = 1000F + var geometryFirstLocation = location(0.0, 0.0) + var geometryLastLocation = location(0.0, 0.0) for ((geoIndex, geo) in it.geometry.withIndex()) { + if (geoIndex == 0) { + geometryFirstLocation= location(geo.lon, geo.lat) + } + if (geoIndex == it.geometry.size-1) { + geometryLastLocation = location(geo.lon, geo.lat) + } 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) { + val streetBearing = geometryFirstLocation.bearingPositive(geometryLastLocation) + if (isBearingValid(it, streetBearing, routeBearing)) { search.add( ElementSearch( it, maxDistance.toDouble(), - streetBearingAvg.absoluteValue + streetBearing.absoluteValue ) ) } @@ -460,7 +463,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo 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 { @@ -470,11 +472,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo return speed } - private fun calculateBearing(element: Elements, streetBearingAvg : Float, routeBearing: Float) : Float { + private fun isBearingValid(element: Elements, streetBearing : Float, routeBearing: Float) : Boolean { return if (element.tags.oneway.isNotEmpty()) { - (streetBearingAvg - routeBearing.absoluteValue).absoluteValue + (streetBearing - routeBearing.absoluteValue) < SPEED_BEARING_DEVIATION } else { - 0F + true } } @@ -488,10 +490,13 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo 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) + synchronized(this) { + val lineString = "${location.latitude},${location.longitude}" + val elements = + Overpass().getSpeedLimit(SPEED_UPDATE_DISTANCE, lineString, street, roadNumbers) + speedElements.clear() + speedElements.addAll(elements) + } } } 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 267b395..f7373ea 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 @@ -1,10 +1,12 @@ package com.kouros.navigation.model import android.location.Location +import android.util.Log import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION import androidx.car.app.navigation.model.Maneuver import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD +import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.NavigationState import com.kouros.navigation.data.Route import com.kouros.navigation.data.StepData 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 0178af6..79bc021 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 @@ -1,6 +1,8 @@ package com.kouros.navigation.utils import android.location.Location +import android.util.Log +import com.kouros.navigation.data.Constants.TAG import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.maplibre.geojson.FeatureCollection 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 f1a7130..f37480e 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 @@ -108,13 +108,12 @@ fun calculateTilt(viewStyle: ViewStyle, newZoom: Double, tilt: Double): Double = return 0.0 } -fun bearing(fromLocation: Location, toLocation: Location, oldBearing: Double): Double { +fun bearingPositive(fromLocation: Location, toLocation: Location, oldBearing: Double): Double { val distance = fromLocation.distanceTo(toLocation) if (distance < 1.0) { return oldBearing } - val bearing = fromLocation.bearingTo(toLocation).toInt().toDouble() - return bearing + return fromLocation.bearingPositive(toLocation).toInt().toDouble() } fun location(longitude: Double, latitude: Double): Location { @@ -124,6 +123,10 @@ fun location(longitude: Double, latitude: Double): Location { return location } +fun Location.bearingPositive(locationTo: Location): Float { + return (this.bearingTo (locationTo) + 360) % 360 +} + fun formatDateTime(time: Long): String { val dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) val dateTime = LocalDateTime.ofEpochSecond(time / 1000, 0, ZoneOffset.UTC) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8cfcc4e..8bdd2d8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,13 @@ [versions] -agp = "9.1.1" +agp = "9.2.0" androidGpxParser = "2.3.1" androidSdkTurf = "6.0.1" datastore = "1.2.1" -gradle = "9.1.1" -koinAndroid = "4.2.0" -koinAndroidxCompose = "4.2.0" -koinComposeViewmodel = "4.2.0" -koinCore = "4.2.0" +gradle = "9.2.0" +koinAndroid = "4.2.1" +koinAndroidxCompose = "4.2.1" +koinComposeViewmodel = "4.2.1" +koinCore = "4.2.1" kotlin = "2.3.20" coreKtx = "1.18.0" junit = "4.13.2" @@ -15,7 +15,7 @@ junitVersion = "1.3.0" espressoCore = "3.7.0" kotlinxSerializationJson = "1.10.0" lifecycleRuntimeKtx = "2.10.0" -composeBom = "2026.03.01" +composeBom = "2026.04.01" appcompat = "1.7.1" material = "1.13.0" carApp = "1.7.0" @@ -26,21 +26,21 @@ mockitoKotlin = "6.3.0" rules = "1.7.0" runner = "1.7.0" material3 = "1.4.0" -runtimeLivedata = "1.10.6" -foundation = "1.10.6" +runtimeLivedata = "1.11.0" +foundation = "1.11.0" maplibre-compose = "0.12.1" playServicesLocation = "21.3.0" -runtime = "1.10.6" +runtime = "1.11.0" accompanist = "0.37.3" -uiVersion = "1.10.6" -uiText = "1.10.6" -navigationCompose = "2.9.7" -uiToolingPreview = "1.10.6" -uiTooling = "1.10.6" +uiVersion = "1.11.0" +uiText = "1.11.0" +navigationCompose = "2.9.8" +uiToolingPreview = "1.11.0" +uiTooling = "1.11.0" material3WindowSizeClass = "1.4.0" -uiGraphics = "1.10.6" +uiGraphics = "1.11.0" window = "1.5.1" -foundationLayout = "1.10.6" +foundationLayout = "1.11.0" datastorePreferences = "1.2.1" datastoreCore = "1.2.1" monitor = "1.8.0" @@ -48,7 +48,7 @@ robolectric = "4.16.1" truth = "1.4.5" testCore = "1.7.0" archCoreTesting = "2.2.0" -kotlinxCoroutinesTest = "1.10.1" +kotlinxCoroutinesTest = "1.10.2" [libraries] android-gpx-parser = { module = "com.github.ticofab:android-gpx-parser", version.ref = "androidGpxParser" }