diff --git a/CHANGELOG.md b/CHANGELOG.md index 0657fee4a0c..ab4099c1f22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,30 @@ # Changelog for the Mapbox Navigation SDK Core Framework for Android -## Navigation SDK Core Framework 3.4.0-rc.1 - 17 September, 2024 +## Navigation SDK Core Framework 3.5.0-beta.1 - 17 October, 2024 #### Features - +- Added support for SVG junction views, see `MapboxJunctionApi#generateJunction(instructions: BannerInstructions, @JunctionViewFormat format: String, consumer: MapboxNavigationConsumer>)`. [#6803](https://github.com/mapbox/mapbox-navigation-android/pull/6803) +- Added experimental `NavigationRoute#routeRefreshMetadata` which contains data related to refresh of the route object. [#6736](https://github.com/mapbox/mapbox-navigation-android/pull/6736) #### Bug fixes and improvements +- Nav SDK now removes passed alternative routes as soon as user passed fork point. [#6813](https://github.com/mapbox/mapbox-navigation-android/pull/6813) +- Fixed a potential route line layers visibility race, which might have happened if you invoked `MapboxRouteLineView#showPrimaryRoute` and `MapboxRouteLineView#renderRouteDrawData` approximately at the same time. [#6751](https://github.com/mapbox/mapbox-navigation-android/pull/6751) +- Optimized CA routes handling by skiping route parsing if it's already exist in direction session [#6868](https://github.com/mapbox/mapbox-navigation-android/pull/6868) - Fixed `CarSearchLocationProvider` produces _NullPointerException_ when using Mapbox Search SDK. [#6702](https://github.com/mapbox/mapbox-navigation-android/pull/6702) +- Fixed an issue preventing Copilot from correctly recording history events. [#6787](https://github.com/mapbox/mapbox-navigation-android/pull/6787) +- Improved reroute and alternative routes behavior [#6989](https://github.com/mapbox/mapbox-navigation-android/pull/6989) #### Known issues :warning: #### Other changes - +- Changed `AutoArrivalController`: moves to a next waypoint immediately. ### Mapbox dependencies This release depends on, and has been tested with, the following Mapbox dependencies: -- Mapbox Maps SDK `v11.7.0-rc.1` ([release notes](https://github.com/mapbox/mapbox-maps-android/releases/tag/v11.7.0-rc.1)) -- Mapbox Navigation Native `v319.0.0` -- Mapbox Core Common `v24.7.0-rc.2` -- Mapbox Java `v7.2.0` ([release notes](https://github.com/mapbox/mapbox-java/releases/tag/v7.2.0)) +- Mapbox Maps SDK `v11.8.0-beta.1` ([release notes](https://github.com/mapbox/mapbox-maps-android/releases/tag/v11.8.0-beta.1)) +- Mapbox Navigation Native `v321.0.0-beta.1` +- Mapbox Core Common `v24.8.0-beta.1` +- Mapbox Java `v7.3.1` ([release notes](https://github.com/mapbox/mapbox-java/releases/tag/v7.3.1)) ## Navigation SDK Core Framework 3.4.0-beta.1 - 05 September, 2024 diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index b2ead851d80..00000000000 --- a/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @mapbox/navigation-android diff --git a/LICENSE.md b/LICENSE.md index 172c3afd537..b7e616e4d9d 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -225,6 +225,11 @@ License: [The Apache Software License, Version 2.0](http://www.apache.org/licens =========================================================================== +Mapbox Navigation uses portions of the Cronet (Cronet API. Does not contain implementation.). +License: [Chromium and built-in dependencies](https://storage.cloud.google.com/chromium-cronet/android/119.0.6045.31/Release/cronet/LICENSE) + +=========================================================================== + Mapbox Navigation uses portions of the Experimental annotation (Java annotation for use on unstable Android API surfaces. When used in conjunction with the Experimental annotation lint checks, this annotation provides functional parity with Kotlin's Experimental annotation.). URL: [https://developer.android.com/jetpack/androidx/releases/annotation#1.3.0](https://developer.android.com/jetpack/androidx/releases/annotation#1.3.0) License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -359,6 +364,26 @@ License: [The Apache Software License, Version 2.0](http://www.apache.org/licens =========================================================================== +Mapbox Navigation uses portions of the play-services-base. +License: [Android Software Development Kit License](https://developer.android.com/studio/terms.html) + +=========================================================================== + +Mapbox Navigation uses portions of the play-services-basement. +License: [Android Software Development Kit License](https://developer.android.com/studio/terms.html) + +=========================================================================== + +Mapbox Navigation uses portions of the play-services-cronet. +License: [Android Software Development Kit License](https://developer.android.com/studio/terms.html) + +=========================================================================== + +Mapbox Navigation uses portions of the play-services-tasks. +License: [Android Software Development Kit License](https://developer.android.com/studio/terms.html) + +=========================================================================== + Mapbox Navigation uses portions of the Retrofit. License: [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -624,6 +649,11 @@ License: [The Apache Software License, Version 2.0](http://www.apache.org/licens =========================================================================== +Mapbox Navigation uses portions of the Cronet (Cronet API. Does not contain implementation.). +License: [Chromium and built-in dependencies](https://storage.cloud.google.com/chromium-cronet/android/119.0.6045.31/Release/cronet/LICENSE) + +=========================================================================== + Mapbox Navigation uses portions of the Experimental annotation (Java annotation for use on unstable Android API surfaces. When used in conjunction with the Experimental annotation lint checks, this annotation provides functional parity with Kotlin's Experimental annotation.). URL: [https://developer.android.com/jetpack/androidx/releases/annotation#1.3.0](https://developer.android.com/jetpack/androidx/releases/annotation#1.3.0) License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -746,6 +776,26 @@ License: [The Apache Software License, Version 2.0](http://www.apache.org/licens =========================================================================== +Mapbox Navigation uses portions of the play-services-base. +License: [Android Software Development Kit License](https://developer.android.com/studio/terms.html) + +=========================================================================== + +Mapbox Navigation uses portions of the play-services-basement. +License: [Android Software Development Kit License](https://developer.android.com/studio/terms.html) + +=========================================================================== + +Mapbox Navigation uses portions of the play-services-cronet. +License: [Android Software Development Kit License](https://developer.android.com/studio/terms.html) + +=========================================================================== + +Mapbox Navigation uses portions of the play-services-tasks. +License: [Android Software Development Kit License](https://developer.android.com/studio/terms.html) + +=========================================================================== + Mapbox Navigation uses portions of the VersionedParcelable (Provides a stable but relatively compact binary serialization format that can be passed across processes or persisted safely.). URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1353,6 +1403,12 @@ License: [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENS #### Nav SDK Test Router module +Mapbox Navigation uses portions of the Activity (Provides the base Activity subclass and the relevant hooks to build a composable structure on top.). +URL: [https://developer.android.com/jetpack/androidx](https://developer.android.com/jetpack/androidx) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + Mapbox Navigation uses portions of the Android App Startup Runtime. URL: [https://developer.android.com/jetpack/androidx/releases/startup#1.1.1](https://developer.android.com/jetpack/androidx/releases/startup#1.1.1) License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1383,12 +1439,30 @@ License: [The Apache Software License, Version 2.0](http://www.apache.org/licens =========================================================================== +Mapbox Navigation uses portions of the Android Lifecycle LiveData. +URL: [https://developer.android.com/topic/libraries/architecture/index.html](https://developer.android.com/topic/libraries/architecture/index.html) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox Navigation uses portions of the Android Lifecycle LiveData Core. +URL: [https://developer.android.com/topic/libraries/architecture/index.html](https://developer.android.com/topic/libraries/architecture/index.html) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + Mapbox Navigation uses portions of the Android Lifecycle Runtime. URL: [https://developer.android.com/jetpack/androidx/releases/lifecycle#2.3.1](https://developer.android.com/jetpack/androidx/releases/lifecycle#2.3.1) License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) =========================================================================== +Mapbox Navigation uses portions of the Android Lifecycle ViewModel. +URL: [https://developer.android.com/topic/libraries/architecture/index.html](https://developer.android.com/topic/libraries/architecture/index.html) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + Mapbox Navigation uses portions of the Android Lifecycle-Common. URL: [https://developer.android.com/jetpack/androidx/releases/lifecycle#2.3.1](https://developer.android.com/jetpack/androidx/releases/lifecycle#2.3.1) License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1412,6 +1486,30 @@ License: [The Apache Software License, Version 2.0](http://www.apache.org/licens =========================================================================== +Mapbox Navigation uses portions of the Android Support Library Custom View (The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later.). +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox Navigation uses portions of the Android Support Library fragment (The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later.). +URL: [https://developer.android.com/jetpack/androidx](https://developer.android.com/jetpack/androidx) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox Navigation uses portions of the Android Support Library loader (The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later.). +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox Navigation uses portions of the Android Support Library View Pager (The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later.). +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + Mapbox Navigation uses portions of the Android Tracing. URL: [https://developer.android.com/jetpack/androidx/releases/tracing#1.0.0](https://developer.android.com/jetpack/androidx/releases/tracing#1.0.0) License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1436,6 +1534,11 @@ License: [The Apache Software License, Version 2.0](http://www.apache.org/licens =========================================================================== +Mapbox Navigation uses portions of the Cronet (Cronet API. Does not contain implementation.). +License: [Chromium and built-in dependencies](https://storage.cloud.google.com/chromium-cronet/android/119.0.6045.31/Release/cronet/LICENSE) + +=========================================================================== + Mapbox Navigation uses portions of the Experimental annotation (Java annotation for use on unstable Android API surfaces. When used in conjunction with the Experimental annotation lint checks, this annotation provides functional parity with Kotlin's Experimental annotation.). URL: [https://developer.android.com/jetpack/androidx/releases/annotation#1.3.0](https://developer.android.com/jetpack/androidx/releases/annotation#1.3.0) License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1575,6 +1678,26 @@ License: [The Apache Software License, Version 2.0](http://www.apache.org/licens =========================================================================== +Mapbox Navigation uses portions of the play-services-base. +License: [Android Software Development Kit License](https://developer.android.com/studio/terms.html) + +=========================================================================== + +Mapbox Navigation uses portions of the play-services-basement. +License: [Android Software Development Kit License](https://developer.android.com/studio/terms.html) + +=========================================================================== + +Mapbox Navigation uses portions of the play-services-cronet. +License: [Android Software Development Kit License](https://developer.android.com/studio/terms.html) + +=========================================================================== + +Mapbox Navigation uses portions of the play-services-tasks. +License: [Android Software Development Kit License](https://developer.android.com/studio/terms.html) + +=========================================================================== + Mapbox Navigation uses portions of the VersionedParcelable (Provides a stable but relatively compact binary serialization format that can be passed across processes or persisted safely.). URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) diff --git a/Makefile b/Makefile index 90d08894d5d..31863f0b6e8 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,16 @@ PUBLIC_API_PREFIX = public-api +# $(1) modules' names(e.g. "libnavigation-core, libnavigation-base...") +define reports-copy-to + for module in $(1) ; do \ + if [ -d "$${module}/build/jacoco" ]; then \ + cp $${module}/build/jacoco/jacoco.xml $(2)/$${module}.xml || exit 1 & \ + else \ + echo "Directory $${module}/build/jacoco does not exist. Skipping." ; \ + fi \ + done +endef + RELEASED_CORE_MODULES = \ libnavigation-base \ libnavigation-metrics \ @@ -30,31 +41,31 @@ APPLICATION_MODULES = \ instrumentation-tests define run-gradle-tasks - COMMAND="./gradlew"; \ + COMMAND="./gradlew"; \ for module in $(1); do \ - COMMAND="$${COMMAND} $$module:$(2)"; \ + COMMAND="$${COMMAND} $$module:$(2)"; \ done; \ echo "executing $$COMMAND"; \ - eval $$COMMAND + eval $$COMMAND; endef .PHONY: check-kotlin-lint check-kotlin-lint: - $(call run-gradle-tasks,$(CORE_MODULES),ktlint) \ - && $(call run-gradle-tasks,$(UI_MODULES),ktlint) \ - && $(call run-gradle-tasks,$(APPLICATION_MODULES),ktlint) + $(call run-gradle-tasks,$(CORE_MODULES),ktlint) + $(call run-gradle-tasks,$(UI_MODULES),ktlint) + $(call run-gradle-tasks,$(APPLICATION_MODULES),ktlint) .PHONY: format-kotlin-lint format-kotlin-lint: - $(call run-gradle-tasks,$(CORE_MODULES),ktlintFormat) \ - && $(call run-gradle-tasks,$(UI_MODULES),ktlintFormat) \ - && $(call run-gradle-tasks,$(APPLICATION_MODULES),ktlintFormat) + $(call run-gradle-tasks,$(CORE_MODULES),ktlintFormat) + $(call run-gradle-tasks,$(UI_MODULES),ktlintFormat) + $(call run-gradle-tasks,$(APPLICATION_MODULES),ktlintFormat) .PHONY: check-android-lint check-android-lint: - $(call run-gradle-tasks,$(CORE_MODULES),lint) \ - && $(call run-gradle-tasks,$(UI_MODULES),lint) \ - && $(call run-gradle-tasks,$(APPLICATION_MODULES),lint) + $(call run-gradle-tasks,$(CORE_MODULES),lint) + $(call run-gradle-tasks,$(UI_MODULES),lint) + $(call run-gradle-tasks,$(APPLICATION_MODULES),lint) .PHONY: license-verification license-verification: @@ -72,13 +83,13 @@ javadoc-dokka: .PHONY: dependency-graphs dependency-graphs: - $(call run-gradle-tasks,$(CORE_MODULES),generateDependencyGraphMapboxLibraries) \ - && $(call run-gradle-tasks,$(UI_MODULES),generateDependencyGraphMapboxLibraries) \ + $(call run-gradle-tasks,$(CORE_MODULES),generateDependencyGraphMapboxLibraries) + $(call run-gradle-tasks,$(UI_MODULES),generateDependencyGraphMapboxLibraries) .PHONY: dependency-updates dependency-updates: - $(call run-gradle-tasks,$(CORE_MODULES),dependencyUpdates) \ - && $(call run-gradle-tasks,$(UI_MODULES),dependencyUpdates) \ + $(call run-gradle-tasks,$(CORE_MODULES),dependencyUpdates) + $(call run-gradle-tasks,$(UI_MODULES),dependencyUpdates) .PHONY: find-all-common-sdk-versions find-all-common-sdk-versions: @@ -106,9 +117,19 @@ core-unit-tests-jacoco: $(call run-gradle-tasks,$(CORE_MODULES),jacocoTestReport) .PHONY: core-unit-tests-release-jacoco +.SILENT: core-unit-tests-release-jacoco core-unit-tests-release-jacoco: $(call run-gradle-tasks,$(CORE_MODULES),jacocoTestReleaseUnitTestReport) +# Prepare core coverage +.PHONY: prepare-core-coverage-reports +.SILENT: prepare-core-coverage-reports +prepare-core-coverage-reports: core-unit-tests-release-jacoco + rm -rf $(CORE_REPORTS_DIR) \ + && mkdir -p $(CORE_REPORTS_DIR) \ + && $(call reports-copy-to,$(RELEASED_CORE_MODULES),$(CORE_REPORTS_DIR)) \ + && wait + .PHONY: core-dependency-graph core-dependency-graph: $(call run-gradle-tasks,$(CORE_MODULES),generateDependencyGraphMapboxLibraries) @@ -156,9 +177,19 @@ ui-unit-tests-jacoco: $(call run-gradle-tasks,$(UI_MODULES),jacocoTestReport) .PHONY: ui-unit-tests-release-jacoco +.SILENT: core-unit-tests-release-jacoco ui-unit-tests-release-jacoco: $(call run-gradle-tasks,$(UI_MODULES),jacocoTestReleaseUnitTestReport) +# Prepare ui coverage +.PHONY: prepare-ui-coverage-reports +.SILENT: prepare-ui-coverage-reports +prepare-ui-coverage-reports: ui-unit-tests-release-jacoco + rm -rf $(UI_REPORTS_DIR) \ + && mkdir -p $(UI_REPORTS_DIR) \ + && $(call reports-copy-to,$(RELEASED_UI_MODULES),$(UI_REPORTS_DIR)) \ + && wait + .PHONY: publish-local publish-local: ./gradlew publishToMavenLocal diff --git a/androidauto/build.gradle b/androidauto/build.gradle index bda466b7456..6d9f8045e9a 100644 --- a/androidauto/build.gradle +++ b/androidauto/build.gradle @@ -30,6 +30,12 @@ android { consumerProguardFiles 'proguard-rules.pro', "../proguard/proguard-project.pro" } + buildTypes { + release { + crunchPngs = false + } + } + testOptions { execution 'ANDROIDX_TEST_ORCHESTRATOR' } diff --git a/androidauto/gradle.properties b/androidauto/gradle.properties index aee6c6a38b5..210eac3ca0e 100644 --- a/androidauto/gradle.properties +++ b/androidauto/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=android-auto-components POM_ARTIFACT_TITLE=Mapbox Navigation SDK POM_DESCRIPTION=Artifact that provides Android Auto compoents \ No newline at end of file diff --git a/build.gradle b/build.gradle index aa5f57ad228..0ad8e43a816 100644 --- a/build.gradle +++ b/build.gradle @@ -85,7 +85,6 @@ subprojects { apply from: "${rootDir}/gradle/dependency-updates.gradle" apply from: "${rootDir}/gradle/checkstyle.gradle" apply from: "${rootDir}/gradle/dependencies-graph.gradle" - apply from: "${rootDir}/gradle/save-sdk-version.gradle" plugins.withId('org.jetbrains.kotlin.jvm') { compileKotlin { diff --git a/gradle.properties b/gradle.properties index 469116820c2..3ace1e24505 100644 --- a/gradle.properties +++ b/gradle.properties @@ -33,5 +33,3 @@ systemProp.kotlin.daemon.jvm.options=-Xss8m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true - -RELEASE_TAG_PREFIX=v \ No newline at end of file diff --git a/gradle/artifact-properties-patcher.gradle b/gradle/artifact-properties-patcher.gradle new file mode 100644 index 00000000000..02bd9335ae9 --- /dev/null +++ b/gradle/artifact-properties-patcher.gradle @@ -0,0 +1,65 @@ +import groovy.json.JsonSlurper +import groovy.json.JsonOutput + +ext.patchPomDependencies = { Node mainNode -> + def dependenciesNode = mainNode['dependencies'] + + def patched = false + + dependenciesNode.each { depNode -> + // Find all dependencies whose artifactId equals to the name of one of project's modules. + def navSdkDependencyNodes = depNode['dependency'].findAll { dependency -> + project.ext.navSdkArtifactSettings.containsKey(dependency['artifactId'].text()) + } + + navSdkDependencyNodes.each { navSdkNode -> + patched = true + + def artifactId = navSdkNode['artifactId'].text() + navSdkNode['artifactId'][0].setValue(project.ext.navSdkArtifactSettings[artifactId].getV1()) + + def versionNode = navSdkNode['version'] + if (versionNode) { + versionNode[0].setValue(project.ext.versionName) + } else { + navSdkNode.appendNode('version', project.ext.versionName) + } + } + } + + if (patched) { + println("pom file has successfully been patched") + } +} + +ext.patchArtifactMetadataFile = { + def taskConfig = project.gradle.startParameter.taskNames + .any { it.contains("Debug") } ? "debug" : "release" + + def moduleFile = layout.buildDirectory.file("publications/$taskConfig/module.json").get().asFile + if (!moduleFile.exists()) { + println("$moduleFile file doesn't exist") + return + } + + def jsonSlurper = new JsonSlurper() + def moduleData = jsonSlurper.parse(moduleFile) + + def patched = false + if (moduleData.variants) { + moduleData.variants.each { variant -> + variant.dependencies.each { dependency -> + if (project.ext.navSdkArtifactSettings.containsKey(dependency.module)) { + patched = true + dependency.module = project.ext.navSdkArtifactSettings[dependency.module].getV1() + dependency.version = [requires: project.ext.versionName] + } + } + } + } + + if (patched) { + moduleFile.text = JsonOutput.prettyPrint(JsonOutput.toJson(moduleData)) + println("module.json file has successfully been patched") + } +} \ No newline at end of file diff --git a/gradle/artifact-settings.gradle b/gradle/artifact-settings.gradle index 652ab2445ab..98859172bac 100644 --- a/gradle/artifact-settings.gradle +++ b/gradle/artifact-settings.gradle @@ -1,6 +1,5 @@ ext { mapboxArtifactGroupId = 'com.mapbox.navigationcore' - mapboxArtifactId = project.property('POM_ARTIFACT_ID') mapboxArtifactTitle = project.property('POM_ARTIFACT_TITLE') mapboxArtifactDescription = project.property('POM_DESCRIPTION') mapboxDeveloperName = 'Mapbox' @@ -13,6 +12,38 @@ ext { snapshot = project.hasProperty("snapshot") ? project.property("snapshot").toBoolean() : false releaseTagPrefix = project.hasProperty('RELEASE_TAG_PREFIX') ? project.property('RELEASE_TAG_PREFIX') : 'mapbox-navigation-android_dash-core_' versionName = getVersionName() + + /** + * Properties used for artifact publishing. + * + * The map consists of: + * - Key: The name of the Gradle module from which the artifact is built. + * - Value: A tuple with two elements: + * 1. The artifact ID. + * 2. The SDK name used in the SDK registry. + */ + navSdkArtifactSettings = [ + 'libnavigation-android' : new Tuple2('android', 'navigation-core-android'), + 'libnavigation-base' : new Tuple2('base', 'navigation-core-base'), + 'libnavigation-core' : new Tuple2('navigation', 'navigation-core-navigation'), + 'libnavigation-copilot' : new Tuple2('copilot', 'navigation-core-copilot'), + 'libnavigation-metrics' : new Tuple2('metrics', 'navigation-core-metrics'), + 'libnavigation-tripdata' : new Tuple2('tripdata', 'navigation-core-tripdata'), + 'libnavigation-util' : new Tuple2('utils', 'navigation-core-utils'), + 'libnavigation-voice' : new Tuple2('voice', 'navigation-core-voice'), + 'libnavigator' : new Tuple2('navigator', 'navigation-core-navigator'), + 'libnavui-base' : new Tuple2('ui-base', 'navigation-core-ui-base'), + 'libnavui-maps' : new Tuple2('ui-maps', 'navigation-core-ui-maps'), + 'libnavui-util' : new Tuple2('ui-utils', 'navigation-core-ui-utils'), + 'libtrip-notification' : new Tuple2('notification', 'navigation-core-notification'), + 'ui-components' : new Tuple2('ui-components', 'navigation-core-ui-components'), + 'androidauto' : new Tuple2('android-auto-components', 'navigation-core-androidauto'), + 'libtesting-router' : new Tuple2('test-router', 'navigation-core-testing-router'), + 'libnavigation-custom-route' : new Tuple2('customroute', 'navigation-core-custom-route'), + 'libnavigation-datainputs' : new Tuple2('datainputs', 'navigation-core-datainputs'), + 'libnavigation-ev' : new Tuple2('ev', 'navigation-core-ev'), + 'libnavigation-ev-ui' : new Tuple2('ev-ui', 'navigation-core-ev-ui') + ] } def getVersionName() { diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 2bca779776c..83961cadef6 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -13,16 +13,16 @@ ext { // version which we should use in this build def mapboxNavigatorVersion = System.getenv("FORCE_MAPBOX_NAVIGATION_NATIVE_VERSION") if (mapboxNavigatorVersion == null || mapboxNavigatorVersion == '') { - mapboxNavigatorVersion = '319.0.0' + mapboxNavigatorVersion = '321.0.0-beta.1' } println("Navigation Native version: " + mapboxNavigatorVersion) version = [ - mapboxMapSdk : '11.7.0-rc.1', - mapboxSdkServices : '7.2.0', + mapboxMapSdk : '11.8.0-beta.1', + mapboxSdkServices : '7.3.1', mapboxNavigator : "${mapboxNavigatorVersion}", - mapboxCommonNative : '24.7.0-rc.2', - mapboxSearch : '2.5.0-rc.2', + mapboxCommonNative : '24.8.0-beta.1', + mapboxSearch : '2.6.0-beta.1', mapboxBaseAndroid : '0.11.0', androidXLifecycle : '2.4.0', androidXCoreVersion : '1.6.0', @@ -48,7 +48,7 @@ ext { leakCanaryVersion : '2.9.1', commonsIO : '2.6', robolectric : '4.7.3', - mockwebserver : '4.9.0', + mockwebserver : '4.10.0', gmsLocation : '17.0.0', ktlint : '0.47.1', kotlinStdLib : kotlinVersion, @@ -56,8 +56,8 @@ ext { multidex : '2.0.1', json : '20180813', coroutinesAndroid : '1.6.4', - okhttp : '4.9.0', - okio : '2.10.0', + okhttp : '4.10.0', + okio : '3.4.0', androidxTestJunit : '1.1.5', androidxTest : '1.5.2', androidxTestCore : '1.5.0', @@ -71,6 +71,8 @@ ext { kaspresso : '1.5.3', equalsVerifier : '3.15.8', toStringVerifier : '1.4.8', + ktor : '2.3.0', + kotlinDateTime : '0.4.1', ] dependenciesList = [ // mapbox @@ -183,7 +185,17 @@ ext { viewBinding : "androidx.databinding:viewbinding:${version.viewBinding}", // Test app crashlytics - firebaseCrashlyticsNdk : "com.google.firebase:firebase-crashlytics-ndk:${version.firebaseCrashlytics}" + firebaseCrashlyticsNdk : "com.google.firebase:firebase-crashlytics-ndk:${version.firebaseCrashlytics}", + + // Ktor + ktorContentNegotiation : "io.ktor:ktor-client-content-negotiation:${version.ktor}", + ktorClientCore : "io.ktor:ktor-client-core:${version.ktor}", + ktorClientOkhttp : "io.ktor:ktor-client-okhttp:${version.ktor}", + ktorClientLogging : "io.ktor:ktor-client-logging:${version.ktor}", + ktorSerializationJson : "io.ktor:ktor-serialization-kotlinx-json:${version.ktor}", + ktorClientEncoding : "io.ktor:ktor-client-encoding:${version.ktor}", + ktorClientAuth : "io.ktor:ktor-client-auth:${version.ktor}", + kotlinXDateTime : "org.jetbrains.kotlinx:kotlinx-datetime:${version.kotlinDateTime}", ] pluginVersion = [ @@ -200,8 +212,8 @@ ext { kotlinHtmlJvm : '0.7.2', jacoco : '0.8.12', googleServices : '4.3.3', - mapboxSdkVersions : '1.1.3', - dokka : '1.6.21', + mapboxSdkVersions : '1.1.5', + dokka : '1.9.20', mapboxSdkRegistry : '0.7.0', mapboxAccessToken : '0.2.1', mapboxNativeDownload : '0.2.2', diff --git a/gradle/publish.gradle b/gradle/publish.gradle index 943722b35c9..2ef5d5fecc8 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -1,6 +1,7 @@ apply plugin: 'maven-publish' apply plugin: 'com.mapbox.sdkRegistry' apply from: file("$rootDir/gradle/artifact-settings.gradle") +apply from: file("$rootDir/gradle/artifact-properties-patcher.gradle") apply from: "$rootDir/gradle/kdoc-settings.gradle" afterEvaluate { @@ -9,7 +10,7 @@ afterEvaluate { release(MavenPublication) { from components.findByName('release') groupId project.ext.mapboxArtifactGroupId - artifactId project.ext.mapboxArtifactId + artifactId project.ext.navSdkArtifactSettings[project.name].getV1() version project.ext.versionName artifact(androidSourcesJar) @@ -34,12 +35,14 @@ afterEvaluate { scmNode.appendNode("connection", project.ext.mapboxArtifactScmUrl) scmNode.appendNode("developerConnection", project.ext.mapboxArtifactScmUrl) scmNode.appendNode("url", project.ext.mapboxArtifactUrl) + + project.ext.patchPomDependencies(mainNode) } } debug(MavenPublication) { from components.findByName('debug') groupId project.ext.mapboxArtifactGroupId - artifactId project.ext.mapboxArtifactId + artifactId project.ext.navSdkArtifactSettings[project.name].getV1() version project.ext.versionName artifact(androidSourcesJar) @@ -49,37 +52,16 @@ afterEvaluate { } } -def sdkNameMap = [:] -sdkNameMap["libnavigation-android"] = "navigation-core-android" -sdkNameMap["libnavigation-base"] = "navigation-core-base" -sdkNameMap["libnavigation-core"] = "navigation-core-navigation" -sdkNameMap["libnavigation-copilot"] = "navigation-core-copilot" -sdkNameMap["libnavigation-metrics"] = "navigation-core-metrics" -sdkNameMap["libnavigation-tripdata"] = "navigation-core-tripdata" -sdkNameMap["libnavigation-voice"] = "navigation-core-voice" -sdkNameMap["libnavigator"] = "navigation-core-navigator" -sdkNameMap["libtrip-notification"] = "navigation-core-notification" -sdkNameMap["libnavigation-util"] = "navigation-core-utils" -sdkNameMap["libnavui-base"] = "navigation-core-ui-base" -sdkNameMap["libnavui-maps"] = "navigation-core-ui-maps" -sdkNameMap["ui-components"] = "navigation-core-ui-components" -sdkNameMap["androidauto"] = "navigation-core-androidauto" -sdkNameMap["libnavui-util"] = "navigation-core-ui-utils" -sdkNameMap["libnavui-androidauto"] = "navigation-core-ui-androidauto" -sdkNameMap["libtesting-router"] = "navigation-core-testing-router" -sdkNameMap["libnavigation-custom-route"] = "navigation-core-custom-route" -sdkNameMap["libnavigation-datainputs"] = "navigation-core-datainputs" - registry { - sdkName = sdkNameMap[project.name] + sdkName = project.ext.navSdkArtifactSettings[project.name].getV2() production = true snapshot = project.ext.snapshot - override = snapshot +// TODO revert override = snapshot + override = true dryRun = false publish = true publishMessage = "cc @mapbox/navigation-android" publications = ["release"] - featureFlag = project.hasProperty('ARTIFACT_FEATURE_FLAG') ? project.property('ARTIFACT_FEATURE_FLAG') : '' } task androidSourcesJar(type: Jar) { @@ -91,3 +73,19 @@ task androidKdocJar(type: Jar) { archiveClassifier.set('javadoc') from kdocPath } + +tasks.register("patchArtifactMetadataFileTask") { + doLast { + project.ext.patchArtifactMetadataFile() + } +} + +afterEvaluate { + tasks.matching { it.name.startsWith('generateMetadataFileFor') }.configureEach { task -> + task.finalizedBy 'patchArtifactMetadataFileTask' + } + + tasks.matching { it.name ==~ /publish\w+PublicationToMavenLocal/ }.configureEach { task -> + task.dependsOn 'patchArtifactMetadataFileTask' + } +} diff --git a/gradle/save-sdk-version.gradle b/gradle/save-sdk-version.gradle deleted file mode 100644 index ee6b9c1b41b..00000000000 --- a/gradle/save-sdk-version.gradle +++ /dev/null @@ -1,9 +0,0 @@ -tasks.configureEach { - if (name == "packageDebugAssets" || name == "packageReleaseAssets" - || name == "mergeDebugAssets" || name == "mergeReleaseAssets") { - def task = tasks.findByName("saveSDKVersion") - if (task != null) { - dependsOn task - } - } -} diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/CoreRerouteTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/CoreRerouteTest.kt index c92b874fc06..bc3761df3af 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/CoreRerouteTest.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/CoreRerouteTest.kt @@ -22,6 +22,8 @@ import com.mapbox.navigation.base.trip.model.RouteProgressState import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.MapboxNavigationProvider import com.mapbox.navigation.core.directions.session.RoutesExtra +import com.mapbox.navigation.core.directions.session.RoutesExtra.ROUTES_UPDATE_REASON_ALTERNATIVE +import com.mapbox.navigation.core.directions.session.RoutesExtra.ROUTES_UPDATE_REASON_REROUTE import com.mapbox.navigation.core.internal.extensions.flowLocationMatcherResult import com.mapbox.navigation.core.reroute.RerouteOptionsAdapter import com.mapbox.navigation.core.reroute.RerouteState @@ -926,6 +928,58 @@ class CoreRerouteTest : BaseCoreNoCleanUpTest() { } } + @Test + fun reroute_from_primary_route_to_ignored_alternative() = sdkTest { + withMapboxNavigation( + historyRecorderRule = mapboxHistoryTestRule, + ) { mapboxNavigation -> + val mockRoute = RoutesProvider.dc_short_alternative_with_fork_point(context) + mockWebServerRule.requestHandlers.addAll(mockRoute.mockRequestHandlers) + + val routes = mapboxNavigation.requestRoutes( + RouteOptions.builder() + .applyDefaultNavigationOptions() + .applyLanguageAndVoiceUnitOptions(context) + .baseUrl(mockWebServerRule.baseUrl) + .waypointsPerRoute(true) + .coordinatesList(mockRoute.routeWaypoints).build(), + ).getSuccessfulResultOrThrowException().routes + + val mainRoute = routes[0] + val alternativeRoute = routes[1] + + mockLocationReplayerRule.playRoute( + directionsRoute = mainRoute.directionsRoute, + ) + + mapboxNavigation.startTripSession() + mapboxNavigation.setNavigationRoutes(routes) + + // alternative fork point passed reason should occur on this route + mapboxNavigation.routesUpdates().filter { + it.reason == ROUTES_UPDATE_REASON_ALTERNATIVE && it.ignoredRoutes.any { + it.navigationRoute == alternativeRoute && + it.reason == "Alternative fork point passed" + } + }.first() + + mockLocationReplayerRule.playRoute( + directionsRoute = alternativeRoute.directionsRoute, + eventsToDrop = 30, // approx value to start alternative rotue after "fork point" + ) + + // reroute to hidden alternative should occur + val rerouteToHiddenAlternativeUpdate = mapboxNavigation.routesUpdates().filter { + it.reason == ROUTES_UPDATE_REASON_REROUTE + }.first() + + assertEquals( + alternativeRoute, + rerouteToHiddenAlternativeUpdate.navigationRoutes[0], + ) + } + } + private fun createMapboxNavigation(customRefreshInterval: Long? = null): MapboxNavigation { var mapboxNavigation: MapboxNavigation? = null diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshTest.kt index d38fad53a49..a21e46ffe69 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshTest.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshTest.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalMapboxNavigationAPI::class) + package com.mapbox.navigation.instrumentation_tests.core import android.content.Context @@ -9,6 +11,7 @@ import com.mapbox.api.directions.v5.models.DirectionsResponse import com.mapbox.api.directions.v5.models.Incident import com.mapbox.api.directions.v5.models.RouteOptions import com.mapbox.geojson.Point +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions import com.mapbox.navigation.base.options.NavigationOptions @@ -287,6 +290,10 @@ class RouteRefreshTest : BaseTest(EmptyTestActivity::class.ja requestedRoutes[1].waypoints, refreshedRoutes[1].waypoints, ) + assertEquals( + listOf(true, true), + refreshedRoutes.map { it.routeRefreshMetadata?.isUpToDate }, + ) } @Test @@ -323,6 +330,10 @@ class RouteRefreshTest : BaseTest(EmptyTestActivity::class.ja .filter { it.reason == ROUTES_UPDATE_REASON_REFRESH } .map { it.navigationRoutes } .first() + assertEquals( + listOf(false, false), + refreshedRoutes.map { it.routeRefreshMetadata?.isUpToDate }, + ) val refreshedRouteCongestions = refreshedRoutes .first() .directionsRoute @@ -346,6 +357,10 @@ class RouteRefreshTest : BaseTest(EmptyTestActivity::class.ja ), observer.getStatesSnapshot(), ) + assertEquals( + listOf(true, true), + mapboxNavigation.getNavigationRoutes().map { it.routeRefreshMetadata?.isUpToDate }, + ) } @Test diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/WaypointsTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/WaypointsTest.kt index 599f8e98fb5..699d3c73f70 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/WaypointsTest.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/WaypointsTest.kt @@ -22,7 +22,6 @@ import com.mapbox.navigation.instrumentation_tests.activity.EmptyTestActivity import com.mapbox.navigation.testing.ui.BaseTest import com.mapbox.navigation.testing.ui.utils.MapboxNavigationRule import com.mapbox.navigation.testing.ui.utils.coroutines.getSuccessfulResultOrThrowException -import com.mapbox.navigation.testing.ui.utils.coroutines.navigateNextRouteLeg import com.mapbox.navigation.testing.ui.utils.coroutines.requestRoutes import com.mapbox.navigation.testing.ui.utils.coroutines.sdkTest import com.mapbox.navigation.testing.ui.utils.coroutines.setNavigationRoutesAndWaitForUpdate @@ -162,7 +161,7 @@ class WaypointsTest : BaseTest(EmptyTestActivity::class.java) Point.fromLngLat(140.025878, 35.660315), Point.fromLngLat(140.02985194436837, 35.6621859075361), Point.fromLngLat(140.0277017481984, 35.65792632910045), - Point.fromLngLat(140.03887765416835, 35.66023142441715), + Point.fromLngLat(140.038772, 35.660329), Point.fromLngLat(140.0231453915486, 35.667495318461164), Point.fromLngLat(140.03969561587877, 35.67009382118668), ) @@ -205,7 +204,6 @@ class WaypointsTest : BaseTest(EmptyTestActivity::class.java) checkLocation(coordinates[1], legWaypoint.location) assertEquals(LegWaypoint.REGULAR, legWaypoint.type) - mapboxNavigation.navigateNextRouteLeg() stayOnPosition(coordinates[2], 270f) delay(1000) assertEquals(1, nextWaypoints.size) @@ -216,7 +214,6 @@ class WaypointsTest : BaseTest(EmptyTestActivity::class.java) checkLocation(coordinates[3], legWaypoint.location) assertEquals(LegWaypoint.REGULAR, legWaypoint.type) - mapboxNavigation.navigateNextRouteLeg() stayOnPosition(coordinates[4], 45f) delay(1000) assertEquals(2, nextWaypoints.size) @@ -263,7 +260,6 @@ class WaypointsTest : BaseTest(EmptyTestActivity::class.java) ) assertEquals(LegWaypoint.EV_CHARGING_ADDED, legWaypoint.type) - mapboxNavigation.navigateNextRouteLeg() stayOnPosition(routes[0].waypoints!![2].location(), 0f) nextWaypoints.waitUntilHasSize(2) legWaypoint = nextWaypoints[1]!! 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 41bae07e72d..59ba8f255db 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 @@ -39,11 +39,16 @@ import com.mapbox.navigation.ui.maps.route.line.MapboxRouteLineApiExtensions.cle import com.mapbox.navigation.ui.maps.route.line.MapboxRouteLineApiExtensions.setNavigationRoutes import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView +import com.mapbox.navigation.ui.maps.route.line.api.RoutesRenderedCallback +import com.mapbox.navigation.ui.maps.route.line.api.RoutesRenderedResult import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue @@ -664,6 +669,62 @@ class RouteLineLayersTest : BaseTest( countDownLatch.await() } + /** + * There was a race with MapboxRouteLineView: + * when you invoke showRoutes and renderRouteDrawData approximately at the same time, + * the wrong visibility might appear as a result: + * + * 1. renderRouteDrawData gets the current visibility, which is none; + * 2. showPrimaryRoute changes the visibility to visible; + * 3. renderRouteDrawData sets the visibility back to none, since it has outdated information. + * + * This test checks the race is not present anymore. + */ + @Test + fun hideRoutesRenderRoutesShowRoutes() { + val map = activity.mapboxMap + val routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build()) + val routeLineView = MapboxRouteLineView( + MapboxRouteLineViewOptions.Builder(activity).build(), + ) + val latch = CountDownLatch(2) + + lateinit var style: Style + sdkTest { + style = waitForStyleLoad(map) + + val setRoutesResult1 = routeLineApi.setNavigationRoutes(listOf(route1, route2, route3)) + routeLineView.renderRouteDrawDataAsync(map, style, setRoutesResult1) + + routeLineView.hidePrimaryRoute(style) + + routeLineView.renderClearRouteLineValueAsync(map, style, routeLineApi.clearRouteLine()) + + val setRoutesResult = routeLineApi.setNavigationRoutes(listOf(route1, route2, route3)) + + val callback = object : RoutesRenderedCallback { + override fun onRoutesRendered(result: RoutesRenderedResult) { + latch.countDown() + } + } + + routeLineView.renderRouteDrawData(style, setRoutesResult, map, callback) + delay(20) + routeLineView.showPrimaryRoute(style) + latch.countDown() + } + + latch.await() + runBlocking(Dispatchers.Main) { + val primaryRouteVisibility = style.getLayer("mapbox-layerGroup-1-main") + ?.visibility + val maskingRouteVisibility = style.getLayer("mapbox-masking-layer-main") + ?.visibility + assertEquals(Visibility.VISIBLE, primaryRouteVisibility) + assertEquals(Visibility.VISIBLE, maskingRouteVisibility) + } + } + @Test fun routes_rendered_callback() = sdkTest { val routes = listOf(route1, route2, route3) diff --git a/libnavigation-android/build.gradle b/libnavigation-android/build.gradle index 9a18fc8d51e..9598a05aad3 100644 --- a/libnavigation-android/build.gradle +++ b/libnavigation-android/build.gradle @@ -36,3 +36,4 @@ dependencies { apply from: "../gradle/track-public-apis.gradle" apply from: "../gradle/jacoco.gradle" apply from: "../gradle/publish.gradle" +apply from: "../../../../scripts_shared/find-all-common-sdk-versions.gradle" diff --git a/libnavigation-android/gradle.properties b/libnavigation-android/gradle.properties index 07d03eff677..1afced59197 100644 --- a/libnavigation-android/gradle.properties +++ b/libnavigation-android/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=android POM_ARTIFACT_TITLE=Mapbox Navigation SDK POM_DESCRIPTION=Artifact that groups all of the Navigation SDK features \ No newline at end of file diff --git a/libnavigation-base/api/current.txt b/libnavigation-base/api/current.txt index d8f9279a784..71be8aab9b1 100644 --- a/libnavigation-base/api/current.txt +++ b/libnavigation-base/api/current.txt @@ -888,6 +888,7 @@ package com.mapbox.navigation.base.route { method public String getResponseOriginAPI(); method public String getResponseUUID(); method public int getRouteIndex(); + method public com.mapbox.navigation.base.route.RouteRefreshMetadata? getRouteRefreshMetadata(); method public java.util.List getUpcomingRoadObjects(); method public java.util.List? getWaypoints(); property public final com.mapbox.api.directions.v5.models.DirectionsRoute directionsRoute; @@ -897,6 +898,7 @@ package com.mapbox.navigation.base.route { property public final String responseOriginAPI; property public final String responseUUID; property public final int routeIndex; + property public final com.mapbox.navigation.base.route.RouteRefreshMetadata? routeRefreshMetadata; property public final java.util.List upcomingRoadObjects; property public final java.util.List? waypoints; field public static final com.mapbox.navigation.base.route.NavigationRoute.Companion Companion; @@ -944,6 +946,11 @@ package com.mapbox.navigation.base.route { method public static java.util.List exclusionViolations(com.mapbox.navigation.base.route.NavigationRoute); } + @com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI public final class RouteRefreshMetadata { + method public boolean isUpToDate(); + property public final boolean isUpToDate; + } + public final class RouteRefreshOptions { method public long getIntervalMillis(); method public com.mapbox.navigation.base.route.RouteRefreshOptions.Builder toBuilder(); diff --git a/libnavigation-base/gradle.properties b/libnavigation-base/gradle.properties index 59954d56298..f991c4404b6 100644 --- a/libnavigation-base/gradle.properties +++ b/libnavigation-base/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=base POM_ARTIFACT_TITLE=Mapbox Navigation Base POM_DESCRIPTION=Artifact that provides the set of interface definitions and DTOs used in the modular Navigation SDK \ No newline at end of file diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/factory/RouteIndicesFactory.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/factory/RouteIndicesFactory.kt index df52dfcfa5e..2c92bc58665 100644 --- a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/factory/RouteIndicesFactory.kt +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/factory/RouteIndicesFactory.kt @@ -12,11 +12,13 @@ object RouteIndicesFactory { routeGeometryIndex: Int, legGeometryIndex: Int, intersectionIndex: Int, + isForkPointPassed: Boolean, ): RouteIndices = RouteIndices( legIndex, stepIndex, routeGeometryIndex, legGeometryIndex, intersectionIndex, + isForkPointPassed, ) } diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/NavigationRouteEx.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/NavigationRouteEx.kt index 166840b01ab..8aed8b162ff 100644 --- a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/NavigationRouteEx.kt +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/NavigationRouteEx.kt @@ -1,4 +1,5 @@ @file:JvmName("NavigationRouteEx") +@file:OptIn(ExperimentalMapboxNavigationAPI::class) package com.mapbox.navigation.base.internal.route @@ -13,9 +14,11 @@ import com.mapbox.api.directions.v5.models.Incident import com.mapbox.api.directions.v5.models.LegAnnotation import com.mapbox.api.directions.v5.models.LegStep import com.mapbox.api.directions.v5.models.RouteLeg +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI import com.mapbox.navigation.base.internal.CongestionNumericOverride import com.mapbox.navigation.base.internal.utils.Constants import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.base.route.RouteRefreshMetadata import com.mapbox.navigation.base.route.toNavigationRoute import com.mapbox.navigation.base.utils.DecodeUtils.stepGeometryToPoints import com.mapbox.navigation.utils.internal.Time @@ -143,6 +146,7 @@ internal fun NavigationRoute.refreshRoute( directionsRouteBlock, waypointsBlock, newExpirationTimeElapsedSeconds ?: expirationTimeElapsedSeconds, + routeRefreshMetadata = RouteRefreshMetadata(isUpToDate = true), ) } @@ -182,6 +186,7 @@ fun NavigationRoute.update( waypointsBlock: List?.() -> List?, newExpirationTimeElapsedSeconds: Long? = this.expirationTimeElapsedSeconds, overriddenTraffic: CongestionNumericOverride? = this.overriddenTraffic, + routeRefreshMetadata: RouteRefreshMetadata? = this.routeRefreshMetadata, ): NavigationRoute { val refreshedRoute = directionsRoute.directionsRouteBlock() return copy( @@ -189,6 +194,7 @@ fun NavigationRoute.update( waypoints = waypoints.waypointsBlock(), expirationTimeElapsedSeconds = newExpirationTimeElapsedSeconds, overriddenTraffic = overriddenTraffic, + routeRefreshMetadata = routeRefreshMetadata, ) } diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/RouteRefreshMetadataFactory.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/RouteRefreshMetadataFactory.kt new file mode 100644 index 00000000000..4584fc1ef08 --- /dev/null +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/RouteRefreshMetadataFactory.kt @@ -0,0 +1,11 @@ +package com.mapbox.navigation.base.internal.route + +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI +import com.mapbox.navigation.base.route.RouteRefreshMetadata + +@ExperimentalMapboxNavigationAPI +fun createRouteRefreshMetadata( + isUpToDate: Boolean, +) = RouteRefreshMetadata( + isUpToDate = isUpToDate, +) diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/trip/model/RouteIndices.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/trip/model/RouteIndices.kt index 1a26018b22e..211a81f13d5 100644 --- a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/trip/model/RouteIndices.kt +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/trip/model/RouteIndices.kt @@ -8,6 +8,7 @@ package com.mapbox.navigation.base.internal.trip.model * @param routeGeometryIndex current index in the route geometry. Analogous to [RouteProgress.currentRouteGeometryIndex] for primary route. * @param legGeometryIndex current index in the leg geometry. Analogous to [RouteLegProgress.geometryIndex] for primary route. * @param intersectionIndex current step-wise intersection index. Analogous to [RouteStepProgress.intersectionIndex] for primary route. + * @param isForkPointPassed indicates whether the fork point was passed. */ class RouteIndices internal constructor( val legIndex: Int, @@ -15,6 +16,7 @@ class RouteIndices internal constructor( val routeGeometryIndex: Int, val legGeometryIndex: Int, val intersectionIndex: Int, + val isForkPointPassed: Boolean, ) { /** @@ -31,6 +33,7 @@ class RouteIndices internal constructor( if (routeGeometryIndex != other.routeGeometryIndex) return false if (legGeometryIndex != other.legGeometryIndex) return false if (intersectionIndex != other.intersectionIndex) return false + if (isForkPointPassed != other.isForkPointPassed) return false return true } @@ -44,6 +47,7 @@ class RouteIndices internal constructor( result = 31 * result + routeGeometryIndex result = 31 * result + legGeometryIndex result = 31 * result + intersectionIndex + result = 31 * result + isForkPointPassed.hashCode() return result } @@ -56,7 +60,8 @@ class RouteIndices internal constructor( "stepIndex=$stepIndex, " + "routeGeometryIndex=$routeGeometryIndex, " + "legGeometryIndex=$legGeometryIndex, " + - "intersectionIndex=$intersectionIndex" + + "intersectionIndex=$intersectionIndex, " + + "isForkPointPassed=$isForkPointPassed" + ")" } } diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/utils/DirectionsResponseUtils.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/utils/DirectionsResponseUtils.kt index 53a8e9b4c9e..dc0533e7d9a 100644 --- a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/utils/DirectionsResponseUtils.kt +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/utils/DirectionsResponseUtils.kt @@ -1,5 +1,6 @@ package com.mapbox.navigation.base.internal.utils +import com.mapbox.api.directions.v5.models.DirectionsResponse import com.mapbox.bindgen.DataRef import com.mapbox.bindgen.Expected import com.mapbox.bindgen.ExpectedFactory @@ -7,7 +8,6 @@ import com.mapbox.navigation.base.internal.route.RoutesResponse import com.mapbox.navigation.base.internal.route.toNavigationRoute import com.mapbox.navigation.base.route.NavigationRoute import com.mapbox.navigation.base.route.RouterOrigin -import com.mapbox.navigation.base.route.toDirectionsResponse import com.mapbox.navigation.utils.internal.logE import com.mapbox.navigator.RouteInterface import kotlinx.coroutines.CoroutineDispatcher @@ -44,13 +44,18 @@ suspend fun parseDirectionsResponse( fun parseRouteInterfaces( routes: List, responseTimeElapsedSeconds: Long, + routeLookup: (routeId: String) -> NavigationRoute?, + routeToDirections: (route: RouteInterface) -> DirectionsResponse, ): Expected> { return try { routes.groupBy { it.responseUuid } .map { (_, routes) -> - val directionsResponse = routes.first().responseJsonRef.toDirectionsResponse() + val directionsResponse by lazy { + routeToDirections(routes.first()) + } + routes.map { - it.toNavigationRoute( + routeLookup(it.routeId) ?: it.toNavigationRoute( responseTimeElapsedSeconds, directionsResponse, ) diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/utils/RoutesParsingManager.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/utils/RoutesParsingManager.kt index 3c01f64d9ff..6487dd1fdf4 100644 --- a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/utils/RoutesParsingManager.kt +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/utils/RoutesParsingManager.kt @@ -2,8 +2,11 @@ package com.mapbox.navigation.base.internal.utils +import com.mapbox.api.directions.v5.models.DirectionsResponse import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import com.mapbox.navigation.base.route.toDirectionsResponse import com.mapbox.navigation.utils.internal.logD +import com.mapbox.navigator.RouteInterface import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.nio.ByteBuffer @@ -30,6 +33,11 @@ interface RouteParsingManager { arguments: AlternativesInfo, parsing: suspend () -> T, ): AlternativesParsingResult + + fun parseRouteToDirections(route: RouteInterface): DirectionsResponse { + logD(LOG_TAG) { "Parsing directions response for routeId = ${route.routeId}" } + return route.responseJsonRef.toDirectionsResponse() + } } fun createRouteParsingManager(): RouteParsingManager { diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/NavigationRoute.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/NavigationRoute.kt index b7d374b2ef2..5b17072c7b0 100644 --- a/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/NavigationRoute.kt +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/NavigationRoute.kt @@ -57,7 +57,8 @@ import kotlin.time.Duration.Companion.milliseconds * Prefer using [waypoints] instead of [DirectionsRoute.waypoints] from [directionsRoute]. * @param responseOriginAPI describes API which generated data for [NavigationRoute]. * @param overriddenTraffic describes a segment of [NavigationRoute] with overridden - * congestion_numeric. + * @param routeRefreshMetadata contains data that describes refreshing of this route object. + * The field is null until the first refresh. */ class NavigationRoute private constructor( val directionsRoute: DirectionsRoute, @@ -70,6 +71,8 @@ class NavigationRoute private constructor( @ExperimentalMapboxNavigationAPI @ResponseOriginAPI val responseOriginAPI: String, + @ExperimentalMapboxNavigationAPI + val routeRefreshMetadata: RouteRefreshMetadata?, ) { internal constructor( @@ -92,6 +95,7 @@ class NavigationRoute private constructor( expirationTimeElapsedSeconds, overriddenTraffic, responseOriginAPI, + null, ) /** @@ -152,6 +156,7 @@ class NavigationRoute private constructor( responseOriginAPI, responseUUID, expirationTimeElapsedSeconds, + routeRefreshMetadata, ) return gson.toJson(state) } @@ -187,6 +192,7 @@ class NavigationRoute private constructor( state.expirationTimeElapsedSeconds, responseOriginAPI = state.responseOriginAPI, overriddenTraffic = null, + routeRefreshMetadata = state.routeRefreshMetadata, ) ExpectedFactory.createValue(route) } catch (t: Throwable) { @@ -625,6 +631,7 @@ class NavigationRoute private constructor( nativeRoute: RouteInterface = this.nativeRoute, overriddenTraffic: CongestionNumericOverride? = this.overriddenTraffic, expirationTimeElapsedSeconds: Long? = this.expirationTimeElapsedSeconds, + routeRefreshMetadata: RouteRefreshMetadata? = this.routeRefreshMetadata, ): NavigationRoute = NavigationRoute( directionsRoute, waypoints, @@ -634,6 +641,7 @@ class NavigationRoute private constructor( expirationTimeElapsedSeconds, overriddenTraffic, this.responseOriginAPI, + routeRefreshMetadata, ) @Keep @@ -648,6 +656,7 @@ class NavigationRoute private constructor( val responseOriginAPI: String, val responseUUID: String, val expirationTimeElapsedSeconds: Long?, + val routeRefreshMetadata: RouteRefreshMetadata?, ) } diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/RouteRefreshMetadata.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/RouteRefreshMetadata.kt new file mode 100644 index 00000000000..025f1e1f37e --- /dev/null +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/RouteRefreshMetadata.kt @@ -0,0 +1,32 @@ +package com.mapbox.navigation.base.route + +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI + +/** + * Contains data that describes how route was refreshed. + * @param isUpToDate indicates if route was successfully refreshed recently. + */ +@ExperimentalMapboxNavigationAPI +class RouteRefreshMetadata internal constructor( + val isUpToDate: Boolean, +) { + + /** + * Returns a hash code value for the object. + */ + override fun hashCode(): Int { + return isUpToDate.hashCode() + } + + /** + * Returns a string representation of the object. + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RouteRefreshMetadata + + return isUpToDate == other.isUpToDate + } +} diff --git a/libnavigation-base/src/test/java/com/mapbox/navigation/base/internal/route/RouteProgressExTest.kt b/libnavigation-base/src/test/java/com/mapbox/navigation/base/internal/route/RouteProgressExTest.kt index 204030678af..c77899ff439 100644 --- a/libnavigation-base/src/test/java/com/mapbox/navigation/base/internal/route/RouteProgressExTest.kt +++ b/libnavigation-base/src/test/java/com/mapbox/navigation/base/internal/route/RouteProgressExTest.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalMapboxNavigationAPI::class) + package com.mapbox.navigation.base.internal.route import com.google.gson.JsonPrimitive @@ -12,6 +14,7 @@ import com.mapbox.api.directions.v5.models.RouteLeg import com.mapbox.api.directions.v5.models.RouteOptions import com.mapbox.geojson.Point import com.mapbox.geojson.utils.PolylineUtils +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI import com.mapbox.navigation.base.route.NavigationRoute import com.mapbox.navigation.base.route.RouterOrigin import com.mapbox.navigation.testing.FileUtils diff --git a/libnavigation-base/src/test/java/com/mapbox/navigation/base/route/NavigationRouteTest.kt b/libnavigation-base/src/test/java/com/mapbox/navigation/base/route/NavigationRouteTest.kt index c2f28a9c179..fe3a3b2d280 100644 --- a/libnavigation-base/src/test/java/com/mapbox/navigation/base/route/NavigationRouteTest.kt +++ b/libnavigation-base/src/test/java/com/mapbox/navigation/base/route/NavigationRouteTest.kt @@ -202,4 +202,22 @@ class NavigationRouteTest { assertNull(value) } + + @Test + fun `route refresh metadata is null after creation`() { + val requestUrl = FileUtils.loadJsonFixture("test_directions_request_url.txt") + val responseJson = FileUtils.loadJsonFixture("test_directions_response.json") + + val navigationRoute = NavigationRoute.create( + directionsResponseJson = responseJson, + routeRequestUrl = requestUrl, + routerOrigin = RouterOrigin.ONLINE, + ) + + assertTrue( + navigationRoute.all { + it.routeRefreshMetadata == null + }, + ) + } } diff --git a/libnavigation-copilot/gradle.properties b/libnavigation-copilot/gradle.properties index 95516cddbdf..c934ca80f7f 100644 --- a/libnavigation-copilot/gradle.properties +++ b/libnavigation-copilot/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=copilot POM_ARTIFACT_TITLE=Mapbox Navigation Copilot POM_DESCRIPTION=Artifact that provides Copilot capabilities \ No newline at end of file diff --git a/libnavigation-copilot/src/main/java/com/mapbox/navigation/copilot/MapboxCopilotImpl.kt b/libnavigation-copilot/src/main/java/com/mapbox/navigation/copilot/MapboxCopilotImpl.kt index e9fc68f6b5b..10611a79331 100644 --- a/libnavigation-copilot/src/main/java/com/mapbox/navigation/copilot/MapboxCopilotImpl.kt +++ b/libnavigation-copilot/src/main/java/com/mapbox/navigation/copilot/MapboxCopilotImpl.kt @@ -38,6 +38,7 @@ import com.mapbox.navigation.core.internal.telemetry.registerUserFeedbackObserve import com.mapbox.navigation.core.internal.telemetry.unregisterUserFeedbackObserver import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp import com.mapbox.navigation.utils.internal.InternalJobControlFactory +import com.mapbox.navigation.utils.internal.logD import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -76,13 +77,8 @@ internal class MapboxCopilotImpl( private val filepaths = HistoryFiles(applicationContext) private val appLifecycleObserver = object : DefaultLifecycleObserver { - override fun onResume(owner: LifecycleOwner) { - push(GoingToForegroundEvent) - } - - override fun onPause(owner: LifecycleOwner) { - push(GoingToBackgroundEvent) - } + override fun onResume(owner: LifecycleOwner) = push(GoingToForegroundEvent) + override fun onPause(owner: LifecycleOwner) = push(GoingToBackgroundEvent) } private val deviceType = mapboxNavigation.navigationOptions.deviceProfile.deviceType private val copilotOptions @@ -107,15 +103,8 @@ internal class MapboxCopilotImpl( private var arrivedAtFinalDestination = false private val arrivalObserver = object : ArrivalObserver { - - override fun onWaypointArrival(routeProgress: RouteProgress) { - // Nothing to do - } - - override fun onNextRouteLegStart(routeLegProgress: RouteLegProgress) { - // Nothing to do - } - + override fun onWaypointArrival(routeProgress: RouteProgress) = Unit + override fun onNextRouteLegStart(routeLegProgress: RouteLegProgress) = Unit override fun onFinalDestinationArrival(routeProgress: RouteProgress) { arrivedAtFinalDestination = true } @@ -218,12 +207,10 @@ internal class MapboxCopilotImpl( } } - private fun pushHistoryJson(eventType: String, eventJson: String) = - mainJobController.scope.launch { - // IMPORTANT! pushHistory calls must be executed from the thread owning - // the native HistoryRecorderHandleInterface instance - copilotHistoryRecorder.pushHistory(eventType, eventJson) - } + private fun pushHistoryJson(eventType: String, eventJson: String) { + logD("pushHistory event=$eventType") + copilotHistoryRecorder.pushHistory(eventType, eventJson) + } private fun pushFeedbackEvent(userFeedback: ExtendedUserFeedback) { val lat = userFeedback.location.latitude() @@ -258,6 +245,7 @@ internal class MapboxCopilotImpl( driveMode = driveMode, recording = recording, ) + logD("startRecording $activeSession") saveCopilotSession() mapboxNavigation.registerArrivalObserver(arrivalObserver) @@ -280,6 +268,7 @@ internal class MapboxCopilotImpl( private fun restartRecordingHistory() { val session = activeSession.copy(endedAt = currentUtcTime()) + logD("stopRecording $session") copilotHistoryRecorder.stopRecording { historyFilePath -> historyFilePath ?: return@stopRecording finishedSessions.add(session) @@ -295,6 +284,7 @@ internal class MapboxCopilotImpl( recording = recording, startedAt = currentUtcTime(), ) + logD("startRecording $activeSession") } private fun uploadRecording() { @@ -308,6 +298,7 @@ internal class MapboxCopilotImpl( pushDriveEndsEvent() val historyFilesCopy = finishedSessions.toMutableList() + logD("stopRecording $session") stopRecording { historyFilesCopy.add(session) limitTotalHistoryFilesSize(historyFilesCopy) @@ -438,5 +429,7 @@ internal class MapboxCopilotImpl( internal const val MEDIA_TYPE_ZIP = "application/zip" internal const val LOG_CATEGORY = "MapboxCopilot" internal const val PROD_BASE_URL = "https://events.mapbox.com" + + internal fun logD(msg: String) = logD(msg, LOG_CATEGORY) } } diff --git a/libnavigation-core/gradle.properties b/libnavigation-core/gradle.properties index c4f69febbdb..7421add2c6d 100644 --- a/libnavigation-core/gradle.properties +++ b/libnavigation-core/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=navigation POM_ARTIFACT_TITLE=Mapbox Navigation SDK POM_DESCRIPTION=Main artifact that provides all default modules and entry points of the Mapbox Navigation SDK \ No newline at end of file diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt index cc0d8df6fe7..fc6ddafcc71 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt @@ -49,6 +49,7 @@ import com.mapbox.navigation.core.directions.session.RoutesSetStartedParams import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult import com.mapbox.navigation.core.directions.session.SetNavigationRoutesStartedObserver import com.mapbox.navigation.core.directions.session.Utils +import com.mapbox.navigation.core.directions.session.routesPlusIgnored import com.mapbox.navigation.core.history.MapboxHistoryReader import com.mapbox.navigation.core.history.MapboxHistoryRecorder import com.mapbox.navigation.core.internal.MapboxNavigationSDKInitializerImpl @@ -589,6 +590,7 @@ class MapboxNavigation @VisibleForTesting internal constructor( tripSession, threadController, routeParsingManager, + directionsSession, ) routeAlternativesController.setRouteUpdateSuggestionListener(::updateRoutes) @OptIn(ExperimentalPreviewMapboxNavigationAPI::class) @@ -641,6 +643,13 @@ class MapboxNavigation @VisibleForTesting internal constructor( navigationOptions.applicationContext, navigator, ) + + registerRouteProgressObserver( + NavigationComponentProvider.createForkPointPassedObserver( + directionsSession, + ::currentLegIndex, + ), + ) } @OptIn(ExperimentalMapboxNavigationAPI::class) @@ -2117,7 +2126,7 @@ class MapboxNavigation @VisibleForTesting internal constructor( private suspend fun prepareNavigationForRoutesParsing() { withContext(Dispatchers.Main.immediate) { - if (directionsSession.routes.size > 1) { + if (directionsSession.routesPlusIgnored.size > 1) { suspendCoroutine { continuation -> setNavigationRoutes(directionsSession.routes.take(1), currentLegIndex()) { continuation.resume(Unit) diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/NavigationComponentProvider.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/NavigationComponentProvider.kt index 89eaf41af52..e2c5b7c0113 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/NavigationComponentProvider.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/NavigationComponentProvider.kt @@ -9,6 +9,7 @@ import com.mapbox.navigation.base.options.RerouteOptions import com.mapbox.navigation.base.trip.notification.TripNotification import com.mapbox.navigation.core.accounts.BillingController import com.mapbox.navigation.core.arrival.ArrivalProgressObserver +import com.mapbox.navigation.core.directions.ForkPointPassedObserver import com.mapbox.navigation.core.directions.session.DirectionsSession import com.mapbox.navigation.core.directions.session.MapboxDirectionsSession import com.mapbox.navigation.core.ev.EVDynamicDataHolder @@ -24,6 +25,7 @@ import com.mapbox.navigation.core.trip.service.MapboxTripService import com.mapbox.navigation.core.trip.service.TripService import com.mapbox.navigation.core.trip.session.MapboxTripSession import com.mapbox.navigation.core.trip.session.NavigationSession +import com.mapbox.navigation.core.trip.session.RouteProgressObserver import com.mapbox.navigation.core.trip.session.TripSession import com.mapbox.navigation.core.trip.session.TripSessionLocationEngine import com.mapbox.navigation.core.trip.session.eh.EHorizonSubscriptionManagerImpl @@ -153,4 +155,12 @@ internal object NavigationComponentProvider { threadController, evDynamicDataHolder, ) + + fun createForkPointPassedObserver( + directionsSession: DirectionsSession, + currentLegIndex: () -> Int, + ): RouteProgressObserver = ForkPointPassedObserver( + directionsSession, + currentLegIndex, + ) } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/arrival/AutoArrivalController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/arrival/AutoArrivalController.kt index 6acb092c05f..4b45623ff5f 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/arrival/AutoArrivalController.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/arrival/AutoArrivalController.kt @@ -1,9 +1,6 @@ package com.mapbox.navigation.core.arrival -import android.os.SystemClock -import com.mapbox.api.directions.v5.models.RouteLeg import com.mapbox.navigation.base.trip.model.RouteLegProgress -import java.util.concurrent.TimeUnit /** * The default controller for arrival. This will move onto the next leg automatically @@ -11,28 +8,10 @@ import java.util.concurrent.TimeUnit */ open class AutoArrivalController : ArrivalController { - private var routeLegCompletedTime: Long? = null - private var currentRouteLeg: RouteLeg? = null - /** - * Moves onto the next leg after 5 seconds have passed. + * Moves onto the next immediately. */ override fun navigateNextRouteLeg(routeLegProgress: RouteLegProgress): Boolean { - if (currentRouteLeg != routeLegProgress.routeLeg) { - currentRouteLeg = routeLegProgress.routeLeg - routeLegCompletedTime = SystemClock.elapsedRealtimeNanos() - } - - val elapsedTimeNanos = SystemClock.elapsedRealtimeNanos() - (routeLegCompletedTime ?: 0L) - val shouldNavigateNextRouteLeg = elapsedTimeNanos >= AUTO_ARRIVAL_NANOS - if (shouldNavigateNextRouteLeg) { - currentRouteLeg = null - routeLegCompletedTime = null - } - return shouldNavigateNextRouteLeg - } - - internal companion object { - val AUTO_ARRIVAL_NANOS = TimeUnit.SECONDS.toNanos(5L) + return true } } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/ForkPointPassedObserver.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/ForkPointPassedObserver.kt new file mode 100644 index 00000000000..8df21d2ccf6 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/ForkPointPassedObserver.kt @@ -0,0 +1,62 @@ +package com.mapbox.navigation.core.directions + +import com.mapbox.navigation.base.internal.extensions.internalAlternativeRouteIndices +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.core.SetRoutes +import com.mapbox.navigation.core.directions.session.DirectionsSession +import com.mapbox.navigation.core.directions.session.DirectionsSessionRoutes +import com.mapbox.navigation.core.directions.session.IgnoredRoute +import com.mapbox.navigation.core.directions.session.routesPlusIgnored +import com.mapbox.navigation.core.trip.session.RouteProgressObserver +import com.mapbox.navigation.utils.internal.logD + +internal class ForkPointPassedObserver( + private val directionsSession: DirectionsSession, + private val currentLegIndex: () -> Int, +) : RouteProgressObserver { + override fun onRouteProgressChanged(routeProgress: RouteProgress) { + val allCurrentRoutes = directionsSession.routesPlusIgnored + + if (allCurrentRoutes.isEmpty()) return + + val needToHideAlternatives = routeProgress + .internalAlternativeRouteIndices() + .filter { it.value.isForkPointPassed } + + val newRoutes = DirectionsSessionRoutes( + acceptedRoutes = allCurrentRoutes.filter { it.id !in needToHideAlternatives }, + ignoredRoutes = allCurrentRoutes.filter { it.id in needToHideAlternatives } + .map { IgnoredRoute(it, REASON_ALTERNATIVE_FORK_POINT_PASSED) }, + setRoutesInfo = SetRoutes.Alternatives(currentLegIndex()), + ) + + when { + newRoutes.ignoredRoutes == directionsSession.ignoredRoutes && + newRoutes.acceptedRoutes == directionsSession.routes -> return + else -> { + if (newRoutes.ignoredRoutes.isNotEmpty()) { + logD( + "Hiding alternatives due to fork point has passed: " + + "${newRoutes.ignoredRoutes}", + TAG, + ) + } + + if (newRoutes.acceptedRoutes != directionsSession.routes) { + logD( + "Settigns new routes due to fork point changes: " + + "${newRoutes.acceptedRoutes}", + TAG, + ) + } + + directionsSession.setNavigationRoutesFinished(newRoutes) + } + } + } + + companion object { + private const val TAG = "ForkPointPassedObserver" + internal const val REASON_ALTERNATIVE_FORK_POINT_PASSED = "Alternative fork point passed" + } +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/DirectionsSession.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/DirectionsSession.kt index bdbd91b451c..e5e0e393353 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/DirectionsSession.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/DirectionsSession.kt @@ -1,5 +1,6 @@ package com.mapbox.navigation.core.directions.session +import androidx.annotation.VisibleForTesting import com.mapbox.api.directions.v5.models.RouteOptions import com.mapbox.navigation.base.route.NavigationRoute import com.mapbox.navigation.base.route.NavigationRouterCallback @@ -9,9 +10,19 @@ import com.mapbox.navigation.core.internal.utils.mapToReason internal interface DirectionsSession : RouteRefresh { + @VisibleForTesting val routesUpdatedResult: RoutesUpdatedResult? + val routes: List + /** + * A list of routes that have been ignored. + * + * Ignored routes are those that are not currently being used for navigation, + * but are still kept in memory for potential future use. + */ + val ignoredRoutes: List + val initialLegIndex: Int fun setNavigationRoutesStarted(params: RoutesSetStartedParams) @@ -72,6 +83,12 @@ internal interface DirectionsSession : RouteRefresh { fun shutdown() } +internal val DirectionsSession.routesPlusIgnored: List + get() = routes + ignoredRoutes.map { it.navigationRoute } + +internal fun DirectionsSession.findRoute(routeId: String): NavigationRoute? = + routesPlusIgnored.find { it.id == routeId } + internal data class DirectionsSessionRoutes( val acceptedRoutes: List, val ignoredRoutes: List, diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/MapboxDirectionsSession.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/MapboxDirectionsSession.kt index bcd1801b0ac..ee74584d9e6 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/MapboxDirectionsSession.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/MapboxDirectionsSession.kt @@ -1,5 +1,6 @@ package com.mapbox.navigation.core.directions.session +import androidx.annotation.VisibleForTesting import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.api.directions.v5.models.RouteOptions import com.mapbox.navigation.base.internal.RouteRefreshRequestData @@ -26,10 +27,15 @@ internal class MapboxDirectionsSession( private val onSetNavigationRoutesStartedObservers = CopyOnWriteArraySet() + @VisibleForTesting override var routesUpdatedResult: RoutesUpdatedResult? = null + override val routes: List get() = routesUpdatedResult?.navigationRoutes ?: emptyList() + override val ignoredRoutes: List + get() = routesUpdatedResult?.ignoredRoutes ?: emptyList() + override var initialLegIndex = DEFAULT_INITIAL_LEG_INDEX private set @@ -39,6 +45,7 @@ internal class MapboxDirectionsSession( override fun setNavigationRoutesFinished(routes: DirectionsSessionRoutes) { this.initialLegIndex = routes.setRoutesInfo.initialLegIndex() + if ( routesUpdatedResult?.navigationRoutes?.isEmpty() == true && routes.acceptedRoutes.isEmpty() @@ -46,7 +53,10 @@ internal class MapboxDirectionsSession( return } - val result = routes.toRoutesUpdatedResult().also { routesUpdatedResult = it } + val result = routes.toRoutesUpdatedResult().also { + routesUpdatedResult = it + } + onSetNavigationRoutesFinishedObservers.forEach { it.onRoutesChanged(result) } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/internal/congestions/speed/SpeedAnalyzeUtils.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/internal/congestions/speed/SpeedAnalyzeUtils.kt index c307d76b8d3..b4b280c9555 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/internal/congestions/speed/SpeedAnalyzeUtils.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/internal/congestions/speed/SpeedAnalyzeUtils.kt @@ -1,6 +1,9 @@ +@file:OptIn(ExperimentalMapboxNavigationAPI::class) + package com.mapbox.navigation.core.internal.congestions.speed import com.mapbox.api.directions.v5.models.LegAnnotation +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI import com.mapbox.navigation.base.internal.CongestionNumericOverride import com.mapbox.navigation.base.internal.route.update import com.mapbox.navigation.base.route.NavigationRoute diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/internal/utils/Bitmap.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/internal/utils/Bitmap.kt index 46cc9dce70c..96dba812881 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/internal/utils/Bitmap.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/internal/utils/Bitmap.kt @@ -1,30 +1,11 @@ package com.mapbox.navigation.core.internal.utils import android.graphics.Bitmap -import android.util.Base64 -import com.mapbox.bindgen.DataRef import com.mapbox.navigation.core.telemetry.events.BitmapEncodeOptions -import com.mapbox.navigator.ScreenshotFormat import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -import java.nio.charset.Charset import kotlin.math.min import kotlin.math.roundToInt -@JvmSynthetic -internal fun String.encodedBitmapToNativeScreenshotFormat(): ScreenshotFormat { - val encoded = toByteArray(Charset.defaultCharset()) - - val byteBuffer = ByteBuffer.allocateDirect(encoded.size) - byteBuffer.put(encoded) - val dataRef = DataRef(byteBuffer) - - return ScreenshotFormat( - dataRef, - Base64.encodeToString(encoded, Base64.DEFAULT), - ) -} - @JvmSynthetic internal fun Bitmap.encode( options: BitmapEncodeOptions = BitmapEncodeOptions.Builder().build(), diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/navigator/NavigatorMapper.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/navigator/NavigatorMapper.kt index 23f1564c910..cf541015c34 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/navigator/NavigatorMapper.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/navigator/NavigatorMapper.kt @@ -192,6 +192,7 @@ private fun NavigationStatus.getRouteProgress( it.geometryIndex, it.shapeIndex, it.intersectionIndex, + it.isForkPointPassed, ) } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/MapboxRerouteController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/MapboxRerouteController.kt index 08e051d3330..85649274a64 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/MapboxRerouteController.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/MapboxRerouteController.kt @@ -19,6 +19,7 @@ import com.mapbox.navigation.base.route.ResponseOriginAPI import com.mapbox.navigation.base.route.RouterFailure import com.mapbox.navigation.base.route.RouterOrigin import com.mapbox.navigation.core.directions.session.DirectionsSession +import com.mapbox.navigation.core.directions.session.routesPlusIgnored import com.mapbox.navigation.core.ev.EVDynamicDataHolder import com.mapbox.navigation.core.internal.router.GetRouteSignature import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdater @@ -142,7 +143,7 @@ internal class MapboxRerouteController @VisibleForTesting constructor( val routeProgress = tripSession.getRouteProgress() val routeAlternativeId = routeProgress?.routeAlternativeId - val routes = directionsSession.routes + val routes = directionsSession.routesPlusIgnored if (!ignoreDeviationToAlternatives && routeAlternativeId != null) { val relevantAlternative = routes.find { it.id == routeAlternativeId } if (relevantAlternative != null) { diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/MapboxRerouteOptionsAdapter.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/MapboxRerouteOptionsAdapter.kt index 890e70f9740..aa86d852a93 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/MapboxRerouteOptionsAdapter.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/MapboxRerouteOptionsAdapter.kt @@ -4,6 +4,7 @@ import androidx.annotation.VisibleForTesting import com.mapbox.api.directions.v5.models.RouteOptions import com.mapbox.navigation.core.ev.EVDynamicDataHolder import com.mapbox.navigation.core.ev.EVRerouteOptionsAdapter +import com.mapbox.navigation.utils.internal.logI internal class MapboxRerouteOptionsAdapter @VisibleForTesting constructor( private val internalOptionsAdapters: List, @@ -32,6 +33,21 @@ internal class MapboxRerouteOptionsAdapter @VisibleForTesting constructor( val internalOptions = internalOptionsAdapters.fold(routeOptions) { value, modifier -> modifier.onRouteOptions(value, params) } - return externalOptionsAdapter?.onRouteOptions(internalOptions) ?: internalOptions + + logI(LOG_CATEGORY) { + "Initial options for reroute: ${internalOptions.toUrl("***")}" + } + + val updatedOptions = externalOptionsAdapter?.onRouteOptions(internalOptions) + if (updatedOptions != null) { + logI(LOG_CATEGORY) { + "Options for reroute have been externally updated: ${updatedOptions.toUrl("***")}" + } + } + return updatedOptions ?: internalOptions + } + + private companion object { + const val LOG_CATEGORY = "MapboxRerouteOptionsAdapter" } } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesController.kt index b5db829155b..4887486e6f1 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesController.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesController.kt @@ -1,6 +1,5 @@ package com.mapbox.navigation.core.routealternatives -import com.mapbox.bindgen.Expected import com.mapbox.bindgen.ExpectedFactory import com.mapbox.navigation.base.internal.utils.AlternativesInfo import com.mapbox.navigation.base.internal.utils.AlternativesParsingResult @@ -12,6 +11,8 @@ import com.mapbox.navigation.base.route.NavigationRoute import com.mapbox.navigation.base.route.RouteAlternativesOptions import com.mapbox.navigation.base.route.RouterOrigin import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.core.directions.session.DirectionsSession +import com.mapbox.navigation.core.directions.session.findRoute import com.mapbox.navigation.core.internal.routealternatives.NavigationRouteAlternativesObserver import com.mapbox.navigation.core.trip.session.TripSession import com.mapbox.navigation.navigator.internal.MapboxNativeNavigator @@ -37,6 +38,7 @@ internal class RouteAlternativesController( private val tripSession: TripSession, private val threadController: ThreadController, private val routeParsingManager: RouteParsingManager, + private val directionSession: DirectionsSession, ) : AlternativeMetadataProvider { @RouterOrigin @@ -63,13 +65,10 @@ internal class RouteAlternativesController( fun setRouteUpdateSuggestionListener(listener: UpdateRoutesSuggestionObserver?) { updateNativeObserver { - if (listener == null) { - defaultAlternativesHandler = null + defaultAlternativesHandler = if (listener == null) { + null } else { - defaultAlternativesHandler = - RouteAlternativesToRouteUpdateSuggestionsAdapter( - listener, - ) + RouteAlternativesToRouteUpdateSuggestionsAdapter(listener) } } } @@ -135,21 +134,19 @@ internal class RouteAlternativesController( } observerProcessingJob?.cancel() - observerProcessingJob = - processRouteAlternatives( - onlinePrimaryRoute, - routeAlternatives, - ) { alternatives, origin -> - logD("${alternatives.size} alternatives available", LOG_CATEGORY) - - val routeProgress = tripSession.getRouteProgress() - ?: run { - logD("skipping alternatives update - no progress", LOG_CATEGORY) - return@processRouteAlternatives - } - - observerToTrigger?.onRouteAlternatives(routeProgress, alternatives, origin) + observerProcessingJob = processRouteAlternatives( + onlinePrimaryRoute, + routeAlternatives, + ) { alternatives, origin -> + logD("${alternatives.size} alternatives available", LOG_CATEGORY) + + val routeProgress = tripSession.getRouteProgress() ?: run { + logD("skipping alternatives update - no progress", LOG_CATEGORY) + return@processRouteAlternatives } + + observerToTrigger?.onRouteAlternatives(routeProgress, alternatives, origin) + } } override fun onError(message: String) { @@ -175,20 +172,22 @@ internal class RouteAlternativesController( val primaryRoutes = onlinePrimaryRoute?.let { listOf(it) } ?: emptyList() val allAlternatives = primaryRoutes + nativeAlternatives.map { it.route } - val alternatives: List = if (allAlternatives.isNotEmpty()) { + val alternatives = if (allAlternatives.isNotEmpty()) { val args = AlternativesInfo( RouteResponseInfo.fromResponses(allAlternatives.map { it.responseJsonRef.buffer }), ) - val alternativesParsingResult: - AlternativesParsingResult>> = - routeParsingManager.parseAlternatives(args) { - withContext(ThreadController.DefaultDispatcher) { - parseRouteInterfaces( - allAlternatives, - responseTimeElapsedSeconds, - ) - } + + val alternativesParsingResult = routeParsingManager.parseAlternatives(args) { + withContext(ThreadController.DefaultDispatcher) { + parseRouteInterfaces( + routes = allAlternatives, + responseTimeElapsedSeconds = responseTimeElapsedSeconds, + routeLookup = directionSession::findRoute, + routeToDirections = routeParsingManager::parseRouteToDirections, + ) } + } + val expected = when (alternativesParsingResult) { AlternativesParsingResult.NotActual -> { ExpectedFactory.createError( @@ -248,13 +247,11 @@ internal class RouteAlternativesController( private class RouteAlternativesToRouteUpdateSuggestionsAdapter( private val suggestRouteUpdate: (UpdateRouteSuggestion) -> Unit, - ) : - NavigationRouteAlternativesObserver { + ) : NavigationRouteAlternativesObserver { override fun onRouteAlternatives( routeProgress: RouteProgress, alternatives: List, - @RouterOrigin - routerOrigin: String, + @RouterOrigin routerOrigin: String, ) { when (routeProgress.navigationRoute.origin) { RouterOrigin.ONLINE -> { diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerProvider.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerProvider.kt index 3ca1047bc27..3436a12cb68 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerProvider.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerProvider.kt @@ -2,6 +2,7 @@ package com.mapbox.navigation.core.routealternatives import com.mapbox.navigation.base.internal.utils.RouteParsingManager import com.mapbox.navigation.base.route.RouteAlternativesOptions +import com.mapbox.navigation.core.directions.session.DirectionsSession import com.mapbox.navigation.core.trip.session.TripSession import com.mapbox.navigation.navigator.internal.MapboxNativeNavigator import com.mapbox.navigation.utils.internal.ThreadController @@ -14,11 +15,13 @@ internal object RouteAlternativesControllerProvider { tripSession: TripSession, threadController: ThreadController, routeParsingManager: RouteParsingManager, + directionsSession: DirectionsSession, ) = RouteAlternativesController( options, navigator, tripSession, threadController, routeParsingManager, + directionsSession, ) } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemover.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemover.kt index a7926018a88..c53b4b8ca46 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemover.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemover.kt @@ -2,6 +2,8 @@ package com.mapbox.navigation.core.routerefresh import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.api.directions.v5.models.RouteLeg +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI +import com.mapbox.navigation.base.internal.route.createRouteRefreshMetadata import com.mapbox.navigation.base.internal.route.update import com.mapbox.navigation.base.internal.time.parseISO8601DateToLocalTimeOrNull import com.mapbox.navigation.base.route.NavigationRoute @@ -35,6 +37,7 @@ internal class ExpiringDataRemover( ) } + @OptIn(ExperimentalMapboxNavigationAPI::class) private fun removeExpiringDataFromRoute( route: NavigationRoute, currentLegIndex: Int, @@ -55,6 +58,7 @@ internal class ExpiringDataRemover( return route.update( directionsRouteBlock = directionsRouteBlock, waypointsBlock = { this }, + routeRefreshMetadata = createRouteRefreshMetadata(isUpToDate = false), ) } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshController.kt index 265cf1e6cd7..acd583eea06 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshController.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshController.kt @@ -3,6 +3,7 @@ package com.mapbox.navigation.core.routerefresh import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI import com.mapbox.navigation.base.route.NavigationRoute import com.mapbox.navigation.core.RoutesInvalidatedObserver +import com.mapbox.navigation.core.directions.ForkPointPassedObserver import com.mapbox.navigation.core.directions.session.RoutesExtra import com.mapbox.navigation.core.directions.session.RoutesObserver import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult @@ -121,7 +122,16 @@ class RouteRefreshController internal constructor( if (result.reason != RoutesExtra.ROUTES_UPDATE_REASON_REFRESH) { routeRefresherResultProcessor.reset() immediateRouteRefreshController.cancel() - plannedRouteRefreshController.startRoutesRefreshing(result.navigationRoutes) + + // "fork point passed" routes still have a chance to be useful for navigation, + // even if they are on the ignore list. We need to refresh them as well. + val validAlternatives = result.ignoredRoutes.filter { + it.reason == ForkPointPassedObserver.REASON_ALTERNATIVE_FORK_POINT_PASSED + }.map { it.navigationRoute } + + plannedRouteRefreshController.startRoutesRefreshing( + result.navigationRoutes + validAlternatives, + ) } } } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/UserFeedback.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/UserFeedback.kt index 04cebfe9698..51cbf4d5bfa 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/UserFeedback.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/UserFeedback.kt @@ -5,10 +5,10 @@ import com.mapbox.geojson.Point import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.internal.telemetry.ExtendedUserFeedback -import com.mapbox.navigation.core.internal.utils.encodedBitmapToNativeScreenshotFormat import com.mapbox.navigation.core.telemetry.events.FeedbackEvent import com.mapbox.navigation.core.telemetry.events.FeedbackHelper import com.mapbox.navigation.core.telemetry.events.FeedbackMetadata +import com.mapbox.navigator.ScreenshotFormat /** * Class for user feedbacks, contains properties that were passed to @@ -17,7 +17,7 @@ import com.mapbox.navigation.core.telemetry.events.FeedbackMetadata * @property feedbackType feedback type, one of [FeedbackEvent.Type] or a custom one * @property feedbackSubTypes list of [FeedbackEvent.SubType] and/or custom feedback subtypes * @property description description message - * @property screenshot screenshot that will be attached to feedback + * @property screenshot base64-encoded screenshot that will be attached to feedback * @property feedbackMetadata use it to attach feedback to a specific passed location. */ @ExperimentalPreviewMapboxNavigationAPI @@ -110,7 +110,7 @@ class UserFeedback private constructor( } /** - * Screenshot that will be attached to feedback + * Base64-encoded screenshot that will be attached to feedback */ fun screenshot(screenshot: String?): Builder = apply { this.screenshot = screenshot @@ -143,7 +143,7 @@ class UserFeedback private constructor( feedbackType, feedbackSubTypes, description, - screenshot?.encodedBitmapToNativeScreenshotFormat(), + ScreenshotFormat(null, screenshot), ) } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/eh/RoadObjectMatcher.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/eh/RoadObjectMatcher.kt index 58350cfecf6..32525bc6f6f 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/eh/RoadObjectMatcher.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/eh/RoadObjectMatcher.kt @@ -50,6 +50,16 @@ class RoadObjectMatcher internal constructor( roadObjectMatcherObservers.add(roadObjectMatcherObserver) } + /** + * Unregister road object matcher observer. + */ + fun unregisterRoadObjectMatcherObserver(roadObjectMatcherObserver: RoadObjectMatcherObserver) { + roadObjectMatcherObservers.remove(roadObjectMatcherObserver) + if (roadObjectMatcherObservers.isEmpty()) { + navigator.roadObjectMatcher.setListener(null) + } + } + private val roadObjectMatcherListener = object : RoadObjectMatcherListener { override fun onRoadObjectMatched( roadObject: Expected, diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationBaseTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationBaseTest.kt index aeff304de2c..bb181c80c62 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationBaseTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationBaseTest.kt @@ -209,7 +209,7 @@ internal open class MapboxNavigationBaseTest { } returns evDynamicDataHolder mockkObject(RouteAlternativesControllerProvider) every { - RouteAlternativesControllerProvider.create(any(), any(), any(), any(), any()) + RouteAlternativesControllerProvider.create(any(), any(), any(), any(), any(), any()) } returns routeAlternativesController mockkObject(MapMatchingAPIProvider) every { diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt index 7913a8b47c8..92a491d28a8 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt @@ -12,6 +12,7 @@ import com.mapbox.navigation.base.trip.model.RouteLegProgress import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.core.arrival.ArrivalController import com.mapbox.navigation.core.arrival.ArrivalProgressObserver +import com.mapbox.navigation.core.directions.ForkPointPassedObserver import com.mapbox.navigation.core.directions.session.DirectionsSessionRoutes import com.mapbox.navigation.core.directions.session.IgnoredRoute import com.mapbox.navigation.core.directions.session.RoutesExtra @@ -2301,6 +2302,19 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { } } + @Test + fun `ForkPointPassedObserver is registered during instantiation`() { + val forkPassedObserverMock = mockk(relaxed = true) + + every { + NavigationComponentProvider.createForkPointPassedObserver(any(), any()) + } returns forkPassedObserverMock + + createMapboxNavigation() + + verify { tripSession.registerRouteProgressObserver(refEq(forkPassedObserverMock)) } + } + private fun interceptRefreshObserver(): RouteRefreshObserver { val observers = mutableListOf() verify { routeRefreshController.registerRouteRefreshObserver(capture(observers)) } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/RoutesProgressDataProviderTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/RoutesProgressDataProviderTest.kt index dcc5c165179..d0070094ecf 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/RoutesProgressDataProviderTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/RoutesProgressDataProviderTest.kt @@ -49,6 +49,7 @@ class RoutesProgressDataProviderTest { altRouteGeometryIndex1, altLegGeometryIndex1, 1, + false, ), altId2 to RouteIndicesFactory.buildRouteIndices( altLegIndex2, @@ -56,6 +57,7 @@ class RoutesProgressDataProviderTest { altRouteGeometryIndex2, altLegGeometryIndex2, 2, + false, ), ) } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/arrival/AutoArrivalControllerTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/arrival/AutoArrivalControllerTest.kt index c9e68a987fd..70448122f01 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/arrival/AutoArrivalControllerTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/arrival/AutoArrivalControllerTest.kt @@ -1,66 +1,17 @@ package com.mapbox.navigation.core.arrival -import android.os.SystemClock -import com.mapbox.api.directions.v5.models.RouteLeg -import com.mapbox.navigation.base.trip.model.RouteLegProgress -import com.mapbox.navigation.core.arrival.AutoArrivalController.Companion.AUTO_ARRIVAL_NANOS -import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.unmockkObject -import org.junit.After -import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Test class AutoArrivalControllerTest { private val arrivalController = AutoArrivalController() - @Before - fun setup() { - mockkStatic(SystemClock::class) - } - - @After - fun teardown() { - unmockkObject(SystemClock.elapsedRealtimeNanos()) - } - - @Test - fun `should navigate next at predicted arrival`() { - val routeLeg = mockk() - - mockNanos(100) - assertFalse(arrivalController.navigateNextRouteLeg(mockProgress(routeLeg))) - mockNanos(100 + AUTO_ARRIVAL_NANOS - 1) - assertFalse(arrivalController.navigateNextRouteLeg(mockProgress(routeLeg))) - mockNanos(100 + AUTO_ARRIVAL_NANOS) - assertTrue(arrivalController.navigateNextRouteLeg(mockProgress(routeLeg))) - } - @Test - fun `should restart timer if rerouted`() { - val routeLeg = mockk() - - mockNanos(100) - assertFalse(arrivalController.navigateNextRouteLeg(mockProgress(routeLeg))) - mockNanos(100L + AUTO_ARRIVAL_NANOS) - val reroutedRouteLeg = mockk() - assertFalse(arrivalController.navigateNextRouteLeg(mockProgress(reroutedRouteLeg))) - mockNanos(100 + AUTO_ARRIVAL_NANOS + AUTO_ARRIVAL_NANOS - 1) - assertFalse(arrivalController.navigateNextRouteLeg(mockProgress(reroutedRouteLeg))) - - mockNanos(100 + AUTO_ARRIVAL_NANOS + AUTO_ARRIVAL_NANOS) - assertTrue(arrivalController.navigateNextRouteLeg(mockProgress(reroutedRouteLeg))) - } - - private fun mockNanos(nanos: Long) = every { - SystemClock.elapsedRealtimeNanos() - } returns nanos + fun `navigateNextRouteLeg fire true`() { + val result = arrivalController.navigateNextRouteLeg(mockk()) - private fun mockProgress(mockedRouteLeg: RouteLeg) = mockk { - every { routeLeg } returns mockedRouteLeg + assertTrue(result) } } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/directions/ForkPointPassedObserverTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/directions/ForkPointPassedObserverTest.kt new file mode 100644 index 00000000000..f90a9bbf2ca --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/directions/ForkPointPassedObserverTest.kt @@ -0,0 +1,209 @@ +package com.mapbox.navigation.core.directions + +import com.mapbox.navigation.base.internal.extensions.internalAlternativeRouteIndices +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.core.SetRoutes +import com.mapbox.navigation.core.directions.session.DirectionsSession +import com.mapbox.navigation.core.directions.session.DirectionsSessionRoutes +import com.mapbox.navigation.core.directions.session.IgnoredRoute +import com.mapbox.navigation.testing.LoggingFrontendTestRule +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class ForkPointPassedObserverTest { + @get:Rule + val logRule = LoggingFrontendTestRule() + + private lateinit var directionsSession: DirectionsSession + private lateinit var currentLegIndex: () -> Int + private lateinit var observer: ForkPointPassedObserver + + @Before + fun setUp() { + directionsSession = mockk(relaxed = true) + currentLegIndex = mockk() + observer = ForkPointPassedObserver(directionsSession, currentLegIndex) + } + + @Test + fun `onRouteProgressChanged hides alternatives when fork point passed`() { + val routeProgress: RouteProgress = mockk { + every { internalAlternativeRouteIndices() } returns mapOf( + "route1" to mockk { every { isForkPointPassed } returns true }, + "route2" to mockk { every { isForkPointPassed } returns false }, + ) + } + + val route1 = mockk { every { id } returns "route1" } + val route2 = mockk { every { id } returns "route2" } + val routes = listOf(route1, route2) + + every { directionsSession.routes } returns routes + every { currentLegIndex.invoke() } returns 0 + + observer.onRouteProgressChanged(routeProgress) + + verify { + directionsSession.setNavigationRoutesFinished( + DirectionsSessionRoutes( + listOf(route2), + listOf( + IgnoredRoute( + route1, + ForkPointPassedObserver.REASON_ALTERNATIVE_FORK_POINT_PASSED, + ), + ), + SetRoutes.Alternatives(0), + ), + ) + } + } + + @Test + fun `onRouteProgressChanged does nothing when no fork point passed`() { + val mainRoute = mockk { every { id } returns "route1" } + val alternativeRoute = mockk { every { id } returns "route2" } + + val routeProgress: RouteProgress = mockk { + every { navigationRoute } returns mainRoute + every { internalAlternativeRouteIndices() } returns mapOf( + "route2" to mockk { every { isForkPointPassed } returns false }, + ) + } + + val routes = listOf( + mainRoute, + alternativeRoute, + ) + + every { directionsSession.routes } returns routes + every { currentLegIndex.invoke() } returns 0 + + observer.onRouteProgressChanged(routeProgress) + + verify(exactly = 0) { directionsSession.setNavigationRoutesFinished(any()) } + } + + @Test + fun `onRouteProgressChanged does nothing when no routes`() { + val routeProgress: RouteProgress = mockk { + every { internalAlternativeRouteIndices() } returns mapOf( + "route1" to mockk { every { isForkPointPassed } returns true }, + ) + } + + every { currentLegIndex.invoke() } returns 0 + every { directionsSession.routes } returns emptyList() + + observer.onRouteProgressChanged(routeProgress) + + verify(exactly = 0) { directionsSession.setNavigationRoutesFinished(any()) } + } + + @Test + fun `ignored alternative becomes available and is returned to routes list`() { + val route1 = mockk { every { id } returns "route1" } + val route2 = mockk { every { id } returns "route2" } + val currentRotues = listOf(route1, route2) + + val ignoredRoute = IgnoredRoute( + navigationRoute = mockk { every { id } returns "ignored_route" }, + reason = ForkPointPassedObserver.REASON_ALTERNATIVE_FORK_POINT_PASSED, + ) + + every { directionsSession.routes } returns currentRotues + every { directionsSession.ignoredRoutes } returns listOf(ignoredRoute) + every { currentLegIndex.invoke() } returns 0 + + val routeProgress: RouteProgress = mockk { + every { internalAlternativeRouteIndices() } returns mapOf( + "route1" to mockk(relaxed = true), + "route2" to mockk(relaxed = true), + "ignored_route" to mockk(relaxed = true), + ) + } + + observer.onRouteProgressChanged(routeProgress) + + verify { + directionsSession.setNavigationRoutesFinished( + DirectionsSessionRoutes( + currentRotues + ignoredRoute.navigationRoute, + emptyList(), + SetRoutes.Alternatives(0), + ), + ) + } + } + + @Test + fun `the same available and ignored routes should not trigger direction session update`() { + val route1 = mockk { every { id } returns "route1" } + val route2 = mockk { every { id } returns "route2" } + val currentRotues = listOf(route1, route2) + + val ignoredRoute = IgnoredRoute( + navigationRoute = mockk { every { id } returns "ignored_route" }, + reason = ForkPointPassedObserver.REASON_ALTERNATIVE_FORK_POINT_PASSED, + ) + + every { directionsSession.routes } returns currentRotues + every { directionsSession.ignoredRoutes } returns listOf(ignoredRoute) + every { currentLegIndex.invoke() } returns 0 + + val routeProgress: RouteProgress = mockk { + every { internalAlternativeRouteIndices() } returns mapOf( + "route1" to mockk(relaxed = true), + "route2" to mockk(relaxed = true), + "ignored_route" to mockk(relaxed = true) { + every { isForkPointPassed } returns true + }, + ) + } + + observer.onRouteProgressChanged(routeProgress) + + verify(exactly = 0) { + directionsSession.setNavigationRoutesFinished(any()) + } + } + + @Test + fun `alternative route with fork point passed from true to false returns to main routes`() { + val route1 = mockk { every { id } returns "route1" } + val route2 = mockk { every { id } returns "route2" } + val ignoredRoute = IgnoredRoute( + navigationRoute = mockk { every { id } returns "ignored_route" }, + reason = ForkPointPassedObserver.REASON_ALTERNATIVE_FORK_POINT_PASSED, + ) + + every { directionsSession.routes } returns listOf(route1, route2) + every { directionsSession.ignoredRoutes } returns listOf(ignoredRoute) + every { currentLegIndex.invoke() } returns 0 + + val routeProgress: RouteProgress = mockk { + every { internalAlternativeRouteIndices() } returns mapOf( + "route1" to mockk { every { isForkPointPassed } returns false }, + "route2" to mockk { every { isForkPointPassed } returns false }, + "ignored_route" to mockk { every { isForkPointPassed } returns false }, + ) + } + + observer.onRouteProgressChanged(routeProgress) + + verify { + directionsSession.setNavigationRoutesFinished( + DirectionsSessionRoutes( + listOf(route1, route2, ignoredRoute.navigationRoute), + emptyList(), + SetRoutes.Alternatives(0), + ), + ) + } + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/internal/congestions/TrafficOverrideHandlerTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/internal/congestions/TrafficOverrideHandlerTest.kt index e2962397575..5da0838ac17 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/internal/congestions/TrafficOverrideHandlerTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/internal/congestions/TrafficOverrideHandlerTest.kt @@ -1,6 +1,9 @@ +@file:OptIn(ExperimentalMapboxNavigationAPI::class) + package com.mapbox.navigation.core.internal.congestions import com.mapbox.api.directions.v5.models.StepIntersection +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI import com.mapbox.navigation.base.internal.CongestionNumericOverride import com.mapbox.navigation.base.internal.route.overriddenTraffic diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/internal/congestions/processor/DecreaseTrafficUpdateActionHandlerTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/internal/congestions/processor/DecreaseTrafficUpdateActionHandlerTest.kt index e2b06bc68b4..8b51f169e73 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/internal/congestions/processor/DecreaseTrafficUpdateActionHandlerTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/internal/congestions/processor/DecreaseTrafficUpdateActionHandlerTest.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalMapboxNavigationAPI::class) + package com.mapbox.navigation.core.internal.congestions.processor import com.mapbox.api.directions.v5.models.LegAnnotation @@ -6,6 +8,7 @@ import com.mapbox.api.directions.v5.models.ManeuverModifier import com.mapbox.api.directions.v5.models.RouteLeg import com.mapbox.api.directions.v5.models.StepIntersection import com.mapbox.api.directions.v5.models.StepManeuver +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI import com.mapbox.navigation.base.internal.CongestionNumericOverride import com.mapbox.navigation.base.internal.route.overriddenTraffic import com.mapbox.navigation.base.internal.route.update diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/internal/router/RouterWrapperTests.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/internal/router/RouterWrapperTests.kt index ffeb15896f0..90bba7956b4 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/internal/router/RouterWrapperTests.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/internal/router/RouterWrapperTests.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalMapboxNavigationAPI::class) + package com.mapbox.navigation.core.internal.router import com.mapbox.api.directions.v5.models.DirectionsResponse @@ -8,6 +10,7 @@ import com.mapbox.bindgen.Expected import com.mapbox.bindgen.ExpectedFactory import com.mapbox.common.MapboxServices import com.mapbox.geojson.Point +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions import com.mapbox.navigation.base.extensions.coordinates import com.mapbox.navigation.base.internal.RouteRefreshRequestData @@ -740,6 +743,8 @@ class RouterWrapperTests { verify(exactly = 1) { routerRefreshCallback.onRefreshReady(capture(routeSlot)) } checkRefreshedNavigationRouteWithWithWaypoints(expected, routeSlot.captured) + assertNotNull(routeSlot.captured.routeRefreshMetadata) + assertTrue(routeSlot.captured.routeRefreshMetadata!!.isUpToDate) } @Test diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/navigator/NavigatorMapperTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/navigator/NavigatorMapperTest.kt index 1a198250e94..3b6ddbe9b8d 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/navigator/NavigatorMapperTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/navigator/NavigatorMapperTest.kt @@ -106,8 +106,8 @@ class NavigatorMapperTest { currentRouteGeometryIndex = routeGeometryIndex, inParkingAisle = true, alternativeRoutesIndices = mapOf( - "id#2" to RouteIndicesFactory.buildRouteIndices(2, 4, 6, 8, 10), - "id#3" to RouteIndicesFactory.buildRouteIndices(3, 7, 5, 11, 9), + "id#2" to RouteIndicesFactory.buildRouteIndices(2, 4, 6, 8, 10, false), + "id#3" to RouteIndicesFactory.buildRouteIndices(3, 7, 5, 11, 9, false), ), ) diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/replay/route/ReplayProgressObserverTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/replay/route/ReplayProgressObserverTest.kt index 9c7e9178d4d..a282c7495cb 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/replay/route/ReplayProgressObserverTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/replay/route/ReplayProgressObserverTest.kt @@ -1,7 +1,10 @@ +@file:OptIn(ExperimentalMapboxNavigationAPI::class) + package com.mapbox.navigation.core.replay.route import com.mapbox.api.directions.v5.models.RouteLeg import com.mapbox.geojson.utils.PolylineUtils +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI import com.mapbox.navigation.base.internal.route.update import com.mapbox.navigation.base.route.NavigationRoute import com.mapbox.navigation.base.trip.model.RouteProgress diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/reroute/MapboxRerouteOptionsAdapterTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/reroute/MapboxRerouteOptionsAdapterTest.kt index 4a979ff6a86..c571c84bc31 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/reroute/MapboxRerouteOptionsAdapterTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/reroute/MapboxRerouteOptionsAdapterTest.kt @@ -1,10 +1,12 @@ package com.mapbox.navigation.core.reroute import com.mapbox.api.directions.v5.DirectionsCriteria +import com.mapbox.navigation.testing.LoggingFrontendTestRule import com.mapbox.navigation.testing.factories.createRouteOptions import io.mockk.every import io.mockk.mockk import org.junit.Assert.assertEquals +import org.junit.Rule import org.junit.Test class MapboxRerouteOptionsAdapterTest { @@ -12,6 +14,9 @@ class MapboxRerouteOptionsAdapterTest { private val externalAdapter = mockk() private val inputOptions = createRouteOptions(profile = DirectionsCriteria.PROFILE_DRIVING) + @get:Rule + val logRule = LoggingFrontendTestRule() + @Test fun noInternalAdaptersNorExternal() { val sut = MapboxRerouteOptionsAdapter(emptyList()) diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerTest.kt index 2820fd6c617..7b0c907befe 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerTest.kt @@ -9,6 +9,8 @@ import com.mapbox.navigation.base.route.NavigationRoute import com.mapbox.navigation.base.route.RouteAlternativesOptions import com.mapbox.navigation.base.route.RouterOrigin import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.core.directions.session.DirectionsSession +import com.mapbox.navigation.core.directions.session.findRoute import com.mapbox.navigation.core.internal.routealternatives.NavigationRouteAlternativesObserver import com.mapbox.navigation.core.trip.session.TripSession import com.mapbox.navigation.navigator.internal.MapboxNativeNavigator @@ -25,14 +27,18 @@ import com.mapbox.navigation.testing.factories.createRouteOptions import com.mapbox.navigation.utils.internal.ThreadController import com.mapbox.navigator.RouteAlternative import com.mapbox.navigator.RouteAlternativesControllerInterface +import com.mapbox.navigator.RouteAlternativesObserver import com.mapbox.navigator.RouteIntersection import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject +import io.mockk.mockkStatic import io.mockk.runs import io.mockk.slot +import io.mockk.spyk import io.mockk.unmockkObject +import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.pauseDispatcher @@ -71,6 +77,8 @@ class RouteAlternativesControllerTest { } private val tripSession: TripSession = mockk(relaxed = true) + private val directionSession: DirectionsSession = mockk(relaxed = true) + private fun createRouteAlternativesController( options: RouteAlternativesOptions = RouteAlternativesOptions.Builder().build(), routeParsingManager: RouteParsingManager = createParsingManagerForTest(), @@ -80,11 +88,16 @@ class RouteAlternativesControllerTest { tripSession, ThreadController(), routeParsingManager, + directionSession, ) @Before fun setup() { mockkObject(ThreadController) + + mockkStatic(DirectionsSession::findRoute) + every { directionSession.findRoute(any()) } returns null + every { ThreadController.IODispatcher } returns coroutineRule.testDispatcher every { ThreadController.DefaultDispatcher } returns coroutineRule.testDispatcher } @@ -92,6 +105,7 @@ class RouteAlternativesControllerTest { @After fun tearDown() { unmockkObject(ThreadController) + unmockkStatic(DirectionsSession::findRoute) } @Test @@ -1488,6 +1502,126 @@ class RouteAlternativesControllerTest { verify(exactly = 1) { controllerInterface.removeObserver(any()) } } + @Test + fun `during alternatives parsing should look up for existing route`() = coroutineRule.runBlockingTest { + fun createRouteWithId( + newRouteId: String, + ) = createNativeAlternativeMock().apply { + val spyk = spyk(route) { + every { routeId } returns newRouteId + } + every { route } returns spyk + } + + val nativeRoute = createRouteWithId("route") + val firstAlternative = createRouteWithId("alternative_route_1") + val secondAlternative = createRouteWithId("alternative_route_2") + + every { directionSession.findRoute(any()) } returns null + + val routeAlternativesController = createRouteAlternativesController() + + val nativeObserver = slot() + every { controllerInterface.addObserver(capture(nativeObserver)) } just runs + + var ignore: UpdateRouteSuggestion? = null + routeAlternativesController.setRouteUpdateSuggestionListener { + ignore = it + } + + nativeObserver.captured.onRouteAlternativesUpdated( + nativeRoute.route, + listOf(firstAlternative, secondAlternative), + emptyList(), + ) + + listOf( + nativeRoute.route.routeId, + firstAlternative.route.routeId, + secondAlternative.route.routeId, + ).forEach { routeId -> + verify(exactly = 1) { + directionSession.findRoute(routeId) + } + } + } + + @Test + fun `during alternatives parsing if route already exist should not parse again`() = coroutineRule.runBlockingTest { + fun createRouteWithId( + newRouteId: String, + newResponseUUID: String, + ) = createNativeAlternativeMock().apply { + val spyk = spyk(route) { + every { routeId } returns newRouteId + every { responseUuid } returns newResponseUUID + } + every { route } returns spyk + } + + val nativeRoute = createRouteWithId("route", "uuid1").route + val firstAlternative = createRouteWithId("alternative_route_1", "uuid2") + val secondAlternative = createRouteWithId("alternative_route_2", "uuid2") + + val firstAlternativeRoute = firstAlternative.route + val secondAlternativeRoute = secondAlternative.route + + every { directionSession.findRoute(eq("route")) } returns null + every { directionSession.findRoute(eq("alternative_route_1")) } returns null + every { + directionSession.findRoute(eq("alternative_route_2")) + } returns mockk( + relaxed = true, + ) + + val routeParsingMock = spyk(createParsingManagerForTest()) { + val fromJson = DirectionsResponse.fromJson( + FileUtils.loadJsonFixture("route_alternative_from_native.json"), + ) + every { parseRouteToDirections(any()) } returns fromJson + } + + val routeAlternativesController = createRouteAlternativesController( + routeParsingManager = routeParsingMock, + ) + + val nativeObserver = slot() + every { controllerInterface.addObserver(capture(nativeObserver)) } just runs + + var ignore: UpdateRouteSuggestion? = null + routeAlternativesController.setRouteUpdateSuggestionListener { + ignore = it + } + + nativeObserver.captured.onRouteAlternativesUpdated( + nativeRoute, + listOf(firstAlternative, secondAlternative), + emptyList(), + ) + + listOf( + nativeRoute.routeId, + firstAlternativeRoute.routeId, + secondAlternativeRoute.routeId, + ).forEach { routeId -> + verify(exactly = 1) { + directionSession.findRoute(routeId) + } + } + + verify(exactly = 1) { + routeParsingMock.parseRouteToDirections(refEq(nativeRoute)) + } + + verify(exactly = 1) { + routeParsingMock.parseRouteToDirections(refEq(firstAlternativeRoute)) + } + + verify(exactly = 0) { + routeParsingMock.parseRouteToDirections(refEq(secondAlternativeRoute)) + } + } + private val nativeInfoFork = com.mapbox.navigator.AlternativeRouteInfo( 100.0, // distance 200.0, // duration @@ -1541,12 +1675,13 @@ class RouteAlternativesControllerTest { val responseJson = FileUtils.loadJsonFixture(fileName) val response = DirectionsResponse.fromJson(responseJson) val nativeRoute = createRouteInterface( - responseJson = FileUtils.loadJsonFixture(fileName), + responseJson = responseJson, requestURI = routeRequestUrl.toString(), routerOrigin = routerOrigin, responseUUID = response.uuid()!!, routeIndex = routeIndex, ) + return mockk { every { route } returns nativeRoute every { isNew } returns true diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/ApproachesRouteOptionsUpdaterTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/ApproachesRouteOptionsUpdaterTest.kt new file mode 100644 index 00000000000..fa606bba7dd --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/ApproachesRouteOptionsUpdaterTest.kt @@ -0,0 +1,116 @@ +package com.mapbox.navigation.core.routeoptions + +import com.mapbox.api.directions.v5.DirectionsCriteria +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI +import com.mapbox.navigation.base.internal.extensions.indexOfNextRequestedCoordinate +import com.mapbox.navigation.base.internal.route.Waypoint +import com.mapbox.navigation.base.internal.utils.internalWaypoints +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.provideRouteOptionsWithCoordinates +import com.mapbox.navigation.core.trip.session.LocationMatcherResult +import com.mapbox.navigation.testing.LoggingFrontendTestRule +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@ExperimentalMapboxNavigationAPI +@RunWith(Parameterized::class) +class ApproachesRouteOptionsUpdaterTest( + private val description: String, + private val routeOptions: RouteOptions, + private val idxOfNextRequestedCoordinate: Int, + private val expectedApproachesList: List?, +) { + + @get:Rule + val loggerRule = LoggingFrontendTestRule() + + private lateinit var routeOptionsUpdater: RouteOptionsUpdater + + companion object { + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun params() = listOf( + arrayOf( + "empty approaches list correspond to null result list", + provideRouteOptionsWithCoordinates().toBuilder() + .approachesList(emptyList()) + .build(), + 2, + null, + ), + arrayOf( + "approaches list exist, index of next coordinate is 1, it has to become " + + "null in the result list", + provideRouteOptionsWithCoordinates().toBuilder() + .approachesList( + provideApproachesList( + null, + DirectionsCriteria.APPROACH_CURB, + DirectionsCriteria.APPROACH_UNRESTRICTED, + DirectionsCriteria.APPROACH_CURB, + ), + ) + .build(), + 1, + provideApproachesList( + null, + DirectionsCriteria.APPROACH_UNRESTRICTED, + DirectionsCriteria.APPROACH_CURB, + ), + ), + ) + + private fun provideApproachesList(vararg approaches: String?): List = + approaches.toList() + } + + @Before + fun setup() { + routeOptionsUpdater = RouteOptionsUpdater() + } + + @Test + fun testCases() { + mockkStatic(::indexOfNextRequestedCoordinate) { + val mockRemainingWaypoints = -1 + val mockWaypoints = listOf(mockk()) + val routeProgress: RouteProgress = mockk(relaxed = true) { + every { remainingWaypoints } returns mockRemainingWaypoints + every { navigationRoute.internalWaypoints() } returns mockWaypoints + } + every { + indexOfNextRequestedCoordinate(mockWaypoints, mockRemainingWaypoints) + } returns idxOfNextRequestedCoordinate + + val updatedRouteOptions = routeOptionsUpdater.update( + routeOptions, + routeProgress, + mockLocationMatcher(), + ).let { + assertTrue(it is RouteOptionsUpdater.RouteOptionsResult.Success) + it as RouteOptionsUpdater.RouteOptionsResult.Success + }.routeOptions + + assertEquals(expectedApproachesList, updatedRouteOptions.approachesList()) + } + } + + private fun mockLocationMatcher(): LocationMatcherResult = mockk { + every { enhancedLocation } returns mockk { + every { latitude } returns 1.1 + every { longitude } returns 2.2 + every { bearing } returns 3.3 + every { zLevel } returns 4 + } + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/BearingRouteOptionsUpdaterTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/BearingRouteOptionsUpdaterTest.kt new file mode 100644 index 00000000000..70657b72b96 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/BearingRouteOptionsUpdaterTest.kt @@ -0,0 +1,173 @@ +package com.mapbox.navigation.core.routeoptions + +import com.mapbox.api.directions.v5.models.Bearing +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.common.location.Location +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI +import com.mapbox.navigation.base.internal.extensions.indexOfNextRequestedCoordinate +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.DEFAULT_REROUTE_BEARING_ANGLE +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.DEFAULT_REROUTE_BEARING_TOLERANCE +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.DEFAULT_Z_LEVEL +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.provideRouteOptionsWithCoordinates +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.provideRouteOptionsWithCoordinatesAndBearings +import com.mapbox.navigation.core.trip.session.LocationMatcherResult +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@ExperimentalMapboxNavigationAPI +@RunWith(Parameterized::class) +class BearingRouteOptionsUpdaterTest( + val routeOptions: RouteOptions, + val indexNextCoordinate: Int, + val expectedBearings: List, +) { + + private lateinit var routeRefreshAdapter: RouteOptionsUpdater + private lateinit var locationMatcherResult: LocationMatcherResult + + companion object { + + @JvmStatic + @Parameterized.Parameters + fun params() = listOf( + arrayOf( + provideRouteOptionsWithCoordinatesAndBearings(), + 1, + listOf( + Bearing.builder() + .angle(DEFAULT_REROUTE_BEARING_ANGLE) + .degrees(10.0) + .build(), + Bearing.builder() + .angle(20.0) + .degrees(20.0) + .build(), + Bearing.builder() + .angle(30.0) + .degrees(30.0) + .build(), + Bearing.builder() + .angle(40.0) + .degrees(40.0) + .build(), + ), + ), + arrayOf( + provideRouteOptionsWithCoordinates(), + 3, + listOf( + Bearing.builder() + .angle(DEFAULT_REROUTE_BEARING_ANGLE) + .degrees(DEFAULT_REROUTE_BEARING_TOLERANCE) + .build(), + null, + ), + ), + arrayOf( + provideRouteOptionsWithCoordinates().toBuilder() + .bearingsList( + listOf( + Bearing.builder() + .angle(1.0) + .degrees(2.0) + .build(), + Bearing.builder() + .angle(3.0) + .degrees(4.0) + .build(), + null, + null, + ), + ) + .build(), + 2, + listOf( + Bearing.builder() + .angle(DEFAULT_REROUTE_BEARING_ANGLE.toDouble()) + .degrees(2.0) + .build(), + null, + null, + ), + ), + arrayOf( + provideRouteOptionsWithCoordinates().toBuilder() + .bearingsList( + listOf( + Bearing.builder().angle(1.0).degrees(2.0).build(), + Bearing.builder().angle(3.0).degrees(4.0).build(), + Bearing.builder().angle(5.0).degrees(6.0).build(), + Bearing.builder().angle(7.0).degrees(8.0).build(), + ), + ) + .build(), + 1, + listOf( + Bearing.builder() + .angle(DEFAULT_REROUTE_BEARING_ANGLE.toDouble()) + .degrees(2.0) + .build(), + Bearing.builder().angle(3.0).degrees(4.0).build(), + Bearing.builder().angle(5.0).degrees(6.0).build(), + Bearing.builder().angle(7.0).degrees(8.0).build(), + ), + ), + ) + } + + @Before + fun setup() { + MockKAnnotations.init(this, relaxUnitFun = true, relaxed = true) + mockLocation() + mockkStatic(::indexOfNextRequestedCoordinate) + + routeRefreshAdapter = RouteOptionsUpdater() + } + + @After + fun cleanup() { + unmockkStatic(::indexOfNextRequestedCoordinate) + } + + @Test + fun bearingOptions() { + val routeProgress: RouteProgress = mockk(relaxed = true) { + every { indexOfNextRequestedCoordinate(any(), any()) } returns indexNextCoordinate + } + + val newRouteOptions = + routeRefreshAdapter.update(routeOptions, routeProgress, locationMatcherResult) + .let { + assertTrue(it is RouteOptionsUpdater.RouteOptionsResult.Success) + return@let it as RouteOptionsUpdater.RouteOptionsResult.Success + } + .routeOptions + + val actualBearings = newRouteOptions.bearingsList() + + assertEquals(expectedBearings, actualBearings) + MapboxRouteOptionsUpdateCommonTest.checkImmutableFields(routeOptions, newRouteOptions) + } + + private fun mockLocation() { + val location = mockk(relaxUnitFun = true) + every { location.longitude } returns -122.4232 + every { location.latitude } returns 23.54423 + every { location.bearing } returns DEFAULT_REROUTE_BEARING_ANGLE + locationMatcherResult = mockk { + every { enhancedLocation } returns location + every { zLevel } returns DEFAULT_Z_LEVEL + } + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdaterTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdaterTest.kt index fa31f255452..12afbcb7fe8 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdaterTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdaterTest.kt @@ -1,20 +1,15 @@ package com.mapbox.navigation.core.routeoptions -import com.google.gson.JsonElement -import com.google.gson.JsonPrimitive import com.jparams.verifier.tostring.ToStringVerifier import com.mapbox.api.directions.v5.DirectionsCriteria import com.mapbox.api.directions.v5.models.Bearing import com.mapbox.api.directions.v5.models.DirectionsWaypoint import com.mapbox.api.directions.v5.models.RouteOptions import com.mapbox.common.location.Location -import com.mapbox.core.constants.Constants import com.mapbox.geojson.Point import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI import com.mapbox.navigation.base.internal.extensions.indexOfNextRequestedCoordinate -import com.mapbox.navigation.base.internal.route.Waypoint -import com.mapbox.navigation.base.internal.utils.WaypointFactory import com.mapbox.navigation.base.internal.utils.internalWaypoints import com.mapbox.navigation.base.options.NavigateToFinalDestination import com.mapbox.navigation.base.options.RerouteDisabled @@ -23,25 +18,31 @@ import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.core.mapmatching.MapMatchingExtras import com.mapbox.navigation.core.mapmatching.MapMatchingOptions import com.mapbox.navigation.core.reroute.PreRouterFailure +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.COORDINATE_1 +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.COORDINATE_2 +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.COORDINATE_3 +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.COORDINATE_4 +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.DEFAULT_REROUTE_BEARING_ANGLE +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.DEFAULT_Z_LEVEL +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.provideDefaultWaypointsList +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.provideRouteOptionsWithCoordinates +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.provideRouteOptionsWithCoordinatesAndArriveByDepartAt +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.provideRouteOptionsWithCoordinatesAndBearings +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.provideRouteOptionsWithCoordinatesAndLayers import com.mapbox.navigation.core.trip.session.LocationMatcherResult import com.mapbox.navigation.testing.LoggingFrontendTestRule -import com.mapbox.navigation.testing.factories.createLocation import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic -import io.mockk.unmockkStatic import io.mockk.verify import nl.jqno.equalsverifier.EqualsVerifier -import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized import java.net.URL @ExperimentalMapboxNavigationAPI @@ -54,118 +55,6 @@ class RouteOptionsUpdaterTest { private lateinit var locationMatcherResult: LocationMatcherResult private lateinit var location: Location - companion object { - - private const val DEFAULT_REROUTE_BEARING_ANGLE = 11.0 - private const val DEFAULT_REROUTE_BEARING_TOLERANCE = 90.0 - private const val DEFAULT_Z_LEVEL = 3 - - private val COORDINATE_1 = Point.fromLngLat(1.0, 1.0) - private val COORDINATE_2 = Point.fromLngLat(2.0, 2.0) - private val COORDINATE_3 = Point.fromLngLat(3.0, 3.0) - private val COORDINATE_4 = Point.fromLngLat(4.0, 4.0) - - private fun provideRouteOptionsWithCoordinates() = - provideDefaultRouteOptionsBuilder() - .coordinatesList(listOf(COORDINATE_1, COORDINATE_2, COORDINATE_3, COORDINATE_4)) - .build() - - private fun provideRouteOptionsWithCoordinatesAndBearings() = - provideRouteOptionsWithCoordinates() - .toBuilder() - .bearingsList( - listOf( - Bearing.builder().angle(10.0).degrees(10.0).build(), - Bearing.builder().angle(20.0).degrees(20.0).build(), - Bearing.builder().angle(30.0).degrees(30.0).build(), - Bearing.builder().angle(40.0).degrees(40.0).build(), - ), - ) - .build() - - private fun provideRouteOptionsWithCoordinatesAndArriveByDepartAt() = - provideRouteOptionsWithCoordinates() - .toBuilder() - .arriveBy("2021-01-01'T'01:01") - .departAt("2021-02-02'T'02:02") - .build() - - private fun provideRouteOptionsWithCoordinatesAndLayers() = - provideRouteOptionsWithCoordinates() - .toBuilder() - .layersList(listOf(0, 1, 2, 4)) - .build() - - private fun provideDefaultRouteOptionsBuilder() = - RouteOptions.builder() - .baseUrl(Constants.BASE_API_URL) - .user(Constants.MAPBOX_USER) - .profile(DirectionsCriteria.PROFILE_DRIVING_TRAFFIC) - .overview(DirectionsCriteria.OVERVIEW_FULL) - .annotationsList( - listOf( - DirectionsCriteria.ANNOTATION_SPEED, - DirectionsCriteria.ANNOTATION_CONGESTION_NUMERIC, - ), - ) - .coordinatesList(emptyList()) - .geometries(DirectionsCriteria.GEOMETRY_POLYLINE6) - .alternatives(true) - .steps(true) - .bannerInstructions(true) - .continueStraight(true) - .exclude(DirectionsCriteria.EXCLUDE_TOLL) - .language("en") - .roundaboutExits(true) - .voiceInstructions(true) - - private fun provideDefaultWaypointsList(): List = - listOf( - WaypointFactory.provideWaypoint( - Point.fromLngLat(1.0, 1.0), - "", - null, - Waypoint.REGULAR, - emptyMap(), - ), - WaypointFactory.provideWaypoint( - Point.fromLngLat(2.0, 2.0), - "", - null, - Waypoint.REGULAR, - emptyMap(), - ), - WaypointFactory.provideWaypoint( - Point.fromLngLat(3.0, 3.0), - "", - null, - Waypoint.REGULAR, - emptyMap(), - ), - WaypointFactory.provideWaypoint( - Point.fromLngLat(4.0, 4.0), - "", - null, - Waypoint.REGULAR, - emptyMap(), - ), - ) - - private fun Any?.isNullToString(): String = if (this == null) "Null" else "NonNull" - - private fun mockWaypoint( - location: Point, - @Waypoint.Type type: Int, - name: String, - target: Point?, - ): Waypoint = mockk { - every { this@mockk.location } returns location - every { this@mockk.type } returns type - every { this@mockk.name } returns name - every { this@mockk.target } returns target - } - } - @Before fun setup() { MockKAnnotations.init(this, relaxUnitFun = true, relaxed = true) @@ -700,6 +589,17 @@ class RouteOptionsUpdaterTest { MapboxRouteOptionsUpdateCommonTest.checkImmutableFields(routeOptions, newRouteOptions) } + @Test + fun testGeneratedEqualsHashcodeToStringFunctions() { + listOf( + RouteOptionsUpdater.RouteOptionsResult.Success::class.java, + RouteOptionsUpdater.RouteOptionsResult.Error::class.java, + ).forEach { + EqualsVerifier.forClass(it).verify() + ToStringVerifier.forClass(it).verify() + } + } + private fun mockLocation() { location = mockk(relaxUnitFun = true) every { location.longitude } returns -122.4232 @@ -711,668 +611,7 @@ class RouteOptionsUpdaterTest { } } - @RunWith(Parameterized::class) - class BearingOptionsParameterized( - val routeOptions: RouteOptions, - val indexNextCoordinate: Int, - val expectedBearings: List, - ) { - - private lateinit var routeRefreshAdapter: RouteOptionsUpdater - private lateinit var locationMatcherResult: LocationMatcherResult - - companion object { - - @JvmStatic - @Parameterized.Parameters - fun params() = listOf( - arrayOf( - provideRouteOptionsWithCoordinatesAndBearings(), - 1, - listOf( - Bearing.builder() - .angle(DEFAULT_REROUTE_BEARING_ANGLE.toDouble()) - .degrees(10.0) - .build(), - Bearing.builder() - .angle(20.0) - .degrees(20.0) - .build(), - Bearing.builder() - .angle(30.0) - .degrees(30.0) - .build(), - Bearing.builder() - .angle(40.0) - .degrees(40.0) - .build(), - ), - ), - arrayOf( - provideRouteOptionsWithCoordinates(), - 3, - listOf( - Bearing.builder() - .angle(DEFAULT_REROUTE_BEARING_ANGLE.toDouble()) - .degrees(DEFAULT_REROUTE_BEARING_TOLERANCE) - .build(), - null, - ), - ), - arrayOf( - provideRouteOptionsWithCoordinates().toBuilder() - .bearingsList( - listOf( - Bearing.builder() - .angle(1.0) - .degrees(2.0) - .build(), - Bearing.builder() - .angle(3.0) - .degrees(4.0) - .build(), - null, - null, - ), - ) - .build(), - 2, - listOf( - Bearing.builder() - .angle(DEFAULT_REROUTE_BEARING_ANGLE.toDouble()) - .degrees(2.0) - .build(), - null, - null, - ), - ), - arrayOf( - provideRouteOptionsWithCoordinates().toBuilder() - .bearingsList( - listOf( - Bearing.builder().angle(1.0).degrees(2.0).build(), - Bearing.builder().angle(3.0).degrees(4.0).build(), - Bearing.builder().angle(5.0).degrees(6.0).build(), - Bearing.builder().angle(7.0).degrees(8.0).build(), - ), - ) - .build(), - 1, - listOf( - Bearing.builder() - .angle(DEFAULT_REROUTE_BEARING_ANGLE.toDouble()) - .degrees(2.0) - .build(), - Bearing.builder().angle(3.0).degrees(4.0).build(), - Bearing.builder().angle(5.0).degrees(6.0).build(), - Bearing.builder().angle(7.0).degrees(8.0).build(), - ), - ), - ) - } - - @Before - fun setup() { - MockKAnnotations.init(this, relaxUnitFun = true, relaxed = true) - mockLocation() - mockkStatic(::indexOfNextRequestedCoordinate) - - routeRefreshAdapter = RouteOptionsUpdater() - } - - @After - fun cleanup() { - unmockkStatic(::indexOfNextRequestedCoordinate) - } - - @Test - fun bearingOptions() { - val routeProgress: RouteProgress = mockk(relaxed = true) { - every { indexOfNextRequestedCoordinate(any(), any()) } returns indexNextCoordinate - } - - val newRouteOptions = - routeRefreshAdapter.update(routeOptions, routeProgress, locationMatcherResult) - .let { - assertTrue(it is RouteOptionsUpdater.RouteOptionsResult.Success) - return@let it as RouteOptionsUpdater.RouteOptionsResult.Success - } - .routeOptions - - val actualBearings = newRouteOptions.bearingsList() - - assertEquals(expectedBearings, actualBearings) - MapboxRouteOptionsUpdateCommonTest.checkImmutableFields(routeOptions, newRouteOptions) - } - - private fun mockLocation() { - val location = mockk(relaxUnitFun = true) - every { location.longitude } returns -122.4232 - every { location.latitude } returns 23.54423 - every { location.bearing } returns DEFAULT_REROUTE_BEARING_ANGLE - locationMatcherResult = mockk { - every { enhancedLocation } returns location - every { zLevel } returns DEFAULT_Z_LEVEL - } - } - } - - @RunWith(Parameterized::class) - class SnappingClosuresOptionsParameterized( - val routeOptions: RouteOptions, - val remainingWaypointsParameter: Int, - val legIndex: Int, - val expectedSnappingClosures: String?, - ) { - - private lateinit var routeRefreshAdapter: RouteOptionsUpdater - private lateinit var locationMatcherResult: LocationMatcherResult - - companion object { - - @JvmStatic - @Parameterized.Parameters(name = "{0}") - fun params() = listOf( - arrayOf( - provideRouteOptionsWithCoordinates().toBuilder() - .snappingIncludeClosuresList( - listOf( - true, - false, - true, - false, - ), - ) - .build(), - 3, - 0, - "true;false;true;false", - ), - arrayOf( - provideRouteOptionsWithCoordinates(), - 1, - 2, - "true;", - ), - arrayOf( - provideRouteOptionsWithCoordinates().toBuilder() - .snappingIncludeClosuresList( - listOf( - true, - false, - false, - false, - ), - ) - .build(), - 2, - 1, - "true;false;false", - ), - arrayOf( - provideRouteOptionsWithCoordinates().toBuilder() - .snappingIncludeClosuresList( - listOf( - true, - false, - true, - false, - ), - ) - .build(), - 1, - 2, - "true;false", - ), - arrayOf( - provideRouteOptionsWithCoordinates().toBuilder() - .snappingIncludeClosures(null) - .profile(DirectionsCriteria.PROFILE_CYCLING) - .build(), - 1, - 2, - null, - ), - ) - } - - @Before - fun setup() { - mockLocation() - - routeRefreshAdapter = RouteOptionsUpdater() - } - - @Test - fun snappingClosuresOptions() { - val routeProgress: RouteProgress = mockk(relaxed = true) { - every { remainingWaypoints } returns remainingWaypointsParameter - every { currentLegProgress?.legIndex } returns legIndex - every { navigationRoute.internalWaypoints() } returns provideDefaultWaypointsList() - } - - val newRouteOptions = - routeRefreshAdapter.update(routeOptions, routeProgress, locationMatcherResult) - .let { - assertTrue(it is RouteOptionsUpdater.RouteOptionsResult.Success) - return@let it as RouteOptionsUpdater.RouteOptionsResult.Success - } - .routeOptions - - val actualSnappingClosures = newRouteOptions.snappingIncludeClosures() - - assertEquals(expectedSnappingClosures, actualSnappingClosures) - MapboxRouteOptionsUpdateCommonTest.checkImmutableFields(routeOptions, newRouteOptions) - } - - private fun mockLocation() { - val location = mockk(relaxUnitFun = true) - every { location.longitude } returns -122.4232 - every { location.latitude } returns 23.54423 - every { location.bearing } returns DEFAULT_REROUTE_BEARING_ANGLE - locationMatcherResult = mockk { - every { enhancedLocation } returns location - every { zLevel } returns DEFAULT_Z_LEVEL - } - } - } - - @RunWith(Parameterized::class) - class SnappingStaticClosuresOptionsParameterized( - val routeOptions: RouteOptions, - val remainingWaypointsParameter: Int, - val legIndex: Int, - val expectedSnappingStaticClosures: String?, - ) { - - @get:Rule - val loggerRule = LoggingFrontendTestRule() - - private lateinit var routeRefreshAdapter: RouteOptionsUpdater - private lateinit var locationMatcherResult: LocationMatcherResult - - companion object { - - @JvmStatic - @Parameterized.Parameters - fun params() = listOf( - arrayOf( - provideRouteOptionsWithCoordinates() - .toBuilder() - .snappingIncludeStaticClosuresList( - listOf( - true, - false, - true, - false, - ), - ) - .build(), - 3, - 0, - "true;false;true;false", - ), - arrayOf( - provideRouteOptionsWithCoordinates(), - 1, - 2, - "true;", - ), - arrayOf( - provideRouteOptionsWithCoordinates() - .toBuilder() - .snappingIncludeStaticClosuresList( - listOf( - true, - false, - false, - false, - ), - ) - .build(), - 2, - 1, - "true;false;false", - ), - arrayOf( - provideRouteOptionsWithCoordinates().toBuilder() - .snappingIncludeStaticClosuresList( - listOf( - true, - false, - true, - false, - ), - ) - .build(), - 1, - 2, - "true;false", - ), - arrayOf( - provideRouteOptionsWithCoordinates().toBuilder() - .snappingIncludeStaticClosuresList(null) - .profile(DirectionsCriteria.PROFILE_CYCLING) - .build(), - 1, - 2, - null, - ), - ) - } - - @Before - fun setup() { - MockKAnnotations.init(this, relaxUnitFun = true, relaxed = true) - mockLocation() - - routeRefreshAdapter = RouteOptionsUpdater() - } - - @Test - fun snappingClosuresOptions() { - val routeProgress: RouteProgress = mockk(relaxed = true) { - every { remainingWaypoints } returns remainingWaypointsParameter - every { currentLegProgress?.legIndex } returns legIndex - every { navigationRoute.internalWaypoints() } returns provideDefaultWaypointsList() - } - - val newRouteOptions = - routeRefreshAdapter.update(routeOptions, routeProgress, locationMatcherResult) - .let { - assertTrue(it is RouteOptionsUpdater.RouteOptionsResult.Success) - return@let it as RouteOptionsUpdater.RouteOptionsResult.Success - } - .routeOptions - - val actualSnappingStaticClosures = newRouteOptions.snappingIncludeStaticClosures() - - assertEquals(expectedSnappingStaticClosures, actualSnappingStaticClosures) - MapboxRouteOptionsUpdateCommonTest.checkImmutableFields(routeOptions, newRouteOptions) - } - - private fun mockLocation() { - val location = createLocation( - -122.4232, - 23.54423, - DEFAULT_REROUTE_BEARING_ANGLE, - ) - locationMatcherResult = mockk { - every { enhancedLocation } returns location - every { zLevel } returns DEFAULT_Z_LEVEL - } - } - } - - @RunWith(Parameterized::class) - class ApproachesTestParameterized( - private val description: String, - private val routeOptions: RouteOptions, - private val idxOfNextRequestedCoordinate: Int, - private val expectedApproachesList: List?, - ) { - - @get:Rule - val loggerRule = LoggingFrontendTestRule() - - private lateinit var routeOptionsUpdater: RouteOptionsUpdater - - companion object { - - @JvmStatic - @Parameterized.Parameters(name = "{0}") - fun params() = listOf( - arrayOf( - "empty approaches list correspond to null result list", - provideRouteOptionsWithCoordinates().toBuilder() - .approachesList(emptyList()) - .build(), - 2, - null, - ), - arrayOf( - "approaches list exist, index of next coordinate is 1, it has to become " + - "null in the result list", - provideRouteOptionsWithCoordinates().toBuilder() - .approachesList( - provideApproachesList( - null, - DirectionsCriteria.APPROACH_CURB, - DirectionsCriteria.APPROACH_UNRESTRICTED, - DirectionsCriteria.APPROACH_CURB, - ), - ) - .build(), - 1, - provideApproachesList( - null, - DirectionsCriteria.APPROACH_UNRESTRICTED, - DirectionsCriteria.APPROACH_CURB, - ), - ), - ) - - private fun provideApproachesList(vararg approaches: String?): List = - approaches.toList() - } - - @Before - fun setup() { - routeOptionsUpdater = RouteOptionsUpdater() - } - - @Test - fun testCases() { - mockkStatic(::indexOfNextRequestedCoordinate) { - val mockRemainingWaypoints = -1 - val mockWaypoints = listOf(mockk()) - val routeProgress: RouteProgress = mockk(relaxed = true) { - every { remainingWaypoints } returns mockRemainingWaypoints - every { navigationRoute.internalWaypoints() } returns mockWaypoints - } - every { - indexOfNextRequestedCoordinate(mockWaypoints, mockRemainingWaypoints) - } returns idxOfNextRequestedCoordinate - - val updatedRouteOptions = routeOptionsUpdater.update( - routeOptions, - routeProgress, - mockLocationMatcher(), - ).let { - assertTrue(it is RouteOptionsUpdater.RouteOptionsResult.Success) - it as RouteOptionsUpdater.RouteOptionsResult.Success - }.routeOptions - - assertEquals(expectedApproachesList, updatedRouteOptions.approachesList()) - } - } - - private fun mockLocationMatcher(): LocationMatcherResult = mockk { - every { enhancedLocation } returns mockk { - every { latitude } returns 1.1 - every { longitude } returns 2.2 - every { bearing } returns 3.3 - every { zLevel } returns 4 - } - } - } - - @RunWith(Parameterized::class) - class UnrecognizedJsonPropertiesTest( - private val description: String, - private val routeOptions: RouteOptions, - private val idxOfNextRequestedCoordinate: Int, - private val expected: Map?, - ) { - - @get:Rule - val loggerRule = LoggingFrontendTestRule() - - private lateinit var routeOptionsUpdater: RouteOptionsUpdater - - companion object { - - @JvmStatic - @Parameterized.Parameters(name = "{0}") - fun params() = listOf( - arrayOf( - "null unrecognized properties to null", - provideRouteOptionsWithCoordinates().toBuilder() - .unrecognizedJsonProperties(null) - .build(), - 2, - null, - ), - arrayOf( - "empty unrecognized properties to empty", - provideRouteOptionsWithCoordinates().toBuilder() - .unrecognizedJsonProperties(emptyMap()) - .build(), - 2, - emptyMap(), - ), - arrayOf( - "ev data present for non ev route should not be changed", - provideRouteOptionsWithCoordinates().toBuilder() - .unrecognizedJsonProperties( - mapOf( - "aaa" to JsonPrimitive("bbb"), - "waypoints.charging_station_id" to JsonPrimitive(";;2;3"), - "waypoints.charging_station_power" to JsonPrimitive(";;2000;3000"), - "waypoints.charging_station_current_type" to - JsonPrimitive(";;ac;dc"), - ), - ) - .build(), - 2, - mapOf( - "aaa" to JsonPrimitive("bbb"), - "waypoints.charging_station_id" to JsonPrimitive(";;2;3"), - "waypoints.charging_station_power" to JsonPrimitive(";;2000;3000"), - "waypoints.charging_station_current_type" to JsonPrimitive(";;ac;dc"), - ), - ), - arrayOf( - "ev data present for ev route should be changed", - provideRouteOptionsWithCoordinates().toBuilder() - .unrecognizedJsonProperties( - mapOf( - "engine" to JsonPrimitive("electric"), - "aaa" to JsonPrimitive("bbb"), - "waypoints.charging_station_id" to JsonPrimitive(";2;;3"), - "waypoints.charging_station_power" to JsonPrimitive(";2000;;3000"), - "waypoints.charging_station_current_type" to - JsonPrimitive(";ac;;dc"), - ), - ) - .build(), - 2, - mapOf( - "engine" to JsonPrimitive("electric"), - "aaa" to JsonPrimitive("bbb"), - "waypoints.charging_station_id" to JsonPrimitive(";;3"), - "waypoints.charging_station_power" to JsonPrimitive(";;3000"), - "waypoints.charging_station_current_type" to JsonPrimitive(";;dc"), - ), - ), - arrayOf( - "ev data present for ev route should not be changed for the first coordinate", - provideRouteOptionsWithCoordinates().toBuilder() - .unrecognizedJsonProperties( - mapOf( - "engine" to JsonPrimitive("electric"), - "aaa" to JsonPrimitive("bbb"), - "waypoints.charging_station_id" to JsonPrimitive(";;2;3"), - "waypoints.charging_station_power" to JsonPrimitive(";;2000;3000"), - "waypoints.charging_station_current_type" to - JsonPrimitive(";;ac;dc"), - ), - ) - .build(), - 1, - mapOf( - "engine" to JsonPrimitive("electric"), - "aaa" to JsonPrimitive("bbb"), - "waypoints.charging_station_id" to JsonPrimitive(";;2;3"), - "waypoints.charging_station_power" to JsonPrimitive(";;2000;3000"), - "waypoints.charging_station_current_type" to JsonPrimitive(";;ac;dc"), - ), - ), - arrayOf( - "ev data present for ev route should be changed for the last coordinate", - provideRouteOptionsWithCoordinates().toBuilder() - .unrecognizedJsonProperties( - mapOf( - "engine" to JsonPrimitive("electric"), - "aaa" to JsonPrimitive("bbb"), - "waypoints.charging_station_id" to JsonPrimitive(";2;;3"), - "waypoints.charging_station_power" to JsonPrimitive(";2000;;3000"), - "waypoints.charging_station_current_type" to - JsonPrimitive(";ac;;dc"), - ), - ) - .build(), - 3, - mapOf( - "engine" to JsonPrimitive("electric"), - "aaa" to JsonPrimitive("bbb"), - "waypoints.charging_station_id" to JsonPrimitive(";3"), - "waypoints.charging_station_power" to JsonPrimitive(";3000"), - "waypoints.charging_station_current_type" to JsonPrimitive(";dc"), - ), - ), - ) - } - - @Before - fun setup() { - routeOptionsUpdater = RouteOptionsUpdater() - } - - @Test - fun unrecognizedJsonProperties() { - mockkStatic(::indexOfNextRequestedCoordinate) { - val mockRemainingWaypoints = -1 - val mockWaypoints = listOf(mockk()) - val routeProgress: RouteProgress = mockk(relaxed = true) { - every { remainingWaypoints } returns mockRemainingWaypoints - every { navigationRoute.internalWaypoints() } returns mockWaypoints - } - every { - indexOfNextRequestedCoordinate(mockWaypoints, mockRemainingWaypoints) - } returns idxOfNextRequestedCoordinate - - val updatedRouteOptions = routeOptionsUpdater.update( - routeOptions, - routeProgress, - mockLocationMatcher(), - ).let { - assertTrue(it is RouteOptionsUpdater.RouteOptionsResult.Success) - it as RouteOptionsUpdater.RouteOptionsResult.Success - }.routeOptions - - assertEquals(expected, updatedRouteOptions.unrecognizedJsonProperties) - } - } - - @Test - fun testGeneratedEqualsHashcodeToStringFunctions() { - listOf( - RouteOptionsUpdater.RouteOptionsResult.Success::class.java, - RouteOptionsUpdater.RouteOptionsResult.Error::class.java, - ).forEach { - EqualsVerifier.forClass(it).verify() - ToStringVerifier.forClass(it).verify() - } - } - - private fun mockLocationMatcher(): LocationMatcherResult = mockk { - every { enhancedLocation } returns mockk { - every { latitude } returns 1.1 - every { longitude } returns 2.2 - every { bearing } returns 3.3 - every { zLevel } returns 4 - } - } + private companion object { + fun Any?.isNullToString(): String = if (this == null) "Null" else "NonNull" } } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdaterTestUtils.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdaterTestUtils.kt new file mode 100644 index 00000000000..a93719eb490 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdaterTestUtils.kt @@ -0,0 +1,107 @@ +package com.mapbox.navigation.core.routeoptions + +import com.mapbox.api.directions.v5.DirectionsCriteria +import com.mapbox.api.directions.v5.models.Bearing +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.core.constants.Constants +import com.mapbox.geojson.Point +import com.mapbox.navigation.base.internal.route.Waypoint +import com.mapbox.navigation.base.internal.utils.WaypointFactory + +object RouteOptionsUpdaterTestUtils { + + const val DEFAULT_REROUTE_BEARING_ANGLE = 11.0 + const val DEFAULT_REROUTE_BEARING_TOLERANCE = 90.0 + const val DEFAULT_Z_LEVEL = 3 + + val COORDINATE_1: Point = Point.fromLngLat(1.0, 1.0) + val COORDINATE_2: Point = Point.fromLngLat(2.0, 2.0) + val COORDINATE_3: Point = Point.fromLngLat(3.0, 3.0) + val COORDINATE_4: Point = Point.fromLngLat(4.0, 4.0) + + fun provideRouteOptionsWithCoordinates() = + provideDefaultRouteOptionsBuilder() + .coordinatesList(listOf(COORDINATE_1, COORDINATE_2, COORDINATE_3, COORDINATE_4)) + .build() + + fun provideRouteOptionsWithCoordinatesAndBearings() = + provideRouteOptionsWithCoordinates() + .toBuilder() + .bearingsList( + listOf( + Bearing.builder().angle(10.0).degrees(10.0).build(), + Bearing.builder().angle(20.0).degrees(20.0).build(), + Bearing.builder().angle(30.0).degrees(30.0).build(), + Bearing.builder().angle(40.0).degrees(40.0).build(), + ), + ) + .build() + + fun provideRouteOptionsWithCoordinatesAndArriveByDepartAt() = + provideRouteOptionsWithCoordinates() + .toBuilder() + .arriveBy("2021-01-01'T'01:01") + .departAt("2021-02-02'T'02:02") + .build() + + fun provideRouteOptionsWithCoordinatesAndLayers() = + provideRouteOptionsWithCoordinates() + .toBuilder() + .layersList(listOf(0, 1, 2, 4)) + .build() + + private fun provideDefaultRouteOptionsBuilder() = + RouteOptions.builder() + .baseUrl(Constants.BASE_API_URL) + .user(Constants.MAPBOX_USER) + .profile(DirectionsCriteria.PROFILE_DRIVING_TRAFFIC) + .overview(DirectionsCriteria.OVERVIEW_FULL) + .annotationsList( + listOf( + DirectionsCriteria.ANNOTATION_SPEED, + DirectionsCriteria.ANNOTATION_CONGESTION_NUMERIC, + ), + ) + .coordinatesList(emptyList()) + .geometries(DirectionsCriteria.GEOMETRY_POLYLINE6) + .alternatives(true) + .steps(true) + .bannerInstructions(true) + .continueStraight(true) + .exclude(DirectionsCriteria.EXCLUDE_TOLL) + .language("en") + .roundaboutExits(true) + .voiceInstructions(true) + + fun provideDefaultWaypointsList(): List = + listOf( + WaypointFactory.provideWaypoint( + Point.fromLngLat(1.0, 1.0), + "", + null, + Waypoint.REGULAR, + emptyMap(), + ), + WaypointFactory.provideWaypoint( + Point.fromLngLat(2.0, 2.0), + "", + null, + Waypoint.REGULAR, + emptyMap(), + ), + WaypointFactory.provideWaypoint( + Point.fromLngLat(3.0, 3.0), + "", + null, + Waypoint.REGULAR, + emptyMap(), + ), + WaypointFactory.provideWaypoint( + Point.fromLngLat(4.0, 4.0), + "", + null, + Waypoint.REGULAR, + emptyMap(), + ), + ) +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/SnappingClosuresRouteOptionsUpdaterTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/SnappingClosuresRouteOptionsUpdaterTest.kt new file mode 100644 index 00000000000..69b28f557c0 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/SnappingClosuresRouteOptionsUpdaterTest.kt @@ -0,0 +1,141 @@ +package com.mapbox.navigation.core.routeoptions + +import com.mapbox.api.directions.v5.DirectionsCriteria +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.common.location.Location +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI +import com.mapbox.navigation.base.internal.utils.internalWaypoints +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.DEFAULT_REROUTE_BEARING_ANGLE +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.DEFAULT_Z_LEVEL +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.provideDefaultWaypointsList +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.provideRouteOptionsWithCoordinates +import com.mapbox.navigation.core.trip.session.LocationMatcherResult +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@ExperimentalMapboxNavigationAPI +@RunWith(Parameterized::class) +class SnappingClosuresRouteOptionsUpdaterTest( + val routeOptions: RouteOptions, + val remainingWaypointsParameter: Int, + val legIndex: Int, + val expectedSnappingClosures: String?, +) { + private lateinit var routeRefreshAdapter: RouteOptionsUpdater + private lateinit var locationMatcherResult: LocationMatcherResult + + companion object { + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun params() = listOf( + arrayOf( + provideRouteOptionsWithCoordinates().toBuilder() + .snappingIncludeClosuresList( + listOf( + true, + false, + true, + false, + ), + ) + .build(), + 3, + 0, + "true;false;true;false", + ), + arrayOf( + provideRouteOptionsWithCoordinates(), + 1, + 2, + "true;", + ), + arrayOf( + provideRouteOptionsWithCoordinates().toBuilder() + .snappingIncludeClosuresList( + listOf( + true, + false, + false, + false, + ), + ) + .build(), + 2, + 1, + "true;false;false", + ), + arrayOf( + provideRouteOptionsWithCoordinates().toBuilder() + .snappingIncludeClosuresList( + listOf( + true, + false, + true, + false, + ), + ) + .build(), + 1, + 2, + "true;false", + ), + arrayOf( + provideRouteOptionsWithCoordinates().toBuilder() + .snappingIncludeClosures(null) + .profile(DirectionsCriteria.PROFILE_CYCLING) + .build(), + 1, + 2, + null, + ), + ) + } + + @Before + fun setup() { + mockLocation() + + routeRefreshAdapter = RouteOptionsUpdater() + } + + @Test + fun snappingClosuresOptions() { + val routeProgress: RouteProgress = mockk(relaxed = true) { + every { remainingWaypoints } returns remainingWaypointsParameter + every { currentLegProgress?.legIndex } returns legIndex + every { navigationRoute.internalWaypoints() } returns provideDefaultWaypointsList() + } + + val newRouteOptions = + routeRefreshAdapter.update(routeOptions, routeProgress, locationMatcherResult) + .let { + assertTrue(it is RouteOptionsUpdater.RouteOptionsResult.Success) + return@let it as RouteOptionsUpdater.RouteOptionsResult.Success + } + .routeOptions + + val actualSnappingClosures = newRouteOptions.snappingIncludeClosures() + + assertEquals(expectedSnappingClosures, actualSnappingClosures) + MapboxRouteOptionsUpdateCommonTest.checkImmutableFields(routeOptions, newRouteOptions) + } + + private fun mockLocation() { + val location = mockk(relaxUnitFun = true) + every { location.longitude } returns -122.4232 + every { location.latitude } returns 23.54423 + every { location.bearing } returns DEFAULT_REROUTE_BEARING_ANGLE + locationMatcherResult = mockk { + every { enhancedLocation } returns location + every { zLevel } returns DEFAULT_Z_LEVEL + } + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/SnappingStaticClosuresRouteOptionsUpdaterTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/SnappingStaticClosuresRouteOptionsUpdaterTest.kt new file mode 100644 index 00000000000..cca9b787959 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/SnappingStaticClosuresRouteOptionsUpdaterTest.kt @@ -0,0 +1,151 @@ +package com.mapbox.navigation.core.routeoptions + +import com.mapbox.api.directions.v5.DirectionsCriteria +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI +import com.mapbox.navigation.base.internal.utils.internalWaypoints +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.DEFAULT_REROUTE_BEARING_ANGLE +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.DEFAULT_Z_LEVEL +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.provideDefaultWaypointsList +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.provideRouteOptionsWithCoordinates +import com.mapbox.navigation.core.trip.session.LocationMatcherResult +import com.mapbox.navigation.testing.LoggingFrontendTestRule +import com.mapbox.navigation.testing.factories.createLocation +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@ExperimentalMapboxNavigationAPI +@RunWith(Parameterized::class) +class SnappingStaticClosuresRouteOptionsUpdaterTest( + val routeOptions: RouteOptions, + val remainingWaypointsParameter: Int, + val legIndex: Int, + val expectedSnappingStaticClosures: String?, +) { + @get:Rule + val loggerRule = LoggingFrontendTestRule() + + private lateinit var routeRefreshAdapter: RouteOptionsUpdater + private lateinit var locationMatcherResult: LocationMatcherResult + + companion object { + + @JvmStatic + @Parameterized.Parameters + fun params() = listOf( + arrayOf( + provideRouteOptionsWithCoordinates() + .toBuilder() + .snappingIncludeStaticClosuresList( + listOf( + true, + false, + true, + false, + ), + ) + .build(), + 3, + 0, + "true;false;true;false", + ), + arrayOf( + provideRouteOptionsWithCoordinates(), + 1, + 2, + "true;", + ), + arrayOf( + provideRouteOptionsWithCoordinates() + .toBuilder() + .snappingIncludeStaticClosuresList( + listOf( + true, + false, + false, + false, + ), + ) + .build(), + 2, + 1, + "true;false;false", + ), + arrayOf( + provideRouteOptionsWithCoordinates().toBuilder() + .snappingIncludeStaticClosuresList( + listOf( + true, + false, + true, + false, + ), + ) + .build(), + 1, + 2, + "true;false", + ), + arrayOf( + provideRouteOptionsWithCoordinates().toBuilder() + .snappingIncludeStaticClosuresList(null) + .profile(DirectionsCriteria.PROFILE_CYCLING) + .build(), + 1, + 2, + null, + ), + ) + } + + @Before + fun setup() { + MockKAnnotations.init(this, relaxUnitFun = true, relaxed = true) + mockLocation() + + routeRefreshAdapter = RouteOptionsUpdater() + } + + @Test + fun snappingClosuresOptions() { + val routeProgress: RouteProgress = mockk(relaxed = true) { + every { remainingWaypoints } returns remainingWaypointsParameter + every { currentLegProgress?.legIndex } returns legIndex + every { navigationRoute.internalWaypoints() } returns provideDefaultWaypointsList() + } + + val newRouteOptions = + routeRefreshAdapter.update(routeOptions, routeProgress, locationMatcherResult) + .let { + assertTrue(it is RouteOptionsUpdater.RouteOptionsResult.Success) + return@let it as RouteOptionsUpdater.RouteOptionsResult.Success + } + .routeOptions + + val actualSnappingStaticClosures = newRouteOptions.snappingIncludeStaticClosures() + + assertEquals(expectedSnappingStaticClosures, actualSnappingStaticClosures) + MapboxRouteOptionsUpdateCommonTest.checkImmutableFields(routeOptions, newRouteOptions) + } + + private fun mockLocation() { + val location = createLocation( + -122.4232, + 23.54423, + DEFAULT_REROUTE_BEARING_ANGLE, + ) + locationMatcherResult = mockk { + every { enhancedLocation } returns location + every { zLevel } returns DEFAULT_Z_LEVEL + } + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/UnrecognizedRouteOptionsUpdaterTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/UnrecognizedRouteOptionsUpdaterTest.kt new file mode 100644 index 00000000000..d1b3a435cf5 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/UnrecognizedRouteOptionsUpdaterTest.kt @@ -0,0 +1,192 @@ +package com.mapbox.navigation.core.routeoptions + +import com.google.gson.JsonElement +import com.google.gson.JsonPrimitive +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI +import com.mapbox.navigation.base.internal.extensions.indexOfNextRequestedCoordinate +import com.mapbox.navigation.base.internal.route.Waypoint +import com.mapbox.navigation.base.internal.utils.internalWaypoints +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdaterTestUtils.provideRouteOptionsWithCoordinates +import com.mapbox.navigation.core.trip.session.LocationMatcherResult +import com.mapbox.navigation.testing.LoggingFrontendTestRule +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@ExperimentalMapboxNavigationAPI +@RunWith(Parameterized::class) +class UnrecognizedRouteOptionsUpdaterTest( + private val description: String, + private val routeOptions: RouteOptions, + private val idxOfNextRequestedCoordinate: Int, + private val expected: Map?, +) { + + @get:Rule + val loggerRule = LoggingFrontendTestRule() + + private lateinit var routeOptionsUpdater: RouteOptionsUpdater + + companion object { + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun params() = listOf( + arrayOf( + "null unrecognized properties to null", + provideRouteOptionsWithCoordinates().toBuilder() + .unrecognizedJsonProperties(null) + .build(), + 2, + null, + ), + arrayOf( + "empty unrecognized properties to empty", + provideRouteOptionsWithCoordinates().toBuilder() + .unrecognizedJsonProperties(emptyMap()) + .build(), + 2, + emptyMap(), + ), + arrayOf( + "ev data present for non ev route should not be changed", + provideRouteOptionsWithCoordinates().toBuilder() + .unrecognizedJsonProperties( + mapOf( + "aaa" to JsonPrimitive("bbb"), + "waypoints.charging_station_id" to JsonPrimitive(";;2;3"), + "waypoints.charging_station_power" to JsonPrimitive(";;2000;3000"), + "waypoints.charging_station_current_type" to + JsonPrimitive(";;ac;dc"), + ), + ) + .build(), + 2, + mapOf( + "aaa" to JsonPrimitive("bbb"), + "waypoints.charging_station_id" to JsonPrimitive(";;2;3"), + "waypoints.charging_station_power" to JsonPrimitive(";;2000;3000"), + "waypoints.charging_station_current_type" to JsonPrimitive(";;ac;dc"), + ), + ), + arrayOf( + "ev data present for ev route should be changed", + provideRouteOptionsWithCoordinates().toBuilder() + .unrecognizedJsonProperties( + mapOf( + "engine" to JsonPrimitive("electric"), + "aaa" to JsonPrimitive("bbb"), + "waypoints.charging_station_id" to JsonPrimitive(";2;;3"), + "waypoints.charging_station_power" to JsonPrimitive(";2000;;3000"), + "waypoints.charging_station_current_type" to + JsonPrimitive(";ac;;dc"), + ), + ) + .build(), + 2, + mapOf( + "engine" to JsonPrimitive("electric"), + "aaa" to JsonPrimitive("bbb"), + "waypoints.charging_station_id" to JsonPrimitive(";;3"), + "waypoints.charging_station_power" to JsonPrimitive(";;3000"), + "waypoints.charging_station_current_type" to JsonPrimitive(";;dc"), + ), + ), + arrayOf( + "ev data present for ev route should not be changed for the first coordinate", + provideRouteOptionsWithCoordinates().toBuilder() + .unrecognizedJsonProperties( + mapOf( + "engine" to JsonPrimitive("electric"), + "aaa" to JsonPrimitive("bbb"), + "waypoints.charging_station_id" to JsonPrimitive(";;2;3"), + "waypoints.charging_station_power" to JsonPrimitive(";;2000;3000"), + "waypoints.charging_station_current_type" to + JsonPrimitive(";;ac;dc"), + ), + ) + .build(), + 1, + mapOf( + "engine" to JsonPrimitive("electric"), + "aaa" to JsonPrimitive("bbb"), + "waypoints.charging_station_id" to JsonPrimitive(";;2;3"), + "waypoints.charging_station_power" to JsonPrimitive(";;2000;3000"), + "waypoints.charging_station_current_type" to JsonPrimitive(";;ac;dc"), + ), + ), + arrayOf( + "ev data present for ev route should be changed for the last coordinate", + provideRouteOptionsWithCoordinates().toBuilder() + .unrecognizedJsonProperties( + mapOf( + "engine" to JsonPrimitive("electric"), + "aaa" to JsonPrimitive("bbb"), + "waypoints.charging_station_id" to JsonPrimitive(";2;;3"), + "waypoints.charging_station_power" to JsonPrimitive(";2000;;3000"), + "waypoints.charging_station_current_type" to + JsonPrimitive(";ac;;dc"), + ), + ) + .build(), + 3, + mapOf( + "engine" to JsonPrimitive("electric"), + "aaa" to JsonPrimitive("bbb"), + "waypoints.charging_station_id" to JsonPrimitive(";3"), + "waypoints.charging_station_power" to JsonPrimitive(";3000"), + "waypoints.charging_station_current_type" to JsonPrimitive(";dc"), + ), + ), + ) + } + + @Before + fun setup() { + routeOptionsUpdater = RouteOptionsUpdater() + } + + @Test + fun unrecognizedJsonProperties() { + mockkStatic(::indexOfNextRequestedCoordinate) { + val mockRemainingWaypoints = -1 + val mockWaypoints = listOf(mockk()) + val routeProgress: RouteProgress = mockk(relaxed = true) { + every { remainingWaypoints } returns mockRemainingWaypoints + every { navigationRoute.internalWaypoints() } returns mockWaypoints + } + every { + indexOfNextRequestedCoordinate(mockWaypoints, mockRemainingWaypoints) + } returns idxOfNextRequestedCoordinate + + val updatedRouteOptions = routeOptionsUpdater.update( + routeOptions, + routeProgress, + mockLocationMatcher(), + ).let { + assertTrue(it is RouteOptionsUpdater.RouteOptionsResult.Success) + it as RouteOptionsUpdater.RouteOptionsResult.Success + }.routeOptions + + assertEquals(expected, updatedRouteOptions.unrecognizedJsonProperties) + } + } + + private fun mockLocationMatcher(): LocationMatcherResult = mockk { + every { enhancedLocation } returns mockk { + every { latitude } returns 1.1 + every { longitude } returns 2.2 + every { bearing } returns 3.3 + every { zLevel } returns 4 + } + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemoverTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemoverTest.kt index af617699f26..0f5ae335e70 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemoverTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemoverTest.kt @@ -1,6 +1,9 @@ +@file:OptIn(ExperimentalMapboxNavigationAPI::class) + package com.mapbox.navigation.core.routerefresh import com.mapbox.api.directions.v5.models.LegAnnotation +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI import com.mapbox.navigation.base.internal.time.parseISO8601DateToLocalTimeOrNull import com.mapbox.navigation.core.internal.RouteProgressData import com.mapbox.navigation.testing.factories.createDirectionsRoute @@ -205,5 +208,15 @@ class ExpiringDataRemoverTest { val actual = sut.removeExpiringDataFromRoutesProgressData(input) assertEquals(expected, actual) + assertEquals( + false, + actual.primaryRouteRefresherResult.route.routeRefreshMetadata?.isUpToDate, + ) + assertEquals( + listOf(false, false), + actual.alternativesRouteRefresherResults.map { + it.route.routeRefreshMetadata?.isUpToDate + }, + ) } } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/telemetry/UserFeedbackTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/telemetry/UserFeedbackTest.kt new file mode 100644 index 00000000000..7106827e6bc --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/telemetry/UserFeedbackTest.kt @@ -0,0 +1,32 @@ +package com.mapbox.navigation.core.telemetry + +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import com.mapbox.navigation.core.telemetry.UserFeedback.Companion.mapToNative +import com.mapbox.navigation.core.telemetry.events.FeedbackEvent +import com.mapbox.navigator.ScreenshotFormat +import org.junit.Assert.assertEquals +import org.junit.Test + +@OptIn(ExperimentalPreviewMapboxNavigationAPI::class) +class UserFeedbackTest { + + @Test + fun `test mapToNative()`() { + val userFeedback = UserFeedback.Builder( + feedbackType = FeedbackEvent.ARRIVAL_FEEDBACK_GOOD, + description = "test-description", + ) + .feedbackSubTypes(listOf(FeedbackEvent.MANEUVER_INCORRECT)) + .screenshot("test-base-64") + .build() + + val nativeUserFeedback = com.mapbox.navigator.UserFeedback( + FeedbackEvent.ARRIVAL_FEEDBACK_GOOD, + listOf(FeedbackEvent.MANEUVER_INCORRECT), + "test-description", + ScreenshotFormat(null, "test-base-64"), + ) + + assertEquals(nativeUserFeedback, userFeedback.mapToNative()) + } +} diff --git a/libnavigation-metrics/gradle.properties b/libnavigation-metrics/gradle.properties index c2468ece0c3..abc7e13424f 100644 --- a/libnavigation-metrics/gradle.properties +++ b/libnavigation-metrics/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=metrics POM_ARTIFACT_TITLE=Mapbox Navigation Metrics POM_DESCRIPTION=Artifact that provides the default implementation of the metrics integration \ No newline at end of file diff --git a/libnavigation-tripdata/gradle.properties b/libnavigation-tripdata/gradle.properties index dfe7a1662a6..b8492a60b7b 100644 --- a/libnavigation-tripdata/gradle.properties +++ b/libnavigation-tripdata/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=tripdata POM_ARTIFACT_TITLE=Mapbox Navigation SDK POM_DESCRIPTION=Artifact that provides trip related data diff --git a/libnavigation-util/gradle.properties b/libnavigation-util/gradle.properties index 7e8671f346a..d38ea4fb26e 100644 --- a/libnavigation-util/gradle.properties +++ b/libnavigation-util/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=utils POM_ARTIFACT_TITLE=Mapbox Navigation Utils POM_DESCRIPTION=Artifact that provides basic navigation utilities diff --git a/libnavigation-voice/gradle.properties b/libnavigation-voice/gradle.properties index a92178d0f04..121e0561b19 100644 --- a/libnavigation-voice/gradle.properties +++ b/libnavigation-voice/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=voice POM_ARTIFACT_TITLE=Mapbox Navigation SDK POM_DESCRIPTION=Artifact that allows users to call into Voice API \ No newline at end of file diff --git a/libnavigation-voice/src/test/java/com/mapbox/navigation/voice/api/MapboxVoiceInstructionsPlayerTest.kt b/libnavigation-voice/src/test/java/com/mapbox/navigation/voice/api/MapboxVoiceInstructionsPlayerTest.kt index c61457dd757..55a32ff9697 100644 --- a/libnavigation-voice/src/test/java/com/mapbox/navigation/voice/api/MapboxVoiceInstructionsPlayerTest.kt +++ b/libnavigation-voice/src/test/java/com/mapbox/navigation/voice/api/MapboxVoiceInstructionsPlayerTest.kt @@ -22,6 +22,7 @@ import io.mockk.verify import io.mockk.verifyOrder import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import java.io.File @@ -604,6 +605,7 @@ class MapboxVoiceInstructionsPlayerTest { } } + @Ignore @Test fun `should abandon focus after a options#abandonFocusDelay`() { val announcement: SpeechAnnouncement = SpeechAnnouncement diff --git a/libnavigator/gradle.properties b/libnavigator/gradle.properties index 5b4cf9d53fa..43500037981 100644 --- a/libnavigator/gradle.properties +++ b/libnavigator/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=navigator POM_ARTIFACT_TITLE=Mapbox Navigation Native wrapper POM_DESCRIPTION=Artifact that provides the native capabilities of the SDK diff --git a/libnavui-base/gradle.properties b/libnavui-base/gradle.properties index 28de95a23f5..6a9b46d19b4 100644 --- a/libnavui-base/gradle.properties +++ b/libnavui-base/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=ui-base POM_ARTIFACT_TITLE=Mapbox Navigation SDK POM_DESCRIPTION=Artifact that provides the set of interface definitions and DTOs used in the modular UI components \ No newline at end of file diff --git a/libnavui-maps/api/current.txt b/libnavui-maps/api/current.txt index 32f89ec5e60..bbd086b982e 100644 --- a/libnavui-maps/api/current.txt +++ b/libnavui-maps/api/current.txt @@ -544,6 +544,7 @@ package com.mapbox.navigation.ui.maps.guidance.junction.api { ctor public MapboxJunctionApi(); method public void cancelAll(); method public void generateJunction(com.mapbox.api.directions.v5.models.BannerInstructions instructions, com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer> consumer); + method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void generateJunction(com.mapbox.api.directions.v5.models.BannerInstructions instructions, @com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewFormat String format, com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer> consumer); } } @@ -562,6 +563,58 @@ package com.mapbox.navigation.ui.maps.guidance.junction.model { property public final android.graphics.Bitmap bitmap; } + @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public final class JunctionViewData { + method public byte[] getData(); + method public com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewData.PngData? getPngData(); + method public String getResponseFormat(); + method public com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewData.SvgData? getSvgData(); + method public boolean isPngData(); + method public boolean isSvgData(); + property public final byte[] data; + property public final boolean isPngData; + property public final boolean isSvgData; + property public final String responseFormat; + } + + public static final class JunctionViewData.PngData { + ctor public JunctionViewData.PngData(byte[] data); + method public android.graphics.Bitmap? getAsBitmap(android.graphics.BitmapFactory.Options options = DEFAULT_DECODE_OPTIONS); + method public byte[] getData(); + property public final byte[] data; + } + + @StringDef({com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewData.ResponseFormat.Companion.PNG, com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewData.ResponseFormat.Companion.SVG, com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewData.ResponseFormat.Companion.UNKNOWN}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public static @interface JunctionViewData.ResponseFormat { + field public static final com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewData.ResponseFormat.Companion Companion; + field public static final String PNG = "png"; + field public static final String SVG = "svg"; + field public static final String UNKNOWN = "unknown"; + } + + public static final class JunctionViewData.ResponseFormat.Companion { + field public static final String PNG = "png"; + field public static final String SVG = "svg"; + field public static final String UNKNOWN = "unknown"; + } + + public static final class JunctionViewData.SvgData { + ctor public JunctionViewData.SvgData(byte[] data); + method public android.graphics.Bitmap? getAsBitmap(int width, int height); + method public android.graphics.Bitmap? getAsBitmap(int width); + method public byte[] getData(); + property public final byte[] data; + } + + @StringDef({com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewFormat.Companion.PNG, com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewFormat.Companion.SVG}) @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface JunctionViewFormat { + field public static final com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewFormat.Companion Companion; + field public static final String PNG = "png"; + field public static final String SVG = "svg"; + } + + public static final class JunctionViewFormat.Companion { + field public static final String PNG = "png"; + field public static final String SVG = "svg"; + } + } package com.mapbox.navigation.ui.maps.guidance.junction.view { diff --git a/libnavui-maps/build.gradle b/libnavui-maps/build.gradle index 78a0369771b..7ced9e341c1 100644 --- a/libnavui-maps/build.gradle +++ b/libnavui-maps/build.gradle @@ -46,7 +46,7 @@ android { dependencies { api project(":libnavui-base") - api (dependenciesList.mapboxMapSdk) + api(dependenciesList.mapboxMapSdk) api dependenciesList.mapboxSdkTurf implementation dependenciesList.androidXAppCompat diff --git a/libnavui-maps/gradle.properties b/libnavui-maps/gradle.properties index 8037f4a7ade..42561356720 100644 --- a/libnavui-maps/gradle.properties +++ b/libnavui-maps/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=ui-maps POM_ARTIFACT_TITLE=Mapbox Navigation SDK POM_DESCRIPTION=Artifact that allows users to call into Maps API \ No newline at end of file diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/JunctionProcessor.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/JunctionProcessor.kt index 8a037442c79..a47923bb868 100644 --- a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/JunctionProcessor.kt +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/JunctionProcessor.kt @@ -80,7 +80,7 @@ internal object JunctionProcessor { ResourceLoadStatus.AVAILABLE -> { val dataRef = responseData.data?.data if (dataRef?.isNotEmpty() == true) { - JunctionResult.JunctionRaster.Success(dataRef) + JunctionResult.JunctionRaster.Success(dataRef, responseData.contentType) } else { JunctionResult.JunctionRaster.Empty } diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/JunctionResult.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/JunctionResult.kt index 5f27db8e1c3..1a1dfdf097a 100644 --- a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/JunctionResult.kt +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/JunctionResult.kt @@ -19,7 +19,7 @@ internal sealed class JunctionResult { sealed class JunctionRaster : JunctionResult() { object Empty : JunctionRaster() data class Failure(val error: String?) : JunctionRaster() - data class Success(val dataRef: DataRef) : JunctionRaster() + data class Success(val dataRef: DataRef, val contentType: String) : JunctionRaster() } sealed class JunctionBitmap : JunctionResult() { diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/api/MapboxJunctionApi.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/api/MapboxJunctionApi.kt index 1a8df76b981..49eb35c2298 100644 --- a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/api/MapboxJunctionApi.kt +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/api/MapboxJunctionApi.kt @@ -6,17 +6,22 @@ import com.mapbox.api.directions.v5.models.BannerComponents import com.mapbox.api.directions.v5.models.BannerInstructions import com.mapbox.bindgen.DataRef import com.mapbox.bindgen.Expected -import com.mapbox.bindgen.ExpectedFactory +import com.mapbox.bindgen.ExpectedFactory.createError +import com.mapbox.bindgen.ExpectedFactory.createValue import com.mapbox.common.MapboxServices import com.mapbox.common.ResourceLoadError import com.mapbox.common.ResourceLoadResult +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI import com.mapbox.navigation.base.internal.utils.MapboxOptionsUtil +import com.mapbox.navigation.base.internal.utils.toByteArray import com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer import com.mapbox.navigation.ui.maps.guidance.junction.JunctionAction import com.mapbox.navigation.ui.maps.guidance.junction.JunctionProcessor import com.mapbox.navigation.ui.maps.guidance.junction.JunctionResult import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionError import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionValue +import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewData +import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewFormat import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoaderFactory import com.mapbox.navigation.ui.utils.internal.resource.load import com.mapbox.navigation.utils.internal.InternalJobControlFactory @@ -27,11 +32,7 @@ import kotlinx.coroutines.launch * Mapbox Junction Api allows you to generate junction for select maneuvers. */ @UiThread -class MapboxJunctionApi() { - - private companion object { - private const val ACCESS_TOKEN = "access_token" - } +class MapboxJunctionApi { private val mainJobController by lazy { InternalJobControlFactory.createMainScopeJobControl() } private val resourceLoader by lazy { ResourceLoaderFactory.getInstance() } @@ -58,20 +59,69 @@ class MapboxJunctionApi() { val action = JunctionAction.CheckJunctionAvailability(instructions) when (val result = JunctionProcessor.process(action)) { is JunctionResult.JunctionAvailable -> { - makeJunctionRequest(result, consumer) + makeJunctionRequest(result.junctionUrl, format = null) { loadResult -> + processResponse(loadResult).onError { + consumer.accept(createError(it)) + }.onValue { (dataRef, _) -> + consumer.accept(processRawJunctionView(dataRef)) + } + } + } + is JunctionResult.JunctionUnavailable -> { + consumer.accept( + createJunctionErrorResult("No junction available for current maneuver."), + ) + } + else -> { + consumer.accept( + createJunctionErrorResult("Inappropriate $result emitted for $action."), + ) + } + } + } + + /** + * The method takes in [BannerInstructions] and generates a junction based on the presence of + * specific banner components. + * + * @param instructions object representing [BannerInstructions] + * @param format the desired format for the junction view image (e.g., "png" or "svg"). + * @param consumer informs about the state of junction. + */ + @ExperimentalPreviewMapboxNavigationAPI + fun generateJunction( + instructions: BannerInstructions, + @JunctionViewFormat format: String, + consumer: MapboxNavigationConsumer>, + ) { + val action = JunctionAction.CheckJunctionAvailability(instructions) + when (val result = JunctionProcessor.process(action)) { + is JunctionResult.JunctionAvailable -> { + makeJunctionRequest(result.junctionUrl, format = format) { loadResult -> + processResponse(loadResult).onError { + consumer.accept(createError(it)) + }.onValue { (dataRef, contentType) -> + consumer.accept( + createValue( + JunctionViewData( + dataRef.toByteArray(), + JunctionViewData.ResponseFormat.createFromContentType( + contentType, + ), + ), + ), + ) + } + } } is JunctionResult.JunctionUnavailable -> { consumer.accept( - ExpectedFactory.createError( - JunctionError("No junction available for current maneuver.", null), - ), + createJunctionErrorResult("No junction available for current maneuver."), ) } else -> { consumer.accept( - ExpectedFactory.createError( - JunctionError("Inappropriate $result emitted for $action.", null), - ), + createJunctionErrorResult("Inappropriate $result emitted for $action."), ) } } @@ -84,73 +134,78 @@ class MapboxJunctionApi() { mainJobController.job.cancelChildren() } + @OptIn(ExperimentalPreviewMapboxNavigationAPI::class) private fun makeJunctionRequest( - result: JunctionResult.JunctionAvailable, - consumer: MapboxNavigationConsumer>, + junctionUrl: String, + @JunctionViewFormat format: String?, + callback: (Expected) -> Unit, ) { val accessToken = MapboxOptionsUtil.getTokenForService(MapboxServices.DIRECTIONS) - val url = Uri.parse(result.junctionUrl).buildUpon() + val url = Uri.parse(junctionUrl).buildUpon() .appendQueryParameter(ACCESS_TOKEN, accessToken) + .apply { + // For backwards compatibility we should not pass format parameter + // if request is made from a function which doesn't accept format + if (format != null) { + appendQueryParameter(IMAGE_FORMAT, format) + } + } .build().toString() + val requestAction = JunctionAction.PrepareJunctionRequest(url) val junctionRequest = JunctionProcessor.process(requestAction) val loadRequest = (junctionRequest as JunctionResult.JunctionRequest).request mainJobController.scope.launch { val loadResult = resourceLoader.load(loadRequest) - onJunctionResponse(loadResult, consumer) + callback(loadResult) } } - private fun onJunctionResponse( + private fun processResponse( loadResult: Expected, - consumer: MapboxNavigationConsumer>, - ) { + ): Expected> { val action = JunctionAction.ProcessJunctionResponse(loadResult) - when (val result = JunctionProcessor.process(action)) { + return when (val result = JunctionProcessor.process(action)) { is JunctionResult.JunctionRaster.Success -> { - onJunctionAvailable(result.dataRef, consumer) + createValue(result.dataRef to result.contentType) } is JunctionResult.JunctionRaster.Failure -> { - consumer.accept( - ExpectedFactory.createError(JunctionError(result.error, null)), - ) + createJunctionErrorResult(result.error) } is JunctionResult.JunctionRaster.Empty -> { - consumer.accept( - ExpectedFactory.createError( - JunctionError("No junction available for current maneuver.", null), - ), - ) + createJunctionErrorResult("No junction available for current maneuver.") } else -> { - consumer.accept( - ExpectedFactory.createError( - JunctionError("Inappropriate $result emitted for $action.", null), - ), - ) + createJunctionErrorResult("Inappropriate $result emitted for $action.") } } } - private fun onJunctionAvailable( - data: DataRef, - consumer: MapboxNavigationConsumer>, - ) { + private fun processRawJunctionView(data: DataRef): Expected { val action = JunctionAction.ParseRasterToBitmap(data) - when (val result = JunctionProcessor.process(action)) { + return when (val result = JunctionProcessor.process(action)) { is JunctionResult.JunctionBitmap.Success -> { - consumer.accept(ExpectedFactory.createValue(JunctionValue(result.junction))) + createValue(JunctionValue(result.junction)) } is JunctionResult.JunctionBitmap.Failure -> { - consumer.accept(ExpectedFactory.createError(JunctionError(result.message, null))) + createJunctionErrorResult(result.message) } else -> { - consumer.accept( - ExpectedFactory.createError( - JunctionError("Inappropriate $result emitted for $action.", null), - ), - ) + createJunctionErrorResult("Inappropriate $result emitted for $action.") } } } + + private companion object { + + private const val ACCESS_TOKEN = "access_token" + private const val IMAGE_FORMAT = "image_format" + + private fun createJunctionErrorResult( + errorMessage: String?, + throwable: Throwable? = null, + ): Expected { + return createError(JunctionError(errorMessage, throwable)) + } + } } diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/model/JunctionError.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/model/JunctionError.kt index 0e0aa6f689a..fbbd0f86a8d 100644 --- a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/model/JunctionError.kt +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/model/JunctionError.kt @@ -8,4 +8,12 @@ package com.mapbox.navigation.ui.maps.guidance.junction.model class JunctionError internal constructor( val errorMessage: String?, val throwable: Throwable?, -) +) { + + /** + * Returns a string representation of the object. + */ + override fun toString(): String { + return "JunctionError(errorMessage=$errorMessage, throwable=$throwable)" + } +} diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/model/JunctionViewData.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/model/JunctionViewData.kt new file mode 100644 index 00000000000..c12b41eb32c --- /dev/null +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/model/JunctionViewData.kt @@ -0,0 +1,260 @@ +package com.mapbox.navigation.ui.maps.guidance.junction.model + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.annotation.StringDef +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import com.mapbox.navigation.ui.utils.internal.SvgUtil +import com.mapbox.navigation.utils.internal.logE +import java.io.ByteArrayInputStream + +/** + * A class that encapsulates the junction view data and its associated format type. + * + * This class is primarily used to store and manage visual representations of road junctions, + * including the raw image data (as a byte array) and the format of the image (e.g., PNG, SVG). + * + * @property data A byte array that contains the raw junction view image data. + * @property responseFormat The format of the image (e.g., "png", "svg"). + */ +@ExperimentalPreviewMapboxNavigationAPI +class JunctionViewData internal constructor( + val data: ByteArray, + @ResponseFormat val responseFormat: String, +) { + + /** + * Checks if the junction view data is in PNG format. + * @return `true` if the data format is PNG, `false` otherwise. + */ + val isPngData: Boolean + get() = responseFormat == ResponseFormat.PNG + + /** + * Checks if the junction view data is in SVG format. + * + * @return `true` if the data format is SVG, `false` otherwise. + */ + val isSvgData: Boolean + get() = responseFormat == ResponseFormat.SVG + + /** + * Retrieves the junction view data as a [PngData] object if the data is in PNG format. + * @return A [PngData] object containing the PNG image data, or `null` if the data is not in PNG format. + */ + fun getPngData(): PngData? { + return if (isPngData) { + PngData(data) + } else { + null + } + } + + /** + * Retrieves the junction view data as an [SvgData] object if the data is in SVG format. + * @return An [SvgData] object containing the SVG image data, or `null` if the data is not in SVG format. + */ + fun getSvgData(): SvgData? { + return if (isSvgData) { + SvgData(data) + } else { + null + } + } + + /** + * Indicates whether some other object is "equal to" this one. + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as JunctionViewData + + if (!data.contentEquals(other.data)) return false + return responseFormat == other.responseFormat + } + + /** + * Returns a hash code value for the object. + */ + override fun hashCode(): Int { + var result = data.contentHashCode() + result = 31 * result + responseFormat.hashCode() + return result + } + + /** + * Returns a string representation of the object. + */ + override fun toString(): String { + return "JunctionViewData(data=${data.contentToString()}, responseFormat='$responseFormat')" + } + + /** + * Annotation class representing the format of the received junction view. + * Available values are: + * - [ResponseFormat.PNG] + * - [ResponseFormat.SVG] + * - [ResponseFormat.UNKNOWN] + */ + @Retention(AnnotationRetention.BINARY) + @StringDef( + ResponseFormat.PNG, + ResponseFormat.SVG, + ResponseFormat.UNKNOWN, + ) + annotation class ResponseFormat { + + companion object { + + /** + * Received junction view is in PNG format. + */ + const val PNG = "png" + + /** + * Received junction view is in SVG format. + */ + const val SVG = "svg" + + /** + * Received junction view has unknown or unsupported image format. + */ + const val UNKNOWN = "unknown" + + @JvmSynthetic + @ResponseFormat + internal fun createFromContentType(contentType: String): String { + return when (contentType.lowercase()) { + "image/svg+xml" -> SVG + "image/png" -> PNG + else -> UNKNOWN + } + } + } + } + + /** + * A class that represents PNG image data. + * @property data The raw PNG image data as a byte array. + */ + class PngData(val data: ByteArray) { + + /** + * Decodes the PNG data into a [Bitmap] object using the specified decoding options. + * + * By default, the bitmap is decoded with the [Bitmap.Config.RGB_565] configuration to optimize memory usage. + * Custom options can be provided via the `options` parameter to control the decoding behavior. + * + * @param options A [BitmapFactory.Options] object to customize the decoding process. + * Defaults to [Bitmap.Config.RGB_565] configuration. + * + * @return A [Bitmap] object if decoding is successful, or `null` if the data cannot be decoded. + */ + fun getAsBitmap(options: BitmapFactory.Options = DEFAULT_DECODE_OPTIONS): Bitmap? { + return BitmapFactory.decodeByteArray(data, 0, data.size, options) + } + + /** + * Indicates whether some other object is "equal to" this one. + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PngData + + return data.contentEquals(other.data) + } + + /** + * Returns a hash code value for the object. + */ + override fun hashCode(): Int { + return data.contentHashCode() + } + + /** + * Returns a string representation of the object. + */ + override fun toString(): String { + return "PngData(data=${data.contentToString()})" + } + + private companion object { + val DEFAULT_DECODE_OPTIONS = BitmapFactory.Options().apply { + inPreferredConfig = Bitmap.Config.RGB_565 + } + } + } + + /** + * A class that represents SVG image data. + * @property data The raw SVG image data as a byte array. + */ + class SvgData(val data: ByteArray) { + + /** + * Renders the SVG data into a [Bitmap] object with specified width and height. + * + * @param width The width of the bitmap. + * @param height The height of the bitmap. + * @return A [Bitmap] object containing the rendered SVG, or `null` if the SVG parsing or rendering fails. + */ + fun getAsBitmap(width: Int, height: Int): Bitmap? { + return try { + return SvgUtil.renderAsBitmapWith(ByteArrayInputStream(data), width, height) + } catch (e: Exception) { + logE { + "Unable to convert SVG to Bitmap: $e" + } + null + } + } + + /** + * Renders the SVG data into a [Bitmap] object with specified width. + * The height will be calculated based on SVG aspect ratio. + * + * @param width The width of the bitmap. + * @return A [Bitmap] object containing the rendered SVG, or `null` if the SVG parsing or rendering fails. + */ + fun getAsBitmap(width: Int): Bitmap? { + return try { + SvgUtil.renderAsBitmapWithWidth(ByteArrayInputStream(data), width) + } catch (e: Exception) { + logE { + "Unable to render SVG: $e" + } + null + } + } + + /** + * Indicates whether some other object is "equal to" this one. + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SvgData + + return data.contentEquals(other.data) + } + + /** + * Returns a hash code value for the object. + */ + override fun hashCode(): Int { + return data.contentHashCode() + } + + /** + * Returns a string representation of the object. + */ + override fun toString(): String { + return "SvgData(data=${data.contentToString()})" + } + } +} diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/model/JunctionViewFormat.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/model/JunctionViewFormat.kt new file mode 100644 index 00000000000..4e86c3abc21 --- /dev/null +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/guidance/junction/model/JunctionViewFormat.kt @@ -0,0 +1,31 @@ +package com.mapbox.navigation.ui.maps.guidance.junction.model + +import androidx.annotation.StringDef +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI + +/** + * Annotation class representing the format options for a junction view. + * Available values are: + * - [JunctionViewFormat.PNG] + * - [JunctionViewFormat.SVG] + */ +@ExperimentalPreviewMapboxNavigationAPI +@Retention(AnnotationRetention.BINARY) +@StringDef( + JunctionViewFormat.PNG, + JunctionViewFormat.SVG, +) +annotation class JunctionViewFormat { + companion object { + + /** + * Specifies the PNG format for the junction view. + */ + const val PNG = "png" + + /** + * Specifies the SVG format for the junction view. + */ + const val SVG = "svg" + } +} diff --git a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/callout/api/MapboxRouteCalloutApi.kt b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/callout/api/MapboxRouteCalloutApi.kt index 3394603774b..d7720e9a6fa 100644 --- a/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/callout/api/MapboxRouteCalloutApi.kt +++ b/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/route/callout/api/MapboxRouteCalloutApi.kt @@ -136,8 +136,6 @@ class MapboxRouteCalloutApi( val primaryRoute = routes.firstOrNull() ?: return emptyList() val alternativeRoutes = routes.drop(1) - if (alternativeRoutes.isEmpty()) return emptyList() - return when (options.routeCalloutType) { RouteCalloutType.RouteDurations -> createRoutePreviewCallouts( primaryRoute, @@ -204,7 +202,7 @@ class MapboxRouteCalloutApi( val durationDiffAbsoluteValue = durationDiff.absoluteValue return durationDiffAbsoluteValue to when { - durationDiffAbsoluteValue < options.similarDurationDelta -> DurationDifferenceType.Same + durationDiffAbsoluteValue <= options.similarDurationDelta -> DurationDifferenceType.Same durationDiff < 0.seconds -> DurationDifferenceType.Slower @@ -217,24 +215,17 @@ class MapboxRouteCalloutApi( primaryRoutePoints: List, alternativeRoutes: List, ): List { - val primaryCallout = RouteCallout.Eta( - primaryRoute, - LineString.fromLngLats(primaryRoutePoints), - isPrimary = true, - ) + return buildList(capacity = alternativeRoutes.size + 1) { + val primaryGeometry = LineString.fromLngLats(primaryRoutePoints) + add(RouteCallout.Eta(primaryRoute, primaryGeometry, isPrimary = true)) - val alternativeCallouts = alternativeRoutes.map { - val geometry = alternativesGeometryDifference[it.id] - ?: it.directionsRoute.completeGeometryToLineString() + alternativeRoutes.mapTo(destination = this) { alternativeRoute -> + val geometry = alternativesGeometryDifference[alternativeRoute.id] + ?: alternativeRoute.directionsRoute.completeGeometryToLineString() - RouteCallout.Eta( - it, - geometry, - isPrimary = false, - ) + RouteCallout.Eta(alternativeRoute, geometry, isPrimary = false) + } } - - return listOf(primaryCallout) + alternativeCallouts } private companion object { 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 18e3045ca78..118866004c5 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 @@ -299,9 +299,11 @@ class MapboxRouteLineView @VisibleForTesting internal constructor( sender.sendInitialOptionsEvent(holder.data) sender.sendRenderRouteDrawDataEvent(style.getStyleId(), routeDrawData) rebuildSourcesAndLayersIfNeeded(style, holder.options) - val primaryRouteTrafficVisibility = getTrafficVisibility(style) - val primaryRouteVisibility = getPrimaryRouteVisibility(style) - val alternativeRouteVisibility = getAlternativeRoutesVisibility(style) + val originalPrimaryRouteLayers = getLayerIdsForPrimaryRoute( + primaryRouteLineLayerGroup, + sourceLayerMap, + style, + ) val updateSourceCommands = mutableListOf<() -> Unit>() val mutationCommands = mutableListOf<() -> Unit>() routeDrawData.value?.let { routeSetValue -> @@ -461,6 +463,13 @@ class MapboxRouteLineView @VisibleForTesting internal constructor( primaryRouteLineLayerGroup = getLayerIdsForPrimaryRoute(style, sourceLayerMap) updateLayerScaling(style, optionsHolder.options) + val primaryRouteTrafficVisibility = + getTrafficVisibility(originalPrimaryRouteLayers, style) + val primaryRouteVisibility = + getPrimaryRouteVisibility(originalPrimaryRouteLayers, style) + val alternativeRouteVisibility = + getAlternativeRoutesVisibility(originalPrimaryRouteLayers, style) + // 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 @@ -838,11 +847,18 @@ class MapboxRouteLineView @VisibleForTesting internal constructor( * @return the visibility value returned by the map for the primary route line. */ fun getTrafficVisibility(style: Style): Visibility? { - return getLayerIdsForPrimaryRoute( - primaryRouteLineLayerGroup, - sourceLayerMap, + return getTrafficVisibility( + getLayerIdsForPrimaryRoute( + primaryRouteLineLayerGroup, + sourceLayerMap, + style, + ), style, - ).firstOrNull { layerId -> + ) + } + + private fun getTrafficVisibility(primaryRouteLayers: Set, style: Style): Visibility? { + return primaryRouteLayers.firstOrNull { layerId -> layerId in trafficLayerIds }?.run { getLayerVisibility(style, this) @@ -857,11 +873,21 @@ class MapboxRouteLineView @VisibleForTesting internal constructor( * @return the visibility value returned by the map. */ fun getPrimaryRouteVisibility(style: Style): Visibility? { - return getLayerIdsForPrimaryRoute( - primaryRouteLineLayerGroup, - sourceLayerMap, + return getPrimaryRouteVisibility( + getLayerIdsForPrimaryRoute( + primaryRouteLineLayerGroup, + sourceLayerMap, + style, + ), style, - ).firstOrNull { layerId -> + ) + } + + private fun getPrimaryRouteVisibility( + primaryRouteLayers: Set, + style: Style, + ): Visibility? { + return primaryRouteLayers.firstOrNull { layerId -> layerId in mainLayerIds }?.run { getLayerVisibility(style, this) @@ -878,10 +904,17 @@ class MapboxRouteLineView @VisibleForTesting internal constructor( fun getAlternativeRoutesVisibility(style: Style): Visibility? { val primaryRouteLineLayers = getLayerIdsForPrimaryRoute(primaryRouteLineLayerGroup, sourceLayerMap, style) + return getAlternativeRoutesVisibility(primaryRouteLineLayers, style) + } + + private fun getAlternativeRoutesVisibility( + primaryRouteLayers: Set, + style: Style, + ): Visibility? { return layerGroup1SourceLayerIds .union(layerGroup2SourceLayerIds) .union(layerGroup3SourceLayerIds) - .subtract(primaryRouteLineLayers).firstOrNull { layerId -> + .subtract(primaryRouteLayers).firstOrNull { layerId -> layerId in mainLayerIds }?.run { getLayerVisibility(style, this) diff --git a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/guidance/junction/JunctionProcessorTest.kt b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/guidance/junction/JunctionProcessorTest.kt index a46ea498bef..ced00fc03d3 100644 --- a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/guidance/junction/JunctionProcessorTest.kt +++ b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/guidance/junction/JunctionProcessorTest.kt @@ -287,13 +287,15 @@ class JunctionProcessorTest { @Test fun `process ProcessJunctionResponse action with ResourceLoadStatus AVAILABLE`() { val blob = byteArrayOf(12, -12, 23, 65, -56, 74, 88, 90, -92, -11) + val contentType = "image/png" val loadResult = resourceLoadResult( data = resourceData(blob), status = ResourceLoadStatus.AVAILABLE, + contentType = contentType, ) val response: Expected = ExpectedFactory.createValue(loadResult) - val expected = JunctionResult.JunctionRaster.Success(blob.toDataRef()) + val expected = JunctionResult.JunctionRaster.Success(blob.toDataRef(), contentType) val action = JunctionAction.ProcessJunctionResponse(response) val result = JunctionProcessor.process(action) as JunctionResult.JunctionRaster.Success diff --git a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/guidance/junction/api/MapboxJunctionApiTest.kt b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/guidance/junction/api/MapboxJunctionApiTest.kt index 6425e56d11c..63769ead1d3 100644 --- a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/guidance/junction/api/MapboxJunctionApiTest.kt +++ b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/guidance/junction/api/MapboxJunctionApiTest.kt @@ -1,13 +1,17 @@ +@file:OptIn(ExperimentalPreviewMapboxNavigationAPI::class) + package com.mapbox.navigation.ui.maps.guidance.junction.api import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.net.Uri import com.mapbox.api.directions.v5.models.BannerInstructions import com.mapbox.bindgen.Expected import com.mapbox.bindgen.ExpectedFactory import com.mapbox.common.MapboxServices import com.mapbox.common.ResourceLoadError import com.mapbox.common.ResourceLoadResult +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI import com.mapbox.navigation.base.internal.utils.MapboxOptionsUtil import com.mapbox.navigation.testing.MainCoroutineRule import com.mapbox.navigation.testing.toDataRef @@ -17,6 +21,8 @@ import com.mapbox.navigation.ui.maps.guidance.junction.JunctionProcessor import com.mapbox.navigation.ui.maps.guidance.junction.JunctionResult import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionError import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionValue +import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewData +import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewFormat import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoadCallback import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoadRequest import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoader @@ -44,6 +50,12 @@ import org.robolectric.RobolectricTestRunner import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +private typealias JunctionValueConsumer = + MapboxNavigationConsumer> + +private typealias JunctionViewDataConsumer = + MapboxNavigationConsumer> + @ExperimentalCoroutinesApi @RunWith(RobolectricTestRunner::class) class MapboxJunctionApiTest { @@ -51,8 +63,6 @@ class MapboxJunctionApiTest { @get:Rule val coroutineRule = MainCoroutineRule() - private val consumer: - MapboxNavigationConsumer> = mockk(relaxed = true) private val bannerInstructions: BannerInstructions = mockk() private lateinit var junctionApi: MapboxJunctionApi @@ -65,7 +75,9 @@ class MapboxJunctionApiTest { mockkStatic(BitmapFactory::class) mockkStatic(MapboxRasterToBitmapParser::class) mockkStatic(MapboxOptionsUtil::class) - every { MapboxOptionsUtil.getTokenForService(MapboxServices.DIRECTIONS) } returns "pk.1234" + every { + MapboxOptionsUtil.getTokenForService(MapboxServices.DIRECTIONS) + } returns DIRECTIONS_TOKEN mockResourceLoader = mockk(relaxed = true) every { ResourceLoaderFactory.getInstance() } returns mockResourceLoader @@ -84,6 +96,8 @@ class MapboxJunctionApiTest { @Test fun `process state junction unavailable`() { + val consumer: JunctionValueConsumer = mockk(relaxed = true) + val expectedError = "No junction available for current maneuver." givenProcessorResults( checkJunctionAvailability = JunctionResult.JunctionUnavailable, @@ -98,6 +112,8 @@ class MapboxJunctionApiTest { @Test fun `process result incorrect result for action`() { + val consumer: JunctionValueConsumer = mockk(relaxed = true) + val mockResult = JunctionResult.JunctionRequest(mockk()) val mockAction = JunctionAction.CheckJunctionAvailability(bannerInstructions) every { JunctionProcessor.process(mockAction) } returns mockResult @@ -114,6 +130,8 @@ class MapboxJunctionApiTest { @Test fun `process state junction available junction empty`() { + val consumer: JunctionValueConsumer = mockk(relaxed = true) + val expectedError = "No junction available for current maneuver." val url = "https//abc.mapbox.com" val loadRequest = mockk() @@ -139,6 +157,8 @@ class MapboxJunctionApiTest { @Test fun `process state junction available junction failure`() { + val consumer: JunctionValueConsumer = mockk(relaxed = true) + val expectedError = "Resource is missing" val url = "https//abc.mapbox.com" val loadRequest = mockk() @@ -166,11 +186,14 @@ class MapboxJunctionApiTest { @Test fun `process state junction available junction raster success parse fail`() { + val consumer: JunctionValueConsumer = mockk(relaxed = true) + val expectedError = "Error parsing raster to bitmap as raster is empty" val url = "https//abc.mapbox.com" val loadRequest = mockk() val loadResponse = mockk>() val rasterData = byteArrayOf().toDataRef() + val contentType = "image/png" val parserFailure = ExpectedFactory.createError(expectedError) givenResourceLoaderResponse( @@ -180,7 +203,10 @@ class MapboxJunctionApiTest { givenProcessorResults( checkJunctionAvailability = JunctionResult.JunctionAvailable(url), prepareJunctionRequest = JunctionResult.JunctionRequest(loadRequest), - processJunctionResponse = JunctionResult.JunctionRaster.Success(rasterData), + processJunctionResponse = JunctionResult.JunctionRaster.Success( + rasterData, + contentType, + ), parseRasterToBitmap = JunctionResult.JunctionBitmap.Failure(parserFailure.error!!), ) @@ -193,12 +219,15 @@ class MapboxJunctionApiTest { @Test fun `process state junction available junction raster success parse success`() { + val consumer: JunctionValueConsumer = mockk(relaxed = true) + val expectedBitmap = mockk() val url = "https//abc.mapbox.com" val loadRequest = mockk() val loadResponse = mockk>() val rasterData = byteArrayOf(12, -12, 23, 45, 67, 65, 44, 45, 12, 34, 45, 56, 76) .toDataRef() + val contentType = "image/png" val parseSuccess: Expected = ExpectedFactory.createValue(expectedBitmap) givenResourceLoaderResponse( @@ -208,7 +237,10 @@ class MapboxJunctionApiTest { givenProcessorResults( checkJunctionAvailability = JunctionResult.JunctionAvailable(url), prepareJunctionRequest = JunctionResult.JunctionRequest(loadRequest), - processJunctionResponse = JunctionResult.JunctionRaster.Success(rasterData), + processJunctionResponse = JunctionResult.JunctionRaster.Success( + rasterData, + contentType, + ), parseRasterToBitmap = JunctionResult.JunctionBitmap.Success(parseSuccess.value!!), ) @@ -220,6 +252,12 @@ class MapboxJunctionApiTest { val messageSlot = slot>() verify(exactly = 1) { consumer.accept(capture(messageSlot)) } assertEquals(expectedBitmap, messageSlot.captured.value!!.bitmap) + verify(exactly = 1) { + JunctionProcessor.process( + JunctionAction.PrepareJunctionRequest(buildExpectedJunctionUrl(url, null)), + ) + } + clearAllMocks(answers = false) // use new token @@ -227,7 +265,10 @@ class MapboxJunctionApiTest { givenProcessorResults( checkJunctionAvailability = JunctionResult.JunctionAvailable(url), prepareJunctionRequest = JunctionResult.JunctionRequest(loadRequest), - processJunctionResponse = JunctionResult.JunctionRaster.Success(rasterData), + processJunctionResponse = JunctionResult.JunctionRaster.Success( + rasterData, + contentType, + ), parseRasterToBitmap = JunctionResult.JunctionBitmap.Success(parseSuccess.value!!), token = newToken, ) @@ -238,11 +279,20 @@ class MapboxJunctionApiTest { val messageSlot2 = slot>() verify(exactly = 1) { consumer.accept(capture(messageSlot2)) } assertEquals(expectedBitmap, messageSlot2.captured.value!!.bitmap) + verify(exactly = 1) { + JunctionProcessor.process( + JunctionAction.PrepareJunctionRequest( + buildExpectedJunctionUrl(url, null, newToken), + ), + ) + } } @Ignore("Make this test an instrumentation test to avoid UnsatisfiedLinkError from Common 11+") @Test fun `process request junction request cancel`() { + val consumer: JunctionValueConsumer = mockk(relaxed = true) + val mockWebServer = MockWebServer() mockWebServer.enqueue(MockResponse().setResponseCode(401)) mockWebServer.start() @@ -268,6 +318,147 @@ class MapboxJunctionApiTest { mockWebServer.shutdown() } + @Test + fun `process state junction unavailable for svg request`() { + val consumer: JunctionViewDataConsumer = mockk(relaxed = true) + + val expectedError = "No junction available for current maneuver." + givenProcessorResults( + checkJunctionAvailability = JunctionResult.JunctionUnavailable, + ) + + junctionApi.generateJunction(bannerInstructions, JunctionViewFormat.Companion.SVG, consumer) + + val messageSlot = slot>() + verify(exactly = 1) { consumer.accept(capture(messageSlot)) } + assertEquals(expectedError, messageSlot.captured.error!!.errorMessage) + } + + @Test + fun `process state junction available junction failure for svg`() { + val consumer: JunctionViewDataConsumer = mockk(relaxed = true) + + val expectedError = "Resource is missing" + val url = "https//abc.mapbox.com" + val loadRequest = mockk() + val loadResponse = mockk>() + val rasterResult = mockk { + every { error } returns expectedError + } + + givenResourceLoaderResponse( + request = loadRequest, + response = loadResponse, + ) + givenProcessorResults( + checkJunctionAvailability = JunctionResult.JunctionAvailable(url), + prepareJunctionRequest = JunctionResult.JunctionRequest(loadRequest), + processJunctionResponse = rasterResult, + ) + + junctionApi.generateJunction(bannerInstructions, JunctionViewFormat.SVG, consumer) + + val messageSlot = slot>() + verify(exactly = 1) { consumer.accept(capture(messageSlot)) } + assertEquals(expectedError, messageSlot.captured.error!!.errorMessage) + } + + @Test + fun `process state junction available for svg`() { + val consumer: JunctionViewDataConsumer = mockk(relaxed = true) + + val url = "https//abc.mapbox.com" + val loadRequest = mockk() + val loadResponse = mockk>() + val svgData = byteArrayOf(12, -12, 23, 45, 67, 65, 44, 45, 12, 34, 45, 56, 76) + val contentType = "image/svg+xml" + + givenResourceLoaderResponse( + request = loadRequest, + response = loadResponse, + ) + givenProcessorResults( + checkJunctionAvailability = JunctionResult.JunctionAvailable(url), + prepareJunctionRequest = JunctionResult.JunctionRequest(loadRequest), + processJunctionResponse = JunctionResult.JunctionRaster.Success( + svgData.toDataRef(), + contentType, + ), + ) + + junctionApi.generateJunction(bannerInstructions, JunctionViewFormat.SVG, consumer) + + val messageSlot = slot>() + verify(exactly = 1) { consumer.accept(capture(messageSlot)) } + + val junctionViewData = JunctionViewData(svgData, JunctionViewData.ResponseFormat.SVG) + assertEquals(junctionViewData, messageSlot.captured.value) + + verify(exactly = 1) { + JunctionProcessor.process( + JunctionAction.PrepareJunctionRequest( + buildExpectedJunctionUrl(url, JunctionViewFormat.SVG), + ), + ) + } + } + + @Test + fun `process state junction available for explicit png`() { + val consumer: JunctionViewDataConsumer = mockk(relaxed = true) + + val url = "https//abc.mapbox.com" + val loadRequest = mockk() + val loadResponse = mockk>() + val svgData = byteArrayOf(12, -12, 23, 45, 67, 65, 44, 45, 12, 34, 45, 56, 76) + val contentType = "image/png" + + givenResourceLoaderResponse( + request = loadRequest, + response = loadResponse, + ) + givenProcessorResults( + checkJunctionAvailability = JunctionResult.JunctionAvailable(url), + prepareJunctionRequest = JunctionResult.JunctionRequest(loadRequest), + processJunctionResponse = JunctionResult.JunctionRaster.Success( + svgData.toDataRef(), + contentType, + ), + ) + + junctionApi.generateJunction(bannerInstructions, JunctionViewFormat.PNG, consumer) + + val messageSlot = slot>() + verify(exactly = 1) { consumer.accept(capture(messageSlot)) } + + val junctionViewData = JunctionViewData(svgData, JunctionViewData.ResponseFormat.PNG) + assertEquals(junctionViewData, messageSlot.captured.value) + + verify(exactly = 1) { + JunctionProcessor.process( + JunctionAction.PrepareJunctionRequest( + buildExpectedJunctionUrl(url, JunctionViewFormat.PNG), + ), + ) + } + } + + private fun buildExpectedJunctionUrl( + baseUrl: String, + @JunctionViewFormat format: String?, + token: String? = DIRECTIONS_TOKEN, + ): String { + return Uri.parse(baseUrl).buildUpon() + .appendQueryParameter("access_token", token) + .apply { + if (format != null) { + appendQueryParameter("image_format", format) + } + } + .build() + .toString() + } + private fun givenResourceLoaderResponse( request: ResourceLoadRequest, response: Expected, @@ -284,7 +475,7 @@ class MapboxJunctionApiTest { prepareJunctionRequest: JunctionResult? = null, processJunctionResponse: JunctionResult? = null, parseRasterToBitmap: JunctionResult? = null, - token: String = "pk.1234", + token: String = DIRECTIONS_TOKEN, ) { every { JunctionProcessor.process(JunctionAction.CheckJunctionAvailability(bannerInstructions)) @@ -313,4 +504,8 @@ class MapboxJunctionApiTest { } returns parseRasterToBitmap } } + + private companion object { + const val DIRECTIONS_TOKEN = "pk.1234" + } } diff --git a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/guidance/junction/model/JunctionViewDataTest.kt b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/guidance/junction/model/JunctionViewDataTest.kt new file mode 100644 index 00000000000..a739f913040 --- /dev/null +++ b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/guidance/junction/model/JunctionViewDataTest.kt @@ -0,0 +1,211 @@ +@file:OptIn(ExperimentalPreviewMapboxNavigationAPI::class) + +package com.mapbox.navigation.ui.maps.guidance.junction.model + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import com.jparams.verifier.tostring.ToStringVerifier +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionViewData.ResponseFormat +import com.mapbox.navigation.ui.utils.internal.SvgUtil +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.slot +import io.mockk.unmockkObject +import io.mockk.unmockkStatic +import io.mockk.verify +import nl.jqno.equalsverifier.EqualsVerifier +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class JunctionViewDataTest { + + @Before + fun setUp() { + mockkStatic(BitmapFactory::class) + mockkObject(SvgUtil) + } + + @After + fun tearDown() { + unmockkStatic(BitmapFactory::class) + unmockkObject(SvgUtil) + } + + @Test + fun `test createFromContentType()`() { + listOf( + ResponseFormat.SVG to "image/svg+xml", + ResponseFormat.SVG to "image/SVG+XML", + + ResponseFormat.PNG to "image/png", + ResponseFormat.PNG to "image/PNG", + + ResponseFormat.UNKNOWN to "image/unknown", + ResponseFormat.UNKNOWN to "test", + ).forEach { (expectedFormat, contentType) -> + val responseFormat = ResponseFormat.createFromContentType(contentType) + + assertEquals( + "Expected $expectedFormat, but was $responseFormat", + expectedFormat, + responseFormat, + ) + } + } + + @Test + fun `test svg junction view data`() { + val svgData = JunctionViewData(TEST_DATA_BYTES, ResponseFormat.SVG) + + assertTrue(svgData.isSvgData) + assertFalse(svgData.isPngData) + + assertEquals(JunctionViewData.SvgData(TEST_DATA_BYTES), svgData.getSvgData()) + assertNull(svgData.getPngData()) + } + + @Test + fun `test png junction view data`() { + val pngData = JunctionViewData(TEST_DATA_BYTES, ResponseFormat.PNG) + + assertTrue(pngData.isPngData) + assertFalse(pngData.isSvgData) + + assertEquals(JunctionViewData.PngData(TEST_DATA_BYTES), pngData.getPngData()) + assertNull(pngData.getSvgData()) + } + + @Test + fun `test junction view data with unknown format`() { + val data = JunctionViewData(TEST_DATA_BYTES, ResponseFormat.UNKNOWN) + + assertFalse(data.isPngData) + assertFalse(data.isSvgData) + + assertNull(data.getPngData()) + assertNull(data.getSvgData()) + } + + @Test + fun `test PngData toBitmap with default options`() { + val pngData = JunctionViewData.PngData(TEST_DATA_BYTES) + + val mockkBitmap = mockk(relaxed = true) + val decodeOptionsSlot = slot() + every { + BitmapFactory.decodeByteArray(any(), any(), any(), capture(decodeOptionsSlot)) + } returns mockkBitmap + + val bitmap = pngData.getAsBitmap() + + val expectedDefaultOptions = BitmapFactory.Options().apply { + inPreferredConfig = Bitmap.Config.RGB_565 + } + + verify(exactly = 1) { + BitmapFactory.decodeByteArray(TEST_DATA_BYTES, 0, TEST_DATA_BYTES.size, any()) + } + + assertSame(mockkBitmap, bitmap) + assertTrue(equals(expectedDefaultOptions, decodeOptionsSlot.captured)) + } + + @Test + fun `test PngData toBitmap with custom options`() { + val pngData = JunctionViewData.PngData(TEST_DATA_BYTES) + + val mockkBitmap = mockk(relaxed = true) + val decodeOptionsSlot = slot() + every { + BitmapFactory.decodeByteArray(any(), any(), any(), capture(decodeOptionsSlot)) + } returns mockkBitmap + + val bitmapOptions = BitmapFactory.Options().apply { + inPreferredConfig = Bitmap.Config.ARGB_8888 + } + + val bitmap = pngData.getAsBitmap(bitmapOptions) + + verify(exactly = 1) { + BitmapFactory.decodeByteArray(TEST_DATA_BYTES, 0, TEST_DATA_BYTES.size, any()) + } + + assertSame(mockkBitmap, bitmap) + assertTrue(equals(bitmapOptions, decodeOptionsSlot.captured)) + } + + @Test + fun `test SvgData toBitmap with specified width and height`() { + val svgData = JunctionViewData.SvgData(TEST_DATA_BYTES) + + val mockkBitmap = mockk(relaxed = true) + every { SvgUtil.renderAsBitmapWith(any(), any(), any()) } returns mockkBitmap + + val bitmap = svgData.getAsBitmap(width = 200, height = 100) + assertSame(mockkBitmap, bitmap) + + verify(exactly = 1) { + SvgUtil.renderAsBitmapWith(any(), 200, 100) + } + } + + @Test + fun `test SvgData toBitmap with specified width`() { + val svgData = JunctionViewData.SvgData(TEST_DATA_BYTES) + + val mockkBitmap = mockk(relaxed = true) + every { SvgUtil.renderAsBitmapWithWidth(any(), any()) } returns mockkBitmap + + val bitmap = svgData.getAsBitmap(width = 200) + assertSame(mockkBitmap, bitmap) + + verify(exactly = 1) { + SvgUtil.renderAsBitmapWithWidth(any(), 200) + } + } + + @Test + fun testGeneratedEqualsHashcodeToStringFunctions() { + listOf( + JunctionViewData::class.java, + JunctionViewData.SvgData::class.java, + JunctionViewData.PngData::class.java, + ).forEach { + EqualsVerifier.forClass(it).verify() + ToStringVerifier.forClass(it).verify() + } + } + + private companion object { + val TEST_DATA_BYTES = byteArrayOf(1, 2, 3) + + fun equals(options1: BitmapFactory.Options, options2: BitmapFactory.Options): Boolean { + return options1.inPreferredConfig == options2.inPreferredConfig && + options1.inScaled == options2.inScaled && + options1.inMutable == options2.inMutable && + options1.inPremultiplied == options2.inPremultiplied && + options1.inJustDecodeBounds == options2.inJustDecodeBounds && + options1.inBitmap == options2.inBitmap && + options1.inDensity == options2.inDensity && + options1.inPreferredColorSpace == options2.inPreferredColorSpace && + options1.inPreferredConfig == options2.inPreferredConfig && + options1.inSampleSize == options2.inSampleSize && + options1.inScreenDensity == options2.inScreenDensity && + options1.inTargetDensity == options2.inTargetDensity && + options1.outColorSpace == options2.outColorSpace && + options1.outWidth == options2.outWidth && + options1.outHeight == options2.outHeight && + options1.outConfig == options2.outConfig && + options1.outMimeType == options2.outMimeType + } + } +} diff --git a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/callout/api/MapboxRouteCalloutApiTest.kt b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/callout/api/MapboxRouteCalloutApiTest.kt index 161883b1793..6246dd3e765 100644 --- a/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/callout/api/MapboxRouteCalloutApiTest.kt +++ b/libnavui-maps/src/test/java/com/mapbox/navigation/ui/maps/route/callout/api/MapboxRouteCalloutApiTest.kt @@ -19,11 +19,11 @@ import kotlin.time.Duration.Companion.seconds class MapboxRouteCalloutApiTest { @Test - fun `generate no callouts when there is only one route`() { + fun `generate a single callout when there is only one route`() { val routes = createMockRoutes(routeCount = 1) val result = MapboxRouteCalloutApi().setNavigationRoutes(routes) - assertTrue(result.callouts.isEmpty()) + assertTrue(result.callouts.singleOrNull() is RouteCallout.Eta) } @Test 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 f0e17b26f26..11b2636db1f 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 @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalMapboxNavigationAPI::class) + package com.mapbox.navigation.ui.maps.route.line.api import android.content.Context @@ -16,6 +18,7 @@ import com.mapbox.maps.RenderedQueryGeometry import com.mapbox.maps.RenderedQueryOptions import com.mapbox.maps.ScreenCoordinate import com.mapbox.maps.Style +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI import com.mapbox.navigation.base.internal.route.update import com.mapbox.navigation.base.route.NavigationRoute import com.mapbox.navigation.base.trip.model.RouteProgress diff --git a/libnavui-util/gradle.properties b/libnavui-util/gradle.properties index 9a7639a26c1..29298699210 100644 --- a/libnavui-util/gradle.properties +++ b/libnavui-util/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=ui-utils POM_ARTIFACT_TITLE=Mapbox Navigation SDK POM_DESCRIPTION=Artifact that provides basic navigation utilities \ No newline at end of file diff --git a/libnavui-util/src/main/java/com/mapbox/navigation/ui/utils/internal/SvgUtil.kt b/libnavui-util/src/main/java/com/mapbox/navigation/ui/utils/internal/SvgUtil.kt index 3d6ba4c1d08..69547f34d54 100644 --- a/libnavui-util/src/main/java/com/mapbox/navigation/ui/utils/internal/SvgUtil.kt +++ b/libnavui-util/src/main/java/com/mapbox/navigation/ui/utils/internal/SvgUtil.kt @@ -7,7 +7,6 @@ import com.caverock.androidsvg.SVG import com.caverock.androidsvg.SVGExternalFileResolver import com.caverock.androidsvg.SVGParseException import com.mapbox.navigation.utils.internal.logE -import java.io.ByteArrayInputStream import java.io.InputStream object SvgUtil { @@ -15,7 +14,7 @@ object SvgUtil { private const val LOG_CATEGORY = "SvgUtil" fun renderAsBitmapWith( - stream: ByteArrayInputStream, + stream: InputStream, desiredWidth: Int, desiredHeight: Int, cssStyles: String? = null, @@ -40,7 +39,7 @@ object SvgUtil { } fun renderAsBitmapWithHeight( - stream: ByteArrayInputStream, + stream: InputStream, desiredHeight: Int, cssStyles: String? = null, ): Bitmap? { diff --git a/libtesting-navigation-core-utils/src/main/java/com/mapbox/navigation/testing/utils/routes/RoutesProvider.kt b/libtesting-navigation-core-utils/src/main/java/com/mapbox/navigation/testing/utils/routes/RoutesProvider.kt index 3d7c108b256..4ade6acaec6 100644 --- a/libtesting-navigation-core-utils/src/main/java/com/mapbox/navigation/testing/utils/routes/RoutesProvider.kt +++ b/libtesting-navigation-core-utils/src/main/java/com/mapbox/navigation/testing/utils/routes/RoutesProvider.kt @@ -196,6 +196,29 @@ object RoutesProvider { ) } + fun dc_short_alternative_with_fork_point(context: Context): MockRoute { + val jsonResponse = + readRawFileText(context, R.raw.route_response_alternative_and_fork_point) + + val coordinates = listOf( + Point.fromLngLat(-77.02949848947188, 38.90802940158288), + Point.fromLngLat(-77.028611, 38.910507), + ) + + return MockRoute( + jsonResponse, + DirectionsResponse.fromJson(jsonResponse), + listOf( + MockDirectionsRequestHandler( + profile = DirectionsCriteria.PROFILE_DRIVING_TRAFFIC, + jsonResponse = jsonResponse, + expectedCoordinates = coordinates, + ), + ), + coordinates, + ) + } + fun dc_short_with_alternative_reroute(context: Context): MockRoute { val jsonResponse = readRawFileText(context, R.raw.route_response_dc_short_with_alternative_reroute) diff --git a/libtesting-resources/src/main/res/raw/route_response_alternative_and_fork_point.json b/libtesting-resources/src/main/res/raw/route_response_alternative_and_fork_point.json new file mode 100644 index 00000000000..e1095870f06 --- /dev/null +++ b/libtesting-resources/src/main/res/raw/route_response_alternative_and_fork_point.json @@ -0,0 +1,941 @@ +{ + "routes": [ + { + "weight_name": "auto", + "weight": 276.351, + "duration": 137.477, + "distance": 389.138, + "legs": [ + { + "via_waypoints": [], + "admins": [{"iso_3166_1_alpha3": "USA", "iso_3166_1": "US"}], + "weight": 276.351, + "duration": 137.477, + "steps": [ + { + "intersections": [ + { + "entry": [true], + "bearings": [360], + "duration": 16.533, + "mapbox_streets_v8": {"class": "secondary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 20.253, + "geometry_index": 0, + "location": [-77.029628, 38.908013] + }, + { + "entry": [true, false, false, false], + "in": 2, + "bearings": [0, 90, 180, 270], + "duration": 2.244, + "turn_weight": 1, + "turn_duration": 0.007, + "mapbox_streets_v8": {"class": "secondary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 3.573, + "geometry_index": 1, + "location": [-77.029631, 38.908508] + }, + { + "mapbox_streets_v8": {"class": "secondary"}, + "location": [-77.029631, 38.908575], + "geometry_index": 2, + "admin_index": 0, + "weight": 5.15, + "is_urban": true, + "traffic_signal": true, + "turn_duration": 2.022, + "turn_weight": 2, + "duration": 4.652, + "bearings": [17, 89, 180, 270, 344], + "out": 0, + "in": 2, + "entry": [true, true, false, false, false] + }, + { + "entry": [true, false, false, false], + "in": 2, + "bearings": [4, 90, 197, 270], + "duration": 5.857, + "turn_weight": 1, + "turn_duration": 0.048, + "mapbox_streets_v8": {"class": "secondary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 8.245, + "geometry_index": 3, + "location": [-77.029597, 38.908663] + }, + { + "entry": [true, true, false], + "in": 2, + "bearings": [24, 89, 184], + "duration": 8.016, + "turn_weight": 0.75, + "turn_duration": 0.026, + "mapbox_streets_v8": {"class": "secondary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 10.673, + "geometry_index": 6, + "location": [-77.029576, 38.908864] + }, + { + "entry": [true, false, false, false], + "in": 2, + "bearings": [35, 111, 204, 293], + "duration": 2.841, + "turn_weight": 1, + "turn_duration": 0.014, + "mapbox_streets_v8": {"class": "secondary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 4.307, + "geometry_index": 10, + "location": [-77.029492, 38.909009] + }, + { + "bearings": [52, 215], + "entry": [true, false], + "in": 1, + "turn_weight": 0.75, + "mapbox_streets_v8": {"class": "secondary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "geometry_index": 11, + "location": [-77.02945, 38.909055] + } + ], + "maneuver": { + "type": "depart", + "instruction": "Drive north on 13th Street Northwest.", + "bearing_after": 360, + "bearing_before": 0, + "location": [-77.029628, 38.908013] + }, + "name": "13th Street Northwest", + "duration": 49.353, + "distance": 139.887, + "driving_side": "right", + "weight": 63.975, + "mode": "driving", + "geometry": "yeweiAvno|qC}]DeC?oDcAaAYoF?_BOqAc@mAi@gAm@y@k@{AsAmAmBiA{BkAgC" + }, + { + "intersections": [ + { + "entry": [true, false, false], + "in": 1, + "bearings": [53, 232, 259], + "duration": 0.901, + "turn_weight": 5.75, + "turn_duration": 0.007, + "mapbox_streets_v8": {"class": "roundabout"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 6.893, + "geometry_index": 14, + "location": [-77.029265, 38.909169] + }, + { + "lanes": [ + { + "indications": ["straight"], + "valid": false, + "active": false + }, + { + "indications": ["straight"], + "valid_indication": "straight", + "valid": true, + "active": true + }, + { + "indications": ["straight"], + "valid": false, + "active": false + }, + {"indications": ["right"], "valid": false, "active": false} + ], + "entry": [true, false], + "in": 1, + "bearings": [44, 233], + "duration": 4.256, + "mapbox_streets_v8": {"class": "roundabout"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 5.227, + "geometry_index": 15, + "location": [-77.029203, 38.909205] + }, + { + "lanes": [ + { + "indications": ["straight"], + "valid": false, + "active": false + }, + { + "indications": ["straight"], + "valid_indication": "straight", + "valid": true, + "active": true + }, + { + "indications": ["straight"], + "valid": false, + "active": false + }, + {"indications": ["right"], "valid": false, "active": false} + ], + "location": [-77.028972, 38.909425], + "geometry_index": 20, + "admin_index": 0, + "weight": 3.969, + "is_urban": true, + "mapbox_streets_v8": {"class": "roundabout"}, + "traffic_signal": true, + "turn_duration": 2.142, + "duration": 5.33, + "bearings": [9, 37, 215], + "out": 0, + "in": 2, + "entry": [true, true, false] + }, + { + "entry": [false, false, false, true], + "in": 1, + "bearings": [96, 189, 276, 360], + "duration": 2.672, + "turn_duration": 0.036, + "mapbox_streets_v8": {"class": "roundabout"}, + "is_urban": true, + "admin_index": 0, + "out": 3, + "weight": 3.308, + "geometry_index": 22, + "location": [-77.028939, 38.909582] + }, + { + "entry": [false, false, true], + "in": 1, + "bearings": [101, 180, 352], + "duration": 0.195, + "turn_duration": 0.033, + "mapbox_streets_v8": {"class": "roundabout"}, + "is_urban": true, + "admin_index": 0, + "out": 2, + "weight": 0.22, + "geometry_index": 27, + "location": [-77.02894, 38.909713] + }, + { + "lanes": [ + { + "indications": ["straight"], + "valid": false, + "active": false + }, + { + "indications": ["straight", "right"], + "valid_indication": "straight", + "valid": true, + "active": true + }, + {"indications": ["right"], "valid": false, "active": false} + ], + "location": [-77.028943, 38.909729], + "geometry_index": 28, + "admin_index": 0, + "weight": 8.451, + "is_urban": true, + "mapbox_streets_v8": {"class": "primary"}, + "turn_duration": 0.159, + "turn_weight": 5, + "duration": 3.022, + "bearings": [39, 172, 341], + "out": 0, + "in": 1, + "entry": [true, false, true] + }, + { + "entry": [true, false, false], + "in": 2, + "bearings": [59, 175, 219], + "duration": 2.03, + "turn_weight": 0.75, + "turn_duration": 0.025, + "mapbox_streets_v8": {"class": "primary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 3.323, + "geometry_index": 32, + "location": [-77.028811, 38.909857] + }, + { + "entry": [true, false, false, false], + "in": 2, + "bearings": [66, 152, 239, 335], + "duration": 15.612, + "turn_duration": 0.011, + "mapbox_streets_v8": {"class": "primary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 19.11, + "geometry_index": 34, + "location": [-77.028745, 38.909888] + }, + { + "bearings": [1, 67, 180, 246], + "entry": [false, true, false, false], + "in": 3, + "turn_duration": 0.008, + "mapbox_streets_v8": {"class": "primary"}, + "is_urban": true, + "admin_index": 0, + "out": 1, + "geometry_index": 35, + "location": [-77.028196, 38.910077] + } + ], + "maneuver": { + "type": "rotary", + "exit": 2, + "instruction": "Enter Logan Circle Northwest and take the 2nd exit onto Rhode Island Avenue Northwest.", + "modifier": "straight", + "bearing_after": 53, + "bearing_before": 52, + "location": [-77.029265, 38.909169] + }, + "rotary_name": "Logan Circle Northwest", + "name": "Rhode Island Avenue Northwest", + "duration": 36.981, + "distance": 159.611, + "driving_side": "right", + "weight": 54.176, + "mode": "driving", + "geometry": "anyeiA`xn|qCgA{BmAsBoAiBwAyAcBwA}A}@aFs@wAMs@Is@AiA@y@FYB_@DwBsBo@m@o@s@g@q@[u@a@mAyJia@cAqE" + }, + { + "intersections": [ + { + "mapbox_streets_v8": {"class": "tertiary"}, + "location": [-77.028091, 38.910111], + "geometry_index": 36, + "admin_index": 0, + "weight": 14.151, + "is_urban": true, + "traffic_signal": true, + "turn_duration": 4.254, + "turn_weight": 10, + "duration": 7.673, + "bearings": [65, 179, 247, 359], + "out": 3, + "in": 2, + "entry": [true, false, false, true] + }, + { + "mapbox_streets_v8": {"class": "primary"}, + "location": [-77.028094, 38.910256], + "geometry_index": 37, + "admin_index": 0, + "weight": 25.094, + "is_urban": true, + "traffic_signal": true, + "turn_duration": 9.102, + "turn_weight": 22.5, + "duration": 11.117, + "bearings": [66, 179, 244, 359], + "out": 2, + "in": 1, + "entry": [false, false, true, true] + }, + { + "bearings": [1, 64, 182, 246], + "entry": [false, false, false, true], + "in": 1, + "turn_duration": 0.008, + "mapbox_streets_v8": {"class": "primary"}, + "is_urban": true, + "admin_index": 0, + "out": 3, + "geometry_index": 38, + "location": [-77.028193, 38.910219] + } + ], + "maneuver": { + "type": "continue", + "instruction": "Make a left U-turn at 12th Street Northwest onto Rhode Island Avenue Northwest.", + "modifier": "uturn", + "bearing_after": 244, + "bearing_before": 67, + "location": [-77.028091, 38.910111] + }, + "name": "Rhode Island Avenue Northwest", + "duration": 23.247, + "distance": 46.666, + "driving_side": "right", + "weight": 44.692, + "mode": "driving", + "geometry": "}h{eiAtnl|qCaHDhAdEvCzL" + }, + { + "intersections": [ + { + "entry": [false, true, true], + "in": 0, + "bearings": [66, 245, 336], + "duration": 6.138, + "turn_weight": 80, + "turn_duration": 2.105, + "mapbox_streets_v8": {"class": "service"}, + "is_urban": true, + "admin_index": 0, + "out": 2, + "weight": 85.145, + "geometry_index": 39, + "location": [-77.028415, 38.910143] + }, + { + "bearings": [66, 156, 246, 336], + "entry": [false, false, false, true], + "in": 1, + "turn_weight": 2, + "turn_duration": 0.007, + "mapbox_streets_v8": {"class": "service"}, + "is_urban": true, + "admin_index": 0, + "out": 3, + "geometry_index": 40, + "location": [-77.028447, 38.910198] + } + ], + "maneuver": { + "type": "turn", + "instruction": "Turn right.", + "modifier": "right", + "bearing_after": 336, + "bearing_before": 246, + "location": [-77.028415, 38.910143] + }, + "name": "", + "duration": 27.897, + "distance": 42.974, + "driving_side": "right", + "weight": 113.508, + "mode": "driving", + "geometry": "}j{eiA|bm|qCmB~@uHnD}@Vw@N}@B_C?" + }, + { + "intersections": [ + { + "bearings": [175], + "entry": [true], + "in": 0, + "admin_index": 0, + "geometry_index": 45, + "location": [-77.028557, 38.910507] + } + ], + "maneuver": { + "type": "arrive", + "instruction": "Your destination is on the left.", + "modifier": "left", + "bearing_after": 0, + "bearing_before": 355, + "location": [-77.028557, 38.910507] + }, + "name": "", + "duration": 0, + "distance": 0, + "driving_side": "right", + "weight": 0, + "mode": "driving", + "geometry": "ua|eiAxkm|qC??" + } + ], + "distance": 389.138, + "summary": "13th Street Northwest, Logan Circle Northwest" + } + ], + "geometry": "yeweiAvno|qC}]DeC?oDcAaAYoF?_BOqAc@mAi@gAm@y@k@{AsAmAmBiA{BkAgCgA{BmAsBoAiBwAyAcBwA}A}@aFs@wAMs@Is@AiA@y@FYB_@DwBsBo@m@o@s@g@q@[u@a@mAyJia@cAqEaHDhAdEvCzLmB~@uHnD}@Vw@N}@B_C?" + }, + { + "weight_name": "auto", + "weight": 285.1, + "duration": 154.506, + "distance": 405.4, + "legs": [ + { + "via_waypoints": [], + "admins": [{"iso_3166_1_alpha3": "USA", "iso_3166_1": "US"}], + "weight": 285.1, + "duration": 154.506, + "steps": [ + { + "intersections": [ + { + "entry": [true], + "bearings": [360], + "duration": 16.533, + "mapbox_streets_v8": {"class": "secondary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 20.253, + "geometry_index": 0, + "location": [-77.029628, 38.908013] + }, + { + "entry": [true, false, false, false], + "in": 2, + "bearings": [0, 90, 180, 270], + "duration": 2.244, + "turn_weight": 1, + "turn_duration": 0.007, + "mapbox_streets_v8": {"class": "secondary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 3.573, + "geometry_index": 1, + "location": [-77.029631, 38.908508] + }, + { + "mapbox_streets_v8": {"class": "secondary"}, + "location": [-77.029631, 38.908575], + "geometry_index": 2, + "admin_index": 0, + "weight": 5.15, + "is_urban": true, + "traffic_signal": true, + "turn_duration": 2.022, + "turn_weight": 2, + "duration": 4.652, + "bearings": [17, 89, 180, 270, 344], + "out": 0, + "in": 2, + "entry": [true, true, false, false, false] + }, + { + "entry": [true, false, false, false], + "in": 2, + "bearings": [4, 90, 197, 270], + "duration": 5.857, + "turn_weight": 1, + "turn_duration": 0.048, + "mapbox_streets_v8": {"class": "secondary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 8.245, + "geometry_index": 3, + "location": [-77.029597, 38.908663] + }, + { + "entry": [true, true, false], + "in": 2, + "bearings": [24, 89, 184], + "duration": 8.016, + "turn_weight": 0.75, + "turn_duration": 0.026, + "mapbox_streets_v8": {"class": "secondary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 10.673, + "geometry_index": 6, + "location": [-77.029576, 38.908864] + }, + { + "entry": [true, false, false, false], + "in": 2, + "bearings": [35, 111, 204, 293], + "duration": 2.841, + "turn_weight": 1, + "turn_duration": 0.014, + "mapbox_streets_v8": {"class": "secondary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 4.307, + "geometry_index": 10, + "location": [-77.029492, 38.909009] + }, + { + "bearings": [52, 215], + "entry": [true, false], + "in": 1, + "turn_weight": 0.75, + "mapbox_streets_v8": {"class": "secondary"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "geometry_index": 11, + "location": [-77.02945, 38.909055] + } + ], + "maneuver": { + "type": "depart", + "instruction": "Drive north on 13th Street Northwest.", + "bearing_after": 360, + "bearing_before": 0, + "location": [-77.029628, 38.908013] + }, + "name": "13th Street Northwest", + "duration": 49.353, + "distance": 139.887, + "driving_side": "right", + "weight": 63.975, + "mode": "driving", + "geometry": "yeweiAvno|qC}]DeC?oDcAaAYoF?_BOqAc@mAi@gAm@y@k@{AsAmAmBiA{BkAgC" + }, + { + "intersections": [ + { + "entry": [true, false, false], + "in": 1, + "bearings": [53, 232, 259], + "duration": 0.901, + "turn_weight": 5.75, + "turn_duration": 0.007, + "mapbox_streets_v8": {"class": "roundabout"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 6.893, + "geometry_index": 14, + "location": [-77.029265, 38.909169] + }, + { + "lanes": [ + { + "indications": ["straight"], + "valid": false, + "active": false + }, + { + "indications": ["straight"], + "valid": false, + "active": false + }, + { + "indications": ["straight"], + "valid_indication": "straight", + "valid": true, + "active": true + }, + {"indications": ["right"], "valid": false, "active": false} + ], + "entry": [true, false], + "in": 1, + "bearings": [44, 233], + "duration": 4.256, + "mapbox_streets_v8": {"class": "roundabout"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 5.227, + "geometry_index": 15, + "location": [-77.029203, 38.909205] + }, + { + "lanes": [ + { + "indications": ["straight"], + "valid": false, + "active": false + }, + { + "indications": ["straight"], + "valid": false, + "active": false + }, + { + "indications": ["straight"], + "valid_indication": "straight", + "valid": true, + "active": true + }, + {"indications": ["right"], "valid": false, "active": false} + ], + "location": [-77.028972, 38.909425], + "geometry_index": 20, + "admin_index": 0, + "weight": 3.969, + "is_urban": true, + "mapbox_streets_v8": {"class": "roundabout"}, + "traffic_signal": true, + "turn_duration": 2.142, + "duration": 5.33, + "bearings": [9, 37, 215], + "out": 0, + "in": 2, + "entry": [true, true, false] + }, + { + "entry": [false, false, false, true], + "in": 1, + "bearings": [96, 189, 276, 360], + "duration": 2.672, + "turn_duration": 0.036, + "mapbox_streets_v8": {"class": "roundabout"}, + "is_urban": true, + "admin_index": 0, + "out": 3, + "weight": 3.308, + "geometry_index": 22, + "location": [-77.028939, 38.909582] + }, + { + "entry": [false, false, true], + "in": 1, + "bearings": [101, 180, 352], + "duration": 0.195, + "turn_duration": 0.033, + "mapbox_streets_v8": {"class": "roundabout"}, + "is_urban": true, + "admin_index": 0, + "out": 2, + "weight": 0.22, + "geometry_index": 27, + "location": [-77.02894, 38.909713] + }, + { + "entry": [true, false, true], + "in": 1, + "bearings": [39, 172, 341], + "duration": 2.777, + "lanes": [ + { + "indications": ["straight"], + "valid": false, + "active": false + }, + { + "indications": ["straight", "right"], + "valid_indication": "straight", + "valid": true, + "active": true + }, + {"indications": ["right"], "valid": false, "active": false} + ], + "turn_duration": 0.041, + "mapbox_streets_v8": {"class": "roundabout"}, + "is_urban": true, + "admin_index": 0, + "out": 2, + "weight": 3.408, + "geometry_index": 28, + "location": [-77.028943, 38.909729] + }, + { + "entry": [false, false, false, true], + "in": 1, + "bearings": [68, 161, 246, 326], + "duration": 3.357, + "turn_duration": 0.048, + "mapbox_streets_v8": {"class": "roundabout"}, + "is_urban": true, + "admin_index": 0, + "out": 3, + "weight": 4.043, + "geometry_index": 33, + "location": [-77.029007, 38.90987] + }, + { + "entry": [false, false, true], + "in": 1, + "bearings": [106, 145, 311], + "duration": 0.175, + "turn_duration": 0.052, + "mapbox_streets_v8": {"class": "roundabout"}, + "is_urban": true, + "admin_index": 0, + "out": 2, + "weight": 0.11, + "geometry_index": 38, + "location": [-77.029151, 38.910033] + }, + { + "lanes": [ + { + "indications": ["straight"], + "valid": false, + "active": false + }, + { + "indications": ["straight", "right"], + "valid_indication": "right", + "valid": true, + "active": true + } + ], + "location": [-77.029163, 38.910041], + "geometry_index": 39, + "admin_index": 0, + "weight": 18.86, + "is_urban": true, + "mapbox_streets_v8": {"class": "street"}, + "turn_duration": 0.1, + "turn_weight": 5, + "duration": 11.471, + "bearings": [131, 307, 350], + "out": 2, + "in": 0, + "entry": [false, true, true] + }, + { + "entry": [true, false, false, false], + "in": 2, + "bearings": [6, 115, 171, 296], + "duration": 24.736, + "turn_weight": 2, + "turn_duration": 0.02, + "mapbox_streets_v8": {"class": "street"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 32.24, + "geometry_index": 42, + "location": [-77.029203, 38.910237] + }, + { + "bearings": [25, 205, 296], + "entry": [true, false, true], + "in": 1, + "turn_weight": 1, + "turn_duration": 0.019, + "mapbox_streets_v8": {"class": "street"}, + "is_urban": true, + "admin_index": 0, + "out": 0, + "geometry_index": 49, + "location": [-77.029041, 38.910645] + } + ], + "maneuver": { + "type": "rotary", + "exit": 3, + "instruction": "Enter Logan Circle Northwest and take the 3rd exit onto Vermont Avenue Northwest.", + "modifier": "straight", + "bearing_after": 53, + "bearing_before": 52, + "location": [-77.029265, 38.909169] + }, + "rotary_name": "Logan Circle Northwest", + "name": "Vermont Avenue Northwest", + "duration": 68.49, + "distance": 207.6, + "driving_side": "right", + "weight": 94.397, + "mode": "driving", + "geometry": "anyeiA`xn|qCgA{BmAsBoAiBwAyAcBwA}A}@aFs@wAMs@Is@AiA@y@FYB_@DG@{@P_AVeA^oAr@gAr@cAv@{@r@}@|@_A`AOViGz@y@LcADqACsAKgAMkAQaAW_A_@sJ{EoKkF" + }, + { + "intersections": [ + { + "entry": [true, true, false], + "in": 2, + "bearings": [24, 114, 205], + "duration": 7.776, + "turn_weight": 82, + "turn_duration": 1.908, + "mapbox_streets_v8": {"class": "service"}, + "is_urban": true, + "admin_index": 0, + "out": 1, + "weight": 89.35, + "geometry_index": 50, + "location": [-77.028923, 38.910845] + }, + { + "bearings": [25, 115, 206, 294], + "entry": [false, true, false, false], + "in": 3, + "turn_weight": 2, + "turn_duration": 0.007, + "mapbox_streets_v8": {"class": "service"}, + "is_urban": true, + "admin_index": 0, + "out": 1, + "geometry_index": 51, + "location": [-77.02882, 38.910809] + } + ], + "maneuver": { + "type": "turn", + "instruction": "Turn right.", + "modifier": "right", + "bearing_after": 114, + "bearing_before": 25, + "location": [-77.028923, 38.910845] + }, + "name": "", + "duration": 36.663, + "distance": 57.913, + "driving_side": "right", + "weight": 126.727, + "mode": "driving", + "geometry": "yv|eiAtbn|qCfAmEzDkO~KA" + }, + { + "intersections": [ + { + "bearings": [0], + "entry": [true], + "in": 0, + "admin_index": 0, + "geometry_index": 53, + "location": [-77.028557, 38.910507] + } + ], + "maneuver": { + "type": "arrive", + "instruction": "Your destination is on the right.", + "modifier": "right", + "bearing_after": 0, + "bearing_before": 180, + "location": [-77.028557, 38.910507] + }, + "name": "", + "duration": 0, + "distance": 0, + "driving_side": "right", + "weight": 0, + "mode": "driving", + "geometry": "ua|eiAxkm|qC??" + } + ], + "distance": 405.4, + "summary": "13th Street Northwest, Logan Circle Northwest" + } + ], + "geometry": "yeweiAvno|qC}]DeC?oDcAaAYoF?_BOqAc@mAi@gAm@y@k@{AsAmAmBiA{BkAgCgA{BmAsBoAiBwAyAcBwA}A}@aFs@wAMs@Is@AiA@y@FYB_@DG@{@P_AVeA^oAr@gAr@cAv@{@r@}@|@_A`AOViGz@y@LcADqACsAKgAMkAQaAW_A_@sJ{EoKkFfAmEzDkO~KA" + } + ], + "waypoints": [ + { + "distance": 0.926, + "name": "13th Street Northwest", + "location": [-77.029628, 38.908013] + }, + {"distance": 4.657, "name": "", "location": [-77.028557, 38.910507]} + ], + "code": "Ok", + "uuid": "mK-PkEwk7pnJBTMJLrqe5BVATqWp2dpaLUwtDXn3UZ42CTXviDma1A==" +} \ No newline at end of file diff --git a/libtesting-router/gradle.properties b/libtesting-router/gradle.properties index 9fd49e17d9d..c732fe7192a 100644 --- a/libtesting-router/gradle.properties +++ b/libtesting-router/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=test-router POM_ARTIFACT_TITLE=Mapbox Navigation Test Router POM_DESCRIPTION=Artifact that provides testing infrastructure to setup routes in test scenarios \ No newline at end of file diff --git a/libtrip-notification/gradle.properties b/libtrip-notification/gradle.properties index 0ad502757d5..46e5bf99058 100644 --- a/libtrip-notification/gradle.properties +++ b/libtrip-notification/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=notification POM_ARTIFACT_TITLE=Mapbox Navigation Notification POM_DESCRIPTION=Artifact that provides the default implementation of the navigation notification diff --git a/ui-components/gradle.properties b/ui-components/gradle.properties index dcd15b9dd18..c2f5b287403 100644 --- a/ui-components/gradle.properties +++ b/ui-components/gradle.properties @@ -1,3 +1,2 @@ -POM_ARTIFACT_ID=ui-components POM_ARTIFACT_TITLE=Mapbox Navigation SDK POM_DESCRIPTION=Artifact that provides UI compoents \ No newline at end of file