Skip to content

Commit b45831a

Browse files
authored
Fix index increase for empty legs (maplibre#167)
* fix: skip empty steps when increasing index * fix: set index inside location processing mutex to avoid race conditions * fix: compilation * fix: Don't skip legitimate arrive steps * fix: Skip entire empty legs, not only steps * chore: Update version and changelog * fix: Remove redundant setIndex call
1 parent d7b1394 commit b45831a

6 files changed

Lines changed: 198 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ Changelog for the MapLibre Navigation SDK for Android
33

44
MapLibre welcomes participation and contributions from everyone.
55

6+
### v5.0.0-pre8 - Jun 27, 2025
7+
8+
- Fix ignoring empty legs when increasing navigation indices [#167](https://github.com/maplibre/maplibre-navigation-android/pull/167)
9+
610
### v5.0.0-pre7 - Jun 18, 2025
711

812
- Fix AppleLocationEngine by enabling background location updates [#166](https://github.com/maplibre/maplibre-navigation-android/pull/166)

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5.0.0-pre7
1+
5.0.0-pre8

maplibre-navigation-core/src/androidUnitTest/kotlin/org/maplibre/navigation/core/navigation/NavigationHelperTest.kt

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,54 @@ class NavigationHelperTest : BaseTest() {
7979
assertEquals(0, newIndices.stepIndex)
8080
}
8181

82+
@Test
83+
@Throws(Exception::class)
84+
fun increaseIndex_skipsZeroDistanceLeg() {
85+
val routeProgress = buildRouteProgressWithZeroDistanceLeg()
86+
val previousIndices = NavigationIndices(0, 1)
87+
88+
val newIndices = increaseIndex(routeProgress, previousIndices)
89+
90+
assertEquals(2, newIndices.legIndex)
91+
assertEquals(0, newIndices.stepIndex)
92+
}
93+
94+
@Test
95+
@Throws(Exception::class)
96+
fun increaseIndex_skipsMultipleZeroDistanceLegs() {
97+
val routeProgress = buildRouteProgressWithMultipleZeroDistanceLegs()
98+
val previousIndices = NavigationIndices(0, 1)
99+
100+
val newIndices = increaseIndex(routeProgress, previousIndices)
101+
102+
assertEquals(3, newIndices.legIndex)
103+
assertEquals(0, newIndices.stepIndex)
104+
}
105+
106+
@Test
107+
@Throws(Exception::class)
108+
fun increaseIndex_doesNotSkipLastLegEvenIfZeroDistance() {
109+
val routeProgress = buildRouteProgressWithZeroDistanceLastLeg()
110+
val previousIndices = NavigationIndices(0, 1)
111+
112+
val newIndices = increaseIndex(routeProgress, previousIndices)
113+
114+
assertEquals(1, newIndices.legIndex)
115+
assertEquals(0, newIndices.stepIndex)
116+
}
117+
118+
@Test
119+
@Throws(Exception::class)
120+
fun increaseIndex_normalLegTransition() {
121+
val routeProgress = buildRouteProgressWithNormalLegs()
122+
val previousIndices = NavigationIndices(0, 1)
123+
124+
val newIndices = increaseIndex(routeProgress, previousIndices)
125+
126+
assertEquals(1, newIndices.legIndex)
127+
assertEquals(0, newIndices.stepIndex)
128+
}
129+
82130
@Test
83131
@Throws(Exception::class)
84132
fun checkMilestones_onlyTriggeredMilestonesGetReturned() {
@@ -531,6 +579,125 @@ class NavigationHelperTest : BaseTest() {
531579
return routeLeg
532580
}
533581

582+
private fun buildRouteProgressWithZeroDistanceLeg(): RouteProgress {
583+
val firstLegSteps = listOf(
584+
mockk<LegStep>(relaxed = true) { every { distance } returns 100.0 },
585+
mockk<LegStep>(relaxed = true) { every { distance } returns 200.0 }
586+
)
587+
val secondLegSteps = listOf(
588+
mockk<LegStep>(relaxed = true) { every { distance } returns 0.0 }, // Zero distance leg (waypoint touch)
589+
mockk<LegStep>(relaxed = true) { every { distance } returns 0.0 }
590+
)
591+
val thirdLegSteps = listOf(
592+
mockk<LegStep>(relaxed = true) { every { distance } returns 150.0 },
593+
mockk<LegStep>(relaxed = true) { every { distance } returns 250.0 }
594+
)
595+
596+
val firstLeg = mockk<RouteLeg>(relaxed = true) {
597+
every { steps } returns firstLegSteps
598+
every { distance } returns 300.0 // Total leg distance
599+
}
600+
val secondLeg = mockk<RouteLeg>(relaxed = true) {
601+
every { steps } returns secondLegSteps
602+
every { distance } returns 0.0 // Zero distance leg (waypoint)
603+
}
604+
val thirdLeg = mockk<RouteLeg>(relaxed = true) {
605+
every { steps } returns thirdLegSteps
606+
every { distance } returns 400.0 // Total leg distance
607+
}
608+
609+
val route = mockk<DirectionsRoute>(relaxed = true) {
610+
every { legs } returns listOf(firstLeg, secondLeg, thirdLeg)
611+
}
612+
return mockk<RouteProgress>(relaxed = true) {
613+
every { directionsRoute } returns route
614+
}
615+
}
616+
617+
private fun buildRouteProgressWithMultipleZeroDistanceLegs(): RouteProgress {
618+
val firstLegSteps = listOf(
619+
mockk<LegStep>(relaxed = true) { every { distance } returns 100.0 },
620+
mockk<LegStep>(relaxed = true) { every { distance } returns 200.0 }
621+
)
622+
623+
val firstLeg = mockk<RouteLeg>(relaxed = true) {
624+
every { steps } returns firstLegSteps
625+
every { distance } returns 300.0
626+
}
627+
val secondLeg = mockk<RouteLeg>(relaxed = true) {
628+
every { steps } returns listOf(mockk(relaxed = true))
629+
every { distance } returns 0.0 // Zero distance leg
630+
}
631+
val thirdLeg = mockk<RouteLeg>(relaxed = true) {
632+
every { steps } returns listOf(mockk(relaxed = true))
633+
every { distance } returns 0.0 // Another zero distance leg
634+
}
635+
val fourthLeg = mockk<RouteLeg>(relaxed = true) {
636+
every { steps } returns listOf(mockk(relaxed = true) { every { distance } returns 500.0 })
637+
every { distance } returns 500.0 // Normal leg
638+
}
639+
640+
val route = mockk<DirectionsRoute>(relaxed = true) {
641+
every { legs } returns listOf(firstLeg, secondLeg, thirdLeg, fourthLeg)
642+
}
643+
return mockk<RouteProgress>(relaxed = true) {
644+
every { directionsRoute } returns route
645+
}
646+
}
647+
648+
private fun buildRouteProgressWithZeroDistanceLastLeg(): RouteProgress {
649+
val firstLegSteps = listOf(
650+
mockk<LegStep>(relaxed = true) { every { distance } returns 100.0 },
651+
mockk<LegStep>(relaxed = true) { every { distance } returns 200.0 }
652+
)
653+
val secondLegSteps = listOf(
654+
mockk<LegStep>(relaxed = true) { every { distance } returns 0.0 }
655+
)
656+
657+
val firstLeg = mockk<RouteLeg>(relaxed = true) {
658+
every { steps } returns firstLegSteps
659+
every { distance } returns 300.0
660+
}
661+
val secondLeg = mockk<RouteLeg>(relaxed = true) {
662+
every { steps } returns secondLegSteps
663+
every { distance } returns 0.0 // Zero distance last leg (should not be skipped)
664+
}
665+
666+
val route = mockk<DirectionsRoute>(relaxed = true) {
667+
every { legs } returns listOf(firstLeg, secondLeg)
668+
}
669+
return mockk<RouteProgress>(relaxed = true) {
670+
every { directionsRoute } returns route
671+
}
672+
}
673+
674+
private fun buildRouteProgressWithNormalLegs(): RouteProgress {
675+
val firstLegSteps = listOf(
676+
mockk<LegStep>(relaxed = true) { every { distance } returns 100.0 },
677+
mockk<LegStep>(relaxed = true) { every { distance } returns 200.0 }
678+
)
679+
val secondLegSteps = listOf(
680+
mockk<LegStep>(relaxed = true) { every { distance } returns 150.0 },
681+
mockk<LegStep>(relaxed = true) { every { distance } returns 250.0 }
682+
)
683+
684+
val firstLeg = mockk<RouteLeg>(relaxed = true) {
685+
every { steps } returns firstLegSteps
686+
every { distance } returns 300.0
687+
}
688+
val secondLeg = mockk<RouteLeg>(relaxed = true) {
689+
every { steps } returns secondLegSteps
690+
every { distance } returns 400.0
691+
}
692+
693+
val route = mockk<DirectionsRoute>(relaxed = true) {
694+
every { legs } returns listOf(firstLeg, secondLeg)
695+
}
696+
return mockk<RouteProgress>(relaxed = true) {
697+
every { directionsRoute } returns route
698+
}
699+
}
700+
534701
companion object {
535702
private const val MULTI_LEG_ROUTE_FIXTURE = "directions_two_leg_route.json"
536703
private const val ANNOTATED_DISTANCE_CONGESTION_ROUTE_FIXTURE =

maplibre-navigation-core/src/androidUnitTest/kotlin/org/maplibre/navigation/core/navigation/engine/MapLibreNavigationEngineTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class MapLibreNavigationEngineTest : BaseTest() {
100100

101101
// Execute - simulate location update 1 second later
102102
coEvery { mockLocationEngine.getLastLocation() } returns step5Location
103-
navigationEngine.processLocationUpdate(step5Location)
103+
navigationEngine.processLocationAndIndexUpdate(step5Location)
104104
testScheduler.advanceUntilIdle()
105105

106106
// Assert indices are preserved after a location update

maplibre-navigation-core/src/commonMain/kotlin/org/maplibre/navigation/core/navigation/NavigationHelper.kt

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import org.maplibre.geojson.turf.TurfMeasurement
2020
import org.maplibre.geojson.turf.TurfMisc
2121
import org.maplibre.geojson.turf.TurfUnit
2222
import org.maplibre.geojson.utils.PolylineUtils
23+
import org.maplibre.navigation.core.models.StepManeuver
2324
import kotlin.jvm.JvmStatic
2425

2526
/**
@@ -206,23 +207,29 @@ object NavigationHelper {
206207
previousIndices: NavigationIndices
207208
): NavigationIndices {
208209
val route = routeProgress.directionsRoute
209-
val previousStepIndex = previousIndices.stepIndex
210-
val previousLegIndex = previousIndices.legIndex
211210
val routeLegSize = route.legs.size
212-
val legStepSize = route.legs[routeProgress.legIndex].steps.size
211+
var indices: NavigationIndices = previousIndices
212+
213+
val legStepSize = route.legs[indices.legIndex].steps.size
214+
val previousLegIndex = indices.legIndex
215+
val previousStepIndex = indices.stepIndex
213216

214217
val isOnLastLeg = previousLegIndex == routeLegSize - 1
215218
val isOnLastStep = previousStepIndex == legStepSize - 1
216219

217-
if (isOnLastStep && !isOnLastLeg) {
218-
return NavigationIndices(previousLegIndex + 1, 0)
220+
indices = when {
221+
isOnLastStep && !isOnLastLeg -> NavigationIndices(previousLegIndex + 1, 0)
222+
// It's the last step of the last leg. There's nowhere to go.
223+
isOnLastStep -> return indices
224+
else -> NavigationIndices(previousLegIndex, previousStepIndex + 1)
219225
}
220226

221-
if (isOnLastStep) {
222-
return previousIndices
227+
// Then skip any zero-distance legs (waypoint legs with no actual travel)
228+
while (indices.legIndex < routeLegSize - 1 && route.legs[indices.legIndex].distance <= 0.0) {
229+
indices = NavigationIndices(indices.legIndex + 1, 0)
223230
}
224231

225-
return NavigationIndices(previousLegIndex, previousStepIndex + 1)
232+
return indices
226233
}
227234

228235
/**

maplibre-navigation-core/src/commonMain/kotlin/org/maplibre/navigation/core/navigation/engine/MapLibreNavigationEngine.kt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ open class MapLibreNavigationEngine(
5353
collectLocationJob?.cancel() // Cancel previous started run
5454

5555
collectLocationJob = backgroundScope.launch {
56-
processLocationUpdate(
56+
processLocationAndIndexUpdate(
5757
locationEngine.getLastLocation() ?: routeUtils.createFirstLocationFromRoute(route)
5858
)
5959

@@ -62,7 +62,7 @@ open class MapLibreNavigationEngine(
6262
minIntervalMilliseconds = LOCATION_ENGINE_INTERVAL,
6363
maxIntervalMilliseconds = LOCATION_ENGINE_INTERVAL,
6464
)
65-
).collect(::processLocationUpdate)
65+
).collect(::processLocationAndIndexUpdate)
6666
}
6767
}
6868

@@ -86,15 +86,20 @@ open class MapLibreNavigationEngine(
8686
}
8787

8888
/**
89-
* Takes a new location model and runs all related engine checks against it
89+
* Takes a new location model and route indices runs all related engine checks against it
9090
* (off-route, milestones, snapped location, and faster-route).
9191
*
9292
* After running through the engines, all data is submitted to [NavigationEventDispatcher].
9393
*
9494
* @param rawLocation hold location, navigation (with options), and distances away from maneuver
9595
*/
96-
suspend fun processLocationUpdate(rawLocation: Location) {
96+
suspend fun processLocationAndIndexUpdate(rawLocation: Location, index: NavigationIndices? = null) {
9797
processingMutex.withLock {
98+
// Index is set inside the mutex to avoid race conditions.
99+
index?.let {
100+
navigationRouteProcessor.setIndex(mapLibreNavigation, it)
101+
}
102+
98103
if (!locationValidator.isValidUpdate(rawLocation)) {
99104
return
100105
}
@@ -203,8 +208,7 @@ open class MapLibreNavigationEngine(
203208
override fun triggerManualRouteUpdate(legIndex: Int, stepIndex: Int) {
204209
backgroundScope.launch {
205210
locationEngine.getLastLocation()?.let { currentLocation ->
206-
navigationRouteProcessor.setIndex(mapLibreNavigation, NavigationIndices(legIndex, stepIndex))
207-
processLocationUpdate(currentLocation)
211+
processLocationAndIndexUpdate(currentLocation, index = NavigationIndices(legIndex, stepIndex))
208212
}
209213
}
210214
}

0 commit comments

Comments
 (0)