Pan Mode
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<String>): List<Elements> {
|
||||
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<String>()
|
||||
|
||||
// 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<Elements> {
|
||||
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<Elements> {
|
||||
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<Elements> {
|
||||
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("<?xml")) {
|
||||
Log.w("OverpassApi", "Received XML instead of JSON")
|
||||
return emptyList()
|
||||
}
|
||||
val gson = GsonBuilder().serializeNulls().create()
|
||||
val overpass = gson.fromJson(response, Amenity::class.java)
|
||||
return overpass.elements
|
||||
gson.fromJson(response, Amenity::class.java).elements
|
||||
} else {
|
||||
Log.e("OverpassApi", responseCode.toString())
|
||||
Log.e("OverpassApi", "Error code: $responseCode")
|
||||
emptyList()
|
||||
}
|
||||
} 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 pointIndex: Int,
|
||||
val possibleCombineWithNext: Boolean,
|
||||
val roadNumbers: List<String>,
|
||||
val roadNumbers: List<String> = emptyList(),
|
||||
val routeOffsetInMeters: Int,
|
||||
val signpostText: String,
|
||||
val street: String? = "",
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<Elements>()
|
||||
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<Elements>()
|
||||
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<ElementSearch>()
|
||||
|
||||
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<ElementSearch> { it.distance }.thenByDescending { it.bearing })
|
||||
if (result.isNotEmpty()) {
|
||||
element = result.first().element
|
||||
//Log.d("NavigationViewModel", "Distance: ${result.first().distance} Bearing: ${result.first().bearing} RouteBearing $routeBearing")
|
||||
speed = if (element.tags.maxspeed == "none" && element.tags.highway == "motorway") {
|
||||
countryCodeSpeedLimit(countryCode)
|
||||
} else {
|
||||
@@ -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<String>
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val lineString = "${location.latitude},${location.longitude}"
|
||||
val elements = Overpass().getSpeedLimit(SPEED_UPDATE_DISTANCE, lineString, street, roadNumbers)
|
||||
speedElements.clear()
|
||||
speedElements.addAll(elements)
|
||||
synchronized(this) {
|
||||
val lineString = "${location.latitude},${location.longitude}"
|
||||
val elements =
|
||||
Overpass().getSpeedLimit(SPEED_UPDATE_DISTANCE, lineString, street, roadNumbers)
|
||||
speedElements.clear()
|
||||
speedElements.addAll(elements)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user