This commit is contained in:
Dimitris
2026-04-24 12:24:53 +02:00
parent 0fa625d785
commit 17e68de017
18 changed files with 242 additions and 205 deletions
+2 -2
View File
@@ -17,8 +17,8 @@ android {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 37 targetSdk = 37
versionCode = 97 versionCode = 98
versionName = "0.2.3.97" versionName = "0.2.3.98"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -1,13 +1,8 @@
package com.kouros.navigation.ui package com.kouros.navigation.ui
import android.Manifest import android.Manifest
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.location.LocationManager import android.location.LocationManager
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge 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.car.TextToSpeechManager
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.INSTRUCTION_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.Constants.TILT
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.BaseStyleModel 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.search.SearchSheet
import com.kouros.navigation.ui.theme.NavigationTheme import com.kouros.navigation.ui.theme.NavigationTheme
import com.kouros.navigation.utils.GeoUtils.snapLocation 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.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
@@ -278,7 +272,7 @@ class MainActivity : ComponentActivity() {
val bearing = if (currentLocation.hasBearing()) { val bearing = if (currentLocation.hasBearing()) {
currentLocation.bearing.toDouble() currentLocation.bearing.toDouble()
} else { } else {
bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing) bearingPositive(lastLocation, currentLocation, cameraPosition.value!!.bearing)
} }
with(routeModel) { with(routeModel) {
@@ -14,8 +14,6 @@ import org.junit.Test
class OverpassTest { class OverpassTest {
val location = Location(LocationManager.GPS_PROVIDER)
@Test @Test
fun `maxSpeed Schmalkaldener 30 `() { fun `maxSpeed Schmalkaldener 30 `() {
val curLocation = location(11.582495, 48.186863) val curLocation = location(11.582495, 48.186863)
@@ -31,7 +29,7 @@ class OverpassTest {
@Test @Test
fun `maxSpeed Isarring 50 `() { fun `maxSpeed Isarring 50 `() {
val curLocation = location(11.5989114, 48.1694783) val curLocation = location(11.5989114, 48.1694783)
executeSpeedTest( curLocation, "Isarring", listOf("B2R"), 190, 50) executeSpeedTest( curLocation, "Isarring", listOf("B2R"), 190, 60)
} }
@Test @Test
@@ -12,10 +12,13 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.kouros.data.BuildConfig import com.kouros.data.BuildConfig
import com.kouros.navigation.data.Constants.a22
import com.kouros.navigation.data.Constants.a9 import com.kouros.navigation.data.Constants.a9
import com.kouros.navigation.data.Constants.a94 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.homeVogelhart
import com.kouros.navigation.data.Constants.ioannina import com.kouros.navigation.data.Constants.ioannina
import com.kouros.navigation.data.Constants.isarring
import com.kouros.navigation.data.Constants.subislawa import com.kouros.navigation.data.Constants.subislawa
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -104,8 +107,8 @@ class DeviceLocationManager(
val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (lastLocation != null) { if (lastLocation != null) {
if (setIndividualLocation) { if (setIndividualLocation) {
onInitialLocation(homeVogelhart) onInitialLocation(a22)
onLocationUpdate(homeVogelhart) onLocationUpdate(a22)
} else { } else {
onInitialLocation(lastLocation) onInitialLocation(lastLocation)
onLocationUpdate(lastLocation) onLocationUpdate(lastLocation)
@@ -54,6 +54,7 @@ import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.GeoUtils.snapLocation import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.bearingPositive
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
@@ -464,23 +465,25 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
*/ */
private fun handleNavigationLocation(location: Location) { private fun handleNavigationLocation(location: Location) {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
routeModel.updateLocation(location, navigationViewModel) routeModel.updateLocation(location, navigationViewModel) // 30 ms
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations()) val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations()) // 160 ms
val streetName = routeModel.currentStep().street val streetName = routeModel.currentStep().street
if (checkLocationDeviation(location, snappedLocation, streetName)) { if (checkLocationDeviation(location, snappedLocation, streetName)) { // 1 ms
if (routeModel.navState.arrived) return if (routeModel.navState.arrived) return
if (guidanceAudio == 1) { if (guidanceAudio == 1) {
handleGuidanceAudio() handleGuidanceAudio()
} }
val currentDate = LocalDateTime.now(ZoneOffset.UTC) val currentDate = LocalDateTime.now(ZoneOffset.UTC)
checkTraffic(currentDate, snappedLocation) checkTraffic(currentDate, snappedLocation) // 0 ms
updateSpeedCamera(snappedLocation) updateSpeedCamera(snappedLocation) // 0 ms
checkRoute(currentDate, snappedLocation) checkRoute(currentDate, snappedLocation) // 0 ms
updateNavigationScreen() updateNavigationScreen() // 660 ms
val endTime = System.currentTimeMillis() - startTime
// Log.d(TAG, "updateNavigationScreen: $endTime")
checkArrival() checkArrival()
} }
val endTime = System.currentTimeMillis() - startTime
Log.d(TAG, "handleNavigationLocation: $endTime")
} }
/** /**
@@ -498,9 +501,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
distance < MAXIMAL_SNAP_CORRECTION -> { distance < MAXIMAL_SNAP_CORRECTION -> {
surfaceRenderer.updateLocation(snappedLocation, streetName) surfaceRenderer.updateLocation(snappedLocation, streetName)
} }
else -> { else -> {
surfaceRenderer.updateLocation(location, streetName) surfaceRenderer.updateLocation(location, streetName)
} }
} }
return true return true
@@ -516,15 +519,14 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
return return
} }
val currentStep = routeModel.route.currentStep() val stepData = routeModel.currentStep() // 121 ms
val stepData = routeModel.currentStep()
navigationScreen.updateTrip( navigationScreen.updateTrip( // 277 ms
isNavigating = routeModel.isNavigating(), isNavigating = routeModel.isNavigating(),
isRerouting = false, isRerouting = false,
hasArrived = routeModel.isArrival(), hasArrived = routeModel.isArrival(),
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext), destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext), // 60 ms
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext), stepTravelEstimate = routeModel.getTravelEstimateStep(carContext), // 60 ms
destinations = mutableListOf(routeModel.getDestination()), destinations = mutableListOf(routeModel.getDestination()),
steps = routeModel.getSteps(carContext), steps = routeModel.getSteps(carContext),
stepRemainingDistance = routeModel.getDistance(), 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. * Updates the trip information and notifies the listener with a new Trip object.
* This includes destination name, address, travel estimate, and loading status. * 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 lastCameraSearch = 0
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW navigationScreen.navigationType = NavigationType.VIEW
navigationScreen.invalidate()
} }
override fun updateTrip(trip: Trip) { override fun updateTrip(trip: Trip) {
@@ -781,26 +784,28 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
private fun updateDistance( private fun updateDistance(
location: Location, location: Location,
) { ) {
val updatedCameras = mutableListOf<Elements>() synchronized(this) {
speedCameras.forEach { val updatedCameras = mutableListOf<Elements>()
val plLocation = speedCameras.forEach {
location(longitude = it.lon, latitude = it.lat) val plLocation =
val distance = plLocation.distanceTo(location) location(longitude = it.lon, latitude = it.lat)
it.distance = distance.toDouble() val distance = plLocation.distanceTo(location)
updatedCameras.add(it) it.distance = distance.toDouble()
} updatedCameras.add(it)
val sortedList = updatedCameras.sortedWith(compareBy { it.distance }) }
val camera = sortedList.firstOrNull() ?: return val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location) val camera = sortedList.firstOrNull() ?: return
val bearingSpeedCamera = try { val bearingRoute = surfaceRenderer.lastLocation.bearingPositive(location)
camera.tags.direction.toFloat() val bearingSpeedCamera = try {
} catch (e: Exception) { camera.tags.direction.toFloat()
0F } catch (e: Exception) {
} 0F
if (camera.distance < 80 && (System.currentTimeMillis() - lastAlert > 5000)) { }
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) { if (camera.distance < 80 && (System.currentTimeMillis() - lastAlert > 10000)) {
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed) if ((bearingSpeedCamera - bearingRoute).absoluteValue < 15.0) {
lastAlert = System.currentTimeMillis() routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
lastAlert = System.currentTimeMillis()
}
} }
} }
} }
@@ -34,7 +34,7 @@ import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.DarkMode import com.kouros.navigation.data.DarkMode
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.BaseStyleModel 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.calculateTilt
import com.kouros.navigation.utils.calculateZoom import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.duration 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.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.math.*
/** /**
@@ -225,15 +226,40 @@ class SurfaceRenderer(
*/ */
override fun onScroll(distanceX: Float, distanceY: Float) { override fun onScroll(distanceX: Float, distanceY: Float) {
synchronized(this@SurfaceRenderer) { 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 viewStyle = ViewStyle.PAN_VIEW
if (distanceX != 0.0F) {
lastLocation.longitude += (distanceX / 1000) / cameraPosition.value!!.zoom // Update location based on rotated scroll distances and calculated factors
} lastLocation.longitude += (rotatedDx / pixelsPerDegreeLon)
if (distanceY != 0.0F) { lastLocation.latitude -= (rotatedDy / pixelsPerDegreeLat)
lastLocation.latitude += (distanceY / 1000) / cameraPosition.value!!.zoom
}
val pos = Position(lastLocation.longitude, lastLocation.latitude) val pos = Position(lastLocation.longitude, lastLocation.latitude)
updateCameraPosition( target = pos) updateCameraPosition(
bearing = bearing,
zoom = zoom,
target = pos,
tilt = tilt
)
navigationSession.invalidateNavigationScreen() navigationSession.invalidateNavigationScreen()
} }
} }
@@ -374,7 +400,7 @@ class SurfaceRenderer(
if (location.hasBearing()) { if (location.hasBearing()) {
location.bearing.toDouble() location.bearing.toDouble()
} else { } else {
bearing( bearingPositive(
lastLocation, lastLocation,
location, location,
cameraPosition.value!!.bearing cameraPosition.value!!.bearing
@@ -330,7 +330,7 @@ fun DrawNavigationImages(
if (speed != null) { if (speed != null) {
CurrentSpeed(width, height, speed, maxSpeed) CurrentSpeed(width, height, speed, maxSpeed)
} }
if (speed != null && maxSpeed > 0) { // && (speed * 3.6) > maxSpeed) { if (speed != null && maxSpeed > 0) {
MaxSpeed(width, height, maxSpeed, speed) MaxSpeed(width, height, maxSpeed, speed)
} }
//DebugInfo(width, height, lat!!) //DebugInfo(width, height, lat!!)
@@ -507,7 +507,7 @@ private fun MaxSpeed(
val textLayoutSpeed = remember(speed) { val textLayoutSpeed = remember(speed) {
textMeasurerSpeed.measure(speed, styleSpeed) textMeasurerSpeed.measure(speed, styleSpeed)
} }
val signColor = if (curSpeed * 3.6 > maxSpeed) { val signColor = if (curSpeed * 3.6 > (maxSpeed + 3)) {
Color.Red Color.Red
} else { } else {
Color.Green Color.Green
@@ -56,14 +56,14 @@ class Simulation {
longitude = point[0] longitude = point[0]
bearing = curBearing bearing = curBearing
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = 10.0f speed = 9.0f
time = System.currentTimeMillis() time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
} }
// Update your app's state as if a real GPS update occurred // Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation) updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second) // Wait before moving to the next point (e.g., every 1 second)
delay(1000) delay(500)
lastLocation = fakeLocation lastLocation = fakeLocation
} }
} }
@@ -100,7 +100,6 @@ open class NavigationScreen(
repository.tripSuggestionFlow.asLiveData().observe(this, Observer { repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces) navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
//navigationViewModel.recentPlacesFlow(carContext, surfaceRenderer.lastLocation ).asLiveData().observe(this, ::test)
tripSuggestion = it tripSuggestion = it
}) })
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer { repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
@@ -142,10 +141,10 @@ open class NavigationScreen(
createAction( createAction(
carContext, carContext,
R.drawable.ic_close_white_24dp, R.drawable.ic_close_white_24dp,
0, 0
{ stopNavigation() }) ) { stopNavigation() }
) )
return NavigationTemplate.Builder() val navigationTemplate = NavigationTemplate.Builder()
.setNavigationInfo( .setNavigationInfo(
getRoutingInfo() getRoutingInfo()
) )
@@ -167,6 +166,7 @@ open class NavigationScreen(
) )
.setBackgroundColor(backGroundColor) .setBackgroundColor(backGroundColor)
.build() .build()
return navigationTemplate
} }
/** /**
@@ -490,8 +490,6 @@ open class NavigationScreen(
} }
} }
/** /**
* Updates navigation state with the current location, checks for arrival, and traffic updates. * Updates navigation state with the current location, checks for arrival, and traffic updates.
*/ */
@@ -523,7 +521,6 @@ open class NavigationScreen(
this.junctionImage = junctionImage this.junctionImage = junctionImage
this.backGroundColor = backGroundColor this.backGroundColor = backGroundColor
this.message = message this.message = message
navigationType = NavigationType.NAVIGATION navigationType = NavigationType.NAVIGATION
invalidate() invalidate()
} }
@@ -128,8 +128,10 @@ object Constants {
val ioannina = location( 20.826237, 39.690174) val ioannina = location( 20.826237, 39.690174)
val subislawa = location(18.570808, 54.420647) val subislawa = location(18.570808, 54.420647)
val a94 = location(11.872097,48.163449) val a94 = location(11.872097,48.163449)
val a9 = location(11.621556, 48.204402,) 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 NEXT_STEP_THRESHOLD = 500.0
const val MAXIMAL_SNAP_CORRECTION = 50.0 const val MAXIMAL_SNAP_CORRECTION = 50.0
@@ -11,59 +11,59 @@ import java.net.URL
class Overpass { class Overpass {
private val gson = GsonBuilder().serializeNulls().create()
var overpassUrl = if (BuildConfig.DEBUG) var overpassUrl = if (BuildConfig.DEBUG)
"http://192.168.1.37/api/interpreter" "http://192.168.1.37/api/interpreter"
else else
"https://kouros-online.de/api/interpreter" "https://kouros-online.de/api/interpreter"
val destination = "[!destination][highway!=\"motorway_link\"]"
fun getSpeedLimit(radius: Float, linestring: String, street: String, roadNumbers: List<String>): List<Elements> { fun getSpeedLimit(radius: Float, linestring: String, street: String, roadNumbers: List<String>): List<Elements> {
val streetName = if (street.length > 10) {
street.substring(0, 10) val wayAround = "way[maxspeed](around:$radius,$linestring)"
} else { val searchClauses = mutableListOf<String>()
street
} // 1. Search by street name (fuzzy match with first 10 characters)
val name = if (streetName.isEmpty()) { val streetPrefix = street.take(10)
"" if (streetPrefix.isNotEmpty()) {
} else { searchClauses.add("$wayAround[name~\"^$streetPrefix\"]")
"[name~\"^${streetName}\"]"
} }
val regex = Regex("""\d+|\D+""") // 2. Search by road numbers (ref or int_ref)
val search = "way[maxspeed](around:$radius,$linestring)$name[!destination][highway!=\"motorway_link\"]" val partRegex = Regex("""\d+|\D+""")
var waySearch = search roadNumbers.forEach { number ->
for ((index, r) in roadNumbers.withIndex()) { val parts = partRegex.findAll(number).map { it.value.trim() }.filter { it.isNotEmpty() }.toList()
val result = regex.findAll(r).map { it.groupValues.first() }.toList() if (parts.isNotEmpty()) {
if (index > 0) { // Construct ref value: e.g., "A1" -> "A 1", "E30" -> "E 30"
waySearch = waySearch.plus(";").plus(search) 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"
var refValue = result.first().trim().plus(" ") searchClauses.add("$wayAround$destination[$tag~\"$refValue\"]")
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" // 3. Fallback to searching everything if no specific filters were added
httpURLConnection.setRequestProperty( if (searchClauses.isEmpty()) {
"Accept", searchClauses.add(wayAround)
"application/json" }
)
httpURLConnection.setDoOutput(true);
// define search query
val searchQuery = """ val searchQuery = """
|[out:json]; |[out:json];
|( |(
| $waySearch; | ${searchClauses.joinToString(";")};
|); |);
|out body geom; |out body geom;
""".trimMargin() """.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 radius: Double
): List<Elements> { ): List<Elements> {
val boundingBox = getBoundingBox(location.latitude, location.longitude, radius) 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 = """ val searchQuery = """
|[out:json]; |[out:json];
|( |(
| node[$type=$category] | node[$type=$category]
| ($boundingBox); | ($boundingBox);
|); |);
|(._;>;); |(._;>;);
|out body geom; |out body geom;
""".trimMargin() """.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<Elements> { private fun overpassApi(connection: HttpURLConnection, searchQuery: String): List<Elements> {
try { return try {
val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream) connection.outputStream.use { os ->
outputStreamWriter.write(searchQuery) OutputStreamWriter(os).use { writer ->
outputStreamWriter.flush() writer.write(searchQuery)
// Check if the connection is successful writer.flush()
httpURLConnection.requestMethod = "POST" }
val responseCode = httpURLConnection.responseCode }
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) { if (responseCode == HttpURLConnection.HTTP_OK) {
val response = httpURLConnection.inputStream.bufferedReader() val response = connection.inputStream.bufferedReader().use { it.readText() }
.use { it.readText() } // defaults to UTF-8
if (response.startsWith("<?xml")) { if (response.startsWith("<?xml")) {
Log.w("OverpassApi", "Received XML instead of JSON")
return emptyList() return emptyList()
} }
val gson = GsonBuilder().serializeNulls().create() gson.fromJson(response, Amenity::class.java).elements
val overpass = gson.fromJson(response, Amenity::class.java)
return overpass.elements
} else { } else {
Log.e("OverpassApi", responseCode.toString()) Log.e("OverpassApi", "Error code: $responseCode")
emptyList()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("OverpassApi", e.toString()) Log.e("OverpassApi", "Exception in Overpass API call", e)
emptyList()
} }
return emptyList()
} }
} }
@@ -11,7 +11,7 @@ data class Instruction(
val point: Point, val point: Point,
val pointIndex: Int, val pointIndex: Int,
val possibleCombineWithNext: Boolean, val possibleCombineWithNext: Boolean,
val roadNumbers: List<String>, val roadNumbers: List<String> = emptyList(),
val routeOffsetInMeters: Int, val routeOffsetInMeters: Int,
val signpostText: String, val signpostText: String,
val street: String? = "", val street: String? = "",
@@ -23,7 +23,7 @@ private const val tomtomFields =
val useLocal = BuildConfig.DEBUG val useLocal = BuildConfig.DEBUG
val useLocalTraffic = BuildConfig.DEBUG val useLocalTraffic = BuildConfig.DEBUG
class TomTomRepository : NavigationRepository() { class TomTomRepository : NavigationRepository() {
override fun getRoute( override fun getRoute(
@@ -40,7 +40,8 @@ class TomTomRepository : NavigationRepository() {
} }
if (useLocal) { if (useLocal) {
return fetchUrl( 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 false
) )
} }
@@ -2,7 +2,6 @@ package com.kouros.navigation.model
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.util.Log
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList 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.ElementSearch
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.overpass.Overpass import com.kouros.navigation.data.overpass.Overpass
import com.kouros.navigation.utils.bearingPositive
import com.kouros.navigation.utils.countryCodeSpeedLimit import com.kouros.navigation.utils.countryCodeSpeedLimit
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
@@ -390,18 +390,20 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
* Posts sorted results to speedCameras LiveData. * Posts sorted results to speedCameras LiveData.
*/ */
fun getSpeedCameras(location: Location, radius: Double) { fun getSpeedCameras(location: Location, radius: Double) {
viewModelScope.launch(Dispatchers.IO) { synchronized(this) {
val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius) viewModelScope.launch(Dispatchers.IO) {
val distAmenities = mutableListOf<Elements>() val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius)
amenities.forEach { val distAmenities = mutableListOf<Elements>()
val plLocation = amenities.forEach {
location(longitude = it.lon, latitude = it.lat) val plLocation =
val distance = plLocation.distanceTo(location) location(longitude = it.lon, latitude = it.lat)
it.distance = distance.toDouble() val distance = plLocation.distanceTo(location)
distAmenities.add(it) 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, countryCode: String,
) { ) {
viewModelScope.launch(Dispatchers.IO) { 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<ElementSearch>() val search = mutableListOf<ElementSearch>()
speedElements.filter { it.type == "way"}.forEach { speedElements.filter { it.type == "way"}.forEach {
var streetBearingSum = 0F
var streetBearingAvg = 0F
var distance = 0F var distance = 0F
var maxDistance = 1000F var maxDistance = 1000F
var geometryFirstLocation = location(0.0, 0.0)
var geometryLastLocation = location(0.0, 0.0)
for ((geoIndex, geo) in it.geometry.withIndex()) { 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) val geometryLocation = location(geo.lon, geo.lat)
distance = geometryLocation.distanceTo(location) distance = geometryLocation.distanceTo(location)
if (distance < maxDistance) { if (distance < maxDistance) {
maxDistance = distance 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) val streetBearing = geometryFirstLocation.bearingPositive(geometryLastLocation)
if (bearing < SPEED_BEARING_DEVIATION) { if (isBearingValid(it, streetBearing, routeBearing)) {
search.add( search.add(
ElementSearch( ElementSearch(
it, it,
maxDistance.toDouble(), maxDistance.toDouble(),
streetBearingAvg.absoluteValue streetBearing.absoluteValue
) )
) )
} }
@@ -460,7 +463,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
val result = search.sortedWith(compareBy<ElementSearch> { it.distance }.thenByDescending { it.bearing }) val result = search.sortedWith(compareBy<ElementSearch> { it.distance }.thenByDescending { it.bearing })
if (result.isNotEmpty()) { if (result.isNotEmpty()) {
element = result.first().element 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") { speed = if (element.tags.maxspeed == "none" && element.tags.highway == "motorway") {
countryCodeSpeedLimit(countryCode) countryCodeSpeedLimit(countryCode)
} else { } else {
@@ -470,11 +472,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return speed 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()) { return if (element.tags.oneway.isNotEmpty()) {
(streetBearingAvg - routeBearing.absoluteValue).absoluteValue (streetBearing - routeBearing.absoluteValue) < SPEED_BEARING_DEVIATION
} else { } else {
0F true
} }
} }
@@ -488,10 +490,13 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
roadNumbers: List<String> roadNumbers: List<String>
) { ) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val lineString = "${location.latitude},${location.longitude}" synchronized(this) {
val elements = Overpass().getSpeedLimit(SPEED_UPDATE_DISTANCE, lineString, street, roadNumbers) val lineString = "${location.latitude},${location.longitude}"
speedElements.clear() val elements =
speedElements.addAll(elements) Overpass().getSpeedLimit(SPEED_UPDATE_DISTANCE, lineString, street, roadNumbers)
speedElements.clear()
speedElements.addAll(elements)
}
} }
} }
@@ -1,10 +1,12 @@
package com.kouros.navigation.model package com.kouros.navigation.model
import android.location.Location import android.location.Location
import android.util.Log
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD 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.NavigationState
import com.kouros.navigation.data.Route import com.kouros.navigation.data.Route
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
@@ -1,6 +1,8 @@
package com.kouros.navigation.utils package com.kouros.navigation.utils
import android.location.Location import android.location.Location
import android.util.Log
import com.kouros.navigation.data.Constants.TAG
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.FeatureCollection
@@ -108,13 +108,12 @@ fun calculateTilt(viewStyle: ViewStyle, newZoom: Double, tilt: Double): Double =
return 0.0 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) val distance = fromLocation.distanceTo(toLocation)
if (distance < 1.0) { if (distance < 1.0) {
return oldBearing return oldBearing
} }
val bearing = fromLocation.bearingTo(toLocation).toInt().toDouble() return fromLocation.bearingPositive(toLocation).toInt().toDouble()
return bearing
} }
fun location(longitude: Double, latitude: Double): Location { fun location(longitude: Double, latitude: Double): Location {
@@ -124,6 +123,10 @@ fun location(longitude: Double, latitude: Double): Location {
return location return location
} }
fun Location.bearingPositive(locationTo: Location): Float {
return (this.bearingTo (locationTo) + 360) % 360
}
fun formatDateTime(time: Long): String { fun formatDateTime(time: Long): String {
val dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) val dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
val dateTime = LocalDateTime.ofEpochSecond(time / 1000, 0, ZoneOffset.UTC) val dateTime = LocalDateTime.ofEpochSecond(time / 1000, 0, ZoneOffset.UTC)
+18 -18
View File
@@ -1,13 +1,13 @@
[versions] [versions]
agp = "9.1.1" agp = "9.2.0"
androidGpxParser = "2.3.1" androidGpxParser = "2.3.1"
androidSdkTurf = "6.0.1" androidSdkTurf = "6.0.1"
datastore = "1.2.1" datastore = "1.2.1"
gradle = "9.1.1" gradle = "9.2.0"
koinAndroid = "4.2.0" koinAndroid = "4.2.1"
koinAndroidxCompose = "4.2.0" koinAndroidxCompose = "4.2.1"
koinComposeViewmodel = "4.2.0" koinComposeViewmodel = "4.2.1"
koinCore = "4.2.0" koinCore = "4.2.1"
kotlin = "2.3.20" kotlin = "2.3.20"
coreKtx = "1.18.0" coreKtx = "1.18.0"
junit = "4.13.2" junit = "4.13.2"
@@ -15,7 +15,7 @@ junitVersion = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
kotlinxSerializationJson = "1.10.0" kotlinxSerializationJson = "1.10.0"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.10.0"
composeBom = "2026.03.01" composeBom = "2026.04.01"
appcompat = "1.7.1" appcompat = "1.7.1"
material = "1.13.0" material = "1.13.0"
carApp = "1.7.0" carApp = "1.7.0"
@@ -26,21 +26,21 @@ mockitoKotlin = "6.3.0"
rules = "1.7.0" rules = "1.7.0"
runner = "1.7.0" runner = "1.7.0"
material3 = "1.4.0" material3 = "1.4.0"
runtimeLivedata = "1.10.6" runtimeLivedata = "1.11.0"
foundation = "1.10.6" foundation = "1.11.0"
maplibre-compose = "0.12.1" maplibre-compose = "0.12.1"
playServicesLocation = "21.3.0" playServicesLocation = "21.3.0"
runtime = "1.10.6" runtime = "1.11.0"
accompanist = "0.37.3" accompanist = "0.37.3"
uiVersion = "1.10.6" uiVersion = "1.11.0"
uiText = "1.10.6" uiText = "1.11.0"
navigationCompose = "2.9.7" navigationCompose = "2.9.8"
uiToolingPreview = "1.10.6" uiToolingPreview = "1.11.0"
uiTooling = "1.10.6" uiTooling = "1.11.0"
material3WindowSizeClass = "1.4.0" material3WindowSizeClass = "1.4.0"
uiGraphics = "1.10.6" uiGraphics = "1.11.0"
window = "1.5.1" window = "1.5.1"
foundationLayout = "1.10.6" foundationLayout = "1.11.0"
datastorePreferences = "1.2.1" datastorePreferences = "1.2.1"
datastoreCore = "1.2.1" datastoreCore = "1.2.1"
monitor = "1.8.0" monitor = "1.8.0"
@@ -48,7 +48,7 @@ robolectric = "4.16.1"
truth = "1.4.5" truth = "1.4.5"
testCore = "1.7.0" testCore = "1.7.0"
archCoreTesting = "2.2.0" archCoreTesting = "2.2.0"
kotlinxCoroutinesTest = "1.10.1" kotlinxCoroutinesTest = "1.10.2"
[libraries] [libraries]
android-gpx-parser = { module = "com.github.ticofab:android-gpx-parser", version.ref = "androidGpxParser" } android-gpx-parser = { module = "com.github.ticofab:android-gpx-parser", version.ref = "androidGpxParser" }