From a41c9f78f97cae4cb2eabfd53844f95f57fc7b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Paczos?= Date: Thu, 15 Oct 2020 13:40:55 +0200 Subject: [PATCH] [ui] refactor vanishing point calculation to use EPSG:3857 projection --- .../examples/core/ReplayActivity.kt | 4 +- libnavigation-ui/api/current.txt | 1 + .../ui/internal/route/RouteConstants.java | 3 +- .../ui/internal/utils/MemoizeUtils.kt | 56 --- .../navigation/ui/route/MapRouteLine.kt | 378 ++++++++++++------ .../route/MapRouteProgressChangeListener.kt | 24 +- .../ui/route/NavigationMapRoute.java | 46 ++- .../ui/internal/utils/MemoizeUtilsTest.kt | 43 -- .../navigation/ui/route/MapRouteLineTest.kt | 257 +++++++----- .../MapRouteProgressChangeListenerTest.kt | 35 +- 10 files changed, 505 insertions(+), 342 deletions(-) delete mode 100644 libnavigation-ui/src/main/java/com/mapbox/navigation/ui/internal/utils/MemoizeUtils.kt delete mode 100644 libnavigation-ui/src/test/java/com/mapbox/navigation/ui/internal/utils/MemoizeUtilsTest.kt diff --git a/examples/src/main/java/com/mapbox/navigation/examples/core/ReplayActivity.kt b/examples/src/main/java/com/mapbox/navigation/examples/core/ReplayActivity.kt index a0f9f3134a6..dc5235fe476 100644 --- a/examples/src/main/java/com/mapbox/navigation/examples/core/ReplayActivity.kt +++ b/examples/src/main/java/com/mapbox/navigation/examples/core/ReplayActivity.kt @@ -76,7 +76,7 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback { this.mapboxMap = mapboxMap mapboxMap.setStyle(Style.MAPBOX_STREETS) { style -> mapboxMap.moveCamera(CameraUpdateFactory.zoomTo(15.0)) - navigationMapboxMap = NavigationMapboxMap(mapView, mapboxMap, this, true) + navigationMapboxMap = NavigationMapboxMap(mapView, mapboxMap, this, null, true, true) initializeFirstLocation() mapboxNavigation?.attachFasterRouteObserver( @@ -98,7 +98,7 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback { RouteOptions.builder().applyDefaultParams() .accessToken(Utils.getMapboxAccessToken(applicationContext)) .coordinates(originLocation.toPoint(), null, latLng.toPoint()) - .alternatives(true) + .alternatives(false) .profile(DirectionsCriteria.PROFILE_DRIVING_TRAFFIC) .overview(DirectionsCriteria.OVERVIEW_FULL) .annotationsList( diff --git a/libnavigation-ui/api/current.txt b/libnavigation-ui/api/current.txt index f8113e7b8cf..e36f21a932e 100644 --- a/libnavigation-ui/api/current.txt +++ b/libnavigation-ui/api/current.txt @@ -605,6 +605,7 @@ package com.mapbox.navigation.ui.route { method public com.mapbox.navigation.ui.route.NavigationMapRoute.Builder withRouteStyleDescriptors(java.util.List!); method public com.mapbox.navigation.ui.route.NavigationMapRoute.Builder withStyle(@StyleRes int); method public com.mapbox.navigation.ui.route.NavigationMapRoute.Builder withVanishRouteLineEnabled(boolean); + method public com.mapbox.navigation.ui.route.NavigationMapRoute.Builder withVanishingRouteLineUpdateIntervalNano(long); } public interface OnRouteSelectionChangeListener { diff --git a/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/internal/route/RouteConstants.java b/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/internal/route/RouteConstants.java index 54da372bee3..ca02935b3e3 100644 --- a/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/internal/route/RouteConstants.java +++ b/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/internal/route/RouteConstants.java @@ -49,7 +49,6 @@ public class RouteConstants { public static final String MAPBOX_LOCATION_ID = "mapbox-location"; public static final double MINIMUM_ROUTE_LINE_OFFSET = .000000001; public static final String LAYER_ABOVE_UPCOMING_MANEUVER_ARROW = "com.mapbox.annotations.points"; - public static final double ROUTE_LINE_UPDATE_MAX_DISTANCE_THRESHOLD_IN_METERS = 1.0; public static final String DEFAULT_ROUTE_DESCRIPTOR_PLACEHOLDER = "mapboxDescriptorPlaceHolderUnused"; - public static final int POINT_DISTANCE_CALCULATION_FUN_CACHE_SIZE = 4500; + public static final double MAX_ELAPSED_SINCE_INDEX_UPDATE_NANO = 1_500_000_000; // 1.5s } diff --git a/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/internal/utils/MemoizeUtils.kt b/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/internal/utils/MemoizeUtils.kt deleted file mode 100644 index 2a35e2a7299..00000000000 --- a/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/internal/utils/MemoizeUtils.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.mapbox.navigation.ui.internal.utils - -import android.util.LruCache - -object MemoizeUtils { - private interface MemoizeCall { - operator fun invoke(func: F): R - } - - // If you don't need the LRU cache then duplicate what is below using a ConcurrentHashMap - private class MemoizeHandlerLimited, out R>( - val func: F, - cacheSize: Int - ) { - private val callMap = LruCache(cacheSize) - operator fun invoke(key: K): R { - return callMap[key] ?: run { - val result = key(func) - synchronized(callMap) { - callMap.put(key, result) - result - } - } - } - } - - private data class MemoizeKey1(val p1: P1) : - MemoizeCall<(P1) -> R, R> { - override fun invoke(func: (P1) -> R) = func(p1) - } - - fun ((P1) -> R).memoize(cacheSize: Int): (P1) -> R { - return object : (P1) -> R { - private val handler = MemoizeHandlerLimited<((P1) -> R), MemoizeKey1, R>( - this@memoize, - cacheSize - ) - override fun invoke(p1: P1) = handler(MemoizeKey1(p1)) - } - } - - private data class MemoizeKey2(val p1: P1, val p2: P2) : - MemoizeCall<(P1, P2) -> R, R> { - override fun invoke(func: (P1, P2) -> R) = func(p1, p2) - } - - fun ((P1, P2) -> R).memoize(cacheSize: Int): (P1, P2) -> R { - return object : (P1, P2) -> R { - private val handler = MemoizeHandlerLimited<((P1, P2) -> R), MemoizeKey2, R>( - this@memoize, - cacheSize - ) - override fun invoke(p1: P1, p2: P2) = handler(MemoizeKey2(p1, p2)) - } - } -} diff --git a/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/route/MapRouteLine.kt b/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/route/MapRouteLine.kt index 3a8d0aa5925..18f79e5bb9f 100644 --- a/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/route/MapRouteLine.kt +++ b/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/route/MapRouteLine.kt @@ -2,11 +2,12 @@ package com.mapbox.navigation.ui.route import android.content.Context import android.graphics.drawable.Drawable -import android.util.LruCache +import android.util.SparseArray import androidx.annotation.AnyRes import androidx.annotation.ColorInt import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat +import com.mapbox.api.directions.v5.DirectionsCriteria import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.api.directions.v5.models.RouteLeg import com.mapbox.core.constants.Constants @@ -14,6 +15,7 @@ import com.mapbox.geojson.Feature import com.mapbox.geojson.FeatureCollection import com.mapbox.geojson.LineString import com.mapbox.geojson.Point +import com.mapbox.geojson.utils.PolylineUtils import com.mapbox.mapboxsdk.location.LocationComponentConstants import com.mapbox.mapboxsdk.maps.Style import com.mapbox.mapboxsdk.style.expressions.Expression @@ -24,19 +26,19 @@ import com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineGradient import com.mapbox.mapboxsdk.style.layers.SymbolLayer import com.mapbox.mapboxsdk.style.sources.GeoJsonOptions import com.mapbox.mapboxsdk.style.sources.GeoJsonSource +import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.ui.R import com.mapbox.navigation.ui.internal.route.MapRouteSourceProvider import com.mapbox.navigation.ui.internal.route.RouteConstants import com.mapbox.navigation.ui.internal.route.RouteConstants.ALTERNATIVE_ROUTE_LAYER_ID import com.mapbox.navigation.ui.internal.route.RouteConstants.ALTERNATIVE_ROUTE_SOURCE_ID import com.mapbox.navigation.ui.internal.route.RouteConstants.HEAVY_CONGESTION_VALUE +import com.mapbox.navigation.ui.internal.route.RouteConstants.MAX_ELAPSED_SINCE_INDEX_UPDATE_NANO import com.mapbox.navigation.ui.internal.route.RouteConstants.MINIMUM_ROUTE_LINE_OFFSET import com.mapbox.navigation.ui.internal.route.RouteConstants.MODERATE_CONGESTION_VALUE -import com.mapbox.navigation.ui.internal.route.RouteConstants.POINT_DISTANCE_CALCULATION_FUN_CACHE_SIZE import com.mapbox.navigation.ui.internal.route.RouteConstants.PRIMARY_ROUTE_LAYER_ID import com.mapbox.navigation.ui.internal.route.RouteConstants.PRIMARY_ROUTE_SOURCE_ID import com.mapbox.navigation.ui.internal.route.RouteConstants.PRIMARY_ROUTE_TRAFFIC_LAYER_ID -import com.mapbox.navigation.ui.internal.route.RouteConstants.ROUTE_LINE_UPDATE_MAX_DISTANCE_THRESHOLD_IN_METERS import com.mapbox.navigation.ui.internal.route.RouteConstants.SEVERE_CONGESTION_VALUE import com.mapbox.navigation.ui.internal.route.RouteConstants.UNKNOWN_CONGESTION_VALUE import com.mapbox.navigation.ui.internal.route.RouteConstants.WAYPOINT_DESTINATION_VALUE @@ -45,9 +47,9 @@ import com.mapbox.navigation.ui.internal.route.RouteConstants.WAYPOINT_PROPERTY_ import com.mapbox.navigation.ui.internal.route.RouteConstants.WAYPOINT_SOURCE_ID import com.mapbox.navigation.ui.internal.route.RouteLayerProvider import com.mapbox.navigation.ui.internal.utils.MapUtils -import com.mapbox.navigation.ui.internal.utils.MemoizeUtils.memoize import com.mapbox.navigation.ui.route.MapRouteLine.MapRouteLineSupport.buildWayPointFeatureCollection -import com.mapbox.navigation.ui.route.MapRouteLine.MapRouteLineSupport.calculatePreciseDistanceTraveledAlongLine +import com.mapbox.navigation.ui.route.MapRouteLine.MapRouteLineSupport.calculateDistance +import com.mapbox.navigation.ui.route.MapRouteLine.MapRouteLineSupport.calculateGranularDistances import com.mapbox.navigation.ui.route.MapRouteLine.MapRouteLineSupport.calculateRouteLineSegments import com.mapbox.navigation.ui.route.MapRouteLine.MapRouteLineSupport.generateFeatureCollection import com.mapbox.navigation.ui.route.MapRouteLine.MapRouteLineSupport.getBelowLayer @@ -58,11 +60,13 @@ import com.mapbox.navigation.utils.internal.ThreadController import com.mapbox.navigation.utils.internal.ifNonNull import com.mapbox.navigation.utils.internal.parallelMap import com.mapbox.turf.TurfConstants +import com.mapbox.turf.TurfException import com.mapbox.turf.TurfMeasurement import com.mapbox.turf.TurfMisc import timber.log.Timber -import java.math.BigDecimal -import kotlin.math.abs +import kotlin.math.ln +import kotlin.math.sin +import kotlin.math.sqrt /** * Responsible for the appearance of the route lines on the map. This class applies styling @@ -92,7 +96,7 @@ internal class MapRouteLine( allRoutesVisible: Boolean, alternativesVisible: Boolean, mapRouteSourceProvider: MapRouteSourceProvider, - vanishPoint: Float, + vanishPoint: Double, routeLineInitializedCallback: MapRouteLineInitializedCallback? ) { @@ -123,7 +127,7 @@ internal class MapRouteLine( true, true, mapRouteSourceProvider, - 0f, + 0.0, routeLineInitializedCallback ) @@ -144,10 +148,14 @@ internal class MapRouteLine( private var alternativesVisible = true private var allLayersAreVisible = true private var primaryRoute: DirectionsRoute? = null - var vanishPointOffset: Float = 0f + var vanishPointOffset: Double = 0.0 private set private var vanishingPointUpdateInhibited: Boolean = true - private val distanceRemainingCache = LruCache(2) + + private var primaryRoutePoints: RoutePoints? = null + private var primaryRouteLineGranularDistances: RouteLineGranularDistances? = null + private var primaryRouteRemainingDistancesIndex: Int? = null + private var lastIndexUpdateTimeNano: Long = 0 @get:ColorInt private val routeLineTraveledColor: Int by lazy { @@ -338,6 +346,8 @@ internal class MapRouteLine( this.drawnWaypointsFeatureCollection = buildWayPointFeatureCollection(routeFeatureData.first().route) + + initPrimaryRoutePoints(routeFeatureData.first().route) } val wayPointGeoJsonOptions = GeoJsonOptions().withMaxZoom(16) @@ -402,14 +412,110 @@ internal class MapRouteLine( ) } - fun updateDistanceRemainingCache(route: DirectionsRoute, distanceRemaining: Float) { - distanceRemainingCache.put(route, distanceRemaining) + private fun initPrimaryRoutePoints(route: DirectionsRoute) { + primaryRoutePoints = parseRoutePoints(route) + primaryRouteLineGranularDistances = + calculateRouteGranularDistances(primaryRoutePoints?.flatList ?: emptyList()) } - fun inhibitVanishingPointUpdate(inhibitVanishingPointUpdate: Boolean) { + /** + * Decodes the route geometry into nested arrays of legs -> steps -> points. + * + * The first and last point of adjacent steps overlap and are duplicated. + */ + private fun parseRoutePoints(route: DirectionsRoute): RoutePoints? { + val precision = + if (route.routeOptions()?.geometries() == DirectionsCriteria.GEOMETRY_POLYLINE) { + Constants.PRECISION_5 + } else { + Constants.PRECISION_6 + } + + val nestedList = route.legs()?.map { routeLeg -> + routeLeg.steps()?.map { legStep -> + legStep.geometry()?.let { geometry -> + PolylineUtils.decode(geometry, precision).toList() + } ?: return null + } ?: return null + } ?: return null + + val flatList = nestedList.flatten().flatten() + + return RoutePoints(nestedList, flatList) + } + + /** + * Tries to find and cache the index of the upcoming [RouteLineDistancesIndex]. + */ + fun updateUpcomingRoutePointIndex(routeProgress: RouteProgress) { + ifNonNull( + routeProgress.currentLegProgress, + routeProgress.currentLegProgress?.currentStepProgress, + primaryRoutePoints + ) { currentLegProgress, currentStepProgress, completeRoutePoints -> + var allRemainingPoints = 0 + /** + * Finds the count of remaining points in the current step. + * + * TurfMisc.lineSliceAlong places an additional point at index 0 to mark the precise + * cut-off point which we can safely ignore. + * We'll add the distance from the upcoming point to the current's puck position later. + */ + allRemainingPoints += try { + TurfMisc.lineSliceAlong( + LineString.fromLngLats(currentStepProgress.stepPoints ?: emptyList()), + currentStepProgress.distanceTraveled.toDouble(), + currentStepProgress.step?.distance() ?: 0.0, + TurfConstants.UNIT_METERS + ).coordinates().drop(1).size + } catch (e: TurfException) { + 0 + } + + /** + * Add to the count of remaining points all of the remaining points on the current leg, + * after the current step. + */ + val currentLegSteps = completeRoutePoints.nestedList[currentLegProgress.legIndex] + allRemainingPoints += if (currentStepProgress.stepIndex < currentLegSteps.size) { + currentLegSteps.slice( + currentStepProgress.stepIndex + 1 until currentLegSteps.size - 1 + ).flatten().size + } else { + 0 + } + + /** + * Add to the count of remaining points all of the remaining legs. + */ + for (i in currentLegProgress.legIndex + 1 until completeRoutePoints.nestedList.size) { + allRemainingPoints += completeRoutePoints.nestedList[i].flatten().size + } + + /** + * When we know the number of remaining points and the number of all points, + * calculate the index of the upcoming point. + */ + val allPoints = completeRoutePoints.flatList.size + primaryRouteRemainingDistancesIndex = allPoints - allRemainingPoints - 1 + } ?: run { primaryRouteRemainingDistancesIndex = null } + + lastIndexUpdateTimeNano = System.nanoTime() + } + + fun inhibitAutomaticVanishingPointUpdate(inhibitVanishingPointUpdate: Boolean) { vanishingPointUpdateInhibited = inhibitVanishingPointUpdate } + fun setVanishingOffset(offset: Double) { + if (offset >= 0) { + val expression = getExpressionAtOffset(offset) + hideCasingLineAtOffset(offset) + hideRouteLineAtOffset(offset) + decorateRouteLine(expression) + } + } + /** * Creates a route line which is applied to the route layer(s) * @@ -717,6 +823,7 @@ internal class MapRouteLine( val expression = getExpressionAtOffset(vanishPointOffset) style.getLayer(PRIMARY_ROUTE_TRAFFIC_LAYER_ID)?.setProperties(lineGradient(expression)) } + initPrimaryRoutePoints(routeData.route) } private fun updateRouteTrafficSegments(routeData: RouteFeatureData) { @@ -748,12 +855,17 @@ internal class MapRouteLine( * * @return the Expression that can be used in a Layer's properties. */ - fun getExpressionAtOffset(distanceOffset: Float): Expression { + fun getExpressionAtOffset(distanceOffset: Double): Expression { vanishPointOffset = distanceOffset val filteredItems = routeLineExpressionData.filter { it.offset > distanceOffset } val trafficExpressions = when (filteredItems.isEmpty()) { true -> when (routeLineExpressionData.isEmpty()) { - true -> listOf(RouteLineExpressionData(distanceOffset, routeUnknownColor)) + true -> listOf( + RouteLineExpressionData( + distanceOffset, + Expression.color(routeUnknownColor) + ) + ) false -> listOf(routeLineExpressionData.last().copy(offset = distanceOffset)) } false -> { @@ -767,8 +879,8 @@ internal class MapRouteLine( } }.map { Expression.stop( - it.offset.toBigDecimal().setScale(9, BigDecimal.ROUND_DOWN), - Expression.color(it.segmentColor) + it.offset, + it.segmentColorExpression ) } @@ -780,7 +892,9 @@ internal class MapRouteLine( } fun clearRouteData() { - vanishPointOffset = 0f + vanishPointOffset = 0.0 + primaryRoutePoints = null + primaryRouteLineGranularDistances = null primaryRoute = null directionsRoutes.clear() routeFeatureData.clear() @@ -795,6 +909,15 @@ internal class MapRouteLine( primaryRouteLineSource.setGeoJson(drawnPrimaryRouteFeatureCollection) } + private fun calculateRouteGranularDistances(coordinates: List): + RouteLineGranularDistances? { + return if (coordinates.isNotEmpty()) { + calculateGranularDistances(coordinates) + } else { + null + } + } + private fun setAlternativeRoutesSource(featureCollection: FeatureCollection) { drawnAlternativeRouteFeatureCollection = featureCollection alternativeRouteLineSource.setGeoJson(drawnAlternativeRouteFeatureCollection) @@ -861,12 +984,12 @@ internal class MapRouteLine( * * @param offset the offset of the visibility in the expression */ - fun hideCasingLineAtOffset(offset: Float) { + fun hideCasingLineAtOffset(offset: Double) { val expression = Expression.step( Expression.lineProgress(), Expression.color(routeLineCasingTraveledColor), Expression.stop( - offset.toBigDecimal().setScale(9, BigDecimal.ROUND_DOWN), + offset, Expression.color(routeCasingColor) ) ) @@ -881,12 +1004,12 @@ internal class MapRouteLine( * * @param offset the offset of the visibility in the expression */ - fun hideRouteLineAtOffset(offset: Float) { + fun hideRouteLineAtOffset(offset: Double) { val expression = Expression.step( Expression.lineProgress(), Expression.color(routeLineTraveledColor), Expression.stop( - offset.toBigDecimal().setScale(9, BigDecimal.ROUND_DOWN), + offset, Expression.color(routeDefaultColor) ) ) @@ -906,55 +1029,46 @@ internal class MapRouteLine( */ fun decorateRouteLine(expression: Expression) { if (style.isFullyLoaded) { - style.getLayer(PRIMARY_ROUTE_TRAFFIC_LAYER_ID)?.setProperties( - lineGradient( - expression - ) - ) + style.getLayer(PRIMARY_ROUTE_TRAFFIC_LAYER_ID)?.setProperties(lineGradient(expression)) } } /** - * Updates the route line appearance from the origin point to the point indicated in - * @param point representing the portion of the route that has been traveled. + * Updates the route line appearance from the origin point to the indicated point + * @param point representing the current position of the puck */ fun updateTraveledRouteLine(point: Point) { - if (vanishingPointUpdateInhibited) { + if (vanishingPointUpdateInhibited || + System.nanoTime() - lastIndexUpdateTimeNano > MAX_ELAPSED_SINCE_INDEX_UPDATE_NANO + ) { return } - ifNonNull(primaryRoute, primaryRoute?.distance()) { primaryRoute, routeDistance -> - val lineString = getLineStringForRoute(primaryRoute) - - if (lineString.coordinates().size < 2) { - return - } - - val nearestPointOnLineDistance = TurfMisc.nearestPointOnLine( - point, - lineString.coordinates(), - TurfConstants.UNIT_METERS - ).getProperty("dist").asDouble - - if (nearestPointOnLineDistance > ROUTE_LINE_UPDATE_MAX_DISTANCE_THRESHOLD_IN_METERS) { - return - } - - val distanceRemainingFromCache: Float = distanceRemainingCache[primaryRoute] - ?: primaryRoute.distance().toFloat() - val distanceRemaining = calculatePreciseDistanceTraveledAlongLine( - lineString, - distanceRemainingFromCache, - point - ) - val distanceTraveled = (routeDistance - distanceRemaining) - val percentTraveled = abs((distanceTraveled / routeDistance)).toFloat() - - if (percentTraveled > MINIMUM_ROUTE_LINE_OFFSET) { - val expression = getExpressionAtOffset(percentTraveled) - hideCasingLineAtOffset(percentTraveled) - hideRouteLineAtOffset(percentTraveled) - decorateRouteLine(expression) + ifNonNull( + primaryRouteLineGranularDistances, + primaryRouteRemainingDistancesIndex + ) { granularDistances, index -> + val traveledIndex = granularDistances.distancesArray[index] + val upcomingPoint = traveledIndex.point + + /** + * Take the remaining distance from the upcoming point on the route and extends it + * by the exact position of the puck. + */ + val remainingDistance = + traveledIndex.distanceRemaining + calculateDistance(upcomingPoint, point) + + /** + * Calculate the percentage of the route traveled and update the expression. + */ + if (granularDistances.distance >= remainingDistance) { + val offset = (1.0 - remainingDistance / granularDistances.distance) + if (offset >= 0) { + val expression = getExpressionAtOffset(offset) + hideCasingLineAtOffset(offset) + hideRouteLineAtOffset(offset) + decorateRouteLine(expression) + } } } } @@ -1219,14 +1333,14 @@ internal class MapRouteLine( false -> calculateRouteLineSegmentsFromCongestion( congestionSections, routeLineString, - route.distance() ?: 0.0, + route.distance(), isPrimaryRoute, congestionColorProvider ) true -> listOf( RouteLineExpressionData( - 0f, - congestionColorProvider("", isPrimaryRoute) + 0.0, + Expression.color(congestionColorProvider("", isPrimaryRoute)) ) ) } @@ -1277,16 +1391,21 @@ internal class MapRouteLine( if (expressionStops.isEmpty()) { expressionStops.add( RouteLineExpressionData( - 0f, - congestionColorProvider(congestionSections[i], isPrimary) + 0.0, + Expression.color( + congestionColorProvider( + congestionSections[i], + isPrimary + ) + ) ) ) } val routeColor = congestionColorProvider(congestionSections[i], isPrimary) expressionStops.add( RouteLineExpressionData( - fractionalDist.toFloat(), - routeColor + fractionalDist, + Expression.color(routeColor) ) ) previousCongestion = congestionSections[i] @@ -1295,8 +1414,8 @@ internal class MapRouteLine( if (expressionStops.isEmpty()) { expressionStops.add( RouteLineExpressionData( - 0f, - congestionColorProvider("", isPrimary) + 0.0, + Expression.color(congestionColorProvider("", isPrimary)) ) ) } @@ -1344,62 +1463,46 @@ internal class MapRouteLine( } } - val getDistanceBetweenPoints: (firstPoint: Point, secondPoint: Point) -> Double = - { firstPoint: Point, secondPoint: Point -> - TurfMeasurement.distance(firstPoint, secondPoint, TurfConstants.UNIT_METERS) - }.memoize(POINT_DISTANCE_CALCULATION_FUN_CACHE_SIZE) - - val getReversedCoordinates: (line: LineString) -> List = { line: LineString -> - line.coordinates().reversed() - }.memoize(1) - - fun calculatePreciseDistanceTraveledAlongLine( - line: LineString, - targetDistance: Float, - targetPoint: Point - ): Double { - val linePoints = getReversedCoordinates(line) - var runningDistance = 0.0 - var lastPointIndex = 0 - - for (currentIndex in linePoints.indices) { - if (currentIndex + 1 < linePoints.size) { - val nextSectionDistance = getDistanceBetweenPoints( - linePoints[currentIndex], - linePoints[currentIndex + 1] - ) - if (runningDistance + nextSectionDistance > targetDistance) { - val currentPointToTargetPointDist = - TurfMeasurement.distance( - linePoints[currentIndex], - targetPoint, - TurfConstants.UNIT_METERS - ) - - if (currentPointToTargetPointDist > nextSectionDistance) { - runningDistance += nextSectionDistance - lastPointIndex = currentIndex + 1 - } else { - lastPointIndex = currentIndex - } - break - } else { - runningDistance += nextSectionDistance - } - } else { - lastPointIndex = currentIndex - } + fun calculateGranularDistances(points: List): RouteLineGranularDistances { + var distance = 0.0 + val indexArray = SparseArray(points.size) + for (i in (points.size - 1) downTo 1) { + val curr = points[i] + val prev = points[i - 1] + distance += calculateDistance(curr, prev) + indexArray.append(i - 1, RouteLineDistancesIndex(prev, distance)) } + indexArray.append( + points.size - 1, + RouteLineDistancesIndex(points[points.size - 1], 0.0) + ) + return RouteLineGranularDistances(distance, indexArray) + } - val distFromTargetToLastIndexPoint = getDistanceBetweenPoints( - linePoints[lastPointIndex], - targetPoint + /** + * Calculates the distance between 2 points using + * [EPSG:3857 projection](https://epsg.io/3857). + * Info in [mapbox-gl-js/issues/9998](https://github.com/mapbox/mapbox-gl-js/issues/9998). + */ + fun calculateDistance(point1: Point, point2: Point): Double { + val d = doubleArrayOf( + (projectX(point1.longitude()) - projectX(point2.longitude())), + (projectY(point1.latitude()) - projectY(point2.latitude())) ) + return sqrt(d[0] * d[0] + d[1] * d[1]) + } - return if (lastPointIndex == 0) { - distFromTargetToLastIndexPoint - } else { - runningDistance + distFromTargetToLastIndexPoint + private fun projectX(x: Double): Double { + return x / 360.0 + 0.5 + } + + private fun projectY(y: Double): Double { + val sin = sin(y * Math.PI / 180) + val y2 = 0.5 - 0.25 * ln((1 + sin) / (1 - sin)) / Math.PI + return when { + y2 < 0 -> 0.0 + y2 > 1 -> 1.1 + else -> y2 } } } @@ -1419,4 +1522,29 @@ internal data class RouteFeatureData( val lineString: LineString ) -internal data class RouteLineExpressionData(val offset: Float, val segmentColor: Int) +internal data class RouteLineExpressionData( + val offset: Double, + val segmentColorExpression: Expression +) + +/** + * @param point the upcoming, not yet visited point on the route + * @param distanceRemaining distance remaining from the upcoming point + */ +internal data class RouteLineDistancesIndex(val point: Point, val distanceRemaining: Double) + +/** + * @param distance full distance of the route + * @param distancesArray array where index is the index of the upcoming not yet visited point on the route + */ +internal data class RouteLineGranularDistances( + val distance: Double, + val distancesArray: SparseArray +) + +/** + * @param nestedList nested arrays of legs -> steps -> points + * @param flatList list of all points on the route. + * The first and last point of adjacent steps overlap and are duplicated in this list. + */ +internal data class RoutePoints(val nestedList: List>>, val flatList: List) diff --git a/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/route/MapRouteProgressChangeListener.kt b/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/route/MapRouteProgressChangeListener.kt index 78fbd92e958..f169e7a790a 100644 --- a/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/route/MapRouteProgressChangeListener.kt +++ b/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/route/MapRouteProgressChangeListener.kt @@ -1,6 +1,5 @@ package com.mapbox.navigation.ui.route -import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.base.trip.model.RouteProgressState import com.mapbox.navigation.core.trip.session.RouteProgressObserver @@ -21,20 +20,11 @@ internal class MapRouteProgressChangeListener( private var shouldReInitializePrimaryRoute = false override fun onRouteProgressChanged(routeProgress: RouteProgress) { - onProgressChange(routeProgress) - } - - private fun onProgressChange(routeProgress: RouteProgress) { - val directionsRoute = routeLine.getPrimaryRoute() - updateRoute(directionsRoute, routeProgress) - } - - private fun updateRoute(directionsRoute: DirectionsRoute?, routeProgress: RouteProgress) { - routeLine.updateDistanceRemainingCache(routeProgress.route, routeProgress.distanceRemaining) + routeLine.updateUpcomingRoutePointIndex(routeProgress) val currentRoute = routeProgress.route val hasGeometry = currentRoute.geometry()?.isNotEmpty() ?: false - if (hasGeometry && currentRoute != directionsRoute) { + if (hasGeometry && currentRoute != routeLine.getPrimaryRoute()) { routeLine.reinitializeWithRoutes(listOf(currentRoute)) shouldReInitializePrimaryRoute = true @@ -59,9 +49,13 @@ internal class MapRouteProgressChangeListener( } when (routeProgress.currentState) { - RouteProgressState.LOCATION_TRACKING -> routeLine.inhibitVanishingPointUpdate(false) - RouteProgressState.ROUTE_COMPLETE -> routeLine.inhibitVanishingPointUpdate(false) - else -> routeLine.inhibitVanishingPointUpdate(true) + RouteProgressState.LOCATION_TRACKING -> + routeLine.inhibitAutomaticVanishingPointUpdate(false) + RouteProgressState.ROUTE_COMPLETE -> { + routeLine.inhibitAutomaticVanishingPointUpdate(true) + routeLine.setVanishingOffset(1.0) + } + else -> routeLine.inhibitAutomaticVanishingPointUpdate(true) } } diff --git a/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/route/NavigationMapRoute.java b/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/route/NavigationMapRoute.java index d069497f83d..3ccdc69200b 100644 --- a/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/route/NavigationMapRoute.java +++ b/libnavigation-ui/src/main/java/com/mapbox/navigation/ui/route/NavigationMapRoute.java @@ -13,6 +13,7 @@ import androidx.lifecycle.OnLifecycleEvent; import com.mapbox.api.directions.v5.models.DirectionsRoute; +import com.mapbox.geojson.Point; import com.mapbox.mapboxsdk.location.OnIndicatorPositionChangedListener; import com.mapbox.navigation.core.trip.session.RouteProgressObserver; import com.mapbox.navigation.ui.R; @@ -72,6 +73,7 @@ public class NavigationMapRoute implements LifecycleObserver { private MapRouteLine routeLine; private MapRouteArrow routeArrow; private boolean vanishRouteLineEnabled; + private long vanishingRouteLineUpdateIntervalNano; @Nullable private MapRouteLineInitializedCallback routeLineInitializedCallback; private List routeStyleDescriptors; @@ -97,9 +99,11 @@ private NavigationMapRoute(@Nullable MapboxNavigation navigation, @Nullable String belowLayer, boolean vanishRouteLineEnabled, @Nullable MapRouteLineInitializedCallback routeLineInitializedCallback, - @Nullable List routeStyleDescriptors) { + @Nullable List routeStyleDescriptors, + long vanishingRouteLineUpdateIntervalNano) { this.routeStyleDescriptors = routeStyleDescriptors; this.vanishRouteLineEnabled = vanishRouteLineEnabled; + this.vanishingRouteLineUpdateIntervalNano = vanishingRouteLineUpdateIntervalNano; this.styleRes = styleRes; this.belowLayer = belowLayer; this.mapView = mapView; @@ -325,9 +329,21 @@ public void onNewRouteProgress(@NonNull RouteProgress routeProgress) { } } - private OnIndicatorPositionChangedListener onIndicatorPositionChangedListener = point -> { - routeLine.updateTraveledRouteLine(point); - }; + private OnIndicatorPositionChangedListener onIndicatorPositionChangedListener + = new OnIndicatorPositionChangedListener() { + + private long lastUpdateNano = 0; + + @Override + public void onIndicatorPositionChanged(@NonNull Point point) { + long currentTimeNano = System.nanoTime(); + if (currentTimeNano - lastUpdateNano < vanishingRouteLineUpdateIntervalNano) { + return; + } + routeLine.updateTraveledRouteLine(point); + lastUpdateNano = currentTimeNano; + } + }; /** * Called during the onStart event of the Lifecycle owner to initialize resources. @@ -433,7 +449,7 @@ private void recreateRouteLine(@NonNull final Style style) { ? Collections.emptyList() : routeStyleDescriptors; final RouteLayerProvider layerProvider = getLayerProvider(routeStyleDescriptorsToUse, context, styleRes); - final float vanishingPointOffset = routeLine.getVanishPointOffset(); + final double vanishingPointOffset = routeLine.getVanishPointOffset(); routeLine = new MapRouteLine( context, style, @@ -483,6 +499,7 @@ public static class Builder { private boolean vanishRouteLineEnabled = false; @Nullable private MapRouteLineInitializedCallback routeLineInitializedCallback; @Nullable private List routeStyleDescriptors; + private long vanishingRouteLineUpdateIntervalNano = 62_500_000; /** * Instantiates a new Builder. @@ -552,6 +569,22 @@ public Builder withRouteStyleDescriptors(List routeStyleDe return this; } + /** + * Set minimum update interval (in nanoseconds) of the vanishing point when enabled. + *

+ * Defaults to 62 500 000, which is 16 times a second. + * Decreasing this number will improve the smoothness of updates but impact the performance. + * + * @return the builder + */ + @NonNull + public Builder withVanishingRouteLineUpdateIntervalNano( + long vanishingRouteLineUpdateIntervalNano + ) { + this.vanishingRouteLineUpdateIntervalNano = vanishingRouteLineUpdateIntervalNano; + return this; + } + /** * Indicate that the route line layer has been added to the current style * @@ -579,7 +612,8 @@ public NavigationMapRoute build() { belowLayer, vanishRouteLineEnabled, routeLineInitializedCallback, - routeStyleDescriptors + routeStyleDescriptors, + vanishingRouteLineUpdateIntervalNano ); } } diff --git a/libnavigation-ui/src/test/java/com/mapbox/navigation/ui/internal/utils/MemoizeUtilsTest.kt b/libnavigation-ui/src/test/java/com/mapbox/navigation/ui/internal/utils/MemoizeUtilsTest.kt deleted file mode 100644 index 5750d2a0071..00000000000 --- a/libnavigation-ui/src/test/java/com/mapbox/navigation/ui/internal/utils/MemoizeUtilsTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.mapbox.navigation.ui.internal.utils - -import com.mapbox.navigation.ui.internal.utils.MemoizeUtils.memoize -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE) -class MemoizeUtilsTest { - - @Test - fun memoizeKey1Test() { - var counter = 0 - val sumFun: (a: Int) -> Int = { a: Int -> - counter += 1 - a + 1 - }.memoize(5) - - val firstResult = sumFun(1) - val secondResult = sumFun(1) - - assertEquals(firstResult, secondResult) - assertEquals(1, counter) - } - - @Test - fun memoizeKey2Test() { - var counter = 0 - val sumFun: (a: Int, b: Int) -> Int = { a: Int, b: Int -> - counter += 1 - a + b - }.memoize(5) - - val firstResult = sumFun(1, 2) - val secondResult = sumFun(1, 2) - - assertEquals(firstResult, secondResult) - assertEquals(1, counter) - } -} diff --git a/libnavigation-ui/src/test/java/com/mapbox/navigation/ui/route/MapRouteLineTest.kt b/libnavigation-ui/src/test/java/com/mapbox/navigation/ui/route/MapRouteLineTest.kt index 0195778ea39..09c6b3a2c76 100644 --- a/libnavigation-ui/src/test/java/com/mapbox/navigation/ui/route/MapRouteLineTest.kt +++ b/libnavigation-ui/src/test/java/com/mapbox/navigation/ui/route/MapRouteLineTest.kt @@ -9,6 +9,7 @@ import com.mapbox.core.constants.Constants import com.mapbox.geojson.FeatureCollection import com.mapbox.geojson.LineString import com.mapbox.geojson.Point +import com.mapbox.geojson.utils.PolylineUtils import com.mapbox.mapboxsdk.location.LocationComponentConstants import com.mapbox.mapboxsdk.maps.Style import com.mapbox.mapboxsdk.style.expressions.Expression @@ -17,6 +18,7 @@ import com.mapbox.mapboxsdk.style.layers.LineLayer import com.mapbox.mapboxsdk.style.layers.PropertyValue import com.mapbox.mapboxsdk.style.layers.SymbolLayer import com.mapbox.mapboxsdk.style.sources.GeoJsonSource +import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.ui.R import com.mapbox.navigation.ui.internal.ThemeSwitcher import com.mapbox.navigation.ui.internal.route.MapRouteSourceProvider @@ -28,8 +30,6 @@ import com.mapbox.navigation.ui.internal.route.RouteConstants.PRIMARY_ROUTE_LAYE import com.mapbox.navigation.ui.internal.route.RouteConstants.PRIMARY_ROUTE_TRAFFIC_LAYER_ID import com.mapbox.navigation.ui.internal.route.RouteConstants.WAYPOINT_LAYER_ID import com.mapbox.navigation.ui.internal.route.RouteLayerProvider -import com.mapbox.navigation.ui.route.MapRouteLine.MapRouteLineSupport.calculatePreciseDistanceTraveledAlongLine -import com.mapbox.turf.TurfConstants import com.mapbox.turf.TurfMeasurement import io.mockk.every import io.mockk.mockk @@ -348,7 +348,7 @@ class MapRouteLineTest { false, false, mapRouteSourceProvider, - 0f, + 0.0, null ) @@ -683,7 +683,7 @@ class MapRouteLineTest { null ).also { it.draw(listOf(route)) } - val expression = mapRouteLine.getExpressionAtOffset(.2f) + val expression = mapRouteLine.getExpressionAtOffset(.2) assertEquals(expectedExpression, expression.toString()) } @@ -703,7 +703,7 @@ class MapRouteLineTest { null ).also { it.draw(listOf(route)) } - val expression = mapRouteLine.getExpressionAtOffset(0f) + val expression = mapRouteLine.getExpressionAtOffset(0.0) assertEquals(expectedExpression, expression.toString()) } @@ -725,7 +725,7 @@ class MapRouteLineTest { null ).also { it.draw(listOf(route)) } - val expression = mapRouteLine.getExpressionAtOffset(.2f) + val expression = mapRouteLine.getExpressionAtOffset(.2) assertEquals(expectedExpression, expression.toString()) } @@ -747,7 +747,7 @@ class MapRouteLineTest { null ).also { it.draw(listOf(route)) } - val expression = mapRouteLine.getExpressionAtOffset(.9f) + val expression = mapRouteLine.getExpressionAtOffset(.9) assertEquals(expectedExpression, expression.toString()) } @@ -1107,11 +1107,11 @@ class MapRouteLineTest { true, false, mapRouteSourceProvider, - 0f, + 0.0, null ) - val expression = mapRouteLine.getExpressionAtOffset(.2f) + val expression = mapRouteLine.getExpressionAtOffset(.2) assertEquals(expectedExpression, expression.toString()) } @@ -1119,16 +1119,22 @@ class MapRouteLineTest { @Test fun updateVanishingPoint() { val expectedRouteLineVanishingExpression = "[\"step\", [\"line-progress\"], " + - "[\"rgba\", 0.0, 0.0, 0.0, 0.0], 0.12680313, [\"rgba\", 86.0, 168.0, 251.0, 1.0]]" + "[\"rgba\", 0.0, 0.0, 0.0, 0.0], 0.01792979, [\"rgba\", 86.0, 168.0, 251.0, 1.0]]" val expectedRouteLineCasingVanishingExpression = "[\"step\", [\"line-progress\"], " + - "[\"rgba\", 0.0, 0.0, 0.0, 0.0], 0.12680313, [\"rgba\", 47.0, 122.0, 198.0, 1.0]]" + "[\"rgba\", 0.0, 0.0, 0.0, 0.0], 0.01792979, [\"rgba\", 47.0, 122.0, 198.0, 1.0]]" val expectedRouteTrafficLineVanishingExpression = "[\"step\", [\"line-progress\"], " + - "[\"rgba\", 0.0, 0.0, 0.0, 0.0], 0.12680313, [\"rgba\", 86.0, 168.0, 251.0, 1.0], " + - "0.150941, [\"rgba\", 86.0, 168.0, 251.0, 1.0], 0.1905395, [\"rgba\", 86.0, 168.0, " + - "251.0, 1.0], 0.26964808, [\"rgba\", 255.0, 77.0, 77.0, 1.0], 0.27944908, " + - "[\"rgba\", 86.0, 168.0, 251.0, 1.0], 0.33272266, [\"rgba\", 86.0, 168.0, 251.0, " + - "1.0], 0.39298487, [\"rgba\", 86.0, 168.0, 251.0, 1.0], 0.48991048, [\"rgba\", " + - "255.0, 77.0, 77.0, 1.0], 0.504132, [\"rgba\", 86.0, 168.0, 251.0, 1.0], 0.8017851, " + + "[\"rgba\", 0.0, 0.0, 0.0, 0.0], 0.01792979, " + + "[\"rgba\", 86.0, 168.0, 251.0, 1.0], 0.056129422, " + + "[\"rgba\", 86.0, 168.0, 251.0, 1.0], 0.09373502, " + + "[\"rgba\", 86.0, 168.0, 251.0, 1.0], 0.150941, " + + "[\"rgba\", 86.0, 168.0, 251.0, 1.0], 0.1905395, " + + "[\"rgba\", 86.0, 168.0, 251.0, 1.0], 0.26964808, " + + "[\"rgba\", 255.0, 77.0, 77.0, 1.0], 0.27944908, " + + "[\"rgba\", 86.0, 168.0, 251.0, 1.0], 0.33272266, " + + "[\"rgba\", 86.0, 168.0, 251.0, 1.0], 0.39298487, " + + "[\"rgba\", 86.0, 168.0, 251.0, 1.0], 0.48991048, " + + "[\"rgba\", 255.0, 77.0, 77.0, 1.0], 0.504132, " + + "[\"rgba\", 86.0, 168.0, 251.0, 1.0], 0.8017851, " + "[\"rgba\", 86.0, 168.0, 251.0, 1.0], 1.0000086, [\"rgba\", 86.0, 168.0, 251.0, 1.0]]" every { style.layers } returns listOf(primaryRouteLayer) every { style.isFullyLoaded } returnsMany listOf( @@ -1159,7 +1165,7 @@ class MapRouteLineTest { route.geometry()!!, Constants.PRECISION_6 ).coordinates() - val inputPoint = TurfMeasurement.midpoint(coordinates[4], coordinates[5]) + val inputPoint = Point.fromLngLat(-122.524000, 37.975207) val mapRouteLine = MapRouteLine( ctx, style, @@ -1169,7 +1175,24 @@ class MapRouteLineTest { mapRouteSourceProvider, null ).also { it.draw(listOf(route)) } - mapRouteLine.inhibitVanishingPointUpdate(false) + mapRouteLine.inhibitAutomaticVanishingPointUpdate(false) + val routeProgress = mockk { + every { currentLegProgress } returns mockk { + every { legIndex } returns 0 + every { currentStepProgress } returns mockk { + every { stepPoints } returns PolylineUtils.decode( + route.legs()!![0].steps()!![0].geometry()!!, + 6 + ) + every { distanceTraveled } returns 15f + every { step } returns mockk { + every { distance() } returns route.legs()!![0].steps()!![0].distance() + } + every { stepIndex } returns 0 + } + } + } + mapRouteLine.updateUpcomingRoutePointIndex(routeProgress) mapRouteLine.updateTraveledRouteLine(inputPoint) @@ -1191,6 +1214,129 @@ class MapRouteLineTest { ) } + @Test + fun doNotUpdateVanishingPointWhenInhibited() { + every { style.layers } returns listOf(primaryRouteLayer) + every { style.isFullyLoaded } returnsMany listOf( + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true + ) + every { + style.getLayerAs("mapbox-navigation-route-casing-layer") + } returns primaryRouteCasingLayer + every { style.getLayer("mapbox-navigation-route-layer") } returns primaryRouteLayer + every { + style.getLayer("mapbox-navigation-route-traffic-layer") + } returns primaryRouteTrafficLayer + val route = getDirectionsRoute() + val inputPoint = Point.fromLngLat(-122.524000, 37.975207) + val mapRouteLine = MapRouteLine( + ctx, + style, + styleRes, + null, + layerProvider, + mapRouteSourceProvider, + null + ).also { it.draw(listOf(route)) } + mapRouteLine.inhibitAutomaticVanishingPointUpdate(false) + val routeProgress = mockk { + every { currentLegProgress } returns mockk { + every { legIndex } returns 0 + every { currentStepProgress } returns mockk { + every { stepPoints } returns PolylineUtils.decode( + route.legs()!![0].steps()!![0].geometry()!!, + 6 + ) + every { distanceTraveled } returns 15f + every { step } returns mockk { + every { distance() } returns route.legs()!![0].steps()!![0].distance() + } + every { stepIndex } returns 0 + } + } + } + mapRouteLine.updateUpcomingRoutePointIndex(routeProgress) + mapRouteLine.inhibitAutomaticVanishingPointUpdate(true) + + mapRouteLine.updateTraveledRouteLine(inputPoint) + + verify(exactly = 0) { primaryRouteCasingLayer.setProperties(any()) } + verify(exactly = 0) { primaryRouteLayer.setProperties(any()) } + verify(exactly = 0) { primaryRouteTrafficLayer.setProperties(any()) } + } + + @Test + fun doNotUpdateVanishingPointWhenRouteProgressOutdated() { + every { style.layers } returns listOf(primaryRouteLayer) + every { style.isFullyLoaded } returnsMany listOf( + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true + ) + every { + style.getLayerAs("mapbox-navigation-route-casing-layer") + } returns primaryRouteCasingLayer + every { style.getLayer("mapbox-navigation-route-layer") } returns primaryRouteLayer + every { + style.getLayer("mapbox-navigation-route-traffic-layer") + } returns primaryRouteTrafficLayer + val route = getDirectionsRoute() + val inputPoint = Point.fromLngLat(-122.524000, 37.975207) + val mapRouteLine = MapRouteLine( + ctx, + style, + styleRes, + null, + layerProvider, + mapRouteSourceProvider, + null + ).also { it.draw(listOf(route)) } + mapRouteLine.inhibitAutomaticVanishingPointUpdate(false) + val routeProgress = mockk { + every { currentLegProgress } returns mockk { + every { legIndex } returns 0 + every { currentStepProgress } returns mockk { + every { stepPoints } returns PolylineUtils.decode( + route.legs()!![0].steps()!![0].geometry()!!, + 6 + ) + every { distanceTraveled } returns 15f + every { step } returns mockk { + every { distance() } returns route.legs()!![0].steps()!![0].distance() + } + every { stepIndex } returns 0 + } + } + } + mapRouteLine.updateUpcomingRoutePointIndex(routeProgress) + + Thread.sleep((RouteConstants.MAX_ELAPSED_SINCE_INDEX_UPDATE_NANO / 1E6).toLong()) + + mapRouteLine.updateTraveledRouteLine(inputPoint) + + verify(exactly = 0) { primaryRouteCasingLayer.setProperties(any()) } + verify(exactly = 0) { primaryRouteLayer.setProperties(any()) } + verify(exactly = 0) { primaryRouteTrafficLayer.setProperties(any()) } + } + @Test fun updateVanishingPointInhibitedByDefault() { every { style.layers } returns listOf(primaryRouteLayer) @@ -1377,79 +1523,6 @@ class MapRouteLineTest { } } - @Test - fun calculatePreciseDistanceTraveledAlongLineTest() { - val route = getDirectionsRoute() - val lineString = LineString.fromPolyline( - route.geometry()!!, - Constants.PRECISION_6 - ) - val targetPoint = TurfMeasurement.midpoint( - lineString.coordinates()[6], - lineString.coordinates()[7] - ) - val subCoordinates = lineString.coordinates().subList(7, lineString.coordinates().size) - val length = TurfMeasurement.length( - listOf(targetPoint).plus(subCoordinates), - TurfConstants.UNIT_METERS - ) - - val result = calculatePreciseDistanceTraveledAlongLine( - lineString, - length.toFloat() - 50, - targetPoint - ) - - assertEquals(length, result, 0.01) - } - - @Test - fun calculatePreciseDistanceTraveledAlongLineWhenTargetFirstPoint() { - val route = getDirectionsRoute() - val lineString = LineString.fromPolyline( - route.geometry()!!, - Constants.PRECISION_6 - ) - val targetPoint = lineString.coordinates().first() - val length = TurfMeasurement.length(lineString.coordinates(), TurfConstants.UNIT_METERS) - - val result = calculatePreciseDistanceTraveledAlongLine( - lineString, - length.toFloat() - 50, - targetPoint - ) - - assertEquals(length, result, 0.01) - } - - @Test - fun calculatePreciseDistanceTraveledAlongLineWhenTargetLastPoint() { - val route = getDirectionsRoute() - val lineString = LineString.fromPolyline( - route.geometry()!!, - Constants.PRECISION_6 - ) - val lineCoordinates = lineString.coordinates() - - val targetPoint = TurfMeasurement.midpoint( - lineCoordinates[lineCoordinates.lastIndex - 1], - lineCoordinates[lineCoordinates.lastIndex] - ) - val expectedLength = TurfMeasurement.distance( - lineCoordinates.last(), - targetPoint, - TurfConstants.UNIT_METERS - ) - - val result = calculatePreciseDistanceTraveledAlongLine( - lineString, - 0f, - targetPoint - ) - - assertEquals(expectedLength, result, 0.01) - } - @Test fun getStyledFloatArrayTest() { val result = MapRouteLine.MapRouteLineSupport.getStyledFloatArray( diff --git a/libnavigation-ui/src/test/java/com/mapbox/navigation/ui/route/MapRouteProgressChangeListenerTest.kt b/libnavigation-ui/src/test/java/com/mapbox/navigation/ui/route/MapRouteProgressChangeListenerTest.kt index f0b2320fd8c..eb2bb470ca2 100644 --- a/libnavigation-ui/src/test/java/com/mapbox/navigation/ui/route/MapRouteProgressChangeListenerTest.kt +++ b/libnavigation-ui/src/test/java/com/mapbox/navigation/ui/route/MapRouteProgressChangeListenerTest.kt @@ -4,6 +4,7 @@ import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.mapboxsdk.style.expressions.Expression import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.base.trip.model.RouteProgressState +import io.mockk.clearMocks import io.mockk.every import io.mockk.mockk import io.mockk.slot @@ -30,7 +31,7 @@ class MapRouteProgressChangeListenerTest { every { routeLine.retrieveDirectionsRoutes() } returns emptyList() every { routeLine.draw(capture(drawDirections)) } returns Unit every { routeLine.reinitializeWithRoutes(capture(routeListSlot)) } returns Unit - every { routeLine.vanishPointOffset } returns 0f + every { routeLine.vanishPointOffset } returns 0.0 every { routeArrow.addUpcomingManeuverArrow(capture(addRouteProgress)) } returns Unit every { routeArrow.routeArrowIsVisible() } returns true } @@ -241,4 +242,36 @@ class MapRouteProgressChangeListenerTest { verify { routeLine.reinitializePrimaryRoute() } } + + @Test + fun `should vanish the whole route on arrival`() { + val progressChangeListener = MapRouteProgressChangeListener(routeLine, routeArrow) + val routeProgress: RouteProgress = mockk(relaxed = true) { + every { currentState } returns RouteProgressState.ROUTE_COMPLETE + } + + progressChangeListener.onRouteProgressChanged(routeProgress) + + verify { routeLine.setVanishingOffset(1.0) } + } + + @Test + fun `should inhibit for automatic vanishing point updates when not tracking`() { + val progressChangeListener = MapRouteProgressChangeListener(routeLine, routeArrow) + + RouteProgressState.values().forEach { + clearMocks(routeLine) + val routeProgress: RouteProgress = mockk(relaxed = true) { + every { currentState } returns it + } + progressChangeListener.onRouteProgressChanged(routeProgress) + if (it == RouteProgressState.LOCATION_TRACKING) { + verify(exactly = 0) { routeLine.inhibitAutomaticVanishingPointUpdate(true) } + verify(exactly = 1) { routeLine.inhibitAutomaticVanishingPointUpdate(false) } + } else { + verify(exactly = 1) { routeLine.inhibitAutomaticVanishingPointUpdate(true) } + verify(exactly = 0) { routeLine.inhibitAutomaticVanishingPointUpdate(false) } + } + } + } }