TomTom Routing

This commit is contained in:
Dimitris
2026-02-10 10:50:42 +01:00
parent 5141041b5c
commit 65ff41995d
26 changed files with 182 additions and 201 deletions

View File

@@ -251,11 +251,11 @@ class NavigationSession : Session(), NavigationScreen.Listener {
fun updateLocation(location: Location) {
if (routeModel.isNavigating()) {
navigationScreen.updateTrip(location)
if (!routeModel.arrived) {
if (!routeModel.navState.arrived) {
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snapedLocation)
if (distance > MAXIMAL_ROUTE_DEVIATION) {
navigationScreen.calculateNewRoute(routeModel.destination)
navigationScreen.calculateNewRoute(routeModel.navState.destination)
return
}
if (distance < MAXIMAL_SNAP_CORRECTION) {

View File

@@ -470,7 +470,7 @@ fun DebugInfo(
contentAlignment = Alignment.CenterStart
) {
val textMeasurerLocation = rememberTextMeasurer()
val location = routeModel.currentLocation.latitude.toString()
val location = routeModel.navState.currentLocation.latitude.toString()
val styleSpeed = TextStyle(
fontSize = 26.sp,
fontWeight = FontWeight.Bold,

View File

@@ -68,8 +68,8 @@ class RouteCarModel() : RouteModel() {
.setManeuver(
maneuver.build()
)
if (destination.street != null) {
step.setRoad(destination.street!!)
if (navState.destination.street != null) {
step.setRoad(navState.destination.street!!)
}
if (stepData.lane.isNotEmpty()) {
addLanes(carContext, step, stepData)
@@ -127,9 +127,9 @@ class RouteCarModel() : RouteModel() {
.setRemainingTimeColor(CarColor.GREEN)
.setRemainingDistanceColor(CarColor.BLUE)
if (travelMessage.isNotEmpty()) {
if (navState.travelMessage.isNotEmpty()) {
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px))
travelBuilder.setTripText(CarText.create(travelMessage))
travelBuilder.setTripText(CarText.create(navState.travelMessage))
}
return travelBuilder.build()
}
@@ -147,10 +147,10 @@ class RouteCarModel() : RouteModel() {
"${direction}_${it2.trim()}"
}
}
val laneDirection = iconMapper.addLanes(direction, stepData)
val laneDirection = navState.iconMapper.addLanes(direction, stepData)
if (laneDirection != LaneDirection.SHAPE_UNKNOWN) {
if (!laneImageAdded) {
step.setLanesImage(createCarIcon(iconMapper.createLaneIcon(carContext, stepData)))
step.setLanesImage(createCarIcon(navState.iconMapper.createLaneIcon(carContext, stepData)))
laneImageAdded = true
}
val laneType =

View File

@@ -44,10 +44,10 @@ class CategoryScreen(
val loc = location(0.0, 0.0)
elements.forEach {
if (loc.latitude == 0.0) {
loc.longitude = it.lon!!
loc.latitude = it.lat!!
loc.longitude = it.lon
loc.latitude = it.lat
}
coordinates.add(listOf(it.lon!!, it.lat!!))
coordinates.add(listOf(it.lon, it.lat))
}
if (elements.isNotEmpty()) {
val route = createPointCollection(coordinates, category)
@@ -111,7 +111,7 @@ class CategoryScreen(
}
val row = Row.Builder()
.setOnClickListener {
val location = location(it.lon!!, it.lat!!)
val location = location(it.lon, it.lat)
surfaceRenderer.setCategoryLocation(location, category)
}
.setTitle(name)

View File

@@ -31,16 +31,12 @@ import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.tomtom.Features
import com.kouros.navigation.data.tomtom.Geometry
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.location
import java.time.LocalDateTime
import java.time.Period
import java.time.ZoneOffset
import kotlin.math.absoluteValue
import kotlin.time.Duration
class NavigationScreen(
carContext: CarContext,
@@ -154,11 +150,11 @@ class NavigationScreen(
}
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
if (routeModel.arrived) {
if (routeModel.navState.arrived) {
val timer = object : CountDownTimer(8000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
routeModel.arrived = false
routeModel.navState = routeModel.navState.copy(arrived = false)
navigationType = NavigationType.VIEW
invalidate()
}
@@ -177,8 +173,8 @@ class NavigationScreen(
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
var street = ""
if (routeModel.destination.street != null) {
street = routeModel.destination.street!!
if (routeModel.navState.destination.street != null) {
street = routeModel.navState.destination.street!!
}
return NavigationTemplate.Builder()
.setNavigationInfo(
@@ -316,7 +312,7 @@ class NavigationScreen(
navigateTo,
surfaceRenderer.carOrientation
)
routeModel.destination = recentPlace
routeModel.navState = routeModel.navState.copy(destination = recentPlace)
}
.build()
}
@@ -444,7 +440,7 @@ class NavigationScreen(
location,
surfaceRenderer.carOrientation
)
routeModel.destination = place
routeModel.navState = routeModel.navState.copy(destination = place)
invalidate()
}
@@ -492,14 +488,14 @@ class NavigationScreen(
updateSpeedCamera(location)
with(routeModel) {
updateLocation(location, viewModel)
if ((maneuverType == Maneuver.TYPE_DESTINATION
|| maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|| maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|| maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT)
if ((navState.maneuverType == Maneuver.TYPE_DESTINATION
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT)
&& routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
stopNavigation()
arrived = true
navState = navState.copy(arrived = true)
surfaceRenderer.routeData.value = ""
navigationType = NavigationType.ARRIVAL
invalidate()

View File

@@ -124,7 +124,7 @@ class PlaceListScreen(
setSpan(
DistanceSpan.create(
Distance.create(
it.distance.toDouble(),
(it.distance/1000).toDouble(),
Distance.UNIT_KILOMETERS
)
), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE

View File

@@ -220,7 +220,7 @@ class RoutePreviewScreen(
}
private fun onRouteSelected(index: Int) {
routeModel.currentRouteIndex = index
routeModel.navState = routeModel.navState.copy(currentRouteIndex = index)
surfaceRenderer.setPreviewRouteData(routeModel)
//setResult(destination)
//finish()

View File

@@ -147,6 +147,10 @@ object Constants {
const val DESTINATION_ARRIVAL_DISTANCE = 40.0
const val NEAREST_LOCATION_DISTANCE = 10F
const val MAXIMUM_LOCATION_DISTANCE = 100000F
}

View File

@@ -50,11 +50,10 @@ 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
return 0.0
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 {

View File

@@ -26,7 +26,6 @@ data class Route(
) {
data class Builder(
var routeEngine: Int = 0,
var summary: Summary = Summary(),
var routes: List<com.kouros.navigation.data.route.Routes> = emptyList(),
@@ -76,8 +75,6 @@ data class Route(
return Route(
routeEngine = 0,
routes = emptyList(),
//waypoints = emptyList(),
//routeGeoJson = "",
)
}
}

View File

@@ -58,7 +58,7 @@ class OsrmRoute {
}
val step = Step(
index = stepIndex,
name = step.name,
street = step.name,
distance = step.distance / 1000,
duration = step.duration,
maneuver = maneuver,

View File

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

View File

@@ -10,6 +10,6 @@ data class Step(
val maneuver: Maneuver,
val duration: Double = 0.0,
val distance: Double = 0.0,
val name : String = "",
val street : String = "",
val intersection: List<Intersection> = mutableListOf(),
)

View File

@@ -14,7 +14,7 @@ data class Instruction(
val roadNumbers: List<String>,
val routeOffsetInMeters: Int,
val signpostText: String,
val street: String = "",
val street: String? = "",
val travelTimeInSeconds: Int,
val turnAngleInDecimalDegrees: Int,
val exitNumber: String? = "0",

View File

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

View File

@@ -18,6 +18,8 @@ val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incidentDetail
private val tomtomFields =
"{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}"
const val useAsset = false
class TomTomRepository : NavigationRepository() {
override fun getRoute(
context: Context,
@@ -26,9 +28,11 @@ class TomTomRepository : NavigationRepository() {
carOrientation: Float,
searchFilter: SearchFilter
): String {
val routeJson = context.resources.openRawResource(R.raw.tomom_routing)
val routeJsonString = routeJson.bufferedReader().use { it.readText() }
return routeJsonString
if (useAsset) {
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" +
@@ -44,7 +48,6 @@ class TomTomRepository : NavigationRepository() {
}
override fun getTraffic(context: Context, location: Location, carOrientation: Float): String {
val useAsset = true
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
return if (useAsset) {
val trafficJson = context.resources.openRawResource(R.raw.tomtom_traffic)

View File

@@ -37,24 +37,28 @@ class TomTomRoute {
val steps = mutableListOf<Step>()
var lastPointIndex = 0
for (index in 1..< route.guidance.instructions.size) {
val lastInstruction = route.guidance.instructions[index-1]
val instruction = route.guidance.instructions[index]
val street = lastInstruction.street ?: ""
val maneuverStreet = instruction.street ?: ""
val maneuver = RouteManeuver(
bearingBefore = 0,
bearingAfter = 0,
type = convertType(instruction.maneuver),
waypoints = points.subList(
lastPointIndex,
instruction.pointIndex,
instruction.pointIndex+1,
),
exit = exitNumber(instruction),
location = location(
instruction.point.longitude, instruction.point.latitude
),
street = maneuverStreet
)
lastPointIndex = instruction.pointIndex
val intersections = mutableListOf<Intersection>()
route.sections.forEach { section ->
route.sections?.forEach { section ->
val lanes = mutableListOf<Lane>()
var startIndex = 0
if (section.startPointIndex <= instruction.pointIndex - 3
@@ -78,10 +82,9 @@ class TomTomRoute {
allIntersections.addAll(intersections)
stepDistance = route.guidance.instructions[index].routeOffsetInMeters - stepDistance
stepDuration = route.guidance.instructions[index].travelTimeInSeconds - stepDuration
val name = instruction.street
val step = Step(
index = stepIndex,
name = name,
street = street,
distance = stepDistance,
duration = stepDuration,
maneuver = maneuver,

View File

@@ -34,7 +34,7 @@ class ValhallaRoute {
if (it.streetNames != null && it.streetNames.isNotEmpty()) {
name = it.streetNames[0]
}
val step = Step( index = stepIndex, name = name, distance = it.length, duration = it.time, maneuver = maneuver)
val step = Step( index = stepIndex, street = name, distance = it.length, duration = it.time, maneuver = maneuver)
steps.add(step)
stepIndex += 1
}

View File

@@ -17,8 +17,7 @@ import java.util.Collections
import java.util.Locale
import kotlin.collections.forEach
class IconMapper(var routeModel: RouteModel) {
class IconMapper() {
fun maneuverIcon(routeManeuverType: Int): Int {
var currentTurnIcon = R.drawable.ic_turn_name_change
@@ -102,7 +101,7 @@ class IconMapper(var routeModel: RouteModel) {
}
}
"left", "slight_left" -> {
"left" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT
else

View File

@@ -2,6 +2,8 @@ package com.kouros.navigation.model
import android.location.Location
import androidx.car.app.navigation.model.Step
import com.kouros.navigation.data.Constants.MAXIMUM_LOCATION_DISTANCE
import com.kouros.navigation.data.Constants.NEAREST_LOCATION_DISTANCE
import com.kouros.navigation.utils.location
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
@@ -12,38 +14,31 @@ class RouteCalculator(var routeModel: RouteModel) {
var lastSpeedIndex: Int = 0
fun findStep(location: Location) {
var nearestDistance = 100000f
var nearestDistance = MAXIMUM_LOCATION_DISTANCE
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
if (index >= routeModel.route.currentStepIndex) {
if (index >= routeModel.navState.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
routeModel.route.currentStepIndex = step.index
routeModel.navState.route.currentStepIndex = step.index
step.waypointIndex = wayIndex
step.wayPointLocation = location(waypoint[0], waypoint[1])
routeModel.routeBearing = routeModel.lastLocation.bearingTo(location)
} else {
if (nearestDistance != 100000f) {
break;
}
routeModel.navState = routeModel.navState.copy(
routeBearing = routeModel.navState.lastLocation.bearingTo(location)
)
}
}
if (nearestDistance == 0F) {
break
}
}
}
if (nearestDistance == 0F) {
break
if (nearestDistance < NEAREST_LOCATION_DISTANCE) {
break;
}
}
}
fun travelLeftTime(): Double {
var timeLeft = 0.0
// time for next step until end step
@@ -102,10 +97,8 @@ class RouteCalculator(var routeModel: RouteModel) {
if (distance > 500 || lastSpeedIndex < routeModel.route.currentStepIndex) {
lastSpeedIndex = routeModel.route.currentStepIndex
lastSpeedLocation = location
viewModel.getMaxSpeed(location, routeModel.previousStreet())
viewModel.getMaxSpeed(location, routeModel.route.currentStep().street)
}
}
}
}

View File

@@ -13,75 +13,81 @@ import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.location
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlin.math.absoluteValue
open class RouteModel() {
open class RouteModel {
var route = Route.Builder().buildEmpty()
// Immutable Data Class
data class NavigationState(
val route: Route = Route.Builder().buildEmpty(),
val iconMapper : IconMapper = IconMapper(),
val navigating: Boolean = false,
val arrived: Boolean = false,
val travelMessage: String = "",
val maneuverType: Int = 0,
val lastLocation: Location = location(0.0, 0.0),
val currentLocation: Location = location(0.0, 0.0),
val routeBearing: Float = 0F,
val currentRouteIndex: Int = 0,
val destination: Place = Place()
)
val routeCalculator = RouteCalculator(this)
var navState = NavigationState()
var iconMapper = IconMapper(this)
var navigating: Boolean = false
var destination: Place = Place()
var arrived: Boolean = false
var maneuverType: Int = 0
var travelMessage: String = ""
val route: Route
get() = navState.route
var currentLocation: Location = location(0.0, 0.0)
val routeCalculator : RouteCalculator = RouteCalculator(this)
var lastLocation: Location = location(0.0, 0.0)
var routeBearing: Float = 0F
var currentRouteIndex = 0
val curRoute: Routes
get() = route.routes[currentRouteIndex]
get() = navState.route.routes[navState.currentRouteIndex]
val curLeg: Leg
get() = route.routes[currentRouteIndex].legs.first()
get() = navState.route.routes[navState.currentRouteIndex].legs.first()
fun startNavigation(routeString: String, context: Context) {
val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
route = Route.Builder()
.routeEngine(routeEngine)
.route(routeString)
.build()
navState = navState.copy(
route = Route.Builder()
.routeEngine(routeEngine)
.route(routeString)
.build()
)
if (hasLegs()) {
navigating = true
navState = navState.copy(navigating = true)
}
}
private fun hasLegs(): Boolean {
return route.routes.isNotEmpty() && route.routes[0].legs.isNotEmpty()
return navState.route.routes.isNotEmpty() && navState.route.routes[0].legs.isNotEmpty()
}
fun stopNavigation() {
route = Route.Builder().buildEmpty()
navigating = false
arrived = false
maneuverType = Maneuver.TYPE_UNKNOWN
navState = navState.copy(
route = Route.Builder().buildEmpty(),
navigating = false,
arrived = false,
maneuverType = Maneuver.TYPE_UNKNOWN
)
}
@OptIn(DelicateCoroutinesApi::class)
fun updateLocation(curLocation: Location, viewModel: ViewModel) {
currentLocation = curLocation
navState = navState.copy(currentLocation = curLocation)
routeCalculator.findStep(curLocation)
routeCalculator.updateSpeedLimit(curLocation, viewModel)
lastLocation = currentLocation
navState = navState.copy(lastLocation = navState.currentLocation)
}
private fun currentLanes(location: Location): List<Lane> {
private fun currentLanes(): List<Lane> {
var lanes = emptyList<Lane>()
if (route.legs().isNotEmpty()) {
route.legs().first().intersection.forEach { it ->
if (navState.route.legs().isNotEmpty()) {
navState.route.legs().first().intersection.forEach {
if (it.lane.isNotEmpty()) {
val distance = lastLocation.distanceTo(location(it.location[0], it.location[1]))
val distance =
navState.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) {
navState.lastLocation.bearingTo(location(it.location[0], it.location[1]))
if (distance < 500 && (navState.routeBearing.absoluteValue - sectionBearing.absoluteValue).absoluteValue < 10) {
lanes = it.lane
}
}
@@ -90,25 +96,24 @@ open class RouteModel() {
return lanes
}
fun currentStep(): StepData {
val distanceToNextStep = routeCalculator.leftStepDistance()
// Determine the maneuver type and corresponding icon
val currentStep = route.nextStep(0)
val currentStep = navState.route.nextStep(0)
// Safely get the street name from the maneuver
val streetName = currentStep.name
val streetName = currentStep.maneuver.street
val curManeuverType = currentStep.maneuver.type
val exitNumber = currentStep.maneuver.exit
val maneuverIcon = iconMapper.maneuverIcon(curManeuverType)
maneuverType = curManeuverType
val maneuverIcon = navState.iconMapper.maneuverIcon(curManeuverType)
navState = navState.copy(maneuverType = curManeuverType)
val lanes = currentLanes(currentLocation)
val lanes = currentLanes()
// Construct and return the final StepData object
return StepData(
streetName,
distanceToNextStep,
maneuverType,
navState.maneuverType,
maneuverIcon,
routeCalculator.arrivalTime(),
routeCalculator.travelLeftDistance(),
@@ -118,22 +123,21 @@ open class RouteModel() {
}
fun nextStep(): StepData {
val step = route.nextStep(1)
val step = navState.route.nextStep(1)
val maneuverType = step.maneuver.type
val distanceLeft = routeCalculator.leftStepDistance()
var text = ""
when (distanceLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> {
}
else -> {
if (step.name.isNotEmpty()) {
text = step.name
if (step.street.isNotEmpty()) {
text = step.street
}
}
}
val maneuverIcon = iconMapper.maneuverIcon(maneuverType)
val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType)
// Construct and return the final StepData object
return StepData(
text,
@@ -147,14 +151,7 @@ open class RouteModel() {
)
}
fun previousStreet(): String {
if (route.currentStepIndex > 0) {
return route.legs().first().steps[route.currentStepIndex - 1].name
}
return ""
}
fun isNavigating(): Boolean {
return navigating
return navState.navigating
}
}

View File

@@ -31,7 +31,6 @@ object GeoUtils {
return newLocation
}
fun decodePolyline(encoded: String, precision: Int = 6): List<List<Double>> {
val factor = 10.0.pow(precision)