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
- Get your Magic Lane API key token: if you do not have a token, see the Getting Started guide
- Download the Maps & Navigation SDK for Android archive file
- Download 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
An android device should be connected via USB cable.
Press SHIFT+F10 to compile, install and run the example on the android device.


Simulates offline navigation along a route on a previously downloaded map.
How it works
You can open the MainActivity.kt file to see how the route computation and simulated navigation are set up.
<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/
.
private val mapName = "Luxembourg"
A private class variable is defined with the name of the map to download.
private val navigationService = NavigationService()
A NavigationService()
is instantiated, which carries out both simulated navigation and real navigation.
private val navigationListener: NavigationListener
= NavigationListener.create(
onNavigationStarted = {
SdkCall.execute {
gemSurfaceView.mapView?.let { mapView ->
mapView.preferences?.enableCursor = false
navRoute?.let { route ->
mapView.presentRoute(route)
}
enableGPSButton()
mapView.followPosition()
}
}
topPanel.visibility = View.VISIBLE
bottomPanel.visibility = View.VISIBLE
showStatusMessage("Simulation started.")
},
onNavigationInstructionUpdated = { instr ->
var instrText = ""
var instrIcon: Bitmap? = null
var instrDistance = ""
var etaText = ""
var rttText = ""
var rtdText = ""
SdkCall.execute {
// Fetch data for the navigation top panel (instruction related info).
instrText = instr.nextStreetName ?: ""
instrIcon = instr.nextTurnImage?.asBitmap(100, 100)
instrDistance = instr.getDistanceInMeters()
// Fetch data for the navigation bottom panel (route related info).
navRoute?.apply {
etaText = getEta() // estimated time of arrival
rttText = getRtt() // remaining travel time
rtdText = getRtd() // remaining travel distance
}
}
// Update the navigation panels info.
navInstruction.text = instrText
navInstructionIcon.setImageBitmap(instrIcon)
navInstructionDistance.text = instrDistance
eta.text = etaText
rtt.text = rttText
rtd.text = rtdText
showStatusMessage("Navigation instruction updated.")
}
)
Define a navigation listener, navigationListener
, that will receive notifications from the navigation service. The onNavigationStarted
and onNavigationInstructionUpdated
callbacks are implemented.
The navigation instructions received include a text instruction, such as “turn left”, a bitmap image, such as an arrow, and a distance, indicating how far ahead the turn is located:
navInstruction.text = instrText
navInstructionIcon.setImageBitmap(instrIcon)
navInstructionDistance.text = instrDistance
Additionally, the ETA, RTT and RTD are also obtained from the navigation service and displayed:
eta.text = // estimated time of arrival
rtt.text = // remaining travel time
rtd.text = // remaining travel distance
private val routingProgressListener = ProgressListener.create(
onStarted = {
progressBar.visibility = View.VISIBLE
showStatusMessage("Routing process started.")
},
onCompleted = { _, _ ->
progressBar.visibility = View.GONE
showStatusMessage("Routing process completed.")
},
postOnMain = true
)
Define a listener to indicate when the route computation is completed, so real or simulated navigation (simulated in this case) can start.
private val contentListener = ProgressListener.create(
onStarted = {
progressBar.visibility = View.VISIBLE
showStatusMessage("Started content store service.")
},
onCompleted = { errorCode, _ ->
progressBar.visibility = View.GONE
showStatusMessage("Content store service completed with error code: $errorCode")
when (errorCode)
{
GemError.NoError ->
{
// No error encountered, we can handle the results.
SdkCall.execute { // Get the list of maps that was retrieved in the content store.
val contentListPair = contentStore.getStoreContentList(EContentType.RoadMap) ?: return@execute
for (map in contentListPair.first)
{
val mapName = map.name ?: continue
if (mapName.compareTo(this.mapName, true) != 0) // searching another map
{
continue
}
if (!map.isCompleted())
{
// Define a listener to the progress of the map download action.
val downloadProgressListener = ProgressListener.create(
onStarted = {
onDownloadStarted(map)
showStatusMessage("Started downloading $mapName.")
},
onStatusChanged = { status ->
onStatusChanged(status)
},
onProgress = { progress ->
onProgressUpdated(progress)
},
onCompleted = { errorCode, _ ->
if (errorCode == GemError.NoError)
{
showStatusMessage("$mapName was downloaded.")
onOnboardMapReady()
}
})
// Start downloading the found map.
map.asyncDownload(downloadProgressListener, GemSdk.EDataSavePolicy.UseDefault, true)
}
break
}
}
}
GemError.Cancel ->
{
// The action was canceled.
}
else ->
{
// There was a problem at retrieving the content store items.
showDialog("Content store service error: ${GemError.getMessage(errorCode)}")
}
}
}
)
This is the 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.
Once the map list is downloaded, the array of roadmaps is obtained from the downloaded local copy of the map list:
val contentListPair = contentStore.getStoreContentList(EContentType.RoadMap) ?: return@execute
The array of maps is: contentListPair.first
.
Then it does a linear search through the list of maps to find the map with defined in the private val mapName above at the top:
val mapName = map.name ?: continue
if (mapName.compareTo(this.mapName, true) != 0) //if 0, then found
If the map is found and it is not downloaded, then a progress listener is created right here:
val 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)
The downloaded map is stored on the device in a directory such as this: /sdcard/Android/data/com.magiclane.examplename/files/Data/Maps/
.
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
gemSurfaceView = findViewById(R.id.gem_surface)
progressBar = findViewById(R.id.progressBar)
followCursorButton = findViewById(R.id.followCursor)
topPanel = findViewById(R.id.top_panel)
navInstruction = findViewById(R.id.nav_instruction)
navInstructionDistance = findViewById(R.id.instr_distance)
navInstructionIcon = findViewById(R.id.nav_icon)
bottomPanel = findViewById(R.id.bottom_panel)
eta = findViewById(R.id.eta)
rtt = findViewById(R.id.rtt)
rtd = findViewById(R.id.rtd)
mapContainer = findViewById(R.id.map_container)
flagIcon = findViewById(R.id.flag_icon)
countryName = findViewById(R.id.country_name)
mapDescription = findViewById(R.id.map_description)
downloadProgressBar = findViewById(R.id.download_progress_bar)
downloadedIcon = findViewById(R.id.downloaded_icon)
statusText = findViewById(R.id.status_text)
val loadMaps = {
mapsCatalogRequested = true
val loadMapsCatalog = {
SdkCall.execute {
// Call to the content store to asynchronously retrieve the list of maps.
contentStore.asyncGetStoreContentList(EContentType.RoadMap, contentListener)
}
}
val token = GemSdk.getTokenFromManifest(this)
if (!token.isNullOrEmpty() && (token != kDefaultToken))
{
loadMapsCatalog()
}
else // if token is not present try to avoid content server requests limitation by delaying the voices catalog request
{
progressBar.visibility = View.VISIBLE
Handler(Looper.getMainLooper()).postDelayed({
loadMapsCatalog()
}, 3000)
}
}
SdkSettings.onMapDataReady = { it ->
if (!requiredMapHasBeenDownloaded)
{
mapReady = it
if (connected && mapReady && !mapsCatalogRequested)
{
loadMaps()
}
}
}
SdkSettings.onConnectionStatusUpdated = { it ->
if (!requiredMapHasBeenDownloaded)
{
connected = it
if (connected && mapReady && !mapsCatalogRequested)
{
loadMaps()
}
}
}
SdkSettings.onApiTokenRejected = {
showDialog("TOKEN REJECTED")
}
gemSurfaceView.onSdkInitSucceeded = onSdkInitSucceeded@{
val localMaps = contentStore.getLocalContentList(EContentType.RoadMap) ?: return@onSdkInitSucceeded
for (map in localMaps)
{
val mapName = map.name ?: continue
if (mapName.compareTo(this.mapName, true) == 0)
{
requiredMapHasBeenDownloaded = map.isCompleted()
break
}
}
// Defines an action that should be done when the the sdk had been loaded.
if (requiredMapHasBeenDownloaded)
{
onOnboardMapReady()
}
}
if (!requiredMapHasBeenDownloaded && !Util.isInternetConnected(this))
{
showDialog("You must be connected to internet!")
}
}
The 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:
- text instructions
- arrow bitmap image
- distance to the instruction event
- estimated time of arrival (ETA)
- remaining travel time (RTT)
- remaining travel distance (RTD)
onCreate()
also checks that internet access is available, and checks that the SDK was implicitly initialized:
gemSurfaceView.onSdkInitSucceeded = onSdkInitSucceeded@{
If the map, defined in the 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.
Back to 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.
private fun startSimulation() = SdkCall.execute {
val waypoints = arrayListOf(
Landmark("Luxembourg", 49.61588784436375, 6.135843869736401),
Landmark("Mersch", 49.74785494642988, 6.103323786692679)
)
navigationService.startSimulation(
waypoints, navigationListener, routingProgressListener
)
}
If the SDK initialization succeeded, the navigation simulation is started: 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.
private fun enableGPSButton() {
// Set actions for entering/ exiting following position mode.
gemSurfaceView.mapView?.apply {
onExitFollowingPosition = {
followCursorButton.visibility = View.VISIBLE
}
onEnterFollowingPosition = {
followCursorButton.visibility = View.GONE
}
// Set on click action for the GPS button.
followCursorButton.setOnClickListener {
SdkCall.execute { followPosition() }
}
}
}
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.
private fun Route.getEta(): String
{
val etaNumber = this.getTimeDistance(true)?.totalTime ?: 0
val time = Time()
time.setLocalTime()
time.longValue = time.longValue + etaNumber * 1000
return String.format("%d:%02d", time.hour, time.minute)
}
private fun Route.getRtt(): String
{
return GemUtil.getTimeText(
this.getTimeDistance(true)?.totalTime ?: 0
).let { pair ->
pair.first + " " + pair.second
}
}
private fun Route.getRtd(): String
{
return GemUtil.getDistText(
this.getTimeDistance(true)?.totalDistance ?: 0, EUnitSystem.Metric
).let { pair ->
pair.first + " " + pair.second
}
}
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.
Android Examples
Maps SDK for Android Examples can be downloaded or cloned with Git