Speed limit

This commit is contained in:
Dimitris
2025-12-22 11:08:53 +01:00
parent 9e453dc955
commit e5c74f6fa2
36 changed files with 1421 additions and 167 deletions

View File

@@ -8,4 +8,6 @@ val RouteColor = Color(0xFF5582D0)
val SpeedColor = Color(0xFF262525)
val MaxSpeedColor = Color(0xFF262525)
val PlaceColor = Color(0xFF868005)

View File

@@ -27,34 +27,14 @@ import java.net.URL
import kotlinx.serialization.json.Json
class NavigationRepository {
abstract class NavigationRepository {
private val placesUrl = "https://kouros-online.de/maps/placespwd";
private val routeUrl = "https://kouros-online.de/valhalla/route?json="
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
// Road classes from highest to lowest are:
// motorway, trunk, primary, secondary, tertiary, unclassified, residential, service_other.
// exclude_toll
fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String {
SearchFilter
val vLocation = listOf(
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude, search_filter = searchFilter),
Locations(lat = location.latitude, lon = location.longitude, search_filter = searchFilter)
)
val valhallaLocation = ValhallaLocation(
locations = vLocation,
costing = "auto",
units = "km",
id = "my_work_route",
language = "de-DE"
)
val routeLocation = Json.encodeToString(valhallaLocation)
return fetchUrl(routeUrl + routeLocation, true)
}
abstract fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String
fun getRouteDistance(currentLocation: Location, location: Location, searchFilter: SearchFilter): Double {
val route = getRoute(currentLocation, location, searchFilter)
@@ -98,7 +78,7 @@ class NavigationRepository {
return places
}
private fun fetchUrl(url: String, authenticator : Boolean): String {
fun fetchUrl(url: String, authenticator : Boolean): String {
try {
if (authenticator) {
Authenticator.setDefault(object : Authenticator() {

View File

@@ -75,8 +75,8 @@ data class Route(
fun route(route: String) = apply {
if (route.isNotEmpty() && route != "[]") {
val gson = GsonBuilder().serializeNulls().create()
val valhalla = gson.fromJson(route, ValhallaJson::class.java)
trip = valhalla.trip
val routeJson = gson.fromJson(route, ValhallaJson::class.java)
trip = routeJson.trip
}
}

View File

@@ -0,0 +1,13 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Intersections(
@SerializedName("out") var out: Int? = null,
@SerializedName("entry") var entry: ArrayList<Boolean> = arrayListOf(),
@SerializedName("bearings") var bearings: ArrayList<Int> = arrayListOf(),
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(),
@SerializedName("lanes") var lanes: ArrayList<Lane> = arrayListOf(),
)

View File

@@ -0,0 +1,8 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Lane(
@SerializedName("valid" ) var valid: Boolean,
@SerializedName("indications" ) var indications: List<String>,
)

View File

@@ -0,0 +1,14 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Legs (
@SerializedName("steps" ) var steps : ArrayList<Steps> = arrayListOf(),
@SerializedName("weight" ) var weight : Double? = null,
@SerializedName("summary" ) var summary : String? = null,
@SerializedName("duration" ) var duration : Double? = null,
@SerializedName("distance" ) var distance : Double? = null
)

View File

@@ -0,0 +1,14 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Maneuver (
@SerializedName("bearing_after" ) var bearingAfter : Int? = null,
@SerializedName("bearing_before" ) var bearingBefore : Int? = null,
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(),
@SerializedName("modifier" ) var modifier : String? = null,
@SerializedName("type" ) var type : String? = null
)

View File

@@ -0,0 +1,12 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class OsrmJson (
@SerializedName("code" ) var code : String? = null,
@SerializedName("routes" ) var routes : ArrayList<Routes> = arrayListOf(),
@SerializedName("waypoints" ) var waypoints : ArrayList<Waypoints> = arrayListOf()
)

View File

@@ -0,0 +1,18 @@
package com.kouros.navigation.data.osrm
import android.location.Location
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
private const val routeUrl = "https://router.project-osrm.org/route/v1/driving/"
class OsrmRepository : NavigationRepository() {
override fun getRoute(
currentLocation: Location,
location: Location,
searchFilter: SearchFilter
): String {
val routeLocation = "${currentLocation.latitude},${currentLocation.longitude};${location.latitude},${location.longitude}?steps=true"
return fetchUrl(routeUrl + routeLocation, true)
}
}

View File

@@ -0,0 +1,15 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Routes (
@SerializedName("legs" ) var legs : ArrayList<Legs> = arrayListOf(),
@SerializedName("weight_name" ) var weightName : String? = null,
@SerializedName("geometry" ) var geometry : String? = null,
@SerializedName("weight" ) var weight : Double? = null,
@SerializedName("duration" ) var duration : Double? = null,
@SerializedName("distance" ) var distance : Double? = null
)

View File

@@ -0,0 +1,18 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Steps (
@SerializedName("intersections" ) var intersections : ArrayList<Intersections> = arrayListOf(),
@SerializedName("driving_side" ) var drivingSide : String? = null,
@SerializedName("geometry" ) var geometry : String? = null,
@SerializedName("maneuver" ) var maneuver : Maneuver? = Maneuver(),
@SerializedName("name" ) var name : String? = null,
@SerializedName("mode" ) var mode : String? = null,
@SerializedName("weight" ) var weight : Double? = null,
@SerializedName("duration" ) var duration : Double? = null,
@SerializedName("distance" ) var distance : Double? = null
)

View File

@@ -0,0 +1,13 @@
package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Waypoints (
@SerializedName("hint" ) var hint : String? = null,
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(),
@SerializedName("name" ) var name : String? = null,
@SerializedName("distance" ) var distance : Double? = null
)

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,8 @@ package com.kouros.navigation.data.overpass
import android.location.Location
import com.google.gson.GsonBuilder
import com.kouros.navigation.utils.GeoUtils.getBoundingBox2
import com.kouros.navigation.utils.GeoUtils.getOverpassBbox
import com.kouros.navigation.utils.NavigationUtils
import kotlinx.serialization.json.Json
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
@@ -12,8 +11,33 @@ import java.net.URL
class Overpass {
val overpassUrl = "https://overpass.kumi.systems/api/interpreter"
fun getAmenities(type: String, category: String, location: Location) : List<Elements> {
val boundingBox = getOverpassBbox(location, 5.0)
fun getAround(radius: Int, linestring: String) : List<Elements> {
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST"
httpURLConnection.setRequestProperty(
"Accept",
"application/json"
)
httpURLConnection.setDoOutput(true);
// define search query
val searchQuery = """
|[out:json];
|(
| way[highway](around:$radius,$linestring)
| ;
|);
|out body;
""".trimMargin()
return overpassApi(httpURLConnection, searchQuery)
}
fun getAmenities(
type: String,
category: String,
location: Location,
radius: Double
): List<Elements> {
val boundingBox = getOverpassBbox(location, radius)
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST"
httpURLConnection.setRequestProperty(
@@ -32,8 +56,11 @@ class Overpass {
|);
|out body;
""".trimMargin()
return overpassApi(httpURLConnection, searchQuery)
}
// Send the JSON we created
fun overpassApi(httpURLConnection: HttpURLConnection, searchQuery: String) : List<Elements> {
// Send the JSON we created
val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream)
outputStreamWriter.write(searchQuery)
outputStreamWriter.flush()
@@ -44,9 +71,9 @@ class Overpass {
.use { it.readText() } // defaults to UTF-8
val gson = GsonBuilder().serializeNulls().create()
val overpass = gson.fromJson(response, Amenity::class.java)
println("Overpass: $type $response")
//println("Overpass: $response")
return overpass.elements
}
return emptyList()
return emptyList()
}
}

View File

@@ -0,0 +1,29 @@
package com.kouros.navigation.data.valhalla
import android.location.Location
import com.kouros.navigation.data.Locations
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.ValhallaLocation
import kotlinx.serialization.json.Json
private const val routeUrl = "https://kouros-online.de/valhalla/route?json="
class ValhallaRepository : NavigationRepository() {
override fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String {
SearchFilter
val vLocation = listOf(
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude, search_filter = searchFilter),
Locations(lat = location.latitude, lon = location.longitude, search_filter = searchFilter)
)
val valhallaLocation = ValhallaLocation(
locations = vLocation,
costing = "auto",
units = "km",
id = "my_work_route",
language = "de-DE"
)
val routeLocation = Json.encodeToString(valhallaLocation)
return fetchUrl(routeUrl + routeLocation, true)
}
}

View File

@@ -4,16 +4,16 @@ import android.location.Location
import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Step
import com.kouros.data.R
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.ManeuverType
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.location
import org.maplibre.turf.TurfMeasurement
import org.maplibre.turf.TurfMisc
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
@@ -21,14 +21,19 @@ open class RouteModel() {
data class RouteState(
val route: Route? = null,
val isNavigating: Boolean = false,
var destination: Place = Place(),
val destination: Place = Place(),
val arrived: Boolean = false,
var maneuverType: Int = 0,
val maneuverType: Int = 0,
var currentShapeIndex: Int = 0,
var distanceToStepEnd: Float = 0F,
var beginIndex: Int = 0,
var endIndex: Int = 0
)
var endIndex: Int = 0,
val travelMessage: String = "",
// max speed for street (maneuver)
val lastSpeedIndex: Int = 0,
val maxSpeed: Int = 0,
)
var routeState = RouteState()
@@ -61,7 +66,8 @@ open class RouteModel() {
)
}
fun updateLocation(location: Location) {
@OptIn(DelicateCoroutinesApi::class)
fun updateLocation(location: Location, viewModel: ViewModel) {
var nearestDistance = 100000.0f
var newShapeIndex = -1
// find nearest waypoint and current shape index
@@ -77,6 +83,24 @@ open class RouteModel() {
// find maneuver
// calculate distance to step end
findManeuver(newShapeIndex)
GlobalScope.launch(Dispatchers.IO) {
updateSpeedLimit(location, viewModel)
}
}
fun updateSpeedLimit(location: Location, viewModel: ViewModel) {
// speed limit for each maneuver index
if (routeState.lastSpeedIndex < route.currentManeuverIndex) {
routeState = routeState.copy(lastSpeedIndex = route.currentManeuverIndex)
val elements = viewModel.getMaxSpeed(location)
elements.forEach {
if (it.tags.name != null && it.tags.maxspeed != null) {
val speed = it.tags.maxspeed!!.toInt()
routeState = routeState.copy(maxSpeed = speed)
}
}
}
}
private fun findManeuver(newShapeIndex: Int) {
@@ -127,7 +151,7 @@ open class RouteModel() {
maneuverType = relevantManeuver.type
}
val maneuverIconPair = maneuverIcon(maneuverType)
routeState.maneuverType = maneuverIconPair.first
routeState = routeState.copy(maneuverType = maneuverIconPair.first)
// Construct and return the final StepData object
return StepData(
streetName,
@@ -149,6 +173,7 @@ open class RouteModel() {
when (distanceLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> {
}
else -> {
if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) {
text = maneuver.streetNames[0]
@@ -296,8 +321,8 @@ open class RouteModel() {
fun hasArrived(type: Int): Boolean {
// return leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE && type == ManeuverType.DestinationRight.value
return type == ManeuverType.DestinationRight.value
// return leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE && type == ManeuverType.DestinationRight.value
return type == ManeuverType.DestinationRight.value
|| type == ManeuverType.Destination.value
|| type == ManeuverType.DestinationLeft.value
}

View File

@@ -251,7 +251,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
fun getAmenities(category: String, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("amenity", category, location)
val amenities = Overpass().getAmenities("amenity", category, location, 5.0)
val distAmenities = mutableListOf<Elements>()
amenities.forEach {
val plLocation =
@@ -267,7 +267,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
fun getSpeedCameras(location: Location) {
viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("highway", "speed_camera", location)
val amenities = Overpass().getAmenities("highway", "speed_camera", location, 5.0)
val distAmenities = mutableListOf<Elements>()
amenities.forEach {
val plLocation =
@@ -281,6 +281,12 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
fun getMaxSpeed(location: Location) : List<Elements> {
val lineString = "${location.latitude},${location.longitude}"
val amenities = Overpass().getAround(10, lineString)
return amenities
}
fun saveFavorite(place: Place) {
place.category = Constants.FAVORITES
savePlace(place)

View File

@@ -74,10 +74,10 @@ fun calculateZoom(speed: Double?): Double {
val speedKmh = (speed * 3.6).toInt()
val zoom = when (speedKmh) {
in 0..10 -> 18.0
in 11..30 -> 17.0
in 31..50 -> 16.0
in 61..70 -> 15.0
else -> 14
in 11..30 -> 17.5
in 31..50 -> 17.0
in 61..70 -> 16.5
else -> 16.0
}
return zoom.toDouble()
}

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500Z"/>
</vector>