Navigation simulation with on-demand map download¶
In this guide you will learn how to download a specific map from the online content store, and simulate online navigation along a route while downloading map data on demand.
Setup¶
First, get an API key token, see the Getting Started guide.
Download the Maps & Navigation SDK for Android archive fileDownload the DownloadingOnboardMapSimulation project
archive file or clone the project with Git
See the Configure Android Example guide.
Run the example¶
In Android Studio, from the File
menu, select Sync Project with Gradle Files
Downloads a specific map, then simulates online navigation along a route while downloading map data on demand if the navigation goes outside the downloaded map.
How it works¶
You can open the MainActivity.kt file to see how the route computation and simulated navigation are set up.
1<uses-permission android:name="android.permission.INTERNET" />
Note that, in AndroidManifest.xml
, the INTERNET permission is
present, as this example downloads online map data on demand.
Downloaded map data is stored on the device in a directory such as this:
/sdcard/Android/data/com.magiclane.examplename/files/Data/Maps/
.
1private val mapName = "Luxembourg"
A private class variable is defined with the name of the map to download.
1private val navigationService = NavigationService()
NavigationService()
is instantiated, which carries out
both simulated navigation and real navigation. 1private val navigationListener: NavigationListener
2= NavigationListener.create(
3 onNavigationStarted = {
4 SdkCall.execute {
5 gemSurfaceView.mapView?.let { mapView ->
6 mapView.preferences?.enableCursor = false
7 navRoute?.let { route ->
8 mapView.presentRoute(route)
9 }
10 enableGPSButton()
11 mapView.followPosition()
12 }
13 }
14 topPanel.visibility = View.VISIBLE
15 bottomPanel.visibility = View.VISIBLE
16 showStatusMessage("Simulation started.")
17 },
18 onNavigationInstructionUpdated = { instr ->
19 var instrText = ""
20 var instrIcon: Bitmap? = null
21 var instrDistance = ""
22 var etaText = ""
23 var rttText = ""
24 var rtdText = ""
25 SdkCall.execute {
26 // Fetch data for the navigation top panel (instruction related info).
27 instrText = instr.nextStreetName ?: ""
28 instrIcon = instr.nextTurnImage?.asBitmap(100, 100)
29 instrDistance = instr.getDistanceInMeters()
30 // Fetch data for the navigation bottom panel (route related info).
31 navRoute?.apply {
32 etaText = getEta() // estimated time of arrival
33 rttText = getRtt() // remaining travel time
34 rtdText = getRtd() // remaining travel distance
35 }
36 }
37 // Update the navigation panels info.
38 navInstruction.text = instrText
39 navInstructionIcon.setImageBitmap(instrIcon)
40 navInstructionDistance.text = instrDistance
41 eta.text = etaText
42 rtt.text = rttText
43 rtd.text = rtdText
44 showStatusMessage("Navigation instruction updated.")
45 }
46)
navigationListener
, that will
receive notifications from the navigation service.
The onNavigationStarted
and onNavigationInstructionUpdated
callbacks are implemented.navInstruction.text = instrText
navInstructionIcon.setImageBitmap(instrIcon)
navInstructionDistance.text = instrDistance
eta.text = // estimated time of arrival
rtt.text = // remaining travel time
rtd.text = // remaining travel distance
1private val routingProgressListener = ProgressListener.create(
2 onStarted = {
3 progressBar.visibility = View.VISIBLE
4 showStatusMessage("Routing process started.")
5 },
6 onCompleted = { _, _ ->
7 progressBar.visibility = View.GONE
8 showStatusMessage("Routing process completed.")
9 },
10 postOnMain = true
11)
1private val contentListener = ProgressListener.create(
2 onStarted = {
3 progressBar.visibility = View.VISIBLE
4 showStatusMessage("Started content store service.")
5 },
6 onCompleted = { errorCode, _ ->
7 progressBar.visibility = View.GONE
8 showStatusMessage("Content store service completed with error code: $errorCode")
9 when (errorCode)
10 {
11 GemError.NoError ->
12 {
13 // No error encountered, we can handle the results.
14 SdkCall.execute { // Get the list of maps that was retrieved in the content store.
15 val contentListPair = contentStore.getStoreContentList(EContentType.RoadMap) ?: return@execute
16 for (map in contentListPair.first)
17 {
18 val mapName = map.name ?: continue
19 if (mapName.compareTo(this.mapName, true) != 0) // searching another map
20 {
21 continue
22 }
23 if (!map.isCompleted())
24 {
25 // Define a listener to the progress of the map download action.
26 val downloadProgressListener = ProgressListener.create(
27 onStarted = {
28 onDownloadStarted(map)
29 showStatusMessage("Started downloading $mapName.")
30 },
31 onStatusChanged = { status ->
32 onStatusChanged(status)
33 },
34 onProgress = { progress ->
35 onProgressUpdated(progress)
36 },
37 onCompleted = { errorCode, _ ->
38 if (errorCode == GemError.NoError)
39 {
40 showStatusMessage("$mapName was downloaded.")
41 onOnboardMapReady()
42 }
43 })
44 // Start downloading the found map.
45 map.asyncDownload(downloadProgressListener, GemSdk.EDataSavePolicy.UseDefault, true)
46 }
47 break
48 }
49 }
50 }
51 GemError.Cancel ->
52 {
53 // The action was canceled.
54 }
55 else ->
56 {
57 // There was a problem at retrieving the content store items.
58 showDialog("Content store service error: ${GemError.getMessage(errorCode)}")
59 }
60 }
61 }
62)
contentListener
used to monitor the progress of obtaining
the list of maps available for download from the online content store server.onStarted
is called when the map list download starts, and onCompleted
is called when the map list download is complete.val contentListPair = contentStore.getStoreContentList(EContentType.RoadMap) ?: return@execute
contentListPair.first
.private val mapName
above at the top:val mapName = map.name ?: continue
if (mapName.compareTo(this.mapName, true) != 0)
//if 0, then foundval downloadProgressListener = ProgressListener.create(onStarted = {
overriding onStarted
, onStatusChanged
, onProgress
and onCompleted
.
Then the download is started, and the progress listener just defined
is used to monitor the progress of downloading the selected map:map.asyncDownload(downloadProgressListener, GemSdk.EDataSavePolicy.UseDefault, true)
/sdcard/Android/data/com.magiclane.examplename/files/Data/Maps/
. 1override fun onCreate(savedInstanceState: Bundle?)
2{
3 super.onCreate(savedInstanceState)
4 setContentView(R.layout.activity_main)
5 gemSurfaceView = findViewById(R.id.gem_surface)
6 progressBar = findViewById(R.id.progressBar)
7 followCursorButton = findViewById(R.id.followCursor)
8 topPanel = findViewById(R.id.top_panel)
9 navInstruction = findViewById(R.id.nav_instruction)
10 navInstructionDistance = findViewById(R.id.instr_distance)
11 navInstructionIcon = findViewById(R.id.nav_icon)
12 bottomPanel = findViewById(R.id.bottom_panel)
13 eta = findViewById(R.id.eta)
14 rtt = findViewById(R.id.rtt)
15 rtd = findViewById(R.id.rtd)
16 mapContainer = findViewById(R.id.map_container)
17 flagIcon = findViewById(R.id.flag_icon)
18 countryName = findViewById(R.id.country_name)
19 mapDescription = findViewById(R.id.map_description)
20 downloadProgressBar = findViewById(R.id.download_progress_bar)
21 downloadedIcon = findViewById(R.id.downloaded_icon)
22 statusText = findViewById(R.id.status_text)
23 val loadMaps = {
24 mapsCatalogRequested = true
25 val loadMapsCatalog = {
26 SdkCall.execute {
27 // Call to the content store to asynchronously retrieve the list of maps.
28 contentStore.asyncGetStoreContentList(EContentType.RoadMap, contentListener)
29 }
30 }
31 val token = GemSdk.getTokenFromManifest(this)
32 if (!token.isNullOrEmpty() && (token != kDefaultToken))
33 {
34 loadMapsCatalog()
35 }
36 else // if token is not present try to avoid content server requests limitation by delaying the voices catalog request
37 {
38 progressBar.visibility = View.VISIBLE
39 Handler(Looper.getMainLooper()).postDelayed({
40 loadMapsCatalog()
41 }, 3000)
42 }
43 }
44 SdkSettings.onMapDataReady = { it ->
45 if (!requiredMapHasBeenDownloaded)
46 {
47 mapReady = it
48 if (connected && mapReady && !mapsCatalogRequested)
49 {
50 loadMaps()
51 }
52 }
53 }
54 SdkSettings.onConnectionStatusUpdated = { it ->
55 if (!requiredMapHasBeenDownloaded)
56 {
57 connected = it
58 if (connected && mapReady && !mapsCatalogRequested)
59 {
60 loadMaps()
61 }
62 }
63 }
64 SdkSettings.onApiTokenRejected = {
65 showDialog("TOKEN REJECTED")
66 }
67 gemSurfaceView.onSdkInitSucceeded = onSdkInitSucceeded@{
68 val localMaps = contentStore.getLocalContentList(EContentType.RoadMap) ?: return@onSdkInitSucceeded
69 for (map in localMaps)
70 {
71 val mapName = map.name ?: continue
72 if (mapName.compareTo(this.mapName, true) == 0)
73 {
74 requiredMapHasBeenDownloaded = map.isCompleted()
75 break
76 }
77 }
78 // Defines an action that should be done when the the sdk had been loaded.
79 if (requiredMapHasBeenDownloaded)
80 {
81 onOnboardMapReady()
82 }
83 }
84 if (!requiredMapHasBeenDownloaded && !Util.isInternetConnected(this))
85 {
86 showDialog("You must be connected to internet!")
87 }
88}
MainActivity
overrides the onCreate()
function which
gets the IDs of the 6 elements on the top and bottom panels to
display the notifications from the navigation service:onCreate()
also checks that internet access
is available, and checks that the SDK was implicitly initialized:gemSurfaceView.onSdkInitSucceeded = onSdkInitSucceeded@{
private val mapName
above at the top, has not
been downloaded, or the internet connection becomes available, the loadMaps()
local function is called, which obtains the list of maps available on the server:contentStore.asyncGetStoreContentList(EContentType.RoadMap, contentListener)
passing in the contentListener
shown above.
Once the list of maps is downloaded, the contentListener
searches the
local copy of the list to see if the map defined in the mapName
variable
is already downloaded, and if not, downloads it.onCreate()
, if the SDK is initialized, as the actions just mentioned
initialize the SDK implicitly, a linear search is done for the map name
defined in the mapName
variable, because this map should be downloaded
at this point, and if it is not, then it means that there is
no internet connection.1private fun startSimulation() = SdkCall.execute {
2 val waypoints = arrayListOf(
3 Landmark("Luxembourg", 49.61588784436375, 6.135843869736401),
4 Landmark("Mersch", 49.74785494642988, 6.103323786692679)
5 )
6 navigationService.startSimulation(
7 waypoints, navigationListener, routingProgressListener
8 )
9}
startSimulation()
The startSimulation()
function first instantiates a list of 2 waypoints,
one for the departure location and one for the destination;
there can be more than 2 waypoints in a route, but not less than 2.
These 2 waypoints are hardcoded here, and the locations can be anywhere on
the planet, so long as a land route can be established between them,
as the required map data will be downloaded.
1private fun enableGPSButton() {
2 // Set actions for entering/ exiting following position mode.
3 gemSurfaceView.mapView?.apply {
4 onExitFollowingPosition = {
5 followCursorButton.visibility = View.VISIBLE
6 }
7 onEnterFollowingPosition = {
8 followCursorButton.visibility = View.GONE
9 }
10 // Set on click action for the GPS button.
11 followCursorButton.setOnClickListener {
12 SdkCall.execute { followPosition() }
13 }
14 }
15}
enableGPSButton()
causes a round purple button to appear in the lower
right corner of the screen, whenever the simulation is active and
the camera is not following the green arrow. If the user pushes this button,
the followPosition()
function is called, and thus the camera
starts to follow the green arrow once again. 1private fun Route.getEta(): String
2{
3 val etaNumber = this.getTimeDistance(true)?.totalTime ?: 0
4
5 val time = Time()
6 time.setLocalTime()
7 time.longValue = time.longValue + etaNumber * 1000
8 return String.format("%d:%02d", time.hour, time.minute)
9}
10private fun Route.getRtt(): String
11{
12 return GemUtil.getTimeText(
13 this.getTimeDistance(true)?.totalTime ?: 0
14 ).let { pair ->
15 pair.first + " " + pair.second
16 }
17}
18private fun Route.getRtd(): String
19{
20 return GemUtil.getDistText(
21 this.getTimeDistance(true)?.totalDistance ?: 0, EUnitSystem.Metric
22 ).let { pair ->
23 pair.first + " " + pair.second
24 }
25}
fun Route.getEta()
function to get the estimated time of arrival for this route.fun Route.getRtt()
function to get the remaining travel time for this route.fun Route.getRtd()
function to get the remaining travel distance for this route.