TomTom Routing

This commit is contained in:
Dimitris
2026-02-07 12:56:45 +01:00
parent eac5b56bcb
commit 0d51c6121d
50 changed files with 8923 additions and 5084 deletions

View File

@@ -213,7 +213,7 @@ class SurfaceRenderer(
DrawNavigationImages(
paddingValues,
currentSpeed,
routeModel.maxSpeed,
routeModel,
width,
height
)

View File

@@ -2,7 +2,6 @@ package com.kouros.navigation.car.map
import android.content.Context
import android.location.Location
import androidx.car.app.connection.CarConnection
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
@@ -32,10 +31,9 @@ import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.data.NavigationColor
import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.data.tomtom.TrafficData
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
@@ -92,7 +90,7 @@ fun MapLibre(
cameraState: CameraState,
baseStyle: BaseStyle.Json,
route: String?,
traffic: Map<String, String> ?,
traffic: Map<String, String>?,
viewStyle: ViewStyle,
speedCameras: String? = ""
) {
@@ -112,6 +110,7 @@ fun MapLibre(
AmenityLayer(route)
} else {
RouteLayer(route, traffic!!)
//RouteLayerPoint(route )
}
SpeedCameraLayer(speedCameras)
}
@@ -164,9 +163,9 @@ fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
type = exponential(1.2f),
input = zoom(),
5 to const(0.4.dp),
6 to const(0.8.dp),
7 to const(2.0.dp),
20 to const(24.dp),
6 to const(0.6.dp),
7 to const(1.8.dp),
20 to const(20.dp),
),
)
LineLayer(
@@ -178,18 +177,44 @@ fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
type = exponential(1.2f),
input = zoom(),
5 to const(0.4.dp),
6 to const(0.7.dp),
7 to const(1.75.dp),
20 to const(22.dp),
6 to const(0.5.dp),
7 to const(1.6.dp),
20 to const(18.dp),
),
)
}
}
@Composable
fun RouteLayerPoint(routeData: String?) {
if (routeData != null && routeData.isNotEmpty()) {
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
val img = image(painterResource(R.drawable.ic_favorite_filled_white_24dp), drawAsSdf = true)
SymbolLayer(
id = "point-layer",
source = routes,
iconOpacity = const(2.0f),
iconColor = const(Color.Red),
iconImage = img,
iconSize =
interpolate(
type = exponential(1.2f),
input = zoom(),
5 to const(0.4f),
6 to const(0.6f),
7 to const(0.8f),
20 to const(1.0f),
),
)
}
}
fun trafficColor(key: String): Expression<ColorValue> {
when (key) {
"queuing" -> return const(Color(0xFFD24417))
"queuing" -> return const(Color(0xFFD24417))
"stationary" -> return const(Color(0xFFFF0000))
"heavy" -> return const(Color(0xFF6B0404))
"slow" -> return const(Color(0xFFC41F1F))
@@ -206,7 +231,7 @@ fun AmenityLayer(routeData: String?) {
if (routeData.contains(Constants.CHARGING_STATION)) {
color = const(Color.Green)
img = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true)
} else if (routeData.contains(Constants.FUEL_STATION)){
} else if (routeData.contains(Constants.FUEL_STATION)) {
color = const(Color.Black)
img = image(painterResource(R.drawable.local_gas_station_48px), drawAsSdf = true)
}
@@ -269,17 +294,18 @@ fun BuildingLayer(tiles: Source) {
fun DrawNavigationImages(
padding: PaddingValues,
speed: Float?,
maxSpeed: Int,
routeModel: RouteModel,
width: Int,
height: Int
) {
NavigationImage(padding, width, height)
if (speed != null) {
CurrentSpeed(width, height, speed, maxSpeed)
CurrentSpeed(width, height, speed, routeModel.maxSpeed)
}
if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) {
MaxSpeed(width, height, maxSpeed)
if (speed != null && routeModel.maxSpeed > 0 && (speed * 3.6) > routeModel.maxSpeed) {
MaxSpeed(width, height, routeModel.maxSpeed)
}
//DebugInfo(width, height, routeModel)
}
@Composable
@@ -429,14 +455,52 @@ private fun MaxSpeed(
}
@Composable
fun rememberBaseStyle( baseStyle : BaseStyle.Json): BaseStyle.Json {
fun DebugInfo(
width: Int,
height: Int,
routeModel: RouteModel,
) {
Box(
modifier = Modifier
.padding(
start = 20.dp,
top = 0.dp
),
contentAlignment = Alignment.CenterStart
) {
val textMeasurerLocation = rememberTextMeasurer()
val location = routeModel.location.latitude.toString()
val styleSpeed = TextStyle(
fontSize = 26.sp,
fontWeight = FontWeight.Bold,
color = Color.Black,
)
val textLayoutLocation = remember(location) {
textMeasurerLocation.measure(location, styleSpeed)
}
Canvas(modifier = Modifier.fillMaxSize()) {
drawText(
textMeasurer = textMeasurerLocation,
text = location,
style = styleSpeed,
topLeft = Offset(
x = center.x - textLayoutLocation.size.width / 2,
y = center.y - textLayoutLocation.size.height / 2,
)
)
}
}
}
@Composable
fun rememberBaseStyle(baseStyle: BaseStyle.Json): BaseStyle.Json {
val rememberBaseStyle by remember() {
mutableStateOf(baseStyle)
}
return rememberBaseStyle
}
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
return when (viewStyle) {
ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues(

View File

@@ -16,7 +16,6 @@
package com.kouros.navigation.car.navigation
import android.text.SpannableString
import android.text.Spanned
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.car.app.AppManager
@@ -27,7 +26,6 @@ import androidx.car.app.model.Alert
import androidx.car.app.model.AlertCallback
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarIconSpan
import androidx.car.app.model.CarText
import androidx.car.app.model.DateTimeWithZone
import androidx.car.app.model.Distance
@@ -42,8 +40,9 @@ import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R
import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel
import org.maplibre.compose.expressions.dsl.step
import com.kouros.navigation.utils.location
import java.util.Collections
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.TimeUnit
@@ -54,18 +53,19 @@ class RouteCarModel() : RouteModel() {
fun currentStep(carContext: CarContext): Step {
val stepData = currentStep()
val currentStepCueWithImage: SpannableString =
createString(stepData.instruction)
createString(stepData.instruction)
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
.setIcon(createCarIcon(carContext, stepData.icon))
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW) {
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
) {
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
}
val step =
Step.Builder(currentStepCueWithImage)
.setManeuver(
maneuver.build()
maneuver.build()
)
if (destination.street != null) {
step.setRoad(destination.street!!)
@@ -81,16 +81,17 @@ class RouteCarModel() : RouteModel() {
val stepData = nextStep()
val currentStepCueWithImage: SpannableString =
createString(stepData.instruction)
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
.setIcon(createCarIcon(carContext, stepData.icon))
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW) {
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
) {
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
}
val step =
Step.Builder(currentStepCueWithImage)
.setManeuver(
maneuver.build()
maneuver.build()
)
.build()
return step
@@ -100,13 +101,13 @@ class RouteCarModel() : RouteModel() {
val timeLeft = travelLeftTime()
val timeToDestinationMillis =
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
val leftDistance = travelLeftDistance()
val leftDistance = travelLeftDistance() / 1000
val displayUnit = if (leftDistance > 1.0) {
Distance.UNIT_KILOMETERS
} else {
Distance.UNIT_METERS
}
val arivalTime = DateTimeWithZone.create(
val arrivalTime = DateTimeWithZone.create(
arrivalTime(),
TimeZone.getTimeZone("Europe/Berlin")
)
@@ -115,7 +116,7 @@ class RouteCarModel() : RouteModel() {
leftDistance,
displayUnit
), // Arrival time at the destination with the destination time zone.
arivalTime
arrivalTime
)
.setRemainingTimeSeconds(
TimeUnit.MILLISECONDS.toSeconds(
@@ -132,161 +133,99 @@ class RouteCarModel() : RouteModel() {
return travelBuilder.build()
}
fun addLanes(carContext: CarContext, step: Step.Builder, stepData: StepData) {
var laneImageAdded = false
stepData.lane.forEach {
if (it.indications.isNotEmpty() && it.valid) {
Collections.sort<String>(it.indications)
var direction = ""
it.indications.forEach { it2 ->
direction = if (direction.isEmpty()) {
it2.trim()
} else {
"${direction}_${it2.trim()}"
}
fun addLanes(carContext: CarContext, step: Step.Builder, stepData: StepData) {
var laneImageAdded = false
stepData.lane.forEach {
if (it.indications.isNotEmpty() && it.valid) {
Collections.sort<String>(it.indications)
var direction = ""
it.indications.forEach { it2 ->
direction = if (direction.isEmpty()) {
it2.trim()
} else {
"${direction}_${it2.trim()}"
}
val laneDirection = when (direction) {
"left_straight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"left" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"straight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"right" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"right_straight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"left_slight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_SLIGHT_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"right_slight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_SLIGHT_RIGHT-> LaneDirection.SHAPE_NORMAL_RIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
else -> {
LaneDirection.SHAPE_UNKNOWN
}
}
if (laneDirection != LaneDirection.SHAPE_UNKNOWN) {
if (!laneImageAdded) {
step.setLanesImage(createCarIcon(createLaneIcon(carContext, stepData)))
laneImageAdded = true
}
val laneType =
Lane.Builder()
.addDirection(LaneDirection.create(laneDirection, false))
.build()
step.addLane(laneType)
}
val laneDirection = addLanes(direction, stepData)
if (laneDirection != LaneDirection.SHAPE_UNKNOWN) {
if (!laneImageAdded) {
step.setLanesImage(createCarIcon(createLaneIcon(carContext, stepData)))
laneImageAdded = true
}
val laneType =
Lane.Builder()
.addDirection(LaneDirection.create(laneDirection, false))
.build()
step.addLane(laneType)
}
}
}
private fun createStringWithIcon(
carContext: CarContext,
text: String,
@DrawableRes iconRes: Int
): SpannableString {
val start = 0
val end = text.length
val span = CarIconSpan.create(createCarIcon(carContext, iconRes), CarIconSpan.ALIGN_CENTER)
val spannableString = SpannableString(text)
spannableString.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
return spannableString
}
fun createString(
text: String
): SpannableString {
val spannableString = SpannableString(text)
return spannableString
}
fun createCarText(carContext: CarContext, @StringRes stringRes: Int): CarText {
return CarText.create(carContext.getString(stringRes))
}
fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
}
fun createCarIcon(iconCompat: IconCompat): CarIcon {
return CarIcon.Builder(iconCompat).build()
}
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String) {
carContext.getCarService<AppManager?>(AppManager::class.java)
.showAlert(createAlert(carContext, distance, maxSpeed, createCarIcon(carContext, R.drawable.speed_camera_24px)))
}
fun createAlert(
carContext: CarContext,
distance: Double,
maxSpeed: String?,
icon: CarIcon
): Alert {
val title = createCarText(carContext, R.string.speed_camera)
val subtitle = CarText.create(maxSpeed!!)
val dismissAction: Action = createToastAction(
carContext,
R.string.exit_action_title, R.string.exit_action_title,
FLAG_DEFAULT
)
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */5000)
.setSubtitle(subtitle)
.setIcon(icon)
.addAction(dismissAction).setCallback(object : AlertCallback {
override fun onCancel(reason: Int) {
}
override fun onDismiss() {
}
}).build()
}
private fun createToastAction(
carContext: CarContext,
@StringRes titleRes: Int, @StringRes toastStringRes: Int,
flags: Int
): Action {
return Action.Builder()
.setOnClickListener { }
.setTitle(createCarText(carContext, titleRes))
.setFlags(flags)
.build()
}
}
fun createString(
text: String
): SpannableString {
val spannableString = SpannableString(text)
return spannableString
}
fun createCarText(carContext: CarContext, @StringRes stringRes: Int): CarText {
return CarText.create(carContext.getString(stringRes))
}
fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
}
// fun createCarIcon(iconCompat: IconCompat): CarIcon {
// return CarIcon.Builder(iconCompat).build()
// }
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String) {
carContext.getCarService<AppManager?>(AppManager::class.java)
.showAlert(
createAlert(
carContext,
maxSpeed,
createCarIcon(carContext, R.drawable.speed_camera_24px)
)
)
}
fun createAlert(
carContext: CarContext,
maxSpeed: String?,
icon: CarIcon
): Alert {
val title = createCarText(carContext, R.string.speed_camera)
val subtitle = CarText.create(maxSpeed!!)
val dismissAction: Action = createToastAction(
carContext,
R.string.exit_action_title, R.string.exit_action_title,
FLAG_DEFAULT
)
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */5000)
.setSubtitle(subtitle)
.setIcon(icon)
.addAction(dismissAction).setCallback(object : AlertCallback {
override fun onCancel(reason: Int) {
}
override fun onDismiss() {
}
}).build()
}
private fun createToastAction(
carContext: CarContext,
@StringRes titleRes: Int, @StringRes toastStringRes: Int,
flags: Int
): Action {
return Action.Builder()
.setOnClickListener { }
.setTitle(createCarText(carContext, titleRes))
.setFlags(flags)
.build()
}
}

View File

@@ -489,7 +489,6 @@ class NavigationScreen(
lastTrafficDate = current
viewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
//updateTraffic(location)
updateSpeedCamera(location)
with(routeModel) {
updateLocation(location, viewModel)
@@ -524,7 +523,7 @@ class NavigationScreen(
val updatedCameras = mutableListOf<Elements>()
speedCameras.forEach {
val plLocation =
location(longitude = it.lon!!, latitude = it.lat!!)
location(longitude = it.lon, latitude = it.lat)
val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble()
updatedCameras.add(it)
@@ -533,7 +532,11 @@ class NavigationScreen(
val camera = sortedList.first()
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
val bearingSpeedCamera = if (camera.tags.direction != null) {
camera.tags.direction!!.toFloat()
try {
camera.tags.direction!!.toFloat()
} catch ( e: Exception) {
0F
}
} else {
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
}

View File

@@ -34,7 +34,7 @@ class SearchScreen(
var categories: List<Category> = listOf(
Category(id = Constants.RECENT, name = carContext.getString(R.string.recent_destinations)),
Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)),
// Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)),
Category(id = Constants.CATEGORIES, name = carContext.getString(R.string.category_title)),
Category(id = Constants.FAVORITES, name = carContext.getString(R.string.favorites))
)

View File

@@ -1,11 +1,5 @@
package com.kouros.navigation.car
import android.location.Location
import android.location.LocationManager
import com.kouros.navigation.data.Constants.home2Location
import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel
@@ -24,16 +18,16 @@ class ViewModelTest {
@Test
fun routeViewModelTest() {
val fromLocation = Location(LocationManager.GPS_PROVIDER)
fromLocation.isMock = true
fromLocation.latitude = homeLocation.latitude
fromLocation.longitude = homeLocation.longitude
val toLocation = Location(LocationManager.GPS_PROVIDER)
toLocation.isMock = true
toLocation.latitude = home2Location.latitude
toLocation.longitude = home2Location.longitude
val route = repo.getRoute(fromLocation, toLocation, SearchFilter())
// val fromLocation = Location(LocationManager.GPS_PROVIDER)
// fromLocation.isMock = true
// fromLocation.latitude = homeLocation.latitude
// fromLocation.longitude = homeLocation.longitude
// val toLocation = Location(LocationManager.GPS_PROVIDER)
// toLocation.isMock = true
// toLocation.latitude = home2Location.latitude
// toLocation.longitude = home2Location.longitude
//
// val route = repo.getRoute(fromLocation, toLocation, SearchFilter())
//model.startNavigation(route)
}
}

View File

@@ -58,7 +58,7 @@ dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.maplibre.compose)
implementation("androidx.compose.material:material-icons-extended:1.7.8")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -29,11 +29,11 @@ import java.net.URL
abstract class NavigationRepository {
//private val nominatimUrl = "https://nominatim.openstreetmap.org/"
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
private val nominatimUrl = "https://kouros-online.de/nominatim/"
//private val nominatimUrl = "https://kouros-online.de/nominatim/"
private val tomtomApiKey = "678k5v6940cSXXIS5oD92qIrDgW3RBZ3"
val tomtomApiKey = "678k5v6940cSXXIS5oD92qIrDgW3RBZ3"
private val tomtomUrl = "https://api.tomtom.com/traffic/services/5/incidentDetails"
@@ -55,18 +55,19 @@ abstract class NavigationRepository {
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
//val route = getRoute(context, currentLocation, location, carOrientation, searchFilter)
//val routeModel = RouteModel()
//routeModel.startNavigation(route, context)
// return routeModel.curRoute.summary.distance
return 0.0
}
fun searchPlaces(search: String, location: Location): String {
val box = calculateSquareRadius(location.latitude, location.longitude, 20.0)
val box = calculateSquareRadius(location.latitude, location.longitude, 100.0)
val viewbox = "&bounded=1&viewbox=${box}"
return fetchUrl(
"${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox",
false
true
)
}

View File

@@ -22,7 +22,7 @@ data class Route(
val routeEngine: Int,
val routes: List<com.kouros.navigation.data.route.Routes>,
var currentStep: Int = 0,
var currentStepIndex: Int = 0,
) {
data class Builder(
@@ -51,15 +51,15 @@ data class Route(
jsonObject["trip"].toString(),
ValhallaResponse::class.java
)
ValhallaRoute().mapJsonToValhalla(routeJson, this)
ValhallaRoute().mapToRoute(routeJson, this)
}
RouteEngine.OSRM.ordinal -> {
val osrmJson = gson.fromJson(route, OsrmResponse::class.java)
OsrmRoute().mapToOsrm(osrmJson, this)
OsrmRoute().mapToRoute(osrmJson, this)
}
else -> {
val tomtomJson = gson.fromJson(route, TomTomResponse::class.java)
TomTomRoute().mapToOsrm(tomtomJson, this)
TomTomRoute().mapToRoute(tomtomJson, this)
}
}
}
@@ -91,21 +91,23 @@ data class Route(
}
}
fun isRouteValid(): Boolean {
return routes.isNotEmpty() && legs().isNotEmpty()
}
fun currentStep(): Step {
return if (routes.isNotEmpty() && legs().isNotEmpty()) {
legs().first().steps[currentStep]
return if (isRouteValid()) {
legs().first().steps[currentStepIndex]
} else {
Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0)))
}
}
fun nextStep(): Step {
val nextIndex = currentStep + 1
return if (nextIndex < legs().first().steps.size) {
fun nextStep(steps : Int): Step {
val nextIndex = currentStepIndex + steps
return if (isRouteValid() && nextIndex < legs().first().steps.size) {
legs().first().steps[nextIndex]
} else {
throw IndexOutOfBoundsException("No next maneuver available.")
currentStep()
}
}

View File

@@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName
data class Legs (
@SerializedName("steps" ) var steps : ArrayList<Steps> = arrayListOf(),
@SerializedName("steps" ) var steps : List<Steps> = listOf(),
@SerializedName("weight" ) var weight : Double = 0.0,
@SerializedName("summary" ) var summary : String = "",
@SerializedName("duration" ) var duration : Double = 0.0,

View File

@@ -1,6 +1,7 @@
package com.kouros.navigation.data.osrm
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.route.Intersection
import com.kouros.navigation.data.route.Lane
import com.kouros.navigation.data.route.Leg
@@ -14,7 +15,7 @@ import com.kouros.navigation.utils.location
class OsrmRoute {
fun mapToOsrm(routeJson: OsrmResponse, builder: Route.Builder) {
fun mapToRoute(routeJson: OsrmResponse, builder: Route.Builder) {
val routes = mutableListOf<com.kouros.navigation.data.route.Routes>()
var stepIndex = 0
@@ -61,7 +62,7 @@ class OsrmRoute {
distance = step.distance / 1000,
duration = step.duration,
maneuver = maneuver,
intersection = intersections
//intersection = intersections
)
steps.add(step)
stepIndex += 1
@@ -80,7 +81,7 @@ class OsrmRoute {
routes.add(newRoute)
}
builder
.routeType(1)
.routeType(RouteEngine.OSRM.ordinal)
.routes(routes)
}

View File

@@ -3,6 +3,6 @@ package com.kouros.navigation.data.route
import java.util.Collections
data class Intersection(
val location: ArrayList<Double> = arrayListOf(0.0, 0.0),
val location: List<Double> = listOf(0.0, 0.0),
val lane : List<Lane> = Collections.emptyList<Lane>(),
)

View File

@@ -1,5 +1,6 @@
package com.kouros.navigation.data.route
data class Leg(
var steps : List<Step> = arrayListOf()
var steps : List<Step> = arrayListOf(),
var intersection: List<Intersection> = arrayListOf()
)

View File

@@ -0,0 +1,5 @@
package com.kouros.navigation.data.tomtom
data class Cause(
val mainCauseCode: Int
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class Guidance(
val instructionGroups: List<InstructionGroup>,
val instructions: List<Instruction>
)

View File

@@ -0,0 +1,21 @@
package com.kouros.navigation.data.tomtom
data class Instruction(
val combinedMessage: String,
val countryCode: String,
val drivingSide: String,
val instructionType: String,
val junctionType: String,
val maneuver: String,
val message: String,
val point: Point,
val pointIndex: Int,
val possibleCombineWithNext: Boolean,
val roadNumbers: List<String>,
val routeOffsetInMeters: Int,
val signpostText: String,
val street: String = "",
val travelTimeInSeconds: Int,
val turnAngleInDecimalDegrees: Int,
val exitNumber: String? = "0",
)

View File

@@ -0,0 +1,8 @@
package com.kouros.navigation.data.tomtom
data class InstructionGroup(
val firstInstructionIndex: Int,
val groupLengthInMeters: Int,
val groupMessage: String,
val lastInstructionIndex: Int
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class Lane(
val directions: List<String>,
val follow: String
)

View File

@@ -0,0 +1,7 @@
package com.kouros.navigation.data.tomtom
data class Leg(
val encodedPolyline: String,
val encodedPolylinePrecision: Int,
val summary: SummaryX
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class Point(
val latitude: Double,
val longitude: Double
)

View File

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

View File

@@ -0,0 +1,8 @@
package com.kouros.navigation.data.tomtom
data class Route(
val guidance: Guidance,
val legs: List<Leg>,
val sections: List<Section>,
val summary: SummaryX
)

View File

@@ -0,0 +1,16 @@
package com.kouros.navigation.data.tomtom
import java.util.Collections
data class Section(
val delayInSeconds: Int,
val effectiveSpeedInKmh: Int,
val endPointIndex: Int,
val eventId: String,
val laneSeparators: List<String>,
val lanes: List<Lane>? = Collections.emptyList<Lane>(),
val magnitudeOfDelay: Int,
val sectionType: String,
val simpleCategory: String,
val startPointIndex: Int,
)

View File

@@ -0,0 +1,10 @@
package com.kouros.navigation.data.tomtom
data class SummaryX(
val arrivalTime: String,
val departureTime: String,
val lengthInMeters: Int,
val trafficDelayInSeconds: Int,
val trafficLengthInMeters: Int,
val travelTimeInSeconds: Int
)

View File

@@ -0,0 +1,36 @@
package com.kouros.navigation.data.tomtom
import android.content.Context
import android.location.Location
import com.kouros.data.R
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
private const val routeUrl = "https://api.tomtom.com/routing/1/calculateRoute/"
class TomTomRepository : NavigationRepository() {
override fun getRoute(
context: Context,
currentLocation: Location,
location: Location,
carOrientation: Float,
searchFilter: SearchFilter
): String {
//val routeJson = context.resources.openRawResource(R.raw.tomom_routing)
//val routeJsonString = routeJson.bufferedReader().use { it.readText() }
//return routeJsonString
val url =
routeUrl + "${currentLocation.latitude},${currentLocation.longitude}:${location.latitude},${location.longitude}" +
"/json?vehicleHeading=90&sectionType=traffic&report=effectiveSettings&routeType=eco" +
"&traffic=true&avoid=unpavedRoads&travelMode=car" +
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
"&instructionsType=text&language=en-GB&sectionType=lanes" +
"&routeRepresentation=encodedPolyline" +
"&vehicleEngineType=combustion&key=$tomtomApiKey"
return fetchUrl(
url,
false
)
}
}

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class TomTomResponse(
val formatVersion: String,
val routes: List<Route>
)

View File

@@ -1,63 +1,120 @@
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.RouteEngine
import com.kouros.navigation.data.route.Intersection
import com.kouros.navigation.data.route.Lane
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.createCenterLocation
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
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) {
fun mapToRoute(routeJson: TomTomResponse, builder: Route.Builder) {
val routes = mutableListOf<com.kouros.navigation.data.route.Routes>()
routeJson.routes.forEach { route ->
val legs = mutableListOf<Leg>()
val waypoints = mutableListOf<List<Double>>()
val legs = mutableListOf<Leg>()
var stepIndex = 0
var points = listOf<List<Double>>()
val summary = Summary(
route.summary.travelTimeInSeconds.toDouble(),
route.summary.lengthInMeters.toDouble() / 1000
route.summary.lengthInMeters.toDouble()
)
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
// )
// )
var stepDistance = 0.0
var stepDuration = 0.0
val allIntersections = mutableListOf<Intersection>()
val steps = mutableListOf<Step>()
for (index in 0..< route.guidance.instructions.size) {
val instruction = route.guidance.instructions[index]
val nextPointIndex = nextPointIndex(index, route)
val maneuver = RouteManeuver(
bearingBefore = 0,
bearingAfter = 0,
type = convertType(instruction.maneuver),
waypoints = points.subList(
instruction.pointIndex,
route.guidance.instructions[nextPointIndex].pointIndex
),
exit = exitNumber(instruction),
location = location(
instruction.point.longitude, instruction.point.latitude
),
)
val intersections = mutableListOf<Intersection>()
route.sections.forEach { section ->
val lanes = mutableListOf<Lane>()
var startIndex = 0
if (section.startPointIndex <= instruction.pointIndex - 3
&& instruction.pointIndex <= section.endPointIndex
) {
section.lanes?.forEach { itLane ->
val lane = Lane(
location(
waypoints[section.startPointIndex][0],
waypoints[section.startPointIndex][1]
),
itLane.directions.first() == itLane.follow,
itLane.directions
)
startIndex = section.startPointIndex
lanes.add(lane)
}
intersections.add(Intersection(waypoints[startIndex], lanes))
}
}
allIntersections.addAll(intersections)
stepDistance = route.guidance.instructions[nextPointIndex].routeOffsetInMeters - stepDistance
stepDuration = route.guidance.instructions[nextPointIndex].travelTimeInSeconds - stepDuration
val name = instruction.street
val step = Step(
index = stepIndex,
name = name,
distance = stepDistance,
duration = stepDuration,
maneuver = maneuver,
intersection = intersections
)
stepDistance = route.guidance.instructions[nextPointIndex].routeOffsetInMeters.toDouble()
stepDuration = route.guidance.instructions[nextPointIndex].travelTimeInSeconds.toDouble()
steps.add(step)
stepIndex += 1
}
route.sections.forEach { section ->
}
legs.add(Leg(steps, allIntersections))
val routeGeoJson = createLineStringCollection(waypoints)
val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
val newRoute = com.kouros.navigation.data.route.Routes(
legs,
summary,
routeGeoJson,
centerLocation = centerLocation,
waypoints = waypoints
)
routes.add(newRoute)
}
println(routeJson)
builder
.routeType(RouteEngine.TOMTOM.ordinal)
.routes(routes)
}
private fun nextPointIndex(index: Int, route: com.kouros.navigation.data.tomtom.Route): Int {
val nextPointIndex = if (index < route.guidance.instructions.size - 1) {
index + 1
} else {
index + 0
}
return nextPointIndex
}
fun convertType(type: String): Int {
@@ -66,7 +123,73 @@ class TomTomRoute {
"DEPART" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART
}
"ARRIVE" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION
}
"ARRIVE_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_LEFT
}
"ARRIVE_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_RIGHT
}
"STRAIGHT", "FOLLOW" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
}
"KEEP_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_KEEP_RIGHT
}
"BEAR_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
}
"BEAR_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
}
"KEEP_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_KEEP_LEFT
}
"TURN_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
}
"TURN_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
}
"SHARP_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SHARP_LEFT
}
"SHARP_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SHARP_RIGHT
}
"ROUNDABOUT_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CCW
}
"ROUNDABOUT_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CW
}
}
return newType
}
}
private fun exitNumber(
instruction: Instruction
): Int {
return if ( instruction.exitNumber == null
|| instruction.exitNumber.isEmpty()) {
0
} else {
instruction.exitNumber.toInt()
}
}

View File

@@ -3,6 +3,7 @@ package com.kouros.navigation.data.valhalla
import androidx.car.app.navigation.model.Maneuver
import com.kouros.data.R
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
import com.kouros.navigation.data.route.Step
@@ -13,7 +14,7 @@ import com.kouros.navigation.utils.location
class ValhallaRoute {
fun mapJsonToValhalla(routeJson: ValhallaResponse, builder: Route.Builder) {
fun mapToRoute(routeJson: ValhallaResponse, builder: Route.Builder) {
val waypoints = decodePolyline(routeJson.legs[0].shape)
val summary = Summary(routeJson.summaryValhalla.time, routeJson.summaryValhalla.length)
val steps = mutableListOf<Step>()
@@ -38,12 +39,9 @@ class ValhallaRoute {
stepIndex += 1
}
builder
.routeType(1)
.routeType(RouteEngine.VALHALLA.ordinal)
// TODO
.routes(emptyList())
//.summary(summary)
//.routeGeoJson(createLineStringCollection(waypoints))
//.waypoints(waypoints)
}
fun convertType(maneuver: Maneuvers): Int {

View File

@@ -6,6 +6,10 @@ import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Matrix
import android.location.Location
import androidx.annotation.DrawableRes
import androidx.car.app.CarContext
import androidx.car.app.model.CarIcon
import androidx.car.app.navigation.model.LaneDirection
import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Step
import androidx.core.graphics.createBitmap
@@ -15,6 +19,7 @@ import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.route.Intersection
import com.kouros.navigation.data.route.Lane
@@ -30,6 +35,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.Collections
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
@@ -45,9 +51,10 @@ open class RouteModel() {
var lastSpeedLocation: Location = location(0.0, 0.0)
var lastSpeedIndex: Int = 0
var maxSpeed: Int = 0
var location: Location = location(0.0, 0.0)
var lastLocation: Location = location(0.0, 0.0)
var bearing: Float = 0F
var routeBearing: Float = 0F
var currentRouteIndex = 0
val curRoute: Routes
@@ -62,7 +69,13 @@ open class RouteModel() {
.routeEngine(routeEngine)
.route(routeString)
.build()
navigating = true
if (hasLegs()) {
navigating = true
}
}
private fun hasLegs(): Boolean {
return route.routes.isNotEmpty() && route.routes[0].legs.isNotEmpty()
}
fun stopNavigation() {
@@ -76,24 +89,24 @@ open class RouteModel() {
@OptIn(DelicateCoroutinesApi::class)
fun updateLocation(curLocation: Location, viewModel: ViewModel) {
location = curLocation
findStep(location)
updateSpeedLimit(location, viewModel)
findStep(curLocation)
updateSpeedLimit(curLocation, viewModel)
lastLocation = location
}
private fun findStep(location: Location) {
var nearestDistance = 100000.0f
var nearestDistance = 100000f
for ((index, step) in curLeg.steps.withIndex()) {
if (index >= route.currentStep) {
if (index >= route.currentStepIndex) {
for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) {
if (wayIndex >= step.waypointIndex) {
val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
if (distance < nearestDistance) {
nearestDistance = distance
route.currentStep = step.index
route.currentStepIndex = step.index
step.waypointIndex = wayIndex
step.wayPointLocation = location(waypoint[0], waypoint[1])
lastLocation = location
bearing = lastLocation.bearingTo(location)
routeBearing = lastLocation.bearingTo(location)
}
}
if (nearestDistance == 0F) {
@@ -108,6 +121,21 @@ open class RouteModel() {
}
private fun currentLanes(location: Location): List<Lane> {
var lanes = emptyList<Lane>()
if (route.legs().isNotEmpty()) {
route.legs().first().intersection.forEach { it ->
if (it.lane.isNotEmpty()) {
val distance = lastLocation.distanceTo(location(it.location[0], it.location[1]))
val sectionBearing =
lastLocation.bearingTo(location(it.location[0], it.location[1]))
if (distance < 500 && (routeBearing.absoluteValue - sectionBearing.absoluteValue).absoluteValue < 10) {
lanes = it.lane
}
}
}
}
return lanes
var inter = Intersection()
var nearestDistance = 100000.0f
route.currentStep().intersection.forEach {
@@ -117,7 +145,9 @@ open class RouteModel() {
if (distance < nearestDistance) {
nearestDistance = distance
if (distance <= NEXT_STEP_THRESHOLD * 3) {
if ((interBearing.absoluteValue - route.currentStep().maneuver.bearingAfter.absoluteValue).absoluteValue < 20) {
if (route.routeEngine == RouteEngine.TOMTOM.ordinal
|| (interBearing.absoluteValue - route.currentStep().maneuver.bearingAfter.absoluteValue).absoluteValue < 20
) {
inter = it
}
}
@@ -129,22 +159,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
val elements = viewModel.getMaxSpeed(location)
elements.forEach {
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
if (isNavigating()) {
val instruction = currentStep().instruction
val levenshtein = Levenshtein()
// speed limit
val distance = lastSpeedLocation.distanceTo(location)
if (distance > 500 || lastSpeedIndex < route.currentStepIndex) {
lastSpeedIndex = route.currentStepIndex
val elements = viewModel.getMaxSpeed(location)
elements.forEach {
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
}
}
}
}
@@ -154,32 +186,13 @@ open class RouteModel() {
}
fun currentStep(): StepData {
val currentStep = route.currentStep()
// Determine if we should display the current or the next maneuver
val distanceToNextStep = leftStepDistance()
val isNearNextManeuver = distanceToNextStep in 0.0..NEXT_STEP_THRESHOLD
val shouldAdvance =
isNearNextManeuver && route.currentStep < (route.legs().first().steps.size)
// Determine the maneuver type and corresponding icon
var curManeuverType = if (hasArrived(currentStep.maneuver.type)) {
currentStep.maneuver.type
} else {
Maneuver.TYPE_STRAIGHT
}
// Get the single, correct maneuver for this state
val relevantStep = if (shouldAdvance) {
route.nextStep() // This advances the route's state
} else {
route.currentStep()
}
val currentStep = route.nextStep(1) // This advances the route's state
// 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 streetName = currentStep.name
val curManeuverType = currentStep.maneuver.type
val exitNumber = currentStep.maneuver.exit
val maneuverIcon = maneuverIcon(curManeuverType)
maneuverType = curManeuverType
@@ -198,9 +211,8 @@ open class RouteModel() {
)
}
fun nextStep(): StepData {
val step = route.nextStep()
val step = route.nextStep(2)
val maneuverType = step.maneuver.type
val distanceLeft = leftStepDistance()
var text = ""
@@ -226,14 +238,13 @@ open class RouteModel() {
travelLeftDistance(),
listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())),
step.maneuver.exit
)
}
fun travelLeftTime(): Double {
var timeLeft = 0.0
// time for next step until end step
for (i in route.currentStep + 1..<curLeg.steps.size) {
for (i in route.currentStepIndex + 1..<curLeg.steps.size) {
val step = curLeg.steps[i]
timeLeft += step.duration
}
@@ -270,14 +281,14 @@ open class RouteModel() {
return (leftDistance / 10.0).roundToInt() * 10.0
}
/** Returns the left distance in km. */
/** Returns the left distance in m. */
fun travelLeftDistance(): Double {
var leftDistance = 0.0
for (i in route.currentStep + 1..<curLeg.steps.size) {
for (i in route.currentStepIndex + 1..<curLeg.steps.size) {
val step = route.legs()[0].steps[i]
leftDistance += step.distance
}
leftDistance += leftStepDistance() / 1000
leftDistance += leftStepDistance()
return leftDistance
}
@@ -343,6 +354,100 @@ open class RouteModel() {
|| type == ManeuverType.DestinationLeft.value
}
fun addLanes(stepData: StepData) {
stepData.lane.forEach {
if (it.indications.isNotEmpty() && it.valid) {
Collections.sort<String>(it.indications)
var direction = ""
it.indications.forEach { it2 ->
direction = if (direction.isEmpty()) {
it2.trim()
} else {
"${direction}_${it2.trim()}"
}
}
val laneDirection = addLanes(direction, stepData)
println(laneDirection)
// TODO:
}
}
}
fun addLanes(direction: String, stepData: StepData): Int {
val laneDirection = when (direction.lowercase(Locale.getDefault())) {
"left_straight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"left", "slight_left" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"straight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"right" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"right_straight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"left_slight", "slight_left" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_SLIGHT_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"right_slight", "slight_right" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
else -> {
LaneDirection.SHAPE_UNKNOWN
}
}
return laneDirection
}
fun createCarIcon(iconCompat: IconCompat): CarIcon {
return CarIcon.Builder(iconCompat).build()
}
fun createCarIconx(carContext: Context, @DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
}
fun createLaneIcon(context: Context, stepData: StepData): IconCompat {
val bitmaps = mutableListOf<Bitmap>()
@@ -396,6 +501,7 @@ open class RouteModel() {
"${direction}_${it.trim()}"
}
}
direction = direction.lowercase()
return when (direction) {
"left_straight" -> {
when (stepData.currentManeuverType) {
@@ -419,8 +525,8 @@ open class RouteModel() {
"right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_RIGHT) "${direction}_o" else "${direction}_x"
"left" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_LEFT) "${direction}_o" else "${direction}_x"
"straight" -> if (stepData.currentManeuverType == Maneuver.TYPE_STRAIGHT) "${direction}_o" else "${direction}_x"
"right_slight" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_RIGHT) "${direction}_o" else "${direction}_x"
"left_slight" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_LEFT) "${direction}_o" else "${direction}_x"
"right_slight", "slight_right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_RIGHT) "${direction}_o" else "${direction}_x"
"left_slight", "slight_left" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_LEFT) "${direction}_o" else "${direction}_x"
else -> {
""
}
@@ -438,6 +544,7 @@ open class RouteModel() {
"right_o" -> R.drawable.right_o
"slight_right_x" -> R.drawable.slight_right_x
"slight_right_o" -> R.drawable.slight_right_o
"slight_left_x" -> R.drawable.left_x
"straight_x" -> R.drawable.straight_x
"right_o_straight_x" -> R.drawable.right_o_straight_x
"right_x_straight_x" -> R.drawable.right_x_straight_x
@@ -446,7 +553,7 @@ open class RouteModel() {
"left_o_straight_x" -> R.drawable.left_o_straight_x
"left_x_straight_o" -> R.drawable.left_x_straight_o
else -> {
R.drawable.ic_close_white_24dp
R.drawable.left_x
}
}
}

View File

@@ -280,18 +280,20 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
fun searchPlaces(search: String, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
val placesJson = repository.searchPlaces(search, location)
val gson = GsonBuilder().serializeNulls().create()
val places = gson.fromJson(placesJson, Search::class.java)
val distPlaces = mutableListOf<SearchResult>()
places.forEach {
val plLocation =
location(longitude = it.lon.toDouble(), latitude = it.lat.toDouble())
val distance = plLocation.distanceTo(location)
it.distance = distance
distPlaces.add(it)
if (placesJson.isNotEmpty()) {
val gson = GsonBuilder().serializeNulls().create()
val places = gson.fromJson(placesJson, Search::class.java)
val distPlaces = mutableListOf<SearchResult>()
places.forEach {
val plLocation =
location(longitude = it.lon.toDouble(), latitude = it.lat.toDouble())
val distance = plLocation.distanceTo(location)
it.distance = distance
distPlaces.add(it)
}
val sortedList = distPlaces.sortedWith(compareBy { it.distance })
searchPlaces.postValue(sortedList)
}
val sortedList = distPlaces.sortedWith(compareBy { it.distance })
searchPlaces.postValue(sortedList)
}
}

View File

@@ -90,6 +90,7 @@ object GeoUtils {
}
fun createLineStringCollection(lineCoordinates: List<List<Double>>): String {
// return createPointCollection(lineCoordinates, "Route")
val lineString = buildLineString {
lineCoordinates.forEach {
add(org.maplibre.spatialk.geojson.Point(

View File

@@ -0,0 +1,11 @@
<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"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"/>
</vector>

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="M120,720L120,640L840,640L840,720L120,720ZM120,520L120,440L840,440L840,520L120,520ZM120,320L120,240L840,240L840,320L120,320Z"/>
</vector>

View File

@@ -24,7 +24,7 @@
},
{
"key": "departAt",
"value": "2026-01-29T08:43:35.397Z"
"value": "2026-02-06T09:09:59.054Z"
},
{
"key": "guidanceVersion",
@@ -44,7 +44,7 @@
},
{
"key": "locations",
"value": "48.18565,11.57928:48.11830,11.59485"
"value": "48.18575,11.57939:48.11654,11.59449"
},
{
"key": "maxAlternatives",
@@ -60,11 +60,11 @@
},
{
"key": "sectionType",
"value": "lanes"
"value": "traffic"
},
{
"key": "sectionType",
"value": "traffic"
"value": "lanes"
},
{
"key": "traffic",
@@ -119,46 +119,28 @@
"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"
"lengthInMeters": 11116,
"travelTimeInSeconds": 1148,
"trafficDelayInSeconds": 0,
"trafficLengthInMeters": 0,
"departureTime": "2026-02-06T10:09:59+01:00",
"arrivalTime": "2026-02-06T10:29:07+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"
"lengthInMeters": 11116,
"travelTimeInSeconds": 1148,
"trafficDelayInSeconds": 0,
"trafficLengthInMeters": 0,
"departureTime": "2026-02-06T10:09:59+01:00",
"arrivalTime": "2026-02-06T10:29:07+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@",
"encodedPolyline": "sfbeHarteAE~DQEy@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~@l@jDj@Qv@IrEP",
"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": [
{
@@ -349,9 +331,8 @@
"travelTimeInSeconds": 0,
"point": {
"latitude": 48.18554,
"longitude": 11.57927
"longitude": 11.57937
},
"exitNumber": "",
"pointIndex": 0,
"instructionType": "LOCATION_DEPARTURE",
"street": "Vogelhartstraße",
@@ -362,8 +343,8 @@
"message": "Leave from Vogelhartstraße"
},
{
"routeOffsetInMeters": 64,
"travelTimeInSeconds": 14,
"routeOffsetInMeters": 72,
"travelTimeInSeconds": 16,
"point": {
"latitude": 48.18557,
"longitude": 11.57841
@@ -380,8 +361,8 @@
"message": "Turn right onto Silcherstraße"
},
{
"routeOffsetInMeters": 218,
"travelTimeInSeconds": 57,
"routeOffsetInMeters": 226,
"travelTimeInSeconds": 60,
"point": {
"latitude": 48.18696,
"longitude": 11.57857
@@ -398,8 +379,8 @@
"message": "Turn right onto Schmalkaldener Straße"
},
{
"routeOffsetInMeters": 650,
"travelTimeInSeconds": 131,
"routeOffsetInMeters": 658,
"travelTimeInSeconds": 134,
"point": {
"latitude": 48.18686,
"longitude": 11.58437
@@ -419,8 +400,8 @@
"message": "Turn right onto Ingolstädter Straße/B13"
},
{
"routeOffsetInMeters": 1713,
"travelTimeInSeconds": 266,
"routeOffsetInMeters": 1720,
"travelTimeInSeconds": 267,
"point": {
"latitude": 48.17733,
"longitude": 11.58503
@@ -441,8 +422,8 @@
"combinedMessage": "Turn left onto Schenkendorfstraße/B2R then keep left at Schenkendorfstraße/B2R toward Messe / ICM"
},
{
"routeOffsetInMeters": 2067,
"travelTimeInSeconds": 309,
"routeOffsetInMeters": 2075,
"travelTimeInSeconds": 307,
"point": {
"latitude": 48.17678,
"longitude": 11.58957
@@ -464,8 +445,8 @@
"combinedMessage": "Keep left at Schenkendorfstraße/B2R toward Messe / ICM then keep left at Schenkendorfstraße/B2R toward Passau"
},
{
"routeOffsetInMeters": 2419,
"travelTimeInSeconds": 332,
"routeOffsetInMeters": 2426,
"travelTimeInSeconds": 329,
"point": {
"latitude": 48.17518,
"longitude": 11.59363
@@ -486,8 +467,8 @@
"message": "Keep left at Schenkendorfstraße/B2R toward Passau"
},
{
"routeOffsetInMeters": 2774,
"travelTimeInSeconds": 357,
"routeOffsetInMeters": 2781,
"travelTimeInSeconds": 353,
"point": {
"latitude": 48.17329,
"longitude": 11.59747
@@ -506,8 +487,8 @@
"message": "Follow Isarring/B2R toward München-Ost"
},
{
"routeOffsetInMeters": 8425,
"travelTimeInSeconds": 806,
"routeOffsetInMeters": 8433,
"travelTimeInSeconds": 734,
"point": {
"latitude": 48.13017,
"longitude": 11.61541
@@ -524,8 +505,8 @@
"message": "Bear right at Ampfingstraße"
},
{
"routeOffsetInMeters": 9487,
"travelTimeInSeconds": 953,
"routeOffsetInMeters": 9495,
"travelTimeInSeconds": 884,
"point": {
"latitude": 48.12089,
"longitude": 11.61285
@@ -543,8 +524,8 @@
"combinedMessage": "Turn right onto Anzinger Straße then keep straight on at Sankt-Martin-Straße"
},
{
"routeOffsetInMeters": 9983,
"travelTimeInSeconds": 1044,
"routeOffsetInMeters": 9991,
"travelTimeInSeconds": 974,
"point": {
"latitude": 48.12087,
"longitude": 11.60621
@@ -561,20 +542,39 @@
"message": "Keep straight on at Sankt-Martin-Straße"
},
{
"routeOffsetInMeters": 10879,
"travelTimeInSeconds": 1170,
"routeOffsetInMeters": 10941,
"travelTimeInSeconds": 1103,
"point": {
"latitude": 48.1183,
"longitude": 11.59485
"latitude": 48.11811,
"longitude": 11.59417
},
"pointIndex": 335,
"instructionType": "TURN",
"street": "Hohenwaldeckstraße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": -90,
"possibleCombineWithNext": true,
"drivingSide": "RIGHT",
"maneuver": "TURN_LEFT",
"message": "Turn left onto Hohenwaldeckstraße",
"combinedMessage": "Turn left onto Hohenwaldeckstraße then you have arrived at Hohenwaldeckstraße. Your destination is on the left"
},
{
"routeOffsetInMeters": 11116,
"travelTimeInSeconds": 1148,
"point": {
"latitude": 48.11655,
"longitude": 11.59422
},
"pointIndex": 338,
"instructionType": "LOCATION_ARRIVAL",
"street": "Sankt-Martin-Straße",
"street": "Hohenwaldeckstraße",
"countryCode": "DEU",
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "ARRIVE",
"message": "You have arrived at Sankt-Martin-Straße"
"maneuver": "ARRIVE_LEFT",
"message": "You have arrived at Hohenwaldeckstraße. Your destination is on the left"
}
],
"instructionGroups": [
@@ -582,19 +582,25 @@
"firstInstructionIndex": 0,
"lastInstructionIndex": 3,
"groupMessage": "Leave from Vogelhartstraße. Take the Ingolstädter Straße/B13",
"groupLengthInMeters": 1713
"groupLengthInMeters": 1720
},
{
"firstInstructionIndex": 4,
"lastInstructionIndex": 7,
"groupMessage": "Take the Schenkendorfstraße, Isarring/B2R toward Messe / ICM, Passau, München-Ost",
"groupLengthInMeters": 6712
"groupLengthInMeters": 6713
},
{
"firstInstructionIndex": 8,
"lastInstructionIndex": 11,
"groupMessage": "Take the Ampfingstraße, Anzinger Straße. Continue to your destination at Sankt-Martin-Straße",
"groupLengthInMeters": 2454
"lastInstructionIndex": 10,
"groupMessage": "Take the Ampfingstraße, Anzinger Straße, Sankt-Martin-Straße",
"groupLengthInMeters": 2508
},
{
"firstInstructionIndex": 11,
"lastInstructionIndex": 12,
"groupMessage": "Get to your destination at Hohenwaldeckstraße",
"groupLengthInMeters": 175
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@
<string name="reject_action_title" msgid="6730366705938402668">"Ablehnen"</string>
<string name="ok_action_title" msgid="7128494973966098611">"OK"</string>
<string name="favorites" msgid="522064494016370117">"Favoriten"</string>
<string name="display_settings" msgid="5726880972110281095">"Einstellungen für die Anzeige"</string>
<string name="display_settings" msgid="5726880972110281095">"Anzeige"</string>
<string name="arrived_exclamation_msg" msgid="9132265698563096988">"Angekommen!"</string>
<string name="category_title" msgid="4783851267093259949">"Kategorien"</string>
<string name="no_places" msgid="7246005909846715898">"Keine Orte"</string>
@@ -42,12 +42,13 @@
<string name="contacts">Kontakte</string>
<string name="route_preview">Route Vorschau</string>
<string name="display">Anzeige</string>
<string name="navigation_settings">Navigations Einstellungen</string>
<string name="navigation_settings">Navigation</string>
<string name="fuel_station">Tankstelle</string>
<string name="pharmacy">Apotheke</string>
<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>
<string name="options">Optionen</string>
</resources>

View File

@@ -10,7 +10,7 @@
<string name="off_action_title">Off</string>
<string name="use_telephon_settings">Use telephon settings</string>
<string name="dark_mode">Dark mode</string>
<string name="display_settings">Display settings</string>
<string name="display_settings">Display</string>
<string name="threed_building">3D building</string>
<string name="arrived_exclamation_msg">Arrived!</string>
<string name="drive_now">Drive now</string>
@@ -24,7 +24,7 @@
<string name="recent_Item_deleted">Recent item deleted</string>
<string name="route_preview">Route preview</string>
<string name="display">Display</string>
<string name="navigation_settings">Navigation settings</string>
<string name="navigation_settings">Navigation</string>
<string name="settings_action_title">Settings</string>
<string name="accept_action_title">Accept</string>
<string name="reject_action_title">Reject</string>
@@ -35,4 +35,5 @@
<string name="routing_engine" translatable="false">Routing engine</string>
<string name="use_car_location">Use car location</string>
<string name="tomtom">TomTom\t</string>
<string name="options">Options</string>
</resources>