From 5e8ac7bf1e510788a729104b096bac02bae4cc1d Mon Sep 17 00:00:00 2001 From: Seth Bourget Date: Wed, 30 Nov 2022 16:25:29 -0800 Subject: [PATCH] Added feature to mask the primary route line in order to give the appearance the active leg is above the inactive legs for multi-leg routes. --- CHANGELOG.md | 1 + .../ui/routeline/RouteLineLayersTest.kt | 140 +++- .../route/line/MapboxRouteLineUtils.kt | 168 ++++- .../ui/maps/route/RouteLayerConstants.kt | 20 + .../maps/route/line/api/MapboxRouteLineApi.kt | 426 +++++++++--- .../route/line/api/MapboxRouteLineView.kt | 438 +++++++++--- .../route/line/model/RouteLineUpdateValue.kt | 19 +- .../ui/maps/route/line/model/RouteSetValue.kt | 14 +- .../ui/maps/util/CacheResultUtils.kt | 18 + .../line/MapboxRouteLineUtilsRoboTest.kt | 76 ++- .../route/line/MapboxRouteLineUtilsTest.kt | 179 +++-- .../line/api/MapboxRouteLineApiRoboTest.kt | 96 ++- .../route/line/api/MapboxRouteLineApiTest.kt | 629 +++++++++++++++++- .../route/line/api/MapboxRouteLineViewTest.kt | 366 +++++++++- .../line/api/VanishingRouteLineRoboTest.kt | 5 +- .../line/model/RouteLineDynamicDataTest.kt | 16 +- .../line/model/RouteLineUpdateValueTest.kt | 13 +- .../route/line/model/RouteSetValueTest.kt | 9 + .../resources/multileg-route-overlaps.json | 1 + .../multileg_route_with_overlap.json | 1 + qa-test-app/src/main/AndroidManifest.xml | 2 + .../qa_test_app/domain/TestActivitySuite.kt | 7 + .../ActiveLegAboveInactiveLegsActivity.kt | 270 ++++++++ ...ctive_leg_above_others_activity_layout.xml | 84 +++ .../res/raw/multileg_route_with_overlap.json | 1 + qa-test-app/src/main/res/values/strings.xml | 2 + 26 files changed, 2648 insertions(+), 353 deletions(-) create mode 100644 libnavui-maps/src/test/resources/multileg-route-overlaps.json create mode 100644 libnavui-maps/src/test/resources/multileg_route_with_overlap.json create mode 100644 qa-test-app/src/main/java/com/mapbox/navigation/qa_test_app/view/ActiveLegAboveInactiveLegsActivity.kt create mode 100644 qa-test-app/src/main/res/layout/active_leg_above_others_activity_layout.xml create mode 100644 qa-test-app/src/main/res/raw/multileg_route_with_overlap.json diff --git a/CHANGELOG.md b/CHANGELOG.md index c84176293a5..431f1c39b16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Mapbox welcomes participation and contributions from everyone. - Fixed approaches list update in `RouteOptionsUpdater`(uses for reroute). It was putting to the origin approach corresponding approach from legacy approach list. [#6540](https://github.com/mapbox/mapbox-navigation-android/pull/6540) - Updated the `MapboxRestAreaApi` logic to load a SAPA map only if the upcoming rest stop is at the current step of the route leg. [#6695](https://github.com/mapbox/mapbox-navigation-android/pull/6695) - Fixed an issue where `MapboxRerouteController` could deliver a delayed interruption state notifications. Now, the interruption state is always delivered synchronously, as soon as it's available. [#6718](https://github.com/mapbox/mapbox-navigation-android/pull/6718) +- Modified the route line implementation so that for multi-leg routes the active leg appears above inactive route legs. [#6742](https://github.com/mapbox/mapbox-navigation-android/pull/6742) ## Mapbox Navigation SDK 2.9.5 - 13 December, 2022 ### Changelog diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/ui/routeline/RouteLineLayersTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/ui/routeline/RouteLineLayersTest.kt index 9727bc044f8..050eec1a734 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/ui/routeline/RouteLineLayersTest.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/ui/routeline/RouteLineLayersTest.kt @@ -98,84 +98,102 @@ class RouteLineLayersTest : BaseTest( "background" ) ) - assertEquals( "mapbox-layerGroup-1-restricted", style.styleLayers[topLevelRouteLayerIndex - 1].id ) assertEquals( - "mapbox-layerGroup-1-traffic", + "mapbox-masking-layer-traffic", style.styleLayers[topLevelRouteLayerIndex - 2].id ) assertEquals( - "mapbox-layerGroup-1-main", + "mapbox-masking-layer-main", style.styleLayers[topLevelRouteLayerIndex - 3].id ) assertEquals( - "mapbox-layerGroup-1-casing", + "mapbox-masking-layer-casing", style.styleLayers[topLevelRouteLayerIndex - 4].id ) assertEquals( - "mapbox-layerGroup-1-trail", + "mapbox-masking-layer-trail", style.styleLayers[topLevelRouteLayerIndex - 5].id ) assertEquals( - "mapbox-layerGroup-1-trailCasing", + "mapbox-masking-layer-trailCasing", style.styleLayers[topLevelRouteLayerIndex - 6].id ) - assertEquals( - "mapbox-layerGroup-2-restricted", + "mapbox-layerGroup-1-traffic", style.styleLayers[topLevelRouteLayerIndex - 7].id ) assertEquals( - "mapbox-layerGroup-2-traffic", + "mapbox-layerGroup-1-main", style.styleLayers[topLevelRouteLayerIndex - 8].id ) assertEquals( - "mapbox-layerGroup-2-main", + "mapbox-layerGroup-1-casing", style.styleLayers[topLevelRouteLayerIndex - 9].id ) assertEquals( - "mapbox-layerGroup-2-casing", + "mapbox-layerGroup-1-trail", style.styleLayers[topLevelRouteLayerIndex - 10].id ) assertEquals( - "mapbox-layerGroup-2-trail", + "mapbox-layerGroup-1-trailCasing", style.styleLayers[topLevelRouteLayerIndex - 11].id ) assertEquals( - "mapbox-layerGroup-2-trailCasing", + "mapbox-layerGroup-2-restricted", style.styleLayers[topLevelRouteLayerIndex - 12].id ) + assertEquals( + "mapbox-layerGroup-2-traffic", + style.styleLayers[topLevelRouteLayerIndex - 13].id + ) + assertEquals( + "mapbox-layerGroup-2-main", + style.styleLayers[topLevelRouteLayerIndex - 14].id + ) + assertEquals( + "mapbox-layerGroup-2-casing", + style.styleLayers[topLevelRouteLayerIndex - 15].id + ) + assertEquals( + "mapbox-layerGroup-2-trail", + style.styleLayers[topLevelRouteLayerIndex - 16].id + ) + assertEquals( + "mapbox-layerGroup-2-trailCasing", + style.styleLayers[topLevelRouteLayerIndex - 17].id + ) assertEquals( "mapbox-layerGroup-3-restricted", - style.styleLayers[topLevelRouteLayerIndex - 13].id + style.styleLayers[topLevelRouteLayerIndex - 18].id ) assertEquals( "mapbox-layerGroup-3-traffic", - style.styleLayers[topLevelRouteLayerIndex - 14].id + style.styleLayers[topLevelRouteLayerIndex - 19].id ) assertEquals( "mapbox-layerGroup-3-main", - style.styleLayers[topLevelRouteLayerIndex - 15].id + style.styleLayers[topLevelRouteLayerIndex - 20].id ) assertEquals( "mapbox-layerGroup-3-casing", - style.styleLayers[topLevelRouteLayerIndex - 16].id + style.styleLayers[topLevelRouteLayerIndex - 21].id ) assertEquals( "mapbox-layerGroup-3-trail", - style.styleLayers[topLevelRouteLayerIndex - 17].id + style.styleLayers[topLevelRouteLayerIndex - 22].id ) assertEquals( "mapbox-layerGroup-3-trailCasing", - style.styleLayers[topLevelRouteLayerIndex - 18].id + style.styleLayers[topLevelRouteLayerIndex - 23].id ) assertEquals( "mapbox-bottom-level-route-layer", - style.styleLayers[topLevelRouteLayerIndex - 19].id + style.styleLayers[topLevelRouteLayerIndex - 24].id ) } } @@ -216,25 +234,45 @@ class RouteLineLayersTest : BaseTest( style.styleLayers[topLevelRouteLayerIndex - 1].id ) assertEquals( - "mapbox-layerGroup-1-traffic", + "mapbox-masking-layer-traffic", style.styleLayers[topLevelRouteLayerIndex - 2].id ) assertEquals( - "mapbox-layerGroup-1-main", + "mapbox-masking-layer-main", style.styleLayers[topLevelRouteLayerIndex - 3].id ) assertEquals( - "mapbox-layerGroup-1-casing", + "mapbox-masking-layer-casing", style.styleLayers[topLevelRouteLayerIndex - 4].id ) assertEquals( - "mapbox-layerGroup-1-trail", + "mapbox-masking-layer-trail", style.styleLayers[topLevelRouteLayerIndex - 5].id ) assertEquals( - "mapbox-layerGroup-1-trailCasing", + "mapbox-masking-layer-trailCasing", style.styleLayers[topLevelRouteLayerIndex - 6].id ) + assertEquals( + "mapbox-layerGroup-1-traffic", + style.styleLayers[topLevelRouteLayerIndex - 7].id + ) + assertEquals( + "mapbox-layerGroup-1-main", + style.styleLayers[topLevelRouteLayerIndex - 8].id + ) + assertEquals( + "mapbox-layerGroup-1-casing", + style.styleLayers[topLevelRouteLayerIndex - 9].id + ) + assertEquals( + "mapbox-layerGroup-1-trail", + style.styleLayers[topLevelRouteLayerIndex - 10].id + ) + assertEquals( + "mapbox-layerGroup-1-trailCasing", + style.styleLayers[topLevelRouteLayerIndex - 11].id + ) // This mimics selecting an alternative route by making the first // alternative the primary route and the original primary route one // of the alternatives. @@ -257,25 +295,45 @@ class RouteLineLayersTest : BaseTest( style.styleLayers[topLevelRouteLayerIndex - 1].id ) assertEquals( - "mapbox-layerGroup-2-traffic", + "mapbox-masking-layer-traffic", style.styleLayers[topLevelRouteLayerIndex - 2].id ) assertEquals( - "mapbox-layerGroup-2-main", + "mapbox-masking-layer-main", style.styleLayers[topLevelRouteLayerIndex - 3].id ) assertEquals( - "mapbox-layerGroup-2-casing", + "mapbox-masking-layer-casing", style.styleLayers[topLevelRouteLayerIndex - 4].id ) assertEquals( - "mapbox-layerGroup-2-trail", + "mapbox-masking-layer-trail", style.styleLayers[topLevelRouteLayerIndex - 5].id ) assertEquals( - "mapbox-layerGroup-2-trailCasing", + "mapbox-masking-layer-trailCasing", style.styleLayers[topLevelRouteLayerIndex - 6].id ) + assertEquals( + "mapbox-layerGroup-2-traffic", + style.styleLayers[topLevelRouteLayerIndex - 7].id + ) + assertEquals( + "mapbox-layerGroup-2-main", + style.styleLayers[topLevelRouteLayerIndex - 8].id + ) + assertEquals( + "mapbox-layerGroup-2-casing", + style.styleLayers[topLevelRouteLayerIndex - 9].id + ) + assertEquals( + "mapbox-layerGroup-2-trail", + style.styleLayers[topLevelRouteLayerIndex - 10].id + ) + assertEquals( + "mapbox-layerGroup-2-trailCasing", + style.styleLayers[topLevelRouteLayerIndex - 11].id + ) countDownLatch.countDown() } @@ -332,9 +390,13 @@ class RouteLineLayersTest : BaseTest( )?.visibility ) assertEquals( - "mapbox-layerGroup-1-traffic", + "mapbox-masking-layer-traffic", style.styleLayers[topLevelRouteLayerIndex - 1].id ) + assertEquals( + "mapbox-layerGroup-1-traffic", + style.styleLayers[topLevelRouteLayerIndex - 6].id + ) // This mimics selecting an alternative route by making the first // alternative the primary route and the original primary route one // of the alternatives. @@ -354,24 +416,34 @@ class RouteLineLayersTest : BaseTest( override fun onFinish() { // Primary route group is now 2 and not visible assertEquals( - "mapbox-layerGroup-2-traffic", + "mapbox-masking-layer-traffic", style.styleLayers[topLevelRouteLayerIndex - 1].id ) + assertEquals( + "mapbox-layerGroup-2-traffic", + style.styleLayers[topLevelRouteLayerIndex - 6].id + ) assertEquals( Visibility.NONE, style.getLayer( style.styleLayers[topLevelRouteLayerIndex - 1].id )?.visibility ) + assertEquals( + Visibility.NONE, + style.getLayer( + style.styleLayers[topLevelRouteLayerIndex - 6].id + )?.visibility + ) // Previously primary route group is 1 and is now visible assertEquals( "mapbox-layerGroup-1-traffic", - style.styleLayers[topLevelRouteLayerIndex - 6].id + style.styleLayers[topLevelRouteLayerIndex - 11].id ) assertEquals( Visibility.VISIBLE, style.getLayer( - style.styleLayers[topLevelRouteLayerIndex - 6].id + style.styleLayers[topLevelRouteLayerIndex - 11].id )?.visibility ) countDownLatch.countDown() diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/internal/route/line/MapboxRouteLineUtils.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/internal/route/line/MapboxRouteLineUtils.kt index a27aff9a266..3dc60921502 100644 --- a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/internal/route/line/MapboxRouteLineUtils.kt +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/internal/route/line/MapboxRouteLineUtils.kt @@ -17,6 +17,7 @@ import com.mapbox.maps.LayerPosition import com.mapbox.maps.Style import com.mapbox.maps.StyleObjectInfo import com.mapbox.maps.extension.style.expressions.dsl.generated.interpolate +import com.mapbox.maps.extension.style.expressions.dsl.generated.literal import com.mapbox.maps.extension.style.expressions.dsl.generated.match import com.mapbox.maps.extension.style.expressions.generated.Expression import com.mapbox.maps.extension.style.layers.addPersistentLayer @@ -45,11 +46,13 @@ import com.mapbox.navigation.ui.maps.route.line.model.NavigationRouteLine import com.mapbox.navigation.ui.maps.route.line.model.RouteFeatureData import com.mapbox.navigation.ui.maps.route.line.model.RouteLineColorResources import com.mapbox.navigation.ui.maps.route.line.model.RouteLineDistancesIndex +import com.mapbox.navigation.ui.maps.route.line.model.RouteLineDynamicData import com.mapbox.navigation.ui.maps.route.line.model.RouteLineExpressionData import com.mapbox.navigation.ui.maps.route.line.model.RouteLineExpressionProvider import com.mapbox.navigation.ui.maps.route.line.model.RouteLineGranularDistances import com.mapbox.navigation.ui.maps.route.line.model.RouteLineScaleValue import com.mapbox.navigation.ui.maps.route.line.model.RouteLineSourceKey +import com.mapbox.navigation.ui.maps.route.line.model.RouteLineTrimExpressionProvider import com.mapbox.navigation.ui.maps.route.line.model.RouteStyleDescriptor import com.mapbox.navigation.ui.maps.util.CacheResultUtils import com.mapbox.navigation.ui.maps.util.CacheResultUtils.cacheResult @@ -79,11 +82,6 @@ internal object MapboxRouteLineUtils { >, List> by lazy { LruCache(NUMBER_OF_SUPPORTED_ROUTES) } - private val extractRouteRestrictionDataCache: LruCache< - CacheResultUtils.CacheResultKeyRoute< - List>, List> - by lazy { LruCache(NUMBER_OF_SUPPORTED_ROUTES) } - private val granularDistancesCache: LruCache< CacheResultUtils.CacheResultKeyRoute< RouteLineGranularDistances?>, RouteLineGranularDistances?> @@ -93,6 +91,7 @@ internal object MapboxRouteLineUtils { val layerGroup2SourceKey = RouteLineSourceKey(RouteLayerConstants.LAYER_GROUP_2_SOURCE_ID) val layerGroup3SourceKey = RouteLineSourceKey(RouteLayerConstants.LAYER_GROUP_3_SOURCE_ID) + // ordering is important val layerGroup1SourceLayerIds = setOf( RouteLayerConstants.LAYER_GROUP_1_TRAIL_CASING, RouteLayerConstants.LAYER_GROUP_1_TRAIL, @@ -101,6 +100,8 @@ internal object MapboxRouteLineUtils { RouteLayerConstants.LAYER_GROUP_1_TRAFFIC, RouteLayerConstants.LAYER_GROUP_1_RESTRICTED ) + + // ordering is important val layerGroup2SourceLayerIds = setOf( RouteLayerConstants.LAYER_GROUP_2_TRAIL_CASING, RouteLayerConstants.LAYER_GROUP_2_TRAIL, @@ -109,6 +110,8 @@ internal object MapboxRouteLineUtils { RouteLayerConstants.LAYER_GROUP_2_TRAFFIC, RouteLayerConstants.LAYER_GROUP_2_RESTRICTED ) + + // ordering is important val layerGroup3SourceLayerIds = setOf( RouteLayerConstants.LAYER_GROUP_3_TRAIL_CASING, RouteLayerConstants.LAYER_GROUP_3_TRAIL, @@ -118,6 +121,15 @@ internal object MapboxRouteLineUtils { RouteLayerConstants.LAYER_GROUP_3_RESTRICTED ) + // ordering is important + val maskingLayerIds = setOf( + RouteLayerConstants.MASKING_LAYER_TRAIL_CASING, + RouteLayerConstants.MASKING_LAYER_TRAIL, + RouteLayerConstants.MASKING_LAYER_CASING, + RouteLayerConstants.MASKING_LAYER_MAIN, + RouteLayerConstants.MASKING_LAYER_TRAFFIC + ) + val sourceLayerMap = mapOf>( Pair(layerGroup1SourceKey, layerGroup1SourceLayerIds), Pair(layerGroup2SourceKey, layerGroup2SourceLayerIds), @@ -524,8 +536,7 @@ internal object MapboxRouteLineUtils { /** * Extracts data from the [DirectionsRoute] and removes items that are deemed duplicates based - * on factors such as traffic congestion and/or road class. The results are cached for - * performance reasons. + * on factors such as traffic congestion and/or road class. */ internal val extractRouteDataWithTrafficAndRoadClassDeDuped: ( route: NavigationRoute, @@ -557,22 +568,27 @@ internal object MapboxRouteLineUtils { * sections. This can be an expensive operation.The implementation defers or avoids the most * expensive operations as much as possible. */ - internal val extractRouteRestrictionData: ( + internal fun extractRouteRestrictionData( route: NavigationRoute, - ) -> List = - { route: NavigationRoute -> - val itemsToReturn = mutableListOf() - val granularDistances by lazy { granularDistancesProvider(route) } - route.directionsRoute.legs()?.forEachIndexed { legIndex, leg -> - val filteredIntersections = filterForRestrictedIntersections(leg) - ifNonNull(filteredIntersections) { stepIntersections -> - val legDistancesArray = granularDistances?.legsDistances - stepIntersections.forEach { stepIntersectionData -> - val geometryIndex = stepIntersectionData.first.geometryIndex() - if (geometryIndex != null && legDistancesArray?.isNotEmpty() == true) { - val distanceRemaining = - legDistancesArray[legIndex][geometryIndex].distanceRemaining - (1.0 - distanceRemaining / granularDistances!!.completeDistance).apply { + distancesProvider: (NavigationRoute) -> RouteLineGranularDistances? + ): List { + val itemsToReturn = mutableListOf() + route.directionsRoute.legs()?.forEachIndexed { legIndex, leg -> + val filteredIntersections = filterForRestrictedIntersections(leg) + ifNonNull(filteredIntersections) { stepIntersections -> + val legDistancesArray = distancesProvider(route)?.legsDistances + stepIntersections.forEach { stepIntersectionData -> + val geometryIndex = stepIntersectionData.first.geometryIndex() + if ( + geometryIndex != null && + legDistancesArray?.isNotEmpty() == true && + legIndex < legDistancesArray.size && + geometryIndex < legDistancesArray[legIndex].size + ) { + val distanceRemaining = + legDistancesArray[legIndex][geometryIndex].distanceRemaining + (1.0 - distanceRemaining / distancesProvider(route)!!.completeDistance) + .apply { if (this in 0.0..1.0) { itemsToReturn.add( ExtractedRouteRestrictionData( @@ -583,12 +599,12 @@ internal object MapboxRouteLineUtils { ) } } - } } } } - itemsToReturn - }.cacheRouteResult(extractRouteRestrictionDataCache) + } + return itemsToReturn + } /** * Filters the [RouteLeg] for intersections that are designated as restricted. If there are @@ -622,12 +638,6 @@ internal object MapboxRouteLineUtils { } } - internal fun routeHasRestrictions(route: NavigationRoute?): Boolean { - return ifNonNull(route) { - extractRouteRestrictionData(it).isNotEmpty() - } == true - } - /** * Extracts data from the [DirectionsRoute] in a format more useful to the route line * API. The data returned here is used by several different calculations. The results @@ -1353,6 +1363,68 @@ internal object MapboxRouteLineUtils { style.addPersistentLayer(this, LayerPosition(null, belowLayerIdToUse, null)) } } + + if (!style.styleLayerExists(RouteLayerConstants.MASKING_LAYER_TRAIL_CASING)) { + LineLayer( + RouteLayerConstants.MASKING_LAYER_TRAIL_CASING, + RouteLayerConstants.LAYER_GROUP_1_SOURCE_ID + ) + .lineCap(LineCap.ROUND) + .lineJoin(LineJoin.ROUND) + .lineWidth(options.resourceProvider.routeCasingLineScaleExpression) + .lineColor(Color.GRAY).apply { + style.addPersistentLayer(this, LayerPosition(null, belowLayerIdToUse, null)) + } + } + if (!style.styleLayerExists(RouteLayerConstants.MASKING_LAYER_TRAIL)) { + LineLayer( + RouteLayerConstants.MASKING_LAYER_TRAIL, + RouteLayerConstants.LAYER_GROUP_1_SOURCE_ID + ) + .lineCap(LineCap.ROUND) + .lineJoin(LineJoin.ROUND) + .lineWidth(options.resourceProvider.routeLineScaleExpression) + .lineColor(Color.GRAY).apply { + style.addPersistentLayer(this, LayerPosition(null, belowLayerIdToUse, null)) + } + } + if (!style.styleLayerExists(RouteLayerConstants.MASKING_LAYER_CASING)) { + LineLayer( + RouteLayerConstants.MASKING_LAYER_CASING, + RouteLayerConstants.LAYER_GROUP_1_SOURCE_ID + ) + .lineCap(LineCap.ROUND) + .lineJoin(LineJoin.ROUND) + .lineWidth(options.resourceProvider.routeCasingLineScaleExpression) + .lineColor(Color.GRAY).apply { + style.addPersistentLayer(this, LayerPosition(null, belowLayerIdToUse, null)) + } + } + if (!style.styleLayerExists(RouteLayerConstants.MASKING_LAYER_MAIN)) { + LineLayer( + RouteLayerConstants.MASKING_LAYER_MAIN, + RouteLayerConstants.LAYER_GROUP_1_SOURCE_ID + ) + .lineCap(LineCap.ROUND) + .lineJoin(LineJoin.ROUND) + .lineWidth(options.resourceProvider.routeLineScaleExpression) + .lineColor(Color.GRAY).apply { + style.addPersistentLayer(this, LayerPosition(null, belowLayerIdToUse, null)) + } + } + if (!style.styleLayerExists(RouteLayerConstants.MASKING_LAYER_TRAFFIC)) { + LineLayer( + RouteLayerConstants.MASKING_LAYER_TRAFFIC, + RouteLayerConstants.LAYER_GROUP_1_SOURCE_ID + ) + .lineCap(LineCap.ROUND) + .lineJoin(LineJoin.ROUND) + .lineWidth(options.resourceProvider.routeTrafficLineScaleExpression) + .lineColor(Color.GRAY).apply { + style.addPersistentLayer(this, LayerPosition(null, belowLayerIdToUse, null)) + } + } + if (options.displayRestrictedRoadSections) { if (!style.styleLayerExists(RouteLayerConstants.LAYER_GROUP_1_RESTRICTED)) { LineLayer( @@ -1413,6 +1485,11 @@ internal object MapboxRouteLineUtils { style.styleLayerExists(RouteLayerConstants.LAYER_GROUP_3_CASING) && style.styleLayerExists(RouteLayerConstants.LAYER_GROUP_3_MAIN) && style.styleLayerExists(RouteLayerConstants.LAYER_GROUP_3_TRAFFIC) && + style.styleLayerExists(RouteLayerConstants.MASKING_LAYER_TRAIL_CASING) && + style.styleLayerExists(RouteLayerConstants.MASKING_LAYER_TRAIL) && + style.styleLayerExists(RouteLayerConstants.MASKING_LAYER_CASING) && + style.styleLayerExists(RouteLayerConstants.MASKING_LAYER_MAIN) && + style.styleLayerExists(RouteLayerConstants.MASKING_LAYER_TRAFFIC) && if (options.displayRestrictedRoadSections) { style.styleLayerExists(RouteLayerConstants.LAYER_GROUP_1_RESTRICTED) && style.styleLayerExists(RouteLayerConstants.LAYER_GROUP_2_RESTRICTED) && @@ -1536,7 +1613,6 @@ internal object MapboxRouteLineUtils { internal fun trimRouteDataCacheToSize(size: Int) { extractRouteDataCache.trimToSize(size) - extractRouteRestrictionDataCache.trimToSize(size) granularDistancesCache.trimToSize(size) } @@ -1662,6 +1738,34 @@ internal object MapboxRouteLineUtils { 1.0 - distanceRemaining / distances.completeDistance } ?: 0.0 } + internal fun getMaskingLayerDynamicData( + route: NavigationRoute?, + offset: Double + ): RouteLineDynamicData? { + return if ( + (route?.directionsRoute?.legs()?.size ?: 1) > 1 + ) { + val trimmedOffsetExpression = literal( + listOf( + 0.0, + offset + ) + ) + RouteLineDynamicData( + baseExpressionProvider = + RouteLineTrimExpressionProvider { trimmedOffsetExpression }, + casingExpressionProvider = + RouteLineTrimExpressionProvider { trimmedOffsetExpression }, + trafficExpressionProvider = + RouteLineTrimExpressionProvider { trimmedOffsetExpression }, + restrictedSectionExpressionProvider = null, + trailExpressionProvider = { trimmedOffsetExpression }, + trailCasingExpressionProvider = { trimmedOffsetExpression }, + ) + } else { + null + } + } private fun projectX(x: Double): Double { return x / 360.0 + 0.5 diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/RouteLayerConstants.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/RouteLayerConstants.kt index ec6f64bc2e7..abca243f851 100644 --- a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/RouteLayerConstants.kt +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/RouteLayerConstants.kt @@ -206,4 +206,24 @@ object RouteLayerConstants { internal const val LAYER_GROUP_3_MAIN = "mapbox-layerGroup-3-main" internal const val LAYER_GROUP_3_TRAFFIC = "mapbox-layerGroup-3-traffic" internal const val LAYER_GROUP_3_RESTRICTED = "mapbox-layerGroup-3-restricted" + + /* + When a line on the map overlaps itself it appears as though the later part of the line + is on top of the earlier part of the line. For multi-leg routes this gives the appearance + the inactive leg(s) appearing on top of the active leg. Customer feedback indicated this + was unsatisfactory. The masking layers were created to address this issue and give the + opposite of the default appearance. The end result is the active route leg appears + above inactive leg(s) when a route overlaps itself. + + The masking layers share the same source as the primary route line. They are placed above + the primary route line layers except for the restricted line which appears above all other + primary route layers. The implementation will set the inactive route legs to transparent + so the primary route line layers are visible. The active leg section of the masking layers + are opaque thus masking the the primary route line layers beneath. + */ + internal const val MASKING_LAYER_TRAIL_CASING = "mapbox-masking-layer-trailCasing" + internal const val MASKING_LAYER_TRAIL = "mapbox-masking-layer-trail" + internal const val MASKING_LAYER_CASING = "mapbox-masking-layer-casing" + internal const val MASKING_LAYER_MAIN = "mapbox-masking-layer-main" + internal const val MASKING_LAYER_TRAFFIC = "mapbox-masking-layer-traffic" } diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineApi.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineApi.kt index 6a3147efe7f..c4d2d1f7671 100644 --- a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineApi.kt +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineApi.kt @@ -61,6 +61,7 @@ import com.mapbox.navigation.utils.internal.InternalJobControlFactory import com.mapbox.navigation.utils.internal.logE import com.mapbox.navigation.utils.internal.logW import com.mapbox.navigation.utils.internal.parallelMap +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.cancelChildren @@ -198,6 +199,7 @@ class MapboxRouteLineApi( private var primaryRoute: NavigationRoute? = null private val routes: MutableList = mutableListOf() private var routeLineExpressionData: List = emptyList() + private var restrictedExpressionData: List = emptyList() private var lastIndexUpdateTimeNano: Long = 0 private var lastPointUpdateTimeNano: Long = 0 private val routeFeatureData: MutableList = mutableListOf() @@ -207,12 +209,14 @@ class MapboxRouteLineApi( private set private val trafficBackfillRoadClasses = CopyOnWriteArrayList() private var alternativesDeviationOffset = mapOf() + private val alternativelyStyleSegmentsNotInLegCache: LruCache< - CacheResultUtils.CacheResultKey2< - Int, List, - List + CacheResultUtils.CacheResultKey3< + Int, List, Int, + List, >, - List> by lazy { LruCache(2) } + List> by lazy { LruCache(4) } + private var lastLocationPoint: Point? = null companion object { @@ -468,16 +472,15 @@ class MapboxRouteLineApi( ifNonNull(granularDistancesProvider(route)) { granularDistances -> if (routeLineOptions.styleInactiveRouteLegsIndependently) { val workingRouteLineExpressionData = - alternativelyStyleSegmentsNotInLeg(activeLegIndex, routeLineExpressionData) - - val restrictedExpressionData: List? = - if (routeLineOptions.displayRestrictedRoadSections && - MapboxRouteLineUtils.routeHasRestrictions(primaryRoute) - ) { - extractRouteRestrictionData(route) - } else { - null - } + alternativelyStyleSegmentsNotInLeg( + activeLegIndex, + routeLineExpressionData, + routeLineOptions + .resourceProvider + .routeLineColorResources + .inActiveRouteLegsColor + ) + routeLineOptions.vanishingRouteLine?.getTraveledRouteLineExpressions( point, granularDistances, @@ -513,7 +516,13 @@ class MapboxRouteLineApi( "alternative routes do not support dynamic updates yet" ) } - ExpectedFactory.createValue( + + val maskingLayerDynamicData = MapboxRouteLineUtils.getMaskingLayerDynamicData( + primaryRoute, + routeLineOptions.vanishingRouteLine?.vanishPointOffset ?: 0.0 + ) + + ExpectedFactory.createValue( RouteLineUpdateValue( primaryRouteLineDynamicData = RouteLineDynamicData( routeLineExpressionProviders.routeLineExpression, @@ -534,7 +543,8 @@ class MapboxRouteLineApi( alternativesProvider, alternativesProvider ) - ) + ), + routeLineMaskingLayerDynamicData = maskingLayerDynamicData ) ) } @@ -596,7 +606,14 @@ class MapboxRouteLineApi( routeLineOptions.vanishingRouteLine?.vanishPointOffset = offset return if (offset >= 0) { val workingExpressionData = if (routeLineOptions.styleInactiveRouteLegsIndependently) { - alternativelyStyleSegmentsNotInLeg(activeLegIndex, routeLineExpressionData) + alternativelyStyleSegmentsNotInLeg( + activeLegIndex, + routeLineExpressionData, + routeLineOptions + .resourceProvider + .routeLineColorResources + .inActiveRouteLegsColor + ) } else { routeLineExpressionData } @@ -631,34 +648,34 @@ class MapboxRouteLineApi( ) } - val restrictedLineExpressionProvider = - ifNonNull(primaryRoute) { route -> - if ( - routeLineOptions.displayRestrictedRoadSections && - MapboxRouteLineUtils.routeHasRestrictions(primaryRoute) - ) { - { - val routeData = extractRouteRestrictionData(route) - MapboxRouteLineUtils.getRestrictedLineExpression( - offset, - activeLegIndex, - routeLineOptions - .resourceProvider - .routeLineColorResources - .restrictedRoadColor, - routeData - ) - } - } else { - null + val restrictedLineExpressionProvider = when (restrictedExpressionData.isEmpty()) { + true -> null + false -> { + { + MapboxRouteLineUtils.getRestrictedLineExpression( + offset, + activeLegIndex, + routeLineOptions + .resourceProvider + .routeLineColorResources + .restrictedRoadColor, + restrictedExpressionData + ) } } + } val alternativesProvider = { throw UnsupportedOperationException( "alternative routes do not support dynamic updates yet" ) } + + val maskingLayerDynamicData = MapboxRouteLineUtils.getMaskingLayerDynamicData( + primaryRoute, + offset + ) + ExpectedFactory.createValue( RouteLineUpdateValue( primaryRouteLineDynamicData = RouteLineDynamicData( @@ -680,7 +697,8 @@ class MapboxRouteLineApi( alternativesProvider, alternativesProvider ) - ) + ), + routeLineMaskingLayerDynamicData = maskingLayerDynamicData ) ) } else { @@ -694,9 +712,9 @@ class MapboxRouteLineApi( * Updates the state of the route line based on data in the [RouteProgress] passing a result * to the consumer that should be rendered by the [MapboxRouteLineView]. * - * If the vanishing route line feature and style inactive route legs independently - * features were not enabled in [MapboxRouteLineOptions], this method does not need to - * be called as it won't produce any updates. + * Calling this method and rendering the result is required in order to use the vanishing + * route line feature and/or to style inactive route legs independently and/or display multi-leg + * routes with the active leg appearing to overlap the inactive leg(s). * * This method will execute tasks on a background thread. * There is a cancel method which will cancel the background tasks. @@ -730,34 +748,166 @@ class MapboxRouteLineApi( updateUpcomingRoutePointIndex(routeProgress) updateVanishingPointState(routeProgress.currentState) + val routeLineMaskingLayerDynamicData = when ( + (routeProgress.currentLegProgress?.legIndex ?: INVALID_ACTIVE_LEG_INDEX) > + activeLegIndex + ) { + true -> getRouteLineDynamicDataForMaskingLayers(currentPrimaryRoute, routeProgress) + false -> null + } + activeLegIndex = routeProgress.currentLegProgress?.legIndex ?: INVALID_ACTIVE_LEG_INDEX + // If the de-emphasize inactive route legs feature is enabled and the vanishing route line // feature is enabled and the active leg index has changed, then calling the // alternativelyStyleSegmentsNotInLeg() method here will get the resulting calculation cached so // that calls to alternativelyStyleSegmentsNotInLeg() made by updateTraveledRouteLine() // won't have to wait for the result. The updateTraveledRouteLine method is much // more time sensitive. - if (routeLineOptions.styleInactiveRouteLegsIndependently) { - when (routeLineOptions.vanishingRouteLine) { - // If the styleInactiveRouteLegsIndependently feature is enabled but the - // vanishingRouteLine feature is not enabled then side effects are generated and - // need to be rendered. - null -> highlightActiveLeg(routeProgress, consumer) - else -> { - ifNonNull(routeProgress.currentLegProgress) { routeLegProgress -> - if (routeLegProgress.legIndex > activeLegIndex) { - jobControl.scope.launch(Dispatchers.Main) { - mutex.withLock { - alternativelyStyleSegmentsNotInLeg( - routeLegProgress.legIndex, - routeLineExpressionData - ) - activeLegIndex = routeLegProgress.legIndex + when { + routeLineOptions.styleInactiveRouteLegsIndependently -> { + when (routeLineOptions.vanishingRouteLine) { + // If the styleInactiveRouteLegsIndependently feature is enabled but the + // vanishingRouteLine feature is not enabled then side effects are generated and + // need to be rendered. + null -> highlightActiveLeg( + routeProgress, + routeLineMaskingLayerDynamicData, + consumer + ) + else -> { + ifNonNull(routeProgress.currentLegProgress) { routeLegProgress -> + if (routeLegProgress.legIndex > activeLegIndex) { + jobControl.scope.launch(Dispatchers.Main) { + mutex.withLock { + alternativelyStyleSegmentsNotInLeg( + routeLegProgress.legIndex, + routeLineExpressionData, + routeLineOptions + .resourceProvider + .routeLineColorResources + .inActiveRouteLegsColor + ) + } } } } + provideRouteLegUpdate(routeLineMaskingLayerDynamicData, consumer) } } } + else -> provideRouteLegUpdate(routeLineMaskingLayerDynamicData, consumer) + } + } + + private fun provideRouteLegUpdate( + routeLineOverlayDynamicData: RouteLineDynamicData?, + consumer: MapboxNavigationConsumer> + ) { + ifNonNull(routeLineOverlayDynamicData) { maskingLayerData -> + getRouteDrawData { expected -> + expected.value?.apply { + consumer.accept( + ExpectedFactory.createValue( + RouteLineUpdateValue( + this.primaryRouteLineData.dynamicData, + this.alternativeRouteLinesData.map { it.dynamicData }, + maskingLayerData + ) + ) + ) + } + } + } + } + + internal fun getRouteLineDynamicDataForMaskingLayers( + segments: List, + legIndex: Int + ): RouteLineDynamicData { + val alteredSegments = alternativelyStyleSegmentsNotInLeg( + legIndex, + segments, + Color.TRANSPARENT + ) + val trafficExp = MapboxRouteLineUtils.getTrafficLineExpression( + 0.0, + Color.TRANSPARENT, + routeLineOptions + .resourceProvider + .routeLineColorResources + .routeUnknownCongestionColor, + alteredSegments + ) + val mainExp = MapboxRouteLineUtils.getRouteLineExpression( + 0.0, + segments, + Color.TRANSPARENT, + routeLineOptions.resourceProvider.routeLineColorResources.routeDefaultColor, + Color.TRANSPARENT, + legIndex + ) + val casingExp = MapboxRouteLineUtils.getRouteLineExpression( + 0.0, + segments, + Color.TRANSPARENT, + routeLineOptions.resourceProvider.routeLineColorResources.routeCasingColor, + Color.TRANSPARENT, + legIndex + ) + val trailExp = MapboxRouteLineUtils.getRouteLineExpression( + 0.0, + segments, + Color.TRANSPARENT, + routeLineOptions.resourceProvider.routeLineColorResources.routeLineTraveledColor, + Color.TRANSPARENT, + legIndex + ) + val trailCasingExp = MapboxRouteLineUtils.getRouteLineExpression( + 0.0, + segments, + Color.TRANSPARENT, + routeLineOptions + .resourceProvider + .routeLineColorResources + .routeLineTraveledCasingColor, + Color.TRANSPARENT, + legIndex + ) + return RouteLineDynamicData( + { mainExp }, + { casingExp }, + { trafficExp }, + restrictedSectionExpressionProvider = null, + trailExpressionProvider = { trailExp }, + trailCasingExpressionProvider = { trailCasingExp } + ) + } + + /* + December 2022 there was a feature request that the active leg of a route visually appear + above the inactive legs. The default behavior of the Map SDK at this time was that when + a line overlapped itself the later part of the line appears above the earlier part of the line. + A customer requested the opposite of this behavior. Specifically the active route leg should + appear above the inactive route leg(s). To achieve this result additional layers were added + to mask the primary route line. The section of the route line representing the inactive route + legs is made transparent revealing the primary route line. The section of the route line + representing the active leg is left opaque masking that section of the primary route line. + These masking layers are kept above the layers used for the primary route line, sharing the + same source data. The route only has one leg the additional gradient calculations aren't + performed and the masking layers are transparent. + */ + internal fun getRouteLineDynamicDataForMaskingLayers( + route: NavigationRoute?, + routeProgress: RouteProgress + ): RouteLineDynamicData? { + return ifNonNull(route, routeProgress.currentLegProgress) { navRoute, currentLegProgress -> + val numLegs = navRoute.directionsRoute.legs()?.size ?: 0 + val legIndex = currentLegProgress.legIndex + if (numLegs > 1 && legIndex < numLegs) { + getRouteLineDynamicDataForMaskingLayers(routeLineExpressionData, legIndex) + } else { + null + } } } @@ -773,6 +923,7 @@ class MapboxRouteLineApi( */ private fun highlightActiveLeg( routeProgress: RouteProgress, + maskingLayerData: RouteLineDynamicData?, consumer: MapboxNavigationConsumer> ) { when (routeProgress.currentLegProgress) { @@ -789,6 +940,7 @@ class MapboxRouteLineApi( else -> { showRouteWithLegIndexHighlighted( routeProgress.currentLegProgress!!.legIndex, + maskingLayerData, consumer ) } @@ -815,6 +967,14 @@ class MapboxRouteLineApi( fun showRouteWithLegIndexHighlighted( legIndexToHighlight: Int, consumer: MapboxNavigationConsumer> + ) { + showRouteWithLegIndexHighlighted(legIndexToHighlight, null, consumer) + } + + private fun showRouteWithLegIndexHighlighted( + legIndexToHighlight: Int, + maskingLayerData: RouteLineDynamicData?, + consumer: MapboxNavigationConsumer> ) { jobControl.scope.launch(Dispatchers.Main) { mutex.withLock { @@ -822,7 +982,11 @@ class MapboxRouteLineApi( if (legIndexToHighlight in 0..routeLegs.lastIndex) { val updatedRouteData = alternativelyStyleSegmentsNotInLeg( legIndexToHighlight, - routeLineExpressionData + routeLineExpressionData, + routeLineOptions + .resourceProvider + .routeLineColorResources + .inActiveRouteLegsColor ) val routeLineExpressionProvider = { MapboxRouteLineUtils.getRouteLineExpression( @@ -869,26 +1033,22 @@ class MapboxRouteLineApi( ) } - val restrictedLineExpressionProvider = if ( - routeLineOptions.displayRestrictedRoadSections && - MapboxRouteLineUtils.routeHasRestrictions(primaryRoute) - ) { - ifNonNull(primaryRoute) { route -> - { - val extractedRouteData = extractRouteRestrictionData(route) - getRestrictedLineExpressionProducer( - extractedRouteData, - 0.0, - legIndexToHighlight, - routeLineOptions - .resourceProvider - .routeLineColorResources - ) + val restrictedLineExpressionProvider = + when (restrictedExpressionData.isEmpty()) { + true -> null + false -> { + { + getRestrictedLineExpressionProducer( + restrictedExpressionData, + 0.0, + legIndexToHighlight, + routeLineOptions + .resourceProvider + .routeLineColorResources + ) + } } } - } else { - null - } val alternativesProvider = { throw UnsupportedOperationException( @@ -916,7 +1076,8 @@ class MapboxRouteLineApi( alternativesProvider, alternativesProvider ) - ) + ), + routeLineMaskingLayerDynamicData = maskingLayerData ) ) } else { @@ -1124,6 +1285,7 @@ class MapboxRouteLineApi( routes.addAll(distinctNewRoutes) primaryRoute = distinctNewRoutes.firstOrNull() MapboxRouteLineUtils.trimRouteDataCacheToSize(size = distinctNewRoutes.size) + activeLegIndex = INVALID_ACTIVE_LEG_INDEX preWarmRouteCaches( distinctNewRoutes, @@ -1154,6 +1316,12 @@ class MapboxRouteLineApi( } } } + + restrictedExpressionData = if (routeLineOptions.displayRestrictedRoadSections) { + extractRouteRestrictionData(routes.first(), granularDistancesProvider) + } else { + listOf() + } } private suspend fun buildDrawRoutesState( @@ -1207,9 +1375,10 @@ class MapboxRouteLineApi( routeLineOptions.resourceProvider.routeLineColorResources.alternativeRouteCasingColor ) - val alternative2PercentageTraveled = partitionedRoutes.second.getOrNull(1)?.route?.run { - alternativesDeviationOffset[this.id] - } ?: 0.0 + val alternative2PercentageTraveled = + partitionedRoutes.second.getOrNull(1)?.route?.run { + alternativesDeviationOffset[this.id] + } ?: 0.0 val alternateRoute2LineColors = getMatchingColors( partitionedRoutes.second.getOrNull(1)?.featureCollection, @@ -1264,9 +1433,8 @@ class MapboxRouteLineApi( val primaryRouteRestrictedSectionsExpressionDef = jobControl.scope.async { partitionedRoutes.first.firstOrNull()?.route?.run { if (routeLineOptions.displayRestrictedRoadSections) { - val extractedRouteData = extractRouteRestrictionData(this) getRestrictedLineExpressionProducer( - extractedRouteData, + restrictedExpressionData, vanishingPointOffset = 0.0, activeLegIndex = activeLegIndex, routeLineOptions.resourceProvider.routeLineColorResources @@ -1302,31 +1470,52 @@ class MapboxRouteLineApi( } val wayPointsFeatureCollection = wayPointsFeatureCollectionDef.await() + val primaryRouteTrafficLineExpressionProducer = + ifNonNull(primaryRouteTrafficLineExpressionDef.await()) { exp -> + RouteLineExpressionProvider { exp } + } + // The RouteLineExpressionData is only needed if the vanishing route line feature - // or styleInactiveRouteLegsIndependently feature are enabled. - if ( + // or styleInactiveRouteLegsIndependently feature are enabled + // or the route has multiple legs so that the masking layers are correctly handled. + // Calling this after primaryRouteTrafficLineExpressionProducer has finished ensures + // the cashed calculations for the primary route line can be fully taken advantage of + // below. Both use the result of MapboxRouteLineUtils.calculateRouteLineSegments. + val segmentsDef: Deferred>? = if ( routeLineOptions.vanishingRouteLine != null || - routeLineOptions.styleInactiveRouteLegsIndependently + routeLineOptions.styleInactiveRouteLegsIndependently || + (partitionedRoutes.first.firstOrNull()?.route?.directionsRoute?.legs()?.size ?: 0) > 1 ) { - jobControl.scope.launch(Dispatchers.Main) { - val segmentsDef = jobControl.scope.async { - partitionedRoutes.first.firstOrNull()?.route?.run { - MapboxRouteLineUtils.calculateRouteLineSegments( - this, - trafficBackfillRoadClasses, - isPrimaryRoute = true, - routeLineOptions.resourceProvider.routeLineColorResources - ) - } ?: listOf() - } - routeLineExpressionData = segmentsDef.await() + jobControl.scope.async { + partitionedRoutes.first.firstOrNull()?.route?.run { + MapboxRouteLineUtils.calculateRouteLineSegments( + this, + trafficBackfillRoadClasses, + isPrimaryRoute = true, + routeLineOptions.resourceProvider.routeLineColorResources + ) + } ?: listOf() } + } else { + null } - - val primaryRouteTrafficLineExpressionProducer = - ifNonNull(primaryRouteTrafficLineExpressionDef.await()) { exp -> - RouteLineExpressionProvider { exp } + // If the route has multiple legs the route line expression data is needed immediately + // to draw the masking line correctly. If not the calculation can be deferred. + segmentsDef?.let { deferred -> + when ( + (partitionedRoutes.first.firstOrNull()?.route?.directionsRoute?.legs()?.size ?: 0) + > 1 + ) { + true -> { + routeLineExpressionData = deferred.await() + } + false -> { + jobControl.scope.launch(Dispatchers.Main) { + routeLineExpressionData = deferred.await() + } + } } + } val alternateRoute1TrafficExpressionProducer = ifNonNull(alternateRoute1TrafficExpressionDef.await()) { exp -> @@ -1480,6 +1669,30 @@ class MapboxRouteLineApi( RouteLineExpressionProvider { exp } } + val maskingLayerData = if ((primaryRoute?.directionsRoute?.legs()?.size ?: 0) > 1) { + activeLegIndex = if (activeLegIndex == INVALID_ACTIVE_LEG_INDEX) { + 0 + } else { + activeLegIndex + } + getRouteLineDynamicDataForMaskingLayers(routeLineExpressionData, activeLegIndex) + } else { + val exp = MapboxRouteLineUtils.getRouteLineExpression( + 1.0, + Color.TRANSPARENT, + Color.TRANSPARENT + ) + RouteLineDynamicData( + baseExpressionProvider = { exp }, + casingExpressionProvider = { exp }, + trafficExpressionProvider = { exp }, + restrictedSectionExpressionProvider = null, + trimOffset = RouteLineTrimOffset(vanishingPointOffset), + trailExpressionProvider = { exp }, + trailCasingExpressionProvider = { exp }, + ) + } + return ExpectedFactory.createValue( RouteSetValue( primaryRouteLineData = RouteLineData( @@ -1521,6 +1734,7 @@ class MapboxRouteLineApi( ) ), wayPointsFeatureCollection, + maskingLayerData ) ) } @@ -1532,17 +1746,15 @@ class MapboxRouteLineApi( internal val alternativelyStyleSegmentsNotInLeg: ( activeLegIndex: Int, - segments: List + segments: List, + substitutionColor: Int ) -> List = - { activeLegIndex: Int, segments: List -> + { activeLegIndex: Int, segments: List, substitutionColor: Int -> segments.parallelMap( { if (it.legIndex != activeLegIndex) { it.copyWithNewSegmentColor( - newSegmentColor = routeLineOptions - .resourceProvider - .routeLineColorResources - .inActiveRouteLegsColor + newSegmentColor = substitutionColor ) } else { it diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineView.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineView.kt index e73293c50dd..c843eb7cca9 100644 --- a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineView.kt +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineView.kt @@ -2,6 +2,7 @@ package com.mapbox.navigation.ui.maps.route.line.api import androidx.annotation.UiThread import com.mapbox.bindgen.Expected +import com.mapbox.common.toValue import com.mapbox.geojson.FeatureCollection import com.mapbox.maps.LayerPosition import com.mapbox.maps.Style @@ -18,6 +19,7 @@ import com.mapbox.navigation.ui.maps.internal.route.line.MapboxRouteLineUtils.ge import com.mapbox.navigation.ui.maps.internal.route.line.MapboxRouteLineUtils.layerGroup1SourceLayerIds import com.mapbox.navigation.ui.maps.internal.route.line.MapboxRouteLineUtils.layerGroup2SourceLayerIds import com.mapbox.navigation.ui.maps.internal.route.line.MapboxRouteLineUtils.layerGroup3SourceLayerIds +import com.mapbox.navigation.ui.maps.internal.route.line.MapboxRouteLineUtils.maskingLayerIds import com.mapbox.navigation.ui.maps.internal.route.line.MapboxRouteLineUtils.sourceLayerMap import com.mapbox.navigation.ui.maps.route.RouteLayerConstants import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_1_CASING @@ -38,6 +40,11 @@ import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_RES import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_TRAFFIC import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_TRAIL import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_TRAIL_CASING +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_CASING +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_MAIN +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_TRAFFIC +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_TRAIL +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_TRAIL_CASING import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineOptions import com.mapbox.navigation.ui.maps.route.line.model.RouteLineClearValue import com.mapbox.navigation.ui.maps.route.line.model.RouteLineData @@ -93,27 +100,32 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { private val trailCasingLayerIds = setOf( LAYER_GROUP_1_TRAIL_CASING, LAYER_GROUP_2_TRAIL_CASING, - LAYER_GROUP_3_TRAIL_CASING + LAYER_GROUP_3_TRAIL_CASING, + MASKING_LAYER_TRAIL_CASING ) private val trailLayerIds = setOf( LAYER_GROUP_1_TRAIL, LAYER_GROUP_2_TRAIL, - LAYER_GROUP_3_TRAIL + LAYER_GROUP_3_TRAIL, + MASKING_LAYER_TRAIL ) private val casingLayerIds = setOf( LAYER_GROUP_1_CASING, LAYER_GROUP_2_CASING, - LAYER_GROUP_3_CASING + LAYER_GROUP_3_CASING, + MASKING_LAYER_CASING ) private val mainLayerIds = setOf( LAYER_GROUP_1_MAIN, LAYER_GROUP_2_MAIN, - LAYER_GROUP_3_MAIN + LAYER_GROUP_3_MAIN, + MASKING_LAYER_MAIN ) private val trafficLayerIds = setOf( LAYER_GROUP_1_TRAFFIC, LAYER_GROUP_2_TRAFFIC, - LAYER_GROUP_3_TRAFFIC + LAYER_GROUP_3_TRAFFIC, + MASKING_LAYER_TRAFFIC ) private val restrictedLayerIds = setOf( LAYER_GROUP_1_RESTRICTED, @@ -198,9 +210,21 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { val gradientCommands = getGradientUpdateCommands( style, sourceKeyFeaturePair.first, - routeLineData, + routeLineData.dynamicData, sourceLayerMap ).reversed() + + val maskingLayerGradientCommands = when (index) { + 0 -> ifNonNull(routeSetValue.routeLineMaskingLayerDynamicData) { + getGradientUpdateCommands( + style, + maskingLayerIds, + it + ) + } ?: listOf() + else -> listOf() + } + // Ignoring the trim offsets if the source was updated with an // empty feature collection. Even though the call to update the // source was made first the trim offset commands seem to get @@ -208,13 +232,37 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { // undesirable flash on the screen. val trimOffsetCommands = when (sourceKeyFeaturePair.second.id()) { null -> listOf() - else -> getTrimOffsetCommands( - style, - sourceKeyFeaturePair.first, - routeLineData, - sourceLayerMap - ) + else -> { + val maskingTrimCommands = if (index == 0) { + listOf( + createTrimOffsetCommand( + routeSetValue.routeLineMaskingLayerDynamicData, + MASKING_LAYER_CASING, + style + ), + createTrimOffsetCommand( + routeSetValue.routeLineMaskingLayerDynamicData, + MASKING_LAYER_MAIN, + style + ), + createTrimOffsetCommand( + routeSetValue.routeLineMaskingLayerDynamicData, + MASKING_LAYER_TRAFFIC, + style + ) + ) + } else { + listOf() + } + getTrimOffsetCommands( + style, + sourceKeyFeaturePair.first, + routeLineData, + sourceLayerMap + ).plus(maskingTrimCommands) + } } + val layerMoveCommand = if (index == 0) { { moveLayersUp( @@ -224,7 +272,11 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { ) } } else { {} } - listOf(layerMoveCommand).plus(trimOffsetCommands).plus(gradientCommands) + + listOf(layerMoveCommand) + .plus(trimOffsetCommands) + .plus(gradientCommands) + .plus(maskingLayerGradientCommands) } ?: listOf() } }).flatten().forEach { mutationCommand -> @@ -245,27 +297,28 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { // routes = showing. Only when an API call to show the primary route is made // should the primary route line become visible. The snippet below adjusts // the layer visibility to maintain that state. - val trafficLayerIds = setOf( - LAYER_GROUP_1_TRAFFIC, - LAYER_GROUP_2_TRAFFIC, - LAYER_GROUP_3_TRAFFIC + adjustLayerVisibility( + style, + primaryRouteTrafficVisibility, + primaryRouteVisibility, + alternativeRouteVisibility ) - sourceLayerMap.values.flatten().map { layerID -> - when (layerID in trafficLayerIds) { - true -> when (primaryRouteLineLayerGroup.contains(layerID)) { - true -> Pair(layerID, primaryRouteTrafficVisibility) - false -> Pair(layerID, alternativeRouteVisibility) - } - false -> when (primaryRouteLineLayerGroup.contains(layerID)) { - true -> Pair(layerID, primaryRouteVisibility) - false -> Pair(layerID, alternativeRouteVisibility) - } - } - }.forEach { - ifNonNull(it.second) { visibility -> - updateLayerVisibility(style, it.first, visibility) + adjustMaskingLayersVisibility( + style, + primaryRouteTrafficVisibility, + primaryRouteVisibility + ) + + val updateMaskingLayerSourceCommands = + getSourceKeyForPrimaryRoute(style).getOrNull()?.run { + getMaskingLayerSourceSetCommands(style, this.sourceId) + } ?: listOf() + + val maskingLayerMoveCommands = getMaskingLayerMoveCommands(style) + updateMaskingLayerSourceCommands.plus(maskingLayerMoveCommands) + .forEach { mutationCommand -> + mutationCommand() } - } } } } @@ -296,6 +349,40 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { }.forEach { updateFun -> updateFun(style) } + + ifNonNull(it.routeLineMaskingLayerDynamicData) { overlayData -> + overlayData.trafficExpressionProvider?.apply { + getExpressionUpdateFun(MASKING_LAYER_TRAFFIC, this)(style) + } + getExpressionUpdateFun( + MASKING_LAYER_MAIN, + overlayData.baseExpressionProvider + )(style) + getExpressionUpdateFun( + MASKING_LAYER_CASING, + overlayData.casingExpressionProvider + )(style) + getExpressionUpdateFun( + MASKING_LAYER_TRAIL, + overlayData.trailExpressionProvider + )(style) + getExpressionUpdateFun( + MASKING_LAYER_TRAIL_CASING, + overlayData.trailCasingExpressionProvider + )(style) + + // This method (renderRouteLineUpdate) is called every time the puck movement is updated and + // on route progress updates. It's not necessary to move the layers + // on trim offset updates. Checking the kind of update that has come in + // saves resources. + if ( + overlayData.baseExpressionProvider !is RouteLineTrimExpressionProvider + ) { + getMaskingLayerMoveCommands(style).forEach { mutationCommand -> + mutationCommand() + } + } + } } } } @@ -381,8 +468,12 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { fun showPrimaryRoute(style: Style) { jobControl.scope.launch(Dispatchers.Main) { mutex.withLock { - getLayerIdsForPrimaryRoute(primaryRouteLineLayerGroup, sourceLayerMap, style) - .forEach { updateLayerVisibility(style, it, Visibility.VISIBLE) } + getLayerIdsForPrimaryRoute( + primaryRouteLineLayerGroup, + sourceLayerMap, + style + ).plus(maskingLayerIds) + .forEach { adjustLayerVisibility(style, it, Visibility.VISIBLE) } } } } @@ -395,8 +486,12 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { fun hidePrimaryRoute(style: Style) { jobControl.scope.launch(Dispatchers.Main) { mutex.withLock { - getLayerIdsForPrimaryRoute(primaryRouteLineLayerGroup, sourceLayerMap, style) - .forEach { updateLayerVisibility(style, it, Visibility.NONE) } + getLayerIdsForPrimaryRoute( + primaryRouteLineLayerGroup, + sourceLayerMap, + style + ).plus(maskingLayerIds) + .forEach { adjustLayerVisibility(style, it, Visibility.NONE) } } } } @@ -415,7 +510,7 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { .union(layerGroup2SourceLayerIds) .union(layerGroup3SourceLayerIds) .subtract(primaryRouteLineLayers).forEach { - updateLayerVisibility(style, it, Visibility.VISIBLE) + adjustLayerVisibility(style, it, Visibility.VISIBLE) } } } @@ -435,7 +530,7 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { .union(layerGroup2SourceLayerIds) .union(layerGroup3SourceLayerIds) .subtract(primaryRouteLineLayers).forEach { - updateLayerVisibility(style, it, Visibility.NONE) + adjustLayerVisibility(style, it, Visibility.NONE) } } } @@ -452,9 +547,10 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { layerGroup1SourceLayerIds .union(layerGroup2SourceLayerIds) .union(layerGroup3SourceLayerIds) + .union(maskingLayerIds) .filter { it in trafficLayerIds } .forEach { layerId -> - updateLayerVisibility(style, layerId, Visibility.NONE) + adjustLayerVisibility(style, layerId, Visibility.NONE) } } } @@ -471,9 +567,10 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { layerGroup1SourceLayerIds .union(layerGroup2SourceLayerIds) .union(layerGroup3SourceLayerIds) + .union(maskingLayerIds) .filter { it in trafficLayerIds } .forEach { layerId -> - updateLayerVisibility(style, layerId, Visibility.VISIBLE) + adjustLayerVisibility(style, layerId, Visibility.VISIBLE) } } } @@ -545,7 +642,7 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { fun showOriginAndDestinationPoints(style: Style) { jobControl.scope.launch(Dispatchers.Main) { mutex.withLock { - updateLayerVisibility( + adjustLayerVisibility( style, RouteLayerConstants.WAYPOINT_LAYER_ID, Visibility.VISIBLE @@ -562,7 +659,7 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { fun hideOriginAndDestinationPoints(style: Style) { jobControl.scope.launch(Dispatchers.Main) { mutex.withLock { - updateLayerVisibility(style, RouteLayerConstants.WAYPOINT_LAYER_ID, Visibility.NONE) + adjustLayerVisibility(style, RouteLayerConstants.WAYPOINT_LAYER_ID, Visibility.NONE) } } } @@ -574,7 +671,7 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { jobControl.job.cancelChildren() } - private fun updateLayerVisibility(style: Style, layerId: String, visibility: Visibility) { + private fun adjustLayerVisibility(style: Style, layerId: String, visibility: Visibility) { if (style.styleLayerExists(layerId)) { style.getLayer(layerId)?.visibility(visibility) } @@ -660,87 +757,208 @@ class MapboxRouteLineView(var options: MapboxRouteLineOptions) { routeLineData: RouteLineData, sourceLayerMap: Map> ): List<() -> Unit> { - val mutationCommands = mutableListOf<() -> Unit>() val trailLayerIds = trailCasingLayerIds.plus(trailLayerIds) - sourceLayerMap[routeLineSourceKey]?.filter { !trailLayerIds.contains(it) } - ?.forEach { layerId -> - val provider = ifNonNull(routeLineData.dynamicData.trimOffset?.offset) { offset -> - RouteLineExpressionProvider { - literal(listOf(0.0, offset)) - } - } + return sourceLayerMap[routeLineSourceKey]?.filter { !trailLayerIds.contains(it) } + ?.map { + createTrimOffsetCommand(routeLineData.dynamicData, it, style) + } ?: listOf() + } + + private fun createTrimOffsetCommand( + dynamicData: RouteLineDynamicData?, + layerId: String, + style: Style + ): () -> Unit { + val provider = ifNonNull(dynamicData, dynamicData?.trimOffset?.offset) { _, offset -> + RouteLineExpressionProvider { + literal(listOf(0.0, offset)) + } + } + + return { updateTrimOffset(layerId, provider)(style) } + } + + private fun getGradientUpdateCommands( + style: Style, + layerIds: Set, + routeLineData: RouteLineDynamicData, + ): List<() -> Unit> { + return layerIds.map { + when (it) { + in trailCasingLayerIds -> Pair(it, routeLineData.trailCasingExpressionProvider) + in trailLayerIds -> Pair(it, routeLineData.trailExpressionProvider) + in casingLayerIds -> Pair(it, routeLineData.casingExpressionProvider) + in mainLayerIds -> Pair(it, routeLineData.baseExpressionProvider) + in trafficLayerIds -> Pair(it, routeLineData.trafficExpressionProvider) + in restrictedLayerIds -> Pair(it, routeLineData.restrictedSectionExpressionProvider) + else -> null + } + }.filter { it?.second != null }.map { + updateGradientCmd( + style, + it!!.second, + it.first + ) + } + } + + private fun getMaskingLayerMoveCommands(style: Style): List<() -> Unit> { + val commands = mutableListOf<() -> Unit>() + val topRestrictedLayer = primaryRouteLineLayerGroup.firstOrNull { + it == LAYER_GROUP_1_RESTRICTED || + it == LAYER_GROUP_2_RESTRICTED || + it == LAYER_GROUP_3_RESTRICTED + } + val belowLayerIdToUse = when { + topRestrictedLayer == null -> RouteLayerConstants.TOP_LEVEL_ROUTE_LINE_LAYER_ID + style.styleLayerExists(topRestrictedLayer) -> topRestrictedLayer + else -> RouteLayerConstants.TOP_LEVEL_ROUTE_LINE_LAYER_ID + } + + commands.add { + style.moveStyleLayer( + MASKING_LAYER_TRAIL_CASING, + LayerPosition(null, belowLayerIdToUse, null) + ) + } + commands.add { + style.moveStyleLayer(MASKING_LAYER_TRAIL, LayerPosition(null, belowLayerIdToUse, null)) + } + commands.add { + style.moveStyleLayer(MASKING_LAYER_CASING, LayerPosition(null, belowLayerIdToUse, null)) + } + commands.add { + style.moveStyleLayer(MASKING_LAYER_MAIN, LayerPosition(null, belowLayerIdToUse, null)) + } + commands.add { + style.moveStyleLayer( + MASKING_LAYER_TRAFFIC, + LayerPosition(null, belowLayerIdToUse, null) + ) + } + + return commands + } - mutationCommands.add { updateTrimOffset(layerId, provider)(style) } + private fun getMaskingSourceId(style: Style): String? { + return try { + style.getStyleLayerProperty( + MASKING_LAYER_TRAFFIC, + "source" + ).value.contents as String + } catch (ex: Exception) { + null + } + } + + private fun getMaskingLayerSourceSetCommands(style: Style, sourceId: String): List<() -> Unit> { + val commands = mutableListOf<() -> Unit>() + val maskingLayerSourceId = getMaskingSourceId(style) + if (sourceId != maskingLayerSourceId) { + commands.add { + style.setStyleLayerProperty( + MASKING_LAYER_TRAFFIC, + "source", + sourceId.toValue() + ) + } + commands.add { + style.setStyleLayerProperty( + MASKING_LAYER_MAIN, + "source", + sourceId.toValue() + ) + } + commands.add { + style.setStyleLayerProperty( + MASKING_LAYER_CASING, + "source", + sourceId.toValue() + ) + } + commands.add { + style.setStyleLayerProperty( + MASKING_LAYER_TRAIL, + "source", + sourceId.toValue() + ) } - return mutationCommands + commands.add { + style.setStyleLayerProperty( + MASKING_LAYER_TRAIL_CASING, + "source", + sourceId.toValue() + ) + } + } + return commands } private fun getGradientUpdateCommands( style: Style, routeLineSourceKey: RouteLineSourceKey?, - routeLineData: RouteLineData, + routeLineData: RouteLineDynamicData, sourceLayerMap: Map> ): List<() -> Unit> { - val mutationCommands = mutableListOf<() -> Unit>() - sourceLayerMap[routeLineSourceKey]?.forEach { layerId -> - when (layerId) { - in trailCasingLayerIds -> { - mutationCommands.add( - updateGradientCmd( - style, - routeLineData.dynamicData.trailCasingExpressionProvider, - layerId - ) - ) - } - in trailLayerIds -> { - mutationCommands.add( - updateGradientCmd( - style, - routeLineData.dynamicData.trailExpressionProvider, - layerId - ) - ) - } - in casingLayerIds -> { - mutationCommands.add( - updateGradientCmd( - style, - routeLineData.dynamicData.casingExpressionProvider, - layerId - ) - ) - } - in mainLayerIds -> { - mutationCommands.add( - updateGradientCmd( - style, - routeLineData.dynamicData.baseExpressionProvider, - layerId - ) - ) - } - in trafficLayerIds -> { - mutationCommands.add( - updateGradientCmd( - style, - routeLineData.dynamicData.trafficExpressionProvider, - layerId - ) - ) + return sourceLayerMap[routeLineSourceKey]?.run { + getGradientUpdateCommands( + style, + this, + routeLineData + ) + } ?: listOf() + } + + // Any layer group can host the primary route. If a call was made to + // hide the primary our alternative routes, that state needs to be maintained + // until a call is made to show the line(s). For example if there are 3 + // route lines showing on the map and a call is made to hide the primary route + // it's expected only the alternative routes are displayed. If a user then + // selects an alternative route in order to make it the primary route, that + // route line should then disappear and the previously hidden primary route line + // should appear since the state is: primary route line = hidden / alternative + // routes = showing. Only when an API call to show the primary route is made + // should the primary route line become visible. The snippet below adjusts + // the layer visibility to maintain that state. + private fun adjustLayerVisibility( + style: Style, + primaryRouteTrafficVisibility: Visibility?, + primaryRouteVisibility: Visibility?, + alternativeRouteVisibility: Visibility? + ) { + sourceLayerMap.values.flatten().map { layerID -> + when (layerID in trafficLayerIds) { + true -> when (primaryRouteLineLayerGroup.contains(layerID)) { + true -> Pair(layerID, primaryRouteTrafficVisibility) + false -> Pair(layerID, alternativeRouteVisibility) } - in restrictedLayerIds -> { - mutationCommands.add( - updateGradientCmd( - style, - routeLineData.dynamicData.restrictedSectionExpressionProvider, - layerId - ) - ) + false -> when (primaryRouteLineLayerGroup.contains(layerID)) { + true -> Pair(layerID, primaryRouteVisibility) + false -> Pair(layerID, alternativeRouteVisibility) } } + }.forEach { + ifNonNull(it.second) { visibility -> + adjustLayerVisibility(style, it.first, visibility) + } + } + } + + private fun adjustMaskingLayersVisibility( + style: Style, + primaryRouteTrafficVisibility: Visibility?, + primaryRouteVisibility: Visibility? + ) { + primaryRouteTrafficVisibility?.apply { + adjustLayerVisibility(style, MASKING_LAYER_TRAFFIC, this) + } + primaryRouteVisibility?.apply { + adjustLayerVisibility(style, MASKING_LAYER_MAIN, this) + adjustLayerVisibility(style, MASKING_LAYER_CASING, this) + adjustLayerVisibility(style, MASKING_LAYER_TRAIL, this) + adjustLayerVisibility(style, MASKING_LAYER_TRAIL, this) + adjustLayerVisibility(style, MASKING_LAYER_TRAIL_CASING, this) } - return mutationCommands } private fun toExpressionUpdateFun( diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/model/RouteLineUpdateValue.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/model/RouteLineUpdateValue.kt index f0780b8dc92..301e6c3344a 100644 --- a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/model/RouteLineUpdateValue.kt +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/model/RouteLineUpdateValue.kt @@ -5,10 +5,12 @@ package com.mapbox.navigation.ui.maps.route.line.model * * @param primaryRouteLineDynamicData the data describing the primary route line * @param alternativeRouteLinesDynamicData the data describing alternative route lines + * @param routeLineMaskingLayerDynamicData the data describing the masking layers */ class RouteLineUpdateValue internal constructor( val primaryRouteLineDynamicData: RouteLineDynamicData, - val alternativeRouteLinesDynamicData: List + val alternativeRouteLinesDynamicData: List, + val routeLineMaskingLayerDynamicData: RouteLineDynamicData? = null ) { /** @@ -16,7 +18,8 @@ class RouteLineUpdateValue internal constructor( */ fun toMutableValue() = MutableRouteLineUpdateValue( primaryRouteLineDynamicData, - alternativeRouteLinesDynamicData + alternativeRouteLinesDynamicData, + routeLineMaskingLayerDynamicData ) /** @@ -24,10 +27,12 @@ class RouteLineUpdateValue internal constructor( * * @param primaryRouteLineDynamicData the data describing the primary route line * @param alternativeRouteLinesDynamicData the data describing alternative route lines + * @param routeLineMaskingLayerDynamicData the data describing the masking layers */ class MutableRouteLineUpdateValue internal constructor( var primaryRouteLineDynamicData: RouteLineDynamicData, - var alternativeRouteLinesDynamicData: List + var alternativeRouteLinesDynamicData: List, + var routeLineMaskingLayerDynamicData: RouteLineDynamicData? = null ) { /** @@ -35,7 +40,8 @@ class RouteLineUpdateValue internal constructor( */ fun toImmutableValue() = RouteLineUpdateValue( primaryRouteLineDynamicData, - alternativeRouteLinesDynamicData + alternativeRouteLinesDynamicData, + routeLineMaskingLayerDynamicData ) } @@ -50,6 +56,7 @@ class RouteLineUpdateValue internal constructor( if (primaryRouteLineDynamicData != other.primaryRouteLineDynamicData) return false if (alternativeRouteLinesDynamicData != other.alternativeRouteLinesDynamicData) return false + if (routeLineMaskingLayerDynamicData != other.routeLineMaskingLayerDynamicData) return false return true } @@ -60,6 +67,7 @@ class RouteLineUpdateValue internal constructor( override fun hashCode(): Int { var result = primaryRouteLineDynamicData.hashCode() result = 31 * result + alternativeRouteLinesDynamicData.hashCode() + result = 31 * result + routeLineMaskingLayerDynamicData.hashCode() return result } @@ -69,7 +77,8 @@ class RouteLineUpdateValue internal constructor( override fun toString(): String { return "RouteLineUpdateValue(" + "primaryRouteLineDynamicData=$primaryRouteLineDynamicData, " + - "alternativeRouteLinesDynamicData=$alternativeRouteLinesDynamicData" + + "alternativeRouteLinesDynamicData=$alternativeRouteLinesDynamicData," + + "routeLineMaskingLayerDynamicData=$routeLineMaskingLayerDynamicData" + ")" } } diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/model/RouteSetValue.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/model/RouteSetValue.kt index 41afe4189a7..36d2eff5e3a 100644 --- a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/model/RouteSetValue.kt +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/line/model/RouteSetValue.kt @@ -9,11 +9,13 @@ import com.mapbox.maps.extension.style.expressions.generated.Expression * @param primaryRouteLineData the data of the primary route line * @param alternativeRouteLinesData the data of the alternative route lines * @param waypointsSource the feature collection for the origin and destination icons + * @param routeLineMaskingLayerDynamicData the data of the masking line */ class RouteSetValue internal constructor( val primaryRouteLineData: RouteLineData, val alternativeRouteLinesData: List, - val waypointsSource: FeatureCollection + val waypointsSource: FeatureCollection, + val routeLineMaskingLayerDynamicData: RouteLineDynamicData? = null ) { /** @@ -22,7 +24,8 @@ class RouteSetValue internal constructor( fun toMutableValue() = MutableRouteSetValue( primaryRouteLineData, alternativeRouteLinesData, - waypointsSource + waypointsSource, + routeLineMaskingLayerDynamicData ) /** @@ -31,11 +34,13 @@ class RouteSetValue internal constructor( * @param primaryRouteLineData the data of the primary route line * @param alternativeRouteLinesData the data of the alternative route lines * @param waypointsSource the feature collection for the origin and destination icons + * @param routeLineMaskingLayerDynamicData the data of the masking line */ class MutableRouteSetValue internal constructor( var primaryRouteLineData: RouteLineData, var alternativeRouteLinesData: List, - var waypointsSource: FeatureCollection + var waypointsSource: FeatureCollection, + var routeLineMaskingLayerDynamicData: RouteLineDynamicData? ) { /** @@ -44,7 +49,8 @@ class RouteSetValue internal constructor( fun toImmutableValue() = RouteSetValue( primaryRouteLineData, alternativeRouteLinesData, - waypointsSource + waypointsSource, + routeLineMaskingLayerDynamicData ) } } diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/util/CacheResultUtils.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/util/CacheResultUtils.kt index d32d0129f35..0b8707d7a83 100644 --- a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/util/CacheResultUtils.kt +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/util/CacheResultUtils.kt @@ -19,6 +19,11 @@ internal object CacheResultUtils { override fun invoke(f: (P1, P2) -> R) = f(p1, p2) } + data class CacheResultKey3(val p1: P1, val p2: P2, val p3: P3) : + CacheResultCall<(P1, P2, P3) -> R, R> { + override fun invoke(f: (P1, P2, P3) -> R) = f(p1, p2, p3) + } + /** * Specialized cache key that can be used for managing results of processing related to a route. * @@ -99,6 +104,19 @@ internal object CacheResultUtils { } } + fun ((P1, P2, P3) -> R).cacheResult( + cache: LruCache, R> + ): (P1, P2, P3) -> R { + return object : (P1, P2, P3) -> R { + private val handler = + CacheResultHandler<((P1, P2, P3) -> R), CacheResultKey3, R>( + this@cacheResult, + cache + ) + override fun invoke(p1: P1, p2: P2, p3: P3) = handler(CacheResultKey3(p1, p2, p3)) + } + } + fun ((NavigationRoute) -> R).cacheRouteResult( cache: LruCache, R> ): (NavigationRoute) -> R { diff --git a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/internal/route/line/MapboxRouteLineUtilsRoboTest.kt b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/internal/route/line/MapboxRouteLineUtilsRoboTest.kt index 5217aa6e071..8411bc83a14 100644 --- a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/internal/route/line/MapboxRouteLineUtilsRoboTest.kt +++ b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/internal/route/line/MapboxRouteLineUtilsRoboTest.kt @@ -49,6 +49,11 @@ import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_SOU import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_TRAFFIC import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_TRAIL import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_TRAIL_CASING +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_CASING +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_MAIN +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_TRAFFIC +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_TRAIL +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_TRAIL_CASING import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.ORIGIN_MARKER_NAME import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.TOP_LEVEL_ROUTE_LINE_LAYER_ID import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.WAYPOINT_LAYER_ID @@ -173,6 +178,21 @@ class MapboxRouteLineUtilsRoboTest { every { styleLayerExists(TOP_LEVEL_ROUTE_LINE_LAYER_ID) } returns false + every { + styleLayerExists(MASKING_LAYER_TRAIL_CASING) + } returns false + every { + styleLayerExists(MASKING_LAYER_TRAIL) + } returns false + every { + styleLayerExists(MASKING_LAYER_CASING) + } returns false + every { + styleLayerExists(MASKING_LAYER_MAIN) + } returns false + every { + styleLayerExists(MASKING_LAYER_TRAFFIC) + } returns false every { getStyleImage(ARROW_HEAD_ICON) } returns null every { getStyleImage(ARROW_HEAD_ICON_CASING) } returns null every { getStyleImage(ORIGIN_MARKER_NAME) } returns null @@ -399,39 +419,61 @@ class MapboxRouteLineUtilsRoboTest { "mapbox-layerGroup-1-traffic", (addStyleLayerSlots[17].contents as HashMap)["id"]!!.contents ) + assertEquals( - "mapbox-layerGroup-1-restricted", + "mapbox-masking-layer-trailCasing", (addStyleLayerSlots[18].contents as HashMap)["id"]!!.contents ) assertEquals( - "mapbox-top-level-route-layer", + "mapbox-masking-layer-trail", (addStyleLayerSlots[19].contents as HashMap)["id"]!!.contents ) assertEquals( - "mapbox-navigation-waypoint-layer", + "mapbox-masking-layer-casing", (addStyleLayerSlots[20].contents as HashMap)["id"]!!.contents ) + assertEquals( + "mapbox-masking-layer-main", + (addStyleLayerSlots[21].contents as HashMap)["id"]!!.contents + ) + assertEquals( + "mapbox-masking-layer-traffic", + (addStyleLayerSlots[22].contents as HashMap)["id"]!!.contents + ) + + assertEquals( + "mapbox-layerGroup-1-restricted", + (addStyleLayerSlots[23].contents as HashMap)["id"]!!.contents + ) + assertEquals( + "mapbox-top-level-route-layer", + (addStyleLayerSlots[24].contents as HashMap)["id"]!!.contents + ) + assertEquals( + "mapbox-navigation-waypoint-layer", + (addStyleLayerSlots[25].contents as HashMap)["id"]!!.contents + ) assertEquals( "bottom-right", - (addStyleLayerSlots[20].contents as HashMap)["icon-anchor"]!!.contents + (addStyleLayerSlots[25].contents as HashMap)["icon-anchor"]!!.contents ) assertEquals( 33.3, ( - (addStyleLayerSlots[20].contents as HashMap) + (addStyleLayerSlots[25].contents as HashMap) ["icon-offset"]!!.contents as ArrayList ).first().contents ) assertEquals( 44.4, ( - (addStyleLayerSlots[20].contents as HashMap) + (addStyleLayerSlots[25].contents as HashMap) ["icon-offset"]!!.contents as ArrayList ).component2().contents ) assertEquals( "viewport", - (addStyleLayerSlots[20].contents as HashMap) + (addStyleLayerSlots[25].contents as HashMap) ["icon-pitch-alignment"]!!.contents ) assertEquals( @@ -518,6 +560,26 @@ class MapboxRouteLineUtilsRoboTest { LocationComponentConstants.MODEL_LAYER, addStyleLayerPositionSlots[20].below ) + assertEquals( + LocationComponentConstants.MODEL_LAYER, + addStyleLayerPositionSlots[21].below + ) + assertEquals( + LocationComponentConstants.MODEL_LAYER, + addStyleLayerPositionSlots[22].below + ) + assertEquals( + LocationComponentConstants.MODEL_LAYER, + addStyleLayerPositionSlots[23].below + ) + assertEquals( + LocationComponentConstants.MODEL_LAYER, + addStyleLayerPositionSlots[24].below + ) + assertEquals( + LocationComponentConstants.MODEL_LAYER, + addStyleLayerPositionSlots[25].below + ) } @Test diff --git a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/internal/route/line/MapboxRouteLineUtilsTest.kt b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/internal/route/line/MapboxRouteLineUtilsTest.kt index c58892e4512..2fac070bb60 100644 --- a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/internal/route/line/MapboxRouteLineUtilsTest.kt +++ b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/internal/route/line/MapboxRouteLineUtilsTest.kt @@ -45,6 +45,11 @@ import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_TRA import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_TRAIL import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_TRAIL_CASING import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LOW_CONGESTION_VALUE +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_CASING +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_MAIN +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_TRAFFIC +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_TRAIL +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_TRAIL_CASING import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MODERATE_CONGESTION_VALUE import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.RESTRICTED_CONGESTION_VALUE import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.SEVERE_CONGESTION_VALUE @@ -54,6 +59,7 @@ import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.WAYPOINT_SOURCE_I import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineOptions import com.mapbox.navigation.ui.maps.route.line.model.RouteLineColorResources import com.mapbox.navigation.ui.maps.route.line.model.RouteLineExpressionData +import com.mapbox.navigation.ui.maps.route.line.model.RouteLineGranularDistances import com.mapbox.navigation.ui.maps.route.line.model.RouteLineScaleValue import com.mapbox.navigation.ui.maps.route.line.model.RouteStyleDescriptor import com.mapbox.navigation.ui.maps.testing.TestingUtil.loadNavigationRoute @@ -200,7 +206,10 @@ class MapboxRouteLineUtilsTest { fun getRestrictedSectionExpressionData() { val route = loadNavigationRoute("route-with-restrictions.json") - val result = MapboxRouteLineUtils.extractRouteRestrictionData(route) + val result = MapboxRouteLineUtils.extractRouteRestrictionData( + route, + MapboxRouteLineUtils.granularDistancesProvider + ) assertEquals(5, result.size) assertTrue(result[1].isInRestrictedSection) @@ -216,7 +225,10 @@ class MapboxRouteLineUtilsTest { "0.4677574367125704, [rgba, 0.0, 0.0, 0.0, 0.0], 0.5021643413784516, " + "[rgba, 0.0, 255.0, 255.0, 1.0], 0.5196445159361185, [rgba, 0.0, 0.0, 0.0, 0.0]]" val route = loadNavigationRoute("route-with-restrictions.json") - val expData = MapboxRouteLineUtils.extractRouteRestrictionData(route) + val expData = MapboxRouteLineUtils.extractRouteRestrictionData( + route, + MapboxRouteLineUtils.granularDistancesProvider + ) val expression = MapboxRouteLineUtils.getRestrictedLineExpression( 0.2, @@ -238,7 +250,10 @@ class MapboxRouteLineUtilsTest { "0.4677574367125704, [rgba, 0.0, 0.0, 0.0, 0.0], 0.5021643413784516, " + "[rgba, 0.0, 255.0, 255.0, 1.0], 0.5196445159361185, [rgba, 0.0, 0.0, 0.0, 0.0]]" val route = loadNavigationRoute("route-with-restrictions.json") - val expData = MapboxRouteLineUtils.extractRouteRestrictionData(route) + val expData = MapboxRouteLineUtils.extractRouteRestrictionData( + route, + MapboxRouteLineUtils.granularDistancesProvider + ) val expression = MapboxRouteLineUtils.getRestrictedLineExpressionProducer( expData, @@ -269,7 +284,10 @@ class MapboxRouteLineUtilsTest { val expectedExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.2, " + "[rgba, 0.0, 0.0, 0.0, 0.0]]" val route = loadNavigationRoute("short_route.json") - val expData = MapboxRouteLineUtils.extractRouteRestrictionData(route) + val expData = MapboxRouteLineUtils.extractRouteRestrictionData( + route, + MapboxRouteLineUtils.granularDistancesProvider + ) val expression = MapboxRouteLineUtils.getRestrictedLineExpression( 0.2, @@ -330,6 +348,11 @@ class MapboxRouteLineUtilsTest { every { styleLayerExists(LAYER_GROUP_3_RESTRICTED) } returns true every { styleLayerExists(TOP_LEVEL_ROUTE_LINE_LAYER_ID) } returns true every { styleLayerExists(BOTTOM_LEVEL_ROUTE_LINE_LAYER_ID) } returns true + every { styleLayerExists(MASKING_LAYER_TRAIL_CASING) } returns true + every { styleLayerExists(MASKING_LAYER_TRAIL) } returns true + every { styleLayerExists(MASKING_LAYER_CASING) } returns true + every { styleLayerExists(MASKING_LAYER_MAIN) } returns true + every { styleLayerExists(MASKING_LAYER_TRAFFIC) } returns true every { styleLayerExists(TOP_LEVEL_ROUTE_LINE_LAYER_ID) @@ -362,6 +385,11 @@ class MapboxRouteLineUtilsTest { verify { style.styleLayerExists(LAYER_GROUP_3_RESTRICTED) } verify { style.styleLayerExists(TOP_LEVEL_ROUTE_LINE_LAYER_ID) } verify { style.styleLayerExists(BOTTOM_LEVEL_ROUTE_LINE_LAYER_ID) } + verify { style.styleLayerExists(MASKING_LAYER_TRAIL_CASING) } + verify { style.styleLayerExists(MASKING_LAYER_TRAIL) } + verify { style.styleLayerExists(MASKING_LAYER_CASING) } + verify { style.styleLayerExists(MASKING_LAYER_MAIN) } + verify { style.styleLayerExists(MASKING_LAYER_TRAFFIC) } } @Test @@ -394,6 +422,11 @@ class MapboxRouteLineUtilsTest { every { styleLayerExists(TOP_LEVEL_ROUTE_LINE_LAYER_ID) } returns true every { styleLayerExists(BOTTOM_LEVEL_ROUTE_LINE_LAYER_ID) } returns true every { styleSourceExists(WAYPOINT_SOURCE_ID) } returns true + every { styleLayerExists(MASKING_LAYER_TRAIL_CASING) } returns true + every { styleLayerExists(MASKING_LAYER_TRAIL) } returns true + every { styleLayerExists(MASKING_LAYER_CASING) } returns true + every { styleLayerExists(MASKING_LAYER_MAIN) } returns true + every { styleLayerExists(MASKING_LAYER_TRAFFIC) } returns true } val result = MapboxRouteLineUtils.layersAreInitialized(style, options) @@ -417,6 +450,11 @@ class MapboxRouteLineUtilsTest { verify { style.styleLayerExists(LAYER_GROUP_3_CASING) } verify { style.styleLayerExists(LAYER_GROUP_3_MAIN) } verify { style.styleLayerExists(LAYER_GROUP_3_TRAFFIC) } + verify { style.styleLayerExists(MASKING_LAYER_TRAIL_CASING) } + verify { style.styleLayerExists(MASKING_LAYER_TRAIL) } + verify { style.styleLayerExists(MASKING_LAYER_CASING) } + verify { style.styleLayerExists(MASKING_LAYER_MAIN) } + verify { style.styleLayerExists(MASKING_LAYER_TRAFFIC) } verify(exactly = 0) { style.styleLayerExists(LAYER_GROUP_1_RESTRICTED) } @@ -455,9 +493,12 @@ class MapboxRouteLineUtilsTest { every { styleLayerExists(LAYER_GROUP_3_TRAFFIC) } returns true every { styleLayerExists(LAYER_GROUP_3_RESTRICTED) } returns true every { styleLayerExists(BOTTOM_LEVEL_ROUTE_LINE_LAYER_ID) } returns true - every { - styleLayerExists(TOP_LEVEL_ROUTE_LINE_LAYER_ID) - } returns true + every { styleLayerExists(TOP_LEVEL_ROUTE_LINE_LAYER_ID) } returns true + every { styleLayerExists(MASKING_LAYER_TRAIL_CASING) } returns true + every { styleLayerExists(MASKING_LAYER_TRAIL) } returns true + every { styleLayerExists(MASKING_LAYER_CASING) } returns true + every { styleLayerExists(MASKING_LAYER_MAIN) } returns true + every { styleLayerExists(MASKING_LAYER_TRAFFIC) } returns true every { styleSourceExists(WAYPOINT_SOURCE_ID) } returns false } @@ -891,7 +932,10 @@ class MapboxRouteLineUtilsTest { fun getRouteLineTrafficExpressionDataWithRestrictedSections() { val route = loadNavigationRoute("route-with-restrictions.json") - val trafficExpressionData = MapboxRouteLineUtils.extractRouteRestrictionData(route) + val trafficExpressionData = MapboxRouteLineUtils.extractRouteRestrictionData( + route, + MapboxRouteLineUtils.granularDistancesProvider + ) assertEquals(0.0, trafficExpressionData[0].offset, 0.0) assertFalse(trafficExpressionData[0].isInRestrictedSection) @@ -1493,29 +1537,14 @@ class MapboxRouteLineUtilsTest { ) } - @Test - fun routeHasRestrictions_whenHasRestrictions() { - val route = loadNavigationRoute("route-with-restrictions.json") - - val result = MapboxRouteLineUtils.routeHasRestrictions(route) - - assertTrue(result) - } - - @Test - fun routeHasRestrictions_whenNotHasRestrictions() { - val route = loadNavigationRoute("motorway-with-road-classes-multi-leg.json") - - val result = MapboxRouteLineUtils.routeHasRestrictions(route) - - assertFalse(result) - } - @Test fun getRouteRestrictedSectionsExpressionData_multiLegRoute() { val route = loadNavigationRoute("two-leg-route-with-restrictions.json") - val result = MapboxRouteLineUtils.extractRouteRestrictionData(route) + val result = MapboxRouteLineUtils.extractRouteRestrictionData( + route, + MapboxRouteLineUtils.granularDistancesProvider + ) assertEquals(7, result.size) assertTrue(result[1].isInRestrictedSection) @@ -1941,7 +1970,10 @@ class MapboxRouteLineUtilsTest { fun `extractRouteRestrictionData with restriction at end of route`() { val route = loadNavigationRoute("route-with-restrictions-at-end.json") - val result = MapboxRouteLineUtils.extractRouteRestrictionData(route) + val result = MapboxRouteLineUtils.extractRouteRestrictionData( + route, + MapboxRouteLineUtils.granularDistancesProvider + ) assertEquals(3, result.size) assertEquals(0.0, result[0].offset, 0.0) @@ -1958,7 +1990,10 @@ class MapboxRouteLineUtilsTest { "[rgba, 0.0, 0.0, 0.0, 0.0], 0.9963424099457971, " + "[rgba, 0.0, 255.0, 255.0, 1.0], 1.0, [rgba, 0.0, 0.0, 0.0, 0.0]]" val route = loadNavigationRoute("route-with-restrictions-at-end.json") - val expressionData = MapboxRouteLineUtils.extractRouteRestrictionData(route) + val expressionData = MapboxRouteLineUtils.extractRouteRestrictionData( + route, + MapboxRouteLineUtils.granularDistancesProvider + ) val result = MapboxRouteLineUtils.getRestrictedLineExpression( 0.0, @@ -1974,7 +2009,10 @@ class MapboxRouteLineUtilsTest { fun `extractRouteRestrictionData with restriction at start of route`() { val route = loadNavigationRoute("route-with-restrictions-at-start.json") - val result = MapboxRouteLineUtils.extractRouteRestrictionData(route) + val result = MapboxRouteLineUtils.extractRouteRestrictionData( + route, + MapboxRouteLineUtils.granularDistancesProvider + ) assertEquals(2, result.size) assertEquals(0.0, result[0].offset, 0.0) @@ -1985,13 +2023,67 @@ class MapboxRouteLineUtilsTest { assertFalse(result[1].isInRestrictedSection) } + @Test + fun `extractRouteRestrictionData when RouteLineGranularDistances null`() { + val route = loadNavigationRoute("route-with-restrictions-at-start.json") + + val result = MapboxRouteLineUtils.extractRouteRestrictionData( + route, + ) { null } + + assertTrue(result.isEmpty()) + } + + @Test + fun `extractRouteRestrictionData when leg distances array size less than route leg size`() { + val route = loadNavigationRoute("multileg_route_two_legs_with_restrictions.json") + val granularDistances = MapboxRouteLineUtils.granularDistancesProvider(route)!! + val updatedDistances = RouteLineGranularDistances( + granularDistances.completeDistance, + granularDistances.routeDistances, + arrayOf(granularDistances.legsDistances.first()), + granularDistances.stepsDistances + ) + + val result = MapboxRouteLineUtils.extractRouteRestrictionData( + route + ) { updatedDistances } + + assertEquals(5, result.size) + } + + @Test + fun `extractRouteRestrictionData when step intersection geometry index not found in leg distances array`() { + val route = loadNavigationRoute("multileg_route_two_legs_with_restrictions.json") + val granularDistances = MapboxRouteLineUtils.granularDistancesProvider(route)!! + val updatedLegDistances = granularDistances.legsDistances[1] + .drop(granularDistances.legsDistances[1].size - 1) + .toTypedArray() + + val updatedDistances = RouteLineGranularDistances( + granularDistances.completeDistance, + granularDistances.routeDistances, + arrayOf(granularDistances.legsDistances.first(), updatedLegDistances), + granularDistances.stepsDistances + ) + + val result = MapboxRouteLineUtils.extractRouteRestrictionData( + route + ) { updatedDistances } + + assertEquals(6, result.size) + } + @Test fun `getRestrictedLineExpression with restriction across two legs`() { val expectedExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0, " + "[rgba, 0.0, 0.0, 0.0, 0.0], 0.3956457979751531, " + "[rgba, 0.0, 255.0, 255.0, 1.0], 0.5540039481345271, [rgba, 0.0, 0.0, 0.0, 0.0]]" val route = loadNavigationRoute("multileg_route_two_legs_with_restrictions.json") - val expressionData = MapboxRouteLineUtils.extractRouteRestrictionData(route) + val expressionData = MapboxRouteLineUtils.extractRouteRestrictionData( + route, + MapboxRouteLineUtils.granularDistancesProvider + ) val result = MapboxRouteLineUtils.getRestrictedLineExpression( vanishingPointOffset = 0.0, @@ -2004,28 +2096,25 @@ class MapboxRouteLineUtilsTest { } @Test - fun routeHasRestrictions_when_routeNull() { - val result = MapboxRouteLineUtils.routeHasRestrictions(null) - - assertFalse(result) - } - - @Test - fun routeHasRestrictions() { - val route = loadNavigationRoute("route-with-restrictions-at-start.json") + fun getMaskingLayerDynamicData() { + val expectedExpression = "[literal, [0.0, 0.1]]" + val route = loadNavigationRoute("multileg_route_two_legs_with_restrictions.json") - val result = MapboxRouteLineUtils.routeHasRestrictions(route) + val result = MapboxRouteLineUtils.getMaskingLayerDynamicData(route, .1)!! - assertTrue(result) + assertEquals( + expectedExpression, + result.baseExpressionProvider.generateExpression().toString() + ) } @Test - fun routeHasRestrictions_whenNoRestrictions() { + fun getMaskingLayerDynamicData_whenSingleLegRoute() { val route = loadNavigationRoute("short_route.json") - val result = MapboxRouteLineUtils.routeHasRestrictions(route) + val result = MapboxRouteLineUtils.getMaskingLayerDynamicData(route, .1) - assertFalse(result) + assertNull(result) } private fun listElementsAreEqual( diff --git a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineApiRoboTest.kt b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineApiRoboTest.kt index 4f378c7b67c..3cf66c5c313 100644 --- a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineApiRoboTest.kt +++ b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineApiRoboTest.kt @@ -84,6 +84,9 @@ class MapboxRouteLineApiRoboTest { private val multiLegRouteTwoLegs by lazy { TestRoute(fileName = "multileg-route-two-legs.json") } + private val multiLegRouteWithOverlap by lazy { + TestRoute(fileName = "multileg_route_with_overlap.json") + } @Before fun setUp() { @@ -121,6 +124,9 @@ class MapboxRouteLineApiRoboTest { "Point{type=Point, bbox=null, coordinates=[-122.523671, 37.975379]}" val expectedWaypointFeature1 = "Point{type=Point, bbox=null, coordinates=[-122.523131, 37.975067]}" + val expectedMaskingExpression = + "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 1.0, [rgba, 0.0, 0.0, 0.0, 0.0]]" + val route = loadRoute("short_route.json") val routes = listOf(RouteLine(route, null)) @@ -160,6 +166,94 @@ class MapboxRouteLineApiRoboTest { expectedWaypointFeature1, result.value!!.waypointsSource.features()!![1].geometry().toString() ) + assertEquals( + expectedMaskingExpression, + result.value!!.routeLineMaskingLayerDynamicData!!.trafficExpressionProvider!! + .generateExpression().toString() + ) + assertEquals( + expectedMaskingExpression, + result.value!!.routeLineMaskingLayerDynamicData!!.baseExpressionProvider + .generateExpression().toString() + ) + assertEquals( + expectedMaskingExpression, + result.value!!.routeLineMaskingLayerDynamicData!!.casingExpressionProvider + .generateExpression().toString() + ) + assertEquals( + expectedMaskingExpression, + result.value!!.routeLineMaskingLayerDynamicData!!.trailExpressionProvider!! + .generateExpression().toString() + ) + assertEquals( + expectedMaskingExpression, + result.value!!.routeLineMaskingLayerDynamicData!!.trailCasingExpressionProvider!! + .generateExpression().toString() + ) + } + + @Test + fun setRoutes_maskingLayerExpressionsWithMultiLegRoute() = coroutineRule.runBlockingTest { + val colors = RouteLineColorResources.Builder() + .routeLowCongestionColor(Color.GRAY) + .routeUnknownCongestionColor(Color.LTGRAY) + .routeDefaultColor(Color.BLUE) + .routeLineTraveledColor(Color.RED) + .routeLineTraveledCasingColor(Color.MAGENTA) + .inActiveRouteLegsColor(Color.YELLOW) + .build() + val resources = RouteLineResources.Builder().routeLineColorResources(colors).build() + val options = MapboxRouteLineOptions.Builder(ctx) + .withRouteLineResources(resources) + .withVanishingRouteLineEnabled(true) + .styleInactiveRouteLegsIndependently(true) + .build() + val api = MapboxRouteLineApi(options) + + val expectedMaskingTrafficExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 136.0, 136.0, 136.0, 1.0], 0.48220037017458817, [rgba, 204.0," + + " 204.0, 204.0, 1.0], 0.48481010101499833, [rgba, 136.0, 136.0, 136.0, 1.0]," + + " 0.49040955817278464, [rgba, 204.0, 204.0, 204.0, 1.0], 0.49222381941898674," + + " [rgba, 136.0, 136.0, 136.0, 1.0], 0.4978029744948492, [rgba, 0.0, 0.0, 0.0, 0.0]]" + val expectedMaskingBaseExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0]," + + " 0.0, [rgba, 0.0, 0.0, 255.0, 1.0], 0.4978029744948492, [rgba, 0.0, 0.0, 0.0, 0.0]]" + val expectedMaskingCasingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 47.0, 122.0, 198.0, 1.0], 0.4978029744948492, [rgba, 0.0, 0.0," + + " 0.0, 0.0]]" + val expectedMaskingTrailExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0]," + + " 0.0, [rgba, 255.0, 0.0, 0.0, 1.0], 0.4978029744948492, [rgba, 0.0, 0.0, 0.0, 0.0]]" + val expectedMaskingTrailCasingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 255.0, 0.0, 255.0, 1.0], 0.4978029744948492, [rgba, 0.0, 0.0," + + " 0.0, 0.0]]" + + val result = api.setNavigationRoutes(listOf(multiLegRouteWithOverlap.navigationRoute)) + + assertEquals( + expectedMaskingTrafficExpression, + result.value!!.routeLineMaskingLayerDynamicData!!.trafficExpressionProvider!! + .generateExpression().toString() + ) + assertEquals( + expectedMaskingBaseExpression, + result.value!!.routeLineMaskingLayerDynamicData!!.baseExpressionProvider + .generateExpression().toString() + ) + assertEquals( + expectedMaskingCasingExpression, + result.value!!.routeLineMaskingLayerDynamicData!!.casingExpressionProvider + .generateExpression().toString() + ) + assertEquals( + expectedMaskingTrailExpression, + result.value!!.routeLineMaskingLayerDynamicData!!.trailExpressionProvider!! + .generateExpression().toString() + ) + assertEquals( + expectedMaskingTrailCasingExpression, + result.value!!.routeLineMaskingLayerDynamicData!!.trailCasingExpressionProvider!! + .generateExpression().toString() + ) } @Test @@ -927,7 +1021,7 @@ class MapboxRouteLineApiRoboTest { ) val api = MapboxRouteLineApi(options) - val result = api.alternativelyStyleSegmentsNotInLeg(1, segments) + val result = api.alternativelyStyleSegmentsNotInLeg(1, segments, Color.YELLOW) assertEquals(12, result.size) assertEquals(-256, result.first().segmentColor) diff --git a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineApiTest.kt b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineApiTest.kt index 396fcdccc2c..817ea3c04e6 100644 --- a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineApiTest.kt +++ b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineApiTest.kt @@ -65,6 +65,7 @@ import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.test.runBlockingTest import org.junit.After import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -94,6 +95,9 @@ class MapboxRouteLineApiTest { private val multiLegRouteTwoLegs by lazy { TestRoute(fileName = "multileg-route-two-legs.json") } + private val multilegRouteWithOverlap by lazy { + TestRoute(fileName = "multileg_route_with_overlap.json") + } @Before fun setUp() { @@ -461,7 +465,7 @@ class MapboxRouteLineApiTest { result.primaryRouteLineDynamicData.trafficExpressionProvider!! .generateExpression().toString() ) - assertEquals(-1, api.activeLegIndex) + assertEquals(0, api.activeLegIndex) } @Test @@ -882,6 +886,629 @@ class MapboxRouteLineApiTest { unmockkObject(MapboxRouteLineUtils) } + @Test + fun `updateWithRouteProgress multileg route when currentLegProgress legIndex less than activeLegIndex`() = coroutineRule.runBlockingTest { + mockkObject(MapboxRouteLineUtils) + var callbackCalled = false + val colors = RouteLineColorResources.Builder() + .routeLowCongestionColor(Color.GRAY) + .routeUnknownCongestionColor(Color.LTGRAY) + .routeDefaultColor(Color.BLUE) + .build() + val resources = RouteLineResources.Builder().routeLineColorResources(colors).build() + val options = MapboxRouteLineOptions.Builder(ctx).withRouteLineResources(resources).build() + val api = MapboxRouteLineApi(options) + val routeProgress = mockRouteProgress(multilegRouteWithOverlap.navigationRoute) + every { routeProgress.currentLegProgress!!.legIndex } returns 0 + every { routeProgress.currentRouteGeometryIndex } returns 43 + api.setNavigationRoutes(listOf(multilegRouteWithOverlap.navigationRoute)) + + api.updateWithRouteProgress(routeProgress) { + callbackCalled = true + } + + assertFalse(callbackCalled) + unmockkObject(MapboxRouteLineUtils) + } + + @Test + fun `updateWithRouteProgress multileg route when styleInactiveRouteLegsIndependently false and vanishing route line disabled`() = coroutineRule.runBlockingTest { + mockkObject(MapboxRouteLineUtils) + var callbackCalled = false + val expectedTrafficExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0," + + " [rgba, 136.0, 136.0, 136.0, 1.0], 0.48220037017458817, [rgba, 204.0, 204.0, 204.0," + + " 1.0], 0.48481010101499833, [rgba, 136.0, 136.0, 136.0, 1.0], 0.49040955817278464, " + + "[rgba, 204.0, 204.0, 204.0, 1.0], 0.49222381941898674, [rgba, 136.0, 136.0, 136.0, " + + "1.0], 0.503940915637458, [rgba, 204.0, 204.0, 204.0, 1.0], 0.5055971863970301, " + + "[rgba, 136.0, 136.0, 136.0, 1.0], 0.5131917124217056, [rgba, 204.0, 204.0, 204.0, " + + "1.0], 0.5147467093417488, [rgba, 136.0, 136.0, 136.0, 1.0], 1.0, [rgba, 204.0, " + + "204.0, 204.0, 1.0]]" + val expectedBaseExpression = + "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0, [rgba, 0.0, 0.0, 255.0, 1.0]]" + val expectedCasingExpression = + "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0]]" + val expectedTrailExpression = + "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0]]" + val expectedTrailCasingExpression = + "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0]]" + val expectedMaskingTrafficExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 136.0, 136.0, " + + "136.0, 1.0], 0.503940915637458, [rgba, 204.0, 204.0, 204.0, 1.0]," + + " 0.5055971863970301, [rgba, 136.0, 136.0, 136.0, 1.0], 0.5131917124217056," + + " [rgba, 204.0, 204.0, 204.0, 1.0], 0.5147467093417488, [rgba, 136.0, 136.0, 136.0," + + " 1.0], 1.0, [rgba, 204.0, 204.0, 204.0, 1.0]]" + val expectedMaskingBaseExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0]," + + " 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 0.0, 0.0, 255.0, 1.0]]" + val expectedMaskingExpression = + "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0]]" + val colors = RouteLineColorResources.Builder() + .routeLowCongestionColor(Color.GRAY) + .routeUnknownCongestionColor(Color.LTGRAY) + .routeDefaultColor(Color.BLUE) + .build() + val resources = RouteLineResources.Builder().routeLineColorResources(colors).build() + val options = MapboxRouteLineOptions.Builder(ctx).withRouteLineResources(resources).build() + val api = MapboxRouteLineApi(options) + val routeProgress = mockRouteProgress(multilegRouteWithOverlap.navigationRoute) + every { routeProgress.currentLegProgress!!.legIndex } returns 1 + every { routeProgress.currentRouteGeometryIndex } returns 43 + api.setNavigationRoutes(listOf(multilegRouteWithOverlap.navigationRoute)) + + api.updateWithRouteProgress(routeProgress) { + val result = it.value!! + val trafficMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .trafficExpressionProvider!!.generateExpression() + val baseMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .baseExpressionProvider.generateExpression() + val casingMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .casingExpressionProvider.generateExpression() + val trailMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .trailExpressionProvider!!.generateExpression() + val trailCasingMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .trailCasingExpressionProvider!!.generateExpression() + + val trafficExpression = + result.primaryRouteLineDynamicData.trafficExpressionProvider!!.generateExpression() + val baseExpression = + result.primaryRouteLineDynamicData.baseExpressionProvider.generateExpression() + val casingExpression = + result.primaryRouteLineDynamicData.casingExpressionProvider.generateExpression() + val trailExpression = + result.primaryRouteLineDynamicData.trailExpressionProvider!!.generateExpression() + val trailCasingExpression = + result.primaryRouteLineDynamicData + .trailCasingExpressionProvider!!.generateExpression() + + assertEquals(expectedMaskingTrafficExpression, trafficMaskingExpression.toString()) + assertEquals(expectedMaskingBaseExpression, baseMaskingExpression.toString()) + assertEquals(expectedMaskingExpression, casingMaskingExpression.toString()) + assertEquals(expectedMaskingExpression, trailMaskingExpression.toString()) + assertEquals(expectedMaskingExpression, trailCasingMaskingExpression.toString()) + + assertEquals(expectedTrafficExpression, trafficExpression.toString()) + assertEquals(expectedBaseExpression, baseExpression.toString()) + assertEquals(expectedCasingExpression, casingExpression.toString()) + assertEquals(expectedTrailExpression, trailExpression.toString()) + assertEquals(expectedTrailCasingExpression, trailCasingExpression.toString()) + + callbackCalled = true + } + + assertTrue(callbackCalled) + unmockkObject(MapboxRouteLineUtils) + } + + @Test + fun `updateWithRouteProgress multileg route when styleInactiveRouteLegsIndependently false and vanishing route line enabled`() = coroutineRule.runBlockingTest { + mockkObject(MapboxRouteLineUtils) + var callbackCalled = false + val expectedTrafficExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0," + + " [rgba, 136.0, 136.0, 136.0, 1.0], 0.48220037017458817, [rgba, 204.0, 204.0, 204.0," + + " 1.0], 0.48481010101499833, [rgba, 136.0, 136.0, 136.0, 1.0], 0.49040955817278464, " + + "[rgba, 204.0, 204.0, 204.0, 1.0], 0.49222381941898674, [rgba, 136.0, 136.0, 136.0, " + + "1.0], 0.503940915637458, [rgba, 204.0, 204.0, 204.0, 1.0], 0.5055971863970301, " + + "[rgba, 136.0, 136.0, 136.0, 1.0], 0.5131917124217056, [rgba, 204.0, 204.0, 204.0, " + + "1.0], 0.5147467093417488, [rgba, 136.0, 136.0, 136.0, 1.0], 1.0, [rgba, 204.0, " + + "204.0, 204.0, 1.0]]" + val expectedBaseExpression = "[step, [line-progress], [rgba, 255.0, 0.0, 0.0, 1.0], 0.0," + + " [rgba, 0.0, 0.0, 255.0, 1.0]]" + val expectedCasingExpression = "[step, [line-progress], [rgba, 255.0, 0.0, 255.0, 1.0]," + + " 0.0, [rgba, 0.0, 0.0, 0.0, 0.0]]" + val expectedTrailExpression = "[step, [line-progress], [rgba, 255.0, 0.0, 0.0, 1.0], 0.0," + + " [rgba, 255.0, 0.0, 0.0, 1.0]]" + val expectedTrailCasingExpression = "[step, [line-progress], [rgba, 255.0, 0.0, 255.0," + + " 1.0], 0.0, [rgba, 255.0, 0.0, 255.0, 1.0]]" + val expectedMaskingTrafficExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 136.0, 136.0, " + + "136.0, 1.0], 0.503940915637458, [rgba, 204.0, 204.0, 204.0, 1.0]," + + " 0.5055971863970301, [rgba, 136.0, 136.0, 136.0, 1.0], 0.5131917124217056," + + " [rgba, 204.0, 204.0, 204.0, 1.0], 0.5147467093417488, [rgba, 136.0, 136.0, 136.0," + + " 1.0], 1.0, [rgba, 204.0, 204.0, 204.0, 1.0]]" + val expectedMaskingBaseExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0]," + + " 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 0.0, 0.0, 255.0, 1.0]]" + val expectedTrailMaskingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0]," + + " 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 255.0, 0.0, 0.0, 1.0]]" + val expectedCasingMaskingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0]]" + val expectedTrailCasingMaskingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 255.0, 0.0," + + " 255.0, 1.0]]" + val colors = RouteLineColorResources.Builder() + .routeLowCongestionColor(Color.GRAY) + .routeUnknownCongestionColor(Color.LTGRAY) + .routeDefaultColor(Color.BLUE) + .routeLineTraveledColor(Color.RED) + .routeLineTraveledCasingColor(Color.MAGENTA) + .build() + val resources = RouteLineResources.Builder().routeLineColorResources(colors).build() + val options = MapboxRouteLineOptions.Builder(ctx) + .withRouteLineResources(resources) + .withVanishingRouteLineEnabled(true) + .build() + val api = MapboxRouteLineApi(options) + val routeProgress = mockRouteProgress(multilegRouteWithOverlap.navigationRoute) + every { routeProgress.currentLegProgress!!.legIndex } returns 1 + every { routeProgress.currentRouteGeometryIndex } returns 43 + api.setNavigationRoutes(listOf(multilegRouteWithOverlap.navigationRoute)) + + api.updateWithRouteProgress(routeProgress) { + val result = it.value!! + val trafficMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .trafficExpressionProvider!!.generateExpression() + val baseMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .baseExpressionProvider.generateExpression() + val casingMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .casingExpressionProvider.generateExpression() + val trailMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .trailExpressionProvider!!.generateExpression() + val trailCasingMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .trailCasingExpressionProvider!!.generateExpression() + + val trafficExpression = + result.primaryRouteLineDynamicData.trafficExpressionProvider!!.generateExpression() + val baseExpression = + result.primaryRouteLineDynamicData.baseExpressionProvider.generateExpression() + val casingExpression = + result.primaryRouteLineDynamicData.casingExpressionProvider.generateExpression() + val trailExpression = + result.primaryRouteLineDynamicData.trailExpressionProvider!!.generateExpression() + val trailCasingExpression = + result.primaryRouteLineDynamicData + .trailCasingExpressionProvider!!.generateExpression() + + assertEquals(expectedMaskingTrafficExpression, trafficMaskingExpression.toString()) + assertEquals(expectedMaskingBaseExpression, baseMaskingExpression.toString()) + assertEquals(expectedCasingMaskingExpression, casingMaskingExpression.toString()) + assertEquals(expectedTrailMaskingExpression, trailMaskingExpression.toString()) + assertEquals( + expectedTrailCasingMaskingExpression, + trailCasingMaskingExpression.toString() + ) + + assertEquals(expectedTrafficExpression, trafficExpression.toString()) + assertEquals(expectedBaseExpression, baseExpression.toString()) + assertEquals(expectedCasingExpression, casingExpression.toString()) + assertEquals(expectedTrailExpression, trailExpression.toString()) + assertEquals(expectedTrailCasingExpression, trailCasingExpression.toString()) + + callbackCalled = true + } + + assertTrue(callbackCalled) + unmockkObject(MapboxRouteLineUtils) + } + + @Test + fun `updateWithRouteProgress multileg route when styleInactiveRouteLegsIndependently true and vanishing route line enabled`() = coroutineRule.runBlockingTest { + mockkObject(MapboxRouteLineUtils) + var callbackCalled = false + val expectedTrafficExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0," + + " [rgba, 136.0, 136.0, 136.0, 1.0], 0.48220037017458817, [rgba, 204.0, 204.0, 204.0," + + " 1.0], 0.48481010101499833, [rgba, 136.0, 136.0, 136.0, 1.0], 0.49040955817278464, " + + "[rgba, 204.0, 204.0, 204.0, 1.0], 0.49222381941898674, [rgba, 136.0, 136.0, 136.0, " + + "1.0], 0.503940915637458, [rgba, 204.0, 204.0, 204.0, 1.0], 0.5055971863970301, " + + "[rgba, 136.0, 136.0, 136.0, 1.0], 0.5131917124217056, [rgba, 204.0, 204.0, 204.0, " + + "1.0], 0.5147467093417488, [rgba, 136.0, 136.0, 136.0, 1.0], 1.0, [rgba, 204.0, " + + "204.0, 204.0, 1.0]]" + val expectedBaseExpression = "[step, [line-progress], [rgba, 255.0, 0.0, 0.0, 1.0], 0.0," + + " [rgba, 0.0, 0.0, 255.0, 1.0]]" + val expectedCasingExpression = "[step, [line-progress], [rgba, 255.0, 0.0, 255.0, 1.0]," + + " 0.0, [rgba, 0.0, 0.0, 0.0, 0.0]]" + val expectedTrailExpression = "[step, [line-progress], [rgba, 255.0, 0.0, 0.0, 1.0], 0.0," + + " [rgba, 255.0, 0.0, 0.0, 1.0]]" + val expectedTrailCasingExpression = "[step, [line-progress], [rgba, 255.0, 0.0, 255.0," + + " 1.0], 0.0, [rgba, 255.0, 0.0, 255.0, 1.0]]" + val expectedMaskingTrafficExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 136.0, 136.0, " + + "136.0, 1.0], 0.503940915637458, [rgba, 204.0, 204.0, 204.0, 1.0]," + + " 0.5055971863970301, [rgba, 136.0, 136.0, 136.0, 1.0], 0.5131917124217056," + + " [rgba, 204.0, 204.0, 204.0, 1.0], 0.5147467093417488, [rgba, 136.0, 136.0, 136.0," + + " 1.0], 1.0, [rgba, 204.0, 204.0, 204.0, 1.0]]" + val expectedMaskingBaseExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0]," + + " 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 0.0, 0.0, 255.0, 1.0]]" + val expectedTrailMaskingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0]," + + " 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 255.0, 0.0, 0.0, 1.0]]" + val expectedCasingMaskingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0]]" + val expectedTrailCasingMaskingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 255.0, 0.0," + + " 255.0, 1.0]]" + val colors = RouteLineColorResources.Builder() + .routeLowCongestionColor(Color.GRAY) + .routeUnknownCongestionColor(Color.LTGRAY) + .routeDefaultColor(Color.BLUE) + .routeLineTraveledColor(Color.RED) + .routeLineTraveledCasingColor(Color.MAGENTA) + .inActiveRouteLegsColor(Color.YELLOW) + .build() + val resources = RouteLineResources.Builder().routeLineColorResources(colors).build() + val options = MapboxRouteLineOptions.Builder(ctx) + .withRouteLineResources(resources) + .withVanishingRouteLineEnabled(true) + .styleInactiveRouteLegsIndependently(true) + .build() + val api = MapboxRouteLineApi(options) + val routeProgress = mockRouteProgress(multilegRouteWithOverlap.navigationRoute) + every { routeProgress.currentLegProgress!!.legIndex } returns 1 + every { routeProgress.currentRouteGeometryIndex } returns 43 + api.setNavigationRoutes(listOf(multilegRouteWithOverlap.navigationRoute)) + + api.updateWithRouteProgress(routeProgress) { + val result = it.value!! + val trafficMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .trafficExpressionProvider!!.generateExpression() + val baseMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .baseExpressionProvider.generateExpression() + val casingMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .casingExpressionProvider.generateExpression() + val trailMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .trailExpressionProvider!!.generateExpression() + val trailCasingMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .trailCasingExpressionProvider!!.generateExpression() + + val trafficExpression = + result.primaryRouteLineDynamicData.trafficExpressionProvider!!.generateExpression() + val baseExpression = + result.primaryRouteLineDynamicData.baseExpressionProvider.generateExpression() + val casingExpression = + result.primaryRouteLineDynamicData.casingExpressionProvider.generateExpression() + val trailExpression = + result.primaryRouteLineDynamicData.trailExpressionProvider!!.generateExpression() + val trailCasingExpression = + result.primaryRouteLineDynamicData + .trailCasingExpressionProvider!!.generateExpression() + + assertEquals(expectedMaskingTrafficExpression, trafficMaskingExpression.toString()) + assertEquals(expectedMaskingBaseExpression, baseMaskingExpression.toString()) + assertEquals(expectedCasingMaskingExpression, casingMaskingExpression.toString()) + assertEquals(expectedTrailMaskingExpression, trailMaskingExpression.toString()) + assertEquals( + expectedTrailCasingMaskingExpression, + trailCasingMaskingExpression.toString() + ) + + assertEquals(expectedTrafficExpression, trafficExpression.toString()) + assertEquals(expectedBaseExpression, baseExpression.toString()) + assertEquals(expectedCasingExpression, casingExpression.toString()) + assertEquals(expectedTrailExpression, trailExpression.toString()) + assertEquals(expectedTrailCasingExpression, trailCasingExpression.toString()) + + callbackCalled = true + } + + assertTrue(callbackCalled) + unmockkObject(MapboxRouteLineUtils) + } + + @Test + fun `updateWithRouteProgress multileg route when styleInactiveRouteLegsIndependently true and vanishing route line disabled route leg 0`() = coroutineRule.runBlockingTest { + mockkObject(MapboxRouteLineUtils) + var callbackCalled = false + val expectedTrafficExpression = "[step, [line-progress], [rgba, 255.0, 0.0, 0.0, 1.0]," + + " 0.0, [rgba, 136.0, 136.0, 136.0, 1.0], 0.48220037017458817, [rgba, 204.0, 204.0," + + " 204.0, 1.0], 0.48481010101499833, [rgba, 136.0, 136.0, 136.0, 1.0]," + + " 0.49040955817278464, [rgba, 204.0, 204.0, 204.0, 1.0], 0.49222381941898674," + + " [rgba, 136.0, 136.0, 136.0, 1.0], 0.4978029744948492, [rgba, 255.0, 255.0, 0.0, 1.0]]" + val expectedBaseExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 255.0, 1.0], 0.0," + + " [rgba, 0.0, 0.0, 255.0, 1.0], 0.4978029744948492, [rgba, 255.0, 255.0, 0.0, 1.0]]" + val expectedCasingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0," + + " [rgba, 0.0, 0.0, 0.0, 0.0]]" + val colors = RouteLineColorResources.Builder() + .routeLowCongestionColor(Color.GRAY) + .routeUnknownCongestionColor(Color.LTGRAY) + .routeDefaultColor(Color.BLUE) + .routeLineTraveledColor(Color.RED) + .routeLineTraveledCasingColor(Color.MAGENTA) + .inActiveRouteLegsColor(Color.YELLOW) + .build() + val resources = RouteLineResources.Builder().routeLineColorResources(colors).build() + val options = MapboxRouteLineOptions.Builder(ctx) + .withRouteLineResources(resources) + .withVanishingRouteLineEnabled(false) + .styleInactiveRouteLegsIndependently(true) + .build() + val api = MapboxRouteLineApi(options) + val routeProgress = mockRouteProgress(multilegRouteWithOverlap.navigationRoute) + every { routeProgress.currentLegProgress!!.legIndex } returns 0 + api.setNavigationRoutes(listOf(multilegRouteWithOverlap.navigationRoute)) + + api.updateWithRouteProgress(routeProgress) { + val result = it.value!! + val trafficExpression = + result.primaryRouteLineDynamicData.trafficExpressionProvider!!.generateExpression() + val baseExpression = + result.primaryRouteLineDynamicData.baseExpressionProvider.generateExpression() + val casingExpression = + result.primaryRouteLineDynamicData.casingExpressionProvider.generateExpression() + + assertNull(result.routeLineMaskingLayerDynamicData) + assertNull(result.primaryRouteLineDynamicData.trailExpressionProvider) + assertNull(result.primaryRouteLineDynamicData.trailCasingExpressionProvider) + + assertEquals(expectedTrafficExpression, trafficExpression.toString()) + assertEquals(expectedBaseExpression, baseExpression.toString()) + assertEquals(expectedCasingExpression, casingExpression.toString()) + + callbackCalled = true + } + + assertTrue(callbackCalled) + unmockkObject(MapboxRouteLineUtils) + } + + @Test + fun `updateWithRouteProgress multileg route when styleInactiveRouteLegsIndependently true and vanishing route line disabled route leg 1`() = coroutineRule.runBlockingTest { + mockkObject(MapboxRouteLineUtils) + var callbackCalled = false + val expectedTrafficExpression = "[step, [line-progress], [rgba, 255.0, 0.0, 0.0, 1.0]," + + " 0.0, [rgba, 255.0, 255.0, 0.0, 1.0], 0.4978029744948492, [rgba, 136.0, 136.0, " + + "136.0, 1.0], 0.503940915637458, [rgba, 204.0, 204.0, 204.0, 1.0]," + + " 0.5055971863970301, [rgba, 136.0, 136.0, 136.0, 1.0], 0.5131917124217056, [rgba," + + " 204.0, 204.0, 204.0, 1.0], 0.5147467093417488, [rgba, 136.0, 136.0, 136.0, 1.0]," + + " 1.0, [rgba, 204.0, 204.0, 204.0, 1.0]]" + val expectedBaseExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 255.0, 1.0], 0.0," + + " [rgba, 255.0, 255.0, 0.0, 1.0], 0.4978029744948492, [rgba, 0.0, 0.0, 255.0, 1.0]]" + val expectedCasingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0," + + " [rgba, 0.0, 0.0, 0.0, 0.0]]" + val expectedMaskingTrafficExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 136.0, 136.0, " + + "136.0, 1.0], 0.503940915637458, [rgba, 204.0, 204.0, 204.0, 1.0]," + + " 0.5055971863970301, [rgba, 136.0, 136.0, 136.0, 1.0], 0.5131917124217056," + + " [rgba, 204.0, 204.0, 204.0, 1.0], 0.5147467093417488, [rgba, 136.0, 136.0, 136.0," + + " 1.0], 1.0, [rgba, 204.0, 204.0, 204.0, 1.0]]" + val expectedMaskingBaseExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0]," + + " 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 0.0, 0.0, 255.0, 1.0]]" + val expectedTrailMaskingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0]," + + " 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 255.0, 0.0, 0.0, 1.0]]" + val expectedCasingMaskingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0]]" + val expectedTrailCasingMaskingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 255.0, 0.0," + + " 255.0, 1.0]]" + val colors = RouteLineColorResources.Builder() + .routeLowCongestionColor(Color.GRAY) + .routeUnknownCongestionColor(Color.LTGRAY) + .routeDefaultColor(Color.BLUE) + .routeLineTraveledColor(Color.RED) + .routeLineTraveledCasingColor(Color.MAGENTA) + .inActiveRouteLegsColor(Color.YELLOW) + .build() + val resources = RouteLineResources.Builder().routeLineColorResources(colors).build() + val options = MapboxRouteLineOptions.Builder(ctx) + .withRouteLineResources(resources) + .withVanishingRouteLineEnabled(false) + .styleInactiveRouteLegsIndependently(true) + .build() + val api = MapboxRouteLineApi(options) + val routeProgress = mockRouteProgress(multilegRouteWithOverlap.navigationRoute) + every { routeProgress.currentLegProgress!!.legIndex } returns 1 + every { routeProgress.currentRouteGeometryIndex } returns 43 + api.setNavigationRoutes(listOf(multilegRouteWithOverlap.navigationRoute)) + + api.updateWithRouteProgress(routeProgress) { + val result = it.value!! + val trafficMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .trafficExpressionProvider!!.generateExpression() + val baseMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .baseExpressionProvider.generateExpression() + val casingMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .casingExpressionProvider.generateExpression() + val trailMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .trailExpressionProvider!!.generateExpression() + val trailCasingMaskingExpression = result.routeLineMaskingLayerDynamicData!! + .trailCasingExpressionProvider!!.generateExpression() + + val trafficExpression = + result.primaryRouteLineDynamicData.trafficExpressionProvider!!.generateExpression() + val baseExpression = + result.primaryRouteLineDynamicData.baseExpressionProvider.generateExpression() + val casingExpression = + result.primaryRouteLineDynamicData.casingExpressionProvider.generateExpression() + + assertEquals(expectedMaskingTrafficExpression, trafficMaskingExpression.toString()) + assertEquals(expectedMaskingBaseExpression, baseMaskingExpression.toString()) + assertEquals(expectedCasingMaskingExpression, casingMaskingExpression.toString()) + assertEquals(expectedTrailMaskingExpression, trailMaskingExpression.toString()) + assertEquals( + expectedTrailCasingMaskingExpression, + trailCasingMaskingExpression.toString() + ) + + assertEquals(expectedTrafficExpression, trafficExpression.toString()) + assertEquals(expectedBaseExpression, baseExpression.toString()) + assertEquals(expectedCasingExpression, casingExpression.toString()) + assertNull(result.primaryRouteLineDynamicData.trailExpressionProvider) + assertNull(result.primaryRouteLineDynamicData.trailCasingExpressionProvider) + + callbackCalled = true + } + + assertTrue(callbackCalled) + unmockkObject(MapboxRouteLineUtils) + } + + @Test + fun getRouteLineDynamicDataForMaskingLayersTest() { + val expectedTrafficExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0," + + " [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 136.0, 136.0, 136.0, 1.0]," + + " 0.503940915637458, [rgba, 204.0, 204.0, 204.0, 1.0], 0.5055971863970301, [rgba," + + " 136.0, 136.0, 136.0, 1.0], 0.5131917124217056, [rgba, 204.0, 204.0, 204.0, 1.0]," + + " 0.5147467093417488, [rgba, 136.0, 136.0, 136.0, 1.0], 1.0, [rgba, 204.0, 204.0," + + " 204.0, 1.0]]" + val expectedBaseExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0," + + " [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 0.0, 0.0, 255.0, 1.0]]" + val expectedCasingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0" + + ", [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 0.0, 255.0, 255.0, 1.0]]" + val expectedTrailExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], 0.0," + + " [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 255.0, 0.0, 0.0, 1.0]]" + val expectedTrailCasingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0]," + + " 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 255.0, 0.0, 255.0, 1.0]]" + val colors = RouteLineColorResources.Builder() + .routeLowCongestionColor(Color.GRAY) + .routeUnknownCongestionColor(Color.LTGRAY) + .routeDefaultColor(Color.BLUE) + .routeLineTraveledColor(Color.RED) + .routeLineTraveledCasingColor(Color.MAGENTA) + .routeCasingColor(Color.CYAN) + .inActiveRouteLegsColor(Color.YELLOW) + .build() + val resources = RouteLineResources.Builder().routeLineColorResources(colors).build() + val options = MapboxRouteLineOptions.Builder(ctx) + .withRouteLineResources(resources) + .build() + val segments = MapboxRouteLineUtils.calculateRouteLineSegments( + multilegRouteWithOverlap.navigationRoute, + listOf(), + true, + options.resourceProvider.routeLineColorResources + ) + + val result = MapboxRouteLineApi(options).getRouteLineDynamicDataForMaskingLayers( + segments, + 1 + ) + + assertNull(result.restrictedSectionExpressionProvider) + assertNull(result.trimOffset) + assertEquals( + expectedTrafficExpression, + result.trafficExpressionProvider!!.generateExpression().toString() + ) + assertEquals( + expectedBaseExpression, + result.baseExpressionProvider.generateExpression().toString() + ) + assertEquals( + expectedCasingExpression, + result.casingExpressionProvider.generateExpression().toString() + ) + assertEquals( + expectedTrailExpression, + result.trailExpressionProvider!!.generateExpression().toString() + ) + assertEquals( + expectedTrailCasingExpression, + result.trailCasingExpressionProvider!!.generateExpression().toString() + ) + } + + @Test + fun getRouteLineDynamicDataForMaskingLayersForRouteProgressTest() = + coroutineRule.runBlockingTest { + val expectedTrafficExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0], " + + "0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 136.0, 136.0, 136.0," + + " 1.0], 0.503940915637458, [rgba, 204.0, 204.0, 204.0, 1.0], 0.5055971863970301," + + " [rgba, 136.0, 136.0, 136.0, 1.0], 0.5131917124217056, [rgba, 204.0, 204.0, " + + "204.0, 1.0], 0.5147467093417488, [rgba, 136.0, 136.0, 136.0, 1.0], 1.0, " + + "[rgba, 204.0, 204.0, 204.0, 1.0]]" + val expectedBaseExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0]," + + " 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, " + + "[rgba, 0.0, 0.0, 255.0, 1.0]]" + val expectedCasingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0]," + + " 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 0.0, 255.0, 255.0," + + " 1.0]]" + val expectedTrailExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0, 0.0]," + + " 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, " + + "[rgba, 255.0, 0.0, 0.0, 1.0]]" + val expectedTrailCasingExpression = "[step, [line-progress], [rgba, 0.0, 0.0, 0.0," + + " 0.0], 0.0, [rgba, 0.0, 0.0, 0.0, 0.0], 0.4978029744948492, [rgba, 255.0, 0.0," + + " 255.0, 1.0]]" + val colors = RouteLineColorResources.Builder() + .routeLowCongestionColor(Color.GRAY) + .routeUnknownCongestionColor(Color.LTGRAY) + .routeDefaultColor(Color.BLUE) + .routeLineTraveledColor(Color.RED) + .routeLineTraveledCasingColor(Color.MAGENTA) + .routeCasingColor(Color.CYAN) + .inActiveRouteLegsColor(Color.YELLOW) + .build() + val resources = RouteLineResources.Builder().routeLineColorResources(colors).build() + val options = MapboxRouteLineOptions.Builder(ctx) + .withRouteLineResources(resources) + .build() + val routeProgress = mockRouteProgress(multilegRouteWithOverlap.navigationRoute) + every { routeProgress.currentLegProgress!!.legIndex } returns 1 + every { routeProgress.currentRouteGeometryIndex } returns 43 + val api = MapboxRouteLineApi(options) + api.setNavigationRoutes(listOf(multilegRouteWithOverlap.navigationRoute)) + + val result = api.getRouteLineDynamicDataForMaskingLayers( + multilegRouteWithOverlap.navigationRoute, + routeProgress + )!! + + assertNull(result.restrictedSectionExpressionProvider) + assertNull(result.trimOffset) + assertEquals( + expectedTrafficExpression, + result.trafficExpressionProvider!!.generateExpression().toString() + ) + assertEquals( + expectedBaseExpression, + result.baseExpressionProvider.generateExpression().toString() + ) + assertEquals( + expectedCasingExpression, + result.casingExpressionProvider.generateExpression().toString() + ) + assertEquals( + expectedTrailExpression, + result.trailExpressionProvider!!.generateExpression().toString() + ) + assertEquals( + expectedTrailCasingExpression, + result.trailCasingExpressionProvider!!.generateExpression().toString() + ) + } + + @Test + fun getRouteLineDynamicDataForMaskingLayersForRouteProgressWhenSingleLegRouteTest() { + val options = MapboxRouteLineOptions.Builder(ctx).build() + val routeProgress = mockRouteProgress(shortRoute.navigationRoute) + every { routeProgress.currentLegProgress!!.legIndex } returns 0 + + val result = MapboxRouteLineApi(options).getRouteLineDynamicDataForMaskingLayers( + shortRoute.navigationRoute, + routeProgress + ) + + assertNull(result) + } + + @Test + fun getRouteLineDynamicDataForMaskingLayers_when_routeLegIndexGreaterThanLegsTest() { + val options = MapboxRouteLineOptions.Builder(ctx).build() + val routeProgress = mockRouteProgress(shortRoute.navigationRoute) + every { routeProgress.currentLegProgress!!.legIndex } returns 3 + + val result = MapboxRouteLineApi(options).getRouteLineDynamicDataForMaskingLayers( + shortRoute.navigationRoute, + routeProgress + ) + + assertNull(result) + } + private fun mockRouteProgress(route: NavigationRoute, stepIndexValue: Int = 0): RouteProgress = mockk { every { currentLegProgress } returns mockk { diff --git a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineViewTest.kt b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineViewTest.kt index ec233a3561f..2f6e9f02d30 100644 --- a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineViewTest.kt +++ b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/line/api/MapboxRouteLineViewTest.kt @@ -6,6 +6,7 @@ import android.graphics.drawable.Drawable import androidx.appcompat.content.res.AppCompatResources import com.mapbox.bindgen.Expected import com.mapbox.bindgen.ExpectedFactory +import com.mapbox.bindgen.Value import com.mapbox.geojson.Feature import com.mapbox.geojson.FeatureCollection import com.mapbox.maps.Style @@ -42,6 +43,11 @@ import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_SOU import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_TRAFFIC import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_TRAIL import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.LAYER_GROUP_3_TRAIL_CASING +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_CASING +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_MAIN +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_TRAFFIC +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_TRAIL +import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.MASKING_LAYER_TRAIL_CASING import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.TOP_LEVEL_ROUTE_LINE_LAYER_ID import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.WAYPOINT_LAYER_ID import com.mapbox.navigation.ui.maps.route.RouteLayerConstants.WAYPOINT_SOURCE_ID @@ -142,6 +148,11 @@ class MapboxRouteLineViewTest { styleLayerExists(LAYER_GROUP_3_MAIN) } returns true every { styleLayerExists(LAYER_GROUP_3_TRAFFIC) } returns true + every { styleLayerExists(MASKING_LAYER_TRAIL_CASING) } returns true + every { styleLayerExists(MASKING_LAYER_TRAIL) } returns true + every { styleLayerExists(MASKING_LAYER_CASING) } returns true + every { styleLayerExists(MASKING_LAYER_MAIN) } returns true + every { styleLayerExists(MASKING_LAYER_TRAFFIC) } returns true every { styleLayerExists(TOP_LEVEL_ROUTE_LINE_LAYER_ID) @@ -251,6 +262,8 @@ class MapboxRouteLineViewTest { val routeLineExp = mockk() val casingLineEx = mockk() val restrictedRoadExp = mockk() + val trailExpression = mockk() + val trailCasingExpression = mockk() val state: Expected = ExpectedFactory.createValue( RouteLineUpdateValue( @@ -258,7 +271,9 @@ class MapboxRouteLineViewTest { { routeLineExp }, { casingLineEx }, { trafficLineExp }, - { restrictedRoadExp } + { restrictedRoadExp }, + trailExpressionProvider = { trailExpression }, + trailCasingExpressionProvider = { trailCasingExpression } ), alternativeRouteLinesDynamicData = listOf( RouteLineDynamicData( @@ -273,6 +288,14 @@ class MapboxRouteLineViewTest { { throw UnsupportedOperationException() }, { throw UnsupportedOperationException() } ) + ), + routeLineMaskingLayerDynamicData = RouteLineDynamicData( + { routeLineExp }, + { casingLineEx }, + { trafficLineExp }, + { restrictedRoadExp }, + trailExpressionProvider = { trailExpression }, + trailCasingExpressionProvider = { trailCasingExpression } ) ) ) @@ -318,6 +341,56 @@ class MapboxRouteLineViewTest { restrictedRoadExp ) } + verify { + style.setStyleLayerProperty( + LAYER_GROUP_1_TRAIL, + "line-gradient", + trailExpression + ) + } + verify { + style.setStyleLayerProperty( + LAYER_GROUP_1_TRAIL_CASING, + "line-gradient", + trailCasingExpression + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_TRAFFIC, + "line-gradient", + trafficLineExp + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_MAIN, + "line-gradient", + routeLineExp + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_CASING, + "line-gradient", + casingLineEx + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_TRAIL, + "line-gradient", + trailExpression + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_TRAIL_CASING, + "line-gradient", + trailCasingExpression + ) + } + unmockkObject(MapboxRouteLineUtils) unmockkStatic("com.mapbox.maps.extension.style.layers.LayerUtils") } @@ -358,6 +431,14 @@ class MapboxRouteLineViewTest { { throw UnsupportedOperationException() }, { throw UnsupportedOperationException() } ) + ), + routeLineMaskingLayerDynamicData = RouteLineDynamicData( + { routeLineExp }, + { casingLineEx }, + { trafficLineExp }, + { restrictedRoadExp }, + trailExpressionProvider = { trailExp }, + trailCasingExpressionProvider = { trailCasingExp } ) ) ) @@ -417,6 +498,41 @@ class MapboxRouteLineViewTest { trailCasingExp ) } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_TRAFFIC, + "line-gradient", + trafficLineExp + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_MAIN, + "line-gradient", + routeLineExp + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_CASING, + "line-gradient", + casingLineEx + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_TRAIL, + "line-gradient", + trailExp + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_TRAIL_CASING, + "line-gradient", + trailCasingExp + ) + } unmockkObject(MapboxRouteLineUtils) unmockkStatic("com.mapbox.maps.extension.style.layers.LayerUtils") } @@ -459,12 +575,18 @@ class MapboxRouteLineViewTest { val route3Main = mockk(relaxed = true) val route3Traffic = mockk(relaxed = true) val route3Restricted = mockk(relaxed = true) + val maskingTrailCasing = mockk(relaxed = true) + val maskingTrail = mockk(relaxed = true) + val maskingCasing = mockk(relaxed = true) + val maskingMain = mockk(relaxed = true) + val maskingTraffic = mockk(relaxed = true) val bottomLevelLayer = mockk(relaxed = true) val topLevelLayer = mockk(relaxed = true) val primaryRouteSource = mockk(relaxed = true) val altRoute1Source = mockk(relaxed = true) val altRoute2Source = mockk(relaxed = true) val wayPointSource = mockk(relaxed = true) + val maskingExpression = mockk() val state: Expected = ExpectedFactory.createValue( RouteSetValue( primaryRouteLineData = RouteLineData( @@ -505,7 +627,16 @@ class MapboxRouteLineViewTest { ) ) ), - waypointsFeatureCollection + waypointsFeatureCollection, + routeLineMaskingLayerDynamicData = RouteLineDynamicData( + { maskingExpression }, + { maskingExpression }, + { maskingExpression }, + { maskingExpression }, + trimOffset = RouteLineTrimOffset(9.9), + trailExpressionProvider = { maskingExpression }, + trailCasingExpressionProvider = { maskingExpression } + ) ) ) val style = getMockedStyle( @@ -527,6 +658,11 @@ class MapboxRouteLineViewTest { route3Main, route3Traffic, route3Restricted, + maskingTrailCasing, + maskingTrail, + maskingCasing, + maskingMain, + maskingTraffic, topLevelLayer, bottomLevelLayer, primaryRouteSource, @@ -537,6 +673,11 @@ class MapboxRouteLineViewTest { every { style.setStyleLayerProperty(any(), any(), any()) } returns ExpectedFactory.createNone() + every { + style.getStyleLayerProperty(MASKING_LAYER_TRAFFIC, "source") + } returns mockk { + every { value } returns Value.valueOf("foobar") + } every { MapboxRouteLineUtils.getTopRouteLineRelatedLayerId(style) } returns LAYER_GROUP_1_MAIN @@ -681,6 +822,42 @@ class MapboxRouteLineViewTest { ) } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_TRAIL_CASING, + "line-gradient", + maskingExpression + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_TRAIL, + "line-gradient", + maskingExpression + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_CASING, + "line-gradient", + maskingExpression + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_MAIN, + "line-gradient", + maskingExpression + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_TRAFFIC, + "line-gradient", + maskingExpression + ) + } + verify(exactly = 0) { style.setStyleLayerProperty( LAYER_GROUP_1_TRAIL_CASING, @@ -810,6 +987,42 @@ class MapboxRouteLineViewTest { ) } + verify(exactly = 0) { + style.setStyleLayerProperty( + MASKING_LAYER_TRAIL_CASING, + "line-trim-offset", + expectedRoute1Expression + ) + } + verify(exactly = 0) { + style.setStyleLayerProperty( + MASKING_LAYER_TRAIL, + "line-trim-offset", + expectedRoute1Expression + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_CASING, + "line-trim-offset", + expectedRoute1Expression + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_MAIN, + "line-trim-offset", + expectedRoute1Expression + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_TRAFFIC, + "line-trim-offset", + expectedRoute1Expression + ) + } + verify(exactly = 1) { style.moveStyleLayer(LAYER_GROUP_1_TRAIL_CASING, any()) } verify(exactly = 1) { style.moveStyleLayer(LAYER_GROUP_1_TRAIL, any()) } verify(exactly = 1) { style.moveStyleLayer(LAYER_GROUP_1_CASING, any()) } @@ -828,6 +1041,11 @@ class MapboxRouteLineViewTest { verify(exactly = 0) { style.moveStyleLayer(LAYER_GROUP_3_MAIN, any()) } verify(exactly = 0) { style.moveStyleLayer(LAYER_GROUP_3_TRAFFIC, any()) } verify(exactly = 0) { style.moveStyleLayer(LAYER_GROUP_3_RESTRICTED, any()) } + verify(exactly = 1) { style.moveStyleLayer(MASKING_LAYER_TRAIL_CASING, any()) } + verify(exactly = 1) { style.moveStyleLayer(MASKING_LAYER_TRAIL, any()) } + verify(exactly = 1) { style.moveStyleLayer(MASKING_LAYER_CASING, any()) } + verify(exactly = 1) { style.moveStyleLayer(MASKING_LAYER_MAIN, any()) } + verify(exactly = 1) { style.moveStyleLayer(MASKING_LAYER_TRAFFIC, any()) } verify { style.setStyleLayerProperty( @@ -935,6 +1153,42 @@ class MapboxRouteLineViewTest { ) } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_TRAIL_CASING, + "line-width", + options.resourceProvider.routeCasingLineScaleExpression + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_TRAIL, + "line-width", + options.resourceProvider.routeLineScaleExpression + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_CASING, + "line-width", + options.resourceProvider.routeCasingLineScaleExpression + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_MAIN, + "line-width", + options.resourceProvider.routeLineScaleExpression + ) + } + verify { + style.setStyleLayerProperty( + MASKING_LAYER_TRAFFIC, + "line-width", + options.resourceProvider.routeTrafficLineScaleExpression + ) + } + unmockkObject(MapboxRouteLineUtils) unmockkStatic("com.mapbox.maps.extension.style.layers.LayerUtils") unmockkStatic("com.mapbox.maps.extension.style.sources.SourceUtils") @@ -976,6 +1230,11 @@ class MapboxRouteLineViewTest { val route3Main = mockk(relaxed = true) val route3Traffic = mockk(relaxed = true) val route3Restricted = mockk(relaxed = true) + val maskingTrailCasing = mockk(relaxed = true) + val maskingTrail = mockk(relaxed = true) + val maskingCasing = mockk(relaxed = true) + val maskingMain = mockk(relaxed = true) + val maskingTraffic = mockk(relaxed = true) val bottomLevelLayer = mockk(relaxed = true) val topLevelLayer = mockk(relaxed = true) val primaryRouteSource = mockk(relaxed = true) @@ -1051,6 +1310,11 @@ class MapboxRouteLineViewTest { route3Main, route3Traffic, route3Restricted, + maskingTrailCasing, + maskingTrail, + maskingCasing, + maskingMain, + maskingTraffic, topLevelLayer, bottomLevelLayer, primaryRouteSource, @@ -1077,6 +1341,11 @@ class MapboxRouteLineViewTest { route3Main, route3Traffic, route3Restricted, + maskingTrailCasing, + maskingTrail, + maskingCasing, + maskingMain, + maskingTraffic, topLevelLayer, bottomLevelLayer, primaryRouteSource, @@ -1120,6 +1389,17 @@ class MapboxRouteLineViewTest { verify(exactly = 0) { style2.moveStyleLayer(LAYER_GROUP_3_TRAFFIC, any()) } verify(exactly = 0) { style2.moveStyleLayer(LAYER_GROUP_3_RESTRICTED, any()) } + verify(exactly = 1) { style.moveStyleLayer(MASKING_LAYER_TRAIL_CASING, any()) } + verify(exactly = 1) { style.moveStyleLayer(MASKING_LAYER_TRAIL, any()) } + verify(exactly = 1) { style.moveStyleLayer(MASKING_LAYER_CASING, any()) } + verify(exactly = 1) { style.moveStyleLayer(MASKING_LAYER_MAIN, any()) } + verify(exactly = 1) { style.moveStyleLayer(MASKING_LAYER_TRAFFIC, any()) } + verify(exactly = 1) { style2.moveStyleLayer(MASKING_LAYER_TRAIL_CASING, any()) } + verify(exactly = 1) { style2.moveStyleLayer(MASKING_LAYER_TRAIL, any()) } + verify(exactly = 1) { style2.moveStyleLayer(MASKING_LAYER_CASING, any()) } + verify(exactly = 1) { style2.moveStyleLayer(MASKING_LAYER_MAIN, any()) } + verify(exactly = 1) { style2.moveStyleLayer(MASKING_LAYER_TRAFFIC, any()) } + unmockkObject(MapboxRouteLineUtils) unmockkStatic("com.mapbox.maps.extension.style.layers.LayerUtils") unmockkStatic("com.mapbox.maps.extension.style.sources.SourceUtils") @@ -1377,6 +1657,11 @@ class MapboxRouteLineViewTest { val route1Main = mockk(relaxed = true) val route1Traffic = mockk(relaxed = true) val route1Restricted = mockk(relaxed = true) + val maskingTrailCasing = mockk(relaxed = true) + val maskingTrail = mockk(relaxed = true) + val maskingCasing = mockk(relaxed = true) + val maskingMain = mockk(relaxed = true) + val maskingTraffic = mockk(relaxed = true) val style = mockk