Before TomTom Routing

This commit is contained in:
Dimitris
2026-01-29 12:13:37 +01:00
parent 7db7cba4fb
commit eac5b56bcb
51 changed files with 5825 additions and 212 deletions

View File

@@ -68,6 +68,7 @@ data class StepData (
var leftDistance: Double,
var lane: List<Lane> = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())),
var exitNumber: Int = 0,
)
@@ -150,5 +151,5 @@ object Constants {
enum class RouteEngine {
VALHALLA, OSRM, GRAPHHOPPER
VALHALLA, OSRM, TOMTOM, GRAPHHOPPER
}

View File

@@ -18,16 +18,13 @@ package com.kouros.navigation.data
import android.content.Context
import android.location.Location
import com.kouros.navigation.data.overpass.Elements
import com.kouros.data.R
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
import com.kouros.navigation.utils.GeoUtils.getBoundingBox
import org.json.JSONArray
import java.net.Authenticator
import java.net.HttpURLConnection
import java.net.PasswordAuthentication
import java.net.URL
import kotlinx.serialization.json.Json
abstract class NavigationRepository {
@@ -36,27 +33,69 @@ abstract class NavigationRepository {
private val nominatimUrl = "https://kouros-online.de/nominatim/"
private val tomtomApiKey = "678k5v6940cSXXIS5oD92qIrDgW3RBZ3"
abstract fun getRoute(currentLocation: Location, location: Location, carOrientation: Float, searchFilter: SearchFilter): String
private val tomtomUrl = "https://api.tomtom.com/traffic/services/5/incidentDetails"
fun getRouteDistance(currentLocation: Location, location: Location, carOrientation : Float, searchFilter: SearchFilter, context: Context): Double {
val route = getRoute(currentLocation, location, carOrientation, searchFilter)
private val tomtomFields =
"{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}"
abstract fun getRoute(
context: Context,
currentLocation: Location,
location: Location,
carOrientation: Float,
searchFilter: SearchFilter
): String
fun getRouteDistance(
currentLocation: Location,
location: Location,
carOrientation: Float,
searchFilter: SearchFilter,
context: Context
): Double {
val route = getRoute(context, currentLocation, location, carOrientation, searchFilter)
val routeModel = RouteModel()
routeModel.startNavigation(route, context)
return routeModel.curRoute.summary.distance
}
fun searchPlaces(search: String, location: Location) : String {
val box = calculateSquareRadius(location.latitude, location.longitude, 20.0)
val viewbox = "&bounded=1&viewbox=${box[2]},${box[0]},${box[3]},${box[1]}"
return fetchUrl("${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox", false)
fun searchPlaces(search: String, location: Location): String {
val box = calculateSquareRadius(location.latitude, location.longitude, 20.0)
val viewbox = "&bounded=1&viewbox=${box}"
return fetchUrl(
"${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox",
false
)
}
fun reverseAddress(location: Location) : String {
return fetchUrl("${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true", false)
fun reverseAddress(location: Location): String {
return fetchUrl(
"${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true",
false
)
}
fun fetchUrl(url: String, authenticator : Boolean): String {
fun getTraffic(context: Context, location: Location, carOrientation: Float): String {
val useAsset = false
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
return if (useAsset) {
val trafficJson = context.resources.openRawResource(R.raw.tomtom_traffic)
trafficJson.bufferedReader().use { it.readText() }
} else {
val trafficResult = fetchUrl(
"$tomtomUrl?key=$tomtomApiKey&bbox=$bbox&fields=$tomtomFields&language=en-GB&timeValidityFilter=present",
false
)
trafficResult.replace(
"{\"incidents\":",
"{\"type\": \"FeatureCollection\", \"features\":"
)
}
}
fun fetchUrl(url: String, authenticator: Boolean): String {
try {
if (authenticator) {
Authenticator.setDefault(object : Authenticator() {
@@ -79,7 +118,7 @@ abstract class NavigationRepository {
val responseCode = httpURLConnection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val response = httpURLConnection.inputStream.bufferedReader()
.use { it.readText() } // defaults to UTF-8
.use { it.readText() }
return response
}
} catch (e: Exception) {

View File

@@ -1,6 +1,5 @@
package com.kouros.navigation.data
import android.location.Location
import com.google.gson.GsonBuilder
import com.kouros.navigation.data.osrm.OsrmResponse
import com.kouros.navigation.data.osrm.OsrmRoute
@@ -8,9 +7,10 @@ import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Maneuver
import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.data.tomtom.TomTomResponse
import com.kouros.navigation.data.tomtom.TomTomRoute
import com.kouros.navigation.data.valhalla.ValhallaResponse
import com.kouros.navigation.data.valhalla.ValhallaRoute
import com.kouros.navigation.utils.GeoUtils.createCenterLocation
import com.kouros.navigation.utils.location
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
@@ -38,6 +38,7 @@ data class Route(
}
fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine }
fun route(route: String) = apply {
if (route.isNotEmpty() && route != "[]") {
val gson = GsonBuilder().serializeNulls().create()
@@ -52,11 +53,14 @@ data class Route(
)
ValhallaRoute().mapJsonToValhalla(routeJson, this)
}
else -> {
RouteEngine.OSRM.ordinal -> {
val osrmJson = gson.fromJson(route, OsrmResponse::class.java)
OsrmRoute().mapToOsrm(osrmJson, this)
}
else -> {
val tomtomJson = gson.fromJson(route, TomTomResponse::class.java)
TomTomRoute().mapToOsrm(tomtomJson, this)
}
}
}
}
@@ -71,9 +75,7 @@ data class Route(
fun buildEmpty(): Route {
return Route(
routeEngine = 0,
//summary = Summary(0.0, 0.0),
routes = emptyList(),
// legs = emptyList(),
//waypoints = emptyList(),
//routeGeoJson = "",
)
@@ -81,14 +83,18 @@ data class Route(
}
val legs: List<Leg>
get() = routes.first().legs
fun legs(): List<Leg> {
return if (routes.isNotEmpty()) {
routes.first().legs
} else {
emptyList()
}
}
fun currentStep(): Step {
return if (legs.isNotEmpty()) {
legs.first().steps[currentStep]
return if (routes.isNotEmpty() && legs().isNotEmpty()) {
legs().first().steps[currentStep]
} else {
Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0)))
}
@@ -96,8 +102,8 @@ data class Route(
fun nextStep(): Step {
val nextIndex = currentStep + 1
return if (nextIndex < legs.first().steps.size) {
legs.first().steps[nextIndex]
return if (nextIndex < legs().first().steps.size) {
legs().first().steps[nextIndex]
} else {
throw IndexOutOfBoundsException("No next maneuver available.")
}

View File

@@ -5,8 +5,8 @@ import com.google.gson.annotations.SerializedName
data class Intersections(
@SerializedName("in") var inV: Int? = null,
@SerializedName("out") var out: Int? = null,
@SerializedName("in") var inV: Int = 0,
@SerializedName("out") var out: Int = 0,
@SerializedName("entry") var entry: ArrayList<Boolean> = arrayListOf(),
@SerializedName("bearings") var bearings: ArrayList<Int> = arrayListOf(),
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(),

View File

@@ -6,9 +6,9 @@ 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
@SerializedName("weight" ) var weight : Double = 0.0,
@SerializedName("summary" ) var summary : String = "",
@SerializedName("duration" ) var duration : Double = 0.0,
@SerializedName("distance" ) var distance : Double = 0.0
)

View File

@@ -3,12 +3,13 @@ package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Maneuver (
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
@SerializedName("bearing_after") var bearingAfter: Int = 0,
@SerializedName("bearing_before") var bearingBefore: Int = 0,
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(),
@SerializedName("modifier") var modifier: String = "",
@SerializedName("type") var type: String = "",
@SerializedName("exit") var exit: Int = 0,
)
)

View File

@@ -1,5 +1,6 @@
package com.kouros.navigation.data.osrm
import android.content.Context
import android.location.Location
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
@@ -8,6 +9,7 @@ private const val routeUrl = "https://kouros-online.de/osrm/route/v1/driving/"
class OsrmRepository : NavigationRepository() {
override fun getRoute(
context: Context,
currentLocation: Location,
location: Location,
carOrientation: Float,

View File

@@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName
data class OsrmResponse (
@SerializedName("code" ) var code : String? = null,
@SerializedName("code" ) var code : String = "",
@SerializedName("routes" ) var routes : ArrayList<Routes> = arrayListOf(),
@SerializedName("waypoints" ) var waypoints : ArrayList<Waypoints> = arrayListOf()

View File

@@ -16,57 +16,55 @@ class OsrmRoute {
fun mapToOsrm(routeJson: OsrmResponse, builder: Route.Builder) {
val routes = mutableListOf<com.kouros.navigation.data.route.Routes>()
var stepIndex = 0
routeJson.routes.forEach { route ->
val legs = mutableListOf<Leg>()
val waypoints = mutableListOf<List<Double>>()
val summary = Summary(route.duration!!, route.distance!! / 1000)
val summary = Summary(route.duration, route.distance / 1000)
route.legs.forEach { leg ->
val steps = mutableListOf<Step>()
leg.steps.forEach { step ->
val intersections = mutableListOf<Intersection>()
if (step.maneuver != null) {
val points = decodePolyline(step.geometry!!, 5)
waypoints.addAll(points)
val maneuver = RouteManeuver(
bearingBefore = step.maneuver.bearingBefore ?: 0,
bearingAfter = step.maneuver.bearingAfter ?: 0,
type = convertType(step.maneuver),
waypoints = points,
location = location(
step.maneuver.location[0],
step.maneuver.location[1]
)
val points = decodePolyline(step.geometry, 5)
waypoints.addAll(points)
val maneuver = RouteManeuver(
bearingBefore = step.maneuver.bearingBefore,
bearingAfter = step.maneuver.bearingAfter,
type = convertType(step.maneuver),
waypoints = points,
exit = step.maneuver.exit,
location = location(
step.maneuver.location[0],
step.maneuver.location[1]
)
step.intersections.forEach { it2 ->
if (it2.location[0] != 0.0) {
val lanes = mutableListOf<Lane>()
it2.lanes.forEach { it3 ->
if (it3.indications.isNotEmpty() && it3.indications.first() != "none") {
val lane = Lane(
location(it2.location[0], it2.location[1]),
it3.valid,
it3.indications
)
lanes.add(lane)
}
)
step.intersections.forEach { it2 ->
if (it2.location[0] != 0.0) {
val lanes = mutableListOf<Lane>()
it2.lanes.forEach { it3 ->
if (it3.indications.isNotEmpty() && it3.indications.first() != "none") {
val lane = Lane(
location(it2.location[0], it2.location[1]),
it3.valid,
it3.indications
)
lanes.add(lane)
}
intersections.add(Intersection(it2.location, lanes))
}
intersections.add(Intersection(it2.location, lanes))
}
val step = Step(
index = stepIndex,
name = step.name!!,
distance = step.distance!! / 1000,
duration = step.duration!!,
maneuver = maneuver,
intersection = intersections
)
steps.add(step)
stepIndex += 1
}
val step = Step(
index = stepIndex,
name = step.name,
distance = step.distance / 1000,
duration = step.duration,
maneuver = maneuver,
intersection = intersections
)
steps.add(step)
stepIndex += 1
}
legs.add(Leg(steps))
}

View File

@@ -8,8 +8,8 @@ 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
@SerializedName("weight" ) var weight : Double = 0.0,
@SerializedName("duration" ) var duration : Double = 0.0,
@SerializedName("distance" ) var distance : Double = 0.0
)

View File

@@ -6,13 +6,13 @@ 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" ) val 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
@SerializedName("driving_side" ) var drivingSide : String = "",
@SerializedName("geometry" ) var geometry : String = "",
@SerializedName("maneuver" ) val maneuver : Maneuver = Maneuver(),
@SerializedName("name" ) var name : String = "",
@SerializedName("mode" ) var mode : String = "",
@SerializedName("weight" ) var weight : Double = 0.0,
@SerializedName("duration" ) var duration : Double = 0.0,
@SerializedName("distance" ) var distance : Double = 0.0,
)

View File

@@ -5,9 +5,9 @@ import com.google.gson.annotations.SerializedName
data class Waypoints (
@SerializedName("hint" ) var hint : String? = null,
@SerializedName("hint" ) var hint : String = "",
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(),
@SerializedName("name" ) var name : String? = null,
@SerializedName("distance" ) var distance : Double? = null
@SerializedName("name" ) var name : String = "",
@SerializedName("distance" ) var distance : Double = 0.0,
)

View File

@@ -5,10 +5,10 @@ import com.google.gson.annotations.SerializedName
data class Elements (
@SerializedName("type" ) var type : String? = null,
@SerializedName("id" ) var id : Long? = null,
@SerializedName("lat" ) var lat : Double? = null,
@SerializedName("lon" ) var lon : Double? = null,
@SerializedName("type" ) var type : String = "",
@SerializedName("id" ) var id : Long = 0,
@SerializedName("lat" ) var lat : Double = 0.0,
@SerializedName("lon" ) var lon : Double = 0.0,
@SerializedName("tags" ) var tags : Tags = Tags(),
var distance : Double = 0.0

View File

@@ -2,8 +2,7 @@ package com.kouros.navigation.data.overpass
import android.location.Location
import com.google.gson.GsonBuilder
import com.kouros.navigation.utils.GeoUtils.getOverpassBbox
import kotlinx.serialization.json.Json
import com.kouros.navigation.utils.GeoUtils.getBoundingBox
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
@@ -41,7 +40,7 @@ class Overpass {
location: Location,
radius: Double
): List<Elements> {
val boundingBox = getOverpassBbox(location, radius)
val boundingBox = getBoundingBox(location.latitude, location.longitude, radius)
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST"
httpURLConnection.setRequestProperty(

View File

@@ -18,7 +18,7 @@ data class Tags(
@SerializedName("ref") var ref: String? = null,
@SerializedName("socket:type2") var socketType2: String? = null,
@SerializedName("socket:type2:output") var socketType2Output: String? = null,
@SerializedName("maxspeed") var maxspeed: String? = null,
@SerializedName("maxspeed") var maxspeed: String = "0",
@SerializedName("direction") var direction: String? = null,
)

View File

@@ -8,4 +8,5 @@ data class Maneuver(
val type: Int = 0,
val waypoints: List<List<Double>>,
val location: Location,
val exit: Int = 0,
)

View File

@@ -0,0 +1,10 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Events (
@SerializedName("description" ) var description : String? = null
)

View File

@@ -0,0 +1,12 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Features (
@SerializedName("type" ) var type : String? = null,
@SerializedName("properties" ) var properties : Properties? = Properties(),
@SerializedName("geometry" ) var geometry : Geometry? = Geometry()
)

View File

@@ -0,0 +1,11 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Geometry (
@SerializedName("type" ) var type : String? = null,
@SerializedName("coordinates" ) var coordinates : List<List<Double>> = arrayListOf()
)

View File

@@ -0,0 +1,12 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Incidents (
@SerializedName("type" ) var type : String? = null,
@SerializedName("properties" ) var properties : Properties? = Properties(),
@SerializedName("geometry" ) var geometry : Geometry? = Geometry()
)

View File

@@ -0,0 +1,11 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Properties (
@SerializedName("iconCategory" ) var iconCategory : Int? = null,
@SerializedName("events" ) var events : ArrayList<Events> = arrayListOf()
)

View File

@@ -0,0 +1,5 @@
package com.kouros.navigation.data.tomtom
data class Report(
val effectiveSettings: List<EffectiveSetting>
)

View File

@@ -0,0 +1,72 @@
package com.kouros.navigation.data.tomtom
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.osrm.OsrmResponse
import com.kouros.navigation.data.osrm.OsrmRoute.ManeuverType
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Maneuver
import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.utils.GeoUtils.decodePolyline
import com.kouros.navigation.utils.location
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
/**
curl -X GET "https://api.tomtom.com/routing/1/calculateRoute/\
48.1856548,11.57928:48.1183,11.59485/json?\
vehicleHeading=90&sectionType=traffic\
&report=effectiveSettings&routeType=eco\
&traffic=true&avoid=unpavedRoadimport com.kouros.navigation.data.route.Maneuver as RouteManeuvers&travelMode=car\
&vehicleMaxSpeed=120&vehicleCommercial=false\
&instructionsType=text&language=en-GB&sectionType=lanes\
&routeRepresentation=encodedPolyline\
&vehicleEngineType=combustion&key=678k5v6940cSXXIS5oD92qIrDgW3RBZ3"
*/
class TomTomRoute {
fun mapToOsrm(routeJson: TomTomResponse, builder: Route.Builder) {
routeJson.routes.forEach { route ->
val legs = mutableListOf<Leg>()
val waypoints = mutableListOf<List<Double>>()
var points = listOf<List<Double>>()
val summary = Summary(
route.summary.travelTimeInSeconds.toDouble(),
route.summary.lengthInMeters.toDouble() / 1000
)
route.legs.forEach { leg ->
points = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision)
waypoints.addAll(points)
}
route.guidance.instructions.forEach { instruction ->
instruction.exitNumber
// val maneuver = RouteManeuver(
// // bearingBefore = step.maneuver.bearingBefore,
// //bearingAfter = step.maneuver.bearingAfter,
// type = convertType(instruction.maneuver),
// waypoints = points.subList(section.startPointIndex, section.endPointIndex + 1),
// exit = instruction.exitNumber.toInt(),
// location = location(
// instruction.point.longitude, instruction.point.latitude
// )
// )
}
route.sections.forEach { section ->
}
}
println(routeJson)
}
fun convertType(type: String): Int {
var newType = 0
when (type) {
"DEPART" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART
}
}
return newType
}
}

View File

@@ -0,0 +1,12 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Traffic (
//@SerializedName("incidents" ) var incidents : ArrayList<Incidents> = arrayListOf()
@SerializedName("type" ) var type : String = "",
@SerializedName("features" ) var features : ArrayList<Features> = arrayListOf()
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class TrafficData (
var traffic : Traffic ,
var trafficData: String = ""
)

View File

@@ -1,5 +1,6 @@
package com.kouros.navigation.data.valhalla
import android.content.Context
import android.location.Location
import com.kouros.navigation.data.Locations
import com.kouros.navigation.data.NavigationRepository
@@ -13,6 +14,7 @@ private const val routeUrl = "https://kouros-online.de/valhalla/route?json="
class ValhallaRepository : NavigationRepository() {
override fun getRoute(
context: Context,
currentLocation: Location,
location: Location,
carOrientation: Float,

View File

@@ -21,6 +21,7 @@ import com.kouros.navigation.data.route.Lane
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.data.valhalla.ManeuverType
import com.kouros.navigation.utils.Levenshtein
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.location
import kotlinx.coroutines.CoroutineScope
@@ -128,16 +129,24 @@ open class RouteModel() {
fun updateSpeedLimit(location: Location, viewModel: ViewModel) = runBlocking {
CoroutineScope(Dispatchers.IO).launch {
val instruction = currentStep().instruction
val levenshtein = Levenshtein()
// speed limit
val distance = lastSpeedLocation.distanceTo(location)
if (distance > 500 || lastSpeedIndex < route.currentStep) {
lastSpeedIndex = route.currentStep
lastSpeedLocation = location
val elements = viewModel.getMaxSpeed(location)
elements.forEach {
if (it.tags.name != null && it.tags.maxspeed != null) {
val speed = it.tags.maxspeed!!.toInt()
maxSpeed = speed
if (it.tags.name != null) {
if (isNavigating()) {
val distance =
levenshtein.distance(it.tags.name!!, instruction)
if (distance < 5) {
val speed = it.tags.maxspeed.toInt()
maxSpeed = speed
lastSpeedLocation = location
}
}
}
}
}
@@ -150,7 +159,7 @@ open class RouteModel() {
val distanceToNextStep = leftStepDistance()
val isNearNextManeuver = distanceToNextStep in 0.0..NEXT_STEP_THRESHOLD
val shouldAdvance =
isNearNextManeuver && route.currentStep < (route.legs.first().steps.size)
isNearNextManeuver && route.currentStep < (route.legs().first().steps.size)
// Determine the maneuver type and corresponding icon
var curManeuverType = if (hasArrived(currentStep.maneuver.type)) {
@@ -166,17 +175,16 @@ open class RouteModel() {
}
// Safely get the street name from the maneuver
val streetName = relevantStep.name
var exitNumber = currentStep.maneuver.exit
if (shouldAdvance) {
curManeuverType = relevantStep.maneuver.type
exitNumber = relevantStep.maneuver.exit
}
val maneuverIcon = maneuverIcon(curManeuverType)
maneuverType = curManeuverType
val lanes = currentLanes(location)
if (lanes.isNotEmpty())
println("Street: $streetName Dist: $distanceToNextStep Lane: ${lanes.size}")
// Construct and return the final StepData object
return StepData(
streetName,
@@ -185,9 +193,9 @@ open class RouteModel() {
maneuverIcon,
arrivalTime(),
travelLeftDistance(),
lanes
lanes,
exitNumber
)
}
@@ -215,7 +223,10 @@ open class RouteModel() {
maneuverType,
maneuverIcon,
arrivalTime(),
travelLeftDistance()
travelLeftDistance(),
listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())),
step.maneuver.exit
)
}
@@ -263,7 +274,7 @@ open class RouteModel() {
fun travelLeftDistance(): Double {
var leftDistance = 0.0
for (i in route.currentStep + 1..<curLeg.steps.size) {
val step = route.legs!![0].steps[i]
val step = route.legs()[0].steps[i]
leftDistance += step.distance
}
leftDistance += leftStepDistance() / 1000

View File

@@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.gson.GsonBuilder
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.ObjectBox.boxStore
import com.kouros.navigation.data.Place
@@ -19,12 +18,16 @@ import com.kouros.navigation.data.nominatim.Search
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.overpass.Overpass
import com.kouros.navigation.data.tomtom.Features
import com.kouros.navigation.data.tomtom.Traffic
import com.kouros.navigation.data.tomtom.TrafficData
import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.location
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.maplibre.geojson.FeatureCollection
import java.time.LocalDateTime
import java.time.ZoneOffset
@@ -34,6 +37,11 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
MutableLiveData()
}
val traffic: MutableLiveData<Map<String, String> > by lazy {
MutableLiveData()
}
val previewRoute: MutableLiveData<String> by lazy {
MutableLiveData()
}
@@ -156,6 +164,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
try {
route.postValue(
repository.getRoute(
context,
currentLocation,
location,
carOrientation,
@@ -168,11 +177,46 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
fun loadTraffic(context: Context, currentLocation: Location, carOrientation : Float) {
viewModelScope.launch(Dispatchers.IO) {
try {
val data = repository.getTraffic(
context,
currentLocation,
carOrientation
)
val trafficData = rebuildTraffic(data)
traffic.postValue(
trafficData
)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun rebuildTraffic(data: String) : Map<String, String> {
val featureCollection = FeatureCollection.fromJson(data)
val incidents = mutableMapOf<String, String>()
val queuing = featureCollection.features()!!.filter { it.properties()!!.get("events").toString().contains("Queuing traffic")}
incidents["queuing"] = FeatureCollection.fromFeatures(queuing).toJson()
val stationary = featureCollection.features()!!.filter { it.properties()!!.get("events").toString().contains("Stationary traffic")}
incidents["stationary"] = FeatureCollection.fromFeatures(stationary).toJson()
val slow = featureCollection.features()!!.filter { it.properties()!!.get("events").toString().contains("Slow traffic")}
incidents["slow"] = FeatureCollection.fromFeatures(slow).toJson()
val heavy = featureCollection.features()!!.filter { it.properties()!!.get("events").toString().contains("Heavy traffic")}
incidents["heavy"] = FeatureCollection.fromFeatures(heavy).toJson()
val roadworks = featureCollection.features()!!.filter { it.properties()!!.get("events").toString().contains("Roadworks")}
incidents["roadworks"] = FeatureCollection.fromFeatures(roadworks).toJson()
return incidents
}
fun loadPreviewRoute(context: Context, currentLocation: Location, location: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) {
try {
previewRoute.postValue(
repository.getRoute(
context,
currentLocation,
location,
carOrientation,

View File

@@ -115,45 +115,31 @@ object GeoUtils {
return featureCollection.toJson()
}
fun getOverpassBbox(location: Location, radius: Double): String {
val bbox = getBoundingBox(location.longitude, location.latitude, radius)
val neLon = bbox["ne"]?.get("lon")
val neLat = bbox["ne"]?.get("lat")
val swLon = bbox["sw"]?.get("lon")
val swLat = bbox["sw"]?.get("lat")
return "$swLon,$swLat,$neLon,$neLat"
}
/**
* Calculate the lat and len of a square around a point.
* @return latMin, latMax, lngMin, lngMax
*/
fun calculateSquareRadius(lat: Double, lng: Double, radius: Double): DoubleArray {
fun calculateSquareRadius(lat: Double, lng: Double, radius: Double): String {
val earthRadius = 6371.0 // earth radius in km
val latMin = lat - toDegrees(radius / earthRadius)
val latMax = lat + toDegrees(radius / earthRadius)
val lngMin = lng - toDegrees(radius / earthRadius / cos(toRadians(lat)))
val lngMax = lng + toDegrees(radius / earthRadius / cos(toRadians(lat)))
return doubleArrayOf(latMin, latMax, lngMin, lngMax)
return "$lngMin,$latMin,$lngMax,$latMax"
}
fun getBoundingBox(
lat: Double,
lon: Double,
radius: Double
): Map<String, Map<String, Double>> {
): String {
val earthRadius = 6371.0
val maxLat = lat + toDegrees(radius / earthRadius)
val minLat = lat - toDegrees(radius / earthRadius)
val maxLon = lon + toDegrees(radius / earthRadius / cos(toRadians(lat)))
val minLon = lon - toDegrees(radius / earthRadius / cos(toRadians(lat)))
return mapOf(
"nw" to mapOf("lat" to maxLat, "lon" to minLon),
"ne" to mapOf("lat" to maxLat, "lon" to maxLon),
"sw" to mapOf("lat" to minLat, "lon" to minLon),
"se" to mapOf("lat" to minLat, "lon" to maxLon)
)
return "$minLat,$minLon,$maxLat,$maxLon"
}
}

View File

@@ -0,0 +1,63 @@
package com.kouros.navigation.utils
import kotlin.math.min
/**
* The Levenshtein distance between two words is the minimum number of single-character edits (insertions, deletions or
* substitutions) required to change one string into the other.
*
* This implementation uses dynamic programming (WagnerFischer algorithm).
*
* [Levenshtein Distance](https://en.wikipedia.org/wiki/Levenshtein_distance)
*/
class Levenshtein {
/**
* The Levenshtein distance, or edit distance, between two words is the minimum number of single-character edits
* (insertions, deletions or substitutions) required to change one word into the other.
*
* It is always at least the difference of the sizes of the two strings.
* It is at most the length of the longer string.
* It is `0` if and only if the strings are equal.
*
* @param first first string to compare.
* @param second second string to compare.
* @param limit the maximum result to compute before stopping, terminating calculation early.
* @return the computed Levenshtein distance.
*/
fun distance(first: CharSequence, second: CharSequence, limit: Int = Int.MAX_VALUE): Int {
if (first == second) return 0
if (first.isEmpty()) return second.length
if (second.isEmpty()) return first.length
// initial costs is the edit distance from an empty string, which corresponds to the characters to inserts.
// the array size is : length + 1 (empty string)
var cost = IntArray(first.length + 1) { it }
var newCost = IntArray(first.length + 1)
for (i in 1..second.length) {
// calculate new costs from the previous row.
// the first element of the new row is the edit distance (deletes) to match empty string
newCost[0] = i
var minCost = i
// fill in the rest of the row
for (j in 1..first.length) {
// if it's the same char at the same position, no edit cost.
val edit = if (first[j - 1] == second[i - 1]) 0 else 1
val replace = cost[j - 1] + edit
val insert = cost[j] + 1
val delete = newCost[j - 1] + 1
newCost[j] = minOf(insert, delete, replace)
minCost = min(minCost, newCost[j])
}
if (minCost >= limit) return limit
// flip references of current and previous row
val swap = cost
cost = newCost
newCost = swap
}
return cost.last()
}
}

View File

@@ -8,6 +8,7 @@ import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.Constants.SHARED_PREF_KEY
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.ViewModel
import java.time.LocalDateTime
@@ -29,7 +30,8 @@ object NavigationUtils {
val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
return when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
else -> ViewModel(OsrmRepository())
RouteEngine.OSRM.ordinal -> ViewModel(OsrmRepository())
else -> ViewModel(TomTomRepository())
}
}

View File

@@ -69,17 +69,7 @@
"source-layer": "landuse",
"maxzoom": 12,
"filter": ["==", ["get", "class"], "residential"],
"paint": {
"fill-color": [
"interpolate",
["linear"],
["zoom"],
9,
"hsla(0,3%,85%,0.84)",
12,
"hsla(35,57%,88%,0.49)"
]
}
"paint": {"fill-color": "rgba(48, 43, 57, 1)"}
},
{
"id": "landcover_wood",
@@ -89,7 +79,7 @@
"filter": ["==", ["get", "class"], "wood"],
"paint": {
"fill-antialias": false,
"fill-color": "hsla(98,61%,72%,0.7)",
"fill-color": "rgba(21, 28, 16, 0.7)",
"fill-opacity": 0.4
}
},
@@ -1303,15 +1293,7 @@
],
"layout": {"line-cap": "round", "line-join": "round"},
"paint": {
"line-color": [
"interpolate",
["linear"],
["zoom"],
5,
"hsl(26,87%,62%)",
6,
"#ab9"
],
"line-color": "rgba(12, 84, 84, 1)",
"line-width": [
"interpolate",
["exponential", 1.2],
@@ -2457,7 +2439,8 @@
"text-field": ["to-string", ["get", "ref"]],
"text-font": ["Noto Sans Regular"],
"text-rotation-alignment": "viewport",
"text-size": 10
"text-size": 10,
"visibility": "none"
}
},
{
@@ -2585,7 +2568,8 @@
"text-letter-spacing": 0.1,
"text-max-width": 9,
"text-size": ["interpolate", ["linear"], ["zoom"], 8, 9, 12, 10],
"text-transform": "uppercase"
"text-transform": "uppercase",
"visibility": "none"
},
"paint": {
"text-color": "#333",

View File

@@ -0,0 +1,603 @@
{
"formatVersion": "0.0.12",
"report": {
"effectiveSettings": [
{
"key": "avoid",
"value": "unpavedRoads"
},
{
"key": "computeBestOrder",
"value": "false"
},
{
"key": "computeTollAmounts",
"value": "none"
},
{
"key": "computeTravelTimeFor",
"value": "none"
},
{
"key": "contentType",
"value": "json"
},
{
"key": "departAt",
"value": "2026-01-29T08:43:35.397Z"
},
{
"key": "guidanceVersion",
"value": "1"
},
{
"key": "includeTollPaymentTypes",
"value": "none"
},
{
"key": "instructionsType",
"value": "text"
},
{
"key": "language",
"value": "en-GB"
},
{
"key": "locations",
"value": "48.18565,11.57928:48.11830,11.59485"
},
{
"key": "maxAlternatives",
"value": "0"
},
{
"key": "routeRepresentation",
"value": "encodedPolyline"
},
{
"key": "routeType",
"value": "eco"
},
{
"key": "sectionType",
"value": "lanes"
},
{
"key": "sectionType",
"value": "traffic"
},
{
"key": "traffic",
"value": "true"
},
{
"key": "travelMode",
"value": "car"
},
{
"key": "vehicleAxleWeight",
"value": "0"
},
{
"key": "vehicleCommercial",
"value": "false"
},
{
"key": "vehicleEngineType",
"value": "combustion"
},
{
"key": "vehicleHeading",
"value": "90"
},
{
"key": "vehicleHeight",
"value": "0.00"
},
{
"key": "vehicleLength",
"value": "0.00"
},
{
"key": "vehicleMaxSpeed",
"value": "120"
},
{
"key": "vehicleNumberOfAxles",
"value": "0"
},
{
"key": "vehicleWeight",
"value": "0"
},
{
"key": "vehicleWidth",
"value": "0.00"
}
]
},
"routes": [
{
"summary": {
"lengthInMeters": 10879,
"travelTimeInSeconds": 1170,
"trafficDelayInSeconds": 76,
"trafficLengthInMeters": 1727,
"departureTime": "2026-01-29T09:43:35+01:00",
"arrivalTime": "2026-01-29T10:03:05+01:00"
},
"legs": [
{
"summary": {
"lengthInMeters": 10879,
"travelTimeInSeconds": 1170,
"trafficDelayInSeconds": 76,
"trafficLengthInMeters": 1727,
"departureTime": "2026-01-29T09:43:35+01:00",
"arrivalTime": "2026-01-29T10:03:05+01:00"
},
"encodedPolyline": "sfbeHmqteAEjDQEy@GQ?wDQFkEH{G?M?sA@kB?_FAeC?o@?[@_@\\Ab@CVAz@CJA@?dBGhAGjAExAGlBEvBKdCKTAfCKLAv@ELA|AGnAGt@ClCKjDQpBIf@BXDPDPBF@ZB?S@SAYAUKi@Go@Cc@BgBBs@Bg@JyAJiAXqBDWNs@TaA\\mAFa@Po@`BwF?YL]FSFSl@iB^kALc@L]Ro@f@yAf@{AFQNe@dAoCdBgEx@qBTi@BITe@L[L_@^}@HSXu@pB}El@eAb@e@f@[h@QZCRAL?j@HRFh@Vf@XLJhAn@lAv@TLtAz@r@`@bAn@pAv@f@X~@j@z@h@vBpA`@VHDFFJFf@X`CzApAh@f@L`@Fz@@f@AXEVEVEhA[h@Yn@e@PQFEJKRWV[PW`@w@t@}AHQN]~BiFP]`AoBh@aADGTa@t@aAt@{@PQJKJGFG@Cd@]XSxDmCf@a@n@o@TY\\g@LQHMJSLUP[f@iAPg@b@yAFODMNi@FS|@qCVaAHUHUn@wBHYh@eBpAkEjBiGfAeDj@yADMFQd@sAf@kAJUh@qAf@eAt@sAn@iALSN[p@kAVc@JOLSj@w@z@}@x@q@pAu@p@_@j@Sl@MLCRCb@E`@?^?L@`ABz@?N@~AFdADJ@rAH`DVpCVrAJd@BfHp@zGl@pAJ|ALnGp@jEh@fBJpAFF?P@N@ZCtC]r@GJCFCLCD?TEVEXGhAYzAg@NGv@]`@QJEvB_AXMVK\\Qb@Qn@QJCNAZC^ENA`@FnBb@xEtA^H^JnCl@z@r@`@Pr@TtBlA~C`Bn@\\xAl@PF`@LrAVlCh@bBLl@BlBJdG\\RDjCHn@?pB?xB?R@`@@GxAC^?ZInBIfCAvC?r@@dD@n@@b@@^D`C?TDxAFbBHdB@VHp@RjAJb@NNH`@VlBFf@PzARhBFd@@LRbBFh@\\nC@FNhAb@lEj@tDPpABTBRZlBTdBXjBn@xEBLDTRpAR~@Fb@",
"encodedPolylinePrecision": 5
}
],
"sections": [
{
"startPointIndex": 83,
"endPointIndex": 147,
"sectionType": "TRAFFIC",
"simpleCategory": "JAM",
"effectiveSpeedInKmh": 35,
"delayInSeconds": 76,
"magnitudeOfDelay": 1,
"tec": {
"causes": [
{
"mainCauseCode": 1
}
],
"effectCode": 4
},
"eventId": "TTL41048054144049000"
},
{
"lanes": [
{
"directions": [
"SLIGHT_LEFT"
],
"follow": "SLIGHT_LEFT"
},
{
"directions": [
"STRAIGHT"
]
},
{
"directions": [
"STRAIGHT"
]
},
{
"directions": [
"STRAIGHT"
]
}
],
"laneSeparators": [
"SINGLE_SOLID",
"SINGLE_SOLID",
"LONG_DASHED",
"LONG_DASHED",
"SINGLE_SOLID"
],
"startPointIndex": 42,
"endPointIndex": 45,
"sectionType": "LANES"
},
{
"lanes": [
{
"directions": [
"STRAIGHT"
],
"follow": "STRAIGHT"
},
{
"directions": [
"SLIGHT_RIGHT"
]
}
],
"laneSeparators": [
"SINGLE_SOLID",
"SHORT_DASHED",
"SINGLE_SOLID"
],
"startPointIndex": 61,
"endPointIndex": 62,
"sectionType": "LANES"
},
{
"lanes": [
{
"directions": [
"SLIGHT_LEFT"
],
"follow": "SLIGHT_LEFT"
},
{
"directions": [
"SLIGHT_LEFT"
],
"follow": "SLIGHT_LEFT"
},
{
"directions": [
"SLIGHT_RIGHT"
]
}
],
"laneSeparators": [
"SINGLE_SOLID",
"LONG_DASHED",
"SHORT_DASHED",
"SINGLE_SOLID"
],
"startPointIndex": 74,
"endPointIndex": 75,
"sectionType": "LANES"
},
{
"lanes": [
{
"directions": [
"STRAIGHT"
]
},
{
"directions": [
"SLIGHT_RIGHT"
],
"follow": "SLIGHT_RIGHT"
}
],
"laneSeparators": [
"SINGLE_SOLID",
"LONG_DASHED",
"SINGLE_SOLID"
],
"startPointIndex": 265,
"endPointIndex": 266,
"sectionType": "LANES"
},
{
"lanes": [
{
"directions": [
"LEFT"
]
},
{
"directions": [
"STRAIGHT"
]
},
{
"directions": [
"STRAIGHT"
]
},
{
"directions": [
"RIGHT"
],
"follow": "RIGHT"
}
],
"laneSeparators": [
"SINGLE_SOLID",
"SINGLE_SOLID",
"SINGLE_SOLID",
"SINGLE_SOLID",
"SINGLE_SOLID"
],
"startPointIndex": 287,
"endPointIndex": 288,
"sectionType": "LANES"
},
{
"lanes": [
{
"directions": [
"LEFT"
]
},
{
"directions": [
"STRAIGHT"
],
"follow": "STRAIGHT"
},
{
"directions": [
"STRAIGHT"
],
"follow": "STRAIGHT"
},
{
"directions": [
"RIGHT"
]
}
],
"laneSeparators": [
"SINGLE_SOLID",
"SHORT_DASHED",
"LONG_DASHED",
"SHORT_DASHED",
"SINGLE_SOLID"
],
"startPointIndex": 302,
"endPointIndex": 304,
"sectionType": "LANES"
}
],
"guidance": {
"instructions": [
{
"routeOffsetInMeters": 0,
"travelTimeInSeconds": 0,
"point": {
"latitude": 48.18554,
"longitude": 11.57927
},
"exitNumber": "",
"pointIndex": 0,
"instructionType": "LOCATION_DEPARTURE",
"street": "Vogelhartstraße",
"countryCode": "DEU",
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "DEPART",
"message": "Leave from Vogelhartstraße"
},
{
"routeOffsetInMeters": 64,
"travelTimeInSeconds": 14,
"point": {
"latitude": 48.18557,
"longitude": 11.57841
},
"pointIndex": 1,
"instructionType": "TURN",
"street": "Silcherstraße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": 90,
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "TURN_RIGHT",
"message": "Turn right onto Silcherstraße"
},
{
"routeOffsetInMeters": 218,
"travelTimeInSeconds": 57,
"point": {
"latitude": 48.18696,
"longitude": 11.57857
},
"pointIndex": 5,
"instructionType": "TURN",
"street": "Schmalkaldener Straße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": 90,
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "TURN_RIGHT",
"message": "Turn right onto Schmalkaldener Straße"
},
{
"routeOffsetInMeters": 650,
"travelTimeInSeconds": 131,
"point": {
"latitude": 48.18686,
"longitude": 11.58437
},
"pointIndex": 15,
"instructionType": "TURN",
"roadNumbers": [
"B13"
],
"street": "Ingolstädter Straße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": 90,
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "TURN_RIGHT",
"message": "Turn right onto Ingolstädter Straße/B13"
},
{
"routeOffsetInMeters": 1713,
"travelTimeInSeconds": 266,
"point": {
"latitude": 48.17733,
"longitude": 11.58503
},
"pointIndex": 45,
"instructionType": "TURN",
"roadNumbers": [
"B2R"
],
"street": "Schenkendorfstraße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": -90,
"possibleCombineWithNext": true,
"drivingSide": "RIGHT",
"maneuver": "TURN_LEFT",
"message": "Turn left onto Schenkendorfstraße/B2R",
"combinedMessage": "Turn left onto Schenkendorfstraße/B2R then keep left at Schenkendorfstraße/B2R toward Messe / ICM"
},
{
"routeOffsetInMeters": 2067,
"travelTimeInSeconds": 309,
"point": {
"latitude": 48.17678,
"longitude": 11.58957
},
"pointIndex": 62,
"instructionType": "TURN",
"roadNumbers": [
"B2R"
],
"street": "Schenkendorfstraße",
"countryCode": "DEU",
"signpostText": "Messe / ICM",
"junctionType": "BIFURCATION",
"turnAngleInDecimalDegrees": -45,
"possibleCombineWithNext": true,
"drivingSide": "RIGHT",
"maneuver": "KEEP_LEFT",
"message": "Keep left at Schenkendorfstraße/B2R toward Messe / ICM",
"combinedMessage": "Keep left at Schenkendorfstraße/B2R toward Messe / ICM then keep left at Schenkendorfstraße/B2R toward Passau"
},
{
"routeOffsetInMeters": 2419,
"travelTimeInSeconds": 332,
"point": {
"latitude": 48.17518,
"longitude": 11.59363
},
"pointIndex": 75,
"instructionType": "TURN",
"roadNumbers": [
"B2R"
],
"street": "Schenkendorfstraße",
"countryCode": "DEU",
"signpostText": "Passau",
"junctionType": "BIFURCATION",
"turnAngleInDecimalDegrees": -45,
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "KEEP_LEFT",
"message": "Keep left at Schenkendorfstraße/B2R toward Passau"
},
{
"routeOffsetInMeters": 2774,
"travelTimeInSeconds": 357,
"point": {
"latitude": 48.17329,
"longitude": 11.59747
},
"pointIndex": 86,
"instructionType": "DIRECTION_INFO",
"roadNumbers": [
"B2R"
],
"street": "Isarring",
"countryCode": "DEU",
"signpostText": "München-Ost",
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "FOLLOW",
"message": "Follow Isarring/B2R toward München-Ost"
},
{
"routeOffsetInMeters": 8425,
"travelTimeInSeconds": 806,
"point": {
"latitude": 48.13017,
"longitude": 11.61541
},
"pointIndex": 266,
"instructionType": "TURN",
"street": "Ampfingstraße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": 45,
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "BEAR_RIGHT",
"message": "Bear right at Ampfingstraße"
},
{
"routeOffsetInMeters": 9487,
"travelTimeInSeconds": 953,
"point": {
"latitude": 48.12089,
"longitude": 11.61285
},
"pointIndex": 288,
"instructionType": "TURN",
"street": "Anzinger Straße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": 90,
"possibleCombineWithNext": true,
"drivingSide": "RIGHT",
"maneuver": "TURN_RIGHT",
"message": "Turn right onto Anzinger Straße",
"combinedMessage": "Turn right onto Anzinger Straße then keep straight on at Sankt-Martin-Straße"
},
{
"routeOffsetInMeters": 9983,
"travelTimeInSeconds": 1044,
"point": {
"latitude": 48.12087,
"longitude": 11.60621
},
"pointIndex": 304,
"instructionType": "TURN",
"street": "Sankt-Martin-Straße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": 0,
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "STRAIGHT",
"message": "Keep straight on at Sankt-Martin-Straße"
},
{
"routeOffsetInMeters": 10879,
"travelTimeInSeconds": 1170,
"point": {
"latitude": 48.1183,
"longitude": 11.59485
},
"pointIndex": 335,
"instructionType": "LOCATION_ARRIVAL",
"street": "Sankt-Martin-Straße",
"countryCode": "DEU",
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "ARRIVE",
"message": "You have arrived at Sankt-Martin-Straße"
}
],
"instructionGroups": [
{
"firstInstructionIndex": 0,
"lastInstructionIndex": 3,
"groupMessage": "Leave from Vogelhartstraße. Take the Ingolstädter Straße/B13",
"groupLengthInMeters": 1713
},
{
"firstInstructionIndex": 4,
"lastInstructionIndex": 7,
"groupMessage": "Take the Schenkendorfstraße, Isarring/B2R toward Messe / ICM, Passau, München-Ost",
"groupLengthInMeters": 6712
},
{
"firstInstructionIndex": 8,
"lastInstructionIndex": 11,
"groupMessage": "Take the Ampfingstraße, Anzinger Straße. Continue to your destination at Sankt-Martin-Straße",
"groupLengthInMeters": 2454
}
]
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -48,5 +48,6 @@
<string name="charging_station">Ladestation</string>
<string name="speed_camera">Speed camera</string>
<string name="use_car_location">Auto GPS verwenden</string>
<string name="tomtom">TomTom\t</string>
</resources>

View File

@@ -34,4 +34,5 @@
<string name="osrm" translatable="false">Osrm</string>
<string name="routing_engine" translatable="false">Routing engine</string>
<string name="use_car_location">Use car location</string>
<string name="tomtom">TomTom\t</string>
</resources>