# Magic Lane - Maps SDK for Android documentation ## Docs ### Get Started With Sample Apps | #### Prerequisites[​](#prerequisites "Direct link to Prerequisites") * You have Android Studio installed. * You have a basic understanding of Android development and Kotlin programming language. * You have created an API key. ##### Create and use an API key[​](#create-and-use-an-api-key "Direct link to Create and use an API key") Follow our [step-by-step guide](https://developer.magiclane.com/docs/guides/get-started) to sign up for a free account, create a project, generate your API key and start building your Android solution. Attention! If the API key is not configured, some features will be restricted, and a watermark will appear on the map. Functionality such as map downloads, updates, and other features may be unavailable or may not function as expected. Please ensure the API token is set correctly. Ensure the API key is stored securely and protected against unauthorized access or exposure. #### Get the examples[​](#get-the-examples "Direct link to Get the examples") 1. Download the [Magic Lane Maps SDK Examples for Android repository](https://github.com/magiclane/magiclane-maps-sdk-examples-for-android) from Github 2. Browse through the sample applications - each one showcases different Maps SDK features to help you get started quickly #### Build and run the examples[​](#build-and-run-the-examples "Direct link to Build and run the examples") After downloading the examples, choose the sample app you want and open the project in Android Studio. An API key must be put in the `AndroidManifest.xml` file, inside the `` tag. Replace `` with your actual API key token. ```xml ``` Synchronize your project with Gradle files and run the app on an emulator or a physical device. --- ### Maps & 3D Scene info Example applications for the Maps SDK for Android are available on [IssueTracker](https://issuetracker.magiclane.com/magiclane/maps-sdk-examples-for-android). You can clone the repository and launch the applications by following the setup instructions provided in the [README](https://issuetracker.magiclane.com/magiclane/maps-sdk-examples-for-android/-/blob/master/README.md?ref_type=heads) file. Bring interactive customizable real world maps to your users in 3D. No matter whether they are online or offline. [![Hello Map image](/docs/android/assets/images/1_hello_map-5e0d71243561d0e95a74bd7569118954.png)](/docs/android/examples/maps-3dscene/hello-map.md) ##### [Hello Map](/docs/android/examples/maps-3dscene/hello-map.md) [Render an interactive map centered on a desired location.](/docs/android/examples/maps-3dscene/hello-map.md) [![Overlapped Maps image](/docs/android/assets/images/2_overlapped_maps-f5bea834360011841caf59597b42f4c1.png)](/docs/android/examples/maps-3dscene/overlapped-maps.md) ##### [Overlapped Maps](/docs/android/examples/maps-3dscene/overlapped-maps.md) [Display overlapped maps, showcasing the ability to layer map views.](/docs/android/examples/maps-3dscene/overlapped-maps.md) [![Maps Perspective Change image](/docs/android/assets/images/3_maps_perspective_change-017cf5278ba8b366c42a4816fa01df1f.png)](/docs/android/examples/maps-3dscene/maps-perspective-change.md) ##### [Maps Perspective Change](/docs/android/examples/maps-3dscene/maps-perspective-change.md) [Toggle between 2D and 3D map perspective.](/docs/android/examples/maps-3dscene/maps-perspective-change.md) [![Map Compass image](/docs/android/assets/images/4_map_compass-8bcafaca25831db0b5675c5d2792a153.png)](/docs/android/examples/maps-3dscene/map-compass.md) ##### [Map Compass](/docs/android/examples/maps-3dscene/map-compass.md) [Render a compass icon that displays the heading rotation of the map.](/docs/android/examples/maps-3dscene/map-compass.md) [![Download Map image](/docs/android/assets/images/5_download_a_map-a4f146431c6ff957ba077561b21bb563.png)](/docs/android/examples/maps-3dscene/download-a-map.md) ##### [Download Map](/docs/android/examples/maps-3dscene/download-a-map.md) [Download maps for offline use.](/docs/android/examples/maps-3dscene/download-a-map.md) [![Apply Map Style image](/docs/android/assets/images/6_apply_map_style-1ba411678e2d167aff0d38c3bf79c8cb.png)](/docs/android/examples/maps-3dscene/apply-map-style.md) ##### [Apply Map Style](/docs/android/examples/maps-3dscene/apply-map-style.md) [Create an app that displays an interactive map with a non-default style.](/docs/android/examples/maps-3dscene/apply-map-style.md) [![Apply Custom Map Style image](/docs/android/assets/images/7_apply_custom_map_style-dcb25f07ab237134eb359955b5ad9659.png)](/docs/android/examples/maps-3dscene/apply-custom-map-style.md) ##### [Apply Custom Map Style](/docs/android/examples/maps-3dscene/apply-custom-map-style.md) [Change the map style by applying a custom map style.](/docs/android/examples/maps-3dscene/apply-custom-map-style.md) [![Social Event Voting image](/docs/android/assets/images/8_social_event_voting-9574007310aea7801ea04da11ee305b7.png)](/docs/android/examples/maps-3dscene/social-event-voting.md) ##### [Social Event Voting](/docs/android/examples/maps-3dscene/social-event-voting.md) [Create an app with an interactive map that allows users to vote and view social events.](/docs/android/examples/maps-3dscene/social-event-voting.md) [![Social Report image](/docs/android/assets/images/9_social_report-66c70353e9625eacd761ccba38bd0696.png)](/docs/android/examples/maps-3dscene/social-report.md) ##### [Social Report](/docs/android/examples/maps-3dscene/social-report.md) [Send a social report automatically as soon as the map is ready.](/docs/android/examples/maps-3dscene/social-report.md) --- ### Apply Custom Map Style |

In this guide you will learn how to render an interactive map, and change the map style by applying a custom map style edited with, and downloaded from, the online editor.
The map is fully 3D, supporting pan, pinch-zoom, pinch-rotate and tilt. ![](/docs/android/assets/images/example_kotlin_applycustommapstyle_screenshot1-1a67376cf4223907e49766b68d53e693.png) **Zoomed in map with custom style** ![](/docs/android/assets/images/example_kotlin_applycustommapstyle_screenshot2-dfe7809e59f60192832f076816819c63.png) **Zoomed out map with custom style** ##### Map Implementation[​](#map-implementation "Direct link to Map Implementation") A map style, previously edited in the online editor and then downloaded, is in the `assets` folder of this example project. This function opens the file, reads it, and if it is not empty, sets the map style using the style file data just read: `mapView?.preferences?.setMapStyleByDataBuffer(DataBuffer(data))`. MainActivity.kt[](ApplyCustomMapStyle/app/src/main/kotlin/com/magiclane/sdk/examples/applycustommapstyle/MainActivity.kt?ref_type=heads#L118) ```kotlin private fun applyCustomAssetStyle(mapView: MapView?) = SdkCall.execute { val filename = "(Desktop) Monochrome Deep Blue (5a1da93a-dbf2-4a36-9b5c-1370386c1496).style" // Opens style input stream. val inputStream = applicationContext.resources.assets.open(filename) // Take bytes. val data = inputStream.readBytes() if (data.isEmpty()) return@execute // Apply style. mapView?.preferences?.setMapStyleByDataBuffer(DataBuffer(data)) } ``` `MainActivity` overrides the `onCreate` function, which checks that internet access is available, and then, when the map is ready, that is, loaded and initialized, automatically applies the custom map style stored in this example project’s `assets` folder: `applyCustomAssetStyle(gemSurfaceView.mapView)` MainActivity.kt[](ApplyCustomMapStyle/app/src/main/kotlin/com/magiclane/sdk/examples/applycustommapstyle/MainActivity.kt?ref_type=heads#L46) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) progressBar = findViewById(R.id.progressBar) gemSurfaceView = findViewById(R.id.gem_surface) SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady applyCustomAssetStyle(gemSurfaceView.mapView) } SdkSettings.onApiTokenRejected = { showDialog("TOKEN REJECTED") } ... } ``` --- ### Apply Map Style |

This example demonstrates how to create an app that displays an interactive map with a non-default style. ![](/docs/android/assets/images/example_kotlin_applymapstyle_screenshot1-37d348344645fdd63e52da261df4793d.png) **Map style with topography** ##### Fetch of Available Map Styles[​](#fetch-of-available-map-styles "Direct link to Fetch of Available Map Styles") First, a content store item is defined, so that map styles can be requested from it. ```kotlin private val contentStore = ContentStore() ``` Then `fetchAvailableStyles()` gets the list of map styles available for download from the online content store server. `contentStore.asyncGetStoreContentList()` launches the request to get the list. The `onStarted` listener is called when the content store service is started, and the list of styles download started. The `onCompleted` listener is defined to detect when the list of styles download is complete. At that point, if the list is not empty, the listener selects the first style (index 0) or the style in the middle of the list (if the list has multiple elements) and starts downloading that style automatically, though in a real-world use case, the user would select which style to download. MainActivity.kt[](ApplyMapStyle/app/src/main/kotlin/com/magiclane/sdk/examples/applymapstyle/MainActivity.kt?ref_type=heads#L155) ```kotlin private fun fetchAvailableStyles() = SdkCall.execute { // Call to the content store to asynchronously retrieve the list of map styles. contentStore.asyncGetStoreContentList(EContentType.ViewStyleHighRes, onStarted = { progressBar.visibility = View.VISIBLE showStatusMessage("Started content store service.") }, onCompleted = onCompleted@{ styles, errorCode, _ -> progressBar.visibility = View.GONE showStatusMessage("Content store service completed, error code: $errorCode") when (errorCode) { GemError.NoError -> { if (styles.size == 0) return@onCompleted // The map style items list is not empty // or null so we will select an item. var style = styles[0] if (styles.size > 1) // does it have a middle element? style = styles[(styles.size / 2) - 1] showStatusMessage("Preparing download.", true) startDownloadingStyle(style) } GemError.Cancel -> { // The action was cancelled. return@onCompleted } else -> { // There was a problem at retrieving the content store items. showDialog("Content store service error: ${GemError.getMessage(errorCode)}") } } }) } ``` `startDownloadingStyle(style: ContentStoreItem)` is the function that the listener calls to download a style from the content store. Note that the function automatically applies the new map style as soon as the download is complete using the method `applyStyle(style)`. MainActivity.kt[](ApplyMapStyle/app/src/main/kotlin/com/magiclane/sdk/examples/applymapstyle/MainActivity.kt?ref_type=heads#L203) ```kotlin private fun startDownloadingStyle(style: ContentStoreItem) = SdkCall.execute { val styleName = style.name ?: "NO_NAME" // Start downloading a map style item. style.asyncDownload( onStarted = { showStatusMessage("Started downloading $styleName.") onDownloadStarted(style) }, onStatusChanged = { status -> onStatusChanged(status) }, onProgress = { progress -> onProgressUpdated(progress) }, onCompleted = { _, _ -> showStatusMessage("$styleName was downloaded. Applying style...") applyStyle(style) showStatusMessage("Style $styleName was applied.") styleContainer.visibility = View.GONE } ) } ``` Set the map to use the newly downloaded map style. MainActivity.kt[](ApplyMapStyle/app/src/main/kotlin/com/magiclane/sdk/examples/applymapstyle/MainActivity.kt?ref_type=heads#L196) ```kotlin private fun applyStyle(style: ContentStoreItem) = SdkCall.execute { // Apply the style to the main map view. gemSurfaceView.mapView?.preferences?.setMapStyleById(style.id) } ``` ##### Map Display[​](#map-display "Direct link to Map Display") `MainActivity` overrides the `onCreate()` function which calls the `fetchAvailableStyles()` function to get the list of map styles from the content store on the server. That function then automatically downloads and then applies a style, as soon as it obtains the list of styles. MainActivity.kt[](ApplyMapStyle/app/src/main/kotlin/com/magiclane/sdk/examples/applymapstyle/MainActivity.kt?ref_type=heads#L62) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) progressBar = findViewById(R.id.progress_bar) gemSurfaceView = findViewById(R.id.gem_surface) statusText = findViewById(R.id.status_text) statusProgressBar = findViewById(R.id.status_progress_bar) styleContainer = findViewById(R.id.style_container) styleName = findViewById(R.id.style_name) downloadProgressBar = findViewById(R.id.download_progress_bar) downloadedIcon = findViewById(R.id.downloaded_icon) SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady fetchAvailableStyles() } } ``` --- ### Download A Map |

This example demonstrates how to list the road maps available on the server for download, how to download a map while indicating the download progress, and how to display the download’s finished status. ![](/docs/android/assets/images/example_kotlin_onlinemaps_screenshot1-5e5a926c5acdb25b30adf792b1cfeda4.png) **Downloadable Maps list** ##### Download Progress Implementation[​](#download-progress-implementation "Direct link to Download Progress Implementation") This is the `progressListener` used to monitor the progress of obtaining the list of maps available for download from the online content store server. The callback `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 models = contentStore.getStoreContentList(EContentType.RoadMap)?.first` If the array is not empty, the first item (at index 0) is obtained: `val mapItem = models[0]` The progress listener is created using: `val progressListener = ProgressListener.create`. Then the download is started, and the progress listener just defined is used to monitor the progress of downloading the map at index 0 in the list of maps. The downloaded map is stored on the device in a directory such as this: `/sdcard/Android/data/com.magiclane.examplename/files/Data/Maps/`. The progress listener also calls `displayList(models)` which uses a `CustomAdapter` to display a scrollable list of the maps available on the server. The list displayed is the local copy that was downloaded above. MainActivity.kt[](DownloadingOnboardMap/app/src/main/kotlin/com/magiclane/sdk/examples/downloadingonboardmap/MainActivity.kt?ref_type=heads#L69) ```kotlin private val progressListener = 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 -> { SdkCall.execute { // No error encountered, we can handle the results. val models = contentStore.getStoreContentList(EContentType.RoadMap)?.first if (!models.isNullOrEmpty()) { // The map items list is not empty or null. val mapItem = models[0] val itemName = mapItem.name // Define a listener to the progress of the map download action. val downloadProgressListener = ProgressListener.create(onStarted = { showStatusMessage("Started downloading $itemName.") }, onProgress = { listView.adapter?.notifyItemChanged(0) }, onCompleted = { errorCode, _ -> listView.adapter?.notifyItemChanged(0) if (errorCode == GemError.NoError) showStatusMessage("$itemName was downloaded.") else showDialog( "Download item error: ${ GemError.getMessage( errorCode ) }" ) EspressoIdlingResource.decrement() }) // Start downloading the first map item. SdkCall.execute { mapItem.asyncDownload( downloadProgressListener, GemSdk.EDataSavePolicy.UseDefault, true ) } } displayList(models) } } GemError.Cancel -> { // The action was cancelled. EspressoIdlingResource.decrement() } else -> { // There was a problem at retrieving the content store items. showDialog("Content store service error: ${GemError.getMessage(errorCode)}") EspressoIdlingResource.decrement() } } } ) ``` ##### UI and Map Integration[​](#ui-and-map-integration "Direct link to UI and Map Integration") `MainActivity` overrides `onCreate()` which checks that internet access is available, and explicitly initializes the SDK, which is required for enabling use of the SDK without a map: `GemSdk.initSdkWithDefaults(this)` When the internet connection is detected, or when the SDK reports that map data is ready, the `loadMaps()` function is called to request the list of maps from the content store server `contentStore.asyncGetStoreContentList(EContentType.RoadMap, progressListener)`
The list of maps is requested, and the `progressListener` shown above is passed in, to get notified when the list of maps has finished downloading. At that point, as we have already seen above, the progress listener automatically selects the first map (unless the list is empty) and downloads it automatically.
In a real world use case, the user would select which map to download. MainActivity.kt[](DownloadingOnboardMap/app/src/main/kotlin/com/magiclane/sdk/examples/downloadingonboardmap/MainActivity.kt?ref_type=heads#L146) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) EspressoIdlingResource.increment() progressBar = findViewById(R.id.progress_bar) statusText = findViewById(R.id.status_text) listView = findViewById(R.id.list_view).apply { layoutManager = LinearLayoutManager(this@MainActivity) val separator = DividerItemDecoration( applicationContext, (layoutManager as LinearLayoutManager).orientation ) addItemDecoration(separator) val lateralPadding = resources.getDimension(R.dimen.bigPadding).toInt() setPadding(lateralPadding, 0, lateralPadding, 0) itemAnimator = null } val loadMaps = { val loadMapsCatalog = { SdkCall.execute { // Call to the content store to asynchronously retrieve the list of maps. contentStore.asyncGetStoreContentList(EContentType.RoadMap, progressListener) } } 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 maps catalog request { Handler(Looper.getMainLooper()).postDelayed({ loadMapsCatalog() }, 3000) } } SdkSettings.onMapDataReady = { mapReady -> if (mapReady) { loadMaps() } } ... } ``` Also, the method `showStatusMessage()` will show the progress status message in a `TextView` below the list. --- ### Hello Map |

In this example you will learn how to render an interactive map centered on a desired location. The map is fully 3D, supporting pan, pinch-zoom, rotate and tilt. ![](/docs/android/assets/images/example_kotlin_hello_map_screenshot1-2aa011ee94fa64bc03290245997b3529.png) **Map View** ##### Map Display and Cleanup[​](#map-display-and-cleanup "Direct link to Map Display and Cleanup") The following code outlines the main activity, which displays the map and handles resource clean-up. The `MainActivity` overrides the `onCreate` function which instantiates and loads the map: `setContentView(R.layout.activity_main)` as defined in `res/layout/activity_main.xml`. MainActivity.kt[](HelloMap/app/src/main/kotlin/com/magiclane/sdk/examples/hellomap/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } override fun onDestroy() { super.onDestroy() // Release the SDK. GemSdk.release() } } ``` The `com.magiclane.sdk.core.GemSurfaceView` is a custom `View` that displays the map, part of Magic Lane's SDK. MainActivity.kt[](HelloMap/app/src/main/res/layout/activity_main.xml) ```xml ``` --- ### Map Compass |

This example demonstrates how to render a compass icon that displays the heading rotation of an interactive map. The compass indicates the direction where 0 degrees is north, 90 degrees is east, 180 degrees is south, and 270 degrees is west. You will also learn how to rotate the map back to its default north-up orientation. ![](/docs/android/assets/images/example_kotlin_mapcompass_screenshot1-af60859c2303fee75fc4a80b3ab09d58.png) **Initial map rotation angle, north pointing up** ![](/docs/android/assets/images/example_kotlin_mapcompass_screenshot2-c8070c5fd00c404e702038ceb118bee3.png) **Map rotation angle changed, north pointing differently** ##### UI and Map Integration[​](#ui-and-map-integration "Direct link to UI and Map Integration") The `MainActivity` overrides the `onCreate()` function which defines the `buttonAsStart` / `buttonAsStop` methods used to turn the compass on or off, as well as color the button `green` / `red`, respectively, and a listener for that button, `btnEnableLiveHeading.setOnClickListener`.
The button listener calls `startLiveHeading()` or `stopLiveHeading()` to start or stop reading the live compass sensor data from the device, by adding or removing a listener to the compass data source.
MainActivity.kt[](MapCompass/app/src/main/kotlin/com/magiclane/sdk/examples/mapcompass/MainActivity.kt?ref_type=heads#L76) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) increment() surfaceView = findViewById(R.id.gem_surface) compass = findViewById(R.id.compass) btnEnableLiveHeading = findViewById(R.id.btn_enable_live_heading) statusText = findViewById(R.id.status_text) // start stop btn buttonAsStart(this, btnEnableLiveHeading) btnEnableLiveHeading.setOnClickListener { isLiveHeadingEnabled.set(!isLiveHeadingEnabled.get()) if (isLiveHeadingEnabled.get()) { buttonAsStop(this, btnEnableLiveHeading) statusText.text = getString(R.string.live_heading_enabled) } else { buttonAsStart(this, btnEnableLiveHeading) statusText.text = getString(R.string.live_heading_disabled) } GemCall.execute { if (isLiveHeadingEnabled.get()) { startLiveHeading() } else { stopLiveHeading() } } } } ``` The compass data source listener, when active, reads the compass sensor, smoothed into an average of several readings, and passes this heading to the `mapView` to rotate the map accordingly, when the device is rotated. MainActivity.kt[](MapCompass/app/src/main/kotlin/com/magiclane/sdk/examples/mapcompass/MainActivity.kt?ref_type=heads#L58) ```kotlin private var dataSourceListener = object : DataSourceListener() { override fun onNewData(data: SenseData) { GemCall.execute { // smooth new compass data val heading = headingSmoother.update(CompassData(data).heading) // update map view based on the recent changes surfaceView.mapView?.preferences?.rotationAngle = heading } } } ``` ##### Compass Functionality[​](#compass-functionality "Direct link to Compass Functionality") This code sets up a callback `onMapDataReady` that executes when map data is ready. If the data isn't ready, it exits. Otherwise, it makes the compass visible and then it links the compass UI: * The compass image rotates as the map view rotates * Tapping the compass icon makes the map smoothly animate to a north-up orientation. MainActivity.kt[](MapCompass/app/src/main/kotlin/com/magiclane/sdk/examples/mapcompass/MainActivity.kt?ref_type=heads#L117) ```kotlin SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady if(!mainActivityIdlingResource.isIdleNow) decrement() compass.visibility = View.VISIBLE btnEnableLiveHeading.visibility = View.VISIBLE statusText.visibility = View.VISIBLE // Get the map view. surfaceView.mapView?.let { mapView -> // Change the compass icon rotation based on the map rotation at rendering. mapView.onMapAngleUpdated = { compass.rotation = -it.toFloat() } // Align the map to north if the compass icon is pressed. compass.setOnClickListener { GemCall.execute { mapView.alignNorthUp(Animation(EAnimation.Linear, 300)) } } } } ``` The red stop button removes the compass sensor data source listener, thus the heading is no longer updated and the map stops rotating when the device is rotated. MainActivity.kt[](MapCompass/app/src/main/kotlin/com/magiclane/sdk/examples/mapcompass/MainActivity.kt?ref_type=heads#L184) ```kotlin private fun stopLiveHeading() = GemCall.execute { dataSource?.release() dataSource = null } ``` --- ### Maps Perspective Change |

This example demonstrates how to toggle the map view angle between 2D (vertical look-down at the map) and 3D (perspective, tilted map, looking towards the horizon). ![](/docs/android/assets/images/example_kotlin_mapperspectivechange_screenshot1-3691093cf58ce441ddb72c73615f5f44.png) **Displaying the map in 2D** ![](/docs/android/assets/images/example_kotlin_mapperspectivechange_screenshot2-397ff84bbe941ff756508f6f55c2aa5d.png) **Displaying the map in 3D** ##### UI and Map Integration[​](#ui-and-map-integration "Direct link to UI and Map Integration") The `MainActivity` overrides the `onCreate()` function, which defines a button to toggle between 2D (vertical) and 3D (perspective/tilted) map modes. The map starts in 2D mode (EViewPerspective.TwoDimensional), and the button shows "3D" to indicate that pressing it will switch the map to 3D mode (EViewPerspective.ThreeDimensional). MainActivity.kt[](MapPerspectiveChange/app/src/main/kotlin/com/magiclane/sdk/examples/mapperspectivechange/MainActivity.kt?ref_type=heads#L38) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) surfaceView = findViewById(R.id.gem_surface) button = findViewById(R.id.button) val twoDimensionalText = resources.getString(R.string.two_dimensional) val threeDimensionalText = resources.getString(R.string.three_dimensional) button.setOnClickListener { // Get the map view. ... } } ``` When the user presses the button, the map perspective is changed based on the `currentPerspective` and using the method `setMapViewPerspective` from the Map View preferences. The `Animation(EAnimation.Linear, 300)` defines the animation type and duration of the perspective change. Every functionality using Maps SDK must be executed on the same thread as the map engine runs using `SdkCall.execute` to place the desired functionality on the correct thread. MainActivity.kt[](MapPerspectiveChange/app/src/main/kotlin/com/magiclane/sdk/examples/mapperspectivechange/MainActivity.kt?ref_type=heads#L51) ```kotlin surfaceView.mapView?.let { mapView -> // Establish the current map view perspective. currentPerspective = if (currentPerspective == EMapViewPerspective.TwoDimensional) { button.text = twoDimensionalText EMapViewPerspective.ThreeDimensional } else { button.text = threeDimensionalText EMapViewPerspective.TwoDimensional } SdkCall.execute { // Change the map view perspective. mapView.preferences?.setMapViewPerspective( currentPerspective, Animation(EAnimation.Linear, 300) ) } } ``` Not using `SdkCall.execute` will result in an exception and the program will stop. --- ### Overlapped Maps |

This example demonstrates how to create an Android application that display overlapped maps, showcasing the ability to layer map views. ![](/docs/android/assets/images/example_kotlin_overlappedmaps_screenshot1-41a0f097a7901f47e3065c4229f0c5bb.png) **Two overlapped Map Views** ##### UI and Map Integration[​](#ui-and-map-integration "Direct link to UI and Map Integration") The main application consists of a simple user interface that displays two maps stacked on top of each other. The user can see both maps simultaneously, allowing for comparison or overlaying of different data. `MainActivity` overrides the `onCreate()` function, which calls the `findViewById()` function to find the surface on which the default map is rendered.
After the default map is created, the overlapped map is created as shown in the code block `gemSurfaceView.onDefaultMapViewCreated = { ... }`. MainActivity.kt[](OverlappedMaps/app/src/main/kotlin/com/magiclane/sdk/examples/overlappedmaps/MainActivity.kt?ref_type=heads#L38) ```kotlin class MainActivity : AppCompatActivity() { private lateinit var gemSurfaceView: GemSurfaceView private var secondMapView: MapView? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Add the small overlapped map gemSurfaceView = findViewById(R.id.gem_surface) gemSurfaceView.onDefaultMapViewCreated = { gemSurfaceView.gemScreen?.let { screen -> val secondViewRect = RectF(0.0f, 0.0f, 0.5f, 0.5f) secondMapView = MapView.produce(screen, secondViewRect, null, true) } } ... } override fun onDestroy() { super.onDestroy() // Deinitialize the SDK. GemSdk.release() } } ``` A short message will be shown on screen if the API Token is rejected using `SdkSettings.onApiTokenRejected`. --- ### Social Event Voting |

This example demonstrates how to create an app with an interactive map that allows users to vote and view social events, using the Maps SDK. ![](/docs/android/assets/images/example_kotlin_socialeventvoting_screenshot1-c599e1e63ef7db95fb3e4556eee9b303.png) **No social event selected** ![](/docs/android/assets/images/example_kotlin_socialeventvoting_screenshot2-52680e8ea0dfac47e83140765111579b.png) **Social event with 2 votes selected** ##### Map Integration[​](#map-integration "Direct link to Map Integration") When the map is loaded, `SdkSettings.onMapDataReady =` a touch handler is defined to get the screen position where a user tap occurred: `gemSurfaceView.mapView?.onTouch = xy ->` then the xy screen position is set so the mapView can use it to search for map elements near that position: `gemSurfaceView.mapView?.cursorScreenPosition = xy` When the user taps on the map, the overlay items, if any, near the tap position are obtained:
`val overlays = gemSurfaceView.mapView?.cursorSelectionOverlayItems`
and if the array is not empty, then the first item, at index 0, is selected:
`val overlay = overlays[0]`
and if it is a social reports item,
`if (overlay.overlayInfo?.uid == ECommonOverlayId.SocialReports.value)`
then the voting panel is displayed:
`Util.postOnMain { showVotingPanel(overlay) }` MainActivity.kt[](SocialEventVoting/app/src/main/kotlin/com/magiclane/sdk/examples/socialeventvoting/MainActivity.kt?ref_type=heads#L65) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) progressBar = findViewById(R.id.progress_bar) gemSurfaceView = findViewById(R.id.gem_surface_view) eventVotingContainer = findViewById(R.id.event_voting_container) icon = findViewById(R.id.icon) text = findViewById(R.id.text) time = findViewById(R.id.time) score = findViewById(R.id.score) thumbUpButton = findViewById(R.id.thumb_up_button) pleaseSelectText = findViewById(R.id.please_select_text) SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady // Defines an action that should be done when the world map is ready (Updated/loaded). gemSurfaceView.mapView?.onTouch = { xy -> SdkCall.execute { gemSurfaceView.mapView?.cursorScreenPosition = xy val overlays = gemSurfaceView.mapView?.cursorSelectionOverlayItems if (!overlays.isNullOrEmpty()) { val overlay = overlays[0] if (overlay.overlayInfo?.uid == ECommonOverlayId.SocialReports.value) { Util.postOnMain { showVotingPanel(overlay) } return@execute } } Util.postOnMain { hideVotingPanel() } } } } SdkSettings.onApiTokenRejected = { showDialog("TOKEN REJECTED") } } ``` ##### Social Events Interaction[​](#social-events-interaction "Direct link to Social Events Interaction") The `showVotingPanel()` function first makes the voting container panel visible, if it is not `eventVotingContainer.visibility = View.VISIBLE` Next, the time of the event is converted to hours and minutes, if it occurred today, or in day, month and year format otherwise. The icon image associated with the event is `icon.setImageBitmap(bitmap)`. The number of votes displayed in the upper right corner of the panel is the `score.text = scoreStr` whereas the name of the event is `text.text = textStr` and the date/time is `time.text = timeStr`. A click listener is set for the panel, `setOnClickListener` and the panel is made to disappear when the user clicks on it: `eventVotingContainer.visibility = View.GONE` MainActivity.kt[](SocialEventVoting/app/src/main/kotlin/com/magiclane/sdk/examples/socialeventvoting/MainActivity.kt?ref_type=heads#L142) ```kotlin private fun showVotingPanel(overlay: OverlayItem) { if (!eventVotingContainer.isVisible) { eventVotingContainer.visibility = View.VISIBLE pleaseSelectText.visibility = View.GONE } var bitmap: Bitmap? = null var textStr = "" var timeStr = "" var scoreStr = "" var thumbUpBitmap: Bitmap? = null val eventImageSize = resources.getDimension(R.dimen.event_image_size).toInt() val imageSize = resources.getDimension(R.dimen.image_size).toInt() SdkCall.execute { bitmap = overlay.image?.asBitmap(eventImageSize, eventImageSize) textStr = overlay.name.toString() scoreStr = overlay.getPreviewData()?.find { it.key == "score" }?.valueString.toString() val stamp = overlay.getPreviewData()?.find { it.key == "create_stamp_utc" }?.valueLong ?: 0 val eventTime = Calendar.getInstance(Locale.getDefault()).also { it.timeInMillis = stamp * 1000 } val now = Calendar.getInstance(Locale.getDefault()).also { it.timeInMillis = System.currentTimeMillis() } val dateFormat = if (eventTime.get(Calendar.YEAR) == now.get(Calendar.YEAR) && eventTime.get(Calendar.MONTH) == now.get(Calendar.MONTH) && eventTime.get(Calendar.DAY_OF_MONTH) == now.get(Calendar.DAY_OF_MONTH)) { "HH:mm" } else { "dd/MM/yyyy" } timeStr = SimpleDateFormat(dateFormat, Locale.getDefault()).format(Date(eventTime.timeInMillis)) if (overlay.getPreviewData()?.find { it.key == "allow_thumb" }?.valueBoolean == true) { thumbUpBitmap = GemUtilImages.asBitmap(ImageDatabase().getImageById(SdkImages.SocialReports.Social_Thumbs_Up.value), imageSize, imageSize) } } icon.setImageBitmap(bitmap) text.text = textStr time.text = timeStr score.text = scoreStr if (thumbUpBitmap != null) { thumbUpButton.apply { visibility = View.VISIBLE setImageBitmap(thumbUpBitmap) setOnClickListener { val errorCode = SdkCall.execute { SocialOverlay.confirmReport(overlay, ProgressListener()) } ?: -1 if (errorCode < 0) { showDialog("Confirm report failed with error: ${GemError.getMessage(errorCode)}") } eventVotingContainer.visibility = View.GONE } } } else { thumbUpButton.visibility = View.GONE } } ``` --- ### Social Report |

In this guide you will learn how to send a social report from within an app. An interactive map is displayed, and the social report is sent automatically as soon as the map is ready. ![](/docs/android/assets/images/example_kotlin_socialreport_screenshot1-92edbfd9f22d2e7cb2ab0abda78fd935.png) **Social report sent** ##### Map Integration[​](#map-integration "Direct link to Map Integration") In this example, the social report is sent automatically when the app is started, so long as a real or mock device position, such as GPS, is available. When the map is loaded, `SdkSettings.onMapDataReady` follow position is activated so the camera follows the position of the device, in case it is in a car in motion, for example `gemSurfaceView.mapView?.followPosition()` A function, `waitForNextImprovedPosition()`, is called to wait until the first good quality position of the device is received from the position sensor, such as GPS. When a good position has been obtained, the social report is sent automatically: `submitReport()` MainActivity.kt[](SocialReport/app/src/main/kotlin/com/magiclane/sdk/examples/socialreport/MainActivity.kt?ref_type=heads#L69) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) gemSurfaceView = findViewById(R.id.gem_surface) statusText = findViewById(R.id.status_text) SdkSettings.onMapDataReady = { isReady -> if (isReady) { // Defines an action that should be done // when the world map is ready (Updated/ loaded). SdkCall.execute { gemSurfaceView.mapView?.followPosition() waitForNextImprovedPosition { submitReport() } } } } SdkSettings.onApiTokenRejected = { showDialog("TOKEN REJECTED") } ... } ``` ##### Report Functionality[​](#report-functionality "Direct link to Report Functionality") The `PositionListener` instance is created and added to the `PositionService.addListener()` to be called when a good position `EDataType.ImprovedPosition` is received from the GPS sensor. When that happens, the function passed in is called automatically, that is, the `onEvent()` parameter, which is `submitReport()`. In this case, causing the social report to be sent upon reading the first valid device position from the sensor. MainActivity.kt[](SocialReport/app/src/main/kotlin/com/magiclane/sdk/examples/socialreport/MainActivity.kt?ref_type=heads#L208) ```kotlin private fun waitForNextImprovedPosition(onEvent: (() -> Unit)) { positionListener = PositionListener { if (it.isValid()) { Util.postOnMain { showStatusMessage("On valid position") } onEvent() PositionService.removeListener(positionListener) } } PositionService.addListener(positionListener, EDataType.ImprovedPosition) // listen for first valid position to start the nav Util.postOnMain { showStatusMessage("Waiting for valid position", true) } } ``` The `submitReport()` function creates the report: `val prepareIdOrError = SocialOverlay.prepareReporting()` and if this fails, an error message is displayed in a dialog box. Otherwise, the report, stored in `prepareIdOrError` is sent: `val error = SocialOverlay.report(prepareIdOrError, subCategory.uid, socialReportListener)` If sending fails, a dialog box is displayed, otherwise, a status message indicating that the report was sent is displayed at the bottom of the viewport. MainActivity.kt[](SocialReport/app/src/main/kotlin/com/magiclane/sdk/examples/socialreport/MainActivity.kt?ref_type=heads#L166) ```kotlin private fun submitReport() = SdkCall.execute { val overlayInfo = SocialOverlay.reportsOverlayInfo ?: return@execute val countryISOCode = MapDetails().isoCodeForCurrentPosition ?: return@execute val categories = overlayInfo.getCategories(countryISOCode) ?: return@execute val mainCategory = categories[0] // Police val subcategories = mainCategory.subcategories ?: return@execute val subCategory = subcategories[0] // My side val prepareIdOrError = SocialOverlay.prepareReporting() if (prepareIdOrError <= 0) { val errorMsg = if (prepareIdOrError == GemError.NotFound || prepareIdOrError == GemError.Required) { "Prepare error: ${GemUtil.getUIString(EStringIds.eStrGPSAccuracyIsNotGoodEnough)}" } else { "Prepare error: ${GemError.getMessage(prepareIdOrError)}" } Util.postOnMain { showDialog(errorMsg) } return@execute } val error = SocialOverlay.report(prepareIdOrError, subCategory.uid, socialReportListener) if (GemError.isError(error)) { Util.postOnMain { showDialog("Report Error: ${GemError.getMessage(error)}") } } else { Util.postOnMain { showStatusMessage("Report Sent!") } } } ``` --- ### Places & Search This collection of articles showcases a comprehensive suite of precision-driven location and search techniques - instantly delivering accurate POI results for landmarks, restaurants, businesses, cities, neighborhoods, attractions, and specific addresses; enabling the discovery of nearby, departure-point, or destination-relevant places (for example, EV charging stations) with essential details such as pricing, hours of operation, and landmark information; and providing global data online or offline, all seamlessly embedded directly in the map for up-to-the-minute, reliable navigation every time. [![Free Text Search image](/docs/android/assets/images/1_free_text_search-5a66e91ce1729d638f9c3fde0533e31d.png)](/docs/android/examples/places-search/free-text-search.md) ##### [Free Text Search](/docs/android/examples/places-search/free-text-search.md) [Search locations by text and display them in a list.](/docs/android/examples/places-search/free-text-search.md) [![Search Nearby image](/docs/android/assets/images/2_search_nearby-783e714fa91405516b3fe9b84c1175b0.png)](/docs/android/examples/places-search/search-nearby.md) ##### [Search Nearby](/docs/android/examples/places-search/search-nearby.md) [Search for POIs near the current location.](/docs/android/examples/places-search/search-nearby.md) [![What’s Nearby Category image](/docs/android/assets/images/3_whats_nearby_category-302cd66d8e4382578c36ec2cf9c97b36.png)](/docs/android/examples/places-search/whats-nearby-category.md) ##### [What’s Nearby Category](/docs/android/examples/places-search/whats-nearby-category.md) [search for certain type of POIs, like gas stations near the current location.](/docs/android/examples/places-search/whats-nearby-category.md) [![Favourites image](/docs/android/assets/images/4_favourites-463c860f895af11c9976c34f8d086bc0.png)](/docs/android/examples/places-search/favourites.md) ##### [Favourites](/docs/android/examples/places-search/favourites.md) [Insert/remove a landmark into/from favourites.](/docs/android/examples/places-search/favourites.md) [![Draw and Fly To Polyline image](/docs/android/assets/images/5_draw_and_fly_to_polyline-bda30ef1a1b69f9b527837f0f2fedae4.png)](/docs/android/examples/places-search/draw-and-fly-to-polyline.md) ##### [Draw and Fly To Polyline](/docs/android/examples/places-search/draw-and-fly-to-polyline.md) [Draw a polyline on the map and fly to the location of the polyline.](/docs/android/examples/places-search/draw-and-fly-to-polyline.md) [![Define Persistent Roadblock image](/docs/android/assets/images/6_define_persistent_roadblock-d65df30a319aeec08be362b67e548d5a.png)](/docs/android/examples/places-search/define-persistent-roadblock.md) ##### [Define Persistent Roadblock](/docs/android/examples/places-search/define-persistent-roadblock.md) [Define road block on an interactive map and fly to the road block.](/docs/android/examples/places-search/define-persistent-roadblock.md) [![Map Selection image](/docs/android/assets/images/7_map_selection-38acc94bddc7e77ec52d88bc0e9b92c8.png)](/docs/android/examples/places-search/map-selection.md) ##### [Map Selection](/docs/android/examples/places-search/map-selection.md) [Compute and draw routes; select POIs.](/docs/android/examples/places-search/map-selection.md) --- ### Define Persistent Roadblock |

In this guide you will learn how to render an interactively defined road block on an interactive map and fly to the road block. ![](/docs/android/assets/images/example_kotlin_definepersistentroadblock_screenshot1-497c1efd52085181e2c29b93c52797dc.png) **Initial map**
Once the map is loaded, zoom in and tap on a road to define a road block. ![](/docs/android/assets/images/example_kotlin_definepersistentroadblock_screenshot2-61f9760a7b0292820727a8abee82511f.png) **Roadblock example #1** ![](/docs/android/assets/images/example_kotlin_definepersistentroadblock_screenshot3-dded7c76a726f321d1eab5bc0baa0741.png) **Roadblock example #2** ![](/docs/android/assets/images/example_kotlin_definepersistentroadblock_screenshot4-f484914b0f5259043a45ea5eca5d82c5.png) **Roadblock example #3**
The selected road section is indicated with red roadblock polylines which may be on one or both sides of the road, depending on the road type.
Clicking on a different road section will move the roadblock there.
The map is interactive and fully 3D, supporting pan, pinch-zoom, rotate and tilt.
The `roadblock` variable is defined in the class `MainActivity : AppCompatActivity()` class to store the current section of road set as a roadblock by the user. ```kotlin private var roadblock: TrafficEvent? = null ``` In the class `MainActivity : AppCompatActivity()`, in the `onCreate()` function the x,y position of a user touch event is obtained using `onTouch` and set in `cursorScreenPosition` so the map can be queried about traffic events, if any, at that location.
If there are 1 or more traffic events at the touch location, `if (!trafficEvents.isNullOrEmpty())` then the roadblock is set on the first traffic event in the list (at index 0).
* `val trafficEvent = trafficEvents[0]` * `if (trafficEvent.isRoadblock())`
Otherwise, if there are 1 or more road sections at the touch location, `if (!streets.isNullOrEmpty())` then the roadblock is set on the first street in the list (at index 0) `streets[0].coordinates?.let { addPersistentRoadblock(it) }`. MainActivity.kt[](DefinePersistentRoadblock/app/src/main/kotlin/com/magiclane/sdk/examples/definepersistentroadblock/MainActivity.kt) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { ... gemSurfaceView.mapView?.onTouch = { xy -> SdkCall.execute { // tell the map view where the touch event happened gemSurfaceView.mapView?.cursorScreenPosition = xy val trafficEvents = gemSurfaceView.mapView?.cursorSelectionTrafficEvents if (!trafficEvents.isNullOrEmpty()) { val trafficEvent = trafficEvents[0] if (trafficEvent.isRoadblock()) { return@execute } } val streets = gemSurfaceView.mapView?.cursorSelectionStreets if (!streets.isNullOrEmpty()) { streets[0].coordinates?.let { addPersistentRoadblock(it) } } } } ... } ``` The `addPersistentRoadblock()` function gets the current time and sets the roadblock end time 1 minute into the future.
The roadblock class member variable is initially set to null, as we have seen above. Here it is checked and if it is not null, `roadblock?.let { roadblock ->` that means the user already set a previous roadblock.
In that case, the previous roadblock is removed first: * `roadblock.referencePoint?.let { coordinates ->` * `traffic.removePersistentRoadblock(coordinates) }`
Then the new roadblock is added, and saved in the class member variable: * `roadblock = traffic.addPersistentRoadblock(` * `coords = arrayListOf(coordinates),` * `startUTC = startTime,` * `expireUTC = endTime,` * `transportMode = ERouteTransportMode.Car.value)`
Finally, the camera flies to the bounding box enclosing the roadblock section to center it in the viewing area such that it fills the viewport. MainActivity.kt[](DefinePersistentRoadblock/app/src/main/kotlin/com/magiclane/sdk/examples/definepersistentroadblock/MainActivity.kt) ```kotlin private fun addPersistentRoadblock(coordinates: Coordinates) { val startTime = Time.getUniversalTime() val endTime = Time.getUniversalTime().also { endTime -> endTime?.let { it.minute += 1 } } if (startTime != null && endTime != null) { val traffic = Traffic() roadblock?.let { roadblock -> roadblock.referencePoint?.let { coordinates -> traffic.removePersistentRoadblock(coordinates) } } roadblock = traffic.addPersistentRoadblock( coords = arrayListOf(coordinates), startUTC = startTime, expireUTC = endTime, transportMode = ERouteTransportMode.Car.value ) if (roadblock?.referencePoint?.valid() == true) { roadblock?.boundingBox?.let { gemSurfaceView.mapView?.centerOnArea( area = it, zoomLevel = -1, xy = null, animation = Animation(EAnimation.Linear) ) } Util.postOnMain { hint.visibility = View.GONE } } } } ``` --- ### Draw and Fly To Polyline |

In this guide you will learn how to render an interactive map, draw a polyline on the map and fly to the location of the polyline. ![](/docs/android/assets/images/example_kotlin_drawpolyline_screenshot1-69f49b752745d8dcbb0b4ce33eb18d97.png) **Close-up polyline** ![](/docs/android/assets/images/example_kotlin_drawpolyline_screenshot2-f865286b3638f2f7fcf33bc67e2c54c6.png) **Zoomed out polyline**
The example draws a polyline on the map and flies to it. Displays an interactive map which is fully 3D, supporting pan, pinch-zoom, rotate and tilt. In the `flyToPolyline()` function: * A `MarkerCollection` instance is created. * A `Marker`populated with the coordinate pairs defining the polyline's perimeter is added to that collection. * The collection is rendered on the map via `MarkerCollectionRenderSettings`. * The collection’s bounding box is retrieved (`markerCollection.area`). * If the bounding box is non-null, it is passed to `mapView.centerOnArea()`, which animates the camera to frame the entire polyline within the viewport. MainActivity.kt[](DrawPolyline/app/src/main/kotlin/com/magiclane/sdk/examples/drawpolyline/MainActivity.kt) ```kotlin private fun flyToPolyline() = SdkCall.execute { gemSurfaceView.mapView?.let { mapView -> /* Make a MarkerCollection and a Marker item that will be stored in the collection. You can create multiple Marker items that can be added in the same collection. */ val markerCollection = MarkerCollection(EMarkerType.Polyline, "My marker collection") // Define a market item and add the necessary coordinates to it. val marker = Marker().apply { add(52.360234, 4.886782) add(52.360495, 4.886266) add(52.360854, 4.885539) add(52.361184, 4.884849) add(52.361439, 4.884344) add(52.361593, 4.883986) } // Add the marker item to the collection. markerCollection.add(marker) // Make a list of settings that will decide how each marker collection will be displayed on the map. val settings = MarkerCollectionRenderSettings( polylineInnerColor = Rgba.magenta(), polylineOuterColor = Rgba.black() ).apply { polylineInnerSize = 1.25 polylineOuterSize = 0.75 } // Add the collection to the desired map view so it can be displayed. mapView.preferences?.markers?.add(markerCollection, settings) // Center the map on this marker collection's area. markerCollection.area?.let { mapView.centerOnArea(it) } } } ``` `MainActivity` overrides `onCreate()` which checks that internet access is available, and when the map is initialized, calls the `flyToPolyline()` function. MainActivity.kt[](DrawPolyline/app/src/main/kotlin/com/magiclane/sdk/examples/drawpolyline/MainActivity.kt) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) gemSurfaceView = findViewById(R.id.gem_surface) EspressoIdlingResource.increment() SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady // Defines an action that should be done when the map is ready. flyToPolyline() lifecycleScope.launch { delay(3000) EspressoIdlingResource.decrement() } } SdkSettings.onApiTokenRejected = {/* The TOKEN you provided in the AndroidManifest.xml file was rejected. Make sure you provide the correct value, or if you don't have a TOKEN, check the magiclane.com website, sign up/sign in and generate one. */ showDialog("TOKEN REJECTED") } if (!Util.isInternetConnected(this)) { showDialog("You must be connected to the internet!") } onBackPressedDispatcher.addCallback(this){ finish() exitProcess(0) } } ``` --- ### Favourites |

In this guide you will learn how to insert a landmark into the favourites, and also show to remove a landmark from the favourites. ![](/docs/android/assets/images/example_kotlin_favorites_screenshot1-70ceb4212b70c7d1c037d9c558d35328.png) **Initial landmark** ![](/docs/android/assets/images/example_kotlin_favorites_screenshot2-7fd438f24593cc8d47bf75299631fd9b.png) **Favourite landmark**
note The star icon shows that the landmark is added to the favorites when it is filled, and not added to the favorites when it is just a contour. In the class `MainActivity : AppCompatActivity()`, an instance of each of the following is created: * `var store: LandmarkStore` a landmark store, so the favorited landmark(s) can be written into the data folder; * `val searchService = SearchService()` a search service, so that the id of a resulted landmark can be obtained. The search service implements the `onStarted` and `onCompleted` callbacks. When the search completes, if the result list is not empty, then the first item in the result list (at index 0) is taken and the camera flies to the location of that result.
MainActivity.kt[](Favourites/app/src/main/kotlin/com/magiclane/sdk/examples/favourites/MainActivity.kt) ```kotlin private lateinit var store: LandmarkStore private val searchService = SearchService( onStarted = { progressBar.visibility = View.VISIBLE showStatusMessage("Search service has started!") }, onCompleted = { results, errorCode, _ -> progressBar.visibility = View.GONE showStatusMessage("Search service completed with error code: $errorCode") a when (errorCode) { GemError.NoError -> { if (results.isNotEmpty()) { val landmark = results[0] flyTo(landmark) displayLocationInfo(landmark) showStatusMessage("The search completed without errors.") } else { // The search completed without errors, but there were no results found. showStatusMessage("The search completed without errors, but there were no results found.") } } GemError.Cancel -> { // The search action was cancelled. } else -> { // There was a problem at computing the search operation. showDialog("Search service error: ${GemError.getMessage(errorCode)}") } } EspressoIdlingResource.decrement() } ) ``` The `flyTo(landmark)` shows on the map the searched landmark. After the search completes and the camera flies to the target, it is up to the user whether to add it to the favorites or not, using the button in the lower right corner of the viewport. MainActivity.kt[](Favourites/app/src/main/kotlin/com/magiclane/sdk/examples/favourites/MainActivity.kt) ```kotlin private fun flyTo(landmark: Landmark) = SdkCall.execute { landmark.geographicArea?.let { area -> gemSurfaceView.mapView?.let { mainMapView -> // Center the map on a specific area using the provided animation. mainMapView.centerOnArea(area) // Highlights a specific area on the map using the provided settings. val displaySettings = HighlightRenderSettings( EHighlightOptions.ShowContour, Rgba(255, 98, 0, 255), Rgba(255, 98, 0, 255), 0.75 ) mainMapView.activateHighlightLandmarks(landmark, displaySettings) } } } ``` The `displayLocationInfo()` function is called when the search completes with a non-empty result list. The function displays the name and coordinates of the landmark on screen, as well as a yellow star-shaped icon.
A click listener is set for the star-shaped icon, `imageView.setOnClickListener`, so the landmark is added to the favorites if the icon is clicked and the landmark is not already in the favorites; and conversely, the landmark is removed from the favorites if the icon is clicked and the landmark is already in the favorites. MainActivity.kt[](Favourites/app/src/main/kotlin/com/magiclane/sdk/examples/favourites/MainActivity.kt) ```kotlin private fun displayLocationInfo(landmark: Landmark) { // Display a view containing the necessary information about the landmark. var name = "" var coordinates = "" EspressoIdlingResource.increment() SdkCall.execute { name = landmark.name ?: "Unnamed Location" landmark.coordinates?.apply { coordinates = "$latitude, $longitude" } } Util.postOnMain { locationDetails.apply { val nameView = findViewById(R.id.name) val coordinatesView = findViewById(R.id.coordinates) val imageView = findViewById(R.id.favourites_icon) // Update the favourites icon based on the status of the landmark. updateFavouritesIcon(imageView, getFavouriteId(landmark) != -1) // Display the name and coordinates of the landmark. nameView.text = name coordinatesView.text = coordinates // Treat favourites icon click event (Add/ Remove from favourites) imageView.setOnClickListener { val landmarkId = getFavouriteId(landmark) if (landmarkId != -1) { deleteFromFavourites(landmarkId) updateFavouritesIcon(imageView, false) showStatusMessage("The landmark was deleted from favourites.") } else { addToFavourites(landmark) updateFavouritesIcon(imageView, true) showStatusMessage("The landmark was added to favourites.") } } this.visibility = View.VISIBLE } EspressoIdlingResource.decrement() } } ``` Note that to add or remove a landmark from the favorites, the ID of that landmark must be obtained, using the `getFavouriteId()` function which does a linear search through all landmarks in a specified set, and looks for coordinates that match within a specified threshold. MainActivity.kt[](Favourites/app/src/main/kotlin/com/magiclane/sdk/examples/favourites/MainActivity.kt) ```kotlin private fun getFavouriteId(landmark: Landmark): Int = SdkCall.execute { /* Get the ID of the landmark saved in the store so we can use it to remove it or to check if it's already a favourite. */ val radius = 5.0 // meters val area = landmark.coordinates?.let { RectangleGeographicArea(it, radius, radius) } val landmarks = area?.let { store.getLandmarksByArea(it) } ?: return@execute -1 val threshold = 0.00001 landmarks.forEach { val itCoordinates = it.coordinates val landmarkCoordinates = landmark.coordinates if (itCoordinates != null && landmarkCoordinates != null) { if ((itCoordinates.latitude - landmarkCoordinates.latitude < threshold) && (itCoordinates.longitude - landmarkCoordinates.longitude < threshold)) return@execute it.id } else return@execute -1 } -1 } ?: -1 ``` `MainActivity` overrides the `onCreate()` function which checks that internet access is available, then calls the `createStore()` function, which instantiates a Landmark store to for the favorites, and then does a search for a hardcoded target:
* `createStore()` * `val text = "Statue of Liberty New York"` * `val coordinates = Coordinates(0.0, 0.0)` * `searchService.searchByFilter(text, coordinates)` Note The coordinates of the target are not required for the search and can be set to zero. MainActivity.kt[](Favourites/app/src/main/kotlin/com/magiclane/sdk/examples/favourites/MainActivity.kt) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) imageSize = resources.getDimensionPixelSize(R.dimen.image_size) progressBar = findViewById(R.id.progressBar) gemSurfaceView = findViewById(R.id.gem_surface) locationDetails = findViewById(R.id.location_details) statusText = findViewById(R.id.status_text) EspressoIdlingResource.increment() SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady // Defines an action that should be done after the world map is ready. SdkCall.execute { createStore() val text = "Statue of Liberty New York" val coordinates = Coordinates(40.68925476, -74.04456329) searchService.searchByFilter(text, coordinates) } } SdkSettings.onApiTokenRejected = { /* The TOKEN you provided in the AndroidManifest.xml file was rejected. Make sure you provide the correct value, or if you don't have a TOKEN, check the magiclane.com website, sign up/sign in and generate one. */ showDialog("TOKEN REJECTED") } if (!Util.isInternetConnected(this)) { showDialog("You must be connected to the internet!") } onBackPressedDispatcher.addCallback(this){ finish() exitProcess(0) } } private fun createStore() { store = LandmarkStoreService().createLandmarkStore("Favourites")?.first!! gemSurfaceView.mapView?.let { mainMapView -> SdkCall.execute { mainMapView.preferences?.landmarkStores?.addAllStoreCategories(store.id) } } } ``` --- ### Free Text Search |

In this guide you will learn how to do a text search and display them in a list. ![](/docs/android/assets/images/example_kotlin_search_screenshot1-8ce3bbc72c6d7594fe83cd7bc6ed5a2c.png) **Search results** ##### Search service[​](#search-service "Direct link to Search service") Inside `MainActivity.kt` retain a `SearchService` instance. Normally in Android's MVVM, services are retained in the Repository layer, injected via dependency injection, and ultimately consumed by ViewModels but for the scope of this example we will use it inside an activity. The search service has a `onCompleted` callback which will update the UI. MainActivity.kt[](Search/app/src/main/kotlin/com/magiclane/sdk/examples/search/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { //... private var searchService = SearchService( onCompleted = { results, errorCode, _ -> //... when (errorCode) { GemError.NoError -> { // No error encountered, we can handle the results. refreshList(results) noResultText.isVisible = results.isEmpty() } GemError.Cancel -> { // The search action was cancelled. } GemError.Busy -> { showDialog("Requested operation cannot be performed. Internal limit reached. Please use an API token in order to avoid this error.") } else -> { // There was a problem at computing the search operation. showDialog("Search service error: ${GemError.getMessage(errorCode)}") } } //... } ) //... } ``` ##### Searching by filter[​](#searching-by-filter "Direct link to Searching by filter") Initiate a new search each time the query gets updated. If there is a pre-existing search process it will be canceled. The reference point will be taken into account when calculating the relative distance of query-matching locations found during the search process. As a result, the relevance of these locations will change, and the order in which the results appear may vary depending on the reference point. MainActivity.kt[](Search/app/src/main/kotlin/com/magiclane/sdk/examples/search/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { private lateinit var searchView: SearchView //... override fun onCreate(savedInstanceState: Bundle?) { searchView = findViewById(R.id.search_input) //... searchView.apply { setOnQueryTextListener( object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { clearFocus() return true } override fun onQueryTextChange(newText: String?): Boolean { val filter = (newText ?: "").trim() if (filter.isNotEmpty()) progressBar.visibility = View.VISIBLE // Search the requested filter. search(filter) return true } } ) requestFocus() } //... } //... private fun search(filter: String) = SdkCall.postAsync({ // Cancel any search that is in progress now. searchService.cancelSearch() if (filter.isBlank()) { refreshList(arrayListOf()) noResultText.isVisible = false } // Give a random position if position is not available val position = PositionService.position reference = if (position?.isValid() == true) position.coordinates else Coordinates(51.5072, 0.1276) // center London val res = searchService.searchByFilter(filter, reference) if (GemError.isError(res) && res != GemError.Cancel) { showDialog(GemError.getMessage(res)) } //... }, 200) //... } ``` ##### Refreshing the list[​](#refreshing-the-list "Direct link to Refreshing the list") After each call to `searchService.searchByFilter(filter, reference)` `onCompleted` callback will be invoked by the SDK. If there have been no errors the received `LandmarkList` is mapped to `SearchItem` data class which holds and displays only key information. The resulted list is held in a lifecycle-aware component (`LiveData`) which updates the UI each time a new result list gets submitted. Finally, a [ListAdapter](https://developer.android.com/reference/androidx/recyclerview/widget/ListAdapter) was used for displaying the list. MainActivity.kt[](Search/app/src/main/kotlin/com/magiclane/sdk/examples/search/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { private val _results = MutableLiveData>() private val results: LiveData> get() = _results //... override fun onCreate(savedInstanceState: Bundle?) { //... //observe the list and update UI results.observe(this) { customAdapter.submitList(it) progressBar.visibility = View.GONE } ///... } //... private fun refreshList(results: ArrayList) = SdkCall.execute { val list = results.map { landmark -> val meters = reference?.let { landmark.coordinates?.getDistance(it)?.toInt() ?: 0 } ?: 0 val dist = GemUtil.getDistText(meters, EUnitSystem.Metric, true) SearchItem( landmark.imageAsBitmap(imageSize), landmark.name.toString(), GemUtil.getLandmarkDescription(landmark, true), dist.first, dist.second ) } _results.postValue(list) } //... /** * UI search item data class. */ data class SearchItem( val image: Bitmap? = null, val name: String = "", val descriptionTxt: String = "", val distance: String = "", val unit: String = "" ) //... } ``` --- ### Map Selection |

![](/docs/android/assets/images/example_kotlin_mapselection_screenshot1-d6d3bf22bc6c2dc79a1a6ba5fa7af8f8.png) **First route option** ![](/docs/android/assets/images/example_kotlin_mapselection_screenshot2-be2f8b469216c5f23d0c975558bfbac6.png) **Second route option**
![](/docs/android/assets/images/example_kotlin_mapselection_screenshot3-dbd7559b6bcbf73cf7a6aeaa86463b44.png) **First POI selected** ![](/docs/android/assets/images/example_kotlin_mapselection_screenshot4-d46cc26e6a2ef511527b4d7b90afeac3.png) **Second POI selected**
In the `calculateRoute()` function, 2 `Landmark` instances are defined, one for the departure, and one for the destination coordinates of the route endpoints. A route must have at least 2 Landmark instances(waypoints), but optionally can have more, for any optional additional waypoints along the route.
The starting, or departure point of the route is the first waypoint in a list of 2 or more Landmarks (2 in this case), each containing a name, latitude (in degrees) and longitude (in degrees). The destination point is the last waypoint in the list.
In this example, the route departure point is London, and the destination is Paris, as specified by the `Coordinates` in the 2 `Landmark` instances.
The list of waypoints is passed to the `routingService` to calculate the route. MainActivity.kt[](MapSelection/app/src/main/kotlin/com/magiclane/sdk/examples/mapselection/MainActivity.kt) ```kotlin private fun calculateRoute() = SdkCall.execute { val waypoints = arrayListOf( Landmark("London", 51.5073204, -0.1276475), Landmark("Paris", 48.8566932, 2.3514616) ) routingService.calculateRoute(waypoints) } ``` A `RoutingService` is instantiated to compute a route and render it on the map. The `onStarted` and `onCompleted` callbacks are implemented, to detect when the route computation is started and when it is completed.
When the route computation is completed, and there is no error, the list of resulting routes is rendered on the map using `presentRoutes()`.
The `displayBubble` flag is set to true to show the distance in km, and the time in hours, next to each route. MainActivity.kt[](MapSelection/app/src/main/kotlin/com/magiclane/sdk/examples/mapselection/MainActivity.kt) ```kotlin private val routingService = RoutingService( onStarted = { progressBar.visibility = View.VISIBLE }, onCompleted = { routes, errorCode, _ -> progressBar.visibility = View.GONE when (errorCode) { GemError.NoError -> { routesList = routes SdkCall.execute { gemSurfaceView.mapView?.presentRoutes(routes, displayBubble = true) gemSurfaceView.mapView?.preferences?.routes?.mainRoute?.let { selectRoute(it) } } flyToRoutesButton.visibility = View.VISIBLE } GemError.Cancel -> { // The routing action was cancelled. } else -> { // There was a problem at computing the routing operation. showDialog("Routing service error: ${GemError.getMessage(errorCode)}") } } EspressoIdlingResource.decrement() } ) ``` The `onCreate()` function calls the `calculateRoute()` function shown above, once the map is ready( instantiated and loaded ). It also uses `findViewById()` to get pointers/handles to each of the various graphical elements, such as the button in the lower left used for flying to the calculated routes, or the other button used for flying to the position of the device, also referred to as GPS position which appears in the lower left of the viewport.
The fly to GPS position button, which looks like a target, accessed by `followCursorButton`, is shown only if the camera is not already following the position of the device, indicated by a green arrow on the map.
The center on routes button, with a stylized s-shaped serpentine road section drawn on it, accessed by `flyToRoutesButton`, is used to fly back to the currently selected precalculated route, after panning away from it for example; this button is shown after the route is calculated and rendered. MainActivity.kt[](MapSelection/app/src/main/kotlin/com/magiclane/sdk/examples/mapselection/MainActivity.kt) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) progressBar = findViewById(R.id.progress_bar) gemSurfaceView = findViewById(R.id.gem_surface) overlayContainer = findViewById(R.id.overlay_container) name = findViewById(R.id.name) description = findViewById(R.id.description) image = findViewById(R.id.image) followCursorButton = findViewById(R.id.follow_cursor) flyToRoutesButton = findViewById(R.id.fly_to_route).also { it.setOnClickListener { SdkCall.execute { gemSurfaceView.mapView?.let { mapView -> mapView.deactivateAllHighlights() mapView.preferences?.routes?.mainRoute?.let { mainRoute -> selectRoute(mainRoute) } } } } } imageSize = resources.getDimension(R.dimen.image_size).toInt() EspressoIdlingResource.increment() SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady // Defines an action that should be done when the world map is ready (Updated/ loaded). calculateRoute() // Set GPS button if location permission is granted, otherwise request permission SdkCall.execute { val hasLocationPermission = PermissionsHelper.hasPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) if (hasLocationPermission) { Util.postOnMain { enableGPSButton() } } else { requestPermissions(this) } } ... } ... } ``` The `requestPermissions()` function asks the user for internet and location permissions. The `onRequestPermissionsResult()` function is overridden, to receive the results of the permission requests. MainActivity.kt[](MapSelection/app/src/main/kotlin/com/magiclane/sdk/examples/mapselection/MainActivity.kt) ```kotlin private fun requestPermissions(activity: Activity): Boolean { val permissions = arrayListOf( Manifest.permission.INTERNET, Manifest.permission.ACCESS_NETWORK_STATE, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION ) return PermissionsHelper.requestPermissions( REQUEST_PERMISSIONS, activity, permissions.toTypedArray() ) } ``` The `onTouch` listener is defined for the `mapView` to get the `cursorScreenPosition`. Once the `x, y` position of the user touch event has been set in the `mapView`, the code verifies what map element was selected.
`val myPosition = gemSurfaceView.mapView?.cursorSelectionSceneObject` First, check if it is a scene object, such as a 3D car or plane, etc, simulated in the scene. If so, `myPosition.coordinates` gets the coordinates of the scene object (it), and uses the `gemSurfaceView.mapView?.centerOnCoordinates` function to move the camera with a flying animation (`Animation(EAnimation.Linear)`) to the selected object.
`val landmarks = gemSurfaceView.mapView?.cursorSelectionLandmarks` Next, check if the selected object is a landmark. `val contour = landmark.getContourGeographicArea()` If landmarks is not null, check if it is a geographic area, (contour is also not null) and in that case, use the `gemSurfaceView.mapView?.centerOnArea() `function to move the camera with a flying animation to the selected region, such that the bounding box of the selected region fits snugly in the viewport.
If contour is null, then this is a simple landmark, so just get its coordinates and move the camera to the landmark with a flying animation: `landmark.coordinates?.let { gemSurfaceView.mapView?.centerOnCoordinates()}` MainActivity.kt[](MapSelection/app/src/main/kotlin/com/magiclane/sdk/examples/mapselection/MainActivity.kt) ```kotlin // onTouch event callback gemSurfaceView.mapView?.onTouch = { xy -> // xy are the coordinates of the touch event SdkCall.execute { // tell the map view where the touch event happened gemSurfaceView.mapView?.cursorScreenPosition = xy gemSurfaceView.mapView?.deactivateAllHighlights() val centerXy = Xy(gemSurfaceView.measuredWidth / 2, gemSurfaceView.measuredHeight / 2) val myPosition = gemSurfaceView.mapView?.cursorSelectionSceneObject if (myPosition != null && isSameMapScene( myPosition, MapSceneObject.getDefPositionTracker().first!! ) ) { showOverlayContainer( getString(R.string.my_position), "", ContextCompat.getDrawable(this, R.drawable.ic_current_location_arrow) ?.toBitmap(imageSize, imageSize) ) myPosition.coordinates?.let { gemSurfaceView.mapView?.centerOnCoordinates( it, -1, centerXy, Animation(EAnimation.Linear), Double.MAX_VALUE, 0.0 ) } return@execute } val landmarks = gemSurfaceView.mapView?.cursorSelectionLandmarks if (!landmarks.isNullOrEmpty()) { val landmark = landmarks[0] landmark.run { showOverlayContainer( name.toString(), description.toString(), image?.asBitmap(imageSize, imageSize) ) } val contour = landmark.getContourGeographicArea() if (contour != null && !contour.isEmpty()) { contour.let { gemSurfaceView.mapView?.centerOnArea( it, -1, centerXy, Animation(EAnimation.Linear), ) val displaySettings = HighlightRenderSettings( EHighlightOptions.ShowContour, Rgba(255, 98, 0, 255), Rgba(255, 98, 0, 255), 0.75 ) gemSurfaceView.mapView?.activateHighlightLandmarks( landmark, displaySettings ) } } else { landmark.coordinates?.let { gemSurfaceView.mapView?.centerOnCoordinates( it, -1, centerXy, Animation(EAnimation.Linear), Double.MAX_VALUE, 0.0 ) } } return@execute } val trafficEvents = gemSurfaceView.mapView?.cursorSelectionTrafficEvents if (!trafficEvents.isNullOrEmpty()) { hideOverlayContainer() openWebActivity(trafficEvents[0].previewUrl.toString()) return@execute } val overlays = gemSurfaceView.mapView?.cursorSelectionOverlayItems if (!overlays.isNullOrEmpty()) { val overlay = overlays[0] if (overlay.overlayInfo?.uid == ECommonOverlayId.Safety.value) { hideOverlayContainer() openWebActivity(overlay.getPreviewUrl(Size()).toString()) } else { overlay.run { showOverlayContainer( name.toString(), overlayInfo?.name.toString(), image?.asBitmap(imageSize, imageSize) ) } overlay.coordinates?.let { gemSurfaceView.mapView?.centerOnCoordinates( it, -1, centerXy, Animation(EAnimation.Linear), Double.MAX_VALUE, 0.0 ) } } return@execute } // get the visible routes at the touch event point val routes = gemSurfaceView.mapView?.cursorSelectionRoutes // check if there is any route if (!routes.isNullOrEmpty()) { // set the touched route as the main route and center on it val route = routes[0] selectRoute(route) return@execute } } } ``` Also check if the selected object is a traffic event * `val trafficEvents = gemSurfaceView.mapView?.cursorSelectionTrafficEvents` and if so, get the string description of the first one(`openWebActivity(trafficEvents[0].previewUrl.toString())`) at index 0, in case there are more than one. MainActivity.kt[](MapSelection/app/src/main/kotlin/com/magiclane/sdk/examples/mapselection/MainActivity.kt) ```kotlin val trafficEvents = gemSurfaceView.mapView?.cursorSelectionTrafficEvents if (!trafficEvents.isNullOrEmpty()) { hideOverlayContainer() openWebActivity(trafficEvents[0].previewUrl.toString()) return@execute } ``` Check if the selected object is an overlay(`val overlays = gemSurfaceView.mapView?.cursorSelectionOverlayItems`) and in the usual case fly to it's coordinates `overlay.coordinates?.let { gemSurfaceView.mapView?.centerOnCoordinates() }`. MainActivity.kt[](MapSelection/app/src/main/kotlin/com/magiclane/sdk/examples/mapselection/MainActivity.kt) ```kotlin val overlays = gemSurfaceView.mapView?.cursorSelectionOverlayItems if (!overlays.isNullOrEmpty()) { val overlay = overlays[0] if (overlay.overlayInfo?.uid == ECommonOverlayId.Safety.value) { hideOverlayContainer() openWebActivity(overlay.getPreviewUrl(Size()).toString()) } else { overlay.run { showOverlayContainer( name.toString(), overlayInfo?.name.toString(), image?.asBitmap(imageSize, imageSize) ) } overlay.coordinates?.let { gemSurfaceView.mapView?.centerOnCoordinates( it, -1, centerXy, Animation(EAnimation.Linear), Double.MAX_VALUE, 0.0 ) } } return@execute } ``` Finally, get the visible routes at the touch event point * `val routes = gemSurfaceView.mapView?.cursorSelectionRoutes` and if there is any route under the cursor, get a list of routes with the `cursorSelectionRoutes` function.
If the resulting list of routes is not empty, then the first route in this list, at index 0, is set as the main route, and `selectRoute()` is called on this route to center it with an animated motion. MainActivity.kt[](MapSelection/app/src/main/kotlin/com/magiclane/sdk/examples/mapselection/MainActivity.kt) ```kotlin ... // get the visible routes at the touch event point val routes = gemSurfaceView.mapView?.cursorSelectionRoutes // check if there is any route if (!routes.isNullOrEmpty()) { // set the touched route as the main route and center on it val route = routes[0] selectRoute(route) return@execute } } } } ``` The `selectRoute()` function uses `centerOnRoutes()` to fly the camera with an animated motion to the set of computed routes, if there are more than one in the result, such that their bounding box fits in the viewport, so that all alternate routes are visible.
If a route is selected by the user then that route is set as the mainRoute in order to highlight that particular route.
Note that centering on a particular route with an animated camera motion can also be done like this: `gemSurfaceView.mapView?.centerOnRoute(route)` MainActivity.kt[](MapSelection/app/src/main/kotlin/com/magiclane/sdk/examples/mapselection/MainActivity.kt) ```kotlin private fun selectRoute(route: Route) { gemSurfaceView.mapView?.apply { route.apply { showOverlayContainer( summary.toString(), "", ContextCompat.getDrawable( this@MainActivity, if (isDarkThemeOn()) R.drawable.ic_baseline_route_24_night else R.drawable.ic_baseline_route_24 )?.toBitmap(imageSize, imageSize) ) } preferences?.routes?.mainRoute = route } gemSurfaceView.mapView?.centerOnRoutes(routesList) } ``` The `showOverlayContainer()` function is used to show for example the outline of the selected building, or park, or the border of the selected country, etc. MainActivity.kt[](MapSelection/app/src/main/kotlin/com/magiclane/sdk/examples/mapselection/MainActivity.kt) ```kotlin private fun showOverlayContainer(name: String, description: String, image: Bitmap?) = Util.postOnMain { if (!overlayContainer.isVisible) { overlayContainer.visibility = View.VISIBLE } this.name.text = name if (description.isNotEmpty()) { this.description.apply { text = description visibility = View.VISIBLE } } else { this.description.visibility = View.GONE } this.image.setImageBitmap(image) } ``` The `onCreate()` function also checks if internet access is available and if not, shows a dialog that internet connection is required (because it is used to download map data). MainActivity.kt[](MapSelection/app/src/main/kotlin/com/magiclane/sdk/examples/mapselection/MainActivity.kt) ```kotlin SdkSettings.onApiTokenRejected = { /* The TOKEN you provided in the AndroidManifest.xml file was rejected. Make sure you provide the correct value, or if you don't have a TOKEN, check the magiclane.com website, sign up/sign in and generate one. */ showDialog("TOKEN REJECTED") } if (!Util.isInternetConnected(this)) { showDialog("You must be connected to the internet!") } ``` --- ### Search Nearby |

In this guide you will learn how to search for points of interest (POIs) near the current location. ![](/docs/android/assets/images/example_kotlin_whatsnearby_screenshot1-75dc89e21e6fd9959bdb36ff5ce10330.png) **Nearby locations**
If location permission was given by the user, then the `search()` function can use the `SearchService` to search around the position given by the `PositionService`. MainActivity.kt[](WhatsNearby/app/src/main/kotlin/com/magiclane/sdk/examples/whatsnearby/MainActivity.kt) ```kotlin private fun search() = SdkCall.execute { // If one of the location permissions is granted, we can do the search around action. val hasPermissions = PermissionsHelper.hasPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) if (!hasPermissions) return@execute // Cancel any search that is in progress now. searchService.cancelSearch() PositionService.getCurrentPosition()?.let { reference = it // Search around position using the provided search preferences and/ or filter. searchService.searchAroundPosition(it) } } ``` Then it starts the search with the API function `searchService.searchAroundPosition(it)` and when the search completes, the `searchService.onCompleted` callback function is invoked to put the results in a list for display: MainActivity.kt[](WhatsNearby/app/src/main/kotlin/com/magiclane/sdk/examples/whatsnearby/MainActivity.kt) ```kotlin onCompleted = onCompleted@{ results, errorCode, _ -> progressBar.visibility = View.GONE when (errorCode) { GemError.NoError -> { // No error encountered, we can handle the results. if (results.isNotEmpty()) { reference?.let { listView.adapter = CustomAdapter(it, results, imageSize) } } else { // The search completed without errors, but there were no results found. showDialog("No results!") } } GemError.Cancel -> { // The search action was cancelled. } else -> { // There was a problem at computing the search operation. showDialog("Search service error: ${GemError.getMessage(errorCode)}") } } } ``` --- ### What's Nearby Category |

In this guide you will learn how to search for points of interest (POIs) near the current location looking for a certain type of POIs, like gas stations. ![](/docs/android/assets/images/example_kotlin_whatsnearbycategory_screenshot1-6a85925157a62e0af031da5fa63f3bfb.png) **Nearby gas stations**
A `searchService = SearchService()` is instantiated to carry out searching for selected category's POIs around the current actual or simulated position of the device. The `onCompleted` listener is defined, which is invoked when the search completes and checks if there was no error and that the results are not empty. When successful, it passes the non-empty result list to the `CustomAdapter` to be displayed in a scrollable list of nearby POIs. MainActivity.kt[](WhatsNearbyCategory/app/src/main/kotlin/com/magiclane/sdk/examples/whatsnearbycategory/MainActivity.kt) ```kotlin private val searchService = SearchService( onStarted = { progressBar.visibility = View.VISIBLE }, onCompleted = onCompleted@{ results, errorCode, _ -> progressBar.visibility = View.GONE when (errorCode) { GemError.NoError -> { val reference = reference ?: return@onCompleted if (results.isEmpty()) { // The search completed without errors, but there were no results found. showDialog("No results!") return@onCompleted } a listView.adapter = CustomAdapter(reference, results, imageSize) decrement() } GemError.Cancel -> { // The search action was cancelled. } else -> { // There was a problem at computing the search operation. showDialog("Search service error: ${GemError.getMessage(errorCode)}") } } } ) ``` `MainActivity` overrides the `onCreate` function, which checks that internet access is available, initializes the SDK, and requests location permission from the user. When the map is ready (loaded and initialized), it automatically invokes the search. MainActivity.kt[](WhatsNearbyCategory/app/src/main/kotlin/com/magiclane/sdk/examples/whatsnearbycategory/MainActivity.kt) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) imageSize = resources.getDimension(R.dimen.landmark_image_size).toInt() listView = findViewById(R.id.list_view) progressBar = findViewById(R.id.progressBar) val layoutManager = LinearLayoutManager(this) listView.layoutManager = layoutManager val separator = DividerItemDecoration(applicationContext, layoutManager.orientation) listView.addItemDecoration(separator) listView.setBackgroundResource(R.color.background_color) val lateralPadding = resources.getDimension(R.dimen.big_padding).toInt() listView.setPadding(lateralPadding, 0, lateralPadding, 0) increment() SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady // Defines an action that should be done after the world map is ready. search() } SdkSettings.onApiTokenRejected = { /* The TOKEN you provided in the AndroidManifest.xml file was rejected. Make sure you provide the correct value, or if you don't have a TOKEN, check the magiclane.com website, sign up/sign in and generate one. */ showDialog("TOKEN REJECTED") } // This step of initialization is mandatory if you want to use the SDK without a map. if (!GemSdk.initSdkWithDefaults(this)) { // The SDK initialization was not completed. finish() } /* The SDK initialization completed with success, but for the search action to be executed properly the app needs permission to get your location. Not requesting this permission or not granting it will make the search fail. */ requestPermissions(this) if (!Util.isInternetConnected(this)) { showDialog("You must be connected to the internet!") } onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { finish() exitProcess(0) } }) } ``` If location permission was given by the user, then the `search()` function can call the `searchAround()` function, which cancels the ongoing search in progress, if any, and then starts a search for gas station POIs: `searchService.searchAroundPosition(EGenericCategoriesIDs.GasStation)`. MainActivity.kt[](WhatsNearbyCategory/app/src/main/kotlin/com/magiclane/sdk/examples/whatsnearbycategory/MainActivity.kt) ```kotlin private fun search() = SdkCall.execute { // If one of the location permissions is granted, we can do the search around action. val hasPermissions = PermissionsHelper.hasPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) if (!hasPermissions) return@execute a PositionService.getCurrentPosition()?.let { searchAround(it) } } private fun searchAround(reference: Coordinates) = SdkCall.execute { this.reference = reference a // Cancel any search that is in progress now. searchService.cancelSearch() a // Search around position using the provided search preferences and/or filter. searchService.searchAroundPosition(EGenericCategoriesIDs.GasStation) } ``` note When no category is selected, the example searches for the closest POIs based on the user actual or simulated location. When the search completes, the `searchService.onCompleted` listener callback function, defined in the `searchService`, is invoked to put the results in a scrollable list for display, using a `CustomAdapter`: MainActivity.kt[](WhatsNearbyCategory/app/src/main/kotlin/com/magiclane/sdk/examples/whatsnearbycategory/MainActivity.kt) ```kotlin /** * This custom adapter is made to facilitate the displaying of the data from the model * and to decide how it is displayed. */ class CustomAdapter( private val reference: Coordinates, private val dataSet: ArrayList, private val imageSize: Int ) : RecyclerView.Adapter() { class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val image: ImageView = view.findViewById(R.id.image) val text: TextView = view.findViewById(R.id.text) val description: TextView = view.findViewById(R.id.description) val status: TextView = view.findViewById(R.id.status_text) val statusDescription: TextView = view.findViewById(R.id.status_description) } override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(viewGroup.context) .inflate(R.layout.list_item, viewGroup, false) return ViewHolder(view) } override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) = SdkCall.execute { val meters = dataSet[position].coordinates?.getDistance(reference)?.toInt() ?: 0 val dist = getDistText(meters, EUnitSystem.Metric, true) viewHolder.run { image.setImageBitmap(dataSet[position].imageAsBitmap(imageSize)) text.text = dataSet[position].name description.text = GemUtil.getLandmarkDescription(dataSet[position], true) status.text = dist.first statusDescription.text = dist.second } } ?: Unit override fun getItemCount() = dataSet.size } ``` --- ### Routing & Navigation Easily find the best route to a desired destination with precise data and live traffic. Regardless whether your users are online or offline. [![Routing On Map image](/docs/android/assets/images/1_routing_on_map-322c4ab73c4f68c99441ab5c15828775.png)](/docs/android/examples/routing-navigation/routing-on-map.md) ##### [Routing On Map](/docs/android/examples/routing-navigation/routing-on-map.md) [Compute and render a route on the map and fly to the route.](/docs/android/examples/routing-navigation/routing-on-map.md) [![Route Instructions image](/docs/android/assets/images/2_route_instructions-3e136192696e00c713ea7dfbd82487bf.png)](/docs/android/examples/routing-navigation/route-instructions.md) ##### [Route Instructions](/docs/android/examples/routing-navigation/route-instructions.md) [Display the text instructions for a computed route.](/docs/android/examples/routing-navigation/route-instructions.md) [![Route Navigation image](/docs/android/assets/images/3_route_navigation-e46dfcbbfa5413f65aceee554bdbb25d.png)](/docs/android/examples/routing-navigation/route-navigation.md) ##### [Route Navigation](/docs/android/examples/routing-navigation/route-navigation.md) [Do real navigation from the current position to a desired destination.](/docs/android/examples/routing-navigation/route-navigation.md) [![Route Simulation With Instructions image](/docs/android/assets/images/4_route_simulation_with_instruction-dd173cb980a057621279c89e4cccbdad.png)](/docs/android/examples/routing-navigation/route-simulation-with-instructions.md) ##### [Route Simulation With Instructions](/docs/android/examples/routing-navigation/route-simulation-with-instructions.md) [Do route simulation with on-screen text and turn arrow instructions.](/docs/android/examples/routing-navigation/route-simulation-with-instructions.md) [![Lane Instructions Simulated Navigation image](/docs/android/assets/images/5_lane_instruction_simulated_navigation-5fc93b165c01533283cd088577f57015.png)](/docs/android/examples/routing-navigation/lane-instruction-simulated-navigation.md) ##### [Lane Instructions Simulated Navigation](/docs/android/examples/routing-navigation/lane-instruction-simulated-navigation.md) [Show lane instruction icons at the bottom of the viewport.](/docs/android/examples/routing-navigation/lane-instruction-simulated-navigation.md) [![External Position Source Navigation image](/docs/android/assets/images/6_external_position_source_navigation-ed622ffbaaef75713424ef0b262e43d8.png)](/docs/android/examples/routing-navigation/external-position-source-navigation.md) ##### [External Position Source Navigation](/docs/android/examples/routing-navigation/external-position-source-navigation.md) [Simulate navigation along a route defined by an external data source.](/docs/android/examples/routing-navigation/external-position-source-navigation.md) [![Voice Downloading image](/docs/android/assets/images/7_voice_downloading-c95d0222117f1afa4d1c17d51a978c61.png)](/docs/android/examples/routing-navigation/voice-downloading.md) ##### [Voice Downloading](/docs/android/examples/routing-navigation/voice-downloading.md) [List all navigation instruction voices available on the server.](/docs/android/examples/routing-navigation/voice-downloading.md) [![Search Along Route image](/docs/android/assets/images/8_search_along_route-f75a9817f2480a6b6bc51fa459972be2.png)](/docs/android/examples/routing-navigation/search-along-route.md) ##### [Search Along Route](/docs/android/examples/routing-navigation/search-along-route.md) [Search along the route for POIs such as fuel stations.](/docs/android/examples/routing-navigation/search-along-route.md) [![Route Terrain Profile image](/docs/android/assets/images/9_route_terrain_profile-9f4629d5f71a666184d4f88d86656c06.png)](/docs/android/examples/routing-navigation/route-terrain-profile.md) ##### [Route Terrain Profile](/docs/android/examples/routing-navigation/route-terrain-profile.md) [Display terrain statistics for a route.](/docs/android/examples/routing-navigation/route-terrain-profile.md) [![Truck Profile image](/docs/android/assets/images/10_truck_profile-caed74c2e736536d5d3d295b041de29f.png)](/docs/android/examples/routing-navigation/truck-profile.md) ##### [Truck Profile](/docs/android/examples/routing-navigation/truck-profile.md) [Compute a route based on the accessibility given by the physical characteristics of a truck.](/docs/android/examples/routing-navigation/truck-profile.md) [![Set TTS Language image](/docs/android/assets/images/11_set_tts_language-d1b121f75f9ac8cf87d7d318244e94e7.png)](/docs/android/examples/routing-navigation/set-tts-language.md) ##### [Set TTS Language](/docs/android/examples/routing-navigation/set-tts-language.md) [Set or change the TTS voice instruction language.](/docs/android/examples/routing-navigation/set-tts-language.md) --- ### External Position Source Navigation |

In this guide you will learn how to simulate navigation along a route defined by an external data source. ![](/docs/android/assets/images/example_kotlin_externalpositionsourcenavigation_screenshot1-06cd2f28b681cb95c083e064eab80f84.png) **Preview of route to navigate** ##### Navigation[​](#navigation "Direct link to Navigation") A navigation is started similar to [Route Simpulation With Instructions Example](/docs/android/examples/routing-navigation/route-simulation-with-instructions.md). The key difference is in the `startSimulation(..)` method. Contine reading. ##### Creating an external data source[​](#creating-an-external-data-source "Direct link to Creating an external data source") To create an external data source an array of coordinates must be provided. These coordinates would normally be provided by the backend but for our example a simple local array of coordinates will be used. MainActivity.kt[](ExternalPositionSourceNavigation/app/src/main/kotlin/com/magiclane/sdk/examples/externalpositionsourcenavigation/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { companion object { val positions = arrayOf( Pair(48.133931, 11.582914), Pair(48.134015, 11.583203), Pair(48.134057, 11.583348), //... } //... } ``` To start a navigation a destination must also be provided : MainActivity.kt[](ExternalPositionSourceNavigation/app/src/main/kotlin/com/magiclane/sdk/examples/externalpositionsourcenavigation/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { companion object { //... val destination = Pair(48.17192581, 11.80789822) } //... } ``` In the code below is shown how to create an external data source. MainActivity.kt[](ExternalPositionSourceNavigation/app/src/main/kotlin/com/magiclane/sdk/examples/externalpositionsourcenavigation/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { //... override fun onCreate(savedInstanceState: Bundle?) { //... SdkSettings.onMapDataReady = { mapReady -> i if (mapReady) { var externalDataSource: ExternalDataSource? SdkCall.execute { externalDataSource = DataSourceFactory.produceExternal(arrayListOf(EDataType.Position)) externalDataSource?.start() positionListener = PositionListener { position: PositionData -> if (position.isValid()) { navigationService.startNavigation( Landmark("Poing", destination.first, destination.second), navigationListener, routingProgressListener ) PositionService.removeListener(positionListener) } } PositionService.dataSource = externalDataSource PositionService.addListener(positionListener) var index = 0 externalDataSource?.let { dataSource -> timer = fixedRateTimer("timer", false, 0L, 1000) { SdkCall.execute { val externalPosition = PositionData.produce( System.currentTimeMillis(), positions[index].first, positions[index].second, -1.0, positions.getBearing(index), positions.getSpeed(index) ) externalPosition?.let { pos -> dataSource.pushData(pos) } } index++ if (index == positions.size) index = 0 } } } } //... } //... } ``` Let's break it down and understand what is happening! MainActivity.kt[](ExternalPositionSourceNavigation/app/src/main/kotlin/com/magiclane/sdk/examples/externalpositionsourcenavigation/MainActivity.kt) ```kotlin //... externalDataSource = DataSourceFactory.produceExternal(arrayListOf(EDataType.Position)) externalDataSource?.start() positionListener = PositionListener { position: PositionData -> if (position.isValid()) { navigationService.startNavigation( Landmark("Poing", destination.first, destination.second), navigationListener, routingProgressListener ) PositionService.removeListener(positionListener) } } PositionService.dataSource = externalDataSource PositionService.addListener(positionListener) //... ``` The `arrayListOf(EDataType.Position)` tells the factory that the data types that are going to be pushed in data source are of type position. After an external data source is produced succesfully it's started and provided to `PositionService`. The `PositionService` receives a `PositionListener` that will start navigation as soon as teh first valid position is received then remove itself. After this `PositioDate` will be fed to the source at fixed time intervals. The `PositionData` must have : acquisitionTimestamp, latitude and longitude. However for a better representation extra data has been provided. MainActivity.kt[](ExternalPositionSourceNavigation/app/src/main/kotlin/com/magiclane/sdk/examples/externalpositionsourcenavigation/MainActivity.kt) ```kotlin //... var index = 0 externalDataSource?.let { dataSource -> timer = fixedRateTimer("timer", false, 0L, 1000) { SdkCall.execute { val externalPosition = PositionData.produce( System.currentTimeMillis(), positions[index].first, positions[index].second, -1.0, positions.getBearing(index), positions.getSpeed(index) ) externalPosition?.let { pos -> dataSource.pushData(pos) } } index++ if (index == positions.size) index = 0 } } ``` To get bearing value: MainActivity.kt[](ExternalPositionSourceNavigation/app/src/main/kotlin/com/magiclane/sdk/examples/externalpositionsourcenavigation/MainActivity.kt) ```kotlin /** * Calculates bearing between 2 points Formula β = atan2(X,Y) where X and Y are two quantities * that can be calculated based on the given latitude and longitude * @return Bearing value between point at [index] and previous point. * If there is no previous point returns -1.0 */ fun Array>.getBearing(index: Int): Double { if ((index > 0) && (index < size)) { val x = cos(this[index].first) * sin(this[index].second - this[index - 1].second) val y = cos(this[index - 1].first) * sin(this[index].first) - sin(this[index - 1].first) * cos( this[index].first ) * cos(this[index].second - this[index - 1].second) return (atan2(x, y) * 180) / Math.PI } return -1.0 } ``` To get speed value: MainActivity.kt[](ExternalPositionSourceNavigation/app/src/main/kotlin/com/magiclane/sdk/examples/externalpositionsourcenavigation/MainActivity.kt) ```kotlin /** * @return speed value equal with distance between point at [index] and previous point. * If there is no previous point returns -1.0 */ fun Array>.getSpeed(index: Int): Double { if ((index > 0) && (index < size)) return this[index - 1].getDistanceOnGeoid(this[index]) return -1.0 } /** * Mathematical formula for calculating real distance between 2 coordinates * @return real distance between 2 geographical points */ fun Pair.getDistanceOnGeoid(to: Pair): Double { val (latitude1, longitude1) = this val (latitude2, longitude2) = to // convert degrees to radians val lat1 = latitude1 * Math.PI / 180.0 val lon1 = longitude1 * Math.PI / 180.0 val lat2 = latitude2 * Math.PI / 180.0 val lon2 = longitude2 * Math.PI / 180.0 // radius of earth in metres val r = 6378100.0 // P val rho1 = r * cos(lat1) val z1 = r * sin(lat1) val x1 = rho1 * cos(lon1) val y1 = rho1 * sin(lon1) // Q val rho2 = r * cos(lat2) val z2 = r * sin(lat2) val x2 = rho2 * cos(lon2) val y2 = rho2 * sin(lon2) // dot product val dot = (x1 * x2 + y1 * y2 + z1 * z2) val cosTheta = dot / (r * r) val theta = acos(cosTheta) // distance in Metres return (r * theta) } ``` --- ### Lane Instructions Simulated Navigation |

In this guide you will learn how to includes lane instruction icons which appear at the bottom of the viewport whenever nearing an upcoming bifurcation, or branching, of the road. ![](/docs/android/assets/images/example_kotlin_laneinstructions_screenshot1-4cf3f9c626232c277d472d0eb1cfb769.png) **Lane panel example #1** ![](/docs/android/assets/images/example_kotlin_laneinstructions_screenshot2-7f365fcfa2193053707c7e1620187e6b.png) **Lane panel example #2**
A navigation is started similar to [Route Navigation Example](/docs/android/examples/routing-navigation/route-navigation.md) but instead of starting a real navigation a simulation is started instead. Since a simulation doesn't need current location there is no need to use `PositionService` to listen for valid positions nor is it needed to have location permission. MainActivity.kt[](LaneInstructions/app/src/main/kotlin/com/magiclane/sdk/examples/laneinstructions/MainActivity.kt) ```kotlin private fun startSimulation() = SdkCall.execute { val waypoints = arrayListOf( Landmark("Toamnei", 45.65060409523955, 25.616351544839894), Landmark("Harmanului", 45.657543255739384, 25.620411332785498) ) navigationService.startSimulation(waypoints, navigationListener, routingProgressListener) } ``` The navigation listener has an `onNavigationInstructionUpdated` callback instance which passes `NavigationInstruction` objects while the navigation is ongoing. Each `NavigationInstruction` object contains a `laneImage` which is converted to a bitmap and then used to update the UI. MainActivity.kt[](LaneInstructions/app/src/main/kotlin/com/magiclane/sdk/examples/laneinstructions/MainActivity.kt) ```kotlin 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() } } }, onDestinationReached = { SdkCall.execute { gemSurfaceView.mapView?.hideRoutes() } }, onNavigationInstructionUpdated = { instr -> // Fetch the bitmap for recommended lanes. val lanes = SdkCall.execute { instr.laneImage?.asBitmap(150, 30, activeColor = Rgba.white()) }?.second // Show the lanes instruction. laneImage.isVisible = lanes != null lanes?.let { laneImage.setImageBitmap(it)} } ) ``` --- ### Route Instructions |

In this guide you will learn how to get the text instructions for a computed route. This example computes a route and then displays the text instructions for navigation along that route. ![](/docs/android/assets/images/example_kotlin_routeinstructions-c70eacf028c48e302483dc26d1e9c48a.png) **Route instruction list** ##### Routing[​](#routing "Direct link to Routing") A route must be calculated similar to `RoutingOnMap` example. See [Routing on Map Example](/docs/android/examples/routing-navigation/routing-on-map.md). MainActivity.kt[](RouteInstructions/app/src/main/kotlin/com/magiclane/sdk/examples/routeinstructions/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { //... private val routingService = RoutingService( onStarted = { progressBar.visibility = View.VISIBLE }, onCompleted = { routes, errorCode, _ -> progressBar.visibility = View.GONE when (errorCode) { GemError.NoError -> { if (routes.size == 0) return@onCompleted // Get the main route from the ones that were found. displayRouteInstructions(routes[0]) } GemError.Cancel -> { // The routing action was cancelled. } else -> { // There was a problem at computing the routing operation. Toast.makeText( this@MainActivity, "Routing service error: ${GemError.getMessage(errorCode)}", Toast.LENGTH_SHORT ).show() } } } ) //... } ``` ##### Displaying route instructions[​](#displaying-route-instructions "Direct link to Displaying route instructions") MainActivity.kt[](RouteInstructions/app/src/main/kotlin/com/magiclane/sdk/examples/routeinstructions/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { //... private fun displayRouteInstructions(route: Route) { // Get the instructions from the route. val instructions = SdkCall.execute { route.instructions } ?: arrayListOf() val imageSize = resources.getDimension(R.dimen.turn_image_size).toInt() listView.adapter = CustomAdapter(instructions, imageSize, isDarkThemeOn()) //... } //... } ``` The function `displayRouteInstructions(..)` passes a list of `RouteInstruction` to a recyclerview adapter. Inside the `onBindViewHolder(..)` method of the adapter the key informations are filtered and prepared for display. MainActivity.kt[](RouteInstructions/app/src/main/kotlin/com/magiclane/sdk/examples/routeinstructions/MainActivity.kt) ```kotlin override fun onBindViewHolder(viewHolder: RouteInstructionViewHolder, position: Int) { val instruction = dataSet[position] var text: String var status: String var description: String var turnImage: Bitmap? SdkCall.execute { if (instruction.hasTurnInfo()) { val aInner = if (isDarkThemeOn) Rgba(255, 255, 255, 255) else Rgba(0, 0, 0, 255) val aOuter = if (isDarkThemeOn) Rgba(0, 0, 0, 255) else Rgba(255, 255, 255, 255) val iInner = Rgba(128, 128, 128, 255) val iOuter = Rgba(128, 128, 128, 255) turnImage = GemUtilImages.asBitmap(instruction.turnDetails?.abstractGeometryImage, imageSize, imageSize, aInner, aOuter, iInner, iOuter) text = instruction.turnInstruction ?: "" if (text.isNotEmpty() && text.last() == '.') { text.removeSuffix(".") } val distance = instruction.traveledTimeDistance?.totalDistance?.toDouble() ?: 0.0 val distText = GemUtil.getDistText(distance.toInt(), SdkSettings.unitSystem) status = distText.first description = distText.second if (status == "0.00") { status = "0" } postOnMain { viewHolder.turnImage.setImageBitmap(turnImage) viewHolder.text.text = text viewHolder.status.text = status viewHolder.description.text = description } } } } ``` --- ### Route Navigation |

In this guide you will learn how to do real navigation along a computed route rendered on an interactive map, from the current position to a desired destination. ![](/docs/android/assets/images/example_kotlin_routenavigation-90dd080d41d3a1c17c9b1a4351426666.png) **Preview of route to navigate** ##### Starting a navigation[​](#starting-a-navigation "Direct link to Starting a navigation") Inside MainActivity.ky retain a RoutingService instance. Normally in Android's MVVM, services are retained in the Repository layer, injected via dependency injection, and ultimately consumed by ViewModels but for the scope of this example we will use it inside an activity. The routing service has a `NavigationListener` and a `ProgressListener` which will update the UI. MainActivity.kt[](RouteNavigation/app/src/main/kotlin/com/magiclane/sdk/examples/routenavigation/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { //... private val navigationService = NavigationService() private val navRoute: Route? get() = navigationService.getNavigationRoute(navigationListener) private val navigationListener: NavigationListener = NavigationListener.create( onNavigationStarted = { SdkCall.execute { gemSurfaceView.mapView?.let { mapView -> mapView.preferences?.enableCursor = false navRoute?.let { route -> mapView.presentRoute(route) val remainingDistance = route.getTimeDistance(true)?.totalDistance ?: 0 Toast.makeText( this@MainActivity, "Distance to destination $remainingDistance m", Toast.LENGTH_LONG ).show() } enableGPSButton() mapView.followPosition() } } } ) //... // Define a listener that will let us know the progress of the routing process. private val routingProgressListener = ProgressListener.create( onStarted = { progressBar.visibility = View.VISIBLE }, onCompleted = { _, _ -> progressBar.visibility = View.GONE }, postOnMain = true ) //... } ``` The `NavigationListener` receives event updates during navigation such as when the destination is reached, or when the route to the desired destination has been recomputed, because a detour away from the original route was taken. Other events include turn instructions, passed waypoints, voiced instructions, navigation errors and so on. When the navigation starts the resulting route is shown on map and the follow position button is enabled. The follow position button has a click listener that calls `mapView.followPosition()` which will make the camera follow the current position at an angle. Starting the navigation requiers the user to give permission to access location and location activated. The check for current position is done with `PositionService` that uses a `PositionListener` get the first available position. After the first valid position is received navigation is started. MainActivity.kt[](RouteNavigation/app/src/main/kotlin/com/magiclane/sdk/examples/routenavigation/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { //... override fun onCreate(savedInstanceState: Bundle?) { //... SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady // Defines an action that should be done when the world map is ready (Updated/ loaded). startNavigation() } //... } //... private fun startNavigation() { val startNavTask = { val hasPermissions = PermissionsHelper.hasPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) if (hasPermissions) { val destination = Landmark("Paris", 48.8566932, 2.3514616) // Cancel any navigation in progress. navigationService.cancelNavigation(navigationListener) // Start the new navigation. val error = navigationService.startNavigation( destination, navigationListener, routingProgressListener, ) Log.i(TAG, "MainActivity.startNavigation: after = $error") } } SdkCall.execute { lateinit var positionListener: PositionListener if (PositionService.position?.isValid() == true) { startNavTask() } else { positionListener = PositionListener { if (!it.isValid()) return@PositionListener PositionService.removeListener(positionListener) startNavTask() } // listen for first valid position to start the nav PositionService.addListener(positionListener) } } } //... } ``` --- ### Route Simulation With Instructions |

In this guide you will learn how to simulate navigation along a computed route rendered on an interactive map, from a departure position to a desired destination. The navigation includes on-screen text and turn arrow instructions in a panel at the top, as well as a bottom panel showing estimated time of arrival (ETA) at the destination, remaining trip time, and remaining travel distance. ![](/docs/android/assets/images/example_kotlin_routesimulationwithinstructions_screenshot1-c54026d095e2ddfa1aab07c82fd0163a.png) **Following position mode on with turn panel** ![](/docs/android/assets/images/example_kotlin_routesimulationwithinstructions_screenshot2-c085fe1706ce6f4d9ad31b99c8c520ef.png) **Following position mode off with turn panel**
A navigation is started similar to [Route Navigation Example](/docs/android/examples/routing-navigation/route-navigation.md) but instead of starting a real navigation a simulation is started instead. Since a simulation doesn't need current location there is no need to use `PositionService` to listen for valid positions nor is it needed to have location permission. MainActivity.kt[](RouteSimulationWithInstructions/app/src/main/kotlin/com/magiclane/sdk/examples/routesimulationwithinstructions/MainActivity.kt) ```kotlin 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 ) } ``` The navigation listener has an `onNavigationInstructionUpdated` callback instance which passes `NavigationInstruction` objects while the navigation is ongoing. The top navigation panel displays: * the turn arrow icon, `instrIcon`; * distance, `instrDistance`; * and an optional instruction, `instrText`. The bottom navigation panel contains: * *ETA* - estimated time of arrival, `etaText`; * *RTT* - remaining travel time, `rttText`; * *RTD* - remaining travel distance, `rtdText`. MainActivity.kt[](RouteSimulationWithInstructions/app/src/main/kotlin/com/magiclane/sdk/examples/routesimulationwithinstructions/MainActivity.kt) ```kotlin 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 }, 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 } ) ``` To get the ETA text: MainActivity.kt[](RouteSimulationWithInstructions/app/src/main/kotlin/com/magiclane/sdk/examples/routesimulationwithinstructions/MainActivity.kt) ```kotlin private fun Route.getEta(): String { val etaNumber = this.getTimeDistance(true)?.totalTime ?: 0 val time = Time() time.setLocalTime() time.longValue += etaNumber * 1000 return String.format(Locale.getDefault(), "%d:%02d", time.hour, time.minute) } ``` To get the RTT text: MainActivity.kt[](RouteSimulationWithInstructions/app/src/main/kotlin/com/magiclane/sdk/examples/routesimulationwithinstructions/MainActivity.kt) ```kotlin private fun Route.getRtt(): String { return GemUtil.getTimeText( this.getTimeDistance(true)?.totalTime ?: 0 ).let { pair -> pair.first + " " + pair.second } } ``` To get the RTD text: MainActivity.kt[](RouteSimulationWithInstructions/app/src/main/kotlin/com/magiclane/sdk/examples/routesimulationwithinstructions/MainActivity.kt) ```kotlin private fun Route.getRtd(): String { return GemUtil.getDistText( this.getTimeDistance(true)?.totalDistance ?: 0, EUnitSystem.Metric ).let { pair -> pair.first + " " + pair.second } } ``` To get the total remaining distance use: MainActivity.kt[](RouteSimulationWithInstructions/app/src/main/kotlin/com/magiclane/sdk/examples/routesimulationwithinstructions/MainActivity.kt) ```kotlin private fun NavigationInstruction.getDistanceInMeters(): String { return GemUtil.getDistText( this.timeDistanceToNextTurn?.totalDistance ?: 0, EUnitSystem.Metric ).let { pair -> pair.first + " " + pair.second } } ``` --- ### Route Terrain Profile |

In this guide you will learn how to obtain and display terrain statistics for a route, given the coordinates of the departure and destination points. The terrain profile statistics include the minimum and the maximum elevation above sea level in meters, as well as the total vertical meters climbed, and the total vertical meters descended during the route. ![](/docs/android/assets/images/example_kotlin_routeterrainprofile_screenshot1-ea4d1ea955f284b940b7fabb47e67d50.png) **Route terrain profile**
Open the `MainActivity.kt` file to see how the route terrain profile statistics are obtained and displayed. MainActivity.kt[](RouteTerrainProfile/app/src/main/kotlin/com/magiclane/sdk/examples/routeterrainprofile/MainActivity.kt) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) progressBar = findViewById(R.id.progressBar) SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady // Defines an action that should be done after the world map is ready. calculateRoute() } SdkSettings.onApiTokenRejected = { Toast.makeText(this, "TOKEN REJECTED", Toast.LENGTH_LONG).show() } if (!GemSdk.initSdkWithDefaults(this)) { // The SDK initialization was not completed. finish() } if (!Util.isInternetConnected(this)) { Toast.makeText(this, "You must be connected to internet!", Toast.LENGTH_LONG).show() } } ``` The `MainActivity` overrides the `onCreate` function which checks that internet access is available using `Util.isInternetConnected()` and calls the `calculateRoute()` function after the map is instantiated, as indicated by the `isReady` flag. MainActivity.kt[](RouteTerrainProfile/app/src/main/kotlin/com/magiclane/sdk/examples/routeterrainprofile/MainActivity.kt) ```kotlin private fun calculateRoute() = SdkCall.execute { val waypoints = arrayListOf( Landmark("Zaragoza", 41.645, -0.883), Landmark("Toulouse", 43.6, 1.438) ) /* Setting setBuildTerrainProfile(true) in the Routing Service preferences is mandatory to get data related to the route terrain profile, otherwise the terrain profile is not calculated in the routing process. */ routingService.preferences.buildTerrainProfile = true routingService.calculateRoute(waypoints) } ``` The `calculateRoute()` function computes a route between two or more landmarks, given as a list of waypoints, where the first one is the departure point, and the last one is the destination point. The landmarks contain a name, latitude and longitude, in degrees. `routingService.preferences.buildTerrainProfile` has to be set to `true` before `routingService.calculateRoute(waypoints)` in order to also get the terrain profile statistics. MainActivity.kt[](RouteTerrainProfile/app/src/main/kotlin/com/magiclane/sdk/examples/routeterrainprofile/MainActivity.kt) ```kotlin @SuppressLint("SetTextI18n") private fun displayTerrainInfo(terrain: RouteTerrainProfile) { var maxElv = .0f var minElv = .0f var elevationAt = .0f var totalUp = .0f var totalDown = .0f var climbingSections = 0 SdkCall.execute { maxElv = terrain.maxElevation minElv = terrain.minElevation elevationAt = terrain.getElevation(1000) // 1 KM totalUp = terrain.totalUp totalDown = terrain.totalDown climbingSections = terrain.climbSections?.size ?: 0 } findViewById(R.id.text).text = "Details: \nMin. Elevation = $minElv m, " + "\nMax. Elevation = $maxElv m, " + "\nElevation after 1KM = $elevationAt m," + "\nTotal Up = $totalUp m, \n" + "Total Down = $totalDown m \n\n\n" + "Number of Climbing Section: \n$climbingSections" } ``` The above function, `displayTerrainInfo()` displays some basic statistics about the computed route in this example in text format, to show how to access the terrain profile data. The `RouteTerrainProfile` class contains a lot of information, but this example presents only the minimum and maximum elevation above sea level in meters, elevation at a certain point along the route, the total vertical meters ascending and descending, and the number of climbing sections in the selected route. To display a chart representing the elevation of the route, the method `getElevationSamples(samplesCount)` can be used, specifying the number of samples, and thus resolution, of the resulting terrain data. There are also additional methods about route terrain profile in the documentation. --- ### Routing On Map |

In this guide you will learn how to render an interactive map, compute and render a route on the map and fly to the route. ![](/docs/android/assets/images/example_kotlin_routingonmap_screenshot2-c8377766f3fd4f4ff9e2dc830009c87d.png) **Displayed routes** ![](/docs/android/assets/images/example_kotlin_routingonmap_screenshot1-5e7941c3c3c22e5f4edf59f0fdd020a4.png) **Centered route** ##### Routing Service[​](#routing-service "Direct link to Routing Service") Inside `MainActivity.kt` retain a `RoutingService` instance. Normally in Android's MVVM, services are retained in the Repository layer, injected via dependency injection, and ultimately consumed by ViewModels but for the scope of this example we will use it inside an activity. The routing service has `onStarted` and `onCompleted` callbacks which will update the UI. MainActivity.kt[](RoutingOnMap/app/src/main/kotlin/com/magiclane/sdk/examples/routingonmap/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { //... private val routingService = RoutingService( onStarted = { progressBar.visibility = View.VISIBLE }, onCompleted = { routes, errorCode, _ -> progressBar.visibility = View.GONE when (errorCode) { GemError.NoError -> { SdkCall.execute { gemSurfaceView.mapView?.presentRoutes(routes, displayBubble = true) } } GemError.Cancel -> { // The routing action was cancelled. } else -> { // There was a problem at computing the routing operation. Toast.makeText( this@MainActivity, "Routing service error: ${GemError.getMessage(errorCode)}", Toast.LENGTH_SHORT ).show() } } } ) //... } ``` ##### Calculating route[​](#calculating-route "Direct link to Calculating route") MainActivity.kt[](RoutingOnMap/app/src/main/kotlin/com/magiclane/sdk/examples/routingonmap/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { //... override fun onCreate(savedInstanceState: Bundle?) { //... // Defines an action that should be done when the world map is ready (Updated/ loaded). SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady calculateRoute() //... } //... } //... } ``` In the `calculateRoute()` function 2 `Landmark` instances are defined, one for the departure, and one for the destination coordinates of the route endpoints. A route must have at least 2 Landmark instances(waypoints), but optionally can have more, for any optional additional waypoints along the route. The starting, or departure point of the route is the first waypoint in a list of 2 or more Landmarks (2 in this case), each containing a name, latitude (in degrees) and longitude (in degrees). The destination point is the last waypoint in the list. In this example, the route departure point is London, and the destination is Paris, as specified by the `Coordinates` in the 2 `Landmark` instances. The list of waypoints is passed to the `routingService` to calculate the route. MainActivity.kt[](RoutingOnMap/app/src/main/kotlin/com/magiclane/sdk/examples/routingonmap/MainActivity.kt) ```kotlin private fun calculateRoute() { val waypoints = arrayListOf( Landmark("London", Coordinates(51.5073204, -0.1276475)), Landmark("Paris", Coordinates(48.8566932, 2.3514616)) ) routingService.calculateRoute(waypoints) } ``` When the map is ready, that is, instantiated and loaded, the `calculateRoute()` function shown above is called. When the route calculation completes, the routing service callback is triggered and the resulting route(s) are rendered on the map. ##### Selecting routes on the map[​](#selecting-routes-on-the-map "Direct link to Selecting routes on the map") MainActivity.kt[](RoutingOnMap/app/src/main/kotlin/com/magiclane/sdk/examples/routingonmap/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { //... override fun onCreate(savedInstanceState: Bundle?) { //... // Defines an action that should be done when the world map is ready (Updated/ loaded). SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady //... gemSurfaceView.mapView?.onTouch = { xy -> // xy are the coordinates of the touch event SdkCall.execute { // tell the map view where the touch event happened gemSurfaceView.mapView?.cursorScreenPosition = xy // get the visible routes at the touch event point val routes = gemSurfaceView.mapView?.cursorSelectionRoutes // check if there is any route if (!routes.isNullOrEmpty()) { // set the touched route as the main route and center on it val route = routes[0] gemSurfaceView.mapView?.apply { preferences?.routes?.mainRoute = route centerOnRoutes(routesList) } } } } } //... } //... } ``` --- ### Search Along Route |

In this guide you will learn how to compute and render a route on an interactive map, from a departure position to a desired destination. The app also includes a search button which searches along the selected route for points of interest (POIs) such as fuel stations, and displays the results of the search in a scrollable pop-up panel. ![](/docs/android/assets/images/example_kotlin_searchalongroute_screenshot1-9e20c9f78abf8faf8fb51b3678de830a.png) **Computed route** ![](/docs/android/assets/images/example_kotlin_searchalongroute_screenshot2-7438071c529871c1e7928f116d790187.png) **Search results along the computed route** ##### Routing[​](#routing "Direct link to Routing") A `RoutingService()` is instantiated to compute and render the route. The `onStarted` and `onCompleted` routing callbacks are overridden and implemented, to show, and then hide, respectively, the routing computation progress bar, which is useful for very slow devices, where the route computation may take long enough to be noticeable by the user. When the route calculation is completed, if there is no error, the resulting routes (as there could be more than one alternate route in the resulting set between the specified departure and destination points) are drawn on the map: `gemSurfaceView.mapView?.presentRoutes()` and a main route is choosen `mainRoute = routes[0]`. Then the search button is set to visible `searchButton.visibility = View.VISIBLE`, so that the user can search the landmarks present along the route. MainActivity.kt[](SearchAlongRoute/app/src/main/kotlin/com/magiclane/sdk/examples/searchalongroute/MainActivity.kt) ```kotlin private val routingService = RoutingService( onStarted = { progressBar.visibility = View.VISIBLE }, onCompleted = { routes, errorCode, _ -> progressBar.visibility = View.GONE when (errorCode) { GemError.NoError -> { if(routes.isNotEmpty()) { routesList = routes mainRoute = routes[0] Util.postOnMain { searchButton.visibility = View.VISIBLE } SdkCall.execute { gemSurfaceView.mapView?.presentRoutes(routes, displayBubble = true) } } } GemError.Cancel -> { // The routing action was cancelled. } else -> { // There was a problem at computing the routing operation. showDialog("Routing service error: ${GemError.getMessage(errorCode)}") } } } ) ``` The function `calculateRoute()` is used to set the waypoints and to initiate the route calculation. The starting, or departure point of the route is the first waypoint in a list of 2 or more Landmarks (2 in this case), each containing a name, latitude (in degrees) and longitude (in degrees). The destination point is the last waypoint in the list. MainActivity.kt[](SearchAlongRoute/app/src/main/kotlin/com/magiclane/sdk/examples/searchalongroute/MainActivity.kt) ```kotlin private fun calculateRoute() = SdkCall.execute { val waypoints = arrayListOf( Landmark("Folkestone", 51.0814, 1.1695), Landmark("Paris", 48.8566932, 2.3514616) ) routingService.calculateRoute(waypoints) } ``` ##### Search[​](#search "Direct link to Search") A `SearchService()` is instantiated, which carries out on-demand search along the route. MainActivity.kt[](SearchAlongRoute/app/src/main/kotlin/com/magiclane/sdk/examples/searchalongroute/MainActivity.kt) ```kotlin private val searchService = SearchService( onStarted = { progressBar.visibility = View.VISIBLE }, onCompleted = onCompleted@{ results, errorCode, _ -> progressBar.visibility = View.GONE when (errorCode) { GemError.NoError -> { // Display results in AlertDialog onSearchCompleted(results) } GemError.Cancel -> { // The search action was canceled. } else -> { // There was a problem in the search operation. Toast.makeText( this@MainActivity, "Search service error: ${GemError.getMessage(errorCode)}", Toast.LENGTH_SHORT ).show() } } } ) ``` When the user clicks the search button, the `searchAlongRoute()` function is called and set to return up to 25 gas stations (defined as `EGenericCategoriesIDs.GasStation`) along the route. MainActivity.kt[](SearchAlongRoute/app/src/main/kotlin/com/magiclane/sdk/examples/searchalongroute/MainActivity.kt) ```kotlin private fun searchAlongRoute(route: Route) = SdkCall.execute { // Set the maximum number of results to 25. searchService.preferences.maxMatches = 25 // Search Gas Stations along the route. searchService.searchAlongRoute(route, EGenericCategoriesIDs.GasStation) } ``` The `onCompleted` listener in the `searchService` calls the `onSearchCompleted()` function when the user-triggered search is finished. Then the results are displayed in a scrollable list inside a pop-up panel. MainActivity.kt[](SearchAlongRoute/app/src/main/kotlin/com/magiclane/sdk/examples/searchalongroute/MainActivity.kt) ```kotlin private fun onSearchCompleted(results: ArrayList) { val builder = AlertDialog.Builder(this) val convertView = layoutInflater.inflate(R.layout.dialog_list, null) convertView.findViewById(R.id.list_view).apply { layoutManager = LinearLayoutManager(this@MainActivity) addItemDecoration(DividerItemDecoration( applicationContext, (layoutManager as LinearLayoutManager).orientation )) setBackgroundResource(R.color.background_color) val lateralPadding = resources.getDimension(R.dimen.big_padding).toInt() setPadding(lateralPadding, 0, lateralPadding, 0) adapter = CustomAdapter(results) } builder.setView(convertView) builder.create().show() } ``` ##### Map Implementation[​](#map-implementation "Direct link to Map Implementation") When the map data is ready using the `onMapDataReady@{}` callback it calculates the route using the function `calculateRoute()` and then attaches a touch event to the `Map View` as seen in `gemSurfaceView.mapView?.onTouch = {}`. This touch event which will set, `gemSurfaceView.mapView?.apply {...}`, the main route to be highlighted and centered on. Searching along the route is triggered at the user’s discretion using the `searchButton`. MainActivity.kt[](SearchAlongRoute/app/src/main/kotlin/com/magiclane/sdk/examples/searchalongroute/MainActivity.kt) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) progressBar = findViewById(R.id.progressBar) gemSurfaceView = findViewById(R.id.gem_surface) searchButton = findViewById(R.id.search_button) SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady // Defines an action that should be done when the world map is ready (Updated/ loaded). calculateRoute() // onTouch event callback gemSurfaceView.mapView?.onTouch = { xy -> // xy are the coordinates of the touch event SdkCall.execute { // tell the map view where the touch event happened gemSurfaceView.mapView?.cursorScreenPosition = xy // get the visible routes at the touch event point val routes = gemSurfaceView.mapView?.cursorSelectionRoutes // check if there is any route if (!routes.isNullOrEmpty()) { // set the touched route as the main route and center on it mainRoute = routes[0] gemSurfaceView.mapView?.apply { preferences?.routes?.mainRoute = mainRoute centerOnRoutes(routesList) } } } } searchButton.setOnClickListener { searchAlongRoute(mainRoute) } } } ``` --- ### Set TTS Language |

In this guide you will learn how to set or change the TTS (text to speech) voice instruction language. ![](/docs/android/assets/images/example_kotlin_setttslanguage_screenshot1-93b5bc4ecb9b785a745ffabb26e7cc98.png) **Starting TTS language** ![](/docs/android/assets/images/example_kotlin_setttslanguage_screenshot2-061ff0985091e8c950dd57a7e225c735.png) **List of available TTS languages** ![](/docs/android/assets/images/example_kotlin_setttslanguage_screenshot3-b9908f342ca396c8e30ebfeff8e03117.png) **New TTS language loaded** ##### TTS Language Change[​](#tts-language-change "Direct link to TTS Language Change") The `onCreate()` function loads the TTS languages `loadTTSLanguages()` when the map instantiation is complete (the map is ready). If the TTS player is not initialized, a listener is set up to wait for the TTS player to be initialized, which causes the TTS languages to be loaded by the callback function defined for that listener: `override fun onTTSPlayerInitialized() { loadTTSLanguages() }`. A click listener is set for clicking on the name of the currently active language `languageContainer.setOnClickListener { onLanguageContainerClicked() }` to display the list of TTS languages. A click listener is set for the play button `playButton.setOnClickListener` at the bottom of the screen to play the “mind your speed” message `SoundPlayingService.playText(GemUtil.getTTSString(EStringIds.eStrMindYourSpeed)`, in the currently selected TTS language. MainActivity.kt[](SetTTSLanguage/app/src/main/kotlin/com/magiclane/sdk/examples/setttslanguage/MainActivity.kt) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) progressBar = findViewById(R.id.progressBar) selectedLanguageTextView = findViewById(R.id.language_value) languageContainer = findViewById(R.id.language_container) playButton = findViewById(R.id.play_button) languageContainer.setOnClickListener { onLanguageContainerClicked() } playButton.setOnClickListener { SdkCall.execute { SoundPlayingService.playText(GemUtil.getTTSString(EStringIds.eStrMindYourSpeed), SoundPlayingListener(), SoundPlayingPreferences()) } } SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady val ttsPlayerIsInitialized = SdkCall.execute { SoundPlayingService.ttsPlayerIsInitialized } ?: false if (!ttsPlayerIsInitialized) { SoundUtils.addTTSPlayerInitializationListener(this) } else { loadTTSLanguages() } EspressoIdlingResource.decrement() } ... } ``` The function to load the TTS languages. MainActivity.kt[](SetTTSLanguage/app/src/main/kotlin/com/magiclane/sdk/examples/setttslanguage/MainActivity.kt) ```kotlin private fun loadTTSLanguages() { SdkCall.execute { ttsLanguages = SoundPlayingService.getTTSLanguages() } runOnUiThread { onTTSLanguagesLoaded() } } ``` When the TTS languages are loaded, the currently selected language, initially the first one (at index 0) is displayed: `private var selectedLanguageIndex = 0` MainActivity.kt[](SetTTSLanguage/app/src/main/kotlin/com/magiclane/sdk/examples/setttslanguage/MainActivity.kt) ```kotlin private fun onTTSLanguagesLoaded() { selectedLanguageTextView.text = ttsLanguages[selectedLanguageIndex].name SoundPlayingService.setTTSLanguage(ttsLanguages[selectedLanguageIndex].code) progressBar.visibility = View.GONE languageContainer.visibility = View.VISIBLE playButton.visibility = View.VISIBLE } ``` When the currently selected language is clicked, a `CustomAdapter` is used to display the list of available TTS languages. MainActivity.kt[](SetTTSLanguage/app/src/main/kotlin/com/magiclane/sdk/examples/setttslanguage/MainActivity.kt) ```kotlin private fun onLanguageContainerClicked() { val builder = AlertDialog.Builder(this) val convertView = layoutInflater.inflate(R.layout.dialog_list, null) val listView = convertView.findViewById(R.id.list_view).apply { layoutManager = LinearLayoutManager(this@MainActivity) addItemDecoration(DividerItemDecoration(applicationContext, (layoutManager as LinearLayoutManager).orientation)) setBackgroundResource(R.color.white) val lateralPadding = resources.getDimension(R.dimen.bigPadding).toInt() setPadding(lateralPadding, 0, lateralPadding, 0) } val adapter = CustomAdapter(selectedLanguageIndex, ttsLanguages) listView.adapter = adapter builder.setView(convertView) val dialog = builder.create() dialog.show() adapter.dialog = dialog } ``` Inside the `CustomAdapter`, clicking on a TTS language in the list causes that language to be set in the sound playing service: `SoundPlayingService.setTTSLanguage(dataSet[position].code)`. MainActivity.kt[](SetTTSLanguage/app/src/main/kotlin/com/magiclane/sdk/examples/setttslanguage/MainActivity.kt) ```kotlin ... itemView.setOnClickListener { selectedLanguageIndex = position SoundPlayingService.setTTSLanguage(dataSet[position].code) selectedLanguageTextView.text = dataSet[position].name dialog?.dismiss() } ... ``` --- ### Truck Profile |

In this guide you will learn how to compute a route based on the accessibility given by the physical characteristics of a truck, such as height, weight, length, width and number of axles, and then render the route on the map. ![](/docs/android/assets/images/example_kotlin_truckprofile_screenshot1-49307d3e0374408a17ef2a88bf6ba540.png) **Route for a truck** ![](/docs/android/assets/images/example_kotlin_truckprofile_screenshot2-73d7e59b6ee0de095cd127ecebd93b92.png) **Truck profile settings panel** ##### Routing[​](#routing "Direct link to Routing") A routing service is instantiated to compute and render the truck route. The `onStarted` and `onCompleted` routing callbacks are overridden and implemented, to show, and then hide, respectively, the routing computation progress bar, which is useful for very slow devices, where the route computation may take long enough to be noticeable by the user. The line `adapter.notifyItemRangeChanged(0, routesList.size)` updates the views for all items from the beginning of the `routesList` up to its current size, because their content may have changed. When the route calculation is completed, if there is no error, the resulting routes (as there could be more than one alternate route in the resulting set between the specified departure and destination points) are drawn on the map: `gemSurfaceView.mapView?.presentRoutes()` Then the settings button is set to visible `settingsButtons.visibility = View.VISIBLE`, so that the user can introduce the physical characteristics of the truck. Upon saving these, the route is computed again, in case it has to change to accommodate the updated characteristics of the truck, such as its dimensions and weight. MainActivity.kt[](TruckProfile/app/src/main/kotlin/com/magiclane/sdk/examples/truckprofile/MainActivity.kt) ```kotlin private val routingService = RoutingService( onStarted = { progressBar.visibility = View.VISIBLE }, onCompleted = { routes, errorCode, _ -> progressBar.visibility = View.GONE when (errorCode) { GemError.NoError -> { routesList = routes adapter.notifyItemRangeChanged(0, routesList.size) SdkCall.execute { gemSurfaceView.mapView?.presentRoutes( routes = routes, displayBubble = true ) } settingsButtons.visibility = View.VISIBLE } GemError.Cancel -> { showDialog("The routing action was canceled.") } else -> { // There was a problem at computing the routing operation. showDialog("Routing service error: ${GemError.getMessage(errorCode)}") } } } ) ``` ##### Map Implementation[​](#map-implementation "Direct link to Map Implementation") A click listener is set for the settings button, to recompute the route when the user changes the physical parameters of the truck. `it.setOnClickListener { onSettingsButtonClicked() }` When the map is loaded and ready, `onMapDataReady@{}` the routing service instantiated and shown above is used to calculate the route between a list of 2 predefined waypoints where the first is the departure point and the second is the destination point: `routingService.calculateRoute(waypoints)` Each waypoint is a `Landmark` containing a name, a latitude, in degrees, and a longitude, in degrees: `Landmark("Paris", 48.8566932, 2.3514616)` The list of waypoints from which a route is calculated must have at least 2 elements, for the departure and destination, respectively, but can have more elements, for additional waypoints along the route. In this example there are 2 waypoints in the list. Also, a default `TruckProfile` is instantiated with some default values for it's fields `preferencesTruckProfile = TruckProfile(...)` A touch listener is defined `gemSurfaceView.mapView?.onTouch = xy ->` and `gemSurfaceView.mapView?.cursorSelectionRoutes` is used to see if the user touched one or more routes on the map. If so, the first touched route (at index 0) is selected and set as the main route, which causes it to be drawn in dark blue on the map. Then the camera centers on the bounding box containing all routes between the departure and destination points. `centerOnRoutes(routesList)` MainActivity.kt[](TruckProfile/app/src/main/kotlin/com/magiclane/sdk/examples/truckprofile/MainActivity.kt) ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) gemSurfaceView = findViewById(R.id.gem_surface_view) progressBar = findViewById(R.id.progress_bar) settingsButtons = findViewById(R.id.settings_button).also { it.setOnClickListener { onSettingsButtonClicked() } } SdkSettings.onMapDataReady = onMapDataReady@{ isReady -> if (!isReady) return@onMapDataReady // Defines an action that should be done when the world map is ready (Updated/ loaded). SdkCall.execute { waypoints = arrayListOf( Landmark("London", 51.5073204, -0.1276475), Landmark("Paris", 48.8566932, 2.3514616) ) routingService.calculateRoute(waypoints) preferencesTruckProfile = TruckProfile( (3 * ETruckProfileUnitConverters.Weight.unit).toInt(), (1.8 * ETruckProfileUnitConverters.Height.unit).toInt(), (5 * ETruckProfileUnitConverters.Length.unit).toInt(), (2 * ETruckProfileUnitConverters.Width.unit).toInt(), (1.5 * ETruckProfileUnitConverters.AxleWeight.unit).toInt(), (60 * ETruckProfileUnitConverters.MaxSpeed.unit).toDouble() ) } gemSurfaceView.mapView?.onTouch = { xy -> SdkCall.execute { // tell the map view where the touch event happened gemSurfaceView.mapView?.cursorScreenPosition = xy // get the visible routes at the touch event point val routes = gemSurfaceView.mapView?.cursorSelectionRoutes // check if there is any route if (!routes.isNullOrEmpty()) { // set the touched route as the main route and center on it val route = routes[0] gemSurfaceView.mapView?.apply { preferences?.routes?.mainRoute = route centerOnRoutes(routesList) } } } } } ... } ``` ##### Settings Change[​](#settings-change "Direct link to Settings Change") The inner class `TruckProfileSettingsAdapter` represents the custom `RecyclerView` adapter for the truck profile settings. ```kotlin private val adapter = TruckProfileSettingsAdapter(getInitialDataSet()) ``` The `getInitialDataSet()` function sets the default initial values in the form with the truck dimensions and other attributes required for identifying a corresponding route between the given departure and destination which is navigable by the specified truck. MainActivity.kt[](TruckProfile/app/src/main/kotlin/com/magiclane/sdk/examples/truckprofile/MainActivity.kt) ```kotlin private fun getInitialDataSet(): List { return mutableListOf().also { it.add( TruckProfileSettingsModel( "Weight", ESeekBarValuesType.DoubleType, "3 t", "3.0 t", "50 t", 0, 0, 0, 3.0f, 3.0f, 50.0f, "t" ) ) ... } } ``` The `onSettingsButtonClicked()` function is called by the listener defined in the `onCreate()` function above, when the user clicks the industrial wheel settings button to modify the truck profile attributes. This function uses the adapter variable to display the truck attribute values and enable the user to modify them, so they can then be used to find a route appropriate for the truck. A save button is defined, which closes the dialog and calls the `onSaveButtonClicked()` function to calculate the route using the updated truck parameter values. MainActivity.kt[](TruckProfile/app/src/main/kotlin/com/magiclane/sdk/examples/truckprofile/MainActivity.kt) ```kotlin private fun onSettingsButtonClicked() { val builder = AlertDialog.Builder(this) val convertView = layoutInflater.inflate(R.layout.truck_profile_settings_view, null) val listView = convertView.findViewById(R.id.truck_profile_settings_list).apply { layoutManager = LinearLayoutManager(this@MainActivity) addItemDecoration( DividerItemDecoration( applicationContext, (layoutManager as LinearLayoutManager).orientation ) ) } listView.adapter = adapter adapter.notifyItemRangeChanged(0, ETruckProfileSettings.entries.size) builder.setTitle(getString(R.string.app_name)) builder.setView(convertView) builder.setNeutralButton(getString(R.string.save)) { dialog, _ -> onSaveButtonClicked() dialog.dismiss() } val dialog = builder.create() dialog.show() } ``` The `onSaveButtonClicked()` function sets the truck profile attributes in the preferences and then calculates the route using `calculateRoute(waypoints)` MainActivity.kt[](TruckProfile/app/src/main/kotlin/com/magiclane/sdk/examples/truckprofile/MainActivity.kt) ```kotlin private fun onSaveButtonClicked() { EspressoIdlingResource.increment() val dataSet = adapter.dataSet // convert m to cm val width = (dataSet[ETruckProfileSettings.Width.ordinal].currentDoubleValue * ETruckProfileUnitConverters.Width.unit).toInt() val height = (dataSet[ETruckProfileSettings.Height.ordinal].currentDoubleValue * ETruckProfileUnitConverters.Height.unit).toInt() val length = (dataSet[ETruckProfileSettings.Length.ordinal].currentDoubleValue * ETruckProfileUnitConverters.Length.unit).toInt() // convert t to kg val weight = (dataSet[ETruckProfileSettings.Weight.ordinal].currentDoubleValue * ETruckProfileUnitConverters.Weight.unit).toInt() val axleWeight = (dataSet[ETruckProfileSettings.AxleWeight.ordinal].currentDoubleValue * ETruckProfileUnitConverters.AxleWeight.unit).toInt() // convert km/h to m/s val maxSpeed = dataSet[ETruckProfileSettings.MaxSpeed.ordinal].currentIntValue * ETruckProfileUnitConverters.MaxSpeed.unit.toDouble() SdkCall.execute { routingService.apply { preferences.alternativesSchema = ERouteAlternativesSchema.Never preferences.transportMode = ERouteTransportMode.Lorry preferencesTruckProfile = TruckProfile( massKg = weight, heightCm = height, lengthCm = length, widthCm = width, axleLoadKg = axleWeight, maxSpeedMs = maxSpeed ) preferences.truckProfile = preferencesTruckProfile calculateRoute(waypoints) } } } ``` --- ### Voice Downloading |

In this guide you will learn how to list all navigation instruction voices available on the server, indicating which are already downloaded, and download a voice. ![](/docs/android/assets/images/example_kotlin_voicedownloading_screenshot1-c1f3df063890619073daf35a6a3f4d9d.png) **Voices list** ![](/docs/android/assets/images/example_kotlin_voicedownloading_screenshot2-c982ae215cc7a9c6122598cadf03e6d4.png) **Downloaded voices** ##### Getting the list[​](#getting-the-list "Direct link to Getting the list") To get the voices list a `ContentStore` must be instantiated. Normally in Android's MVVM, stores are retained in the Repository layer, injected via dependency injection, and ultimately consumed by ViewModels but for the scope of this example we will use it inside an activity. To request the list there also needs to be an established internet connection. In the code below is shown how to make a request for the voice list. MainActivity.kt[](VoiceDownloading/app/src/main/kotlin/com/magiclane/sdk/examples/voicedownloading/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { //... private val contentStore = ContentStore() private var voicesCatalogRequested = false //... override fun onCreate(savedInstanceState: Bundle?) { //... // Defines an action that should be done after the network is connected. SdkSettings.onConnectionStatusUpdated = { connected -> if (connected && !voicesCatalogRequested) { voicesCatalogRequested = true //... SdkCall.execute { // Call to the content store to asynchronously retrieve the list of voices. contentStore.asyncGetStoreContentList( EContentType.HumanVoice, progressListener ) } //... } //... } } ``` If retrieving the list is succesfull the `ProgressListener.onCompleted` callback is triggered with no error. Download is started on the first succesfull retrieved item of the list. The `voiceItem.asyncDownload(..)` also receives progress callbacks that will update the UI. MainActivity.kt[](VoiceDownloading/app/src/main/kotlin/com/magiclane/sdk/examples/voicedownloading/MainActivity.kt) ```kotlin class MainActivity : AppCompatActivity() { //... private val progressListener = 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 -> { SdkCall.execute { // No error encountered, we can handle the results. // Get the list of voices that was retrieved in the content store. val models = contentStore.getStoreContentList(EContentType.HumanVoice)?.first if (!models.isNullOrEmpty()) { // The voice items list is not empty or null. val voiceItem = models[0] val itemName = voiceItem.name // Start downloading the first voice item. SdkCall.execute { voiceItem.asyncDownload(GemSdk.EDataSavePolicy.UseDefault, true, onStarted = { showStatusMessage("Started downloading $itemName.") }, onCompleted = { _, _ -> listView?.adapter?.notifyItemChanged(0) showStatusMessage("$itemName was downloaded.") }, onProgress = { listView?.adapter?.notifyItemChanged(0) }) } } displayList(models) } } GemError.Cancel -> { // The action was cancelled. } else -> { // There was a problem at retrieving the content store items. showDialog("Content store service error: ${GemError.getMessage(errorCode)}") } } }, postOnMain = true ) } ``` Finally, the list is displayed using a `RecyclerView.Adapter`. MainActivity.kt[](VoiceDownloading/app/src/main/kotlin/com/magiclane/sdk/examples/voicedownloading/MainActivity.kt) ```kotlin private fun displayList(models: ArrayList?) { if (models != null) { val adapter = CustomAdapter(models) listView?.adapter = adapter } } ``` The example also uses a `CustomAdapter` to display a scrollable list of country names, flags and voice names with the `displayList()` function. --- ### Alarms In modern mobile applications, providing real-time notifications and alerts is crucial for ensuring a seamless user experience, especially in location-based services. The GemSDK offers a robust alarm system that enables developers to monitor and respond to various events based on the user's location, speed, and interactions with geographical boundaries or landmarks. #### [📄️ Get started with Alarms](/docs/android/guides/alarms/get-started-alarms.md) [The alarm system within the GemSDK offers a range of monitoring and notification functionalities. It allows for the detection and management of different types of alarms, such as boundary crossings, speed limit violations, and landmark alerts. The system provides the ability to configure parameters like alarm distance, overspeed thresholds, and whether monitoring should occur even without a route being followed.](/docs/android/guides/alarms/get-started-alarms.md) #### [📄️ Speed warnings](/docs/android/guides/alarms/speed-alarms.md) [The SDK provides features for monitoring and notifying users about speed limits and violations. You can configure alerts for when a user exceeds the speed limit, when the speed limit changes on the new road segment with respect to the previous, and when the user returns to a normal speed range (onNormalSpeed). The SDK also allows you to set customizable thresholds for speed violations, which can be adjusted for both city and non-city areas. These features help provide timely and relevant speed-related notifications based on the user's location and current speed.](/docs/android/guides/alarms/speed-alarms.md) #### [📄️ Landmark and overlay alarms](/docs/android/guides/alarms/landmark-and-overlay-alarms.md) [The AlarmService can be configured to send notifications upon approaching specific landmarks or overlay items within a defined proximity. This behavior can be tailored to trigger notifications exclusively during navigation or simulation modes, or while freely exploring the map without a predefined route.](/docs/android/guides/alarms/landmark-and-overlay-alarms.md) #### [📄️ Areas alarms](/docs/android/guides/alarms/areas-alarms.md) [Another powerful use case is triggering operations the moment a user enters or exits a defined geographic area.](/docs/android/guides/alarms/areas-alarms.md) #### [📄️ Other alarms](/docs/android/guides/alarms/other-alarms.md) [The Maps SDK for Android provides advanced notification capabilities, enabling users to receive alerts for significant environmental changes. This includes notifications for entering and exiting tunnels, as well as transitions between day and night, based on the user's current location and time of year.](/docs/android/guides/alarms/other-alarms.md) --- ### Areas alarms |

Another powerful use case is triggering operations the moment a user enters or exits a defined **geographic area**. The Magic Lane Android SDK includes a built-in `AlarmService` class, making it effortless to configure and manage all your geofence events. #### Add areas to be monitored[​](#add-areas-to-be-monitored "Direct link to Add areas to be monitored") Define your geographic areas-`RectangleGeographicArea`, `CircleGeographicArea`, and `PolygonGeographicArea`-then invoke the `monitorArea` method on your `AlarmService` instance: * Kotlin * Java ```kotlin // Create a rectangular area using minLat, maxLat, minLon, maxLon val rect = RectangleGeographicArea( minLat = 0.5, // minimum latitude maxLat = 1.0, // maximum latitude minLon = 0.5, // minimum longitude maxLon = 1.0 // maximum longitude ) // Create a circular area with center coordinates and radius in meters val circle = CircleGeographicArea( center = Coordinates(latitude = 1.0, longitude = 0.5), radius = 100 // radius in meters ) // Create a polygon area with a list of coordinates (first and last should be identical to close the polygon) val polygonCoordinates = arrayListOf( Coordinates(latitude = 1.0, longitude = 0.5), Coordinates(latitude = 0.5, longitude = 1.0), Coordinates(latitude = 1.0, longitude = 1.0), Coordinates(latitude = 1.0, longitude = 0.5) // Close the polygon ) val polygon = PolygonGeographicArea(polygonCoordinates) // Monitor the areas SdkCall.execute { alarmService?.monitorArea(rect) alarmService?.monitorArea(circle) alarmService?.monitorArea(polygon) } ``` ```java // Create a rectangular area using minLat, maxLat, minLon, maxLon RectangleGeographicArea rect = new RectangleGeographicArea( 0.5, // minimum latitude 1.0, // maximum latitude 0.5, // minimum longitude 1.0 // maximum longitude ); // Create a circular area with center coordinates and radius in meters CircleGeographicArea circle = new CircleGeographicArea( new Coordinates(1.0, 0.5), 100 // radius in meters ); // Create a polygon area with a list of coordinates (first and last should be identical to close the polygon) ArrayList polygonCoordinates = new ArrayList<>(); polygonCoordinates.add(new Coordinates(1.0, 0.5)); polygonCoordinates.add(new Coordinates(0.5, 1.0)); polygonCoordinates.add(new Coordinates(1.0, 1.0)); polygonCoordinates.add(new Coordinates(1.0, 0.5)); // Close the polygon PolygonGeographicArea polygon = new PolygonGeographicArea(polygonCoordinates); // Monitor the areas SdkCall.execute(() -> { if (alarmService != null) { alarmService.monitorArea(rect); alarmService.monitorArea(circle); alarmService.monitorArea(polygon); } }); ``` The SDK automatically manages area identification internally for boundary crossing detection. #### Get a list of monitored areas[​](#get-a-list-of-monitored-areas "Direct link to Get a list of monitored areas") Access your active geofences via the `monitoredAreas` property, which returns a list of `AlarmMonitoredArea` objects, each one reflecting the parameters you provided to `monitorArea`. * Kotlin * Java ```kotlin val monitoredAreas = alarmService?.monitoredAreas ?: emptyList() for (monitoredArea in monitoredAreas) { val area = monitoredArea.area val id = monitoredArea.id Log.d("AreaMonitoring", "Monitoring area: $id") } ``` ```java List monitoredAreas = alarmService != null ? alarmService.getMonitoredAreas() : Collections.emptyList(); for (AlarmMonitoredArea monitoredArea : monitoredAreas) { GeographicArea area = monitoredArea.getArea(); int id = monitoredArea.getId(); Log.d("AreaMonitoring", "Monitoring area: " + id); } ``` Tip When defining a `PolygonGeographicArea`, always "close" the shape by making the first and last coordinates identical. Otherwise, the SDK may return polygons that don't match the one you provided. #### Unmonitor an area[​](#unmonitor-an-area "Direct link to Unmonitor an area") To remove a monitored area, call the `unmonitorArea` method and pass in the same `GeographicArea` instance you originally supplied to `monitorArea`. This will unregister that zone and stop all related geofence events. * Kotlin * Java ```kotlin val rect = RectangleGeographicArea( minLat = 0.5, maxLat = 1.0, minLon = 0.5, maxLon = 1.0 ) alarmService?.monitorArea(rect) // Later, unmonitor the same area alarmService?.unmonitorArea(rect) ``` ```java RectangleGeographicArea rect = new RectangleGeographicArea( 0.5, 1.0, 0.5, 1.0 ); if (alarmService != null) { alarmService.monitorArea(rect); // Later, unmonitor the same area alarmService.unmonitorArea(rect); } ``` To unmonitor multiple areas, call `unmonitorArea` for each area individually. #### Get notified when the user enters an area:[​](#get-notified-when-the-user-enters-an-area "Direct link to Get notified when the user enters an area:") Attach your `AlarmListener`-including the `onBoundaryCrossed` callback - to your `AlarmService`. This callback is triggered when the user crosses area boundaries. * Kotlin * Java ```kotlin val alarmListener = AlarmListener.create { onBoundaryCrossed { Log.d("BoundaryCrossed", "Boundary crossed detected") // Handle area entry/exit events // Update UI based on area changes // Use alarmService?.crossedBoundaries to get detailed boundary information } } val alarmService = AlarmService.produce(alarmListener) ``` ```java AlarmListener alarmListener = AlarmListener.create( () -> { Log.d("BoundaryCrossed", "Boundary crossed detected"); // Handle area entry/exit events // Update UI based on area changes // Use alarmService.getCrossedBoundaries() to get detailed boundary information } ); AlarmService alarmService = AlarmService.produce(alarmListener); ``` #### Get the list of areas where the user is located[​](#get-the-list-of-areas-where-the-user-is-located "Direct link to Get the list of areas where the user is located") Retrieve the zones the user is currently inside by calling the `insideAreas` property on your `AlarmService` instance: * Kotlin * Java ```kotlin val insideAreas = alarmService?.insideAreas ?: emptyList() Log.d("AreaMonitoring", "User is inside ${insideAreas.size} areas") ``` ```java List insideAreas = alarmService != null ? alarmService.getInsideAreas() : Collections.emptyList(); Log.d("AreaMonitoring", "User is inside " + insideAreas.size() + " areas"); ``` info For the insideAreas property to return a non-empty list, the user must be inside at least one monitored area and must move or change position within that area. To retrieve the zones the user has exited, call the `outsideAreas` property on your `AlarmService` instance. Here's a complete Android Activity example that demonstrates area alarm functionality: * Kotlin * Java ```kotlin import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.magiclane.sdk.core.* import com.magiclane.sdk.places.Coordinates class AreaAlarmsActivity : AppCompatActivity() { private var alarmService: AlarmService? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setupAreaAlarms() createAndMonitorAreas() } private fun setupAreaAlarms() { // Create alarm listener with boundary crossing callback val alarmListener = AlarmListener.create { onBoundaryCrossed { Log.d("AreaAlarm", "Boundary crossed detected") // Update UI to show area status changes // Check crossedBoundaries for detailed information SdkCall.execute { val crossedBoundaries = alarmService?.crossedBoundaries Log.i("AreaAlarm", "Crossed boundaries: $crossedBoundaries") } } } // Create alarm service alarmService = AlarmService.produce(alarmListener) } private fun createAndMonitorAreas() { // Create different types of geographic areas val rectangularArea = RectangleGeographicArea( minLat = 37.7649, maxLat = 37.7849, minLon = -122.4294, maxLon = -122.4094 ) val circularArea = CircleGeographicArea( center = Coordinates(latitude = 37.7749, longitude = -122.4194), radius = 500 // 500 meters radius ) val polygonCoordinates = arrayListOf( Coordinates(latitude = 37.7749, longitude = -122.4194), Coordinates(latitude = 37.7799, longitude = -122.4144), Coordinates(latitude = 37.7849, longitude = -122.4194), Coordinates(latitude = 37.7799, longitude = -122.4244), Coordinates(latitude = 37.7749, longitude = -122.4194) // Close the polygon ) val polygonArea = PolygonGeographicArea(polygonCoordinates) // Monitor all areas alarmService?.monitorArea(rectangularArea) alarmService?.monitorArea(circularArea) alarmService?.monitorArea(polygonArea) Log.d("AreaAlarm", "Started monitoring 3 geographic areas") checkMonitoredAreas() } } private fun checkMonitoredAreas() { SdkCall.execute { val monitoredAreas = alarmService?.monitoredAreas ?: emptyList() Log.d("AreaAlarm", "Currently monitoring ${monitoredAreas.size} areas") for (area in monitoredAreas) { Log.d("AreaAlarm", "Area ID: ${area.id}") } // Check which areas user is currently inside val insideAreas = alarmService?.insideAreas ?: emptyList() Log.d("AreaAlarm", "User is inside ${insideAreas.size} areas") } } override fun onDestroy() { super.onDestroy() // Clean up - unmonitor all areas SdkCall.execute { // Unmonitor areas individually using the original area instances alarmService?.unmonitorArea(rectangularArea) alarmService?.unmonitorArea(circularArea) alarmService?.unmonitorArea(polygonArea) Log.d("AreaAlarm", "Unmonitored all areas") } } } ``` ```java import android.os.Bundle; import android.util.Log; import androidx.appcompat.app.AppCompatActivity; import com.magiclane.sdk.core.*; import com.magiclane.sdk.places.Coordinates; class AreaAlarmsActivity extends AppCompatActivity { private AlarmService alarmService = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setupAreaAlarms(); createAndMonitorAreas(); } private void setupAreaAlarms() { // Create alarm listener with boundary crossing callback AlarmListener alarmListener = AlarmListener.create( () -> { Log.d("AreaAlarm", "Boundary crossed detected"); // Update UI to show area status changes // Check crossedBoundaries for detailed information SdkCall.execute(() -> { Object crossedBoundaries = alarmService != null ? alarmService.getCrossedBoundaries() : null; Log.i("AreaAlarm", "Crossed boundaries: " + crossedBoundaries); }); } ); // Create alarm service alarmService = AlarmService.produce(alarmListener); } private void createAndMonitorAreas() { // Create different types of geographic areas RectangleGeographicArea rectangularArea = new RectangleGeographicArea( 37.7649, 37.7849, -122.4294, -122.4094 ); CircleGeographicArea circularArea = new CircleGeographicArea( new Coordinates(37.7749, -122.4194), 500 // 500 meters radius ); ArrayList polygonCoordinates = new ArrayList<>(); polygonCoordinates.add(new Coordinates(37.7749, -122.4194)); polygonCoordinates.add(new Coordinates(37.7799, -122.4144)); polygonCoordinates.add(new Coordinates(37.7849, -122.4194)); polygonCoordinates.add(new Coordinates(37.7799, -122.4244)); polygonCoordinates.add(new Coordinates(37.7749, -122.4194)); // Close the polygon PolygonGeographicArea polygonArea = new PolygonGeographicArea(polygonCoordinates); // Monitor all areas if (alarmService != null) { alarmService.monitorArea(rectangularArea); alarmService.monitorArea(circularArea); alarmService.monitorArea(polygonArea); } Log.d("AreaAlarm", "Started monitoring 3 geographic areas"); checkMonitoredAreas(); } } private void checkMonitoredAreas() { SdkCall.execute(() -> { List monitoredAreas = alarmService != null ? alarmService.getMonitoredAreas() : Collections.emptyList(); Log.d("AreaAlarm", "Currently monitoring " + monitoredAreas.size() + " areas"); for (AlarmMonitoredArea area : monitoredAreas) { Log.d("AreaAlarm", "Area ID: " + area.getId()); } // Check which areas user is currently inside List insideAreas = alarmService != null ? alarmService.getInsideAreas() : Collections.emptyList(); Log.d("AreaAlarm", "User is inside " + insideAreas.size() + " areas"); }); } @Override protected void onDestroy() { super.onDestroy(); // Clean up - unmonitor all areas SdkCall.execute(() -> { if (alarmService != null) { // Unmonitor areas individually using the original area instances alarmService.unmonitorArea(rectangularArea); alarmService.unmonitorArea(circularArea); alarmService.unmonitorArea(polygonArea); } Log.d("AreaAlarm", "Unmonitored all areas"); }); } } ``` This example demonstrates: * Creating different types of geographic areas (rectangle, circle, polygon) * Monitoring areas with the AlarmService * Handling boundary crossing events with proper Android logging * Checking currently monitored and inside areas * Proper cleanup when the activity is destroyed --- ### Get started with Alarms |

The alarm system within the GemSDK offers a range of monitoring and notification functionalities. It allows for the detection and management of different types of alarms, such as boundary crossings, speed limit violations, and landmark alerts. The system provides the ability to configure parameters like alarm distance, overspeed thresholds, and whether monitoring should occur even without a route being followed. The key feature of this system is its ability to monitor specific geographic areas or routes and trigger alarms when predefined conditions, such as crossing a boundary or entering/exiting a tunnel, are met. It also provides customization options for alarm behavior based on location (e.g., inside or outside city limits). Additionally, users can set up callbacks to receive notifications about specific events, including changes in monitoring state or when a landmark alarm is triggered. The system supports interaction with various alarm types, including overlay item and landmark alarms, and offers an easy interface for both setting and getting alarm-related information. Tip Multiple alarm services and listeners can operate simultaneously, allowing for the monitoring of various events concurrently. #### Configure AlarmService and AlarmListener[​](#configure-alarmservice-and-alarmlistener "Direct link to Configure AlarmService and AlarmListener") The code snippet provided below defines an `AlarmListener` and creates an `AlarmService` based on this listener: * Kotlin * Java ```kotlin // Create alarm listener using lambda-based approach val alarmListener = AlarmListener.create( onBoundaryCrossed = { // Handle boundary crossing events // Use alarmService?.crossedBoundaries to get affected geographic areas }, onMonitoringStateChanged = { isMonitoringActive -> // Handle monitoring state changes (GPS availability) }, onTunnelEntered = { // Handle tunnel entry events (switch UI to night mode) }, onTunnelLeft = { // Handle tunnel exit events (switch UI to day mode) }, onLandmarkAlarmsUpdated = { // Handle new landmark alarms // Use alarmService?.landmarkAlarms to get active landmark alarms }, onOverlayItemAlarmsUpdated = { // Handle new overlay item alarms (e.g., speed cameras) // Use alarmService?.overlayItemAlarms to get active overlay alarms }, onLandmarkAlarmsPassedOver = { // Handle passed landmark alarms // Use alarmService?.landmarkAlarmsPassedOver to get deactivated alarms }, onOverlayItemAlarmsPassedOver = { // Handle passed overlay item alarms // Use alarmService?.overlayItemAlarmsPassedOver to get deactivated alarms }, onHighSpeed = { limit, insideCityArea -> // Handle speed limit exceeded events }, onSpeedLimit = { speed, limit, insideCityArea -> // Handle speed limit change events }, onNormalSpeed = { limit, insideCityArea -> // Handle return to normal speed events }, onEnterDayMode = { // Handle day mode transition }, onEnterNightMode = { // Handle night mode transition } ) // Create alarm service based on the previously created listener val alarmService = AlarmService.produce(alarmListener) ``` ```java // Create alarm listener using lambda-based approach AlarmListener alarmListener = AlarmListener.create( () -> { // Handle boundary crossing events // Use alarmService.getCrossedBoundaries() to get affected geographic areas }, (Boolean isMonitoringActive) -> { // Handle monitoring state changes (GPS availability) }, () -> { // Handle tunnel entry events (switch UI to night mode) }, () -> { // Handle tunnel exit events (switch UI to day mode) }, () -> { // Handle new landmark alarms // Use alarmService.getLandmarkAlarms() to get active landmark alarms }, () -> { // Handle new overlay item alarms (e.g., speed cameras) // Use alarmService.getOverlayItemAlarms() to get active overlay alarms }, () -> { // Handle passed landmark alarms // Use alarmService.getLandmarkAlarmsPassedOver() to get deactivated alarms }, () -> { // Handle passed overlay item alarms // Use alarmService.getOverlayItemAlarmsPassedOver() to get deactivated alarms }, (Double limit, Boolean insideCityArea) -> { // Handle speed limit exceeded events }, (Double speed, Double limit, Boolean insideCityArea) -> { // Handle speed limit change events }, (Double limit, Boolean insideCityArea) -> { // Handle return to normal speed events }, () -> { // Handle day mode transition }, () -> { // Handle night mode transition } ); // Create alarm service based on the previously created listener AlarmService alarmService = AlarmService.produce(alarmListener); ``` Alternatively, you can extend the `AlarmListener` class directly for more complex implementations: * Kotlin * Java ```kotlin class CustomAlarmListener : AlarmListener() { override fun onBoundaryCrossed() { // Handle boundary crossing } override fun onMonitoringStateChanged(isMonitoringActive: Boolean) { // Handle monitoring state changes } override fun onHighSpeed(limit: Double, insideCityArea: Boolean) { // Handle speed limit violations } // Override other methods as needed } val alarmService = AlarmService.produce(CustomAlarmListener()) ``` ```java class CustomAlarmListener extends AlarmListener { @Override public void onBoundaryCrossed() { // Handle boundary crossing } @Override public void onMonitoringStateChanged(boolean isMonitoringActive) { // Handle monitoring state changes } @Override public void onHighSpeed(double limit, boolean insideCityArea) { // Handle speed limit violations } // Override other methods as needed } AlarmService alarmService = AlarmService.produce(new CustomAlarmListener()); ``` Each callback method listed above can be specified to receive notifications about different events or topics, depending on your needs. These events cover a range of scenarios, such as boundary crossings, monitoring state changes, tunnel entries or exits, speed limits, and more. By customizing the callbacks, you can tailor the notifications to suit specific use cases, ensuring that the system responds appropriately to various triggers or conditions. danger The `AlarmListener` and `AlarmService` objects must remain in memory for the duration of the notification period. If these objects are removed, the callbacks will not be triggered. It's recommended to keep the `AlarmListener` and `AlarmService` variables in a class that is alive during the whole session. #### Change the alarm listener[​](#change-the-alarm-listener "Direct link to Change the alarm listener") The alarm listener associated with the alarm service can be updated at any time, allowing for dynamic configuration and flexibility in handling various notifications. * Kotlin * Java ```kotlin val newAlarmListener = AlarmListener.create( onHighSpeed = { limit, insideCityArea -> // New implementation for speed alerts } ) alarmService?.setAlarmListener(newAlarmListener) ``` ```java AlarmListener newAlarmListener = AlarmListener.create( (Double limit, Boolean insideCityArea) -> { // New implementation for speed alerts } ); if (alarmService != null) { alarmService.setAlarmListener(newAlarmListener); } ``` --- ### Landmark and overlay alarms |

The AlarmService can be configured to send notifications upon approaching specific landmarks or overlay items within a defined proximity. This behavior can be tailored to trigger notifications exclusively during navigation or simulation modes, or while freely exploring the map without a predefined route. This can be used to implement different use cases such as: * Notify users about incoming reports such as speed cameras, police, accidents or other road hazards. * Notify users when approaching points of interest, such as historical landmarks, monuments, or scenic viewpoints. * Notify users about traffic signs such as stop and give way signs. Tip You can search for landmarks along the active route, whether in navigation or simulation mode, using specific categories or other criteria. Once identified, these landmarks can be added for monitoring. Be sure to account for potential route deviations. danger If notifications are not sent via the `AlarmListener` make sure that: * The `AlarmService` and `AlarmListener` are properly initialized and are kept alive * The `alarmDistance` and `monitorWithoutRoute` properties are configured as needed * The stores / overlays to be monitored are **successfully** added to the `AlarmService` * The overlay items are on the correct side of the road. If the items are on the opposite side of the road, notifications will not be triggered #### Configure the alarm distance[​](#configure-the-alarm-distance "Direct link to Configure the alarm distance") The distance threshold measured in meters for triggering notifications when approaching a landmark or overlay item can be obtained as follows: * Kotlin * Java ```kotlin val alarmDistance = alarmService?.alarmDistance ?: 0.0 ``` ```java double alarmDistance = alarmService != null ? alarmService.getAlarmDistance() : 0.0; ``` This threshold can also be adjusted as needed. For example, the following snippet will configure the alarm service to start sending notifications when within 200 meters of one of the monitored items: * Kotlin * Java ```kotlin alarmService?.alarmDistance = 200.0 ``` ```java if (alarmService != null) { alarmService.setAlarmDistance(200.0); } ``` #### Configure alarms without active navigation[​](#configure-alarms-without-active-navigation "Direct link to Configure alarms without active navigation") The AlarmService can be configured to control whether notifications about approaching landmarks or overlay items are triggered exclusively during navigation (or navigation simulation) on a predefined route, or if they should also occur while freely exploring the map without an active navigation session. The following snippet demonstrates how to enable notifications for monitored landmarks at all times, regardless of whether navigation is active: * Kotlin * Java ```kotlin alarmService?.monitorWithoutRoute = true ``` ```java if (alarmService != null) { alarmService.setMonitorWithoutRoute(true); } ``` The value of the preference set above can be accessed as follows: * Kotlin * Java ```kotlin val isMonitoringWithoutRoute = alarmService?.monitorWithoutRoute ?: false ``` ```java boolean isMonitoringWithoutRoute = alarmService != null ? alarmService.getMonitorWithoutRoute() : false; ``` #### Landmark alarms[​](#landmark-alarms "Direct link to Landmark alarms") ##### Configure alarms listeners[​](#configure-alarms-listeners "Direct link to Configure alarms listeners") Users are notified through the `onLandmarkAlarmsUpdated` and `onLandmarkAlarmsPassedOver` callbacks when approaching the specified landmarks and when the landmarks have been passed respectively. The following snippet demonstrates how to retrieve the next landmark to be intercepted and the distance to it and how to detect when a landmark has been passed: * Kotlin * Java ```kotlin val alarmListener = AlarmListener.create( onLandmarkAlarmsUpdated = { SdkCall.execute { // The landmark alarm list containing the landmarks that are to be intercepted val landmarkAlarms = alarmService?.landmarkAlarms if (landmarkAlarms != null && landmarkAlarms.size > 0) { // Get the closest landmark and its associated distance (in meters) val closestLandmark = landmarkAlarms.getItem(0) val distance = landmarkAlarms.getDistance(0) closestLandmark?.let { landmark -> Log.i("LandmarkAlarm", "The landmark ${landmark.name} is ${distance.toInt()} meters away") } } } }, onLandmarkAlarmsPassedOver = { Log.d("LandmarkAlarm", "Landmark was passed over") SdkCall.execute { // The landmarks that were passed over val landmarkAlarmsPassedOver = alarmService?.landmarkAlarmsPassedOver // Process the landmarks that were passed over landmarkAlarmsPassedOver?.let { passedAlarms -> for (i in 0 until passedAlarms.size) { val landmark = passedAlarms.getItem(i) landmark?.let { Log.d("LandmarkAlarm", "Passed landmark: ${it.name}") } } } } } ) ``` ```java AlarmListener alarmListener = AlarmListener.create( () -> { SdkCall.execute(() -> { // The landmark alarm list containing the landmarks that are to be intercepted LandmarkAlarmList landmarkAlarms = alarmService != null ? alarmService.getLandmarkAlarms() : null; if (landmarkAlarms != null && landmarkAlarms.size() > 0) { // Get the closest landmark and its associated distance (in meters) Landmark closestLandmark = landmarkAlarms.getItem(0); double distance = landmarkAlarms.getDistance(0); if (closestLandmark != null) { Log.i("LandmarkAlarm", "The landmark " + closestLandmark.getName() + " is " + (int) distance + " meters away"); } } }); }, () -> { Log.d("LandmarkAlarm", "Landmark was passed over"); SdkCall.execute(() -> { // The landmarks that were passed over LandmarkAlarmList landmarkAlarmsPassedOver = alarmService != null ? alarmService.getLandmarkAlarmsPassedOver() : null; // Process the landmarks that were passed over if (landmarkAlarmsPassedOver != null) { for (int i = 0; i < landmarkAlarmsPassedOver.size(); i++) { Landmark landmark = landmarkAlarmsPassedOver.getItem(i); if (landmark != null) { Log.d("LandmarkAlarm", "Passed landmark: " + landmark.getName()); } } } }); } ); ``` info The `onLandmarkAlarmsUpdated` callback will be continuously triggered once the threshold distance is exceeded, until the landmark is intercepted. The `onLandmarkAlarmsPassedOver` callback is called once when the landmark is intercepted. danger The items within `landmarkAlarmsPassedOver` do not have a predefined sort order. To identify the most recently passed landmark, you can compare the current list of items with the previous list of `landmarkAlarmsPassedOver` entries. ##### Specify the landmarks to be monitored[​](#specify-the-landmarks-to-be-monitored "Direct link to Specify the landmarks to be monitored") Landmarks can be defined and added to the AlarmService for monitoring. The user will receive notifications through the `onLandmarkAlarmsUpdated` callback when approaching specified landmarks. * Kotlin * Java ```kotlin // Create landmarks to monitor val landmark1 = Landmark().apply { name = "Landmark 1" coordinates = Coordinates(49.0576, 1.9705) } val landmark2 = Landmark().apply { name = "Landmark 2" coordinates = Coordinates(43.7704, 1.2360) } //Create a landmark store and add the landmarks to it LandmarkStoreService().createLandmarkStore("Landmarks to be monitored").also { result -> result?.let { (store, err) -> if (!GemError.isError(err)) store?.let { it.addLandmark(landmark1) it.addLandmark(landmark2) //add the landmark store to the alarm service for monitoring alarmService?.landmarkStores?.add(it) } } } ``` ```java // Create landmarks to monitor Landmark landmark1 = new Landmark(); landmark1.setName("Landmark 1"); landmark1.setCoordinates(new Coordinates(49.0576, 1.9705)); Landmark landmark2 = new Landmark(); landmark2.setName("Landmark 2"); landmark2.setCoordinates(new Coordinates(43.7704, 1.2360)); // Create a landmark store and add the landmarks to it Pair result = new LandmarkStoreService().createLandmarkStore("Landmarks to be monitored"); if (result != null) { LandmarkStore store = result.first; int err = result.second; if (!GemError.isError(err) && store != null) { store.addLandmark(landmark1); store.addLandmark(landmark2); // Add the landmark store to the alarm service for monitoring if (alarmService != null) { alarmService.getLandmarkStores().add(store); } } } ``` Multiple landmark stores can be added to the monitoring list simultaneously. These stores can be updated later by modifying the landmarks within them or removed from the monitoring list as needed. #### Overlay alarms[​](#overlay-alarms "Direct link to Overlay alarms") The workflow for overlay items is similar to that for landmarks, with comparable behavior and functionality. Notifications for overlay items are triggered in the same way as for landmarks, based on proximity or other criteria. The steps for monitoring, and handling events related to overlay items align with those for landmarks. All the notices specified above for landmarks are also applicable for overlay items. danger To enable overlay alarms, a map view must be created, and a style containing the overlay to be monitored should be applied. Additionally, the overlay needs to be enabled for the alarms to function properly. ##### Configure alarms listeners[​](#configure-alarms-listeners-1 "Direct link to Configure alarms listeners") The following snippet demonstrates how to retrieve the next overlay item to be intercepted and the distance to it and how to detect when a overlay item has been passed: * Kotlin * Java ```kotlin val alarmListener = AlarmListener.create( onOverlayItemAlarmsUpdated = { SdkCall.execute { // The overlay item alarm list containing the overlay items that are to be intercepted val overlayItemAlarms = alarmService?.overlayItemAlarms if (overlayItemAlarms != null && overlayItemAlarms.size > 0) { // Get the closest overlay item and its associated distance val closestOverlayItem = overlayItemAlarms.getItem(0) val distance = overlayItemAlarms.getDistance(0) closestOverlayItem?.let { overlayItem -> Log.i("OverlayAlarm", "The overlay item is ${distance.toInt()} meters away") } } } }, onOverlayItemAlarmsPassedOver = { Log.d("OverlayAlarm", "Overlay item was passed over") SdkCall.execute { // The overlay items that were passed over val overlayItemAlarmsPassedOver = alarmService?.overlayItemAlarmsPassedOver // Process the overlay items that were passed over overlayItemAlarmsPassedOver?.let { passedAlarms -> for (i in 0 until passedAlarms.size) { val overlayItem = passedAlarms.getItem(i) overlayItem?.let { Log.d("OverlayAlarm", "Passed overlay item at distance: ${passedAlarms.getDistance(i)}") } } } } } ) ``` ```java AlarmListener alarmListener = AlarmListener.create( () -> { SdkCall.execute(() -> { // The overlay item alarm list containing the overlay items that are to be intercepted OverlayItemAlarmList overlayItemAlarms = alarmService != null ? alarmService.getOverlayItemAlarms() : null; if (overlayItemAlarms != null && overlayItemAlarms.size() > 0) { // Get the closest overlay item and its associated distance OverlayItem closestOverlayItem = overlayItemAlarms.getItem(0); double distance = overlayItemAlarms.getDistance(0); if (closestOverlayItem != null) { Log.i("OverlayAlarm", "The overlay item is " + (int) distance + " meters away"); } } }); }, () -> { Log.d("OverlayAlarm", "Overlay item was passed over"); SdkCall.execute(() -> { // The overlay items that were passed over OverlayItemAlarmList overlayItemAlarmsPassedOver = alarmService != null ? alarmService.getOverlayItemAlarmsPassedOver() : null; // Process the overlay items that were passed over if (overlayItemAlarmsPassedOver != null) { for (int i = 0; i < overlayItemAlarmsPassedOver.size(); i++) { OverlayItem overlayItem = overlayItemAlarmsPassedOver.getItem(i); if (overlayItem != null) { Log.d("OverlayAlarm", "Passed overlay item at distance: " + overlayItemAlarmsPassedOver.getDistance(i)); } } } }); } ); ``` ##### Specify the overlays to be monitored[​](#specify-the-overlays-to-be-monitored "Direct link to Specify the overlays to be monitored") The workflow for specifying the overlay items to be monitored is a bit different from the workflow for landmarks. Instead of specifying the landmarks one by one, the overlay items are specified as a whole, based on the overlay and on the overlay categories. The snippet below shows how to add specific overlay types to be monitored: * Kotlin * Java ```kotlin // Add safety cameras overlay (speed cameras, red light cameras, etc.) alarmService?.startAlarmForOverlay(ECommonOverlayId.Safety) // Add multiple overlay types at once val overlayIds = arrayListOf( ECommonOverlayId.Safety, ECommonOverlayId.SocialReports ) alarmService?.startAlarmForOverlays(overlayIds) // Alternative: Directly access overlays collection alarmService?.overlays?.let { overlaysCollection -> // Add specific overlay by getting it from OverlayService val overlayService = OverlayService() val safetyOverlays = overlayService.getOverlaysById(arrayListOf(ECommonOverlayId.Safety)) overlaysCollection.add(safetyOverlays.first) } ``` ```java // Add safety cameras overlay (speed cameras, red light cameras, etc.) if (alarmService != null) { alarmService.startAlarmForOverlay(ECommonOverlayId.Safety); // Add multiple overlay types at once ArrayList overlayIds = new ArrayList<>(); overlayIds.add(ECommonOverlayId.Safety); overlayIds.add(ECommonOverlayId.SocialReports); alarmService.startAlarmForOverlays(overlayIds); // Alternative: Directly access overlays collection OverlayCollection overlaysCollection = alarmService.getOverlays(); if (overlaysCollection != null) { // Add specific overlay by getting it from OverlayService OverlayService overlayService = new OverlayService(); ArrayList safetyIds = new ArrayList<>(); safetyIds.add(ECommonOverlayId.Safety); Pair safetyOverlays = overlayService.getOverlaysById(safetyIds); overlaysCollection.add(safetyOverlays.first); } } ``` You can also remove specific overlays from monitoring: * Kotlin * Java ```kotlin // Stop monitoring safety cameras alarmService?.stopAlarmForOverlay(ECommonOverlayId.Safety) // Stop monitoring multiple overlay types val overlayIdsToRemove = arrayListOf( ECommonOverlayId.Safety, ECommonOverlayId.SocialReports ) alarmService?.stopAlarmForOverlays(overlayIdsToRemove) ``` ```java // Stop monitoring safety cameras if (alarmService != null) { alarmService.stopAlarmForOverlay(ECommonOverlayId.Safety); // Stop monitoring multiple overlay types ArrayList overlayIdsToRemove = new ArrayList<>(); overlayIdsToRemove.add(ECommonOverlayId.Safety); overlayIdsToRemove.add(ECommonOverlayId.SocialReports); alarmService.stopAlarmForOverlays(overlayIdsToRemove); } ``` Here's a complete example demonstrating landmark and overlay alarm monitoring in an Android application: * Kotlin * Java ```kotlin class LandmarkOverlayAlarmsActivity : AppCompatActivity() { private var alarmService: AlarmService? = null private lateinit var alarmStatusText: TextView private val alarmListener = AlarmListener.create( onLandmarkAlarmsUpdated = { handleLandmarkAlarmsUpdated() }, onLandmarkAlarmsPassedOver = { handleLandmarkAlarmsPassedOver() }, onOverlayItemAlarmsUpdated = { handleOverlayAlarmsUpdated() }, onOverlayItemAlarmsPassedOver = { handleOverlayAlarmsPassedOver() }, postOnMain = true ) private fun initializeAlarmService() { SdkCall.execute { alarmService = AlarmService.produce(alarmListener) alarmService?.apply { // Configure alarm settings alarmDistance = 300.0 // 300 meters monitorWithoutRoute = true // Add landmarks to monitor addCustomLandmarks() // Add overlay monitoring startAlarmForOverlay(ECommonOverlayId.Safety) startAlarmForOverlay(ECommonOverlayId.PointsOfInterest) } } } private fun addCustomLandmarks() { SdkCall.execute { alarmService?.landmarkStores?.let { landmarkStores -> val defaultStore = landmarkStores.default defaultStore?.let { store -> // Add custom landmarks val monument = Landmark().apply { name = "Historic Monument" coordinates = Coordinates(52.3676, 4.9041) // Amsterdam } val viewpoint = Landmark().apply { name = "Scenic Viewpoint" coordinates = Coordinates(52.3702, 4.8952) } store.add(monument) store.add(viewpoint) } } } } private fun handleLandmarkAlarmsUpdated() { SdkCall.execute { val landmarkAlarms = alarmService?.landmarkAlarms if (landmarkAlarms != null && landmarkAlarms.size > 0) { val closestLandmark = landmarkAlarms.getItem(0) val distance = landmarkAlarms.getDistance(0) closestLandmark?.let { landmark -> runOnUiThread { alarmStatusText.text = "Approaching: ${landmark.name} (${distance.toInt()}m)" alarmStatusText.setTextColor(Color.BLUE) } Log.i("LandmarkAlarm", "Approaching landmark: ${landmark.name} at ${distance}m") } } } } private fun handleLandmarkAlarmsPassedOver() { Log.d("LandmarkAlarm", "Landmark passed") runOnUiThread { alarmStatusText.text = "Landmark passed" alarmStatusText.setTextColor(Color.GREEN) } } private fun handleOverlayAlarmsUpdated() { SdkCall.execute { val overlayAlarms = alarmService?.overlayItemAlarms if (overlayAlarms != null && overlayAlarms.size > 0) { val closestOverlay = overlayAlarms.getItem(0) val distance = overlayAlarms.getDistance(0) closestOverlay?.let { overlayItem -> runOnUiThread { alarmStatusText.text = "Safety camera ahead: ${distance.toInt()}m" alarmStatusText.setTextColor(Color.RED) } Log.w("OverlayAlarm", "Safety camera detected at ${distance}m") // Play warning sound for safety cameras playAlarmSound() } } } } private fun handleOverlayAlarmsPassedOver() { Log.d("OverlayAlarm", "Overlay item passed") runOnUiThread { alarmStatusText.text = "Safety camera passed" alarmStatusText.setTextColor(Color.GRAY) } } private fun playAlarmSound() { // Implementation depends on your audio system // Example: MediaPlayer, TTS, or notification sounds } override fun onDestroy() { super.onDestroy() SdkCall.execute { // Clean up monitoring alarmService?.stopAlarmForOverlay(ECommonOverlayId.Safety) alarmService?.stopAlarmForOverlay(ECommonOverlayId.PointsOfInterest) alarmService?.setAlarmListener(null) } alarmService = null } } ``` ```java class LandmarkOverlayAlarmsActivity extends AppCompatActivity { private AlarmService alarmService = null; private TextView alarmStatusText; private final AlarmListener alarmListener = AlarmListener.create( () -> { handleLandmarkAlarmsUpdated(); }, () -> { handleLandmarkAlarmsPassedOver(); }, () -> { handleOverlayAlarmsUpdated(); }, () -> { handleOverlayAlarmsPassedOver(); }, true // postOnMain ); private void initializeAlarmService() { SdkCall.execute(() -> { alarmService = AlarmService.produce(alarmListener); if (alarmService != null) { // Configure alarm settings alarmService.setAlarmDistance(300.0); // 300 meters alarmService.setMonitorWithoutRoute(true); // Add landmarks to monitor addCustomLandmarks(); // Add overlay monitoring alarmService.startAlarmForOverlay(ECommonOverlayId.Safety); alarmService.startAlarmForOverlay(ECommonOverlayId.PointsOfInterest); } }); } private void addCustomLandmarks() { SdkCall.execute(() -> { if (alarmService != null) { LandmarkStoreCollection landmarkStores = alarmService.getLandmarkStores(); if (landmarkStores != null) { LandmarkStore defaultStore = landmarkStores.getDefault(); if (defaultStore != null) { // Add custom landmarks Landmark monument = new Landmark(); monument.setName("Historic Monument"); monument.setCoordinates(new Coordinates(52.3676, 4.9041)); // Amsterdam Landmark viewpoint = new Landmark(); viewpoint.setName("Scenic Viewpoint"); viewpoint.setCoordinates(new Coordinates(52.3702, 4.8952)); defaultStore.add(monument); defaultStore.add(viewpoint); } } } }); } private void handleLandmarkAlarmsUpdated() { SdkCall.execute(() -> { LandmarkAlarmList landmarkAlarms = alarmService != null ? alarmService.getLandmarkAlarms() : null; if (landmarkAlarms != null && landmarkAlarms.size() > 0) { Landmark closestLandmark = landmarkAlarms.getItem(0); double distance = landmarkAlarms.getDistance(0); if (closestLandmark != null) { runOnUiThread(() -> { alarmStatusText.setText("Approaching: " + closestLandmark.getName() + " (" + (int) distance + "m)"); alarmStatusText.setTextColor(Color.BLUE); }); Log.i("LandmarkAlarm", "Approaching landmark: " + closestLandmark.getName() + " at " + distance + "m"); } } }); } private void handleLandmarkAlarmsPassedOver() { Log.d("LandmarkAlarm", "Landmark passed"); runOnUiThread(() -> { alarmStatusText.setText("Landmark passed"); alarmStatusText.setTextColor(Color.GREEN); }); } private void handleOverlayAlarmsUpdated() { SdkCall.execute(() -> { OverlayItemAlarmList overlayAlarms = alarmService != null ? alarmService.getOverlayItemAlarms() : null; if (overlayAlarms != null && overlayAlarms.size() > 0) { OverlayItem closestOverlay = overlayAlarms.getItem(0); double distance = overlayAlarms.getDistance(0); if (closestOverlay != null) { runOnUiThread(() -> { alarmStatusText.setText("Safety camera ahead: " + (int) distance + "m"); alarmStatusText.setTextColor(Color.RED); }); Log.w("OverlayAlarm", "Safety camera detected at " + distance + "m"); // Play warning sound for safety cameras playAlarmSound(); } } }); } private void handleOverlayAlarmsPassedOver() { Log.d("OverlayAlarm", "Overlay item passed"); runOnUiThread(() -> { alarmStatusText.setText("Safety camera passed"); alarmStatusText.setTextColor(Color.GRAY); }); } private void playAlarmSound() { // Implementation depends on your audio system // Example: MediaPlayer, TTS, or notification sounds } @Override protected void onDestroy() { super.onDestroy(); SdkCall.execute(() -> { // Clean up monitoring if (alarmService != null) { alarmService.stopAlarmForOverlay(ECommonOverlayId.Safety); alarmService.stopAlarmForOverlay(ECommonOverlayId.PointsOfInterest); alarmService.setAlarmListener(null); } }); alarmService = null; } } ``` This example demonstrates: * **Proper initialization** of alarm service with both landmark and overlay monitoring * **Custom landmark creation** and monitoring setup * **Overlay monitoring** for safety cameras and points of interest * **Real-time UI updates** showing alarm status and distances * **Proper cleanup** in the activity lifecycle * **Thread-safe operations** using `SdkCall.execute` and `runOnUiThread` --- ### Other alarms |

The Maps SDK for Android provides advanced notification capabilities, enabling users to receive alerts for significant environmental changes. This includes notifications for entering and exiting tunnels, as well as transitions between day and night, based on the user's current location and time of year. #### Get notified when entering and exiting tunnels[​](#get-notified-when-entering-and-exiting-tunnels "Direct link to Get notified when entering and exiting tunnels") This snippet below demonstrates how to set up notifications for entering and exiting tunnels using the AlarmListener: * Kotlin * Java ```kotlin val alarmListener = AlarmListener.create { onTunnelEntered { Log.d("TunnelAlarm", "Tunnel entered") // Update UI for tunnel mode (e.g., switch to night theme) } onTunnelLeft { Log.d("TunnelAlarm", "Tunnel left") // Update UI for normal mode (e.g., switch back to day theme) } } ``` ```java AlarmListener alarmListener = AlarmListener.create( () -> { Log.d("TunnelAlarm", "Tunnel entered"); // Update UI for tunnel mode (e.g., switch to night theme) }, () -> { Log.d("TunnelAlarm", "Tunnel left"); // Update UI for normal mode (e.g., switch back to day theme) } ); ``` #### Get notified when transitioning between day and night[​](#get-notified-when-transitioning-between-day-and-night "Direct link to Get notified when transitioning between day and night") The Maps SDK for Android offers functionality to notify users when their location transitions between day and night, based on the geographical region and the corresponding seasonal changes: * Kotlin * Java ```kotlin val alarmListener = AlarmListener.create { onEnterDayMode { Log.d("DayNightAlarm", "Day mode entered") // Update UI for day mode (e.g., light theme) } onEnterNightMode { Log.d("DayNightAlarm", "Night mode entered") // Update UI for night mode (e.g., dark theme) } } ``` ```java AlarmListener alarmListener = AlarmListener.create( () -> { Log.d("DayNightAlarm", "Day mode entered"); // Update UI for day mode (e.g., light theme) }, () -> { Log.d("DayNightAlarm", "Night mode entered"); // Update UI for night mode (e.g., dark theme) } ); ``` --- ### Speed warnings |

The SDK provides features for monitoring and notifying users about speed limits and violations. You can configure alerts for when a user exceeds the speed limit, when the speed limit changes on the new road segment with respect to the previous, and when the user returns to a normal speed range (onNormalSpeed). The SDK also allows you to set customizable thresholds for speed violations, which can be adjusted for both city and non-city areas. These features help provide timely and relevant speed-related notifications based on the user's location and current speed. info All speed values in the SDK are provided in **meters per second (mps)**. You may want to convert these to km/h or mph for display purposes using the conversion: `km/h = mps * 3.6` or `mph = mps * 2.237`. #### Configure the speed limit listeners[​](#configure-the-speed-limit-listeners "Direct link to Configure the speed limit listeners") * Kotlin * Java ```kotlin val alarmListener = AlarmListener.create( onHighSpeed = { limit, insideCityArea -> val areaType = if (insideCityArea) "inside city area" else "outside city area" Log.w("SpeedAlarm", "Speed limit exceeded while $areaType - limit is ${limit.toInt()} mps") }, onSpeedLimit = { speed, limit, insideCityArea -> val areaType = if (insideCityArea) "inside city area" else "outside city area" Log.i("SpeedAlarm", "New speed limit updated to ${limit.toInt()} mps while $areaType. The current speed is ${speed.toInt()} mps") }, onNormalSpeed = { limit, insideCityArea -> val areaType = if (insideCityArea) "inside city area" else "outside city area" Log.d("SpeedAlarm", "Normal speed restored while $areaType - limit is ${limit.toInt()} mps") } ) // Create alarm service based on the previously created listener val alarmService = AlarmService.produce(alarmListener) ``` ```java AlarmListener alarmListener = AlarmListener.create( (Double limit, Boolean insideCityArea) -> { String areaType = insideCityArea ? "inside city area" : "outside city area"; Log.w("SpeedAlarm", "Speed limit exceeded while " + areaType + " - limit is " + limit.intValue() + " mps"); }, (Double speed, Double limit, Boolean insideCityArea) -> { String areaType = insideCityArea ? "inside city area" : "outside city area"; Log.i("SpeedAlarm", "New speed limit updated to " + limit.intValue() + " mps while " + areaType + ". The current speed is " + speed.intValue() + " mps"); }, (Double limit, Boolean insideCityArea) -> { String areaType = insideCityArea ? "inside city area" : "outside city area"; Log.d("SpeedAlarm", "Normal speed restored while " + areaType + " - limit is " + limit.intValue() + " mps"); } ); // Create alarm service based on the previously created listener AlarmService alarmService = AlarmService.produce(alarmListener); ``` Alternatively, you can extend the `AlarmListener` class for more complex speed monitoring: * Kotlin * Java ```kotlin class SpeedMonitoringListener : AlarmListener() { override fun onHighSpeed(limit: Double, insideCityArea: Boolean) { // val limitKmh = (limit * 3.6).toInt() // Convert mps to km/h // val areaType = if (insideCityArea) "city" else "highway" // Log speed violation Log.w("SpeedWarning", "Speed limit exceeded in $areaType: ${limitKmh} km/h") // Show user notification // showSpeedWarning("Speed limit: ${limitKmh} km/h", insideCityArea) // Play audio warning if needed // playSpeedWarningSound() } override fun onSpeedLimit(speed: Double, limit: Double, insideCityArea: Boolean) { // val speedKmh = (speed * 3.6).toInt() // val limitKmh = (limit * 3.6).toInt() // val areaType = if (insideCityArea) "city area" else "highway" // // updateSpeedLimitDisplay(limitKmh, areaType) // Check if currently speeding // if (speed > limit) { // showSpeedViolationWarning(speedKmh, limitKmh) // } } override fun onNormalSpeed(limit: Double, insideCityArea: Boolean) { // Hide any active speed warnings // hideSpeedWarning() // Update UI to show normal speed status // showNormalSpeedStatus() } private fun showSpeedWarning(message: String, isCity: Boolean) { // Update UI with speed warning } private fun updateSpeedLimitDisplay(limit: Int, areaType: String) { // speedLimitDisplay.text = "${limit} km/h ($areaType)" } } val alarmService = AlarmService.produce(SpeedMonitoringListener()) ``` ```java class SpeedMonitoringListener extends AlarmListener { @Override public void onHighSpeed(double limit, boolean insideCityArea) { // int limitKmh = (int) (limit * 3.6); // Convert mps to km/h // String areaType = insideCityArea ? "city" : "highway"; // Log speed violation Log.w("SpeedWarning", "Speed limit exceeded in " + areaType + ": " + limitKmh + " km/h"); // Show user notification // showSpeedWarning("Speed limit: " + limitKmh + " km/h", insideCityArea); // Play audio warning if needed // playSpeedWarningSound(); } @Override public void onSpeedLimit(double speed, double limit, boolean insideCityArea) { // int speedKmh = (int) (speed * 3.6); // int limitKmh = (int) (limit * 3.6); // String areaType = insideCityArea ? "city area" : "highway"; // // updateSpeedLimitDisplay(limitKmh, areaType); // Check if currently speeding // if (speed > limit) { // showSpeedViolationWarning(speedKmh, limitKmh); // } } @Override public void onNormalSpeed(double limit, boolean insideCityArea) { // Hide any active speed warnings // hideSpeedWarning(); // Update UI to show normal speed status // showNormalSpeedStatus(); } private void showSpeedWarning(String message, boolean isCity) { // Update UI with speed warning } private void updateSpeedLimitDisplay(int limit, String areaType) { // speedLimitDisplay.setText(limit + " km/h (" + areaType + ")"); } } AlarmService alarmService = AlarmService.produce(new SpeedMonitoringListener()); ``` The `onHighSpeed` will continuously send notifications while the user exceeds with a given threshold the maximum speed limit for the current road section. The `onSpeedLimit` will be triggered once the current road section has a different speed than the previous road section. The `onNormalSpeed` will be triggered once the user speed becomes within the limit of the maximum speed limit for the current road section. info Although the parameter is named `insideCityArea`, it refers to areas with generally lower speed limits, such as cities, towns, villages, or similar settlements, regardless of their classification. danger The `limit` parameter provided to `onSpeedLimit` will be 0 if the matched road section does not have a maximum speed limit available or if no road could be found. #### Set the threshold for speed[​](#set-the-threshold-for-speed "Direct link to Set the threshold for speed") The threshold for the maximum speed excess that triggers the `onHighSpeed` callback can be configured as follows: * Kotlin * Java ```kotlin // Set speed thresholds in meters per second (mps) // Trigger onHighSpeed when the speed limit is exceeded by 1.39 mps (5 km/h) inside a city area alarmService?.setOverSpeedThreshold(1.39, true) // Trigger onHighSpeed when the speed limit is exceeded by 2.78 mps (10 km/h) outside city areas alarmService?.setOverSpeedThreshold(2.78, false) // Additional examples with different tolerance levels: // Very strict: 2.78 mps (10 km/h) tolerance for city areas alarmService?.setOverSpeedThreshold(2.78, true) // Moderate: 4.17 mps (15 km/h) tolerance for highways alarmService?.setOverSpeedThreshold(4.17, false) // Lenient: 5.56 mps (20 km/h) tolerance for highways alarmService?.setOverSpeedThreshold(5.56, false) ``` ```java // Set speed thresholds in meters per second (mps) // Trigger onHighSpeed when the speed limit is exceeded by 1.39 mps (5 km/h) inside a city area if (alarmService != null) { alarmService.setOverSpeedThreshold(1.39, true); // Trigger onHighSpeed when the speed limit is exceeded by 2.78 mps (10 km/h) outside city areas alarmService.setOverSpeedThreshold(2.78, false); // Additional examples with different tolerance levels: // Very strict: 2.78 mps (10 km/h) tolerance for city areas alarmService.setOverSpeedThreshold(2.78, true); // Moderate: 4.17 mps (15 km/h) tolerance for highways alarmService.setOverSpeedThreshold(4.17, false); // Lenient: 5.56 mps (20 km/h) tolerance for highways alarmService.setOverSpeedThreshold(5.56, false); } ``` #### Get the threshold for the speed[​](#get-the-threshold-for-the-speed "Direct link to Get the threshold for the speed") The configured threshold can be accessed as follows: * Kotlin * Java ```kotlin val currentThresholdInsideCity = alarmService?.getOverSpeedThreshold(true) ?: 0.0 val currentThresholdOutsideCity = alarmService?.getOverSpeedThreshold(false) ?: 0.0 // Convert to km/h for display val thresholdCityKmh = (currentThresholdInsideCity * 3.6).toInt() val thresholdHighwayKmh = (currentThresholdOutsideCity * 3.6).toInt() println("Speed thresholds: City=${thresholdCityKmh} km/h, Highway=${thresholdHighwayKmh} km/h") ``` ```java double currentThresholdInsideCity = alarmService != null ? alarmService.getOverSpeedThreshold(true) : 0.0; double currentThresholdOutsideCity = alarmService != null ? alarmService.getOverSpeedThreshold(false) : 0.0; // Convert to km/h for display int thresholdCityKmh = (int) (currentThresholdInsideCity * 3.6); int thresholdHighwayKmh = (int) (currentThresholdOutsideCity * 3.6); System.out.println("Speed thresholds: City=" + thresholdCityKmh + " km/h, Highway=" + thresholdHighwayKmh + " km/h"); ``` #### Complete speed monitoring example[​](#complete-speed-monitoring-example "Direct link to Complete speed monitoring example") Here's a complete implementation demonstrating speed limit monitoring in an Android application: * Kotlin * Java ```kotlin class SpeedMonitoringActivity : AppCompatActivity() { private var alarmService: AlarmService? = null private lateinit var speedLimitText: TextView private lateinit var currentSpeedText: TextView private lateinit var speedWarningPanel: View private val speedAlarmListener = AlarmListener.create( onHighSpeed = { limit, insideCityArea -> handleSpeedViolation(limit, insideCityArea) }, onSpeedLimit = { speed, limit, insideCityArea -> handleSpeedLimitChange(speed, limit, insideCityArea) }, onNormalSpeed = { limit, insideCityArea -> handleNormalSpeed(limit, insideCityArea) }, postOnMain = true ) private fun initializeSpeedMonitoring() { SdkCall.execute { alarmService = AlarmService.produce(speedAlarmListener) alarmService?.apply { // Set reasonable speed thresholds in meters per second (mps) setOverSpeedThreshold(1.39, true) // 5 km/h tolerance in city areas setOverSpeedThreshold(2.78, false) // 10 km/h tolerance on highways // Enable monitoring without requiring an active route monitorWithoutRoute = true } } } private fun handleSpeedViolation(limit: Double, insideCityArea: Boolean) { val limitKmh = (limit * 3.6).toInt() val areaType = if (insideCityArea) "city area" else "highway" // Show warning UI speedWarningPanel.visibility = View.VISIBLE speedWarningPanel.setBackgroundColor(Color.RED) // Update warning text val warningText = "SPEED LIMIT EXCEEDED!\nLimit: ${limitKmh} km/h in $areaType" Toast.makeText(this, warningText, Toast.LENGTH_SHORT).show() // Play warning sound playWarningSound() } private fun handleSpeedLimitChange(speed: Double, limit: Double, insideCityArea: Boolean) { val speedKmh = (speed * 3.6).toInt() val limitKmh = if (limit > 0) (limit * 3.6).toInt() else 0 val areaType = if (insideCityArea) "City" else "Highway" // Update speed limit display val displayText = if (limitKmh > 0) { "$limitKmh km/h ($areaType)" } else { "No limit ($areaType)" } speedLimitText.text = displayText currentSpeedText.text = "${speedKmh} km/h" // Update warning state if (limit > 0 && speed > limit) { speedWarningPanel.setBackgroundColor(Color.YELLOW) speedWarningPanel.visibility = View.VISIBLE } } private fun handleNormalSpeed(limit: Double, insideCityArea: Boolean) { // Hide warning UI speedWarningPanel.visibility = View.GONE // Update status val limitKmh = if (limit > 0) (limit * 3.6).toInt() else 0 val statusText = if (limitKmh > 0) { "Speed: Normal (limit ${limitKmh} km/h)" } else { "Speed: Normal (no limit)" } Toast.makeText(this, statusText, Toast.LENGTH_SHORT).show() } private fun playWarningSound() { // Implementation depends on your audio system // Example using MediaPlayer or TTS } override fun onDestroy() { super.onDestroy() alarmService?.setAlarmListener(null) alarmService = null } } ``` ```java class SpeedMonitoringActivity extends AppCompatActivity { private AlarmService alarmService = null; private TextView speedLimitText; private TextView currentSpeedText; private View speedWarningPanel; private final AlarmListener speedAlarmListener = AlarmListener.create( (Double limit, Boolean insideCityArea) -> { handleSpeedViolation(limit, insideCityArea); }, (Double speed, Double limit, Boolean insideCityArea) -> { handleSpeedLimitChange(speed, limit, insideCityArea); }, (Double limit, Boolean insideCityArea) -> { handleNormalSpeed(limit, insideCityArea); }, true // postOnMain ); private void initializeSpeedMonitoring() { SdkCall.execute(() -> { alarmService = AlarmService.produce(speedAlarmListener); if (alarmService != null) { // Set reasonable speed thresholds in meters per second (mps) alarmService.setOverSpeedThreshold(1.39, true); // 5 km/h tolerance in city areas alarmService.setOverSpeedThreshold(2.78, false); // 10 km/h tolerance on highways // Enable monitoring without requiring an active route alarmService.setMonitorWithoutRoute(true); } }); } private void handleSpeedViolation(double limit, boolean insideCityArea) { int limitKmh = (int) (limit * 3.6); String areaType = insideCityArea ? "city area" : "highway"; // Show warning UI speedWarningPanel.setVisibility(View.VISIBLE); speedWarningPanel.setBackgroundColor(Color.RED); // Update warning text String warningText = "SPEED LIMIT EXCEEDED!\nLimit: " + limitKmh + " km/h in " + areaType; Toast.makeText(this, warningText, Toast.LENGTH_SHORT).show(); // Play warning sound playWarningSound(); } private void handleSpeedLimitChange(double speed, double limit, boolean insideCityArea) { int speedKmh = (int) (speed * 3.6); int limitKmh = limit > 0 ? (int) (limit * 3.6) : 0; String areaType = insideCityArea ? "City" : "Highway"; // Update speed limit display String displayText; if (limitKmh > 0) { displayText = limitKmh + " km/h (" + areaType + ")"; } else { displayText = "No limit (" + areaType + ")"; } speedLimitText.setText(displayText); currentSpeedText.setText(speedKmh + " km/h"); // Update warning state if (limit > 0 && speed > limit) { speedWarningPanel.setBackgroundColor(Color.YELLOW); speedWarningPanel.setVisibility(View.VISIBLE); } } private void handleNormalSpeed(double limit, boolean insideCityArea) { // Hide warning UI speedWarningPanel.setVisibility(View.GONE); // Update status int limitKmh = limit > 0 ? (int) (limit * 3.6) : 0; String statusText; if (limitKmh > 0) { statusText = "Speed: Normal (limit " + limitKmh + " km/h)"; } else { statusText = "Speed: Normal (no limit)"; } Toast.makeText(this, statusText, Toast.LENGTH_SHORT).show(); } private void playWarningSound() { // Implementation depends on your audio system // Example using MediaPlayer or TTS } @Override protected void onDestroy() { super.onDestroy(); if (alarmService != null) { alarmService.setAlarmListener(null); } alarmService = null; } } ``` --- ### Core The articles will guide you in working with landmarks and markers, providing a deeper understanding of predefined and user-defined points of interest, rich metadata, and dynamic annotations. Additionally, you’ll learn how to integrate layered map data through overlays and create navigable routes for various use cases. Finally, you’ll delve into real-time navigation system entities, capable of supporting turn-by-turn guidance and route simulation to enhance user experience. #### [📄️ Base entities](/docs/android/guides/core/base-entities.md) [On this page, we present the simpler ones (coordinates, position, path, geographic areas), while in the following pages we cover the more complex ones (landmarks, markers, overlays, routes).](/docs/android/guides/core/base-entities.md) #### [📄️ Positions](/docs/android/guides/core/positions.md) [The PositionData and ImprovedPostionData classes provide a comprehensive representation of geographical and movement data for GPS-based systems. They include details like coordinates, speed, altitude, direction, and accuracy, along with road-related metadata such as speed limits and modifiers. With robust support for position quality assessment and timestamped data, it is well-suited for navigation and sensor-driven applications.](/docs/android/guides/core/positions.md) #### [📄️ Landmarks](/docs/android/guides/core/landmarks.md) [A landmark is a predefined, permanent location that holds detailed information such as its name, address, description, geographic area, categories (e.g., Gas Station, Shopping), entrance locations, contact details, and sometimes associated multimedia (e.g., icons or images). It represents significant, categorized locations with rich metadata, providing structured context about a place.](/docs/android/guides/core/landmarks.md) #### [📄️ Markers](/docs/android/guides/core/markers.md) [A marker is a visual representation (such as an icon or a geometry, like a polyline or polygon) placed at a specific geographic location on a map to indicate an important point of interest, event, or location.](/docs/android/guides/core/markers.md) #### [📄️ Overlays](/docs/android/guides/core/overlays.md) [An Overlay is an additional map layer, either default or user-defined, with data stored on Magic Lane servers, accessible in online and offline modes, with few exceptions.](/docs/android/guides/core/overlays.md) #### [📄️ Landmarks vs Markers vs Overlays](/docs/android/guides/core/landmarks-markers-overlays.md) [When building a sophisticated mapping application, choosing the right type of object to use for your specific needs is crucial. To assist in making an informed decision, we compare the three core mapping entities in the table below:](/docs/android/guides/core/landmarks-markers-overlays.md) #### [📄️ Routes](/docs/android/guides/core/routes.md) [A Route usually represents a navigable path between two or more landmarks (waypoints). It includes data such as distance, estimated time, and navigation instructions.](/docs/android/guides/core/routes.md) #### [📄️ Navigation instructions](/docs/android/guides/core/navigation-instructions.md) [The Maps SDK for Android offers comprehensive real-time navigation guidance, providing detailed information on the current and upcoming route, including road details, street names, speed limits, and turn directions. It delivers essential data such as remaining travel time, distance to destination, and upcoming turn or road information, ensuring users receive accurate, timely instructions. Designed for both navigation and simulation scenarios, this feature enhances the overall user experience by supporting smooth and efficient route planning and execution.](/docs/android/guides/core/navigation-instructions.md) #### [📄️ Traffic Events](/docs/android/guides/core/traffic-events.md) [The Maps SDK for Android provides real-time information about traffic events, such as delays, which can occur in various forms.](/docs/android/guides/core/traffic-events.md) #### [📄️ Images](/docs/android/guides/core/images.md) [The images are represented in an abstract form which is not directly drawable on the UI. The responsible classes are Image, AbstractGeometryImage, LaneImage, SignpostImage, and RoadInfoImage.](/docs/android/guides/core/images.md) --- ### Base entities |

On this page, we present the simpler ones (coordinates, position, path, geographic areas), while in the following pages we cover the more complex ones (landmarks, markers, overlays, routes). Reading this helps you understand and use the SDK effectively. #### Coordinates[​](#coordinates "Direct link to Coordinates") The `Coordinates` class is a core component designed to represent geographic positions with an optional altitude. The Maps SDK for Android uses the [WGS](https://en.wikipedia.org/wiki/World_Geodetic_System) coordinates standard. Below is an overview of its functionality: Key Features: * **Latitude**: Specifies the north-south position. Range: -90.0 to +90.0. * **Longitude**: Specifies the east-west position. Range: -180.0 to +180.0. * **Altitude** (optional): Specifies the height in meters. Can be positive or negative. info You can check if a `Coordinates` instance is valid by using the `valid()` method, which returns `true` if the latitude and longitude are within their respective valid ranges. ##### Instantiate Coordinates[​](#instantiate-coordinates "Direct link to Instantiate Coordinates") To create a `Coordinates` instance using latitude and longitude: * Kotlin * Java ```kotlin val coordinates = Coordinates(37.7749, -122.4194) // San Francisco ``` ```java Coordinates coordinates = new Coordinates(37.7749, -122.4194); // San Francisco ``` ##### Distance between coordinates[​](#distance-between-coordinates "Direct link to Distance between coordinates") To calculate the distance between two coordinates the `distance` method can be used. This method provides the distance between two coordinates in meters. It also takes into account the altitude if both coordinates have a value for this field. * Kotlin * Java ```kotlin val coord1 = Coordinates(37.7749, -122.4194) // San Francisco val coord2 = Coordinates(34.0522, -118.2437) // Los Angeles val distance = coord1.getDistance(coord2) // Distance in meters ``` ```java Coordinates coord1 = new Coordinates(37.7749, -122.4194); // San Francisco Coordinates coord2 = new Coordinates(34.0522, -118.2437); // Los Angeles double distance = coord1.getDistance(coord2); // Distance in meters ``` ##### Get Azimuth[​](#get-azimuth "Direct link to Get Azimuth") The `getAzimuth` method calculates the azimuth (bearing) from the current coordinate to another coordinate. The azimuth is the angle in degrees from the north direction to the line connecting the two points, measured clockwise. ```text val coordinates = Coordinates(37.7749, -122.4194) val northPoint = Coordinates(90.0, 0.0) val azimuth = coordinates.getAzimuth(CnorthPoint) // Azimuth in degrees ``` #### Path[​](#path "Direct link to Path") A `Path` represents a sequence of connected coordinates. The `Path` class is a core component for representing and managing paths on a map. It offers functionality for path creation, manipulation, and data export, allowing users to define paths and perform various operations programmatically. Key Features * **Path Creation & Management** * Paths can be created from data buffers in multiple formats (e.g., GPX, KML, GeoJSON). * Supports cloning paths in reverse order or between specific coordinates. * **Coordinates Handling** * Provides read-only access to internal coordinates lists. * Retrieves a coordinates based on a percentage along the path. * **Path Properties** * **name**: Manage the name of the path. * **area**: Retrieve the bounding rectangle of the path. * **wayPoints**: Access waypoints along the path. * **Export Functionality** * Export path data in various formats such as GPX, KML, and GeoJSON. ##### Create a Path using coordinates[​](#create-a-path-using-coordinates "Direct link to Create a Path using coordinates") * Kotlin * Java ```kotlin val coordinatesList = CoordinatesList( listOf( Coordinates(37.774947, -122.419449), Coordinates(37.776342, -122.417662), Coordinates(37.777558, -122.416217) ) ) val path = Path.produceWithCoords(CoordinatesList(coordinatesList)) ``` ```java CoordinatesList coordinatesList = new CoordinatesList( Arrays.asList( new Coordinates(37.774947, -122.419449), new Coordinates(37.776342, -122.417662), new Coordinates(37.777558, -122.416217) ) ); Path path = Path.produceWithCoords(new CoordinatesList(coordinatesList)); ``` ##### Create a Path using data buffer[​](#create-a-path-using-data-buffer "Direct link to Create a Path using data buffer") * Kotlin * Java ```kotlin val gpxData: ByteArray = ... // Load GPX data as ByteArray val format : EPathFileFormat = EPathFileFormat.Gpx val path = Path.produceWithDataBuffer(gpxData, format) ``` ```java byte[] gpxData = ...; // Load GPX data as byte array EPathFileFormat format = EPathFileFormat.Gpx; Path path = Path.produceWithDataBuffer(gpxData, format); ``` The code above creates a `Path` instance from GPX data provided as a `ByteArray`. The `EPathFileFormat.Gpx` enum value specifies the format of the data being used. ##### Export a Path to data buffer[​](#export-a-path-to-data-buffer "Direct link to Export a Path to data buffer") To export a Path to some given format (like GeoJson for example) you can proceed like this: * Kotlin * Java ```kotlin val format : EPathFileFormat = EPathFileFormat.GeoJson path.exportAs(format) // Returns a DataBuffer or null if the export failed ``` ```java EPathFileFormat format = EPathFileFormat.GeoJson; path.exportAs(format); // Returns a DataBuffer or null if the export failed ``` ### Geographic areas Geographic areas represent specific regions of the world and serve various purposes, such as centering, restricting searches to a specific region, geofencing, and more. Multiple entities can return a bounding box as a geographic area, defining the zone that contains the item. info The geographic area types are: * **Rectangle Geographic Area**: Represents a rectangular area with the top and bottom sides parallel to the longitude and latitude lines. * **Circle Geographic Area**: Encompasses an area around a specific coordinates with a certain distance. * **Polygon Geographic Area**: Represents a complex area with high precision, ideal for more detailed geographic boundaries. At the foundation of the geographic area hierarchy is the abstract GeographicArea class, which defines the following operations: | Method / Field | Description | Return Type | | ---------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------- | | `boundingBox` | Get the bounding box of the geographic area, which is the smallest rectangle surrounding the area. | `RectangleGeographicArea` | | `centerPoint` | Retrieves the center point of the geographic area, calculated as the geographic center. | `Coordinates` | | `convert(area:GeographicArea)` | Converts the geographic area to another type, if possible. | `GeographicArea?` | | `containsCoordinates(value:Coordinates)` | Checks if the specified point is contained within the geographic area. | `Boolean` | | `isDefault()` | Checks if the geographic area has default values. | `Boolean` | | `isEmpty()` | Checks if the geographic area is empty. | `Boolean` | | `type` | Retrieves the specific type of the geographic area. | `GeographicAreaType` | | `reset()` | Resets the geographic area to its default state. | `Unit` | #### Rectangle Geographic Area[​](#rectangle-geographic-area "Direct link to Rectangle Geographic Area") The RectangleGeographicArea class represents a rectangular geographic area defined by two coordinates: the top-left and bottom-right corners. It provides operations to check for intersections, containment, and unions with other rectangles. To create a new RectangleGeographicArea, the constructor can be used by providing the **top-left** and **bottom-right** coordinates. * Kotlin * Java ```kotlin val topLeft = Coordinates(37.7749, -122.4194) // San Francisco val bottomRight = Coordinates(34.0522, -118.2437) // Los Angeles val rectangleArea = RectangleGeographicArea(topLeft, bottomRight) ``` ```java Coordinates topLeft = new Coordinates(37.7749, -122.4194); // San Francisco Coordinates bottomRight = new Coordinates(34.0522, -118.2437); // Los Angeles RectangleGeographicArea rectangleArea = new RectangleGeographicArea(topLeft, bottomRight); ``` danger Ensure that the top-left coordinate has a latitude greater than the bottom-right coordinate and a longitude less than the bottom-right coordinate. If this condition is not met, a **GemError.InvalidInput** exception will be thrown. ### Circle Geographic Area The CircleGeographicArea class represents a circular geographic area defined by a center point and a radius. It provides methods for checking if a point lies within the circle, calculating the bounding box, and more. To create a new CircleGeographicArea, the constructor can be used by providing the center point and the distance in meters: * Kotlin * Java ```kotlin val radius = 5000// Radius in meters val center = Coordinates(37.7749, -122.4194) // San Francisco val circleArea = CircleGeographicArea(center, radius) ``` ```java int radius = 5000; // Radius in meters Coordinates center = new Coordinates(37.7749, -122.4194); // San Francisco CircleGeographicArea circleArea = new CircleGeographicArea(center, radius); ``` #### Polygon Geographic Area[​](#polygon-geographic-area "Direct link to Polygon Geographic Area") The PolygonGeographicArea class can be used to represent complex custom areas with a high level of precision. They can be created by providing the list of coordinates: * Kotlin * Java ```kotlin val coordinatesList = CoordinatesList( listOf( Coordinates(37.774947, -122.419449), Coordinates(37.776342, -122.417662), Coordinates(37.777558, -122.416217), ) ) val polygonArea = PolygonGeographicArea() polygonArea.coordinates =coordinatesList ``` ```java CoordinatesList coordinatesList = new CoordinatesList( Arrays.asList( new Coordinates(37.774947, -122.419449), new Coordinates(37.776342, -122.417662), new Coordinates(37.777558, -122.416217) ) ); PolygonGeographicArea polygonArea = new PolygonGeographicArea(); polygonArea.setCoordinates(coordinatesList); ``` info The polygon will be automatically closed, meaning that the last coordinate will be connected to the first one to form a closed shape. danger A polygon must have at least 3 coordinates to be valid. --- ### Images |

The images are represented in an abstract form which is not directly drawable on the UI. The responsible classes are `Image`, `AbstractGeometryImage`, `LaneImage`, `SignpostImage`, and `RoadInfoImage`. These classes provide the following methods and properties: * `uid`: Gets the unique ID of the image. If two images are the same, then they have the same UID. This allows the user to optimize UI redraws and trigger an update only when the image changes. * `isValid()`: Verifies if the image data is valid. If the image is not valid, then `asBitmap()` and `render()` methods will return `null` or fail. * `asBitmap(width, height)`: Returns an Android `Bitmap` which can be displayed on the UI using `ImageView.setImageBitmap()`. It returns `null` if the image is invalid. * `render(bitmap)`: Renders the image into the provided `IBitmap` object. Returns error codes for validation. #### Plain images[​](#plain-images "Direct link to Plain images") The `Image` class corresponds to plain (usually non-vector) images. These images have a recommended size and aspect ratio but can be resized to any dimension in order to be drawn (with possible loss of quality). The user can instantiate an `Image` class: * Using the `Image.produceWithDataBuffer()` method which takes a `DataBuffer` containing the image data and an `EImageFileFormat`. * Using the `Image.produceWithPath()` method which takes a file path to an image file. * Using the `ImageDatabase` to retrieve predefined SDK images by ID. #### Special images[​](#special-images "Direct link to Special images") The `AbstractGeometryImage`, `LaneImage`, `SignpostImage`, and `RoadInfoImage` classes correspond to vector images generated by the SDK based on internal data. They provide customizable options for rendering. They do not have a default size or aspect ratio. The `LaneImage`, `SignpostImage`, and `RoadInfoImage` classes provide specialized `asBitmap()` methods that handle resizing automatically based on the image content. These images can also be configured with custom render settings for colors and appearance. The particularities of each image type are presented in the table below: | Class | Size and aspect ratio | Customizable render options | The size of the bitmap returned by the `asBitmap()` methods | Instantiable by the user | | ----------------------- | -------------------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `Image` | Usually fixed and retrievable via the `size` and `aspectRatio` properties. | Not available | Will always render at the size specified by the user if provided, or the recommended size for the particular image otherwise. | Yes, via the provided factory methods `Image.produceWithDataBuffer()` and `Image.produceWithPath()`. It can also be provided by the SDK. | | `AbstractGeometryImage` | Generated by the SDK. Size and aspect ratio not retrievable. | Yes, via `AbstractGeometryImageRenderSettings` | Will always render at the size specified by the user if provided, or the SDK default image size otherwise. | No. Can only be provided by the SDK. | | `LaneImage` | Generated by the SDK. Size and aspect ratio not retrievable. | Yes, via `LaneImageRenderSettings` | Returns a `Pair` where the first value is the actual width used (when width is 0, auto-calculated), and the second is the bitmap. | No. Can only be provided by the SDK. | | `SignpostImage` | Generated by the SDK. Size and aspect ratio not retrievable. | Yes, via `SignpostImageRenderSettings` | Returns a `Pair` where the first value is the actual width used (when width is 0, auto-calculated), and the second is the bitmap. | No. Can only be provided by the SDK. | | `RoadInfoImage` | Generated by the SDK. Size and aspect ratio not retrievable. | Background color customizable via `Rgba` | Returns a `Pair` where the first value is the actual width used (when width is 0, auto-calculated), and the second is the bitmap. | No. Can only be provided by the SDK. | Tip For debugging purposes, image bitmaps can be saved to files or viewed directly on Android devices. You can use the `Bitmap.compress()` method to save bitmaps to PNG format for inspection during development. #### Approaches[​](#approaches "Direct link to Approaches") The different approaches for getting images as Android `Bitmap` objects from the SDK are illustrated below using the lane image contained within `NavigationInstruction`. The general principles are the same for all types of images. ##### Approach 1[​](#approach-1 "Direct link to Approach 1") * Kotlin * Java ```kotlin val laneImage = instruction.laneImage val bitmap = laneImage?.asBitmap(200, 100) // Usage example if (bitmap != null) { // Successfully obtained lane image bitmap imageView.setImageBitmap(bitmap) } else { // Handle error case println("Failed to get lane image bitmap") } ``` ```java LaneImage laneImage = instruction.getLaneImage(); Bitmap bitmap = null; if (laneImage != null) { bitmap = laneImage.asBitmap(200, 100); } // Usage example if (bitmap != null) { // Successfully obtained lane image bitmap imageView.setImageBitmap(bitmap); } else { // Handle error case System.out.println("Failed to get lane image bitmap"); } ``` This approach is simple and straightforward, with minimal boilerplate code. However, it comes with a few limitations: * You don't have access to custom color settings for the lane image. * There is no way to retrieve the actual image size when using auto-sizing. * The image UID is not directly accessible through this method. The `asBitmap` method returns null if the image is invalid. ##### Approach 2[​](#approach-2 "Direct link to Approach 2") * Kotlin * Java ```kotlin val laneImage = instruction.laneImage val (actualWidth, bitmap) = laneImage?.asBitmap(200, 100) ?: Pair(0, null) // Get more data about the lane image val uid = laneImage?.uid ?: 0L ``` ```java LaneImage laneImage = instruction.getLaneImage(); int actualWidth = 0; Bitmap bitmap = null; if (laneImage != null) { Pair result = laneImage.asBitmap(200, 100); if (result != null) { actualWidth = result.first; bitmap = result.second; } } // Get more data about the lane image long uid = laneImage != null ? laneImage.getUid() : 0L; ``` This approach provides the actual width used when rendering the image, which is particularly useful for lane images that auto-calculate their width based on content. The `asBitmap` method returns a `Pair` where: * The first value is the actual width used (especially when you pass 0 for width to auto-calculate) * The second value is the rendered bitmap or null if invalid You can efficiently use the image UID to optimize redraws by checking if the image has changed before re-rendering. ##### Approach 3[​](#approach-3 "Direct link to Approach 3") * Kotlin * Java ```kotlin val laneImage = instruction.laneImage val backgroundColor = Rgba(118, 99, 200, 255) val activeColor = Rgba(255, 255, 255, 255) val inactiveColor = Rgba(24, 33, 21, 255) val (actualWidth, bitmap) = laneImage?.asBitmap( 200, 100, backgroundColor, activeColor, inactiveColor ) ?: Pair(0, null) // Get more data about the lane image val uid = laneImage?.uid ?: 0L ``` ```java LaneImage laneImage = instruction.getLaneImage(); Rgba backgroundColor = new Rgba(118, 99, 200, 255); Rgba activeColor = new Rgba(255, 255, 255, 255); Rgba inactiveColor = new Rgba(24, 33, 21, 255); int actualWidth = 0; Bitmap bitmap = null; if (laneImage != null) { Pair result = laneImage.asBitmap( 200, 100, backgroundColor, activeColor, inactiveColor ); if (result != null) { actualWidth = result.first; bitmap = result.second; } } // Get more data about the lane image long uid = laneImage != null ? laneImage.getUid() : 0L; ``` This approach provides the greatest flexibility by allowing custom color configuration: * `backgroundColor`: The background color of the lane image * `activeColor`: Color for active/highlighted lanes * `inactiveColor`: Color for inactive/non-highlighted lanes The returned `Pair` contains the actual dimensions and the rendered bitmap with your custom colors applied. ##### Approach 4 - Using GemUtilImages[​](#approach-4---using-gemutilimages "Direct link to Approach 4 - Using GemUtilImages") * Kotlin * Java ```kotlin val laneImage = instruction.laneImage val backgroundColor = Rgba(118, 99, 200, 255) val activeColor = Rgba(255, 255, 255, 255) val inactiveColor = Rgba(24, 33, 21, 255) val resultPair = SdkCall.execute { GemUtilImages.asBitmap( laneImage, 200, 100, backgroundColor, activeColor, inactiveColor ) } val errorCode = resultPair?.first ?: -1 val bitmap = resultPair?.second if (errorCode == 0 && bitmap != null) { // Successfully obtained lane image bitmap imageView.setImageBitmap(bitmap) } else { // Handle error case println("Failed to get lane image, error code: $errorCode") } ``` ```java LaneImage laneImage = instruction.getLaneImage(); Rgba backgroundColor = new Rgba(118, 99, 200, 255); Rgba activeColor = new Rgba(255, 255, 255, 255); Rgba inactiveColor = new Rgba(24, 33, 21, 255); Pair resultPair = SdkCall.execute(() -> GemUtilImages.asBitmap( laneImage, 200, 100, backgroundColor, activeColor, inactiveColor ) ); int errorCode = resultPair != null ? resultPair.first : -1; Bitmap bitmap = resultPair != null ? resultPair.second : null; if (errorCode == 0 && bitmap != null) { // Successfully obtained lane image bitmap imageView.setImageBitmap(bitmap); } else { // Handle error case System.out.println("Failed to get lane image, error code: " + errorCode); } ``` This approach uses the `GemUtilImages` utility class directly and provides error codes for more detailed error handling. The method returns a `Pair` where the first value is an error code (0 for success) and the second is the bitmap. This method should be called within `SdkCall.execute` for thread safety. #### Working with Render Settings[​](#working-with-render-settings "Direct link to Working with Render Settings") Different image types provide customizable render settings to control their appearance: ##### Lane Image Settings[​](#lane-image-settings "Direct link to Lane Image Settings") * Kotlin * Java ```kotlin val laneRenderSettings = LaneImageRenderSettings().apply { backgroundColor = Rgba(118, 99, 200, 255) // Purple background activeColor = Rgba(255, 255, 255, 255) // White for active lanes inactiveColor = Rgba(128, 128, 128, 255) // Gray for inactive lanes } // Use the settings when rendering val (width, bitmap) = laneImage?.asBitmap(200, 100, laneRenderSettings.backgroundColor, laneRenderSettings.activeColor, laneRenderSettings.inactiveColor ) ?: Pair(0, null) ``` ```java LaneImageRenderSettings laneRenderSettings = new LaneImageRenderSettings(); laneRenderSettings.setBackgroundColor(new Rgba(118, 99, 200, 255)); // Purple background laneRenderSettings.setActiveColor(new Rgba(255, 255, 255, 255)); // White for active lanes laneRenderSettings.setInactiveColor(new Rgba(128, 128, 128, 255)); // Gray for inactive lanes // Use the settings when rendering int width = 0; Bitmap bitmap = null; if (laneImage != null) { Pair result = laneImage.asBitmap(200, 100, laneRenderSettings.getBackgroundColor(), laneRenderSettings.getActiveColor(), laneRenderSettings.getInactiveColor() ); if (result != null) { width = result.first; bitmap = result.second; } } ``` ##### Signpost Image Settings[​](#signpost-image-settings "Direct link to Signpost Image Settings") * Kotlin * Java ```kotlin val signpostSettings = SignpostImageRenderSettings().apply { borderSize = 10 // Border size in pixels borderRoundCorners = true // Use rounded corners maxRows = 3 // Maximum rows of details smallMode = false // Use large mode } // Apply settings when rendering signpost images val (width, bitmap) = signpostImage?.asBitmap(300, 150) ?: Pair(0, null) ``` ```java SignpostImageRenderSettings signpostSettings = new SignpostImageRenderSettings(); signpostSettings.setBorderSize(10); // Border size in pixels signpostSettings.setBorderRoundCorners(true); // Use rounded corners signpostSettings.setMaxRows(3); // Maximum rows of details signpostSettings.setSmallMode(false); // Use large mode // Apply settings when rendering signpost images int width = 0; Bitmap bitmap = null; if (signpostImage != null) { Pair result = signpostImage.asBitmap(300, 150); if (result != null) { width = result.first; bitmap = result.second; } } ``` ##### Abstract Geometry Image Settings[​](#abstract-geometry-image-settings "Direct link to Abstract Geometry Image Settings") * Kotlin * Java ```kotlin val geometrySettings = AbstractGeometryImageRenderSettings().apply { activeInnerColor = Rgba(255, 255, 255, 255) // White inner active activeOuterColor = Rgba(0, 0, 0, 255) // Black outer active inactiveInnerColor = Rgba(128, 128, 128, 255) // Gray inner inactive inactiveOuterColor = Rgba(64, 64, 64, 255) // Dark gray outer inactive } // Use custom colors for turn arrows val turnBitmap = abstractGeometryImage?.asBitmap(80, 80, geometrySettings.activeInnerColor, geometrySettings.activeOuterColor, geometrySettings.inactiveInnerColor, geometrySettings.inactiveOuterColor ) ``` ```java AbstractGeometryImageRenderSettings geometrySettings = new AbstractGeometryImageRenderSettings(); geometrySettings.setActiveInnerColor(new Rgba(255, 255, 255, 255)); // White inner active geometrySettings.setActiveOuterColor(new Rgba(0, 0, 0, 255)); // Black outer active geometrySettings.setInactiveInnerColor(new Rgba(128, 128, 128, 255)); // Gray inner inactive geometrySettings.setInactiveOuterColor(new Rgba(64, 64, 64, 255)); // Dark gray outer inactive // Use custom colors for turn arrows Bitmap turnBitmap = null; if (abstractGeometryImage != null) { turnBitmap = abstractGeometryImage.asBitmap(80, 80, geometrySettings.getActiveInnerColor(), geometrySettings.getActiveOuterColor(), geometrySettings.getInactiveInnerColor(), geometrySettings.getInactiveOuterColor() ); } ``` #### Common Use Cases[​](#common-use-cases "Direct link to Common Use Cases") ##### Displaying Lane Instructions[​](#displaying-lane-instructions "Direct link to Displaying Lane Instructions") * Kotlin * Java ```kotlin // From navigation instruction val instruction: NavigationInstruction = // ... obtained from navigation val laneImage = instruction.laneImage // Display lane guidance with custom colors laneImage?.let { image -> val (actualWidth, bitmap) = image.asBitmap( 0, 60, // Auto-calculate width, 60px height Rgba(30, 30, 30, 255), // Dark background Rgba(0, 255, 0, 255), // Green for active lanes Rgba(100, 100, 100, 255) // Gray for inactive lanes ) if (bitmap != null) { laneImageView.setImageBitmap(bitmap) // Optionally adjust view width based on actualWidth } } ``` ```java // From navigation instruction NavigationInstruction instruction = // ... obtained from navigation LaneImage laneImage = instruction.getLaneImage(); // Display lane guidance with custom colors if (laneImage != null) { Pair result = laneImage.asBitmap( 0, 60, // Auto-calculate width, 60px height new Rgba(30, 30, 30, 255), // Dark background new Rgba(0, 255, 0, 255), // Green for active lanes new Rgba(100, 100, 100, 255) // Gray for inactive lanes ); if (result != null && result.second != null) { int actualWidth = result.first; Bitmap bitmap = result.second; laneImageView.setImageBitmap(bitmap); // Optionally adjust view width based on actualWidth } } ``` ##### Displaying Turn Instructions[​](#displaying-turn-instructions "Direct link to Displaying Turn Instructions") * Kotlin * Java ```kotlin // From navigation instruction val turnImage = instruction.nextTurnDetails?.abstractGeometryImage turnImage?.let { image -> val bitmap = image.asBitmap(80, 80, Rgba(255, 255, 255, 255), // Active inner: white Rgba(0, 0, 0, 255), // Active outer: black Rgba(128, 128, 128, 255), // Inactive inner: gray Rgba(64, 64, 64, 255) // Inactive outer: dark gray ) if (bitmap != null) { turnArrowImageView.setImageBitmap(bitmap) } } ``` ```java // From navigation instruction AbstractGeometryImage turnImage = null; if (instruction.getNextTurnDetails() != null) { turnImage = instruction.getNextTurnDetails().getAbstractGeometryImage(); } if (turnImage != null) { Bitmap bitmap = turnImage.asBitmap(80, 80, new Rgba(255, 255, 255, 255), // Active inner: white new Rgba(0, 0, 0, 255), // Active outer: black new Rgba(128, 128, 128, 255), // Inactive inner: gray new Rgba(64, 64, 64, 255) // Inactive outer: dark gray ); if (bitmap != null) { turnArrowImageView.setImageBitmap(bitmap); } } ``` ##### Creating Custom Images[​](#creating-custom-images "Direct link to Creating Custom Images") * Kotlin * Java ```kotlin // Load an image from assets val customImage = SdkCall.execute { Image.produceWithPath("/path/to/image.png") } // Or from a data buffer val imageData: ByteArray = // ... load your image data val dataBuffer = DataBuffer().apply { // Populate with image data } val customImage2 = Image.produceWithDataBuffer(dataBuffer, EImageFileFormat.Png) // Convert to bitmap for display val bitmap = customImage?.asBitmap(100, 100) ``` ```java // Load an image from assets Image customImage = SdkCall.execute(() -> Image.produceWithPath("/path/to/image.png") ); // Or from a data buffer byte[] imageData = // ... load your image data DataBuffer dataBuffer = new DataBuffer(); // Populate with image data Image customImage2 = Image.produceWithDataBuffer(dataBuffer, EImageFileFormat.Png); // Convert to bitmap for display Bitmap bitmap = null; if (customImage != null) { bitmap = customImage.asBitmap(100, 100); } ``` ##### Using Predefined SDK Images[​](#using-predefined-sdk-images "Direct link to Using Predefined SDK Images") * Kotlin * Java ```kotlin // Access predefined images from the SDK val tollImage = ImageDatabase.tollImage val ferryImage = ImageDatabase.ferryImage val searchPin = ImageDatabase.searchResultsPin // Convert to bitmaps for display val tollBitmap = tollImage?.asBitmap(32, 32) val ferryBitmap = ferryImage?.asBitmap(32, 32) ``` ```java // Access predefined images from the SDK Image tollImage = ImageDatabase.getTollImage(); Image ferryImage = ImageDatabase.getFerryImage(); Image searchPin = ImageDatabase.getSearchResultsPin(); // Convert to bitmaps for display Bitmap tollBitmap = tollImage != null ? tollImage.asBitmap(32, 32) : null; Bitmap ferryBitmap = ferryImage != null ? ferryImage.asBitmap(32, 32) : null; ``` --- ### Landmarks |

A **landmark** is a predefined, permanent location that holds detailed information such as its name, address, description, geographic area, categories (e.g., Gas Station, Shopping), entrance locations, contact details, and sometimes associated multimedia (e.g., icons or images). It represents significant, categorized locations with rich metadata, providing structured context about a place. Landmarks are typically used in mapping and navigation applications to help users identify and interact with important locations. They can be points of interest (POIs) like restaurants, parks, museums, or any other notable places that users might want to find or navigate to. #### Landmark Structure[​](#landmark-structure "Direct link to Landmark Structure") ###### Geographic Details[​](#geographic-details "Direct link to Geographic Details") A landmark's position is defined by its `coordinates`, which represent the centroid, and its `geographicArea`, representing the full boundary (e.g., circle, rectangle, or polygon). Since landmarks can correspond to buildings, roads, settlements, or regions, the geographic area can be complex. For specific bounding areas, the `getContourGeographicArea` method is used. ###### Descriptive Information[​](#descriptive-information "Direct link to Descriptive Information") Landmarks include attributes like `name`, `description`, and `author`. The name adapts to the SDK's language settings, ensuring localization where applicable. ###### Metadata[​](#metadata "Direct link to Metadata") Landmarks can belong to one or more `categories`, described by `LandmarkCategory`. Additional details like `contactInfo` ( `ContactInfo` is a specialized class for storing common data like phone and email) and `extraInfo` (a structured hashmap) add flexibility for storing metadata. Example of how to set metadata: * Kotlin * Java ```kotlin val landmark = Landmark() val contactInfo = ContactInfo() contactInfo.addField(EContactInfoFieldType.Email, "your_address@yourdomain.com", "business email") contactInfo.addField(EContactInfoFieldType.Phone, "+123456789", "business phone") landmark.contactInfo = contactInfo landmark.extraInfo = arrayListOf("Working hours: 10:00-18:00") ``` ```java Landmark landmark = new Landmark(); ContactInfo contactInfo = new ContactInfo(); contactInfo.addField(EContactInfoFieldType.Email, "your_address@yourdomain.com", "business email"); contactInfo.addField(EContactInfoFieldType.Phone, "+123456789", "business phone"); landmark.setContactInfo(contactInfo); ArrayList extraInfo = new ArrayList<>(); extraInfo.add("Working hours: 10:00-18:00"); landmark.setExtraInfo(extraInfo); ``` ###### Media and Imagery[​](#media-and-imagery "Direct link to Media and Imagery") Images associated with landmarks can be retrieved using getters like `image` (primary image) or `extraImage` (secondary images). Please note that these images might contain invalid data and it is the user's responsability to check the validity of the objects using the provided methods. ###### Advanced Metadata[​](#advanced-metadata "Direct link to Advanced Metadata") Attributes such as `landmarkStoreId`, `landmarkStoreType` provide information about assigned landmark store, the landmark store type. The `timeStamp` records information about the time the landmark was inserted into a store. ###### Adress[​](#adress "Direct link to Adress") The `address` attribute connects landmarks to `AddressInfo`, providing details about the physical address of the location. ###### Unique Identifier[​](#unique-identifier "Direct link to Unique Identifier") The `id` ensures every landmark is uniquely identifiable. Its value is initially **-1** if it does not have an associated ID. You can't modify this value. The `id` is set when the landmark is added to a store. #### Instantiating Landmarks[​](#instantiating-landmarks "Direct link to Instantiating Landmarks") Landmarks can be instantiated in multiple ways: 1. **Default Initialization**: `Landmark()` creates a basic landmark object. 2. **Using Latitude & Longitude**: `Landmark(name,latitude,longitude)` ties a landmark to geographic coordinates. 3. **With a Coordinates Object**: `Landmark(name,coordinates)` uses a predefined `Coordinates` object. danger Creating a new landmark does not automatically make it visible on the map. Refer to the [Display landmarks](/docs/android/guides/maps/display-map-items/display-landmarks.md) guide for detailed instructions on how to display a landmark. #### Copy Landmarks[​](#copy-landmarks "Direct link to Copy Landmarks") Landmarks can be duplicated using the `assign` method, which creates a new landmark instance with identical properties. * Kotlin * Java ```kotlin val originalLandmark = Landmark("Original", 40.7128, -74.0060) val copiedLandmark = Landmark().assign(originalLandmark) ``` ```java Landmark originalLandmark = new Landmark("Original", 40.7128, -74.0060); Landmark copiedLandmark = new Landmark().assign(originalLandmark); ``` #### Interaction with Landmarks[​](#interaction-with-landmarks "Direct link to Interaction with Landmarks") ##### Selecting landmarks[​](#selecting-landmarks "Direct link to Selecting landmarks") Landmarks are selectable by default, meaning user interactions, such as taps or clicks, can identify specific landmarks programmatically (e.g., through the function `cursorSelectionLandmarks()`). Refer to the [Landmark selection guide](/docs/android/guides/maps/interact-with-map.md#landmark-selection) for more details. ##### Highlighting landmarks[​](#highlighting-landmarks "Direct link to Highlighting landmarks") A list of landmarks can be highlighted, enabling customization of their visual appearance. Highlighting also allows displaying a list of user created landmarks on the map. When displaying them, we can also provide an identifier for the highlight. It is possible to de-activate that highlight or to update it - in this case the old highlight is overridden. Refer to the [Highlight landmarks](/docs/android/guides/maps/display-map-items/display-landmarks.md#highlight-landmarks) guide for more details. ##### Searching landmarks[​](#searching-landmarks "Direct link to Searching landmarks") Landmarks are searchable (both landmarks from map and user created landmarks). Search can be performed based on name, geographic location, proximity to a given route, address, and more. Options to filter the search based on landmark categories are available. Refer to the [Get started with Search](/docs/android/guides/search/get-started-search.md) guide for more details. ##### Calculating route with landmarks[​](#calculating-route-with-landmarks "Direct link to Calculating route with landmarks") Landmarks are the sole entities used for route calculations. For detailed guidance, refer to the [Get started with Routing](/docs/android/guides/routing/get-started-routing.md) guide. ##### Get notifications when approaching landmarks[​](#get-notifications-when-approaching-landmarks "Direct link to Get notifications when approaching landmarks") Alarms can be configured to notify users when approaching specific landmarks that have been custom-selected. See the [Landmarks and overlay alarms](/docs/android/guides/todo.md) guide for more details about implementing this feature. #### Other usages[​](#other-usages "Direct link to Other usages") * Most map POIs (such as settlements, roads, addresses, businesses, etc.) are landmarks. * Search results return a list of landmarks. * Intermediary points in a Route are landmarks. #### Landmark Categories[​](#landmark-categories "Direct link to Landmark Categories") Landmarks are categorized based on their assigned categories. Each category is defined by a unique ID, an image (which can be used in various UI components created by the SDK user), and a name that is localized based on the language set for the SDK in the case of default categories. Additionally, a landmark may be associated with a parent landmark store if assigned to one. info A single landmark can belong to multiple categories simultaneously. ##### Predefined generic categories[​](#predefined-generic-categories "Direct link to Predefined generic categories") The default landmark categories are presented below: | **Category** | **Description** | | -------------------------------- | ----------------------------------------------------------------------------- | | **GasStation** | Locations where fuel is available for vehicles. | | **Parking** | Designated areas for vehicle parking, including public and private lots. | | **FoodAndDrink** | Places offering food and beverages, such as restaurants, cafes, or bars. | | **Accommodation** | Facilities providing lodging, including hotels, motels, and hostels. | | **MedicalServices** | Healthcare facilities like hospitals, clinics, and pharmacies. | | **Shopping** | Retail stores, shopping malls, and markets for purchasing goods. | | **CarServices** | Auto repair shops, car washes, and other vehicle maintenance services. | | **PublicTransport** | Locations associated with buses, trains, trams, and other public transit. | | **Wikipedia** | Points of interest with available Wikipedia information for added context. | | **Education** | Educational institutions such as schools, universities, and training centers. | | **Entertainment** | Places for leisure activities, such as cinemas, theaters, or amusement parks. | | **PublicServices** | Government or civic buildings like post offices and administrative offices. | | **GeographicalArea** | Specific geographical zones or regions of interest. | | **Business** | Office buildings, corporate headquarters, and other business establishments. | | **Sightseeing** | Tourist attractions, landmarks, and scenic points of interest. | | **ReligiousPlaces** | Places of worship, such as churches, mosques, temples, or synagogues. | | **Roadside** | Features or amenities located along the side of roads, such as rest areas. | | **Sports** | Facilities for sports and fitness activities, like stadiums and gyms. | | **Uncategorized** | Landmarks that do not fall into any specific category. | | **Hydrants** | Locations of water hydrants, typically for firefighting purposes. | | **EmergencyServicesSupport** | Facilities supporting emergency services, such as dispatch centers. | | **CivilEmergencyInfrastructure** | Infrastructure related to emergency preparedness, such as shelters. | | **ChargingStation** | Stations for charging electric vehicles. | | **BicycleChargingStation** | Locations where bicycles can be charged, typically for e-bikes. | | **BicycleParking** | Designated parking areas for bicycles. | The id for each category can be found in the `EGenericCategoriesIDs` enum, each value having assigned a `value`. Use the `getCategory` method from the `GenericCategories` class to get the `LandmarkCategory` class associated with a `EGenericCategoriesIDs` value. * Kotlin * Java ```kotlin val landmarkCategory = GenericCategories().getCategory(EGenericCategoriesIDs.FoodAndDrink.value) ``` ```java LandmarkCategory landmarkCategory = new GenericCategories().getCategory(EGenericCategoriesIDs.FoodAndDrink.getValue()); ``` warning There are 2 functions in `GenericCategories` class: `getCategory` and get `getGenericCategory`. The first is the one that is explained above, while the second is used to get the generic landmark category associated with a **POI category**. * Kotlin * Java ```kotlin val storeService = LandmarkStoreService() val store = storeService.getLandmarkStoreById(storeService.mapPoisLandmarkStoreId) val mapPoiCategory = store?.categories?.find { it.name == "Coffee Shop" } mapPoiCategory?.let { category -> val landmarkCategory = GenericCategories().getGenericCategory(category.id) Log.d("CATEGORY", " ${landmarkCategory?.name} ") // ->> Food and Drink } ``` ```java LandmarkStoreService storeService = new LandmarkStoreService(); LandmarkStore store = storeService.getLandmarkStoreById(storeService.getMapPoisLandmarkStoreId()); if (store != null && store.getCategories() != null) { LandmarkCategory mapPoiCategory = null; for (LandmarkCategory cat : store.getCategories()) { if ("Coffee Shop".equals(cat.getName())) { mapPoiCategory = cat; break; } } if (mapPoiCategory != null) { LandmarkCategory landmarkCategory = new GenericCategories().getGenericCategory(mapPoiCategory.getId()); if (landmarkCategory != null) { Log.d("CATEGORY", " " + landmarkCategory.getName() + " "); // ->> Food and Drink } } } ``` In addition to the predefined categories, custom landmark categories can be created, offering flexibility to define tailored classifications for specific needs or applications. ##### Tree structure[​](#tree-structure "Direct link to Tree structure") Each **generic landmark category** can include multiple **poi subcategories**. The `LandmarkCategory` is used both for generic categories and poi subcategories. For example, the *Parking* generic category contains *Park and Ride*, *Parking Garage*, *Parking Lot*, *RV Park*, *Truck Parking*, *Truck Stop* and *Parking meter* poi subcategories. To retrieve poi subcategories for a generic category, use the `getPoiCategories` method provided in the `GenericCategories` class. To find the parent generic category of a given subcategory use the `getGenericCategory` method provided in the `GenericCategories` class. ##### Usage[​](#usage "Direct link to Usage") * Can be used as a filter parameter within the various types of search. * Landmark visibility on the map can be toggled based on the categories. * Landmark organization within a store #### Landmark Stores[​](#landmark-stores "Direct link to Landmark Stores") **Landmark stores** are the primary collections used for organizing landmarks across the SDK. Each store contains landmarks and landmark categories, and is identified by a unique name and id. Stores are persisted on the device as SQLite database files and remain accessible across sessions. ##### Manage landmark stores[​](#manage-landmark-stores "Direct link to Manage landmark stores") The operations related to LandmarkStore management can be found within the `LandmarkStoreService` class. ###### Create a new landmark store[​](#create-a-new-landmark-store "Direct link to Create a new landmark store") A new landmark store can be created in the following way: * Kotlin * Java ```kotlin val (store , errorCode) = LandmarkStoreService().createLandmarkStore("MyLandmarkStore") ?: Pair(null, GemError.General) ``` ```java Pair result = new LandmarkStoreService().createLandmarkStore("MyLandmarkStore"); LandmarkStore store = result != null ? result.first : null; int errorCode = result != null ? result.second : GemError.General; ``` The `createLandmarkStore` will create a new landmark store with the given name if it does not already exist or will return the existing landmark store if an instance with the given name already exists. The method also provides optional parameters to set the zoom level at which the landmarks contained within are visible on the map and a custom file path for storing the landmark on the file system can also be provided. The method returns a pair consisting of the created or existing landmark store and an error code indicating the success or failure of the operation. danger Since the landmark store is persistent, the `createLandmarkStore` method may return a `LandmarkStore` instance that already contains landmarks and categories from previous sessions, if a store with the same name was created before. ###### Get landmark store by id[​](#get-landmark-store-by-id "Direct link to Get landmark store by id") A landmark store can be obtained by its id: * Kotlin * Java ```kotlin LandmarkStoreService().getLandmarkStoreById(1234) ``` ```java new LandmarkStoreService().getLandmarkStoreById(1234); ``` The `getLandmarkStoreById` method retrieves a valid `LandmarkStore` object when provided with a valid ID. If the ID does not correspond to any `LandmarkStore`, the method returns null. ###### Get landmark store by name[​](#get-landmark-store-by-name "Direct link to Get landmark store by name") A landmark store can be obtained by its name: * Kotlin * Java ```kotlin LandmarkStoreService().getLandmarkStoreByName("MyLandmarkStore") ``` ```java new LandmarkStoreService().getLandmarkStoreByName("MyLandmarkStore"); ``` The `getLandmarkStoreByName` method returns a valid `LandmarkStore` object if a landmark store exists with the given name and null if the given name does not correspond to any `LandmarkStore`. ###### Get all landmark stores[​](#get-all-landmark-stores "Direct link to Get all landmark stores") The list of landmark stores can be retrieved via the `landmarkStores` getter: * Kotlin * Java ```kotlin val landmarkStores = LandmarkStoreService().landmarkStores ``` ```java List landmarkStores = new LandmarkStoreService().getLandmarkStores(); ``` info The `landmarkStores` getter returns both user created landmark stores and predefined stores in the Magic Lane Maps SDK for Android. ###### Remove landmark stores[​](#remove-landmark-stores "Direct link to Remove landmark stores") A landmark store can be removed in the following way: * Kotlin * Java ```kotlin val storeId = landmarkStore.id landmarkStore.release() LandmarkStoreService().removeLandmarkStore(storeId) ``` ```java int storeId = landmarkStore.getId(); landmarkStore.release(); new LandmarkStoreService().removeLandmarkStore(storeId); ``` This will also remove the store from the persistent storage. danger Disposing the `LandmarkStore` object is mandatory before calling the `removeLandmarkStore` method. If the store is not disposed then it **will not be removed**. Any operation called on a disposed `LandmarkStore` instance will result in an exception being thrown. Therefore, it is crucial to obtain the landmark store ID before it is disposed. The `removeLandmarkStore` method will fail if the store is in use (for example, if it is displayed on the map or monitored within the `AlarmService`). In this case, the `ApiErrorService.apiError` will be set to `GemError.inUse`. ###### Retrieving the landmark store type[​](#retrieving-the-landmark-store-type "Direct link to Retrieving the landmark store type") The type of a `LandmarkStore` can be returned by calling `LandmarkStoreService.getLandmarkStoreType` with the store ID. * Kotlin * Java ```kotlin val storeType = LandmarkStoreService().getLandmarkStoreType(landmarkStore.id) ``` ```java int storeType = new LandmarkStoreService().getLandmarkStoreType(landmarkStore.getId()); ``` ###### Predefined landmark stores[​](#predefined-landmark-stores "Direct link to Predefined landmark stores") The Maps SDK for Android includes several predefined landmark stores, and their IDs can be retrieved as follows: * Kotlin * Java ```kotlin val landmarkStoreService = LandmarkStoreService() val mapPoisLandmarkStoreId = landmarkStoreService.mapPoisLandmarkStoreId val mapAddressLandmarkStoreId = landmarkStoreService.mapAddressLandmarkStoreId val mapCitiesLandmarkStoreId = landmarkStoreService.mapCitiesLandmarkStoreId ``` ```java LandmarkStoreService landmarkStoreService = new LandmarkStoreService(); int mapPoisLandmarkStoreId = landmarkStoreService.getMapPoisLandmarkStoreId(); int mapAddressLandmarkStoreId = landmarkStoreService.getMapAddressLandmarkStoreId(); int mapCitiesLandmarkStoreId = landmarkStoreService.getMapCitiesLandmarkStoreId(); ``` These values can be useful when determining whether a landmark originated from the default map elements. If its associated `landmarkStoreId` has one of these 3 values, it means it comes from the map rather than from other custom data. danger These three landmark stores are not intended to be modified. They are only used for functionalities like: * Filter the categories of landmarks displayed on the map. * Check whether a landmark comes from the map. * When searching, to filter the significant landmarks. * When using alarms, to filter the significant landmarks. ###### Import landmark stores[​](#import-landmark-stores "Direct link to Import landmark stores") You can import a landmark store using data from a file or a raw data buffer. The `importLandmarks` and `importLandmarksBuffer` methods import data into an existing `LandmarkStore`. Before calling these methods, you must first create or retrieve the target LandmarkStore. The import can be performed from a supported file format (e.g., KML, GeoJSON) or directly from a raw data buffer. You can assign a category and a custom image to the imported landmarks. A ProgressListener is returned to monitor the import progress, and an optional callback can be used to handle completion and error states. * Kotlin * Java ```kotlin landmarkStore.importLandmarks( "Your/path/file", ELandmarkFileFormat.Kml, Image.produceWithPath("img/path/file"), ProgressListener.create(onCompleted = { err, msg -> if (GemError.isError(err)) { //handle error } else { //... } }) ) ``` ```java landmarkStore.importLandmarks( "Your/path/file", ELandmarkFileFormat.Kml, Image.produceWithPath("img/path/file"), ProgressListener.create((err, msg) -> { if (GemError.isError(err)) { //handle error } else { //... } }) ); ``` **Parameters:** * `filePath`: The file path of the landmark file to import. * `format`: The file format (e.g., KML, GeoJSON), see `LandmarkFileFormat`. * `image`: An image to associate with the landmarks. * `onComplete`: A callback invoked once the import finishes, providing a `GemError`. ###### Import from data buffer[​](#import-from-data-buffer "Direct link to Import from data buffer") Landmark stores can also be imported from a raw data buffer: * Kotlin * Java ```kotlin landmarkStore.importLandmarksBuffer( dataBuffer, ELandmarkFileFormat.Kml, Image.produceWithPath("img/path/file"), ProgressListener.create(onCompleted = { err, msg -> if (GemError.isError(err)) { //handle error } else { //... } }) ) ``` ```java landmarkStore.importLandmarksBuffer( dataBuffer, ELandmarkFileFormat.Kml, Image.produceWithPath("img/path/file"), ProgressListener.create((err, msg) -> { if (GemError.isError(err)) { //handle error } else { //... } }) ); ``` **Parameters:** * `buffer`: The binary data representing the landmark file content. * `format`: The file format (e.g., KML, GeoJSON), see `LandmarkFileFormat`. * `image`: A map image to associate with the landmarks. * `onComplete`: A callback triggered upon completion with a `GemError`. Tip Use this method when you receive the data as a binary. ##### Browse landmark stores[​](#browse-landmark-stores "Direct link to Browse landmark stores") The `LandmarkBrowseSession` class provides an efficient way to browse and search within a `LandmarkStore` that contains a large number of landmarks. To create a `LandmarkBrowseSession`, use the `createLandmarkBrowseSession` method available in the `LandmarkStore` class. This method takes a `LandmarkBrowseSessionSettings` object as an argument, which defines the sorting and filtering criteria for the session. * Kotlin * Java ```kotlin val browsingSession = landmarkStore.createLandmarkBrowseSession( LandmarkBrowseSessionSettings().apply { //configure settings } ) ``` ```java LandmarkBrowseSessionSettings settings = new LandmarkBrowseSessionSettings(); //configure settings LandmarkBrowseSession browsingSession = landmarkStore.createLandmarkBrowseSession(settings); ``` danger Only the landmark contained within the store before the session creation are available in the browse session. The available sorting and filter settings are: | Field Name | Type | Default Value | Description | | ------------------ | --------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `descendingOrder` | `Boolean` | `false` | Specifies whether the sorting of landmarks should be in descending order. By default, sorting is ascending. | | `orderBy` | `LandmarkOrder` | `LandmarkOrder.name` | Specifies the criteria used for sorting landmarks. By default, landmarks are sorted by name. Other options may include sorting by distance, and insertion date. | | `nameFilter` | `String` | empty string | A filter applied to landmark names. Only landmarks that match this name substring will be included. | | `categoryIdFilter` | `Integer` | `LandmarkStore.invalidLandmarkCategId` | Used to filter landmarks by category ID. The default value is considered invalid, meaning all categories are matched. | | `coordinates` | `Coordinates` | an invalid instance | Specifies a point of reference used when ordering landmarks by distance. Only relevant when `orderBy == LandmarkOrder.distance`. | The operations available within the `LandmarkBrowseSesssion` are: | Member | Type | Description | | --------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `id` | `Integer` (getter) | Retrieves the unique ID of this session. | | `landmarkStoreId` | `Integer` (getter) | Returns the ID of the associated `LandmarkStore`. | | `landmarksCount` | `Integer` (getter) | Gets the total number of landmarks in this session. | | `getLandmarks(start : Integer, end: Integer)` | `LandmarkList?` | Retrieves landmarks between indices `[start, end)`. Used for pagination or slicing the landmark list. | | `getLandmarkPos(landmarkId : Integer)` | `Integer` | Returns the 0-based index of a landmark by its ID, or a not-found code. | | `settings` | `LandmarkBrowseSessionSettings` (getter) | Gets the current session settings. Modifying this object does not affect the session. | ##### Available operations[​](#available-operations "Direct link to Available operations") The `LandmarkStore` instance provides the following operations for managing landmarks and categories: | Operation | Description | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `addCategory(category:LandmarkCategory)` | Adds a new category to the store. The category must have a name. After addition, the category belongs to this store. | | `addLandmark` | Adds a **copy of the landmark** to a specified category in the store. Updates category info if the landmark already exists. Can specify a category. Defaults to uncategorized if no category is specified. | | `getLandmark(landmarkId: Int)` | Retrieves the landmark with the specified landmarkId from the store. Returns null if the landmark does not exist in the store. | | `updateLandmark(landmark: Landmark)` | Updates information about a specific landmark in the store. This does not affect the landmark's category. The landmark must belong to this store. | | `categories` | Retrieves a list of all categories in the store. | | `getCategoryById` | Fetches a category by its ID. Returns `null` if not found. | | `getLandmarks` | Retrieves a list of landmarks in a specified category. Defaults to all categories if none is specified. | | `removeCategory` | Removes a category by its ID. Optionally removes landmarks in the category or marks them as uncategorized. | | `removeLandmark` | Removes a specific landmark from the store. | | `updateCategory` | Updates a specific category's details. The category must belong to this store. | | `removeAllLandmarks` | Removes all landmarks from the store. | | `id` | Retrieves the ID of the landmark store. | | `name` | Retrieves the name of the landmark store. | | `type` | Retrieves the type of the landmark store. Can be none, defaultType, mapAddress, mapPoi, mapCity, mapHighwayExit or mapCountry. | | `startFastUpdateMode()` | Fast update mode allows fast insert, delete and update operations This mode should be used with caution because if a power failure or process crash interrupts it the database will likely go corrupt and will be deleted at next startup This is intended for fast importing external landmarks into application format. | | `stopFastUpdateMode()` | Stop landmark store fast update mode. | | `isFastUpdateMode()` | Checks if fast update mode is currently enabled. Returns `true` if it is, `false` otherwise. | | `importLandmarks(...)` | Imports landmarks from a file into the store. Supports various formats (e.g., KML, GeoJSON). Optionally associates an image with the landmarks. Provides a callback for completion and error handling. | | `importLandmarksBuffer(...)` | Imports landmarks from a raw data buffer into the store. Supports various formats (e.g., KML, GeoJSON). Optionally associates an image with the landmarks. Provides a callback for completion and error handling. | | `cancelImportLandmarks()` | cancels any ongoing landmark import operation. | ##### Usage[​](#usage-1 "Direct link to Usage") * Landmarks are displayed on the map through landmark stores. * Landmark stores are used to customize search functionality. * Alarms for approaching landmarks are managed through landmark stores. * Landmark stores are used for persisting landmarks between different sessions. --- ### Landmarks vs Markers vs Overlays |

When building a sophisticated mapping application, choosing the right type of object to use for your specific needs is crucial. To assist in making an informed decision, we compare the three core mapping entities in the table below: | Characteristic | Landmarks | Markers | Overlays | | --------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | Select from map | basic selection using `cursorSelectionLandmarks()` | advanced selection using `cursorSelectionMarkers()`, providing matched marker and the collection it belongs to, plus positional details such as the marker’s index within its collection, which part/segment or vertex was hit and so on | basic selection using `cursorSelectionOverlayItems()` | | On the map by default | yes | no | yes, if present within the style | | Customizable render settings | basic level of customization using highlights | high level of customization using MarkerRenderSettings | within the style (in Studio). Also allows customization using highlights | | Visibility and layering | toggleable based on the category and store | can individually be changed | toggleable based on the category and overlay | | Searchable | yes | no | yes | | Can be used for route calculation | yes | no | no | | Can be used for alarms | yes | no | yes | | Create custom items | programmatically within the client application | programmatically within the client application | using uploaded GeoJSON Data Sets (in Studio); they cannot be created within the client application(1) | | Available offline | yes | yes | no, with some exceptions | | Shared among users | yes, only for predefined landmarks. Changes made to landmarks and custom landmarks are local | no | yes, the overlay items are accessible by all users given they have the correct style applied | | Extra info | address contact info, category, etc. | no | data with flexible structure (`ParameterList`) | info Social reports can be created and modified by app clients and are accessible to all other users. --- ### Markers |

A marker is a visual representation (such as an icon or a geometry, like a polyline or polygon) placed at a specific geographic location on a map to indicate an important point of interest, event, or location. Markers can represent temporary or user-specified points on the map, such as user-defined locations, waypoints, or temporary annotations. While they are often represented by icons, they can also take the form of more complex geometries, like lines or shapes, depending on the context or requirements. Markers typically contain only basic metadata, such as their position, title, or description, without extensive associated details. By default, the map does not include any visual elements categorized as markers. Users have the ability to create and add markers to the map as needed. #### Instantiating Markers[​](#instantiating-markers "Direct link to Instantiating Markers") Markers can be instantiated via: 1. **Default Initialization** : `Marker()` creates a basic marker object. 2. **With CoordinatesList** : `Marker(coordinates: CoordinatesList)` initializes a marker at specified geographic coordinates 3. **With Coordinates and radius in meters** : `Marker(coordinates: Coordinates, radius: Int)` creates a circular marker centered at the given coordinates with a defined radius. 4. **With Coordinates, horizontal radius, and vertical radius in meters** : `Marker(coordinates: Coordinates, horizRadius: Int, vertRadius: Int)` creates a rectangle marker centered at the specified coordinates with defined horizontal and vertical radii. danger Creating a marker does not automatically display it on the map. Ensure you set its coordinates and attach it to the desired map. Refer to the [Display markers guide](/docs/android/guides/maps/display-map-items/display-markers.md) for detailed instructions. #### Types of Markers[​](#types-of-markers "Direct link to Types of Markers") There are 3 types of markers: * **Point markers** (each part is a group of points - array of coordinates) * **Polyline markers** (each part is a polyline - array of coordinates) * **Polygon markers** (each part is a polygon - array of coordinates) The marker has methods for managing and manipulating markers on a map, including operations such as adding, updating, and deleting coordinates or parts. A marker can be rendered in multiple ways on the map, either through default settings or user-specified rendering options: * An image icon * A polygon drawn with custom collors for border and shape. * A polyline having an associated image at each point ![](/docs/android/assets/images/markerPOI-969016c7920bddf74e0f619d4aa8fe80.png) **Point marker** ![](/docs/android/assets/images/polylineWithPOIs-531e26b86d3622dba94452217847a142.png) **Polyline marker with point markers at each point** ![](/docs/android/assets/images/markerPolygon-3a012f912ddf32888cf7d0a16013236f.png) **Polygon marker** #### Customization options[​](#customization-options "Direct link to Customization options") Markers offer extensive customization options through `MarkerRenderSettings` class and `MarkerCollectionRenderSettings`, enabling developers to tailor their appearance and behavior. Customizable features include: * **Colors**: Modify the fill color, contour color, and text color to match the desired style. * **Sizes**: Adjust dimensions such as line width, label size, and margins to fit specific requirements. * **Labeling and Positioning**: Define custom labeling modes, reposition item or group labels, and adjust the alignment of labels and images relative to geographic coordinates. * **Grouping Behavior**: Configure how multiple markers are grouped when located in proximity. * **Icons**: Customize icons for individual markers or groups, including options for image fit and alignment. * **Polyline and Polygon Textures**: Apply unique textures to polylines and polygons for enhanced visualization. #### Interaction with Markers[​](#interaction-with-markers "Direct link to Interaction with Markers") ##### Selecting markers[​](#selecting-markers "Direct link to Selecting markers") Markers are selectable by default, meaning user interactions, such as taps or clicks, can identify specific markers programmatically (e.g., through the function `mapview.cursorSelectionMarkers()`). Tip When cursor is hovering over a grouped marker cluster, the `mapview.cursorSelectionMarkers()` method will return the `MarkerMatchList` with a **group head marker**. See more about group head markers at Marker Clustering. The result is a `MarkerMatchList`. The `MarkerMatch` item contains detailed information about the match: * the matched `marker: Marker` * the marker's `type: EMarkerMatchType` * the matched marker's `index: Int` in collection * the matched `markerCollection: MarkerCollection` of the marker * the matched marker's `segment: Int` * the matched position `coordinates: Coordinates` * the `distance: Int` from the matched position to the cursor position in meters ##### Searching markers[​](#searching-markers "Direct link to Searching markers") Markers are **not searchable**. ##### Calculating route with marker[​](#calculating-route-with-marker "Direct link to Calculating route with marker") Markers are **not** designed for route calculation. Tip To enable route calculation and navigation, create a new landmark using the relevant coordinates of the marker and a representative name and use that object for routing. #### Marker[​](#marker "Direct link to Marker") | Name | Type | Description | | ---------------------------------------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------- | | `id` | Long | Unique identifier for the marker. | | `name` | String | Name of the marker. | | `type` | EMarkerType | Type of the marker (Point, Polyline, Polygon). | | `area` | RectangleGeographicArea | The geographic area covered by the marker. | | `partCount` | Int | Number of parts in the marker. | | `getCoordinates()` | CoordinatesList | Returns the coordinates of the marker. | | `setCoordinates(coordinates: CoordinatesList)` | Unit | Sets the coordinates of the marker. | | `set(center: Coordinates, radius: Int)` | Unit | Sets the marker as a circle with the specified center and radius in meters. | | `set(center: Coordinates, horizontalRadius: Int, verticalRadius: Int)` | Unit | Sets the marker as an ellipse with the specified center and horizontal/vertical radii in meters. | | `set(corner1: Coordinates, corner2: Coordinates)` | Unit | Sets the marker as a rectangle defined by two corner coordinates. | | `add(coordinates: Coordinates, index: Int, part: Int)` | Unit | Adds coordinates to the marker at the specified index and part. | | `add(latitude: Double, longitude: Double, index: Int, part: Int)` | Unit | Adds coordinates to the marker using latitude and longitude values at the specified index and part. | #### MarkerCollection[​](#markercollection "Direct link to MarkerCollection") The `MarkerCollection` class is the main collection holding markers. All the markers within a collection have the same type and are styled in the same way. | Name | Type | Description | | ---------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------- | | `name` | String | The name of the marker collection. | | `size` | Int | Returns the number of markers in the collection. | | `type` | EMarkerType | Retrieves the type of the marker collection. | | `area` | RectangleGeographicArea | The geographic area enclosing all markers in the collection. | | `markers` | ArrayList\? | The list of markers in the collection. | | `add(marker:Marker,index:Int)` | Unit | Adds a marker to the collection at a specific index (default is the end of the collection). | | `del(index: Int)` | Unit | Deletes a marker from the collection by index. | | `clear()` | Unit | Deletes all markers from the collection. | | `getMarkerAt(index:Int)` | Marker | Returns the marker at a specific index or an empty marker if the index is invalid. | | `getMarkerById(markerId:Long)` | Marker | Retrieves a marker by its unique ID. | | `getPointsGroupHead(groupId:Long)` | Marker | Retrieves the head of a points group for a given marker ID. | | `getPointsGroupComponents(groupId:Long)` | ArrayList\? | Retrieves the components of a points group by its ID. | | `save(buffer: DataBuffer)` | Int | Serializes the collection and adds it to a DataBuffer. | --- ### Navigation instructions |

The Maps SDK for Android offers comprehensive real-time navigation guidance, providing detailed information on the current and upcoming route, including road details, street names, speed limits, and turn directions. It delivers essential data such as remaining travel time, distance to destination, and upcoming turn or road information, ensuring users receive accurate, timely instructions. Designed for both navigation and simulation scenarios, this feature enhances the overall user experience by supporting smooth and efficient route planning and execution. The main class responsible for turn-by-turn live navigation guidance is the `NavigationInstruction` class. info It is important to distinguish between `NavigationInstruction` and `RouteInstruction`. `NavigationInstruction` offers real-time, turn-by-turn navigation based on the user's current position and is relevant only during navigation or simulation. In contrast, `RouteInstruction` provides an overview of the entire route available as soon as the route is calculated and the list of instructions do not change as the user navigates on the route. #### Instantiating navigation instructions[​](#instantiating-navigation-instructions "Direct link to Instantiating navigation instructions") Navigation instructions cannot be directly instantiated. Instead, they must be provided by the SDK while navigating. For detailed guidance on how to navigate on routes, refer to the [Getting Started with Navigation Guide](/docs/android/guides/navigation/get-started-navigation.md). There are two main ways of getting a navigation instruction: * `NavigationInstruction` instances can be obtained via the callback provided as parameters to the `startNavigation` and `startSimulation` methods through the `NavigationListener`. * The `NavigationService` class provides a `getNavigationInstruction` method which returns the currently available navigation instruction. Make sure navigation/simulation is active before using the method provided above. ##### Import statements[​](#import-statements "Direct link to Import statements") To work with navigation instructions in your Android project, you'll need the following imports: * Kotlin * Java ```kotlin import com.magiclane.sdk.routesandnavigation.NavigationInstruction import com.magiclane.sdk.routesandnavigation.NavigationListener import com.magiclane.sdk.routesandnavigation.NavigationService import com.magiclane.sdk.core.Rgba import com.magiclane.sdk.util.GemUtilImages import com.magiclane.sdk.util.SdkCall import android.graphics.Bitmap import android.widget.Toast ``` ```java import com.magiclane.sdk.routesandnavigation.NavigationInstruction; import com.magiclane.sdk.routesandnavigation.NavigationListener; import com.magiclane.sdk.routesandnavigation.NavigationService; import com.magiclane.sdk.core.Rgba; import com.magiclane.sdk.util.GemUtilImages; import com.magiclane.sdk.util.SdkCall; import android.graphics.Bitmap; import android.widget.Toast; ``` ##### Getting navigation instructions through NavigationListener[​](#getting-navigation-instructions-through-navigationlistener "Direct link to Getting navigation instructions through NavigationListener") The most common way to receive navigation instructions is through the `NavigationListener.onNavigationInstructionUpdated` callback: * Kotlin * Java ```kotlin private val navigationListener: NavigationListener = NavigationListener.create( onNavigationInstructionUpdated = { instr -> // Handle the navigation instruction val currentStreetName = instr.currentStreetName val nextTurnInstruction = instr.nextTurnInstruction // Process instruction data... } ) ``` ```java private final NavigationListener navigationListener = NavigationListener.create( instr -> { // Handle the navigation instruction String currentStreetName = instr.getCurrentStreetName(); String nextTurnInstruction = instr.getNextTurnInstruction(); // Process instruction data... } ); ``` ##### Getting navigation instructions from NavigationService[​](#getting-navigation-instructions-from-navigationservice "Direct link to Getting navigation instructions from NavigationService") You can also directly query the navigation service for the current instruction: * Kotlin * Java ```kotlin private val navigationService = NavigationService() // Get current navigation instruction (ensure navigation is active) val currentInstruction = navigationService.getNavigationInstruction() ``` ```java private final NavigationService navigationService = new NavigationService(); // Get current navigation instruction (ensure navigation is active) NavigationInstruction currentInstruction = navigationService.getNavigationInstruction(); ``` #### NavigationInstruction structure[​](#navigationinstruction-structure "Direct link to NavigationInstruction structure") | Member | Type | Description | | ------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `currentCountryCodeISO` | String? | Returns the ISO 3166-1 alpha-3 country code for the current navigation instruction. Empty string means no country. | | `currentStreetName` | String? | Returns the current street name. | | `currentStreetSpeedLimit` | Double | Returns the maximum speed limit on the current street in meters per second. Returns 0 if not available. | | `driveSide` | `EDriveSide` | Returns the drive side flag of the current traveled road. | | `hasNextNextTurnInfo()` | Boolean | Returns true if next-next turn information is available. | | `hasNextTurnInfo()` | Boolean | Returns true if next turn information is available. | | `instructionIndex` | Int | Returns the index of the current route instruction on the current route segment. | | `laneImage` | `Image?` | Returns a customizable image representation of current lane configuration. The user is responsible to verify if the image is valid. | | `navigationStatus` | `ENavigationStatus` | Returns the navigation/simulation status. | | `nextCountryCodeISO` | String? | Returns the ISO 3166-1 alpha-3 country code for the next navigation instruction. | | `nextNextStreetName` | String? | Returns the next-next street name. | | `nextNextTurnDetails` | `TurnDetails?` | Returns the full details for the next-next turn. Used for customizing turn display in UI. | | `nextNextTurnInstruction` | String? | Returns the textual description for the next-next turn. | | `nextStreetName` | String? | Returns the next street name. | | `nextTurnDetails` | `TurnDetails?` | Returns the full details for the next turn. Used for customizing turn display in UI. | | `nextTurnInstruction` | String? | Returns the textual description for the next turn. | | `remainingTravelTimeDistance` | `TimeDistance?` | Returns the remaining travel time in seconds and distance in meters. | | `remainingTravelTimeDistanceToNextWaypoint` | `TimeDistance?` | Returns the remaining travel time in seconds and distance in meters to the next waypoint. | | `currentRoadInformation` | `List<RoadInfo>?` | Returns the current road information list. | | `nextRoadInformation` | `List<RoadInfo>?` | Returns the next road information list. | | `nextNextRoadInformation` | `List<RoadInfo>?` | Returns the next-next road information list. | | `segmentIndex` | Int | Returns the index of the current route segment. | | `signpostDetails` | SignpostDetails? | Returns the extended signpost details. | | `signpostInstruction` | String? | Returns the textual description for the signpost information. | | `timeDistanceToNextNextTurn` | `TimeDistance?` | Returns the time (seconds) and distance (meters) to the next-next turn. Returns values for next turn if no next-next turn available. | | `timeDistanceToNextTurn` | `TimeDistance?` | Returns the time (seconds) and distance (meters) to the next turn. | | `traveledTimeDistance` | `TimeDistance?` | Returns the traveled time in seconds and distance in meters. | The field `nextTurnInstruction` provides an instruction in text format, suitable for displaying on UI. Please use the `onNavigationSound` callback in NavigationListener for getting an instruction suitable for text-to-speech. #### Turn details[​](#turn-details "Direct link to Turn details") ##### Next turn details[​](#next-turn-details "Direct link to Next turn details") The following snippet shows how to extract detailed instructions for the next turn along the route. It's typically used in the navigation UI to show users the upcoming maneuver. You may also use this to provide turn-by-turn instructions with images or detailed text for navigation display: * Kotlin * Java ```kotlin // If hasNextTurnInfo() is false some details are not available val hasNextTurnInfo = navigationInstruction.hasNextTurnInfo() if (hasNextTurnInfo) { // The next turn instruction val nextTurnInstruction = navigationInstruction.nextTurnInstruction // The next turn details val turnDetails = navigationInstruction.nextTurnDetails turnDetails?.let { // Turn event type (continue straight, turn right, turn left, etc.) val event = it.event // Roundabout exit number (-1 if not a roundabout) val roundaboutExitNumber = it.roundaboutExitNumber // Get turn image using GemUtilImages val turnImageSize = 300 val turnImage = getNextTurnImage(navigationInstruction, turnImageSize, turnImageSize) } } // Helper function to get turn image (based on sample code) private fun getNextTurnImage( navInstr: NavigationInstruction, width: Int, height: Int ): Bitmap? { return SdkCall.execute { if (!navInstr.hasNextTurnInfo()) return@execute null val image = navInstr.nextTurnDetails?.abstractGeometryImage val aInner = Rgba(255, 255, 255, 255) val aOuter = Rgba(0, 0, 0, 255) val iInner = Rgba(128, 128, 128, 255) val iOuter = Rgba(128, 128, 128, 255) GemUtilImages.asBitmap(image, width, height, aInner, aOuter, iInner, iOuter) } } ``` ```java // If hasNextTurnInfo() is false some details are not available boolean hasNextTurnInfo = navigationInstruction.hasNextTurnInfo(); if (hasNextTurnInfo) { // The next turn instruction String nextTurnInstruction = navigationInstruction.getNextTurnInstruction(); // The next turn details TurnDetails turnDetails = navigationInstruction.getNextTurnDetails(); if (turnDetails != null) { // Turn event type (continue straight, turn right, turn left, etc.) Object event = turnDetails.getEvent(); // Roundabout exit number (-1 if not a roundabout) int roundaboutExitNumber = turnDetails.getRoundaboutExitNumber(); // Get turn image using GemUtilImages int turnImageSize = 300; Bitmap turnImage = getNextTurnImage(navigationInstruction, turnImageSize, turnImageSize); } } // Helper method to get turn image (based on sample code) private Bitmap getNextTurnImage( NavigationInstruction navInstr, int width, int height ) { return SdkCall.execute(() -> { if (!navInstr.hasNextTurnInfo()) return null; TurnDetails details = navInstr.getNextTurnDetails(); Object image = details != null ? details.getAbstractGeometryImage() : null; Rgba aInner = new Rgba(255, 255, 255, 255); Rgba aOuter = new Rgba(0, 0, 0, 255); Rgba iInner = new Rgba(128, 128, 128, 255); Rgba iOuter = new Rgba(128, 128, 128, 255); return GemUtilImages.asBitmap(image, width, height, aInner, aOuter, iInner, iOuter); }); } ``` See the [TurnDetails](/docs/android/guides/core/routes.md#turn-details) guide for more details about the fields within the `TurnDetails` class. ##### Next next turn details[​](#next-next-turn-details "Direct link to Next next turn details") Details about the subsequent turn (i.e., the turn following the next one) can be crucial for certain use cases, such as providing a preview of upcoming maneuvers. These details can be accessed in a similar manner: * Kotlin * Java ```kotlin // If hasNextNextTurnInfo() is false some details are not available val hasNextNextTurnInfo = navigationInstruction.hasNextNextTurnInfo() if (hasNextNextTurnInfo) { val nextNextTurnInstruction = navigationInstruction.nextNextTurnInstruction val nextNextTurnDetails = navigationInstruction.nextNextTurnDetails // Get next next turn image similarly to next turn image nextNextTurnDetails?.let { val nextNextTurnImage = getNextNextTurnImage(navigationInstruction, 300, 300) } } // Helper function for next next turn image private fun getNextNextTurnImage( navInstr: NavigationInstruction, width: Int, height: Int ): Bitmap? { return SdkCall.execute { if (!navInstr.hasNextNextTurnInfo()) return@execute null val image = navInstr.nextNextTurnDetails?.abstractGeometryImage val aInner = Rgba(255, 255, 255, 255) val aOuter = Rgba(0, 0, 0, 255) val iInner = Rgba(128, 128, 128, 255) val iOuter = Rgba(128, 128, 128, 255) GemUtilImages.asBitmap(image, width, height, aInner, aOuter, iInner, iOuter) } } ``` ```java // If hasNextNextTurnInfo() is false some details are not available boolean hasNextNextTurnInfo = navigationInstruction.hasNextNextTurnInfo(); if (hasNextNextTurnInfo) { String nextNextTurnInstruction = navigationInstruction.getNextNextTurnInstruction(); TurnDetails nextNextTurnDetails = navigationInstruction.getNextNextTurnDetails(); // Get next next turn image similarly to next turn image if (nextNextTurnDetails != null) { Bitmap nextNextTurnImage = getNextNextTurnImage(navigationInstruction, 300, 300); } } // Helper method for next next turn image private Bitmap getNextNextTurnImage( NavigationInstruction navInstr, int width, int height ) { return SdkCall.execute(() -> { if (!navInstr.hasNextNextTurnInfo()) return null; TurnDetails details = navInstr.getNextNextTurnDetails(); Object image = details != null ? details.getAbstractGeometryImage() : null; Rgba aInner = new Rgba(255, 255, 255, 255); Rgba aOuter = new Rgba(0, 0, 0, 255); Rgba iInner = new Rgba(128, 128, 128, 255); Rgba iOuter = new Rgba(128, 128, 128, 255); return GemUtilImages.asBitmap(image, width, height, aInner, aOuter, iInner, iOuter); }); } ``` The `hasNextNextTurnInfo()` might be false if the next instruction is the destination. The same operations discussed earlier for the next turn details can also be applied to the subsequent turn details. #### Street information[​](#street-information "Direct link to Street information") ##### Current street information[​](#current-street-information "Direct link to Current street information") The following snippet shows how to get information about the current road: * Kotlin * Java ```kotlin // Current street name val currentStreetName = navigationInstruction.currentStreetName // Road info related to the current road val currentRoadInfo = navigationInstruction.currentRoadInformation // Country ISO code val countryCode = navigationInstruction.currentCountryCodeISO // The drive direction (left or right) val driveDirection = navigationInstruction.driveSide ``` ```java // Current street name String currentStreetName = navigationInstruction.getCurrentStreetName(); // Road info related to the current road List currentRoadInfo = navigationInstruction.getCurrentRoadInformation(); // Country ISO code String countryCode = navigationInstruction.getCurrentCountryCodeISO(); // The drive direction (left or right) EDriveSide driveDirection = navigationInstruction.getDriveSide(); ``` It is important to note that some streets may not have an assigned name. In such cases, `currentStreetName` will return null. The `RoadInfo` class offers additional details, including the road name and shield type, which correspond to the official codes or names assigned to a road. For example, the `currentStreetName` might return "Bloomsbury Street," while the `roadname` field in the associated RoadInfo instance could provide the official designation, such as "A400." This distinction ensures comprehensive road identification. ##### Next & next next street information[​](#next--next-next-street-information "Direct link to Next & next next street information") Information about the next street, as well as the street following it (next-next street), can also be retrieved. These details include the street name, type, and other associated metadata, enabling enhanced navigation and situational awareness: * Kotlin * Java ```kotlin // Street name val nextStreetName = navigationInstruction.nextStreetName val nextNextStreetName = navigationInstruction.nextNextStreetName // Road info val nextRoadInformation = navigationInstruction.nextRoadInformation val nextNextRoadInformation = navigationInstruction.nextNextRoadInformation // Next country iso code val nextCountryCodeISO = navigationInstruction.nextCountryCodeISO ``` ```java // Street name String nextStreetName = navigationInstruction.getNextStreetName(); String nextNextStreetName = navigationInstruction.getNextNextStreetName(); // Road info List nextRoadInformation = navigationInstruction.getNextRoadInformation(); List nextNextRoadInformation = navigationInstruction.getNextNextRoadInformation(); // Next country iso code String nextCountryCodeISO = navigationInstruction.getNextCountryCodeISO(); ``` The fields associated with these streets retain the same meanings as discussed earlier about the current street. danger Ensure that `hasNextTurnInfo()` and `hasNextNextTurnInfo()` are true before attempting to access the respective fields. This verification prevents errors and ensures the availability of reliable data for the requested information. #### Speed limit information[​](#speed-limit-information "Direct link to Speed limit information") The `NavigationInstruction` class not only provides information about the current road's speed limit but also offers details about upcoming speed limits within a specified distance, assuming the user adheres to the recommended navigation route. The following snippet demonstrates how to retrieve these details and handle various scenarios appropriately: * Kotlin * Java ```kotlin SdkCall.execute { // The current street speed limit in m/s (0.0 if not available) val currentStreetSpeedLimit = navigationInstruction.currentStreetSpeedLimit } ``` ```java SdkCall.execute(() -> { // The current street speed limit in m/s (0.0 if not available) double currentStreetSpeedLimit = navigationInstruction.getCurrentStreetSpeedLimit(); }); ``` #### Lane image[​](#lane-image "Direct link to Lane image") The lane image can be used to more effectively illustrate the correct lane for upcoming turns, providing clearer guidance: * Kotlin * Java ```kotlin val laneImage = navigationInstruction.laneImage // Helper function to get lane image (based on sample code) private fun getLaneInfoImage( navInstr: NavigationInstruction, width: Int, height: Int ): Pair { return SdkCall.execute { var resultWidth = width if (resultWidth == 0) resultWidth = (2.5 * height).toInt() val bkColor = Rgba(118, 99, 200, 255) val activeColor = Rgba(255, 255, 255, 255) val inactiveColor = Rgba(24, 33, 21, 255) val image = navInstr.laneImage val resultPair = GemUtilImages.asBitmap( image, resultWidth, height, bkColor, activeColor, inactiveColor ) // resultPair is Pair where first is error code, second is bitmap val errorCode = resultPair.first val bitmap = resultPair.second return@execute Pair(errorCode, bitmap) } ?: Pair(-1, null) } // Usage example val laneImageData = getLaneInfoImage(navigationInstruction, 500, 300) val errorCode = laneImageData.first val laneImageBitmap = laneImageData.second if (errorCode == 0 && laneImageBitmap != null) { // Successfully obtained lane image bitmap // Use laneImageBitmap for display } else { // Handle error case println("Failed to get lane image, error code: $errorCode") } ``` ```java Object laneImage = navigationInstruction.getLaneImage(); // Helper method to get lane image (based on sample code) private Pair getLaneInfoImage( NavigationInstruction navInstr, int width, int height ) { Pair result = SdkCall.execute(() -> { int resultWidth = width; if (resultWidth == 0) resultWidth = (int) (2.5 * height); Rgba bkColor = new Rgba(118, 99, 200, 255); Rgba activeColor = new Rgba(255, 255, 255, 255); Rgba inactiveColor = new Rgba(24, 33, 21, 255); Object image = navInstr.getLaneImage(); Pair resultPair = GemUtilImages.asBitmap( image, resultWidth, height, bkColor, activeColor, inactiveColor ); // resultPair is Pair where first is error code, second is bitmap int errorCode = resultPair.first; Bitmap bitmap = resultPair.second; return new Pair<>(errorCode, bitmap); }); if (result != null) { return result; } return new Pair<>(-1, null); } // Usage example Pair laneImageData = getLaneInfoImage(navigationInstruction, 500, 300); int errorCode = laneImageData.first; Bitmap laneImageBitmap = laneImageData.second; if (errorCode == 0 && laneImageBitmap != null) { // Successfully obtained lane image bitmap // Use laneImageBitmap for display } else { // Handle error case System.out.println("Failed to get lane image, error code: " + errorCode); } ``` Below is an example of a rendered lane image: ![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYYAAABICAYAAADyFVKVAAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAZ25vbWUtc2NyZWVuc2hvdO8Dvz4AAAArdEVYdENyZWF0aW9uIFRpbWUATHUgMjcgb2N0IDIwMjUgMTY6Mzg6NDcgKzAyMDDnLWIGAAAWUklEQVR4nO3deXAc133g8e/vdc+F+yBA8BLvA7wk0rREiS7bq8iJT0WRIkvxJSuJvbY32Srlj03V7v6xWXtrq1K7lcRH1kekyLt2ZEeydVixfMkqWSQlUiRFiuIFXiBBECCIi7jm6O739g+AEA/MEMDMcAaD96maKqCnp/t1T8/79Ttb2s52mO2/6ufXz3WRSmosy7Ks2UcU3LKsggc+PxdV6MRYlmVZxcUVQASUEpSSQqfHsizLKgBRoBQIgqscRUVViMb5UbzUNVVJxox9Qq5ffu2ya5df/vvabVy7/Nrtp1s+2fTc6O9iTc/l7eQjPVNJ21TTk2m/xZyeqVzDV372SoVIz5Xrl3p6rtxfpvRcm4bJpCfTfjOdhxJOjyihriFCJOrgjq4niEyw3XQ/6Mksv/z3teumW36jbU91vzY900vbVNOT6b1iTk82351Nz81Jz2Q+P930THedEk6PCCAwMJiwbQyWZVnWKGMMx06ct4HBsizLGiVATV3NaFWSlT8Gg+fHae14jSNtL5DyB7mlfhtrl95HRawRmUx1SxEJtMfAcDv7TzzFxYF3qIwtZMPSB5lXtxHHCRc6eaVDDGUVsGyt4Z6PrqHtTDfbf32B7g6XwJ9Z18x0KAVlVZpbt0bY9oGV7Nt9kr07RujvVhhd+sdfaHK+/YJ56/Uhdr7cc33js5UVgyGe7GdvyxO09+zGDxIYDEq5VMcW8Z6Vf0ZT3XpEcllwu7ZRKXc/okB7tHXtYt/JJxlJdmOMRkSIuFUsa/o9Niz9JKFQDMnhPm+e/J236aiqg/s+vYiVaxpxXMEYGB5K8dpvTvHGby/hpZwc77F4jl8E5i9R3P/Z5cydV4WjFIE29PWM8NJPj3Fkv4fRtrIj10SgaWGMZZsdW2LIH0Mi2c9bx7/P2Yvb0cYff0drj/6RVnYd/RZ3Nv9HGmpWo1R2X4U2AZ29b+MHyfGGJUdcmuo24qhQVtsGCHSKtgu72dXyLVL+MJczEmMMCe8Sx9p/hutEWbv4XlwnNqNKQl4Qp7P3IMaM3hiJKCpijdRULC5IkBOB5WtdVq5pJBR+NwBUVUf50CdW43lH2fO7QXwv+8zRGE3XpaMkvcHLCwg5MRpq1uA6kay3Px3KMWzZVs/8hTXj15GrhDmN5Tz06K384DsHOHnERwcz5xqbaWxgyANjDClviN1Hv0d7766rgsK762iGEh28cfQb3NX8V9TXLEfJ9O8CtfZ44+g3GUn2jC+Lhmu4945vZhkYDNpoznXtYe+JfyLlD024TqA9Dp19Bs8fZuOyhwmFymZEycFgGEn28to7fzv+PSlxWL3wY2xe8Qhk8Z1kY91tdSjn+vMXDjv8wb0riURO8NovL6GD7NKntc+BUz+gq//w+LLK2Dzu2fTVggWGSEwxf1HFdTcXIkI44vDwn6/n6ScPcvxgQGCDQ17Y8ljOGRKpfnYd/Q7nenYS6FSGNQ0D8XZ+987/pLPnIGaivs5T2bPRGBOMvzAac10VwdRoHdDauZ2dR/6OkVRPhjUNgU5yrP3fOHLmBXw/kdV+by6DIbji3Onx0kOhDA6kP3+xsjB3f2QVW95XgUiQ1X4M5rrrxlDYY9cBJBLehO+JCOXlER747DoWrVCI2OrvfFAzqMRf9AyGlDfCnpYnaOvegZ5k5hL3etnd8m0u9B0qeIZ0pUB7nLmwk73Hv0egk5P6jDYeh9qeYf/Jf8Hz43lOYek6/NYwqVT6TD8ccfnEJ9dx5z2VOG52wb/YpJKa1uPD6CD9b6GiMsqjf7GZNbeFEVVax18MlJ0GIzeMMYzEu9l56Ouc7d4xVi0xuQv23Wqlr19V111IfpCktXM7e058j4Q3MKWSR6A9Tnb8igOnfoznjzDZ82CNMgZaWzz2vd6GlyY4iIAbUnzo46vYdFcEN1z4ayZXtBZ2v9bN6ZM9aD3xcY1WKynufWgNi1YEiCqd4y8kA/iebwNDLoz2Pupj/8kf0t77JlpPXAzOuA2jGUp0sfvYtznfsx9tsqsiyEagPVo7d/DWySdJpPqZesZu8II4Le0/4+Dpp0l5w1lXk802XlLx0k862PHKaTwvfXCIlYX5xCc3sOV9lTihwl0zOWVgZMDhn79+mHfe6iAIJr52RITq2hiPfHkLqzeEUY69xrIlAiHHXK5Ksid0+gzJ1CA7D/8DrV2vos3Ug8L4lkzAYLydXUe/Re/AqQIEB4PW/nhDczzVm9W2Ap3iSNtzvH366dGuuiUfHExOX37K5eWfdbHn9VaCDNUqkYjDh/9oFe/ZVoZT0Mwxd8dujCFIRfnXJ4/T0d6f9viVEsrKQzz0pxtousVDFaavQMkQhBVL5472SrINDdNlSKQG2X3sO3RdOjhh76Opb9EwnOxi+6H/xXtXfZH59ZtyPM4hw76N4XTHa+w9+TgpfzAn29TGp6X9RUSE9YsfIBKuzMl2i41BE+gRRIKrbrNk/P3Rv801yyZa70pBAl74cQvK0bz3zhVpZ0CORkN8+L5mlGrhjd9eArLvojwVxgT4ZhiR0SOY7DFmPD8GvDh89+9e53Nf2szyVXMn7AYtIsRiIR75ymae/v4RTh624xyyI7a76nQZY0h6A+w8/A06evfm/O5+KN7BrmPf4q7mx5hbuy7vwSHQKc527mJ3yz9OuqF5Kts+eu4FxMDG5X9SsG6Q+SBiqKrTfPBjdWx67xYcpbg6e4OJw0G69yf4X8BxbjwtfqwsxMceaCYIjrB3+/BN6ecvomlYYLj34SUsXjqXaR3fjc6PGBxHZR4bI1BVHeMz/34jT35zP60tARgbHKbLBoZp8oM4e1v+mY6+fTkpKVxrtH99D2+2fJcPbvzPVJbNy/k+xvdlArr6j7L35OP4Okk+qha19jne8QtEhblt+cM3rRSUTyJQ1wif+mIzTQuqUEpNakLZ/KVHcEOKD9+3kqHBAxw7MNr1M38MS9e4PPC5ZmpqYzhOYb9TESESdnnwkbX86InDtJ3QFHoE+0w183+dBdI7cJqz3a9nbGhWEsJ1YhnfDznpB4IZoxkYOceR1hezTm8m2gQcOfPCWJtCmoY+FGGnAkkz4EuQ0ffT/hANKX+E4+0vMjjSmXY/M4njwvotVcxbWD12R1voFI3186+IcO9D64mW5beNyg1rPvAHi6itKyt4ULhMlFBTF2Pb3U0l1433ZiqOb3MGGkl1EwTpByEpcWmq3cSC+jvSrCGUReeweuF9OE4kfXBAc2HgAPnMSLUJ6B1qybAPIRyqonnRAyiZuJApoli14ONURBdkGMFtCIxHPNU74XNLZppQyGF1c1NRTv8Ri4VpbKrK3w7Gqm7mzqsuuic/KiUsuKWOsnI7qeN02cAwTa5TlrY6RInDgvqtbG3+MmWR2gnXES5PvfD7bFnxBSKhmjR7EsojTblJdNo9CNFQfdp3yyJzeP+6/0RT3QZUuiogUVSVL+Df3fZfqK9ak/bciChcFc1NwgtMlBCJFmfmE/gBgwP5G2AogOMoXLf4ugEZA/29w6RSua/inS1sYJim+spl1JQvvW65EpdFc7axtflLxMI13KiOU8Rh+fy72bDkIcLu9T12wm4lzbf84Q23kw2lQqxc8GEcdXWjsCCURRrYuuYvaaxbe8O5nESgMtbEHau/TEPV2utKQUpcGiqbqa5YWJR32aVCa82eN87Q1z39rtMzmZfy2b+7k2TcDnqbLtv4PE3RSA3b1j7GjiN/z8DwWQwBjoqyqP5ONq/6HJFQ1aTnnBFxWLHgHsqjDbzZ8l0SXh9gCLtVrF/8II01q/N6LEoUS5veR/9QK61drxLoBCIO5ZG5bF3zH5hTvXLSE/yJCDUVi7iz+S/ZdfT/0D14DG18HAkxr3YTdzR/CdcpjRLDjQSBHq0yy7baTEaD7mTq8YPA8Nab53j5+T6MLtzdvDEGrQ25GsSvJtErC8BLBfzm58c5sCuOve+dPhsYpkmJQ3XFfD6w4a/pGThOyh+hMjqX+uqVhNyxBudJZggi4DoRFszZTHnsv9I/1IoxAdXli6mtXJyTabNvkAJCbhlbVj3K4rnbGIx3EHJizKlaRXlszpR7EIkoKsuaeP/Gv6ar7xAJ7xKxcC1zqlcRCeWx3rvIvPT821zqdjAmm9KRwXWFW++oZc26BRkbuH0v4MCedn750/N4KVXQdhzf1zz347dIDmd/ExCOGrbdvYAFC+syFpyTSZ/tL59m96sD+J4tkWbDBoYsiDhUxBqpiDXmZHtKudRVLqGucklOtjcVIoLjhGmqW08T63OwPUUkVMGixnSN76Xv4L52LnXVQxaBwXFg7uI4y1auybie72tOtnTz/L+0k0oUvt4/8DX7d3cSxBuy2o4bElZuTNAwd23aoGAM+H7Anp1neeXFnpw8p2K2s4HBsvJGrnhN49PKsHKDy/2fuYNIJP1PVWvDO/vO87MftxVFULhaFkHRhds/UMZH7n8PoVD64wr8gO0vn+blF7rwPZul5YI9i5ZVhERplq01PPToBqKx9D2ftDYcO3SBZ3/YSipenD2kpiMU1qzZFPDR+5sz9nxKJn0OvNnOr57txOjSOf5Cs4GhZAhKHBx5tz1CxC2JEcb5JghKQuM9pUQ5Y+M1ClRPLdC0yOWPP7uWaDSUtl1Ba8ORg508/cRpUvHwtNoURK65bkRQ4ha015hShrWby7n/0824GUoKvhfw+qtn+M1zF21QyDEbGEqEUi63r/oK/tigO4PBVREcZX8wmQgQC9dx15rHuHKensryJgo1lFmAVRtcKquiSJqeOL6nObT/PD/9Qeu0gwKMdiHesORPSKYGgNHrJuTECLsV00x99twQrNlYRThD9Vkq6fPab0/x6kvdtvooD+wZLRFKHBY0bC50MmYgIeTGWNx0Z6ETcpUlK+pJ1zvT9zWnjvfwwo/aSMbDWXWHFVE01WXf2SCXQmGHOY0VE8ZlY0bbFN7c2carP+8rwjaV0mDrGSyrCJ1q6UJPUAzQ2vD23nM89d0TDA+oUphy6jqpVMDFC4PXlYKMGR0b8sovW3jpmU5Siek37FuZ2cBgFTVjNE4oSbTcA8nPzK/Fxhg4flAz0J+4KnM0xnDowHl+8ZPzxIcdSjVT9FNw9O1BPO/qKS28lM8bv2vl1Zf68D1bUsgnW5VkFScZbYS87S6Xez66EcdVDPTHeerxw/RdCJXEJHyZdLUbvv2/d/LFx7ZSXhHFGMPuHad45cV+UonSvp8zRji0N07b2V/ypb+6m0jEJQgMv3j+EPt3pGybwk1gz7BVlFwX7vpQFXd/ZAWRyGgvmcqqCF947FZ+/swpDu2L35QH0RSK1jDUW80/fPVtautDJOI+Q5cEo52SD4oAgQ+XLjTw93+zj+qaMAOXkiRGQnl+voR1mQ0MVlGKlStuf98CIpF3u2sqJdTUlvOJh1bS17OfjrOKwC/t4KATIbraYTb+VLWG+FCI+JABbO+6m6m0y6TWDGVw3BT1cyqv65kiIlRVx/j8X2xi7eYoouwMmpaVazYwWEVIwIQZGUn/7Onyiigf++OVNN8WhUnOYmtZ1uTYwGAVpeHBgCNvX0DrNI8aFaiujvHAZ5pZfVsIxAYHy8oVGxisouT7wq+f76TlcCdBMHGmL0qIlYe5/9NrWbs5imN7MFpWTtjAYBUlo2GgV3j2h2c51dKN703cHUUpoao6yv2fWs3y9Q5uaBZ02bGsPLOBwZoyEUEpNfZycBwHRylEFKIEUaNVPeOvqfyv3v0fRoPDvz5xmjOnezFp+mmKCBVVMT75yHqWrHZQtuRgWVmZfX3grOyIUFFezbKly6ipqRmfhVOJw8p5i2ioiXH5fuPKDkVXZumSYdnl5cKVU9rBnp0dNDSWU1VTljZpFZURHnp0Hc89dYzD+xIYbe97LGs6bGAoEcZovCCB1u9OI6BE4bqxST+v+UaUUixdvJL/8d+/xvp1zYTDofEcXRBEHASV3UwNl6PCNX+LgDOJZ/5WVEb5w4dXMzz4Nq3HrtxYhl0aTcobxoyFIkFQyiXkRif1+ZnNkPLj11w3Dq4bzdl1Y808NjCUCG18Xtn/VRKpS+PLIqFKfm/Tf0NdfgZ1lsrKyvibr3yN927ZTChUnJeOiFBRGeVTX9jI//3Hd2hvzfxAeoNhOHGRVw58DT02rFaJw9J5H2Td4j8q6HMJboZA++w++m16Bk4Ao2GwLNrAtvWPEQvXFDZxVsEU56/bmjJjNEOJTkaS3ePLYkEt2vgZPjU1y1bM5fY7NhVtULhMKaG8IsKtt9fTcaaH4AYT7wXGYyB+bvyuWYlLItXH7JiwTzOc7GIg3ja+TEtwVQnCmn1sJaw1aavXzcv47N1iIiLMX1hbqGftWNYMZmxgsCYvHC7sIx+nwhjDubM9s2LCOcvKJd8ztirJKj3GGIaHkux/oxdtB0Rb1uQJVFSW28Bg5YbWmmTSz1u1vFKC4ypcN3NVljGGvp4RfvT4ITrbNMbYQrFlTZqBQNsSg5UDxhj6+4b4xt++gp+IkesunqJgzlz47Be3UltXmTEdPReH+bdnjnPulLFBwbKmyABHWs7ZwGDlhuf5XOr1CNNELgODCETKk9z74FqqayrSrmcMxEc8nv7+EdpOaLSeGW0hllVUjMGIsoHBKl4iUD3H58HPr+aWpXWoNAPcjIH+vmGe+f5hzp4I7Ihny8qSDQxWURKBylqPT31hDQsXZwoKhqHBJE/90zucOyU2KFhWDtjAYBWlUAQ+/uCSzEFBG/r74vzoiYO0nTQYW31kWTlhA4NVhAyxcmFF85yMJYXui0M8+8OjtJ3AlhQsK4dsYLCKlEc0NvED4I0xJOI+P/l/Rzh7HNvQbFk5Zm+zrCIkBH6Y7q6B60YuG2Po7Rnm8a/v48xxbQewWVYe2MBgFaXEsGHPjk6SSW88OGht6O8d4dkfHOX8advQbFn5YquSrKIUBPD6b/tobzvAtrubWHjLHN58/SRv7Ryipwvb0GxZeWQDg1WUjAEvpTh1OODs8Q7gPEYLfgAYGxQsK59sYLCKmjGCl7rc0DC5J7JZljU9BjBa28BQSkQEEXXlAsRmpJMiqNFzZ8zY1OKz5bzJ2GNZ1VVLrNnrUv+ADQylQonLwvqtpPzh8WWuE0OpUAFTVfwECDvlLG58P9oEY5miUFe5nNkQHJQommpvIxZpGA8I0XA1rhMtcMqsQhARFs2vs4GhVIg4bFn1p2gTXLFM2Qe635AQjdSwtfnLwGh3WBF5twRR4pRyWL/kgSuuG0FEUGKzhtlIgLmNNTYw5JfgqBAht2zC91wnmrNi+2g1kks+50X0PU0yEaCueYqbweClNDO197MgOGriwXSZGG3wxs7JRN+iDiZYWHQEpaZ+3RgDRkMqqUnGrz/QVMqOMZmpRATpvnjRvPm7Abb/pmfsx23lisEQBCkCnZrwfRFFyInNmDvTUFjhhtSEmWAQaJIJn5kaHKZDKSEUVjjOxME9kfBnSHCYHqWEcERNOG1JEGh8XxP4BUiYNS0iMG9RGb9/X4MtMeSTILhOBNeJFDopOeGl9A1uHmZPUIDRAXfJRAnn/DegtSExQWnBmvlm1y/ZsizLuiEbGCzLsqyr2MBgWZZlXcUN/NFGIq1HX5ZlWdbsIwKB1niez/8Hl1cvngploeYAAAAASUVORK5CYII=) **Lane image containing three lanes** #### Change the language of the instructions[​](#change-the-language-of-the-instructions "Direct link to Change the language of the instructions") The texts used in navigation instructions and related classes follow the language set in the SDK. See [the internationalization guide](/docs/android/guides/todo.md) for more details. --- ### Overlays |

An **Overlay** is an additional map layer, either default or user-defined, with data stored on Magic Lane servers, accessible in **online** and **offline** modes, with few exceptions. In order to define overlay data you can use the [Magic Lane Map Studio](https://developer.magiclane.com/documentation/OnlineStudio/guide_creating_a_style.html). It allows uploading data regarding the POIs and their corresponding categories and binary information (via `GeoJSON` format). An **Overlay** can have multiple associated **categories** and **subcategories**. A single item from an overlay is called an **overlay item**. warning If the corresponding map region has been downloaded locally, overlays will not be available in offline mode unless they are downloaded as well. Refer to the [Downloading overlays](/docs/android/guides/todo.md) guide for more details. Most overlay features require a `GemMap` widget and a style containing the overlay to be applied. #### OverlayCategory[​](#overlaycategory "Direct link to OverlayCategory") The `OverlayCategory` class represents hierarchical data related to overlay categories. ##### OverlayCategory structure[​](#overlaycategory-structure "Direct link to OverlayCategory structure") | Method | Description | Type | | -------------------- | --------------------------------------------- | ----------------------------- | | `uid` | Unique ID of the category within the overlay. | Int | | `name` | Name of the category. | String? | | `image` | Image associated with the category. | `Image?` | | `overlayUid` | Parent overlay UID. | Int | | `hasSubcategories()` | Checks if the category has subcategories. | Boolean | | `subcategories` | Gets the subcategories of the category. | `ArrayList?` | ##### Usage[​](#usage "Direct link to Usage") The `OverlayCategory` class provides the category `uid`, which can be utilized in various search functionalities to filter results. Additionally, the `uid` can be used to filter and manage overlay items that trigger alerts within the `AlarmService`. #### OverlayInfo[​](#overlayinfo "Direct link to OverlayInfo") `OverlayInfo` is the class that contains information about an overlay. ##### OverlayInfo structure[​](#overlayinfo-structure "Direct link to OverlayInfo structure") | Method | Description | Type | | ------------------------ | ----------------------------------------------- | ----------------------------- | | `uid` | Unique identifier for the overlay. | Int | | `name` | Name of the overlay. | String? | | `categories` | List of categories associated with the overlay. | `ArrayList?` | | `image` | Image associated with the overlay. | `Image?` | | `getCategory(id: Int)` | Gets the overlay category by its ID. | `OverlayCategory?` | | `hasCategories(id: Int)` | Checks if a category has subcategories. | Boolean | ##### Usage[​](#usage-1 "Direct link to Usage") The primary function of the `OverlayInfo` class is to provide the categories within an overlay. It also provides the `uid` of the overlay which can be used to filter the results of search and also can be used to toggle the visibility of the overlay on the map. Other fields can be displayed on the UI. #### OverlayItem[​](#overlayitem "Direct link to OverlayItem") An **OverlayItem** represents a single item within an overlay. It contains specific information about that item but also about the overlay it is part of. ##### OverlayItem structure[​](#overlayitem-structure "Direct link to OverlayItem structure") | Method | Description | Type | | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------- | | `uid` | The unique ID of the item within the overlay. | Int | | `categoryId` | This item's category ID. | Int | | `image` | This item's image. | `Image?` | | `overlayInfo` | The parent overlay information. | `OverlayInfo` | | `coordinates` | This item's coordinates. | `Coordinates` | | `overlayUid` | The parent overlay UID. | Int | | `getPreviewData()` | Returns a ParameterList with OverlayItem-specific data. | `ParameterList` | | `getPreviewData(previewFormat: EPreviewDataType)` | Returns a string with OverlayItem-specific data based on the specified preview format. | String | | `getPreviewUrl()` | Returns the preview URL, which can be opened in a web browser to present more details about this item. | String | | `hasPreviewExtendedData()` | Checks if this type of OverlayItem has preview extended data (dynamic data that needs to be downloaded). | Boolean | | `getPreviewExtendedData(paramList: GemList, progressListener: () -> Unit)` | Asynchronously retrieves OverlayItem preview extended data as a parameter list. A progress listener is mandatory. | Unit | | `cancelGetPreviewExtendedData()` | Cancels an asynchronous retrieval of OverlayItem preview extended data. | Unit | | `assign(overlayItem: OverlayItem)` | Assigns the properties of another OverlayItem to this one. | Unit | ##### Usage[​](#usage-2 "Direct link to Usage") `OverlayItems` can be selected from the map or be provided by the `AlarmService` on approach. Other fields and information can be displayed on the UI. #### Classification[​](#classification "Direct link to Classification") `ECommonOverlayIds` is an enumeration that contains overlay IDs of the predefined overlays. You can access the overlay's ID by using the `value` property. ##### ECommonOverlayIds structure[​](#ecommonoverlayids-structure "Direct link to ECommonOverlayIds structure") | Overlay Name | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------ | | `Safety` | Overlay for safety-related information. | | `SocialLabels` | Overlay for social labels, including user-generated tags or annotations on the map. **These are yet to be implemented.** | | `SocialReports` | Overlay for social reports, such as user-submitted incidents or events. | | `PublicTransport` | Overlay for public transport information, including routes, stops, and schedules. | Tip You can define your own overlay data at [Magic Lane Map Studio](https://developer.magiclane.com/documentation/OnlineStudio/guide_creating_a_style.html). ###### Safety overlay items examples:[​](#safety-overlay-items-examples "Direct link to Safety overlay items examples:") ![](/docs/android/assets/images/example_speed_limit-a97c79c826062eb46fc71b07ae0c7237.png) **Speed limit overlay item** ![](/docs/android/assets/images/example_red_light_control-1588c5ecc8ee6e30188c85d80439f172.png) **Red light control overlay item** ###### Public Transport overlay items example:[​](#public-transport-overlay-items-example "Direct link to Public Transport overlay items example:") ![](/docs/android/assets/images/example_bus_station_overlay-68e85e6389e9502288a8f15df1805e98.png) **Bus station overlay items** ![](/docs/android/assets/images/example_train_stations_overlay-85e6798219f58de98b108648cb8c52c3.png) **Train station overlay items** ###### Social reports overlay items examples:[​](#social-reports-overlay-items-examples "Direct link to Social reports overlay items examples:") ![](/docs/android/assets/images/example_constructions_overlay-5db7f219b17edd3193e581b8c3bbc38f.png) **Constructions overlay item** ![](/docs/android/assets/images/example_fixed_camera_overlay-c3cadf31ce8682122a5d896c817c944b.png) **Fixed camera overlay item** #### Interaction with overlays[​](#interaction-with-overlays "Direct link to Interaction with overlays") ##### OverlayService[​](#overlayservice "Direct link to OverlayService") The `OverlayService` is a key component for managing overlays in the Maps SDK. It provides methods to retrieve, enable, disable, and manage overlay data, both online and offline. ###### Retrieving overlay info with OverlayService[​](#retrieving-overlay-info-with-overlayservice "Direct link to Retrieving overlay info with OverlayService") You can retrieve the list of all available overlays for the current map style using the `getAvailableOverlays(onCompleted: (errorCode: Int, hint: String?) -> Unit)` method from the `OverlayService`. This method returns a `Pair?, Boolean>?`. The list of `OverlayInfo` objects represents the available overlays, and the `Boolean` indicates that some information is not available and will be downloaded when network is available. If not all overlay information is available onboard, a notification will be sent when it will be downloaded via a call to the lambda provided as a parameter to the `getAvailableOverlays()` method. * Kotlin * Java ```kotlin val (availableOverlaysList, isDataAvailable) = service.getAvailableOverlays(onCompleted = { errorCode, hint -> if (GemError.isError(errorCode)) { // handle error } else { //handle data } }) ?: Pair(null, null) if (isDataAvailable == null) { //handle unexpected error } ``` ```java Pair, Boolean> result = service.getAvailableOverlays((errorCode, hint) -> { if (GemError.isError(errorCode)) { // handle error } else { //handle data } }); ArrayList availableOverlaysList = result != null ? result.first : null; Boolean isDataAvailable = result != null ? result.second : null; if (isDataAvailable == null) { //handle unexpected error } ``` To get overlay info for a specific overlay you can use the `getOverlayInfo(overlayUid: ECommonOverlayId, onCompleted: (errorCode: Int, hint: String?) -> Unit)` method which returns a `Pair` object. The `Boolean` indicates that some information is not available and will be downloaded when network is available. If not all overlays info is available onboard a notification will be sent when it will be downloaded via a call to the lambda provided as a parameter to the `getOverlayInfo()` method. * Kotlin * Java ```kotlin val (overlayInfo, isDataAvailable) = service.getOverlayInfo(overlayUid, onCompleted = { errorCode, hint -> if (GemError.isError(errorCode)) { // handle error } else { //handle data } }) ?: Pair(null, null) if (isDataAvailable == null) { //handle unexpected error } ``` ```java Pair result = service.getOverlayInfo(overlayUid, (errorCode, hint) -> { if (GemError.isError(errorCode)) { // handle error } else { //handle data } }); OverlayInfo overlayInfo = result != null ? result.first : null; Boolean isDataAvailable = result != null ? result.second : null; if (isDataAvailable == null) { //handle unexpected error } ``` To get overlay info for a group of specific overlays you can use the `getOverlaysInfo( ArrayList, onCompleted: (errorCode: Int, hint: String?) -> Unit)` method which returns a `Pair` object. The `Boolean` indicates that some information is not available and will be downloaded when network is available. If not all overlays info is available onboard a notification will be sent when it will be downloaded via a call to the lambda provided as a parameter to the `getOverlayInfo()` method. * Kotlin * Java ```kotlin val overlayUids = arrayListOf(ECommonOverlayId.Safety, ECommonOverlayId.PublicTransport) val (overlaysInfo, isDataAvailable) = service.getOverlaysInfo(overlayUids, onCompleted = { errorCode, hint -> if (GemError.isError(errorCode)) { // handle error } else { //handle data } }) ?: Pair(null, null) if (isDataAvailable == null) { //handle unexpected error } ``` ```java ArrayList overlayUids = new ArrayList<>(); overlayUids.add(ECommonOverlayId.Safety); overlayUids.add(ECommonOverlayId.PublicTransport); Pair, Boolean> result = service.getOverlaysInfo(overlayUids, (errorCode, hint) -> { if (GemError.isError(errorCode)) { // handle error } else { //handle data } }); ArrayList overlaysInfo = result != null ? result.first : null; Boolean isDataAvailable = result != null ? result.second : null; if (isDataAvailable == null) { //handle unexpected error } ``` ###### Enabling and disabling overlays[​](#enabling-and-disabling-overlays "Direct link to Enabling and disabling overlays") You can enable or disable overlays on the map using the `enableOverlay(overlayUid: Int, categoryID: Int)` method and `disableOverlay(overlayUid: Int, categoryID: Int)` from the `OverlayService`. Check whether an overlay is enabled or disabled using the `isOverlayEnabled(overlayUid: Int, categoryID: Int)` method. * Kotlin * Java ```kotlin val service = GemSdk.getOverlayService() val overlayUid = ECommonOverlayId.Safety.value val categoryID = -1 // Use -1 to refer to the entire overlay, a specific category ID to refer to a category within the overlay // Enable an overlay val errorCodeWhileEnabling = service.enableOverlay(overlayUid, categoryID) // Disable an overlay val errorCodeWhileDisabling = service.disableOverlay(overlayUid, categoryID) // Check if an overlay is enabled val isEnabled = service.isOverlayEnabled(overlayUid, categoryID) ``` ```java OverlayService service = GemSdk.getOverlayService(); int overlayUid = ECommonOverlayId.Safety.getValue(); int categoryID = -1; // Use -1 to refer to the entire overlay, a specific category ID to refer to a category within the overlay // Enable an overlay int errorCodeWhileEnabling = service.enableOverlay(overlayUid, categoryID); // Disable an overlay int errorCodeWhileDisabling = service.disableOverlay(overlayUid, categoryID); // Check if an overlay is enabled boolean isEnabled = service.isOverlayEnabled(overlayUid, categoryID); ``` ###### Retrieving overlay data offline[​](#retrieving-overlay-data-offline "Direct link to Retrieving overlay data offline") The **offline data grabber** downloads an overlay covering dataset for every new downloaded road map content. The offline data is automatically grabbed immediately after a road map content download finishes and is stored in the SDK's permanent cache. To enable or disable the overlay data grabber, use the following methods from the `OverlayService`: `enableOverlayOfflineDataGrabber(overlayUid: Int)` and `disableOverlayOfflineDataGrabber(overlayUid: Int)`. You can check if the offline data grabber is enabled using the `isOfflineDataGrabberEnabled(overlayUid: Int)` method. * Kotlin * Java ```kotlin val service = OverlayService() val isEnabled = service.isOverlayOfflineDataGrabberEnabled(overlayId) val errorCodeWhileEnabling = service.enableOverlayOfflineDataGrabber(overlayId) val errorCodeWhileDisabling = service.disableOverlayOfflineDataGrabber(overlayId) ``` ```java OverlayService service = new OverlayService(); boolean isEnabled = service.isOverlayOfflineDataGrabberEnabled(overlayId); int errorCodeWhileEnabling = service.enableOverlayOfflineDataGrabber(overlayId); int errorCodeWhileDisabling = service.disableOverlayOfflineDataGrabber(overlayId); ``` danger Avoid confusing the uid of `OverlayInfo`, `OverlayCategory`, and `OverlayItem`, as they each serve distinct purposes. ##### Selecting overlay items[​](#selecting-overlay-items "Direct link to Selecting overlay items") Overlay items are selectable. When a user taps or clicks, you can identify specific overlay items programmatically (e.g., through the function `cursorSelectionOverlayItems()`). Please refer to the [Map Selection Functionality](/docs/android/guides/maps/interact-with-map.md#map-selection-functionality) guide for more details. ##### Searching overlay items[​](#searching-overlay-items "Direct link to Searching overlay items") Overlays are searchable in multiple ways, typically by setting the appropriate properties in the search preferences during a regular search. More details can be found within the [Get started with Search](/docs/android/guides/search/get-started-search.md) guide. ##### Calculating route with overlay items[​](#calculating-route-with-overlay-items "Direct link to Calculating route with overlay items") Overlay items are **not** designed for route calculation and navigation. Tip Create a new landmark using the overlay item's relevant coordinates and a representative name, then utilize this object for routing purposes. ##### Displaying overlay item information[​](#displaying-overlay-item-information "Direct link to Displaying overlay item information") Overlay items can contain additional information that can be displayed to the user. This information can be accessed using the `getPreviewData(previewFormat: EPreviewDataType)` or `getPreviewData()` and `getPreviewUrl()` methods from the `OverlayItem` class. The `getPreviewData(previewFormat: EPreviewDataType)` method returns a string with overlay item-specific data based on the specified preview format, while `getPreviewData()` gets the same data as a `ParameterList` .The `getPreviewUrl()` method returns a URL that can be opened in a web browser to present more details about the item. Here is an example of how to display overlay item information: * Kotlin * Java ```kotlin overlayItem.getPreviewData()?.let { parameters -> var imageBitmap = ImageBitmap(1, 1) overlayItem.image?.let { image -> GemUtilImages.asBitmap( img = image, width = (overlayImageSize * (image.aspectRatio!!.width / image.aspectRatio!!.height)).toInt(), height = overlayImageSize )?.let { bmp -> imageBitmap = bmp.asImageBitmap() } } for (parameter in parameters) { when (parameter.name) { "speedValue" -> titleText = parameter.value as String "speedUnit" -> descriptionText = parameter.value as String // other parameters... } } } ``` ```java ParameterList parameters = overlayItem.getPreviewData(); if (parameters != null) { ImageBitmap imageBitmap = new ImageBitmap(1, 1); Image image = overlayItem.getImage(); if (image != null) { Bitmap bmp = GemUtilImages.asBitmap( image, (int) (overlayImageSize * (image.getAspectRatio().getWidth() / image.getAspectRatio().getHeight())), overlayImageSize ); if (bmp != null) { imageBitmap = bmp; // Convert as needed for your UI framework } } for (Parameter parameter : parameters) { switch (parameter.getName()) { case "speedValue": titleText = (String) parameter.getValue(); break; case "speedUnit": descriptionText = (String) parameter.getValue(); break; // other parameters... } } } ``` danger The `previewData` returned is not available if the parent map tile is disposed. Please get the preview data before further interactions with the map. ##### Get notifications when approaching overlay items[​](#get-notifications-when-approaching-overlay-items "Direct link to Get notifications when approaching overlay items") Alarms can be configured to notify users when they approach specific overlay items from selected overlays. See the [Landmarks and overlay alarms](/docs/android/guides/todo.md) guide for more details about implementing this feature. ##### Highlight overlay items on the map[​](#highlight-overlay-items-on-the-map "Direct link to Highlight overlay items on the map") You can't directly highlight overlay items on the map, but you can create a `Landmark` filled with relevant information and an image to represent the overlay item on the map. * Kotlin * Java ```kotlin fun highlightPlace(coordinates: Coordinates, image: Image, mapView: MapView) { val landmark = Landmark() landmark.coordinates = coordinates landmark.image = image mapView.centerOnCoordinates(coordinates) val displaySettings = HighlightRenderSettings() displaySettings.setOptions(EHighlightOptions.ShowLandmark.value or EHighlightOptions.Overlap.value) mapView.activateHighlightLandmarks(landmark, displaySettings) } ``` ```java void highlightPlace(Coordinates coordinates, Image image, MapView mapView) { Landmark landmark = new Landmark(); landmark.setCoordinates(coordinates); landmark.setImage(image); mapView.centerOnCoordinates(coordinates); HighlightRenderSettings displaySettings = new HighlightRenderSettings(); displaySettings.setOptions(EHighlightOptions.ShowLandmark.getValue() | EHighlightOptions.Overlap.getValue()); mapView.activateHighlightLandmarks(landmark, displaySettings); } ``` --- ### Positions |

The `PositionData` and `ImprovedPostionData` classes provide a comprehensive representation of geographical and movement data for GPS-based systems. They include details like coordinates, speed, altitude, direction, and accuracy, along with road-related metadata such as speed limits and modifiers. With robust support for position quality assessment and timestamped data, it is well-suited for navigation and sensor-driven applications. tip Do not confuse `Coordinates` with `Position`. The `Coordinates` class represents a geographic location using latitude, longitude, and altitude, and is used widely throughout the SDK. In contrast, `PositionData` and `ImprovedPostionData` carry additional sensor data and are primarily used to represent the user's current location and movement. #### Instantiating Positions[​](#instantiating-positions "Direct link to Instantiating Positions") The `PositionData` class can be instantiated using the provided methods within the `SenseDataFactory` class. Additionally, it can be accessed through the methods exposed by the Maps SDK for Android. For more details, refer to the [Get Started with Positioning](/docs/android/guides/positioning/get-started-positioning.md) guide. #### Raw position data[​](#raw-position-data "Direct link to Raw position data") Raw position data represents unprocessed data from the GPS sensors of devices. It provides basic information. It corresponds to the `PositionData` interface. #### Map matched position data[​](#map-matched-position-data "Direct link to Map matched position data") Map matching is a method in location-based services that aligns raw GPS data with a digital map, correcting inaccuracies by snapping the position to the nearest logical location, such as roads. It corresponds with the `ImprovedPostionData` interface. #### Raw position data vs map matched position data[​](#raw-position-data-vs-map-matched-position-data "Direct link to Raw position data vs map matched position data") The Map Matched positions provide more information, as it can be seen in the table below: | Attribute | Raw | Map Matched | When is available | Description | | -------------------- | --- | ----------- | --------------------- | ------------------------------------------------------------------------------------------------------------------- | | acquisitionTimestamp | ✅ | ✅ | always | The system time when the data was collected from sensors. | | satelliteTime | ✅ | ✅ | always | The satellite timestamp when position was collected by the sensors. | | provider | ✅ | ✅ | always | The provider type (GPS, network, unknown) | | latitude & longitude | ✅ | ✅ | hasCoordinates | The latitude and longitude at the position in degrees | | altitude | ✅ | ✅ | hasAltitude | The altitude at the given position. Might be negative | | speed | ✅ | ✅ | hasSpeed | The current speed (always non-negative) | | speedAccuracy | ✅ | ✅ | hasSpeedAccuracy | The current speed accuracy (always non-negative). Typical accuracy is 2 m/s in good conditions | | course | ✅ | ✅ | hasCourse | The current direction of movement in degrees (0 north, 90 east, 180 south, 270 west) | | courseAccuracy | ✅ | ✅ | hasCourseAccuracy | The current heading accuracy is degrees (typical accuracy is 25 degrees) | | horizontalAccuracy | ✅ | ✅ | hasHorizontalAccuracy | The horizontal accuracy in meters. Always positive. (typical accuracy 5-20 meters) | | verticalAccuracy | ✅ | ✅ | hasVerticalAccuracy | The vertical accuracy in meters. Always positive. | | fixQuality | ✅ | ✅ | always | The accuracy quality (inertial – based on extrapolation, low – inaccurate, high – good accuracy, invalid – unknown) | | coordinates | ✅ | ✅ | hasCoordinates | The coordinates of the position | | roadModifier | ❌ | ✅ | hasRoadLocalization | The road modifiers (such as tunnel, bridge, ramp, etc.) | | roadSpeedLimit | ❌ | ✅ | always | The speed limit on the current road in m/s. It is 0 if no speedLimit information is available | note The `roadSpeedLimit` field may not always have a value, even if the position is map matched. This can happen if data is unavailable for the current road segment or if the position is not on a road. In such cases, the `roadSpeedLimit` field will be set to 0. tip One common use case for `speed` and `roadSpeedLimit` is to check if a user is exceeding the legal speed limit. The `AlarmService` class offers a reliable solution for this scenario. Refer to the [speed warnings guide](/docs/android/guides/todo.md) for more details. --- ### Routes |

A Route usually represents a navigable path between two or more landmarks (waypoints). It includes data such as distance, estimated time, and navigation instructions. Routes can be computed in different ways: * Based on 2 or more intermediary landmarks (waypoints) - route is navigable. * Over-track routes (based on a predefined `path`, which could come from a GPX file but is not limited to this) are navigable. * Route ranges: These routes are **not** navigable and do not have segments or instructions. A navigable route consists of one or more segments. Each segment represents the portion of the route between two consecutive waypoints and includes its own set of route instructions. #### Instantiating Routes[​](#instantiating-routes "Direct link to Instantiating Routes") Routes cannot be instantiated directly. Instead, they must be computed based on a predefined list of landmarks. For detailed guidance on how to calculate routes, refer to the [Getting Started with Routing Guide](/docs/android/guides/routing/get-started-routing.md). danger Calculating a route does not automatically display it on the map. Refer to the [Display markers guide](/docs/android/guides/maps/display-map-items/display-markers.md) for detailed instructions on how to display one or more routes. #### Route specializations[​](#route-specializations "Direct link to Route specializations") There are 2 special types of routes: 1. **Normal Routes** - Standard routes computed for typical navigation scenarios. 2. **Public Transport (PT) Routes** - Routes calculated using a public transport mode, including additional details such as service frequency, ticket purchase information, and other transit-specific data. ##### Specific Classes[​](#specific-classes "Direct link to Specific Classes") Each route type is associated with specific classes that offer functionality suited to its requirements: | **Route Type** | **Route Class** | **Segment Class** | **Instruction Class** | | ---------------------- | --------------- | ----------------- | --------------------- | | Normal Route | `Route` | `RouteSegment` | `RouteInstruction` | | Public Transport Route | `PTRoute` | `PTRouteSegment` | `PTRouteInstruction` | #### Route structure[​](#route-structure "Direct link to Route structure") | Field | Type | Explanation | | ------------------------------------------------------------------------------------ | ----------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `dominantRoads` | ArrayList\? | Dominant road names. If a road has multiple names, they will be presented as "name1 / name2 / ... / namex". | | `extraInfo` | `ParameterList?` | Direct access to the extra info attached to this route. | | `preferences` | `RoutePreferences?` | Route preferences. | | `routeListener` | `IRouteListener?` | Route-related events listener. | | `segments` | `ArrayList?` | Route segments. | | `status` | `ERouteStatus` | Route status. | | `summary` | String? | Summary of the route segment. | | `hasTollRoads()` | Boolean | Returns true if the route contains toll roads. | | `hasIncursCosts()` | Boolean | Checks if traveling the route or route segment incurs cost to the user. | | `hasFerryConnections()` | Boolean | Returns true if the route contains ferry connections. | | `isPTRoute()` | Boolean | Returns true if the route is a Public Transport route. | | `toPTRoute()` | `PTRoute?` | Convert route to a Public Transport route. | | `getTimeDistance(activePart = true)` | `TimeDistance?` | If activePart is true, returns only the active part of the route metrics; if false, returns whole route metrics. | | `getCoordinateOnRoute(distance : Int)` | `Coordinates?` | Gets a coordinate on the route at the given distance. | | `getDistanceOnRoute(coordinates :Coordinates, activePart : Boolean)` | Int | Gets route distance from departure at the given coordinate. | | `getClosestSegment(coordinates :Coordinates)` | Int | Gets the index of the closest route segment to the given coordinates. | | `getPath()` | `Path?` | Builds a path from the route start to the end segment. | | `getTimeDistanceCoordinates(start : Int, step : Int, end : Int, stepType : Boolean)` | `TimeDistanceCoordinateList?` | Builds a list of timestamp coordinates from a route. | #### Route segment structure[​](#route-segment-structure "Direct link to Route segment structure") A route segment represents the portion of a route between two consecutive waypoints. For public transport routes, segments are further categorized as either pedestrian sections or public transit sections, depending on the mode of travel within that segment. | Method | Return Type | Description | | ---------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `geographicArea` | RectangleGeographicArea? | Geographic area of the route. The geographic area is the smallest rectangle that can be drawn around the route. | | `instructions` | `ArrayList?` | Get route instructions list. | | `summary` | String? | Summary of the route segment. | | `timeDistance` | `TimeDistance?` | Length in meters and estimated travel time in seconds for the route / route segment. | | `waypoints` | `LandmarkList?` | List of waypoints. The waypoints are ordered like: departure, first waypoint, ..., destination. Only the route can have intermediate waypoints. The segments have only departure and destination. | | `hasIncursCosts()` | Boolean | Method to check if traveling the route or route segment incurs cost to the user. | | `isCommon()` | Boolean | Check if this segment is of common type. | | `isPublicTransportSegment()` | Boolean | Check if this segment is public transport type. | | `toPTRouteSegment()` | `PTRouteSegment?` | Convert to a PTRouteSegment. | #### Route instruction structure[​](#route-instruction-structure "Direct link to Route instruction structure") A `RouteInstruction` provides detailed guidance for navigation, offering various methods to retrieve specific information about each maneuver the user needs to make, such as coordinates, turn directions, distances and time to next waypoints. | Method | Return Type | Description | | ------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------- | | `countryCodeISO` | String? | Country code in ISO format. | | `coordinates` | `Coordinates?` | Coordinates of the instruction. | | `turnDetails` | `TurnDetails?` | Turn details. | | `turnInstruction` | String? | Turn instruction text. | | `turnImage` | `Image?` | The image for the turn. The API client should check if the image is valid. | | `followRoadInstruction` | String? | Follow road instruction text. | | `roadInfo` | `ArrayList?` | Road information list. | | `roadInfoImage` | `RoadInfoImage?` | Road info image. | | `signpostDetails` | `SignpostDetails?` | Signpost details. | | `signpostInstruction` | String? | Signpost instruction text. | | `realisticNextTurnImage` | `AbstractGeometryImage?` | Image for the realistic next turn. The API client should check if the image is valid. | | `timeDistanceToNextTurn` | `TimeDistance?` | Time/distance to next turn. | | `remainingTravelTimeDistance` | `TimeDistance?` | Remaining travel time/distance. | | `remainingTravelTimeDistanceToNextWaypoint` | `TimeDistance?` | Remaining time/distance to next waypoint. | | `traveledTimeDistance` | `TimeDistance?` | Traveled time/distance. | | `hasTurnInfo()` | Boolean | Check if turn information is available. | | `hasFollowRoadInfo()` | Boolean | Check if follow road information is available. | | `hasSignpostInfo()` | Boolean | Check if signpost information is available. | | `hasRoadInfo` | Boolean | Check if road information is available. | | `isFerry()` | Boolean | Check if this is a ferry. | | `isTollRoad()` | Boolean | Check if this is a toll road. | | `isCommon()` | Boolean | Check if this instruction is of common type. | | `toPTRouteInstruction()` | `PTRouteInstruction?` | Casts this Route instruction to a Public transport instruction. | #### Other classes[​](#other-classes "Direct link to Other classes") ##### Time distance[​](#time-distance "Direct link to Time distance") The TimeDistance class is used to get details about the time and distance to/from certain important points of interests. The TimeDistance class differentiates between unrestricted and restricted road portions. Restricted segments refer to non-public roads, while unrestricted represent publicly accessible roads: | Property/Method | Type | Explanation | | ------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ------------------------------------------------------------------------------------------ | | `totalTime` | `Int` | Total time in seconds (sum of unrestricted and restricted times). | | `restrictedTime` | `Int` | Restricted time in seconds. | | `restrictedBeginEndRatio` | `Double` | Ratio representing the division of restricted time/distance between the begin and the end. | | `restrictedDistance` | `Int` | Restricted distance in meters. | | `restrictedDistanceAtBegin` | `Int` | Restricted distance allocated to the beginning, based on the ratio. | | `restrictedDistanceAtEnd` | `Int` | Restricted distance allocated to the end, based on the ratio. | | `restrictedTimeAtBegin` | `Int` | Restricted time allocated to the beginning, based on the ratio. | | `restrictedTimeAtEnd` | `Int` | Restricted time allocated to the end, based on the ratio. | | `totalDistance` | `Int` | Total distance in meters (sum of unrestricted and restricted distances). | | `unrestrictedDistance` | `Int` | Unrestricted distance in meters. | | `hasRestrictedBeginEndDifferentiation()` | `Boolean` | Indicates if the begin and end have differentiated restricted values based on the ratio. | | `assign(unrestrictedTimeS : Int, unrestrictedDistanceM : Int, restrictedTimeS : Int, restrictedDistanceM : Int, ndBeginEndRatio : Double)` | `Unit` | Assigns values to the TimeDistance object. | | `isEmpty()` | `Boolean` | Indicates whether the total time is zero. | ##### Signpost details[​](#signpost-details "Direct link to Signpost details") Signposts near roadways typically indicate intersections and directions to various destinations. The SDK provides realistic renderings of these signposts, along with relevant supplementary information. Below is an example of a rendered signpost details image: ![](/docs/android/assets/images/signpost_image-eeff2259d472a997f59b32830e683611.png) **Signpost image captured during highway navigation**
The `SignpostDetails` class also provides properties such as: | Property/Method | Type | Description | | ---------------------- | -------------------------- | ------------------------------------------------------------------------- | | `image` | `SignpostImage?` | Returns the signpost image representation. | | `hasBackgroundColor()` | `Boolean` | Indicates whether the signpost has a background color. | | `backgroundColor` | `Rgba?` | Retrieves the background color of the signpost. | | `hasBorderColor()` | `Boolean` | Indicates whether the signpost has a border color. | | `borderColor` | `Rgba?` | Retrieves the border color of the signpost. | | `hasTextColor()` | `Boolean` | Indicates whether the signpost has a text color. | | `textColor` | `Rgba?` | Retrieves the text color of the signpost. | | `items` | `ArrayList?` | Retrieves a list of `SignpostItem` elements associated with the signpost. | ###### Signpost item[​](#signpost-item "Direct link to Signpost item") Each `SignpostItem` has the following properties: | Property/Method | Type | Description | | ---------------------- | ------------------------ | -------------------------------------------------------------------------------------- | | `column` | `Int` | Retrieves the one-based column of the item. Zero indicates not applicable. | | `connectionInfo` | `SignpostConnectionInfo` | Retrieves the connection type of the item (branch, towards, exit, invalid) | | `phoneme` | `String` | Retrieves the phoneme assigned to the item if available, otherwise empty. | | `pictogramType` | `SignpostPictogramType` | Retrieves the pictogram type for the item (airport, busStation, parkingFacility, etc). | | `row` | `Int` | Retrieves the one-based row of the item. Zero indicates not applicable. | | `shieldType` | `RoadShieldType` | Retrieves the shield type for the item (county, state, federal, interstate, etc) | | `text` | `String` | Retrieves the text assigned to the item, if available. | | `type` | `SignpostItemType` | Retrieves the type of the item (placeName, routeNumber, routeName, etc). | | `hasAmbiguity()` | `Boolean` | Indicates if the item has ambiguity. Avoid using such items for TTS. | | `hasSameShieldLevel()` | `Boolean` | Indicates if the road code item has the same shield level as its road. | ##### Turn Details[​](#turn-details "Direct link to Turn Details") The `TurnDetails` class provides details such as: | Property | Type | Description | | ----------------------- | ------------------------ | --------------------------------------------------------- | | `abstractGeometry` | `AbstractGeometry?` | Returns the abstract geometry representation of the turn. | | `abstractGeometryImage` | `AbstractGeometryImage?` | Returns the abstract geometry image for the turn. | | `event` | `ETurnEvent` | Returns the turn event type. | | `roundaboutExitNumber` | `Int` | Returns the exit number for roundabout turns. | ###### Turn details abstract image vs. turn image:[​](#turn-details-abstract-image-vs-turn-image "Direct link to Turn details abstract image vs. turn image:") The difference between images obtained via `abstractGeometryImg` (left) and `turnImage` (right) is shown below: ![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAAVyklEQVR4nO3deXAUVbvH8WcmCTsKBBEJGlBRpISLLIpUUHaIslxBsYSobIoUm6UCllVixD9A61VAEAt5Ay+vvCAaFgXZ7o0CsojAlX03BgUhgGFLWEzI3DoNQTLpyTLTPdNn5vupSkmlJ0nTDr/0OX3O84gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgM1cdv8A+C85Odnj1OuXnJzMewdB5w7+jwQA/xBYALRBYAHQBoEFQBsEFgBtEFgAtEFgAdAGgQVAGwQWAG0QWAC0QWAB0AaBBUAbBBYAbRBYALRBYAHQBoEFQBsEFgBtEFgAtEFgAdAGgQVAGwQWAG0QWAC0QWAB0AaBBUAbBBYAbRBYALRBYAHQBoEFQBsEFgBtEFgAtEFgAdAGgQVAGwQWAG0QWAC0QWAB0AaBBUAbBBYAbRBYALRBYAHQBoEFQBsEFgBtEFgAtEFgAdAGgQVAG9GhPgEgDHhKOO4K0nmEPe6wAHvDquA1pXkdSkBgAf4rawgRXAEisAALw6p69epStWpVv74WJSOwgLIrEjjdunWT9PR0ycrKkvPnz8vBgwfl/fffl/j4+OK+B8FVRgQWEKBWrVrJokWLpH79+jc+16BBAxkzZowcPnxYpk6dKjVq1PD15YRWGRBYQIDGjRsnMTExpseio6Nl+PDhcuDAAenVq5evb0FolRKBBQSgUqVK0qFDhxJfV7NmTVm4cKHMnDnT+BoThFYpEFhA2RQKloYNG0q5cuVK/cWDBw+WtWvXSlxcXInfG0URWEAAYmNjy/w1LVq0kJ9++sn4rwlCqxgEFhAAl8u/Rex16tSRNWvWSKdOncwOE1o+EFhAAC5evOj311auXFm++eYb6dmzp9lhQssEgQUE4NixYwFdvwoVKhiT8QMHDjQ7zFotLwQWUDaFxoBHjx6Vy5cvB3QNo6KijKeHI0aM8PUS7rauI7CAAOTm5sr27dsDvoZut1s+/vhjmThxoq+XeAL+IWGAwAICtGnTJsuu4dixY2XatGlGgJnwSIQjsAAHBZYybNgwmTNnjrFK3oRHIhiBBQRo48aNll/DpKQkWbBggZQvX97ssEciFIEFBDjxrp4U7tu3z/LrqPYerlixwle5Go9EIAILsIAKFju0a9dO0tLSfK2o90iEIbAAC6xcudK269iyZUtZt24d+w8JLMCaYaEKlOzsbNsuZ6NGjeSHH36Qe++9N6LvtLjDAixw5coVY2+gnerXr28EY+PGjSM2tAgswOHzWDe744475LvvvjOGiZEYWgQWoFFgFRQDVKHVvn17ibTQIrAAi+axfv31V6P5RDBUqVJFli5dKl26dImo0CKwAA3vshRValmVp/FRKz4sQ4vAAjRZ3mBGlWf+8ssv5cUXX4yI0CKwAAupJ4WBFPXztzzNrFmzjHrx4V5Ti8ACLJzHUrWxvv/++6BfU7fbLTNmzJCRI0f6eklYhBaBBVhMNVUNBbfbLVOmTAnrmloEFmCxxYsXy19//RWy6zp27NiwDS0CC7B4WHjmzBnbV72XJrSmT58edoUACSzABqqxRKgNHTo07AoBEliATfNYeXl5Ib+2SUlJMnfuXImJiQmL0CKwABuGhadPnzY2KjvBs88+a8yrqZZiuocWgQWE8bCwwJNPPmmswldbenQOLQILsDGwrl696pjr27ZtWyO0brnlFm1Di8ACbBoWZmZmyoYNGxx1fRMSEoxKD6rig46hRWABNkpNTXXc9W3evLmsXbtW6tSpo11oEViAjb766ivJz8933DVu1KiRsYXozjvv1Cq0CCzAxmHhiRMnLG+0apX77rtPuzrxBBYQQU8LvcXHxxuhpUudeAILiNBhYYHatWsbW4l0qBNPYAE2DwuPHj0qW7ZscfR1rlGjhqxevVpat27t6NAisIAIHxYWqFatmhFaHTt2FKeGFoEFBGl5g5OHhQUqV65s1IlPTEwUJ4aW6TZuhFeDT1WbSX2oPyOow0LPzR11fv75Z2MNlNNVrFhRlixZIn379jW7M/R4D3mDicAKI+fOnZOMjAw5cuSIsflWfdhYXzzkv211vMvSIbAKmlssWLBABg0aZJSocUpohSwpUbLk5OQSQ+Hs2bOyc+dO2bVrl5w6dYrL6mB33323/PLLL6KT/Px8efnllyUlJcUR+cEdlqZ+//13Y/3MoUOHxOPhZkcH6enpsn37dmnatKnowu12y2effSZVq1aVyZMnex/2BDu4CCzNqA21q1atMt780I+aE9IpsApCa9KkSUZovffee2IiaENEAksTatJcLe7bvHlzqUuWqNK49erVM3bmqzpIt956q68a3wgSJ5WbKavx48cbPRCTk5NDFlpGYPk9pHAVOr9yIvKwiDwuIg+qrUoioraDVxIRVYDnvIioGeA/ROSAiOwRkbUisllEcm98Fz/PxVX4XMKKmptSE7bq7qo4KpC6du0q7dq1M8qINGjQwJg8BazyzjvvSKVKlWTMmDEhCS3/7rD+Dgf167qTiDwvIv+tlnEU81W3XP+oLSLNbvp8jogsEZF/i8j/ist1bbEK8zIGNaG+dOlSyc39O9O9tW/f3pgY7dmzp68yuIBlRo8ebYTWiBEjzG527L/TUj+01B/XzijGI9LfI7Lfcy1arPpQ3+/F69+/TOcVjhITEz0ul6ug1XiRj86dO3s2bdrkAUIhJSXFExUV5ev9aZ8yBJX6eMwjstvioPL+OOAR6XTjZ0ZoYPkKqri4OE9qampI3qTAzebNm+eJjo4OWmi5SrveJzo3V7quWiXNt20LznDN5ZKtLVrIqi5dJM+8r1rYUhtlv/32W9Nj3bt3l9mzZ0tsbGzQzwsws2zZMnnmmWfk8uXLZoctHR6W6pFR9TNnZHBKijTfujV4c0sej7TYskUG/fOfUu3MGYkUe/fuleXLl5see/fdd40tE4QVnKRbt25GH0a1pceEJ6iBddvJkzJg9my5/cQJCYXaJ07IoJQU47/hLisry9h46j3EVUsRPv30Uxk3bhzLEuBIarO0GhXY3Uas2MC6PTNTBs6aJVXPqxUJoVMlO1te/Ne/jPMJ5/U5qtCb2W31hx9+KK+88kpIzgsoLbWcRnXkUbW17Aotd3HDwKS5c6WC+bg06NR59Js7V6qdPSvhaOPGjXL8+PEin3/rrbfk1VdfDck5AWWlqpaqmlo+pi08tgRWdF6e9PnyS6ly4YI4SdULF+S5+fONBwDhVmVB7Qs0a3ypVhcDOmnevLnxfrajjZhpYHVduVJqm/y2d4JamZnSefVqCSdpaWnG1pubqdvq+fPnG1shAN088MADxvCwbt26loZWkcC668iRa0sXHKzl1q1SLyNDwsGZM2dk9+7dRT4/YcIEozkAoKv7779f1q9fb2kbsUKB5b56VbotW+b8bTEejyQuXy5uDUrOlkT9D/UundusWTMZPHhwyM4JsLKNmGrYqsLLhCegwGqyc6fcpkkRuFonT8qDu3aF+jQCooaBZndXb7/9NssXEDbq1q0r69atkyZNmgQcWjcCy5WfLwkbNohO2qxfLy6n3w0WY9++fUXqrKuxf48ePUJ2ToAdatWqZcxp+SgR7SlzYN2dni6xp0+LTmqeOiX1f/1VdGV2dzVgwADurhCWYmNjjQdMgfQ+vBFY/7Vjh+ioiabnreatfvvtt0KfU08E+/XrF7JzAuymarapirlqkak/oWUEVtTVq9LwgKqpp5+G+/cbDwt0o7oBew8HH3roIV9rV4CwobbvqP2yPqY+ig0towxC3NGjEuO1Dsjwj3+I1KwpjqGGrG+8UehT5a9ckTp//CFH77xTdAssbz5+6wBhp0KFCsZWtLL2PjQCK/7IEfPv+vTT6rmkOIY6T6/AUtSaLN0CS/UM9NaqVauQnAsQCqp89xdffCEvvPCCsUi6NKHlLlgioLNamizFuNmff/5Z5HMNGzYMybkAoaIapXz++edGw9bSDA+NwKpp8o9HJ7o93VTOe1XAUE00VKNNINJERUUZvQ+HDRtWYmgZgeW0Tc7+bIrWjfeEuyrqTwMJRCq32y3Tpk0zFk0XF1pGYJUzm3DXSHkNz997s7NqUglEuvHjx8vEiRN9HjcCKyYvT3QWo2G5mTyva16+fPmQnQvgJPfcc48xt+UzsHI1b/KQGxMT6lMAYAH1tPC5554r8gu9UGD9pXl34Cuanz8AkZkzZ0pSUpLPsLoRWBc0nz+5cItqKA1AV9OnT5chQ4YUKbV0XaFW8/Knk1az++E0PfoAbb3//vvGkgYfDZELLR41AuvkbbeJzk5pfv5AJIfVm2++6etwkZXuxmz7kXr1zF+emuq8vYQmMurXD/qpAPCfGvqNHj1aPvroI18v8b2X8FhcnDHxXmQ9lsm+Pae5Ur68/EGFA0CrsFKt66ZOnVrm1vbGkPBqVJQc0HQf2/4HHpB8d4kNrAE4pGGw6lfgT1gpN/6l7zCvt+x4up43EGny8vKkf//+Mnv2bL/CqlBgpd9zj3ZPC9VkO/NXgB5b0fr06SNz5871O6wKBZbH5ZL1CQmik/Vt2hjnDcC5Ll68KN27d5fFixebHS7TP+BCkz87GzeWU7VqiQ4yb79ddj/4YKhPA0AxcnJyjLBabd6tvcx3G4UCKz8qSpY9+aQqziSO5nLJisREJtsBBzt79qx06tTJaO9lwq+QKfJ47bf4eNnaooU42U8tW/peOwYg5LKysqRz586yadMms8N+3xGZrgdY1aWLnLjjDnHqUPB/OnUK9WkA8CEzM1Patm0rW7ZsMTsc0PDNNLDyoqNlQZ8+ku2wTdFqk/Z8VXqCcjKAI6lem23atJFdu3aZHQ54rsnnisuz1avL3KQkuVyhgjiBOo//JCXJuWrVQn0qAExkZGQYreoOHTpkdtiSiXF3ScOvWQMHyvkQl29Rd3pz+vc3zgeA8+zfv18SEhIkPT3d7LBlT/GMb+SjrMP1VxgvUTPci1RzYgm+/xORXmqPthRznqrrjGYK/WXi4+ON31CAbvbu3SsdO3aU48ePmx229B9myZvwroWE+pfUWtXZKqmVtIXUz5l2/ecWG1YAQmPbtm3y2GOPBSWslNLtGr4WFpdFRDUOe1xETGfULLRTRB4TkRGqIANhBTjP+vXrpX379qZNge0IK6X0ZQ5UaF0Lrh9EpJmIvCAi+yw+n70i8ryINFfX46afCcBB1qxZI4mJiUUaAl9n2/xM2euyXAsQVSX+cxFRe2PUoqh/q1UHfp7D+etfr75PYxFRuyPzCCrAmZYvXy5PPPGEZGdnmx22dTK55En3Yr/a5V0M8OHrQ0YVZPeJSB3VWFpE1GPGc+qBn4iowe4BEdktImtFRK0u+7tNhp/nwqR76KnH2YsWqWcz8KVWrVoyYMAAbS9Qamqq9O3bV3LNe4Fq9+QLpee5+SM+Pt6ju8WLFxf6O/FR9BqMHDnSo6t58+Z5oqOjff1/DQpKdQJB9PTTT4djz0BXsM6DwAKCpHbt2tK6tVqlE549A4OBwAKCpHfv3hIVFRW2PQODweiaA9gokidiPd6BFc49A4OBwAKCEFbq6aBaEa6D/Px8eeONN2TSpEmO+yVEYAFB0KtXLy2Gg/n5+TJq1CiZNk3tinPeHTOBBQSBDsPBq1evyqBBg2TOnDmOHdoTWIDNw8HY2FijAqfT23D17dtXFi5c6NiwUggswGZPPfWUREdHO7oNV+/evWXlypWODivFuVcRCBNOXiyak5MjPXv2lLS0NMeHlUJgATYOB6tVq2aUDXZqG67ExET58ccftQgrhcACbB4OlitXznHX+OTJk0Ybrh07dmgTVgqBBUTY08Hjx48bDU737NmjVVgpbM0BbBwOdujQwVHXNyMjw1jAqmNYKQQWYJMePXpIBYe0ySvobKN6Bh4+fFh0DCuFwAIiYDi4Z88eo/760aNHtQ0rhcACbBgOVq1a1ZjUdoKtW7fK448/HrTONnYisAAbdO/e3RHDwXXr1hnzaMHsbGMnAgsI08WiK1askK5duwa9s42dCCzA4uFgpUqVpEuXLiG9rkuXLjUqRFy6dClswkohsAAbhoMqtEJl/vz5Rlhdvqx6H4dPWCkEFmAxFRahMmPGDEc0i7ALgQVYSFVlCNXTwU8++USGDh3qiGYRdiGwAAvnr1RXHLXCPRT114cPH+6YZhF2IbAAC6mncsE2btw4xzWLsAubnwELqXItwZKfny+vvfaaTJkyJSLCSiGwAP95vBulNmnSJGj111966SWZPXt2xISVQmABFg4H3W53UOqv9+vXT1JTUyMmqAoQWIBGw8GLGtVftwOBBVhA9Rzs2LGj7SWNu3XrJhs2bIjIsFIILMCC+atHHnlEatSoYdu1zMzMNIac27dvj9iwUggswOHDwSNHjhgljQ8dOhTRYaWwDgtw8Pqrffv2SUJCAmF1HYEFBKhmzZrSrFkzy6/jli1bjPrrulcJtRKBBViwHcfq5Qxr1641JvFPnz5tdtglEYrAAgL06KOPWnoNly1bZsyJhVPhPasQWICDAmvevHlhWXjPKgQWEOD6qxYtWlhyDadOnSrPP/+85Obmmh12WfJDNEdgAQGIi4uTypUrW1IeZuTIkWFdy8oKrMMCAphwV4EVCBVQr7/+ukyePNnXSwirmxBYQACqVKkSUMWFIUOGSEpKiq+XEFZeCCwgAP4uZ8jJyTEqLnz99de+XkJYmSCwgABkZWWV+WuOHTsmPXv2lG3btpkdJqiKwaQ7UDaFAuXgwYPG0K600tLSpHnz5oSVnwgsIADnzp2TzZs3l6ro3tixY42OOqryggnurEqBwAICNGHCBF/LEQyrV6+Wpk2bygcffMCyBWj9ePzGR3x8vEd3ixcvLvR38l4CEEaK/D1HjRrluXTp0o1rof68ZMkST7t27cyuSbhfH4QhAktvRcKnSpUqnrZt23oefvhhT8WKFUsKKsLKDzwlBPzj8g6d7OxsWbNmTWm/Fn5gDgvwnz/BQ1gFgMACAuMqw+sIqwAxJAQCVxBENw8RCScbEFiAdQgpmzEkBKANAguANggsANogsABog8ACoA0CC4A2CCwA2iCwAGiDwAKgDQILgDYILADaILAAaIPAAqANAguANggsANogsABog8ACoA0CC4A2CCwA2iCwAGiDwAKgDQILgDYILADaILAAaIPAAqANAguANggsANogsABog8ACoA0CC4A2CCwA2iCwAGiDwAKgDQILgDYILADaILAAaIPAAqANAguANggsANogsABog8ACoA0CC4A2CCwA2nCF+gQinKfgD9HR0XLXXXeJznJyciQzM9P707zHYBneTA4JrDDGewyWYUgIOxFWsBRvqNAL17ss3luwHG8qZwmH8OI9BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANju/wHH5cLKKAZklQAAAABJRU5ErkJggg==) **Turn details abstract image** ![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAAY5klEQVR4nO3deVBUZ7rH8WZTcMGFOCoaxShC0Mi4JYiggMFdiOiNqGgSk3FNSNTovf5hJbmmxmh5NXgdkxiSmQhuiRpBxYgoiOCCuEURUVaNskTZ97Dceiu5NTMZHQT69On39PdTdf5rul+ebn685/T7PsdMB0ioR48ePd3d3d1dXV1d+/+mW7du3bp06dLF2tra2tLS0rK8vLy8rKysrLi4uDg9PT09IyMjIzU1NfXs2bNnb9++nab27wBAo8zNzS28vLy8//KXv2wX2dPQ0NDYmiM/P79g3759386cOfM/bGxs2qn9+wHQgF69evX+85//vP7Bgwe5rQ2pJx3l5eUVf/vb374ZOnToMLV/XwAScnR0HBgeHr6rpqamVqmgetwRFxd32sfHZ5zavz8ACXTu3LnL+vXrP6mqqqo2ZFD9/oiOjj4xaNCgwWrXA4CRmjhx4qT79+8/UDOo/vGora39RYSnpaWlldq1AWAk2rdv3yE0NPQrtQPqSUdCQkJiv379nlO7TgBU1rNnT/vk5ORLaodSU8fDhw8fjR071kvtegFQiaur6x/v3bv3k9ph9LSHuK42d+7cID4wgIkZMmSIq5i1qB1CzT3q6+sbli9fvkLt+gEw4JKF3NzcPLXDpzWHuBjPB8bwLFR4TZiw7t2790hMTEzs2bNnT53EPDw8PNq1a9c+JiYmRu2xAFCAhYWFZWxsbJzasyN9Hp999tnnZmZm5nxgAI3ZsGHDRrUDRonjm2++2SnCWO36AtCTESNGjPzll1/q1A4XpY6IiIjItm3bWvOBASQnZh+XLl26rHaoKH2cPHnylFgEq3a9AbTCsmXL3lY7TAy5ebpjx462fGAACYl9eNnZ2TlqB4khj4sXLybb2dk9o3bttYhvN6CoBQsWLOjTp08fUyrz8OHDh8fHx8eLbUdqj0VrzNQeALTtxo0bKS4uLi46E3Tnzp07L7/88sv37t27q/ZYtIIZFhT9ZtBUw0pwdHR0PHPmzJkBAwY4qj0WrSCwoJj58+fPN/XyitNhcXpIM0D94JQQisnMzMxycHBwoMQ6XVFRUdHkyZMnX7hw4Tz1aDlmWFCEaHhHWP2duP3YiRMnTnh7e/vwkWs5AguK8PLyotnd73To0KHD4cOHD48fP34CH7uWIbCgiBdeeOEFSvuv2rVr1y4yMjJyxowZM6lP8xFYUISTk5MTpX28Nm3atNm7d+/e119//Q1q1DwEFhQxYMCAAZT2ySwsLCxCQ0NDFy1atJg6PT0CC4ro2rVrV0rbxB+fubn59u3bt7///vurqNXTIbCgiI4dO3aktE0zMzMz27hx40ZaLj8d1mFB78zNzS3q6urqKG3zbNmyZcvKlStX6nS6Rmr3eAQWFFFdXV0jLi5T3uYJCwsLExvG6+vrCfzH4JQQiigvLy+ntM03b968eSK0RFse6vevCCwoorCwsJDStkxgYGDg/v3797dp06YtNfxnBBYUkZGRkUFpW87Pz8/vhx9++KFDhw58efEPCCwoIi0tLY3Stn5708mTJ0926dKFJSK/IbCgiOvXr1+ntK03cuTIkWLTNC2Xf0VgQRFxcXFxlFY/hg0bNkw0ArS3t+9l6jVlWQMUk5WVld23b9++lFg/srOzs0XL5czMTJO9PsgMC4qJioqKorz64+Dg4CBmrgMHDjTZjeUEFhQj1hNRXv3q3bt3b9Fy2dXV9Y+mWFtOCaGomzdvpjo7OztTZv0qLi4uFi2Xz58/f86Uamuh9gCgeWbiD0vtQWiNtbW1tVhgmpSUlJSVlZWpMxHMsKCotm3bWotFpPb29txUVAE1NTU1IrgiIiIO6UwAMywoSmzira+vb5g4ceJESq1/lpaWlgEBAQG3bt1Ku3nz5k2t15gZFhRnYWFhmZycnOzq6upKuZVRX19fv3DhwoV//etfv9ZyjZlhQXGNjY0NV65cuSLapoiGdZRcme6lfn5+fiUlJSVavvchgQWDuH///n2xJ87Nzc2NkivDzMzMTJx619TU1CYkJCRosc4EFgxGbOT19vb2Frdvp+zKGTdu3Dhra2ubkydPxlBnoBXEfrjc3Ny8hoaGRg5laxASErKV69RAKw0ePPiFhw8fPiKwlA/tsLCwcPGlh1Y+tFwAhSpefPHFl0TbFO6uo7yIiIiIWbNmzaqtra3RSY69hFBFUlLShUmTJk0S32rxFijL39/f/8CBAwfEdS3Za80MC6oaOnToMNEKuFu3bt14K5QVHx8fP23atGllZWWlstaawILqnJycnGNiYmJ69epl8g3qlJacnJwslj4UFhY+0kmIwIJR6Nu3r4MIrf79+/dXeyxal5KSkuLr6+ubl5eXq5MM17BgFHJycrI9PT09xR+T2mPRukGDBg2KjY2N7d2797M6yRBYMBriP76Pj4/P1atXr6o9Fq1zcnJyEn3i+/fvP0AnEQILRuXnn38uEKvhz507Z1KN6dTQt2/fviK0xLo4nSQILBidkpKSYnGNRVzTUnssWtejR48eok/8yJEjX9RJgMCCUaqsrKyYOnXq1EOHDplEYzo1de3atWt0dHS0u7v7aLXHAkjNysqqzb59+75lG4/y23jKy8srfH19x+uMGN0aYNQaGhrqDx48eFDcLWbo0KFD1R6PlllZWVmJLTwpKSk3b926laozQgQWjF5jY2NjZGTk4U6dOnWin5ayLCwsLGbOnDkzJycn59q1a9d0RobAgjSOHz9+XCx29vLy8lJ7LKbQvfTBgwe5ly9fvqwzIgQWpHL69Om4qqqqanHLdrXHovXupVOnTp1aVlZWZkz3PiSwIJ3ExMTE/Pz8AtHtgR7xyhG1nTBhwgRj6l5KYEHaTbwZGRmZ4tRFnMKoPR4t8/Dw8Ojataud6Kqh9lgILEjr+vXrP167du3H6dOnTxf351N7PFr20ksvvSQWmUZFRUWJ70HUHg8grUmTJk2uqKioZK1Wg+JrtcLDw3dpqeUyoApPT88xJSUlpYRWg+KhFREREdm2bVtrNd5n+mFBM4YPHz5CXGexs7OzU3ssWhcbGxsrrh9WVFSUG/J1CSxoiouLyyBxc4uePXv2VHssWnfmzJkzvy19KDXZwBJfoYoPm42NjU27du3aidXNao8JcnF2dnbevHnzZrHVRO2xaN3FixcvipbLRUVFhZoPrB49evQcM2bMGPG1qeiC6Ojo6Cj2jKk5JgDNc/369evjx48fn5+fn6fTWmA5OjoOnD179uxXX331VRcXFxdDvz4A/btz584dsfvg3r17d3WyB5aZmZl5QEBAwIoVK1aMGjVqlCFeE4BhiQ3TovFienr6HSkDSwTVvHnz5q1du3Ytd0MBtC8vLy9PhFZKSsoNqQLLzc1tVEhISMjIkSNHKvUaAIzPo0ePHol9nsnJyRf1/dx634NlaWlp9cEHH3yYkJCQQFgBpsfOzs7u1KlTp7y9vX2MeoYl7uC7d+/eva6urq76fF4A8qmsrKwU166jo6NFHzPj2vzs5eXlLRbsOTg4OOjrOQHIy8rKykqsBkhNTb2Vmpp602gCa86cOXO/++6779q3b99eH88HQDstlwMCAgJEKyCxXqvVz9faJ5gxY8bMXbt27WJVMYDHEf3K/P39/W/fvn27td8etuoa1iuvvDL922+//ZZeRACaUltbWytmW1FRUUd1hg6sIUOGuJ49e/as2O/X0ucAYFqqqqqqRo8ePfrq1atXDBZYnTt37iJa1D733HPPteTnAZiu9PT09BEjRowoLS0tMcg6rNDQ0FDCCkBLDBgwYMCOHTt2GGSGFRAQMGP//v37W/JiAPD/xIX4w4cPR+qUCqyOHTvapqamptrb29s35+cA4Pfu3r17V7SVak7X0madEr733nvvEVYA9KFPnz59li9fvlyRGZatrW2nrKysrC5dunRp0egA4HcKCwsLxe6Y8vLyMp0+Z1jLli1bRlgB0KeuXbt2Xbp06VJ9z7DMREdBeloB0Lfs7Ozs31YdNOplhuXj4+NDWAFQgjglFItJn+axTxVYgYGBga0eFQA8QVBQUJBOX6eEmZmZWbSNAaCUzMzMzAEDBvRv9Qxr4MCBToQVACWJa1h9+/Z1aHVgubu7u+ttVADwBJ6enp661gbW888//3xTjwEAfdyxu9WBxc1OARiCk5OTU6sDSyyf19uIAOAJ+vXr10/X2sCytbW1beoxANBanTp16tTqwOrYsWPHVo8EAPSQNU0Glo2NjU1TjwGA1mrTpk0bg9/5GQBaorS0tLSpxxBYAIxCWVlZky1mCCwARqGgoKCgqccQWACMQlpaWlpTjyGwABgFAguANC5fvny51e1lKioqKlnaAEBJlZWVlaJdcm1tbc2/exynhABUl5CQkNBUWAkEFgDVHTx48ODTPI5TQgCqqq2trRX3Oy0sLHzU1GOZYQFQVURERMTThJVAYAFQ1aZNmzY97WMJLACqOXbs2LGLFy8mPe3jCSwAqmhoaGj48MMPP2zOzxBYAFTx5Zdfftmc2ZXAt4QADC4/Pz9f3OCmuLi4qDk/xwwLgMFPBRcsWLCguWElEFgADGrdunXrjh07FtWSn+WUEIDBHDly5Ii/v79/Y2NjQ0t+nhkWAINITExMDAwMDGxpWAkEFgDFXbhw4cKkSZMmVVZWVrTmeQgsAIp3YpgwYcKE8vLyJnu2N4XAAqCY2NjYWDGzKi0tLdHH8xFYABS7wD558uTJFRUV5fp6TgILgN7t2bNnT0BAQEBNTU21Pp+XwAKgVzt27NgRFBQUVFdX94t+n5nAAqBH27Zt27Z48eLFrVm68O8wwwKgFxs2bNgQHBz8jk6na9QpxFKpJwZgGhobGxtXrVq1avPmzf+j9GsRWE3IyMjIKC4uLlb6jYB+ubq6ulpaWvL5NsBG5iVLliz58ssvd+gMgDe0CatXr179/fffP9UdPWAUzDZu3Lhx+PDhw9UeiNbV19fXv/nmm2/u3LnzG0O9JoEFLTH79NNPPw0ODg5WeyCmcKeb2bNnzzb0P3MCC5pgbm5uERoaGvr666+/rvZYTOEuzdOnT59+4sSJaEO/NoEF6VlZWbXZvXv37hkzZsxQeyxaV15eXu7n5+cXFxcXq8brE1iQmo2NTTtx12CxuVbtsWhdUVFRkdgXmJSUdEGtMRBYkFb79u07iJtw+vj4+Kg9Fq3Ly8vLGz9+/PgbN25cV3McBBak1Llz5y5RUVFRbm5ubmqPRetycnJyfH19fdPT0++oPRYCC9L5wx/+0D06Ojp6yJAhQ9Qei9alpaWlibD66aef7umMAIEFqTz77LN9YmJiYhwdHR3VHovW3bx586YIq9zc3Ac6I8FeQkjDwcGhX1xcXBxhpbzk5OTkMWPGjDGmsBIILEjh+eefdxGtdvv169dP7bFoXXx8fPy4cePGFRYWPtIZGQILRm/YsGHDT58+fdre3t5e7bFo3bFjx45NnDhxYllZWanOCBFYMGoeHh6ep06dOvXMM888o/ZYtC4yMjJSrGCvrq6u0hkpAgtGy9vb20csXbC1tbVVeyxat2vXrl1ip0BtbW2NzogRWDBKU6dOnXb06NGjHTp06KD2WLTu888//3z+/Pnz6+vr63RGjsCC0Zk9e/Ycsd3G2traWu2xmEKX0KVLly5RqqWxvhFYMCoLFy5cFBYWFkbzPcOE1Zo1a/5LJxECC0Zj1apVqz/77LPPzM3N+Vwq3NJ4xYoVK2QLK4GV7jAKq1ev/s9PPvnkE7XHYQpdQhctWrTo66+//konIQILajPbtGnTJvEfX+2BaF1dXV3dggULFoSHh4fpJEVgQdUuoeIbqrfeeust3gZl1dTU1AQGBgZGREQckrnWBBZUC6udO3funDNnzhzeAuW7hPr7+/vHxsaekr3WBBbUYLZ9+/bthJXyiouLi6dMmTLl3LlzZ3UaQGDB4EJCQkIWLly4kNIrq6CgoEC0jr527dpVrdSawIJB/elPf1r4zjvviNuZQ+GWxr6+vr4pKSk3tFRo1rvAYIYMGeIq7htIyZWVnZ2d7enp6am1sBIILBiEtbW1zYEDBw7Y2NjYUHLlpKampnp4eHhkZGSka7HOBBYMYu3atWv79+/fn3Ir58qVK1fGjh079sGDB/e1WmcCC4pzcXEZ9P77779PqZVz7ty5c+J2Zw8fPvxZy3UmsKC4devWrbOysrKi1Mo4ffr0afFtYElJSbHWa0xgQfFe7GLRImVWxtGjR4+KuzGXl5eXmUKNCSwoas2aNWvovqCMffv27TP2lsb6RmBBMba2tp1E211KrH/h4eHhQUFBQXV1db+YUn0JLChm1qxZs1jGoH/bt2/f/tprr70mQ0tjfSOwoGhgUV79+vjjjz9+++23l8nS0ljf2JoDRbRt29ba3d3dnfLqzwcffPDBunXr/tuUa0pgQRGjR48ezU0k9NfSePny5cu3bt0aojNxBBYU4ebm5kZp9dPSWHQJDQsL20k9CSwoxNnZ2Znitk5tbW3t3Llz5x44cGA/tfwVMywowsnJyYnStq6lsfjSIjIyMoI6/h2BBUXY29vbU9qWKSsrK5s2bdq0+Pj409TwnxFYUIStra0tpW1ZS2Ox1ebChQvnqd+/IrCgBLP27du3p7TNk5+fny82Mf/444/XqN3jEViAEbh79+5d0dL4zp07t9UeizFjpTuU0FhRUVFBaZ9OVlZWlre3tzdh1TQCC4ooLS0tpbRNS0lJSRGLbLOysjKpV9MILCji/v37mm3Tqy+XL1++7OXl5ZWXl5er9lhkQWBBEWlpaWmU9skSEhISREvjR48ePaROT4/AgiIIrCeLjo6OFt8GlpaWlvDxax4CC4o4f/4864ge48iRI0dEy+iqqqpKPnrNR2BBEYmJiYnV1dXVlPfv9uzZsycgICCgpqaGurQQgQVFiD/Ks2fPnqW8v/riiy++MMWWxvpGYEExe/fu3Ut5dbpt27ZtW7JkyRJT7RKqTwQWFL2rS1VVlcnc0eVxNmzYsCE4OPgdsZhW7bFoAYEFxZSVlZUeOHDggKl2CV25cuXKNWvW/JfaY9ESAguKWr9+/fqGhoYGUwurd999990tW7ZsVnssWkNgQVGpqak3Dx06dMiUWhq/8cYbb2zbtu1/1R6LFhFYUNzatWvXina/ptAldObMmTN37tz5jdpj0SoCCwaZZW3atGmTlktdWVlZ6efn5xcREWEys0k1EFgw2A1A09PT07VY7pKSkhKx1ebEiRPRao9F6wgsGER1dXWVOF3S2jKHoqKiIhFWiYmJCWqPxRQQWDAY0fo3ODg4WCslz8vLyxs7duzYpKSkC2qPxVQQWDCor776KjQkJET6Oxjn5OTkeHp6et64ceO62mMxJQQWDE7cdn3Hjh07ZG6dI8IqIyNDk9fkjBmBBTU0Ll26dGl4eHi4bOW/cuXKFXEa+NNPP91TeyymiMCCKhoaGurnz58//6OPPvpIrAyX4W04fvz4cdHSuKCgIF/tsZgqAgtqavzoo48+FCvDjfnbQxGo4rrblClTpoj9kWqPx5QRWFCdWBk+bNiwYZcuXbqkMzIFBQUFr7zyyivLly9/T8wK1R6PqSOwYBTS0tJujRo1apQ4RRSrxo1hVrV79+7dgwcPHnz48OFItceDXxFYMBqiG6c4RRw4cOBA8S2iWl0ekpKSksaMGTMmKCho7sOHD39WYwx4PAILRufBgwf3Fy9evMjZ2dl569atWw0x4xIzqpiYmBixH9DNzc2NleuSqqioqGxoaGg01WP69OkBar8Hps7Ozu6Z4ODgd8+cOZNQV1dXr8/3NzU19da6des+dnJyclb790TTzJ4msGxsbGx0JmrGjBkzvv/++4NqjwO/6tWrV29fX19fsXDT3d3d3cnJyak5tcnNzc0Vd/QRNzI9efLkyZSUlBvUVh4EVhMILONmbW1t0/833bt3796pU6dO4h+spaWlZUVFRYXopCA2KGeIZenp6eklJSXFao8ZLWfZip8FjKILhJglMVMyDVx0ByANAguANAgsANIgsABIg8ACIA0CC4A0CCwA0iCwAEiDwAIgDQILgDQILADSILAASIPAAiANAguANAgsANIgsABIg8ACIA0CC4A0CCwA0iCwAEiDwAIgDQILgDQILADSILAASIPAAiANAguANAgsANIgsABIg8ACIA0CC4A0CCwA0iCwAEiDwAIgDQILgDQILADSILAASIPAAiANAguANAgsANIgsABIg8ACIA0CC4A0CCwA0iCwAEiDwAIgDQILgDQILADSILAASIPAAiANAguANAgsANIgsABIg8ACIA0CC4A0CCwA0iCwAEiDwAIgDQILgDQILADSILAASIPAAiANAguANAgsANIgsABIg8ACIA0CC4A0CCwA0iCwAEiDwAIgDQILgDQILADSILAASIPAAiANAguANAgsANIgsABIg8ACIA0CC4A0CCwA0iCwAEiDwAIgDQILgDQILADSILAASIPAAiANAguANAgsANIgsABIg8ACIA0CC4BOFv8HKu7y1rG93mEAAAAASUVORK5CYII=) **Turn image**
* **abstractGeometryImg**: Provides a detailed representation of the entire intersection, offering a comprehensive view of the turn geometry. This image can be customized, including options to adjust various colors for better visual alignment with the application's theme. * **turnImage**: Delivers a simplified schematic of the upcoming turn, focusing solely on the essential turn direction. Unlike abstractGeometryImg, this method does not support customization. Tip Each image is assigned a unique identifier. You can use the `uid` getter of the images classes to retrieve this id and update the image in the UI only when the ID changes. This strategy can significantly enhance performance during navigation, especially in scenarios where frequent updates might otherwise impact efficiency. ###### Abstract geometry image customization[​](#abstract-geometry-image-customization "Direct link to Abstract geometry image customization") The `AbstractGeometryImageRenderSettings` allows for abstract geometry render settings customization, with the possibility of specifying the colors, improving the overall user experience: * Kotlin * Java ```kotlin val settings = AbstractGeometryImageRenderSettings().apply { activeInnerColor = Rgba.red() activeOuterColor = Rgba.green() inactiveInnerColor = Rgba.blue() inactiveOuterColor =Rgba.yellow() } ``` ```java AbstractGeometryImageRenderSettings settings = new AbstractGeometryImageRenderSettings(); settings.setActiveInnerColor(Rgba.red()); settings.setActiveOuterColor(Rgba.green()); settings.setInactiveInnerColor(Rgba.blue()); settings.setInactiveOuterColor(Rgba.yellow()); ``` The render settings from above will create the following image when used: ![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAAWqklEQVR4nO3dCXRc1WGH8f8s0kjWYknel2IwtgGvrHYWs4YtYIMhxCc0JyQ5adPkJA1toYUSKKQhBQJZTsnJ3gYIaRoC2MbEwYGEHWIMBBtsbLzgfZVlC0n2aBlNz32yXI/0RpZm3pt5d+b7nWM41jJ6Fujzu+/dd29o3oI5AgAbhPN9AADQXwQLgDUIFgBrECwA1iBYAKxBsABYg2ABsAbBAmANggXAGgQLgDUIFgBrECwA1iBYAKxBsABYg2ABsAbBAmANggXAGgQLgDUIFgBrECwA1iBYAKxBsABYg2ABsAbBAmCNaL4PAOktmL0/efTvt7WM0DlLHsz5t+yFyz6rsRW7U9521Uu1oZwfCIoeZ1gArEGwAFiDYAGwBsECYA2CBcAaBAuANQgWAGsQLADWIFgArEGwAFiDYAGwBsECYA2CBcAaBAuANQgWAGsQLADWIFgArEGwAFiDYAGwBsECYA2CBcAaBAuANQgWAGsQLADWIFgArEGwAFiDYAGwBsECYA2CBcAaBAuANQgWAGsQLADWIFgArEGwAFiDYAGwBsECYA2CBcAaBAuANQgWAGsQLADWIFgArEGwAFiDYAGwRjTfB1CsOiz/1tt+/LBPlP/rAE8kj/H+EN9nbzAkBPyNVffH9OfjcAwEC8jcQCNEuLJEsAAPY1XR2qnytmN2jLOtDHHlFBi4XsE5fVNcn3uhUcM/SDi/31kT1fLxZXp6WoX2VkX6eg2ubw0AwQKyNHFXm25Ysl+Rzv/v2KgDHbrizWZd/laznplaod/OrFJzmeuAxnwS0eonhoRAlj6xvCklVkeLdEqXrGzR9x7eo5kb4ulegiFiPxEsIAulHUlN3dZ2zI+rinfqn37foC/+6YDzOS6IVj8QLGBgUsIyZn+Hoon+t+aC1Qd1x+P1qmtJHPO10RvBArJQGe8c8OeM39Oubz1S7/zbBdHqA8ECshDKMC+1LQndvqBe07a2ur2baKVBsIAstJVkfoMv1p7UvzzZoDM3ul6MJ1ouCBaQhYYK1zlW/VaSSDoX489796Dbu5kZ3wPBAgYm5ZRqX2VY7ZHsplGFk9Lf/emALl3Zku5DONs6jGABWUiEQ9o0rMSTa2Fmpvy1r36Q7kOSWX+RAkCwgCy9N7LUs+/hlW806/PPN6a7mJ9UkSNYQJbWjcz+DOtol7zdoq88Yx71cX13UkWMYAFZem+Ud2dY3WavPaTrlzY4F+VdJFWkCBYwcKGedwq313q/joB59vDmxQ3plqtJqggRLMADb40r8+X7OGVbq25dVO88i+ii6KJFsAAPvDUu5tv38cTd7bqd5w8dBAvwYFi4ZnSp4lnMej+WsQ0duuOxeo1o7CjqMy2CBXjATB5dPca/syxj+AcJ3fH4Ph23r3gfmiZYgAXDwqMfmr5twT5nmFiM0SJYQMAvvPdUFe/UbQvrnQvyxRYtggVkLuWi1Z7qiLP5RC6UtSd105MNmrGluKJFsADLhoXdzFLL//y7hnRrxRdktAgWYOGwsJtZnvkfnmrQuWvSLk9TUAgW4KHVY0rVFs3trl3hpPSlPx5w1osv9DW1CBaQnVDP6Q2rxsZyfxBJ6W+fPaBLVxT2mloEC/DYshNzOyxMWVPrxcJeU4tgAR4zW9R3ZLkKabZral1boNEiWED2UurUEgs717Ly6co3mvWFAlwIkGABPlh2Ynnev68XFeBCgAQL8MFrJ5YpEYCfrtlrD+mrT5toFcaaWgH4lgKFNyxsKgtrzejc3y108+F1h3Tjkv0FsXopwQIK7G6hm9M2xfWvTzQ4j/TYHC2CBfh4HaszfzcLe5m8vVU3P7HP6iWXCRbgnZQ8NQ4Ka60PG1Rk4+Sdbc5KD7YuuUywAB8tm5D/u4U9jd/TteSyWVvLtmgRLMBHf55QrmSAhoVHL7n8bwv2aUizXdEiWIC3UvJ0YFDY052hvTTqQNc68SMtWieeYAE5mJMVVMOaEk60bFknnmABPns1oMPCbjUHO53hoQ3rxBMswO+doSsj2jA8mMPCbpXxTn39iX2atKtNQY4WwQKKbBJpOoNaO/X1Rfs0bWtw14knWECOpjcEeVjYLdbetU78qZuDGa3cbPGBvAmH4gqHDykcalUkdCij19hyUIon+V9lgEJH/4CbHXXeH1bizIEKutKOpG5c0qD7L651OzM0f6a8pZf/CwtISWSfKkrf1aDStYpFdygW3alIuCnr171rrflnbeD+trXxUR0bgtW9ucX1TzXoJx+r0fMnD1JQokWwLFcSqVdN+csaXP6KEykE16sTy/taCTRwwoc3twh3Ss9ODka0CJalBpWu19DKJ1QVe4uTHUuYYeGmoSU6vt6OsyzDrFj6xWcPqLw9qSUzKpTmLDtn4SJYlimLbtXI6l+pIrYq34eCDCeR2hSs7mhd92Kjyto69fhZVXKRs7MtgmWJeCKkx7YnNX7YbQrJ9fmvXszSuMOaOlR1qNNZB2lQW1KhJJee8qkzbMGtwjTmL2tyhomPzsxftJxgLZi3OLPPDqUcn5kZN1PSuZKmSpokabQZvUiqlmQG72anR3OhxVzGNacIz5trkWY7tyOvkuEP1FUL56pQbT0Y0bfXVmtTS7LPWJl5NDO2tGrK9jadtKNVoxoTzsVTwCvXvNakWEdSv/qI+ZHOfbSiWYbKzOO6SNJnJM2T1GuQe5Tqw79GSjr9qLebnR8XSnpI0jMKhboW6uFMwPHc3ph+sK5KrX2sBDdlW6suXHVQZ26Mp1sGF/DM3DebVdqe1APnDna7VexrtKIZxqpE0qcl3SzppCyPoeLwa3368JnXXZL+R6FQe7FHa/GOcv1sY2Xa+QPTt7Tqk681aaL74xSAby55u8U50/rpBTVuq6r6Fq1oBmdV50j6ofmL3YfjMfF7QNItkr6qUOhp561FGq6fbqx0fXtdS0KffaFRszbEc35MQLfz3j3onNH/8MJatx2CfIlWaN6COVowe/+xi2CWVL31femhXbmZMmj+qJ8bKX3zBClWXE8QLdlZrh9tcI/VGe/HnbkxaZa4BXLuzePL9L2P16rdfbdrz6IVVUc/nyXcHJcuXSk9mKNYGebr/GJX19fdUjxnEi/Xx/TjNLH65LIm55EJYoUgOX1TXDcsaXAe6XHhaTGOHaw1B6W5b0urzLXxPHinRfr4213/LnA74xHdv76q139hMw/mb55r1CeWN6XbehzIK/Ow9E1P+r+NWN/BWt0izXlb2pnni7p72qR573QdT4Eyfznds6ZaLR29z6A/81KjLiyCYMNuU7a1OjvymLW1/IqWc9F9W8uI3u/Y0qJhn3xDEff1nnOvsUOJ+Wu097Hz1DG213NN1ntqd1Ibmnv/N533erMuW0GsYAezauktT+zTXVcMcXa/9vpCvBOsc5Y8mPLGWHtcj953oUbuCda1o8juuBrnb9VVNz2reEnwtk/KZpWFCcNucpaA6bnx5fxl9jwsCxhmRQqzTvyd84Zof0VEXkbLdUh426M3acrWFQqik3as0i2PfV2FZETVI71iZU6rv7Z0v/MoBGCbMfs7dNvCfarzeBuxXsE6a/0ruvalXyjIPv3iz/Wh915UISiN7FF1uXk6KZVZhsRsDgDYavT+Dn3jsXqN8HAbsZRgRRPtuvPX1wf+AVlzfHc8cqMinQG5vpaFoZVP9no+8IS97bpgtXnsErDbsKaEbl+wz4mXi2RWwZr32m80ceca2WDSjtW6Yvlv830YWTHDwMHlr/Z6O9MXUEjqmhO64/F6Hee+rE4yo2BFOhP60h++K5t8eel3FE7aO2yqLnvdWXO959jfzGYHCkn1oU7nmlaaJaKTAw7WR9c8q/G718kmE3at1YfXmhVq7OR2dmWez2JyKApRVbxTty7Kbu/DI8G6etmvZaOrlv2vbGSuW5nNIo5m7gjOXpvZzjaADcyabbcs2udMMs0kWk6wSjradOGKJ2Wji1csdm4W2Ka8dEOv4eDxe9tV29K/1UQBW5nHd25e3JDu0kfymBNHT930uga1udyVuu8+aehQBUZ9vXTjjSlvqow3afrmv+jN8WaxU3uUl6zv9bY0f+sABackkdQ/PrV/wHsfOsGauf5l91e95hpp3DgFxubNvYJlzFr3onXBMnsG9jRxNwvxoXhEE0l9bWmDs57Wy5PK+xWtcPcUAZtN2vmubOMWrDRzVYCCFemUvvL0fp3vPu8w6Ros2+4O9mTj8UcjDSm/N3+VjGjk+hWKT/jw3odm2eVjRcsJ1vDGXbLZ8AO9z1aCLtLjgrtZ1J8NJFCsQknp88836urlTX1GywlWRWuzbFbZ6vqHDLSedwjNzrpAsZu/rMl5jjadcPdyMjYra7Nv7pLZFOhonF0BXUY2JpxrW2mD1VrS67aiVeKlhbM2FlDMXp5Urv+82HUXHofz5paY+6YHtmiOuW6dDcAif5wySD+4KH2sDOdduwePks1214zO9yEAyMIfplXo5+fXKOm+FmnKVvN6f8RE2Wyj5ccPFLNFZ1Tqv923vTdSEuYE673Rp8hm60adnO9DAJBhrH794ep07w65PpqzbOJs9w9/9NHgPUvo4s+Tzsn5oQDInBn6PfzRav3u1LTXz9M/S7hi3Bk6GKvQoNYeM01dntsLmuayKq0cd1q+DwPAAGL14NmD9dT0Crd397mjjjMkbI+W6ukZc2SjpadeoUTY6S6AgOsMST+5oCajWBlHbiAumPkp2WjBLDuPGyg2ibCclRmeO8V1I+R+7VV4JFgvnXKBdXfbzMV2rl8BwdcRCen7l9bppZNcJ3n3e2PVI8HqDIX140tukE1+dMkNznEDCK62aEjfvrxOy8e7PlEzoF2gU37aF541X+tG2THFYc2YqVp85jX5PgwAfWgtCemeOXVaeVzM7d0D3rI+JVgdkRLdeu33lQwN+HVyyhzfN+bfy8V2IMAOxsL61pVDtGqsN7EynNtrL1z22ZQ3tuw5QZUPb1RQtVw3Xvde/zNJ5pedvvyXfB8B4J/msrDumjtEG0aUuL074zMiJ1hjK3anvvXuEdKK3ZL7CoD5NblClXcOV2VZj2O2zrB8HwDgi8ZBXWdWW4Z4GyvD/Yp1LCw9cLI0olSBMrJUevgUqYwL7UAQ1VdFdMfVQ32JlZH+J/+4MumRydLggEzKNMfxm8nSX7mOhwHk2d7qiP79qqHaWePaDE8ujPd9qjK5QnpymjQ6z2da5kxv4dSu4wEQODtqo7r9E0O1pzri9m7P7uJ1pXDoS+lf8GznXcdLelxSPh7ae1O7267W+W9tVjL9uudXLZwry7CIOwrCtrqoc81qf4W/sTKOfTGoKxKbJH1E0g9z+INmvs4PDn/dPmMFID82Di/RN64empNYGf27et0VC7NTxVcknSvpbflrpSSzZszfm7lnxAoInjWjSvXNeUPV5H4TzJfJnP2/3Wai1RWuFyWdLuk6SV5vuWy2oP6MpDPM441HfU0AAbJ6TEx3XzFEh0pdu+TbzPOBzw/oCojZU/2XkqZKukjSQ5Iy3Rzwg8Ofb15nmqSHndcnVEAg/WVcme6eW6d4SW5jZWQ2Z6E7JqGQ2T3smcO/viBp5uEhownZJElmdwizpKBZA7XRTICVZLZpXivpHUnPS1p+OICprw3rmNvZr51o95Zxfht8sFPnvXtQtlo2wWzDVaNEOPexOhKsjO+wLUhZ9M9E55XDvzJn390+HLa9LtrX+tyQdOmKAD49MoA9A816Vmm24crJA8gBmRUKFIcPbbBvl/LuPQN/ft6xt+HyG8ECcqTmYKcm7Wyzcs/AX/RzGy6/ESwgR2ZtOKRwsrC34fIbwYLfgr24mr9S8jRzg5nKaI9FAYuVQbCAHMSq+lCnTtneWtB7BuYCwQJyYKYlw8FkSHrg7MFa6r4NV97PmAkWkAOzLBgOdpo9Az9Wo+dPznwbLr8RLMB7KedSVfFOTQ74cLAjEtL9F9dqmfvE30DEyiBYgM/O2hhXxDwTEuBtuL5zWZ1WeLSzjZ8IFuCzWesPBXobrnsvr9M7Hu5s4yeCBfg4HBzU2qkp29sCuw3XXXPrtM7slWBBrAyCBfho5sa4oong3R78oLxrZ5vNQ/3ZLMIvBAsosruD+ysi+o8rh2hrnX+bRfiFYAE+Dgenbm0N3M42d145RLsH2xcrg2ABPjnz/bhKAjQc3FEbdWLVUJmb9df9QLCAIhgObq2LOsPAXG0W4ReCBXgj5VSqvC2p6VtaA7OzzV1XDMnpZhF+IViAD07fFIzh4LujS/XtObnfLMIvBAvwwYcCMFn0rXExfffjdc5M9kKIlUGwgOylnEqVdiQ1I8/DwTdOKNP3L61Ve6RwYmUQLMBjZ7wfd6JVzJtF+IVgAQV0d/CZqRX6L7P+ep43i/ALwQI8ZFZlmJ6nyaJLp1XogYBsFuEXggVkJ6UPk3a1OTPcc21RANdf9wPBAjw0Y3Puh4OPzKrS42dVFXysDIIFeOjUzbkbDiZD0kOzB+v3M4K5/rofCBbg0XDQbJQ6bl97ztZf/+kFNXruFNf11wsyVgbBAjwcDoaSOVp//aIaLZtQXjSh6kawAI+cloPhYJtF66/7gWABHjB7Dk7d1ur7ksb3zKnT2lH2LGnsNYIFZCZl8Ddhd5sq4/5NZ2gcFHZWXNhk2ZLGXiNYQMDvDu6t6lrSeGeNnauEeolgAR441af5V9tro85mETavEuolggVkyezsfMJe76czbBhRorvnFsbCe14hWEC2j+PsbPN8OsPqMaW69/LCWXjPKwQLyJJ5ftBLbx7ftZZVIS285xWCBWRpoofB6lrLqkaJMLFyQ7CALOdfnbjHm+tXT02v0INnF+5aVl4gWEAW6poTirVnfwGrWJaHyRbBAgYmpU51LYmsvn/mbOqXswdrSRGtuJANggVkIZuzK7Piws/Or9Gzk4trxYVsECwgy2tYmWgtMSsu1Or18WXpPoRYuSBYQBaaY+5b0/SloSKi+y6vc3ZkdkGo+kCwgIExQTlyXrWzJuIM7fp7pvXO2Jjuv7jWeZg5zWujDwQLyHLJl/UjSo85edQsumfWXl98WiXTFrIw8PNZACkWnpk2Qo6Vx8V006eG6YnTiVW2OMMCPHiUxmwG8devfKCSRNfY0GwRv2JcTL+fXqFVY11XB+3GMHAACBaQ5XUsw+xcY6YnjN/T5jwDuGVISbpnAXu+DgaAYAEeRSteEtLqMbH+fi4ywDUsIHOZhIdYZYFgAdkJDeDjiFWWGBIC2esO0dFDROLkA4IFeIdI+YwhIQBrECwA1iBYAKxBsABYg2ABsAbBAmANggXAGgQLgDUIFgBrECwA1iBYAKxBsABYg2ABsAbBAmANggXAGgQLgDUIFgBrECwA1iBYAKxBsABYg2ABsAbBAmANggXAGgQLgDUIFgBrECwA1iBYAKxBsABYg2ABsAbBAmANggXAGgQLgDUIFgBrECwA1iBYAKxBsABYg2ABsAbBAmANggXAGgQLgDUIFgBrECwA1iBYAKwRzfcBFLGQpGT3bxoqIvradSNks9ao+SMB/iFYAZEIS3uqI/k+DCDQGBLCT5xywVMEK78K+Qe6kP9syBOGhMH6wT5yTctihAq+IVh5ElWH25v5YQf6wJAQgDUIFgBrECwA1iBYAKxBsABYg2ABsAbBAmANggXAGgQLgDUIFgBrECwA1iBYAKxBsABYg2ABsAbBAmANggXAGgQLgDUIFgBrECwA1iBYAKxBsABYg2ABsAbBAmANggXAGgQLgGzxf9BVMv5f9CujAAAAAElFTkSuQmCC) **Customized next turn details abstract image** ###### Turn events[​](#turn-events "Direct link to Turn events") `ETurnEvent` is an enumeration that defines various types of turn events that can occur during navigation. These events help in providing accurate and context-aware navigation instructions to users. The different turn events are: * **NotAvailable** - No turn information available * **Straight** - Continue straight ahead * **Right** - Turn right * **Right1** - Turn right (variant 1) * **Right2** - Turn right (variant 2) * **Left** - Turn left * **Left1** - Turn left (variant 1) * **Left2** - Turn left (variant 2) * **LightLeft** - Slight left turn * **LightLeft1** - Slight left turn (variant 1) * **LightLeft2** - Slight left turn (variant 2) * **LightRight** - Slight right turn * **LightRight1** - Slight right turn (variant 1) * **LightRight2** - Slight right turn (variant 2) * **SharpRight** - Sharp right turn * **SharpRight1** - Sharp right turn (variant 1) * **SharpRight2** - Sharp right turn (variant 2) * **SharpLeft** - Sharp left turn * **SharpLeft1** - Sharp left turn (variant 1) * **SharpLeft2** - Sharp left turn (variant 2) * **RoundaboutExitRight** - Exit roundabout to the right * **Roundabout** - Enter roundabout * **RoundRight** - Round right maneuver * **RoundLeft** - Round left maneuver * **ExitRight** - Exit to the right * **ExitRight1** - Exit to the right (variant 1) * **ExitRight2** - Exit to the right (variant 2) * **InfoGeneric** - Generic information * **DriveOn** - Continue driving * **ExitNo** - No exit * **ExitLeft** - Exit to the left * **ExitLeft1** - Exit to the left (variant 1) * **ExitLeft2** - Exit to the left (variant 2) * **RoundaboutExitLeft** - Exit roundabout to the left * **IntoRoundabout** - Enter into roundabout * **StayOn** - Stay on current road * **BoatFerry** - Take boat ferry * **RailFerry** - Take rail ferry * **InfoLane** - Lane information * **InfoSign** - Sign information * **LeftRight** - Left then right maneuver * **RightLeft** - Right then left maneuver * **KeepLeft** - Keep to the left * **KeepRight** - Keep to the right * **Start** - Start of route * **Intermediate** - Intermediate waypoint * **Stop** - End of route #### Change the language of the instructions[​](#change-the-language-of-the-instructions "Direct link to Change the language of the instructions") The texts used in route instructions and related classes follow the language set in the SDK. See [the internationalization guide](/docs/android/guides/todo.md) for more details. --- ### Traffic Events |

The Maps SDK for Android provides real-time information about traffic events, such as delays, which can occur in various forms. When enabled and supported by the applied map style, traffic events are visually represented on the map as red overlays on affected road segments. Based on the source of the event: * Most traffic events are provided by the Magic Lane servers if online and provide up-to-date traffic data * The user can add custom user-defined roadblocks in order to blacklist certain road segments or areas. Based on the impact zone: * The traffic event can be a path-based traffic area, following the shape of a road. * The traffic event can be an area-based traffic area, including a larger geographic area. The central class for handling both traffic events and roadblocks is `TrafficEvent`. Instances of this class can be obtained either through user interaction with the map (e.g., selecting a road segment) or as part of user-defined roadblock operations. For route-specific traffic data, the SDK provides the `RouteTrafficEvent` class, which extends `TrafficEvent` by including detailed information relevant to a specific route. These events are provided by the route. Traffic events, including delays and user-defined roadblocks, are fully integrated into the routing and navigation logic. This ensures that calculated routes dynamically account for traffic conditions and any restricted segments. #### TrafficEvent structure[​](#trafficevent-structure "Direct link to TrafficEvent structure") The `TrafficEvent` class has the following structure: | Member | Type | Description | | ----------------------- | -------------------------- | --------------------------------------------------------------------------------------------------- | | `isRoadblock()` | Boolean | Returns `true` if the event represents a roadblock. | | `delay` | Int | Estimated delay in seconds caused by the traffic event. For roadblocks it returns `-1`. | | `length` | Int | Length in meters of the road segment affected by the traffic event. | | `referencePoint` | `Coordinates?` | The central coordinate for the event. Returns `null` if not available. | | `boundingBox` | `RectangleGeographicArea?` | Geographical bounding box surrounding the event. | | `description` | `String?` | Human-readable description of the traffic event. | | `eventClass` | `ETrafficEventClass` | Classification of the traffic event. | | `eventSeverity` | `ETrafficEventSeverity` | Severity level of the event. | | `image` | `Image?` | Image of the traffic event. Returns `null` if not available. | | `previewUrl` | `String?` | Returns a URL to preview the traffic event. Returns `null` if not available. | | `isUserRoadblock()` | `Boolean` | Returns `true` if the event is a user-defined roadblock. | | `affectedTransportMode` | Int | Returns all transport modes affected by the event as a bitmask. | | `startTime` | `Time?` | UTC start time of the traffic event, if available. | | `endTime` | `Time?` | UTC end time of the traffic event, if available. | | `hasOppositeSibling()` | `Boolean` | Returns `true` if a sibling event exists in the opposite direction. Relevant for path-based events. | #### RouteTrafficEvent structure[​](#routetrafficevent-structure "Direct link to RouteTrafficEvent structure") The `RouteTrafficEvent` class extends `TrafficEvent` with the following members: | Member | Type | Description | | ------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------ | | `distanceToDestination` | Int | Distance in meters from starting point on current route of the traffic event to the destination. | | `from` | `Coordinates?` | Route traffic event start point. | | `to` | `Coordinates?` | Route traffic event end point. | | `fromLandmark` | `Pair?` | Traffic event start point as landmark and a flag indicating if data is cached locally. | | `toLandmark` | `Pair?` | Traffic event end point as landmark and a flag indicating if data is cached locally. | | `asyncUpdateToFromData()` | Unit | Updates the `from` and `to` landmarks' address and description info from the server. | | `cancelUpdate()` | Unit | Cancels the pending async update request for landmark data. | #### Usage[​](#usage "Direct link to Usage") * Used to provide traffic information about a route. See the [Get ETA and Traffic information guide](/docs/android/guides/routing/get-started-routing.md#get-eta-and-traffic-information) for more details. * Used to implement user-defined roadblocks. See the [Get ETA and Traffic information guide](/docs/android/guides/routing/get-started-routing.md#get-eta-and-traffic-information) for more details. #### Traffic Event Classes and Severities[​](#traffic-event-classes-and-severities "Direct link to Traffic Event Classes and Severities") Traffic events provide insights into road conditions, delays, closures, and more. info Some events provided by the `ETrafficEventClass` are: * TrafficRestrictions * Roadworks * Parking * Delays * Accidents * RoadConditions info `ETrafficEventSeverity` possible values are: * Stationary * Queuing * SlowTraffic * PossibleDelay * Unknown --- ### Driver Behaviour |

The Driver Behaviour feature enables the analysis and scoring of a driver's behavior during a trip, identifying risky driving patterns and summarizing them with safety scores. This feature tracks both real-time and session-level driving events, such as harsh braking, cornering, or ignoring traffic signs, and evaluates overall risk using multiple criteria. This data can be used to offer user feedback, identify unsafe habits, and assess safety levels over time. All information is processed using on-device sensor data (via the configured `DataSource`) and optionally matched to the road network if `useMapMatchedPosition` is enabled. #### Starting and Stopping Analysis[​](#starting-and-stopping-analysis "Direct link to Starting and Stopping Analysis") To use the Driver Behaviour module, you first need to create a data source and then produce a `DriverBehaviour` instance using the factory method. The session is started using `startAnalysis()` and closed using `stopAnalysis()`, which returns a `DriverBehaviourAnalysis` instance representing the complete analysis. * Kotlin * Java ```kotlin val dataSource = DataSourceFactory.produceLive() val driverBehaviour = DriverBehaviour.produce(dataSource!!, useMapMatchedPosition = true) val started = driverBehaviour?.startAnalysis() ?: false if (!started) { Log.e("DriverBehaviour", "Failed to start analysis") return } // ... after some driving val result = driverBehaviour.stopAnalysis() if (result?.valid != true) { Log.w("DriverBehaviour", "The analysis is invalid and cannot be used") return } // Process the analysis result // processAnalysisResult(result) ``` ```java DataSource dataSource = DataSourceFactory.produceLive(); DriverBehaviour driverBehaviour = DriverBehaviour.produce(dataSource, true); boolean started = false; if (driverBehaviour != null) { started = driverBehaviour.startAnalysis(); } if (!started) { Log.e("DriverBehaviour", "Failed to start analysis"); return; } // ... after some driving DriverBehaviourAnalysis result = driverBehaviour.stopAnalysis(); if (result == null || !result.getValid()) { Log.w("DriverBehaviour", "The analysis is invalid and cannot be used"); return; } // Process the analysis result // processAnalysisResult(result); ``` danger All `DriverBehaviourAnalysis` instances expose a `valid` property to determine whether the analysis is valid. Always verify this property before accessing or relying on the data it contains. #### Inspecting a Driving Session[​](#inspecting-a-driving-session "Direct link to Inspecting a Driving Session") The result returned by `stopAnalysis()` (or via `lastAnalysis`) contains aggregate and detailed information on the trip: * Kotlin * Java ```kotlin if (result?.valid != true) { Log.w("DriverBehaviour", "The analysis is invalid and cannot be used") return } val startTime = result.startTime // Time object val finishTime = result.finishTime // Time object val distance = result.kilometersDriven // Double val drivingDuration = result.minutesDriven // Double val speedingTime = result.minutesSpeeding // Double val totalElapsedTime = result.minutesTotalElapsed // Double val tailgatingTime = result.minutesTailgating // Double ``` ```java if (result == null || !result.getValid()) { Log.w("DriverBehaviour", "The analysis is invalid and cannot be used"); return; } Time startTime = result.getStartTime(); // Time object Time finishTime = result.getFinishTime(); // Time object double distance = result.getKilometersDriven(); // Double double drivingDuration = result.getMinutesDriven(); // Double double speedingTime = result.getMinutesSpeeding(); // Double double totalElapsedTime = result.getMinutesTotalElapsed(); // Double double tailgatingTime = result.getMinutesTailgating(); // Double ``` The session also includes risk scores: * Kotlin * Java ```kotlin val scores = result.drivingScores if (scores == null) { Log.w("DriverBehaviour", "No driving scores available") return } val speedRisk = scores.speedAverageRiskScore val speedVariableRisk = scores.speedVariableRiskScore val harshAccelerationRisk = scores.harshAccelerationScore val brakingRisk = scores.harshBrakingScore val swervingRisk = scores.swervingScore val corneringRisk = scores.corneringScore val tailgatingRisk = scores.tailgatingScore val ignoredSignsRisk = scores.ignoredStopSignsScore val fatigue = scores.fatigueScore val overallScore = scores.aggregateScore ``` ```java DrivingScores scores = result.getDrivingScores(); if (scores == null) { Log.w("DriverBehaviour", "No driving scores available"); return; } double speedRisk = scores.getSpeedAverageRiskScore(); double speedVariableRisk = scores.getSpeedVariableRiskScore(); double harshAccelerationRisk = scores.getHarshAccelerationScore(); double brakingRisk = scores.getHarshBrakingScore(); double swervingRisk = scores.getSwervingScore(); double corneringRisk = scores.getCorneringScore(); double tailgatingRisk = scores.getTailgatingScore(); double ignoredSignsRisk = scores.getIgnoredStopSignsScore(); double fatigue = scores.getFatigueScore(); double overallScore = scores.getAggregateScore(); ``` info Each score ranges from 0 (unsafe) to 100 (safe). A score of -1 means invalid or unavailable. #### Inspecting Driving Events[​](#inspecting-driving-events "Direct link to Inspecting Driving Events") Use the `drivingEvents` property of the session result to access discrete driving incidents that were detected: * Kotlin * Java ```kotlin val events = result.drivingEvents events?.asArrayList()?.forEach { event -> val time = event.time val latitude = event.latitudeDeg val longitude = event.longitudeDeg val eventType = event.eventType Log.d("DriverBehaviour", "Event at $latitude, $longitude at ${time?.asLong()} with type $eventType") } ``` ```java DrivingEventList events = result.getDrivingEvents(); if (events != null) { for (DrivingEvent event : events.asArrayList()) { Time time = event.getTime(); double latitude = event.getLatitudeDeg(); double longitude = event.getLongitudeDeg(); EDrivingEvent eventType = event.getEventType(); Log.d("DriverBehaviour", "Event at " + latitude + ", " + longitude + " at " + (time != null ? time.asLong() : 0) + " with type " + eventType); } } ``` Event types are defined by the `EDrivingEvent` enum: #### Driving Event Types[​](#driving-event-types "Direct link to Driving Event Types") | Enum Value | Description | | ------------------- | ---------------------- | | `NoEvent` | No event | | `StartingTrip` | Starting a trip | | `FinishingTrip` | Finishing a trip | | `Resting` | Resting | | `HarshAcceleration` | Harsh acceleration | | `HarshBraking` | Harsh braking | | `Cornering` | Cornering | | `Swerving` | Swerving | | `Tailgating` | Tailgating | | `IgnoringSigns` | Ignoring traffic signs | #### Real-time Feedback[​](#real-time-feedback "Direct link to Real-time Feedback") If the analysis is ongoing, you can fetch real-time scores using: * Kotlin * Java ```kotlin val instantScores = driverBehaviour?.instantaneousScores instantScores?.let { scores -> val currentSpeedRisk = scores.speedAverageRiskScore val currentBrakingRisk = scores.harshBrakingScore val currentOverallScore = scores.aggregateScore // Use scores for real-time feedback updateUIWithScores(currentSpeedRisk, currentBrakingRisk, currentOverallScore) } ``` ```java DrivingScores instantScores = driverBehaviour != null ? driverBehaviour.getInstantaneousScores() : null; if (instantScores != null) { double currentSpeedRisk = instantScores.getSpeedAverageRiskScore(); double currentBrakingRisk = instantScores.getHarshBrakingScore(); double currentOverallScore = instantScores.getAggregateScore(); // Use scores for real-time feedback updateUIWithScores(currentSpeedRisk, currentBrakingRisk, currentOverallScore); } ``` These reflect the user's current behavior and are useful for immediate in-app feedback. #### Stop Analysis and Get Last Analysis[​](#stop-analysis-and-get-last-analysis "Direct link to Stop Analysis and Get Last Analysis") To stop an ongoing analysis you can use: * Kotlin * Java ```kotlin val analysis = driverBehaviour?.stopAnalysis() if (analysis?.valid != true) { Log.w("DriverBehaviour", "No valid analysis available") return } // Process the completed analysis processCompletedAnalysis(analysis) ``` ```java DriverBehaviourAnalysis analysis = driverBehaviour != null ? driverBehaviour.stopAnalysis() : null; if (analysis == null || !analysis.getValid()) { Log.w("DriverBehaviour", "No valid analysis available"); return; } // Process the completed analysis processCompletedAnalysis(analysis); ``` You can also retrieve the last completed analysis: * Kotlin * Java ```kotlin val lastAnalysis = driverBehaviour?.lastAnalysis if (lastAnalysis?.valid != true) { Log.w("DriverBehaviour", "No valid analysis available") return } // Process the last analysis processLastAnalysis(lastAnalysis) ``` ```java DriverBehaviourAnalysis lastAnalysis = driverBehaviour != null ? driverBehaviour.getLastAnalysis() : null; if (lastAnalysis == null || !lastAnalysis.getValid()) { Log.w("DriverBehaviour", "No valid analysis available"); return; } // Process the last analysis processLastAnalysis(lastAnalysis); ``` #### Retrieve Past Analyses[​](#retrieve-past-analyses "Direct link to Retrieve Past Analyses") All completed sessions are stored locally and accessible via: * Kotlin * Java ```kotlin val pastSessions = driverBehaviour?.allDriverBehaviourAnalyses pastSessions?.forEach { analysis -> if (analysis.valid) { // Process each valid analysis processAnalysis(analysis) } } ``` ```java List pastSessions = driverBehaviour != null ? driverBehaviour.getAllDriverBehaviourAnalyses() : null; if (pastSessions != null) { for (DriverBehaviourAnalysis analysis : pastSessions) { if (analysis.getValid()) { // Process each valid analysis processAnalysis(analysis); } } } ``` You can also obtain a combined analysis over a time interval: * Kotlin * Java ```kotlin val startTime = Time().apply { longValue = System.currentTimeMillis() - (7 * 24 * 60 * 60 * 1000) // 7 days ago } val endTime = Time().apply { longValue = System.currentTimeMillis() // Now } val combined = driverBehaviour?.getCombinedAnalysis(startTime, endTime) if (combined?.valid == true) { // Process combined analysis processCombinedAnalysis(combined) } ``` ```java Time startTime = new Time(); startTime.setLongValue(System.currentTimeMillis() - (7L * 24 * 60 * 60 * 1000)); // 7 days ago Time endTime = new Time(); endTime.setLongValue(System.currentTimeMillis()); // Now DriverBehaviourAnalysis combined = driverBehaviour != null ? driverBehaviour.getCombinedAnalysis(startTime, endTime) : null; if (combined != null && combined.getValid()) { // Process combined analysis processCombinedAnalysis(combined); } ``` #### Analyses Storage Location[​](#analyses-storage-location "Direct link to Analyses Storage Location") Driver behaviour analyses are stored locally on the device. Inside the app's directory, a folder named **`DriverBehaviour`** is created (at the same level as `Data`). #### Data Cleanup[​](#data-cleanup "Direct link to Data Cleanup") To save space or comply with privacy policies, older sessions can be erased: * Kotlin * Java ```kotlin val thirtyDaysAgo = Time().apply { longValue = System.currentTimeMillis() - (30L * 24 * 60 * 60 * 1000) // 30 days ago } driverBehaviour?.eraseAnalysesOlderThan(thirtyDaysAgo) ``` ```java Time thirtyDaysAgo = new Time(); thirtyDaysAgo.setLongValue(System.currentTimeMillis() - (30L * 24 * 60 * 60 * 1000)); // 30 days ago if (driverBehaviour != null) { driverBehaviour.eraseAnalysesOlderThan(thirtyDaysAgo); } ``` info Driver behaviour analysis requires a properly configured DataSource. Use `DataSourceFactory.produceLive()` to create a live data source for real-time position updates. To ensure reliable results, make sure to start and stop the analysis appropriately and avoid frequent interruptions or overlapping sessions. #### Memory Management[​](#memory-management "Direct link to Memory Management") Remember to properly release resources when done: * Kotlin * Java ```kotlin driverBehaviour?.stopAnalysis() driverBehaviour?.release() dataSource?.release() ``` ```java if (driverBehaviour != null) { driverBehaviour.stopAnalysis(); driverBehaviour.release(); } if (dataSource != null) { dataSource.release(); } ``` #### Background Location[​](#background-location "Direct link to Background Location") For driver behavior analysis while the app is in the background, ensure your app has the necessary location permissions: * Kotlin * Java ```kotlin private fun requestPermissions(activity: Activity): Boolean { val permissions = arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION // For API 29+ ) return PermissionsHelper.requestPermissions(REQUEST_PERMISSIONS, activity, permissions) } ``` ```java private boolean requestPermissions(Activity activity) { String[] permissions = new String[]{ Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION // For API 29+ }; return PermissionsHelper.requestPermissions(REQUEST_PERMISSIONS, activity, permissions); } ``` --- ### Get started The Magic Lane SDKs enable developers to build advanced navigation and mapping applications with minimal setup. The guides below walk you through creating an API key, integrating the SDK of your choice, building your first application, and following best practices for optimal performance. #### [📄️ Integrate the SDK](/docs/android/guides/get-started/integrate-sdk.md) [This guide describes the steps to install the most recent version of the Maps SDK for Android and configure your Android app to use the SDK.](/docs/android/guides/get-started/integrate-sdk.md) #### [📄️ Create your first app](/docs/android/guides/get-started/create-your-first-app.md) [This guide describes the steps to create a new Android application that uses the Maps SDK for Android.](/docs/android/guides/get-started/create-your-first-app.md) #### [📄️ Usage guidelines](/docs/android/guides/get-started/usage-guildelines.md) [Legal requirements](/docs/android/guides/get-started/usage-guildelines.md) #### [📄️ Coding with AI](/docs/android/guides/get-started/coding-with-ai.md) [Use LLM-friendly documentation files and AI coding assistants to accelerate development with the Magic Lane Maps SDK for Android.](/docs/android/guides/get-started/coding-with-ai.md) --- ### Coding with AI |

Magic Lane provides machine-readable documentation files that integrate with popular AI coding assistants. Use them to get accurate, context-aware answers about the Maps SDK for Android while you code. #### Available resources[​](#available-resources "Direct link to Available resources") Two files are generated with every documentation build and published alongside the docs: | File | Contents | URL | | ----------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | **llms-full.txt** | Developer guides, explanations, and code examples | [developer.magiclane.com/docs/android/llms-full.txt](https://developer.magiclane.com/docs/android/llms-full.txt) | | **llms.txt** | Concise API overview and page index | [developer.magiclane.com/docs/android/llms.txt](https://developer.magiclane.com/docs/android/llms.txt) | These files follow the [llms.txt](https://llmstxt.org/) standard and contain detailed concept explanations, code examples, documentation references, and source file locations for verification. #### Using with AI coding tools[​](#using-with-ai-coding-tools "Direct link to Using with AI coding tools") Download both files and place them in your project. Each tool reads them differently - follow the setup for the one you use. ##### Cursor[​](#cursor "Direct link to Cursor") 1. Create a `.cursor` directory in your project root. 2. Place `llms-full.txt` and `llms.txt` inside it. 3. Open **Cursor Settings ? Rules** and add a new rule with type **Apply Intelligently**. 4. Set the description to: *Use this rule whenever a question is asked about Magic Lane Maps SDK for Android.* 5. Reference the files in the rule body. Cursor will automatically use them when relevant. ##### GitHub Copilot[​](#github-copilot "Direct link to GitHub Copilot") 1. Create a `.copilot` directory in your project root. 2. Place `llms-full.txt` and `llms.txt` inside it. 3. In Copilot Chat, reference the files by stating: ```text Please use the context from the llms.txt files in the .copilot directory when answering questions about the Magic Lane Maps SDK. ``` ##### JetBrains AI Assistant[​](#jetbrains-ai-assistant "Direct link to JetBrains AI Assistant") 1. Create a `.idea/ai` directory in your project root. 2. Place `llms-full.txt` and `llms.txt` inside it. 3. Reference the files in AI Assistant chat when asking about the SDK. ##### Windsurf[​](#windsurf "Direct link to Windsurf") 1. Create a `.windsurf` directory in your project root. 2. Place `llms-full.txt` and `llms.txt` inside it. 3. Reference the files in chat for context-aware responses. ##### Claude Desktop / Claude Code[​](#claude-desktop--claude-code "Direct link to Claude Desktop / Claude Code") Pass the `llms-full.txt` URL directly in your prompt: ``` Use https://developer.magiclane.com/docs/android/llms-full.txt as context for the following question about the Magic Lane Maps SDK for Android: [YOUR QUESTION HERE] ``` #### Context7 MCP server[​](#context7-mcp-server "Direct link to Context7 MCP server") For a more integrated experience, use the [Context7 MCP server](https://context7.com/magiclane/magiclane-maps-sdk-for-android-docs-context7) which connects AI tools directly to the Magic Lane documentation. info **Requirements:** * Latest version of Visual Studio Code * [GitHub Copilot extension](https://code.visualstudio.com/docs/copilot/overview) enabled * GitHub account sign-in ##### Step 1: Install the Context7 MCP server[​](#step-1-install-the-context7-mcp-server "Direct link to Step 1: Install the Context7 MCP server") Open the **Extensions** tab in Visual Studio Code and search for the [Context7 MCP Server](vscode:extension/Upstash.context7-mcp). Click **Install**. Verify the Context7 MCP Server appears under **MCP Servers - Installed** in the Extensions tab. Right-click on the Context7 MCP Server and select **Start Server**. ##### Step 2: Configure in Copilot Chat[​](#step-2-configure-in-copilot-chat "Direct link to Step 2: Configure in Copilot Chat") In Copilot Chat, click **Configure Tools...**. Select the `MCP Server: Context7` option from the available tools. Set the chat to **Agent mode**. Your first prompt should include a variation of this text: ```text Always use context7 for each request. Do never assume the name of classes or methods. Use multiple requests to context7 if required. Check the classes or methods. Use the Magic Lane Maps SDK for Android Documentation docs: https://context7.com/magiclane/magiclane-maps-sdk-for-android-docs-context7 [ENTER WHAT YOU NEED TO DO HERE] ``` info You can add this prompt to the Custom Instruction file. See the [VS Code docs](https://code.visualstudio.com/docs/copilot/copilot-customization?originUrl=%2Fdocs%2Fcopilot%2Fchat%2Fcopilot-chat-context#_custom-instructions) for details. Grant access to the **Context7 MCP Server** when prompted. The LLM will have full access to the Magic Lane SDK documentation. Context7 also works with Cursor, Claude Desktop, and Windsurf. #### Tips[​](#tips "Direct link to Tips") * Include `use context7` in your prompts if the AI references classes or methods that don't exist. * Experiment with different prompts and models to get the best results. * The `llms-full.txt` file is best for general questions and guides. Use `llms.txt` for a quick overview of available pages. * Always verify generated code against the official [API reference](/docs/android/api-reference/). --- ### Create your first app |

This guide describes the steps to create a new Android application that uses the Maps SDK for Android. You will create a simple application that displays a map. This tutorial has 4 parts: * Create and use an API key * Create an Android project * Integrate the Maps SDK for Android * Implement the application code #### Prerequisites[​](#prerequisites "Direct link to Prerequisites") * You have Android Studio installed. * You have a basic understanding of Android development and Kotlin programming language. ##### Create and use an API key[​](#create-and-use-an-api-key "Direct link to Create and use an API key") Follow our [step-by-step guide](https://developer.magiclane.com/docs/guides/get-started) to sign up for a free account, create a project, generate your API key and start building your Android solution. danger If the API key is not configured, some features will be restricted, and a watermark will appear on the map. Functionality such as map downloads, updates, and other features may be unavailable or may not function as expected. Please ensure the API token is set correctly. Ensure the API key is stored securely and protected against unauthorized access or exposure. The key must be put in the `AndroidManifest.xml` file, inside the `` tag. Replace `` with your actual API key token. ```xml ``` #### Create an Android project[​](#create-an-android-project "Direct link to Create an Android project") 1. Open Android Studio and create a new project by selecting
`File` > `New` > `New Project...`. 2. **For Android Views project structure**: Select the `No Activity` template and click `Next`. **For Jetpack Compose project structure**: Select the `Empty Activity` template and click `Next`. 3. Enter a name for your application, such as `MagicLaneHelloMap`. 4. Set the package name to `com.example.magiclanehellomap`. 5. Choose a save location, and select `Kotlin` as the language. 6. Set the minimum API level to `API 21: Android 5.0 (Lollipop)` or higher. 7. Click `Finish` to create the project. ![](/docs/android/assets/images/create-Hello-World-app-fc24b844e5de3ea1748d9f540ac18868.png) **Create new project** #### Integrate the Maps SDK for Android[​](#integrate-the-maps-sdk-for-android "Direct link to Integrate the Maps SDK for Android") There are 2 ways to integrate the SDK: * via Maven dependency * via an .aar downloaded file See the [Integrate the SDK](/docs/android/guides/get-started/integrate-sdk.md) guide to add the SDK to your project. #### Implement the application code[​](#implement-the-application-code "Direct link to Implement the application code") Let's create a simple app that initializes the SDK engine and displays a map. ##### For Android Views project structure[​](#for-android-views-project-structure "Direct link to For Android Views project structure") Create a new empty Activity named `MainActivity.kt`. Right click on the package name from the Project window then select `New > Activity > Empty Views Activity`. Replace its content with the following code: * Kotlin * Java ```kotlin package com.example.magiclanehellomap import android.os.Bundle import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import com.magiclane.sdk.core.GemSdk import com.magiclane.sdk.core.SdkSettings class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContentView(R.layout.activity_main) ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets } SdkSettings.onApiTokenRejected = { Toast.makeText(this, "TOKEN REJECTED", Toast.LENGTH_LONG).show() } if (!GemSdk.initSdkWithDefaults(this)) { // The SDK initialization was not completed. finish() } } override fun onDestroy() { super.onDestroy() // Release the SDK. GemSdk.release() } } ``` ```java package com.example.magiclanehellomap; import android.os.Bundle; import android.widget.Toast; import androidx.activity.EdgeToEdge; import androidx.appcompat.app.AppCompatActivity; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.magiclane.sdk.core.GemSdk; import com.magiclane.sdk.core.SdkSettings; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EdgeToEdge.enable(this); setContentView(R.layout.activity_main); ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { WindowInsetsCompat.Type.InsetsType systemBars = WindowInsetsCompat.Type.systemBars(); v.setPadding( insets.getInsets(systemBars).left, insets.getInsets(systemBars).top, insets.getInsets(systemBars).right, insets.getInsets(systemBars).bottom ); return insets; }); SdkSettings.setOnApiTokenRejected(() -> { Toast.makeText(this, "TOKEN REJECTED", Toast.LENGTH_LONG).show(); }); if (!GemSdk.initSdkWithDefaults(this)) { // The SDK initialization was not completed. finish(); } } @Override protected void onDestroy() { super.onDestroy(); // Release the SDK. GemSdk.release(); } } ``` In the above code, we initialize the SDK engine with default parameters using `GemSdk.initSdkWithDefaults(this)`. We also set up a callback to handle the case where the API token is rejected. Finally, we release the SDK in the `onDestroy` method. Replace the content `of R.layout.activity_main` with the following layout file `res/layout/activity_main.xml`: ```xml ``` In the above layout, we added a `GemSurfaceView` which is the view that will display the map. info `GemSurfaceView` will initialize the SDK engine with default parameters if it is not already initialized. In the `AndroidManifest.xml` you need to make the `MainActivity` the default activity in the application like this: ```xml ``` ##### For Jetpack Compose project structure[​](#for-jetpack-compose-project-structure "Direct link to For Jetpack Compose project structure") When you created the project using the `Empty Activity` template, a `MainActivity.kt` file was automatically created for you. Replace its content with the following code: * Kotlin * Java ```kotlin package com.example.magiclanehellomap import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import com.example.magiclanehellomap.ui.theme.MagicLaneHelloMapTheme import com.magiclane.sdk.core.GemSdk import com.magiclane.sdk.core.GemSurfaceView class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { MagicLaneHelloMapTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> GEMMap(Modifier.padding(innerPadding)) } } } if (!GemSdk.initSdkWithDefaults(this)) finish() } override fun onDestroy() { super.onDestroy() GemSdk.release() } } @Composable fun GEMMap(modifier: Modifier = Modifier) { AndroidView(modifier = modifier, factory = { context -> GemSurfaceView(context) }) } ``` ```java package com.example.magiclanehellomap; import android.os.Bundle; import androidx.activity.ComponentActivity; import androidx.activity.EdgeToEdge; import com.magiclane.sdk.core.GemSdk; import com.magiclane.sdk.core.GemSurfaceView; // Note: Jetpack Compose is Kotlin-first. For Java, use the Android Views // approach with GemSurfaceView in an XML layout instead. // This example shows the Activity setup only. public class MainActivity extends ComponentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EdgeToEdge.enable(this); // For Java, use setContentView with an XML layout containing GemSurfaceView setContentView(R.layout.activity_main); if (!GemSdk.initSdkWithDefaults(this)) finish(); } @Override protected void onDestroy() { super.onDestroy(); GemSdk.release(); } } ``` The Composable function named `GEMMap` will display the map using `GemComposeView`. We initialize the SDK engine with default parameters and release the SDK in the `onDestroy` method. #### Final steps[​](#final-steps "Direct link to Final steps") ##### Add necessary permissions and run the app[​](#add-necessary-permissions-and-run-the-app "Direct link to Add necessary permissions and run the app") warning Make sure you have the necessary permissions declared in you AndroidManifest file and internet connection to load the map tiles! Add these network permissions to your `AndroidManifest.xml` file before the `` tag in order to access the network: ```xml ``` For more information on this topic see **[how to declare permissions in Android](https://developer.android.com/training/permissions/declaring)** and **[how to connect to the network](https://developer.android.com/develop/connectivity/network-ops/connecting)**. Run the app on an emulator or a physical device. You should see a map displayed on the screen with a watermark text. In order to get rid of it, you must also use an API key. ##### What's next?[​](#whats-next "Direct link to What's next?") Check out our sample apps to get a better hands on understanding of our SDK. Go to the [Get Started With Sample Apps](/docs/android/examples/get-started-with-sample-apps.md) guide or explore more features in the Guides next section. --- ### Integrate the SDK |

This guide describes the steps to install the most recent version of the Maps SDK for Android and configure your Android app to use the SDK. #### Prerequisites[​](#prerequisites "Direct link to Prerequisites") Download and install Android Studio from the official [Android Developer website](https://developer.android.com/studio). info The procedures described in this document were carried out with Android Studio Narwhal 3 Feature Drop | 2025.1.3. #### Option 1: Maven dependency[​](#option-1-maven-dependency "Direct link to Option 1: Maven dependency") Magic Lane provides the Maps SDK for Android via Maven. ##### Step 1: Configure the Maven repository[​](#step-1-configure-the-maven-repository "Direct link to Step 1: Configure the Maven repository") Open your android project and open `settings.gradle.kts` file. Inside `dependencyResolutionManagement` tag and it's `repositiories` child add the following maven dependency : * Kotlin * Java ```kotlin ... dependencyResolutionManagement { ... repositories { ... maven { url = uri("https://developer.magiclane.com/packages/android") } } } ``` ```java // In settings.gradle (Groovy DSL) // ... dependencyResolutionManagement { // ... repositories { // ... maven { url 'https://developer.magiclane.com/packages/android' } } } ``` ##### Step 2: Add the Maps SDK for Android dependency[​](#step-2-add-the-maps-sdk-for-android-dependency "Direct link to Step 2: Add the Maps SDK for Android dependency") Open your module-level `build.gradle.kts` file and add the following code inside the dependencies block to include the Maps SDK for Android: * Kotlin * Java ```kotlin dependencies { ... implementation("com.magiclane:maps-kotlin:1.7.0") } ``` ```java // In build.gradle (Groovy DSL) dependencies { // ... implementation 'com.magiclane:maps-kotlin:1.7.0' } ``` After that, press `Sync Project with Gradle files`. #### Option 2: Local AAR library[​](#option-2-local-aar-library "Direct link to Option 2: Local AAR library") Optionally you can download the Maps SDK for Android `.aar` file, then create the following project structure: **your\_project/app/libs/**. | Version | Download | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1.8.4 (Latest) | [MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.26.12.ADB30465\_x86\_64-armeabi\_v7a-arm64\_v8a.zip](https://developer.magiclane.com/sdk/sdk/Android/20260320/MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.26.12.ADB30465_x86_64-armeabi_v7a-arm64_v8a.zip) | | 1.8.3 | [MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.26.12.FC99A40E\_x86\_64-armeabi\_v7a-arm64\_v8a.zip](https://developer.magiclane.com/sdk/sdk/Android/20260318/MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.26.12.FC99A40E_x86_64-armeabi_v7a-arm64_v8a.zip) | | 1.8.2 | [MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.26.9.45AF722D\_armeabi\_v7a-arm64\_v8a-x86\_64.zip](https://developer.magiclane.com/sdk/sdk/Android/20260226/MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.26.9.45AF722D_armeabi_v7a-arm64_v8a-x86_64.zip) | | 1.8.1 | [MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.26.8.4EE9C177\_armeabi\_v7a-arm64\_v8a-x86\_64.zip](https://developer.magiclane.com/sdk/sdk/Android/20260219/MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.26.8.4EE9C177_armeabi_v7a-arm64_v8a-x86_64.zip) | | 1.8.0 | [MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.26.8.79E4DAF7\_armeabi\_v7a-arm64\_v8a-x86\_64.zip](https://developer.magiclane.com/sdk/sdk/Android/20260216/MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.26.8.79E4DAF7_armeabi_v7a-arm64_v8a-x86_64.zip) | | 1.7.1 | [MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.25.47.AA1CA1F2\_armeabi\_v7a-arm64\_v8a-x86\_64.zip](https://developer.magiclane.com/sdk/sdk/Android/20251120/MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.25.47.AA1CA1F2_armeabi_v7a-arm64_v8a-x86_64.zip) | | 1.7.0 | [MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.25.41.85504BAA\_x86\_64-x86-armeabi\_v7a-arm64\_v8a.zip](https://developer.magiclane.com/sdk/sdk/Android/20251008/MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.25.41.85504BAA_x86_64-x86-armeabi_v7a-arm64_v8a.zip) | | 1.6.2 | [MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.25.29.063A32EE\_x86\_64-x86-armeabi\_v7a-arm64\_v8a.zip](https://developer.magiclane.com/sdk/sdk/Android/20250716/MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.25.29.063A32EE_x86_64-x86-armeabi_v7a-arm64_v8a.zip) | | 1.6.1 | [MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.25.24.A0E07DC1\_x86\_64-x86-armeabi\_v7a-arm64\_v8a.zip](https://developer.magiclane.com/sdk/sdk/Android/20250615/MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.25.24.A0E07DC1_x86_64-x86-armeabi_v7a-arm64_v8a.zip) | | 1.6.0 | [MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.25.22.5235077E\_x86\_64-x86-armeabi\_v7a-arm64\_v8a.zip](https://developer.magiclane.com/sdk/sdk/Android/20250529/MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.25.22.5235077E_x86_64-x86-armeabi_v7a-arm64_v8a.zip) | | 1.5.5 | [MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.25.10.7B562D2D\_x86\_64-x86-armeabi\_v7a-arm64\_v8a.zip](https://developer.magiclane.com/sdk/sdk/Android/20250303/MAGICLANE-MAPS-SDK-ANDROID-KOTLIN-7.1.25.10.7B562D2D_x86_64-x86-armeabi_v7a-arm64_v8a.zip) | Add the `.aar` in the **libs** directory and the following code inside the dependencies block from the module-level `build.gradle.kts` file to include the Maps SDK for Android: * Kotlin * Java ```kotlin dependencies { implementation(files("libs/.aar")) } ``` ```java // In build.gradle (Groovy DSL) dependencies { implementation files('libs/.aar') } ``` You are now ready to go to the next step: [Implement the application code](/docs/android/guides/get-started/create-your-first-app.md#implement-the-application-code). --- ### Usage guidelines | #### Legal requirements[​](#legal-requirements "Direct link to Legal requirements") Some features, such as police and speed camera reports, may be illegal in certain countries. Ensure that the `safety` overlay and the corresponding entries in `socialReports` overlay are disabled where applicable. Additionally, restrict the availability of the `SocialOverlay` features based on current local regulations. The Magic Lane Android SDK utilizes data from OpenStreetMap. Please ensure proper attribution is given in accordance with the [OpenStreetMap license](https://osmfoundation.org/wiki/Licence). Any use or display of Wikipedia content must include appropriate attribution, as outlined in the [Reusers' rights and obligations](https://en.wikipedia.org/wiki/Wikipedia:Copyrights#Reusers'_rights_and_obligations) section of Wikipedia's copyright guidelines. #### SDK thread[​](#sdk-thread "Direct link to SDK thread") The Maps SDK for Android is designed to be thread-safe. Most operations must be performed on specific threads to ensure optimal performance and avoid potential issues. You can interact with SDK objects only from the SDK thread. The SDK thread is initialized when the SDK is initialized and runs in the background. You can use the `SDKCall.execute` method to run code on the SDK thread. * Kotlin * Java ```kotlin SDKCall.execute { // Your code here } ``` ```java SDKCall.execute(() -> { // Your code here }); ``` This ensures that the code inside the block runs on the SDK thread, allowing safe interaction with SDK objects. danger If you perform SDK operations from a different thread the sdk will throw an exception: `java.lang.Exception: Please ensure that your call is placed on GEMSdkThread or GLThread. See GemCall.execute{ ... }!` #### Units of measurement[​](#units-of-measurement "Direct link to Units of measurement") The SDK primarily uses SI units for measurements, including: * Meter (distance). * Second (time). * Kilogram (mass). * Meters per second (speed). * Watts for power (consumption). The length value and measurement unit used in TTS (text-to-speech) instructions, such as "After 150 meters, bear left," are determined by the unit system configured through the `SDKSettings.unitSystem` setter. danger In certain cases, other units of measurement are used as they are better suited. For instance, the `height`, `length` and `width` fields of the `TruckProfile` class are measured in centimeters. These exceptions are clearly indicated in the API reference. #### Error handling[​](#error-handling "Direct link to Error handling") The Maps SDK for Android uses a standardized error handling system through the `GemError` class. All SDK operations return error codes that indicate the success or failure of the requested operation. ##### Understanding Error Codes[​](#understanding-error-codes "Direct link to Understanding Error Codes") Error codes are represented as integers (type alias `ErrorCode = Int`). The SDK provides various error constants in the `GemError` object: * **`GemError.NoError` (0)**: Operation completed successfully * **`GemError.Cancel` (-3)**: Operation was canceled (not considered an error) * **Negative values**: Indicate various error conditions ##### Checking for Errors[​](#checking-for-errors "Direct link to Checking for Errors") Use `GemError.isError()` to determine if an error code represents an actual error: * Kotlin * Java ```kotlin val errorCode = someSDKOperation() if (GemError.isError(errorCode)) { // Handle the error val errorMessage = GemError.getMessage(errorCode, context) Log.e("SDK", "Operation failed: $errorMessage") } else { // Operation succeeded or was canceled } ``` ```java int errorCode = someSDKOperation(); if (GemError.isError(errorCode)) { // Handle the error String errorMessage = GemError.getMessage(errorCode, context); Log.e("SDK", "Operation failed: " + errorMessage); } else { // Operation succeeded or was canceled } ``` ##### Getting Error Messages[​](#getting-error-messages "Direct link to Getting Error Messages") The `GemError.getMessage()` method provides human-readable error descriptions: * Kotlin * Java ```kotlin val errorMessage = GemError.getMessage(errorCode, context) ``` ```java String errorMessage = GemError.getMessage(errorCode, context); ``` ##### Common Error Types[​](#common-error-types "Direct link to Common Error Types") The SDK defines several categories of errors: **File and I/O Errors:** * `GemError.Io` (-6): General I/O error * `GemError.AccessDenied` (-7): Access denied to resource * `GemError.NotFound` (-11): Required item not found * `GemError.NoDiskSpace` (-9): Insufficient disk space **Network Errors:** * `GemError.NoConnection` (-24): No network connection available * `GemError.NetworkTimeout` (-29): Network operation timed out * `GemError.NetworkFailed` (-23): Network connection failed * `GemError.ConnectionRequired` (-25): Network connection required **Routing and Navigation Errors:** * `GemError.NoRoute` (-18): No route could be calculated * `GemError.WaypointAccess` (-19): One or more waypoints not accessible * `GemError.RouteTooLong` (-20): Route is too long, use intermediate waypoints **Resource and Memory Errors:** * `GemError.NoMemory` (-14): Insufficient memory * `GemError.ResourceMissing` (-36): Required resources missing, reinstall needed * `GemError.InvalidInput` (-15): Invalid input provided ##### Handling Asynchronous Operations[​](#handling-asynchronous-operations "Direct link to Handling Asynchronous Operations") Many SDK operations are asynchronous and use listeners to report completion. Use `ProgressListener` to handle errors in async operations: * Kotlin * Java ```kotlin val listener = ProgressListener.create( onCompleted = { errorCode, hint -> if (GemError.isError(errorCode)) { val errorMessage = GemError.getMessage(errorCode, this) // Handle error appropriately showErrorDialog("Operation failed: $errorMessage") } else { // Handle successful completion onOperationSuccess() } } ) ``` ```java ProgressListener listener = ProgressListener.create( (errorCode, hint) -> { if (GemError.isError(errorCode)) { String errorMessage = GemError.getMessage(errorCode, this); // Handle error appropriately showErrorDialog("Operation failed: " + errorMessage); } else { // Handle successful completion onOperationSuccess(); } } ); ``` ##### Best Practices[​](#best-practices "Direct link to Best Practices") 1. **Always check error codes**: Never assume operations succeed without checking the return value 2. **Provide user feedback**: Convert technical error codes to user-friendly messages 3. **Handle network errors gracefully**: Implement retry logic for network-related operations 4. **Log errors for debugging**: Include error codes and messages in your application logs 5. **Handle specific error types**: Implement different handling strategies for different error categories * Kotlin * Java ```kotlin // Example: Comprehensive error handling fun handleSDKError(errorCode: ErrorCode, context: Context) { when (errorCode) { GemError.NoError -> { // Success - no action needed } GemError.Cancel -> { // Operation was canceled by user showMessage("Operation canceled") } GemError.NoConnection, GemError.NetworkFailed, GemError.NetworkTimeout -> { // Network-related errors showRetryDialog("Network error. Please check your connection and try again.") } GemError.NoRoute -> { // Routing specific error showMessage("Could not calculate route. Please check your destination.") } GemError.ResourceMissing -> { // Critical error requiring app reinstall showMessage("App resources are missing. Please reinstall the application.") } else -> { // Generic error handling val message = GemError.getMessage(errorCode, context) showMessage("Error: $message") } } } ``` ```java // Example: Comprehensive error handling public void handleSDKError(int errorCode, Context context) { if (errorCode == GemError.NoError) { // Success - no action needed } else if (errorCode == GemError.Cancel) { // Operation was canceled by user showMessage("Operation canceled"); } else if (errorCode == GemError.NoConnection || errorCode == GemError.NetworkFailed || errorCode == GemError.NetworkTimeout) { // Network-related errors showRetryDialog("Network error. Please check your connection and try again."); } else if (errorCode == GemError.NoRoute) { // Routing specific error showMessage("Could not calculate route. Please check your destination."); } else if (errorCode == GemError.ResourceMissing) { // Critical error requiring app reinstall showMessage("App resources are missing. Please reinstall the application."); } else { // Generic error handling String message = GemError.getMessage(errorCode, context); showMessage("Error: " + message); } } ``` #### Do not extend the classes provided by the SDK[​](#do-not-extend-the-classes-provided-by-the-sdk "Direct link to Do not extend the classes provided by the SDK") The SDK is designed to deliver all necessary functionalities in an intuitive and straightforward manner. Avoid extending any of the Maps SDK for Android classes. Use the callback methods provided by the SDK for seamless integration. #### DateTime Parameter Rules[​](#datetime-parameter-rules "Direct link to DateTime Parameter Rules") Make sure the `DateTime` passed to method is a valid positive utc value. Values below the `DateTime.utc(0)` are not supported. #### General principles related to listeners[​](#general-principles-related-to-listeners "Direct link to General principles related to listeners") * Use the provided register methods or constructor parameters **to subscribe** to events. * Only one callback can be active per event type. If multiple callbacks are registered for the same event type, the most recently registered callback will **override** the previous one. * **To unsubscribe** from events, register an empty callback. #### Provide SDK logs when reporting issues[​](#provide-sdk-logs-when-reporting-issues "Direct link to Provide SDK logs when reporting issues") danger The logs may contain sensitive information, so review them before sharing publicly. ##### Collecting SDK logs and crash reports[​](#collecting-sdk-logs-and-crash-reports "Direct link to Collecting SDK logs and crash reports") The SDK automatically generates logs and crash reports that are essential for diagnosing issues. To access and send these logs: ###### Automatic log collection[​](#automatic-log-collection "Direct link to Automatic log collection") The SDK automatically creates log files at runtime. You can access these logs using the `GemSdk.appLogPath` property, which returns the path to the current application log file. ###### Crash reports[​](#crash-reports "Direct link to Crash reports") If the application crashes, crash logs are automatically stored in the SDK's internal storage path under `GMcrashlogs/last/`. You can access the internal storage path using `GemSdk.internalStoragePath`. ###### Example implementation[​](#example-implementation "Direct link to Example implementation") Here's how to programmatically collect and send debug information (based on the SendDebugInfo example): * Kotlin * Java ```kotlin private fun sendDebugInfo() { // Get SDK version for subject line var subject = "" SdkCall.execute { subject = GemSdk.sdkVersion?.let { String.format("User feedback (SDK) - %d.%d.%d.%d.%s", it.major, it.minor, it.year, it.week, it.revision) } ?: "User feedback" } // Create email intent val sendIntent = Intent(Intent.ACTION_SEND_MULTIPLE) sendIntent.type = "message/rfc822" sendIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf("support@magicearth.com")) sendIntent.putExtra(Intent.EXTRA_SUBJECT, subject) val uris = ArrayList() // Attach application logs val privateLogPath = GemSdk.appLogPath privateLogPath?.let { logPath -> val publicPath = GemUtil.getApplicationPublicFilesAbsolutePath(this, "phoneLog.txt") if (GemUtil.copyFile(logPath, publicPath)) { val file = File(publicPath) file.deleteOnExit() try { uris.add(FileProvider.getUriForFile(this, packageName + ".provider", file)) } catch (e: Exception) { GEMLog.error(this, "Error attaching log file: ${e.message}") } } } // Attach crash reports if available if (GemSdk.internalStoragePath.isNotEmpty()) { val crashPath = "${GemSdk.internalStoragePath}/GMcrashlogs/last" val crashDir = File(crashPath) if (crashDir.exists() && crashDir.isDirectory) { crashDir.listFiles()?.firstOrNull()?.let { crashFile -> try { uris.add(FileProvider.getUriForFile(this, packageName + ".provider", crashFile)) } catch (e: Exception) { GEMLog.error(this, "Error attaching crash file: ${e.message}") } } } } sendIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris) startActivity(sendIntent) } ``` ```java private void sendDebugInfo() { // Get SDK version for subject line final String[] subject = {""}; SdkCall.execute(() -> { SdkVersion version = GemSdk.getSdkVersion(); if (version != null) { subject[0] = String.format("User feedback (SDK) - %d.%d.%d.%d.%s", version.getMajor(), version.getMinor(), version.getYear(), version.getWeek(), version.getRevision()); } else { subject[0] = "User feedback"; } }); // Create email intent Intent sendIntent = new Intent(Intent.ACTION_SEND_MULTIPLE); sendIntent.setType("message/rfc822"); sendIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{"support@magicearth.com"}); sendIntent.putExtra(Intent.EXTRA_SUBJECT, subject[0]); ArrayList uris = new ArrayList<>(); // Attach application logs String privateLogPath = GemSdk.getAppLogPath(); if (privateLogPath != null) { String publicPath = GemUtil.getApplicationPublicFilesAbsolutePath(this, "phoneLog.txt"); if (GemUtil.copyFile(privateLogPath, publicPath)) { File file = new File(publicPath); file.deleteOnExit(); try { uris.add(FileProvider.getUriForFile(this, getPackageName() + ".provider", file)); } catch (Exception e) { GEMLog.error(this, "Error attaching log file: " + e.getMessage()); } } } // Attach crash reports if available if (!GemSdk.getInternalStoragePath().isEmpty()) { String crashPath = GemSdk.getInternalStoragePath() + "/GMcrashlogs/last"; File crashDir = new File(crashPath); if (crashDir.exists() && crashDir.isDirectory()) { File[] crashFiles = crashDir.listFiles(); if (crashFiles != null && crashFiles.length > 0) { try { uris.add(FileProvider.getUriForFile(this, getPackageName() + ".provider", crashFiles[0])); } catch (Exception e) { GEMLog.error(this, "Error attaching crash file: " + e.getMessage()); } } } } sendIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); startActivity(sendIntent); } ``` ###### Required Android manifest configuration[​](#required-android-manifest-configuration "Direct link to Required Android manifest configuration") To share log files, you need to configure a `FileProvider` in your `AndroidManifest.xml`: ```xml ``` And create a `res/xml/provider_paths.xml` file: ```xml ``` ###### Adding debug logs to your code[​](#adding-debug-logs-to-your-code "Direct link to Adding debug logs to your code") You can add custom debug messages to help with troubleshooting: * Kotlin * Java ```kotlin GEMLog.debug(this, "This is a debug message!") GEMLog.error(this, "This is an error message!") ``` ```java GEMLog.debug(this, "This is a debug message!"); GEMLog.error(this, "This is an error message!"); ``` ##### Other useful information[​](#other-useful-information "Direct link to Other useful information") When reporting issues, please provide the following information to help us diagnose and resolve the problem effectively: * Clear and concise description of the bug or issue. * Steps to reproduce the issue, including a minimal code sample if possible. * Expected behavior versus actual behavior you are experiencing. * Screenshots or videos to help visualize the issue (if applicable). * SDK version you are using (obtainable via `GemSdk.sdkVersion`). * Platform details (e.g., iOS/Android, OS version, device model). * **SDK logs and crash reports** (using the method described above). * Geographical location where the issue occurred, if relevant (for example, routing, navigation, or search-related issues). * Any other relevant information that could assist us in diagnosing and resolving the issue. * Additionally, ensure that your SDK is up-to-date, as newer versions often include bug fixes and performance improvements. --- ### Maps SDK for Android ![](/docs/android/assets/images/maps_sdk_banner_android-2104ccd11bea01155e64d71bcc216a41.png) |



Welcome to the Developer Guide for the Magic Lane Maps SDK for Android. This guide provides everything you need to integrate seamless mapping and navigation experiences into your applications. #### Why use the Maps SDK for Android ?[​](#why-use-the-maps-sdk-for-android- "Direct link to Why use the Maps SDK for Android ?") With a unique combination of capabilities, Magic Lane has turned the vision of advanced navigation into a reality: * **Global Coverage**: Access to up-to-date, high-quality OpenStreetMap data provides global coverage for mapping and navigation needs. * **3D Terrain Topography**: A visually immersive experience with detailed 3D terrain modeling. * **Lightweight and Efficient**: Designed for high performance, the SDKs run smoothly even on lower-resource devices * **Optimized Navigation**: * **Turn-by-Turn Navigation**: Offers optimized routing for car, pedestrian, and bicycle navigation, ensuring accurate and efficient routes tailored to each mode of transportation. * **Voice Guidance**: Optional voice guidance available in multiple languages, enhancing the navigation experience. * **Offline Functionality**: * **Offline Maps**: Download maps and use them offline, ensuring uninterrupted service even in areas without internet connectivity. * **Offline Features**: Key features such as mapping, searching, routing, and turn-by-turn navigation continue to work seamlessly offline. * **Customizable Routing**: Flexible routing options, allowing developers to customize routing preferences based on specific requirements * **Real-Time Traffic and Incident Information**: Integration of real-time traffic data and incident updates to adjust routes dynamically and improve accuracy. * **Routing with Traffic Awareness**: Enhanced routing that considers real-time traffic data, ensuring optimal travel times and route adjustments as traffic conditions change. * **Customizable Map Styles**: Customizable map appearance, enabling developers to adjust colors, elements, and design to fit their app’s branding and user experience. * **Advanced Search Capabilities**: Comprehensive search functionality, including POI (Points of Interest), addresses and geocoding features * **Recorder Feature**: Advanced sensor recorder to capture, manage, and analyze sensor data during navigation sessions, enabling developers to collect valuable insights and providing comprehensive activity tracking. * **Flexible API & SDKs**: Developer-friendly API and SDK with documentation, making integration into various applications straightforward and flexible. * **Cross-Platform Support**: Full support for Android, iOS, ensuring compatibility with a broad range of devices and environments. * **Comprehensive Developer Documentation**: Extensive documentation with sample code, best practices and API reference, empowering developers to get started quickly and easily. These guides enable you to get quickly started using Magic Lane - Maps SDK for Android to render your first interactive map, then plot a route, simulate navigation along the route, and search for points of interest. #### Audience[​](#audience "Direct link to Audience") This documentation is tailored for developers already acquainted with Android development concepts written in Kotlin. It assumes a basic understanding of user-facing map and navigation applications. With this guide, you'll be equipped to explore and build applications using the Maps SDK for Android. --- ### Minimum requirements |

Before commencing development with the Maps SDK for Android, it is critical to validate your environment against the minimum system requirements. Adhering to these specifications ensures baseline compatibility and facilitates optimal runtime performance in location-aware applications. Key factors include supported API levels, Java/Kotlin language features, OpenGL ES compatibility, and device memory configurations. #### Development machine[​](#development-machine "Direct link to Development machine") Magic Lane's Maps SDK for Android supports the same operating systems as the official Android SDK from Google. Development is generally supported on Apple macOS, Linux and Windows. For details about specific versions, refer to [Google's documentation](http://developer.android.com/sdk/index.html). #### Software requirements[​](#software-requirements "Direct link to Software requirements") * Apps must be built using Android SDK 21 or higher. * Java and Kotlin are both supported. * Devices should support OpenGL ES 2.0/3.0. The SDK has been compiled with support for [16 KB page sizes](https://developer.android.com/guide/practices/page-sizes) using [NDK r27 LTS](https://github.com/android/ndk/wiki/Changelog-r27). #### Target devices[​](#target-devices "Direct link to Target devices") Compatibility is assured with both real devices and emulators used for development. The Maps SDK for Android is available for all [Google supported ABIs](https://developer.android.com/ndk/guides/abis#sa). ##### Minimum supported hardware specifications:[​](#minimum-supported-hardware-specifications "Direct link to Minimum supported hardware specifications:") * RAM: At least 1 GB memory. * ROM: At leas 250MB of storage. * GPU: All GPUs that support OpenGL ES 2.0/3.0. danger Please be aware that on lower-end devices, performance may be reduced due to hardware limitations. This can affect the smoothness of the user interface and overall responsiveness of the application, particularly in areas with complex or resource-intensive scenes. --- ### Location Wikipedia |

Landmarks can include Wikipedia data such as title, image title, URL, description, page summary, language, and more. To demonstrate how to retrieve Wikipedia information, we introduce the `ExternalInfo` class, which handles Wikipedia data. The `ExternalInfo` class is part of the core SDK and can be instantiated directly to access external information services. #### Check if Wikipedia data is available[​](#check-if-wikipedia-data-is-available "Direct link to Check if Wikipedia data is available") Use the `hasWikiInfo` method of the `ExternalInfo` class to check if a landmark has Wikipedia data available: * Kotlin * Java ```kotlin val externalInfoService = ExternalInfo() val hasExternalInfo = externalInfoService.hasWikiInfo(landmark) ``` ```java ExternalInfo externalInfoService = new ExternalInfo(); boolean hasExternalInfo = externalInfoService.hasWikiInfo(landmark); ``` danger Make sure the Wikipedia related fields from the `extraInfo` property of the `Landmark` object are not tampered with if changes are made to the landmark data. #### ExternalInfo class[​](#externalinfo-class "Direct link to ExternalInfo class") This class provides Wikipedia information for a landmark. Create an instance of `ExternalInfo` and use the `requestWikiInfo` method to retrieve Wikipedia data. * Kotlin * Java ```kotlin val externalInfoService = ExternalInfo() val wikipediaProgressListener = ProgressListener.create( onStarted = { // Show loading indicator progressBar.visibility = View.VISIBLE }, onCompleted = { errorCode, _ -> progressBar.visibility = View.GONE if (errorCode == GemError.NoError) { // Success - access Wikipedia data SdkCall.execute { val title = externalInfoService.wikiPageTitle val content = externalInfoService.wikiPageDescription val language = externalInfoService.wikiPageLanguage val pageUrl = externalInfoService.wikiPageURL // Use the retrieved data displayWikipediaInfo(title, content, language, pageUrl) } } else { // Handle error showDialog("Error getting wiki info: ${GemError.getMessage(errorCode)}") } }, postOnMain = true ) SdkCall.execute { externalInfoService.requestWikiInfo(landmark, wikipediaProgressListener) } ``` ```java ExternalInfo externalInfoService = new ExternalInfo(); ProgressListener wikipediaProgressListener = ProgressListener.create( () -> { // Show loading indicator progressBar.setVisibility(View.VISIBLE); }, (errorCode, hint) -> { progressBar.setVisibility(View.GONE); if (errorCode == GemError.NoError) { // Success - access Wikipedia data SdkCall.execute(() -> { String title = externalInfoService.getWikiPageTitle(); String content = externalInfoService.getWikiPageDescription(); String language = externalInfoService.getWikiPageLanguage(); String pageUrl = externalInfoService.getWikiPageURL(); // Use the retrieved data displayWikipediaInfo(title, content, language, pageUrl); }); } else { // Handle error showDialog("Error getting wiki info: " + GemError.getMessage(errorCode)); } }, true ); SdkCall.execute(() -> { externalInfoService.requestWikiInfo(landmark, wikipediaProgressListener); }); ``` The `requestWikiInfo` method triggers a progress listener that will be notified when the operation completes. You can cancel the request using the `cancelWikiInfo` method. info Wikipedia data is provided in the language specified in `SdkSettings`. The method provides a result based on the outcome of the operation: * On success returns `GemError.NoError` and the Wikipedia data is available through the `ExternalInfo` properties. * On failure returns one of the following `GemError` values: * `GemError.InvalidInput` : The specified landmark does not contain Wikipedia-related information. * `GemError.Connection` : No internet connection is available. * `GemError.NotFound` : Wikipedia information could not be retrieved for the given landmark. * `GemError.General` : An unspecified error occurred. #### Wikipedia image data[​](#wikipedia-image-data "Direct link to Wikipedia image data") The `ExternalInfo` class provides the following details regarding images: * Kotlin * Java ```kotlin SdkCall.execute { val imgCount = externalInfoService.wikiImagesCount val imageUrl = externalInfoService.getWikiImageURL(0) val imageDescription = externalInfoService.getWikiImageDescription(0) val imageTitle = externalInfoService.getWikiImageTitle(0) } ``` ```java SdkCall.execute(() -> { int imgCount = externalInfoService.getWikiImagesCount(); String imageUrl = externalInfoService.getWikiImageURL(0); String imageDescription = externalInfoService.getWikiImageDescription(0); String imageTitle = externalInfoService.getWikiImageTitle(0); }); ``` ##### Requesting Wikipedia image content[​](#requesting-wikipedia-image-content "Direct link to Requesting Wikipedia image content") You can request the actual image content using the `requestWikiImage` method: * Kotlin * Java ```kotlin val image = Image() val externalInfoService = ExternalInfo() val imageProgressListener = ProgressListener.create( onCompleted = { errorCode, _ -> if (errorCode == GemError.NoError) { // Image loaded successfully SdkCall.execute { //val bitmap = image.asBitmap(width, height) // Display the image //imageView.setImageBitmap(bitmap) } } }, postOnMain = true ) externalInfoService.requestWikiImage( imageProgressListener, image, nImageIdx = 0, EExternalImageQuality.Medium ) ``` ```java Image image = new Image(); ExternalInfo externalInfoService = new ExternalInfo(); ProgressListener imageProgressListener = ProgressListener.create( (errorCode, hint) -> { if (errorCode == GemError.NoError) { // Image loaded successfully SdkCall.execute(() -> { //Bitmap bitmap = image.asBitmap(width, height); // Display the image //imageView.setImageBitmap(bitmap); }); } }, true ); externalInfoService.requestWikiImage( imageProgressListener, image, 0, EExternalImageQuality.Medium ); ``` ##### Wikipedia image information[​](#wikipedia-image-information "Direct link to Wikipedia image information") Detailed information about an image can be retrieved using the `requestWikiImageInfo` method: * Kotlin * Java ```kotlin val externalInfoService = ExternalInfo() val result = GemString() val imageInfoListener = ProgressListener.create( onCompleted = { errorCode, _ -> if (errorCode == GemError.NoError) { // Image info retrieved successfully val imageInfo = result.value// GemString result // Process the image information } else { showDialog("Error getting wiki image info: ${GemError.getMessage(errorCode)}") } }, postOnMain = true ) externalInfoService.requestWikiImageInfo(imageInfoListener, nImageIdx = 0, result) ``` ```java ExternalInfo externalInfoService = new ExternalInfo(); GemString result = new GemString(); ProgressListener imageInfoListener = ProgressListener.create( (errorCode, hint) -> { if (errorCode == GemError.NoError) { // Image info retrieved successfully String imageInfo = result.getValue(); // GemString result // Process the image information } else { showDialog("Error getting wiki image info: " + GemError.getMessage(errorCode)); } }, true ); externalInfoService.requestWikiImageInfo(imageInfoListener, 0, result); ``` ##### Canceling requests[​](#canceling-requests "Direct link to Canceling requests") You can cancel various requests using the available cancel methods: * Kotlin * Java ```kotlin // Cancel main Wikipedia info request externalInfoService.cancelWikiInfo() // Cancel specific image content request externalInfoService.cancelWikiImageContentRequest(imageProgressListener) // Cancel specific image info request externalInfoService.cancelWikiImageInfoRequest(imageInfoListener) ``` ```java // Cancel main Wikipedia info request externalInfoService.cancelWikiInfo(); // Cancel specific image content request externalInfoService.cancelWikiImageContentRequest(imageProgressListener); // Cancel specific image info request externalInfoService.cancelWikiImageInfoRequest(imageInfoListener); ``` --- ### Maps Through this series, we’ll take you step-by-step into key concepts such as adjusting map views, interacting with maps using gestures, and implementing personalized styles. Additionally, you’ll discover how to handle map overlays, markers, routes, and navigation, all while ensuring a smooth and engaging user experience. Whether you’re a seasoned developer or new to mapping technology, this guide will empower you to build innovative, map-driven applications with ease. #### [📄️ Get started with maps](/docs/android/guides/maps/get-started.md) [The Maps SDK for Android delivers powerful mapping capabilities, enabling developers to effortlessly integrate dynamic map views into their applications. Core features include embedding and customizing map views, controlling displayed locations, and fine-tuning map properties. At the center of the mapping API is the GEMSurfaceView class, which serves as the primary component for rendering maps and handling user interactions.](/docs/android/guides/maps/get-started.md) #### [📄️ Adjust the map view](/docs/android/guides/maps/adjust-map.md) [The Maps SDK for Android provides multiple ways to modify the map view, center on coordinates or areas, including \`MapView\` functionality for exploring different perspectives.](/docs/android/guides/maps/adjust-map.md) #### [📄️ Interact with the map](/docs/android/guides/maps/interact-with-map.md) [The Maps SDK for Android map view natively supports common gestures like pinch and double-tap for zooming. The table below outlines the available gestures and their default behaviors on the map.](/docs/android/guides/maps/interact-with-map.md) #### [🗃️ Display map items](/docs/android/guides/maps/display-map-items.md) [6 items](/docs/android/guides/maps/display-map-items.md) #### [📄️ Styling](/docs/android/guides/maps/styling.md) [The appearance of the map can be tailored by applying different styles. You can either download a predefined map style using the \`ContentStore\` class, which offers a variety of ready-to-use styles, or create a custom style using Magic Lane Map Studio which you can download and configure. In this guide, we'll explore both methods in detail.](/docs/android/guides/maps/styling.md) --- ### Adjust the map view |

The Maps SDK for Android provides multiple ways to modify the map view, center on coordinates or areas, including `MapView` functionality for exploring different perspectives. The SDK enables a range of features, including zooming in and out, tilting the camera, centering on specific locations, and more, all through the `MapView` and `MapViewPreferences` classes provided by the SDK. * Use `viewport` property to return the current viewport of the map view. * Center on specific coordinates with `centerOnCoordinates` which takes extra parameters for centering preferences such as zoom level, screen position, map angle, animation and more. * Center on a specific geographic area represented by a rectangle with coordinates as corners with `centerOnArea` and `centerOnRectArea`. * Align map according to the north direction by using `alignNorthUp`. * Adjust the current zoom level using `setZoomLevel`, where lower values result in a more zoomed-out view. * Perform a scrolling behavior based on horizontal and vertical scroll offsets with `scroll` method. * `MapCamera` class provides further functionality such as manipulating orientation, position, and state. #### Map viewport[​](#map-viewport "Direct link to Map viewport") The map viewport refers to the area displayed by the `MapView`. Getting the current viewport provides a `Rect` object which consists of x, y (left and top) screen coordinates and width and height. The top left coordinate of the screen is represented by \[0, 0] and bottom right \[`viewport.width`, `viewport.height`]. * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val currentViewport = mapView?.viewport ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); Rect currentViewport = mapView != null ? mapView.getViewport() : null; ``` This viewport can be useful when you need to use methods such as `centerOnRectArea`. info The width and height of the map view is measured in physical pixels. To transform them into logical pixels you may need to consider the device's pixel density. #### Map centering[​](#map-centering "Direct link to Map centering") Map centering can be achieved using the `centerOnCoordinates`, `centerOnArea`, `centerOnRectArea`, `centerOnRoute`, `centerOnDistRoute`, `centerOnRouteInstruction`, `centerOnRouteTrafficEvent` methods. ##### Map centering on coordinates[​](#map-centering-on-coordinates "Direct link to Map centering on coordinates") In order to center the [WGS](https://en.wikipedia.org/wiki/World_Geodetic_System) coordinates on the viewport coordinates you can use the `centerOnCoordinates` method like so: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView mapView?.centerOnCoordinates(Coordinates(45.0, 25.0)) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); if (mapView != null) { mapView.centerOnCoordinates(new Coordinates(45.0, 25.0)); } ``` A linear animation can be incorporated while centering, as demonstrated below: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView mapView?.centerOnCoordinates( Coordinates(52.14569, 1.0615), animation = Animation(EAnimation.Linear, 2000) ) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); if (mapView != null) { mapView.centerOnCoordinates( new Coordinates(52.14569, 1.0615), new Animation(EAnimation.Linear, 2000) ); } ``` Tip You can call the `skipAnimation()` method of `MapView` to bypass the animation. To check if an animation is in progress the `isAnimationInProgress()` method can be used. To check if the camera is moving (as a consequence of an animation or not), the `isCameraMoving()` method can be used. danger Do not confuse the `zoomLevel` with the `slippyZoomLevel`. The `slippyZoomLevel` is a value linked with the tile system. ##### Converting between screen and WGS coordinates[​](#converting-between-screen-and-wgs-coordinates "Direct link to Converting between screen and WGS coordinates") In order to convert a screen position to WGS coordinates, the `MapView.transformScreenToWgs()` method is used: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val coordsToCenter = mapView?.transformScreenToWgs(Xy(pos.x, pos.y)) coordsToCenter?.let { mapView?.centerOnCoordinates(it, zoomLevel = 70) } ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); Coordinates coordsToCenter = mapView != null ? mapView.transformScreenToWgs(new Xy(pos.x, pos.y)) : null; if (coordsToCenter != null && mapView != null) { mapView.centerOnCoordinates(coordsToCenter, 70); } ``` info If the applied style includes elevation and terrain data is loaded, the `transformScreenToWgs` method returns `Coordinates` objects that include altitude. To convert WGS coordinates to screen coordinates, the `MapView.transformWgsToScreen()` method is used: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val wgsCoordinates = Coordinates(8.0, 25.0) val screenPosition = mapView?.transformWgsToScreen(wgsCoordinates) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); Coordinates wgsCoordinates = new Coordinates(8.0, 25.0); Xy screenPosition = mapView != null ? mapView.transformWgsToScreen(wgsCoordinates) : null; ``` Tip In order to convert a screen rectangle to a list of WGS geographic areas, use `MapView.transformScreenToWgsListArea` method. This centers the view precisely on the specified coordinates, positioning them at the cursor position (which by default is in the center of the screen). ##### Map centering on coordinates at given screen position[​](#map-centering-on-coordinates-at-given-screen-position "Direct link to Map centering on coordinates at given screen position") To center on a different area of the viewport (not the cursor position), provide an `xy` parameter, represented as an `Xy`. Note that `x` coordinate should be in \[0, `viewport.width`] and `y` coordinate between \[0, `viewport.height`]. danger The `xy` parameter is defined in physical pixels. The following example demonstrates how to center the map at one-third of its height: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val physicalHeightPixels = mapView?.viewport?.height ?: 0 val physicalWidthPixels = mapView?.viewport?.width ?: 0 mapView?.centerOnCoordinates( Coordinates(52.48209, -2.48888), zoomLevel = 40, xy = Xy(physicalWidthPixels / 2, physicalHeightPixels / 3) ) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); int physicalHeightPixels = mapView != null && mapView.getViewport() != null ? mapView.getViewport().getHeight() : 0; int physicalWidthPixels = mapView != null && mapView.getViewport() != null ? mapView.getViewport().getWidth() : 0; if (mapView != null) { mapView.centerOnCoordinates( new Coordinates(52.48209, -2.48888), 40, // zoomLevel new Xy(physicalWidthPixels / 2, physicalHeightPixels / 3) ); } ``` ![](/docs/android/assets/images/example_android_center_coordinates1-853e1f8276fc6928f48b7ce0e5b30686.png) **Centered at one-third of map height** Tip More parameters such as `animation`, `mapAngle`, `viewAngle` and `zoomLevel` can be passed to the method in order to achieve a higher level of control. ##### Map centering on area[​](#map-centering-on-area "Direct link to Map centering on area") Centering can be done on a specific `RectangleGeographicArea` which consists of top left and bottom right coordinates. * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val topLeftCoords = Coordinates(44.93343, 25.09946) val bottomRightCoords = Coordinates(44.93324, 25.09987) val area = RectangleGeographicArea(topLeftCoords, bottomRightCoords) mapView?.centerOnArea(area) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); Coordinates topLeftCoords = new Coordinates(44.93343, 25.09946); Coordinates bottomRightCoords = new Coordinates(44.93324, 25.09987); RectangleGeographicArea area = new RectangleGeographicArea(topLeftCoords, bottomRightCoords); if (mapView != null) { mapView.centerOnArea(area); } ``` This will center the view on the geographic area ensuring the `RectangleGeographicArea` covers most of the viewport. For centering the geographic area on a particular coordinate of the viewport, the `xy` parameter, represented as an `Xy` should be provided. Alternatively, to center the `RectangleGeographicArea` on a specific region of the viewport, you can use the `centerOnRectArea` method. This requires passing the `viewRc` parameter, represented as a `Rect`, which defines the targeted region of the screen. The `Rect` passed to the `viewRc` parameter determines the positioning of the centered area relative to the top-left coordinates. Consequently, the bottom-right corner will be at `left` + `Rect`'s width and `top` + `Rect`'s height. info As the width and height of `Rect` decrease, the centering will result in a more zoomed-out view. For a more zoomed-in perspective, use larger values within the range \[1, viewport.width - x] and \[1, viewport.height - y]. Tip Use the `getOptimalRoutesCenterViewport` and `getOptimalHighlightCenterViewport` methods to compute the viewport region that best fits given routes and highlights. ##### Map centering on area with padding[​](#map-centering-on-area-with-padding "Direct link to Map centering on area with padding") Centering on an area using padding can be done by altering the screen coordinates (in physical pixels) by adding/subtracting the padding value. Then a new `RectangleGeographicArea` object needs to be instantiated with the padded screen coordinates transformed into WGS coordinates using `MapView.transformScreenToWgs(xy)`. The following code exemplifies the process: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView // Getting the RectangleGeographicArea in which the route belongs val routeArea = route.geographicArea val paddingPixels = 200 // Getting the top left point screen coordinates in physical pixels val routeAreaTopLeftPoint = mapView?.transformWgsToScreen(routeArea.topLeft) // Adding padding by shifting point in the top left val topLeftPadded = routeAreaTopLeftPoint?.let { point -> Xy( point.x - paddingPixels, point.y - paddingPixels ) } val routeAreaBottomRightPoint = mapView?.transformWgsToScreen(routeArea.bottomRight) // Adding padding by shifting point downwards three times the padding val bottomRightPadded = routeAreaBottomRightPoint?.let { point -> Xy( point.x + paddingPixels, point.y + 3 * paddingPixels ) } // Converting points with padding to WGS coordinates val paddedTopLeftCoordinate = topLeftPadded?.let { mapView?.transformScreenToWgs(it) } val paddedBottomRightCoordinate = bottomRightPadded?.let { mapView?.transformScreenToWgs(it) } if (paddedTopLeftCoordinate != null && paddedBottomRightCoordinate != null) { mapView?.centerOnArea( RectangleGeographicArea( paddedTopLeftCoordinate, paddedBottomRightCoordinate ) ) } ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); // Getting the RectangleGeographicArea in which the route belongs RectangleGeographicArea routeArea = route.getGeographicArea(); int paddingPixels = 200; // Getting the top left point screen coordinates in physical pixels Xy routeAreaTopLeftPoint = mapView != null ? mapView.transformWgsToScreen(routeArea.getTopLeft()) : null; // Adding padding by shifting point in the top left Xy topLeftPadded = null; if (routeAreaTopLeftPoint != null) { topLeftPadded = new Xy( routeAreaTopLeftPoint.x - paddingPixels, routeAreaTopLeftPoint.y - paddingPixels ); } Xy routeAreaBottomRightPoint = mapView != null ? mapView.transformWgsToScreen(routeArea.getBottomRight()) : null; // Adding padding by shifting point downwards three times the padding Xy bottomRightPadded = null; if (routeAreaBottomRightPoint != null) { bottomRightPadded = new Xy( routeAreaBottomRightPoint.x + paddingPixels, routeAreaBottomRightPoint.y + 3 * paddingPixels ); } // Converting points with padding to WGS coordinates Coordinates paddedTopLeftCoordinate = topLeftPadded != null && mapView != null ? mapView.transformScreenToWgs(topLeftPadded) : null; Coordinates paddedBottomRightCoordinate = bottomRightPadded != null && mapView != null ? mapView.transformScreenToWgs(bottomRightPadded) : null; if (paddedTopLeftCoordinate != null && paddedBottomRightCoordinate != null && mapView != null) { mapView.centerOnArea( new RectangleGeographicArea( paddedTopLeftCoordinate, paddedBottomRightCoordinate ) ); } ``` ![](/docs/android/assets/images/example_android_area_without_padding-3d5f7629b87218a4b791f6bac186c8df.png) **Route without padding** ![](/docs/android/assets/images/example_android_area_with_padding-c8433c256f28fc4c50b343ecf80ec86e.png) **Route with center padding**
danger When applying padding, note that the height is measured in physical pixels. A conversion may be required depending on your use case. #### Map zoom[​](#map-zoom "Direct link to Map zoom") To get the current zoom level use the `zoomLevel` property. A bigger value means the camera is closer to the terrain. Changing the zoom level is done through the `setZoomLevel` method of the `MapView` class in the following way: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val zoomLevel = mapView?.zoomLevel mapView?.setZoomLevel(50) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); Integer zoomLevel = mapView != null ? mapView.getZoomLevel() : null; if (mapView != null) { mapView.setZoomLevel(50); } ``` The maximum allowed zoom level can be accessed via the `maxZoomLevel` property from the `MapView` class. In order to check if a particular zoom level can be applied, check if it's within the valid range. #### Map rotation angle[​](#map-rotation-angle "Direct link to Map rotation angle") To get the current rotation angle of the map, use the `rotationAngle` property from the `MapViewPreferences` class. Changing the rotation angle is done through the `rotationAngle` setter of `MapViewPreferences` like so: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val rotationAngle = mapView?.preferences?.rotationAngle ?: 0.0 mapView?.preferences?.rotationAngle = 45.0 ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); double rotationAngle = mapView != null && mapView.getPreferences() != null ? mapView.getPreferences().getRotationAngle() : 0.0; if (mapView != null && mapView.getPreferences() != null) { mapView.getPreferences().setRotationAngle(45.0); } ``` The provided value needs to be between 0 and 360. By default, the camera has a rotation angle value of 0 degrees corresponding to the north-up alignment. Note that the rotation axis is always perpendicular to the ground and passes through the camera, regardless of the current camera orientation. #### Map view angle[​](#map-view-angle "Direct link to Map view angle") The camera can transform the flat 2D map into a 3D perspective, allowing you to view features like distant roads appearing on the horizon. By default, the camera has a top-down perspective (viewAngle = 90°). In addition to adjusting the camera's view angle, you can modify its tilt angle. The `tiltAngle` is defined as the complement of the `viewAngle`, calculated as `tiltAngle = 90-viewAngle` In order to change the view angle of camera you need to access the `preferences` field of `MapView` like so: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val viewAngle = mapView?.preferences?.viewAngle ?: 30 mapView?.preferences?.setViewAngle(45) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); int viewAngle = mapView != null && mapView.getPreferences() != null ? mapView.getPreferences().getViewAngle() : 30; if (mapView != null && mapView.getPreferences() != null) { mapView.getPreferences().setViewAngle(45); } ``` ![](/docs/android/assets/images/example_android_map_perspective2-6954172f469464edf0fbb7b1f0c6e81c.png) **Map with a view angle of 60 degrees** ![](/docs/android/assets/images/example_android_map_perspective1-9fdfe174c35c8d75411182803654b551.png) **Map with a view angle of 0 degrees**
To adjust the camera's perspective dynamically, you can utilize both the `tiltAngle` and `viewAngle` properties. The difference between the different types of angles is shown below: **Tilt angle & view angle** **Rotation angle**
info Keep in mind that adjusting the rotation value produces different outcomes depending on the camera's tilt. When the camera is tilted, changing the rotation will shift the target location, whereas with no tilt, the target location remains fixed. #### Map perspective[​](#map-perspective "Direct link to Map perspective") Map perspective can be either two dimensional or three dimensional and can also be set by using `MapViewPreferences` method `setMapViewPerspective`: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val perspective = mapView?.preferences?.mapViewPerspective ?: EMapViewPerspective.ThreeDimensional mapView?.preferences?.setMapViewPerspective(EMapViewPerspective.ThreeDimensional) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); EMapViewPerspective perspective = mapView != null && mapView.getPreferences() != null ? mapView.getPreferences().getMapViewPerspective() : EMapViewPerspective.ThreeDimensional; if (mapView != null && mapView.getPreferences() != null) { mapView.getPreferences().setMapViewPerspective(EMapViewPerspective.ThreeDimensional); } ``` By default, the map perspective is three-dimensional. ![](/docs/android/assets/images/example_android_map_perspective3-18356dff297d2fa86e6c5d67a5d1f260.png) **Map with a two-dimensional perspective** ![](/docs/android/assets/images/example_android_map_perspective4-24db7e5d4fe3aea3a9f8eb27ce207119.png) **Map with a three-dimensional perspective**
A three-dimensional perspective gives buildings a realistic, 3D appearance, while a two-dimensional perspective makes them appear as flat shapes. info To ensure three-dimensional buildings are visible, the camera angle should not be perpendicular to the map. Instead, the view angle must be less than 90 degrees. The same effect can be implemented more precisely using the `tiltAngle`/`viewAngle` fields. #### Building visibility[​](#building-visibility "Direct link to Building visibility") Building visibility can be controlled using the `buildingsVisibility` property from the `MapViewPreferences` class: * `EBuildingsVisibility.Default`: Uses the default visibility defined in the map style. * `EBuildingsVisibility.Hide`: Hides all buildings. * `EBuildingsVisibility.TwoDimensional`: Displays buildings as flat 2D polygons without height. * `EBuildingsVisibility.ThreeDimensional`: Displays buildings as 3D polygons with height. - Kotlin - Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val visibility = mapView?.preferences?.buildingsVisibility ?: EBuildingsVisibility.Default mapView?.preferences?.buildingsVisibility = EBuildingsVisibility.TwoDimensional ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); EBuildingsVisibility visibility = mapView != null && mapView.getPreferences() != null ? mapView.getPreferences().getBuildingsVisibility() : EBuildingsVisibility.Default; if (mapView != null && mapView.getPreferences() != null) { mapView.getPreferences().setBuildingsVisibility(EBuildingsVisibility.TwoDimensional); } ``` Buildings become visible when the camera is zoomed in close to the ground. The 3D effect is most noticeable when viewed from a tilted angle. Note that the 3D buildings do not reflect realistic or accurate heights. #### Store and restore a view[​](#store-and-restore-a-view "Direct link to Store and restore a view") The map camera object has properties and methods for position and orientation ensuring a high level of control over the map view. For storing a particular view the `saveCameraState` method can be used. This method returns a `DataBuffer` object and depending on the use case this can be stored inside a variable or serialized to a file. * Kotlin * Java ```kotlin val state = mapView.camera?.saveCameraState() ``` ```java DataBuffer state = mapView.getCamera() != null ? mapView.getCamera().saveCameraState() : null; ``` Restoring a saved view can be done easily using the `restoreCameraState` method: * Kotlin * Java ```kotlin state?.let { mapView.camera?.restoreCameraState(it) } ``` ```java if (state != null && mapView.getCamera() != null) { mapView.getCamera().restoreCameraState(state); } ``` Alternatively the `position` and `orientation` can be stored and restored separately using the provided properties. info Please note that `saveCameraState` does not contain information about the current style. #### Download individual map tiles[​](#download-individual-map-tiles "Direct link to Download individual map tiles") A map tile is a small, rectangular image or data chunk that represents a specific geographic area at a particular zoom level on a `MapView`. Tiles are usually downloaded when panning or zooming in on a map, and they are used to render the map's visual content. However, you can also download tiles that are not currently visible on the screen, using the `MapDownloaderService` class. ##### Configuring the MapDownloaderService[​](#configuring-the-mapdownloaderservice "Direct link to Configuring the MapDownloaderService") The service can be configured by setting specific maximum area size in square kilometers to download by using the `maxSquareKm` property: * Kotlin * Java ```kotlin val service = MapDownloaderService() // Set a new value service.maxSquareKm = 100 // Verify the new value val updatedMaxSquareKm = service.maxSquareKm ``` ```java MapDownloaderService service = new MapDownloaderService(); // Set a new value service.setMaxSquareKm(100); // Verify the new value int updatedMaxSquareKm = service.getMaxSquareKm(); ``` The larger the area, the more tiles can be downloaded, which can lead to increased memory usage. The default value is 1000 square kilometers. danger If the `RectangleGeographicArea` surface exceeds the `maxSquareKm`, the `MapDownloaderService` will return `GemError.OutOfRange`. Downloading tiles is done by calling the `startDownload` method of `MapDownloaderService` like so: * Kotlin * Java ```kotlin val service = MapDownloaderService() service.maxSquareKm = 300 val areas = arrayListOf( // Area in which the tiles will be downloaded that is under 300 square kilometers RectangleGeographicArea( Coordinates(67.69866, 24.81115), Coordinates(67.58326, 25.36093) ) ) service.startDownload(areas, object : ProgressListener() { override fun notifyComplete(errorCode: ErrorCode, hint: String) { // Handle completion } }) ``` ```java MapDownloaderService service = new MapDownloaderService(); service.setMaxSquareKm(300); ArrayList areas = new ArrayList<>(); // Area in which the tiles will be downloaded that is under 300 square kilometers areas.add(new RectangleGeographicArea( new Coordinates(67.69866, 24.81115), new Coordinates(67.58326, 25.36093) )); service.startDownload(areas, new ProgressListener() { @Override public void notifyComplete(ErrorCode errorCode, String hint) { // Handle completion } }); ``` When tiles are downloaded, the `notifyComplete` callback is invoked with an `ErrorCode` parameter indicating the success or failure of the operation. If the download is successful, the error will be `ErrorCode.Success`. Downloaded tiles are stored in the cache and can be used later for features such as viewing map content without requiring an internet connection. ![](/docs/android/assets/images/example_android_downloaded_tiles-736bc4a9c8e1f95cac393f1c73b7c408.png) **Downloaded tiles centered in the middle, top and bottom tiles are not available** info SearchService.search method will return an error when trying to search in a downloaded tiles area as it requires indexing, which is not available for downloaded tiles. Download can be canceled by calling the `cancelDownload` method of `MapDownloaderService` and the `notifyComplete` callback will be invoked with `ErrorCode.Cancelled`. Tip Trying to download previously downloaded tiles will not result in an error, as downloaded tiles are present inside the application's cache folder as data files. You can access detailed download statistics for map tiles using the `transferStatistics` property. danger Downloaded map tiles via `MapDownloaderService` do not support operations such as free-text search, routing, or turn-by-turn navigation while offline. They are primarily intended for caching map data for visual display purposes only. For full offline functionality, including search and navigation, refer to the [Manage Offline Content Guide](/docs/android/guides/todo.md) to learn how to download roadmap data designed for full offline use. #### Change map settings while following the current position[​](#change-map-settings-while-following-the-current-position "Direct link to Change map settings while following the current position") The `FollowPositionPreferences` class provides more customization while the camera is in the follow position mode. To retrieve an instance, use the snippet below: * Kotlin * Java ```kotlin val preferences = mapView.preferences?.followPositionPreferences ``` ```java FollowPositionPreferences preferences = mapView.getPreferences() != null ? mapView.getPreferences().getFollowPositionPreferences() : null; ``` See the [customize follow position settings guide](/docs/android/guides/positioning/show-your-location-on-the-map.md#customize-follow-position-settings) for more details. --- ### Display map items This collection of articles covers a wide range of features and techniques for displaying various elements on a map within a mobile application, including landmarks, markers, overlays, routes, instructions, and paths. #### [📄️ Display landmarks](/docs/android/guides/maps/display-map-items/display-landmarks.md) [When displaying the map, we can choose what types of landmarks we want to display. Each landmark can have one or more LandmarkCategory. To selectively display specific categories of landmarks programmatically, you can use the addStoreCategoryId method provided by the LandmarkStoresCollection class:](/docs/android/guides/maps/display-map-items/display-landmarks.md) #### [📄️ Display markers](/docs/android/guides/maps/display-map-items/display-markers.md) [The base class for the marker hierarchy is Marker. It encapsulates coordinates assigned to a specific part. Multiple coordinates can be added to the same marker and be separated into different parts. If no part is specified, the coordinates are added to a default part, indexed as 0. The coordinates are stored in a list-like structure, where you can specify their index explicitly. By default, the index is set to -1, meaning the coordinate will be appended to the end of the list.](/docs/android/guides/maps/display-map-items/display-markers.md) #### [📄️ Display overlays](/docs/android/guides/maps/display-map-items/display-overlays.md) [Overlays are used to provide enhanced, layered information that adds contextual value to a base map, offering users dynamic, interactive, and customized data that can be visualized on top of other map elements.](/docs/android/guides/maps/display-map-items/display-overlays.md) #### [📄️ Display routes](/docs/android/guides/maps/display-map-items/display-routes.md) [Routes can be displayed on the map by using mapView.preferences?.routes?.add(route, isMainRoute). Multiple routes can be displayed at the same time, but only one is the main one, the others being treated as secondary. Specifying which one is the main route can be done when calling MapViewRoutesCollection.add() by passing true to the mainRoute parameter, or by calling the MapViewRoutesCollection.mainRoute setter.](/docs/android/guides/maps/display-map-items/display-routes.md) #### [📄️ Display route instructions](/docs/android/guides/maps/display-map-items/display-route-instructions.md) [Instructions are represented as arrows on the map and can be displayed by using \`MapView.centerOnRouteInstruction(instruction, zoomLevel, xy, animation, viewAngle)\`. To obtain a route's instructions, see the Route structure section. The following example iterates through all instructions of a route and displays each one by centering:](/docs/android/guides/maps/display-map-items/display-route-instructions.md) #### [📄️ Display paths](/docs/android/guides/maps/display-map-items/display-paths.md) [Paths can be displayed by adding them into \`MapViewPathCollection. The MapViewPathCollection is an iterable collection, having fields like size, add, remove, removeAt, getPathAt and getPathByName\`.](/docs/android/guides/maps/display-map-items/display-paths.md) --- ### Display landmarks |

### Filter landmarks by category When displaying the map, we can choose what types of landmarks we want to display. Each landmark can have one or more `LandmarkCategory`. To selectively display specific categories of landmarks programmatically, you can use the `addStoreCategoryId` method provided by the `LandmarkStoresCollection` class: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView // Get the landmark store service to access map POIs val landmarkStoreService = LandmarkStoreService() val mapPoisStoreId = landmarkStoreService.mapPoisLandmarkStoreId // Clear all the landmark types on the map mapView?.preferences?.landmarkStores?.removeAllStoreCategories(mapPoisStoreId) // Display only gas stations mapView?.preferences?.landmarkStores?.addStoreCategoryId( mapPoisStoreId, EGenericCategoriesIDs.GasStation.value ) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); // Get the landmark store service to access map POIs LandmarkStoreService landmarkStoreService = new LandmarkStoreService(); int mapPoisStoreId = landmarkStoreService.getMapPoisLandmarkStoreId(); // Clear all the landmark types on the map if (mapView != null) { mapView.getPreferences().getLandmarkStores().removeAllStoreCategories(mapPoisStoreId); // Display only gas stations mapView.getPreferences().getLandmarkStores().addStoreCategoryId( mapPoisStoreId, EGenericCategoriesIDs.GasStation.getValue() ); } ``` This allows filtering the default map data. Next, we might want to also add custom landmarks to the map (see the next section). #### Display custom landmarks[​](#display-custom-landmarks "Direct link to Display custom landmarks") Landmarks can be added to the map by storing them in a `LandmarkStore`. Next, the `LandmarkStore` is added to the `LandmarkStoresCollection` within `MapViewPreferences`. The following code demonstrates all these steps: first, creating custom landmarks, then adding them to a store, and finally adding the store to the collection of stores. * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val landmarksToAdd = arrayListOf() // Load image from assets val imageData = Image.fromFile("assets/poi83.png") val landmark1 = Landmark().apply { name = "Added landmark1" coordinates = Coordinates(51.509865, -0.118092) image = imageData } landmarksToAdd.add(landmark1) val landmark2 = Landmark().apply { name = "Added landmark2" coordinates = Coordinates(51.505165, -0.112992) image = imageData } landmarksToAdd.add(landmark2) // Create landmark store val landmarkStoreService = LandmarkStoreService() val (landmarkStore, errorCode) = landmarkStoreService.createLandmarkStore("landmarks") if (errorCode == GemError.NoError && landmarkStore != null) { // Add landmarks to the store for (landmark in landmarksToAdd) { landmarkStore.addLandmark(landmark) } // Add the store to the map view preferences mapView?.preferences?.landmarkStores?.add(landmarkStore) } ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); ArrayList landmarksToAdd = new ArrayList<>(); // Load image from assets Image imageData = Image.fromFile("assets/poi83.png"); Landmark landmark1 = new Landmark(); landmark1.setName("Added landmark1"); landmark1.setCoordinates(new Coordinates(51.509865, -0.118092)); landmark1.setImage(imageData); landmarksToAdd.add(landmark1); Landmark landmark2 = new Landmark(); landmark2.setName("Added landmark2"); landmark2.setCoordinates(new Coordinates(51.505165, -0.112992)); landmark2.setImage(imageData); landmarksToAdd.add(landmark2); // Create landmark store LandmarkStoreService landmarkStoreService = new LandmarkStoreService(); Pair result = landmarkStoreService.createLandmarkStore("landmarks"); LandmarkStore landmarkStore = result.getFirst(); int errorCode = result.getSecond(); if (errorCode == GemError.NoError && landmarkStore != null) { // Add landmarks to the store for (Landmark landmark : landmarksToAdd) { landmarkStore.addLandmark(landmark); } // Add the store to the map view preferences if (mapView != null) { mapView.getPreferences().getLandmarkStores().add(landmarkStore); } } ``` ![](/docs/android/assets/images/example_android_add_landmarks1-e23f9a097f6485af4f047e14090a19d9.png) **Landmarks displayed**
Some methods of `LandmarkStoresCollection` class are explained below: * `add(LandmarkStore lms)`: add a new store to be displayed on map. All the landmarks from the store will be displayed, regardless of the category provided. * `addAllStoreCategories(Int storeId)`: does the same thing as `add` but uses the `storeId`, not the `LandmarkStore` instance. * `addStoreCategoryId(Int storeId, Int categoryId)`: adds the landmarks with the specified category from the landmark store on the map. * `removeAllStoreCategories(Int storeId)`: removes all landmark stores from the map. * `contains(Int storeId, Int categoryId)`: check if the specified category ID from the specified store ID was already added. * `containsStore(LandmarkStore lms)`: check if the specified store has any categories of landmarks shown on the map. #### Highlight landmarks[​](#highlight-landmarks "Direct link to Highlight landmarks") Highlights allow customizing a list of landmarks, making them more visible and providing options to customize the render settings. Highlights can only be used upon Landmarks. By default, highlighted landmarks are not selectable but can be made selectable if necessary. Highlighting a landmark allows: * Customizing its appearance. * Temporarily isolating it from standard interactions - it cannot be selected (default behavior, can be modified for each highlight). Tip Landmarks retrieved through search or other means can be highlighted to enhance their prominence and customize their appearance. Additionally, custom landmarks can be highlighted, but they have to be added to a `LandmarkStore` first. ##### Activate highlights[​](#activate-highlights "Direct link to Activate highlights") Highlights can be displayed on map by using `MapView.activateHighlightLandmarks`. For demonstration purposes, a new `Landmark` object will be instantiated and filled with specifications using available setters. Next, the created landmark needs to be added to a `List` and a `HighlightRenderSettings` needs to be provided in order to enable desired customizations. Then `activateHighlightLandmarks` can be called with a unique `highlightId`. * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val landmarksToHighlight = arrayListOf() // Load image from assets val imageData = Image.fromFile("assets/poi83.png") val landmark = Landmark().apply { name = "New Landmark" coordinates = Coordinates(52.48209, -2.48888) image = imageData extraImage = imageData } landmarksToHighlight.add(landmark) // Configure highlight render settings val settings = HighlightRenderSettings( options = EHighlightOptions.ShowLandmark, innerColor = Rgba(255, 98, 0, 255), outerColor = Rgba(255, 98, 0, 255), innerSize = 1.5, outerSize = 0.0 ) // Apply additional options using bitwise OR settings.setOptions( EHighlightOptions.ShowLandmark.value or EHighlightOptions.NoFading.value or EHighlightOptions.Overlap.value ) // First add landmark to a store val landmarkStoreService = LandmarkStoreService() val (landmarkStore, errorCode) = landmarkStoreService.createLandmarkStore("landmarks") if (errorCode == GemError.NoError && landmarkStore != null) { landmarkStore.addLandmark(landmark) mapView?.preferences?.landmarkStores?.add(landmarkStore) // Activate highlight mapView?.activateHighlightLandmarks( landmarksToHighlight, settings, highlightId = 2 ) // Center on the landmark mapView?.centerOnCoordinates(Coordinates(52.48209, -2.48888), zoomLevel = 40) } ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); ArrayList landmarksToHighlight = new ArrayList<>(); // Load image from assets Image imageData = Image.fromFile("assets/poi83.png"); Landmark landmark = new Landmark(); landmark.setName("New Landmark"); landmark.setCoordinates(new Coordinates(52.48209, -2.48888)); landmark.setImage(imageData); landmark.setExtraImage(imageData); landmarksToHighlight.add(landmark); // Configure highlight render settings HighlightRenderSettings settings = new HighlightRenderSettings( EHighlightOptions.ShowLandmark, new Rgba(255, 98, 0, 255), new Rgba(255, 98, 0, 255), 1.5, 0.0 ); // Apply additional options using bitwise OR settings.setOptions( EHighlightOptions.ShowLandmark.getValue() | EHighlightOptions.NoFading.getValue() | EHighlightOptions.Overlap.getValue() ); // First add landmark to a store LandmarkStoreService landmarkStoreService = new LandmarkStoreService(); Pair result = landmarkStoreService.createLandmarkStore("landmarks"); LandmarkStore landmarkStore = result.getFirst(); int errorCode = result.getSecond(); if (errorCode == GemError.NoError && landmarkStore != null) { landmarkStore.addLandmark(landmark); if (mapView != null) { mapView.getPreferences().getLandmarkStores().add(landmarkStore); // Activate highlight mapView.activateHighlightLandmarks( landmarksToHighlight, settings, 2 // highlightId ); // Center on the landmark mapView.centerOnCoordinates(new Coordinates(52.48209, -2.48888), 40); } } ``` ##### Hightlight options[​](#hightlight-options "Direct link to Hightlight options") The `EHighlightOptions` enum provides several options to customize the behavior of highlighted landmarks: | Option | Description | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `ShowLandmark` | Shows the landmark icon & text. By default is enabled. | | `ShowContour` | Shows the landmark contour area if available. By default, this option is enabled. | | `Group` | Groups landmarks when many are present in close proximity. Available only with `ShowLandmark`. By default it is disabled. | | `Overlap` | Overlap highlight over existing map data. Available only with `ShowLandmark`. By default, disabled. | | `NoFading` | Disable highlight fading in/out. Available only with `ShowLandmark`. By default, disabled. | | `Bubble` | Displays highlights in a bubble with custom icon placement inside the text. Available only with `ShowLandmark`. Automatically invalidates `Group`. By default, disabled. | | `Selectable` | The highlights are selectable using setCursorScreenPosition.This option is available only in conjunction with ShowLandmark By default, the option is true | danger When showing bubble highlights, if the whole bubble does not fit on the screen, it will not be displayed at all. Make sure to truncate the text if the text length is very long. danger For a landmark contour to be displayed, the landmark must have a valid contour area. Landmarks which have a polygon representation on OpenStreetMap will have a contour area. Make sure the contour geographic related fields from the `extraInfo` property of the `Landmark` are not removed or altered. ##### Deactivate highlights[​](#deactivate-highlights "Direct link to Deactivate highlights") To remove a displayed landmark from the map, use `MapView.deactivateHighlight(id)` to selectively remove a specific landmark, or `MapView.deactivateAllHighlights()` to clear all displayed landmarks at once. * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView mapView?.deactivateHighlight(highlightId = 2) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); if (mapView != null) { mapView.deactivateHighlight(2); } ``` ##### Get highlighted landmarks[​](#get-highlighted-landmarks "Direct link to Get highlighted landmarks") To get the highlighted landmarks based on a particular highlight id, you can use the geographic area of the highlight: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val highlightArea = mapView?.getHighlightArea(highlightId = 2) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); Object highlightArea = null; if (mapView != null) { highlightArea = mapView.getHighlightArea(2); } ``` info The Android SDK provides `getHighlightArea()` instead of `getHighlight()` to retrieve information about highlights. Tip Overlay items can also be highlighted using the `activateOverlayItemsHighlight` method in a similar way. #### Disabling landmarks[​](#disabling-landmarks "Direct link to Disabling landmarks") Removing all presented landmarks can be done by calling `removeAllStoreCategories(storeId)` method of `LandmarkStoresCollection` class. The following code does just that: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView // Get the landmark store service to access map POIs val landmarkStoreService = LandmarkStoreService() val mapPoisStoreId = landmarkStoreService.mapPoisLandmarkStoreId mapView?.preferences?.landmarkStores?.removeAllStoreCategories(mapPoisStoreId) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); // Get the landmark store service to access map POIs LandmarkStoreService landmarkStoreService = new LandmarkStoreService(); int mapPoisStoreId = landmarkStoreService.getMapPoisLandmarkStoreId(); if (mapView != null) { mapView.getPreferences().getLandmarkStores().removeAllStoreCategories(mapPoisStoreId); } ``` --- ### Display markers |

The base class for the marker hierarchy is `Marker`. It encapsulates coordinates assigned to a specific part. Multiple coordinates can be added to the same marker and be separated into different parts. If no part is specified, the coordinates are added to a default part, indexed as 0. The coordinates are stored in a list-like structure, where you can specify their index explicitly. By default, the index is set to -1, meaning the coordinate will be appended to the end of the list. ![](/docs/android/assets/images/example_android_polyline_marker_part1-10444789a0e20d05469cc9feaaac91f8.png) **Displaying a marker with coordinates separated into different parts** ![](/docs/android/assets/images/example_android_polyline_marker_part2-50c9ac8fc608c36f88e4535d394bab5f.png) **Displaying a marker with coordinates added to same part**
* Kotlin * Java ```kotlin // code used for displaying a marker with coordinates separated into different parts val marker1 = Marker().apply { add(Coordinates(52.1459, 1.0613), part = 0) add(Coordinates(52.14569, 1.0615), part = 0) add(Coordinates(52.14585, 1.06186), part = 1) add(Coordinates(52.14611, 1.06215), part = 1) } ``` ```java // code used for displaying a marker with coordinates separated into different parts Marker marker1 = new Marker(); marker1.add(new Coordinates(52.1459, 1.0613), 0); marker1.add(new Coordinates(52.14569, 1.0615), 0); marker1.add(new Coordinates(52.14585, 1.06186), 1); marker1.add(new Coordinates(52.14611, 1.06215), 1); ``` * Kotlin * Java ```kotlin // code used for displaying a marker with coordinates added to the same part val marker1 = Marker().apply { add(Coordinates(52.1459, 1.0613), part = 0) add(Coordinates(52.14569, 1.0615), part = 0) add(Coordinates(52.14585, 1.06186), part = 0) add(Coordinates(52.14611, 1.06215), part = 0) } ``` ```java // code used for displaying a marker with coordinates added to the same part Marker marker1 = new Marker(); marker1.add(new Coordinates(52.1459, 1.0613), 0); marker1.add(new Coordinates(52.14569, 1.0615), 0); marker1.add(new Coordinates(52.14585, 1.06186), 0); marker1.add(new Coordinates(52.14611, 1.06215), 0); ``` To display any type of marker on a map, it must first be added to a `MarkerCollection`. Creating a collection of markers requires providing a name and specifying the desired `EMarkerType` enum as parameters for its constructor. The collection of markers displayed above used `EMarkerType.Polyline`, but it can also be `EMarkerType.Point` or `EMarkerType.Polygon`. Once the `MarkerCollection` object has been populated, it must be added to the `MapViewMarkerCollections` field within the `MapViewPreferences` class. This can be accessed through the `MapView`, as shown below: * Kotlin * Java ```kotlin mapView.preferences?.markers?.add(markerCollection) ``` ```java if (mapView.getPreferences() != null) { mapView.getPreferences().getMarkers().add(markerCollection); } ``` ##### Point Type Marker[​](#point-type-marker "Direct link to Point Type Marker") Visually represented as an icon, it is used to dynamically highlight user-defined locations. To display a point-type marker, the `MarkerCollection` to which the markers are added must be of the `EMarkerType.Point` type. * Kotlin * Java ```kotlin val marker = Marker().apply { add(Coordinates(52.1459, 1.0613), part = 0) add(Coordinates(52.14569, 1.0615), part = 0) add(Coordinates(52.14585, 1.06186), part = 1) add(Coordinates(52.14611, 1.06215), part = 1) } val markerCollection = MarkerCollection(EMarkerType.Point, "myCollection") markerCollection.add(marker) mapView.preferences?.markers?.add(markerCollection) mapView.centerOnArea(markerCollection.area) ``` ```java Marker marker = new Marker(); marker.add(new Coordinates(52.1459, 1.0613), 0); marker.add(new Coordinates(52.14569, 1.0615), 0); marker.add(new Coordinates(52.14585, 1.06186), 1); marker.add(new Coordinates(52.14611, 1.06215), 1); MarkerCollection markerCollection = new MarkerCollection(EMarkerType.Point, "myCollection"); markerCollection.add(marker); if (mapView.getPreferences() != null) { mapView.getPreferences().getMarkers().add(markerCollection); } mapView.centerOnArea(markerCollection.getArea()); ``` The result will be the following: ![](/docs/android/assets/images/example_android_point_marker1-c5e990489e9bd9837607e1f2cf5edc8a.png) **Displaying point-type markers on map**
warning By default, point-type markers appear as blue circles up to a specific zoom level. When the zoom threshold is exceeded, they automatically cluster into orange circles, and at higher levels of clustering, they transition to red circles. Learn more at [Marker Clustering](/docs/android/guides/maps/display-map-items/display-markers.md#marker-clustering) ##### Polyline Type Marker[​](#polyline-type-marker "Direct link to Polyline Type Marker") This type of marker is designed to display a continuous line consisting of one or more connected straight-line segments. To use it, ensure the `MarkerCollection` specifies `type` as `EMarkerType.Polyline`. It's important to note that markers can include multiple coordinates, which may or may not belong to the same part. Coordinates within the same part are connected by a polyline, which is red by default, while coordinates outside the part remain unconnected. For more information, see [Markers section](/docs/android/guides/core/markers.md#types-of-markers). ##### Polygon Type Marker[​](#polygon-type-marker "Direct link to Polygon Type Marker") This type of marker is designed to display a closed two-dimensional figure composed of straight-line segments that meet at their endpoints. To use it, ensure the `MarkerCollection` specifies `type` as `EMarkerType.Polygon`. ![](/docs/android/assets/images/example_android_draw_shapes2-247a39ea2f2d1f3739d0142a94a7b732.png) **Polygon drawn between three coordinates**
info To successfully create a polygon, at least three coordinates must be added to the same part. Otherwise, the result will be an open polyline rather than a closed shape. Polygons can be customized using properties like `polygonFillColor`. Additionally, since polygon edges are essentially polylines, you can further refine their appearance with polyline-related attributes such as `polylineInnerColor`, `polylineOuterColor`, `polylineInnerSize`, and `polylineOuterSize`. ##### Marker Customizations[​](#marker-customizations "Direct link to Marker Customizations") To customize the appearance of markers on the map, you can use the `MarkerCollectionRenderSettings` class. This class is designed for customizing the appearance of individual markers. It includes various fields that can influence a marker's appearance, regardless of its type, as it provides customizable features for all marker types. For example: * For markers of type `EMarkerType.Polyline`, you can use fields such as `polylineInnerColor`, `polylineOuterColor`, `polylineInnerSize`, and `polylineOuterSize`. * For `EMarkerType.Polygon`, the `polygonFillColor` fields are available, among others. * For `EMarkerType.Point`, you can use fields such as `labelTextColor`, `labelTextSize`, `image`, `imageSize`. All dimensional sizes (`imageSize`, `labelTextSize`, etc.) are measured in millimeters. info If customizations unrelated to a marker's specific type are applied - for example, using `polylineInnerColor` for a `EMarkerType.Point`-they will simply be ignored, and the marker's appearance will remain unaffected. For `EMarkerType.Point`, a key customizable field is `labelingMode`. This field uses values from `EMarkerLabelingMode` enum. This allows you to enable desired features, such as positioning the label text above the icon by setting the appropriate labeling mode as shown below: * Kotlin * Java ```kotlin val renderSettings = MarkerCollectionRenderSettings().apply { labelingMode = EMarkerLabelingMode.Item //labelingMode = EMarkerLabelingMode.ItemCenter } mapView.preferences?.markers?.add(markerCollection, renderSettings) ``` ```java MarkerCollectionRenderSettings renderSettings = new MarkerCollectionRenderSettings(); renderSettings.setLabelingMode(EMarkerLabelingMode.Item); //renderSettings.setLabelingMode(EMarkerLabelingMode.ItemCenter); if (mapView.getPreferences() != null) { mapView.getPreferences().getMarkers().add(markerCollection, renderSettings); } ``` info To hide a marker's name or its group's name, create a `MarkerCollectionRenderSettings` object with a `labelingMode` that excludes `EMarkerLabelingMode.Item` and `EMarkerLabelingMode.Group`. By default, both options are enabled. The above code will result in the following marker appearance: ![](/docs/android/assets/images/example_android_marker_labeling1-92db1a97a5467adf8778211f942891ef.png) **Displaying a marker with text above icon** ![](/docs/android/assets/images/example_android_marker_labeling2-d2ddba0463f2b3845fce1681e69926e6.png) **Displaying a marker with text centered on icon**
info To assign a name to a marker, use the `name` property of the `Marker` class. To customize the icons of the displayed markers, add the collection to `MapViewMarkerCollections` and configure a `MarkerCollectionRenderSettings` instance with the relevant image field. This field controls the appearance of the entire collection. * Kotlin * Java ```kotlin val imageData = assets.open("poi83.png").readBytes() val image = Image.produceWithData(imageData) val renderSettings = MarkerCollectionRenderSettings().apply { this.image = image } ``` ```java byte[] imageData = getAssets().open("poi83.png").readAllBytes(); Image image = Image.produceWithData(imageData); MarkerCollectionRenderSettings renderSettings = new MarkerCollectionRenderSettings(); renderSettings.setImage(image); ``` Code above is setting a custom icon to a marker. The result is the following: ![](/docs/android/assets/images/example_android_point_marker2-0df928d5d4efb09b74c909f06f5767e6.png) **Displaying point-type markers with render settings** ###### Marker Sketches[​](#marker-sketches "Direct link to Marker Sketches") To customize the appearance of each marker individually, use the `MarkerSketches` class, which extends `MarkerCollection`. This lets you define unique styles and properties for every marker. You can obtain a MarkerSketches object using the `MapViewMarkerCollections.sketches()` method: * Kotlin * Java ```kotlin val sketches = mapView.preferences?.markers?.sketches(EMarkerType.Point) ``` ```java MarkerSketches sketches = null; if (mapView.getPreferences() != null) { sketches = mapView.getPreferences().getMarkers().sketches(EMarkerType.Point); } ``` Typical operations are adding a sketch with an optional per‑marker render configuration and position, reading a sketch's rendering configuration. info There are only three `MarkerSketches` collections, one for each marker type: `EMarkerType.Point`, `EMarkerType.Polyline`, and `EMarkerType.Polygon`. Each collection is singleton. Adding markers to a `MarkerSketches` collection is similar to adding them to a `MarkerCollection`. However, when adding markers to a `MarkerSketches` collection, you can specify individual `MarkerRenderSettings` and index for each marker. This allows for greater customization of each marker's appearance. * Kotlin * Java ```kotlin val marker1 = Marker().apply { add(Coordinates(39.76741, -46.8962)) name = "HelloMarker" } sketches?.add(marker1, MarkerRenderSettings().apply { labelTextColor = Rgba.red() labelTextSize = 3.0 image = Image.produceWithId(SdkImages.Core.Toll.value) }, index = 0) ``` ```java Marker marker1 = new Marker(); marker1.add(new Coordinates(39.76741, -46.8962)); marker1.setName("HelloMarker"); MarkerRenderSettings markerRenderSettings = new MarkerRenderSettings(); markerRenderSettings.setLabelTextColor(Rgba.red()); markerRenderSettings.setLabelTextSize(3.0); markerRenderSettings.setImage(Image.produceWithId(SdkImages.Core.Toll.getValue())); if (sketches != null) { sketches.add(marker1, markerRenderSettings, 0); } ``` ![](/docs/android/assets/images/example_android_marker_sketches1-3236890bc87792743656ddc287ad5b2c.png) **Displaying a marker using MarkerSketches** In order to change a marker's appearance after it has been added to a `MarkerSketches` collection, you can use `setRenderSettings` method: * Kotlin * Java ```kotlin sketches?.setRenderSettings( 0, // marker index MarkerRenderSettings().apply { labelTextColor = Rgba.red() labelTextSize = 3.0 image = Image.produceWithId(SdkImages.Core.Toll.value) } ) ``` ```java MarkerRenderSettings newSettings = new MarkerRenderSettings(); newSettings.setLabelTextColor(Rgba.red()); newSettings.setLabelTextSize(3.0); newSettings.setImage(Image.produceWithId(SdkImages.Core.Toll.getValue())); if (sketches != null) { sketches.setRenderSettings(0, newSettings); } ``` In order to obtain the current render settings of a marker, you can use `getRenderSettings` method called with the marker index: * Kotlin * Java ```kotlin val returnedSettings = sketches?.getRenderSettings(0) ``` ```java MarkerRenderSettings returnedSettings = null; if (sketches != null) { returnedSettings = sketches.getRenderSettings(0); } ``` danger Calling `getRenderSettings` with an invalid index will return a `MarkerRenderSettings` object with default values. The `MarkerSketches` collection does not need to be added to `MapViewMarkerCollections`, as it is already part of it. Any changes made to the `MarkerSketches` collection will be automatically reflected on the map. Tip Adding a `MarkerSketches` object to `MapViewMarkerCollections` with `MarkerCollectionRenderSettings` will be overwritten by the individual `MarkerRenderSettings` of markers from the collection. ##### Marker Clustering[​](#marker-clustering "Direct link to Marker Clustering") Clustering or grouping is a default feature of markers. Beyond a certain zoom level, the markers automatically cluster into a single marker containing a number of items lesser than `lowDensityPointsGroupMaxCount` if the group is a low density one. The image of those groups can be customized with `lowDensityPointsGroupImage`, `mediumDensityPointsGroupImage`, `highDensityPointsGroupImage` fields of `MarkerCollectionRenderSettings`. The number of markers contained by a group can be set through `lowDensityPointsGroupMaxCount`, `mediumDensityPointsGroupMaxCount`. **Grouping behaviour** * Kotlin * Java ```kotlin // code for markers not grouping at zoom level 70 val renderSettings = MarkerCollectionRenderSettings() mapView.preferences?.markers?.add(markerCollection, renderSettings) mapView.centerOnCoordinates(Coordinates(52.14611, 1.06215), zoomLevel = 70) ``` ```java // code for markers not grouping at zoom level 70 MarkerCollectionRenderSettings renderSettings = new MarkerCollectionRenderSettings(); if (mapView.getPreferences() != null) { mapView.getPreferences().getMarkers().add(markerCollection, renderSettings); } mapView.centerOnCoordinates(new Coordinates(52.14611, 1.06215), 70); ``` ![](/docs/android/assets/images/example_android_marker_grouping1-83f1a3dc40fae4b3ce3fb2616ce35a0f.png) **Markers not clustering** * Kotlin * Java ```kotlin // code for markers grouping at zoom level 70 val renderSettings = MarkerCollectionRenderSettings().apply { labelTextSize = 3.0 labelingMode = EMarkerLabelingMode.Item pointsGroupingZoomLevel = 70 } mapView.preferences?.markers?.add(markerCollection, renderSettings) mapView.centerOnCoordinates(Coordinates(52.14611, 1.06215), zoomLevel = 70) ``` ```java // code for markers grouping at zoom level 70 MarkerCollectionRenderSettings renderSettings = new MarkerCollectionRenderSettings(); renderSettings.setLabelTextSize(3.0); renderSettings.setLabelingMode(EMarkerLabelingMode.Item); renderSettings.setPointsGroupingZoomLevel(70); if (mapView.getPreferences() != null) { mapView.getPreferences().getMarkers().add(markerCollection, renderSettings); } mapView.centerOnCoordinates(new Coordinates(52.14611, 1.06215), 70); ``` ![](/docs/android/assets/images/example_android_marker_grouping2-a67d97faa319983edf86c0df61e82a0a.png) **Clustered markers** info You can disable marker clustering by setting the `pointsGroupingZoomLevel` to 0. However, note that doing so for a large number of markers may significantly impact performance, as rendering each individual marker increases GPU resource usage. Marker clusters are represented by the first marker from the collection as the **group head**. The group head marker is returned by the `getPointsGroupHead` method: * Kotlin * Java ```kotlin val markerCollection = MarkerCollection(EMarkerType.Point, "Collection1") val marker1 = Marker().apply { add(Coordinates(39.76717, -46.89583)) name = "NiceName" } val marker2 = Marker().apply { add(Coordinates(39.767138, -46.895640)) name = "NiceName2" } val marker3 = Marker().apply { add(Coordinates(39.767145, -46.895690)) name = "NiceName3" } markerCollection.add(marker1) markerCollection.add(marker2) markerCollection.add(marker3) mapView.preferences?.markers?.add(markerCollection, MarkerCollectionRenderSettings().apply { buildPointsGroupConfig = true }) // This centering triggers marker grouping mapView.centerOnCoordinates( Coordinates(39.76717, -46.89583), zoomLevel = 50) // Wait for the center process to finish Thread.sleep(250) val marker = markerCollection.getPointsGroupHead(marker2.id) // Returns marker1 ``` ```java MarkerCollection markerCollection = new MarkerCollection(EMarkerType.Point, "Collection1"); Marker marker1 = new Marker(); marker1.add(new Coordinates(39.76717, -46.89583)); marker1.setName("NiceName"); Marker marker2 = new Marker(); marker2.add(new Coordinates(39.767138, -46.895640)); marker2.setName("NiceName2"); Marker marker3 = new Marker(); marker3.add(new Coordinates(39.767145, -46.895690)); marker3.setName("NiceName3"); markerCollection.add(marker1); markerCollection.add(marker2); markerCollection.add(marker3); MarkerCollectionRenderSettings groupSettings = new MarkerCollectionRenderSettings(); groupSettings.setBuildPointsGroupConfig(true); if (mapView.getPreferences() != null) { mapView.getPreferences().getMarkers().add(markerCollection, groupSettings); } // This centering triggers marker grouping mapView.centerOnCoordinates( new Coordinates(39.76717, -46.89583), 50); // Wait for the center process to finish Thread.sleep(250); Marker marker = markerCollection.getPointsGroupHead(marker2.getId()); // Returns marker1 ``` Since marker grouping depends on the loading of tiles at a certain zoom level, you need to wait for them to load; otherwise, calling `getPointsGroupHead` will return a reference to the queried marker, because the markers are not yet grouped. Thus `getPointsGroupComponents` will return an empty list. danger This behavior occurs only when the `MarkerCollection` is added to `MapViewMarkerCollections` using `MarkerCollectionRenderSettings().apply { buildPointsGroupConfig = true }` and the markers are **grouped** based on the zoom level. In all other cases, the method returns a direct reference to the queried marker. All markers from a group can be returned by using `getPointsGroupComponents` method called with the head marker id, returned by `MarkerCollection.getPointsGroupHead` method, which is considered the `groupId`. This method returns all markers except the group head marker. * Kotlin * Java ```kotlin val marker = markerCollection.getPointsGroupHead(marker2.id) val groupMarkers = markerCollection.getPointsGroupComponents(marker?.id ?: 0) ``` ```java Marker marker = markerCollection.getPointsGroupHead(marker2.getId()); List groupMarkers = markerCollection.getPointsGroupComponents( marker != null ? marker.getId() : 0); ``` danger If `getPointsGroupComponents` is not invoked with the ID of the group head marker, the method will return an empty list. ##### Adding large amount of markers[​](#adding-large-amount-of-markers "Direct link to Adding large amount of markers") If there is a need for adding lots of markers at the same time, you can add them directly to a `MarkerCollection` efficiently. For individual marker customization, use `MarkerSketches`. The following example shows how to add multiple markers: * Kotlin * Java ```kotlin val markerCollection = MarkerCollection(EMarkerType.Point, "PoiMarkers") // Add markers to the collection for (i in 0 until 8000) { // Generate random coordinates to display some markers val randomLat = minLat + Random.nextDouble() * (maxLat - minLat) val randomLon = minLon + Random.nextDouble() * (maxLon - minLon) val marker = Marker().apply { add(Coordinates(randomLat, randomLon)) name = "POI $i" } markerCollection.add(marker) } // Create the settings for the collections val settings = MarkerCollectionRenderSettings().apply { // Set the label size labelGroupTextSize = 2.0 // The zoom level at which the markers will be grouped together pointsGroupingZoomLevel = 35 // Set the image of the collection image = Image.produceWithData(imageBytes) } // Add the markers and the settings on the map mapView.preferences?.markers?.add(markerCollection, settings) // To clear all markers you can use: mapView.preferences?.markers?.clear() ``` ```java MarkerCollection markerCollection = new MarkerCollection(EMarkerType.Point, "PoiMarkers"); // Add markers to the collection for (int i = 0; i < 8000; i++) { // Generate random coordinates to display some markers double randomLat = minLat + Math.random() * (maxLat - minLat); double randomLon = minLon + Math.random() * (maxLon - minLon); Marker marker = new Marker(); marker.add(new Coordinates(randomLat, randomLon)); marker.setName("POI " + i); markerCollection.add(marker); } // Create the settings for the collections MarkerCollectionRenderSettings settings = new MarkerCollectionRenderSettings(); // Set the label size settings.setLabelGroupTextSize(2.0); // The zoom level at which the markers will be grouped together settings.setPointsGroupingZoomLevel(35); // Set the image of the collection settings.setImage(Image.produceWithData(imageBytes)); // Add the markers and the settings on the map if (mapView.getPreferences() != null) { mapView.getPreferences().getMarkers().add(markerCollection, settings); // To clear all markers you can use: mapView.getPreferences().getMarkers().clear(); } ``` --- ### Display overlays |

Overlays are used to provide enhanced, layered information that adds contextual value to a base map, offering users dynamic, interactive, and customized data that can be visualized on top of other map elements. ##### Disabling overlays[​](#disabling-overlays "Direct link to Disabling overlays") Overlays can be disabled either by applying a predefined, custom map style created in [Magic Lane Map Studio](https://developer.magiclane.com/documentation/OnlineStudio/guide_creating_a_style.html)-where certain overlays are already disabled - or by using the `disableOverlay` method, as shown below: * Kotlin * Java ```kotlin val overlayService = OverlayService() val error = overlayService.disableOverlay(ECommonOverlayId.PublicTransport.value, categoryId = -1) ``` ```java OverlayService overlayService = new OverlayService(); int error = overlayService.disableOverlay(ECommonOverlayId.PublicTransport.getValue(), -1); ``` Passing -1 (default value) as the optional `categoryId` parameter indicates that we want to disable the entire public transport overlay, rather than targeting a specific category. The error returned will be `success` if the overlay was disabled or `notFound` if no overlay (or overlay category) with the specified ID was found in the applied style. To disable specific overlays within a category, you'll need to retrieve their unique identifiers (uid) as shown below: * Kotlin * Java ```kotlin val overlayService = OverlayService() // Get available overlays with callback val overlayCollection = overlayService.getAvailableOverlays { error, _ -> if (error == GemError.NoError) { // Access overlays through MapView preferences (if available) } } overlayCollection?.first?.let { collection -> if (collection.size > 0) { val overlayInfo = collection[0] val uid = overlayInfo.uid val disableError = overlayService.disableOverlay(uid, -1) // Handle the error result Log.d("Overlay", "Disable result: $disableError") } } ``` ```java OverlayService overlayService = new OverlayService(); // Get available overlays with callback Pair, Integer> overlayCollection = overlayService.getAvailableOverlays((error, result) -> { if (error == GemError.NoError) { // Access overlays through MapView preferences (if available) } }); if (overlayCollection != null && overlayCollection.getFirst() != null) { List collection = overlayCollection.getFirst(); if (collection.size() > 0) { OverlayInfo overlayInfo = collection.get(0); int uid = overlayInfo.getUid(); int disableError = overlayService.disableOverlay(uid, -1); // Handle the error result Log.d("Overlay", "Disable result: " + disableError); } } ``` ##### Enabling overlays[​](#enabling-overlays "Direct link to Enabling overlays") In a similar way, the overlay can be enabled using the `enableOverlay` method by providing the overlay id. It also has an optional `categoryId` parameter, which when left as default, it activates whole overlay rather than a specific category. * Kotlin * Java ```kotlin val overlayService = OverlayService() val error = overlayService.enableOverlay(ECommonOverlayId.PublicTransport.value, categoryId = -1) ``` ```java OverlayService overlayService = new OverlayService(); int error = overlayService.enableOverlay(ECommonOverlayId.PublicTransport.getValue(), -1); ``` --- ### Display paths |

[Paths](/docs/android/guides/core/base-entities.md#path) can be displayed by adding them into `MapViewPathCollection`. The `MapViewPathCollection` is an iterable collection, having fields like `size`, `add`, `remove`, `removeAt`, `getPathAt` and `getPathByName`. * Kotlin * Java ```kotlin mapView.preferences?.paths?.add(path) ``` ```java if (mapView.getPreferences() != null) { mapView.getPreferences().getPaths().add(path); } ``` The `add` method of `MapViewPathCollection` includes optional parameters for customizing the appearance of paths on the map, such as `border`, `fill`, `szBorder`, and `szInner`. To center the map on a path, use the `MapView.centerOnArea()` method with the path's area retrieved from the area property. * Kotlin * Java ```kotlin mapView.preferences?.paths?.add( path, border = Rgba.black(), fill = Rgba.orange(), szBorder = 0.5, szInner = 1.0 ) path.area?.let { mapView.centerOnArea(it) } ``` ```java if (mapView.getPreferences() != null) { mapView.getPreferences().getPaths().add( path, Rgba.black(), // border Rgba.orange(), // fill 0.5, // szBorder 1.0 // szInner ); } if (path.getArea() != null) { mapView.centerOnArea(path.getArea()); } ``` ![](/docs/android/assets/images/example_android_present_path1-78e8b11c2ba2e4b023e383379e4edb5c.png) **Path displayed** #### Convenience methods[​](#convenience-methods "Direct link to Convenience methods") The `MapView` class provides convenient methods for displaying and managing paths: ##### Display paths[​](#display-paths-1 "Direct link to Display paths") * Kotlin * Java ```kotlin // Display a single path mapView.displayPath(path, border = Rgba.black(), fill = Rgba.orange()) // Display multiple paths mapView.displayPaths(pathsList, border = Rgba.black(), fill = Rgba.orange()) ``` ```java // Display a single path mapView.displayPath(path, Rgba.black(), Rgba.orange()); // Display multiple paths mapView.displayPaths(pathsList, Rgba.black(), Rgba.orange()); ``` ##### Present a path with centering[​](#present-a-path-with-centering "Direct link to Present a path with centering") * Kotlin * Java ```kotlin // Present a path and automatically center the map on it mapView.presentPath( path = path, border = Rgba.black(), fill = Rgba.orange(), doCenterOn = true, animation = Animation(EAnimation.Linear, 1000), zoomLevel = -1 // Automatic zoom level selection ) ``` ```java // Present a path and automatically center the map on it mapView.presentPath( path, Rgba.black(), // border Rgba.orange(), // fill true, // doCenterOn new Animation(EAnimation.Linear, 1000), // animation -1 // Automatic zoom level selection ); ``` ##### Remove paths[​](#remove-paths "Direct link to Remove paths") * Kotlin * Java ```kotlin // Hide a specific path mapView.hidePath(path) // Remove all paths from the map mapView.preferences?.paths?.clear() // Remove a specific path from the collection mapView.preferences?.paths?.remove(path) // Remove a path by index mapView.preferences?.paths?.removeAt(index) ``` ```java // Hide a specific path mapView.hidePath(path); // Remove all paths from the map if (mapView.getPreferences() != null) { mapView.getPreferences().getPaths().clear(); // Remove a specific path from the collection mapView.getPreferences().getPaths().remove(path); // Remove a path by index mapView.getPreferences().getPaths().removeAt(index); } ``` #### Working with path collections[​](#working-with-path-collections "Direct link to Working with path collections") * Kotlin * Java ```kotlin SdkCall.execute { mapView.preferences?.paths?.let { pathCollection -> // Add a path with custom appearance pathCollection.add( path, border = Rgba(255, 0, 0, 255), // Red border fill = Rgba(0, 255, 0, 255), // Green fill szBorder = 1.0, // Border size in mm szInner = 2.0 // Inner size in mm ) // Get collection size val pathCount = pathCollection.size // Get a path by index val firstPath = pathCollection.getPathAt(0) // Get a path by name val namedPath = pathCollection.getPathByName("My Path") // Get path colors and sizes val borderColor = pathCollection.getBorderColorAt(0) val fillColor = pathCollection.getFillColorAt(0) val borderSize = pathCollection.getBorderSizeAt(0) val innerSize = pathCollection.getInnerSizeAt(0) } } ``` ```java SdkCall.execute(() -> { if (mapView.getPreferences() != null) { MapViewPathCollection pathCollection = mapView.getPreferences().getPaths(); // Add a path with custom appearance pathCollection.add( path, new Rgba(255, 0, 0, 255), // Red border new Rgba(0, 255, 0, 255), // Green fill 1.0, // Border size in mm 2.0 // Inner size in mm ); // Get collection size int pathCount = pathCollection.getSize(); // Get a path by index Path firstPath = pathCollection.getPathAt(0); // Get a path by name Path namedPath = pathCollection.getPathByName("My Path"); // Get path colors and sizes Rgba borderColor = pathCollection.getBorderColorAt(0); Rgba fillColor = pathCollection.getFillColorAt(0); double borderSize = pathCollection.getBorderSizeAt(0); double innerSize = pathCollection.getInnerSizeAt(0); } }); ``` #### Creating paths from GPX data[​](#creating-paths-from-gpx-data "Direct link to Creating paths from GPX data") * Kotlin * Java ```kotlin SdkCall.execute { // Load a path from GPX file in assets val input = applicationContext.resources.assets.open("gpx/route.gpx") val path = Path.produceWithGpx(input) path?.let { // Display the path on the map mapView.presentPath(it, border = Rgba.blue(), fill = Rgba.cyan()) } } ``` ```java SdkCall.execute(() -> { // Load a path from GPX file in assets InputStream input = getApplicationContext().getResources().getAssets().open("gpx/route.gpx"); Path path = Path.produceWithGpx(input); if (path != null) { // Display the path on the map mapView.presentPath(path, Rgba.blue(), Rgba.cyan()); } }); ``` To remove all paths from the map use `MapViewPathCollection.clear()`. To remove selectively, use `MapViewPathCollection.remove(path)` or `MapViewPathCollection.removeAt(index)`. --- ### Display route instructions |

Instructions are represented as arrows on the map and can be displayed by using `MapView.centerOnRouteInstruction(instruction, zoomLevel, xy, animation, viewAngle)`. To obtain a route's instructions, see the [Route structure](/docs/android/guides/core/routes.md#route-instruction-structure) section. The following example iterates through all instructions of a route and displays each one by centering: * Kotlin * Java ```kotlin val routeInstruction = route.instructions[0] mapView.centerOnRouteInstruction( instruction = routeInstruction, zoomLevel = 75, xy = null, // Use default center position animation = Animation(EAnimation.Linear, 1000), // 1 second animation viewAngle = Double.MAX_VALUE // Use default view angle ) ``` ```java RouteInstruction routeInstruction = route.getInstructions().get(0); mapView.centerOnRouteInstruction( routeInstruction, 75, // zoomLevel null, // Use default center position new Animation(EAnimation.Linear, 1000), // 1 second animation Double.MAX_VALUE // Use default view angle ); ``` ![](/docs/android/assets/images/example_android_display_route_instruction-cbad713a06ff6b984b9bf0e36b985b49.png) **Turn right arrow instruction** The route instruction arrow is automatically cleared when a new route instruction is centered on or when the route is cleared using `mapView.hideRoutes()`. --- ### Display routes |

Routes can be displayed on the map by using `mapView.preferences?.routes?.add(route, isMainRoute)`. Multiple routes can be displayed at the same time, but only one is the main one, the others being treated as secondary. Specifying which one is the main route can be done when calling `MapViewRoutesCollection.add()` by passing `true` to the `mainRoute` parameter, or by calling the `MapViewRoutesCollection.mainRoute` setter. * Kotlin * Java ```kotlin mapView.preferences?.routes?.add(route, true) mapView.centerOnRoute(route) ``` ```java if (mapView.getPreferences() != null) { mapView.getPreferences().getRoutes().add(route, true); } mapView.centerOnRoute(route); ``` ![](/docs/android/assets/images/example_android_display_routes1-34c95d1808aec210f42ad26316ac1f67.png) **Route displayed** Tip To center on a route with padding, refer to the [Adjust Map View](/docs/android/guides/maps/adjust-map.md#map-centering-on-area-with-padding) guide. Utilize the `rect` parameter in the `centerOnRoute` method to define the specific region of the viewport that should be centered. * Kotlin * Java ```kotlin mapView.preferences?.routes?.add(route1, true) mapView.preferences?.routes?.add(route2, false) mapView.preferences?.routes?.add(route3, false) mapView.centerOnRoutes(arrayListOf(route1, route2, route3)) ``` ```java if (mapView.getPreferences() != null) { mapView.getPreferences().getRoutes().add(route1, true); mapView.getPreferences().getRoutes().add(route2, false); mapView.getPreferences().getRoutes().add(route3, false); } ArrayList routes = new ArrayList<>(); routes.add(route1); routes.add(route2); routes.add(route3); mapView.centerOnRoutes(routes); ``` ![](/docs/android/assets/images/example_android_display_routes2-e3a8baf962dfaff34b34e8492767dda7.png) **Three routes displayed, one in the middle is main** Route appearance on map can be customized via `RouteRenderSettings` when added, passed to the `MapViewRoutesCollection.addWithRenderSettings()` method, or later on, via `MapViewRoute.renderSettings` setter. * Kotlin * Java ```kotlin val renderSettings = RouteRenderSettings().apply { innerColor = Rgba(255, 0, 0, 255) } mapView.preferences?.routes?.addWithRenderSettings(route, renderSettings) ``` ```java RouteRenderSettings renderSettings = new RouteRenderSettings(); renderSettings.setInnerColor(new Rgba(255, 0, 0, 255)); if (mapView.getPreferences() != null) { mapView.getPreferences().getRoutes().addWithRenderSettings(route, renderSettings); } ``` * Kotlin * Java ```kotlin val mapViewRoute = mapView.preferences?.routes?.getMapViewRouteByIndex(0) mapViewRoute?.renderSettings = RouteRenderSettings().apply { innerColor = Rgba(255, 0, 0, 255) } ``` ```java MapViewRoute mapViewRoute = null; if (mapView.getPreferences() != null) { mapViewRoute = mapView.getPreferences().getRoutes().getMapViewRouteByIndex(0); } if (mapViewRoute != null) { RouteRenderSettings renderSettings = new RouteRenderSettings(); renderSettings.setInnerColor(new Rgba(255, 0, 0, 255)); mapViewRoute.setRenderSettings(renderSettings); } ``` All dimensional sizes within the `RouteRenderSettings` are measured in millimeters. ![](/docs/android/assets/images/example_android_display_routes3-00b0a6b388e968aa4118c093540eb04a.png) **Route displayed with custom render settings** ##### Convenience methods[​](#convenience-methods "Direct link to Convenience methods") The `MapView` class provides convenient `presentRoute` and `presentRoutes` methods that combine adding routes to the collection and centering the map: * Kotlin * Java ```kotlin // Present a single route with default settings mapView.presentRoute(route) // Present a single route with custom settings mapView.presentRoute( route, displayBubble = true, displayRouteName = true, displayTrafficIcon = true, centerMapView = true ) // Present multiple routes mapView.presentRoutes( routes, mainRoute = routes.first(), displayBubble = true, centerMapView = true ) ``` ```java // Present a single route with default settings mapView.presentRoute(route); // Present a single route with custom settings mapView.presentRoute( route, true, // displayBubble true, // displayRouteName true, // displayTrafficIcon true // centerMapView ); // Present multiple routes mapView.presentRoutes( routes, routes.get(0), // mainRoute true, // displayBubble true // centerMapView ); ``` To remove displayed routes, use `MapViewRoutesCollection.clear()`. You can also remove individual routes with `mapView.preferences?.routes?.remove(route)`. ##### Set route labels[​](#set-route-labels "Direct link to Set route labels") A route can include a label that provides information such as ETA, distance, toll prices, and more. To attach a label to a route, use the `addWithAttributes` method of the `MapViewRoutesCollection`: * Kotlin * Java ```kotlin mapView.preferences?.routes?.addWithAttributes(route, true, "Added label") ``` ```java if (mapView.getPreferences() != null) { mapView.getPreferences().getRoutes().addWithAttributes(route, true, "Added label"); } ``` ![](/docs/android/assets/images/example_android_route_label1-dae1f891a5944b71918a083ce9ff5cf1.png) **Route with label** You can enhance the label by adding up to **two icons** using the `images` parameter, which accepts an `ImageList`. Available icons can be accessed through the `ImageDatabase` class. * Kotlin * Java ```kotlin val images = ImageList().apply { ImageDatabase.tollImage?.let { add(it) } ImageDatabase.ferryImage?.let { add(it) } } mapView.preferences?.routes?.addWithAttributes( route, true, "This is a custom label", images ) ``` ```java ImageList images = new ImageList(); if (ImageDatabase.getTollImage() != null) { images.add(ImageDatabase.getTollImage()); } if (ImageDatabase.getFerryImage() != null) { images.add(ImageDatabase.getFerryImage()); } if (mapView.getPreferences() != null) { mapView.getPreferences().getRoutes().addWithAttributes( route, true, "This is a custom label", images ); } ``` ![](/docs/android/assets/images/example_android_custom_label-29dcfb5bcd11419df53ec869661fa601.png) **Label with custom icons** The label can also be auto-generated using route information: * Kotlin * Java ```kotlin // Auto-generated labels are created using route's timeDistance information val route = routes.first() val timeDistance = route.timeDistance!! val distText = GemUtil.getDistText(timeDistance.totalDistance, SdkSettings.unitSystem, true) val timeText = GemUtil.getTimeText(timeDistance.totalTime) val autoLabel = "${distText.first} ${distText.second}\n${timeText.first} ${timeText.second}" mapView.preferences?.routes?.addWithAttributes(route, true, autoLabel) ``` ```java // Auto-generated labels are created using route's timeDistance information Route route = routes.get(0); TimeDistance timeDistance = route.getTimeDistance(); Pair distText = GemUtil.getDistText(timeDistance.getTotalDistance(), SdkSettings.getUnitSystem(), true); Pair timeText = GemUtil.getTimeText(timeDistance.getTotalTime()); String autoLabel = distText.getFirst() + " " + distText.getSecond() + "\n" + timeText.getFirst() + " " + timeText.getSecond(); if (mapView.getPreferences() != null) { mapView.getPreferences().getRoutes().addWithAttributes(route, true, autoLabel); } ``` ![](/docs/android/assets/images/example_android_route_label2-a6681a848d0ff0ac5b76af93a2e6c114.png) **Route with generated label** The label of a route added to the collection can be hidden by calling `MapViewRoutesCollection.hideLabel(route)`. Labels can also be managed through a `MapViewRoute` object. The `label` property is used to assign a label, while the `hideLabel()` method can be used to hide it. danger Auto-generated labels will override any customizations made with the `label` and `images` parameters. ##### Check what portion of a route is visible on a screen region[​](#check-what-portion-of-a-route-is-visible-on-a-screen-region "Direct link to Check what portion of a route is visible on a screen region") To retrieve the visible portion of a route - defined by its start and end distances in meters - use the `getVisibleRouteInterval` method from the `MapView`: * Kotlin * Java ```kotlin val visibleInterval = mapView.getVisibleRouteInterval(route, null) val startRouteVisibleIntervalMeters = visibleInterval?.x?.toInt() ?: 0 val endRouteVisibleIntervalMeters = visibleInterval?.y?.toInt() ?: 0 ``` ```java PointF visibleInterval = mapView.getVisibleRouteInterval(route, null); int startRouteVisibleIntervalMeters = visibleInterval != null ? (int) visibleInterval.x : 0; int endRouteVisibleIntervalMeters = visibleInterval != null ? (int) visibleInterval.y : 0; ``` You can also provide a custom screen region to the `getVisibleRouteInterval` method by passing a `Rect` parameter, instead of using the entire viewport. The method will return `null` if the route is not visible on the provided viewport/region of the viewport --- ### Get started with maps |

The Maps SDK for Android delivers powerful mapping capabilities, enabling developers to effortlessly integrate dynamic map views into their applications. Core features include embedding and customizing map views, controlling displayed locations, and fine-tuning map properties. At the center of the mapping API is the `GEMSurfaceView` class, which serves as the primary component for rendering maps and handling user interactions. #### Display a map[​](#display-a-map "Direct link to Display a map") See the [Code Implementation section from Create your first app guide](/docs/android/guides/get-started/create-your-first-app.md#implement-the-application-code) for a complete example of displaying a basic map in your application. ![](/docs/android/assets/images/example_android_hello_map1-bdb5edaa18b263ef6f5eca72726c2f8c.png) **Displaying a default day map**
#### GEMSurfaceView[​](#gemsurfaceview "Direct link to GEMSurfaceView") The `GemSurfaceView` class is the primary component for displaying maps in an Android application. It extends `GLSurfaceView` and provides a complete OpenGL rendering surface for map display with built-in touch interaction handling. ##### Key Features[​](#key-features "Direct link to Key Features") * **Automatic SDK Initialization**: If GemSdk isn't initialized, the surface automatically calls `GemSdk.initSdkWithDefaults()` when attached to a window * **Default MapView Creation**: Creates a default `MapView` automatically unless disabled * **Touch Event Handling**: Built-in support for pan, zoom, and other map interactions * **Lifecycle Management**: Automatic resource management with configurable cleanup behavior * **OpenGL Context Management**: Handles EGL context creation and configuration ##### Properties[​](#properties "Direct link to Properties") | Property | Type | Description | | -------------------------- | --------------------------- | ---------------------------------------------------------- | | `gemGlContext` | `OpenGLContext?` | The OpenGL context created by this surface | | `gemScreen` | `Screen?` | The screen object for rendering operations | | `mapView` | `MapView?` | The default MapView instance (if auto-creation is enabled) | | `hadBeenInitialized` | `Boolean` | Flag indicating if this instance has been initialized | | `hadBeenReleased` | `Boolean` | Flag indicating if this instance has been released | | `visibilityChangeListener` | `VisibilityChangeListener?` | Listener for visibility change events | ##### Callback Functions[​](#callback-functions "Direct link to Callback Functions") | Callback | Description | | --------------------------- | ------------------------------------------------------------------------------------------------------------ | | `onInitSdk` | Called when the view is about to initialize the SDK. If provided, user is responsible for SDK initialization | | `onSdkInitSucceeded` | Triggered after successful SDK initialization | | `onScreenCreated` | Called after the OpenGL screen has been successfully created | | `onDefaultMapViewCreated` | Triggered after the default MapView has been created | | `onPreHandleTouchListener` | Called before the screen handles touch events | | `onPostHandleTouchListener` | Called after the screen handles touch events | | `onDrawFrameCustom` | Custom drawing operations on the OpenGL thread | ##### XML Attributes[​](#xml-attributes "Direct link to XML Attributes") You can configure `GemSurfaceView` behavior using XML attributes: ```xml ``` | Attribute | Type | Default | Description | | --------------------------------- | --------- | ------- | -------------------------------------------------------- | | `createDefaultMapView` | `Boolean` | `true` | Whether to automatically create a default MapView | | `autoReleaseOnDetachedFromWindow` | `Boolean` | `true` | Whether to automatically release resources when detached | | `sdkToken` | `String` | `null` | SDK authorization token | ##### Methods[​](#methods "Direct link to Methods") | Method | Description | | ------------------------- | ------------------------------------------------------------------ | | `release()` | Releases the drawing context and associated resources | | `releaseDefaultMapView()` | Releases only the default MapView while keeping the surface active | ##### Usage Example[​](#usage-example "Direct link to Usage Example") * Kotlin * Java ```kotlin // In your Activity or Fragment val gemSurfaceView = findViewById(R.id.gemSurfaceView) // Configure callbacks gemSurfaceView.onDefaultMapViewCreated = { mapView -> // Configure your map view mapView.centerOnCoordinates(coordinates, zoomLevel) } gemSurfaceView.onSdkInitSucceeded = { // SDK is ready Log.d("Map", "SDK initialized successfully") } // Add to your layout parentLayout.addView(gemSurfaceView) ``` ```java // In your Activity or Fragment GemSurfaceView gemSurfaceView = findViewById(R.id.gemSurfaceView); // Configure callbacks gemSurfaceView.setOnDefaultMapViewCreated(mapView -> { // Configure your map view mapView.centerOnCoordinates(coordinates, zoomLevel); }); gemSurfaceView.setOnSdkInitSucceeded(() -> { // SDK is ready Log.d("Map", "SDK initialized successfully"); }); // Add to your layout parentLayout.addView(gemSurfaceView); ``` ##### Programmatic Creation[​](#programmatic-creation "Direct link to Programmatic Creation") * Kotlin * Java ```kotlin val gemSurfaceView = GemSurfaceView( context = this, doCreateDefaultMapView = true, sdkToken = "your_sdk_token", autoReleaseOnDetachedFromWindow = true, postLambdasOnMain = true ) ``` ```java GemSurfaceView gemSurfaceView = new GemSurfaceView( this, true, // doCreateDefaultMapView "your_sdk_token", true, // autoReleaseOnDetachedFromWindow true // postLambdasOnMain ); ``` --- ### Interact with the map |

The Maps SDK for Android map view natively supports common gestures like pinch and double-tap for zooming. The table below outlines the available gestures and their default behaviors on the map. | Gesture | Description | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Tap | **Tap the screen with one finger**. This gesture does not have a predefined map action. | | Double Tap | **To zoom the map in by a fixed amount**, tap the screen twice with one finger. | | Long Press | **Press and hold one finger to the screen**. This gesture does not have a predefined map action. | | Pan | **To move the map**, press and hold one finger to the screen, and move it in any direction. The map will keep moving with a little momentum after the finger was lifted. | | 2 Finger Pan / Shove | **To tilt the map**, press and hold two fingers to the screen, and move them vertically. No behavior is predefined for other directions. | | 2 Finger Tap | **To align map towards north**, tap the screen with two fingers. | | Pinch | **To zoom in or out continuously**, press and hold two fingers to the screen, and increase or decrease the distance between them. **To rotate the map continuously**, press and hold two fingers to the screen, and change the angle between them either by rotating them both or by moving one of them. | The SDK provides support in `MapView`, for informing whenever the user performs an action that could be detected. Usually, you will want to add a specific behavior to your application after a gesture was detected, like performing a selection after a tap on map. * Tap: `onTouch` * Double Tap (one finger taps the same area in quick succession): `onDoubleTouch` * Two Taps (two fingers tap the screen simultaneously): `onTwoTouches` * Long Press: `onLongDown` * Pan: `onMove` obtains the two points between which the movement occurred. * Shove: `onShove` obtains the angle, and gesture specific points * Rotate: `onMapAngleUpdated` * Fling: `onSwipe` * Pinch: `onPinch` The user can also listen for composite gestures: * Tap followed by a pan: `onTouchMove` * Pinch followed by a swipe: `onPinchSwipe` * Tap followed by a pinch: `onTouchPinch` * Two double touches: `onTwoDoubleTouches` danger Keep in mind that only one listener can be active at a time for a specific gesture. If multiple listeners are registered, only the most recently set listener will be invoked when the gesture is detected. Use `isCameraMoving()` to check if the map camera is currently moving. #### Enable and disable gestures[​](#enable-and-disable-gestures "Direct link to Enable and disable gestures") Touch gestures can be disabled or enabled by calling `enableTouchGestures` method like so: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView mapView?.preferences?.enableTouchGestures( ETouchGestures.OnTouch.value or ETouchGestures.OnMove.value, false ) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); if (mapView != null && mapView.getPreferences() != null) { mapView.getPreferences().enableTouchGestures( ETouchGestures.OnTouch.getValue() | ETouchGestures.OnMove.getValue(), false ); } ``` info The desired gestures in the `ETouchGestures` enum can be enabled or disabled by setting the `enabled` parameter to `true` or `false`, respectively. By default, all gestures are enabled. The `ETouchGestures` enum supports the following gesture types: * *Basic Touch*: `OnTouch`, `OnLongDown`, `OnDoubleTouch`, `OnTwoPointersTouch`, `OnTwoPointersDoubleTouch` * *Movement*: `OnMove`, `OnTouchMove`, `OnSwipe` * *Pinch and Rotation*: `OnPinchSwipe`, `OnPinch`, `OnRotate`, `OnShove` * *Combined Gestures*: `OnTouchPinch`, `OnTouchRotate`, `OnTouchShove`, `OnRotatingSwipe` For checking if a gesture is enabled, the `isTouchGestureEnabled` method can be used: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView val isTouchEnabled = mapView?.preferences?.isTouchGestureEnabled(ETouchGestures.OnTouch) ?: false ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); boolean isTouchEnabled = mapView != null && mapView.getPreferences() != null ? mapView.getPreferences().isTouchGestureEnabled(ETouchGestures.OnTouch) : false; ``` #### Implement gesture listeners[​](#implement-gesture-listeners "Direct link to Implement gesture listeners") Let's see an example of how gesture listeners can be registered. The `MapView` provides specific listeners for each gesture. As soon as you register a listener, it will receive all related events for that gesture via the dedicated callback. * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView // Set up gesture listeners mapView?.onMapAngleUpdated = { angle -> Log.d("Gesture", "onMapAngleUpdated: $angle") } mapView?.onTouch = { point -> Log.d("Gesture", "onTouch: $point") } mapView?.onMove = { start, end -> Log.d("Gesture", "onMove from (${start.x}, ${start.y}) to (${end.x}, ${end.y})") } mapView?.onLongDown = { point -> Log.d("Gesture", "onLongDown: $point") } ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); // Set up gesture listeners if (mapView != null) { mapView.setOnMapAngleUpdated(angle -> { Log.d("Gesture", "onMapAngleUpdated: " + angle); }); mapView.setOnTouch(point -> { Log.d("Gesture", "onTouch: " + point); }); mapView.setOnMove((start, end) -> { Log.d("Gesture", "onMove from (" + start.x + ", " + start.y + ") to (" + end.x + ", " + end.y + ")"); }); mapView.setOnLongDown(point -> { Log.d("Gesture", "onLongDown: " + point); }); } ``` danger Executing resource-intensive tasks within map-related callbacks can degrade performance. #### Implement map render listeners[​](#implement-map-render-listeners "Direct link to Implement map render listeners") The `onViewportResized` listener allows you to monitor when the map's viewport dimensions change. This can occur when the user resizes the application window or changes the orientation of the device. In this callback, you receive a `Rect` object representing the new viewport size. * Kotlin * Java ```kotlin mapView?.onViewportResized = { rect -> Log.d("Viewport", "Viewport resized to: ${rect.width}x${rect.height}") } ``` ```java if (mapView != null) { mapView.setOnViewportResized(rect -> { Log.d("Viewport", "Viewport resized to: " + rect.getWidth() + "x" + rect.getHeight()); }); } ``` Use cases include: * Adjusting overlays or UI elements to fit the new viewport size. * Triggering animations or updates based on the map's dimensions. The `onViewRendered` listener is triggered after the map completes a rendering cycle. This listener provides `EViewDataTransitionStatus` and `EViewCameraTransitionStatus` parameters containing details about the rendering process. * Kotlin * Java ```kotlin mapView?.onViewRendered = { dataStatus, cameraStatus -> Log.d("Render", "View rendered - Data: $dataStatus, Camera: $cameraStatus") } ``` ```java if (mapView != null) { mapView.setOnViewRendered((dataStatus, cameraStatus) -> { Log.d("Render", "View rendered - Data: " + dataStatus + ", Camera: " + cameraStatus); }); } ``` #### Map selection functionality[​](#map-selection-functionality "Direct link to Map selection functionality") After detecting a gesture, such as a tap, usually some specific action like selecting a landmark or a route is performed on the MapView. This selection is made using a map cursor, which is invisible by default. To showcase its functionality, the cursor can be made visible using the `MapViewPreferences` setting: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView // Enable cursor (default is false) mapView?.preferences?.enableCursor = true // Enable cursor to render on screen mapView?.preferences?.enableCursorRender = true ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); // Enable cursor (default is false) if (mapView != null && mapView.getPreferences() != null) { mapView.getPreferences().setEnableCursor(true); // Enable cursor to render on screen mapView.getPreferences().setEnableCursorRender(true); } ``` Doing this will result in a crosshair-like icon in center of screen. ![](/docs/android/assets/images/example_android_display_cursor_street_name1-e51059b22fec264d7359553f06dcb2e7.png) **Displaying a cursor** ##### Landmark selection[​](#landmark-selection "Direct link to Landmark selection") To get the selected landmarks, you can use the following code: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView mapView?.onTouch = { pos -> // Set the cursor position mapView.cursorScreenPosition = pos // Get the landmarks at the cursor position val landmarks = mapView.cursorSelectionLandmarks landmarks?.forEach { landmark -> // handle landmark Log.d("Selection", "Selected landmark: ${landmark.name}") } } ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); if (mapView != null) { mapView.setOnTouch(pos -> { // Set the cursor position mapView.setCursorScreenPosition(pos); // Get the landmarks at the cursor position LandmarkList landmarks = mapView.getCursorSelectionLandmarks(); if (landmarks != null) { for (Landmark landmark : landmarks) { // handle landmark Log.d("Selection", "Selected landmark: " + landmark.getName()); } } }); } ``` info At higher zoom levels, landmarks provided by the `cursorSelectionLandmarks` property may lack some details for optimization purposes. Use `SearchService` to retrieve full landmark details if needed. To unregister the callback: * Kotlin * Java ```kotlin mapView?.onTouch = null ``` ```java if (mapView != null) { mapView.setOnTouch(null); } ``` info The selected landmarks are returned by the `cursorSelectionLandmarks` property, which is accessed after updating the cursor's position. This step is essential because the SDK only detects landmarks that are positioned directly under the cursor. danger The cursor screen position is also used for determining the default screen position for centering (unless other values are specified). Modifying the screen position might change the behavior of centering in unexpected ways. ##### Street selection[​](#street-selection "Direct link to Street selection") The following code can be used to return selected streets under the cursor: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView // Register touch callback to set cursor to tapped position mapView?.onTouch = { point -> mapView.cursorScreenPosition = point val streets = mapView.cursorSelectionStreets val currentStreetName = if (streets?.isEmpty() != false) "Unnamed street" else streets.first().name Log.d("Selection", "Street name: $currentStreetName") } ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); // Register touch callback to set cursor to tapped position if (mapView != null) { mapView.setOnTouch(point -> { mapView.setCursorScreenPosition(point); LandmarkList streets = mapView.getCursorSelectionStreets(); String currentStreetName = (streets == null || streets.isEmpty()) ? "Unnamed street" : streets.get(0).getName(); Log.d("Selection", "Street name: " + currentStreetName); }); } ``` Street name can then be displayed on screen. This is the result: ![](/docs/android/assets/images/example_android_display_cursor_street_name2-1bf8213e189b1f7abb8c270f981cfcbc.png) **Displaying a cursor selected street name** info The visibility of the cursor has no impact whatsoever on the selection logic. Getting the current cursor screen position is done by accessing the `cursorScreenPosition` property of `MapView`. ##### List of selection types[​](#list-of-selection-types "Direct link to List of selection types") To summarize, there are multiple properties used to select different types of elements on the map. You can see all those in the following table. | Entity | Select method | Result type | Observations | | -------------- | ------------------------------ | ------------------- | ------------------------------------------------ | | Landmark | `cursorSelectionLandmarks` | `LandmarkList?` | | | Marker | `cursorSelectionMarkers` | `MarkerMatchList?` | Returns `MarkerMatchList`, not a `MarkerList` | | OverlayItem | `cursorSelectionOverlayItems` | `OverlayItemList?` | | | Street | `cursorSelectionStreets` | `LandmarkList?` | Streets are handled as landmarks | | Route | `cursorSelectionRoutes` | `RouteList?` | | | Path | `cursorSelectionPath` | `Path?` | Null is returned if no path is found | | MapSceneObject | `cursorSelectionSceneObject` | `MapSceneObject?` | Null is returned if no map scene object is found | | TrafficEvent | `cursorSelectionTrafficEvents` | `TrafficEventList?` | | As you can see, when selecting markers a `MarkerMatchList` is returned. The match specifies information about the matched marker (the marker collection in which the marker resides, the index of the marker in the collection, the matched part index, the matched index of the point in the part). You can also register callbacks that are called when the cursor is placed over elements on the `MapView` class: * `onCursorSelectionUpdatedLandmarks` for landmarks * `onCursorSelectionUpdatedMarkerMatch` for markers * `onCursorSelectionUpdatedOverlayItems` for overlay items * `onCursorSelectionUpdatedRoutes` for routes * `onCursorSelectionUpdatedTrafficEvents` for traffic events These callbacks are triggered whenever the selection changes - for example, when new elements are selected, the selection switches to different elements, or the selection is cleared (in which case the callback is invoked with null or an empty list). To unregister a callback, simply set the corresponding property to null. #### Capture the map view as an image[​](#capture-the-map-view-as-an-image "Direct link to Capture the map view as an image") In certain situations, it may be necessary to save the map as an image - for example, to generate previews that are too expensive to redraw in real time. The Maps SDK for Android provides a dedicated method for capturing the map view through the `GemSurfaceView`. * Kotlin * Java ```kotlin // Using GemSurfaceView's takeScreenshot method val surface = findViewById(R.id.gem_surface) surface.takeScreenshot { bitmap -> // Convert to byte array if needed val stream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream) val imageBytes = stream.toByteArray() } ``` ```java // Using GemSurfaceView's takeScreenshot method GemSurfaceView surface = findViewById(R.id.gem_surface); surface.takeScreenshot(bitmap -> { // Convert to byte array if needed ByteArrayOutputStream stream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream); byte[] imageBytes = stream.toByteArray(); }); ``` Tip To ensure that any ongoing map animations or loading have completed, wait for the `onViewRendered` callback to be triggered with data transition status set to `EViewDataTransitionStatus.Complete` before capturing the image. Make sure to implement a timeout, as the `onViewRendered` is only triggered when the map is rendering - and will not be called if everything is already loaded. --- ### Styling |

The appearance of the map can be tailored by applying different styles. You can either download a predefined map style using the `ContentStore` class, which offers a variety of ready-to-use styles, or create a custom style using [Magic Lane Map Studio](https://developer.magiclane.com/documentation/OnlineStudio/guide_creating_a_style.html) which you can download and configure. In this guide, we'll explore both methods in detail. #### Apply predefined styles[​](#apply-predefined-styles "Direct link to Apply predefined styles") To apply a predefined map style, it must first be downloaded, as it is not loaded into memory by default. As mentioned earlier, this can be achieved using the `ContentStore` class. To begin, we'll retrieve a list of all available styles for preview purposes and then proceed to download the ones we wish to use. Here's how you can get previews of the available map styles, represented as a `List`, with the following code: * Kotlin * Java ```kotlin fun getStyles() { val contentStore = ContentStore() val (storeContentList, onCompleted) = contentStore.asyncGetStoreContentList( EContentType.ViewStyleLowRes, onCompleted = { items, error, hint -> if (error == ErrorCode.NoError && items.isNotEmpty()) { for (item in items) { stylesList.add(item) } // Clear any previous messages } } ) } ``` ```java void getStyles() { ContentStore contentStore = new ContentStore(); contentStore.asyncGetStoreContentList( EContentType.ViewStyleLowRes, (items, error, hint) -> { if (error == ErrorCode.NoError && !items.isEmpty()) { for (ContentStoreItem item : items) { stylesList.add(item); } // Clear any previous messages } } ); } ``` Method `asyncGetStoreContentList` can be used to obtain other content such as car models, road maps, tts voices and more. info There are two types of preview styles available: `EContentType.ViewStyleHighRes` and `EContentType.ViewStyleLowRes.` * `EContentType.ViewStyleHighRes` is designed for obtaining styles optimized for high-resolution displays, such as those on mobile devices. * `EContentType.ViewStyleLowRes` is intended for styles suited to low-resolution displays, such as desktop monitors. In the `onCompleted` parameter of the `asyncGetStoreContentList` method, several values are provided: * `ErrorCode` object that indicates whether any errors occurred during the operation. * `List` that contains the items retrieved from the content store, such as map styles in this case. If the error code is not `ErrorCode.NoError`, this list will be empty. * boolean value that specifies whether the content store item (e.g., the map style) is already available in cache memory (and thus doesn't require downloading) or if it needs to be downloaded. If the operation failed, this value will be false. ##### ContentStoreItem[​](#contentstoreitem "Direct link to ContentStoreItem") A `ContentStoreItem` has the following attributes/methods: | Attribute/Methods | Explanation | | ------------------------- | ---------------------------------------------------------------------------- | | name | Gets the name of the associated product. | | id | Get the unique id of the item in the content store. | | chapterName | Gets the product chapter name translated to interface language. | | countryCodes | Gets the country code (ISO 3166-1 alpha-3) list of the product as text. | | language | Gets the full language code for the product. | | type | Gets the type of the product as a \[ContentType] value. | | fileName | Gets the full path to the content data file when available. | | clientVersion | Gets the client version of the content. | | totalSize | Get the size of the content in bytes. | | availableSize | Gets the available size of the content in bytes. | | isCompleted | Checks if the item is completed downloaded. | | status | Gets current item status. | | pauseDownload() | Pause a previous download operation. | | cancelDownload() | Cancel a previous download operation. | | downloadProgress() | Get current download progress. | | canDeleteContent() | Check if associated content can be deleted. | | deleteContent() | Delete the associated content | | isImagePreviewAvailable() | Check if there is an image preview available on the client. | | imgPreview | Get the preview. The user is responsible to check if the image is valid. | | contentParameters | Get additional parameters for the content. | | updateItem | Get corresponding update item. | | isUpdatable | Check if item is updatable, i.e. it has a newer version available. | | updateSize | Get update size (if an update is available for this item). | | updateVersion | Get update version (if an update is available for this item). | | asyncDownload() | Asynchronous start/resume the download of the content store product content. | danger Keep in mind that certain attributes may not apply to specific types of `ContentStoreItem`. For instance, the `countryCodes` attribute will not provide meaningful data for a `EContentType.ViewStyleLowRes`, as styles are not associated with any particular country. Downloading a map style is done by calling `ContentStoreItem.asyncDownload()` as shown below: * Kotlin * Java ```kotlin private suspend fun downloadStyle(style: ContentStoreItem): Boolean { isDownloadingStyle = true return suspendCoroutine { continuation -> style.asyncDownload({ error -> if (error != ErrorCode.NoError) { // An error was encountered during download isDownloadingStyle = false continuation.resume(false) return@asyncDownload } // Download was successful isDownloadingStyle = false continuation.resume(true) }, onProgress = { progress -> // Gets called every time download progresses with a value between [0, 100] Log.d("StyleDownload", "progress: $progress") }, allowChargedNetworks = true) } } ``` ```java private void downloadStyle(ContentStoreItem style, DownloadCallback callback) { isDownloadingStyle = true; style.asyncDownload(error -> { if (error != ErrorCode.NoError) { // An error was encountered during download isDownloadingStyle = false; callback.onResult(false); return; } // Download was successful isDownloadingStyle = false; callback.onResult(true); }, progress -> { // Gets called every time download progresses with a value between [0, 100] Log.d("StyleDownload", "progress: " + progress); }, true); // allowChargedNetworks } ``` Now, all that is left to do is applying the downloaded style by using `MapViewPreferences.setMapStyleByPath(path)` called with the filename, which contains the path: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView!! val filename = currentStyle.fileName mapView?.preferences?.setMapStyleByPath(filename) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); String filename = currentStyle.getFileName(); if (mapView != null && mapView.getPreferences() != null) { mapView.getPreferences().setMapStyleByPath(filename); } ``` To wrap things up, this is the code that incorporates all steps: * Kotlin * Java ```kotlin if (stylesList.isEmpty()) { showMessage("The map styles are loading.") getStyles() return } val indexOfNextStyle = if (indexOfCurrentStyle >= stylesList.size - 1) { 0 } else { indexOfCurrentStyle + 1 } val currentStyle = stylesList[indexOfNextStyle] if (currentStyle.isCompleted == false) { val didDownloadSuccessfully = downloadStyle(currentStyle) if (!didDownloadSuccessfully) return } indexOfCurrentStyle = indexOfNextStyle val filename = currentStyle.fileName mapView?.preferences?.setMapStyleByPath(filename) ``` ```java if (stylesList.isEmpty()) { showMessage("The map styles are loading."); getStyles(); return; } int indexOfNextStyle; if (indexOfCurrentStyle >= stylesList.size() - 1) { indexOfNextStyle = 0; } else { indexOfNextStyle = indexOfCurrentStyle + 1; } ContentStoreItem currentStyle = stylesList.get(indexOfNextStyle); if (!currentStyle.isCompleted()) { downloadStyle(currentStyle, didDownloadSuccessfully -> { if (!didDownloadSuccessfully) return; indexOfCurrentStyle = indexOfNextStyle; String filename = currentStyle.getFileName(); if (mapView != null && mapView.getPreferences() != null) { mapView.getPreferences().setMapStyleByPath(filename); } }); return; } indexOfCurrentStyle = indexOfNextStyle; String filename = currentStyle.getFileName(); if (mapView != null && mapView.getPreferences() != null) { mapView.getPreferences().setMapStyleByPath(filename); } ``` Map styles can be set by using `MapViewPreferences.setMapStyleByContentStoreItem()` or `MapViewPreferences.setMapStyleById()`. * `MapViewPreferences.setMapStyleByContentStoreItem()` takes as parameter the `ContentStoreItem` which needs to be of type `EContentType.ViewStyleHighRes` or `EContentType.ViewStyleLowRes` * `MapViewPreferences.setMapStyleById()` takes as parameter the unique id of the `ContentStoreItem`, obtained by calling `ContentStoreItem.id` - Kotlin - Java ```kotlin mapView?.preferences?.setMapStyleByContentStoreItem(currentStyle) mapView?.preferences?.setMapStyleById(currentStyle.id) ``` ```java if (mapView != null && mapView.getPreferences() != null) { mapView.getPreferences().setMapStyleByContentStoreItem(currentStyle); mapView.getPreferences().setMapStyleById(currentStyle.getId()); } ``` #### Apply custom styles[​](#apply-custom-styles "Direct link to Apply custom styles") A custom map style can be created in [Magic Lane Map Studio](https://developer.magiclane.com/documentation/OnlineStudio/guide_creating_a_style.html). By following the guide you'll end up with a .style file. This file will be loaded into application and applied as a style. We need to create an `assets` directory in the Android project (typically under `src/main/assets`), where the .style file will be placed. Loading the style into memory is done with the following code: * Kotlin * Java ```kotlin // Method to load style and return it as bytes private fun loadStyle(): ByteArray { // Load style from assets directory val inputStream = assets.open("Basic_1_Oldtime-1_21_656.style") return inputStream.readBytes() } ``` ```java // Method to load style and return it as bytes private byte[] loadStyle() throws IOException { // Load style from assets directory InputStream inputStream = getAssets().open("Basic_1_Oldtime-1_21_656.style"); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] data = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(data)) != -1) { buffer.write(data, 0, bytesRead); } return buffer.toByteArray(); } ``` Once the map style bytes are obtained, the style can be set by using `MapViewPreferences.setMapStyleByDataBuffer(styleData)`: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView!! val styleData = loadStyle() val dataBuffer = DataBuffer(styleData) mapView?.preferences?.setMapStyleByDataBuffer(dataBuffer, smoothTransition = true) ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); byte[] styleData = loadStyle(); DataBuffer dataBuffer = new DataBuffer(styleData); if (mapView != null && mapView.getPreferences() != null) { mapView.getPreferences().setMapStyleByDataBuffer(dataBuffer, true); // smoothTransition } ``` A smooth transition can be enabled by passing the `smoothTransition` parameter of `setMapStyleByDataBuffer` as true. ![](/docs/android/assets/images/example_android_assets_map_style1-2c6b563c9645e62897fc75deb63446ed.png) **Default map style** ![](/docs/android/assets/images/example_android_assets_map_style2-de1e843588d613f7a4503abe5e81e4de.png) **Custom added map style**
In order to have a map style already applied when creating a `GemSurfaceView`, you can set the style immediately after the map view is created in the `onDefaultMapViewCreated` callback: * Kotlin * Java ```kotlin gemSurfaceView.onDefaultMapViewCreated = { defaultMapView -> val styleData = loadStyle() val dataBuffer = DataBuffer(styleData) defaultMapView.preferences?.setMapStyleByDataBuffer(dataBuffer, smoothTransition = false) } ``` ```java gemSurfaceView.setOnDefaultMapViewCreated(defaultMapView -> { byte[] styleData = loadStyle(); DataBuffer dataBuffer = new DataBuffer(styleData); if (defaultMapView.getPreferences() != null) { defaultMapView.getPreferences().setMapStyleByDataBuffer(dataBuffer, false); // smoothTransition } }); ``` #### Get notified about style changes[​](#get-notified-about-style-changes "Direct link to Get notified about style changes") The user can be notified when the style changes by providing a callback using the `onMapStyleChanged` property from the `MapView`: * Kotlin * Java ```kotlin // Get MapView from GemSurfaceView val gemSurfaceView = findViewById(R.id.gem_surface) val mapView = gemSurfaceView.mapView!! mapView?.onMapStyleChanged = { styleId, stylePath -> Log.d("MapStyle", "The style with id $styleId and path $stylePath has been set.") } ``` ```java // Get MapView from GemSurfaceView GemSurfaceView gemSurfaceView = findViewById(R.id.gem_surface); MapView mapView = gemSurfaceView.getMapView(); if (mapView != null) { mapView.setOnMapStyleChanged((styleId, stylePath) -> { Log.d("MapStyle", "The style with id " + styleId + " and path " + stylePath + " has been set."); }); } ``` The callback provides the following parameters: * `styleId`: The id of the style * `stylePath`: The path to the `.style` file --- ### Navigation The Magic Lane Android SDK offers developers a comprehensive solution for building turn-by-turn navigation systems within mobile applications. The SDK features critical tools like live guidance, offline support, and warning alerts (such as speed limits and traffic conditions). Real-time navigation adapts dynamically to the user's progress, notifying them of any route deviations and recalculating the path as needed. The SDK also supports a location simulator for testing navigation functionalities during app development. #### [📄️ Get started with Navigation](/docs/android/guides/navigation/get-started-navigation.md) [The Maps SDK for Android provides developers with comprehensive tools to build a robust turn-by-turn navigation system. This functionality enables applications to track the current device location relative to a predefined route and deliver real-time navigational guidance.](/docs/android/guides/navigation/get-started-navigation.md) #### [📄️ Add voice guidance](/docs/android/guides/navigation/voice-guidance.md) [Voice guidance in the Maps SDK for Android allows you to enhance navigation experiences with spoken instructions. This guide covers how to enable built-in Text-to-Speech (TTS), manage voice settings, switch voices and languages, and integrate custom playback using the onNavigationSound callback for maximum flexibility.](/docs/android/guides/navigation/voice-guidance.md) #### [📄️ Better route detection](/docs/android/guides/navigation/better-route-detection.md) [The Maps SDK for Android continuously monitors traffic conditions and automatically evaluates alternative routes to ensure optimal navigation. This feature enhances user experience by providing real-time route adjustments, reducing travel time, and improving overall efficiency, especially in dynamic traffic environments.](/docs/android/guides/navigation/better-route-detection.md) #### [📄️ Roadblocks](/docs/android/guides/navigation/roadblocks.md) [A roadblock is a user-defined restriction applied to a specific road segment or geographic area, used to reflect traffic disruptions such as construction, closures, or areas to avoid.](/docs/android/guides/navigation/roadblocks.md) --- ### Better route detection |

The Maps SDK for Android continuously monitors traffic conditions and automatically evaluates alternative routes to ensure optimal navigation. This feature enhances user experience by providing real-time route adjustments, reducing travel time, and improving overall efficiency, especially in dynamic traffic environments. #### Requirements[​](#requirements "Direct link to Requirements") ###### Route preferences[​](#route-preferences "Direct link to Route preferences") For this feature to function, the route used in navigation or simulation must be computed with specific settings within the `RoutePreferences` object: * the `transportMode` needs to be `ERouteTransportMode.Car` or `ERouteTransportMode.Lorry` * the `avoidTraffic` needs to be `ETrafficAvoidance.All` or `ETrafficAvoidance.Roadblocks` * the `routeType` needs to be `ERouteType.Fastest` Unless the settings are set as above the better route detection feature will not trigger. * Kotlin * Java ```kotlin val routePreferences = RoutePreferences().apply { routeType = ERouteType.Fastest avoidTraffic = ETrafficAvoidance.Roadblocks transportMode = ERouteTransportMode.Car } ``` ```java RoutePreferences routePreferences = new RoutePreferences(); routePreferences.setRouteType(ERouteType.Fastest); routePreferences.setAvoidTraffic(ETrafficAvoidance.Roadblocks); routePreferences.setTransportMode(ERouteTransportMode.Car); ``` Additional settings can be configured within the `RoutePreferences` object during route calculation, as long as they do not override or conflict with the required preferences mentioned above. ###### Traffic[​](#traffic "Direct link to Traffic") For the callbacks to be triggered, traffic needs to be present of the route on which the navigation is active. ###### Significant time gain[​](#significant-time-gain "Direct link to Significant time gain") A newly identified route must have a substantial time delay compared to the current route for it to be considered. The relevant callback will only be triggered if an alternative route offers a time savings of more than five minutes, ensuring that route adjustments are meaningful and beneficial to the user. danger The better route detection feature will not function as intended if any of the required conditions outlined above are not met. #### Listen for notification events[​](#listen-for-notification-events "Direct link to Listen for notification events") The `startSimulation` and `startNavigation` methods provided by the `NavigationService` class enable the registration of the following callbacks through the `NavigationListener`: * `onBetterRouteDetected` : Triggered when a better route is identified. It provides information such as the newly detected route, its total travel time, the traffic-induced delay on the new route, and the time savings compared to the current route. * `onBetterRouteInvalidated` : Triggered when a previously detected better route is no longer valid. This can occur if the user deviates from the shared trunk of both routes, an even better alternative becomes available, or changing traffic conditions eliminate the previously detected advantage. * `onBetterRouteRejected` : Triggered when no suitable alternative route is found during the better route check. It is the responsibility of the API user to manage the recommended route. The navigation service does not automatically switch to the better route, requiring explicit handling and implementation by the user. * Kotlin * Java ```kotlin val navigationListener = NavigationListener.create( onBetterRouteDetected = { route, travelTime, delay, timeGain -> Log.d("Navigation", "A better route has been detected - total travel time: ${travelTime}s, traffic delay on the better route: ${delay}s, time gain from current route: ${timeGain}s") // Do something with the route ... }, onBetterRouteInvalidated = { Log.d("Navigation", "The previously found better route is no longer valid") }, onBetterRouteRejected = { error -> Log.d("Navigation", "The check for better route failed with reason: $error") } ) navigationService.startSimulation( route, navigationListener, progressListener ) ``` ```java NavigationListener navigationListener = NavigationListener.create( /* onBetterRouteDetected */ (route, travelTime, delay, timeGain) -> { Log.d("Navigation", "A better route has been detected - total travel time: " + travelTime + "s, traffic delay on the better route: " + delay + "s, time gain from current route: " + timeGain + "s"); // Do something with the route ... }, /* onBetterRouteInvalidated */ () -> { Log.d("Navigation", "The previously found better route is no longer valid"); }, /* onBetterRouteRejected */ (error) -> { Log.d("Navigation", "The check for better route failed with reason: " + error); } ); navigationService.startSimulation( route, navigationListener, progressListener ); ``` #### Force the check for better route[​](#force-the-check-for-better-route "Direct link to Force the check for better route") The system automatically performs the better route check at predefined intervals, provided all required conditions are met. info In the Android SDK, there is currently no public API to manually trigger a better route check. The system automatically handles route checking at appropriate intervals during navigation or simulation when the required conditions are met. --- ### Get started with Navigation |

The Maps SDK for Android provides developers with comprehensive tools to build a robust turn-by-turn navigation system. This functionality enables applications to track the current device location relative to a predefined route and deliver real-time navigational guidance. ![](/docs/android/assets/images/example_android_navigation-6c30ac51bb2bfd3de0444c04f0830db6.png) **Navigating on route** Key Features: * **Turn-by-Turn Directions**: Provides detailed route instructions based on the device's current location, ensuring accurate navigation. * **Live Guidance**: Navigation instructions can be delivered as text and integrated with a Text-to-Speech (TTS) system for voice-based guidance. * **Warning Alerts**: A versatile alert system that notifies users of conditions such as speed limits, traffic reports, and other important events along the route. * **Offline Functionality**: Essential navigation features remain operational offline, provided that map data has been pre-downloaded or cached. The turn-by-turn navigation system relies on continuous acquisition of device data, including location, speed, and heading. These data points are matched against the mapped route and used to generate accurate guidance for the user. Instructions are dynamically updated as the user progresses along the route. In the event that the user deviates from the planned route, the system will notify them of their off-track distance and provide the option for route recalculations or updates. Additionally, the system can dynamically adjust the route based on real-time traffic conditions, offering more efficient and faster alternatives to optimize navigation. Additionally, developers can leverage a built-in location simulator to test navigation functionalities during the app development phase. #### How does it work?[​](#how-does-it-work "Direct link to How does it work?") To enable navigation, the first step is to compute a valid, navigable route (note that non-navigable routes, such as range routes, are not supported). The SDK offers two methods for navigating a route: * **Navigation:** This method relies on the position data provided by the `PositionService` to guide the user along the route. * **Simulation:** This method does not require user-provided position data. Instead, it simulates the navigation instructions that would be delivered to the user, allowing developers to test and preview the experience without needing an actual position. If we are in navigation mode, the position is provided by `PositionService`. It can use: * **Real GPS Data:** When `PositionService.setLiveDataSource` is called, the service will use real-time GPS data to provide position updates. This requires the appropriate application permissions. Additionally, the application must programmatically request these permissions from the user. * **Custom Position Data:** In this mode, a custom data source can be configured to supply position updates. No permissions are required in this case, as the positions are provided through the custom source rather than the device's GPS. If you want to use a custom position take a look at [Custom positioning](/docs/android/guides/positioning/custom-positioning.md). Currently, only one of navigation and simulation can be active at a time, regardless of the number of maps present within the application. #### Starting a navigation[​](#starting-a-navigation "Direct link to Starting a navigation") Given that a route has been computed, the simplest way to navigate it is by implementing the following code: * Kotlin * Java ```kotlin import com.magiclane.sdk.routesandnavigation.NavigationService import com.magiclane.sdk.routesandnavigation.NavigationListener import com.magiclane.sdk.routesandnavigation.NavigationInstruction import com.magiclane.sdk.core.ProgressListener import com.magiclane.sdk.core.ErrorCode import com.magiclane.sdk.core.GemError import com.magiclane.sdk.places.Landmark class MainActivity : AppCompatActivity() { private val navigationService = NavigationService() private val navigationListener = NavigationListener.create( onNavigationInstructionUpdated = { instruction -> // Handle navigation instruction updates val instructionText = instruction.nextTurnInstruction // Update UI with instruction }, onDestinationReached = { destination -> // Handle destination reached }, onNavigationError = { error -> // Handle navigation error when (error) { GemError.NoError -> { // Success } GemError.Cancel -> { // Navigation was cancelled } GemError.WaypointAccess -> { // Waypoint couldn't be reached } else -> { // Other error occurred val errorMessage = GemError.getMessage(error, this@MainActivity) // Handle error appropriately } } } ) private val progressListener = ProgressListener.create( onStarted = { // Show progress indicator }, onCompleted = { _, _ -> // Hide progress indicator } ) private fun startNavigation() { val error = navigationService.startNavigation( route, navigationListener, progressListener ) // [Optional] Set the camera to follow position. // Usually we want this when in navigation mode mapView.followPosition() } // At any moment, we can cancel the navigation private fun cancelNavigation() { navigationService.cancelNavigation(navigationListener) } } ``` ```java import com.magiclane.sdk.routesandnavigation.NavigationService; import com.magiclane.sdk.routesandnavigation.NavigationListener; import com.magiclane.sdk.routesandnavigation.NavigationInstruction; import com.magiclane.sdk.core.ProgressListener; import com.magiclane.sdk.core.ErrorCode; import com.magiclane.sdk.core.GemError; import com.magiclane.sdk.places.Landmark; public class MainActivity extends AppCompatActivity { private final NavigationService navigationService = new NavigationService(); private final NavigationListener navigationListener = NavigationListener.create( /* onNavigationInstructionUpdated */ (instruction) -> { // Handle navigation instruction updates String instructionText = instruction.getNextTurnInstruction(); // Update UI with instruction }, /* onDestinationReached */ (destination) -> { // Handle destination reached }, /* onNavigationError */ (error) -> { // Handle navigation error if (error == GemError.NoError) { // Success } else if (error == GemError.Cancel) { // Navigation was cancelled } else if (error == GemError.WaypointAccess) { // Waypoint couldn't be reached } else { // Other error occurred String errorMessage = GemError.getMessage(error, MainActivity.this); // Handle error appropriately } } ); private final ProgressListener progressListener = ProgressListener.create( /* onStarted */ () -> { // Show progress indicator }, /* onCompleted */ (result, error) -> { // Hide progress indicator } ); private void startNavigation() { int error = navigationService.startNavigation( route, navigationListener, progressListener ); // [Optional] Set the camera to follow position. // Usually we want this when in navigation mode mapView.followPosition(); } // At any moment, we can cancel the navigation private void cancelNavigation() { navigationService.cancelNavigation(navigationListener); } } ``` info The `NavigationService.startNavigation` method returns `GemError.NoError` on success. Error details will be delivered through the `onNavigationError` callback function of the `NavigationListener`. We declared a `NavigationListener` that handles the main navigation events: * a navigation error (you can see a detailed table below). * destination is reached. * a new instruction is available. The `error` provided by the callback function can have the following values: | Value | Significance | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GemError.NoError` | successfully completed | | `GemError.Cancel` | cancelled by the user | | `GemError.WaypointAccess` | couldn't be found with the current preferences | | `GemError.ConnectionRequired` | if allowOnlineCalculation = false in the routing preferences and the calculation can't be done on the client side due to missing data | | `GemError.Expired` | calculation can't be done on client side due to missing necessary data and the client world map data version is no longer supported by the online routing service | | `GemError.RouteTooLong` | routing was executed on the online service and the operation took too much time to complete (usually more than 1 min, depending on the server overload state) | | `GemError.Invalidated` | the offline map data changed ( offline map downloaded, erased, updated ) during the calculation | | `GemError.NoMemory` | routing engine couldn't allocate the necessary memory for the calculation | The navigation can be stopped at any moment or it will be stopped when we reach the destination. ![](/docs/android/assets/images/example_android_navigation1-7f573ae70dc31cd43f9f05bf4d05bc3b.png) **Navigating on route** Typically (optional), before starting navigation, we instruct the map to begin following the user's position. More information about the `followPosition` method and related customization options can be found inside the [Show your location on the map](/docs/android/guides/positioning/show-your-location-on-the-map.md) guide. To enhance navigation clarity, the route is displayed on a map. This also includes turn-by-turn navigation arrows that disappear once the user has passed them. More about presenting routes [here](/docs/android/guides/maps/display-map-items/display-routes.md). **Navigating on displayed route** Navigating on said route will change color of parsed route portion with `traveledInnerColor` parameter of `RouteRenderSettings`. ![](/docs/android/assets/images/example_android_navigation3-12e14cdfe4c699889f3a15ffb3078ea7.png) **Parsed route is displayed with a gray color (default)** #### Starting a simulation[​](#starting-a-simulation "Direct link to Starting a simulation") To start a simulation, you can use the following approach: * Kotlin * Java ```kotlin class MainActivity : AppCompatActivity() { private val navigationService = NavigationService() private val simulationListener = NavigationListener.create( onNavigationInstructionUpdated = { instruction -> // Handle simulation instruction updates val instructionText = instruction.nextTurnInstruction // Update UI with instruction } ) private val progressListener = ProgressListener.create( onStarted = { // Show progress indicator }, onCompleted = { _, _ -> // Hide progress indicator } ) private fun startSimulation() { // Add route to map preferences to display it mapView.presentRoute(route) val error = navigationService.startSimulation( route, simulationListener, progressListener, speedMultiplier = 2.0f ) // [Optional] Set the camera to follow position. // Usually we want this when in navigation mode mapView.followPosition() } // At any moment, we can cancel the simulation private fun cancelSimulation() { navigationService.cancelNavigation(simulationListener) } } ``` ```java public class MainActivity extends AppCompatActivity { private final NavigationService navigationService = new NavigationService(); private final NavigationListener simulationListener = NavigationListener.create( /* onNavigationInstructionUpdated */ (instruction) -> { // Handle simulation instruction updates String instructionText = instruction.getNextTurnInstruction(); // Update UI with instruction } ); private final ProgressListener progressListener = ProgressListener.create( /* onStarted */ () -> { // Show progress indicator }, /* onCompleted */ (result, error) -> { // Hide progress indicator } ); private void startSimulation() { // Add route to map preferences to display it mapView.presentRoute(route); int error = navigationService.startSimulation( route, simulationListener, progressListener, 2.0f // speedMultiplier ); // [Optional] Set the camera to follow position. // Usually we want this when in navigation mode mapView.followPosition(); } // At any moment, we can cancel the simulation private void cancelSimulation() { navigationService.cancelNavigation(simulationListener); } } ``` When simulating we can specify a `speedMultiplier` to set the simulation speed (1.0 is default and corresponds to the maximum speed limit for each road segment). See `navigationService.simulationMinSpeedMultiplier` and `navigationService.simulationMaxSpeedMultiplier` for the range of allowed values. #### Listen for navigation events[​](#listen-for-navigation-events "Direct link to Listen for navigation events") A wide range of navigation-related events can be monitored. In the previous examples, we demonstrated handling the `onNavigationInstructionUpdated` event. Additionally, it is possible to listen for and handle numerous other events: * Kotlin * Java ```kotlin private val navigationListener = NavigationListener.create( onNavigationInstructionUpdated = { instruction -> // Handle navigation instruction updates }, onNavigationStarted = { // Handle navigation started }, onNavigationSound = { sound -> // Handle TTS instructions }, onWaypointReached = { landmark -> // Handle waypoint reached }, onDestinationReached = { landmark -> // Handle destination reached }, onRouteUpdated = { route -> // Handle route updates }, onBetterRouteDetected = { route, travelTime, delay, timeGain -> // Handle better route detection }, onBetterRouteRejected = { error -> // Handle better route rejection }, onBetterRouteInvalidated = { // Handle better route invalidation }, onNavigationError = { error -> // Handle navigation errors }, onNotifyStatusChange = { status -> // Handle navigation status changes } ) val error = navigationService.startNavigation( route, navigationListener, progressListener ) ``` ```java private final NavigationListener navigationListener = NavigationListener.create( /* onNavigationInstructionUpdated */ (instruction) -> { // Handle navigation instruction updates }, /* onNavigationStarted */ () -> { // Handle navigation started }, /* onNavigationSound */ (sound) -> { // Handle TTS instructions }, /* onWaypointReached */ (landmark) -> { // Handle waypoint reached }, /* onDestinationReached */ (landmark) -> { // Handle destination reached }, /* onRouteUpdated */ (route) -> { // Handle route updates }, /* onBetterRouteDetected */ (route, travelTime, delay, timeGain) -> { // Handle better route detection }, /* onBetterRouteRejected */ (error) -> { // Handle better route rejection }, /* onBetterRouteInvalidated */ () -> { // Handle better route invalidation }, /* onNavigationError */ (error) -> { // Handle navigation errors }, /* onNotifyStatusChange */ (status) -> { // Handle navigation status changes } ); int error = navigationService.startNavigation( route, navigationListener, progressListener ); ``` These events are described in the following table: | Event | Explanation | | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | onNavigationInstructionUpdated(NavigationInstruction instruction) | Triggered when a new navigation instruction is available, providing details about the instruction. This method is called periodically, usually at 1 second, to update the navigation information for the UI. | | onNavigationStarted() | Called when navigation begins, signaling the start of the route guidance. This method is called when first valid position for navigation arrives. | | onNavigationSound(ISound sound) | Provides a sound object for a maneuver that can be played through the device's audio system for voice guidance. | | onWaypointReached(Landmark landmark) | Invoked when a waypoint in the route is reached, including details of the waypoint. This notification is not sent when the destination is reached. | | onDestinationReached(Landmark landmark) | Called upon reaching the final destination, with information about the destination landmark. This is the moment when the navigation request finished with success. | | onRouteUpdated(Route route) | Fired when the current route is updated, providing the new route details. | | onBetterRouteDetected(Route route, Int travelTime, Int delay, Int timeGain) | Triggered when a better alternative route is detected, including the new route and details such as travel time, delays caused by traffic and time gains. The previous better route (if exists) must be considered automatically invalidated. | | onBetterRouteRejected(ErrorCode error) | Called when a check for better routes fails, with details of the rejection error. Used especially for debugging. | | onBetterRouteInvalidated() | Indicates that a previously suggested better route is no longer valid. This notification is sent when current position is no more on the previous calculated better route. | | onNavigationError(ErrorCode error) | Signal that the navigation request finished with error. | | onNotifyStatusChange(ENavigationStatus status) | Notifies new value of navigation status (Running, WaitingRoute, WaitingGPS, WaitingReturnToRoute). | info Most callbacks from the table provided above can be used for both simulation and navigation. Some specific navigation behaviors like route recalculation do not apply to simulation mode. #### Data source based navigation[​](#data-source-based-navigation "Direct link to Data source based navigation") Navigation typically relies on the current GPS position. However, it is also entirely valid to perform navigation using custom-defined positions. This can be done by creating a custom data source, setting the position service to the given data source, starting the data source and starting navigation as you would with live data source. See the [custom positioning guide](/docs/android/guides/positioning/custom-positioning.md) for more information on how to create a custom data source. #### Stop navigation/simulation[​](#stop-navigationsimulation "Direct link to Stop navigation/simulation") The `cancelNavigation` method from the `NavigationService` class can be used to stop both navigations and simulations, passing the `NavigationListener` used to start the navigation or simulation. At the moment it is not possible to pause the simulation. * Kotlin * Java ```kotlin // Cancel navigation using the navigation listener navigationService.cancelNavigation(navigationListener) ``` ```java // Cancel navigation using the navigation listener navigationService.cancelNavigation(navigationListener); ``` info After stopping the simulation the data source used in the position service is set back to the previous data source if it exists. #### Export Navigation instruction[​](#export-navigation-instruction "Direct link to Export Navigation instruction") The `exportAs` method serializes the current **navigation instruction** into a `DataBuffer`. For now, the only supported format is `EPathFileFormat.PackedGeometry`. * Kotlin * Java ```kotlin import com.magiclane.sdk.core.EPathFileFormat // Get current navigation instruction val instruction = navigationService.getNavigationInstruction(navigationListener) instruction?.let { val dataBuffer = it.exportAs(EPathFileFormat.PackedGeometry) // Use the data buffer as needed } ``` ```java import com.magiclane.sdk.core.EPathFileFormat; // Get current navigation instruction NavigationInstruction instruction = navigationService.getNavigationInstruction(navigationListener); if (instruction != null) { DataBuffer dataBuffer = instruction.exportAs(EPathFileFormat.PackedGeometry); // Use the data buffer as needed } ``` danger `exportAs` only works with **`EPathFileFormat.PackedGeometry`**. Passing any other value will return an empty DataBuffer. --- ### Roadblocks |

A roadblock is a user-defined restriction applied to a specific road segment or geographic area, used to reflect traffic disruptions such as construction, closures, or areas to avoid. It influences route planning by marking certain paths or zones as unavailable for navigation. Roadblocks can be **path-based** (defined by a sequence of coordinates) or **area-based** (covering a geographic region), and may be either **temporary** or **persistent**, depending on their intended duration. Persistent roadblocks remain after a SDK uninitialization. Temporary roadblocks are short-lived. The primary entity responsible for representing roadblocks is the `TrafficEvent` class. Check the [Traffic Events guide](/docs/android/guides/core/traffic-events.md) for more details. Roadblocks are mainly managed through the `Traffic` class. While some roadblocks are provided in real time by online data from Magic Lane servers, users can also define their own **user roadblocks** to customize routing behavior. If the applied style includes traffic data and traffic display is enabled (`MapViewPreferences.setTrafficVisibility` is set to true), a visual indication of the blocked portion will appear on the map, highlighted in red. tip Adding/removing user roadblocks affects only the current user and does not impact other users' routes. To create reports that are visible to all users, refer to the [Social Reports guide](/docs/android/guides/todo.md). #### Configure the traffic service[​](#configure-the-traffic-service "Direct link to Configure the traffic service") Traffic behavior can be customized through the `TrafficPreferences` instance, accessible via the `Traffic` class. The `TrafficPreferences` class provides the `useTraffic` property, which defines how traffic data should be applied during routing and navigation. The `ETrafficUsage` enum offers the following configuration options: | Value | Description | | ------------ | ------------------------------------------------------------------ | | `UseNone` | Disables all traffic data usage. | | `UseOnline` | Uses both online and offline traffic data (default setting). | | `UseOffline` | Uses only offline traffic data, including user-defined roadblocks. | For example, in order to set allow only offline usage the following line can be used: * Kotlin * Java ```kotlin val traffic = Traffic() traffic.preferences?.useTraffic = ETrafficUsage.UseOffline ``` ```java Traffic traffic = new Traffic(); TrafficPreferences preferences = traffic.getPreferences(); if (preferences != null) { preferences.setUseTraffic(ETrafficUsage.UseOffline); } ``` #### Add a temporary user roadblock while in navigation[​](#add-a-temporary-user-roadblock-while-in-navigation "Direct link to Add a temporary user roadblock while in navigation") A roadblock can be added to bypass a portion of the route for a specified distance. Once the roadblock is applied, the route will be recalculated, and the updated route will be returned via the `onRouteUpdated` callback provided to either the `startNavigation` or `startSimulation` method. The snippet below will add a roadblock of 100 meters starting from the current position: * Kotlin * Java ```kotlin val navigationService = NavigationService() navigationService.setNavigationRoadBlock(100) ``` ```java NavigationService navigationService = new NavigationService(); navigationService.setNavigationRoadBlock(100); ``` Roadblocks added through the `setNavigationRoadBlock` method provided by the `NavigationService` only affect the ongoing navigation. #### Check if a geographic position has traffic information[​](#check-if-a-geographic-position-has-traffic-information "Direct link to Check if a geographic position has traffic information") The `getOnlineServiceRestrictions` method can be used. It takes a `Coordinates` object as argument and returns an integer containing flags from the `ETrafficOnlineRestrictions` enum. For example, in order to check if traffic events are available for a certain geographic position: * Kotlin * Java ```kotlin val coords = Coordinates(50.108, 8.783) val traffic = Traffic() val restrictions = traffic.getOnlineServiceRestrictions(coords) ``` ```java Coordinates coords = new Coordinates(50.108, 8.783); Traffic traffic = new Traffic(); int restrictions = traffic.getOnlineServiceRestrictions(coords); ``` The `ETrafficOnlineRestrictions` enum provides the following values: * `Settings`: Online traffic is disabled in the `TrafficPreferences` object. * `Connection`: No internet connection is available. * `NetworkType`: Not allowed on extra charged networks (e.g., roaming). * `ProviderData`: Required provider data is missing. * `WorldMapVersion`: The world map version is outdated and incompatible. Please update the road map. * `DiskSpace`: Insufficient disk space to download or store traffic data. #### Add a user-defined persistent roadblock[​](#add-a-user-defined-persistent-roadblock "Direct link to Add a user-defined persistent roadblock") To add a persistent user-defined roadblock, the user must provide the following: * **startTime** : the timestamp indicating when the roadblock becomes active. * **expireTime** : the timestamp indicating when the roadblock is no longer in effect. * **transportMode** : the specific mode of transport affected by the roadblock. * **id** : a unique string ID for the roadblock. * **coords/area** : either: * a list of coordinates (for path-based roadblocks), or * a geographic area (for area-based roadblocks). When a user-defined roadblock is added, it will affect routing and navigation between the specified **startTime** and **expireTime**. Once the **expireTime** is reached, the roadblock is automatically removed without any user intervention. danger The following conditions apply when adding a roadblock: * If roadblocks are disabled in the `TrafficPreferences` object, the addition will fail and return `null`. * If a roadblock already exists at the same location where the user attempts to add a new one, the operation will fail and return `null`. * If the input parameters are invalid (e.g., **expireTime** is later than **startTime**, missing **id**, or invalid coordinates/area object), the addition will fail and return `null`. * If a roadblock with the same **id** already exists, the addition will fail and return `null`. ##### Add a area-based persistent roadblock[​](#add-a-area-based-persistent-roadblock "Direct link to Add a area-based persistent roadblock") The `addPersistentRoadblock` method with a `GeographicArea` parameter is used to add **area-based** user roadblocks. It accepts a `GeographicArea` object which represents the area to be avoided. The method returns: * If the addition is successful, the method returns the newly created `TrafficEvent` instance. * If the addition fails, the method returns `null`. For example, adding a area-based user-defined persistent roadblock on a given area, starting from now and available for 1 hour which affects cars can be done in the following way: * Kotlin * Java ```kotlin val topLeft = Coordinates(46.764942, 7.122563) val bottomRight = Coordinates(46.762031, 7.127992) val area = RectangleGeographicArea(topLeft, bottomRight) val startTime = Time.getUniversalTime() val endTime = Time.getUniversalTime()?.apply { minute += 60 } val traffic = Traffic() val trafficEvent = if (startTime != null && endTime != null) { traffic.addPersistentRoadblock( area = area, startUTC = startTime, expireUTC = endTime, transportMode = ERouteTransportMode.Car.value, id = "test_id" ) } else null if (trafficEvent != null) { println("The addition was successful") } else { println("The addition failed") } ``` ```java Coordinates topLeft = new Coordinates(46.764942, 7.122563); Coordinates bottomRight = new Coordinates(46.762031, 7.127992); RectangleGeographicArea area = new RectangleGeographicArea(topLeft, bottomRight); Time startTime = Time.getUniversalTime(); Time endTime = Time.getUniversalTime(); Traffic traffic = new Traffic(); TrafficEvent trafficEvent = null; if (startTime != null && endTime != null) { endTime.setMinute(endTime.getMinute() + 60); trafficEvent = traffic.addPersistentRoadblock( area, startTime, endTime, ERouteTransportMode.Car.getValue(), "test_id" ); } if (trafficEvent != null) { System.out.println("The addition was successful"); } else { System.out.println("The addition failed"); } ``` ##### Add a path-based persistent roadblock[​](#add-a-path-based-persistent-roadblock "Direct link to Add a path-based persistent roadblock") The `addPersistentRoadblock` method with a `CoordinatesList` parameter is used to add **path-based** user roadblocks. It accepts a list of `Coordinates` objects and supports two modes of operation: * **Single Coordinate**: Defines a **point-based** roadblock. This may result in two roadblocks being created - one for each travel direction. * **Multiple Coordinates**: Defines a **path-based** roadblock, starting at the first coordinate and ending at the last. This is used to restrict access along a specific road segment. For example, adding a path-based user-defined persistent roadblock on both sides of the matching road, starting from now and available for 1 hour which affects cars can be done in the following way: * Kotlin * Java ```kotlin val coords = arrayListOf(Coordinates(405.847994, 24.956233)) val startTime = Time.getUniversalTime() val endTime = Time.getUniversalTime()?.apply { minute += 60 } val traffic = Traffic() val trafficEvent = if (startTime != null && endTime != null) { traffic.addPersistentRoadblock( coords = coords, startUTC = startTime, expireUTC = endTime, transportMode = ERouteTransportMode.Car.value, id = "test_id" ) } else null if (trafficEvent != null) { println("The addition was successful") } else { println("The addition failed") } ``` ```java ArrayList coords = new ArrayList<>(); coords.add(new Coordinates(405.847994, 24.956233)); Time startTime = Time.getUniversalTime(); Time endTime = Time.getUniversalTime(); Traffic traffic = new Traffic(); TrafficEvent trafficEvent = null; if (startTime != null && endTime != null) { endTime.setMinute(endTime.getMinute() + 60); trafficEvent = traffic.addPersistentRoadblock( coords, startTime, endTime, ERouteTransportMode.Car.getValue(), "test_id" ); } if (trafficEvent != null) { System.out.println("The addition was successful"); } else { System.out.println("The addition failed"); } ``` The method returns the result in a similar way to the area-based method. danger In addition to the scenarios described above, the path-based `addPersistentRoadblock` method may also fail in the following cases: * **No Suitable Road Found**: If a valid road cannot be identified at the specified coordinates, or if no road data (online or offline) is available for the given location, the method will return `null`. * **Route Computation Failed**: If multiple coordinates are provided but a valid route cannot be computed between them, the method will return `null`. ##### Add an anti-area persistent roadblock[​](#add-an-anti-area-persistent-roadblock "Direct link to Add an anti-area persistent roadblock") info The Android SDK does not currently provide a specific method for creating anti-area roadblocks. This functionality may be available in future versions of the SDK. #### Get all user-defined persistent roadblocks[​](#get-all-user-defined-persistent-roadblocks "Direct link to Get all user-defined persistent roadblocks") The `persistentRoadblocks` property provided by the `Traffic` class provides the list of persistent roadblocks. The following snippet iterates through all persistent roadblocks - both path-based and area-based - and prints their descriptions. * Kotlin * Java ```kotlin val traffic = Traffic() val roadblocks = traffic.persistentRoadblocks roadblocks?.forEach { roadblock -> if (roadblock.isUserRoadblock()) println(roadblock.description) } ``` ```java Traffic traffic = new Traffic(); List roadblocks = traffic.getPersistentRoadblocks(); if (roadblocks != null) { for (TrafficEvent roadblock : roadblocks) { if (roadblock.isUserRoadblock()) { System.out.println(roadblock.getDescription()); } } } ``` All user-defined roadblocks that are currently active or scheduled to become active are returned. Expired roadblocks are automatically removed. #### Get user-defined persistent roadblocks[​](#get-user-defined-persistent-roadblocks "Direct link to Get user-defined persistent roadblocks") info The Android SDK does not currently provide a specific method to get a roadblock by ID. You can iterate through the `persistentRoadblocks` list to find a specific roadblock by comparing IDs or other properties. * Kotlin * Java ```kotlin val traffic = Traffic() val roadblocks = traffic.persistentRoadblocks val targetId = "unique_id" val event = roadblocks?.find { it.description == targetId } if (event != null) { println("Event was found") } else { println("Event does not exist") } ``` ```java Traffic traffic = new Traffic(); List roadblocks = traffic.getPersistentRoadblocks(); String targetId = "unique_id"; TrafficEvent event = null; if (roadblocks != null) { for (TrafficEvent rb : roadblocks) { if (targetId.equals(rb.getDescription())) { event = rb; break; } } } if (event != null) { System.out.println("Event was found"); } else { System.out.println("Event does not exist"); } ``` #### Remove user-defined roadblocks[​](#remove-user-defined-roadblocks "Direct link to Remove user-defined roadblocks") ##### Remove persistent user-defined roadblock by id[​](#remove-persistent-user-defined-roadblock-by-id "Direct link to Remove persistent user-defined roadblock by id") Use the `removePersistentRoadblock` method with a string ID to remove a roadblock if the identifier is known. * Kotlin * Java ```kotlin val traffic = Traffic() val errorCode = traffic.removePersistentRoadblock("identifier") if (errorCode == GemError.Success) { println("Removal succeeded") } else { println("Removal failed with error code $errorCode") } ``` ```java Traffic traffic = new Traffic(); int errorCode = traffic.removePersistentRoadblock("identifier"); if (errorCode == GemError.Success) { System.out.println("Removal succeeded"); } else { System.out.println("Removal failed with error code " + errorCode); } ``` The method returns `GemError.Success` if the roadblock was removed and `GemError.NotFound` if no roadblock was found with the given id. This method works both for path-based and area-based roadblocks. ##### Remove persistent user-defined roadblock by coordinates[​](#remove-persistent-user-defined-roadblock-by-coordinates "Direct link to Remove persistent user-defined roadblock by coordinates") Use the `removePersistentRoadblock` method with a `Coordinates` parameter to remove path-based roadblocks by providing the method with the *first* coordinate of the roadblock to be removed. * Kotlin * Java ```kotlin val traffic = Traffic() val errorCode = traffic.removePersistentRoadblock(coords) if (errorCode == GemError.Success) { println("Removal succeeded") } else { println("Removal failed with error code $errorCode") } ``` ```java Traffic traffic = new Traffic(); int errorCode = traffic.removePersistentRoadblock(coords); if (errorCode == GemError.Success) { System.out.println("Removal succeeded"); } else { System.out.println("Removal failed with error code " + errorCode); } ``` The method returns `GemError.Success` if the roadblock was removed and `GemError.NotFound` if no roadblock was found starting with the given coordinate. ##### Remove user-defined roadblock given the TrafficEvent[​](#remove-user-defined-roadblock-given-the-trafficevent "Direct link to Remove user-defined roadblock given the TrafficEvent") If the `TrafficEvent` instance is available, it can be removed using the `removeUserRoadblock` method: * Kotlin * Java ```kotlin val event: TrafficEvent = // ... obtained from somewhere val traffic = Traffic() traffic.removeUserRoadblock(event) ``` ```java TrafficEvent event = // ... obtained from somewhere Traffic traffic = new Traffic(); traffic.removeUserRoadblock(event); ``` Tip This method can be used for both persistent and non-persistent roadblocks. ##### Remove all user-defined persistent roadblocks[​](#remove-all-user-defined-persistent-roadblocks "Direct link to Remove all user-defined persistent roadblocks") Use the `removeAllPersistentRoadblocks` method to delete all existing user-defined roadblocks. * Kotlin * Java ```kotlin val traffic = Traffic() traffic.removeAllPersistentRoadblocks() ``` ```java Traffic traffic = new Traffic(); traffic.removeAllPersistentRoadblocks(); ``` #### Get preview of a path-based user-defined roadblock[​](#get-preview-of-a-path-based-user-defined-roadblock "Direct link to Get preview of a path-based user-defined roadblock") info The Android SDK does not currently provide a specific method for generating roadblock path previews. This functionality may be available in future versions of the SDK, or you can implement path preview by using the routing services to calculate routes between coordinates. #### Persistent roadblock listener[​](#persistent-roadblock-listener "Direct link to Persistent roadblock listener") The Magic Lane SDK for Android also allows users to register for notifications related to persistent roadblocks. These notifications are triggered in the following cases: * When a roadblock's `startTime` becomes greater than the current time - via the `onRoadblocksActivated` callback * When a roadblock's `endTime` becomes less than the current time - via the `onRoadblocksExpired` callback These callbacks provide the activated/expired `TrafficEventList`. Create a `PersistentRoadblockListener` instance: * Kotlin * Java ```kotlin val listener = object : PersistentRoadblockListener() { override fun onRoadblocksActivated(list: TrafficEventList) { // Do something with the activated events list.forEach { event -> println("Roadblock activated: ${event.description}") } } override fun onRoadblocksExpired(list: TrafficEventList) { // Do something with the expired events list.forEach { event -> println("Roadblock expired: ${event.description}") } } } ``` ```java PersistentRoadblockListener listener = new PersistentRoadblockListener() { @Override public void onRoadblocksActivated(TrafficEventList list) { // Do something with the activated events for (TrafficEvent event : list) { System.out.println("Roadblock activated: " + event.getDescription()); } } @Override public void onRoadblocksExpired(TrafficEventList list) { // Do something with the expired events for (TrafficEvent event : list) { System.out.println("Roadblock expired: " + event.getDescription()); } } }; ``` Register the listener using the `setPersistentRoadblockListener` method of the `Traffic` class: * Kotlin * Java ```kotlin val traffic = Traffic() traffic.setPersistentRoadblockListener(listener) ``` ```java Traffic traffic = new Traffic(); traffic.setPersistentRoadblockListener(listener); ``` --- ### Add voice guidance |

Voice guidance in the Maps SDK for Android allows you to enhance navigation experiences with spoken instructions. This guide covers how to enable built-in Text-to-Speech (TTS), manage voice settings, switch voices and languages, and integrate custom playback using the `onNavigationSound` callback for maximum flexibility. The Maps SDK for Android provides two options for instruction playback: * **Built-in solutions** - playback using human voice recordings or computer-generated TTS. * **External integration** - manual handling of sound playback through the `onNavigationSound` callback. The built-in solution also provides automatic audio session management, ducking other playbacks (such as music) while instructions are playing. #### Quick start[​](#quick-start "Direct link to Quick start") Enable voice guidance using the built-in `SoundPlayingService` by implementing proper sound handling in your navigation listener: * Kotlin * Java ```kotlin import com.magiclane.sdk.core.SoundPlayingService import com.magiclane.sdk.core.SoundPlayingListener import com.magiclane.sdk.routesandnavigation.NavigationListener // Create a sound listener for navigation sounds val soundListener = object : SoundPlayingListener() { override fun notifyStart(hasProgress: Boolean) { // Navigation sound started } override fun notifyComplete(errorCode: Int, hint: String) { // Navigation sound completed } } // Add navigation listener with sound handling val navigationListener = NavigationListener.create( onNavigationSound = { sound -> // Play the navigation sound using the service val preferences = SoundPlayingService.getPlayingPreferences() SoundPlayingService.play(sound, soundListener, preferences) } // Handle other navigation events... ) // Start navigation with voice guidance val navigationService = NavigationService() navigationService.startNavigationWithRoute( route = route, navigationListener = navigationListener, progressListener = ProgressListener.create() ) ``` ```java import com.magiclane.sdk.core.SoundPlayingService; import com.magiclane.sdk.core.SoundPlayingListener; import com.magiclane.sdk.routesandnavigation.NavigationListener; // Create a sound listener for navigation sounds SoundPlayingListener soundListener = new SoundPlayingListener() { @Override public void notifyStart(boolean hasProgress) { // Navigation sound started } @Override public void notifyComplete(int errorCode, String hint) { // Navigation sound completed } }; // Add navigation listener with sound handling NavigationListener navigationListener = NavigationListener.create( /* onNavigationSound */ (sound) -> { // Play the navigation sound using the service SoundPlayingPreferences preferences = SoundPlayingService.getPlayingPreferences(); SoundPlayingService.play(sound, soundListener, preferences); } // Handle other navigation events... ); // Start navigation with voice guidance NavigationService navigationService = new NavigationService(); navigationService.startNavigationWithRoute( route, navigationListener, ProgressListener.create() ); ``` For external TTS integration, override the default sound handling: * Kotlin * Java ```kotlin val navigationListener = NavigationListener.create( onNavigationSound = { sound -> // Custom TTS handling val instructionText = sound.getAsString() if (instructionText.isNotEmpty()) { // Use your preferred TTS engine customTtsEngine.speak(instructionText) } } // Handle other events... ) ``` ```java NavigationListener navigationListener = NavigationListener.create( /* onNavigationSound */ (sound) -> { // Custom TTS handling String instructionText = sound.getAsString(); if (instructionText != null && !instructionText.isEmpty()) { // Use your preferred TTS engine customTtsEngine.speak(instructionText); } } // Handle other events... ); ``` > **Note:** The SDK does not provide automatic sound playback. You must implement sound handling in the `onNavigationSound` callback to hear voice instructions. #### Basic Voice Guidance[​](#basic-voice-guidance "Direct link to Basic Voice Guidance") To enable voice guidance, you need to handle navigation sounds in your `NavigationListener` and use the `SoundPlayingService` to play them: * Kotlin * Java ```kotlin import com.magiclane.sdk.core.SoundPlayingService import com.magiclane.sdk.core.SoundPlayingListener import com.magiclane.sdk.core.SoundPlayingPreferences // Configure sound preferences val preferences = SoundPlayingService.getPlayingPreferences() preferences.volume = 8 // Set volume (0-10) // Create navigation listener with sound handling val navigationListener = NavigationListener.create( onNavigationSound = { sound -> val soundListener = object : SoundPlayingListener() { override fun notifyComplete(errorCode: Int, hint: String) { if (errorCode != 0) { println("Sound playback failed: $errorCode") } } } // Play the navigation instruction sound SoundPlayingService.play(sound, soundListener, preferences) } ) ``` ```java import com.magiclane.sdk.core.SoundPlayingService; import com.magiclane.sdk.core.SoundPlayingListener; import com.magiclane.sdk.core.SoundPlayingPreferences; // Configure sound preferences SoundPlayingPreferences preferences = SoundPlayingService.getPlayingPreferences(); preferences.setVolume(8); // Set volume (0-10) // Create navigation listener with sound handling NavigationListener navigationListener = NavigationListener.create( /* onNavigationSound */ (sound) -> { SoundPlayingListener soundListener = new SoundPlayingListener() { @Override public void notifyComplete(int errorCode, String hint) { if (errorCode != 0) { System.out.println("Sound playback failed: " + errorCode); } } }; // Play the navigation instruction sound SoundPlayingService.play(sound, soundListener, preferences); } ); ``` warning Ensure that a valid TTS voice is configured and the voice volume is set to a positive value within the allowed range (0-10) to hear voice instructions. tip By default, the current voice is set to the best computer TTS voice matching the default SDK language. #### The Sound Playing Service[​](#the-sound-playing-service "Direct link to The Sound Playing Service") The `SoundPlayingService` provides the following key methods for sound management: | Method | Description | | ----------------------------------------------------- | ------------------------------------------------------------- | | `play(sound, listener, preferences)` | Plays an ISound object with monitoring and custom preferences | | `playText(text, listener, preferences)` | Plays TTS text using the current TTS voice | | `playFile(filePath, mimeType, listener, preferences)` | Plays an audio file with specified MIME type | | `getPlayingPreferences()` | Gets the current sound playing preferences (volume, etc.) | | `setTTSLanguage(languageCode)` | Sets the TTS language for computer voices | | `getTTSLanguages()` | Gets list of available TTS languages | | `cancel(listener)` | Cancels playback associated with a specific listener | | `playingSoundsCount()` | Returns the number of sounds currently playing | #### Voice[​](#voice "Direct link to Voice") The Maps SDK for Android provides a list of voices for each supported language. These voices can be downloaded and activated to provide navigation prompts such as turn instructions, warnings, and announcements. The SDK offers two types of voice guidance, defined by the `EVoiceType` enum: * `EVoiceType.Human`: Uses pre-recorded human voices to deliver instructions in a friendly and natural tone. These voices support only basic instruction types and **do not** include road or settlement names. * `EVoiceType.Computer`: Leverages the device's Text-to-Speech (TTS) engine to provide more detailed and flexible guidance. These voices **fully support** street and place names in the spoken instructions. The quality and availability is dependent on the device. ##### Voice Structure[​](#voice-structure "Direct link to Voice Structure") The `Voice` class provides the following details: | Property | Type | Description | | ---------- | ---------- | --------------------------------------------------------------------------------- | | `id` | Long | Unique identifier for the voice. | | `name` | String | Display name of the voice. | | `filename` | String | File name which can be used to load the voice (available for `EVoiceType.Human`). | | `language` | Language | Associated language object. | | `type` | EVoiceType | Enum: `Human` or `Computer`. | danger Do not confuse the `Voice` and `Language` concepts. The `Language` defines **what** is said - it determines the words, phrasing, and localization. The `Voice` defines **how** it is said - it controls attributes like accent, tone, and gender. Always ensure that the selected `Voice` is compatible with the chosen `Language`, as mismatched combinations may result in unnatural or incorrect pronunciation. ###### Relevance[​](#relevance "Direct link to Relevance") * `Language` is relevant for both the built-in TTS system and custom solutions using the `onNavigationSound` callback. See the [internationalization guide](/docs/android/guides/todo.md) for more info about the `Language` class. * `Voice` is relevant **only** for the built-in voice-guidance (using human and computer voices) playback. danger The SDK distinguishes between two language settings: * **SDK language** (`SdkSettings.language`) : Defines the language used for all on-screen text and UI intended strings. * **Voice language** (`Voice.language`) : Defines the language used for spoken output, whether through the built-in engine (computer/human voices) or via the `onNavigationSound` callback. Both settings use the same `Language` class. The synchronization between the SDK language and the voice language should be made by the user, depending on the use case. ##### Get the Current Voice[​](#get-the-current-voice "Direct link to Get the Current Voice") Use the `voice` property provided by the `SdkSettings` class: * Kotlin * Java ```kotlin val currentVoice = SdkSettings.voice ``` ```java Voice currentVoice = SdkSettings.getVoice(); ``` ##### Get the List of Available Human Voices[​](#get-the-list-of-available-human-voices "Direct link to Get the List of Available Human Voices") The available human voices list is provided by the `getLocalContentList` method provided by the `ContentStore` class. * Kotlin * Java ```kotlin val contentStore = ContentStore() val items = contentStore.getLocalContentList(EContentType.HumanVoice) items?.forEach { contentStoreItem -> // The voice name (ex: Ella) val name = contentStoreItem.name // The absolute path to the voice file. Used for applying the voice. val filePath = contentStoreItem.filename // Unique identifier for the voice val id = contentStoreItem.id // Voice language information val language = contentStoreItem.language // Voice type (Human or Computer) val type = contentStoreItem.type // Country codes associated with this voice val countryCodes = contentStoreItem.countryCodes } ``` ```java ContentStore contentStore = new ContentStore(); List items = contentStore.getLocalContentList(EContentType.HumanVoice); if (items != null) { for (ContentStoreItem contentStoreItem : items) { // The voice name (ex: Ella) String name = contentStoreItem.getName(); // The absolute path to the voice file. Used for applying the voice. String filePath = contentStoreItem.getFilename(); // Unique identifier for the voice long id = contentStoreItem.getId(); // Voice language information Language language = contentStoreItem.getLanguage(); // Voice type (Human or Computer) EVoiceType type = contentStoreItem.getType(); // Country codes associated with this voice List countryCodes = contentStoreItem.getCountryCodes(); } } ``` tip Check the [Manage Content Guide](/docs/android/guides/todo.md) for information about managing voices and performing operations such as downloading, deleting, and more and details about the `ContentStore` and `ContentStoreItem` classes. ##### Apply a Voice by path[​](#apply-a-voice-by-path "Direct link to Apply a Voice by path") A human voice can be applied by providing the absolute path (obtained from the `ContentStoreItem.filename` getter) to the `setVoiceByPath` method provided by the `SdkSettings` class: * Kotlin * Java ```kotlin val filePath = contentStoreItem.filename SdkSettings.setVoiceByPath(filePath) ``` ```java String filePath = contentStoreItem.getFilename(); SdkSettings.setVoiceByPath(filePath); ``` Tip The `SdkSettings.setVoiceByPath` method can also be used to set computer voices, provided the computer voice path is known. For setting TTS computer voices, it's recommended to use `SoundPlayingService.setTTSLanguage` with the appropriate language code instead. ##### Apply a Voice by language[​](#apply-a-voice-by-language "Direct link to Apply a Voice by language") Computer voices can be applied using the `setTTSLanguage` method provided by the `SoundPlayingService` class. The method requires a language code string: * Kotlin * Java ```kotlin // Get available TTS languages val ttsLanguages = SoundPlayingService.getTTSLanguages() // Set TTS language by language code if (ttsLanguages.isNotEmpty()) { SoundPlayingService.setTTSLanguage(ttsLanguages[0].code) } // Or set directly with a known language code SoundPlayingService.setTTSLanguage("en-US") ``` ```java // Get available TTS languages List ttsLanguages = SoundPlayingService.getTTSLanguages(); // Set TTS language by language code if (!ttsLanguages.isEmpty()) { SoundPlayingService.setTTSLanguage(ttsLanguages.get(0).getCode()); } // Or set directly with a known language code SoundPlayingService.setTTSLanguage("en-US"); ``` The computer voice is implemented using the device included TTS capabilities. danger Selecting a computer voice in an unsupported language may cause a mismatch between the spoken voice and the instruction content. The exact behavior depends on the device and its available text-to-speech capabilities. #### Get the TTS Instruction Strings[​](#get-the-tts-instruction-strings "Direct link to Get the TTS Instruction Strings") The TTS instructions can be provided by the `NavigationService` as strings in order to be further processed and played with external tools. For example, Android's built-in TTS engine or third-party TTS libraries can be used for playing sound based on the TTS text instructions provided by the SDK. The voice instructions strings will come on the `onNavigationSound` callback set during navigation or simulation. Add Android's built-in TTS or a third-party TTS library to your project. For Android's built-in TTS, ensure you have the necessary permissions in your `AndroidManifest.xml`. We can use the `onNavigationSound` callback of the navigation listener in the following way: * Kotlin * Java ```kotlin import android.speech.tts.TextToSpeech import com.magiclane.sdk.core.ISound import com.magiclane.sdk.routesandnavigation.NavigationInstruction import com.magiclane.sdk.routesandnavigation.NavigationListener // instantiate Android TTS val tts = TextToSpeech(context) { status -> if (status == TextToSpeech.SUCCESS) { // TTS engine initialized successfully } } fun simulationInstructionUpdated(instruction: NavigationInstruction) { // handle instruction } fun handleNavigationSound(sound: ISound) { val instructionText = sound.getAsString() if (!instructionText.isNullOrEmpty()) { tts.speak(instructionText, TextToSpeech.QUEUE_FLUSH, null, "NavigationTTS") } } val navigationListener = NavigationListener.create( onNavigationInstructionUpdated = { instruction -> simulationInstructionUpdated(instruction) }, onNavigationSound = { sound -> handleNavigationSound(sound) } ) val navigationService = NavigationService() navigationService.startSimulationWithRoute( route = route, navigationListener = navigationListener, progressListener = ProgressListener.create(), speedMultiplier = 2.0f ) ``` ```java import android.speech.tts.TextToSpeech; import com.magiclane.sdk.core.ISound; import com.magiclane.sdk.routesandnavigation.NavigationInstruction; import com.magiclane.sdk.routesandnavigation.NavigationListener; // instantiate Android TTS TextToSpeech tts = new TextToSpeech(context, (status) -> { if (status == TextToSpeech.SUCCESS) { // TTS engine initialized successfully } }); private void simulationInstructionUpdated(NavigationInstruction instruction) { // handle instruction } private void handleNavigationSound(ISound sound) { String instructionText = sound.getAsString(); if (instructionText != null && !instructionText.isEmpty()) { tts.speak(instructionText, TextToSpeech.QUEUE_FLUSH, null, "NavigationTTS"); } } NavigationListener navigationListener = NavigationListener.create( /* onNavigationInstructionUpdated */ (instruction) -> { simulationInstructionUpdated(instruction); }, /* onNavigationSound */ (sound) -> { handleNavigationSound(sound); } ); NavigationService navigationService = new NavigationService(); navigationService.startSimulationWithRoute( route, navigationListener, ProgressListener.create(), 2.0f // speedMultiplier ); ``` For navigation we can set the `onNavigationSound` callback in a similar way with `startNavigation`. See the Android documentation for [TextToSpeech](https://developer.android.com/reference/android/speech/tts/TextToSpeech) for information on how to set the TTS voice, language and other options such as pitch, volume, speech rate, etc. Tip To change the language of the instructions provided through the `onNavigationSound` callback, you can: * Use `SoundPlayingService.setTTSLanguage` and specify the preferred language code. * Or use `SdkSettings.setVoiceByPath` with a voice path corresponding to the desired language. To disable the internal playback engine, simply don't call `SoundPlayingService.play()` in your `onNavigationSound` callback. The instruction will still be delivered via the callback, but no audio will be played. #### Monitor Sound Playback Events[​](#monitor-sound-playback-events "Direct link to Monitor Sound Playback Events") The Maps SDK provides sound playback monitoring through the `SoundPlayingListener` interface. This listener receives callbacks during sound operations and allows you to track playback progress. Here's how to create a custom sound playing listener: * Kotlin * Java ```kotlin import com.magiclane.sdk.core.SoundPlayingListener import com.magiclane.sdk.core.SoundPlayingService import com.magiclane.sdk.core.SoundPlayingPreferences class CustomSoundPlayingListener : SoundPlayingListener() { override fun notifyStart(hasProgress: Boolean) { // Sound playback started println("Sound playback started. Has progress: $hasProgress") } override fun notifyProgress(progress: Int) { // Progress update (0-100) println("Playback progress: $progress%") } override fun notifyComplete(errorCode: Int, hint: String) { // Sound playback completed if (errorCode == 0) { println("Sound playback completed successfully") } else { println("Sound playback failed with error: $errorCode") } } override fun onVolumeChangedByKeys(newVolume: Int) { // Volume changed via hardware keys println("Volume changed to: $newVolume") } } // Use the listener when playing sounds val listener = CustomSoundPlayingListener() val preferences = SoundPlayingPreferences() // Play text with monitoring SoundPlayingService.playText( text = "Turn left in 100 meters", listener = listener, preferences = preferences ) ``` ```java import com.magiclane.sdk.core.SoundPlayingListener; import com.magiclane.sdk.core.SoundPlayingService; import com.magiclane.sdk.core.SoundPlayingPreferences; public class CustomSoundPlayingListener extends SoundPlayingListener { @Override public void notifyStart(boolean hasProgress) { // Sound playback started System.out.println("Sound playback started. Has progress: " + hasProgress); } @Override public void notifyProgress(int progress) { // Progress update (0-100) System.out.println("Playback progress: " + progress + "%"); } @Override public void notifyComplete(int errorCode, String hint) { // Sound playback completed if (errorCode == 0) { System.out.println("Sound playback completed successfully"); } else { System.out.println("Sound playback failed with error: " + errorCode); } } @Override public void onVolumeChangedByKeys(int newVolume) { // Volume changed via hardware keys System.out.println("Volume changed to: " + newVolume); } } // Use the listener when playing sounds CustomSoundPlayingListener listener = new CustomSoundPlayingListener(); SoundPlayingPreferences preferences = new SoundPlayingPreferences(); // Play text with monitoring SoundPlayingService.playText( "Turn left in 100 meters", listener, preferences ); ``` ##### Monitoring Navigation Sounds[​](#monitoring-navigation-sounds "Direct link to Monitoring Navigation Sounds") To monitor navigation sounds specifically, you can create a listener and use it with the sound received from `onNavigationSound`: * Kotlin * Java ```kotlin val soundListener = object : SoundPlayingListener() { override fun notifyStart(hasProgress: Boolean) { // Navigation sound started playing } override fun notifyComplete(errorCode: Int, hint: String) { // Navigation sound finished } } val navigationListener = NavigationListener.create( onNavigationSound = { sound -> // Play the navigation sound with monitoring val preferences = SoundPlayingService.getPlayingPreferences() SoundPlayingService.play(sound, soundListener, preferences) } ) ``` ```java SoundPlayingListener soundListener = new SoundPlayingListener() { @Override public void notifyStart(boolean hasProgress) { // Navigation sound started playing } @Override public void notifyComplete(int errorCode, String hint) { // Navigation sound finished } }; NavigationListener navigationListener = NavigationListener.create( /* onNavigationSound */ (sound) -> { // Play the navigation sound with monitoring SoundPlayingPreferences preferences = SoundPlayingService.getPlayingPreferences(); SoundPlayingService.play(sound, soundListener, preferences); } ); ``` ##### Getting Playback Information[​](#getting-playback-information "Direct link to Getting Playback Information") You can check the current playback status using these methods: * Kotlin * Java ```kotlin // Check how many sounds are currently playing val playingCount = SoundPlayingService.playingSoundsCount() // Get current playing preferences val preferences = SoundPlayingService.getPlayingPreferences() val currentVolume = preferences.volume // Volume level (0-10) val maxPlayingTime = preferences.maxPlayingTime // Max duration in seconds // Cancel a specific playback SoundPlayingService.cancel(listener) ``` ```java // Check how many sounds are currently playing int playingCount = SoundPlayingService.playingSoundsCount(); // Get current playing preferences SoundPlayingPreferences preferences = SoundPlayingService.getPlayingPreferences(); int currentVolume = preferences.getVolume(); // Volume level (0-10) int maxPlayingTime = preferences.getMaxPlayingTime(); // Max duration in seconds // Cancel a specific playback SoundPlayingService.cancel(listener); ``` info The `SoundPlayingListener` is used for all sound operations in the SDK, including TTS, file playback, and navigation sounds. Only one sound can be played at a time through the service. #### Play custom instructions[​](#play-custom-instructions "Direct link to Play custom instructions") Sometimes you may need to play custom instructions, such as road information warnings or social reports. To do this, use the `playText` method of the `SoundPlayingService` to play back a custom string. This uses the currently selected TTS voice and requires a listener and preferences. * Kotlin * Java ```kotlin import com.magiclane.sdk.core.SoundPlayingService import com.magiclane.sdk.core.SoundPlayingListener import com.magiclane.sdk.core.SoundPlayingPreferences // Create a listener for the custom instruction val customInstructionListener = object : SoundPlayingListener() { override fun notifyStart(hasProgress: Boolean) { println("Custom instruction started") } override fun notifyComplete(errorCode: Int, hint: String) { if (errorCode == 0) { println("Custom instruction completed") } else { println("Custom instruction failed: $errorCode") } } } // Get default preferences or create custom ones val preferences = SoundPlayingService.getPlayingPreferences() // Play a custom instruction SoundPlayingService.playText( text = "Speed camera ahead", listener = customInstructionListener, preferences = preferences ) ``` ```java import com.magiclane.sdk.core.SoundPlayingService; import com.magiclane.sdk.core.SoundPlayingListener; import com.magiclane.sdk.core.SoundPlayingPreferences; // Create a listener for the custom instruction SoundPlayingListener customInstructionListener = new SoundPlayingListener() { @Override public void notifyStart(boolean hasProgress) { System.out.println("Custom instruction started"); } @Override public void notifyComplete(int errorCode, String hint) { if (errorCode == 0) { System.out.println("Custom instruction completed"); } else { System.out.println("Custom instruction failed: " + errorCode); } } }; // Get default preferences or create custom ones SoundPlayingPreferences preferences = SoundPlayingService.getPlayingPreferences(); // Play a custom instruction SoundPlayingService.playText( "Speed camera ahead", customInstructionListener, preferences ); ``` tip Check the [speed warnings](/docs/android/guides/todo.md) and [landmark & overlay alarms](/docs/android/guides/todo.md) docs to learn how to get notified about speed warnings and reports. --- ### Offline The SDK provides extensive offline functionality through downloadable map regions. Once downloaded, users can search for landmarks, calculate and navigate routes, and explore the map - all without an active internet connection. The SDK also supports downloading and managing additional content such as maps and styles. #### [📄️ Introduction](/docs/android/guides/offline/introduction.md) [The Maps SDK for Android provides extensive offline functionality through its map download capabilities. Users can search for landmarks, calculate and navigate routes, and explore the map without requiring an active internet connection.](/docs/android/guides/offline/introduction.md) #### [📄️ Manage content](/docs/android/guides/offline/manage-content.md) [The Maps SDK for Android offers comprehensive functionality for managing offline content.](/docs/android/guides/offline/manage-content.md) #### [📄️ Update content](/docs/android/guides/offline/update-content.md) [The Magic Lane SDK allows updating downloaded content to stay synchronized with the latest map data. New map versions are released every few weeks.](/docs/android/guides/offline/update-content.md) --- ### Introduction |

The Maps SDK for Android provides extensive offline functionality through its map download capabilities. Users can search for landmarks, calculate and navigate routes, and explore the map without requiring an active internet connection. However, certain features, such as overlays, live traffic information and other online-dependent services, are unavailable in offline mode. The SDK empowers users to download maps for entire countries or specific regions directly to their devices, enabling seamless offline access to essential features. Additionally, it allows users to update their downloaded maps, ensuring they always have access to the latest data and improvements for offline use. The SDK provides automatic update capabilities for map data. New map versions are typically released every few weeks globally, providing regular enhancements and improvements. Additionally, the SDK notifies developers about available updates, keeping them informed and in control. Developers can configure update preferences, such as restricting updates or downloads to Wi-Fi connections only, or permitting them over cellular data, offering flexibility based on user needs and network conditions. #### SDK Features available offline[​](#sdk-features-available-offline "Direct link to SDK Features available offline") ##### Core Entities[​](#core-entities "Direct link to Core Entities") | Entity | Offline Availability | | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Base entities](/docs/android/guides/core/base-entities.md) | Fully operational in offline use cases, as they do not require an active internet connection. | | [Position](/docs/android/guides/core/positions.md) | Partially available. Raw position data is always accessible, but map-matched position data is only available if the relevant region has been previously downloaded or cached. | | [Landmarks](/docs/android/guides/core/landmarks.md) | Fully available in offline mode. | | [Markers](/docs/android/guides/core/markers.md) | Fully available in offline mode. | | [Overlays](/docs/android/guides/core/overlays.md) | Not available in offline mode. | | [Routes](/docs/android/guides/core/routes.md) | Partially available. The `trafficEvents` getter will return an empty list when there is no internet connection. | | [Navigation](/docs/android/guides/core/navigation-instructions.md) | Fully available in offline mode if navigation is started on an offline-calculated route. | info Map tiles are automatically cached based on the user's location, camera position, and calculated routes to enhance performance and offline accessibility. ##### MapView[​](#mapview "Direct link to MapView") The `MapView` methods function as expected in offline mode. However, methods that request data from regions that are not covered or cached will return empty results (e.g., an empty list or null, depending on the method). For instance, calling `MapView` methods with coordinates outside of covered areas will result in an empty list or null. ##### Map Styling[​](#map-styling "Direct link to Map Styling") Setting a new map style is supported in offline mode, provided the style has been downloaded beforehand. Note that styles containing extensive data, such as Satellite and Weather styles, may not display meaningful information when offline. ##### Services[​](#services "Direct link to Services") The following services are available offline only within downloaded map regions: * `RoutingService` * `SearchService` * `GuidedAddressSearchService` * `NavigationService` * `LandmarkStoreService` * `PositionService` The remaining services such as `OverlayService` and `ContentStore` are **not supported** in offline mode. Calling `SearchService.search` with queries outside of downloaded map regions will result in an error. Same goes for `RoutingService.calculateRoute`. Other services might return connection-related errors. info `AlarmService` has limited functionality during offline sessions, as overlay-related features are unavailable. ##### SDK Settings[​](#sdk-settings "Direct link to SDK Settings") Most methods in `SdkSettings` function independently of the internet connection status, with the exception of authorization-related functionalities. Authorization with the API token **requires** an active internet connection. Consequently, you cannot authorize the Maps SDK for Android without being online. --- ### Manage Content |

The Maps SDK for Android offers comprehensive functionality for managing offline content. The supported downloadable content types are defined in the `EContentType` enum: * `ViewStyleHighRes`: High-dpi screen optimized map styles that can be applied offline. These include both a selection of default styles and user-created styles from the studio, based on the API key. * `ViewStyleLowRes`: Low-dpi screen map styles, optimized for smaller file sizes while maintaining essential details. * `RoadMap`: Offline maps covering countries and regions. Within a downloaded region, users can perform searches, calculate routes, and navigate without an internet connection. * `HumanVoice`: Pre-recorded voice files used to deliver spoken navigation instructions and warnings. Tip For most use cases, the high-resolution map styles option is recommended over its low-resolution counterpart. The `ContentStore` class is responsible for managing and providing a list of downloadable items. Each item is represented by the `ContentStoreItem` class, which encapsulates details such as name, image, type, version, and size. Additionally, it offers operations for downloading and deleting content. danger Ensure that the API token is both set and valid. Some operations might fail if no valid API token is set. danger Modifying downloaded maps (download, delete, update) may interrupt ongoing operations such as search, route calculation, or navigation. #### List Online Content[​](#list-online-content "Direct link to List Online Content") To retrieve a list of available content from the Magic Lane servers, use the `asyncGetStoreContentList` method from the `ContentStore` class. This method returns an `ErrorCode` and a `ProgressListener` for tracking the operation. The operation can be canceled using the returned `ProgressListener`. The method accepts the content type as an argument and provides a callback with: * The operation error code * A list of `ContentStoreItem` objects - Kotlin - Java ```kotlin val (errorCode, progressListener) = ContentStore().asyncGetStoreContentList( EContentType.RoadMap, onStarted = { // Handle operation start }, onProgress = { // Handle progress updates }, onCompleted = { resultList, errorCode, hint -> if (errorCode == GemError.NoError) { // Use the resultList of ContentStoreItem objects } else { Log.e("ContentStore", "Failed to get content list: ${GemError.getMessage(errorCode)}") } } ) ``` ```java Pair result = new ContentStore().asyncGetStoreContentList( EContentType.RoadMap, () -> { // Handle operation start }, (progress) -> { // Handle progress updates }, (resultList, errorCode, hint) -> { if (errorCode == GemError.NoError) { // Use the resultList of ContentStoreItem objects } else { Log.e("ContentStore", "Failed to get content list: " + GemError.getMessage(errorCode)); } } ); int errorCode = result.first; ProgressListener progressListener = result.second; ``` info The `asyncGetStoreContentList` method should be called only when an active internet connection is available and the current offline version is not expired. If no internet connection is available or if the current offline map version is expired, use the `getLocalContentList` method to retrieve the offline content list instead. danger Do not invoke `asyncGetStoreContentList` before the map data is ready. Wait until the SDK has initialized the map properly before calling it. #### List Local Content[​](#list-local-content "Direct link to List Local Content") The `getLocalContentList` method can be used to get the list of local content available offline. * Kotlin * Java ```kotlin val contentStore = ContentStore() val items = contentStore.getLocalContentList(EContentType.RoadMap) if (items != null) { // Do something with the items for (item in items) { Log.d("ContentStore", "Item: ${item.name}") } } ``` ```java ContentStore contentStore = new ContentStore(); ContentStoreItemList items = contentStore.getLocalContentList(EContentType.RoadMap); if (items != null) { // Do something with the items for (ContentStoreItem item : items) { Log.d("ContentStore", "Item: " + item.getName()); } } ``` info The `getLocalContentList` method returns content store items that are either ready for use, currently downloading, or pending download. #### Filter Content[​](#filter-content "Direct link to Filter Content") To obtain a **filtered** list of available content from the Magic Lane servers, use the `asyncGetStoreFilteredList` method from the `ContentStore` class. You can filter content by specifying country ISO 3166-1 alpha-3 codes and by geographic area. * Kotlin * Java ```kotlin val countries = arrayListOf("USA", "CAN") val area = RectangleGeographicArea( topLeft = Coordinates(latitude = 53.7731, longitude = -1.7990), bottomRight = Coordinates(latitude = 38.4549, longitude = 21.1696) ) val (errorCode, progressListener) = ContentStore().asyncGetStoreFilteredList( EContentType.RoadMap, countries, area, onCompleted = { resultList, errorCode, hint -> if (errorCode == GemError.NoError) { // Use the filtered resultList } } ) ``` ```java ArrayList countries = new ArrayList<>(Arrays.asList("USA", "CAN")); RectangleGeographicArea area = new RectangleGeographicArea( new Coordinates(53.7731, -1.7990), new Coordinates(38.4549, 21.1696) ); Pair result = new ContentStore().asyncGetStoreFilteredList( EContentType.RoadMap, countries, area, (resultList, errorCode, hint) -> { if (errorCode == GemError.NoError) { // Use the filtered resultList } } ); int errorCode = result.first; ProgressListener progressListener = result.second; ``` info The filtered content should be requested via a call to `asyncGetStoreFilteredList` method. Use the `storeFilteredList` property to retrieve previously filtered results. ##### Method behaviour[​](#method-behaviour "Direct link to Method behaviour") | Condition | `onComplete` Result | | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | The `countries` list contains invalid ISO 3166-1 alpha-3 codes | `GemError.NoError` with an empty `ContentStoreItem` list. | | The `countries` list includes countries incompatible with the specified `RectangleGeographicArea` | `GemError.NoError` with an empty `ContentStoreItem` list. | | Insufficient memory to complete the operation | `GemError.NoMemory` with an empty `ContentStoreItem` list. | | Invalid `GeographicArea` (e.g., invalid coordinates) | `GemError.NoError` with a full list of `ContentStoreItem`; behaves as if no filter was applied. | | The `area` parameter is an empty `GeographicArea` | `GemError.InvalidInput` with an empty `ContentStoreItem` list. | | HTTP request failed | `GemError.General` with an empty `ContentStoreItem` list. | #### Content Store Item Fields[​](#content-store-item-fields "Direct link to Content Store Item Fields") ###### Fields Containing General Information[​](#fields-containing-general-information "Direct link to Fields Containing General Information") | Field Name | Description | | ------------------- | ---------------------------------------------------------------- | | `name` | The name of the associated product, automatically translated. | | `id` | The unique ID of the item in the content store. | | `type` | The type of the product as an `EContentType` value. | | `filename` | The full path to the content data file. | | `totalSize` | The total size of the content in bytes. | | `availableSize` | The downloaded size of the content. | | `updateSize` | The update size if an update is available. | | `status` | The current status of the item as an `EContentStoreItemStatus`. | | `contentParameters` | Additional information about an item as a `ParameterList` object | | `imagePreview` | The image associated with the content store item if available | Tip For checking if a `ContentStoreItem` is downloaded/available/downloading/updating use the `status` field value: * `Unavailable`: The content store item is not downloaded and cannot be used. * `Completed`: The content store item has been downloaded and is ready to be used. * `Paused`: The download operation has been paused by the user. * `DownloadQueued`: The download is queued and will proceed once resources are available. * `DownloadWaiting`: Waiting for a connection or other condition to proceed. * `DownloadWaitingFreeNetwork`: The SDK is waiting for a free network connection to continue the download. * `DownloadRunning`: The download is actively in progress. * `UpdateWaiting`: An update operation is underway. The `contentParameters` field provides information such as: * For `RoadMap` type: * `Copyright`: Value containing the copyright information for the road map * `MapDataProvider`: Value containing the name of the map data provider * `ReleaseDate`: Value containing the release date for the road map * `DefaultName`: Value containing the name of the item * For `ViewStyleHighRes` type: * `Background-Color`: Value containing the background color in decimal format. Can be used to check if a style is a dark-mode or light-mode recommended style. * For `HumanVoice` type: * Language and voice-related parameters The image can be retrieved via the `imagePreview` property: * Kotlin * Java ```kotlin val item: ContentStoreItem = // ... obtain from store if (item.isImagePreviewAvailable()) { val image = item.imagePreview // Use the image object } ``` ```java ContentStoreItem item = // ... obtain from store if (item.isImagePreviewAvailable()) { Image image = item.getImagePreview(); // Use the image object } ``` danger Content store items of type `RoadMap` do not have an image preview. The `MapDetails.getCountryFlag()` method can be used to get the flag image associated with a country code instead. Use the `countryCodes` property to obtain the country codes associated with a content store item of type `RoadMap`. ###### Fields Containing Download and Update Information[​](#fields-containing-download-and-update-information "Direct link to Fields Containing Download and Update Information") | Field Name | Description | | -------------------- | ---------------------------------------------------------- | | `clientVersion` | The client version of the content. | | `updateVersion` | The update version if an update is available. | | `downloadProgress` | The current download progress as a percentage. | | `updateItem` | The corresponding update item if an update is in progress. | | `isCompleted()` | Checks if the item has been completely downloaded. | | `isUpdatable()` | Checks if the item has a newer version available. | | `canDeleteContent()` | Checks if the content can be deleted. | While the download is in progress, you can retrieve details about the downloaded content: * **`isCompleted()`**: Returns `true` if the download is completed; otherwise, returns `false`. * **`downloadProgress`**: Returns the download progress as an integer between 0 and 100. * **`status`**: Returns the current status of the content store item. ###### Fields Relevant to `EContentType.RoadMap` Type Items[​](#fields-relevant-to-econtenttyperoadmap-type-items "Direct link to fields-relevant-to-econtenttyperoadmap-type-items") | Field Name | Description | | -------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `chapterName` | Some large countries are divided into multiple content store items. All items within the same country share the same chapter. | | `countryCodes` | A list of country codes (ISO 3166-1 alpha-3) associated with the product. | | `language` | The full language details for the product. | ###### Fields Relevant to `EContentType.HumanVoice` Type Items[​](#fields-relevant-to-econtenttypehumanvoice-type-items "Direct link to fields-relevant-to-econtenttypehumanvoice-type-items") | Field Name | Description | | -------------- | ------------------------------------------------------------------------- | | `countryCodes` | A list of country codes (ISO 3166-1 alpha-3) associated with the product. | | `language` | The full language details for the product. | #### Download Content Store Item[​](#download-content-store-item "Direct link to Download Content Store Item") For downloading a content store item, the `asyncDownload` method can be used: * Kotlin * Java ```kotlin val item: ContentStoreItem = // ... obtain from store val errorCode = item.asyncDownload( onProgress = { Log.d("Download", "Progress: $it%") }, onCompleted = { error,_ -> if (error == GemError.NoError) { Log.d("Download", "Completed successfully") } else { Log.e("Download", "Error: ${GemError.getMessage(error)}") } } ) ``` ```java ContentStoreItem item = // ... obtain from store int errorCode = item.asyncDownload( (progress) -> { Log.d("Download", "Progress: " + progress + "%"); }, (error, hint) -> { if (error == GemError.NoError) { Log.d("Download", "Completed successfully"); } else { Log.e("Download", "Error: " + GemError.getMessage(error)); } } ); ``` The `onCompleted` callback is invoked at the end of the operation, returning the result of the download. If the item is successfully downloaded, the error code is `GemError.NoError`. In case of an error, other values may be returned. Additionally, users can provide an optional `onProgress` callback to receive real-time progress updates. This callback is triggered with the current download progress, represented as an integer between 0 and 100. ##### Download on Charged Networks[​](#download-on-charged-networks "Direct link to Download on Charged Networks") The SDK includes functionality to restrict downloads on networks with additional charges. To enable downloads on such networks, use the `allowChargedNetworks` parameter in the `asyncDownload` method: * Kotlin * Java ```kotlin val errorCode = item.asyncDownload( allowChargedNetworks = true, // Allow downloads on charged networks onCompleted = { error,_ -> // Handle completion } ) ``` ```java int errorCode = item.asyncDownload( true, // Allow downloads on charged networks (error, hint) -> { // Handle completion } ); ``` If a download operation is requested while the user is on a charged network and `allowChargedNetworks` is false, the download will be automatically scheduled and will proceed once the user switches to a non-charged network. In this case, the `status` field of the corresponding `ContentStoreItem` object will be set to `EContentStoreItemStatus.DownloadWaitingFreeNetwork`. ##### Pause Download[​](#pause-download "Direct link to Pause Download") The download can be paused using the `pauseDownload()` method. To resume the download, call `asyncDownload()` again. * Kotlin * Java ```kotlin val item: ContentStoreItem = // ... obtain from store val errorCode = item.pauseDownload() if (errorCode == GemError.NoError) { Log.d("Download", "Download paused") } ``` ```java ContentStoreItem item = // ... obtain from store int errorCode = item.pauseDownload(); if (errorCode == GemError.NoError) { Log.d("Download", "Download paused"); } ``` danger No further download operations should be performed on the `ContentStoreItem` object until the pause operation has completed. ##### Delete Downloaded Content[​](#delete-downloaded-content "Direct link to Delete Downloaded Content") Downloaded content can be removed from local storage by calling the `deleteContent()` method on the corresponding `ContentStoreItem` object: * Kotlin * Java ```kotlin val item: ContentStoreItem = // ... obtain from store if (item.canDeleteContent()) { val errorCode = item.deleteContent() if (errorCode == GemError.NoError) { Log.d("ContentStore", "Item ${item.name} deleted successfully") } } else { Log.w("ContentStore", "Item cannot be deleted") } ``` ```java ContentStoreItem item = // ... obtain from store if (item.canDeleteContent()) { int errorCode = item.deleteContent(); if (errorCode == GemError.NoError) { Log.d("ContentStore", "Item " + item.getName() + " deleted successfully"); } } else { Log.w("ContentStore", "Item cannot be deleted"); } ``` danger Do not confuse the functionality provided by the `ContentStore` / `ContentStoreItem` classes with that of the `MapDownloaderService` class. * The `ContentStore` API is designed for **downloading full offline content**, including data required for features such as free-text search, routing, and turn-by-turn navigation. * In contrast, the `MapDownloaderService` is intended for caching map tiles mainly for visual display purposes. Tiles downloaded via `MapDownloaderService` **do not support** most search operations, routing or navigation while offline. See the [download individual map tiles documentation](/docs/android/guides/maps/adjust-map.md#download-individual-map-tiles) for more details about the `MapDownloaderService`. Also, do not confuse the `LandmarkStore` class with the `ContentStore` class. The `LandmarkStore` is a collection used for managing landmark data, while the `ContentStore` is a service used for managing offline map content. ##### Delete Downloaded Content[​](#delete-downloaded-content-1 "Direct link to Delete Downloaded Content") Downloaded content can be removed from local storage by calling the `deleteContent` method on the corresponding `ContentStoreItem` object, after checking if the item can be removed: ```dart if (contentStoreItem.canDeleteContent){ final error = contentStoreItem.deleteContent(); showSnackbar("Item ${contentStoreItem.name} deletion resulted with code $error"); } else { showSnackbar("Item cannot be deleted"); } ``` danger Do not confuse the functionality provided by the `ContentStore` / `ContentStoreItem` classes with that of the `MapDownloaderService` class. * The `ContentStore` API is designed for **downloading full offline content**, including data required for features such as free-text search, routing, and turn-by-turn navigation. * In contrast, the `MapDownloaderService` is intended for caching map tiles mainly for visual display purposes. Tiles downloaded via `MapDownloaderService` **do not support** most search operations, routing or navigation while offline. See the [download individual map tiles documentation](/docs/android/guides/maps/adjust-map.md#download-individual-map-tiles) for more details about the `MapDownloaderService`. Also, do not confuse the `LandmarkStore` class with the `ContentStore` class. The `LandmarkStore` is a collection used for managing landmark data, while the `ContentStore` is a service used for managing offline map content. #### Downloading overlays[​](#downloading-overlays "Direct link to Downloading overlays") Overlays can be downloaded for specific regions to enable offline functionality. To do this, you must first download a map region, after which the overlays become available for download within those offline areas. The offline data grabber automatically downloads overlay data when enabled. * Kotlin * Java ```kotlin val overlayService = OverlayService() val overlayId = ECommonOverlayId.Safety.value // Example overlay UID (e.g., speed cameras) if (!overlayService.isOverlayOfflineDataGrabberSupported(overlayId)) { Log.w("OverlayService", "Overlay offline data grabber not supported for this overlay") return } // Enable the offline data grabber val errorCode = overlayService.enableOverlayOfflineDataGrabber(overlayId) if (errorCode == GemError.NoError) { Log.d("OverlayService", "Offline data grabber enabled successfully") } else { Log.e("OverlayService", "Failed to enable offline data grabber: ${GemError.getMessage(errorCode)}") } // Disable the grabber when it's no longer needed // val disableErrorCode = overlayService.disableOverlayOfflineDataGrabber(overlayId) ``` ```java OverlayService overlayService = new OverlayService(); int overlayId = ECommonOverlayId.Safety.getValue(); // Example overlay UID (e.g., speed cameras) if (!overlayService.isOverlayOfflineDataGrabberSupported(overlayId)) { Log.w("OverlayService", "Overlay offline data grabber not supported for this overlay"); return; } // Enable the offline data grabber int errorCode = overlayService.enableOverlayOfflineDataGrabber(overlayId); if (errorCode == GemError.NoError) { Log.d("OverlayService", "Offline data grabber enabled successfully"); } else { Log.e("OverlayService", "Failed to enable offline data grabber: " + GemError.getMessage(errorCode)); } // Disable the grabber when it's no longer needed // int disableErrorCode = overlayService.disableOverlayOfflineDataGrabber(overlayId); ``` danger Not all overlays support offline data grabbing. Use the `isOverlayOfflineDataGrabberSupported` method to check if a specific overlay supports this feature. After enabling the offline data grabber, the overlay data will be automatically downloaded when a map region is downloaded or updated. The overlay items will then be available in offline mode within the downloaded map regions. You can verify if the overlay data has been successfully downloaded if overlay items are visible inside the downloaded map region in offline mode. ![](/docs/android/assets/images/example_android_offline_overlay_visible-f2c08dd016b614575567e240e483285d.png) **Offline speed camera overlay item visible** Tip Ensure to enable the offline data grabber using `enableOverlayOfflineDataGrabber` and verify the returned error code is `GemError.NoError`. If the overlay UID is unsupported, the method will return an error code other than `GemError.NoError`. info Not all overlay types support offline functionality (e.g., Alerts or Public Transit Stops). For instance, public transport stops will still require an internet connection to display the relevant data, thus will be rendered as landmarks instead of overlay items in offline mode. You can check if the overlay data grabber has been enabled for a specific overlay using the `isOverlayOfflineDataGrabberEnabled` method. Keeping the grabber enabled will make the grabber automatically start downloading overlay data when a new map region is downloaded or updated. This enables the user to have the latest offline overlay data available. #### Download or update content in background[​](#download-or-update-content-in-background "Direct link to Download or update content in background") To enable content updates and downloads while the app runs in the background, you must configure your app to support foreground services. This ensures that the Android system does not terminate the download process when the app is backgrounded. For detailed guidance on implementing foreground services for background operations, please refer to the [Background Location](/docs/android/guides/positioning/background-location.md) guide. --- ### Update Content |

The Magic Lane SDK allows updating downloaded content to stay synchronized with the latest map data. New map versions are released every few weeks. The update operation supports the `RoadMap`, `ViewStyleLowRes`, and `ViewStyleHighRes` content types. This article focuses on the `RoadMap` type, as it is the most common use case. Updating styles mostly follows the same process. The SDK requires all road map content store items to have the same version. It is not possible to have multiple `RoadMap` items with different versions, meaning partial updates of individual items are not supported. Based on the client's content version relative to the newest available release, it can be in one of three states, as defined by the `EOffboardListenerStatus` enum: | Status | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ExpiredData` | The client version is significantly outdated and no longer supports online operations.

All features - such as search, route calculation, and navigation - will function exclusively on the device using downloaded regions, even if an internet connection is available. If an operation like route calculation is attempted in non-downloaded regions, it will fail.

An update is **mandatory** to restore online functionality. Only relevant for `EContentType.RoadMap` elements. | | `OldData` | The client version is outdated but still supports online operations.

Features will work online when a connection is available and offline when not. While offline, only downloaded regions are accessible, but online access allows operations across the entire map.

An update is **recommended** as soon as possible. Relevant for all types of content store elements. | | `UpToDate` | The client version has the latest map data. Features function online when connected and offline using downloaded regions.

No updates are available. Relevant for all types of content store elements. | The Magic Lane servers support online operations for the most up-to-date version (`EOffboardListenerStatus.UpToDate`) and the previous version (`EOffboardListenerStatus.OldData`) of road map data. info After installation, the app includes a default map version of type `ExpiredData`, which contains no available content. Therefore, an update - either manually triggered or performed automatically - is required before the app can be used. Internet access is required for the initial use of the app. #### Update process overview[​](#update-process-overview "Direct link to Update process overview") 1. The map update process is initiated by the API user or can be triggered manually. 2. The process downloads the newer data in the background while ensuring the full usability of the current (old) map dataset for browsing, search and navigation. The content is downloaded in a close-to-current user position order, i.e. nearby maps are downloaded first. 3. Once all new version data is downloaded: * If the update is applied via the `ContentUpdater.apply()` method, the update is atomically applied by replacing the files. If the user's storage size does not allow the existence of the old and new dataset at the same time, the update requires handling: 4. The remaining offline maps which did not download because of space constraints can be downloaded by calling `ContentStoreItem.asyncDownload()` on each remaining item. info The auto-update behavior is different between the Magic Lane SDKs: * The C++ SDK does not provide an auto-update mechanism * The Kotlin SDK provides an update mechanism through ContentUpdater * The iOS SDK does not provide an auto-update mechanism #### Check for updates[​](#check-for-updates "Direct link to Check for updates") You can listen for map updates by registering a callback with `OffboardListener` to monitor the status of road map versions. * Kotlin * Java ```kotlin val contentStore = ContentStore() val errorCode = contentStore.checkForUpdate(EContentType.RoadMap) if (errorCode == GemError.NoError) { // Check initiated successfully } else if (errorCode == GemError.ConnectionRequired) { Log.e("Update", "No internet connection available") } ``` ```java ContentStore contentStore = new ContentStore(); int errorCode = contentStore.checkForUpdate(EContentType.RoadMap); if (errorCode == GemError.NoError) { // Check initiated successfully } else if (errorCode == GemError.ConnectionRequired) { Log.e("Update", "No internet connection available"); } ``` The `checkForUpdate` method returns: * `GemError.NoError` if the check has been initiated * `GemError.ConnectionRequired` if no internet connection is available * Other error codes for different failure scenarios You can monitor status changes through the `OffboardListener` callbacks: * Kotlin * Java ```kotlin object : OffboardListener() { override fun onWorldwideRoadMapSupportStatus(state: EOffboardListenerStatus) { when (state) { EOffboardListenerStatus.UpToDate -> { Log.i("Update", "Map version is up-to-date") } EOffboardListenerStatus.OldData -> { Log.i("Update", "New map version available - online operations still supported") } EOffboardListenerStatus.ExpiredData -> { Log.w("Update", "Map version expired - only offline operations available") } } } } ``` ```java new OffboardListener() { @Override public void onWorldwideRoadMapSupportStatus(EOffboardListenerStatus state) { if (state == EOffboardListenerStatus.UpToDate) { Log.i("Update", "Map version is up-to-date"); } else if (state == EOffboardListenerStatus.OldData) { Log.i("Update", "New map version available - online operations still supported"); } else if (state == EOffboardListenerStatus.ExpiredData) { Log.w("Update", "Map version expired - only offline operations available"); } } }; ``` #### Create content updater[​](#create-content-updater "Direct link to Create content updater") To update road maps or styles, you must create a `ContentUpdater` object. This object manages all operations related to the update process: * Kotlin * Java ```kotlin val contentStore = ContentStore() val (contentUpdater, creationError) = contentStore.createContentUpdater(EContentType.RoadMap) ?: return if (creationError == GemError.NoError) { // ContentUpdater successfully created } else if (creationError == GemError.Exist) { // A ContentUpdater for this type already exists, but it's still valid } else { Log.e("Update", "Failed to create ContentUpdater: ${GemError.getMessage(creationError)}") return } ``` ```java ContentStore contentStore = new ContentStore(); Pair result = contentStore.createContentUpdater(EContentType.RoadMap); if (result == null) { return; } ContentUpdater contentUpdater = result.first; int creationError = result.second; if (creationError == GemError.NoError) { // ContentUpdater successfully created } else if (creationError == GemError.Exist) { // A ContentUpdater for this type already exists, but it's still valid } else { Log.e("Update", "Failed to create ContentUpdater: " + GemError.getMessage(creationError)); return; } ``` The `createContentUpdater` method returns a `Pair` with: * A `ContentUpdater` instance (null if creation failed completely) * An error code indicating the status: * `GemError.NoError`: Successfully created and ready for use * `GemError.Exist`: Already exists from a previous operation (still valid) * Other error codes: Creation failed #### Start the update[​](#start-the-update "Direct link to Start the update") In order to start the update process, call the `update` method from the `ContentUpdater` object: * Kotlin * Java ```kotlin val (contentUpdater, creationError) = contentStore.createContentUpdater(EContentType.RoadMap) ?: return val errorCode = contentUpdater?.update( allowChargeNetwork = true, // Allow updates on charged networks listener = ProgressListener.create( onStarted = { Log.d("Update", "Update process started") }, onProgress = { progress -> Log.d("Update", "Update progress: $progress/100") }, onCompleted = { error, hint -> when (error) { GemError.NoError -> { Log.i("Update", "Update completed successfully") } GemError.InUse -> { Log.w("Update", "Update is already running") } GemError.NoDiskSpace -> { Log.e("Update", "Not enough disk space") } else -> { Log.e("Update", "Update failed: ${GemError.getMessage(error)}") } } }, onStatusChanged = { status -> Log.d("Update", "Status changed to: $status") } ) ) ``` ```java Pair result = contentStore.createContentUpdater(EContentType.RoadMap); if (result == null) return; ContentUpdater contentUpdater = result.first; int errorCode = contentUpdater.update( true, // Allow updates on charged networks ProgressListener.create( () -> { Log.d("Update", "Update process started"); }, (progress) -> { Log.d("Update", "Update progress: " + progress + "/100"); }, (error, hint) -> { if (error == GemError.NoError) { Log.i("Update", "Update completed successfully"); } else if (error == GemError.InUse) { Log.w("Update", "Update is already running"); } else if (error == GemError.NoDiskSpace) { Log.e("Update", "Not enough disk space"); } else { Log.e("Update", "Update failed: " + GemError.getMessage(error)); } }, (status) -> { Log.d("Update", "Status changed to: " + status); } ) ); ``` The `update` method accepts: * `allowChargeNetwork`: Boolean indicating whether the update can proceed on networks that may incur charges. If false, the update will only run on free networks like Wi-Fi. * `listener`: A `ProgressListener` to track the operation progress and status The method returns an `Int` error code: * `GemError.NoError` if the update process was successfully started * `GemError.InUse` if an update is already running * `GemError.NotSupported` if the update operation is not supported for the content type * `GemError.NoDiskSpace` if there is not enough space on the device * Other error codes for other failure scenarios #### Content updater status[​](#content-updater-status "Direct link to Content updater status") The `EContentUpdaterStatus` enum represents the current state of the update operation: | Enum Value | Description | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `Idle` | The update process has not started. It's the default state of the `ContentUpdater` | | `WaitConnection` | Waiting for an internet connection to proceed (Wi-Fi or mobile). | | `WaitWIFIConnection` | Waiting for a Wi-Fi connection before continuing. Used when the update was called with `allowChargeNetwork = false`. | | `CheckForUpdate` | Checking for available updates. | | `Download` | Downloading the updated content. The overall progress and the per-item progress is available. | | `FullyReady` | The update is fully downloaded and ready to be applied. Call `apply()` to complete the update. | | `PartiallyReady` | The update is partially downloaded but can still be applied. Applying now will remove outdated items that weren't downloaded. | | `DownloadRemainingContent` | Downloading any remaining content after applying a partial update. | | `DownloadPendingContent` | Downloads any pending content that has not yet been retrieved. | | `Complete` | The update process has finished successfully. | | `Error` | The update process encountered an error. | You can check the current status and progress: * Kotlin * Java ```kotlin val contentUpdater: ContentUpdater = // ... val status = contentUpdater.status val progress = contentUpdater.progress val canApply = contentUpdater.canApply() val isStarted = contentUpdater.isStarted() Log.d("Update", "Status: $status, Progress: $progress%, CanApply: $canApply") ``` ```java ContentUpdater contentUpdater = // ... EContentUpdaterStatus status = contentUpdater.getStatus(); int progress = contentUpdater.getProgress(); boolean canApply = contentUpdater.canApply(); boolean isStarted = contentUpdater.isStarted(); Log.d("Update", "Status: " + status + ", Progress: " + progress + "%, CanApply: " + canApply); ``` #### Apply the update[​](#apply-the-update "Direct link to Apply the update") Once the update status reaches `FullyReady` or `PartiallyReady`, you can apply it using the `apply()` method: * Kotlin * Java ```kotlin val contentUpdater: ContentUpdater = // ... if (contentUpdater.status == EContentUpdaterStatus.FullyReady || contentUpdater.status == EContentUpdaterStatus.PartiallyReady) { if (!contentUpdater.canApply()) { Log.e("Update", "Cannot apply update at this time") return } val applyError = contentUpdater.apply() when (applyError) { GemError.NoError -> { Log.i("Update", "Update applied successfully") } GemError.UpToDate -> { Log.i("Update", "Already up to date") } GemError.Invalidated -> { Log.e("Update", "Update was invalidated") } GemError.Io -> { Log.e("Update", "File system error during apply") } else -> { Log.e("Update", "Failed to apply: ${GemError.getMessage(applyError)}") } } } ``` ```java ContentUpdater contentUpdater = // ... if (contentUpdater.getStatus() == EContentUpdaterStatus.FullyReady || contentUpdater.getStatus() == EContentUpdaterStatus.PartiallyReady) { if (!contentUpdater.canApply()) { Log.e("Update", "Cannot apply update at this time"); return; } int applyError = contentUpdater.apply(); if (applyError == GemError.NoError) { Log.i("Update", "Update applied successfully"); } else if (applyError == GemError.UpToDate) { Log.i("Update", "Already up to date"); } else if (applyError == GemError.Invalidated) { Log.e("Update", "Update was invalidated"); } else if (applyError == GemError.Io) { Log.e("Update", "File system error during apply"); } else { Log.e("Update", "Failed to apply: " + GemError.getMessage(applyError)); } } ``` When the status is `PartiallyReady`, applying the update will: * Remove outdated items that were not fully downloaded * Restrict offline functionality to the updated content only * Continue downloading the remaining items automatically The `apply()` method returns an `Int` error code indicating the result of the operation. #### Resume a pending update[​](#resume-a-pending-update "Direct link to Resume a pending update") The ContentUpdater supports operation resume between SDK running sessions. To check if there is a pending update: * Kotlin * Java ```kotlin val contentStore = ContentStore() val (contentUpdater, creationError) = contentStore.createContentUpdater(EContentType.RoadMap) ?: return contentUpdater?.let { updater -> // Check if there's a pending update from a previous session if (updater.status != EContentUpdaterStatus.Idle) { Log.i("Update", "Found pending update with status: ${updater.status}") // Resume the update updater.update(true, progressListener) } } ``` ```java ContentStore contentStore = new ContentStore(); Pair result = contentStore.createContentUpdater(EContentType.RoadMap); if (result == null) return; ContentUpdater contentUpdater = result.first; if (contentUpdater != null) { // Check if there's a pending update from a previous session if (contentUpdater.getStatus() != EContentUpdaterStatus.Idle) { Log.i("Update", "Found pending update with status: " + contentUpdater.getStatus()); // Resume the update contentUpdater.update(true, progressListener); } } ``` #### Cancel an update[​](#cancel-an-update "Direct link to Cancel an update") You can cancel an ongoing update operation: * Kotlin * Java ```kotlin val contentUpdater: ContentUpdater = // ... contentUpdater.cancel() ``` ```java ContentUpdater contentUpdater = // ... contentUpdater.cancel(); ``` Canceling will stop the update process and return the ContentUpdater to an idle state. --- ### Positioning & Sensors The Positioning module provides powerful tools for managing location data in your app, enabling features like navigation, tracking, and location-based services. It offers flexibility by supporting both real GPS data and custom location sources, allowing for dynamic and accurate position tracking. In addition, the Recorder module complements the Positioning capabilities by enabling the recording of sensor data. With customizable settings, it allows you to tailor the recording process for various data types, such as video, audio, or sensor information. #### [📄️ Sensors and data sources](/docs/android/guides/positioning/sensors-and-data-sources.md) [This section provides an overview of how the Maps Android SDK integrates with various sensors and external data sources to enhance map functionality and interactivity. From GPS and compass data to accelerometer readings and custom telemetry inputs, the SDK is designed to support a wide range of sensor-driven scenarios.](/docs/android/guides/positioning/sensors-and-data-sources.md) #### [📄️ Get started with positioning](/docs/android/guides/positioning/get-started-positioning.md) [The Positioning module enables your application to obtain and utilize location data, serving as the foundation for features like navigation, tracking, and location-based services. This data can be sourced either from the device's GPS or from a custom data source, offering flexibility to suit diverse application needs.](/docs/android/guides/positioning/get-started-positioning.md) #### [📄️ Show location on map](/docs/android/guides/positioning/show-your-location-on-the-map.md) [The location of the device is shown by default using an arrow position tracker. If the data source has been successfully set and the required permissions were granted then the position tracker showing the current location should be visible on the map as an arrow.](/docs/android/guides/positioning/show-your-location-on-the-map.md) #### [📄️ Custom positioning](/docs/android/guides/positioning/custom-positioning.md) [The Maps SDK for Android allows setting custom data source with the PositionService to dynamically manage and simulate location data. This approach allows for external or simulated positioning data, providing flexibility beyond traditional GPS signals, and is ideal for testing or custom tracking solutions.](/docs/android/guides/positioning/custom-positioning.md) #### [📄️ Recorder](/docs/android/guides/positioning/recorder.md) [The Recorder module is a comprehensive tool designed for managing sensor data recording in the Android SDK.](/docs/android/guides/positioning/recorder.md) #### [📄️ Projections](/docs/android/guides/positioning/projections.md) [Besides the Coordinates class, the Maps SDK for Android provides a Projection abstract class that represents the base class for different geocoordinate systems such as:](/docs/android/guides/positioning/projections.md) #### [📄️ Background Location](/docs/android/guides/positioning/background-location.md) [Some use cases require location access even when the app is in the background. In such cases, you'll need to configure the Android platform appropriately. The SDK supports this scenario, but platform permissions and services must be correctly set up for it to work.](/docs/android/guides/positioning/background-location.md) --- ### Background Location |

Some use cases require location access even when the app is in the background. In such cases, you'll need to configure the Android platform appropriately. The SDK supports this scenario, but platform permissions and services must be correctly set up for it to work. We recommend enabling background location support if your application includes features like recording, navigation, or content download (especially for maps, which can be quite large and may take a while to fetch). #### Android Setup[​](#android-setup "Direct link to Android Setup") You'll need to declare permissions in your manifest, and then implement a foreground service to keep location updates alive. ###### Required AndroidManifest Changes[​](#required-androidmanifest-changes "Direct link to Required AndroidManifest Changes") Make sure you include the necessary permissions and service declarations in your `AndroidManifest.xml`. ```xml ``` Tip On Android 10+ (API level 29+), you also need to request **ACCESS\_BACKGROUND\_LOCATION** explicitly at runtime. Make sure to handle this in your app's permission request flow. danger You should create a foreground service on Android to ensure that location updates continue when the app is in the background. Failing to do so may result in the operating system terminating your app's background processes, leading to loss of location updates. ###### Foreground Service Implementation[​](#foreground-service-implementation "Direct link to Foreground Service Implementation") Android won't let you run background location tracking unless you create a foreground service. Here you can find more about foreground services: [Background Location Example](https://developer.android.com/guide/components/foreground-services). #### Background Location Configuration[​](#background-location-configuration "Direct link to Background Location Configuration") To enable background location within the Android SDK, you need to set the `AllowsBackgroundLocationUpdates` configuration key to true in your Kotlin code. * Kotlin * Java ```kotlin val dataSource = DataSourceFactory.produceLive() ?: throw IllegalStateException("Error creating data source") // Configure background location preferences val preferences = ParameterList() preferences.add(Parameter(key = ESConfigKeys.Position.AllowsBackgroundLocationUpdates, value = "true")) val err = dataSource.setPreferences(EDataType.Position, preferences) if (err != GemError.NoError) { throw IllegalStateException("Error setting data source preferences: $err") } ``` ```java DataSource dataSource = DataSourceFactory.produceLive(); if (dataSource == null) { throw new IllegalStateException("Error creating data source"); } // Configure background location preferences ParameterList preferences = new ParameterList(); preferences.add(new Parameter(ESConfigKeys.Position.AllowsBackgroundLocationUpdates, "true")); int err = dataSource.setPreferences(EDataType.Position, preferences); if (err != GemError.NoError) { throw new IllegalStateException("Error setting data source preferences: " + err); } ``` --- ### Custom positioning |

The Maps SDK for Android allows setting custom data source with the PositionService to dynamically manage and simulate location data. This approach allows for external or simulated positioning data, providing flexibility beyond traditional GPS signals, and is ideal for testing or custom tracking solutions. Tip Utilizing a custom data source eliminates the need for the previously discussed location permission management. #### Create custom data source[​](#create-custom-data-source "Direct link to Create custom data source") The following code snippet illustrates how to integrate a custom data source with the PositionService to manage and simulate location data dynamically. Instead of relying on real GPS signals, the custom data source provides flexibility by allowing external or simulated position data to be used in the application. * Kotlin * Java ```kotlin import com.magiclane.sdk.sensordatasource.* import com.magiclane.sdk.sensordatasource.enums.EDataType import java.util.Timer import kotlin.concurrent.fixedRateTimer // Create a custom data source val dataSource = DataSourceFactory.produceExternal(arrayListOf(EDataType.Position)) if (dataSource == null) { Log.e(TAG, "The datasource could not be created") return } // Positions will be provided from the data source PositionService.dataSource = dataSource // Start the data source dataSource.start() // Push the first position in the data source val firstPosition = PositionData.produce( acquisitionTimestamp = System.currentTimeMillis(), latitudeInDegrees = 48.85682, longitudeInDegrees = 2.34375, altitudeInMeters = 0.0, courseInDegrees = 0.0, speedInMpS = 0.0 ) firstPosition?.let { dataSource.pushData(it) } // Optional, usually done if we want the map to take into // account the current position mapView.startFollowingPosition() // Simulate continuous position updates var timer: Timer? = null var index = 0 val ticks = 10 timer = fixedRateTimer("positionTimer", false, 0L, 50) { // Provide latitude, longitude, heading, speed val lat = 45.0 val lon = 10.0 val head = 0.0 val speed = 0.0 // Add each position to data source val position = PositionData.produce( acquisitionTimestamp = System.currentTimeMillis(), latitudeInDegrees = lat, longitudeInDegrees = lon, altitudeInMeters = 0.0, courseInDegrees = head, speedInMpS = speed ) position?.let { dataSource.pushData(it) } index++ if (index >= ticks) { timer?.cancel() } } ``` ```java import com.magiclane.sdk.sensordatasource.*; import com.magiclane.sdk.sensordatasource.enums.EDataType; import java.util.Timer; import java.util.TimerTask; import java.util.ArrayList; import java.util.Arrays; // Create a custom data source ArrayList dataTypes = new ArrayList<>(Arrays.asList(EDataType.Position)); DataSource dataSource = DataSourceFactory.produceExternal(dataTypes); if (dataSource == null) { Log.e(TAG, "The datasource could not be created"); return; } // Positions will be provided from the data source PositionService.setDataSource(dataSource); // Start the data source dataSource.start(); // Push the first position in the data source PositionData firstPosition = PositionData.produce( System.currentTimeMillis(), 48.85682, 2.34375, 0.0, 0.0, 0.0 ); if (firstPosition != null) { dataSource.pushData(firstPosition); } // Optional, usually done if we want the map to take into // account the current position mapView.startFollowingPosition(); // Simulate continuous position updates Timer timer = new Timer(); final int[] index = {0}; int ticks = 10; timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { // Provide latitude, longitude, heading, speed double lat = 45.0; double lon = 10.0; double head = 0.0; double speed = 0.0; // Add each position to data source PositionData position = PositionData.produce( System.currentTimeMillis(), lat, lon, 0.0, head, speed ); if (position != null) { dataSource.pushData(position); } index[0]++; if (index[0] >= ticks) { timer.cancel(); } } }, 0L, 50L); ``` How It Works: * **Creating and Registering a Custom Data Source**: A custom DataSource object is created and configured to handle position data. This data source is registered with the PositionService, overriding the default GPS-based data provider. This allows the application to retrieve location updates from the custom source. * **Starting the Data Source**: The custom data source is activated by calling the `start()` method. Once started, it becomes ready to accept and process location data that is pushed into it. * **Pushing Initial Position Data**: An initial position is sent to the data source using the `pushData()` method. This data includes details such as latitude, longitude, altitude, heading, speed, and a timestamp. It acts as a starting point for tracking the location. * **Enabling Map Follow Mode**: The `startFollowingPosition()` method ensures the map camera follows the position tracker. As the custom data source provides new position updates, the map view adjusts automatically to keep the position tracker in focus. * **Updating Location Data in Real-Time**: A timer continuously generates and pushes simulated position updates to the data source at regular intervals (every 50 milliseconds). These updates include coordinates, heading, and speed. This dynamic update mechanism allows the application to simulate movement or integrate location data from custom sources, such as a mock GPS or external tracking systems. #### Improve custom data source positions[​](#improve-custom-data-source-positions "Direct link to Improve custom data source positions") While providing latitude, longitude, and timestamp may suffice for some use cases, this data may not offer sufficient accuracy, particularly during navigation. In such cases, the system might occasionally register incorrect turns or unexpected deviations. To improve precision, the `courseInDegrees` field of `PositionData` is utilized to indicate the direction of movement, which is factored into the positioning calculations. ##### Calculate heading[​](#calculate-heading "Direct link to Calculate heading") A simple function to calculate the heading knowing the current coordinate and the next coordinate is presented below: * Kotlin * Java ```kotlin import kotlin.math.* fun getHeading(from: Coordinates, to: Coordinates): Double { val dx = to.longitude - from.longitude val dy = to.latitude - from.latitude val radianToDegree = 57.2957795 var value = atan2(dx, dy) * radianToDegree if (value < 0) value += 360 return value } ``` ```java public double getHeading(Coordinates from, Coordinates to) { double dx = to.getLongitude() - from.getLongitude(); double dy = to.getLatitude() - from.getLatitude(); double radianToDegree = 57.2957795; double value = Math.atan2(dx, dy) * radianToDegree; if (value < 0) value += 360; return value; } ``` ##### Calculate speed[​](#calculate-speed "Direct link to Calculate speed") The `speedInMpS` field from the `PositionData` can be computed by dividing the distance between the two coordinates by the duration of the movement between the two coordinates. The distance can be computed using the `distance` method from the `Coordinates` class. * Kotlin * Java ```kotlin fun getSpeed(from: Coordinates, to: Coordinates, timestampAtFrom: Long, timestampAtTo: Long): Double { val timeDiff = (timestampAtTo - timestampAtFrom) / 1000.0 // Convert to seconds val distance = from.distance(to) return if (timeDiff == 0.0) { 0.0 } else { distance / timeDiff } } ``` ```java public double getSpeed(Coordinates from, Coordinates to, long timestampAtFrom, long timestampAtTo) { double timeDiff = (timestampAtTo - timestampAtFrom) / 1000.0; // Convert to seconds double distance = from.distance(to); if (timeDiff == 0.0) { return 0.0; } else { return distance / timeDiff; } } ``` If the coordinates to be pushed in the custom data source are not known, they can be extrapolated based on the previous values. #### Remove the custom datasource[​](#remove-the-custom-datasource "Direct link to Remove the custom datasource") To remove the data source once we don't need it anymore, we can proceed in the following way: * Kotlin * Java ```kotlin val dataSource = DataSourceFactory.produceExternal(arrayListOf(EDataType.Position)) if (dataSource == null) { Log.e(TAG, "The datasource could not be created") return } PositionService.dataSource = dataSource dataSource.start() // Do something with the data source... // Stop the data source dataSource.stop() // Remove the data source from the position service PositionService.dataSource = null ``` ```java ArrayList dataTypes = new ArrayList<>(Arrays.asList(EDataType.Position)); DataSource dataSource = DataSourceFactory.produceExternal(dataTypes); if (dataSource == null) { Log.e(TAG, "The datasource could not be created"); return; } PositionService.setDataSource(dataSource); dataSource.start(); // Do something with the data source... // Stop the data source dataSource.stop(); // Remove the data source from the position service PositionService.setDataSource(null); ``` danger It's important to stop the data source and remove it from the position service once work is finished with it. Otherwise there can be unexpected problems especially when trying to use other data sources (live or custom). #### Create a simulation data source[​](#create-a-simulation-data-source "Direct link to Create a simulation data source") The Magic Lane SDK allows you to create simulation data source from existing route data. This simulates the device moving along a predefined route. * Kotlin * Java ```kotlin val route = // ... obtain a route from routing service val dataSource = DataSourceFactory.produceSimulation(route) if (dataSource == null) { Log.e(TAG, "The simulation datasource could not be created") return } dataSource.start() PositionService.dataSource = dataSource ``` ```java Route route = // ... obtain a route from routing service DataSource dataSource = DataSourceFactory.produceSimulation(route); if (dataSource == null) { Log.e(TAG, "The simulation datasource could not be created"); return; } dataSource.start(); PositionService.setDataSource(dataSource); ``` How It Works: * **Creating and Registering a Custom Data Source**: A custom DataSource object is created and configured to handle position data. This data source is registered with the PositionService, overriding the default GPS-based data provider. This allows the application to retrieve location updates from the custom source. * **Starting the Data Source**: The custom data source is activated by calling the start() method. Once started, it starts simulating the given route. #### Remove the simulation datasource[​](#remove-the-simulation-datasource "Direct link to Remove the simulation datasource") To remove the simulation data source once we don't need it anymore, we can proceed in the following way: * Kotlin * Java ```kotlin val route = // ... obtain a route from routing service val dataSource = DataSourceFactory.produceSimulation(route) if (dataSource == null) { Log.e(TAG, "The simulation datasource could not be created") return } dataSource.start() PositionService.dataSource = dataSource // Do something with the data source... // Stop the data source dataSource.stop() // Remove the data source from the position service PositionService.dataSource = null ``` ```java Route route = // ... obtain a route from routing service DataSource dataSource = DataSourceFactory.produceSimulation(route); if (dataSource == null) { Log.e(TAG, "The simulation datasource could not be created"); return; } dataSource.start(); PositionService.setDataSource(dataSource); // Do something with the data source... // Stop the data source dataSource.stop(); // Remove the data source from the position service PositionService.setDataSource(null); ``` danger It's important to stop the data source and remove it from the position service once work is finished with it. Otherwise there can be unexpected problems especially when trying to use other data sources (live or custom). #### Create log data source[​](#create-log-data-source "Direct link to Create log data source") The following code snippet illustrates how to integrate a log data source with the PositionService to manage and simulate location data dynamically. Instead of relying on real GPS signals, the log data source provides flexibility by replaying a given log file. This enables the application to replay location data from a stable and predefined log, ensuring uniformity across different runs. * Kotlin * Java ```kotlin // Create a log data source val dataSource = DataSourceFactory.produceLog("path/to/logfile.gpx") if (dataSource == null) { Log.e(TAG, "The log datasource could not be created") return } // Start the data source dataSource.start() // Positions will be provided from the data source PositionService.dataSource = dataSource // Optionally, you can control playback if the data source supports it dataSource.playback?.let { playback -> playback.speedMultiplier = 2.0f // Play at 2x speed playback.resume() } ``` ```java // Create a log data source DataSource dataSource = DataSourceFactory.produceLog("path/to/logfile.gpx"); if (dataSource == null) { Log.e(TAG, "The log datasource could not be created"); return; } // Start the data source dataSource.start(); // Positions will be provided from the data source PositionService.setDataSource(dataSource); // Optionally, you can control playback if the data source supports it Playback playback = dataSource.getPlayback(); if (playback != null) { playback.setSpeedMultiplier(2.0f); // Play at 2x speed playback.resume(); } ``` info Log data sources need to be explicitly started with `dataSource.start()`. You can also control playback speed and state through the `playback` interface if supported. How It Works: * **Creating and Registering a Log Data Source**: A log DataSource object is created from a file path and configured to handle position data from the log file. This data source is registered with the PositionService, overriding the default GPS-based data provider. * **Automatic Playback**: The log data source automatically starts playing back the recorded data once it's created and registered, providing consistent position updates based on the log file content. #### Remove the log datasource[​](#remove-the-log-datasource "Direct link to Remove the log datasource") To remove the data source once we don't need it anymore, we can proceed in the following way: * Kotlin * Java ```kotlin val dataSource = DataSourceFactory.produceLog("path/to/logfile.gpx") if (dataSource == null) { Log.e(TAG, "The log datasource could not be created") return } PositionService.dataSource = dataSource // Do something with the data source... // Stop the data source dataSource.stop() // Remove the data source from the position service PositionService.dataSource = null ``` ```java DataSource dataSource = DataSourceFactory.produceLog("path/to/logfile.gpx"); if (dataSource == null) { Log.e(TAG, "The log datasource could not be created"); return; } PositionService.setDataSource(dataSource); // Do something with the data source... // Stop the data source dataSource.stop(); // Remove the data source from the position service PositionService.setDataSource(null); ``` danger The log data source cannot be of type `.gm`. Using this file type is not supported in the public SDK. You can use another format, such as `gpx`, `nmea` or `kml`, that can be exported from the `.gm` file. danger It's important to stop the data source and remove it from the position service once work is finished with it. Otherwise there can be unexpected problems especially when trying to use other data sources (live or custom). --- ### Get started with positioning |

The Positioning module enables your application to obtain and utilize location data, serving as the foundation for features like navigation, tracking, and location-based services. This data can be sourced either from the device's GPS or from a custom data source, offering flexibility to suit diverse application needs. Using the Positioning module, you can: * Leverage real GPS data: Obtain highly accurate, real-time location updates directly from the device's built-in GPS sensor. * Integrate custom location data: Configure the module to use location data provided by external sources, such as mock services or specialized hardware. In the following sections, you will learn how to grant the necessary location permissions for your app, set up live data sources, and manage location updates effectively. Additionally, we will explore how to customize the position tracker to align with your application's design and functionality requirements. This comprehensive guide will help you integrate robust and flexible positioning capabilities into your app. #### Granting permissions[​](#granting-permissions "Direct link to Granting permissions") ##### Add application location permissions[​](#add-application-location-permissions "Direct link to Add application location permissions") Location permissions must be configured in your Android application. In order to give the application the location permission on Android, you need to alter the `app/src/main/AndroidManifest.xml` and add the following permissions within the `` block: ```xml ``` More information about permissions can be found in the [Android Manifest documentation](https://developer.android.com/reference/android/Manifest.permission). ##### Get user consent[​](#get-user-consent "Direct link to Get user consent") Defining location permissions in the application, as explained in the previous section, is a mandatory step. * Kotlin * Java ```kotlin // Check if location permissions are granted if (ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED ) { // After the permission was granted, we can set the live data source (in most cases the GPS). // The data source should be set only once, otherwise we'll get an error. SdkCall.execute { val liveDataSource = DataSourceFactory.produceLive() liveDataSource?.let { PositionService.dataSource = it Log.d(TAG, "Live data source set successfully") } ?: run { Log.e(TAG, "Failed to create live data source") } } } else { // Request location permission ActivityCompat.requestPermissions( this, arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION ), LOCATION_PERMISSION_REQUEST_CODE ) } ``` ```java // Check if location permissions are granted if (ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED ) { // After the permission was granted, we can set the live data source (in most cases the GPS). // The data source should be set only once, otherwise we'll get an error. SdkCall.execute(() -> { DataSource liveDataSource = DataSourceFactory.produceLive(); if (liveDataSource != null) { PositionService.setDataSource(liveDataSource); Log.d(TAG, "Live data source set successfully"); } else { Log.e(TAG, "Failed to create live data source"); } }); } else { // Request location permission ActivityCompat.requestPermissions( this, new String[]{ Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION }, LOCATION_PERMISSION_REQUEST_CODE ); } ``` Handle the permission request result in your activity: * Kotlin * Java ```kotlin override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) when (requestCode) { LOCATION_PERMISSION_REQUEST_CODE -> { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Permission granted, set up live data source SdkCall.execute { val liveDataSource = DataSourceFactory.produceLive() liveDataSource?.let { PositionService.dataSource = it Log.d(TAG, "Live data source set successfully") } } } else { // Permission denied Log.w(TAG, "Location permission denied") } } } } ``` ```java @Override public void onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode) { case LOCATION_PERMISSION_REQUEST_CODE: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Permission granted, set up live data source SdkCall.execute(() -> { DataSource liveDataSource = DataSourceFactory.produceLive(); if (liveDataSource != null) { PositionService.setDataSource(liveDataSource); Log.d(TAG, "Live data source set successfully"); } }); } else { // Permission denied Log.w(TAG, "Location permission denied"); } break; } } ``` In the previous code, we request the location permissions. If the user grants them, we notify the engine to use real GPS positions for navigation by creating a live data source and setting it on the `PositionService`. Alternatively, you can use `PermissionHelper` from the SDK utility package to simplify permission handling: * Kotlin * Java ```kotlin SdkCall.execute { val hasLocationPermission = PermissionsHelper.hasPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) if (hasLocationPermission) SdkCall.execute { val liveDataSource = DataSourceFactory.produceLive() liveDataSource?.let { PositionService.dataSource = it Log.d(TAG, "Live data source set successfully") } } else requestPermissions(this) } ``` ```java SdkCall.execute(() -> { boolean hasLocationPermission = PermissionsHelper.hasPermission(this, Manifest.permission.ACCESS_FINE_LOCATION); if (hasLocationPermission) { SdkCall.execute(() -> { DataSource liveDataSource = DataSourceFactory.produceLive(); if (liveDataSource != null) { PositionService.setDataSource(liveDataSource); Log.d(TAG, "Live data source set successfully"); } }); } else { requestPermissions(this); } }); ``` If the live data source is set and the right permissions are granted, the position cursor should be visible on the map as an arrow. Tip For debugging purposes, the Android Emulator's [extended controls](https://developer.android.com/studio/run/emulator-extended-controls) can be used to mock the current position. On a real device, apps such as [Mock Locations](https://play.google.com/store/apps/details?id=ru.gavrikov.mocklocations) can be utilized to simulate location data. Don't forget to add the necessary import statements and member declarations to your activity: * Kotlin * Java ```kotlin import android.Manifest import android.content.pm.PackageManager import android.util.Log import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.magiclane.sdk.sensordatasource.DataSourceFactory import com.magiclane.sdk.sensordatasource.PositionService import com.magiclane.sdk.util.SdkCall class YourActivity : AppCompatActivity() { // ... companion object { private const val LOCATION_PERMISSION_REQUEST_CODE = 1001 private const val TAG = "PositioningExample" } } ``` ```java import android.Manifest; import android.content.pm.PackageManager; import android.util.Log; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import com.magiclane.sdk.sensordatasource.DataSourceFactory; import com.magiclane.sdk.sensordatasource.PositionService; import com.magiclane.sdk.util.SdkCall; public class YourActivity extends AppCompatActivity { // ... private static final int LOCATION_PERMISSION_REQUEST_CODE = 1001; private static final String TAG = "PositioningExample"; } ``` #### Receive location updates[​](#receive-location-updates "Direct link to Receive location updates") To receive updates about changes in the current position, we can register a callback function using the `PositionListener`. The given listener will be called continuously as new updates about current position are made. Tip Consult the [Positions guide](/docs/android/guides/core/positions.md) for more information about the `PositionData` class and the differences between raw positions and map matched position. ##### Raw positions[​](#raw-positions "Direct link to Raw positions") To listen for raw position updates (as they are pushed to the data source or received via the sensors), you can add a `PositionListener` for the `EDataType.Position`: * Kotlin * Java ```kotlin val positionListener = PositionListener { position: PositionData -> // Process the position if (position.isValid()) { val coordinates = position.coordinates println("New raw position: $coordinates") } } // Add the listener PositionService.addListener(positionListener, EDataType.Position) ``` ```java PositionListener positionListener = (PositionData position) -> { // Process the position if (position.isValid()) { Coordinates coordinates = position.getCoordinates(); System.out.println("New raw position: " + coordinates); } }; // Add the listener PositionService.addListener(positionListener, EDataType.Position); ``` ##### Map matched positions[​](#map-matched-positions "Direct link to Map matched positions") To register a callback to receive map matched positions, use `EDataType.ImprovedPosition`: * Kotlin * Java ```kotlin val improvedPositionListener = PositionListener { position: PositionData -> if (position.isValid() && position is ImprovedPositionData) { // Current coordinates val coordinates = position.coordinates println("New improved position: $coordinates") // Speed in m/s (-1 if not available) val speed = position.speed println("Speed: $speed m/s") // Speed limit in m/s on the current road (0 if not available) val speedLimit = position.roadSpeedLimit println("Speed limit: $speedLimit m/s") // Heading angle in degrees (N=0, E=90, S=180, W=270, -1 if not available) val course = position.course println("Course: $course degrees") // Information about current road (if it is in a tunnel, bridge, ramp, one way, etc.) val roadModifiers = position.roadModifier println("Road modifiers: $roadModifiers") // Quality of the current position val fixQuality = position.fixQuality println("Fix quality: $fixQuality") // Horizontal and vertical accuracy in meters val horizontalAccuracy = position.horizontalAccuracy val verticalAccuracy = position.verticalAccuracy println("Accuracy - H: $horizontalAccuracy, V: $verticalAccuracy") // Road address information (if available) if (position.hasRoadLocalization()) { val roadAddress = position.roadAddress println("Road address: $roadAddress") } } } // Add the listener PositionService.addListener(improvedPositionListener, EDataType.ImprovedPosition) ``` ```java PositionListener improvedPositionListener = (PositionData position) -> { if (position.isValid() && position instanceof ImprovedPositionData) { ImprovedPositionData improvedPosition = (ImprovedPositionData) position; // Current coordinates Coordinates coordinates = improvedPosition.getCoordinates(); System.out.println("New improved position: " + coordinates); // Speed in m/s (-1 if not available) double speed = improvedPosition.getSpeed(); System.out.println("Speed: " + speed + " m/s"); // Speed limit in m/s on the current road (0 if not available) double speedLimit = improvedPosition.getRoadSpeedLimit(); System.out.println("Speed limit: " + speedLimit + " m/s"); // Heading angle in degrees (N=0, E=90, S=180, W=270, -1 if not available) double course = improvedPosition.getCourse(); System.out.println("Course: " + course + " degrees"); // Information about current road (if it is in a tunnel, bridge, ramp, one way, etc.) int roadModifiers = improvedPosition.getRoadModifier(); System.out.println("Road modifiers: " + roadModifiers); // Quality of the current position int fixQuality = improvedPosition.getFixQuality(); System.out.println("Fix quality: " + fixQuality); // Horizontal and vertical accuracy in meters double horizontalAccuracy = improvedPosition.getHorizontalAccuracy(); double verticalAccuracy = improvedPosition.getVerticalAccuracy(); System.out.println("Accuracy - H: " + horizontalAccuracy + ", V: " + verticalAccuracy); // Road address information (if available) if (improvedPosition.hasRoadLocalization()) { String roadAddress = improvedPosition.getRoadAddress(); System.out.println("Road address: " + roadAddress); } } }; // Add the listener PositionService.addListener(improvedPositionListener, EDataType.ImprovedPosition); ``` Remember to remove listeners when they are no longer needed: * Kotlin * Java ```kotlin // Remove specific listener PositionService.removeListener(positionListener) PositionService.removeListener(improvedPositionListener) ``` ```java // Remove specific listener PositionService.removeListener(positionListener); PositionService.removeListener(improvedPositionListener); ``` info During simulation, the positions provided through the `PositionListener` methods correspond to the simulated locations generated as part of the navigation simulation process. #### Get current location[​](#get-current-location "Direct link to Get current location") To retrieve the current location, we can use the `position` and `improvedPosition` properties from the `PositionService` object. These properties return a `PositionData` object containing the latest location information or `null` if no position data is available. This method is useful for accessing the most recent position data without registering for continuous updates. * Kotlin * Java ```kotlin SdkCall.execute { val position = PositionService.position if (position == null) { Log.w(TAG, "No position available") } else { Log.d(TAG, "Position: ${position.coordinates}") } } ``` ```java SdkCall.execute(() -> { PositionData position = PositionService.getPosition(); if (position == null) { Log.w(TAG, "No position available"); } else { Log.d(TAG, "Position: " + position.getCoordinates()); } }); ``` For map-matched positions, use the `improvedPosition` property: * Kotlin * Java ```kotlin SdkCall.execute { val improvedPosition = PositionService.improvedPosition if (improvedPosition == null) { Log.w(TAG, "No improved position available") } else { Log.d(TAG, "Improved position: ${improvedPosition.coordinates}") // Access additional improved position data if (improvedPosition is ImprovedPositionData) { if (improvedPosition.hasRoadLocalization()) { val roadAddress = improvedPosition.roadAddress println("Current road: $roadAddress") } val speedLimit = improvedPosition.roadSpeedLimit println("Speed limit: $speedLimit m/s") } } } ``` ```java SdkCall.execute(() -> { PositionData improvedPosition = PositionService.getImprovedPosition(); if (improvedPosition == null) { Log.w(TAG, "No improved position available"); } else { Log.d(TAG, "Improved position: " + improvedPosition.getCoordinates()); // Access additional improved position data if (improvedPosition instanceof ImprovedPositionData) { ImprovedPositionData improved = (ImprovedPositionData) improvedPosition; if (improved.hasRoadLocalization()) { String roadAddress = improved.getRoadAddress(); System.out.println("Current road: " + roadAddress); } double speedLimit = improved.getRoadSpeedLimit(); System.out.println("Speed limit: " + speedLimit + " m/s"); } } }); ``` You can also use the convenience methods to get coordinates directly: * Kotlin * Java ```kotlin SdkCall.execute { val currentCoords = PositionService.getCurrentPosition() val improvedCoords = PositionService.getCurrentImprovedPosition() currentCoords?.let { println("Current coordinates: $it") } improvedCoords?.let { println("Improved coordinates: $it") } } ``` ```java SdkCall.execute(() -> { Coordinates currentCoords = PositionService.getCurrentPosition(); Coordinates improvedCoords = PositionService.getCurrentImprovedPosition(); if (currentCoords != null) { System.out.println("Current coordinates: " + currentCoords); } if (improvedCoords != null) { System.out.println("Improved coordinates: " + improvedCoords); } }); ``` --- ### Projections |

Besides the `Coordinates` class, the Maps SDK for Android provides a `Projection` abstract class that represents the base class for different geocoordinate systems such as: * `ProjectionWGS84` (World Geodetic System 1984) * `ProjectionGK` (Gauss-Kruger) * `ProjectionUTM` (Universal Transverse Mercator) * `ProjectionLAM` (Lambert) * `ProjectionBNG` (British National Grid) * `ProjectionMGRS` (Military Grid Reference System) * `ProjectionW3W` (What three words) To work with projections, you'll need to import the following classes: * Kotlin * Java ```kotlin import com.magiclane.sdk.places.Coordinates import com.magiclane.sdk.projection.* import com.magiclane.sdk.core.ProgressListener import com.magiclane.sdk.core.GemError ``` ```java import com.magiclane.sdk.places.Coordinates; import com.magiclane.sdk.projection.*; import com.magiclane.sdk.core.ProgressListener; import com.magiclane.sdk.core.GemError; ``` To know the type of the `Projection` you can use the `type` property: * Kotlin * Java ```kotlin val type = projection.type ``` ```java EProjectionType type = projection.getType(); ``` #### ProjectionWGS84[​](#projectionwgs84 "Direct link to ProjectionWGS84") The `ProjectionWGS84` projection is a widely used geodetic datum that serves as the foundation for GPS and other mapping systems. It provides a standard reference frame for the Earth's surface, allowing for accurate positioning and navigation. A `ProjectionWGS84` projection can be instantiated using a `Coordinates` object: * Kotlin * Java ```kotlin val obj = ProjectionWGS84(Coordinates(latitude = 5.0, longitude = 5.0)) ``` ```java ProjectionWGS84 obj = new ProjectionWGS84(new Coordinates(5.0, 5.0)); ``` Then, the coordinates can be accessed and set using the `coordinates` property: * Kotlin * Java ```kotlin val coordinates = obj.coordinates // Get coordinates obj.coordinates = Coordinates(latitude = 10.0, longitude = 10.0) // Set coordinates ``` ```java Coordinates coordinates = obj.getCoordinates(); // Get coordinates obj.setCoordinates(new Coordinates(10.0, 10.0)); // Set coordinates ``` #### ProjectionGK[​](#projectiongk "Direct link to ProjectionGK") The `ProjectionGK` (Gauss-Kruger) projection is a cylindrical map projection that is commonly used for large-scale mapping in regions with a north-south orientation. It divides the Earth into zones, each with its own coordinate system, allowing for accurate representation of geographic features. A `ProjectionGK` projection can be instantiated using the following constructor: * Kotlin * Java ```kotlin val obj = ProjectionGK(x = 6325113.72, y = 5082540.66, zone = 1) ``` ```java ProjectionGK obj = new ProjectionGK(6325113.72, 5082540.66, 1); ``` In order to obtain the x, y and zone values, the `getEasting()`, `getNorthing()` and `getZone()` methods can be used, while setting them can be done using the `set()` method: * Kotlin * Java ```kotlin val obj = ProjectionGK(x = 6325113.72, y = 5082540.66, zone = 1) val type = obj.type // EProjectionType.EPR_Gk val zone = obj.getZone() // 1 val easting = obj.getEasting() // 6325113.72 val northing = obj.getNorthing() // 5082540.66 obj.set(x = 1.0, y = 1.0, zone = 2) val newZone = obj.getZone() // 2 val newEasting = obj.getEasting() // 1.0 val newNorthing = obj.getNorthing() // 1.0 ``` ```java ProjectionGK obj = new ProjectionGK(6325113.72, 5082540.66, 1); EProjectionType type = obj.getType(); // EProjectionType.EPR_Gk int zone = obj.getZone(); // 1 double easting = obj.getEasting(); // 6325113.72 double northing = obj.getNorthing(); // 5082540.66 obj.set(1.0, 1.0, 2); int newZone = obj.getZone(); // 2 double newEasting = obj.getEasting(); // 1.0 double newNorthing = obj.getNorthing(); // 1.0 ``` danger The `ProjectionGK` projection is currently supported only for countries that use **Bessel ellipsoid**. Trying to convert to and from `ProjectionGK` projection for other countries will result in a `GemError.notSupported` error. #### ProjectionBNG[​](#projectionbng "Direct link to ProjectionBNG") The `ProjectionBNG` (British National Grid) projection is a coordinate system used in Great Britain for mapping and navigation. It provides a grid reference system that allows for precise location identification within the country. A `ProjectionBNG` projection can be instantiated using the following constructor: * Kotlin * Java ```kotlin val obj = ProjectionBNG(easting = 500000.0, northing = 4649776.0) ``` ```java ProjectionBNG obj = new ProjectionBNG(500000.0, 4649776.0); ``` Alternatively, you can create a `ProjectionBNG` using a grid reference string: * Kotlin * Java ```kotlin val obj = ProjectionBNG("TQ123456") ``` ```java ProjectionBNG obj = new ProjectionBNG("TQ123456"); ``` In order to obtain the easting and northing values, the `getEasting()` and `getNorthing()` methods can be used, while setting them can be done using the `set()` method. You can also access the grid reference using the `gridReference` property: * Kotlin * Java ```kotlin val obj = ProjectionBNG(easting = 6325113.72, northing = 5082540.66) obj.set(easting = 1.0, northing = 1.0) val type = obj.type // EProjectionType.EPR_Bng val newEasting = obj.getEasting() // 1.0 val newNorthing = obj.getNorthing() // 1.0 val gridRef = obj.gridReference // Grid reference string ``` ```java ProjectionBNG obj = new ProjectionBNG(6325113.72, 5082540.66); obj.set(1.0, 1.0); EProjectionType type = obj.getType(); // EProjectionType.EPR_Bng double newEasting = obj.getEasting(); // 1.0 double newNorthing = obj.getNorthing(); // 1.0 String gridRef = obj.getGridReference(); // Grid reference string ``` #### ProjectionMGRS[​](#projectionmgrs "Direct link to ProjectionMGRS") The `ProjectionMGRS` (Military Grid Reference System) projection is a coordinate system used by the military for precise location identification. It combines the UTM and UPS coordinate systems to provide a grid reference system that is easy to use in the field. A `ProjectionMGRS` projection can be instantiated using the following constructor: * Kotlin * Java ```kotlin val obj = ProjectionMGRS(easting = 99316, northing = 10163, zone = "30U", letters = "XC") ``` ```java ProjectionMGRS obj = new ProjectionMGRS(99316, 10163, "30U", "XC"); ``` In order to obtain the easting, northing, zone and letters values, the `getEasting()`, `getNorthing()`, `getZone()` and `getSq100kIdentifier()` methods can be used, while setting them can be done using the `set()` method: * Kotlin * Java ```kotlin val obj = ProjectionMGRS( easting = 6325113, northing = 5082540, zone = "A", letters = "letters") obj.set( easting = 1, northing = 1, zone = "B", letters = "newLetters") val type = obj.type // EProjectionType.EPR_Mgrs val newZone = obj.getZone() // "B" val newEasting = obj.getEasting() // 1 val newNorthing = obj.getNorthing() // 1 val newLetters = obj.getSq100kIdentifier() // "newLetters" ``` ```java ProjectionMGRS obj = new ProjectionMGRS( 6325113, 5082540, "A", "letters"); obj.set( 1, 1, "B", "newLetters"); EProjectionType type = obj.getType(); // EProjectionType.EPR_Mgrs String newZone = obj.getZone(); // "B" int newEasting = obj.getEasting(); // 1 int newNorthing = obj.getNorthing(); // 1 String newLetters = obj.getSq100kIdentifier(); // "newLetters" ``` #### ProjectionW3W[​](#projectionw3w "Direct link to ProjectionW3W") The `ProjectionW3W` (What three words) projection is a geocoding system that divides the world into a grid of 3m x 3m squares, each identified by a unique combination of three words. This system provides a simple and memorable way to reference specific locations. A `ProjectionW3W` projection can be instantiated using the following constructor: * Kotlin * Java ```kotlin val obj = ProjectionW3W("your-api-token") ``` ```java ProjectionW3W obj = new ProjectionW3W("your-api-token"); ``` In order to obtain and set the token and words values, the `getToken()`, `setToken()`, `getWords()` and `setWords()` methods can be used: * Kotlin * Java ```kotlin val obj = ProjectionW3W() obj.setToken("your-api-token") obj.setWords("index.home.raft") val token = obj.getToken() val words = obj.getWords() ``` ```java ProjectionW3W obj = new ProjectionW3W(); obj.setToken("your-api-token"); obj.setWords("index.home.raft"); String token = obj.getToken(); String words = obj.getWords(); ``` #### ProjectionLAM[​](#projectionlam "Direct link to ProjectionLAM") The `ProjectionLAM` (Lambert) projection is a conic map projection that is commonly used for large-scale mapping in regions with an east-west orientation. It provides a way to represent geographic features accurately while minimizing distortion. A `ProjectionLAM` projection can be instantiated using the following constructor: * Kotlin * Java ```kotlin val obj = ProjectionLAM(x = 6325113.72, y = 5082540.66) ``` ```java ProjectionLAM obj = new ProjectionLAM(6325113.72, 5082540.66); ``` In order to obtain the x and y values, the `getX()` and `getY()` methods can be used, while setting them can be done using the `set()` method: * Kotlin * Java ```kotlin val obj = ProjectionLAM(x = 6325113.72, y = 5082540.66) obj.set(x = 1.0, y = 1.0) val type = obj.type // EProjectionType.EPR_Lam val newX = obj.getX() // 1.0 val newY = obj.getY() // 1.0 ``` ```java ProjectionLAM obj = new ProjectionLAM(6325113.72, 5082540.66); obj.set(1.0, 1.0); EProjectionType type = obj.getType(); // EProjectionType.EPR_Lam double newX = obj.getX(); // 1.0 double newY = obj.getY(); // 1.0 ``` #### ProjectionUTM[​](#projectionutm "Direct link to ProjectionUTM") The `ProjectionUTM` (Universal Transverse Mercator) projection is a global map projection that divides the world into a series of zones, each with its own coordinate system. It provides a way to represent geographic features accurately while minimizing distortion. A `ProjectionUTM` projection can be instantiated using the following constructor: * Kotlin * Java ```kotlin val obj = ProjectionUTM(x = 6325113.72, y = 5082540.66, zone = 1, hemisphere = EHemisphere.HEM_South) ``` ```java ProjectionUTM obj = new ProjectionUTM(6325113.72, 5082540.66, 1, EHemisphere.HEM_South); ``` In order to obtain the x, y, zone and hemisphere values, the `getX()`, `getY()`, `getZone()` and `getHemisphere()` methods can be used, while setting them can be done using the `set()` method: * Kotlin * Java ```kotlin val obj = ProjectionUTM(x = 6325113.72, y = 5082540.66, zone = 1, hemisphere = EHemisphere.HEM_South) obj.set(x = 1.0, y = 1.0, zone = 2, hemisphere = EHemisphere.HEM_North.value) val type = obj.type // EProjectionType.EPR_Utm val newZone = obj.getZone() // 2 val newX = obj.getX() // 1.0 val newY = obj.getY() // 1.0 val newHemisphere = EHemisphere.entries[obj.getHemisphere()] // EHemisphere.HEM_North ``` ```java ProjectionUTM obj = new ProjectionUTM(6325113.72, 5082540.66, 1, EHemisphere.HEM_South); obj.set(1.0, 1.0, 2, EHemisphere.HEM_North.getValue()); EProjectionType type = obj.getType(); // EProjectionType.EPR_Utm int newZone = obj.getZone(); // 2 double newX = obj.getX(); // 1.0 double newY = obj.getY(); // 1.0 EHemisphere newHemisphere = EHemisphere.values()[obj.getHemisphere()]; // EHemisphere.HEM_North ``` #### Projection Service[​](#projection-service "Direct link to Projection Service") The `ProjectionService` object provides a method to convert between different projection types. It allows you to transform coordinates from one projection to another, making it easier to work with various geospatial data formats. The class is an object and features a static `convert` method: * Kotlin * Java ```kotlin val from = ProjectionWGS84(Coordinates(latitude = 51.5074, longitude = -0.1278)) val to = ProjectionMGRS() val progressListener = ProgressListener.create( onCompleted = { errorCode, _ -> if (errorCode == GemError.NoError) { // Conversion successful val easting = to.getEasting() // 99316 val northing = to.getNorthing() // 10163 val zone = to.getZone() // "30U" val letters = to.getSq100kIdentifier() // "XC" } else { // Handle conversion error println("Conversion failed with error: $errorCode") } } ) // Start the conversion ProjectionService.convert(from, to, progressListener) ``` ```java ProjectionWGS84 from = new ProjectionWGS84(new Coordinates(51.5074, -0.1278)); ProjectionMGRS to = new ProjectionMGRS(); ProgressListener progressListener = ProgressListener.create( (errorCode, hint) -> { if (errorCode == GemError.NoError) { // Conversion successful int easting = to.getEasting(); // 99316 int northing = to.getNorthing(); // 10163 String zone = to.getZone(); // "30U" String letters = to.getSq100kIdentifier(); // "XC" } else { // Handle conversion error System.out.println("Conversion failed with error: " + errorCode); } } ); // Start the conversion ProjectionService.convert(from, to, progressListener); ``` danger ProjectionService.convert works with `ProjectionW3W` only if the `ProjectionW3W` object has a **valid** token that can be obtained from [what3words.com](https://developer.what3words.com/public-api). If the token is not set, the conversion will fail and the `GemError.NotSupported` error will be returned via the progress listener. #### Practical Example[​](#practical-example "Direct link to Practical Example") Here's a practical example showing how to convert a WGS84 coordinate to multiple projection systems, similar to the approach used in the SDK's sample applications: * Kotlin * Java ```kotlin // Starting coordinates val wgs84Projection = ProjectionWGS84(Coordinates(latitude = 51.5074, longitude = -0.1278)) // Convert to different projection types val projectionTypes = listOf( EProjectionType.EPR_Mgrs, EProjectionType.EPR_Utm, EProjectionType.EPR_Bng, EProjectionType.EPR_Lam, EProjectionType.EPR_Gk ) for (projectionType in projectionTypes) { val targetProjection: Projection = when (projectionType) { EProjectionType.EPR_Mgrs -> ProjectionMGRS() EProjectionType.EPR_Utm -> ProjectionUTM() EProjectionType.EPR_Bng -> ProjectionBNG() EProjectionType.EPR_Lam -> ProjectionLAM() EProjectionType.EPR_Gk -> ProjectionGK() EProjectionType.EPR_WhatThreeWords -> { ProjectionW3W().also { it.setToken("your-valid-w3w-token") } } else -> continue } val progressListener = ProgressListener.create( onCompleted = { errorCode, _ -> if (errorCode == GemError.NoError) { // Handle successful conversion when (targetProjection) { is ProjectionMGRS -> { println("MGRS: ${targetProjection.getEasting()}, ${targetProjection.getNorthing()}") println("Zone: ${targetProjection.getZone()}, Letters: ${targetProjection.getSq100kIdentifier()}") } is ProjectionUTM -> { println("UTM: ${targetProjection.getX()}, ${targetProjection.getY()}") println("Zone: ${targetProjection.getZone()}, Hemisphere: ${EHemisphere.entries[targetProjection.getHemisphere()]}") } is ProjectionBNG -> { println("BNG: ${targetProjection.getEasting()}, ${targetProjection.getNorthing()}") println("Grid Reference: ${targetProjection.gridReference}") } // Add other projection types as needed } } else { println("Conversion to ${projectionType} failed with error: $errorCode") } } ) ProjectionService.convert(wgs84Projection, targetProjection, progressListener) } ``` ```java // Starting coordinates ProjectionWGS84 wgs84Projection = new ProjectionWGS84(new Coordinates(51.5074, -0.1278)); // Convert to different projection types EProjectionType[] projectionTypes = { EProjectionType.EPR_Mgrs, EProjectionType.EPR_Utm, EProjectionType.EPR_Bng, EProjectionType.EPR_Lam, EProjectionType.EPR_Gk }; for (EProjectionType projectionType : projectionTypes) { Projection targetProjection; switch (projectionType) { case EPR_Mgrs: targetProjection = new ProjectionMGRS(); break; case EPR_Utm: targetProjection = new ProjectionUTM(); break; case EPR_Bng: targetProjection = new ProjectionBNG(); break; case EPR_Lam: targetProjection = new ProjectionLAM(); break; case EPR_Gk: targetProjection = new ProjectionGK(); break; case EPR_WhatThreeWords: ProjectionW3W w3w = new ProjectionW3W(); w3w.setToken("your-valid-w3w-token"); targetProjection = w3w; break; default: continue; } Projection finalTarget = targetProjection; ProgressListener progressListener = ProgressListener.create( (errorCode, hint) -> { if (errorCode == GemError.NoError) { // Handle successful conversion if (finalTarget instanceof ProjectionMGRS) { ProjectionMGRS mgrs = (ProjectionMGRS) finalTarget; System.out.println("MGRS: " + mgrs.getEasting() + ", " + mgrs.getNorthing()); System.out.println("Zone: " + mgrs.getZone() + ", Letters: " + mgrs.getSq100kIdentifier()); } else if (finalTarget instanceof ProjectionUTM) { ProjectionUTM utm = (ProjectionUTM) finalTarget; System.out.println("UTM: " + utm.getX() + ", " + utm.getY()); System.out.println("Zone: " + utm.getZone() + ", Hemisphere: " + EHemisphere.values()[utm.getHemisphere()]); } else if (finalTarget instanceof ProjectionBNG) { ProjectionBNG bng = (ProjectionBNG) finalTarget; System.out.println("BNG: " + bng.getEasting() + ", " + bng.getNorthing()); System.out.println("Grid Reference: " + bng.getGridReference()); } // Add other projection types as needed } else { System.out.println("Conversion to " + projectionType + " failed with error: " + errorCode); } } ); ProjectionService.convert(wgs84Projection, targetProjection, progressListener); } ``` This example demonstrates how to convert a single WGS84 coordinate to multiple projection systems and extract the relevant information from each converted projection. --- ### Recorder |

The **Recorder** module is a comprehensive tool designed for managing sensor data recording in the Android SDK. The Recorder supports a wide range of configurable parameters through the `RecorderConfiguration`, allowing developers to tailor recording behavior to specific application needs. Features include: * **Customizable storage options**: Define log directories, manage disk space, and specify the duration of recordings. * **Data type selection**: Specify which data types (e.g., video, audio, or sensor data) to record. * **Video and audio options**: Set video resolution, enable/disable audio recording, and manage chunk durations. It's possible to record only sensor data, sensor data + video, sensor data + audio or sensor data + video + audio. The Recorder provides comprehensive methods to control the recording lifecycle: * **Start and stop** recordings. * **Runtime configuration updates** for recording parameters. * **Listener management** for recording events and status changes. * **Status monitoring** to track the current recording state. * **User data injection** for adding custom metadata during recording. * Automatic restarts for continuous recording with chunked durations. The Recorder supports various transportation modes (e.g., car, pedestrian, bike), enabling detailed analysis and classification of recordings based on context. To prevent overwhelming the device's storage, the Recorder allows for setting disk space limits and automatically manages logs based on specified retention thresholds. #### Initialize the Recorder[​](#initialize-the-recorder "Direct link to Initialize the Recorder") The Recorder cannot be directly instantiated. Instead, the `produce` method is used to obtain a configured Recorder instance. * Kotlin * Java ```kotlin import com.magiclane.sdk.dashcam.* import com.magiclane.sdk.sensordatasource.* import com.magiclane.sdk.sensordatasource.enums.EDataType import com.magiclane.sdk.core.enums.EResolution import com.magiclane.sdk.core.Parameter import com.magiclane.sdk.core.ParameterList import android.util.Log val dataSource = DataSourceFactory.produceLive() if (dataSource == null) { Log.e(TAG, "The datasource could not be created") return } val recorderConfiguration = RecorderConfiguration( logsDir = tracksPath, dataSource = dataSource, types = arrayListOf(EDataType.Position) ) val recorder = Recorder.produce(recorderConfiguration) if (recorder == null) { Log.e(TAG, "Recorder could not be created") return } ``` ```java import com.magiclane.sdk.dashcam.*; import com.magiclane.sdk.sensordatasource.*; import com.magiclane.sdk.sensordatasource.enums.EDataType; import com.magiclane.sdk.core.enums.EResolution; import com.magiclane.sdk.core.Parameter; import com.magiclane.sdk.core.ParameterList; import android.util.Log; DataSource dataSource = DataSourceFactory.produceLive(); if (dataSource == null) { Log.e(TAG, "The datasource could not be created"); return; } ArrayList types = new ArrayList<>(Arrays.asList(EDataType.Position)); RecorderConfiguration recorderConfiguration = new RecorderConfiguration( tracksPath, dataSource, types ); Recorder recorder = Recorder.produce(recorderConfiguration); if (recorder == null) { Log.e(TAG, "Recorder could not be created"); return; } ``` info If the `dataSource`, `logsDir` and `types` parameters are not populated with valid data, the `startRecording` method will return `GemError.General` and no data will be recorded. ##### Configure the `RecorderConfiguration`[​](#configure-the-recorderconfiguration "Direct link to configure-the-recorderconfiguration") | Recorder configuration attribute | Description | | -------------------------------- | ---------------------------------------------------------------------------- | | logsDir | The directory used to keep the logs | | dataSource | The source providing the data to be recorded | | types | The data types that will be recorded (recordedTypes in constructor) | | continuousRecording | Whether the recording should continue automatically when chunk time achieved | | chunkDurationSeconds | The chunk duration time in seconds | | enableAudio | This flag will be used to determine if audio is needed to be recorded or not | | videoQuality | The video quality (EResolution enum) | | keepMinimumSeconds | Will not delete any record if this threshold is not reached | | deleteOlderThanKeepMin | Older logs that exceeds minimum kept seconds threshold should be deleted | | diskSpaceLimitMb | When reached, it will stop the recording | danger If the log duration is shorter than a minimum threshold, the `stopRecording` method of `Recorder` will not save the recording and will return `GemError.General`. The `GemError.General` result might be returned if the application has been sent to background without making the required configuration. See the [record while app is in background](#record-while-app-is-in-background) section below. Always ensure that the `EDataType` values passed to the `types` parameter are supported by the target platform. Tip The Android file system provides external storage paths for saving recordings. The following snippet shows how to obtain a valid folder path: * Kotlin * Java ```kotlin import android.content.Context import java.io.File fun getTracksPath(context: Context): String { val rootDir = context.getExternalFilesDir(null) val tracksDir = File(rootDir, "Data/Tracks") if (!tracksDir.exists()) { tracksDir.mkdirs() } return tracksDir.absolutePath } ``` ```java import android.content.Context; import java.io.File; public String getTracksPath(Context context) { File rootDir = context.getExternalFilesDir(null); File tracksDir = new File(rootDir, "Data/Tracks"); if (!tracksDir.exists()) { tracksDir.mkdirs(); } return tracksDir.getAbsolutePath(); } ``` The `videoQuality` parameter is based on the `EResolution` enum defined below. Each value represents a standard video resolution that affects recording quality and storage requirements. ##### Camera Resolutions[​](#camera-resolutions "Direct link to Camera Resolutions") | EResolution Enum Value | Description | Dimensions (pixels) | | ------------------------- | ------------------- | ------------------- | | `EResolution.Unknown` | No resolution set. | - | | `EResolution.SD480p` | Standard Definition | 640 × 480 | | `EResolution.HD720p` | High Definition | 1280 × 720 | | `EResolution.FullHD1080p` | Full HD | 1920 × 1080 | | `EResolution.WQHD1440p` | Wide Quad HD | 2560 × 1440 | | `EResolution.UHD4K2160p` | Ultra HD (4K) | 3840 × 2160 | | `EResolution.UHD8K4320p` | Ultra HD (8K) | 7680 × 4320 | Tip The actual disk usage depends on platform and encoding settings, but here are rough size estimates used internally by the SDK for calculating space requirements: | Resolution | Approx. Bytes/sec | Approx. MB/min | | ------------- | ----------------- | -------------- | | `sd480p` | 210,000 | ~12 MB/min | | `hd720p` | 629,760 | ~37 MB/min | | `fullHD1080p` | 3,774,874 | ~130 MB/min | > Note: 1 MB = 1,048,576 bytes (binary MB). These are estimates and may vary slightly by platform and encoding settings. > Note: These values are used to pre-check disk availability when `chunkDurationSeconds` is set. ##### Recording lifecycle[​](#recording-lifecycle "Direct link to Recording lifecycle") 1. **Starting the Recorder**: * Call the `startRecording` method to initiate recording. The recorder transitions to the `recording` state. 2. **Chunked Recordings**: * If a chunk duration is set in the configuration, the recording automatically stops when the duration is reached. A new recording will begin seamlessly if continuous recording is enabled, ensuring uninterrupted data capture. 3. **Stopping the Recorder**: * The `stopRecording` method halts recording, and the system ensures logs meet the configured minimum duration before saving them as .gm files inside `logsDir`. #### How to use the Recorder[​](#how-to-use-the-recorder "Direct link to How to use the Recorder") The following sections will present some use cases for the recorder, demonstrating its functionality in different scenarios. * Kotlin * Java ```kotlin val tracksPath = getTracksPath(this) val dataSource = DataSourceFactory.produceLive() if (dataSource == null) { Log.e(TAG, "The datasource could not be created") return } val recorderConfiguration = RecorderConfiguration( logsDir = tracksPath, dataSource = dataSource, types = arrayListOf(EDataType.Position) ) // Create recorder based on configuration val recorder = Recorder.produce(recorderConfiguration) if (recorder == null) { Log.e(TAG, "Recorder could not be created") return } val errorStart = recorder.startRecording() if (errorStart != GemError.NoError) { Log.e(TAG, "Error starting recording: $errorStart") } // Other code val errorStop = recorder.stopRecording() if (errorStop != GemError.NoError) { Log.e(TAG, "Error stopping recording: $errorStop") } ``` ```java String tracksPath = getTracksPath(this); DataSource dataSource = DataSourceFactory.produceLive(); if (dataSource == null) { Log.e(TAG, "The datasource could not be created"); return; } ArrayList types = new ArrayList<>(Arrays.asList(EDataType.Position)); RecorderConfiguration recorderConfiguration = new RecorderConfiguration( tracksPath, dataSource, types ); // Create recorder based on configuration Recorder recorder = Recorder.produce(recorderConfiguration); if (recorder == null) { Log.e(TAG, "Recorder could not be created"); return; } GemError errorStart = recorder.startRecording(); if (errorStart != GemError.NoError) { Log.e(TAG, "Error starting recording: " + errorStart); } // Other code GemError errorStop = recorder.stopRecording(); if (errorStop != GemError.NoError) { Log.e(TAG, "Error stopping recording: " + errorStop); } ``` danger The `Recorder` will only save data explicitly defined in the `types` list, any other data will be ignored. danger The `startRecording` and `stopRecording` methods should be called appropriately to ensure proper execution. Otherwise it may lead to unexpected behavior. Tip Don't forget to request permission for location usage before starting a recorder. #### Recorder Permissions[​](#recorder-permissions "Direct link to Recorder Permissions") To use the recorder with **camera**, **microphone**, and optionally **location or external media access**, you need to declare the appropriate permissions in your `AndroidManifest.xml`. By default, recordings can be saved in your app's internal storage. This requires only camera, audio, and location permissions. However, if you want to save recordings to the **device's gallery**, **Downloads folder**, or any **public/external storage**, you'll also need additional media permissions. ```xml ``` > These permissions should be placed inside the `` block of your `android/app/src/main/AndroidManifest.xml`. > Runtime permission requests are also required on Android 6.0+ (API 23+). Use `ActivityCompat.requestPermissions()` to request permissions at runtime. More info: [Android Manifest permissions](https://developer.android.com/reference/android/Manifest.permission) #### Record audio[​](#record-audio "Direct link to Record audio") The recorder also supports audio recording. To enable this functionality, set the `enableAudio` parameter in the `RecorderConfiguration` to true and invoke the `startAudioRecording` method from the `Recorder` class. To stop the audio recording, use the `stopAudioRecording` method. The following code snippet illustrates this process: * Kotlin * Java ```kotlin val recorderConfiguration = RecorderConfiguration( logsDir = tracksPath, dataSource = dataSource, types = arrayListOf(EDataType.Position) ) // Enable audio in configuration recorderConfiguration.enableAudio = true // Create recorder based on configuration val recorder = Recorder.produce(recorderConfiguration) if (recorder == null) { Log.e(TAG, "Recorder could not be created") return } val errorStart = recorder.startRecording() if (errorStart != GemError.NoError) { Log.e(TAG, "Error starting recording: $errorStart") } // At any moment enable audio recording recorder.startAudioRecording() // Other code // At any moment stop audio recording recorder.stopAudioRecording() val errorStop = recorder.stopRecording() if (errorStop != GemError.NoError) { Log.e(TAG, "Error stopping recording: $errorStop") } ``` ```java ArrayList types = new ArrayList<>(Arrays.asList(EDataType.Position)); RecorderConfiguration recorderConfiguration = new RecorderConfiguration( tracksPath, dataSource, types ); // Enable audio in configuration recorderConfiguration.setEnableAudio(true); // Create recorder based on configuration Recorder recorder = Recorder.produce(recorderConfiguration); if (recorder == null) { Log.e(TAG, "Recorder could not be created"); return; } GemError errorStart = recorder.startRecording(); if (errorStart != GemError.NoError) { Log.e(TAG, "Error starting recording: " + errorStart); } // At any moment enable audio recording recorder.startAudioRecording(); // Other code // At any moment stop audio recording recorder.stopAudioRecording(); GemError errorStop = recorder.stopRecording(); if (errorStop != GemError.NoError) { Log.e(TAG, "Error stopping recording: " + errorStop); } ``` Tip The audio recording will result in a log file of type `.mp4`. This file also contains the binary data of a `.gm` file, but it is accessible by system players. danger Don't forget to request permission for microphone usage if you set the `enableAudio` parameter to `true`. #### Record video[​](#record-video "Direct link to Record video") The recorder also supports video recording. To enable this functionality, add `EDataType.Camera` to the `types` and set the `videoQuality` parameter in the `RecorderConfiguration` to your desired resolution, we recommend using `EResolution.HD720p`. The video recording starts at the call of the `startRecording` method and stops at the `stopRecording` method. The following code snippet illustrates this process: * Kotlin * Java ```kotlin val recorderConfiguration = RecorderConfiguration( logsDir = tracksPath, dataSource = dataSource, types = arrayListOf(EDataType.Position, EDataType.Camera) ) // Set video quality recorderConfiguration.videoQuality = EResolution.HD720p // Create recorder based on configuration val recorder = Recorder.produce(recorderConfiguration) if (recorder == null) { Log.e(TAG, "Recorder could not be created") return } val errorStart = recorder.startRecording() if (errorStart != GemError.NoError) { Log.e(TAG, "Error starting recording: $errorStart") } // Other code val errorStop = recorder.stopRecording() if (errorStop != GemError.NoError) { Log.e(TAG, "Error stopping recording: $errorStop") } ``` ```java ArrayList types = new ArrayList<>(Arrays.asList(EDataType.Position, EDataType.Camera)); RecorderConfiguration recorderConfiguration = new RecorderConfiguration( tracksPath, dataSource, types ); // Set video quality recorderConfiguration.setVideoQuality(EResolution.HD720p); // Create recorder based on configuration Recorder recorder = Recorder.produce(recorderConfiguration); if (recorder == null) { Log.e(TAG, "Recorder could not be created"); return; } GemError errorStart = recorder.startRecording(); if (errorStart != GemError.NoError) { Log.e(TAG, "Error starting recording: " + errorStart); } // Other code GemError errorStop = recorder.stopRecording(); if (errorStop != GemError.NoError) { Log.e(TAG, "Error stopping recording: " + errorStop); } ``` Tip The camera recording will result in a log file of type `.mp4`. This file also contains the binary data of a `.gm` file, but it is accessible by system players. danger Don't forget to request permission for camera usage if you add the `EDataType.Camera` parameter to `types`. If using `chunkDuration` When `chunkDuration` is set, the SDK will **check available disk space before starting the recording**. If there isn't enough space to store an entire chunk (based on the selected resolution), the recorder will not start and will return `GemError.noDiskSpace`. Make sure to estimate required storage ahead of time. See [Camera Resolutions](#camera-resolutions) for expected sizes. #### Record multimedia[​](#record-multimedia "Direct link to Record multimedia") To record a combination of audio, video and sensors you can do that by setting up the `RecorderConfiguration` with all the desired functionalities. The following code snippet illustrates this process: * Kotlin * Java ```kotlin val recorderConfiguration = RecorderConfiguration( logsDir = tracksPath, dataSource = dataSource, types = arrayListOf(EDataType.Position, EDataType.Camera) ) // Set video quality and enable audio recorderConfiguration.videoQuality = EResolution.HD720p recorderConfiguration.enableAudio = true // Create recorder based on configuration val recorder = Recorder.produce(recorderConfiguration) if (recorder == null) { Log.e(TAG, "Recorder could not be created") return } val errorStart = recorder.startRecording() if (errorStart != GemError.NoError) { Log.e(TAG, "Error starting recording: $errorStart") } // At any moment enable audio recording recorder.startAudioRecording() // Other code // At any moment stop audio recording recorder.stopAudioRecording() val errorStop = recorder.stopRecording() if (errorStop != GemError.NoError) { Log.e(TAG, "Error stopping recording: $errorStop") } ``` ```java ArrayList types = new ArrayList<>(Arrays.asList(EDataType.Position, EDataType.Camera)); RecorderConfiguration recorderConfiguration = new RecorderConfiguration( tracksPath, dataSource, types ); // Set video quality and enable audio recorderConfiguration.setVideoQuality(EResolution.HD720p); recorderConfiguration.setEnableAudio(true); // Create recorder based on configuration Recorder recorder = Recorder.produce(recorderConfiguration); if (recorder == null) { Log.e(TAG, "Recorder could not be created"); return; } GemError errorStart = recorder.startRecording(); if (errorStart != GemError.NoError) { Log.e(TAG, "Error starting recording: " + errorStart); } // At any moment enable audio recording recorder.startAudioRecording(); // Other code // At any moment stop audio recording recorder.stopAudioRecording(); GemError errorStop = recorder.stopRecording(); if (errorStop != GemError.NoError) { Log.e(TAG, "Error stopping recording: " + errorStop); } ``` Tip The audio recording will result in a log file of type `.mp4`. This file also contains the binary data of a `.gm` file, but it is accessible by system players. danger Don't forget to request permission for camera and microphone usage if you set the `enableAudio` parameter to `true` and add the `EDataType.Camera` parameter to `types`. danger Don't forget to request permission for camera and microphone usage if you set the `enableAudio` parameter to `true` and add the `EDataType.Camera` parameter to `types`. #### Runtime Configuration Updates[​](#runtime-configuration-updates "Direct link to Runtime Configuration Updates") The Recorder supports updating certain configuration parameters during runtime without stopping the recording: * Kotlin * Java ```kotlin val recorder = Recorder.produce(recorderConfiguration) if (recorder == null) { Log.e(TAG, "Recorder could not be created") return } // Update disk space limit during runtime (in MB) recorder.setDiskSpaceLimit(500) // Update minimum keep duration during runtime (in seconds) recorder.setKeepMinimumSeconds(60) // Update chunk duration during runtime (in seconds) recorder.setChunkDurationInSeconds(120) // Enable/disable continuous recording during runtime recorder.setContinuousRecording(true) ``` ```java Recorder recorder = Recorder.produce(recorderConfiguration); if (recorder == null) { Log.e(TAG, "Recorder could not be created"); return; } // Update disk space limit during runtime (in MB) recorder.setDiskSpaceLimit(500); // Update minimum keep duration during runtime (in seconds) recorder.setKeepMinimumSeconds(60); // Update chunk duration during runtime (in seconds) recorder.setChunkDurationInSeconds(120); // Enable/disable continuous recording during runtime recorder.setContinuousRecording(true); ``` These methods allow you to dynamically adjust recording behavior based on changing conditions, such as available storage space or user preferences. Tip Runtime configuration changes take effect immediately and apply to the current and subsequent recordings. #### Audio Recording Status[​](#audio-recording-status "Direct link to Audio Recording Status") You can check whether audio recording is currently active using the `isAudioRecording` method: * Kotlin * Java ```kotlin val recorder = Recorder.produce(recorderConfiguration) if (recorder == null) { Log.e(TAG, "Recorder could not be created") return } // Start recording recorder.startRecording() // Start audio recording recorder.startAudioRecording() // Check if audio recording is active if (recorder.isAudioRecording()) { Log.d(TAG, "Audio recording is currently active") } else { Log.d(TAG, "Audio recording is not active") } ``` ```java Recorder recorder = Recorder.produce(recorderConfiguration); if (recorder == null) { Log.e(TAG, "Recorder could not be created"); return; } // Start recording recorder.startRecording(); // Start audio recording recorder.startAudioRecording(); // Check if audio recording is active if (recorder.isAudioRecording()) { Log.d(TAG, "Audio recording is currently active"); } else { Log.d(TAG, "Audio recording is not active"); } ``` #### Recorder Status and Configuration[​](#recorder-status-and-configuration "Direct link to Recorder Status and Configuration") You can access the current recorder status and configuration at any time: * Kotlin * Java ```kotlin val recorder = Recorder.produce(recorderConfiguration) if (recorder == null) { Log.e(TAG, "Recorder could not be created") return } // Get current recording status val currentStatus = recorder.status Log.d(TAG, "Current recorder status: $currentStatus") // Get current configuration val currentConfig = recorder.configuration Log.d(TAG, "Current configuration: $currentConfig") ``` ```java Recorder recorder = Recorder.produce(recorderConfiguration); if (recorder == null) { Log.e(TAG, "Recorder could not be created"); return; } // Get current recording status Object currentStatus = recorder.getStatus(); Log.d(TAG, "Current recorder status: " + currentStatus); // Get current configuration RecorderConfiguration currentConfig = recorder.getConfiguration(); Log.d(TAG, "Current configuration: " + currentConfig); ``` #### Adding User Data[​](#adding-user-data "Direct link to Adding User Data") The Recorder allows you to add custom user data to recordings, which can be useful for marking specific events or adding contextual information: * Kotlin * Java ```kotlin val recorder = Recorder.produce(recorderConfiguration) if (recorder == null) { Log.e(TAG, "Recorder could not be created") return } recorder.startRecording() // Add user data at any point during recording val userData = "Important event occurred at intersection" recorder.addUserData(userData) // You can add multiple data points throughout the recording recorder.addUserData("Speed bump detected") recorder.addUserData("Traffic light: red") ``` ```java Recorder recorder = Recorder.produce(recorderConfiguration); if (recorder == null) { Log.e(TAG, "Recorder could not be created"); return; } recorder.startRecording(); // Add user data at any point during recording String userData = "Important event occurred at intersection"; recorder.addUserData(userData); // You can add multiple data points throughout the recording recorder.addUserData("Speed bump detected"); recorder.addUserData("Traffic light: red"); ``` Tip User data is embedded in the `.gm` file and can be retrieved when processing the recorded logs. #### Recorder Event Listeners[​](#recorder-event-listeners "Direct link to Recorder Event Listeners") The Recorder supports event listeners for monitoring recording status changes and receiving notifications: * Kotlin * Java ```kotlin // Define a listener interface (this would typically be provided by the SDK) val recordingListener = object : RecordingEventListener { override fun onRecordingStarted() { Log.d(TAG, "Recording started") } override fun onRecordingStopped() { Log.d(TAG, "Recording stopped") } override fun onChunkCompleted(chunkPath: String) { Log.d(TAG, "Chunk completed: $chunkPath") } override fun onError(error: String) { Log.e(TAG, "Recording error: $error") } } val recorder = Recorder.produce(recorderConfiguration) if (recorder == null) { Log.e(TAG, "Recorder could not be created") return } // Add listener recorder.addListener(recordingListener) // Start recording recorder.startRecording() // Remove listener when no longer needed recorder.removeListener(recordingListener) ``` ```java // Define a listener interface (this would typically be provided by the SDK) RecordingEventListener recordingListener = new RecordingEventListener() { @Override public void onRecordingStarted() { Log.d(TAG, "Recording started"); } @Override public void onRecordingStopped() { Log.d(TAG, "Recording stopped"); } @Override public void onChunkCompleted(String chunkPath) { Log.d(TAG, "Chunk completed: " + chunkPath); } @Override public void onError(String error) { Log.e(TAG, "Recording error: " + error); } }; Recorder recorder = Recorder.produce(recorderConfiguration); if (recorder == null) { Log.e(TAG, "Recorder could not be created"); return; } // Add listener recorder.addListener(recordingListener); // Start recording recorder.startRecording(); // Remove listener when no longer needed recorder.removeListener(recordingListener); ``` Tip Use listeners to update your UI, handle errors, or trigger actions when recording events occur. #### Background Location Recording[​](#background-location-recording "Direct link to Background Location Recording") To enable **location recording while the app is in background**, the `allowsBackgroundLocationUpdates` flag must be enabled on the `PositionSensorConfiguration` of the data source. You must also update the Android platform-specific configuration files. ##### Android Example[​](#android-example "Direct link to Android Example") * Kotlin * Java ```kotlin val tracksPath = getTracksPath(this) // Create the live data source val dataSource = DataSourceFactory.produceLive() if (dataSource == null) { Log.e(TAG, "The datasource could not be created") return } // Enable background location updates val config = dataSource.getPreferences(EDataType.Position) ?: arrayListOf() config.add(Parameter("allowsBackgroundLocationUpdates", "true")) val result = dataSource.setPreferences(EDataType.Position, config) if (result != 0) { Log.e(TAG, "Failed to set background location preferences: $result") } // Create the recorder with config val recorderConfiguration = RecorderConfiguration( logsDir = tracksPath, dataSource = dataSource, types = arrayListOf(EDataType.Position) ) val recorder = Recorder.produce(recorderConfiguration) if (recorder == null) { Log.e(TAG, "Recorder could not be created") return } // Start recording val errorStart = recorder.startRecording() if (errorStart != GemError.NoError) { Log.e(TAG, "Error starting recording: $errorStart") } ``` ```java String tracksPath = getTracksPath(this); // Create the live data source DataSource dataSource = DataSourceFactory.produceLive(); if (dataSource == null) { Log.e(TAG, "The datasource could not be created"); return; } // Enable background location updates ArrayList config = dataSource.getPreferences(EDataType.Position); if (config == null) { config = new ArrayList<>(); } config.add(new Parameter("allowsBackgroundLocationUpdates", "true")); int result = dataSource.setPreferences(EDataType.Position, config); if (result != 0) { Log.e(TAG, "Failed to set background location preferences: " + result); } // Create the recorder with config ArrayList types = new ArrayList<>(Arrays.asList(EDataType.Position)); RecorderConfiguration recorderConfiguration = new RecorderConfiguration( tracksPath, dataSource, types ); Recorder recorder = Recorder.produce(recorderConfiguration); if (recorder == null) { Log.e(TAG, "Recorder could not be created"); return; } // Start recording GemError errorStart = recorder.startRecording(); if (errorStart != GemError.NoError) { Log.e(TAG, "Error starting recording: " + errorStart); } ``` ##### Android Manifest[​](#android-manifest "Direct link to Android Manifest") Add the following permissions to your `android/app/src/main/AndroidManifest.xml`: ```xml ``` Tip To record while the app is in background, ensure the device/battery settings allow background activity. ##### Runtime Permission[​](#runtime-permission "Direct link to Runtime Permission") On Android 6.0+ (API 23+), background location requires runtime permissions. Use `ActivityCompat.requestPermissions()` to request permissions: * Kotlin * Java ```kotlin import androidx.core.app.ActivityCompat import android.Manifest // Request background location permission ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), REQUEST_BACKGROUND_LOCATION_CODE ) ``` ```java import androidx.core.app.ActivityCompat; import android.Manifest; // Request background location permission ActivityCompat.requestPermissions( this, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, REQUEST_BACKGROUND_LOCATION_CODE ); ``` danger If the `allowsBackgroundLocationUpdates` flag is not enabled and the app is backgrounded during recording, calling `stopRecording` may result in `GemError.general`. #### Related[​](#related "Direct link to Related") * [Recorder setup](#initialize-the-recorder) * [Permissions](#recorder-permissions) #### Recorder Bookmarks and Metadata[​](#recorder-bookmarks-and-metadata "Direct link to Recorder Bookmarks and Metadata") The SDK utilizes the proprietary `.gm` file format for recordings, offering several advantages over standard file types: * Supports multiple data types, including acceleration, rotation, and more. * Allows embedding custom user data in binary format. * Enables automatic storage management by the SDK to optimize space usage. Recordings are saved as `.gm` or `.mp4` files by the Recorder. The `RecorderBookmarks` class provides functionality for managing recordings, including exporting `.gm` or `.mp4` files to other formats such as `.gpx`, as well as importing external formats and converting them to `.gm` for seamless SDK integration. Additionally, the `LogMetadata` class serves as an object-oriented representation of a `.gm` or `.mp4` file, offering features such as retrieving start and end timestamps, coordinates, and path details at varying levels of precision. The `RecorderBookmarks` class for enhanced log management: * **Export and Import Logs**: Convert logs to/from different formats such as GPX, NMEA, and KML. * **Log Metadata**: Retrieve details like start and end timestamps, transport mode and size. In the following class diagram you can see the main classes used by the `RecorderBookmarks` and the relationships between them: ![](/docs/android/assets/images/RecorderBookmarks_UML_image-e5268bad09d3f49275affb52d1bdac87.png) **RecorderBookmarks** ##### Export logs[​](#export-logs "Direct link to Export logs") * Kotlin * Java ```kotlin // Create recorderBookmarks // It loads all .gm and .mp4 files at logsDir val bookmarks = RecorderBookmarks.produce(tracksPath) if (bookmarks == null) { Log.e(TAG, "Bookmarks could not be created") return } // Get list of logs val logList = bookmarks.logsList // Export last recording with a given album name // Assumes the logList is not empty if (logList.isNotEmpty()) { val exportResult = bookmarks.exportLog( logFile = logList.last(), albumName = "My_Album_Name" ) { resultCode, message -> // Completion callback if (resultCode == 0) { Log.d(TAG, "Export successful: $message") } else { Log.e(TAG, "Export failed with code $resultCode: $message") } } // Check immediate return value if (exportResult != 0) { Log.e(TAG, "Export failed to start with code: $exportResult") } } ``` ```java // Create recorderBookmarks // It loads all .gm and .mp4 files at logsDir RecorderBookmarks bookmarks = RecorderBookmarks.produce(tracksPath); if (bookmarks == null) { Log.e(TAG, "Bookmarks could not be created"); return; } // Get list of logs List logList = bookmarks.getLogsList(); // Export last recording with a given album name // Assumes the logList is not empty if (!logList.isEmpty()) { int exportResult = bookmarks.exportLog( logList.get(logList.size() - 1), "My_Album_Name", (resultCode, message) -> { // Completion callback if (resultCode == 0) { Log.d(TAG, "Export successful: " + message); } else { Log.e(TAG, "Export failed with code " + resultCode + ": " + message); } } ); // Check immediate return value if (exportResult != 0) { Log.e(TAG, "Export failed to start with code: " + exportResult); } } ``` The resulting file will be exported to the specified album. The completion callback provides the final result of the export operation. danger Exporting a `.gm` file to other formats may result in data loss, depending on the data types supported by each format. Tip The exported file will be in the same directory as the original log file. Tip The exported file will be in the same directory as the original log file. ##### Log Management Operations[​](#log-management-operations "Direct link to Log Management Operations") The `RecorderBookmarks` class provides additional methods for managing recordings: * Kotlin * Java ```kotlin val bookmarks = RecorderBookmarks.produce(tracksPath) if (bookmarks == null) { Log.e(TAG, "Bookmarks could not be created") return } val logList = bookmarks.logsList if (logList.isNotEmpty()) { val logFile = logList.first() // Mark a log as protected (prevents automatic deletion) val protectResult = bookmarks.markLogProtected(logFile, true) if (protectResult == 0) { Log.d(TAG, "Log marked as protected") } // Mark a log as uploaded val uploadedResult = bookmarks.markLogUploaded(logFile, true) if (uploadedResult == 0) { Log.d(TAG, "Log marked as uploaded") } // Delete a specific log val deleteResult = bookmarks.deleteLog(logFile) if (deleteResult == 0) { Log.d(TAG, "Log deleted successfully") } else { Log.e(TAG, "Failed to delete log: $deleteResult") } } ``` ```java RecorderBookmarks bookmarks = RecorderBookmarks.produce(tracksPath); if (bookmarks == null) { Log.e(TAG, "Bookmarks could not be created"); return; } List logList = bookmarks.getLogsList(); if (!logList.isEmpty()) { String logFile = logList.get(0); // Mark a log as protected (prevents automatic deletion) int protectResult = bookmarks.markLogProtected(logFile, true); if (protectResult == 0) { Log.d(TAG, "Log marked as protected"); } // Mark a log as uploaded int uploadedResult = bookmarks.markLogUploaded(logFile, true); if (uploadedResult == 0) { Log.d(TAG, "Log marked as uploaded"); } // Delete a specific log int deleteResult = bookmarks.deleteLog(logFile); if (deleteResult == 0) { Log.d(TAG, "Log deleted successfully"); } else { Log.e(TAG, "Failed to delete log: " + deleteResult); } } ``` Tip Protected logs will not be automatically deleted by the SDK's storage management system, even when disk space limits are reached. danger Be careful when using `deleteLog` as it permanently removes the recording file from storage. ##### Access metadata[​](#access-metadata "Direct link to Access metadata") Each log contains metadata accessible through the `LogMetadata` class. * Kotlin * Java ```kotlin val bookmarks = RecorderBookmarks.produce(logsDir) if (bookmarks != null) { val logList = bookmarks.logsList if (!logList.isNullOrEmpty()) { val logMetadata = bookmarks.getMetadata(logList.last()) } } ``` ```java RecorderBookmarks bookmarks = RecorderBookmarks.produce(logsDir); if (bookmarks != null) { List logList = bookmarks.getLogsList(); if (logList != null && !logList.isEmpty()) { LogMetadata logMetadata = bookmarks.getMetadata(logList.get(logList.size() - 1)); } } ``` danger The `getMetadata` method will return `null` if the log file does not exist inside the `logsDir` directory or if the log file is not a valid `.gm` file. The metadata within a `LogMetadata` object contains: * **startPosition / endPosition**: Geographic coordinates for the log's beginning and end. * **route**: A list of route coordinates from the recorded log. * **startTimestampInMillis / endTimestampInMillis**: Timestamp of the first/last sensor data. * **durationMillis**: Log duration. * **isProtected()**: Check if a log file is protected. Protected logs will not be automatically deleted. * **isUploaded()**: Check if log file was uploaded. * **logSize**: Get the log size in bytes. * **isDataTypeAvailable(type)**: Verify if a data type is produced by the log file. * **soundMarks**: A list of recorded soundmarks. * **availableDataTypes**: List of the available data types in this log. To visualize the recorded route, a `Path` object can be constructed using the route coordinates from the `LogMetadata`. This path can then be displayed on a map. For more details, refer to the documentation on the [path entity](/docs/android/guides/core/base-entities.md#path) and [display paths](/docs/android/guides/maps/display-map-items/display-paths.md). info Some features mentioned in other documentation versions (such as custom user metadata, activity records, and log metrics) are not available in the current Android SDK implementation. #### Record while app is in background[​](#record-while-app-is-in-background "Direct link to Record while app is in background") The recording might fail with error code `GemError.General` when calling `stopRecording` if the app is sent to the background during the recording. Enable `allowsBackgroundLocationUpdates` on the `PositionSensorConfiguration` associated to the data source before instantiating the `Recorder`: In the app's manifest file add the `ACCESS_BACKGROUND_LOCATION` permission. * Kotlin * Java ```kotlin val dataSource = DataSourceFactory.produceLive() if (dataSource == null) { Log.e(TAG, "The datasource could not be created") return } val sensorConfiguration = dataSource.getPreferences(EDataType.Position) ?: arrayListOf() sensorConfiguration.add(Parameter("allowsBackgroundLocationUpdates", "true")) val result = dataSource.setPreferences(EDataType.Position, sensorConfiguration) if (result != 0) { Log.e(TAG, "Failed to set background location preferences: $result") } val recorderConfiguration = RecorderConfiguration( logsDir = logsDir, dataSource = dataSource, types = arrayListOf(EDataType.Position) ) val recorder = Recorder.produce(recorderConfiguration) ``` ```java DataSource dataSource = DataSourceFactory.produceLive(); if (dataSource == null) { Log.e(TAG, "The datasource could not be created"); return; } ArrayList sensorConfiguration = dataSource.getPreferences(EDataType.Position); if (sensorConfiguration == null) { sensorConfiguration = new ArrayList<>(); } sensorConfiguration.add(new Parameter("allowsBackgroundLocationUpdates", "true")); int result = dataSource.setPreferences(EDataType.Position, sensorConfiguration); if (result != 0) { Log.e(TAG, "Failed to set background location preferences: " + result); } ArrayList types = new ArrayList<>(Arrays.asList(EDataType.Position)); RecorderConfiguration recorderConfiguration = new RecorderConfiguration( logsDir, dataSource, types ); Recorder recorder = Recorder.produce(recorderConfiguration); ``` --- ### Sensors and data sources |

This section provides an overview of how the Maps Android SDK integrates with various sensors and external data sources to enhance map functionality and interactivity. From GPS and compass data to accelerometer readings and custom telemetry inputs, the SDK is designed to support a wide range of sensor-driven scenarios. You'll learn how to access and configure these inputs, how the SDK responds to real-time changes, and how to incorporate your own data streams into the mapping experience. Whether you're building navigation apps, augmented reality layers, or location-aware services, this section will guide you through the sensor and data integration process. #### Sensor types[​](#sensor-types "Direct link to Sensor types") The supported sensor data types can be summarized in the following table: | **Type** | **Description** | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **Acceleration** | Measures linear movement of the device in three-dimensional space. Useful for detecting motion, steps, or sudden changes in speed. | | **Activity** | Represents user activity such as walking, running, or being stationary, typically inferred from motion data. Only available on Android devices. | | **Attitude** | Describes the orientation of the device in 3D space, often expressed as Euler angles or quaternions. | | **Battery** | Provides battery status information such as charge level and power state. | | **Camera** | Indicates data coming from or triggered by the device's camera, such as frames or detection events. | | **Compass** | Gives directional heading relative to magnetic or true north using magnetometer data. | | **Magnetic Field** | Reports raw magnetic field strength, useful for environmental sensing or heading correction. | | **Orientation** | Combines multiple sensors (like accelerometer and magnetometer) to calculate absolute device orientation. | | **Position** | Basic geographic position data, including latitude, longitude, and optionally altitude. | | **Improved Position** | Enhanced position data that has been refined using filtering, correction services, or sensor fusion. | | **Gyroscope** | Measures the rate of rotation around the device's axes, used to detect turns and angular movement. | | **Temperature** | Provides temperature readings, either ambient or internal device temperature. | | **Notification** | Represents external or system-level events that are not tied to physical sensors. | | **Mount Information** | Describes how the device is physically mounted or oriented within a fixed system, such as in a vehicle. | | **Heart Rate** | Biometric data representing beats per minute, typically from a fitness or health sensor. | | **NMEA Chunk** | Raw navigation data in NMEA sentence format, typically from GNSS receivers for high-precision tracking. Only available on Android devices. | | **Unknown** | A fallback type used when the source of the data cannot be determined. | More details about the `PositionData` and `ImprovedPositionData` classes are available [here](/docs/android/guides/core/positions.md). danger When using EDataType values, ensure that the specific types are supported on the target platform. Attempting to create data sources or recordings with unsupported types may result in failures. #### Working with data sources[​](#working-with-data-sources "Direct link to Working with data sources") A simplified view of the main classes used to work with data sources can be seen in the following diagram: ![](/docs/android/assets/images/DataSource_class_diagram-679e380cbbd04e201cdcd82495067174.png) **DataSource** There are multiple possible data types, represented by the `EDataType` enum. Each sensor value is stored in a class that is derived from `SenseData`. Two such classes are `PositionData` and `AccelerationData`. If you want to create objects of these types, helper methods are provided in the companion objects of these classes. For example, `PositionData.produce()` and `AccelerationData.produce()` can create custom sensor data. In principle, this will only be necessary if you want to create a custom data source that will be fed with custom data. You can create a `DataSource` by using one of the static methods in `DataSourceFactory`: * `DataSourceFactory.produceLive()`: Creates a data source that collects data from the device's built-in sensors in real time. This is the most common use case for applications relying on actual sensor input. * `DataSourceFactory.produceExternal(availableDataTypes)`: Creates a custom data source that accepts user-supplied data. You can feed data into this source via the `pushData` method. * `DataSourceFactory.produceLog(path)`: Creates a data source that replays data from a previously recorded session (log file: gpx, nmea). This is useful for debugging, training, or offline data processing. See the [Recorder docs](/docs/android/guides/positioning/recorder.md) for information about recording data. * `DataSourceFactory.produceSimulation(route)`: Creates a data source that simulates movement along a specified route. It can be used for UI prototyping, testing, or feature validation without relying on real-world movement. The first two types (live and external) are categorized under `EDataSourceType.Live`, whereas the latter two (log and simulation) fall under `EDataSourceType.Playback`. info By default, a data source starts automatically upon creation. However, it's possible that it hasn't fully initialized by the time you obtain the data source object. If you add a `DataSourceListener` immediately after acquiring the data source, there's a chance you'll miss the initial "playing status changed" notification that indicates the data source has started - since it may already be in the started state when the listener is attached. ##### Configuring and Controlling a Data Source[​](#configuring-and-controlling-a-data-source "Direct link to Configuring and Controlling a Data Source") Once created, a data source can be stopped or started using the appropriate control methods: * Kotlin * Java ```kotlin dataSource.stop() // ... dataSource.start() ``` ```java dataSource.stop(); // ... dataSource.start(); ``` You can also configure a data source's behavior using methods like: * `setPreferences()`: to set the sampling rate or data filtering behavior. * `setMockData()`: to simulate sensor updates. danger The `setMockData()` method is only available for live data sources and supports only the `EDataType.Position` type. To mock other data types, use an external `DataSource`. ##### Using `DataSourceListener`[​](#using-datasourcelistener "Direct link to using-datasourcelistener") To receive updates from a data source, you can register a `DataSourceListener`. This listener allows you to react to various events such as: * Changes in the playing status of the data source. * Interruptions in data flow (e.g., sensor stopped, app went to background, etc.). * New sensor data becoming available. * Progress updates during playback. You can create a listener using the `create` factory method and pass the appropriate callbacks: * Kotlin * Java ```kotlin val listener = DataSourceListener.create( onPlayingStatusChanged = { dataType, status -> println("Status for $dataType changed to $status") }, onDataInterruptionEvent = { dataType, reason, ended -> println("Data interruption on $dataType: $reason. Ended: $ended") }, onNewData = { data -> println("New data received: $data") }, onProgressChanged = { progress -> println("Playback progress: $progress ms") } ) ``` ```java DataSourceListener listener = DataSourceListener.create( (dataType, status) -> { System.out.println("Status for " + dataType + " changed to " + status); }, (dataType, reason, ended) -> { System.out.println("Data interruption on " + dataType + ": " + reason + ". Ended: " + ended); }, (data) -> { System.out.println("New data received: " + data); }, (progress) -> { System.out.println("Playback progress: " + progress + " ms"); } ); ``` Once created, this listener can be registered with a `DataSource`, for a specific `EDataType` (in this case the position): * Kotlin * Java ```kotlin dataSource.addListener(listener, EDataType.Position) ``` ```java dataSource.addListener(listener, EDataType.Position); ``` Later, you can remove the listener when it's no longer needed: * Kotlin * Java ```kotlin dataSource.removeListener(listener, EDataType.Position) ``` ```java dataSource.removeListener(listener, EDataType.Position); ``` #### Using the `Playback` interface[​](#using-the-playback-interface "Direct link to using-the-playback-interface") The `Playback` interface allows you to control data sources that support playback functionality - specifically those of type `EDataSourceType.Playback`, such as *log files* or *simulated route replays*. **It is not compatible with live or custom data sources**. To access a `Playback` instance, you can check the type of the data source and retrieve it accordingly: * Kotlin * Java ```kotlin if (dataSource.dataSourceType == EDataSourceType.Playback) { val playback = dataSource.playback playback?.pause() // ... playback?.resume() } ``` ```java if (dataSource.getDataSourceType() == EDataSourceType.Playback) { Playback playback = dataSource.getPlayback(); if (playback != null) { playback.pause(); // ... playback.resume(); } } ``` As shown above, playback-enabled data sources can be paused and resumed. Additionally, you can adjust the playback speed by setting a `speedMultiplier`, which must fall within the range defined by `playback.minSpeedMultiplier` and `playback.maxSpeedMultiplier`. To control playback position, use `playback.currentPosition`, which represents the elapsed time in milliseconds from the beginning of the log or simulation. This allows you to skip to any point in the playback. #### Tracking positions[​](#tracking-positions "Direct link to Tracking positions") Positions from a `DataSource` can be tracked on a map by rendering a marker polyline between relevant map points. This is done by using the `MapView` methods directly. ![](/docs/android/assets/images/example_android_track_positions-75ec1472e633842b7f5d732800553d20.png) **Tracked path** The following code illustrates the functionality shown in the screenshot above. * Kotlin * Java ```kotlin SdkCall.execute { mapView.startTrackPositions( updatePositionMs = 500, settings = MarkerCollectionRenderSettings( polylineInnerColor = Rgba(255, 0, 0, 255), polylineOuterColor = Rgba(255, 255, 0, 255) ), dataSource = dataSource ) // other code ... mapView.stopTrackPositions() } ``` ```java SdkCall.execute(() -> { mapView.startTrackPositions( 500, new MarkerCollectionRenderSettings( new Rgba(255, 0, 0, 255), new Rgba(255, 255, 0, 255) ), dataSource ); // other code ... mapView.stopTrackPositions(); }); ``` | **Method** | **Parameters** | **Return type** | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | **startTrackPositions** | - `updatePositionMs`: The tracked position collection update frequency in milliseconds. High frequency may decrease rendering performance on low-end devices
- `settings`: The markers collection rendering settings in the map view
- `dataSource`: The DataSource object whose positions are tracked | `GemError` | | **stopTrackPositions** | | `GemError` | | **isTrackedPositions** | | `Boolean` | | **getTrackedPositions** | | `Pair?, GemError>` | info If the `dataSource` parameter is left null, tracking will use the current `DataSource` set in `PositionService`. If no `DataSource` is set in `PositionService`, `GemError.NotFound` will be returned. ##### Getting tracked positions[​](#getting-tracked-positions "Direct link to Getting tracked positions") After calling `MapView.startTrackPositions()`, you can retrieve the tracked positions later using the `getTrackedPositions()` method. This method returns a pair containing a list of Coordinates and a GemError that indicates the success of the operation. * Kotlin * Java ```kotlin SdkCall.execute { val (trackedPositions, error) = mapView.getTrackedPositions() if (!GemError.isError(error)) { // Use the tracked positions trackedPositions?.let { positions -> // Process the list of coordinates } } mapView.stopTrackPositions() } ``` ```java SdkCall.execute(() -> { Pair, GemError> result = mapView.getTrackedPositions(); ArrayList trackedPositions = result.first; GemError error = result.second; if (!GemError.isError(error)) { // Use the tracked positions if (trackedPositions != null) { // Process the list of coordinates } } mapView.stopTrackPositions(); }); ``` danger Calling `getTrackedPositions()` **after** `stopTrackPositions()` is called will result in returning an empty list. --- ### Show location on map |

The location of the device is shown by default using an arrow position tracker. If the data source has been successfully set and the required permissions were granted then the position tracker showing the current location should be visible on the map as an arrow. ![](/docs/android/assets/images/example_android_position_default-2186c936d7348e9b661554007cdf4265.png) **Default position tracker showing current position** At the moment it is not possible to have multiple position trackers on the map. info GPS accuracy may be limited in environments such as indoor spaces, areas with weak GPS signals, or locations with significant obstructions, such as narrow streets or between tall buildings. In these situations, the tracker may exhibit erratic movement within a confined area. Additionally, the performance of device sensors, such as the accelerometer and gyroscope, can further impact GPS positioning accuracy. This behavior is more pronounced when the device is stationary. #### Start follow position[​](#start-follow-position "Direct link to Start follow position") Following the position tracker can be done by calling the `startFollowingPosition()` method on the MapView. * Kotlin * Java ```kotlin mapView.startFollowingPosition() ``` ```java mapView.startFollowingPosition(); ``` When the `startFollowingPosition()` method is called, the camera enters a mode where it automatically follows the movement and rotation of the position tracker. This ensures the user's current location and orientation are consistently centered and updated on the map. The `startFollowingPosition()` method can take parameters such as `animation`, which controls the movement from the current map camera position to the position of the tracker, `zoomLevel` and `viewAngle`. ##### Set map rotation mode[​](#set-map-rotation-mode "Direct link to Set map rotation mode") When following position, it is possible to have the map rotated with the user orientation: * Kotlin * Java ```kotlin val prefs = mapView.preferences?.followPositionPreferences prefs?.setMapRotationMode(EFollowPositionMapRotationMode.PositionHeading) ``` ```java FollowPositionPreferences prefs = mapView.getPreferences().getFollowPositionPreferences(); if (prefs != null) { prefs.setMapRotationMode(EFollowPositionMapRotationMode.PositionHeading); } ``` If you want to use the compass sensor for map rotation use `EFollowPositionMapRotationMode.Compass`: * Kotlin * Java ```kotlin val prefs = mapView.preferences?.followPositionPreferences prefs?.setMapRotationMode(EFollowPositionMapRotationMode.Compass) ``` ```java FollowPositionPreferences prefs = mapView.getPreferences().getFollowPositionPreferences(); if (prefs != null) { prefs.setMapRotationMode(EFollowPositionMapRotationMode.Compass); } ``` The map rotation can also be fixed to a given angle using the `EFollowPositionMapRotationMode.Fixed` value and providing a `mapAngle` value: * Kotlin * Java ```kotlin val prefs = mapView.preferences?.followPositionPreferences prefs?.setMapRotationMode(EFollowPositionMapRotationMode.Fixed, angle = 30.0) ``` ```java FollowPositionPreferences prefs = mapView.getPreferences().getFollowPositionPreferences(); if (prefs != null) { prefs.setMapRotationMode(EFollowPositionMapRotationMode.Fixed, 30.0); } ``` A value of `0.0` given for the `angle` parameter represents north-up alignment. The `mapRotationMode` returns a `Pair` containing: * the current `EFollowPositionMapRotationMode` mode * the map angle set in case of `EFollowPositionMapRotationMode.Fixed` #### Exit follow position[​](#exit-follow-position "Direct link to Exit follow position") The `stopFollowingPosition()` method from the MapView can be used to programmatically stop following the position. * Kotlin * Java ```kotlin mapView.stopFollowingPosition() ``` ```java mapView.stopFollowingPosition(); ``` The follow mode will be exited automatically if the user interacts with the map. Actions such as panning, or tilting will disable the camera's automatic tracking. This can be deactivated by setting `touchHandlerExitAllow` to false (see the section below). #### Customize follow position settings[​](#customize-follow-position-settings "Direct link to Customize follow position settings") The `FollowPositionPreferences` class has options which can be used to customize the behavior of following the position. This can be accessed from the `preferences` property of the MapView. The fields defined in `FollowPositionPreferences` take effect only when the camera is in follow position mode. To customize camera behavior when not following the position, refer to the fields available in `MapViewPreferences` and `MapView`. | Field | Type | Explanation | | --------------------------------- | ------- | ---------------------------------------------------------------------------------------------------- | | cameraFocus | XyF | The position on the viewport where the position tracker is located on the screen. | | timeBeforeTurnPresentation | Int | The time interval before starting a turn presentation | | touchHandlerExitAllow | Boolean | If set to false then gestures made by the user will exit follow position mode | | touchHandlerModifyPersistent | Boolean | If set to true then changes made by the user using gestures are persistent | | viewAngle | Double | The viewAngle used within follow position mode | | zoomLevel | Int | The zoomLevel used within follow position mode | | accuracyCircleVisible | Boolean | Specifies if the accuracy circle should be visible (regardless if is in follow position mode or not) | | isTrackObjectFollowingMapRotation | Boolean | Specifies if the track object should follow the map rotation | Please refer to the [adjust map guide](/docs/android/guides/maps/adjust-map.md) for more information about the `viewAngle`, `zoomLevel` and `cameraFocus` fields. If no zoom level is set, a default value is used. ###### Use of *touchHandlerModifyPersistent*[​](#use-of-touchhandlermodifypersistent "Direct link to use-of-touchhandlermodifypersistent") When the camera enters follow position mode and manually adjusts the zoom level or view angle, these modifications are retained until the mode is exited, either manually or programmatically. If `touchHandlerModifyPersistent` is set to `true`, then invoking `startFollowingPosition()` (with default parameters for zoom and angle) will restore the zoom level and view angle from the previous follow position session. If `touchHandlerModifyPersistent` is set to `false`, then calling `startFollowingPosition()` (with default zoom and angle parameters) will result in appropriate values for the zoom level and view angle being recalculated. Tip It is recommended to set the `touchHandlerModifyPersistent` property value right before calling the `startFollowingPosition()` method. ###### Use of *touchHandlerExitAllow*[​](#use-of-touchhandlerexitallow "Direct link to use-of-touchhandlerexitallow") If the camera is in follow position mode and the `touchHandlerExitAllow` property is set to `true`, a two-finger pan gesture in a non-vertical direction will cause the camera to exit follow position mode. If `touchHandlerExitAllow` is set to false, the user cannot manually exit follow position mode through touch gestures. In this case, the mode can only be exited programmatically by calling the `stopFollowingPosition()` method. ##### Set circle visibility[​](#set-circle-visibility "Direct link to Set circle visibility") For example, in order to show the accuracy circle visibility on the map (which is by default hidden): * Kotlin * Java ```kotlin val prefs = mapView.preferences?.followPositionPreferences prefs?.accuracyCircleVisible = true ``` ```java FollowPositionPreferences prefs = mapView.getPreferences().getFollowPositionPreferences(); if (prefs != null) { prefs.setAccuracyCircleVisible(true); } ``` ![](/docs/android/assets/images/example_android_position_circle-93606e5623b16383fad8428ce2f6fe4a.png) **Accuracy circle turned on** ##### Customize circle color[​](#customize-circle-color "Direct link to Customize circle color") The accuracy circle color can be set using the `setDefPositionTrackerAccuracyCircleColor()` static method from the `MapSceneObject` class: * Kotlin * Java ```kotlin val setErrorCode = MapSceneObject.setDefPositionTrackerAccuracyCircleColor(Rgba(255, 0, 0, 255)) Log.d(TAG, "Error code for setting the circle color: $setErrorCode") ``` ```java GemError setErrorCode = MapSceneObject.setDefPositionTrackerAccuracyCircleColor(new Rgba(255, 0, 0, 255)); Log.d(TAG, "Error code for setting the circle color: " + setErrorCode); ``` Tip It is recommended to use colors with partial opacity instead of fully opaque colors for improved visibility and usability. The current color can be retrieved using the `getDefPositionTrackerAccuracyCircleColor()` static method: * Kotlin * Java ```kotlin val color = MapSceneObject.getDefPositionTrackerAccuracyCircleColor() ``` ```java Rgba color = MapSceneObject.getDefPositionTrackerAccuracyCircleColor(); ``` The color can be reset to the default value using the `resetDefPositionTrackerAccuracyCircleColor()` static method: * Kotlin * Java ```kotlin val resetErrorCode = MapSceneObject.resetDefPositionTrackerAccuracyCircleColor() Log.d(TAG, "Error code for resetting the circle color: $resetErrorCode") ``` ```java GemError resetErrorCode = MapSceneObject.resetDefPositionTrackerAccuracyCircleColor(); Log.d(TAG, "Error code for resetting the circle color: " + resetErrorCode); ``` ##### Set position of the position tracker on the viewport[​](#set-position-of-the-position-tracker-on-the-viewport "Direct link to Set position of the position tracker on the viewport") In order to set the position tracker on a particular spot of the viewport while in follow position mode the `cameraFocus` property can be used: * Kotlin * Java ```kotlin // Calculate the position relative to the viewport val twoThirdsX = 2.0f / 3.0f val threeFifthsY = 3.0f / 5.0f val position = XyF(twoThirdsX, threeFifthsY) // Set the position of the position tracker in the viewport // while in follow position mode val prefs = mapView.preferences?.followPositionPreferences prefs?.cameraFocus = position mapView.startFollowingPosition() ``` ```java // Calculate the position relative to the viewport float twoThirdsX = 2.0f / 3.0f; float threeFifthsY = 3.0f / 5.0f; XyF position = new XyF(twoThirdsX, threeFifthsY); // Set the position of the position tracker in the viewport // while in follow position mode FollowPositionPreferences prefs = mapView.getPreferences().getFollowPositionPreferences(); if (prefs != null) { prefs.setCameraFocus(position); } mapView.startFollowingPosition(); ``` The `cameraFocus` property uses a coordinate system relative to the viewport, not physical pixels. In this way `XyF(0.0f, 0.0f)` corresponds with top left corner and `XyF(1.0f, 1.0f)` corresponds with right bottom corner. #### Customize position icon[​](#customize-position-icon "Direct link to Customize position icon") The SDK supports customizing the position tracker to suit your application's requirements. For example, you can set a simple PNG as the position tracker using the following approach: * Kotlin * Java ```kotlin // Read the file and load the image as a binary resource val inputStream = assets.open("navArrow.png") val imageByteArray = inputStream.readBytes() // Customize the position tracker MapSceneObject.customizeDefPositionTracker(imageByteArray, SceneObjectFileFormat.tex) ``` ```java // Read the file and load the image as a binary resource InputStream inputStream = getAssets().open("navArrow.png"); byte[] imageByteArray = inputStream.readAllBytes(); // Customize the position tracker MapSceneObject.customizeDefPositionTracker(imageByteArray, SceneObjectFileFormat.tex); ``` Besides simple 2D icons, 3D objects as `glb` files can be set. The format parameter of the `customizeDefPositionTracker()` should be set to `SceneObjectFileFormat.glb` in this case. At this moment it is not possible to set different icons for different maps. danger Make sure the resource (in this example `navArrow.png`) is correctly placed in the `assets` folder. See the [Android documentation](https://developer.android.com/guide/topics/resources/providing-resources) for more information. ![](/docs/android/assets/images/example_android_position_custom_arrow-faa4bc8336d78a08f96198f9b80bbb43.png) **Custom position tracker** #### Other position tracker settings[​](#other-position-tracker-settings "Direct link to Other position tracker settings") Other settings such as scale and the visibility of the position tracker can be changed using the methods available on the `MapSceneObject` which can be obtained using `MapSceneObject.getDefPositionTracker()`. ##### Change the position tracker scale[​](#change-the-position-tracker-scale "Direct link to Change the position tracker scale") To change the scale of the position tracker we can use the `scale` property: * Kotlin * Java ```kotlin // Get the position tracker val mapSceneObject = MapSceneObject.getDefPositionTracker() // Change the scale mapSceneObject?.scale = 0.5f ``` ```java // Get the position tracker MapSceneObject mapSceneObject = MapSceneObject.getDefPositionTracker(); // Change the scale if (mapSceneObject != null) { mapSceneObject.setScale(0.5f); } ``` A value of 1.0 corresponds with the default scale value. The parameter passed to the property should be in the range `(0, mapSceneObject.maxScale]`. The code snippet from above sets half the scale. info The scale of the position tracker stays constant on the viewport regardless of the map zoom level. ##### Change the position tracker visibility[​](#change-the-position-tracker-visibility "Direct link to Change the position tracker visibility") To change the visibility of the position tracker we can use the `visibility` property: * Kotlin * Java ```kotlin // Get the position tracker val mapSceneObject = MapSceneObject.getDefPositionTracker() // Change the visibility mapSceneObject?.visibility = false ``` ```java // Get the position tracker MapSceneObject mapSceneObject = MapSceneObject.getDefPositionTracker(); // Change the visibility if (mapSceneObject != null) { mapSceneObject.setVisibility(false); } ``` The snippet above makes the position tracker invisible. #### Follow position events[​](#follow-position-events "Direct link to Follow position events") The SDK provides callbacks to monitor when the MapView enters or exits follow position mode: * Kotlin * Java ```kotlin mapView.onEnterFollowingPosition = { Log.d(TAG, "Entered follow position mode") // Hide GPS button or update UI } mapView.onExitFollowingPosition = { Log.d(TAG, "Exited follow position mode") // Show GPS button or update UI } ``` ```java mapView.setOnEnterFollowingPosition(() -> { Log.d(TAG, "Entered follow position mode"); // Hide GPS button or update UI }); mapView.setOnExitFollowingPosition(() -> { Log.d(TAG, "Exited follow position mode"); // Show GPS button or update UI }); ``` You can also check if the MapView is currently following position: * Kotlin * Java ```kotlin val isFollowing = mapView.isFollowingPosition() Log.d(TAG, "Currently following position: $isFollowing") ``` ```java boolean isFollowing = mapView.isFollowingPosition(); Log.d(TAG, "Currently following position: " + isFollowing); ``` --- ### Public Transit stops |

This API provides detailed access to public transport data including agencies, routes, stops, and trips. It integrates with interactive map-based applications using the Magic Lane SDK and allows developers to dynamically fetch and explore real-time public transportation information from selected positions on the map. Tip The structure of the public transport data is modeled after the [General Transit Feed Specification (GTFS)](https://gtfs.org/documentation/schedule/reference/) and offers access to a subset of the fields and entities defined in GTFS. #### Key Features[​](#key-features "Direct link to Key Features") * Query public transport overlays by screen position * Retrieve structured information about transport agencies, stops, routes, and trips * Support for real-time data including delays and cancellations * Metadata about accessibility, bike allowances, and platform details * Filter trips by route type, route short name, or agency #### How It Works[​](#how-it-works "Direct link to How It Works") Set a cursor position on the map and query for public transport overlay items at that location. Each overlay item provides access to detailed stop information including agencies, routes, and trip schedules. * Kotlin * Java ```kotlin mapView.onLongDown = { screenPosition -> // Set cursor position on the screen mapView.cursorScreenPosition = screenPosition // Get the public transit overlay items at that position val overlayItems = mapView.cursorSelectionOverlayItems overlayItems?.forEach { overlayItem -> // Check if this is a public transit overlay if (overlayItem.overlayUid == ECommonOverlayId.PublicTransport.value) { // Get the preview extended data val previewDataList = GemList() overlayItem.getPreviewExtendedData( previewDataList, ProgressListener.create( onCompleted = { errorCode -> if (errorCode == GemError.NoError) { // Process the public transit data previewDataList.forEach { parameter -> Log.d("PT", "${parameter.key}: ${parameter.valueString}") } } } ) ) } } } ``` ```java mapView.setOnLongDown(screenPosition -> { // Set cursor position on the screen mapView.setCursorScreenPosition(screenPosition); // Get the public transit overlay items at that position List overlayItems = mapView.getCursorSelectionOverlayItems(); if (overlayItems != null) { for (OverlayItem overlayItem : overlayItems) { // Check if this is a public transit overlay if (overlayItem.getOverlayUid() == ECommonOverlayId.PublicTransport.getValue()) { // Get the preview extended data GemList previewDataList = new GemList<>(); overlayItem.getPreviewExtendedData( previewDataList, ProgressListener.create( errorCode -> { if (errorCode == GemError.NoError) { // Process the public transit data for (Parameter parameter : previewDataList) { Log.d("PT", parameter.getKey() + ": " + parameter.getValueString()); } } } ) ); } } } }); ``` Tip You can also obtain public transit overlay items by performing an overlay search using `ECommonOverlayId.PublicTransport`. Once you retrieve the corresponding `OverlayItem`s, use their `getPreviewExtendedData` method to access the stop information. See the [Search on overlays](/docs/android/guides/search/get-started-search.md#search-on-overlays) guide for more details. danger All returned times are local times represented as Unix timestamps in seconds. Use the `TimeService` to convert them to other time zones. danger There are two types of public transit stops on the map: * Stops which are of type `OverlayItem` and can be selected via the `cursorSelectionOverlayItems` property. These stops provide extensive details accessible through `getPreviewExtendedData`. They are displayed with a blue icon when using the default style. * Stops which are of type `Landmark` and can be selected via the `cursorSelectionLandmarks` property. These stops do not provide extensive details. They are displayed with a gray icon when using the default style. #### Working with Public Transit Data[​](#working-with-public-transit-data "Direct link to Working with Public Transit Data") The public transit data is provided as a list of `Parameter` objects. Each parameter contains a key-value pair representing different aspects of the transit stop information. ##### Accessing Stop Information[​](#accessing-stop-information "Direct link to Accessing Stop Information") * Kotlin * Java ```kotlin val previewDataList = GemList() overlayItem.getPreviewExtendedData( previewDataList, ProgressListener.create( onCompleted = { errorCode -> if (errorCode == GemError.NoError) { previewDataList.forEach { parameter -> when (parameter.key) { EPublicTransitOverlayParamsKeys.StopId.value -> { val stopId = parameter.valueLong Log.d("PT", "Stop ID: $stopId") } EPublicTransitOverlayParamsKeys.StopName.value -> { val stopName = parameter.valueString Log.d("PT", "Stop Name: $stopName") } EPublicTransitOverlayParamsKeys.AgencyId.value -> { val agencyId = parameter.valueLong Log.d("PT", "Agency ID: $agencyId") } EPublicTransitOverlayParamsKeys.AgencyName.value -> { val agencyName = parameter.valueString Log.d("PT", "Agency Name: $agencyName") } } } } } ) ) ``` ```java GemList previewDataList = new GemList<>(); overlayItem.getPreviewExtendedData( previewDataList, ProgressListener.create( errorCode -> { if (errorCode == GemError.NoError) { for (Parameter parameter : previewDataList) { String key = parameter.getKey(); if (key.equals(EPublicTransitOverlayParamsKeys.StopId.getValue())) { long stopId = parameter.getValueLong(); Log.d("PT", "Stop ID: " + stopId); } else if (key.equals(EPublicTransitOverlayParamsKeys.StopName.getValue())) { String stopName = parameter.getValueString(); Log.d("PT", "Stop Name: " + stopName); } else if (key.equals(EPublicTransitOverlayParamsKeys.AgencyId.getValue())) { long agencyId = parameter.getValueLong(); Log.d("PT", "Agency ID: " + agencyId); } else if (key.equals(EPublicTransitOverlayParamsKeys.AgencyName.getValue())) { String agencyName = parameter.getValueString(); Log.d("PT", "Agency Name: " + agencyName); } } } } ) ); ``` #### Parameter Keys[​](#parameter-keys "Direct link to Parameter Keys") The following parameter keys are available for public transit data: ##### Agency Information[​](#agency-information "Direct link to Agency Information") | Parameter Key | Type | Description | | -------------------------------------------- | -------- | ------------------------------- | | `EPublicTransitOverlayParamsKeys.AgencyId` | `Long` | Agency ID | | `EPublicTransitOverlayParamsKeys.AgencyName` | `String` | Full name of the transit agency | | `EPublicTransitOverlayParamsKeys.AgencyURL` | `String` | URL of the transit agency | ##### Route Information[​](#route-information "Direct link to Route Information") | Parameter Key | Type | Description | | ------------------------------------------------ | -------- | --------------------------------------------- | | `EPublicTransitOverlayParamsKeys.RouteId` | `Long` | Route ID | | `EPublicTransitOverlayParamsKeys.RouteShortName` | `String` | Short name of a route (e.g., "32", "100X") | | `EPublicTransitOverlayParamsKeys.RouteLongName` | `String` | Full name of a route with destination or stop | | `EPublicTransitOverlayParamsKeys.RouteType` | `Int` | Type of route (see `ERouteType`) | | `EPublicTransitOverlayParamsKeys.RouteColor` | `String` | Route color designation | | `EPublicTransitOverlayParamsKeys.RouteTextColor` | `String` | Text color for route | | `EPublicTransitOverlayParamsKeys.RouteHeading` | `String` | Optional heading information | ##### Stop Information[​](#stop-information "Direct link to Stop Information") | Parameter Key | Type | Description | | --------------------------------------------------- | --------------- | ------------------------------------------------ | | `EPublicTransitOverlayParamsKeys.StopId` | `Long` | Stop/platform, station, entrance/exit identifier | | `EPublicTransitOverlayParamsKeys.StopName` | `String` | Name of the location | | `EPublicTransitOverlayParamsKeys.StopTimeLatitude` | `ParameterList` | Latitude coordinate of the stop | | `EPublicTransitOverlayParamsKeys.StopTimeLongitude` | `Double` | Longitude coordinate of the stop | | `EPublicTransitOverlayParamsKeys.IsStation` | `Boolean` | Whether this location is a station | | `EPublicTransitOverlayParamsKeys.Routes` | `ParameterList` | Associated routes for the stop | ##### Trip Information[​](#trip-information "Direct link to Trip Information") | Parameter Key | Type | Description | | ---------------------------------------------------------- | --------------- | ---------------------------------------------------------- | | `EPublicTransitOverlayParamsKeys.TripIndex` | `Long` | Trip index | | `EPublicTransitOverlayParamsKeys.TripDate` | `Long` | Date of the trip (Unix timestamp in seconds) | | `EPublicTransitOverlayParamsKeys.TripDepartureTime` | `Long` | Departure time from first stop (Unix timestamp in seconds) | | `EPublicTransitOverlayParamsKeys.TripHasRealtime` | `Boolean` | Whether real-time data is available | | `EPublicTransitOverlayParamsKeys.TripIsCancelled` | `Boolean` | Whether the trip is cancelled | | `EPublicTransitOverlayParamsKeys.TripDelayMinutes` | `Int` | Delay in minutes | | `EPublicTransitOverlayParamsKeys.TripStopIndex` | `Int` | Stop index | | `EPublicTransitOverlayParamsKeys.TripStopPlatformCode` | `String` | Platform code | | `EPublicTransitOverlayParamsKeys.TripWheelchairAccessible` | `Int` | Wheelchair accessibility (see `EWheelchairAccessible`) | | `EPublicTransitOverlayParamsKeys.TripBikesAllowed` | `Int` | Bikes allowed status (see `EBikesAllowed`) | | `EPublicTransitOverlayParamsKeys.TripAgencyId` | `Long` | Agency ID for this trip | | `EPublicTransitOverlayParamsKeys.TripStopTimes` | `ParameterList` | List of stop times for this trip | | `EPublicTransitOverlayParamsKeys.TripHasShape` | `Boolean` | Whether trip has shape data | ##### Stop Time Information[​](#stop-time-information "Direct link to Stop Time Information") | Parameter Key | Type | Description | | ------------------------------------------------------- | --------------- | ----------------------------------------------- | | `EPublicTransitOverlayParamsKeys.StopTimeName` | `String` | Name of the serviced stop | | `EPublicTransitOverlayParamsKeys.StopTimeLatitude` | `ParameterList` | Latitude of the stop | | `EPublicTransitOverlayParamsKeys.StopTimeLongitude` | `Double` | Longitude of the stop | | `EPublicTransitOverlayParamsKeys.StopTimeHasRealTime` | `Boolean` | Whether data is provided in real-time | | `EPublicTransitOverlayParamsKeys.StopTimeDelay` | `Int` | Delay in seconds | | `EPublicTransitOverlayParamsKeys.StopTimeDepartureTime` | `Long` | Departure time (Unix timestamp in seconds) | | `EPublicTransitOverlayParamsKeys.StopTimeIsBefore` | `Boolean` | Whether stop time is before current time | | `EPublicTransitOverlayParamsKeys.StopTimeDetails` | `Int` | Stop details including wheelchair accessibility | #### Route Types[​](#route-types "Direct link to Route Types") The `ERouteType` enum represents different types of public transport. | Enum Value | Description | | ---------------- | ---------------------------------------------------------------------------------------------- | | `Bus` | Bus, Trolleybus. Used for short and long-distance bus routes. | | `Underground` | Subway, Metro. Any underground rail system within a metropolitan area. | | `Railway` | Rail. Used for intercity or long-distance travel. | | `Tram` | Tram, Streetcar, Light rail. Any light rail or street level system within a metropolitan area. | | `WaterTransport` | Water transport. Used for ferries and other water-based transit. | | `Misc` | Miscellaneous. Includes other types of public transport not covered by the other categories. | ##### Filtering by Route Type[​](#filtering-by-route-type "Direct link to Filtering by Route Type") * Kotlin * Java ```kotlin previewDataList.forEach { parameter -> if (parameter.key == EPublicTransitOverlayParamsKeys.RouteType.value) { when (parameter.valueLong.toInt()) { ERouteType.Bus.value -> Log.d("PT", "Bus route") ERouteType.Underground.value -> Log.d("PT", "Underground route") ERouteType.Railway.value -> Log.d("PT", "Railway route") ERouteType.Tram.value -> Log.d("PT", "Tram route") ERouteType.WaterTransport.value -> Log.d("PT", "Water transport route") ERouteType.Misc.value -> Log.d("PT", "Miscellaneous route") } } } ``` ```java for (Parameter parameter : previewDataList) { if (parameter.getKey().equals(EPublicTransitOverlayParamsKeys.RouteType.getValue())) { int routeType = (int) parameter.getValueLong(); if (routeType == ERouteType.Bus.getValue()) { Log.d("PT", "Bus route"); } else if (routeType == ERouteType.Underground.getValue()) { Log.d("PT", "Underground route"); } else if (routeType == ERouteType.Railway.getValue()) { Log.d("PT", "Railway route"); } else if (routeType == ERouteType.Tram.getValue()) { Log.d("PT", "Tram route"); } else if (routeType == ERouteType.WaterTransport.getValue()) { Log.d("PT", "Water transport route"); } else if (routeType == ERouteType.Misc.getValue()) { Log.d("PT", "Miscellaneous route"); } } } ``` #### Wheelchair Accessibility[​](#wheelchair-accessibility "Direct link to Wheelchair Accessibility") The `EWheelchairAccessible` enum indicates wheelchair accessibility information. | Enum Value | Description | | ---------- | ---------------------------------------------------------- | | `NoInfo` | No accessibility information for the trip | | `Yes` | Vehicle can accommodate at least one rider in a wheelchair | | `No` | No riders in wheelchairs can be accommodated on this trip | #### Bikes Allowed[​](#bikes-allowed "Direct link to Bikes Allowed") The `EBikesAllowed` enum indicates whether bikes are allowed. | Enum Value | Description | | ---------- | -------------------------------------------- | | `NoInfo` | No bike information for the trip | | `Yes` | Vehicle can accommodate at least one bicycle | | `No` | No bicycles are allowed on this trip | --- ### Routing Routing functionality provides a robust solution for calculating, customizing, and analyzing routes for various transportation needs. You can calculate routes between a start and destination point, include intermediate waypoints for multi-stop routes, and determine areas accessible within a specific range. Additionally, the routing service allows you to retrieve turn-by-turn instructions, traffic event details, and estimate time of arrival (ETA) along with travel durations and distances. The system supports a variety of options such as computing terrain profiles for elevation data, displaying routes on a map, and handling public transit routes. #### [📄️ Get started with routing](/docs/android/guides/routing/get-started-routing.md) [Here's a quick overview of what you can do with routing:](/docs/android/guides/routing/get-started-routing.md) #### [📄️ Handling RoutePreferences](/docs/android/guides/routing/route-preferences.md) [Before computing a route, we need to specify some route options using the RoutePreferences class.](/docs/android/guides/routing/route-preferences.md) #### [📄️ Advanced features](/docs/android/guides/routing/advanced-features.md) [Compute route ranges](/docs/android/guides/routing/advanced-features.md) --- ### Advanced features | #### Compute route ranges[​](#compute-route-ranges "Direct link to Compute route ranges") In order to compute a route range we need to: * Specify in the `RoutePreferences` the most important route preferences (others can also be used): * `routeRanges` list containing a list of range values, one for each route we compute. Measurement units are corresponding to the specified `routeType` (see the table below) * \[optional] `transportMode` (by default `ERouteTransportMode.Car`) * \[optional] `routeType` (can be `ERouteType.Fastest`, `ERouteType.Economic`, `ERouteType.Shortest` - by default is fastest) * \[optional] `routeRangesQuality` ( a value in the interval \[0, 100], default 100) representing the quality of the generated polygons. * The list of landmarks will contain only one landmark, the starting point for the route range computation. | Preference | Measurement unit | | ---------- | ---------------- | | fastest | seconds | | shortest | meters | | economic | Wh | danger Routes computed using route ranges are **not navigable**. danger The `ERouteType.Scenic` route type is not supported for route ranges. Route can be computed with a code like the following. It is a range route computation because it only has a simple `Landmark` and `routeRanges` contains values (in this case 2 routes will be computed). * Kotlin * Java ```kotlin SdkCall.execute { // Define the departure. val startLandmark = Landmark("Start", 48.85682, 2.34375) // Define the route preferences. // Compute 2 ranges, 30 min and 60 min val routePreferences = RoutePreferences().apply { routeType = ERouteType.Fastest setRouteRanges(arrayListOf(1800, 3600), 100) // quality = 100 } val routingService = RoutingService( preferences = routePreferences, onCompleted = { routes, errorCode, hint -> when (errorCode) { GemError.NoError -> { // Route range computed successfully // Display routes on map using the additional fill color setting mapView?.presentRoutes(routes) } GemError.Cancel -> { // Route computation canceled } else -> { // Error occurred showDialog("Error: ${GemError.getMessage(errorCode)}") } } } ) routingService.calculateRoute(arrayListOf(startLandmark)) } ``` ```java SdkCall.execute(() -> { // Define the departure. Landmark startLandmark = new Landmark("Start", 48.85682, 2.34375); // Define the route preferences. // Compute 2 ranges, 30 min and 60 min RoutePreferences routePreferences = new RoutePreferences(); routePreferences.setRouteType(ERouteType.Fastest); routePreferences.setRouteRanges(new ArrayList<>(Arrays.asList(1800, 3600)), 100); // quality = 100 RoutingService routingService = new RoutingService( routePreferences, (routes, errorCode, hint) -> { if (errorCode == GemError.NoError) { // Route range computed successfully // Display routes on map using the additional fill color setting if (mapView != null) { mapView.presentRoutes(routes); } } else if (errorCode == GemError.Cancel) { // Route computation canceled } else { // Error occurred showDialog("Error: " + GemError.getMessage(errorCode)); } } ); routingService.calculateRoute(new ArrayList<>(Collections.singletonList(startLandmark))); }); ``` info The computed routes can be displayed on the map, just like any regular route, with the only difference that the additional settings for polygon fill color can be configured in the route render settings. #### Compute path based routes[​](#compute-path-based-routes "Direct link to Compute path based routes") A `Path` is a structure containing a list of coordinates (a track). It can be created based on: * custom coordinates specified by the user * coordinates recorded in a GPX file * coordinates obtained by doing a finger draw on the map A **Path backed landmark** is a special kind of `Landmark` that has a `Path` inside it. Sometimes we want to compute routes based on a list of one or more **Path backed landmark**(s) and optionally some regular `Landmark`(s). In this case the result will only contain one route. The path provided as waypoint track is used as a hint for the routing algorithm. You can see an example below (the highlighted area represents the code necessary to create the list with one element of type landmark built based on a path): * Kotlin * Java ```kotlin SdkCall.execute { val coords = arrayListOf( Coordinates(40.786, -74.202), Coordinates(40.690, -74.209), Coordinates(40.695, -73.814), Coordinates(40.782, -73.710) ) val gemPath = Path.produceWithCoords(coords) // A list containing only one Path backed Landmark val landmarkList = arrayListOf(gemPath?.toLandmark()) // Define the route preferences. val routePreferences = RoutePreferences() val routingService = RoutingService( preferences = routePreferences, onCompleted = { routes, errorCode, hint -> when (errorCode) { GemError.NoError -> { // Route computed successfully showSnackbar("Number of routes: ${routes.size}") } GemError.Cancel -> { // Route computation canceled } else -> { // Error occurred showDialog("Error: ${GemError.getMessage(errorCode)}") } } } ) routingService.calculateRoute(landmarkList.filterNotNull() as ArrayList) } ``` ```java SdkCall.execute(() -> { ArrayList coords = new ArrayList<>(); coords.add(new Coordinates(40.786, -74.202)); coords.add(new Coordinates(40.690, -74.209)); coords.add(new Coordinates(40.695, -73.814)); coords.add(new Coordinates(40.782, -73.710)); Path gemPath = Path.produceWithCoords(coords); // A list containing only one Path backed Landmark ArrayList landmarkList = new ArrayList<>(); if (gemPath != null) { landmarkList.add(gemPath.toLandmark()); } // Define the route preferences. RoutePreferences routePreferences = new RoutePreferences(); RoutingService routingService = new RoutingService( routePreferences, (routes, errorCode, hint) -> { if (errorCode == GemError.NoError) { // Route computed successfully showSnackbar("Number of routes: " + routes.size()); } else if (errorCode == GemError.Cancel) { // Route computation canceled } else { // Error occurred showDialog("Error: " + GemError.getMessage(errorCode)); } } ); routingService.calculateRoute(landmarkList); }); ``` Tip The `Path` object associated to a path based landmark can be modified using the `trackData` property available on the `Landmark` object. See the [Landmarks guide](/docs/android/guides/core/landmarks.md) for more details about this. danger When computing a route based on a path backed landmark **and** non-path backed landmarks, it is mandatory to set the `accurateTrackMatch` field from `RoutePreferences` to `true`. Otherwise, the routing computation will fail with a `GemError.Unsupported` error. The `isTrackResume` field from `RoutePreferences` can also be set to configure the behaviour of the routing engine when one track based landmark is used as a waypoint together with other landmarks. If this field is set to `true`, the routing engine will try to match the entire track of the path based landmark. Otherwise, if set to `false`, only the end point of the track will be used as waypoints. #### Computing a route based on a GPX file[​](#computing-a-route-based-on-a-gpx-file "Direct link to Computing a route based on a GPX file") You can compute a route based on a GPX file by using the `path based landmark` described in the previous section. The only difference is how we compute the `gemPath`. * Kotlin * Java ```kotlin SdkCall.execute { // Load GPX file from assets val gpxAssetsFilename = "gpx/recorded_route.gpx" val inputStream = applicationContext.resources.assets.open(gpxAssetsFilename) // Produce a Path based on the GPX data val gemPath = Path.produceWithGpx(inputStream) gemPath?.let { path -> // LandmarkList will contain only one path based landmark. val landmarkList = arrayListOf(path.toLandmark()) // Define the route preferences. val routePreferences = RoutePreferences().apply { transportMode = ERouteTransportMode.Bicycle } val routingService = RoutingService( preferences = routePreferences, onCompleted = { routes, errorCode, hint -> when (errorCode) { GemError.NoError -> { // Handle successful route calculation mapView?.presentRoutes(routes) } GemError.Cancel -> { // Route computation canceled } else -> { // Error occurred showDialog("Error: ${GemError.getMessage(errorCode)}") } } } ) routingService.calculateRoute(landmarkList) } ?: run { showDialog("GPX file could not be loaded") } } ``` ```java SdkCall.execute(() -> { // Load GPX file from assets String gpxAssetsFilename = "gpx/recorded_route.gpx"; InputStream inputStream = getApplicationContext().getResources().getAssets().open(gpxAssetsFilename); // Produce a Path based on the GPX data Path gemPath = Path.produceWithGpx(inputStream); if (gemPath != null) { // LandmarkList will contain only one path based landmark. ArrayList landmarkList = new ArrayList<>(); landmarkList.add(gemPath.toLandmark()); // Define the route preferences. RoutePreferences routePreferences = new RoutePreferences(); routePreferences.setTransportMode(ERouteTransportMode.Bicycle); RoutingService routingService = new RoutingService( routePreferences, (routes, errorCode, hint) -> { if (errorCode == GemError.NoError) { // Handle successful route calculation if (mapView != null) { mapView.presentRoutes(routes); } } else if (errorCode == GemError.Cancel) { // Route computation canceled } else { // Error occurred showDialog("Error: " + GemError.getMessage(errorCode)); } } ); routingService.calculateRoute(landmarkList); } else { showDialog("GPX file could not be loaded"); } }); ``` #### Finger drawn path[​](#finger-drawn-path "Direct link to Finger drawn path") When necessary, it is possible to record a path based on drawing with the finger on the map. It is also possible to record multiple paths. In this situation a straight line is added between any 2 consecutive finger drawn paths. When you want to enter this recording mode: * Kotlin * Java ```kotlin SdkCall.execute { mapView?.enableDrawMarkersMode() } ``` ```java SdkCall.execute(() -> { if (mapView != null) { mapView.enableDrawMarkersMode(); } }); ``` When you want to exit this mode, you can get the generated `List` with the following: * Kotlin * Java ```kotlin SdkCall.execute { val landmarks = mapView?.disableDrawMarkersMode() landmarks?.let { landmarkList -> val routePreferences = RoutePreferences().apply { accurateTrackMatch = false ignoreRestrictionsOverTrack = true } val routingService = RoutingService( preferences = routePreferences, onCompleted = { routes, errorCode, hint -> when (errorCode) { GemError.NoError -> { // Handle successful route calculation mapView?.presentRoutes(routes) } GemError.Cancel -> { // Route computation canceled } else -> { // Error occurred showDialog("Error: ${GemError.getMessage(errorCode)}") } } } ) routingService.calculateRoute(landmarkList) } } ``` ```java SdkCall.execute(() -> { List landmarks = mapView != null ? mapView.disableDrawMarkersMode() : null; if (landmarks != null) { RoutePreferences routePreferences = new RoutePreferences(); routePreferences.setAccurateTrackMatch(false); routePreferences.setIgnoreRestrictionsOverTrack(true); RoutingService routingService = new RoutingService( routePreferences, (routes, errorCode, hint) -> { if (errorCode == GemError.NoError) { // Handle successful route calculation if (mapView != null) { mapView.presentRoutes(routes); } } else if (errorCode == GemError.Cancel) { // Route computation canceled } else { // Error occurred showDialog("Error: " + GemError.getMessage(errorCode)); } } ); routingService.calculateRoute(new ArrayList<>(landmarks)); } }); ``` The resulted `List` will only contain one element, a path based `Landmark`. #### Compute public transit routes[​](#compute-public-transit-routes "Direct link to Compute public transit routes") In order to compute a public transit route we need to set the `transportMode` field in the `RoutePreferences` like this: * Kotlin * Java ```kotlin // Define the route preferences with public transport mode. val routePreferences = RoutePreferences().apply { transportMode = ERouteTransportMode.Public } ``` ```java // Define the route preferences with public transport mode. RoutePreferences routePreferences = new RoutePreferences(); routePreferences.setTransportMode(ERouteTransportMode.Public); ``` danger Public transit routes are not navigable. The full source code to compute a public transit route and handle it could look like this: * Kotlin * Java ```kotlin SdkCall.execute { // Define the departure. val departureLandmark = Landmark("Departure", 45.6646, 25.5872) // Define the destination. val destinationLandmark = Landmark("Destination", 45.6578, 25.6233) // Define the route preferences with public transport mode. val routePreferences = RoutePreferences().apply { transportMode = ERouteTransportMode.Public } val routingService = RoutingService( preferences = routePreferences, onCompleted = { routes, errorCode, hint -> when (errorCode) { GemError.NoError -> { if (routes.isNotEmpty()) { // Get the routes collection from map preferences. val routesMap = mapView?.preferences?.routes // Display the routes on map. routes.forEachIndexed { index, route -> routesMap?.add(route, index == 0, if (index == 0) "Route" else null) } // Convert normal route to PTRoute val ptRoute = routes.first().toPTRoute() // Convert each segment to PTRouteSegment val ptSegments = ptRoute?.segments?.map { segment -> segment.toPTRouteSegment() } ptSegments?.forEach { segment -> segment?.let { ptSegment -> val transitType = ptSegment.transitType if (ptSegment.isCommon()) { // PT segment val ptInstructions = ptSegment.instructions?.map { instruction -> instruction.toPTRouteInstruction() } ptInstructions?.forEach { ptInstr -> ptInstr?.let { instruction -> // handle public transit instruction val stationName = instruction.name val departure = instruction.departureTime val arrival = instruction.arrivalTime // ... } } } else { // walk segment val instructions = ptSegment.instructions instructions?.forEach { walkInstr -> // handle walk instruction } } } } } } GemError.Cancel -> { // Route computation canceled } else -> { // Error occurred showDialog("Error: ${GemError.getMessage(errorCode)}") } } } ) routingService.calculateRoute(arrayListOf(departureLandmark, destinationLandmark)) } ``` ```java SdkCall.execute(() -> { // Define the departure. Landmark departureLandmark = new Landmark("Departure", 45.6646, 25.5872); // Define the destination. Landmark destinationLandmark = new Landmark("Destination", 45.6578, 25.6233); // Define the route preferences with public transport mode. RoutePreferences routePreferences = new RoutePreferences(); routePreferences.setTransportMode(ERouteTransportMode.Public); RoutingService routingService = new RoutingService( routePreferences, (routes, errorCode, hint) -> { if (errorCode == GemError.NoError) { if (!routes.isEmpty()) { // Get the routes collection from map preferences. RoutesCollection routesMap = mapView != null ? mapView.getPreferences().getRoutes() : null; // Display the routes on map. for (int index = 0; index < routes.size(); index++) { Route route = routes.get(index); if (routesMap != null) { routesMap.add(route, index == 0, index == 0 ? "Route" : null); } } // Convert normal route to PTRoute PTRoute ptRoute = routes.get(0).toPTRoute(); if (ptRoute != null && ptRoute.getSegments() != null) { // Convert each segment to PTRouteSegment for (RouteSegment segment : ptRoute.getSegments()) { PTRouteSegment ptSegment = segment.toPTRouteSegment(); if (ptSegment != null) { int transitType = ptSegment.getTransitType(); if (ptSegment.isCommon()) { // PT segment if (ptSegment.getInstructions() != null) { for (RouteInstruction instruction : ptSegment.getInstructions()) { PTRouteInstruction ptInstr = instruction.toPTRouteInstruction(); if (ptInstr != null) { // handle public transit instruction String stationName = ptInstr.getName(); Time departure = ptInstr.getDepartureTime(); Time arrival = ptInstr.getArrivalTime(); // ... } } } } else { // walk segment List instructions = ptSegment.getInstructions(); if (instructions != null) { for (RouteInstruction walkInstr : instructions) { // handle walk instruction } } } } } } } } else if (errorCode == GemError.Cancel) { // Route computation canceled } else { // Error occurred showDialog("Error: " + GemError.getMessage(errorCode)); } } ); ArrayList waypoints = new ArrayList<>(); waypoints.add(departureLandmark); waypoints.add(destinationLandmark); routingService.calculateRoute(waypoints); }); ``` Once routes are computed, if the computation was for public transport route, you can convert a resulted route to a public transit route via `toPTRoute()`. After that you have full access to the methods specific to this kind of route. A public transit route is a sequence of one or more segments. Each segment is either a walking segment, either a public transit segment. You can determine the segment type based on the `ETransitType`. `ETransitType` can have the following values: `Walk`, `Bus`, `Underground`, `Railway`, `Tram`, `WaterTransport`, `Other`, `SharedBike`, `SharedScooter`, `SharedCar`, `Unknown`. Tip Other settings related to public transit (such as departure/arrival time) can be specified within the `RoutePreferences` object passed to the `calculateRoute` method: * Kotlin * Java ```kotlin val customRoutePreferences = RoutePreferences().apply { transportMode = ERouteTransportMode.Public // The arrival time is set to one hour from now. algorithmType = EPTAlgorithmType.Arrival timestamp = Time(System.currentTimeMillis() + 3600000) // 1 hour from now // Sort the routes by the best time. sortingStrategy = EPTSortingStrategy.BestTime // Accessibility preferences useBikes = false useWheelchair = false } ``` ```java RoutePreferences customRoutePreferences = new RoutePreferences(); customRoutePreferences.setTransportMode(ERouteTransportMode.Public); // The arrival time is set to one hour from now. customRoutePreferences.setAlgorithmType(EPTAlgorithmType.Arrival); customRoutePreferences.setTimestamp(new Time(System.currentTimeMillis() + 3600000)); // 1 hour from now // Sort the routes by the best time. customRoutePreferences.setSortingStrategy(EPTSortingStrategy.BestTime); // Accessibility preferences customRoutePreferences.setUseBikes(false); customRoutePreferences.setUseWheelchair(false); ``` #### Export a Route to file[​](#export-a-route-to-file "Direct link to Export a Route to file") The `exportToFile` method allows you to export a route from `RouteBookmarks` into a file on disk. This makes it possible to store the route for later use or share it with other systems. The file will be saved at the exact location provided in the **filePath** parameter, so always ensure the directory exists and is writable. * Kotlin * Java ```kotlin SdkCall.execute { val routeBookmarks = RouteBookmarks.produce("MyRoutes") routeBookmarks?.let { bookmarks -> //index of the route in this bookmark collection and filepath val error = bookmarks.exportToFile(index, filePath) when (error) { GemError.NoError -> { // Route exported successfully } GemError.NotFound -> { // The given route index does not exist } GemError.Io -> { // File cannot be created or written to } else -> { // Other error occurred } } } } ``` ```java SdkCall.execute(() -> { RouteBookmarks routeBookmarks = RouteBookmarks.produce("MyRoutes"); if (routeBookmarks != null) { //index of the route in this bookmark collection and filepath int error = routeBookmarks.exportToFile(index, filePath); if (error == GemError.NoError) { // Route exported successfully } else if (error == GemError.NotFound) { // The given route index does not exist } else if (error == GemError.Io) { // File cannot be created or written to } else { // Other error occurred } } }); ``` Tip When exporting a route, make sure to handle possible errors: * **`GemError.NotFound`** - This occurs if the given route index does not exist. * **`GemError.Io`** - This occurs if the file cannot be created or written to. #### Export a Route as String[​](#export-a-route-as-string "Direct link to Export a Route as String") The `exportAs` method allows you to export a route into a textual representation. The returned value is a `DataBuffer` containing the full route data in the requested format. This makes it easy to store the route as a file or share it with other applications that support formats like GPX, KML, NMEA, or GeoJSON. * Kotlin * Java ```kotlin SdkCall.execute { val route = routes.first() val dataBuffer = route.exportAs(EPathFileFormat.Gpx) dataBuffer?.let { buffer -> // You now have the full GPX as a DataBuffer // Convert to string if needed val gpxString = String(buffer.data) } } ``` ```java SdkCall.execute(() -> { Route route = routes.get(0); DataBuffer dataBuffer = route.exportAs(EPathFileFormat.Gpx); if (dataBuffer != null) { // You now have the full GPX as a DataBuffer // Convert to string if needed String gpxString = new String(dataBuffer.getData()); } }); ``` --- ### Get started with routing |

Here's a quick overview of what you can do with routing: * Calculate routes from a start point to a destination. * Include intermediary waypoints for multi-stop routes. * Compute range routes to determine areas reachable within a specific range. * Plan routes over predefined tracks. * Customize routes with preferences like route types, restrictions, and more. * Retrieve maneuvers and turn-by-turn instructions. * Access detailed route profiles for further analysis. #### Calculate routes[​](#calculate-routes "Direct link to Calculate routes") You can calculate a route with the code below. This route is navigable, which means that later it is possible to do a navigation/ simulation on it. * Kotlin * Java ```kotlin // Define the departure landmark val departureLandmark = Landmark().apply { coordinates = Coordinates(48.85682, 2.34375) } // Define the destination landmark val destinationLandmark = Landmark().apply { coordinates = Coordinates(50.84644, 4.34587) } // Define the route preferences (all default) val routePreferences = RoutePreferences() // Create waypoints list val waypoints = arrayListOf(departureLandmark, destinationLandmark) // Create routing service val routingService = RoutingService( preferences = routePreferences, onCompleted = { routes, errorCode, hint -> if (errorCode == GemError.Success) { Log.d("Routing", "Number of routes: ${routes.size}") } else if (errorCode == GemError.Cancel) { Log.d("Routing", "Route computation canceled") } else { Log.e("Routing", "Error: $errorCode") } } ) // Calculate the route val result = routingService.calculateRoute(waypoints) ``` ```java // Define the departure landmark Landmark departureLandmark = new Landmark(); departureLandmark.setCoordinates(new Coordinates(48.85682, 2.34375)); // Define the destination landmark Landmark destinationLandmark = new Landmark(); destinationLandmark.setCoordinates(new Coordinates(50.84644, 4.34587)); // Define the route preferences (all default) RoutePreferences routePreferences = new RoutePreferences(); // Create waypoints list ArrayList waypoints = new ArrayList<>(); waypoints.add(departureLandmark); waypoints.add(destinationLandmark); // Create routing service RoutingService routingService = new RoutingService( routePreferences, (routes, errorCode, hint) -> { if (errorCode == GemError.Success) { Log.d("Routing", "Number of routes: " + routes.size()); } else if (errorCode == GemError.Cancel) { Log.d("Routing", "Route computation canceled"); } else { Log.e("Routing", "Error: " + errorCode); } } ); // Calculate the route int result = routingService.calculateRoute(waypoints); ``` info The `RoutingService.calculateRoute` method returns an `Int` status code. When the computation fails to initiate, the `onCompleted` callback will be triggered with the appropriate error code. You can call `RoutingService.cancelRoute()` to cancel an ongoing route calculation. The `errorCode` provided by the callback function can have the following values: | Value | Significance | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GemError.Success` | successfully completed | | `GemError.Cancel` | cancelled by the user | | `GemError.WaypointAccess` | couldn't be found with the current preferences | | `GemError.ConnectionRequired` | if allowOnlineCalculation = false in the routing preferences and the calculation can't be done on the client side due to missing data | | `GemError.Expired` | calculation can't be done on client side due to missing necessary data and the client world map data version is no longer supported by the online routing service | | `GemError.RouteTooLong` | routing was executed on the online service and the operation took too much time to complete (usually more than 1 min, depending on the server overload state) | | `GemError.Invalidated` | the offline map data changed ( offline map downloaded, erased, updated ) during the calculation | | `GemError.NoMemory` | routing engine couldn't allocate the necessary memory for the calculation | The previous example shows how to define your start and end points, set route preferences, and handle the callback for results. If needed, you can cancel the ongoing computation: * Kotlin * Java ```kotlin routingService.cancelRoute() ``` ```java routingService.cancelRoute(); ``` When the route is canceled, the callback will return `errorCode` = `GemError.Cancel`. #### Get ETA and traffic information[​](#get-eta-and-traffic-information "Direct link to Get ETA and traffic information") Once the route is computed, you can retrieve additional details like the estimated time of arrival (ETA) and traffic information. Here's how you can access these: * Kotlin * Java ```kotlin val td = route.getTimeDistance(activePart = false) val totalDistance = td?.totalDistance // same with: //val totalDistance = (td?.unrestrictedDistance ?: 0) + (td?.restrictedDistance ?: 0) val totalDuration = td?.totalTime // same with: //val totalDuration = (td?.unrestrictedTime ?: 0) + (td?.restrictedTime ?: 0) // by default activePart = true val remainTd = route.getTimeDistance(activePart = true) val totalRemainDistance = remainTd?.totalDistance val totalRemainDuration = remainTd?.totalTime ``` ```java TimeDistance td = route.getTimeDistance(false); Integer totalDistance = td != null ? td.getTotalDistance() : null; // same with: //int totalDistance = (td != null ? td.getUnrestrictedDistance() : 0) + (td != null ? td.getRestrictedDistance() : 0); Integer totalDuration = td != null ? td.getTotalTime() : null; // same with: //int totalDuration = (td != null ? td.getUnrestrictedTime() : 0) + (td != null ? td.getRestrictedTime() : 0); // by default activePart = true TimeDistance remainTd = route.getTimeDistance(true); Integer totalRemainDistance = remainTd != null ? remainTd.getTotalDistance() : null; Integer totalRemainDuration = remainTd != null ? remainTd.getTotalTime() : null; ``` By using the method `Route.getTimeDistance` we can get the time and distance for a route. If the `activePart` parameter is `false`, it means the distance is computed for the entire route initially computed, otherwise it is computed only for the active part (the part still remaining to be navigated). The default value for this parameter is `true`. In the example `unrestricted` means the part of the route that is on public property and `restricted` the part of the route that is on private property. Time is measured in seconds and distance in meters. If you want to gather traffic details, you can do so like this: * Kotlin * Java ```kotlin val trafficEvents = route.trafficEvents trafficEvents?.forEach { event -> val transportMode = event.affectedTransportMode val description = event.description val eventClass = event.eventClass val eventSeverity = event.eventSeverity val from = event.from val to = event.to val isRoadBlock = event.isRoadblock() } ``` ```java List trafficEvents = route.getTrafficEvents(); if (trafficEvents != null) { for (TrafficEvent event : trafficEvents) { int transportMode = event.getAffectedTransportMode(); String description = event.getDescription(); int eventClass = event.getEventClass(); int eventSeverity = event.getEventSeverity(); Coordinates from = event.getFrom(); Coordinates to = event.getTo(); boolean isRoadBlock = event.isRoadblock(); } } ``` Check the [Traffic Events guide](/docs/android/guides/core/traffic-events.md) for more details. #### Display routes on map[​](#display-routes-on-map "Direct link to Display routes on map") After calculating the routes, they are not automatically displayed on the map. To visualize and center the map on the route, refer to the [display routes on maps](/docs/android/guides/maps/display-map-items/display-routes.md) related documentation. The Maps SDK for Android offers extensive customization options, allowing for flexible preferences to tailor the display to your needs. #### Get the Terrain Profile[​](#get-the-terrain-profile "Direct link to Get the Terrain Profile") When computing the route we can choose to also build the `TerrainProfile` for the route. In order to do that `RoutePreferences` must specify we want to also generate the terrain profile: * Kotlin * Java ```kotlin val routePreferences = RoutePreferences().apply { buildTerrainProfile = true } ``` ```java RoutePreferences routePreferences = new RoutePreferences(); routePreferences.setBuildTerrainProfile(true); ``` danger Setting the `buildTerrainProfile` property to `true` in the preferences used within `calculateRoute` is mandatory for getting route terrain profile data. Later, use the profile for elevation data or other terrain-related details: * Kotlin * Java ```kotlin val terrainProfile = route.terrainProfile terrainProfile?.let { profile -> val minElevation = profile.minElevation val maxElevation = profile.maxElevation val minElevDist = profile.minElevationDistance val maxElevDist = profile.maxElevationDistance val totalUp = profile.totalUp val totalDown = profile.totalDown // elevation at 100m from the route start val elevation = profile.getElevation(100) profile.roadTypeSections?.forEach { section -> val roadType = section.type val startDistance = section.startDistanceM } profile.surfaceSections?.forEach { section -> val surfaceType = section.type val startDistance = section.startDistanceM } profile.climbSections?.forEach { section -> val grade = section.grade val slope = section.slope val startDistanceM = section.startDistanceM val endDistanceM = section.endDistanceM } val categs = arrayListOf(-16f, -10f, -7f, -4f, -1f, 1f, 4f, 7f, 10f, 16f) val steepSections = profile.getSteepSections(categs) steepSections?.forEach { section -> val categ = section.category val startDistanceM = section.startDistanceM } } ``` ```java TerrainProfile terrainProfile = route.getTerrainProfile(); if (terrainProfile != null) { int minElevation = terrainProfile.getMinElevation(); int maxElevation = terrainProfile.getMaxElevation(); int minElevDist = terrainProfile.getMinElevationDistance(); int maxElevDist = terrainProfile.getMaxElevationDistance(); int totalUp = terrainProfile.getTotalUp(); int totalDown = terrainProfile.getTotalDown(); // elevation at 100m from the route start int elevation = terrainProfile.getElevation(100); List roadTypeSections = terrainProfile.getRoadTypeSections(); if (roadTypeSections != null) { for (RoadTypeSection section : roadTypeSections) { int roadType = section.getType(); int startDistance = section.getStartDistanceM(); } } List surfaceSections = terrainProfile.getSurfaceSections(); if (surfaceSections != null) { for (SurfaceSection section : surfaceSections) { int surfaceType = section.getType(); int startDistance = section.getStartDistanceM(); } } List climbSections = terrainProfile.getClimbSections(); if (climbSections != null) { for (ClimbSection section : climbSections) { int grade = section.getGrade(); float slope = section.getSlope(); int startDistanceM = section.getStartDistanceM(); int endDistanceM = section.getEndDistanceM(); } } ArrayList categs = new ArrayList<>(Arrays.asList(-16f, -10f, -7f, -4f, -1f, 1f, 4f, 7f, 10f, 16f)); List steepSections = terrainProfile.getSteepSections(categs); if (steepSections != null) { for (SteepSection section : steepSections) { int categ = section.getCategory(); int startDistanceM = section.getStartDistanceM(); } } } ``` `ERoadType` possible values are: Motorways, StateRoad, Road, Street, Cycleway, Path, SingleTrack. `ESurfaceType` possible values are: Asphalt, Paved, Unpaved, Unknown. ![](/docs/android/assets/images/example_android_route_profile1-70286625435ec7075489059bf58b9419.png) **Route profile chart** ![](/docs/android/assets/images/example_android_route_profile2-54285f32c7f962bdee095d6ae4aa73fa.png) **Route profile sections**
#### Get the route segments and instructions[​](#get-the-route-segments-and-instructions "Direct link to Get the route segments and instructions") Once a route has been successfully computed, you can retrieve a detailed list of its `segments`. Each segment represents the portion of the route between two consecutive waypoints and includes its own set of `route instructions`. For instance, if a route is computed with five waypoints, it will consist of four segments, each with distinct instructions. In the case of public transit routes, segments can represent either pedestrian paths or public transit sections. Here's an example of how to access and use this information, focusing on some key `RouteInstruction` properties: | Field | Type | Explanation | | ----------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------ | | traveledTimeDistance | TimeDistance? | Time and distance from the beginning of the route. | | remainingTravelTimeDistance | TimeDistance? | Time and distance to the end of the route. | | coordinates | Coordinates? | The coordinates indicating the location of the instruction. | | remainingTravelTimeDistanceToNextWaypoint | TimeDistance? | Time and distance until the next waypoint. | | timeDistanceToNextTurn | TimeDistance? | Time and distance until the next instruction. | | turnDetails | TurnDetails? | Get full details for the turn. | | turnInstruction | String? | Get textual description for the turn. | | roadInfo | ArrayList\? | Get road information. | | hasRoadInfo | Boolean | Check if road information is available. | | followRoadInstruction | String? | Get textual description for the follow road information. | | hasFollowRoadInfo | Boolean | Check if follow road information is available. | | countryCodeISO | String? | Get ISO 3166-1 alpha-3 country code for the navigation instruction. | | hasSignpostInfo | Boolean | Check if signpost information is available. | | signpostInstruction | String? | Get textual description for the signpost information. | | signpostDetails | SignpostDetails? | Get extended signpost details. | | hasTurnInfo | Boolean | Check if turn information is available. | | turnImage | Image? | Get turn image. The user is responsible to check if the image is valid. | | realisticNextTurnImage | AbstractGeometryImage? | Get customizable image for the realistic turn information. The user is responsible to check if the image is valid. | | roadInfoImage | RoadInfoImage? | Get customizable road image. The user is responsible to check if the image is valid. | | isFerry | Boolean | Returns true if the route instruction is a ferry. | | isTollRoad | Boolean | Returns true if the route instruction is a toll road. | | isCommon | Boolean | Check if this instruction is of common type. | ![](/docs/android/assets/images/example_android_route_instructions-b63ede328c9eae67dc10e4accff18f65.png) **List containing route instructions** Data from the instruction list above is obtained via the following properties of `RouteInstruction`: * `turnInstruction` : Bear left onto A 5. * `followRoadInstruction` : Follow A 5 for 132m. * `traveledTimeDistance?.totalDistance` : 6.2km. (after formatting to km) * `turnDetails?.abstractGeometryImage?.asBitmap()` : Instruction image or null when image is invalid. --- ### Handling RoutePreferences |

Before computing a route, we need to specify some route options using the `RoutePreferences` class. #### Route Preferences structure[​](#route-preferences-structure "Direct link to Route Preferences structure") ##### Core Routing Properties[​](#core-routing-properties "Direct link to Core Routing Properties") | Property | Type | Default Value | Explanation | | -------------------------------- | ------------------------ | -------------------------------- | --------------------------------------------------------------------------- | | **Basic Route Configuration** | | | | | routeType | ERouteType | ERouteType.Fastest | Preferred route type (Fastest, Shortest, Economic, Scenic). | | transportMode | ERouteTransportMode | ERouteTransportMode.Car | Transport mode (Car, Lorry, Pedestrian, Bicycle, Public, SharedVehicles). | | resultDetails | ERouteResultDetails | ERouteResultDetails.Full | Level of details in the route result (Full, TimeDistance, Path). | | alternativesSchema | ERouteAlternativesSchema | ERouteAlternativesSchema.Default | Schema for alternative routes (Default, Never, Always). | | pathAlgorithm | ERoutePathAlgorithm | ERoutePathAlgorithm.MagicEarth | Algorithm used for path calculation (MagicEarth, ExternalCh). | | **Route Constraints** | | | | | maximumDistanceConstraint | Boolean | true | Enables maximum distance constraints based on transport and result details. | | alternativeRoutesBalancedSorting | Boolean | true | Balances sorting of alternative routes. | | accurateTrackMatch | Boolean | true | Enables accurate track matching for routes. | | ignoreRestrictionsOverTrack | Boolean | false | Ignores map restrictions in route-over-track mode. | | **Timing and Schedule** | | | | | timestamp | Time? | null (automatic) | Custom timestamp for PT routes (departure/arrival time). | | isAutomaticTimestamp() | Boolean | true | Returns if timestamp is set to automatic mode. | | setIsAutomaticTimestamp() | Function | - | Sets timestamp to automatic mode for PT routes. | | **Geofencing** | | | | | avoidGeofenceAreas | ArrayList\ | empty list | List of geofence area IDs to avoid during routing. | | stickInsideGeofenceAreas | ArrayList\ | empty list | List of geofence area IDs to stay inside during routing. | ##### Route Avoidance Options[​](#route-avoidance-options "Direct link to Route Avoidance Options") | Property | Type | Default Value | Explanation | | -------------------------- | ----------------- | ---------------------- | --------------------------------------------------- | | avoidMotorways | Boolean | false | Avoids motorways in the route. | | avoidTollRoads | Boolean | false | Avoids toll roads in the route. | | avoidFerries | Boolean | false | Avoids ferries in the route. | | avoidUnpavedRoads | Boolean | false | Avoids unpaved roads in the route. | | avoidCarpoolLanes | Boolean | false | Avoids carpool lanes. | | avoidTurnAroundInstruction | Boolean | false | Avoids turn-around instructions during navigation. | | avoidTraffic | ETrafficAvoidance | ETrafficAvoidance.None | Traffic avoidance strategy (None, All, Roadblocks). | | avoidBikingHillFactor | Float | 0.5 | Factor to avoid biking hills (0.0-1.0). | ##### Vehicle Profile Configuration[​](#vehicle-profile-configuration "Direct link to Vehicle Profile Configuration") | Property | Type | Default Value | Explanation | | ---------------------------- | -------------------- | ----------------------- | --------------------------------------------------------------------- | | **Car Profile** | | | | | carProfile | CarProfile? | null | Car-specific routing preferences (fuel type, mass, max speed). | | **Truck Profile** | | | | | truckProfile | TruckProfile? | null | Truck-specific routing preferences (dimensions, weight, axle load). | | **Electric Vehicle Profile** | | | | | evProfile | EVProfile? | null | Electric vehicle routing preferences (battery, charging, efficiency). | | **Bike Profiles** | | | | | bikeProfile | EBikeProfile | EBikeProfile.Road | Selected bike profile (Road, Cross, City, Mountain). | | eBikeProfile | ElectricBikeProfile? | null | Electric bike profile configuration. | | defaultEBikeProfile | ElectricBikeProfile? | null | Default electric bike profile. | | setBikeProfile() | Function | - | Sets bike profile with optional electric bike configuration. | | **Pedestrian Profile** | | | | | pedestrianProfile | EPedestrianProfile | EPedestrianProfile.Walk | Pedestrian profile (Walk, Hike). | ##### Public Transport Options[​](#public-transport-options "Direct link to Public Transport Options") | Property | Type | Default Value | Explanation | | ---------------------------- | ------------------ | --------------------------- | --------------------------------------------------------------------- | | algorithmType | EPTAlgorithmType | EPTAlgorithmType.Departure | Algorithm type for PT routing (Departure, Arrival). | | sortingStrategy | EPTSortingStrategy | EPTSortingStrategy.BestTime | Strategy for sorting PT routes (BestTime, LeastWalk, LeastTransfers). | | minimumTransferTimeInMinutes | Int | 1 | Minimum transfer time in minutes. | | maximumTransferTimeInMinutes | Int | 300 | Maximum transfer time in minutes. | | maximumWalkDistance | Int | 5000 | Maximum walking distance in meters. | | routeTypePreferences | Int | 0 (no preference) | PT route type preferences (bit flags for Bus, Underground, etc.). | | useBikes | Boolean | false | Enables use of bikes in PT routes. | | useWheelchair | Boolean | false | Enables wheelchair-friendly PT routes. | | routeGroupIdsEarlierLater | ArrayList\? | null | IDs for earlier/later route groups. | ##### Route Enhancement Features[​](#route-enhancement-features "Direct link to Route Enhancement Features") | Property | Type | Default Value | Explanation | | ------------------------------ | ---------------- | --------------- | --------------------------------------------------------------------- | | **Terrain Profile** | | | | | buildTerrainProfile | Boolean | false | Enables building of terrain profile. | | setBuildTerrainProfile() | Function | - | Sets terrain profile build with optional minimum elevation variation. | | **Route Connections** | | | | | setBuildConnections() | Function | - | Enables building of route connections with max length. | | getBuildConnections() | Function | - | Returns if route connections building is enabled. | | getBuildConnectionsMaxLength() | Function | - | Returns maximum connection length in meters. | | **Route Ranges (Isochrones)** | | | | | routeRanges | ArrayList\? | null | Route ranges for isochrone calculation. | | routeRangesQuality | Int | 100 | Quality level for route ranges (0-100). | | setRouteRanges() | Function | - | Sets route ranges with quality level. | | **Departure Heading** | | | | | departureHeading | Double | -1 (no heading) | Departure heading in degrees (0-360, -1 = no heading). | | setDepartureHeading() | Function | - | Sets departure heading with accuracy. | ##### Emergency Vehicle Options[​](#emergency-vehicle-options "Direct link to Emergency Vehicle Options") | Property | Type | Default Value | Explanation | | ------------------------- | -------- | ------------- | ------------------------------------------------------ | | emergencyVehicleMode | Boolean | false | Enables emergency vehicle mode (read-only). | | setEmergencyVehicleMode() | Function | - | Sets emergency vehicle mode with extra freedom levels. | ##### Read-Only Properties[​](#read-only-properties "Direct link to Read-Only Properties") | Property | Type | Default Value | Explanation | | --------------- | ---------------- | --------------------- | ----------------------------------------------- | | routeResultType | ERouteResultType | ERouteResultType.Path | Type of route result (Path, Range) - read-only. | info For **timestamp** usage with Public Transport routes, the SDK supports both automatic and manual timestamp setting. When `isAutomaticTimestamp()` returns true, the departure timestamp is set to the local time of the departing waypoint. * Kotlin * Java ```kotlin // Set automatic timestamp (default behavior) routePreferences.setIsAutomaticTimestamp() // Set custom timestamp val customTime = Time().apply { // Set your desired departure/arrival time setLocalTime() // or set specific time values } routePreferences.timestamp = customTime // Check if automatic timestamp is enabled val isAutomatic = routePreferences.isAutomaticTimestamp() ``` ```java // Set automatic timestamp (default behavior) routePreferences.setIsAutomaticTimestamp(); // Set custom timestamp Time customTime = new Time(); // Set your desired departure/arrival time customTime.setLocalTime(); // or set specific time values routePreferences.setTimestamp(customTime); // Check if automatic timestamp is enabled boolean isAutomatic = routePreferences.isAutomaticTimestamp(); ``` ##### Basic Usage Example[​](#basic-usage-example "Direct link to Basic Usage Example") A short example of how to configure basic route preferences for the fastest car route with terrain profile: * Kotlin * Java ```kotlin val routePreferences = RoutePreferences().apply { // Basic route configuration transportMode = ERouteTransportMode.Car routeType = ERouteType.Fastest resultDetails = ERouteResultDetails.Full // Enable terrain profile calculation setBuildTerrainProfile(true, 5.0f) // 5m minimum elevation variation // Avoid certain road types avoidMotorways = false avoidTollRoads = true avoidFerries = false // Set traffic avoidance avoidTraffic = ETrafficAvoidance.All } ``` ```java RoutePreferences routePreferences = new RoutePreferences(); // Basic route configuration routePreferences.setTransportMode(ERouteTransportMode.Car); routePreferences.setRouteType(ERouteType.Fastest); routePreferences.setResultDetails(ERouteResultDetails.Full); // Enable terrain profile calculation routePreferences.setBuildTerrainProfile(true, 5.0f); // 5m minimum elevation variation // Avoid certain road types routePreferences.setAvoidMotorways(false); routePreferences.setAvoidTollRoads(true); routePreferences.setAvoidFerries(false); // Set traffic avoidance routePreferences.setAvoidTraffic(ETrafficAvoidance.All); ``` #### Profiles structure[​](#profiles-structure "Direct link to Profiles structure") ##### Car Profile[​](#car-profile "Direct link to Car Profile") The `CarProfile` class is responsible for defining car specific routing preferences. The available options are presented in the following table: | Member | Type | Default | Description | | -------- | --------- | -------------------------------- | ------------------------- | | fuelType | EFuelType | EFuelType.Petrol | Engine fuel type | | mass | Int | 0 - not considered in routing. | Vehicle mass in kg. | | maxSpeed | Double | 0.0 - not considered in routing. | Vehicle max speed in m/s. | `EFuelType` can have the following values: `Petrol`, `Diesel`, `LPG` (liquid petroleum gas), `Electric`. By default, all fields except `fuelType` have default value 0, meaning they are not considered in the routing. `fuelType` by default is `EFuelType.Petrol`. * Kotlin * Java ```kotlin val carProfile = CarProfile( fuelType = EFuelType.Diesel, mass = 1500, // kg maxSpeed = 50.0 // m/s ) ``` ```java CarProfile carProfile = new CarProfile( EFuelType.Diesel, 1500, // kg 50.0 // m/s ); ``` ##### Truck Profile[​](#truck-profile "Direct link to Truck Profile") The `TruckProfile` class is responsible for defining truck specific routing preferences. The available options are presented in the following table: | Member | Type | Default | Description | | -------- | ------ | ------------------------------- | ------------------------- | | axleLoad | Int | 0 - not considered in routing | Truck axle load in kg. | | height | Int | 0 - not considered in routing | Truck height in cm. | | length | Int | 0 - not considered in routing | Truck length in cm. | | mass | Int | 0 - not considered in routing | Vehicle mass in kg. | | maxSpeed | Double | 0.0 - not considered in routing | Vehicle max speed in m/s. | | width | Int | 0 - not considered in routing | Truck width in cm. | * Kotlin * Java ```kotlin val truckProfile = TruckProfile( massKg = 3000, heightCm = 350, lengthCm = 1200, widthCm = 250, axleLoadKg = 11000, maxSpeedMs = 25.0 ) ``` ```java TruckProfile truckProfile = new TruckProfile( 3000, // massKg 350, // heightCm 1200, // lengthCm 250, // widthCm 11000, // axleLoadKg 25.0 // maxSpeedMs ); ``` ##### Electric Vehicle Profile[​](#electric-vehicle-profile "Direct link to Electric Vehicle Profile") The `EVProfile` class is responsible for defining electric vehicle specific routing preferences. The available options are presented in the following table: | Member | Type | Default | Description | | ------------------- | ------- | ------- | --------------------------------------------------------------- | | name | String? | null | Car model name. | | towbarPossible | Int | 0 | Maximum weight available on vehicle towbar. | | ports | Int | 0 | Supported charging ports (combination of EEVChargingConnector). | | departureSoc | Float | 0.0 | Departure battery state of charge (0-1). | | destinationSoc | Float | 0.0 | Destination min battery state of charge (0-1). | | chargerDestSoc | Float | 0.0 | Charger destination min battery state of charge (0-1). | | chargerDepSoc | Float | 0.0 | Charger departure max battery state of charge (0-1). | | chargerOverheadMins | Int | 0 | Charger time overhead in minutes. | | batteryHealth | Float | 0.0 | Battery health (0-1, where 1 is brand new). | | batteryCapacity | Int | 0 | Battery capacity in watt hours. | | vehicleRange | Int | 0 | Vehicle range in meters. | | efficiency | Int | 0 | Consumption in Wh/km. | | fastCharge | Int | 0 | How many km charged in one hour (10-80 interval). | ##### Electric Bike Profile[​](#electric-bike-profile "Direct link to Electric Bike Profile") The `ElectricBikeProfile` class is responsible for defining electric bike specific routing preferences. The available options are presented in the following table: | Member | Type | Default | Description | | ----------------------- | ---------- | --------------------------- | ----------------------------------------------------------- | | type | EEBikeType | EEBikeType.None | E-bike type (None, Pedelec, PowerOnDemand). | | bikeMass | Float | 0.0 - default value is used | Bike mass in kg. | | bikerMass | Float | 0.0 - default value is used | Biker mass in kg. | | auxConsumptionDay | Float | 0.0 - default value is used | Bike auxiliary power consumption during day in Watts. | | auxConsumptionNight | Float | 0.0 - default value is used | Bike auxiliary power consumption during night in Watts. | | refSpeed | Float | 0.0 - default value is used | Reference speed in m/s. | | ignoreLegalRestrictions | Boolean | false | Ignore country-based legal restrictions related to e-bikes. | * Kotlin * Java ```kotlin val electricBikeProfile = ElectricBikeProfile( bikeType = EEBikeType.Pedelec, bikeMassKg = 25.0f, bikerMassKg = 70.0f, auxConsDayW = 10.0f, auxConsNightW = 15.0f ).apply { ignoreLegalRestrictions = false } // Set bike profile with electric bike configuration routePreferences.setBikeProfile(EBikeProfile.City, electricBikeProfile) ``` ```java ElectricBikeProfile electricBikeProfile = new ElectricBikeProfile( EEBikeType.Pedelec, 25.0f, // bikeMassKg 70.0f, // bikerMassKg 10.0f, // auxConsDayW 15.0f // auxConsNightW ); electricBikeProfile.setIgnoreLegalRestrictions(false); // Set bike profile with electric bike configuration routePreferences.setBikeProfile(EBikeProfile.City, electricBikeProfile); ``` #### Computing truck routes[​](#computing-truck-routes "Direct link to Computing truck routes") To compute routes for trucks we can write code like the following by initializing the `truckProfile` field of `RoutePreferences`: * Kotlin * Java ```kotlin // Define the departure landmark val departureLandmark = Landmark().apply { coordinates = Coordinates(48.87126, 2.33787) } // Define the destination landmark val destinationLandmark = Landmark().apply { coordinates = Coordinates(51.4739, -0.0302) } // Create truck profile val truckProfile = TruckProfile( massKg = 3000, // kg heightCm = 180, // cm lengthCm = 500, // cm widthCm = 200, // cm axleLoadKg = 1500, // kg maxSpeedMs = 16.67 // m/s (60 km/h) ) // Define the route preferences with truck profile and lorry transport mode val routePreferences = RoutePreferences().apply { this.truckProfile = truckProfile transportMode = ERouteTransportMode.Lorry // <- This field is crucial } // Create waypoints list val waypoints = arrayListOf(departureLandmark, destinationLandmark) // Create routing service and calculate route val routingService = RoutingService( preferences = routePreferences, onCompleted = { routes, errorCode, hint -> if (errorCode == GemError.Success) { // Handle successful route calculation println("Number of routes: ${routes.size}") } else { // Handle error println("Error calculating route: $errorCode - $hint") } } ) val result = routingService.calculateRoute(waypoints) ``` ```java // Define the departure landmark Landmark departureLandmark = new Landmark(); departureLandmark.setCoordinates(new Coordinates(48.87126, 2.33787)); // Define the destination landmark Landmark destinationLandmark = new Landmark(); destinationLandmark.setCoordinates(new Coordinates(51.4739, -0.0302)); // Create truck profile TruckProfile truckProfile = new TruckProfile( 3000, // massKg 180, // heightCm 500, // lengthCm 200, // widthCm 1500, // axleLoadKg 16.67 // maxSpeedMs (60 km/h) ); // Define the route preferences with truck profile and lorry transport mode RoutePreferences routePreferences = new RoutePreferences(); routePreferences.setTruckProfile(truckProfile); routePreferences.setTransportMode(ERouteTransportMode.Lorry); // <- This field is crucial // Create waypoints list ArrayList waypoints = new ArrayList<>(); waypoints.add(departureLandmark); waypoints.add(destinationLandmark); // Create routing service and calculate route RoutingService routingService = new RoutingService( routePreferences, (routes, errorCode, hint) -> { if (errorCode == GemError.Success) { // Handle successful route calculation System.out.println("Number of routes: " + routes.size()); } else { // Handle error System.out.println("Error calculating route: " + errorCode + " - " + hint); } } ); int result = routingService.calculateRoute(waypoints); ``` #### Computing caravan routes[​](#computing-caravan-routes "Direct link to Computing caravan routes") Certain vehicles, such as caravans or trailers, may be restricted on some roads due to their size or weight, yet still permitted on roads where trucks are prohibited. To calculate routes for caravans or trailers, we can use the `truckProfile` field of `RoutePreferences` with the appropriate dimensions and weight. * Kotlin * Java ```kotlin // Define the departure landmark val departureLandmark = Landmark().apply { coordinates = Coordinates(48.87126, 2.33787) } // Define the destination landmark val destinationLandmark = Landmark().apply { coordinates = Coordinates(51.4739, -0.0302) } // Create caravan profile using TruckProfile val caravanProfile = TruckProfile( heightCm = 180, // cm lengthCm = 500, // cm widthCm = 200, // cm axleLoadKg = 1500 // kg ) // Define the route preferences with caravan profile and car transport mode val routePreferences = RoutePreferences().apply { this.truckProfile = caravanProfile transportMode = ERouteTransportMode.Car // <- This field is crucial to distinguish caravan from truck } // Create waypoints list val waypoints = arrayListOf(departureLandmark, destinationLandmark) // Create routing service and calculate route val routingService = RoutingService( preferences = routePreferences, onCompleted = { routes, errorCode, hint -> // Handle results } ) val result = routingService.calculateRoute(waypoints) ``` ```java // Define the departure landmark Landmark departureLandmark = new Landmark(); departureLandmark.setCoordinates(new Coordinates(48.87126, 2.33787)); // Define the destination landmark Landmark destinationLandmark = new Landmark(); destinationLandmark.setCoordinates(new Coordinates(51.4739, -0.0302)); // Create caravan profile using TruckProfile TruckProfile caravanProfile = new TruckProfile( 0, // massKg 180, // heightCm 500, // lengthCm 200, // widthCm 1500, // axleLoadKg 0.0 // maxSpeedMs ); // Define the route preferences with caravan profile and car transport mode RoutePreferences routePreferences = new RoutePreferences(); routePreferences.setTruckProfile(caravanProfile); routePreferences.setTransportMode(ERouteTransportMode.Car); // <- This field is crucial to distinguish caravan from truck // Create waypoints list ArrayList waypoints = new ArrayList<>(); waypoints.add(departureLandmark); waypoints.add(destinationLandmark); // Create routing service and calculate route RoutingService routingService = new RoutingService( routePreferences, (routes, errorCode, hint) -> { // Handle results } ); int result = routingService.calculateRoute(waypoints); ``` At least one of the fields `height`, `length`, `width` or `axleLoad` must be set to a non-zero value in order for the settings to be taken into account during routing. If all these fields are set to 0 then a normal car route will be calculated. #### Computing electric vehicle routes[​](#computing-electric-vehicle-routes "Direct link to Computing electric vehicle routes") To compute routes for electric vehicles, you can use the `evProfile` field of `RoutePreferences`: * Kotlin * Java ```kotlin // Create EV profile val evProfile = EVProfile().apply { name = "Tesla Model 3" departureSoc = 0.8f // 80% charge at departure destinationSoc = 0.2f // Minimum 20% charge at destination batteryCapacity = 75000 // 75 kWh in Wh vehicleRange = 500000 // 500 km in meters efficiency = 150 // 150 Wh/km fastCharge = 250 // 250 km per hour charging ports = EEVChargingConnector.Type2.value or EEVChargingConnector.CSS2.value } // Define the route preferences with EV profile val routePreferences = RoutePreferences().apply { this.evProfile = evProfile transportMode = ERouteTransportMode.Car } ``` ```java // Create EV profile EVProfile evProfile = new EVProfile(); evProfile.setName("Tesla Model 3"); evProfile.setDepartureSoc(0.8f); // 80% charge at departure evProfile.setDestinationSoc(0.2f); // Minimum 20% charge at destination evProfile.setBatteryCapacity(75000); // 75 kWh in Wh evProfile.setVehicleRange(500000); // 500 km in meters evProfile.setEfficiency(150); // 150 Wh/km evProfile.setFastCharge(250); // 250 km per hour charging evProfile.setPorts(EEVChargingConnector.Type2.getValue() | EEVChargingConnector.CSS2.getValue()); // Define the route preferences with EV profile RoutePreferences routePreferences = new RoutePreferences(); routePreferences.setEvProfile(evProfile); routePreferences.setTransportMode(ERouteTransportMode.Car); ``` #### Setting departure heading[​](#setting-departure-heading "Direct link to Setting departure heading") You can specify a departure heading to influence the initial direction of the route: * Kotlin * Java ```kotlin val routePreferences = RoutePreferences().apply { // Set departure heading to 45 degrees with 25 degrees accuracy setDepartureHeading(45.0, 25.0) } ``` ```java RoutePreferences routePreferences = new RoutePreferences(); // Set departure heading to 45 degrees with 25 degrees accuracy routePreferences.setDepartureHeading(45.0, 25.0); ``` #### Building terrain profile and connections[​](#building-terrain-profile-and-connections "Direct link to Building terrain profile and connections") To get detailed terrain information and route connections: * Kotlin * Java ```kotlin val routePreferences = RoutePreferences().apply { // Enable terrain profile with minimum elevation variation setBuildTerrainProfile(true, 5.0f) // 5 meters minimum variation // Enable route connections with maximum connection length setBuildConnections(true, 1000) // 1000 meters max connection length } ``` ```java RoutePreferences routePreferences = new RoutePreferences(); // Enable terrain profile with minimum elevation variation routePreferences.setBuildTerrainProfile(true, 5.0f); // 5 meters minimum variation // Enable route connections with maximum connection length routePreferences.setBuildConnections(true, 1000); // 1000 meters max connection length ``` #### Working with route ranges (Isochrones)[​](#working-with-route-ranges-isochrones "Direct link to Working with route ranges (Isochrones)") To calculate isochrone areas (reachable areas within specific time/distance): * Kotlin * Java ```kotlin val routePreferences = RoutePreferences().apply { transportMode = ERouteTransportMode.Car routeType = ERouteType.Fastest // Set route ranges for isochrone calculation // For fastest routes: values are in seconds // For shortest routes: values are in meters val ranges = arrayListOf(300, 600, 900) // 5, 10, 15 minutes setRouteRanges(ranges, 80) // 80% quality } ``` ```java RoutePreferences routePreferences = new RoutePreferences(); routePreferences.setTransportMode(ERouteTransportMode.Car); routePreferences.setRouteType(ERouteType.Fastest); // Set route ranges for isochrone calculation // For fastest routes: values are in seconds // For shortest routes: values are in meters ArrayList ranges = new ArrayList<>(Arrays.asList(300, 600, 900)); // 5, 10, 15 minutes routePreferences.setRouteRanges(ranges, 80); // 80% quality ``` #### Public Transport routing[​](#public-transport-routing "Direct link to Public Transport routing") For public transport routes, you can configure specific PT preferences: * Kotlin * Java ```kotlin val routePreferences = RoutePreferences().apply { transportMode = ERouteTransportMode.Public algorithmType = EPTAlgorithmType.Departure sortingStrategy = EPTSortingStrategy.BestTime minimumTransferTimeInMinutes = 2 maximumTransferTimeInMinutes = 240 maximumWalkDistance = 1000 // 1 km useWheelchair = true useBikes = false // Set route type preferences (combine multiple types with bitwise OR) routeTypePreferences = EPTRouteTypePreference.Bus.value or EPTRouteTypePreference.Underground.value } ``` ```java RoutePreferences routePreferences = new RoutePreferences(); routePreferences.setTransportMode(ERouteTransportMode.Public); routePreferences.setAlgorithmType(EPTAlgorithmType.Departure); routePreferences.setSortingStrategy(EPTSortingStrategy.BestTime); routePreferences.setMinimumTransferTimeInMinutes(2); routePreferences.setMaximumTransferTimeInMinutes(240); routePreferences.setMaximumWalkDistance(1000); // 1 km routePreferences.setUseWheelchair(true); routePreferences.setUseBikes(false); // Set route type preferences (combine multiple types with bitwise OR) routePreferences.setRouteTypePreferences( EPTRouteTypePreference.Bus.getValue() | EPTRouteTypePreference.Underground.getValue() ); ``` #### Calculate round trips[​](#calculate-round-trips "Direct link to Calculate round trips") Generate routes that start and end at the same location using roundtrip parameters. **Key differences from normal routing:** * **Waypoint requirements** - Normal routes require at least two waypoints. Roundtrips need only one departure point; additional waypoints are ignored. The algorithm generates multiple output waypoints automatically * **Determinism** - Normal routing produces the same route when inputs remain unchanged. Roundtrip generation is random by design, creating different routes each time. Use a seed value for deterministic results * **Preferences** - All normal routing preferences apply, including vehicle profiles and fitness factors ##### Range types[​](#range-types "Direct link to Range types") The `ERangeType` enumeration specifies units for the `range` parameter: | ERangeType value | Description | | ---------------- | ------------------------------------------------------------------------------- | | Default | Uses `DistanceBased` for shortest routes; `TimeBased` for all other route types | | DistanceBased | Distance measured in meters | | TimeBased | Duration measured in seconds | | (EnergyBased) | Not currently supported | Tip Specify `DistanceBased` or `TimeBased` explicitly to avoid confusion. A value of 10,000 means 10 km for distance-based requests but ~3 hours for time-based requests. ##### Roundtrip parameters[​](#roundtrip-parameters "Direct link to Roundtrip parameters") Configure roundtrips using **RoundTripParameters**: | Parameter | Type | Description | | ---------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------- | | range | Int | Target length or duration. The algorithm approximates this value but does not guarantee exact matches | | rangeType | ERangeType | Units for the range value (distance or time) | | randomSeed | Int | Set to `0` for random generation each time. Any other value produces deterministic results when other parameters remain unchanged | ##### Create a round trip route[​](#create-a-round-trip-route "Direct link to Create a round trip route") Set `roundTripParameters` in **RoutePreferences** with a non-zero `range` value: * Kotlin * Java ```kotlin // Define the departure landmark val departureLandmark = Landmark().apply { coordinates = Coordinates(48.85682, 2.34375) } // Define round trip preferences val tripPreferences = RoundTripParameters( range = 5000, rangeType = ERangeType.DistanceBased, randomSeed = 0 // 0 for random generation, other values for deterministic results ) // Define route preferences to include round trip parameters val routePreferences = RoutePreferences().apply { transportMode = ERouteTransportMode.Bicycle roundTripParameters = tripPreferences } // Create routing service and calculate round trip route // Use only the departure landmark - additional waypoints are ignored val routingService = RoutingService( preferences = routePreferences, onCompleted = { routes, errorCode, hint -> if (errorCode == GemError.Success) { // Handle successful round trip calculation println("Number of routes: ${routes.size}") } else { // Handle error println("Error calculating round trip: $errorCode - $hint") } } ) val waypoints = arrayListOf(departureLandmark) val result = routingService.calculateRoute(waypoints) ``` ```java // Define the departure landmark Landmark departureLandmark = new Landmark(); departureLandmark.setCoordinates(new Coordinates(48.85682, 2.34375)); // Define round trip preferences RoundTripParameters tripPreferences = new RoundTripParameters( 5000, ERangeType.DistanceBased, 0 // 0 for random generation, other values for deterministic results ); // Define route preferences to include round trip parameters RoutePreferences routePreferences = new RoutePreferences(); routePreferences.setTransportMode(ERouteTransportMode.Bicycle); routePreferences.setRoundTripParameters(tripPreferences); // Create routing service and calculate round trip route // Use only the departure landmark - additional waypoints are ignored RoutingService routingService = new RoutingService( routePreferences, (routes, errorCode, hint) -> { if (errorCode == GemError.Success) { // Handle successful round trip calculation System.out.println("Number of routes: " + routes.size()); } else { // Handle error System.out.println("Error calculating round trip: " + errorCode + " - " + hint); } } ); ArrayList waypoints = new ArrayList<>(); waypoints.add(departureLandmark); int result = routingService.calculateRoute(waypoints); ``` danger If more than one waypoint is provided in a round trip calculation, only the first is considered; others are ignored. --- ### Search The SDK supports several search methods including text search, proximity search, and category-based search, with customizable preferences like fuzzy matching and distance limits. Additionally, users can search for custom landmarks or within overlays. For geocoding, the SDK provides reverse geocoding, converting geographic coordinates into comprehensive address details, and geocoding, which allows locating specific places based on address components. The SDK also offers integration with Wikipedia for location-based content, and auto-suggestions to dynamically generate search results while typing. #### [📄️ Getting started with Search](/docs/android/guides/search/get-started-search.md) [The Maps SDK for Android provides a flexible and robust search functionality, allowing the search of locations using text queries and coordinates:](/docs/android/guides/search/get-started-search.md) #### [📄️ Search & Geocoding features](/docs/android/guides/search/search-geocoding-features.md) [The Maps SDK for Android provides geocoding and reverse geocoding capabilities. Key features include:](/docs/android/guides/search/search-geocoding-features.md) --- ### Getting started with Search |

The Maps SDK for Android provides a flexible and robust search functionality, allowing the search of locations using text queries and coordinates: * Text Search: Perform searches using a text query and geographic coordinates to prioritize results within a specific area. * Search Preferences: Customize search behavior using various options, such as allowing fuzzy results, limiting search distance, or specifying the number of results. * Category-Based Search: Filter search results by predefined categories, such as gas stations or parking areas. * Proximity Search: Retrieve all nearby landmarks without specifying a text query. #### Text search[​](#text-search "Direct link to Text search") The simplest way to search for something is by providing text and specifying coordinates. The coordinates serve as a hint, prioritizing points of interest (POIs) within the indicated area. * Kotlin * Java ```kotlin val textFilter = "Paris" val reference = Coordinates(45.0, 10.0) val preferences = SearchPreferences().apply { maxMatches = 40 allowFuzzyResults = true } val searchService = SearchService( preferences = preferences, onCompleted = { results, errorCode, hint -> // If there is an error or there aren't any results, the method will return an empty list. when (errorCode) { GemError.NoError -> { if (results.isEmpty()) { Log.d("SearchService", "No results") } else { Log.d("SearchService", "Number of results: ${results.size}") } } else -> { Log.e("SearchService", "Error: ${GemError.getMessage(errorCode)}") } } } ) val result = searchService.searchByFilter(textFilter, reference) ``` ```java String textFilter = "Paris"; Coordinates reference = new Coordinates(45.0, 10.0); SearchPreferences preferences = new SearchPreferences(); preferences.setMaxMatches(40); preferences.setAllowFuzzyResults(true); SearchService searchService = new SearchService( preferences, (results, errorCode, hint) -> { // If there is an error or there aren't any results, the method will return an empty list. if (errorCode == GemError.NoError) { if (results.isEmpty()) { Log.d("SearchService", "No results"); } else { Log.d("SearchService", "Number of results: " + results.size()); } } else { Log.e("SearchService", "Error: " + GemError.getMessage(errorCode)); } } ); int result = searchService.searchByFilter(textFilter, reference); ``` info The `searchByFilter` method returns an error code. If the search fails to initialize, the error details will be delivered through the `onCompleted` callback function. The `errorCode` provided by the callback function can have the following values: | Value | Significance | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `GemError.NoError` | successfully completed | | `GemError.Cancel` | cancelled by the user | | `GemError.NoMemory` | search engine couldn't allocate the necessary memory for the operation | | `GemError.OperationTimeout` | search was executed on the online service and the operation took too much time to complete (usually more than 1 min, depending on the server overload state) | | `GemError.NetworkTimeout` | can't establish the connection or the server didn't respond on time | | `GemError.NetworkFailed` | search was executed on the online service and the operation failed due to bad network connection | #### Specifying preferences[​](#specifying-preferences "Direct link to Specifying preferences") As seen in the previous example, before searching we need to specify some `SearchPreferences`. The following characteristics apply to a search: | Field | Type | Default Value | Explanation | | ---------------------- | ------- | ------------- | ------------------------------------------------------------------------------------ | | allowFuzzyResults | Boolean | true | Allows fuzzy search results, enabling approximate matches for queries. | | exactMatchEnabled | Boolean | false | Restricts results to only those that exactly match the query. | | maxMatches | Int | 40 | Specifies the maximum number of search results to return. | | searchAddressesEnabled | Boolean | true | Includes addresses in the search results. This option also includes roads. | | searchMapPOIsEnabled | Boolean | true | Includes points of interest (POIs) on the map in the search results. | | searchOnlyOnboard | Boolean | false | Limits the search to onboard (offline) data only. | | thresholdDistance | Int | 2147483647 | Defines the maximum distance (in meters) for search results from the query location. | ##### Search by category[​](#search-by-category "Direct link to Search by category") The Maps SDK for Android allows the user to filter results based on the category. Some predefined categories are available and can be accessed using `EGenericCategoriesIDs`. In the following example, we perform a search for a text query, limiting the results to gas stations: * Kotlin * Java ```kotlin val textFilter = "Paris" val reference = Coordinates(45.0, 10.0) val preferences = SearchPreferences().apply { maxMatches = 40 allowFuzzyResults = true searchMapPOIsEnabled = true searchAddressesEnabled = false } val searchService = SearchService( preferences = preferences, onCompleted = { results, errorCode, hint -> when (errorCode) { GemError.NoError -> { if (results.isEmpty()) { Log.d("SearchService", "No results") } else { Log.d("SearchService", "Number of results: ${results.size}") } } else -> { Log.e("SearchService", "Error: ${GemError.getMessage(errorCode)}") } } } ) val result = searchService.searchByFilter( textFilter, reference, arrayListOf(EGenericCategoriesIDs.GasStation) ) ``` ```java String textFilter = "Paris"; Coordinates reference = new Coordinates(45.0, 10.0); SearchPreferences preferences = new SearchPreferences(); preferences.setMaxMatches(40); preferences.setAllowFuzzyResults(true); preferences.setSearchMapPOIsEnabled(true); preferences.setSearchAddressesEnabled(false); SearchService searchService = new SearchService( preferences, (results, errorCode, hint) -> { if (errorCode == GemError.NoError) { if (results.isEmpty()) { Log.d("SearchService", "No results"); } else { Log.d("SearchService", "Number of results: " + results.size()); } } else { Log.e("SearchService", "Error: " + GemError.getMessage(errorCode)); } } ); ArrayList categories = new ArrayList<>(); categories.add(EGenericCategoriesIDs.GasStation); int result = searchService.searchByFilter(textFilter, reference, categories); ``` Tip Set the `searchAddressesEnabled` to false in order to filter non-relevant results. ##### Search on custom landmarks[​](#search-on-custom-landmarks "Direct link to Search on custom landmarks") By default all search methods operate on the landmarks provided by default on the map. You can enable search functionality for custom landmarks by creating a landmark store containing the desired landmarks and adding it to the search preferences. * Kotlin * Java ```kotlin // Create the landmarks to be added val landmark1 = Landmark("My Custom Landmark1", Coordinates(25.0, 30.0)) val landmark2 = Landmark("My Custom Landmark2", Coordinates(25.005, 30.005)) // Create a store and add the landmarks val store = LandmarkStoreService.createLandmarkStore("LandmarksToBeSearched") store.addLandmark(landmark1) store.addLandmark(landmark2) // Add the store to the search preferences val preferences = SearchPreferences().apply { landmarkStores?.add(store) // If no results from the map POIs should be returned then searchMapPOIsEnabled should be set to false searchMapPOIsEnabled = false // If no results from the addresses should be returned then searchAddressesEnabled should be set to false searchAddressesEnabled = false } val searchService = SearchService( preferences = preferences, onCompleted = { results, errorCode, hint -> when (errorCode) { GemError.NoError -> { if (results.isEmpty()) { Log.d("SearchService", "No results") } else { Log.d("SearchService", "Number of results: ${results.size}") } } else -> { Log.e("SearchService", "Error: ${GemError.getMessage(errorCode)}") } } } ) val result = searchService.searchByFilter( "My Custom Landmark", Coordinates(25.003, 30.003) ) ``` ```java // Create the landmarks to be added Landmark landmark1 = new Landmark("My Custom Landmark1", new Coordinates(25.0, 30.0)); Landmark landmark2 = new Landmark("My Custom Landmark2", new Coordinates(25.005, 30.005)); // Create a store and add the landmarks LandmarkStore store = LandmarkStoreService.createLandmarkStore("LandmarksToBeSearched"); store.addLandmark(landmark1); store.addLandmark(landmark2); // Add the store to the search preferences SearchPreferences preferences = new SearchPreferences(); if (preferences.getLandmarkStores() != null) { preferences.getLandmarkStores().add(store); } // If no results from the map POIs should be returned then searchMapPOIsEnabled should be set to false preferences.setSearchMapPOIsEnabled(false); // If no results from the addresses should be returned then searchAddressesEnabled should be set to false preferences.setSearchAddressesEnabled(false); SearchService searchService = new SearchService( preferences, (results, errorCode, hint) -> { if (errorCode == GemError.NoError) { if (results.isEmpty()) { Log.d("SearchService", "No results"); } else { Log.d("SearchService", "Number of results: " + results.size()); } } else { Log.e("SearchService", "Error: " + GemError.getMessage(errorCode)); } } ); int result = searchService.searchByFilter( "My Custom Landmark", new Coordinates(25.003, 30.003) ); ``` danger The landmark store **retains** the landmarks added to it across sessions, until the app is **uninstalled**. This means a previously created landmark store with the same name might already exist in persistent storage and may contain pre-existing landmarks. For more details, refer to the [documentation on LandmarkStore](/docs/android/guides/core/landmarks.md#landmark-stores). Tip Set the `searchAddressesEnabled` and `searchMapPOIsEnabled` to false in order to filter non-relevant results. ##### Search on overlays[​](#search-on-overlays "Direct link to Search on overlays") You can perform searches on overlays by specifying the overlay ID. It is recommended to consult the [Overlay documentation](/docs/android/guides/core/overlays.md) for a deeper understanding and details about proper usage. In the example below, we demonstrate how to search within items from the safety overlay. Custom overlays can also be used, provided they are activated in the applied map style: * Kotlin * Java ```kotlin // Get the overlay id of safety val overlayId = ECommonOverlayIds.Safety.value // Add the overlay to the search preferences val preferences = SearchPreferences().apply { // We can set searchMapPOIsEnabled and searchAddressesEnabled to false if no results from the map POIs and addresses should be returned searchMapPOIsEnabled = false searchAddressesEnabled = false } val searchService = SearchService( preferences = preferences, onCompleted = { results, errorCode, hint -> when (errorCode) { GemError.NoError -> { if (results.isEmpty()) { Log.d("SearchService", "No results") } else { val mapView: MapView? = findViewById(R.id.gem_surface).mapview mapView?.centerOnCoordinates(results.first().coordinates) Log.d("SearchService", "Number of results: ${results.size}") } } else -> { Log.e("SearchService", "Error: ${GemError.getMessage(errorCode)}") } } } ) val result = searchService.searchByFilter( "Speed", Coordinates(48.76930, 2.34483) ) ``` ```java // Get the overlay id of safety int overlayId = ECommonOverlayIds.Safety.getValue(); // Add the overlay to the search preferences SearchPreferences preferences = new SearchPreferences(); // We can set searchMapPOIsEnabled and searchAddressesEnabled to false if no results from the map POIs and addresses should be returned preferences.setSearchMapPOIsEnabled(false); preferences.setSearchAddressesEnabled(false); SearchService searchService = new SearchService( preferences, (results, errorCode, hint) -> { if (errorCode == GemError.NoError) { if (results.isEmpty()) { Log.d("SearchService", "No results"); } else { MapView mapView = findViewById(R.id.gem_surface).getMapview(); if (mapView != null) { mapView.centerOnCoordinates(results.get(0).getCoordinates()); } Log.d("SearchService", "Number of results: " + results.size()); } } else { Log.e("SearchService", "Error: " + GemError.getMessage(errorCode)); } } ); int result = searchService.searchByFilter( "Speed", new Coordinates(48.76930, 2.34483) ); ``` In order to convert the returned `Landmark` to a `OverlayItem` use the `overlayItem` property of the `Landmark` class. This property returns the associated `OverlayItem` if available, null otherwise. Tip Set the `searchAddressesEnabled` and `searchMapPOIsEnabled` to false in order to filter non-relevant results. #### Search for location[​](#search-for-location "Direct link to Search for location") If you don't specify any text, all the landmarks in the closest proximity are returned, limited to `maxMatches`. * Kotlin * Java ```kotlin val reference = Coordinates(45.0, 10.0) val preferences = SearchPreferences().apply { maxMatches = 40 allowFuzzyResults = true } val searchService = SearchService( preferences = preferences, onCompleted = { results, errorCode, hint -> // If there is an error or there aren't any results, the method will return an empty list. when (errorCode) { GemError.NoError -> { if (results.isEmpty()) { Log.d("SearchService", "No results") } else { Log.d("SearchService", "Number of results: ${results.size}") } } else -> { Log.e("SearchService", "Error: ${GemError.getMessage(errorCode)}") } } } ) val result = searchService.searchAroundPosition(reference) ``` ```java Coordinates reference = new Coordinates(45.0, 10.0); SearchPreferences preferences = new SearchPreferences(); preferences.setMaxMatches(40); preferences.setAllowFuzzyResults(true); SearchService searchService = new SearchService( preferences, (results, errorCode, hint) -> { // If there is an error or there aren't any results, the method will return an empty list. if (errorCode == GemError.NoError) { if (results.isEmpty()) { Log.d("SearchService", "No results"); } else { Log.d("SearchService", "Number of results: " + results.size()); } } else { Log.e("SearchService", "Error: " + GemError.getMessage(errorCode)); } } ); int result = searchService.searchAroundPosition(reference); ``` To limit the search to a specific area, provide a `RectangleGeographicArea` to the optional `locationHint` parameter. * Kotlin * Java ```kotlin val reference = Coordinates(41.68905, -72.64296) val searchArea = RectangleGeographicArea( Coordinates(41.98846, -73.12412), // topLeft Coordinates(41.37716, -72.02342) // bottomRight ) val searchService = SearchService( preferences = SearchPreferences().apply { maxMatches = 400 }, locationHint = searchArea, onCompleted = { results, errorCode, hint -> // Handle results } ) val result = searchService.searchByFilter("N", reference) ``` ```java Coordinates reference = new Coordinates(41.68905, -72.64296); RectangleGeographicArea searchArea = new RectangleGeographicArea( new Coordinates(41.98846, -73.12412), // topLeft new Coordinates(41.37716, -72.02342) // bottomRight ); SearchPreferences preferences = new SearchPreferences(); preferences.setMaxMatches(400); SearchService searchService = new SearchService( preferences, searchArea, (results, errorCode, hint) -> { // Handle results } ); int result = searchService.searchByFilter("N", reference); ``` danger The reference coordinates used for search must be located within the `RectangleGeographicArea` provided to the `locationHint` parameter. Otherwise, the search will return an empty list. #### Show the results on the map[​](#show-the-results-on-the-map "Direct link to Show the results on the map") In most use cases the landmarks found by search are already present on the map. If the search was made on custom landmark stores see the [add map landmarks](/docs/android/guides/maps/display-map-items/display-landmarks.md#display-custom-landmarks) section for adding landmarks to the map. To zoom to a landmark found via search, we can use `MapView.centerOnCoordinates()` on the coordinates of the landmark found (`Landmark.coordinates`). See the documentation for [map centering](/docs/android/guides/maps/adjust-map.md#map-centering) for more info. #### Change the language of the results[​](#change-the-language-of-the-results "Direct link to Change the language of the results") The language of search results and category names is determined by the `SdkSettings.language` setting. Check the [the internationalization guide](/docs/android/guides/todo.md) section for more details. --- ### Search & Geocoding features |

The Maps SDK for Android provides geocoding and reverse geocoding capabilities. Key features include: * Reverse Geocoding: Transform geographic coordinates into comprehensive address details, such as country, city, street name, postal code, and more. * Geocoding: Locate specific places (e.g., cities, streets, or house numbers) based on address components. * Route-Based Search: Perform searches along predefined routes to identify landmarks and points of interest. * Wikipedia Integration: Access Wikipedia descriptions and related information for identified landmarks. * Auto-Suggestion Implementation: Dynamically generate search suggestions for Android UI components. #### Reverse geocode coordinates to address[​](#reverse-geocode-coordinates-to-address "Direct link to Reverse geocode coordinates to address") Given a coordinate, we can get the corresponding address by searching around the given location and getting the `AddressInfo` associated with the nearest Landmark found nearby. The AddressInfo provides information about the country, city, street name, street number, postal code, state, district, country code, and other relevant information. Fields from an `AddressInfo` object can be accessed via the `getField` method or can be automatically converted to a string containing the address info using the `format` method. * Kotlin * Java ```kotlin val preferences = SearchPreferences().apply { thresholdDistance = 50 } val coordinates = Coordinates(51.519305, -0.128022) val searchService = SearchService( preferences = preferences, onCompleted = { results, errorCode, hint -> when (errorCode) { GemError.NoError -> { if (results.isEmpty()) { Log.d("SearchService", "No results found") } else { val landmark = results.first() val addressInfo = landmark.addressInfo val country = addressInfo?.getField(EAddressField.Country) val city = addressInfo?.getField(EAddressField.City) val street = addressInfo?.getField(EAddressField.StreetName) val streetNumber = addressInfo?.getField(EAddressField.StreetNumber) val fullAddress = addressInfo?.format() Log.d("SearchService", "Address: $fullAddress") } } else -> { Log.e("SearchService", "Error: ${GemError.getMessage(errorCode)}") } } } ) val result = searchService.searchAroundPosition(coordinates) ``` ```java SearchPreferences preferences = new SearchPreferences(); preferences.setThresholdDistance(50); Coordinates coordinates = new Coordinates(51.519305, -0.128022); SearchService searchService = new SearchService( preferences, (results, errorCode, hint) -> { if (errorCode == GemError.NoError) { if (results.isEmpty()) { Log.d("SearchService", "No results found"); } else { Landmark landmark = results.get(0); AddressInfo addressInfo = landmark.getAddressInfo(); String country = addressInfo != null ? addressInfo.getField(EAddressField.Country) : null; String city = addressInfo != null ? addressInfo.getField(EAddressField.City) : null; String street = addressInfo != null ? addressInfo.getField(EAddressField.StreetName) : null; String streetNumber = addressInfo != null ? addressInfo.getField(EAddressField.StreetNumber) : null; String fullAddress = addressInfo != null ? addressInfo.format() : null; Log.d("SearchService", "Address: " + fullAddress); } } else { Log.e("SearchService", "Error: " + GemError.getMessage(errorCode)); } } ); int result = searchService.searchAroundPosition(coordinates); ``` #### Geocode address to location[​](#geocode-address-to-location "Direct link to Geocode address to location") The Maps SDK for Android provides geocoding capabilities to convert addresses into geographic coordinates. Addresses represent a tree-like structure, where each node is a `Landmark` with a specific `EAddressDetailLevel`. At the root of the tree, we have the country-level landmarks, followed by other levels such as cities, streets, and house numbers. danger The address structure is not the same in all countries. For example, some countries do not have states or provinces. Use the `getNextAddressDetailLevel` method from the `GuidedAddressSearchService` class to get the next available levels in the address hierarchy. ##### Search countries by name[​](#search-countries-by-name "Direct link to Search countries by name") It is possible to do a search at a country level. In this case you can use code like the following: * Kotlin * Java ```kotlin val guidedSearchService = GuidedAddressSearchService( onCompleted = { results, errorCode, hint -> when (errorCode) { GemError.NoError, GemError.ReducedResult -> { if (results.isEmpty()) { Log.d("GuidedSearch", "No results") } else { // do something with results Log.d("GuidedSearch", "Found ${results.size} countries") } } else -> { Log.e("GuidedSearch", "Error: ${GemError.getMessage(errorCode)}") } } } ) // Search for countries by name - this will be handled by creating a search method // that uses the general search functionality for country-level results ``` ```java GuidedAddressSearchService guidedSearchService = new GuidedAddressSearchService( (results, errorCode, hint) -> { if (errorCode == GemError.NoError || errorCode == GemError.ReducedResult) { if (results.isEmpty()) { Log.d("GuidedSearch", "No results"); } else { // do something with results Log.d("GuidedSearch", "Found " + results.size() + " countries"); } } else { Log.e("GuidedSearch", "Error: " + GemError.getMessage(errorCode)); } } ); // Search for countries by name - this will be handled by creating a search method // that uses the general search functionality for country-level results ``` This can provide the parent landmark for the other `GuidedAddressSearchService` methods. It does a search restricted to country-level results. This allows the use of more flexible search terms as it works regardless of the language. ##### Search the hierarchical address structure[​](#search-the-hierarchical-address-structure "Direct link to Search the hierarchical address structure") We will create the example in two steps. First, we create a function that for a parent landmark, an `EAddressDetailLevel` and a text returns the children having the required detail level and matching the text. The possible values for `EAddressDetailLevel` are: NoDetail, Country, State, County, District, City, Settlement, PostalCode, Street, StreetSection, StreetLane, StreetAlley, HouseNumber, Crossing. * Kotlin * Java ```kotlin // Address search method using coroutines suspend fun searchAddress( landmark: Landmark, detailLevel: EAddressDetailLevel, text: String ): Landmark? = suspendCoroutine { continuation -> val guidedSearchService = GuidedAddressSearchService( onCompleted = { results, errorCode, hint -> when (errorCode) { GemError.NoError, GemError.ReducedResult -> { if (results.isEmpty()) { continuation.resume(null) } else { continuation.resume(results.first()) } } else -> { continuation.resume(null) } } } ) guidedSearchService.search(landmark, text, detailLevel) } ``` ```java // Address search method using CompletableFuture CompletableFuture searchAddress( Landmark landmark, EAddressDetailLevel detailLevel, String text ) { CompletableFuture future = new CompletableFuture<>(); GuidedAddressSearchService guidedSearchService = new GuidedAddressSearchService( (results, errorCode, hint) -> { if (errorCode == GemError.NoError || errorCode == GemError.ReducedResult) { if (results.isEmpty()) { future.complete(null); } else { future.complete(results.get(0)); } } else { future.complete(null); } } ); guidedSearchService.search(landmark, text, detailLevel); return future; } ``` Using the function above, we can look for the children landmarks following the conditions: * Kotlin * Java ```kotlin // Use coroutines in a lifecycle-aware scope lifecycleScope.launch { val guidedSearchService = GuidedAddressSearchService() val countryLandmark = guidedSearchService.getCountryLevelItem("ESP") if (countryLandmark == null) { Log.e("GuidedSearch", "Country not found") return@launch } Log.d("GuidedSearch", "Country: ${countryLandmark.name}") // Use the address search to get a landmark for a city in Spain (e.g., Barcelona). val cityLandmark = searchAddress( landmark = countryLandmark, detailLevel = EAddressDetailLevel.City, text = "Barcelona" ) if (cityLandmark == null) { Log.e("GuidedSearch", "City not found") return@launch } Log.d("GuidedSearch", "City: ${cityLandmark.name}") // Use the address search to get a predefined street's landmark in the city (e.g., Carrer de Mallorca). val streetLandmark = searchAddress( landmark = cityLandmark, detailLevel = EAddressDetailLevel.Street, text = "Carrer de Mallorca" ) if (streetLandmark == null) { Log.e("GuidedSearch", "Street not found") return@launch } Log.d("GuidedSearch", "Street: ${streetLandmark.name}") // Use the address search to get a predefined house number's landmark on the street (e.g., House Number 401). val houseNumberLandmark = searchAddress( landmark = streetLandmark, detailLevel = EAddressDetailLevel.HouseNumber, text = "401" ) if (houseNumberLandmark == null) { Log.e("GuidedSearch", "House number not found") return@launch } Log.d("GuidedSearch", "House number: ${houseNumberLandmark.name}") } ``` ```java // Use CompletableFuture chaining GuidedAddressSearchService guidedSearchService = new GuidedAddressSearchService(); Landmark countryLandmark = guidedSearchService.getCountryLevelItem("ESP"); if (countryLandmark == null) { Log.e("GuidedSearch", "Country not found"); return; } Log.d("GuidedSearch", "Country: " + countryLandmark.getName()); // Use the address search to get a landmark for a city in Spain (e.g., Barcelona). searchAddress(countryLandmark, EAddressDetailLevel.City, "Barcelona") .thenAccept(cityLandmark -> { if (cityLandmark == null) { Log.e("GuidedSearch", "City not found"); return; } Log.d("GuidedSearch", "City: " + cityLandmark.getName()); // Use the address search to get a predefined street's landmark in the city (e.g., Carrer de Mallorca). searchAddress(cityLandmark, EAddressDetailLevel.Street, "Carrer de Mallorca") .thenAccept(streetLandmark -> { if (streetLandmark == null) { Log.e("GuidedSearch", "Street not found"); return; } Log.d("GuidedSearch", "Street: " + streetLandmark.getName()); // Use the address search to get a predefined house number's landmark on the street (e.g., House Number 401). searchAddress(streetLandmark, EAddressDetailLevel.HouseNumber, "401") .thenAccept(houseNumberLandmark -> { if (houseNumberLandmark == null) { Log.e("GuidedSearch", "House number not found"); return; } Log.d("GuidedSearch", "House number: " + houseNumberLandmark.getName()); }); }); }); ``` The `getCountryLevelItem` method returns the root node corresponding to the specified country based on the provided country code. If the country code is invalid, the function will return null. #### Geocode location to Wikipedia[​](#geocode-location-to-wikipedia "Direct link to Geocode location to Wikipedia") A useful functionality when looking for something, is to obtain the content of a Wikipedia page for a specific text filter. This can be done by a normal search, followed by a call to `ExternalInfo.requestWikiInfo`. * Kotlin * Java ```kotlin val externalInfo = ExternalInfo() val progressListener = object : ProgressListener() { override fun notifyComplete(errorCode: ErrorCode, hint: String) { when (errorCode) { GemError.NoError -> { val title = externalInfo.wikiPageTitle val description = externalInfo.wikiPageDescription val url = externalInfo.wikiPageURL Log.d("Wikipedia", "Title: $title") Log.d("Wikipedia", "Description: $description") Log.d("Wikipedia", "URL: $url") } else -> { Log.e("Wikipedia", "Error getting Wikipedia info: ${GemError.getMessage(errorCode)}") } } } } // Check if landmark has Wikipedia info and request it if (externalInfo.hasWikiInfo(landmark)) { externalInfo.requestWikiInfo(landmark, progressListener) } else { Log.d("Wikipedia", "No Wikipedia info available for this landmark") } ``` ```java ExternalInfo externalInfo = new ExternalInfo(); ProgressListener progressListener = new ProgressListener() { @Override public void notifyComplete(int errorCode, String hint) { if (errorCode == GemError.NoError) { String title = externalInfo.getWikiPageTitle(); String description = externalInfo.getWikiPageDescription(); String url = externalInfo.getWikiPageURL(); Log.d("Wikipedia", "Title: " + title); Log.d("Wikipedia", "Description: " + description); Log.d("Wikipedia", "URL: " + url); } else { Log.e("Wikipedia", "Error getting Wikipedia info: " + GemError.getMessage(errorCode)); } } }; // Check if landmark has Wikipedia info and request it if (externalInfo.hasWikiInfo(landmark)) { externalInfo.requestWikiInfo(landmark, progressListener); } else { Log.d("Wikipedia", "No Wikipedia info available for this landmark"); } ``` See the [Location Wikipedia guide](/docs/android/guides/todo.md) for more info. #### Get auto-suggestions[​](#get-auto-suggestions "Direct link to Get auto-suggestions") Auto-suggestions can be implemented by calling `SearchService.searchByFilter` with the text filter set to the current text from the Android search widget when the field value is changed. A simple example is demonstrated below: * Kotlin * Java ```kotlin class SearchActivity : AppCompatActivity() { private lateinit var searchView: SearchView private var currentSearchService: SearchService? = null private val searchHandler = Handler(Looper.getMainLooper()) private var searchRunnable: Runnable? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) searchView = findViewById(R.id.search_view) setupSearchView() } private fun setupSearchView() { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { // Handle search submission return true } override fun onQueryTextChange(newText: String?): Boolean { // Cancel previous search and debounce new searches searchRunnable?.let { searchHandler.removeCallbacks(it) } searchRunnable = Runnable { getAutoSuggestions(newText ?: "") } searchHandler.postDelayed(searchRunnable!!, 300) // 300ms debounce return true } }) } private fun getAutoSuggestions(query: String) { if (query.isEmpty()) return Log.d("AutoSuggestion", "New auto suggestion search for: $query") // Cancel previous search currentSearchService?.cancelSearch() val refCoordinates = Coordinates(48.0, 2.0) // Or use current position val preferences = SearchPreferences().apply { allowFuzzyResults = true maxMatches = 10 } currentSearchService = SearchService( preferences = preferences, onCompleted = { results, errorCode, hint -> when (errorCode) { GemError.NoError -> { displaySuggestions(results) Log.d("AutoSuggestion", "Got ${results.size} results for: $query") } GemError.Cancel -> { Log.d("AutoSuggestion", "Search cancelled for: $query") } else -> { Log.e("AutoSuggestion", "Search error for $query: ${GemError.getMessage(errorCode)}") } } } ) currentSearchService?.searchByFilter(query, refCoordinates) } private fun displaySuggestions(landmarks: LandmarkList) { // Update your RecyclerView adapter or suggestion list here // This is where you would populate your suggestions UI for (landmark in landmarks) { Log.d("Suggestion", "Landmark: ${landmark.name}") // Add to suggestions adapter } } } ``` ```java public class SearchActivity extends AppCompatActivity { private SearchView searchView; private SearchService currentSearchService; private final Handler searchHandler = new Handler(Looper.getMainLooper()); private Runnable searchRunnable; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); searchView = findViewById(R.id.search_view); setupSearchView(); } private void setupSearchView() { searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { // Handle search submission return true; } @Override public boolean onQueryTextChange(String newText) { // Cancel previous search and debounce new searches if (searchRunnable != null) { searchHandler.removeCallbacks(searchRunnable); } searchRunnable = () -> getAutoSuggestions(newText != null ? newText : ""); searchHandler.postDelayed(searchRunnable, 300); // 300ms debounce return true; } }); } private void getAutoSuggestions(String query) { if (query.isEmpty()) return; Log.d("AutoSuggestion", "New auto suggestion search for: " + query); // Cancel previous search if (currentSearchService != null) { currentSearchService.cancelSearch(); } Coordinates refCoordinates = new Coordinates(48.0, 2.0); // Or use current position SearchPreferences preferences = new SearchPreferences(); preferences.setAllowFuzzyResults(true); preferences.setMaxMatches(10); currentSearchService = new SearchService( preferences, (results, errorCode, hint) -> { if (errorCode == GemError.NoError) { displaySuggestions(results); Log.d("AutoSuggestion", "Got " + results.size() + " results for: " + query); } else if (errorCode == GemError.Cancel) { Log.d("AutoSuggestion", "Search cancelled for: " + query); } else { Log.e("AutoSuggestion", "Search error for " + query + ": " + GemError.getMessage(errorCode)); } } ); currentSearchService.searchByFilter(query, refCoordinates); } private void displaySuggestions(LandmarkList landmarks) { // Update your RecyclerView adapter or suggestion list here // This is where you would populate your suggestions UI for (Landmark landmark : landmarks) { Log.d("Suggestion", "Landmark: " + landmark.getName()); // Add to suggestions adapter } } } ``` The preferences with `allowFuzzyResults` set to true allows for partial match during search. The `refCoordinates` might be replaced with a more suitable value, such as the user current position or the map viewport center. #### Search along a route[​](#search-along-a-route "Direct link to Search along a route") It is possible also to do a search along a route, not based on some coordinates. In this case you can use code like the following: * Kotlin * Java ```kotlin val searchService = SearchService( onCompleted = { results, errorCode, hint -> when (errorCode) { GemError.NoError -> { if (results.isEmpty()) { Log.d("RouteSearch", "No results") } else { Log.d("RouteSearch", "Results size: ${results.size}") for (landmark in results) { // do something with landmarks Log.d("RouteSearch", "Found: ${landmark.name}") } } } else -> { Log.e("RouteSearch", "No results found: ${GemError.getMessage(errorCode)}") } } } ) val result = searchService.searchAlongRoute(route) ``` ```java SearchService searchService = new SearchService( (results, errorCode, hint) -> { if (errorCode == GemError.NoError) { if (results.isEmpty()) { Log.d("RouteSearch", "No results"); } else { Log.d("RouteSearch", "Results size: " + results.size()); for (Landmark landmark : results) { // do something with landmarks Log.d("RouteSearch", "Found: " + landmark.getName()); } } } else { Log.e("RouteSearch", "No results found: " + GemError.getMessage(errorCode)); } } ); int result = searchService.searchAlongRoute(route); ``` We can set a custom value for `SearchPreferences.thresholdDistance` in order to specify the maximum distance to the route for the landmarks to be searched. Other `SearchPreferences` fields can be specified depending on the usecase. --- ### Settings Service |

The Magic Lane SDK for Android provides functionality for storing key-value pairs in permanent storage. This is managed via the `SettingsService` class. The settings are saved within a `.ini` file. #### Create a Settings Service[​](#create-a-settings-service "Direct link to Create a Settings Service") You can create or open a settings storage using the factory method of `SettingsService`. If no path is provided, a default one will be used: * Kotlin * Java ```kotlin val settings = SettingsService.produce(null) ``` ```java SettingsService settings = SettingsService.produce(null); ``` Or a custom path can be provided: * Kotlin * Java ```kotlin val settings = SettingsService.produce("/custom/settings/path") ``` ```java SettingsService settings = SettingsService.produce("/custom/settings/path"); ``` You can also access the current file path where the settings are stored: * Kotlin * Java ```kotlin val currentPath = settings?.path ``` ```java String currentPath = settings != null ? settings.getPath() : null; ``` #### Add and get values[​](#add-and-get-values "Direct link to Add and get values") You can store various types of data using appropriate set methods: * Kotlin * Java ```kotlin settings?.setStringValue("username", "john_doe") settings?.setBooleanValue("isLoggedIn", true) settings?.setIntValue("launchCount", 5) settings?.setLongValue("highScore", 1234567890123L) settings?.setDoubleValue("volume", 0.75) ``` ```java if (settings != null) { settings.setStringValue("username", "john_doe"); settings.setBooleanValue("isLoggedIn", true); settings.setIntValue("launchCount", 5); settings.setLongValue("highScore", 1234567890123L); settings.setDoubleValue("volume", 0.75); } ``` To retrieve values, use the corresponding get methods. These methods take an optional `defaultValue` parameter which is returned when the key could not be found in the selected group. The `defaultValue` does not set the value. * Kotlin * Java ```kotlin val username = settings?.getStringValue("username", "guest") val isLoggedIn = settings?.getBooleanValue("isLoggedIn", false) val launchCount = settings?.getIntValue("launchCount", 0) val highScore = settings?.getLongValue("highScore", 0L) val volume = settings?.getDoubleValue("volume", 1.0) ``` ```java String username = settings != null ? settings.getStringValue("username", "guest") : "guest"; boolean isLoggedIn = settings != null ? settings.getBooleanValue("isLoggedIn", false) : false; int launchCount = settings != null ? settings.getIntValue("launchCount", 0) : 0; long highScore = settings != null ? settings.getLongValue("highScore", 0L) : 0L; double volume = settings != null ? settings.getDoubleValue("volume", 1.0) : 1.0; ``` When the set is made on a type and the get is made on another type, a conversion is done. For example: * Kotlin * Java ```kotlin settings?.setIntValue("count", 1234) val value = settings?.getStringValue("count") // Returns "1234" ``` ```java if (settings != null) { settings.setIntValue("count", 1234); } String value = settings != null ? settings.getStringValue("count") : null; // Returns "1234" ``` Tip Each change may take up to one second to be written to storage. Use the `flush` method to ensure the changes are written to permanent storage. #### Groups[​](#groups "Direct link to Groups") Groups allow you to organize settings in logical units. The default group is `DEFAULT`. Only one group can be active at a time, and nested groups are not allowed. * Kotlin * Java ```kotlin // All operations above this are made inside DEFAULT settings?.beginGroup("USER_PREFERENCES") // All operations here are made inside USER_PREFERENCES settings?.beginGroup("OTHER_SETTINGS") // All operations here are made inside OTHER_SETTINGS settings?.endGroup() //All operations below this are made inside DEFAULT ``` ```java // All operations above this are made inside DEFAULT if (settings != null) { settings.beginGroup("USER_PREFERENCES"); } // All operations here are made inside USER_PREFERENCES if (settings != null) { settings.beginGroup("OTHER_SETTINGS"); } // All operations here are made inside OTHER_SETTINGS if (settings != null) { settings.endGroup(); } // All operations below this are made inside DEFAULT ``` In order to get the current group use the `groupName` property: * Kotlin * Java ```kotlin val currentGroup = settings?.groupName ``` ```java String currentGroup = settings != null ? settings.getGroupName() : null; ``` danger The values passed to `beginGroup` are converted to upper-case. Tip A `flush` is automatically done after the group is changed. #### Remove values[​](#remove-values "Direct link to Remove values") ##### Remove value by key[​](#remove-value-by-key "Direct link to Remove value by key") The `remove` method takes the key (or a pattern) and returns the number of deleted entries from the current group. * Kotlin * Java ```kotlin val removedCount = settings?.remove("username") ``` ```java int removedCount = settings != null ? settings.remove("username") : 0; ``` ##### Clear all values[​](#clear-all-values "Direct link to Clear all values") Use the `clear` method which removes all the settings from all the groups. * Kotlin * Java ```kotlin settings?.clear() ``` ```java if (settings != null) { settings.clear(); } ``` --- ### Social reports |

Social reports are user-generated alerts about real-time driving conditions or incidents on the road. These reports can include various types of information, such as accidents, police presence, road construction and more. Users can create new reports, with the possibility to provide information such as category, name, image and other parameters. They can provide feedback, enabling voting on the accuracy of incidents and commenting on reported events. They can confirm or deny the validity of reports, delete their own reports, and contribute additional comments for further context. These interactions help enhance the accuracy and reliability of the information shared within the community. Social reports are visible by all users with the social overlay enabled, given a compatible map style. #### Report categories[​](#report-categories "Direct link to Report categories") The following categories and subcategories are provided in the form of a hierarchical structure, based on the `OverlayCategory` class: ```text ┌ Police Car (id 256) │ - My Side (id 264) │ - Opposite Side (id 272) │ - Both Sides (280) └■ ┌ Fixed Camera (id 512) │ - My Side (id 520) │ - Opposite Side (id 528) │ - Both Sides (536) └■ ┌ Traffic (id 768) │ - Moderate (id 776) │ - Heavy (id 784) │ - Standstill (792) └■ ┌ Crash (id 1024) │ - My Side (id 1032) │ - Opposite Side (id 1040) └■ ┌ Crash (id 1024) │ - My Side (id 1032) │ - Opposite Side (id 1040) └■ ┌ Road Hazard (id 1280) │ - Pothole (id 1288) │ - Constructions (id 1296) │ - Animals (id 1312) │ - Object on Road (id 1328) │ - Vehicle Stopped on Road (id 1344) └■ ┌ Weather Hazard (id 1536) │ - Fog (id 1544) │ - Ice on Road (id 1552) │ - Flood (id 1560) │ - Hail (id 1568) └■ ┌ Road Closure (id 3072) │ - My Side (id 3080) │ - Opposite Side (id 3088) │ - Both Sides (id 3096) └■ ``` The main categories and subcategories can be retrieved via the following snippet: * Kotlin * Java ```kotlin val overlayInfo = SocialOverlay.reportsOverlayInfo val categories = overlayInfo?.categories categories?.forEach { category -> Log.d("SocialReports", "Category name: ${category.name}") Log.d("SocialReports", "Category id: ${category.uid}") category.subcategories?.forEach { subCategory -> Log.d("SocialReports", "Subcategory name: ${subCategory.name}") Log.d("SocialReports", "Subcategory id: ${subCategory.uid}") } } ``` ```java OverlayInfo overlayInfo = SocialOverlay.getReportsOverlayInfo(); List categories = overlayInfo != null ? overlayInfo.getCategories() : null; if (categories != null) { for (OverlayCategory category : categories) { Log.d("SocialReports", "Category name: " + category.getName()); Log.d("SocialReports", "Category id: " + category.getUid()); List subcategories = category.getSubcategories(); if (subcategories != null) { for (OverlayCategory subCategory : subcategories) { Log.d("SocialReports", "Subcategory name: " + subCategory.getName()); Log.d("SocialReports", "Subcategory id: " + subCategory.getUid()); } } } } ``` More details about the `OverlayCategory` class structure can be found in the [Overlay documentation](/docs/android/guides/core/overlays.md). #### Uploading a Social Report[​](#uploading-a-social-report "Direct link to Uploading a Social Report") Before uploading a social report, it must first be prepared. The `SocialOverlay` class provides methods like `prepareReporting` and `prepareReportingWithCoordinates` to handle the report preparation phase. The `prepareReporting` method takes a category ID and uses the current user's location or a specified data source, while `prepareReportingWithCoordinates` accepts both a category ID and a `Coordinates` entity, enabling reporting from a different location. Those methods return an integer, called `prepareId` which is later passed to `report` method, in order to upload a social overlay item. The following code snippet performs prepare and report of a social overlay item: * Kotlin * Java ```kotlin // Get the reporting id (uses current position) val prepareIdOrError = SocialOverlay.prepareReporting() if (prepareIdOrError <= 0) { // Handle error Log.e("SocialReports", "Prepare error: ${GemError.getMessage(prepareIdOrError)}") return } // Get the subcategory id val overlayInfo = SocialOverlay.reportsOverlayInfo ?: return val countryISOCode = MapDetails().isoCodeForCurrentPosition ?: return val categories = overlayInfo.getCategories(countryISOCode) ?: return val mainCategory = categories.first() val subcategories = mainCategory.subcategories ?: return val subCategory = subcategories.first() // Create progress listener val progressListener = ProgressListener.create( onCompleted = { errorCode, hint -> Log.d("SocialReports", "Report result error: ${GemError.getMessage(errorCode)}") } ) // Report val error = SocialOverlay.report( prepareId = prepareIdOrError, categoryId = subCategory.uid, listener = progressListener ) ``` ```java // Get the reporting id (uses current position) int prepareIdOrError = SocialOverlay.prepareReporting(); if (prepareIdOrError <= 0) { // Handle error Log.e("SocialReports", "Prepare error: " + GemError.getMessage(prepareIdOrError)); return; } // Get the subcategory id OverlayInfo overlayInfo = SocialOverlay.getReportsOverlayInfo(); if (overlayInfo == null) return; String countryISOCode = new MapDetails().getIsoCodeForCurrentPosition(); if (countryISOCode == null) return; List categories = overlayInfo.getCategories(countryISOCode); if (categories == null) return; OverlayCategory mainCategory = categories.get(0); List subcategories = mainCategory.getSubcategories(); if (subcategories == null) return; OverlayCategory subCategory = subcategories.get(0); // Create progress listener ProgressListener progressListener = ProgressListener.create( (errorCode, hint) -> { Log.d("SocialReports", "Report result error: " + GemError.getMessage(errorCode)); } ); // Report int error = SocialOverlay.report( prepareIdOrError, subCategory.getUid(), progressListener ); ``` If you want to use a location from a specific data source, you can pass the data source to the `prepareReporting` method, as shown below: * Kotlin * Java ```kotlin // Assuming 'ds' is an instance of a valid DataSource val prepareIdOrError = SocialOverlay.prepareReporting(dataSource = ds) ``` ```java // Assuming 'ds' is an instance of a valid DataSource int prepareIdOrError = SocialOverlay.prepareReporting(ds); ``` danger `prepareReporting` must be called with a `DataSource` whose position data is classified as **high accuracy** by the map-matching system (the only exception is the `Weather Hazard` category, `categId = 1536`, which does not require accurate positioning). If a high-accuracy data source is not provided, the method will return `GemError.notFound` and the report cannot be created. The report is displayed for a limited duration before being automatically removed. The report result will be provided via the `onCompleted` callback, with the following `GemError` values: * `InvalidInput` if the category id is invalid/ the parameters are ill formatted or if the snapshot is an invalid image. * `Suspended` if the rate limit for the user is exceeded. * `Expired` if the prepared report is too old. * `NotFound` if no accurate data source is detected. * `NoError` if the operation has succeeded The method returns an error code which can be checked with `GemError.isError()`. If the operation could not be started, the method returns an error code. danger Most report categories require the use of the `prepareReporting` method, ensuring higher report accuracy by confirming the user's proximity to the reported location. See the [Get started with Positioning](/docs/android/guides/positioning/get-started-positioning.md) guide for more information about configuring the data source. The `prepareReportingWithCoordinates` method works only for `Weather Hazard` categories and subcategories contained within. danger While reporting events, the `prepareReporting` method needs to be in preparing mode (categoryId=0) rather than dry run mode (categoryId != 0). Tip The `report` function accepts the following optional parameters: * `snapshot`: Used to provide an image for the report. This should be an `Image` object. * `description`: A string description for the report. * `params`: A `ParameterList` configuration object that allows further customization of the report details. These parameters are optional and can be omitted if not needed. #### Updating a Social Report[​](#updating-a-social-report "Direct link to Updating a Social Report") In order to update an existing report's parameters the `SocialOverlay.updateReport(item, params, listener)` method can be used as shown below: * Kotlin * Java ```kotlin val overlays = mapView.cursorSelectionOverlayItems val overlayItem = overlays?.firstOrNull() ?: return val params = overlayItem.getPreviewData() ?: return // Find and update specific parameters as needed val locationParam = params.find { it.key == "location_address" } locationParam?.run { val updatedParam = Parameter(key!!, "New address",name ) assign(updatedParam) } val progressListener = ProgressListener.create( onCompleted = { errorCode, hint -> Log.d("SocialReports", "Update result error: ${GemError.getMessage(errorCode)}") } ) val error = SocialOverlay.updateReport( item = overlayItem, params = params, listener = progressListener ) ``` ```java List overlays = mapView.getCursorSelectionOverlayItems(); if (overlays == null || overlays.isEmpty()) return; OverlayItem overlayItem = overlays.get(0); ParameterList params = overlayItem.getPreviewData(); if (params == null) return; // Find and update specific parameters as needed Parameter locationParam = null; for (Parameter p : params) { if ("location_address".equals(p.getKey())) { locationParam = p; break; } } if (locationParam != null) { Parameter updatedParam = new Parameter(locationParam.getKey(), "New address", locationParam.getName()); locationParam.assign(updatedParam); } ProgressListener progressListener = ProgressListener.create( (errorCode, hint) -> { Log.d("SocialReports", "Update result error: " + GemError.getMessage(errorCode)); } ); int error = SocialOverlay.updateReport( overlayItem, params, progressListener ); ``` The structure of the `ParameterList` object passed to the `updateReport` method should follow the structure returned by the `OverlayItem`'s `getPreviewData()`. The keys of the fields accepted can be found inside predefined overlay parameter constants. The `updateReport` method might provide the following `GemError` values via the `onCompleted` callback: * `InvalidInput` if the `ParameterList`'s structure is incorrect. * `NoError` if the operation has been successfully completed. Tip A user can obtain a report `OverlayItem` through the following methods: * **Map Selection**: By selecting an item directly from the map using the `cursorSelectionOverlayItems()` method provided by the `MapView`. * **Search**: By performing a search that includes preferences configured to return overlay items. * **Proximity Alerts**: Via the `AlarmListener`, when approaching a report that triggers an alert. #### Deleting a Social Report[​](#deleting-a-social-report "Direct link to Deleting a Social Report") This is accomplished through `SocialOverlay.deleteReport(overlayItem, listener)`, with the restriction that only the original creator of the report has the authority to delete it. The `deleteReport` method might provide the following `GemError` values on the `onCompleted` method: * `InvalidInput` if the item is not a social report overlay item or not the result of an alarm notification. * `AccessDenied` if the user does not have the required rights. * `NoError` if the operation was successfully completed. #### Interact with a Social Report[​](#interact-with-a-social-report "Direct link to Interact with a Social Report") ##### Provide positive feedback[​](#provide-positive-feedback "Direct link to Provide positive feedback") Users can provide positive feedback for a reported event using `SocialOverlay.confirmReport(overlayItem, listener)`, which increases the value of the score key within the `OverlayItem` preview data. ##### Provide negative feedback[​](#provide-negative-feedback "Direct link to Provide negative feedback") If a report is found to be inaccurate, it can be denied by other users using `SocialOverlay.denyReport(overlayItem, listener)`, which accepts an `OverlayItem` object as its parameter. If a report gets many downvotes it will be removed. Both `confirmReport` and `denyReport` will provide the following `GemError` values using the `onCompleted` callback: * `InvalidInput` if the item is not a social report overlay item or not the result of an alarm notification. * `AccessDenied` if the user already voted. * `NoError` if the operation was successfully completed. ##### Add comment[​](#add-comment "Direct link to Add comment") Additionally, users can contribute comments to a reported event by calling `SocialOverlay.addComment`, as shown below: * Kotlin * Java ```kotlin val progressListener = ProgressListener.create( onCompleted = { errorCode, hint -> Log.d("SocialReports", "Add comment result error: ${GemError.getMessage(errorCode)}") } ) val error = SocialOverlay.addComment( item = overlayItem, comment = "This is a comment", listener = progressListener ) ``` ```java ProgressListener progressListener = ProgressListener.create( (errorCode, hint) -> { Log.d("SocialReports", "Add comment result error: " + GemError.getMessage(errorCode)); } ); int error = SocialOverlay.addComment( overlayItem, "This is a comment", progressListener ); ``` Added comments can be viewed within the `OverlayItem`'s preview data The `addComment` method will provide the following `GemError` values on the `onCompleted` callback: * `InvalidInput` if the item is not a social report overlay item or not the result of an alarm notification. * `ConnectionRequired` if no internet connection is available. * `Busy` if another comment operation is in progress. * `NoError` if the comment was added. #### Get updates about a report[​](#get-updates-about-a-report "Direct link to Get updates about a report") A user may be interested in tracking changes to a report - whether they made changes or not. For this purpose, the `SocialOverlayItemListener` class can be used. To create and register a listener for a specific `OverlayItem` report, refer to the following snippet: * Kotlin * Java ```kotlin val listener = SocialOverlayItemListener.create( onOverlayItemUpdated = { report -> Log.d("SocialReports", "The report has been updated") } ) SocialOverlay.registerOverlayItemListener(overlayItem , listener) ``` ```java SocialOverlayItemListener listener = SocialOverlayItemListener.create( report -> { Log.d("SocialReports", "The report has been updated"); } ); SocialOverlay.registerOverlayItemListener(overlayItem, listener); ``` The `registerOverlayItemListener` method does not return an error code. The registration happens directly without validation errors. In order to unregister the listener: * Kotlin * Java ```kotlin SocialOverlay.unregisterOverlayItemListener(overlayItem, listener) ``` ```java SocialOverlay.unregisterOverlayItemListener(overlayItem, listener); ``` The `unregisterOverlayItemListener` method does not return an error code. The unregistration happens directly. --- ### Timezone service |

The `TimezoneService` provides functionality for managing and retrieving time zone information. It allows you to transform a UTC `Time` to a specific time zone, but also provides other details regarding the offset and the timezone. The `TimezoneService` object provides methods to get timezone information online (more accurate and takes into account new changes) or offline (faster, works regardless of network access but may be outdated). * `getTimezoneInfoWithCoordinates` and `getTimezoneInfoWithTimezoneId` methods retrieve timezone information asynchronously. * Both methods support an `accurateResult` parameter that determines whether to use online services (more accurate) or offline data (faster). #### The TimezoneResult structure[​](#the-timezoneresult-structure "Direct link to The TimezoneResult structure") The `TimezoneResult` class represents the result of a time zone lookup operation. It contains the following properties: | Member | Type | Description | | ------------ | ----------- | ------------------------------------------------------------------------------------------------------ | | `dstOffset` | `Int` | Daylight Saving Time (DST) offset in seconds. | | `utcOffset` | `Int` | The raw UTC offset in seconds, excluding DST. Can be negative. | | `offset` | `Int` | The total offset in seconds in respect to UTC (`dstOffset` + `utcOffset`). Can be negative. | | `status` | `ETZStatus` | Status of the response. See the values below. | | `timezoneId` | `String?` | The timezone identifier in format `Continent/City_Name`. Examples: `Europe/Paris`, `America/New_York`. | | `localTime` | `Time?` | The local time as a `Time` object representing the local time of the requested timezone. | danger The `TimezoneResult` properties are read-only and can only be accessed after a successful timezone lookup operation. The `ETZStatus` enum represents the status of a time zone lookup operation. It can have the following values: * `Success` - the request was successful. * `InvalidCoordinate` - the provided geographic coordinates were invalid or out of range. * `WrongTimezoneId` - the provided timezone identifier was malformed or not recognized. * `WrongTimestamp` - the provided timestamp (Time) was invalid or could not be parsed. * `TimezoneNotFound` - no timezone could be found for the given input. #### Get timezone info by coordinates[​](#get-timezone-info-by-coordinates "Direct link to Get timezone info by coordinates") The `getTimezoneInfoWithCoordinates` method allows you to retrieve time zone information based on geographic coordinates (latitude and longitude) and a UTC `Time` object. This can be useful for applications that need to determine the local time zone for a specific location. ##### Basic usage example[​](#basic-usage-example "Direct link to Basic usage example") * Kotlin * Java ```kotlin val timezoneResult = TimezoneResult() val coordinates = Coordinates(55.626, 37.457) val utcTime = Time.getUniversalTime() // Current UTC time val timezoneListener = ProgressListener.create( onCompleted = { errorCode, _ -> if (errorCode == GemError.NoError) { // Success - access timezone data when (timezoneResult.status) { ETZStatus.Success -> { val timezoneId = timezoneResult.timezoneId val offset = timezoneResult.offset // Total offset in seconds val dstOffset = timezoneResult.dstOffset // DST offset in seconds val utcOffset = timezoneResult.utcOffset // UTC offset in seconds val localTime = timezoneResult.localTime // Use the timezone information //displayTimezoneInfo(timezoneId, offset, localTime) } ETZStatus.InvalidCoordinate -> { Log.e("TimezoneService", "Invalid coordinates provided") } ETZStatus.TimezoneNotFound -> { Log.e("TimezoneService", "No timezone found for location") } else -> { Log.e("TimezoneService", "Timezone lookup failed: ${timezoneResult.status}") } } } else { Log.e("TimezoneService", "Request failed: ${GemError.getMessage(errorCode)}") } }, postOnMain = true ) // Request timezone information // accurateResult = true: Use online service (more accurate, requires internet) // accurateResult = false: Use offline data (faster, may be outdated) val errorCode = TimezoneService.getTimezoneInfoWithCoordinates( timezoneResult = timezoneResult, coordinates = coordinates, time = utcTime!!, listener = timezoneListener, accurateResult = true ) if (GemError.isError(errorCode)) { Log.e("TimezoneService", "Failed to start timezone request: ${GemError.getMessage(errorCode)}") } ``` ```java TimezoneResult timezoneResult = new TimezoneResult(); Coordinates coordinates = new Coordinates(55.626, 37.457); Time utcTime = Time.getUniversalTime(); // Current UTC time ProgressListener timezoneListener = ProgressListener.create( (errorCode, hint) -> { if (errorCode == GemError.NoError) { // Success - access timezone data if (timezoneResult.getStatus() == ETZStatus.Success) { String timezoneId = timezoneResult.getTimezoneId(); int offset = timezoneResult.getOffset(); // Total offset in seconds int dstOffset = timezoneResult.getDstOffset(); // DST offset in seconds int utcOffset = timezoneResult.getUtcOffset(); // UTC offset in seconds Time localTime = timezoneResult.getLocalTime(); // Use the timezone information //displayTimezoneInfo(timezoneId, offset, localTime); } else if (timezoneResult.getStatus() == ETZStatus.InvalidCoordinate) { Log.e("TimezoneService", "Invalid coordinates provided"); } else if (timezoneResult.getStatus() == ETZStatus.TimezoneNotFound) { Log.e("TimezoneService", "No timezone found for location"); } else { Log.e("TimezoneService", "Timezone lookup failed: " + timezoneResult.getStatus()); } } else { Log.e("TimezoneService", "Request failed: " + GemError.getMessage(errorCode)); } }, true ); // Request timezone information // accurateResult = true: Use online service (more accurate, requires internet) // accurateResult = false: Use offline data (faster, may be outdated) int errorCode = TimezoneService.getTimezoneInfoWithCoordinates( timezoneResult, coordinates, utcTime, timezoneListener, true ); if (GemError.isError(errorCode)) { Log.e("TimezoneService", "Failed to start timezone request: " + GemError.getMessage(errorCode)); } ``` ##### Using offline data[​](#using-offline-data "Direct link to Using offline data") For faster results that don't require internet connectivity, use `accurateResult = false`: * Kotlin * Java ```kotlin val timezoneResult = TimezoneResult() val coordinates = Coordinates(55.626, 37.457) val utcTime = Time.getUniversalTime() val timezoneListener = ProgressListener.create( onCompleted = { errorCode, _ -> if (errorCode == GemError.NoError && timezoneResult.status == ETZStatus.Success) { // Process offline timezone result val localTime = timezoneResult.localTime val timezoneId = timezoneResult.timezoneId // Note: Offline data may be outdated } }, postOnMain = true ) // Use offline data for faster response TimezoneService.getTimezoneInfoWithCoordinates( timezoneResult, coordinates, utcTime!!, timezoneListener, accurateResult = false ) ``` ```java TimezoneResult timezoneResult = new TimezoneResult(); Coordinates coordinates = new Coordinates(55.626, 37.457); Time utcTime = Time.getUniversalTime(); ProgressListener timezoneListener = ProgressListener.create( (errorCode, hint) -> { if (errorCode == GemError.NoError && timezoneResult.getStatus() == ETZStatus.Success) { // Process offline timezone result Time localTime = timezoneResult.getLocalTime(); String timezoneId = timezoneResult.getTimezoneId(); // Note: Offline data may be outdated } }, true ); // Use offline data for faster response TimezoneService.getTimezoneInfoWithCoordinates( timezoneResult, coordinates, utcTime, timezoneListener, false ); ``` #### Get timezone info by timezone ID[​](#get-timezone-info-by-timezone-id "Direct link to Get timezone info by timezone ID") The `getTimezoneInfoWithTimezoneId` method allows you to retrieve time zone information based on a specific timezone ID and a UTC `Time` object. ##### Basic usage example[​](#basic-usage-example-1 "Direct link to Basic usage example") * Kotlin * Java ```kotlin val timezoneResult = TimezoneResult() val timezoneId = "Europe/Moscow" val utcTime = Time.getUniversalTime() val timezoneListener = ProgressListener.create( onCompleted = { errorCode, _ -> if (errorCode == GemError.NoError) { when (timezoneResult.status) { ETZStatus.Success -> { val offset = timezoneResult.offset val localTime = timezoneResult.localTime val dstOffset = timezoneResult.dstOffset // Use the timezone information //processTimezoneData(offset, localTime, dstOffset) } ETZStatus.WrongTimezoneId -> { Log.e("TimezoneService", "Invalid timezone ID: $timezoneId") } else -> { Log.e("TimezoneService", "Timezone lookup failed") } } } else { Log.e("TimezoneService", "Request failed: ${GemError.getMessage(errorCode)}") } }, postOnMain = true ) // Request timezone information by ID val errorCode = TimezoneService.getTimezoneInfoWithTimezoneId( timezoneResult = timezoneResult, timezoneId = timezoneId, time = utcTime!!, listener = timezoneListener, accurateResult = true ) if (GemError.isError(errorCode)) { Log.e("TimezoneService", "Failed to start timezone request") } ``` ```java TimezoneResult timezoneResult = new TimezoneResult(); String timezoneId = "Europe/Moscow"; Time utcTime = Time.getUniversalTime(); ProgressListener timezoneListener = ProgressListener.create( (errorCode, hint) -> { if (errorCode == GemError.NoError) { if (timezoneResult.getStatus() == ETZStatus.Success) { int offset = timezoneResult.getOffset(); Time localTime = timezoneResult.getLocalTime(); int dstOffset = timezoneResult.getDstOffset(); // Use the timezone information //processTimezoneData(offset, localTime, dstOffset); } else if (timezoneResult.getStatus() == ETZStatus.WrongTimezoneId) { Log.e("TimezoneService", "Invalid timezone ID: " + timezoneId); } else { Log.e("TimezoneService", "Timezone lookup failed"); } } else { Log.e("TimezoneService", "Request failed: " + GemError.getMessage(errorCode)); } }, true ); // Request timezone information by ID int errorCode = TimezoneService.getTimezoneInfoWithTimezoneId( timezoneResult, timezoneId, utcTime, timezoneListener, true ); if (GemError.isError(errorCode)) { Log.e("TimezoneService", "Failed to start timezone request"); } ``` ##### Using offline data[​](#using-offline-data-1 "Direct link to Using offline data") For faster results without internet dependency: * Kotlin * Java ```kotlin val timezoneResult = TimezoneResult() val timezoneListener = ProgressListener.create( onCompleted = { errorCode, _ -> if (errorCode == GemError.NoError && timezoneResult.status == ETZStatus.Success) { // Process offline result val offset = timezoneResult.offset val localTime = timezoneResult.localTime } }, postOnMain = true ) TimezoneService.getTimezoneInfoWithTimezoneId( timezoneResult, "Europe/Moscow", Time.getUniversalTime()!!, timezoneListener, accurateResult = false // Use offline data ) ``` ```java TimezoneResult timezoneResult = new TimezoneResult(); ProgressListener timezoneListener = ProgressListener.create( (errorCode, hint) -> { if (errorCode == GemError.NoError && timezoneResult.getStatus() == ETZStatus.Success) { // Process offline result int offset = timezoneResult.getOffset(); Time localTime = timezoneResult.getLocalTime(); } }, true ); TimezoneService.getTimezoneInfoWithTimezoneId( timezoneResult, "Europe/Moscow", Time.getUniversalTime(), timezoneListener, false // Use offline data ); ``` #### Working with Time objects[​](#working-with-time-objects "Direct link to Working with Time objects") The `Time` class provides utilities for creating and manipulating time values: * Kotlin * Java ```kotlin // Create UTC time val utcTime = Time.getUniversalTime() // Create local time val localTime = Time.getLocalTime() // Create custom time val customTime = Time().apply { year = 2025 month = 7 day = 1 hour = 6 minute = 0 second = 0 } // Convert to timestamp (milliseconds since epoch) val timestamp = customTime.asLong() // Create from timestamp val timeFromTimestamp = Time().apply { fromLong(timestamp) } ``` ```java // Create UTC time Time utcTime = Time.getUniversalTime(); // Create local time Time localTime = Time.getLocalTime(); // Create custom time Time customTime = new Time(); customTime.setYear(2025); customTime.setMonth(7); customTime.setDay(1); customTime.setHour(6); customTime.setMinute(0); customTime.setSecond(0); // Convert to timestamp (milliseconds since epoch) long timestamp = customTime.asLong(); // Create from timestamp Time timeFromTimestamp = new Time(); timeFromTimestamp.fromLong(timestamp); ``` --- ### This part is undergoing development |

coming-soon This section of the documentation is currently being developed and will be available soon. Please check back later for updates. --- ### Weather The Magic Lane Android SDK’s WeatherService class offers a comprehensive suite of weather capabilities. With it, you can retrieve current conditions, hourly forecasts, and daily outlooks - including temperature, air quality, atmospheric pressure, wind speed, UV index, and many more parameters. #### [📄️ Base Entities](/docs/android/guides/weather/base-entities.md) [Weather-related functionalities are organized into distinct classes, each designed to encapsulate specific weather data. These include LocationForecast, Conditions, and WeatherParameter. This document provides a detailed explanation of each class and its purpose.](/docs/android/guides/weather/base-entities.md) #### [📄️ Weather Service](/docs/android/guides/weather/weather-service.md) [The WeatherService class contains methods for getting the current, hourly and daily forecasts.](/docs/android/guides/weather/weather-service.md) --- ### Base Entities |

Weather-related functionalities are organized into distinct classes, each designed to encapsulate specific weather data. These include `LocationForecast`, `Conditions`, and `WeatherParameter`. This document provides a detailed explanation of each class and its purpose. #### LocationForecast class[​](#locationforecast-class "Direct link to LocationForecast class") This class is responsible for retaining data such as the forecast update timestamp, the geographic location and forecast data. | Property | Type | Description | | ------------- | ----------------- | ------------------------------- | | `updated` | `Time?` | Forecast update timestamp (UTC) | | `coordinates` | `Coordinates?` | Geographic location | | `forecast` | `ConditionsList?` | Forecast data | ![](/docs/android/assets/images/example_android_weather_base_entities-1c1419ae963055675f6bb613ee1c3d61.png) **Current Weather Forecast explained** #### Conditions class[​](#conditions-class "Direct link to Conditions class") This class is responsible for retaining weather conditions for a given timestamp. | Property | Type | Description | | ------------- | ----------------------- | --------------------------------------------------------------- | | `type` | `String?` | For possible values see add ref\[PredefinedParameterTypeValues] | | `timestamp` | `Time?` | Timestamp for condition (UTC) | | `image` | `Image?` | Image representation | | `description` | `String?` | Description translated according to the current SDK language | | `daylight` | `EDaylight` | Daylight condition | | `parameters` | `WeatherParameterList?` | Parameter list | ##### PredefinedParameterTypeValues[​](#predefinedparametertypevalues "Direct link to PredefinedParameterTypeValues") This class contains the common values for `Parameter.type` and `Conditions.type`. | Property | Description | Unit | | ----------------- | ----------------- | ---- | | `airQuality` | 'AirQuality' | - | | `dewPoint` | 'DewPoint' | °C | | `feelsLike` | 'FeelsLike' | °C | | `humidity` | 'Humidity' | % | | `pressure` | 'Pressure' | mb | | `sunRise` | 'Sunrise' | - | | `sunSet` | 'Sunset' | - | | `temperature` | 'Temperature' | °C | | `uv` | 'UV' | - | | `visibility` | 'Visibility' | km | | `windDirection` | 'WindDirection' | ° | | `windSpeed` | 'WindSpeed' | km/h | | `temperatureLow` | 'TemperatureLow' | °C | | `temperatureHigh` | 'TemperatureHigh' | °C | danger The `WeatherService` may return data with varying property types, depending on data availability. A response might include only a subset of the values listed above. #### WeatherParameter class[​](#weatherparameter-class "Direct link to WeatherParameter class") Class responsible for containing weather parameter data. | Property | Type | Description | | -------- | --------- | --------------------------------------------------------------- | | `type` | `String?` | For possible values see add ref\[PredefinedParameterTypeValues] | | `value` | `Double` | Value | | `name` | `String?` | Name translated according to the current SDK language | | `unit` | `String?` | Unit | #### Usage Example[​](#usage-example "Direct link to Usage Example") Here's how to use these classes in Kotlin: * Kotlin * Java ```kotlin // Initialize WeatherService val weatherService = WeatherService() // Define coordinates for the location val coordinates = Coordinates().apply { latitude = 48.864716 // Paris latitude longitude = 2.349014 // Paris longitude } val coordinatesList = CoordinatesList().apply { add(coordinates) } // Get current weather forecast weatherService.getCurrent(coordinatesList, onCompleted = { results, error, message -> if (error == GemError.NoError) { results.firstOrNull()?.let { locationForecast -> // Access forecast update timestamp val updated = locationForecast.updated // Access geographic location val location = locationForecast.coordinates // Access forecast data locationForecast.forecast?.forEach { condition -> // Access condition properties val type = condition.type val timestamp = condition.timestamp val description = condition.description val daylight = condition.daylight val image = condition.image // Access weather parameters condition.parameters?.forEach { parameter -> val paramType = parameter.type val paramValue = parameter.value val paramName = parameter.name val paramUnit = parameter.unit // Process parameter data based on type when (paramType) { "Temperature" -> { // Handle temperature data } "Humidity" -> { // Handle humidity data } // Handle other parameter types... } } } } } else { // Handle error Log.e("WeatherService", "Error getting weather: $message") } }) ``` ```java // Initialize WeatherService WeatherService weatherService = new WeatherService(); // Define coordinates for the location Coordinates coordinates = new Coordinates(); coordinates.setLatitude(48.864716); // Paris latitude coordinates.setLongitude(2.349014); // Paris longitude CoordinatesList coordinatesList = new CoordinatesList(); coordinatesList.add(coordinates); // Get current weather forecast weatherService.getCurrent(coordinatesList, (results, error, message) -> { if (error == GemError.NoError) { if (!results.isEmpty()) { LocationForecast locationForecast = results.get(0); // Access forecast update timestamp Time updated = locationForecast.getUpdated(); // Access geographic location Coordinates location = locationForecast.getCoordinates(); // Access forecast data ConditionsList forecast = locationForecast.getForecast(); if (forecast != null) { for (Conditions condition : forecast) { // Access condition properties String type = condition.getType(); Time timestamp = condition.getTimestamp(); String description = condition.getDescription(); EDaylight daylight = condition.getDaylight(); Image image = condition.getImage(); // Access weather parameters WeatherParameterList parameters = condition.getParameters(); if (parameters != null) { for (WeatherParameter parameter : parameters) { String paramType = parameter.getType(); double paramValue = parameter.getValue(); String paramName = parameter.getName(); String paramUnit = parameter.getUnit(); // Process parameter data based on type if ("Temperature".equals(paramType)) { // Handle temperature data } else if ("Humidity".equals(paramType)) { // Handle humidity data } // Handle other parameter types... } } } } } } else { // Handle error Log.e("WeatherService", "Error getting weather: " + message); } }); ``` --- ### Weather Service |

The `WeatherService` class contains methods for getting the current, hourly and daily forecasts. #### Get Current Weather Forecast[​](#get-current-weather-forecast "Direct link to Get Current Weather Forecast") To retrieve the current weather forecast, use the `getCurrent` method of the `WeatherService` class. Provide the coordinates for the desired location, and the forecast will be returned through the callback. The following example demonstrates how to fetch the current forecast for Paris coordinates. * Kotlin * Java ```kotlin val weatherService = WeatherService() val locationCoordinates = Coordinates().apply { latitude = 48.864716 longitude = 2.349014 } val coordinatesList = CoordinatesList().apply { add(locationCoordinates) } weatherService.getCurrent(coordinatesList, onCompleted = { results, error, message -> if (error == GemError.NoError) { // Process the current forecast results Log.d("Weather", "Forecast list size: ${results.size}") results.firstOrNull()?.let { locationForecast -> // Access forecast data locationForecast.forecast?.forEach { condition -> Log.d("Weather", "Condition: ${condition.description}") } } } else { Log.e("Weather", "Error getting current forecast: $message") } }) ``` ```java WeatherService weatherService = new WeatherService(); Coordinates locationCoordinates = new Coordinates(); locationCoordinates.setLatitude(48.864716); locationCoordinates.setLongitude(2.349014); CoordinatesList coordinatesList = new CoordinatesList(); coordinatesList.add(locationCoordinates); weatherService.getCurrent(coordinatesList, (results, error, message) -> { if (error == GemError.NoError) { // Process the current forecast results Log.d("Weather", "Forecast list size: " + results.size()); if (!results.isEmpty()) { LocationForecast locationForecast = results.get(0); // Access forecast data ConditionsList forecast = locationForecast.getForecast(); if (forecast != null) { for (Conditions condition : forecast) { Log.d("Weather", "Condition: " + condition.getDescription()); } } } } else { Log.e("Weather", "Error getting current forecast: " + message); } }); ``` danger The API user is responsible for verifying whether the `LocationForecast` contains any `Conditions`, and whether each `Condition` includes a `WeatherParameter`. If data is unavailable for the specified location and time, the API may return empty lists of`Condition`s or `WeatherParameter`s. info The result will contain as many `LocationForecast` objects as the list size of given coordinates to `coords` parameter. #### Get Hourly Weather Forecast[​](#get-hourly-weather-forecast "Direct link to Get Hourly Weather Forecast") To retrieve the hourly weather forecast, use the `getHourlyForecast` method of the `WeatherService` class. Provide the coordinates for the desired location, and the forecast will be returned through the callback. The following example demonstrates how to fetch the hourly forecast for Paris coordinates. * Kotlin * Java ```kotlin val weatherService = WeatherService() val locationCoordinates = Coordinates().apply { latitude = 48.864716 longitude = 2.349014 } val coordinatesList = CoordinatesList().apply { add(locationCoordinates) } weatherService.getHourlyForecast(24, coordinatesList, onCompleted = { results, error, message -> if (error == GemError.NoError) { // Process the hourly forecast results Log.d("Weather", "Hourly forecast list size: ${results.size}") results.firstOrNull()?.forecast?.forEach { condition -> // Process each hourly condition val timestamp = condition.timestamp val description = condition.description condition.parameters?.forEach { parameter -> when (parameter.type) { "Temperature" -> { Log.d("Weather", "Temperature: ${parameter.value}${parameter.unit}") } "Humidity" -> { Log.d("Weather", "Humidity: ${parameter.value}${parameter.unit}") } } } } } else { Log.e("Weather", "Error getting hourly forecast: $message") } }) ``` ```java WeatherService weatherService = new WeatherService(); Coordinates locationCoordinates = new Coordinates(); locationCoordinates.setLatitude(48.864716); locationCoordinates.setLongitude(2.349014); CoordinatesList coordinatesList = new CoordinatesList(); coordinatesList.add(locationCoordinates); weatherService.getHourlyForecast(24, coordinatesList, (results, error, message) -> { if (error == GemError.NoError) { // Process the hourly forecast results Log.d("Weather", "Hourly forecast list size: " + results.size()); if (!results.isEmpty()) { ConditionsList forecast = results.get(0).getForecast(); if (forecast != null) { for (Conditions condition : forecast) { // Process each hourly condition Time timestamp = condition.getTimestamp(); String description = condition.getDescription(); WeatherParameterList parameters = condition.getParameters(); if (parameters != null) { for (WeatherParameter parameter : parameters) { if ("Temperature".equals(parameter.getType())) { Log.d("Weather", "Temperature: " + parameter.getValue() + parameter.getUnit()); } else if ("Humidity".equals(parameter.getType())) { Log.d("Weather", "Humidity: " + parameter.getValue() + parameter.getUnit()); } } } } } } } else { Log.e("Weather", "Error getting hourly forecast: " + message); } }); ``` You'll need to provide the number of hours for which the forecast is requested. danger The number of requested hours must not exceed 240. Exceeding this limit will result in an empty response and an error of type `GemError.outOfRange`. #### Get the Daily Forecast[​](#get-the-daily-forecast "Direct link to Get the Daily Forecast") To retrieve the daily weather forecast, use the `getDailyForecast` method of the `WeatherService` class. Provide the coordinates for the desired location, and the forecast will be returned through the callback. The following example demonstrates how to fetch the daily forecast for Paris coordinates. * Kotlin * Java ```kotlin val weatherService = WeatherService() val locationCoordinates = Coordinates().apply { latitude = 48.864716 longitude = 2.349014 } val coordinatesList = CoordinatesList().apply { add(locationCoordinates) } weatherService.getDailyForecast(10, coordinatesList, onCompleted = { results, error, message -> if (error == GemError.NoError) { // Process the daily forecast results Log.d("Weather", "Daily forecast list size: ${results.size}") results.firstOrNull()?.forecast?.forEach { condition -> // Process each daily condition val description = condition.description val daylight = condition.daylight condition.parameters?.forEach { parameter -> when (parameter.type) { "TemperatureHigh" -> { Log.d("Weather", "High temp: ${parameter.value}${parameter.unit}") } "TemperatureLow" -> { Log.d("Weather", "Low temp: ${parameter.value}${parameter.unit}") } "Sunrise" -> { // Convert timestamp to readable time val sunriseTime = Date((parameter.value * 1000).toLong()) Log.d("Weather", "Sunrise: $sunriseTime") } "Sunset" -> { // Convert timestamp to readable time val sunsetTime = Date((parameter.value * 1000).toLong()) Log.d("Weather", "Sunset: $sunsetTime") } } } } } else { Log.e("Weather", "Error getting daily forecast: $message") } }) ``` ```java WeatherService weatherService = new WeatherService(); Coordinates locationCoordinates = new Coordinates(); locationCoordinates.setLatitude(48.864716); locationCoordinates.setLongitude(2.349014); CoordinatesList coordinatesList = new CoordinatesList(); coordinatesList.add(locationCoordinates); weatherService.getDailyForecast(10, coordinatesList, (results, error, message) -> { if (error == GemError.NoError) { // Process the daily forecast results Log.d("Weather", "Daily forecast list size: " + results.size()); if (!results.isEmpty()) { ConditionsList forecast = results.get(0).getForecast(); if (forecast != null) { for (Conditions condition : forecast) { // Process each daily condition String description = condition.getDescription(); EDaylight daylight = condition.getDaylight(); WeatherParameterList parameters = condition.getParameters(); if (parameters != null) { for (WeatherParameter parameter : parameters) { if ("TemperatureHigh".equals(parameter.getType())) { Log.d("Weather", "High temp: " + parameter.getValue() + parameter.getUnit()); } else if ("TemperatureLow".equals(parameter.getType())) { Log.d("Weather", "Low temp: " + parameter.getValue() + parameter.getUnit()); } else if ("Sunrise".equals(parameter.getType())) { // Convert timestamp to readable time Date sunriseTime = new Date((long) (parameter.getValue() * 1000)); Log.d("Weather", "Sunrise: " + sunriseTime); } else if ("Sunset".equals(parameter.getType())) { // Convert timestamp to readable time Date sunsetTime = new Date((long) (parameter.getValue() * 1000)); Log.d("Weather", "Sunset: " + sunsetTime); } } } } } } } else { Log.e("Weather", "Error getting daily forecast: " + message); } }); ``` You'll need to provide the number of days for which the forecast is requested. danger The number of requested days must not exceed 10. Exceeding this limit will result in an empty response and an error of type `GemError.outOfRange`. #### Get the Weather Forecast[​](#get-the-weather-forecast "Direct link to Get the Weather Forecast") To retrieve the weather forecast at a specific time and coordinates, use the `getForecast` method of `WeatherService` class. You'll need to provide a list of `TimeDistanceCoordinate`, and the callback will retrieve as many `LocationForecast` objects as the size of the coordinate list. * Kotlin * Java ```kotlin val weatherService = WeatherService() // Create coordinates for Paris val coordinates = Coordinates().apply { latitude = 48.864716 longitude = 2.349014 } // Create time-distance coordinate for forecast 2 days in the future val timeDistanceCoordinate = TimeDistanceCoordinate().also { it.coordinates = coordinates // Set duration to 2 days in the future (duration in seconds) it.timestamp = 2 * 24 * 60 * 60L // 2 days in seconds } val coordinatesList = TimeDistanceCoordinateList().also { it.add(0,timeDistanceCoordinate) } weatherService.getForecast(coordinatesList, onCompleted = { results, error, message -> if (error == GemError.NoError) { // Process the forecast results Log.d("Weather", "Forecast list size: ${results.size}") results.forEach { locationForecast -> locationForecast.forecast?.forEach { condition -> Log.d("Weather", "Forecast condition: ${condition.description}") // Process weather parameters condition.parameters?.forEach { parameter -> Log.d("Weather", "${parameter.name}: ${parameter.value}${parameter.unit}") } } } } else { Log.e("Weather", "Error getting forecast: $message") } }) ``` ```java WeatherService weatherService = new WeatherService(); // Create coordinates for Paris Coordinates coordinates = new Coordinates(); coordinates.setLatitude(48.864716); coordinates.setLongitude(2.349014); // Create time-distance coordinate for forecast 2 days in the future TimeDistanceCoordinate timeDistanceCoordinate = new TimeDistanceCoordinate(); timeDistanceCoordinate.setCoordinates(coordinates); // Set duration to 2 days in the future (duration in seconds) timeDistanceCoordinate.setTimestamp(2 * 24 * 60 * 60L); // 2 days in seconds TimeDistanceCoordinateList coordinatesList = new TimeDistanceCoordinateList(); coordinatesList.add(0, timeDistanceCoordinate); weatherService.getForecast(coordinatesList, (results, error, message) -> { if (error == GemError.NoError) { // Process the forecast results Log.d("Weather", "Forecast list size: " + results.size()); for (LocationForecast locationForecast : results) { ConditionsList forecast = locationForecast.getForecast(); if (forecast != null) { for (Conditions condition : forecast) { Log.d("Weather", "Forecast condition: " + condition.getDescription()); // Process weather parameters WeatherParameterList parameters = condition.getParameters(); if (parameters != null) { for (WeatherParameter parameter : parameters) { Log.d("Weather", parameter.getName() + ": " + parameter.getValue() + parameter.getUnit()); } } } } } } else { Log.e("Weather", "Error getting forecast: " + message); } }); ``` info The `duration` parameter in `TimeDistanceCoordinate` specifies the relative time offset into the future (in seconds) for which the forecast is requested. #### Working with Weather Images[​](#working-with-weather-images "Direct link to Working with Weather Images") Weather condition images are provided as `Image` objects. Here's how to convert them to Android Bitmaps for display: * Kotlin * Java ```kotlin condition.image?.let { weatherImage -> // Convert Image to Bitmap val bitmap = weatherImage.asBitmap(100,100) // Use the bitmap in your ImageView //imageView.setImageBitmap(bitmap) } ``` ```java Image weatherImage = condition.getImage(); if (weatherImage != null) { // Convert Image to Bitmap Bitmap bitmap = weatherImage.asBitmap(100, 100); // Use the bitmap in your ImageView //imageView.setImageBitmap(bitmap); } ``` ---