Skip to content

Commit d7b1394

Browse files
authored
added graphhopper sample (#162)
* added graphhopper sample * use graphhopper URL in both samples that works with a demo API key
1 parent a30fc5c commit d7b1394

File tree

7 files changed

+354
-26
lines changed

7 files changed

+354
-26
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ MapLibre welcomes participation and contributions from everyone.
1212
- Fix crash on MapFpsDelegate, caused by null modifier [#141](https://github.com/maplibre/maplibre-navigation-android/issues/141)
1313
- Add explicit setting of leg/step index to MapLibreNavigation [#164](https://github.com/maplibre/maplibre-navigation-android/pull/164)
1414

15+
Added sample code on how to use the GraphHopper routing server directly in GraphHopperNavigationActivity. Please make sure to add this line to the app/main/res/values/developer-config.xml:
16+
17+
```xml
18+
<string name="graphhopper_url" translatable="false">https://graphhopper.com/api/1/navigate?key=YOUR_API_KEY</string>
19+
```
20+
1521
### v5.0.0-pre5 - May 16, 2025
1622

1723
- Fix threading and platform characteristics for Apple location engine [#159](https://github.com/maplibre/maplibre-navigation-android/pull/159)

app/src/main/AndroidManifest.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
android:name="android.support.PARENT_ACTIVITY"
2929
android:value=".MainActivity" />
3030
</activity>
31+
<activity
32+
android:name=".GraphHopperNavigationActivity"
33+
android:label="@string/title_graphhopper_navigation">
34+
<meta-data
35+
android:name="android.support.PARENT_ACTIVITY"
36+
android:value=".MainActivity" />
37+
</activity>
3138
<activity
3239
android:name=".NavigationUIActivity"
3340
android:label="@string/title_navigation_ui">
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
package org.maplibre.navigation.android.example
2+
3+
import android.annotation.SuppressLint
4+
import android.os.Bundle
5+
import android.view.View
6+
import androidx.appcompat.app.AppCompatActivity
7+
import com.google.android.material.snackbar.Snackbar
8+
import com.google.gson.Gson
9+
import org.maplibre.geojson.model.Point
10+
import org.maplibre.android.annotations.MarkerOptions
11+
import org.maplibre.android.camera.CameraPosition
12+
import org.maplibre.android.geometry.LatLng
13+
import org.maplibre.android.location.LocationComponent
14+
import org.maplibre.android.location.LocationComponentActivationOptions
15+
import org.maplibre.android.location.modes.CameraMode
16+
import org.maplibre.android.location.modes.RenderMode
17+
import org.maplibre.android.maps.MapLibreMap
18+
import org.maplibre.android.maps.OnMapReadyCallback
19+
import org.maplibre.android.maps.Style
20+
import org.maplibre.navigation.android.example.databinding.ActivityNavigationUiBinding
21+
import org.maplibre.navigation.android.navigation.ui.v5.NavigationLauncher
22+
import org.maplibre.navigation.android.navigation.ui.v5.NavigationLauncherOptions
23+
import org.maplibre.navigation.core.models.DirectionsResponse
24+
import org.maplibre.navigation.core.models.DirectionsRoute
25+
import org.maplibre.navigation.core.models.RouteOptions
26+
import okhttp3.MediaType.Companion.toMediaType
27+
import okhttp3.OkHttpClient
28+
import okhttp3.Request
29+
import okhttp3.RequestBody.Companion.toRequestBody
30+
import org.maplibre.geojson.turf.TurfUnit
31+
import org.maplibre.navigation.android.navigation.ui.v5.route.NavigationMapRoute
32+
import timber.log.Timber
33+
import java.io.IOException
34+
import java.util.Locale
35+
36+
class GraphHopperNavigationActivity :
37+
AppCompatActivity(),
38+
OnMapReadyCallback,
39+
MapLibreMap.OnMapClickListener {
40+
private lateinit var mapLibreMap: MapLibreMap
41+
42+
// Navigation related variables
43+
private var language = Locale.getDefault().language
44+
private var route: DirectionsRoute? = null
45+
private var navigationMapRoute: NavigationMapRoute? = null
46+
private var destination: Point? = null
47+
private var locationComponent: LocationComponent? = null
48+
49+
private lateinit var binding: ActivityNavigationUiBinding
50+
51+
private var simulateRoute = false
52+
53+
@SuppressLint("MissingPermission")
54+
override fun onCreate(savedInstanceState: Bundle?) {
55+
super.onCreate(savedInstanceState)
56+
57+
if (BuildConfig.DEBUG) {
58+
Timber.plant(Timber.DebugTree())
59+
}
60+
61+
binding = ActivityNavigationUiBinding.inflate(layoutInflater)
62+
setContentView(binding.root)
63+
binding.mapView.apply {
64+
onCreate(savedInstanceState)
65+
getMapAsync(this@GraphHopperNavigationActivity)
66+
}
67+
68+
binding.startRouteButton.setOnClickListener {
69+
route?.let { route ->
70+
val userLocation = mapLibreMap.locationComponent.lastKnownLocation ?: return@let
71+
val options = NavigationLauncherOptions.builder()
72+
.directionsRoute(route)
73+
.shouldSimulateRoute(simulateRoute)
74+
.initialMapCameraPosition(
75+
CameraPosition.Builder()
76+
.target(LatLng(userLocation.latitude, userLocation.longitude)).build()
77+
)
78+
.lightThemeResId(R.style.TestNavigationViewLight)
79+
.darkThemeResId(R.style.TestNavigationViewDark)
80+
.build()
81+
NavigationLauncher.startNavigation(this@GraphHopperNavigationActivity, options)
82+
}
83+
}
84+
85+
binding.simulateRouteSwitch.setOnCheckedChangeListener { _, checked ->
86+
simulateRoute = checked
87+
}
88+
89+
binding.clearPoints.setOnClickListener {
90+
if (::mapLibreMap.isInitialized) {
91+
mapLibreMap.markers.forEach {
92+
mapLibreMap.removeMarker(it)
93+
}
94+
}
95+
destination = null
96+
it.visibility = View.GONE
97+
binding.startRouteLayout.visibility = View.GONE
98+
99+
navigationMapRoute?.removeRoute()
100+
}
101+
}
102+
103+
override fun onMapReady(mapLibreMap: MapLibreMap) {
104+
this.mapLibreMap = mapLibreMap
105+
mapLibreMap.setStyle(
106+
Style.Builder().fromUri(getString(R.string.map_style_light))
107+
) { style ->
108+
enableLocationComponent(style)
109+
navigationMapRoute = NavigationMapRoute(binding.mapView, mapLibreMap)
110+
mapLibreMap.addOnMapClickListener(this)
111+
112+
Snackbar.make(
113+
findViewById(R.id.container),
114+
"Tap map to place destination",
115+
Snackbar.LENGTH_LONG,
116+
).show()
117+
}
118+
}
119+
120+
@SuppressWarnings("MissingPermission")
121+
private fun enableLocationComponent(style: Style) {
122+
// Get an instance of the component
123+
locationComponent = mapLibreMap.locationComponent
124+
125+
locationComponent?.let {
126+
// Activate with a built LocationComponentActivationOptions object
127+
it.activateLocationComponent(
128+
LocationComponentActivationOptions.builder(this, style).build(),
129+
)
130+
131+
// Enable to make component visible
132+
it.isLocationComponentEnabled = true
133+
134+
// Set the component's camera mode
135+
it.cameraMode = CameraMode.TRACKING_GPS_NORTH
136+
137+
// Set the component's render mode
138+
it.renderMode = RenderMode.NORMAL
139+
}
140+
}
141+
142+
override fun onMapClick(point: LatLng): Boolean {
143+
destination = Point(point.longitude, point.latitude)
144+
145+
mapLibreMap.addMarker(MarkerOptions().position(point))
146+
binding.clearPoints.visibility = View.VISIBLE
147+
calculateRoute()
148+
return true
149+
}
150+
151+
private fun calculateRoute() {
152+
binding.startRouteLayout.visibility = View.GONE
153+
val userLocation = mapLibreMap.locationComponent.lastKnownLocation
154+
val destination = destination
155+
if (userLocation == null) {
156+
Timber.d("calculateRoute: User location is null, therefore, origin can't be set.")
157+
return
158+
}
159+
160+
if (destination == null) {
161+
Timber.d("calculateRoute: destination is null, therefore, destination can't be set.")
162+
return
163+
}
164+
165+
val origin = Point(userLocation.longitude, userLocation.latitude)
166+
if (org.maplibre.geojson.turf.TurfMeasurement.distance(origin, destination, TurfUnit.METRES) < 50) {
167+
Timber.d("calculateRoute: distance < 50 m")
168+
binding.startRouteButton.visibility = View.GONE
169+
return
170+
}
171+
172+
// The full GraphHopper API is documented here:
173+
// https://docs.graphhopper.com/openapi/routing
174+
val requestBody = mapOf(
175+
"type" to "mapbox",
176+
"profile" to "car",
177+
"locale" to language,
178+
"points" to listOf(
179+
listOf(origin.longitude, origin.latitude),
180+
listOf(destination.longitude, destination.latitude)
181+
)
182+
)
183+
184+
val requestBodyJson = Gson().toJson(requestBody)
185+
val client = OkHttpClient()
186+
187+
// Create request object. Requires graphhopper_url to be set in developer-config.xml
188+
val request = Request.Builder()
189+
.header("User-Agent", "MapLibre Android Navigation SDK Demo App")
190+
.url(getString(R.string.graphhopper_url))
191+
.post(requestBodyJson.toRequestBody("application/json; charset=utf-8".toMediaType()))
192+
.build()
193+
194+
Timber.d("calculateRoute enqueued requestBodyJson: %s", requestBodyJson)
195+
client.newCall(request).enqueue(object : okhttp3.Callback {
196+
197+
override fun onFailure(call: okhttp3.Call, e: IOException) {
198+
Timber.e(e, "calculateRoute Failed to get route from GraphHopperRouting")
199+
}
200+
201+
override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
202+
response.use {
203+
if (response.isSuccessful) {
204+
Timber.e(
205+
"calculateRoute to GraphHopperRouting successful with status code: %s",
206+
response.code
207+
)
208+
val responseBodyJson = response.body!!.string()
209+
Timber.d(
210+
"calculateRoute GraphHopperRouting responseBodyJson: %s",
211+
responseBodyJson
212+
)
213+
val maplibreResponse = DirectionsResponse.fromJson(responseBodyJson);
214+
this@GraphHopperNavigationActivity.route = maplibreResponse.routes
215+
.first()
216+
.copy(
217+
routeOptions = RouteOptions(
218+
// See ValhallaNavigationActivity why these dummy route options are necessary
219+
baseUrl = "https://valhalla.routing",
220+
profile = "valhalla",
221+
user = "valhalla",
222+
accessToken = "valhalla",
223+
voiceInstructions = true,
224+
bannerInstructions = true,
225+
language = language,
226+
coordinates = listOf(origin, destination),
227+
requestUuid = "0000-0000-0000-0000"
228+
)
229+
)
230+
231+
runOnUiThread {
232+
navigationMapRoute?.addRoutes(maplibreResponse.routes)
233+
binding.startRouteLayout.visibility = View.VISIBLE
234+
}
235+
} else {
236+
Timber.e(
237+
"calculateRoute Request to GraphHopper failed with status code: %s: %s",
238+
response.code,
239+
response.body
240+
)
241+
}
242+
}
243+
}
244+
})
245+
}
246+
247+
override fun onResume() {
248+
super.onResume()
249+
binding.mapView.onResume()
250+
}
251+
252+
override fun onPause() {
253+
super.onPause()
254+
binding.mapView.onPause()
255+
}
256+
257+
override fun onStart() {
258+
super.onStart()
259+
binding.mapView.onStart()
260+
}
261+
262+
override fun onStop() {
263+
super.onStop()
264+
binding.mapView.onStop()
265+
}
266+
267+
override fun onLowMemory() {
268+
super.onLowMemory()
269+
binding.mapView.onLowMemory()
270+
}
271+
272+
override fun onDestroy() {
273+
super.onDestroy()
274+
if (::mapLibreMap.isInitialized) {
275+
mapLibreMap.removeOnMapClickListener(this)
276+
}
277+
binding.mapView.onDestroy()
278+
}
279+
280+
override fun onSaveInstanceState(outState: Bundle) {
281+
super.onSaveInstanceState(outState)
282+
binding.mapView.onSaveInstanceState(outState)
283+
}
284+
}

app/src/main/java/org/maplibre/navigation/android/example/MainActivity.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ protected void onCreate(Bundle savedInstanceState) {
4848
getString(R.string.description_vallhalla_navigation),
4949
ValhallaNavigationActivity.class
5050
));
51+
list.add(new SampleItem(
52+
getString(R.string.title_graphhopper_navigation),
53+
getString(R.string.description_graphhopper_navigation),
54+
GraphHopperNavigationActivity.class
55+
));
5156
list.add(new SampleItem(
5257
getString(R.string.title_navigation_ui),
5358
getString(R.string.description_navigation_ui),

app/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
<string name="title_valhalla_navigation">Valhalla Navigation</string>
2020
<string name="description_vallhalla_navigation">Use Valhalla routing server to generate directions.</string>
2121

22+
<string name="title_graphhopper_navigation">GraphHopper Navigation</string>
23+
<string name="description_graphhopper_navigation">Use GraphHopper routing server to generate directions.</string>
24+
2225
<string name="title_off_route_detection">Off route detection</string>
2326
<string name="description_off_route_detection">Uses the Route Utils class to determine if a users off route.</string>
2427

gradle/developer-config.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ task accessToken {
1717
" <!-- Your valhalla url (example: https://valhalla1.openstreetmap.de/route) -->\n" +
1818
" <!-- Don't use the following server in production, it is for demonstration purposes only: -->\n" +
1919
" <string name=\"valhalla_url\" translatable=\"false\">https://valhalla1.openstreetmap.de/route</string>\n" +
20+
" <!-- Instead of valhalla you can use GraphHopper for the path finding. Example: https://graphhopper.com/api/1/navigate?key=YOUR_API_KEY or a local server -->\n" +
21+
" <!-- Don't use the following API key in production, it is for demonstration purposes only: -->\n" +
22+
" <string name=\"graphhopper_url\" translatable=\"false\">https://graphhopper.com/api/1/navigate?key=7088b84f-4cee-4059-96de-fd0cbda2fdff</string>\n" +
2023
" <!-- Your Mapbox access token (example: pk.abc...) -->\n" +
2124
" <string name=\"mapbox_access_token\" translatable=\"false\">" + mapboxAccessToken + "</string>\n" +
2225
" <!-- Map tile provider for light design (example: https://api.maptiler.com/maps/basic-v2/style.json?key=...) -->\n" +

0 commit comments

Comments
 (0)