From c3b3fa059641c2ce833d8de1a6bace0c5132b324 Mon Sep 17 00:00:00 2001 From: Eike Send Date: Mon, 23 Sep 2024 16:36:37 +0200 Subject: [PATCH 01/10] Start working on an example that communicates directly with a Valhalla backend --- app/src/main/AndroidManifest.xml | 8 + .../navigation/testapp/MainActivity.java | 11 +- .../testapp/ValhallaMockNavigationActivity.kt | 302 ++++++++++++++++++ .../activity_valhalla_mock_navigation.xml | 91 ++++++ app/src/main/res/values/strings.xml | 4 + 5 files changed, 413 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/mapbox/services/android/navigation/testapp/ValhallaMockNavigationActivity.kt create mode 100644 app/src/main/res/layout/activity_valhalla_mock_navigation.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 33374ac4..9008e785 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + @@ -14,6 +15,13 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> + + + diff --git a/app/src/main/java/com/mapbox/services/android/navigation/testapp/MainActivity.java b/app/src/main/java/com/mapbox/services/android/navigation/testapp/MainActivity.java index 1f68f783..be9633da 100644 --- a/app/src/main/java/com/mapbox/services/android/navigation/testapp/MainActivity.java +++ b/app/src/main/java/com/mapbox/services/android/navigation/testapp/MainActivity.java @@ -39,9 +39,14 @@ protected void onCreate(Bundle savedInstanceState) { // Specify an adapter list.add(new SampleItem( - getString(R.string.title_mock_navigation), - getString(R.string.description_mock_navigation), - MockNavigationActivity.class + getString(R.string.title_mock_navigation), + getString(R.string.description_mock_navigation), + MockNavigationActivity.class + )); + list.add(new SampleItem( + getString(R.string.title_valhalla_mock_navigation), + getString(R.string.description_vallhalla_mock_navigation), + ValhallaMockNavigationActivity.class )); list.add(new SampleItem( getString(R.string.title_navigation_ui), diff --git a/app/src/main/java/com/mapbox/services/android/navigation/testapp/ValhallaMockNavigationActivity.kt b/app/src/main/java/com/mapbox/services/android/navigation/testapp/ValhallaMockNavigationActivity.kt new file mode 100644 index 00000000..0bcccfde --- /dev/null +++ b/app/src/main/java/com/mapbox/services/android/navigation/testapp/ValhallaMockNavigationActivity.kt @@ -0,0 +1,302 @@ +package com.mapbox.services.android.navigation.testapp + +//import com.mapbox.api.directions.v5.models.DirectionsResponse +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.snackbar.Snackbar +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.mapbox.api.directions.v5.DirectionsAdapterFactory +import com.mapbox.geojson.Point +import com.mapbox.mapboxsdk.annotations.MarkerOptions +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.geometry.LatLng +import com.mapbox.mapboxsdk.location.LocationComponent +import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions +import com.mapbox.mapboxsdk.location.modes.CameraMode +import com.mapbox.mapboxsdk.location.modes.RenderMode +import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.maps.OnMapReadyCallback +import com.mapbox.mapboxsdk.maps.Style +import com.mapbox.services.android.navigation.testapp.databinding.ActivityNavigationUiBinding +import com.mapbox.services.android.navigation.ui.v5.NavigationLauncher +import com.mapbox.services.android.navigation.ui.v5.NavigationLauncherOptions +import com.mapbox.services.android.navigation.ui.v5.route.NavigationRoute +import com.mapbox.services.android.navigation.v5.milestone.* +import com.mapbox.services.android.navigation.v5.models.DirectionsResponse +import com.mapbox.services.android.navigation.v5.models.DirectionsRoute +import com.mapbox.services.android.navigation.v5.navigation.* +import com.mapbox.turf.TurfConstants +import com.mapbox.turf.TurfMeasurement +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import timber.log.Timber +import java.io.IOException + +class ValhallaMockNavigationActivity : + AppCompatActivity(), + OnMapReadyCallback, + MapboxMap.OnMapClickListener { + private lateinit var mapboxMap: MapboxMap + + // Navigation related variables + private var route: DirectionsRoute? = null + private var navigationMapRoute: NavigationMapRoute? = null + private var destination: Point? = null + private var waypoint: Point? = null + private var locationComponent: LocationComponent? = null + + private lateinit var binding: ActivityNavigationUiBinding + + private var simulateRoute = false + + @SuppressLint("MissingPermission") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + + binding = ActivityNavigationUiBinding.inflate(layoutInflater) + setContentView(binding.root) + binding.mapView.apply { + onCreate(savedInstanceState) + getMapAsync(this@ValhallaMockNavigationActivity) + } + + binding.startRouteButton.setOnClickListener { + route?.let { route -> + val userLocation = mapboxMap.locationComponent.lastKnownLocation ?: return@let + val options = NavigationLauncherOptions.builder() + .directionsRoute(route) + .shouldSimulateRoute(simulateRoute) + .initialMapCameraPosition(CameraPosition.Builder().target(LatLng(userLocation.latitude, userLocation.longitude)).build()) + .lightThemeResId(R.style.TestNavigationViewLight) + .darkThemeResId(R.style.TestNavigationViewDark) + .build() + NavigationLauncher.startNavigation(this@ValhallaMockNavigationActivity, options) + } + } + + binding.simulateRouteSwitch.setOnCheckedChangeListener { _, checked -> + simulateRoute = checked + } + + binding.clearPoints.setOnClickListener { + if (::mapboxMap.isInitialized) { + mapboxMap.markers.forEach { + mapboxMap.removeMarker(it) + } + } + destination = null + waypoint = null + it.visibility = View.GONE + binding.startRouteLayout.visibility = View.GONE + + navigationMapRoute?.removeRoute() + } + } + + override fun onMapReady(mapboxMap: MapboxMap) { + this.mapboxMap = mapboxMap + mapboxMap.setStyle(Style.Builder().fromUri(getString(R.string.map_style_light))) { style -> + enableLocationComponent(style) + } + + navigationMapRoute = NavigationMapRoute( + binding.mapView, + mapboxMap + ) + + mapboxMap.addOnMapClickListener(this) + Snackbar.make( + findViewById(R.id.container), + "Tap map to place waypoint", + Snackbar.LENGTH_LONG, + ).show() + } + + @SuppressWarnings("MissingPermission") + private fun enableLocationComponent(style: Style) { + // Get an instance of the component + locationComponent = mapboxMap.locationComponent + + locationComponent?.let { + // Activate with a built LocationComponentActivationOptions object + it.activateLocationComponent( + LocationComponentActivationOptions.builder(this, style).build(), + ) + + // Enable to make component visible + it.isLocationComponentEnabled = true + + // Set the component's camera mode + it.cameraMode = CameraMode.TRACKING_GPS_NORTH + + // Set the component's render mode + it.renderMode = RenderMode.NORMAL + } + } + + override fun onMapClick(point: LatLng): Boolean { + var addMarker = true + when { + destination == null -> destination = Point.fromLngLat(point.longitude, point.latitude) + waypoint == null -> waypoint = Point.fromLngLat(point.longitude, point.latitude) + else -> { + Toast.makeText(this, "Only 2 waypoints supported", Toast.LENGTH_LONG).show() + addMarker = false + } + } + + if (addMarker) { + mapboxMap.addMarker(MarkerOptions().position(point)) + binding.clearPoints.visibility = View.VISIBLE + } + calculateRoute() + return true + } + + private fun calculateRoute() { + binding.startRouteLayout.visibility = View.GONE + val userLocation = mapboxMap.locationComponent.lastKnownLocation + val destination = destination + if (userLocation == null) { + Timber.d("calculateRoute: User location is null, therefore, origin can't be set.") + return + } + + if (destination == null) { + Timber.d("calculateRoute: destination is null, therefore, origin can't be set.") + return + } + + val origin = Point.fromLngLat(userLocation.longitude, userLocation.latitude) + if (TurfMeasurement.distance(origin, destination, TurfConstants.UNIT_METERS) < 50) { + Timber.d("calculateRoute: distance < 50 m") + binding.startRouteButton.visibility = View.GONE + return + } + + // Construct the request body using mapOf + val requestBody = mapOf( + "format" to "osrm", + "costing" to "auto", + "banner_instructions" to true, + "voice_instructions" to true, + "directions_options" to mapOf( + "units" to "kilometers" + ), + "costing_options" to mapOf( + "auto" to mapOf( + "shortest" to true + ) + ), + "locations" to listOf( + mapOf( + "lon" to origin.longitude(), + "lat" to origin.latitude(), + "type" to "break" + ), + mapOf( + "lon" to destination.longitude(), + "lat" to destination.latitude(), + "type" to "break" + ) + ) + ) + + // Convert the map to JSON using Gson + val requestBodyJson = Gson().toJson(requestBody) + + // Create OkHttp client + val client = OkHttpClient() + + // Create request object. Requires valhalla_url to be set in developer-config.xml + // Don't use this server in production, it is for demonstration purposes only: + // https://valhalla1.openstreetmap.de/route + val request = Request.Builder() + .url(getString(R.string.valhalla_url)) + .post(requestBodyJson.toRequestBody("application/json; charset=utf-8".toMediaType())) + .build() + + Timber.d("calculateRoute request will be enqueued") + Timber.d( + "calculateRoute requestBodyJson: %s", + requestBodyJson + ) + client.newCall(request).enqueue(object : okhttp3.Callback { + + override fun onFailure(call: okhttp3.Call, e: IOException) { + // Handle request failure + Timber.e(e, "calculateRoute Failed to get route from ValhallaRouting") + } + + override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) { + response.use { + if (response.isSuccessful) { + Timber.e( + "calculateRoute to ValhallaRouting successful with status code: %s", + response.code + ) + Timber.d("calculateRoute ValhallaRouting body: %s", response.body) + val jsonResponse = response.body!!.string() + Timber.d("calculateRoute ValhallaRouting response: %s", jsonResponse) + val maplibreResponse = DirectionsResponse.fromJson(jsonResponse); + this@ValhallaMockNavigationActivity.route = maplibreResponse.routes().first() + runOnUiThread { + navigationMapRoute?.addRoutes(maplibreResponse.routes()) + binding.startRouteLayout.visibility = View.VISIBLE + } + } else { + Timber.e("calculateRoute Request to Valhalla failed with status code: %s: %s", response.code, response.body) + } + } + } + }) + } + + override fun onResume() { + super.onResume() + binding.mapView.onResume() + } + + override fun onPause() { + super.onPause() + binding.mapView.onPause() + } + + override fun onStart() { + super.onStart() + binding.mapView.onStart() + } + + override fun onStop() { + super.onStop() + binding.mapView.onStop() + } + + override fun onLowMemory() { + super.onLowMemory() + binding.mapView.onLowMemory() + } + + override fun onDestroy() { + super.onDestroy() + if (::mapboxMap.isInitialized) { + mapboxMap.removeOnMapClickListener(this) + } + binding.mapView.onDestroy() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + binding.mapView.onSaveInstanceState(outState) + } +} diff --git a/app/src/main/res/layout/activity_valhalla_mock_navigation.xml b/app/src/main/res/layout/activity_valhalla_mock_navigation.xml new file mode 100644 index 00000000..24aff9b3 --- /dev/null +++ b/app/src/main/res/layout/activity_valhalla_mock_navigation.xml @@ -0,0 +1,91 @@ + + + + + +