This commit is contained in:
Dimitris
2025-12-24 07:24:12 +01:00
parent ddae6f2189
commit d0a07e1315
17 changed files with 234 additions and 206 deletions

View File

@@ -236,7 +236,7 @@ class MainActivity : ComponentActivity() {
if (isNavigating()) {
updateLocation(currentLocation, navigationViewModel)
stepData.value = currentStep()
if (route.currentManeuverIndex + 1 <= route.maneuvers.size) {
if (route.currentStep + 1 <= legs.steps.size) {
nextStepData.value = nextStep()
}
if (routeState.maneuverType == 39
@@ -302,20 +302,21 @@ class MainActivity : ComponentActivity() {
@OptIn(DelicateCoroutinesApi::class)
fun simulate() = GlobalScope.async {
for ((_, loc) in routeModel.route.waypoints.withIndex()) {
if (routeModel.isNavigating()) {
mock.setMockLocation(loc[1], loc[0])
delay(500L) //
for ((index, step) in routeModel.legs.steps.withIndex()) {
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
mock.setMockLocation(waypoint[1], waypoint[0])
delay(1000L) //
}
}
}
fun test() {
for ((index, loc) in routeModel.route.waypoints.withIndex()) {
if (index > 300) {
routeModel.updateLocation(location(loc[0], loc[1]), navigationViewModel)
for ((index, step) in routeModel.legs.steps.withIndex()) {
println("${step.maneuver.waypoints.size}")
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
routeModel.updateLocation(location(waypoint[0], waypoint[1]), navigationViewModel)
routeModel.currentStep()
if (routeModel.route.currentManeuverIndex + 1 <= routeModel.route.maneuvers.size) {
if (index + 1 <= routeModel.legs.steps.size) {
nextStepData.value = routeModel.nextStep()
}
println(routeModel.routeState.maneuverType)

View File

@@ -300,7 +300,7 @@ class SurfaceRenderer(
with(routeModel) {
routeData.value = route.routeGeoJson
centerLocation = route.centerLocation
previewDistance = route.distance
previewDistance = route.summary!!.distance
}
updateCameraPosition(
0.0,

View File

@@ -196,7 +196,7 @@ fun DrawNavigationImages(
@Composable
fun NavigationImage(padding: PaddingValues, width: Int, height: Int) {
val imageSize = (height / 6)
val imageSize = (height / 8)
val color = remember { NavigationColor }
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) {
Canvas(

View File

@@ -89,7 +89,7 @@ class RoutePreviewScreen(
)
.build()
val message = if (routeModel.isNavigating() && routeModel.route.waypoints.isNotEmpty()) {
val message = if (routeModel.isNavigating() && routeModel.route.waypoints!!.isNotEmpty()) {
createRouteText()
} else {
CarText.Builder("Wait")
@@ -173,8 +173,8 @@ class RoutePreviewScreen(
.build()
private fun createRouteText(): CarText {
val time = routeModel.route.summary.time
val length = BigDecimal(routeModel.route.distance).setScale(1, RoundingMode.HALF_EVEN)
val time = routeModel.route.summary!!.duration
val length = BigDecimal(routeModel.route.summary!!.distance).setScale(1, RoundingMode.HALF_EVEN)
val firstRoute = SpannableString(" \u00b7 $length km")
firstRoute.setSpan(
DurationSpan.create(time.toLong()), 0, 1, 0

View File

@@ -40,7 +40,7 @@ abstract class NavigationRepository {
val route = getRoute(currentLocation, location, searchFilter)
val routeModel = RouteModel()
routeModel.startNavigation(route)
return routeModel.route.distance
return routeModel.route.summary!!.distance
}
fun searchPlaces(search: String, location: Location) : String {

View File

@@ -2,125 +2,91 @@ package com.kouros.navigation.data
import android.location.Location
import com.google.gson.GsonBuilder
import com.kouros.navigation.data.valhalla.Maneuvers
import com.kouros.navigation.data.valhalla.Summary
import com.kouros.navigation.data.valhalla.Trip
import com.kouros.navigation.data.valhalla.ValhallaJson
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary
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.GeoUtils.createLineStringCollection
import com.kouros.navigation.utils.GeoUtils.decodePolyline
import com.kouros.navigation.utils.location
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject
import org.maplibre.geojson.Point
data class Route(
/**
* A Leg is a route between only two waypoints.
*
* @since 1.0.0
*/
val maneuvers: List<Maneuvers>,
/**
* The distance traveled from origin to destination.
*
* @return a double number with unit meters
* @since 1.0.0
*/
val distance: Double,
val routeEngine: Int,
val summary: Summary?,
val legs: List<Leg>?,
val routeGeoJson: String = "",
val centerLocation : Location = location(0.0, 0.0),
var currentStep : Int = 0,
val waypoints: List<List<Double>>?,
) {
/**
* List of [List<Double>] objects. Each `waypoint` is an input coordinate
* snapped to the road and path network. The `waypoint` appear in the list in the order of
* the input coordinates.
*
* @since 1.0.0
*/
val waypoints: List<List<Double>>,
data class Builder (
/**
* List of [List<Point>] objects. Each `Point` is an input coordinate
* snapped to the road and path network. The `waypoint` appear in the list in the order of
* the input coordinates.
*
* @since 1.0.0
*/
val pointLocations: List<Point>,
val summary: Summary,
val trip: Trip,
val time: Double,
val routeGeoJson: String,
val currentManeuverIndex : Int,
val centerLocation: Location
) {
class Builder {
private lateinit var maneuvers: List<Maneuvers>
private var distance: Double = 0.0
private var time: Double = 0.0
private lateinit var waypoints: List<List<Double>>
private lateinit var pointLocations: List<Point>
private lateinit var summary: Summary
private lateinit var trip: Trip
private var routeGeoJson = ""
private var centerLocation = location(0.0, 0.0)
var routeEngine : Int = 0,
var summary: Summary? = null,
var legs: List<Leg>? = null,
var routeGeoJson: String = "",
var centerLocation: Location = location(0.0, 0.0),
var waypoints : List<List<Double>>? = null,) {
fun routeType (routeEngine: Int) = apply {this.routeEngine = routeEngine }
fun summary(summary: Summary) = apply { this.summary = summary }
fun legs(legs: List<Leg>) = apply { this.legs = legs }
fun routeGeoJson(routeGeoJson: String) = apply {
this.routeGeoJson = routeGeoJson
centerLocation = createCenterLocation(routeGeoJson)
}
fun waypoints(waypoints: List<List<Double>>) = apply { this.waypoints = waypoints }
fun route(route: String) = apply {
if (route.isNotEmpty() && route != "[]") {
val gson = GsonBuilder().serializeNulls().create()
val routeJson = gson.fromJson(route, ValhallaJson::class.java)
trip = routeJson.trip
val jsonObject: Map<String, JsonElement> = Json.parseToJsonElement(route).jsonObject
val routeJson =
gson.fromJson(jsonObject["trip"].toString(), ValhallaResponse::class.java)
ValhallaRoute().mapJsonToValhalla(routeJson, this)
}
}
fun build(): Route {
maneuvers = trip.legs[0].maneuvers
summary = trip.summary
distance = summary.length
time = summary.time
waypoints = decodePolyline(trip.legs[0].shape)
val points = mutableListOf<Point>()
for (loc in waypoints) {
val point = Point.fromLngLat(loc[0], loc[1])
points.add(point)
}
pointLocations = points
routeGeoJson = createLineStringCollection( waypoints)
centerLocation = createCenterLocation(routeGeoJson)
return Route(
maneuvers,
distance,
waypoints,
pointLocations,
summary,
trip,
time,
routeGeoJson,
0,
centerLocation
routeEngine = this.routeEngine,
summary = this.summary,
legs = this.legs,
waypoints = this.waypoints,
routeGeoJson = this.routeGeoJson,
)
}
}
fun maneuverLocations(): List<Point> {
return pointLocations
val step = currentStep()
val waypoints = step.maneuver.waypoints
val points = mutableListOf<Point>()
for (loc in waypoints) {
val point = Point.fromLngLat(loc[0], loc[1])
points.add(point)
}
return points
}
fun currentManeuver(): Maneuvers {
return maneuvers[currentManeuverIndex]
fun currentStep(): Step {
if (legs != null) {
return legs.first().steps[currentStep]
} else {
throw IndexOutOfBoundsException("No legs available.")
}
}
fun nextManeuver(): Maneuvers {
val nextIndex = currentManeuverIndex + 1
return if (nextIndex < maneuvers.size) {
maneuvers[nextIndex]
fun nextStep(): Step {
val nextIndex = currentStep + 1
return if (nextIndex < legs!!.first().steps.size) {
legs.first().steps[currentStep + 1]
} else {
throw IndexOutOfBoundsException("No next maneuver available.")
}

View File

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

View File

@@ -0,0 +1,8 @@
package com.kouros.navigation.data.route
data class Maneuver(
val bearingBefore : Int = 0,
val bearingAfter : Int = 0,
val type: Int = 0,
val waypoints: List<List<Double>>,
)

View File

@@ -0,0 +1,10 @@
package com.kouros.navigation.data.route
class Step(
var index : Int = 0,
var waypointIndex : Int = 0,
val maneuver: Maneuver,
val duration: Double = 0.0,
val distance: Double = 0.0,
val name : String = "",
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.route
data class Summary(
var duration : Double = 0.0,
var distance : Double = 0.0,
)

View File

@@ -2,7 +2,6 @@ package com.kouros.navigation.data.valhalla
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
@@ -11,7 +10,7 @@ import kotlinx.serialization.json.JsonIgnoreUnknownKeys
data class Legs (
@SerializedName("maneuvers" ) var maneuvers : ArrayList<Maneuvers> = arrayListOf(),
@SerializedName("summary" ) var summary : Summary = Summary(),
@SerializedName("summary" ) var summaryValhalla : SummaryValhalla = SummaryValhalla(),
@SerializedName("shape" ) var shape : String = ""
)

View File

@@ -2,13 +2,12 @@ package com.kouros.navigation.data.valhalla
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
@OptIn(ExperimentalSerializationApi::class)
@JsonIgnoreUnknownKeys
data class Summary (
data class SummaryValhalla (
@SerializedName("has_time_restrictions" ) var hasTimeRestrictions : Boolean = false,
@SerializedName("has_toll" ) var hasToll : Boolean = false,

View File

@@ -2,7 +2,6 @@ package com.kouros.navigation.data.valhalla
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
@@ -12,10 +11,10 @@ data class Trip (
@SerializedName("locations" ) var locations : ArrayList<Locations> = arrayListOf(),
@SerializedName("legs" ) var legs : ArrayList<Legs> = arrayListOf(),
@SerializedName("summary" ) var summary : Summary = Summary(),
@SerializedName("summary" ) var summaryValhalla : SummaryValhalla = SummaryValhalla(),
@SerializedName("status_message" ) var statusMessage : String = "",
@SerializedName("status" ) var status : Int = 0,
@SerializedName("units" ) var units : String = "",
@SerializedName("language" ) var language : String = "",
)
)

View File

@@ -0,0 +1,20 @@
package com.kouros.navigation.data.valhalla
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
@OptIn(ExperimentalSerializationApi::class)
@JsonIgnoreUnknownKeys
data class ValhallaResponse(
@SerializedName("locations") var locations: ArrayList<Locations> = arrayListOf(),
@SerializedName("legs") var legs: ArrayList<Legs> = arrayListOf(),
@SerializedName("summary") var summaryValhalla: SummaryValhalla = SummaryValhalla(),
@SerializedName("status_message") var statusMessage: String = "",
@SerializedName("status") var status: Int = 0,
@SerializedName("units") var units: String = "",
@SerializedName("language") var language: String = "",
)

View File

@@ -0,0 +1,43 @@
package com.kouros.navigation.data.valhalla
import com.kouros.navigation.data.Route
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.createLineStringCollection
import com.kouros.navigation.utils.GeoUtils.decodePolyline
class ValhallaRoute {
fun mapJsonToValhalla(routeJson: ValhallaResponse, builder: Route.Builder) {
val waypoints = decodePolyline(routeJson.legs[0].shape)
val summary = Summary()
summary.distance = routeJson.summaryValhalla.length
summary.duration = routeJson.summaryValhalla.time
val steps = mutableListOf<Step>()
var stepIndex = 0
routeJson.legs[0].maneuvers.forEach {
val maneuver = Maneuver(
bearingBefore = 0,
bearingAfter = it.bearingAfter,
type = it.type,
waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1)
)
var name = ""
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)
steps.add(step)
stepIndex += 1
}
val leg = Leg(steps)
builder
.routeType(1)
.summary(summary)
.routeGeoJson(createLineStringCollection(waypoints))
.legs(listOf(leg))
.waypoints(waypoints)
}
}

View File

@@ -9,6 +9,7 @@ import com.kouros.navigation.data.ManeuverType
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.utils.location
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -24,16 +25,11 @@ open class RouteModel() {
val destination: Place = Place(),
val arrived: Boolean = false,
val maneuverType: Int = 0,
var currentShapeIndex: Int = 0,
var distanceToStepEnd: Float = 0F,
var beginIndex: Int = 0,
var endIndex: Int = 0,
val travelMessage: String = "",
// max speed for street (maneuver)
val lastSpeedIndex: Int = 0,
val maxSpeed: Int = 0,
)
)
var routeState = RouteState()
@@ -43,6 +39,10 @@ open class RouteModel() {
routeState = routeState.copy(route = value)
}
val legs: Leg
get() = routeState.route!!.legs!!.first()
fun startNavigation(routeString: String) {
val newRoute = Route.Builder()
.route(routeString)
@@ -59,40 +59,39 @@ open class RouteModel() {
isNavigating = false,
arrived = false,
maneuverType = 0,
currentShapeIndex = 0,
distanceToStepEnd = 0F,
beginIndex = 0,
endIndex = 0
)
}
@OptIn(DelicateCoroutinesApi::class)
fun updateLocation(location: Location, viewModel: ViewModel) {
var nearestDistance = 100000.0f
var newShapeIndex = -1
// find nearest waypoint and current shape index
// start search at last shape index
for (i in routeState.currentShapeIndex..<route.waypoints.size) {
val waypoint = route.waypoints[i]
val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
if (distance < nearestDistance) {
nearestDistance = distance
newShapeIndex = i
}
}
// find maneuver
// calculate distance to step end
findManeuver(newShapeIndex)
findStep(location)
GlobalScope.launch(Dispatchers.IO) {
updateSpeedLimit(location, viewModel)
}
}
private fun findStep(location: Location) {
var nearestDistance = 100000.0f
for ((index, step) in legs.steps.withIndex()) {
if (index >= route.currentStep && nearestDistance > 0) {
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
step.waypointIndex = wayIndex
}
}
}
}
}
}
fun updateSpeedLimit(location: Location, viewModel: ViewModel) {
// speed limit for each maneuver index
if (routeState.lastSpeedIndex < route.currentManeuverIndex) {
routeState = routeState.copy(lastSpeedIndex = route.currentManeuverIndex)
if (routeState.lastSpeedIndex < route.currentStep) {
routeState = routeState.copy(lastSpeedIndex = route.currentStep)
val elements = viewModel.getMaxSpeed(location)
elements.forEach {
if (it.tags.name != null && it.tags.maxspeed != null) {
@@ -103,52 +102,30 @@ open class RouteModel() {
}
}
private fun findManeuver(newShapeIndex: Int) {
for (i in route.currentManeuverIndex..<route.maneuvers.size) {
val maneuver = route.maneuvers[i]
if (maneuver.beginShapeIndex <= newShapeIndex && maneuver.endShapeIndex >= newShapeIndex) {
route = route.copy(currentManeuverIndex = i)
routeState.apply {
currentShapeIndex = newShapeIndex
beginIndex = maneuver.beginShapeIndex
endIndex = maneuver.endShapeIndex
distanceToStepEnd = 0F
// calculate shape distance to step end
for (j in newShapeIndex + 1..maneuver.endShapeIndex) {
val loc1 = location(route!!.waypoints[j - 1][0], route.waypoints[j - 1][1])
val loc2 = location(route.waypoints[j][0], route.waypoints[j][1])
distanceToStepEnd += loc1.distanceTo(loc2)
}
break
}
}
}
}
fun currentStep(): StepData {
val currentManeuver = route.currentManeuver()
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.currentManeuverIndex < (route.maneuvers.size)
isNearNextManeuver && route.currentStep < (route.legs!!.first().steps.size)
// Determine the maneuver type and corresponding icon
var maneuverType = if (hasArrived(currentManeuver.type)) {
currentManeuver.type
var maneuverType = if (hasArrived(currentStep.maneuver.type)) {
currentStep.maneuver.type
} else {
ManeuverType.None.value
}
// Get the single, correct maneuver for this state
val relevantManeuver = if (shouldAdvance) {
route.nextManeuver() // This advances the route's state
route.nextStep() // This advances the route's state
} else {
route.currentManeuver()
route.currentStep()
}
// Safely get the street name from the maneuver
val streetName = relevantManeuver.streetNames?.firstOrNull() ?: ""
val streetName = relevantManeuver.name
if (shouldAdvance) {
maneuverType = relevantManeuver.type
maneuverType = relevantManeuver.maneuver.type
}
val maneuverIconPair = maneuverIcon(maneuverType)
routeState = routeState.copy(maneuverType = maneuverIconPair.first)
@@ -165,8 +142,8 @@ open class RouteModel() {
fun nextStep(): StepData {
val maneuver = route.nextManeuver()
val maneuverType = maneuver.type
val step = route.nextStep()
val maneuverType = step.maneuver.type
val distanceLeft = leftStepDistance()
var text = ""
@@ -175,8 +152,8 @@ open class RouteModel() {
}
else -> {
if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) {
text = maneuver.streetNames[0]
if (step.name.isNotEmpty()) {
text = step.name
}
}
}
@@ -195,18 +172,16 @@ open class RouteModel() {
fun travelLeftTime(): Double {
var timeLeft = 0.0
for (i in route.currentManeuverIndex + 1..<route.maneuvers.size) {
val maneuver = route.maneuvers[i]
timeLeft += maneuver.time
}
if (routeState.endIndex > 0) {
val maneuver = route.currentManeuver()
val curTime = maneuver.time
val percent =
100 * (routeState.endIndex - routeState.currentShapeIndex) / (routeState.endIndex - routeState.beginIndex)
val time = curTime * percent / 100
timeLeft += time
for (i in route.currentStep + 1..<legs.steps.size) {
val step = legs.steps[i]
timeLeft += step.duration
}
val step = route.nextStep()
val curTime = step.duration
val percent =
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
val time = curTime * percent / 100
timeLeft += time
return timeLeft
}
@@ -221,11 +196,11 @@ open class RouteModel() {
/** Returns the current [Step] left distance in m. */
fun leftStepDistance(): Double {
val maneuver = route.currentManeuver()
var leftDistance = maneuver.length
if (routeState.endIndex > 0) {
leftDistance = (routeState.distanceToStepEnd / 1000).toDouble()
}
val step = route.currentStep()
var leftDistance = step.distance
val percent =
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
leftDistance = leftDistance * percent / 100
// The remaining distance to the step, rounded to the nearest 10 units.
return (leftDistance * 1000 / 10.0).roundToInt() * 10.0
@@ -234,18 +209,17 @@ open class RouteModel() {
/** Returns the left distance in km. */
fun travelLeftDistance(): Double {
var leftDistance = 0.0
for (i in route.currentManeuverIndex + 1..<route.maneuvers.size) {
val maneuver = route.maneuvers[i]
leftDistance += maneuver.length
}
if (routeState.endIndex > 0) {
val maneuver = route.currentManeuver()
val curDistance = maneuver.length
val percent =
100 * (routeState.endIndex - routeState.currentShapeIndex) / (routeState.endIndex - routeState.beginIndex)
val time = curDistance * percent / 100
leftDistance += time
for (i in route.currentStep + 1..<legs.steps.size) {
val step = route.legs!![0].steps[i]
leftDistance += step.distance
}
val step = route.currentStep()
val curDistance = step.distance
val percent =
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
val time = curDistance * percent / 100
leftDistance += time
return leftDistance
}
@@ -311,7 +285,6 @@ open class RouteModel() {
currentTurnIcon = R.drawable.ic_roundabout_ccw
}
}
//routeState.maneuverType = type
return Pair(type, currentTurnIcon)
}
@@ -321,7 +294,6 @@ open class RouteModel() {
fun hasArrived(type: Int): Boolean {
// return leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE && type == ManeuverType.DestinationRight.value
return type == ManeuverType.DestinationRight.value
|| type == ManeuverType.Destination.value
|| type == ManeuverType.DestinationLeft.value

View File

@@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M190,840L160,810L480,80L800,810L770,840L480,708L190,840Z"/>
android:pathData="M190,840L160,810L480,80L800,810L770,840L480,708L190,840ZM258,742L480,644L702,742L480,228L258,742ZM480,644L480,644L480,644L480,644Z"/>
</vector>