diff --git a/android/examples/__pycache__/main.cpython-311.pyc b/android/examples/__pycache__/main.cpython-311.pyc index 90b1b3a8fa3..6a11621acd0 100644 Binary files a/android/examples/__pycache__/main.cpython-311.pyc and b/android/examples/__pycache__/main.cpython-311.pyc differ diff --git a/android/examples/camera/max-min-zoom/index.html b/android/examples/camera/max-min-zoom/index.html index ab1eb0d30f3..4198f4da17d 100644 --- a/android/examples/camera/max-min-zoom/index.html +++ b/android/examples/camera/max-min-zoom/index.html @@ -1045,7 +1045,7 @@
Welcome to the examples documentation of MapLibre Android.
Quickstart
Learn how to include MapLibre Android in your project
Getting started
Find us on Slack
Discuss the project and ask questions in the #maplibre-android
channel
Slack
Contribute to these Docs
Share your own examples with the community!
Documentation on GitHub
You can learn how to use the API from MapLibre Android by stuying the source code of existing apps that intergrate MapLibre Android. Here are some open-source apps that use MapLibre Android:
#maplibre-native
and #maplibre-android
channels.This guide will explain various ways to create a map.
When working with maps, you likely want to configure the MapView
.
There are several ways to build a MapView
:
MapView
in the layout.MapLibreMapOptions
and passing builder function values into the MapView
.SupportMapFragment
with the help of MapLibreMapOptions
.Before diving into MapView
configurations, let's understand the capabilities of both XML namespaces and MapLibreMapOptions
.
Here are some common configurations you can set:
We will explore how to achieve these configurations in XML layout and programmatically in Activity code, step by step.
"},{"location":"configuration/#mapview-configuration-with-an-xml-layout","title":"MapView
Configuration with an XML layout","text":"To configure MapView
within an XML layout, you need to use the right namespace and provide the necessary data in the layout file.
<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n xmlns:tools=\"http://schemas.android.com/tools\"\n android:id=\"@+id/main\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\"\n tools:context=\".activity.options.MapOptionsXmlActivity\">\n\n <org.maplibre.android.maps.MapView\n android:id=\"@+id/mapView\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\"\n app:maplibre_apiBaseUri=\"https://api.maplibre.org\"\n app:maplibre_cameraBearing=\"0.0\"\n app:maplibre_cameraPitchMax=\"90.0\"\n app:maplibre_cameraPitchMin=\"0.0\"\n app:maplibre_cameraTargetLat=\"42.31230486601532\"\n app:maplibre_cameraTargetLng=\"64.63967338936439\"\n app:maplibre_cameraTilt=\"0.0\"\n app:maplibre_cameraZoom=\"3.9\"\n app:maplibre_cameraZoomMax=\"26.0\"\n app:maplibre_cameraZoomMin=\"2.0\"\n app:maplibre_localIdeographFontFamilies=\"@array/array_local_ideograph_family_test\"\n app:maplibre_localIdeographFontFamily=\"Droid Sans\"\n app:maplibre_uiCompass=\"true\"\n app:maplibre_uiCompassFadeFacingNorth=\"true\"\n app:maplibre_uiCompassGravity=\"top|end\"\n app:maplibre_uiDoubleTapGestures=\"true\"\n app:maplibre_uiHorizontalScrollGestures=\"true\"\n app:maplibre_uiRotateGestures=\"true\"\n app:maplibre_uiScrollGestures=\"true\"\n app:maplibre_uiTiltGestures=\"true\"\n app:maplibre_uiZoomGestures=\"true\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n
This can be found in activity_map_options_xml.xml
.
You can assign any other existing values to the maplibre...
tags. Then, you only need to create MapView
and MapLibreMap
objects with a simple setup in the Activity.
package org.maplibre.android.testapp.activity.options\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport org.maplibre.android.maps.MapLibreMap\nimport org.maplibre.android.maps.MapView\nimport org.maplibre.android.maps.OnMapReadyCallback\nimport org.maplibre.android.maps.Style\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\n\n/**\n * TestActivity demonstrating configuring MapView with XML\n */\n\nclass MapOptionsXmlActivity : AppCompatActivity(), OnMapReadyCallback {\n private lateinit var mapView: MapView\n private lateinit var maplibreMap: MapLibreMap\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_map_options_xml)\n mapView = findViewById(R.id.mapView)\n mapView.onCreate(savedInstanceState)\n mapView.getMapAsync(this)\n }\n\n override fun onMapReady(maplibreMap: MapLibreMap) {\n this.maplibreMap = maplibreMap\n this.maplibreMap.setStyle(\"https://demotiles.maplibre.org/style.json\")\n }\n\n override fun onStart() {\n super.onStart()\n mapView.onStart()\n }\n\n override fun onResume() {\n super.onResume()\n mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n mapView.onPause()\n }\n\n override fun onStop() {\n super.onStop()\n mapView.onStop()\n }\n\n override fun onSaveInstanceState(outState: Bundle) {\n super.onSaveInstanceState(outState)\n mapView.onSaveInstanceState(outState)\n }\n\n override fun onDestroy() {\n super.onDestroy()\n mapView.onDestroy()\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n mapView.onLowMemory()\n }\n}\n
This can be found in MapOptionsXmlActivity.kt
.
MapView
configuration with MapLibreMapOptions
","text":"Here we don't have to create MapView from XML since we want to create it programmatically.
<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:id=\"@+id/container\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\"\n android:orientation=\"vertical\"/>\n
This can be found in activity_map_options_runtime.xml
.
A MapLibreMapOptions
object must be created and passed to the MapView constructor. All setup is done in the Activity code:
package org.maplibre.android.testapp.activity.options\n\nimport android.os.Bundle\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.appcompat.app.AppCompatActivity\nimport org.maplibre.android.camera.CameraPosition\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.MapLibreMap\nimport org.maplibre.android.maps.MapLibreMapOptions\nimport org.maplibre.android.maps.MapView\nimport org.maplibre.android.maps.OnMapReadyCallback\nimport org.maplibre.android.maps.Style\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\n\n/**\n * TestActivity demonstrating configuring MapView with MapOptions\n */\nclass MapOptionsRuntimeActivity : AppCompatActivity(), OnMapReadyCallback {\n\n private lateinit var maplibreMap: MapLibreMap\n private lateinit var mapView: MapView\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_map_options_runtime)\n\n // Create map configuration\n val maplibreMapOptions = MapLibreMapOptions.createFromAttributes(this)\n maplibreMapOptions.apply {\n apiBaseUri(\"https://api.maplibre.org\")\n camera(\n CameraPosition.Builder()\n .bearing(0.0)\n .target(LatLng(42.31230486601532, 64.63967338936439))\n .zoom(3.9)\n .tilt(0.0)\n .build()\n )\n maxPitchPreference(90.0)\n minPitchPreference(0.0)\n maxZoomPreference(26.0)\n minZoomPreference(2.0)\n localIdeographFontFamily(\"Droid Sans\")\n zoomGesturesEnabled(true)\n compassEnabled(true)\n compassFadesWhenFacingNorth(true)\n scrollGesturesEnabled(true)\n rotateGesturesEnabled(true)\n tiltGesturesEnabled(true)\n }\n\n // Create map programmatically, add to view hierarchy\n mapView = MapView(this, maplibreMapOptions)\n mapView.getMapAsync(this)\n mapView.onCreate(savedInstanceState)\n (findViewById<View>(R.id.container) as ViewGroup).addView(mapView)\n }\n\n override fun onMapReady(maplibreMap: MapLibreMap) {\n this.maplibreMap = maplibreMap\n this.maplibreMap.setStyle(\"https://demotiles.maplibre.org/style.json\")\n }\n\n override fun onStart() {\n super.onStart()\n mapView.onStart()\n }\n\n override fun onResume() {\n super.onResume()\n mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n mapView.onPause()\n }\n\n override fun onStop() {\n super.onStop()\n mapView.onStop()\n }\n\n override fun onSaveInstanceState(outState: Bundle) {\n super.onSaveInstanceState(outState)\n mapView.onSaveInstanceState(outState)\n }\n\n override fun onDestroy() {\n super.onDestroy()\n mapView.onDestroy()\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n mapView.onLowMemory()\n }\n}\n
This can be found in MapOptionsRuntimeActivity.kt
.
Finally you will see a result similar to this:
For the full contents of MapOptionsRuntimeActivity
and MapOptionsXmlActivity
, please take a look at the source code of MapLibreAndroidTestApp.
You can read more about MapLibreMapOptions
in the Android API documentation.
SupportMapFragment
with the help of MapLibreMapOptions
.","text":"If you are using MapFragment in your project, it is also easy to provide initial values to the newInstance()
static method of SupportMapFragment
, which requires a MapLibreMapOptions
parameter.
Let's see how this can be done in a sample activity:
package org.maplibre.android.testapp.activity.fragment\n\nimport android.os.Bundle // ktlint-disable import-ordering\nimport androidx.appcompat.app.AppCompatActivity\nimport org.maplibre.android.camera.CameraPosition\nimport org.maplibre.android.camera.CameraUpdateFactory\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.* // ktlint-disable no-wildcard-imports\nimport org.maplibre.android.maps.MapFragment.OnMapViewReadyCallback\nimport org.maplibre.android.maps.MapView.OnDidFinishRenderingFrameListener\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\n\n/**\n * Test activity showcasing using the MapFragment API using Support Library Fragments.\n *\n *\n * Uses MapLibreMapOptions to initialise the Fragment.\n *\n */\nclass SupportMapFragmentActivity :\n AppCompatActivity(),\n OnMapViewReadyCallback,\n OnMapReadyCallback,\n OnDidFinishRenderingFrameListener {\n private lateinit var maplibreMap: MapLibreMap\n private lateinit var mapView: MapView\n private var initialCameraAnimation = true\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_map_fragment)\n val mapFragment: SupportMapFragment?\n if (savedInstanceState == null) {\n mapFragment = SupportMapFragment.newInstance(createFragmentOptions())\n supportFragmentManager\n .beginTransaction()\n .add(R.id.fragment_container, mapFragment, TAG)\n .commit()\n } else {\n mapFragment = supportFragmentManager.findFragmentByTag(TAG) as SupportMapFragment?\n }\n mapFragment!!.getMapAsync(this)\n }\n\n private fun createFragmentOptions(): MapLibreMapOptions {\n val options = MapLibreMapOptions.createFromAttributes(this, null)\n options.scrollGesturesEnabled(false)\n options.zoomGesturesEnabled(false)\n options.tiltGesturesEnabled(false)\n options.rotateGesturesEnabled(false)\n options.debugActive(false)\n val dc = LatLng(38.90252, -77.02291)\n options.minZoomPreference(9.0)\n options.maxZoomPreference(11.0)\n options.camera(\n CameraPosition.Builder()\n .target(dc)\n .zoom(11.0)\n .build()\n )\n return options\n }\n\n override fun onMapViewReady(map: MapView) {\n mapView = map\n mapView.addOnDidFinishRenderingFrameListener(this)\n }\n\n override fun onMapReady(map: MapLibreMap) {\n maplibreMap = map\n maplibreMap.setStyle(TestStyles.getPredefinedStyleWithFallback(\"Satellite Hybrid\"))\n }\n\n override fun onDestroy() {\n super.onDestroy()\n mapView.removeOnDidFinishRenderingFrameListener(this)\n }\n\n override fun onDidFinishRenderingFrame(fully: Boolean, frameEncodingTime: Double, frameRenderingTime: Double) {\n if (initialCameraAnimation && fully && this::maplibreMap.isInitialized) {\n maplibreMap.animateCamera(\n CameraUpdateFactory.newCameraPosition(CameraPosition.Builder().tilt(45.0).build()),\n 5000\n )\n initialCameraAnimation = false\n }\n }\n\n companion object {\n private const val TAG = \"com.mapbox.map\"\n }\n}\n
You can also find the full contents of SupportMapFragmentActivity
in the MapLibreAndroidTestApp.
To learn more about SupportMapFragment
, please visit the Android API documentation.
This guide will teach you how to use GeoJsonSource
by deep diving into GeoJSON file format.
After finishing this documentation you should be able to:
Style
, Layer
, and Source
interact with each other.GeoJsonSource
s.Styles consist of collections of layers and a data source. Layers reference data sources. Hence, they require a unique source ID when you construct them. It would be meaningless if we don't have any data to show, so we need know how to supply data through a data source.
Firstly, we need to understand how to store data and pass it into a data source; therefore, we will discuss GeoJSON in the next session.
"},{"location":"geojson-guide/#2-geojson","title":"2. GeoJSON","text":"GeoJSON is a JSON file for encoding various geographical data structures. It defines several JSON objects to represent geospatial information. Typicalle the.geojson
extension is used for GeoJSON files. We define the most fundamental objects:
Geometry
refers to a single geometric shape that contains one or more coordinates. These shapes are visual objects displayed on a map. A geometry can be one of the following six types:Feautue
is a compound object that combines a single geometry with user-defined attributes, such as name, color.FeatureCollection
is set of features stored in an array. It is a root object that introduces all other features.A typical GeoJSON structure might look like:
{\n \"type\": \"Feature\",\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [125.6, 10.1]\n },\n \"properties\": {\n \"name\": \"Dinagat Islands\"\n }\n}\n
So far we learned describing geospatial data in GeoJSON files. We will start applying this knowledge into our map applications.
"},{"location":"geojson-guide/#3-geojsonsource","title":"3. GeoJsonSource","text":"As we discussed before, map requires some sort data to be rendered. We use different sources such as Vector, Raster and GeoJSON. We will focus exclusively on GeoJsonSource
and will not address other sources.
GeoJsonSource
is a type of source that has a unique String
ID and GeoJSON data.
There are several ways to construct a GeoJsonSource
:
A sample GeoJsonSource
:
val source = GeoJsonSource(\"source\", featureCollection)\nval lineLayer = LineLayer(\"layer\", \"source\")\n .withProperties(\n PropertyFactory.lineColor(Color.RED),\n PropertyFactory.lineWidth(10f)\n )\n\nstyle.addSource(source)\nstyle.addLayer(lineLayer)\n
Note that you can not simply show data on a map. Layers must reference them. Therefore, you create a layer that gives visual appearance to it.
"},{"location":"geojson-guide/#creating-geojson-sources","title":"Creating GeoJSON sources","text":"There are various ways you can create a GeoJSONSource
. Some of the options are shown below.
binding.mapView.getMapAsync { map ->\n map.moveCamera(CameraUpdateFactory.newLatLngZoom(cameraTarget, cameraZoom))\n map.setStyle(\n Style.Builder()\n .withImage(imageId, imageIcon)\n .withSource(GeoJsonSource(sourceId, URI(\"asset://points-sf.geojson\")))\n .withLayer(SymbolLayer(layerId, sourceId).withProperties(iconImage(imageId)))\n )\n}\n
Loading with raw folder fileval source: Source = try {\n GeoJsonSource(\"amsterdam-spots\", ResourceUtils.readRawResource(this, R.raw.amsterdam))\n} catch (ioException: IOException) {\n Toast.makeText(\n this@RuntimeStyleActivity,\n \"Couldn't add source: \" + ioException.message,\n Toast.LENGTH_SHORT\n ).show()\n return\n}\nmaplibreMap.style!!.addSource(source)\nvar layer: FillLayer? = FillLayer(\"parksLayer\", \"amsterdam-spots\")\nlayer!!.setProperties(\n PropertyFactory.fillColor(Color.RED),\n PropertyFactory.fillOutlineColor(Color.BLUE),\n PropertyFactory.fillOpacity(0.3f),\n PropertyFactory.fillAntialias(true)\n)\n
Parsing inline JSONfun readRawResource(context: Context?, @RawRes rawResource: Int): String {\n var json = \"\"\n if (context != null) {\n val writer: Writer = StringWriter()\n val buffer = CharArray(1024)\n context.resources.openRawResource(rawResource).use { `is` ->\n val reader: Reader = BufferedReader(InputStreamReader(`is`, \"UTF-8\"))\n var numRead: Int\n while (reader.read(buffer).also { numRead = it } != -1) {\n writer.write(buffer, 0, numRead)\n }\n }\n json = writer.toString()\n }\n return json\n}\n
Loading from remote servicesprivate fun createEarthquakeSource(): GeoJsonSource {\n return GeoJsonSource(EARTHQUAKE_SOURCE_ID, URI(EARTHQUAKE_SOURCE_URL))\n}\n
companion object {\n private const val EARTHQUAKE_SOURCE_URL =\n \"https://maplibre.org/maplibre-gl-js-docs/assets/earthquakes.geojson\"\n private const val EARTHQUAKE_SOURCE_ID = \"earthquakes\"\n private const val HEATMAP_LAYER_ID = \"earthquakes-heat\"\n private const val HEATMAP_LAYER_SOURCE = \"earthquakes\"\n private const val CIRCLE_LAYER_ID = \"earthquakes-circle\"\n}\n
Parsing string with the fromJson method of FeatureCollectionreturn FeatureCollection.fromJson(\n \"\"\"\n {\n \"type\": \"FeatureCollection\",\n \"features\": [\n {\n \"type\": \"Feature\",\n \"properties\": {},\n \"geometry\": {\n \"type\": \"Polygon\",\n \"coordinates\": [\n [\n [\n -77.06867337226866,\n 38.90467655551809\n ],\n [\n -77.06233263015747,\n 38.90479344272695\n ],\n [\n -77.06234335899353,\n 38.906463238984344\n ],\n [\n -77.06290125846863,\n 38.907206285691615\n ],\n [\n -77.06364154815674,\n 38.90684728656818\n ],\n [\n -77.06326603889465,\n 38.90637140121084\n ],\n [\n -77.06321239471436,\n 38.905561553883246\n ],\n [\n -77.0691454410553,\n 38.905436318935635\n ],\n [\n -77.06912398338318,\n 38.90466820642439\n ],\n [\n -77.06867337226866,\n 38.90467655551809\n ]\n ]\n ]\n }\n }\n ]\n }\n \"\"\".trimIndent()\n).features()!![0].geometry() as Polygon\n
Creating Geometry, Feature, and FeatureCollections from scratchval properties = JsonObject()\nproperties.addProperty(\"key1\", \"value1\")\nval source = GeoJsonSource(\n \"test-source\",\n FeatureCollection.fromFeatures(\n arrayOf(\n Feature.fromGeometry(Point.fromLngLat(17.1, 51.0), properties),\n Feature.fromGeometry(Point.fromLngLat(17.2, 51.0), properties),\n Feature.fromGeometry(Point.fromLngLat(17.3, 51.0), properties),\n Feature.fromGeometry(Point.fromLngLat(17.4, 51.0), properties)\n )\n )\n)\nstyle.addSource(source)\nval visible = Expression.eq(Expression.get(\"key1\"), Expression.literal(\"value1\"))\nval invisible = Expression.neq(Expression.get(\"key1\"), Expression.literal(\"value1\"))\nval layer = CircleLayer(\"test-layer\", source.id)\n .withFilter(visible)\nstyle.addLayer(layer)\n
Note that the GeoJSON objects we discussed earlier have classes defined in the MapLibre SDK. Therefore, we can either map JSON objects to regular Java/Kotlin objects or build them directly.
"},{"location":"geojson-guide/#4-updating-data-at-runtime","title":"4. Updating data at runtime","text":"The key feature of GeoJsonSource
s is that once we add one, we can set another set of data. We achieve this using setGeoJson()
method. For instance, we create a source variable and check if we have not assigned it, then we create a new source object and add it to style; otherwise, we set a different data source:
private fun createFeatureCollection(): FeatureCollection {\n val point = if (isInitialPosition) {\n Point.fromLngLat(-74.01618140, 40.701745)\n } else {\n Point.fromLngLat(-73.988097, 40.749864)\n }\n val properties = JsonObject()\n properties.addProperty(KEY_PROPERTY_SELECTED, isSelected)\n val feature = Feature.fromGeometry(point, properties)\n return FeatureCollection.fromFeatures(arrayOf(feature))\n}\n
private fun updateSource(style: Style?) {\n val featureCollection = createFeatureCollection()\n if (source != null) {\n source!!.setGeoJson(featureCollection)\n } else {\n source = GeoJsonSource(SOURCE_ID, featureCollection)\n style!!.addSource(source!!)\n }\n}\n
See this guide for an advanced example that showcases random cars and a passenger on a map updating their positions with smooth animation.
"},{"location":"geojson-guide/#summary","title":"Summary","text":"GeoJsonSources have their pros and cons. They are most effective when you want to add additional data to your style or provide features like animating objects on your map.
However, working with large datasets can be challenging if you need to manipulate and store data within the app; in such cases, it\u2019s better to use a remote data source.
"},{"location":"getting-started/","title":"Quickstart","text":"Add bintray Maven repositories to your project-level Gradle file (usually <project>/<app-module>/build.gradle
).
allprojects {\n repositories {\n ...\n mavenCentral()\n }\n}\n
Add the library as a dependency into your module Gradle file (usually <project>/<app-module>/build.gradle
). Replace <version>
with the latest MapLibre Android version (e.g.: org.maplibre.gl:android-sdk:11.5.2
):
dependencies {\n ...\n implementation 'org.maplibre.gl:android-sdk:<version>'\n ...\n}\n
Sync your Android project with Gradle files.
Add a MapView
to your layout XML file (usually <project>/<app-module>/src/main/res/layout/activity_main.xml
).
...\n<org.maplibre.android.maps.MapView\n android:id=\"@+id/mapView\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\"\n />\n...\n
Initialize the MapView
in your MainActivity
file by following the example below:
import androidx.appcompat.app.AppCompatActivity\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport org.maplibre.android.Maplibre\nimport org.maplibre.android.camera.CameraPosition\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.MapView\nimport org.maplibre.android.testapp.R\n\nclass MainActivity : AppCompatActivity() {\n\n // Declare a variable for MapView\n private lateinit var mapView: MapView\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n\n // Init MapLibre\n MapLibre.getInstance(this)\n\n // Init layout view\n val inflater = LayoutInflater.from(this)\n val rootView = inflater.inflate(R.layout.activity_main, null)\n setContentView(rootView)\n\n // Init the MapView\n mapView = rootView.findViewById(R.id.mapView)\n mapView.getMapAsync { map ->\n map.setStyle(\"https://demotiles.maplibre.org/style.json\")\n map.cameraPosition = CameraPosition.Builder().target(LatLng(0.0,0.0)).zoom(1.0).build()\n }\n }\n\n override fun onStart() {\n super.onStart()\n mapView.onStart()\n }\n\n override fun onResume() {\n super.onResume()\n mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n mapView.onPause()\n }\n\n override fun onStop() {\n super.onStop()\n mapView.onStop()\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n mapView.onLowMemory()\n }\n\n override fun onDestroy() {\n super.onDestroy()\n mapView.onDestroy()\n }\n\n override fun onSaveInstanceState(outState: Bundle) {\n super.onSaveInstanceState(outState)\n mapView.onSaveInstanceState(outState)\n }\n}\n
Build and run the app. If you run the app successfully, a map will be displayed as seen in the screenshot below.
This guide will demonstrate how to utilize the LocationComponent to represent the user's current location.
When implementing the LocationComponent, the application should request location permissions. Declare the need for foreground location in the AndroidManifest.xml
file. For more information, please refer to the Android Developer Documentation.
<manifest ... >\n <!-- Always include this permission -->\n <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />\n\n <!-- Include only if your app benefits from precise location access. -->\n <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />\n</manifest>\n
Create a new activity named BasicLocationPulsingCircleActivity
:
OnMapReadyCallback
interface. The onMapReady()
method is triggered when the map is ready to be used.permissionsManager
to manage permissions.locationComponent
to manage user location.onCreate()
method, call checkPermissions()
to ensure that the application can access the user's location./**\n * This activity shows a basic usage of the LocationComponent's pulsing circle. There's no\n * customization of the pulsing circle's color, radius, speed, etc.\n */\nclass BasicLocationPulsingCircleActivity : AppCompatActivity(), OnMapReadyCallback {\n private var lastLocation: Location? = null\n private lateinit var mapView: MapView\n private var permissionsManager: PermissionsManager? = null\n private var locationComponent: LocationComponent? = null\n private lateinit var maplibreMap: MapLibreMap\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_location_layer_basic_pulsing_circle)\n mapView = findViewById(R.id.mapView)\n if (savedInstanceState != null) {\n lastLocation = savedInstanceState.getParcelable(SAVED_STATE_LOCATION, Location::class.java)\n }\n mapView.onCreate(savedInstanceState)\n checkPermissions()\n }\n
In the checkPermissions()
method, the PermissionManager is used to request location permissions at runtime and handle the callbacks for permission granting or rejection.Additionally, you should pass the results of Activity.onRequestPermissionResult()
to it. If the permissions are granted, call mapView.getMapAsync(this)
to register the activity as a listener for onMapReady event.
private fun checkPermissions() {\n if (PermissionsManager.areLocationPermissionsGranted(this)) {\n mapView.getMapAsync(this)\n } else {\n permissionsManager = PermissionsManager(object : PermissionsListener {\n override fun onExplanationNeeded(permissionsToExplain: List<String>) {\n Toast.makeText(\n this@BasicLocationPulsingCircleActivity,\n \"You need to accept location permissions.\",\n Toast.LENGTH_SHORT\n ).show()\n }\n\n override fun onPermissionResult(granted: Boolean) {\n if (granted) {\n mapView.getMapAsync(this@BasicLocationPulsingCircleActivity)\n } else {\n finish()\n }\n }\n })\n permissionsManager!!.requestLocationPermissions(this)\n }\n}\n\noverride fun onRequestPermissionsResult(\n requestCode: Int,\n permissions: Array<String>,\n grantResults: IntArray\n) {\n super.onRequestPermissionsResult(requestCode, permissions, grantResults)\n permissionsManager!!.onRequestPermissionsResult(requestCode, permissions, grantResults)\n}\n
In the onMapReady()
method, first set the style and then handle the user's location using the LocationComponent.
To configure the LocationComponent, developers should use LocationComponentOptions.
In this demonstration, we create an instance of this class.
In this method:
@SuppressLint(\"MissingPermission\")
to suppress warnings related to missing location access permissions.setStyle(),
you can utilize other public and token-free styles like demotiles instead of the predefined styles.pulseEnabled(true)
to enable the pulse animation, which enhances awareness of the user's location.buildLocationComponentActivationOptions()
to set LocationComponentActivationOptions, then activate locatinoComponent
with it.activateLocationComponent()
of locationComponent
. You can also set locationComponent
's various properties like isLocationComponentEnabled
, cameraMode
, etc...CameraMode.TRACKING
1 means that when the user's location is updated, the camera will reposition accordingly.locationComponent!!.forceLocationUpdate(lastLocation)
updates the the user's last known location.@SuppressLint(\"MissingPermission\")\noverride fun onMapReady(maplibreMap: MapLibreMap) {\n this.maplibreMap = maplibreMap\n maplibreMap.setStyle(TestStyles.getPredefinedStyleWithFallback(\"Streets\")) { style: Style ->\n locationComponent = maplibreMap.locationComponent\n val locationComponentOptions =\n LocationComponentOptions.builder(this@BasicLocationPulsingCircleActivity)\n .pulseEnabled(true)\n .build()\n val locationComponentActivationOptions =\n buildLocationComponentActivationOptions(style, locationComponentOptions)\n locationComponent!!.activateLocationComponent(locationComponentActivationOptions)\n locationComponent!!.isLocationComponentEnabled = true\n locationComponent!!.cameraMode = CameraMode.TRACKING\n locationComponent!!.forceLocationUpdate(lastLocation)\n }\n}\n
LocationComponentActivationOptions is used to hold the style, LocationComponentOptions and other locating behaviors.
private fun buildLocationComponentActivationOptions(\n style: Style,\n locationComponentOptions: LocationComponentOptions\n): LocationComponentActivationOptions {\n return LocationComponentActivationOptions\n .builder(this, style)\n .locationComponentOptions(locationComponentOptions)\n .useDefaultLocationEngine(true)\n .locationEngineRequest(\n LocationEngineRequest.Builder(750)\n .setFastestInterval(750)\n .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY)\n .build()\n )\n .build()\n}\n
For further customization, you can also utilize the foregroundTintColor()
and pulseColor()
methods on the LocationComponentOptions builder:
val locationComponentOptions =\n LocationComponentOptions.builder(this@BasicLocationPulsingCircleActivity)\n .pulseEnabled(true)\n .pulseColor(Color.RED) // Set color of pulse\n .foregroundTintColor(Color.BLACK) // Set color of user location\n .build()\n
Here is the final results with different color configurations. For the complete content of this demo, please refer to the source code of the Test App.
Map data OpenStreetMap. \u00a9 OpenMapTiles.
A variety of camera modes determine how the camera will track the user location. They provide the right context to your users at the correct time.\u00a0\u21a9
This guide will help you walk through how to use MapSnapshotter.
"},{"location":"snapshotter/#map-snapshot-with-local-style","title":"Map Snapshot with Local Style","text":"Note
You can find the full source code of this example in MapSnapshotterLocalStyleActivity.kt
of the MapLibreAndroidTestApp.
To get started we will show how to use the map snapshotter with a local style.
Add the source code of the Demotiles style as demotiles.json
to the res/raw
directory of our app1. First we will read this style:
val styleJson = resources.openRawResource(R.raw.demotiles).reader().readText()\n
Next, we configure the MapSnapshotter, passing height and width, the style we just read and the camera position:
mapSnapshotter = MapSnapshotter(\n applicationContext,\n MapSnapshotter.Options(\n container.measuredWidth.coerceAtMost(1024),\n container.measuredHeight.coerceAtMost(1024)\n )\n .withStyleBuilder(Style.Builder().fromJson(styleJson))\n .withCameraPosition(\n CameraPosition.Builder().target(LatLng(LATITUDE, LONGITUDE))\n .zoom(ZOOM).build()\n )\n)\n
Lastly we use the .start()
method to create the snapshot, and pass callbacks for when the snapshot is ready or for when an error occurs.
mapSnapshotter.start({ snapshot ->\n Timber.i(\"Snapshot ready\")\n val imageView = findViewById<View>(R.id.snapshot_image) as ImageView\n imageView.setImageBitmap(snapshot.bitmap)\n}) { error -> Timber.e(error )}\n
"},{"location":"snapshotter/#show-a-grid-of-snapshots","title":"Show a Grid of Snapshots","text":"Note
You can find the full source code of this example in MapSnapshotterActivity.kt
of the MapLibreAndroidTestApp.
In this example, we demonstrate how to use the MapSnapshotter
to create multiple map snapshots with different styles and camera positions, displaying them in a grid layout.
First we create a GridLayout
and a list of MapSnapshotter
instances. We create a Style.Builder
with a different style for each cell in the grid.
val styles = arrayOf(\n TestStyles.DEMOTILES,\n TestStyles.AMERICANA,\n TestStyles.OPENFREEMAP_LIBERY,\n TestStyles.AWS_OPEN_DATA_STANDARD_LIGHT,\n TestStyles.PROTOMAPS_LIGHT,\n TestStyles.PROTOMAPS_DARK,\n TestStyles.PROTOMAPS_WHITE,\n TestStyles.PROTOMAPS_GRAYSCALE,\n TestStyles.VERSATILES\n)\nval builder = Style.Builder().fromUri(\n styles[(row * grid.rowCount + column) % styles.size]\n)\n
Next we create a MapSnapshotter.Options
object to customize the settings of each snapshot(ter).
val options = MapSnapshotter.Options(\n grid.measuredWidth / grid.columnCount,\n grid.measuredHeight / grid.rowCount\n)\n .withPixelRatio(1f)\n .withLocalIdeographFontFamily(MapLibreConstants.DEFAULT_FONT)\n
For some rows we randomize the visible region of the snapshot:
if (row % 2 == 0) {\n options.withRegion(\n LatLngBounds.Builder()\n .include(\n LatLng(\n randomInRange(-80f, 80f).toDouble(),\n randomInRange(-160f, 160f).toDouble()\n )\n )\n .include(\n LatLng(\n randomInRange(-80f, 80f).toDouble(),\n randomInRange(-160f, 160f).toDouble()\n )\n )\n .build()\n )\n}\n
For some columns we randomize the camera position:
if (column % 2 == 0) {\n options.withCameraPosition(\n CameraPosition.Builder()\n .target(\n options.region?.center ?: LatLng(\n randomInRange(-80f, 80f).toDouble(),\n randomInRange(-160f, 160f).toDouble()\n )\n )\n .bearing(randomInRange(0f, 360f).toDouble())\n .tilt(randomInRange(0f, 60f).toDouble())\n .zoom(randomInRange(0f, 10f).toDouble())\n .padding(1.0, 1.0, 1.0, 1.0)\n .build()\n )\n}\n
In the last column of the first row we add two bitmaps. See the next example for more details.
if (row == 0 && column == 2) {\n val carBitmap = BitmapUtils.getBitmapFromDrawable(\n ResourcesCompat.getDrawable(resources, R.drawable.ic_directions_car_black, theme)\n )\n\n // Marker source\n val markerCollection = FeatureCollection.fromFeatures(\n arrayOf(\n Feature.fromGeometry(\n Point.fromLngLat(4.91638, 52.35673),\n featureProperties(\"1\", \"Android\")\n ),\n Feature.fromGeometry(\n Point.fromLngLat(4.91638, 12.34673),\n featureProperties(\"2\", \"Car\")\n )\n )\n )\n val markerSource: Source = GeoJsonSource(MARKER_SOURCE, markerCollection)\n\n // Marker layer\n val markerSymbolLayer = SymbolLayer(MARKER_LAYER, MARKER_SOURCE)\n .withProperties(\n PropertyFactory.iconImage(Expression.get(TITLE_FEATURE_PROPERTY)),\n PropertyFactory.iconIgnorePlacement(true),\n PropertyFactory.iconAllowOverlap(true),\n PropertyFactory.iconSize(\n Expression.switchCase(\n Expression.toBool(Expression.get(SELECTED_FEATURE_PROPERTY)),\n Expression.literal(1.5f),\n Expression.literal(1.0f)\n )\n ),\n PropertyFactory.iconAnchor(Property.ICON_ANCHOR_BOTTOM),\n PropertyFactory.iconColor(Color.BLUE)\n )\n builder.withImage(\"Car\", Objects.requireNonNull(carBitmap!!), false)\n .withSources(markerSource)\n .withLayers(markerSymbolLayer)\n options\n .withRegion(null)\n .withCameraPosition(\n CameraPosition.Builder()\n .target(\n LatLng(5.537109374999999, 52.07950600379697)\n )\n .zoom(1.0)\n .padding(1.0, 1.0, 1.0, 1.0)\n .build()\n )\n}\n
"},{"location":"snapshotter/#map-snapshot-with-bitmap-overlay","title":"Map Snapshot with Bitmap Overlay","text":"Note
You can find the full source code of this example in MapSnapshotterBitMapOverlayActivity.kt
of the MapLibreAndroidTestApp.
This example adds a bitmap on top of the snapshot. It also demonstrates how you can add a click listener to a snapshot.
MapSnapshotterBitMapOverlayActivity.ktpackage org.maplibre.android.testapp.activity.snapshot\n\nimport android.annotation.SuppressLint\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.Canvas\nimport android.graphics.PointF\nimport android.os.Bundle\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.ViewTreeObserver.OnGlobalLayoutListener\nimport android.widget.ImageView\nimport androidx.annotation.VisibleForTesting\nimport androidx.appcompat.app.AppCompatActivity\nimport org.maplibre.android.camera.CameraPosition\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.Style\nimport org.maplibre.android.snapshotter.MapSnapshot\nimport org.maplibre.android.snapshotter.MapSnapshotter\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\nimport timber.log.Timber\n\n/**\n * Test activity showing how to use a the [MapSnapshotter] and overlay\n * [android.graphics.Bitmap]s on top.\n */\nclass MapSnapshotterBitMapOverlayActivity :\n AppCompatActivity(),\n MapSnapshotter.SnapshotReadyCallback {\n private var mapSnapshotter: MapSnapshotter? = null\n\n @get:VisibleForTesting\n var mapSnapshot: MapSnapshot? = null\n private set\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_map_snapshotter_marker)\n val container = findViewById<View>(R.id.container)\n container.viewTreeObserver\n .addOnGlobalLayoutListener(object : OnGlobalLayoutListener {\n override fun onGlobalLayout() {\n container.viewTreeObserver.removeOnGlobalLayoutListener(this)\n Timber.i(\"Starting snapshot\")\n mapSnapshotter = MapSnapshotter(\n applicationContext,\n MapSnapshotter.Options(\n Math.min(container.measuredWidth, 1024),\n Math.min(container.measuredHeight, 1024)\n )\n .withStyleBuilder(\n Style.Builder().fromUri(TestStyles.AMERICANA)\n )\n .withCameraPosition(\n CameraPosition.Builder().target(LatLng(52.090737, 5.121420))\n .zoom(15.0).build()\n )\n )\n mapSnapshotter!!.start(this@MapSnapshotterBitMapOverlayActivity)\n }\n })\n }\n\n override fun onStop() {\n super.onStop()\n mapSnapshotter!!.cancel()\n }\n\n @SuppressLint(\"ClickableViewAccessibility\")\n override fun onSnapshotReady(snapshot: MapSnapshot) {\n mapSnapshot = snapshot\n Timber.i(\"Snapshot ready\")\n val imageView = findViewById<View>(R.id.snapshot_image) as ImageView\n val image = addMarker(snapshot)\n imageView.setImageBitmap(image)\n imageView.setOnTouchListener { v: View?, event: MotionEvent ->\n if (event.action == MotionEvent.ACTION_DOWN) {\n val latLng = snapshot.latLngForPixel(PointF(event.x, event.y))\n Timber.e(\"Clicked LatLng is %s\", latLng)\n return@setOnTouchListener true\n }\n false\n }\n }\n\n private fun addMarker(snapshot: MapSnapshot): Bitmap {\n val canvas = Canvas(snapshot.bitmap)\n val marker =\n BitmapFactory.decodeResource(resources, R.drawable.maplibre_marker_icon_default, null)\n // Dom toren\n val markerLocation = snapshot.pixelForLatLng(LatLng(52.090649433011315, 5.121310651302338))\n canvas.drawBitmap(\n marker, /* Subtract half of the width so we center the bitmap correctly */\n markerLocation.x - marker.width / 2, /* Subtract half of the height so we align the bitmap bottom correctly */\n markerLocation.y - marker.height / 2,\n null\n )\n return snapshot.bitmap\n }\n}\n
"},{"location":"snapshotter/#map-snapshotter-with-heatmap-layer","title":"Map Snapshotter with Heatmap Layer","text":"Note
You can find the full source code of this example in MapSnapshotterHeatMapActivity.kt
of the MapLibreAndroidTestApp.
In this example, we demonstrate how to use the MapSnapshotter
to create a snapshot of a map that includes a heatmap layer. The heatmap represents earthquake data loaded from a GeoJSON source.
First, we create the MapSnapshotterHeatMapActivity
class, which extends AppCompatActivity
and implements MapSnapshotter.SnapshotReadyCallback
to receive the snapshot once it's ready.
class MapSnapshotterHeatMapActivity : AppCompatActivity(), MapSnapshotter.SnapshotReadyCallback {\n
In the onCreate
method, we set up the layout and initialize the MapSnapshotter
once the layout is ready.
override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_map_snapshotter_marker)\n val container = findViewById<View>(R.id.container)\n container.viewTreeObserver\n .addOnGlobalLayoutListener(object : OnGlobalLayoutListener {\n override fun onGlobalLayout() {\n container.viewTreeObserver.removeOnGlobalLayoutListener(this)\n Timber.i(\"Starting snapshot\")\n val builder = Style.Builder().fromUri(TestStyles.AMERICANA)\n .withSource(earthquakeSource!!)\n .withLayerAbove(heatmapLayer, \"water\")\n mapSnapshotter = MapSnapshotter(\n applicationContext,\n MapSnapshotter.Options(container.measuredWidth, container.measuredHeight)\n .withStyleBuilder(builder)\n .withCameraPosition(\n CameraPosition.Builder()\n .target(LatLng(15.0, (-94).toDouble()))\n .zoom(5.0)\n .padding(1.0, 1.0, 1.0, 1.0)\n .build()\n )\n )\n mapSnapshotter!!.start(this@MapSnapshotterHeatMapActivity)\n }\n })\n}\n
Here, we wait for the layout to be laid out using an OnGlobalLayoutListener
before initializing the MapSnapshotter
. We create a Style.Builder
with a base style (TestStyles.AMERICANA
), add the earthquake data source, and add the heatmap layer above the \"water\" layer.
The heatmapLayer
property defines the HeatmapLayer
used to visualize the earthquake data.
private val heatmapLayer: HeatmapLayer\n get() {\n val layer = HeatmapLayer(HEATMAP_LAYER_ID, EARTHQUAKE_SOURCE_ID)\n layer.maxZoom = 9f\n layer.sourceLayer = HEATMAP_LAYER_SOURCE\n layer.setProperties(\n PropertyFactory.heatmapColor(\n Expression.interpolate(\n Expression.linear(), Expression.heatmapDensity(),\n Expression.literal(0), Expression.rgba(33, 102, 172, 0),\n Expression.literal(0.2), Expression.rgb(103, 169, 207),\n Expression.literal(0.4), Expression.rgb(209, 229, 240),\n Expression.literal(0.6), Expression.rgb(253, 219, 199),\n Expression.literal(0.8), Expression.rgb(239, 138, 98),\n Expression.literal(1), Expression.rgb(178, 24, 43)\n )\n ),\n PropertyFactory.heatmapWeight(\n Expression.interpolate(\n Expression.linear(),\n Expression.get(\"mag\"),\n Expression.stop(0, 0),\n Expression.stop(6, 1)\n )\n ),\n PropertyFactory.heatmapIntensity(\n Expression.interpolate(\n Expression.linear(),\n Expression.zoom(),\n Expression.stop(0, 1),\n Expression.stop(9, 3)\n )\n ),\n PropertyFactory.heatmapRadius(\n Expression.interpolate(\n Expression.linear(),\n Expression.zoom(),\n Expression.stop(0, 2),\n Expression.stop(9, 20)\n )\n ),\n PropertyFactory.heatmapOpacity(\n Expression.interpolate(\n Expression.linear(),\n Expression.zoom(),\n Expression.stop(7, 1),\n Expression.stop(9, 0)\n )\n )\n )\n return layer\n }\n
This code sets up the heatmap layer's properties, such as color ramp, weight, intensity, radius, and opacity, using expressions that interpolate based on data properties and zoom level.
We also define the earthquakeSource
, which loads data from a GeoJSON file containing earthquake information.
private val earthquakeSource: Source?\n get() {\n var source: Source? = null\n try {\n source = GeoJsonSource(EARTHQUAKE_SOURCE_ID, URI(EARTHQUAKE_SOURCE_URL))\n } catch (uriSyntaxException: URISyntaxException) {\n Timber.e(uriSyntaxException, \"That's not a valid URL.\")\n }\n return source\n }\n
When the snapshot is ready, the onSnapshotReady
callback is invoked, where we set the snapshot bitmap to an ImageView
to display it.
@SuppressLint(\"ClickableViewAccessibility\")\noverride fun onSnapshotReady(snapshot: MapSnapshot) {\n Timber.i(\"Snapshot ready\")\n val imageView = findViewById<ImageView>(R.id.snapshot_image)\n imageView.setImageBitmap(snapshot.bitmap)\n}\n
Finally, we ensure to cancel the snapshotter in the onStop
method to free up resources.
override fun onStop() {\n super.onStop()\n mapSnapshotter?.cancel()\n}\n
"},{"location":"snapshotter/#map-snapshotter-with-expression","title":"Map Snapshotter with Expression","text":"Note
You can find the full source code of this example in MapSnapshotterWithinExpression.kt
of the MapLibreAndroidTestApp.
In this example the map on top is a live while the map on the bottom is a snapshot that is updated as you pan the map. We style of the snapshot is modified: using a within expression only POIs within a certain distance to a line is shown. A highlight for this area is added to the map as are various points.
MapSnapshotterWithinExpression.ktpackage org.maplibre.android.testapp.activity.turf\n\nimport android.graphics.Color\nimport android.os.Bundle\nimport android.os.PersistableBundle\nimport androidx.appcompat.app.AppCompatActivity\nimport org.maplibre.geojson.*\nimport org.maplibre.android.camera.CameraPosition\nimport org.maplibre.android.camera.CameraUpdateFactory\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.MapView\nimport org.maplibre.android.maps.MapLibreMap\nimport org.maplibre.android.maps.Style\nimport org.maplibre.android.snapshotter.MapSnapshot\nimport org.maplibre.android.snapshotter.MapSnapshotter\nimport org.maplibre.android.style.expressions.Expression.within\nimport org.maplibre.android.style.layers.CircleLayer\nimport org.maplibre.android.style.layers.FillLayer\nimport org.maplibre.android.style.layers.LineLayer\nimport org.maplibre.android.style.layers.Property.NONE\nimport org.maplibre.android.style.layers.PropertyFactory.*\nimport org.maplibre.android.style.layers.SymbolLayer\nimport org.maplibre.android.style.sources.GeoJsonOptions\nimport org.maplibre.android.style.sources.GeoJsonSource\nimport org.maplibre.android.testapp.databinding.ActivityMapsnapshotterWithinExpressionBinding\nimport org.maplibre.android.testapp.styles.TestStyles.getPredefinedStyleWithFallback\n\n/**\n * An Activity that showcases the use of MapSnapshotter with 'within' expression\n */\nclass MapSnapshotterWithinExpression : AppCompatActivity() {\n private lateinit var binding: ActivityMapsnapshotterWithinExpressionBinding\n private lateinit var maplibreMap: MapLibreMap\n private lateinit var snapshotter: MapSnapshotter\n private var snapshotInProgress = false\n\n private val cameraListener = object : MapView.OnCameraDidChangeListener {\n override fun onCameraDidChange(animated: Boolean) {\n if (!snapshotInProgress) {\n snapshotInProgress = true\n snapshotter.setCameraPosition(maplibreMap.cameraPosition)\n snapshotter.start(object : MapSnapshotter.SnapshotReadyCallback {\n override fun onSnapshotReady(snapshot: MapSnapshot) {\n binding.imageView.setImageBitmap(snapshot.bitmap)\n snapshotInProgress = false\n }\n })\n }\n }\n }\n\n private val snapshotterObserver = object : MapSnapshotter.Observer {\n override fun onStyleImageMissing(imageName: String) {\n }\n\n override fun onDidFinishLoadingStyle() {\n // Show only POI labels inside geometry using within expression\n (snapshotter.getLayer(\"poi-label\") as SymbolLayer).setFilter(\n within(\n bufferLineStringGeometry()\n )\n )\n // Hide other types of labels to highlight POI labels\n (snapshotter.getLayer(\"road-label\") as SymbolLayer).setProperties(visibility(NONE))\n (snapshotter.getLayer(\"transit-label\") as SymbolLayer).setProperties(visibility(NONE))\n (snapshotter.getLayer(\"road-number-shield\") as SymbolLayer).setProperties(visibility(NONE))\n }\n }\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n binding = ActivityMapsnapshotterWithinExpressionBinding.inflate(layoutInflater)\n setContentView(binding.root)\n\n binding.mapView.onCreate(savedInstanceState)\n binding.mapView.getMapAsync { map ->\n maplibreMap = map\n\n // Setup camera position above Georgetown\n maplibreMap.cameraPosition = CameraPosition.Builder().target(LatLng(38.90628988399711, -77.06574689337494)).zoom(15.5).build()\n\n // Wait for the map to become idle before manipulating the style and camera of the map\n binding.mapView.addOnDidBecomeIdleListener(object : MapView.OnDidBecomeIdleListener {\n override fun onDidBecomeIdle() {\n maplibreMap.easeCamera(\n CameraUpdateFactory.newCameraPosition(\n CameraPosition.Builder().zoom(16.0).target(LatLng(38.905156245642814, -77.06535338052844)).bearing(80.68015859462369).tilt(55.0).build()\n ),\n 1000\n )\n binding.mapView.removeOnDidBecomeIdleListener(this)\n }\n })\n // Load mapbox streets and add lines and circles\n setupStyle()\n }\n }\n\n private fun setupStyle() {\n // Assume the route is represented by an array of coordinates.\n val coordinates = listOf<Point>(\n Point.fromLngLat(-77.06866264343262, 38.90506061276737),\n Point.fromLngLat(-77.06283688545227, 38.905194197410545),\n Point.fromLngLat(-77.06285834312439, 38.906429843444094),\n Point.fromLngLat(-77.0630407333374, 38.90680554236621)\n )\n\n // Setup style with additional layers,\n // using streets as a base style\n maplibreMap.setStyle(\n Style.Builder().fromUri(getPredefinedStyleWithFallback(\"Streets\"))\n ) {\n binding.mapView.addOnCameraDidChangeListener(cameraListener)\n }\n\n val options = MapSnapshotter.Options(binding.imageView.measuredWidth / 2, binding.imageView.measuredHeight / 2)\n .withCameraPosition(maplibreMap.cameraPosition)\n .withPixelRatio(2.0f).withStyleBuilder(\n Style.Builder().fromUri(getPredefinedStyleWithFallback(\"Streets\")).withSources(\n GeoJsonSource(\n POINT_ID,\n LineString.fromLngLats(coordinates)\n ),\n GeoJsonSource(\n FILL_ID,\n FeatureCollection.fromFeature(\n Feature.fromGeometry(bufferLineStringGeometry())\n ),\n GeoJsonOptions().withBuffer(0).withTolerance(0.0f)\n )\n ).withLayerBelow(\n LineLayer(LINE_ID, POINT_ID).withProperties(\n lineWidth(7.5f),\n lineColor(Color.LTGRAY)\n ),\n \"poi-label\"\n ).withLayerBelow(\n CircleLayer(POINT_ID, POINT_ID).withProperties(\n circleRadius(7.5f),\n circleColor(Color.DKGRAY),\n circleOpacity(0.75f)\n ),\n \"poi-label\"\n ).withLayerBelow(\n FillLayer(FILL_ID, FILL_ID).withProperties(\n fillOpacity(0.12f),\n fillColor(Color.YELLOW)\n ),\n LINE_ID\n )\n )\n snapshotter = MapSnapshotter(this, options)\n snapshotter.setObserver(snapshotterObserver)\n }\n\n override fun onStart() {\n super.onStart()\n binding.mapView.onStart()\n }\n\n override fun onResume() {\n super.onResume()\n binding.mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n binding.mapView.onPause()\n }\n\n override fun onStop() {\n super.onStop()\n binding.mapView.onStop()\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n binding.mapView.onLowMemory()\n }\n\n override fun onDestroy() {\n super.onDestroy()\n binding.mapView.onDestroy()\n }\n\n override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {\n super.onSaveInstanceState(outState, outPersistentState)\n binding.mapView.onSaveInstanceState(outState)\n }\n private fun bufferLineStringGeometry(): Polygon {\n // TODO replace static data by Turf#Buffer: mapbox-java/issues/987\n // # --8<-- [start:fromJson]\n return FeatureCollection.fromJson(\n \"\"\"\n {\n \"type\": \"FeatureCollection\",\n \"features\": [\n {\n \"type\": \"Feature\",\n \"properties\": {},\n \"geometry\": {\n \"type\": \"Polygon\",\n \"coordinates\": [\n [\n [\n -77.06867337226866,\n 38.90467655551809\n ],\n [\n -77.06233263015747,\n 38.90479344272695\n ],\n [\n -77.06234335899353,\n 38.906463238984344\n ],\n [\n -77.06290125846863,\n 38.907206285691615\n ],\n [\n -77.06364154815674,\n 38.90684728656818\n ],\n [\n -77.06326603889465,\n 38.90637140121084\n ],\n [\n -77.06321239471436,\n 38.905561553883246\n ],\n [\n -77.0691454410553,\n 38.905436318935635\n ],\n [\n -77.06912398338318,\n 38.90466820642439\n ],\n [\n -77.06867337226866,\n 38.90467655551809\n ]\n ]\n ]\n }\n }\n ]\n }\n \"\"\".trimIndent()\n ).features()!![0].geometry() as Polygon\n // # --8<-- [end:fromJson]\n }\n\n companion object {\n const val POINT_ID = \"point\"\n const val FILL_ID = \"fill\"\n const val LINE_ID = \"line\"\n }\n}\n
See App resources overview for this and other ways you can provide resources to your app.\u00a0\u21a9
This example demonstrates how you can add markers in bulk.
BulkMarkerActivity.ktpackage org.maplibre.android.testapp.activity.annotation\n\nimport android.app.ProgressDialog\nimport android.os.Bundle\nimport android.view.Menu\nimport android.view.View\nimport android.widget.AdapterView\nimport android.widget.AdapterView.OnItemSelectedListener\nimport android.widget.ArrayAdapter\nimport android.widget.Spinner\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.lifecycleScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.maplibre.android.annotations.MarkerOptions\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.MapView\nimport org.maplibre.android.maps.MapLibreMap\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\nimport org.maplibre.android.testapp.utils.GeoParseUtil\nimport timber.log.Timber\nimport java.io.IOException\nimport java.text.DecimalFormat\nimport java.util.*\nimport kotlin.math.min\n\n/**\n * Test activity showcasing adding a large amount of Markers.\n */\nclass BulkMarkerActivity : AppCompatActivity(), OnItemSelectedListener {\n private lateinit var maplibreMap: MapLibreMap\n private lateinit var mapView: MapView\n private var locations: List<LatLng>? = null\n private var progressDialog: ProgressDialog? = null\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_marker_bulk)\n mapView = findViewById(R.id.mapView)\n mapView.onCreate(savedInstanceState)\n mapView.getMapAsync { initMap(it) }\n }\n\n private fun initMap(maplibreMap: MapLibreMap) {\n this.maplibreMap = maplibreMap\n maplibreMap.setStyle(TestStyles.getPredefinedStyleWithFallback(\"Streets\"))\n }\n\n override fun onCreateOptionsMenu(menu: Menu): Boolean {\n val spinnerAdapter = ArrayAdapter.createFromResource(\n this,\n R.array.bulk_marker_list,\n android.R.layout.simple_spinner_item\n )\n spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)\n menuInflater.inflate(R.menu.menu_bulk_marker, menu)\n val item = menu.findItem(R.id.spinner)\n val spinner = item.actionView as Spinner\n spinner.adapter = spinnerAdapter\n spinner.onItemSelectedListener = this@BulkMarkerActivity\n return true\n }\n\n override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {\n val amount = Integer.valueOf(resources.getStringArray(R.array.bulk_marker_list)[position])\n if (locations == null) {\n progressDialog = ProgressDialog.show(this, \"Loading\", \"Fetching markers\", false)\n lifecycleScope.launch(Dispatchers.IO) {\n locations = loadLocationTask(this@BulkMarkerActivity)\n withContext(Dispatchers.Main) {\n onLatLngListLoaded(locations, amount)\n }\n }\n } else {\n showMarkers(amount)\n }\n }\n\n private fun onLatLngListLoaded(latLngs: List<LatLng>?, amount: Int) {\n progressDialog!!.hide()\n locations = latLngs\n showMarkers(amount)\n }\n\n private fun showMarkers(amount: Int) {\n if (!this::maplibreMap.isInitialized || locations == null || mapView.isDestroyed) {\n return\n }\n maplibreMap.clear()\n showGlMarkers(min(amount, locations!!.size))\n }\n\n private fun showGlMarkers(amount: Int) {\n val markerOptionsList: MutableList<MarkerOptions> = ArrayList()\n val formatter = DecimalFormat(\"#.#####\")\n val random = Random()\n var randomIndex: Int\n for (i in 0 until amount) {\n randomIndex = random.nextInt(locations!!.size)\n val latLng = locations!![randomIndex]\n markerOptionsList.add(\n MarkerOptions()\n .position(latLng)\n .title(i.toString())\n .snippet(formatter.format(latLng.latitude) + \"`, \" + formatter.format(latLng.longitude))\n )\n }\n maplibreMap.addMarkers(markerOptionsList)\n }\n\n override fun onNothingSelected(parent: AdapterView<*>?) {\n // nothing selected, nothing to do!\n }\n\n override fun onStart() {\n super.onStart()\n mapView.onStart()\n }\n\n override fun onResume() {\n super.onResume()\n mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n mapView.onPause()\n }\n\n override fun onStop() {\n super.onStop()\n mapView.onStop()\n }\n\n override fun onSaveInstanceState(outState: Bundle) {\n super.onSaveInstanceState(outState)\n mapView.onSaveInstanceState(outState)\n }\n\n override fun onDestroy() {\n super.onDestroy()\n if (progressDialog != null) {\n progressDialog!!.dismiss()\n }\n mapView.onDestroy()\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n mapView.onLowMemory()\n }\n\n private fun loadLocationTask(\n activity: BulkMarkerActivity,\n ) : List<LatLng>? {\n try {\n val json = GeoParseUtil.loadStringFromAssets(\n activity.applicationContext,\n \"points.geojson\"\n )\n return GeoParseUtil.parseGeoJsonCoordinates(json)\n } catch (exception: IOException) {\n Timber.e(exception, \"Could not add markers\")\n }\n return null\n }\n}\n
"},{"location":"annotations/marker-annotations/","title":"Annotation: Marker","text":"This guide will show you how to add Markers in the map.
Annotation
is an overlay on top of a Map. In package org.maplibre.android.annotations
, it has the following subclasses:
A Marker shows an icon image at a geographical location. By default, marker uses a provided image as its icon.
Or, the icon can be customized using IconFactory to generate an Icon using a provided image.
For more customization, please read the documentation about MarkerOptions.
In this showcase, we continue the code from the Quickstart, rename Activity into JsonApiActivity
, and pull the GeoJSON data from a free and public API. Then add markers to the map with GeoJSON:
In your module Gradle file (usually <project>/<app-module>/build.gradle
), add okhttp
to simplify code for making HTTP requests.
dependencies {\n ...\n implementation 'com.squareup.okhttp3:okhttp:4.10.0'\n ...\n}\n
Sync your Android project the with Gradle files.
In JsonApiActivity
we add a new variable for MapLibreMap
. It is used to add annotations to the map instance.
class JsonApiActivity : AppCompatActivity() {\n\n // Declare a variable for MapView\n private lateinit var mapView: MapView\n\n // Declare a variable for MapLibreMap\n private lateinit var maplibreMap: MapLibreMap\n
Call mapview.getMapSync()
in order to get a MapLibreMap
object. After maplibreMap
is assigned, call the getEarthQuakeDataFromUSGS()
method to make a HTTP request and transform data into the map annotations.
mapView.getMapAsync { map ->\n maplibreMap = map\n\n maplibreMap.setStyle(\"https://demotiles.maplibre.org/style.json\")\n\n // Fetch data from USGS\n getEarthQuakeDataFromUSGS()\n}\n
Define a function getEarthQuakeDataFromUSGS()
to fetch GeoJSON data from a public API. If we successfully get the response, call addMarkersToMap()
on the UI thread.
// Get Earthquake data from usgs.gov, read API doc at:\n// https://earthquake.usgs.gov/fdsnws/event/1/\nprivate fun getEarthQuakeDataFromUSGS() {\n val url = \"https://earthquake.usgs.gov/fdsnws/event/1/query\".toHttpUrl().newBuilder()\n .addQueryParameter(\"format\", \"geojson\")\n .addQueryParameter(\"starttime\", \"2022-01-01\")\n .addQueryParameter(\"endtime\", \"2022-12-31\")\n .addQueryParameter(\"minmagnitude\", \"5.8\")\n .addQueryParameter(\"latitude\", \"24\")\n .addQueryParameter(\"longitude\", \"121\")\n .addQueryParameter(\"maxradius\", \"1.5\")\n .build()\n val request: Request = Request.Builder().url(url).build()\n\n OkHttpClient().newCall(request).enqueue(object : Callback {\n override fun onFailure(call: Call, e: IOException) {\n Toast.makeText(this@JsonApiActivity, \"Fail to fetch data\", Toast.LENGTH_SHORT)\n .show()\n }\n\n override fun onResponse(call: Call, response: Response) {\n val featureCollection = response.body?.string()\n ?.let(FeatureCollection::fromJson)\n ?: return\n // If FeatureCollection in response is not null\n // Then add markers to map\n runOnUiThread { addMarkersToMap(featureCollection) }\n }\n })\n}\n
Now it is time to add markers into the map.
addMarkersToMap()
method, we define two types of bitmap for the marker icon.Finally, move the camera to the bounds of the newly added markers
private fun addMarkersToMap(data: FeatureCollection) {\n val bounds = mutableListOf<LatLng>()\n\n // Get bitmaps for marker icon\n val infoIconDrawable = ResourcesCompat.getDrawable(\n this.resources,\n // Intentionally specify package name\n // This makes copy from another project easier\n org.maplibre.android.R.drawable.maplibre_info_icon_default,\n theme\n )!!\n val bitmapBlue = infoIconDrawable.toBitmap()\n val bitmapRed = infoIconDrawable\n .mutate()\n .apply { setTint(Color.RED) }\n .toBitmap()\n\n // Add symbol for each point feature\n data.features()?.forEach { feature ->\n val geometry = feature.geometry()?.toJson() ?: return@forEach\n val point = Point.fromJson(geometry) ?: return@forEach\n val latLng = LatLng(point.latitude(), point.longitude())\n bounds.add(latLng)\n\n // Contents in InfoWindow of each marker\n val title = feature.getStringProperty(\"title\")\n val epochTime = feature.getNumberProperty(\"time\")\n val dateString = SimpleDateFormat(\"yyyy/MM/dd HH:mm\", Locale.TAIWAN).format(epochTime)\n\n // If magnitude > 6.0, show marker with red icon. If not, show blue icon instead\n val mag = feature.getNumberProperty(\"mag\")\n val icon = IconFactory.getInstance(this)\n .fromBitmap(if (mag.toFloat() > 6.0) bitmapRed else bitmapBlue)\n\n // Use MarkerOptions and addMarker() to add a new marker in map\n val markerOptions = MarkerOptions()\n .position(latLng)\n .title(dateString)\n .snippet(title)\n .icon(icon)\n maplibreMap.addMarker(markerOptions)\n }\n\n // Move camera to newly added annotations\n maplibreMap.getCameraForLatLngBounds(LatLngBounds.fromLatLngs(bounds))?.let {\n val newCameraPosition = CameraPosition.Builder()\n .target(it.target)\n .zoom(it.zoom - 0.5)\n .build()\n maplibreMap.cameraPosition = newCameraPosition\n }\n}\n
Here is the final result. For the full contents of JsonApiActivity
, please visit source code of our Test App.
Note
You can find the full source code of this example in CameraAnimationTypeActivity.kt
of the MapLibreAndroidTestApp.
This example showcases the different animation types.
MapLibreMap.moveCamera
method.MapLibreMap.easeCamera
method.MapLibreMap.animateCamera
method.The MapLibreMap.moveCamera
method jumps to the camera position provided.
val cameraPosition =\n CameraPosition.Builder()\n .target(nextLatLng)\n .zoom(14.0)\n .tilt(30.0)\n .tilt(0.0)\n .build()\nmaplibreMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))\n
"},{"location":"camera/animation-types/#ease","title":"Ease","text":"The MapLibreMap.moveCamera
eases to the camera position provided (with constant ground speed).
val cameraPosition =\n CameraPosition.Builder()\n .target(nextLatLng)\n .zoom(15.0)\n .bearing(180.0)\n .tilt(30.0)\n .build()\nmaplibreMap.easeCamera(\n CameraUpdateFactory.newCameraPosition(cameraPosition),\n 7500,\n callback\n)\n
"},{"location":"camera/animation-types/#animate","title":"Animate","text":"The MapLibreMap.animateCamera
uses a powered flight animation move to the camera position provided1.
val cameraPosition =\n CameraPosition.Builder().target(nextLatLng).bearing(270.0).tilt(20.0).build()\nmaplibreMap.animateCamera(\n CameraUpdateFactory.newCameraPosition(cameraPosition),\n 7500,\n callback\n)\n
"},{"location":"camera/animation-types/#animation-callbacks","title":"Animation Callbacks","text":"In the previous section a CancellableCallback
was passed to the last two animation methods. This callback shows a toast message when the animation is cancelled or when it is finished.
private val callback: CancelableCallback =\n object : CancelableCallback {\n override fun onCancel() {\n Timber.i(\"Duration onCancel Callback called.\")\n Toast.makeText(\n applicationContext,\n \"Ease onCancel Callback called.\",\n Toast.LENGTH_LONG\n )\n .show()\n }\n\n override fun onFinish() {\n Timber.i(\"Duration onFinish Callback called.\")\n Toast.makeText(\n applicationContext,\n \"Ease onFinish Callback called.\",\n Toast.LENGTH_LONG\n )\n .show()\n }\n }\n
The implementation is based on Van Wijk, Jarke J.; Nuij, Wim A. A. \u201cSmooth and efficient zooming and panning.\u201d INFOVIS \u201903. pp. 15\u201322. https://www.win.tue.nl/~vanwijk/zoompan.pdf#page=5 \u21a9
This example showcases how to use the Animator API to schedule a sequence of map animations.
CameraAnimatorActivity.ktpackage org.maplibre.android.testapp.activity.camera\n\nimport android.animation.Animator\nimport android.animation.AnimatorSet\nimport android.animation.TypeEvaluator\nimport android.animation.ValueAnimator\nimport android.os.Bundle\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.animation.AnticipateOvershootInterpolator\nimport android.view.animation.BounceInterpolator\nimport android.view.animation.Interpolator\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.collection.LongSparseArray\nimport androidx.core.view.animation.PathInterpolatorCompat\nimport androidx.interpolator.view.animation.FastOutLinearInInterpolator\nimport androidx.interpolator.view.animation.FastOutSlowInInterpolator\nimport org.maplibre.android.camera.CameraPosition\nimport org.maplibre.android.camera.CameraUpdateFactory\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.*\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\n\n/** Test activity showcasing using Android SDK animators to animate camera position changes. */\nclass CameraAnimatorActivity : AppCompatActivity(), OnMapReadyCallback {\n private val animators = LongSparseArray<Animator>()\n private lateinit var set: Animator\n private lateinit var mapView: MapView\n private lateinit var maplibreMap: MapLibreMap\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_camera_animator)\n mapView = findViewById<View>(R.id.mapView) as MapView\n if (::mapView.isInitialized) {\n mapView.onCreate(savedInstanceState)\n mapView.getMapAsync(this)\n }\n }\n\n override fun onMapReady(map: MapLibreMap) {\n maplibreMap = map\n map.setStyle(TestStyles.getPredefinedStyleWithFallback(\"Streets\"))\n initFab()\n }\n\n private fun initFab() {\n findViewById<View>(R.id.fab).setOnClickListener { view: View ->\n view.visibility = View.GONE\n val animatedPosition =\n CameraPosition.Builder()\n .target(LatLng(37.789992, -122.402214))\n .tilt(60.0)\n .zoom(14.5)\n .bearing(135.0)\n .build()\n set = createExampleAnimator(maplibreMap.cameraPosition, animatedPosition)\n set.start()\n }\n }\n\n //\n // Animator API used for the animation on the FAB\n //\n private fun createExampleAnimator(\n currentPosition: CameraPosition,\n targetPosition: CameraPosition\n ): Animator {\n val animatorSet = AnimatorSet()\n animatorSet.play(createLatLngAnimator(currentPosition.target!!, targetPosition.target!!))\n animatorSet.play(createZoomAnimator(currentPosition.zoom, targetPosition.zoom))\n animatorSet.play(createBearingAnimator(currentPosition.bearing, targetPosition.bearing))\n animatorSet.play(createTiltAnimator(currentPosition.tilt, targetPosition.tilt))\n return animatorSet\n }\n\n private fun createLatLngAnimator(currentPosition: LatLng, targetPosition: LatLng): Animator {\n val latLngAnimator =\n ValueAnimator.ofObject(LatLngEvaluator(), currentPosition, targetPosition)\n latLngAnimator.duration = (1000 * ANIMATION_DELAY_FACTOR).toLong()\n latLngAnimator.interpolator = FastOutSlowInInterpolator()\n latLngAnimator.addUpdateListener { animation: ValueAnimator ->\n maplibreMap.moveCamera(\n CameraUpdateFactory.newLatLng((animation.animatedValue as LatLng))\n )\n }\n return latLngAnimator\n }\n\n private fun createZoomAnimator(currentZoom: Double, targetZoom: Double): Animator {\n val zoomAnimator = ValueAnimator.ofFloat(currentZoom.toFloat(), targetZoom.toFloat())\n zoomAnimator.duration = (2200 * ANIMATION_DELAY_FACTOR).toLong()\n zoomAnimator.startDelay = (600 * ANIMATION_DELAY_FACTOR).toLong()\n zoomAnimator.interpolator = AnticipateOvershootInterpolator()\n zoomAnimator.addUpdateListener { animation: ValueAnimator ->\n maplibreMap.moveCamera(\n CameraUpdateFactory.zoomTo((animation.animatedValue as Float).toDouble())\n )\n }\n return zoomAnimator\n }\n\n private fun createBearingAnimator(currentBearing: Double, targetBearing: Double): Animator {\n val bearingAnimator =\n ValueAnimator.ofFloat(currentBearing.toFloat(), targetBearing.toFloat())\n bearingAnimator.duration = (1000 * ANIMATION_DELAY_FACTOR).toLong()\n bearingAnimator.startDelay = (1000 * ANIMATION_DELAY_FACTOR).toLong()\n bearingAnimator.interpolator = FastOutLinearInInterpolator()\n bearingAnimator.addUpdateListener { animation: ValueAnimator ->\n maplibreMap.moveCamera(\n CameraUpdateFactory.bearingTo((animation.animatedValue as Float).toDouble())\n )\n }\n return bearingAnimator\n }\n\n private fun createTiltAnimator(currentTilt: Double, targetTilt: Double): Animator {\n val tiltAnimator = ValueAnimator.ofFloat(currentTilt.toFloat(), targetTilt.toFloat())\n tiltAnimator.duration = (1000 * ANIMATION_DELAY_FACTOR).toLong()\n tiltAnimator.startDelay = (1500 * ANIMATION_DELAY_FACTOR).toLong()\n tiltAnimator.addUpdateListener { animation: ValueAnimator ->\n maplibreMap.moveCamera(\n CameraUpdateFactory.tiltTo((animation.animatedValue as Float).toDouble())\n )\n }\n return tiltAnimator\n }\n\n //\n // Interpolator examples\n //\n private fun obtainExampleInterpolator(menuItemId: Int): Animator? {\n return animators[menuItemId.toLong()]\n }\n\n override fun onCreateOptionsMenu(menu: Menu): Boolean {\n menuInflater.inflate(R.menu.menu_animator, menu)\n return true\n }\n\n override fun onOptionsItemSelected(item: MenuItem): Boolean {\n if (!::maplibreMap.isInitialized) {\n return false\n }\n if (item.itemId != android.R.id.home) {\n findViewById<View>(R.id.fab).visibility = View.GONE\n resetCameraPosition()\n playAnimation(item.itemId)\n }\n return super.onOptionsItemSelected(item)\n }\n\n private fun resetCameraPosition() {\n maplibreMap.moveCamera(\n CameraUpdateFactory.newCameraPosition(\n CameraPosition.Builder()\n .target(START_LAT_LNG)\n .zoom(11.0)\n .bearing(0.0)\n .tilt(0.0)\n .build()\n )\n )\n }\n\n private fun playAnimation(itemId: Int) {\n val animator = obtainExampleInterpolator(itemId)\n if (animator != null) {\n animator.cancel()\n animator.start()\n }\n }\n\n private fun obtainExampleInterpolator(interpolator: Interpolator, duration: Long): Animator {\n val zoomAnimator = ValueAnimator.ofFloat(11.0f, 16.0f)\n zoomAnimator.duration = (duration * ANIMATION_DELAY_FACTOR).toLong()\n zoomAnimator.interpolator = interpolator\n zoomAnimator.addUpdateListener { animation: ValueAnimator ->\n maplibreMap.moveCamera(\n CameraUpdateFactory.zoomTo((animation.animatedValue as Float).toDouble())\n )\n }\n return zoomAnimator\n }\n\n //\n // MapView lifecycle\n //\n override fun onStart() {\n super.onStart()\n mapView.onStart()\n }\n\n override fun onResume() {\n super.onResume()\n mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n mapView.onPause()\n }\n\n override fun onStop() {\n super.onStop()\n mapView.onStop()\n for (i in 0 until animators.size()) {\n animators[animators.keyAt(i)]!!.cancel()\n }\n if (this::set.isInitialized) {\n set.cancel()\n }\n }\n\n override fun onSaveInstanceState(outState: Bundle) {\n super.onSaveInstanceState(outState)\n mapView.onSaveInstanceState(outState)\n }\n\n override fun onDestroy() {\n super.onDestroy()\n if (::mapView.isInitialized) {\n mapView.onDestroy()\n }\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n if (::mapView.isInitialized) {\n mapView.onLowMemory()\n }\n }\n\n /** Helper class to evaluate LatLng objects with a ValueAnimator */\n private class LatLngEvaluator : TypeEvaluator<LatLng> {\n private val latLng = LatLng()\n override fun evaluate(fraction: Float, startValue: LatLng, endValue: LatLng): LatLng {\n latLng.latitude = startValue.latitude + (endValue.latitude - startValue.latitude) * fraction\n latLng.longitude = startValue.longitude + (endValue.longitude - startValue.longitude) * fraction\n return latLng\n }\n }\n\n companion object {\n private const val ANIMATION_DELAY_FACTOR = 1.5\n private val START_LAT_LNG = LatLng(37.787947, -122.407432)\n }\n\n init {\n val accelerateDecelerateAnimatorSet = AnimatorSet()\n accelerateDecelerateAnimatorSet.playTogether(\n createLatLngAnimator(START_LAT_LNG, LatLng(37.826715, -122.422795)),\n obtainExampleInterpolator(FastOutSlowInInterpolator(), 2500)\n )\n animators.put(\n R.id.menu_action_accelerate_decelerate_interpolator.toLong(),\n accelerateDecelerateAnimatorSet\n )\n val bounceAnimatorSet = AnimatorSet()\n bounceAnimatorSet.playTogether(\n createLatLngAnimator(START_LAT_LNG, LatLng(37.787947, -122.407432)),\n obtainExampleInterpolator(BounceInterpolator(), 3750)\n )\n animators.put(R.id.menu_action_bounce_interpolator.toLong(), bounceAnimatorSet)\n animators.put(\n R.id.menu_action_anticipate_overshoot_interpolator.toLong(),\n obtainExampleInterpolator(AnticipateOvershootInterpolator(), 2500)\n )\n animators.put(\n R.id.menu_action_path_interpolator.toLong(),\n obtainExampleInterpolator(\n PathInterpolatorCompat.create(.22f, .68f, 0f, 1.71f),\n 2500\n )\n )\n }\n}\n
"},{"location":"camera/cameraposition/","title":"CameraPosition Capabilities","text":"Note
You can find the full source code of this example in CameraPositionActivity.kt
of the MapLibreAndroidTestApp.
This example showcases how to listen to camera change events.
The camera animation is kicked off with this code:
val cameraPosition = CameraPosition.Builder().target(LatLng(latitude, longitude)).zoom(zoom).bearing(bearing).tilt(tilt).build()\n\nmaplibreMap?.animateCamera(\n CameraUpdateFactory.newCameraPosition(cameraPosition),\n 5000,\n object : CancelableCallback {\n override fun onCancel() {\n Timber.v(\"OnCancel called\")\n }\n\n override fun onFinish() {\n Timber.v(\"OnFinish called\")\n }\n }\n)\n
Notice how the color of the button in the bottom right changes color. Depending on the state of the camera.
We can listen for changes to the state of the camera by registering a OnCameraMoveListener
, OnCameraIdleListener
, OnCameraMoveCanceledListener
or OnCameraMoveStartedListener
with the MapLibreMap
. For example, the OnCameraMoveListener
is defined with:
private val moveListener = OnCameraMoveListener {\n Timber.e(\"OnCameraMove\")\n fab.setColorFilter(\n ContextCompat.getColor(this@CameraPositionActivity, android.R.color.holo_orange_dark)\n )\n}\n
And registered with:
maplibreMap.addOnCameraMoveListener(moveListener)\n
Refer to the full example to learn the methods to register the other types of camera change events.
"},{"location":"camera/gesture-detector/","title":"Gesture Detector","text":"The gesture detector of MapLibre Android is encapsulated in the maplibre-gestures-android
package.
You can add listeners for move, rotate, scale and shove gestures. For example, adding a move gesture listener with MapLibreMap.addOnRotateListener
:
maplibreMap.addOnMoveListener(\n object : OnMoveListener {\n override fun onMoveBegin(detector: MoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_START, \"MOVE START\")\n )\n }\n\n override fun onMove(detector: MoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_PROGRESS, \"MOVE PROGRESS\")\n )\n }\n\n override fun onMoveEnd(detector: MoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_END, \"MOVE END\")\n )\n recalculateFocalPoint()\n }\n }\n)\n
Refer to the full example below for examples of listeners for the other gesture types.
"},{"location":"camera/gesture-detector/#settings","title":"Settings","text":"You can access an UISettings
object via MapLibreMap.uiSettings
. Available settings include:
UiSettings.isQuickZoomGesturesEnabled
).UiSettings.isScaleVelocityAnimationEnabled
.uiSettings.isRotateGesturesEnabled
.uiSettings.isZoomGesturesEnabled
.package org.maplibre.android.testapp.activity.camera\n\nimport android.annotation.SuppressLint\nimport android.os.Bundle\nimport android.os.Handler\nimport android.os.Looper\nimport android.view.LayoutInflater\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.RelativeLayout\nimport android.widget.TextView\nimport androidx.annotation.ColorInt\nimport androidx.annotation.IntDef\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.content.ContextCompat\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport org.maplibre.android.gestures.AndroidGesturesManager\nimport org.maplibre.android.gestures.MoveGestureDetector\nimport org.maplibre.android.gestures.RotateGestureDetector\nimport org.maplibre.android.gestures.ShoveGestureDetector\nimport org.maplibre.android.gestures.StandardScaleGestureDetector\nimport org.maplibre.android.annotations.Marker\nimport org.maplibre.android.annotations.MarkerOptions\nimport org.maplibre.android.camera.CameraUpdateFactory\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.MapLibreMap\nimport org.maplibre.android.maps.MapLibreMap.CancelableCallback\nimport org.maplibre.android.maps.MapLibreMap.OnMoveListener\nimport org.maplibre.android.maps.MapLibreMap.OnRotateListener\nimport org.maplibre.android.maps.MapLibreMap.OnScaleListener\nimport org.maplibre.android.maps.MapLibreMap.OnShoveListener\nimport org.maplibre.android.maps.MapView\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\nimport org.maplibre.android.testapp.utils.FontCache\nimport org.maplibre.android.testapp.utils.ResourceUtils\n\n/** Test activity showcasing APIs around gestures implementation. */\nclass GestureDetectorActivity : AppCompatActivity() {\n private lateinit var mapView: MapView\n private lateinit var maplibreMap: MapLibreMap\n private lateinit var recyclerView: RecyclerView\n private var gestureAlertsAdapter: GestureAlertsAdapter? = null\n private var gesturesManager: AndroidGesturesManager? = null\n private var marker: Marker? = null\n private var focalPointLatLng: LatLng? = null\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_gesture_detector)\n mapView = findViewById(R.id.mapView)\n mapView.onCreate(savedInstanceState)\n mapView.getMapAsync { map: MapLibreMap ->\n maplibreMap = map\n maplibreMap.setStyle(TestStyles.getPredefinedStyleWithFallback(\"Streets\"))\n initializeMap()\n }\n recyclerView = findViewById(R.id.alerts_recycler)\n recyclerView.setLayoutManager(LinearLayoutManager(this))\n gestureAlertsAdapter = GestureAlertsAdapter()\n recyclerView.setAdapter(gestureAlertsAdapter)\n }\n\n override fun onResume() {\n super.onResume()\n mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n gestureAlertsAdapter!!.cancelUpdates()\n mapView.onPause()\n }\n\n override fun onStart() {\n super.onStart()\n mapView.onStart()\n }\n\n override fun onStop() {\n super.onStop()\n mapView.onStop()\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n mapView.onLowMemory()\n }\n\n override fun onDestroy() {\n super.onDestroy()\n mapView.onDestroy()\n }\n\n override fun onSaveInstanceState(outState: Bundle) {\n super.onSaveInstanceState(outState)\n mapView.onSaveInstanceState(outState)\n }\n\n private fun initializeMap() {\n gesturesManager = maplibreMap.gesturesManager\n val layoutParams = recyclerView.layoutParams as RelativeLayout.LayoutParams\n layoutParams.height = (mapView.height / 1.75).toInt()\n layoutParams.width = mapView.width / 3\n recyclerView.layoutParams = layoutParams\n attachListeners()\n fixedFocalPointEnabled(maplibreMap.uiSettings.focalPoint != null)\n }\n\n fun attachListeners() {\n // # --8<-- [start:addOnMoveListener]\n maplibreMap.addOnMoveListener(\n object : OnMoveListener {\n override fun onMoveBegin(detector: MoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_START, \"MOVE START\")\n )\n }\n\n override fun onMove(detector: MoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_PROGRESS, \"MOVE PROGRESS\")\n )\n }\n\n override fun onMoveEnd(detector: MoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_END, \"MOVE END\")\n )\n recalculateFocalPoint()\n }\n }\n )\n // # --8<-- [end:addOnMoveListener]\n maplibreMap.addOnRotateListener(\n object : OnRotateListener {\n override fun onRotateBegin(detector: RotateGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_START, \"ROTATE START\")\n )\n }\n\n override fun onRotate(detector: RotateGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_PROGRESS, \"ROTATE PROGRESS\")\n )\n recalculateFocalPoint()\n }\n\n override fun onRotateEnd(detector: RotateGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_END, \"ROTATE END\")\n )\n }\n }\n )\n maplibreMap.addOnScaleListener(\n object : OnScaleListener {\n override fun onScaleBegin(detector: StandardScaleGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_START, \"SCALE START\")\n )\n if (focalPointLatLng != null) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(\n GestureAlert.TYPE_OTHER,\n \"INCREASING MOVE THRESHOLD\"\n )\n )\n gesturesManager!!.moveGestureDetector.moveThreshold =\n ResourceUtils.convertDpToPx(this@GestureDetectorActivity, 175f)\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(\n GestureAlert.TYPE_OTHER,\n \"MANUALLY INTERRUPTING MOVE\"\n )\n )\n gesturesManager!!.moveGestureDetector.interrupt()\n }\n recalculateFocalPoint()\n }\n\n override fun onScale(detector: StandardScaleGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_PROGRESS, \"SCALE PROGRESS\")\n )\n }\n\n override fun onScaleEnd(detector: StandardScaleGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_END, \"SCALE END\")\n )\n if (focalPointLatLng != null) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(\n GestureAlert.TYPE_OTHER,\n \"REVERTING MOVE THRESHOLD\"\n )\n )\n gesturesManager!!.moveGestureDetector.moveThreshold = 0f\n }\n }\n }\n )\n maplibreMap.addOnShoveListener(\n object : OnShoveListener {\n override fun onShoveBegin(detector: ShoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_START, \"SHOVE START\")\n )\n }\n\n override fun onShove(detector: ShoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_PROGRESS, \"SHOVE PROGRESS\")\n )\n }\n\n override fun onShoveEnd(detector: ShoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_END, \"SHOVE END\")\n )\n }\n }\n )\n }\n\n override fun onCreateOptionsMenu(menu: Menu): Boolean {\n menuInflater.inflate(R.menu.menu_gestures, menu)\n return true\n }\n\n override fun onOptionsItemSelected(item: MenuItem): Boolean {\n val uiSettings = maplibreMap.uiSettings\n when (item.itemId) {\n R.id.menu_gesture_focus_point -> {\n fixedFocalPointEnabled(focalPointLatLng == null)\n return true\n }\n R.id.menu_gesture_animation -> {\n uiSettings.isScaleVelocityAnimationEnabled =\n !uiSettings.isScaleVelocityAnimationEnabled\n uiSettings.isRotateVelocityAnimationEnabled =\n !uiSettings.isRotateVelocityAnimationEnabled\n uiSettings.isFlingVelocityAnimationEnabled =\n !uiSettings.isFlingVelocityAnimationEnabled\n return true\n }\n R.id.menu_gesture_rotate -> {\n uiSettings.isRotateGesturesEnabled = !uiSettings.isRotateGesturesEnabled\n return true\n }\n R.id.menu_gesture_tilt -> {\n uiSettings.isTiltGesturesEnabled = !uiSettings.isTiltGesturesEnabled\n return true\n }\n R.id.menu_gesture_zoom -> {\n uiSettings.isZoomGesturesEnabled = !uiSettings.isZoomGesturesEnabled\n return true\n }\n R.id.menu_gesture_scroll -> {\n uiSettings.isScrollGesturesEnabled = !uiSettings.isScrollGesturesEnabled\n return true\n }\n R.id.menu_gesture_double_tap -> {\n uiSettings.isDoubleTapGesturesEnabled = !uiSettings.isDoubleTapGesturesEnabled\n return true\n }\n R.id.menu_gesture_quick_zoom -> {\n uiSettings.isQuickZoomGesturesEnabled = !uiSettings.isQuickZoomGesturesEnabled\n return true\n }\n R.id.menu_gesture_scroll_horizontal -> {\n uiSettings.isHorizontalScrollGesturesEnabled =\n !uiSettings.isHorizontalScrollGesturesEnabled\n return true\n }\n }\n return super.onOptionsItemSelected(item)\n }\n\n private fun fixedFocalPointEnabled(enabled: Boolean) {\n if (enabled) {\n focalPointLatLng = LatLng(51.50325, -0.12968)\n marker = maplibreMap.addMarker(MarkerOptions().position(focalPointLatLng))\n maplibreMap.easeCamera(\n CameraUpdateFactory.newLatLngZoom(focalPointLatLng!!, 16.0),\n object : CancelableCallback {\n override fun onCancel() {\n recalculateFocalPoint()\n }\n\n override fun onFinish() {\n recalculateFocalPoint()\n }\n }\n )\n } else {\n if (marker != null) {\n maplibreMap.removeMarker(marker!!)\n marker = null\n }\n focalPointLatLng = null\n maplibreMap.uiSettings.focalPoint = null\n }\n }\n\n private fun recalculateFocalPoint() {\n if (focalPointLatLng != null) {\n maplibreMap.uiSettings.focalPoint =\n maplibreMap.projection.toScreenLocation(focalPointLatLng!!)\n }\n }\n\n private class GestureAlertsAdapter : RecyclerView.Adapter<GestureAlertsAdapter.ViewHolder>() {\n private var isUpdating = false\n private val updateHandler = Handler(Looper.getMainLooper())\n private val alerts: MutableList<GestureAlert> = ArrayList()\n\n class ViewHolder internal constructor(view: View) : RecyclerView.ViewHolder(view) {\n var alertMessageTv: TextView\n\n init {\n val typeface = FontCache.get(\"Roboto-Regular.ttf\", view.context)\n alertMessageTv = view.findViewById(R.id.alert_message)\n alertMessageTv.typeface = typeface\n }\n }\n\n override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n val view =\n LayoutInflater.from(parent.context)\n .inflate(R.layout.item_gesture_alert, parent, false)\n return ViewHolder(view)\n }\n\n override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n val alert = alerts[position]\n holder.alertMessageTv.text = alert.message\n holder.alertMessageTv.setTextColor(\n ContextCompat.getColor(holder.alertMessageTv.context, alert.color)\n )\n }\n\n override fun getItemCount(): Int {\n return alerts.size\n }\n\n fun addAlert(alert: GestureAlert) {\n for (gestureAlert in alerts) {\n if (gestureAlert.alertType != GestureAlert.TYPE_PROGRESS) {\n break\n }\n if (alert.alertType == GestureAlert.TYPE_PROGRESS && gestureAlert == alert) {\n return\n }\n }\n if (itemCount >= MAX_NUMBER_OF_ALERTS) {\n alerts.removeAt(itemCount - 1)\n }\n alerts.add(0, alert)\n if (!isUpdating) {\n isUpdating = true\n updateHandler.postDelayed(updateRunnable, 250)\n }\n }\n\n @SuppressLint(\"NotifyDataSetChanged\")\n private val updateRunnable = Runnable {\n notifyDataSetChanged()\n isUpdating = false\n }\n\n fun cancelUpdates() {\n updateHandler.removeCallbacksAndMessages(null)\n }\n }\n\n private class GestureAlert(\n @field:Type @param:Type\n val alertType: Int,\n val message: String?\n ) {\n @Retention(AnnotationRetention.SOURCE)\n @IntDef(TYPE_NONE, TYPE_START, TYPE_PROGRESS, TYPE_END, TYPE_OTHER)\n annotation class Type\n\n @ColorInt var color = 0\n override fun equals(other: Any?): Boolean {\n if (this === other) {\n return true\n }\n if (other == null || javaClass != other.javaClass) {\n return false\n }\n val that = other as GestureAlert\n if (alertType != that.alertType) {\n return false\n }\n return if (message != null) message == that.message else that.message == null\n }\n\n override fun hashCode(): Int {\n var result = alertType\n result = 31 * result + (message?.hashCode() ?: 0)\n return result\n }\n\n companion object {\n const val TYPE_NONE = 0\n const val TYPE_START = 1\n const val TYPE_END = 2\n const val TYPE_PROGRESS = 3\n const val TYPE_OTHER = 4\n }\n\n init {\n when (alertType) {\n TYPE_NONE -> color = android.R.color.black\n TYPE_END -> color = android.R.color.holo_red_dark\n TYPE_OTHER -> color = android.R.color.holo_purple\n TYPE_PROGRESS -> color = android.R.color.holo_orange_dark\n TYPE_START -> color = android.R.color.holo_green_dark\n }\n }\n }\n\n companion object {\n private const val MAX_NUMBER_OF_ALERTS = 30\n }\n}\n
"},{"location":"camera/lat-lng-bounds/","title":"LatLngBounds API","text":"Note
You can find the full source code of this example in LatLngBoundsActivity.kt
of the MapLibreAndroidTestApp.
This example demonstrates setting the camera to some bounds defined by some features. It sets these bounds when the map is initialized and when the bottom sheet is opened or closed.
Here you can see how the feature collection is loaded and how MapLibreMap.getCameraForLatLngBounds
is used to set the bounds during map initialization:
val featureCollection: FeatureCollection =\n fromJson(GeoParseUtil.loadStringFromAssets(this, \"points-sf.geojson\"))\nbounds = createBounds(featureCollection)\n\nmap.getCameraForLatLngBounds(bounds, createPadding(peekHeight))?.let {\n map.cameraPosition = it\n}\n
The createBounds
function uses the LatLngBounds
API to include all points within the bounds:
private fun createBounds(featureCollection: FeatureCollection): LatLngBounds {\n val boundsBuilder = LatLngBounds.Builder()\n featureCollection.features()?.let {\n for (feature in it) {\n val point = feature.geometry() as Point\n boundsBuilder.include(LatLng(point.latitude(), point.longitude()))\n }\n }\n return boundsBuilder.build()\n}\n
"},{"location":"camera/max-min-zoom/","title":"Max/Min Zoom","text":"Note
You can find the full source code of this example in MaxMinZoomActivity.kt
of the MapLibreAndroidTestApp.
This example shows how to configure a maximum and a minimum zoom level.
maplibreMap.setMinZoomPreference(3.0)\nmaplibreMap.setMaxZoomPreference(5.0)\n
"},{"location":"camera/max-min-zoom/#bonus-add-click-listener","title":"Bonus: Add Click Listener","text":"As a bonus, this example also shows how you can define a click listener to the map.
maplibreMap.addOnMapClickListener {\n if (this::maplibreMap.isInitialized) {\n maplibreMap.setStyle(Style.Builder().fromUri(TestStyles.AMERICANA))\n }\n true\n}\n
You can remove a click listener again with MapLibreMap.removeOnMapClickListener
. To use this API you need to assign the click listener to a variable, since you need to pass the listener to that method.
Note
You can find the full source code of this example in ScrollByActivity.kt
of the MapLibreAndroidTestApp.
This example shows how you can move the map by x/y pixels.
maplibreMap.scrollBy(\n (seekBarX.progress * MULTIPLIER_PER_PIXEL).toFloat(),\n (seekBarY.progress * MULTIPLIER_PER_PIXEL).toFloat()\n)\n
"},{"location":"camera/zoom-methods/","title":"Zoom Methods","text":"Note
You can find the full source code of this example in ManualZoomActivity.kt
of the MapLibreAndroidTestApp.
This example shows different methods of zooming in.
Each method uses MapLibreMap.animateCamera
, but with a different CameraUpdateFactory
.
maplibreMap.animateCamera(CameraUpdateFactory.zoomIn())\n
"},{"location":"camera/zoom-methods/#zooming-out","title":"Zooming Out","text":"maplibreMap.animateCamera(CameraUpdateFactory.zoomOut())\n
"},{"location":"camera/zoom-methods/#zoom-by-some-amount-of-zoom-levels","title":"Zoom By Some Amount of Zoom Levels","text":"maplibreMap.animateCamera(CameraUpdateFactory.zoomBy(2.0))\n
"},{"location":"camera/zoom-methods/#zoom-to-a-zoom-level","title":"Zoom to a Zoom Level","text":"maplibreMap.animateCamera(CameraUpdateFactory.zoomTo(2.0))\n
"},{"location":"camera/zoom-methods/#zoom-to-a-point","title":"Zoom to a Point","text":"val view = window.decorView\nmaplibreMap.animateCamera(\n CameraUpdateFactory.zoomBy(\n 1.0,\n Point(view.measuredWidth / 4, view.measuredHeight / 4)\n )\n)\n
"},{"location":"styling/animated-image-source/","title":"Animated Image Source","text":"Note
You can find the full source code of this example in AnimatedImageSourceActivity.kt
of the MapLibreAndroidTestApp.
In this example we will see how we can animate an image source. This is the MapLibre Native equivalent of this MapLibre GL JS example.
Map data OpenStreetMap. \u00a9 OpenMapTiles.
We set up an image source in a particular quad. Then we kick of a runnable that periodically updates the image source.
Creating the image sourceval quad = LatLngQuad(\n LatLng(46.437, -80.425),\n LatLng(46.437, -71.516),\n LatLng(37.936, -71.516),\n LatLng(37.936, -80.425)\n)\nval imageSource = ImageSource(ID_IMAGE_SOURCE, quad, R.drawable.southeast_radar_0)\nval layer = RasterLayer(ID_IMAGE_LAYER, ID_IMAGE_SOURCE)\nmap.setStyle(\n Style.Builder()\n .fromUri(TestStyles.AMERICANA)\n .withSource(imageSource)\n .withLayer(layer)\n) { style: Style? ->\n runnable = RefreshImageRunnable(imageSource, handler)\n runnable?.let {\n handler.postDelayed(it, 100)\n }\n}\n
Updating the image sourceimageSource.setImage(drawables[drawableIndex++]!!)\nif (drawableIndex > 3) {\n drawableIndex = 0\n}\nhandler.postDelayed(this, 1000)\n
"},{"location":"styling/animated-symbol-layer/","title":"Animated SymbolLayer","text":"Note
You can find the full source code of this example in AnimatedSymbolLayerActivity.kt
of the MapLibreAndroidTestApp.
Map data OpenStreetMap. \u00a9 OpenMapTiles.
Notice that there are (red) cars randomly moving around, and a (yellow) taxi that is always heading to the passenger (indicated by the M symbol), which upon arrival hops to a different location again. We will focus on the passanger and the taxi, because the cars randomly moving around follow a similar pattern.
In a real application you would of course retrieve the locations from some sort of external API, but for the purposes of this example a random latitude longtitude pair within bounds of the currently visible screen will do.
Getter method to get a random location on the screenprivate val latLngInBounds: LatLng\n get() {\n val bounds = maplibreMap.projection.visibleRegion.latLngBounds\n val generator = Random()\n\n val randomLat = bounds.latitudeSouth + generator.nextDouble() * (bounds.latitudeNorth - bounds.latitudeSouth)\n val randomLon = bounds.longitudeWest + generator.nextDouble() * (bounds.longitudeEast - bounds.longitudeWest)\n\n return LatLng(randomLat, randomLon)\n }\n
Adding a passenger at a random location (on screen)private fun addPassenger(style: Style) {\n passenger = latLngInBounds\n val featureCollection = FeatureCollection.fromFeatures(\n arrayOf(\n Feature.fromGeometry(\n Point.fromLngLat(\n passenger!!.longitude,\n passenger!!.latitude\n )\n )\n )\n )\n style.addImage(\n PASSENGER,\n ResourcesCompat.getDrawable(resources, R.drawable.icon_burned, theme)!!\n )\n val geoJsonSource = GeoJsonSource(PASSENGER_SOURCE, featureCollection)\n style.addSource(geoJsonSource)\n val symbolLayer = SymbolLayer(PASSENGER_LAYER, PASSENGER_SOURCE)\n symbolLayer.withProperties(\n PropertyFactory.iconImage(PASSENGER),\n PropertyFactory.iconIgnorePlacement(true),\n PropertyFactory.iconAllowOverlap(true)\n )\n style.addLayerBelow(symbolLayer, RANDOM_CAR_LAYER)\n}\n
Adding the taxi on screen is done very similarly.
Adding the taxi with bearingprivate fun addTaxi(style: Style) {\n val latLng = latLngInBounds\n val properties = JsonObject()\n properties.addProperty(PROPERTY_BEARING, Car.getBearing(latLng, passenger))\n val feature = Feature.fromGeometry(\n Point.fromLngLat(\n latLng.longitude,\n latLng.latitude\n ),\n properties\n )\n val featureCollection = FeatureCollection.fromFeatures(arrayOf(feature))\n taxi = Car(feature, passenger, duration)\n style.addImage(\n TAXI,\n (ResourcesCompat.getDrawable(resources, R.drawable.ic_taxi_top, theme) as BitmapDrawable).bitmap\n )\n taxiSource = GeoJsonSource(TAXI_SOURCE, featureCollection)\n style.addSource(taxiSource!!)\n val symbolLayer = SymbolLayer(TAXI_LAYER, TAXI_SOURCE)\n symbolLayer.withProperties(\n PropertyFactory.iconImage(TAXI),\n PropertyFactory.iconRotate(Expression.get(PROPERTY_BEARING)),\n PropertyFactory.iconAllowOverlap(true),\n PropertyFactory.iconIgnorePlacement(true)\n )\n style.addLayer(symbolLayer)\n}\n
For animating the taxi we use a ValueAnimator
.
private fun animateTaxi(style: Style) {\n val valueAnimator = ValueAnimator.ofObject(LatLngEvaluator(), taxi!!.current, taxi!!.next)\n valueAnimator.addUpdateListener(object : AnimatorUpdateListener {\n private var latLng: LatLng? = null\n override fun onAnimationUpdate(animation: ValueAnimator) {\n latLng = animation.animatedValue as LatLng\n taxi!!.current = latLng\n updateTaxiSource()\n }\n })\n valueAnimator.addListener(object : AnimatorListenerAdapter() {\n override fun onAnimationEnd(animation: Animator) {\n super.onAnimationEnd(animation)\n updatePassenger(style)\n animateTaxi(style)\n }\n })\n valueAnimator.addListener(object : AnimatorListenerAdapter() {\n override fun onAnimationStart(animation: Animator) {\n super.onAnimationStart(animation)\n taxi!!.feature.properties()!!\n .addProperty(\"bearing\", Car.getBearing(taxi!!.current, taxi!!.next))\n }\n })\n valueAnimator.duration = (7 * taxi!!.current!!.distanceTo(taxi!!.next!!)).toLong()\n valueAnimator.interpolator = AccelerateDecelerateInterpolator()\n valueAnimator.start()\n animators.add(valueAnimator)\n}\n
"},{"location":"styling/building-layer/","title":"Building Layer","text":"Note
You can find the full source code of this example in BuildingFillExtrusionActivity.kt
of the MapLibreAndroidTestApp.
In this example will show how to add a Fill Extrusion layer to a style.
Map data OpenStreetMap. \u00a9 OpenMapTiles.
We use the OpenFreeMap Bright style which, unlike OpenFreeMap Libery, does not have a fill extrusion layer by default. However, if you inspect this style with Maputnik you will find that the multipolygons in the building
layer (of the openfreemap
source) each have render_min_height
and render_height
properties.
val fillExtrusionLayer = FillExtrusionLayer(\"building-3d\", \"openmaptiles\")\nfillExtrusionLayer.sourceLayer = \"building\"\nfillExtrusionLayer.setFilter(\n Expression.all(\n Expression.has(\"render_height\"),\n Expression.has(\"render_min_height\")\n )\n)\nfillExtrusionLayer.minZoom = 15f\nfillExtrusionLayer.setProperties(\n PropertyFactory.fillExtrusionColor(Color.LTGRAY),\n PropertyFactory.fillExtrusionHeight(Expression.get(\"render_height\")),\n PropertyFactory.fillExtrusionBase(Expression.get(\"render_min_height\")),\n PropertyFactory.fillExtrusionOpacity(0.9f)\n)\nstyle.addLayer(fillExtrusionLayer)\n
Changing the light directionisInitPosition = !isInitPosition\nif (isInitPosition) {\n light!!.position = Position(1.5f, 90f, 80f)\n} else {\n light!!.position = Position(1.15f, 210f, 30f)\n}\n
Changing the light colorisRedColor = !isRedColor\nlight!!.setColor(ColorUtils.colorToRgbaString(if (isRedColor) Color.RED else Color.BLUE))\n
"},{"location":"styling/circle-layer/","title":"Circle Layer (with Clustering)","text":"Note
You can find the full source code of this example in CircleLayerActivity.kt
of the MapLibreAndroidTestApp.
In this example we will add a circle layer for a GeoJSON source. We also show how you can use the cluster property of a GeoJSON source.
Create a GeoJsonSource
instance, pass a unique identifier for the source and the URL where the GeoJSON is available. Next add the source to the style.
try {\n source = GeoJsonSource(SOURCE_ID, URI(URL_BUS_ROUTES))\n} catch (exception: URISyntaxException) {\n Timber.e(exception, \"That's not an url... \")\n}\nstyle.addSource(source!!)\n
Now you can create a CircleLayer
, pass it a unique identifier for the layer and the source identifier of the GeoJSON source just created. You can use a PropertyFactory
to pass circle layer properties. Lastly add the layer to your style.
layer = CircleLayer(LAYER_ID, SOURCE_ID)\nlayer!!.setProperties(\n PropertyFactory.circleColor(Color.parseColor(\"#FF9800\")),\n PropertyFactory.circleRadius(2.0f)\n)\nstyle.addLayer(layer!!)\n
"},{"location":"styling/circle-layer/#clustering","title":"Clustering","text":"Next we will show you how you can use clustering. Create a GeoJsonSource
as before, but with some additional options to enable clustering.
style.addSource(\n GeoJsonSource(\n SOURCE_ID_CLUSTER,\n URI(URL_BUS_ROUTES),\n GeoJsonOptions()\n .withCluster(true)\n .withClusterMaxZoom(14)\n .withClusterRadius(50)\n )\n)\n
When enabling clustering some special attributes will be available to the points in the newly created layer. One is cluster
, which is true if the point indicates a cluster. We want to show a bus stop for points that are not clustered.
val unclustered = SymbolLayer(\"unclustered-points\", SOURCE_ID_CLUSTER)\nunclustered.setProperties(\n PropertyFactory.iconImage(\"bus-icon\"),\n)\nunclustered.setFilter(\n Expression.neq(Expression.get(\"cluster\"), true)\n)\nstyle.addLayer(unclustered)\n
Next we define which point amounts correspond to which colors. More than 150 points will get a red circle, clusters with 21-150 points will be green and clusters with 20 or less points will be green.
Define different colors for different point amountsval layers = arrayOf(\n 150 to ResourcesCompat.getColor(\n resources,\n R.color.redAccent,\n theme\n ),\n 20 to ResourcesCompat.getColor(resources, R.color.greenAccent, theme),\n 0 to ResourcesCompat.getColor(\n resources,\n R.color.blueAccent,\n theme\n )\n)\n
Lastly we iterate over the array of Pair
s to create a CircleLayer
for each element.
for (i in layers.indices) {\n // Add some nice circles\n val circles = CircleLayer(\"cluster-$i\", SOURCE_ID_CLUSTER)\n circles.setProperties(\n PropertyFactory.circleColor(layers[i].second),\n PropertyFactory.circleRadius(18f)\n )\n\n val pointCount = Expression.toNumber(Expression.get(\"point_count\"))\n circles.setFilter(\n if (i == 0) {\n Expression.all(\n Expression.has(\"point_count\"),\n Expression.gte(\n pointCount,\n Expression.literal(layers[i].first)\n )\n )\n } else {\n Expression.all(\n Expression.has(\"point_count\"),\n Expression.gt(\n pointCount,\n Expression.literal(layers[i].first)\n ),\n Expression.lt(\n pointCount,\n Expression.literal(layers[i - 1].first)\n )\n )\n }\n )\n\n style.addLayer(circles)\n}\n
"},{"location":"styling/custom-sprite/","title":"Add Custom Sprite","text":"Note
You can find the full source code of this example in CustomSpriteActivity.kt
of the MapLibreAndroidTestApp.
This example showcases adding a sprite image and using it in a Symbol Layer.
// Add an icon to reference later\nstyle.addImage(\n CUSTOM_ICON,\n BitmapFactory.decodeResource(\n resources,\n R.drawable.ic_car_top\n )\n)\n\n// Add a source with a geojson point\npoint = Point.fromLngLat(13.400972, 52.519003)\nsource = GeoJsonSource(\n \"point\",\n FeatureCollection.fromFeatures(arrayOf(Feature.fromGeometry(point)))\n)\nmaplibreMap.style!!.addSource(source!!)\n\n// Add a symbol layer that references that point source\nlayer = SymbolLayer(\"layer\", \"point\")\nlayer.setProperties( // Set the id of the sprite to use\n PropertyFactory.iconImage(CUSTOM_ICON),\n PropertyFactory.iconAllowOverlap(true),\n PropertyFactory.iconIgnorePlacement(true)\n)\n\n// lets add a circle below labels!\nmaplibreMap.style!!.addLayerBelow(layer, \"water-intermittent\")\nfab.setImageResource(R.drawable.ic_directions_car_black)\n
"},{"location":"styling/data-driven-style/","title":"Data Driven Style","text":"Note
You can find the full source code of this example in DataDrivenStyleActivity.kt
of the MapLibreAndroidTestApp.
In this example we will look at various types of data-driven styling.
The examples with 'Source' in the title apply data-driven styling the parks of Amsterdam. Those examples often are based on the somewhat arbitrary stroke-width
property part of the GeoJSON features. These examples are therefore most interesting to learn about the Kotlin API that can be used for data-driven styling.
Tip
Refer to the MapLibre Style Spec for more information about expressions such as interpolate
and step
.
layer.setProperties(\n PropertyFactory.fillColor(\n Expression.interpolate(\n Expression.exponential(0.5f),\n Expression.zoom(),\n Expression.stop(1, Expression.color(Color.RED)),\n Expression.stop(5, Expression.color(Color.BLUE)),\n Expression.stop(10, Expression.color(Color.GREEN))\n )\n )\n)\n
"},{"location":"styling/data-driven-style/#interval-zoom-function","title":"Interval Zoom Function","text":"layer.setProperties(\n PropertyFactory.fillColor(\n Expression.step(\n Expression.zoom(),\n Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1, Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(5, Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f)),\n Expression.stop(10, Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f))\n )\n )\n)\n
Equivalent JSON[\"step\",[\"zoom\"],[\"rgba\",0.0,255.0,255.0,1.0],1.0,[\"rgba\",255.0,0.0,0.0,1.0],5.0,[\"rgba\",0.0,0.0,255.0,1.0],10.0,[\"rgba\",0.0,255.0,0.0,1.0]]\n
"},{"location":"styling/data-driven-style/#exponential-source-function","title":"Exponential Source Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillColor(\n Expression.interpolate(\n Expression.exponential(0.5f),\n Expression.get(\"stroke-width\"),\n Expression.stop(1f, Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(5f, Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f)),\n Expression.stop(10f, Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f))\n )\n )\n)\n
"},{"location":"styling/data-driven-style/#categorical-source-function","title":"Categorical Source Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillColor(\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.literal(\"Jordaan\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.literal(\"Prinseneiland\"),\n Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f),\n Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f)\n )\n )\n)\n
"},{"location":"styling/data-driven-style/#identity-source-function","title":"Identity Source Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillOpacity(\n Expression.get(\"fill-opacity\")\n )\n)\n
"},{"location":"styling/data-driven-style/#interval-source-function","title":"Interval Source Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillColor(\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f))\n )\n )\n)\n
"},{"location":"styling/data-driven-style/#composite-exponential-function","title":"Composite Exponential Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillColor(\n Expression.interpolate(\n Expression.exponential(1f),\n Expression.zoom(),\n Expression.stop(\n 12,\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(0.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f))\n )\n ),\n Expression.stop(\n 15,\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(255.0f, 255.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(211.0f, 211.0f, 211.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f))\n )\n ),\n Expression.stop(\n 18,\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(0.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(128.0f, 128.0f, 128.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f))\n )\n )\n )\n )\n)\n
"},{"location":"styling/data-driven-style/#identity-source-function_1","title":"Identity Source Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillOpacity(\n Expression.get(\"fill-opacity\")\n )\n)\n
"},{"location":"styling/data-driven-style/#composite-interval-function","title":"Composite Interval Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillColor(\n Expression.interpolate(\n Expression.linear(),\n Expression.zoom(),\n Expression.stop(\n 12,\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(0.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f))\n )\n ),\n Expression.stop(\n 15,\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(255.0f, 255.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(211.0f, 211.0f, 211.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f))\n )\n ),\n Expression.stop(\n 18,\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(0.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(128.0f, 128.0f, 128.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f))\n )\n )\n )\n )\n)\n
"},{"location":"styling/data-driven-style/#composite-categorical-function","title":"Composite Categorical Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillColor(\n Expression.step(\n Expression.zoom(),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(\n 7f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 8f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 9f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 10f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 11f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 12f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 13f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 14f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.literal(\"Jordaan\"),\n Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f),\n Expression.literal(\"PrinsenEiland\"),\n Expression.rgba(0.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 15f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 16f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 17f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 18f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.literal(\"Jordaan\"),\n Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 19f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 20f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 21f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 22f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n )\n )\n )\n)\n
"},{"location":"styling/distance-expression/","title":"Distance Expression","text":"Note
You can find the full source code of this example in DistanceExpressionActivity.kt
of the MapLibreAndroidTestApp.
This example shows how you can modify a style to only show certain features within a certain distance to a point. For this the distance expression is used.
Map data OpenStreetMap. \u00a9 OpenMapTiles.
First we add a fill layer and a GeoJSON source.
val center = Point.fromLngLat(lon, lat)\nval circle = TurfTransformation.circle(center, 150.0, TurfConstants.UNIT_METRES)\nmaplibreMap.setStyle(\n Style.Builder()\n .fromUri(TestStyles.OPENFREEMAP_BRIGHT)\n .withSources(\n GeoJsonSource(\n POINT_ID,\n Point.fromLngLat(lon, lat)\n ),\n GeoJsonSource(CIRCLE_ID, circle)\n )\n .withLayerBelow(\n FillLayer(CIRCLE_ID, CIRCLE_ID)\n .withProperties(\n fillOpacity(0.5f),\n fillColor(Color.parseColor(\"#3bb2d0\"))\n ),\n \"poi\"\n )\n
Next, we only show features from symbol layers that are less than a certain distance from the point. All symbol layers whose identifier does not start with poi
are completely hidden.
for (layer in style.layers) {\n if (layer is SymbolLayer) {\n if (layer.id.startsWith(\"poi\")) {\n layer.setFilter(lt(\n distance(\n Point.fromLngLat(lon, lat)\n ),\n 150\n ))\n } else {\n layer.setProperties(visibility(NONE))\n }\n }\n}\n
"},{"location":"styling/draggable-marker/","title":"Draggable Marker","text":"Note
You can find the full source code of this example in DraggableMarkerActivity.kt
of the MapLibreAndroidTestApp.
maplibreMap.addOnMapClickListener {\n // Adding a marker on map click\n val features = maplibreMap.queryRenderedSymbols(it, layerId)\n if (features.isEmpty()) {\n addMarker(it)\n } else {\n // Displaying marker info on marker click\n Snackbar.make(\n mapView,\n \"Marker's position: %.4f, %.4f\".format(it.latitude, it.longitude),\n Snackbar.LENGTH_LONG\n )\n .show()\n }\n\n false\n}\n
"},{"location":"styling/draggable-marker/#allowing-markers-to-be-dragged","title":"Allowing markers to be dragged","text":"This is slightly more involved, as we implement it by implementing a DraggableSymbolsManager
helper class.
This class is initialized and we pass a few callbacks when when markers are start or end being dragged.
draggableSymbolsManager = DraggableSymbolsManager(\n mapView,\n maplibreMap,\n featureCollection,\n source,\n layerId,\n actionBarHeight,\n 0\n)\n\n// Adding symbol drag listeners\ndraggableSymbolsManager?.addOnSymbolDragListener(object : DraggableSymbolsManager.OnSymbolDragListener {\n override fun onSymbolDragStarted(id: String) {\n binding.draggedMarkerPositionTv.visibility = View.VISIBLE\n Snackbar.make(\n mapView,\n \"Marker drag started (%s)\".format(id),\n Snackbar.LENGTH_SHORT\n )\n .show()\n }\n\n override fun onSymbolDrag(id: String) {\n val point = featureCollection.features()?.find {\n it.id() == id\n }?.geometry() as Point\n binding.draggedMarkerPositionTv.text = \"Dragged marker's position: %.4f, %.4f\".format(point.latitude(), point.longitude())\n }\n\n override fun onSymbolDragFinished(id: String) {\n binding.draggedMarkerPositionTv.visibility = View.GONE\n Snackbar.make(\n mapView,\n \"Marker drag finished (%s)\".format(id),\n Snackbar.LENGTH_SHORT\n )\n .show()\n }\n})\n
The implementation of DraggableSymbolsManager
follows. In its initializer we define a handler for when a user long taps on a marker. This then starts dragging that marker. It does this by temporarily suspending all other gestures.
We create a custom implementation of MoveGestureDetector.OnMoveGestureListener
and pass this to an instance of AndroidGesturesManager
linked to the map view.
Tip
See maplibre-gestures-android for the implementation details of the gestures library used by MapLibre Android.
/**\n * A manager, that allows dragging symbols after they are long clicked.\n * Since this manager lives outside of the Maps SDK, we need to intercept parent's motion events\n * and pass them with [DraggableSymbolsManager.onParentTouchEvent].\n * If we were to try and overwrite [AppCompatActivity.onTouchEvent], those events would've been\n * consumed by the map.\n *\n * We also need to setup a [DraggableSymbolsManager.androidGesturesManager],\n * because after disabling map's gestures and starting the drag process\n * we still need to listen for move gesture events which map won't be able to provide anymore.\n *\n * @param mapView the mapView\n * @param maplibreMap the maplibreMap\n * @param symbolsCollection the collection that contains all the symbols that we want to be draggable\n * @param symbolsSource the source that contains the [symbolsCollection]\n * @param symbolsLayerId the ID of the layer that the symbols are displayed on\n * @param touchAreaShiftX X-axis padding that is applied to the parent's window motion event,\n * as that window can be bigger than the [mapView].\n * @param touchAreaShiftY Y-axis padding that is applied to the parent's window motion event,\n * as that window can be bigger than the [mapView].\n * @param touchAreaMaxX maximum value of X-axis motion event\n * @param touchAreaMaxY maximum value of Y-axis motion event\n */\nclass DraggableSymbolsManager(\n mapView: MapView,\n private val maplibreMap: MapLibreMap,\n private val symbolsCollection: FeatureCollection,\n private val symbolsSource: GeoJsonSource,\n private val symbolsLayerId: String,\n private val touchAreaShiftY: Int = 0,\n private val touchAreaShiftX: Int = 0,\n private val touchAreaMaxX: Int = mapView.width,\n private val touchAreaMaxY: Int = mapView.height\n) {\n\n private val androidGesturesManager: AndroidGesturesManager = AndroidGesturesManager(mapView.context, false)\n private var draggedSymbolId: String? = null\n private val onSymbolDragListeners: MutableList<OnSymbolDragListener> = mutableListOf()\n\n init {\n maplibreMap.addOnMapLongClickListener {\n // Starting the drag process on long click\n draggedSymbolId = maplibreMap.queryRenderedSymbols(it, symbolsLayerId).firstOrNull()?.id()?.also { id ->\n maplibreMap.uiSettings.setAllGesturesEnabled(false)\n maplibreMap.gesturesManager.moveGestureDetector.interrupt()\n notifyOnSymbolDragListeners {\n onSymbolDragStarted(id)\n }\n }\n false\n }\n\n androidGesturesManager.setMoveGestureListener(MyMoveGestureListener())\n }\n\n inner class MyMoveGestureListener : MoveGestureDetector.OnMoveGestureListener {\n override fun onMoveBegin(detector: MoveGestureDetector): Boolean {\n return true\n }\n\n override fun onMove(detector: MoveGestureDetector, distanceX: Float, distanceY: Float): Boolean {\n if (detector.pointersCount > 1) {\n // Stopping the drag when we don't work with a simple, on-pointer move anymore\n stopDragging()\n return true\n }\n\n // Updating symbol's position\n draggedSymbolId?.also { draggedSymbolId ->\n val moveObject = detector.getMoveObject(0)\n val point = PointF(moveObject.currentX - touchAreaShiftX, moveObject.currentY - touchAreaShiftY)\n\n if (point.x < 0 || point.y < 0 || point.x > touchAreaMaxX || point.y > touchAreaMaxY) {\n stopDragging()\n }\n\n val latLng = maplibreMap.projection.fromScreenLocation(point)\n\n symbolsCollection.features()?.indexOfFirst {\n it.id() == draggedSymbolId\n }?.also { index ->\n symbolsCollection.features()?.get(index)?.also { oldFeature ->\n val properties = oldFeature.properties()\n val newFeature = Feature.fromGeometry(\n Point.fromLngLat(latLng.longitude, latLng.latitude),\n properties,\n draggedSymbolId\n )\n symbolsCollection.features()?.set(index, newFeature)\n symbolsSource.setGeoJson(symbolsCollection)\n notifyOnSymbolDragListeners {\n onSymbolDrag(draggedSymbolId)\n }\n return true\n }\n }\n }\n\n return false\n }\n\n override fun onMoveEnd(detector: MoveGestureDetector, velocityX: Float, velocityY: Float) {\n // Stopping the drag when move ends\n stopDragging()\n }\n }\n\n private fun stopDragging() {\n maplibreMap.uiSettings.setAllGesturesEnabled(true)\n draggedSymbolId?.let {\n notifyOnSymbolDragListeners {\n onSymbolDragFinished(it)\n }\n }\n draggedSymbolId = null\n }\n\n fun onParentTouchEvent(ev: MotionEvent?) {\n androidGesturesManager.onTouchEvent(ev)\n }\n\n private fun notifyOnSymbolDragListeners(action: OnSymbolDragListener.() -> Unit) {\n onSymbolDragListeners.forEach(action)\n }\n\n fun addOnSymbolDragListener(listener: OnSymbolDragListener) {\n onSymbolDragListeners.add(listener)\n }\n\n fun removeOnSymbolDragListener(listener: OnSymbolDragListener) {\n onSymbolDragListeners.remove(listener)\n }\n\n interface OnSymbolDragListener {\n fun onSymbolDragStarted(id: String)\n fun onSymbolDrag(id: String)\n fun onSymbolDragFinished(id: String)\n }\n}\n
"},{"location":"styling/live-realtime-data/","title":"Add live realtime data","text":"Note
You can find the full source code of this example in RealTimeGeoJsonActivity.kt
of the MapLibreAndroidTestApp.
In this example you will learn how to add a live GeoJSON source. We have set up a lambda function that returns a new GeoJSON point every time it is called.
First we will create a GeoJSONSource
.
try {\n style.addSource(GeoJsonSource(ID_GEOJSON_SOURCE, URI(URL_GEOJSON_SOURCE)))\n} catch (malformedUriException: URISyntaxException) {\n Timber.e(malformedUriException, \"Invalid URL\")\n}\n
Next we will create a SymbolLayer
that uses the source.
val layer = SymbolLayer(ID_GEOJSON_LAYER, ID_GEOJSON_SOURCE)\nlayer.setProperties(\n PropertyFactory.iconImage(\"plane\"),\n PropertyFactory.iconAllowOverlap(true)\n)\nstyle.addLayer(layer)\n
We use define a Runnable
and use android.os.Handler
with a android.os.Looper
to update the GeoJSON source every 2 seconds.
private inner class RefreshGeoJsonRunnable(\n private val maplibreMap: MapLibreMap,\n private val handler: Handler\n) : Runnable {\n override fun run() {\n val geoJsonSource = maplibreMap.style!!.getSource(ID_GEOJSON_SOURCE) as GeoJsonSource\n geoJsonSource.setUri(URL_GEOJSON_SOURCE)\n val features = geoJsonSource.querySourceFeatures(null)\n setIconRotation(features)\n handler.postDelayed(this, 2000)\n }\n}\n
"},{"location":"styling/live-realtime-data/#bonus-set-icon-rotation","title":"Bonus: set icon rotation","text":"You can set the icon rotation of the icon when ever the point is updated based on the last two points.
Defining a Runnable for updating the GeoJSON sourceif (features.size != 1) {\n Timber.e(\"Expected only one feature\")\n return\n}\n\nval feature = features[0]\nval geometry = feature.geometry()\nif (geometry !is Point) {\n Timber.e(\"Expected geometry to be a point\")\n return\n}\n\nif (lastLocation == null) {\n lastLocation = geometry\n return\n}\n\nmaplibreMap.style!!.getLayer(ID_GEOJSON_LAYER)!!.setProperties(\n PropertyFactory.iconRotate(calculateRotationAngle(lastLocation!!, geometry)),\n)\n
"}]}
\ No newline at end of file
+{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"MapLibre Android Examples","text":"Welcome to the examples documentation of MapLibre Android.
Quickstart
Learn how to include MapLibre Android in your project
Getting started
Find us on Slack
Discuss the project and ask questions in the #maplibre-android
channel
Slack
Contribute to these Docs
Share your own examples with the community!
Documentation on GitHub
You can learn how to use the API from MapLibre Android by stuying the source code of existing apps that intergrate MapLibre Android. Here are some open-source apps that use MapLibre Android:
#maplibre-native
and #maplibre-android
channels.This guide will explain various ways to create a map.
When working with maps, you likely want to configure the MapView
.
There are several ways to build a MapView
:
MapView
in the layout.MapLibreMapOptions
and passing builder function values into the MapView
.SupportMapFragment
with the help of MapLibreMapOptions
.Before diving into MapView
configurations, let's understand the capabilities of both XML namespaces and MapLibreMapOptions
.
Here are some common configurations you can set:
We will explore how to achieve these configurations in XML layout and programmatically in Activity code, step by step.
"},{"location":"configuration/#mapview-configuration-with-an-xml-layout","title":"MapView
Configuration with an XML layout","text":"To configure MapView
within an XML layout, you need to use the right namespace and provide the necessary data in the layout file.
<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n xmlns:tools=\"http://schemas.android.com/tools\"\n android:id=\"@+id/main\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\"\n tools:context=\".activity.options.MapOptionsXmlActivity\">\n\n <org.maplibre.android.maps.MapView\n android:id=\"@+id/mapView\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\"\n app:maplibre_apiBaseUri=\"https://api.maplibre.org\"\n app:maplibre_cameraBearing=\"0.0\"\n app:maplibre_cameraPitchMax=\"90.0\"\n app:maplibre_cameraPitchMin=\"0.0\"\n app:maplibre_cameraTargetLat=\"42.31230486601532\"\n app:maplibre_cameraTargetLng=\"64.63967338936439\"\n app:maplibre_cameraTilt=\"0.0\"\n app:maplibre_cameraZoom=\"3.9\"\n app:maplibre_cameraZoomMax=\"26.0\"\n app:maplibre_cameraZoomMin=\"2.0\"\n app:maplibre_localIdeographFontFamilies=\"@array/array_local_ideograph_family_test\"\n app:maplibre_localIdeographFontFamily=\"Droid Sans\"\n app:maplibre_uiCompass=\"true\"\n app:maplibre_uiCompassFadeFacingNorth=\"true\"\n app:maplibre_uiCompassGravity=\"top|end\"\n app:maplibre_uiDoubleTapGestures=\"true\"\n app:maplibre_uiHorizontalScrollGestures=\"true\"\n app:maplibre_uiRotateGestures=\"true\"\n app:maplibre_uiScrollGestures=\"true\"\n app:maplibre_uiTiltGestures=\"true\"\n app:maplibre_uiZoomGestures=\"true\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n
This can be found in activity_map_options_xml.xml
.
You can assign any other existing values to the maplibre...
tags. Then, you only need to create MapView
and MapLibreMap
objects with a simple setup in the Activity.
package org.maplibre.android.testapp.activity.options\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport org.maplibre.android.maps.MapLibreMap\nimport org.maplibre.android.maps.MapView\nimport org.maplibre.android.maps.OnMapReadyCallback\nimport org.maplibre.android.maps.Style\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\n\n/**\n * TestActivity demonstrating configuring MapView with XML\n */\n\nclass MapOptionsXmlActivity : AppCompatActivity(), OnMapReadyCallback {\n private lateinit var mapView: MapView\n private lateinit var maplibreMap: MapLibreMap\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_map_options_xml)\n mapView = findViewById(R.id.mapView)\n mapView.onCreate(savedInstanceState)\n mapView.getMapAsync(this)\n }\n\n override fun onMapReady(maplibreMap: MapLibreMap) {\n this.maplibreMap = maplibreMap\n this.maplibreMap.setStyle(\"https://demotiles.maplibre.org/style.json\")\n }\n\n override fun onStart() {\n super.onStart()\n mapView.onStart()\n }\n\n override fun onResume() {\n super.onResume()\n mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n mapView.onPause()\n }\n\n override fun onStop() {\n super.onStop()\n mapView.onStop()\n }\n\n override fun onSaveInstanceState(outState: Bundle) {\n super.onSaveInstanceState(outState)\n mapView.onSaveInstanceState(outState)\n }\n\n override fun onDestroy() {\n super.onDestroy()\n mapView.onDestroy()\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n mapView.onLowMemory()\n }\n}\n
This can be found in MapOptionsXmlActivity.kt
.
MapView
configuration with MapLibreMapOptions
","text":"Here we don't have to create MapView from XML since we want to create it programmatically.
<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:id=\"@+id/container\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\"\n android:orientation=\"vertical\"/>\n
This can be found in activity_map_options_runtime.xml
.
A MapLibreMapOptions
object must be created and passed to the MapView constructor. All setup is done in the Activity code:
package org.maplibre.android.testapp.activity.options\n\nimport android.os.Bundle\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.appcompat.app.AppCompatActivity\nimport org.maplibre.android.camera.CameraPosition\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.MapLibreMap\nimport org.maplibre.android.maps.MapLibreMapOptions\nimport org.maplibre.android.maps.MapView\nimport org.maplibre.android.maps.OnMapReadyCallback\nimport org.maplibre.android.maps.Style\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\n\n/**\n * TestActivity demonstrating configuring MapView with MapOptions\n */\nclass MapOptionsRuntimeActivity : AppCompatActivity(), OnMapReadyCallback {\n\n private lateinit var maplibreMap: MapLibreMap\n private lateinit var mapView: MapView\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_map_options_runtime)\n\n // Create map configuration\n val maplibreMapOptions = MapLibreMapOptions.createFromAttributes(this)\n maplibreMapOptions.apply {\n apiBaseUri(\"https://api.maplibre.org\")\n camera(\n CameraPosition.Builder()\n .bearing(0.0)\n .target(LatLng(42.31230486601532, 64.63967338936439))\n .zoom(3.9)\n .tilt(0.0)\n .build()\n )\n maxPitchPreference(90.0)\n minPitchPreference(0.0)\n maxZoomPreference(26.0)\n minZoomPreference(2.0)\n localIdeographFontFamily(\"Droid Sans\")\n zoomGesturesEnabled(true)\n compassEnabled(true)\n compassFadesWhenFacingNorth(true)\n scrollGesturesEnabled(true)\n rotateGesturesEnabled(true)\n tiltGesturesEnabled(true)\n }\n\n // Create map programmatically, add to view hierarchy\n mapView = MapView(this, maplibreMapOptions)\n mapView.getMapAsync(this)\n mapView.onCreate(savedInstanceState)\n (findViewById<View>(R.id.container) as ViewGroup).addView(mapView)\n }\n\n override fun onMapReady(maplibreMap: MapLibreMap) {\n this.maplibreMap = maplibreMap\n this.maplibreMap.setStyle(\"https://demotiles.maplibre.org/style.json\")\n }\n\n override fun onStart() {\n super.onStart()\n mapView.onStart()\n }\n\n override fun onResume() {\n super.onResume()\n mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n mapView.onPause()\n }\n\n override fun onStop() {\n super.onStop()\n mapView.onStop()\n }\n\n override fun onSaveInstanceState(outState: Bundle) {\n super.onSaveInstanceState(outState)\n mapView.onSaveInstanceState(outState)\n }\n\n override fun onDestroy() {\n super.onDestroy()\n mapView.onDestroy()\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n mapView.onLowMemory()\n }\n}\n
This can be found in MapOptionsRuntimeActivity.kt
.
Finally you will see a result similar to this:
For the full contents of MapOptionsRuntimeActivity
and MapOptionsXmlActivity
, please take a look at the source code of MapLibreAndroidTestApp.
You can read more about MapLibreMapOptions
in the Android API documentation.
SupportMapFragment
with the help of MapLibreMapOptions
.","text":"If you are using MapFragment in your project, it is also easy to provide initial values to the newInstance()
static method of SupportMapFragment
, which requires a MapLibreMapOptions
parameter.
Let's see how this can be done in a sample activity:
package org.maplibre.android.testapp.activity.fragment\n\nimport android.os.Bundle // ktlint-disable import-ordering\nimport androidx.appcompat.app.AppCompatActivity\nimport org.maplibre.android.camera.CameraPosition\nimport org.maplibre.android.camera.CameraUpdateFactory\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.* // ktlint-disable no-wildcard-imports\nimport org.maplibre.android.maps.MapFragment.OnMapViewReadyCallback\nimport org.maplibre.android.maps.MapView.OnDidFinishRenderingFrameListener\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\n\n/**\n * Test activity showcasing using the MapFragment API using Support Library Fragments.\n *\n *\n * Uses MapLibreMapOptions to initialise the Fragment.\n *\n */\nclass SupportMapFragmentActivity :\n AppCompatActivity(),\n OnMapViewReadyCallback,\n OnMapReadyCallback,\n OnDidFinishRenderingFrameListener {\n private lateinit var maplibreMap: MapLibreMap\n private lateinit var mapView: MapView\n private var initialCameraAnimation = true\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_map_fragment)\n val mapFragment: SupportMapFragment?\n if (savedInstanceState == null) {\n mapFragment = SupportMapFragment.newInstance(createFragmentOptions())\n supportFragmentManager\n .beginTransaction()\n .add(R.id.fragment_container, mapFragment, TAG)\n .commit()\n } else {\n mapFragment = supportFragmentManager.findFragmentByTag(TAG) as SupportMapFragment?\n }\n mapFragment!!.getMapAsync(this)\n }\n\n private fun createFragmentOptions(): MapLibreMapOptions {\n val options = MapLibreMapOptions.createFromAttributes(this, null)\n options.scrollGesturesEnabled(false)\n options.zoomGesturesEnabled(false)\n options.tiltGesturesEnabled(false)\n options.rotateGesturesEnabled(false)\n options.debugActive(false)\n val dc = LatLng(38.90252, -77.02291)\n options.minZoomPreference(9.0)\n options.maxZoomPreference(11.0)\n options.camera(\n CameraPosition.Builder()\n .target(dc)\n .zoom(11.0)\n .build()\n )\n return options\n }\n\n override fun onMapViewReady(map: MapView) {\n mapView = map\n mapView.addOnDidFinishRenderingFrameListener(this)\n }\n\n override fun onMapReady(map: MapLibreMap) {\n maplibreMap = map\n maplibreMap.setStyle(TestStyles.getPredefinedStyleWithFallback(\"Satellite Hybrid\"))\n }\n\n override fun onDestroy() {\n super.onDestroy()\n mapView.removeOnDidFinishRenderingFrameListener(this)\n }\n\n override fun onDidFinishRenderingFrame(fully: Boolean, frameEncodingTime: Double, frameRenderingTime: Double) {\n if (initialCameraAnimation && fully && this::maplibreMap.isInitialized) {\n maplibreMap.animateCamera(\n CameraUpdateFactory.newCameraPosition(CameraPosition.Builder().tilt(45.0).build()),\n 5000\n )\n initialCameraAnimation = false\n }\n }\n\n companion object {\n private const val TAG = \"com.mapbox.map\"\n }\n}\n
You can also find the full contents of SupportMapFragmentActivity
in the MapLibreAndroidTestApp.
To learn more about SupportMapFragment
, please visit the Android API documentation.
This guide will teach you how to use GeoJsonSource
by deep diving into GeoJSON file format.
After finishing this documentation you should be able to:
Style
, Layer
, and Source
interact with each other.GeoJsonSource
s.Styles consist of collections of layers and a data source. Layers reference data sources. Hence, they require a unique source ID when you construct them. It would be meaningless if we don't have any data to show, so we need know how to supply data through a data source.
Firstly, we need to understand how to store data and pass it into a data source; therefore, we will discuss GeoJSON in the next session.
"},{"location":"geojson-guide/#2-geojson","title":"2. GeoJSON","text":"GeoJSON is a JSON file for encoding various geographical data structures. It defines several JSON objects to represent geospatial information. Typicalle the.geojson
extension is used for GeoJSON files. We define the most fundamental objects:
Geometry
refers to a single geometric shape that contains one or more coordinates. These shapes are visual objects displayed on a map. A geometry can be one of the following six types:Feautue
is a compound object that combines a single geometry with user-defined attributes, such as name, color.FeatureCollection
is set of features stored in an array. It is a root object that introduces all other features.A typical GeoJSON structure might look like:
{\n \"type\": \"Feature\",\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [125.6, 10.1]\n },\n \"properties\": {\n \"name\": \"Dinagat Islands\"\n }\n}\n
So far we learned describing geospatial data in GeoJSON files. We will start applying this knowledge into our map applications.
"},{"location":"geojson-guide/#3-geojsonsource","title":"3. GeoJsonSource","text":"As we discussed before, map requires some sort data to be rendered. We use different sources such as Vector, Raster and GeoJSON. We will focus exclusively on GeoJsonSource
and will not address other sources.
GeoJsonSource
is a type of source that has a unique String
ID and GeoJSON data.
There are several ways to construct a GeoJsonSource
:
A sample GeoJsonSource
:
val source = GeoJsonSource(\"source\", featureCollection)\nval lineLayer = LineLayer(\"layer\", \"source\")\n .withProperties(\n PropertyFactory.lineColor(Color.RED),\n PropertyFactory.lineWidth(10f)\n )\n\nstyle.addSource(source)\nstyle.addLayer(lineLayer)\n
Note that you can not simply show data on a map. Layers must reference them. Therefore, you create a layer that gives visual appearance to it.
"},{"location":"geojson-guide/#creating-geojson-sources","title":"Creating GeoJSON sources","text":"There are various ways you can create a GeoJSONSource
. Some of the options are shown below.
binding.mapView.getMapAsync { map ->\n map.moveCamera(CameraUpdateFactory.newLatLngZoom(cameraTarget, cameraZoom))\n map.setStyle(\n Style.Builder()\n .withImage(imageId, imageIcon)\n .withSource(GeoJsonSource(sourceId, URI(\"asset://points-sf.geojson\")))\n .withLayer(SymbolLayer(layerId, sourceId).withProperties(iconImage(imageId)))\n )\n}\n
Loading with raw folder fileval source: Source = try {\n GeoJsonSource(\"amsterdam-spots\", ResourceUtils.readRawResource(this, R.raw.amsterdam))\n} catch (ioException: IOException) {\n Toast.makeText(\n this@RuntimeStyleActivity,\n \"Couldn't add source: \" + ioException.message,\n Toast.LENGTH_SHORT\n ).show()\n return\n}\nmaplibreMap.style!!.addSource(source)\nvar layer: FillLayer? = FillLayer(\"parksLayer\", \"amsterdam-spots\")\nlayer!!.setProperties(\n PropertyFactory.fillColor(Color.RED),\n PropertyFactory.fillOutlineColor(Color.BLUE),\n PropertyFactory.fillOpacity(0.3f),\n PropertyFactory.fillAntialias(true)\n)\n
Parsing inline JSONfun readRawResource(context: Context?, @RawRes rawResource: Int): String {\n var json = \"\"\n if (context != null) {\n val writer: Writer = StringWriter()\n val buffer = CharArray(1024)\n context.resources.openRawResource(rawResource).use { `is` ->\n val reader: Reader = BufferedReader(InputStreamReader(`is`, \"UTF-8\"))\n var numRead: Int\n while (reader.read(buffer).also { numRead = it } != -1) {\n writer.write(buffer, 0, numRead)\n }\n }\n json = writer.toString()\n }\n return json\n}\n
Loading from remote servicesprivate fun createEarthquakeSource(): GeoJsonSource {\n return GeoJsonSource(EARTHQUAKE_SOURCE_ID, URI(EARTHQUAKE_SOURCE_URL))\n}\n
companion object {\n private const val EARTHQUAKE_SOURCE_URL =\n \"https://maplibre.org/maplibre-gl-js-docs/assets/earthquakes.geojson\"\n private const val EARTHQUAKE_SOURCE_ID = \"earthquakes\"\n private const val HEATMAP_LAYER_ID = \"earthquakes-heat\"\n private const val HEATMAP_LAYER_SOURCE = \"earthquakes\"\n private const val CIRCLE_LAYER_ID = \"earthquakes-circle\"\n}\n
Parsing string with the fromJson method of FeatureCollectionreturn FeatureCollection.fromJson(\n \"\"\"\n {\n \"type\": \"FeatureCollection\",\n \"features\": [\n {\n \"type\": \"Feature\",\n \"properties\": {},\n \"geometry\": {\n \"type\": \"Polygon\",\n \"coordinates\": [\n [\n [\n -77.06867337226866,\n 38.90467655551809\n ],\n [\n -77.06233263015747,\n 38.90479344272695\n ],\n [\n -77.06234335899353,\n 38.906463238984344\n ],\n [\n -77.06290125846863,\n 38.907206285691615\n ],\n [\n -77.06364154815674,\n 38.90684728656818\n ],\n [\n -77.06326603889465,\n 38.90637140121084\n ],\n [\n -77.06321239471436,\n 38.905561553883246\n ],\n [\n -77.0691454410553,\n 38.905436318935635\n ],\n [\n -77.06912398338318,\n 38.90466820642439\n ],\n [\n -77.06867337226866,\n 38.90467655551809\n ]\n ]\n ]\n }\n }\n ]\n }\n \"\"\".trimIndent()\n).features()!![0].geometry() as Polygon\n
Creating Geometry, Feature, and FeatureCollections from scratchval properties = JsonObject()\nproperties.addProperty(\"key1\", \"value1\")\nval source = GeoJsonSource(\n \"test-source\",\n FeatureCollection.fromFeatures(\n arrayOf(\n Feature.fromGeometry(Point.fromLngLat(17.1, 51.0), properties),\n Feature.fromGeometry(Point.fromLngLat(17.2, 51.0), properties),\n Feature.fromGeometry(Point.fromLngLat(17.3, 51.0), properties),\n Feature.fromGeometry(Point.fromLngLat(17.4, 51.0), properties)\n )\n )\n)\nstyle.addSource(source)\nval visible = Expression.eq(Expression.get(\"key1\"), Expression.literal(\"value1\"))\nval invisible = Expression.neq(Expression.get(\"key1\"), Expression.literal(\"value1\"))\nval layer = CircleLayer(\"test-layer\", source.id)\n .withFilter(visible)\nstyle.addLayer(layer)\n
Note that the GeoJSON objects we discussed earlier have classes defined in the MapLibre SDK. Therefore, we can either map JSON objects to regular Java/Kotlin objects or build them directly.
"},{"location":"geojson-guide/#4-updating-data-at-runtime","title":"4. Updating data at runtime","text":"The key feature of GeoJsonSource
s is that once we add one, we can set another set of data. We achieve this using setGeoJson()
method. For instance, we create a source variable and check if we have not assigned it, then we create a new source object and add it to style; otherwise, we set a different data source:
private fun createFeatureCollection(): FeatureCollection {\n val point = if (isInitialPosition) {\n Point.fromLngLat(-74.01618140, 40.701745)\n } else {\n Point.fromLngLat(-73.988097, 40.749864)\n }\n val properties = JsonObject()\n properties.addProperty(KEY_PROPERTY_SELECTED, isSelected)\n val feature = Feature.fromGeometry(point, properties)\n return FeatureCollection.fromFeatures(arrayOf(feature))\n}\n
private fun updateSource(style: Style?) {\n val featureCollection = createFeatureCollection()\n if (source != null) {\n source!!.setGeoJson(featureCollection)\n } else {\n source = GeoJsonSource(SOURCE_ID, featureCollection)\n style!!.addSource(source!!)\n }\n}\n
See this guide for an advanced example that showcases random cars and a passenger on a map updating their positions with smooth animation.
"},{"location":"geojson-guide/#summary","title":"Summary","text":"GeoJsonSources have their pros and cons. They are most effective when you want to add additional data to your style or provide features like animating objects on your map.
However, working with large datasets can be challenging if you need to manipulate and store data within the app; in such cases, it\u2019s better to use a remote data source.
"},{"location":"getting-started/","title":"Quickstart","text":"Add bintray Maven repositories to your project-level Gradle file (usually <project>/<app-module>/build.gradle
).
allprojects {\n repositories {\n ...\n mavenCentral()\n }\n}\n
Add the library as a dependency into your module Gradle file (usually <project>/<app-module>/build.gradle
). Replace <version>
with the latest MapLibre Android version (e.g.: org.maplibre.gl:android-sdk:11.5.2
):
dependencies {\n ...\n implementation 'org.maplibre.gl:android-sdk:<version>'\n ...\n}\n
Sync your Android project with Gradle files.
Add a MapView
to your layout XML file (usually <project>/<app-module>/src/main/res/layout/activity_main.xml
).
...\n<org.maplibre.android.maps.MapView\n android:id=\"@+id/mapView\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\"\n />\n...\n
Initialize the MapView
in your MainActivity
file by following the example below:
import androidx.appcompat.app.AppCompatActivity\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport org.maplibre.android.Maplibre\nimport org.maplibre.android.camera.CameraPosition\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.MapView\nimport org.maplibre.android.testapp.R\n\nclass MainActivity : AppCompatActivity() {\n\n // Declare a variable for MapView\n private lateinit var mapView: MapView\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n\n // Init MapLibre\n MapLibre.getInstance(this)\n\n // Init layout view\n val inflater = LayoutInflater.from(this)\n val rootView = inflater.inflate(R.layout.activity_main, null)\n setContentView(rootView)\n\n // Init the MapView\n mapView = rootView.findViewById(R.id.mapView)\n mapView.getMapAsync { map ->\n map.setStyle(\"https://demotiles.maplibre.org/style.json\")\n map.cameraPosition = CameraPosition.Builder().target(LatLng(0.0,0.0)).zoom(1.0).build()\n }\n }\n\n override fun onStart() {\n super.onStart()\n mapView.onStart()\n }\n\n override fun onResume() {\n super.onResume()\n mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n mapView.onPause()\n }\n\n override fun onStop() {\n super.onStop()\n mapView.onStop()\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n mapView.onLowMemory()\n }\n\n override fun onDestroy() {\n super.onDestroy()\n mapView.onDestroy()\n }\n\n override fun onSaveInstanceState(outState: Bundle) {\n super.onSaveInstanceState(outState)\n mapView.onSaveInstanceState(outState)\n }\n}\n
Build and run the app. If you run the app successfully, a map will be displayed as seen in the screenshot below.
This guide will demonstrate how to utilize the LocationComponent to represent the user's current location.
When implementing the LocationComponent, the application should request location permissions. Declare the need for foreground location in the AndroidManifest.xml
file. For more information, please refer to the Android Developer Documentation.
<manifest ... >\n <!-- Always include this permission -->\n <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />\n\n <!-- Include only if your app benefits from precise location access. -->\n <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />\n</manifest>\n
Create a new activity named BasicLocationPulsingCircleActivity
:
OnMapReadyCallback
interface. The onMapReady()
method is triggered when the map is ready to be used.permissionsManager
to manage permissions.locationComponent
to manage user location.onCreate()
method, call checkPermissions()
to ensure that the application can access the user's location./**\n * This activity shows a basic usage of the LocationComponent's pulsing circle. There's no\n * customization of the pulsing circle's color, radius, speed, etc.\n */\nclass BasicLocationPulsingCircleActivity : AppCompatActivity(), OnMapReadyCallback {\n private var lastLocation: Location? = null\n private lateinit var mapView: MapView\n private var permissionsManager: PermissionsManager? = null\n private var locationComponent: LocationComponent? = null\n private lateinit var maplibreMap: MapLibreMap\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_location_layer_basic_pulsing_circle)\n mapView = findViewById(R.id.mapView)\n if (savedInstanceState != null) {\n lastLocation = savedInstanceState.getParcelable(SAVED_STATE_LOCATION, Location::class.java)\n }\n mapView.onCreate(savedInstanceState)\n checkPermissions()\n }\n
In the checkPermissions()
method, the PermissionManager is used to request location permissions at runtime and handle the callbacks for permission granting or rejection.Additionally, you should pass the results of Activity.onRequestPermissionResult()
to it. If the permissions are granted, call mapView.getMapAsync(this)
to register the activity as a listener for onMapReady event.
private fun checkPermissions() {\n if (PermissionsManager.areLocationPermissionsGranted(this)) {\n mapView.getMapAsync(this)\n } else {\n permissionsManager = PermissionsManager(object : PermissionsListener {\n override fun onExplanationNeeded(permissionsToExplain: List<String>) {\n Toast.makeText(\n this@BasicLocationPulsingCircleActivity,\n \"You need to accept location permissions.\",\n Toast.LENGTH_SHORT\n ).show()\n }\n\n override fun onPermissionResult(granted: Boolean) {\n if (granted) {\n mapView.getMapAsync(this@BasicLocationPulsingCircleActivity)\n } else {\n finish()\n }\n }\n })\n permissionsManager!!.requestLocationPermissions(this)\n }\n}\n\noverride fun onRequestPermissionsResult(\n requestCode: Int,\n permissions: Array<String>,\n grantResults: IntArray\n) {\n super.onRequestPermissionsResult(requestCode, permissions, grantResults)\n permissionsManager!!.onRequestPermissionsResult(requestCode, permissions, grantResults)\n}\n
In the onMapReady()
method, first set the style and then handle the user's location using the LocationComponent.
To configure the LocationComponent, developers should use LocationComponentOptions.
In this demonstration, we create an instance of this class.
In this method:
@SuppressLint(\"MissingPermission\")
to suppress warnings related to missing location access permissions.setStyle(),
you can utilize other public and token-free styles like demotiles instead of the predefined styles.pulseEnabled(true)
to enable the pulse animation, which enhances awareness of the user's location.buildLocationComponentActivationOptions()
to set LocationComponentActivationOptions, then activate locatinoComponent
with it.activateLocationComponent()
of locationComponent
. You can also set locationComponent
's various properties like isLocationComponentEnabled
, cameraMode
, etc...CameraMode.TRACKING
1 means that when the user's location is updated, the camera will reposition accordingly.locationComponent!!.forceLocationUpdate(lastLocation)
updates the the user's last known location.@SuppressLint(\"MissingPermission\")\noverride fun onMapReady(maplibreMap: MapLibreMap) {\n this.maplibreMap = maplibreMap\n maplibreMap.setStyle(TestStyles.getPredefinedStyleWithFallback(\"Streets\")) { style: Style ->\n locationComponent = maplibreMap.locationComponent\n val locationComponentOptions =\n LocationComponentOptions.builder(this@BasicLocationPulsingCircleActivity)\n .pulseEnabled(true)\n .build()\n val locationComponentActivationOptions =\n buildLocationComponentActivationOptions(style, locationComponentOptions)\n locationComponent!!.activateLocationComponent(locationComponentActivationOptions)\n locationComponent!!.isLocationComponentEnabled = true\n locationComponent!!.cameraMode = CameraMode.TRACKING\n locationComponent!!.forceLocationUpdate(lastLocation)\n }\n}\n
LocationComponentActivationOptions is used to hold the style, LocationComponentOptions and other locating behaviors.
private fun buildLocationComponentActivationOptions(\n style: Style,\n locationComponentOptions: LocationComponentOptions\n): LocationComponentActivationOptions {\n return LocationComponentActivationOptions\n .builder(this, style)\n .locationComponentOptions(locationComponentOptions)\n .useDefaultLocationEngine(true)\n .locationEngineRequest(\n LocationEngineRequest.Builder(750)\n .setFastestInterval(750)\n .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY)\n .build()\n )\n .build()\n}\n
For further customization, you can also utilize the foregroundTintColor()
and pulseColor()
methods on the LocationComponentOptions builder:
val locationComponentOptions =\n LocationComponentOptions.builder(this@BasicLocationPulsingCircleActivity)\n .pulseEnabled(true)\n .pulseColor(Color.RED) // Set color of pulse\n .foregroundTintColor(Color.BLACK) // Set color of user location\n .build()\n
Here is the final results with different color configurations. For the complete content of this demo, please refer to the source code of the Test App.
Map data OpenStreetMap. \u00a9 OpenMapTiles.
A variety of camera modes determine how the camera will track the user location. They provide the right context to your users at the correct time.\u00a0\u21a9
This guide will help you walk through how to use MapSnapshotter.
"},{"location":"snapshotter/#map-snapshot-with-local-style","title":"Map Snapshot with Local Style","text":"Note
You can find the full source code of this example in MapSnapshotterLocalStyleActivity.kt
of the MapLibreAndroidTestApp.
To get started we will show how to use the map snapshotter with a local style.
Add the source code of the Demotiles style as demotiles.json
to the res/raw
directory of our app1. First we will read this style:
val styleJson = resources.openRawResource(R.raw.demotiles).reader().readText()\n
Next, we configure the MapSnapshotter, passing height and width, the style we just read and the camera position:
mapSnapshotter = MapSnapshotter(\n applicationContext,\n MapSnapshotter.Options(\n container.measuredWidth.coerceAtMost(1024),\n container.measuredHeight.coerceAtMost(1024)\n )\n .withStyleBuilder(Style.Builder().fromJson(styleJson))\n .withCameraPosition(\n CameraPosition.Builder().target(LatLng(LATITUDE, LONGITUDE))\n .zoom(ZOOM).build()\n )\n)\n
Lastly we use the .start()
method to create the snapshot, and pass callbacks for when the snapshot is ready or for when an error occurs.
mapSnapshotter.start({ snapshot ->\n Timber.i(\"Snapshot ready\")\n val imageView = findViewById<View>(R.id.snapshot_image) as ImageView\n imageView.setImageBitmap(snapshot.bitmap)\n}) { error -> Timber.e(error )}\n
"},{"location":"snapshotter/#show-a-grid-of-snapshots","title":"Show a Grid of Snapshots","text":"Note
You can find the full source code of this example in MapSnapshotterActivity.kt
of the MapLibreAndroidTestApp.
In this example, we demonstrate how to use the MapSnapshotter
to create multiple map snapshots with different styles and camera positions, displaying them in a grid layout.
First we create a GridLayout
and a list of MapSnapshotter
instances. We create a Style.Builder
with a different style for each cell in the grid.
val styles = arrayOf(\n TestStyles.DEMOTILES,\n TestStyles.AMERICANA,\n TestStyles.OPENFREEMAP_LIBERY,\n TestStyles.AWS_OPEN_DATA_STANDARD_LIGHT,\n TestStyles.PROTOMAPS_LIGHT,\n TestStyles.PROTOMAPS_DARK,\n TestStyles.PROTOMAPS_WHITE,\n TestStyles.PROTOMAPS_GRAYSCALE,\n TestStyles.VERSATILES\n)\nval builder = Style.Builder().fromUri(\n styles[(row * grid.rowCount + column) % styles.size]\n)\n
Next we create a MapSnapshotter.Options
object to customize the settings of each snapshot(ter).
val options = MapSnapshotter.Options(\n grid.measuredWidth / grid.columnCount,\n grid.measuredHeight / grid.rowCount\n)\n .withPixelRatio(1f)\n .withLocalIdeographFontFamily(MapLibreConstants.DEFAULT_FONT)\n
For some rows we randomize the visible region of the snapshot:
if (row % 2 == 0) {\n options.withRegion(\n LatLngBounds.Builder()\n .include(\n LatLng(\n randomInRange(-80f, 80f).toDouble(),\n randomInRange(-160f, 160f).toDouble()\n )\n )\n .include(\n LatLng(\n randomInRange(-80f, 80f).toDouble(),\n randomInRange(-160f, 160f).toDouble()\n )\n )\n .build()\n )\n}\n
For some columns we randomize the camera position:
if (column % 2 == 0) {\n options.withCameraPosition(\n CameraPosition.Builder()\n .target(\n options.region?.center ?: LatLng(\n randomInRange(-80f, 80f).toDouble(),\n randomInRange(-160f, 160f).toDouble()\n )\n )\n .bearing(randomInRange(0f, 360f).toDouble())\n .tilt(randomInRange(0f, 60f).toDouble())\n .zoom(randomInRange(0f, 10f).toDouble())\n .padding(1.0, 1.0, 1.0, 1.0)\n .build()\n )\n}\n
In the last column of the first row we add two bitmaps. See the next example for more details.
if (row == 0 && column == 2) {\n val carBitmap = BitmapUtils.getBitmapFromDrawable(\n ResourcesCompat.getDrawable(resources, R.drawable.ic_directions_car_black, theme)\n )\n\n // Marker source\n val markerCollection = FeatureCollection.fromFeatures(\n arrayOf(\n Feature.fromGeometry(\n Point.fromLngLat(4.91638, 52.35673),\n featureProperties(\"1\", \"Android\")\n ),\n Feature.fromGeometry(\n Point.fromLngLat(4.91638, 12.34673),\n featureProperties(\"2\", \"Car\")\n )\n )\n )\n val markerSource: Source = GeoJsonSource(MARKER_SOURCE, markerCollection)\n\n // Marker layer\n val markerSymbolLayer = SymbolLayer(MARKER_LAYER, MARKER_SOURCE)\n .withProperties(\n PropertyFactory.iconImage(Expression.get(TITLE_FEATURE_PROPERTY)),\n PropertyFactory.iconIgnorePlacement(true),\n PropertyFactory.iconAllowOverlap(true),\n PropertyFactory.iconSize(\n Expression.switchCase(\n Expression.toBool(Expression.get(SELECTED_FEATURE_PROPERTY)),\n Expression.literal(1.5f),\n Expression.literal(1.0f)\n )\n ),\n PropertyFactory.iconAnchor(Property.ICON_ANCHOR_BOTTOM),\n PropertyFactory.iconColor(Color.BLUE)\n )\n builder.withImage(\"Car\", Objects.requireNonNull(carBitmap!!), false)\n .withSources(markerSource)\n .withLayers(markerSymbolLayer)\n options\n .withRegion(null)\n .withCameraPosition(\n CameraPosition.Builder()\n .target(\n LatLng(5.537109374999999, 52.07950600379697)\n )\n .zoom(1.0)\n .padding(1.0, 1.0, 1.0, 1.0)\n .build()\n )\n}\n
"},{"location":"snapshotter/#map-snapshot-with-bitmap-overlay","title":"Map Snapshot with Bitmap Overlay","text":"Note
You can find the full source code of this example in MapSnapshotterBitMapOverlayActivity.kt
of the MapLibreAndroidTestApp.
This example adds a bitmap on top of the snapshot. It also demonstrates how you can add a click listener to a snapshot.
MapSnapshotterBitMapOverlayActivity.ktpackage org.maplibre.android.testapp.activity.snapshot\n\nimport android.annotation.SuppressLint\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.Canvas\nimport android.graphics.PointF\nimport android.os.Bundle\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.ViewTreeObserver.OnGlobalLayoutListener\nimport android.widget.ImageView\nimport androidx.annotation.VisibleForTesting\nimport androidx.appcompat.app.AppCompatActivity\nimport org.maplibre.android.camera.CameraPosition\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.Style\nimport org.maplibre.android.snapshotter.MapSnapshot\nimport org.maplibre.android.snapshotter.MapSnapshotter\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\nimport timber.log.Timber\n\n/**\n * Test activity showing how to use a the [MapSnapshotter] and overlay\n * [android.graphics.Bitmap]s on top.\n */\nclass MapSnapshotterBitMapOverlayActivity :\n AppCompatActivity(),\n MapSnapshotter.SnapshotReadyCallback {\n private var mapSnapshotter: MapSnapshotter? = null\n\n @get:VisibleForTesting\n var mapSnapshot: MapSnapshot? = null\n private set\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_map_snapshotter_marker)\n val container = findViewById<View>(R.id.container)\n container.viewTreeObserver\n .addOnGlobalLayoutListener(object : OnGlobalLayoutListener {\n override fun onGlobalLayout() {\n container.viewTreeObserver.removeOnGlobalLayoutListener(this)\n Timber.i(\"Starting snapshot\")\n mapSnapshotter = MapSnapshotter(\n applicationContext,\n MapSnapshotter.Options(\n Math.min(container.measuredWidth, 1024),\n Math.min(container.measuredHeight, 1024)\n )\n .withStyleBuilder(\n Style.Builder().fromUri(TestStyles.AMERICANA)\n )\n .withCameraPosition(\n CameraPosition.Builder().target(LatLng(52.090737, 5.121420))\n .zoom(15.0).build()\n )\n )\n mapSnapshotter!!.start(this@MapSnapshotterBitMapOverlayActivity)\n }\n })\n }\n\n override fun onStop() {\n super.onStop()\n mapSnapshotter!!.cancel()\n }\n\n @SuppressLint(\"ClickableViewAccessibility\")\n override fun onSnapshotReady(snapshot: MapSnapshot) {\n mapSnapshot = snapshot\n Timber.i(\"Snapshot ready\")\n val imageView = findViewById<View>(R.id.snapshot_image) as ImageView\n val image = addMarker(snapshot)\n imageView.setImageBitmap(image)\n imageView.setOnTouchListener { v: View?, event: MotionEvent ->\n if (event.action == MotionEvent.ACTION_DOWN) {\n val latLng = snapshot.latLngForPixel(PointF(event.x, event.y))\n Timber.e(\"Clicked LatLng is %s\", latLng)\n return@setOnTouchListener true\n }\n false\n }\n }\n\n private fun addMarker(snapshot: MapSnapshot): Bitmap {\n val canvas = Canvas(snapshot.bitmap)\n val marker =\n BitmapFactory.decodeResource(resources, R.drawable.maplibre_marker_icon_default, null)\n // Dom toren\n val markerLocation = snapshot.pixelForLatLng(LatLng(52.090649433011315, 5.121310651302338))\n canvas.drawBitmap(\n marker, /* Subtract half of the width so we center the bitmap correctly */\n markerLocation.x - marker.width / 2, /* Subtract half of the height so we align the bitmap bottom correctly */\n markerLocation.y - marker.height / 2,\n null\n )\n return snapshot.bitmap\n }\n}\n
"},{"location":"snapshotter/#map-snapshotter-with-heatmap-layer","title":"Map Snapshotter with Heatmap Layer","text":"Note
You can find the full source code of this example in MapSnapshotterHeatMapActivity.kt
of the MapLibreAndroidTestApp.
In this example, we demonstrate how to use the MapSnapshotter
to create a snapshot of a map that includes a heatmap layer. The heatmap represents earthquake data loaded from a GeoJSON source.
First, we create the MapSnapshotterHeatMapActivity
class, which extends AppCompatActivity
and implements MapSnapshotter.SnapshotReadyCallback
to receive the snapshot once it's ready.
class MapSnapshotterHeatMapActivity : AppCompatActivity(), MapSnapshotter.SnapshotReadyCallback {\n
In the onCreate
method, we set up the layout and initialize the MapSnapshotter
once the layout is ready.
override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_map_snapshotter_marker)\n val container = findViewById<View>(R.id.container)\n container.viewTreeObserver\n .addOnGlobalLayoutListener(object : OnGlobalLayoutListener {\n override fun onGlobalLayout() {\n container.viewTreeObserver.removeOnGlobalLayoutListener(this)\n Timber.i(\"Starting snapshot\")\n val builder = Style.Builder().fromUri(TestStyles.AMERICANA)\n .withSource(earthquakeSource!!)\n .withLayerAbove(heatmapLayer, \"water\")\n mapSnapshotter = MapSnapshotter(\n applicationContext,\n MapSnapshotter.Options(container.measuredWidth, container.measuredHeight)\n .withStyleBuilder(builder)\n .withCameraPosition(\n CameraPosition.Builder()\n .target(LatLng(15.0, (-94).toDouble()))\n .zoom(5.0)\n .padding(1.0, 1.0, 1.0, 1.0)\n .build()\n )\n )\n mapSnapshotter!!.start(this@MapSnapshotterHeatMapActivity)\n }\n })\n}\n
Here, we wait for the layout to be laid out using an OnGlobalLayoutListener
before initializing the MapSnapshotter
. We create a Style.Builder
with a base style (TestStyles.AMERICANA
), add the earthquake data source, and add the heatmap layer above the \"water\" layer.
The heatmapLayer
property defines the HeatmapLayer
used to visualize the earthquake data.
private val heatmapLayer: HeatmapLayer\n get() {\n val layer = HeatmapLayer(HEATMAP_LAYER_ID, EARTHQUAKE_SOURCE_ID)\n layer.maxZoom = 9f\n layer.sourceLayer = HEATMAP_LAYER_SOURCE\n layer.setProperties(\n PropertyFactory.heatmapColor(\n Expression.interpolate(\n Expression.linear(), Expression.heatmapDensity(),\n Expression.literal(0), Expression.rgba(33, 102, 172, 0),\n Expression.literal(0.2), Expression.rgb(103, 169, 207),\n Expression.literal(0.4), Expression.rgb(209, 229, 240),\n Expression.literal(0.6), Expression.rgb(253, 219, 199),\n Expression.literal(0.8), Expression.rgb(239, 138, 98),\n Expression.literal(1), Expression.rgb(178, 24, 43)\n )\n ),\n PropertyFactory.heatmapWeight(\n Expression.interpolate(\n Expression.linear(),\n Expression.get(\"mag\"),\n Expression.stop(0, 0),\n Expression.stop(6, 1)\n )\n ),\n PropertyFactory.heatmapIntensity(\n Expression.interpolate(\n Expression.linear(),\n Expression.zoom(),\n Expression.stop(0, 1),\n Expression.stop(9, 3)\n )\n ),\n PropertyFactory.heatmapRadius(\n Expression.interpolate(\n Expression.linear(),\n Expression.zoom(),\n Expression.stop(0, 2),\n Expression.stop(9, 20)\n )\n ),\n PropertyFactory.heatmapOpacity(\n Expression.interpolate(\n Expression.linear(),\n Expression.zoom(),\n Expression.stop(7, 1),\n Expression.stop(9, 0)\n )\n )\n )\n return layer\n }\n
This code sets up the heatmap layer's properties, such as color ramp, weight, intensity, radius, and opacity, using expressions that interpolate based on data properties and zoom level.
We also define the earthquakeSource
, which loads data from a GeoJSON file containing earthquake information.
private val earthquakeSource: Source?\n get() {\n var source: Source? = null\n try {\n source = GeoJsonSource(EARTHQUAKE_SOURCE_ID, URI(EARTHQUAKE_SOURCE_URL))\n } catch (uriSyntaxException: URISyntaxException) {\n Timber.e(uriSyntaxException, \"That's not a valid URL.\")\n }\n return source\n }\n
When the snapshot is ready, the onSnapshotReady
callback is invoked, where we set the snapshot bitmap to an ImageView
to display it.
@SuppressLint(\"ClickableViewAccessibility\")\noverride fun onSnapshotReady(snapshot: MapSnapshot) {\n Timber.i(\"Snapshot ready\")\n val imageView = findViewById<ImageView>(R.id.snapshot_image)\n imageView.setImageBitmap(snapshot.bitmap)\n}\n
Finally, we ensure to cancel the snapshotter in the onStop
method to free up resources.
override fun onStop() {\n super.onStop()\n mapSnapshotter?.cancel()\n}\n
"},{"location":"snapshotter/#map-snapshotter-with-expression","title":"Map Snapshotter with Expression","text":"Note
You can find the full source code of this example in MapSnapshotterWithinExpression.kt
of the MapLibreAndroidTestApp.
In this example the map on top is a live while the map on the bottom is a snapshot that is updated as you pan the map. We style of the snapshot is modified: using a within expression only POIs within a certain distance to a line is shown. A highlight for this area is added to the map as are various points.
MapSnapshotterWithinExpression.ktpackage org.maplibre.android.testapp.activity.turf\n\nimport android.graphics.Color\nimport android.os.Bundle\nimport android.os.PersistableBundle\nimport androidx.appcompat.app.AppCompatActivity\nimport org.maplibre.geojson.*\nimport org.maplibre.android.camera.CameraPosition\nimport org.maplibre.android.camera.CameraUpdateFactory\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.MapView\nimport org.maplibre.android.maps.MapLibreMap\nimport org.maplibre.android.maps.Style\nimport org.maplibre.android.snapshotter.MapSnapshot\nimport org.maplibre.android.snapshotter.MapSnapshotter\nimport org.maplibre.android.style.expressions.Expression.within\nimport org.maplibre.android.style.layers.CircleLayer\nimport org.maplibre.android.style.layers.FillLayer\nimport org.maplibre.android.style.layers.LineLayer\nimport org.maplibre.android.style.layers.Property.NONE\nimport org.maplibre.android.style.layers.PropertyFactory.*\nimport org.maplibre.android.style.layers.SymbolLayer\nimport org.maplibre.android.style.sources.GeoJsonOptions\nimport org.maplibre.android.style.sources.GeoJsonSource\nimport org.maplibre.android.testapp.databinding.ActivityMapsnapshotterWithinExpressionBinding\nimport org.maplibre.android.testapp.styles.TestStyles.getPredefinedStyleWithFallback\n\n/**\n * An Activity that showcases the use of MapSnapshotter with 'within' expression\n */\nclass MapSnapshotterWithinExpression : AppCompatActivity() {\n private lateinit var binding: ActivityMapsnapshotterWithinExpressionBinding\n private lateinit var maplibreMap: MapLibreMap\n private lateinit var snapshotter: MapSnapshotter\n private var snapshotInProgress = false\n\n private val cameraListener = object : MapView.OnCameraDidChangeListener {\n override fun onCameraDidChange(animated: Boolean) {\n if (!snapshotInProgress) {\n snapshotInProgress = true\n snapshotter.setCameraPosition(maplibreMap.cameraPosition)\n snapshotter.start(object : MapSnapshotter.SnapshotReadyCallback {\n override fun onSnapshotReady(snapshot: MapSnapshot) {\n binding.imageView.setImageBitmap(snapshot.bitmap)\n snapshotInProgress = false\n }\n })\n }\n }\n }\n\n private val snapshotterObserver = object : MapSnapshotter.Observer {\n override fun onStyleImageMissing(imageName: String) {\n }\n\n override fun onDidFinishLoadingStyle() {\n // Show only POI labels inside geometry using within expression\n (snapshotter.getLayer(\"poi-label\") as SymbolLayer).setFilter(\n within(\n bufferLineStringGeometry()\n )\n )\n // Hide other types of labels to highlight POI labels\n (snapshotter.getLayer(\"road-label\") as SymbolLayer).setProperties(visibility(NONE))\n (snapshotter.getLayer(\"transit-label\") as SymbolLayer).setProperties(visibility(NONE))\n (snapshotter.getLayer(\"road-number-shield\") as SymbolLayer).setProperties(visibility(NONE))\n }\n }\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n binding = ActivityMapsnapshotterWithinExpressionBinding.inflate(layoutInflater)\n setContentView(binding.root)\n\n binding.mapView.onCreate(savedInstanceState)\n binding.mapView.getMapAsync { map ->\n maplibreMap = map\n\n // Setup camera position above Georgetown\n maplibreMap.cameraPosition = CameraPosition.Builder().target(LatLng(38.90628988399711, -77.06574689337494)).zoom(15.5).build()\n\n // Wait for the map to become idle before manipulating the style and camera of the map\n binding.mapView.addOnDidBecomeIdleListener(object : MapView.OnDidBecomeIdleListener {\n override fun onDidBecomeIdle() {\n maplibreMap.easeCamera(\n CameraUpdateFactory.newCameraPosition(\n CameraPosition.Builder().zoom(16.0).target(LatLng(38.905156245642814, -77.06535338052844)).bearing(80.68015859462369).tilt(55.0).build()\n ),\n 1000\n )\n binding.mapView.removeOnDidBecomeIdleListener(this)\n }\n })\n // Load mapbox streets and add lines and circles\n setupStyle()\n }\n }\n\n private fun setupStyle() {\n // Assume the route is represented by an array of coordinates.\n val coordinates = listOf<Point>(\n Point.fromLngLat(-77.06866264343262, 38.90506061276737),\n Point.fromLngLat(-77.06283688545227, 38.905194197410545),\n Point.fromLngLat(-77.06285834312439, 38.906429843444094),\n Point.fromLngLat(-77.0630407333374, 38.90680554236621)\n )\n\n // Setup style with additional layers,\n // using streets as a base style\n maplibreMap.setStyle(\n Style.Builder().fromUri(getPredefinedStyleWithFallback(\"Streets\"))\n ) {\n binding.mapView.addOnCameraDidChangeListener(cameraListener)\n }\n\n val options = MapSnapshotter.Options(binding.imageView.measuredWidth / 2, binding.imageView.measuredHeight / 2)\n .withCameraPosition(maplibreMap.cameraPosition)\n .withPixelRatio(2.0f).withStyleBuilder(\n Style.Builder().fromUri(getPredefinedStyleWithFallback(\"Streets\")).withSources(\n GeoJsonSource(\n POINT_ID,\n LineString.fromLngLats(coordinates)\n ),\n GeoJsonSource(\n FILL_ID,\n FeatureCollection.fromFeature(\n Feature.fromGeometry(bufferLineStringGeometry())\n ),\n GeoJsonOptions().withBuffer(0).withTolerance(0.0f)\n )\n ).withLayerBelow(\n LineLayer(LINE_ID, POINT_ID).withProperties(\n lineWidth(7.5f),\n lineColor(Color.LTGRAY)\n ),\n \"poi-label\"\n ).withLayerBelow(\n CircleLayer(POINT_ID, POINT_ID).withProperties(\n circleRadius(7.5f),\n circleColor(Color.DKGRAY),\n circleOpacity(0.75f)\n ),\n \"poi-label\"\n ).withLayerBelow(\n FillLayer(FILL_ID, FILL_ID).withProperties(\n fillOpacity(0.12f),\n fillColor(Color.YELLOW)\n ),\n LINE_ID\n )\n )\n snapshotter = MapSnapshotter(this, options)\n snapshotter.setObserver(snapshotterObserver)\n }\n\n override fun onStart() {\n super.onStart()\n binding.mapView.onStart()\n }\n\n override fun onResume() {\n super.onResume()\n binding.mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n binding.mapView.onPause()\n }\n\n override fun onStop() {\n super.onStop()\n binding.mapView.onStop()\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n binding.mapView.onLowMemory()\n }\n\n override fun onDestroy() {\n super.onDestroy()\n binding.mapView.onDestroy()\n }\n\n override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {\n super.onSaveInstanceState(outState, outPersistentState)\n binding.mapView.onSaveInstanceState(outState)\n }\n private fun bufferLineStringGeometry(): Polygon {\n // TODO replace static data by Turf#Buffer: mapbox-java/issues/987\n // # --8<-- [start:fromJson]\n return FeatureCollection.fromJson(\n \"\"\"\n {\n \"type\": \"FeatureCollection\",\n \"features\": [\n {\n \"type\": \"Feature\",\n \"properties\": {},\n \"geometry\": {\n \"type\": \"Polygon\",\n \"coordinates\": [\n [\n [\n -77.06867337226866,\n 38.90467655551809\n ],\n [\n -77.06233263015747,\n 38.90479344272695\n ],\n [\n -77.06234335899353,\n 38.906463238984344\n ],\n [\n -77.06290125846863,\n 38.907206285691615\n ],\n [\n -77.06364154815674,\n 38.90684728656818\n ],\n [\n -77.06326603889465,\n 38.90637140121084\n ],\n [\n -77.06321239471436,\n 38.905561553883246\n ],\n [\n -77.0691454410553,\n 38.905436318935635\n ],\n [\n -77.06912398338318,\n 38.90466820642439\n ],\n [\n -77.06867337226866,\n 38.90467655551809\n ]\n ]\n ]\n }\n }\n ]\n }\n \"\"\".trimIndent()\n ).features()!![0].geometry() as Polygon\n // # --8<-- [end:fromJson]\n }\n\n companion object {\n const val POINT_ID = \"point\"\n const val FILL_ID = \"fill\"\n const val LINE_ID = \"line\"\n }\n}\n
See App resources overview for this and other ways you can provide resources to your app.\u00a0\u21a9
This example demonstrates how you can add markers in bulk.
BulkMarkerActivity.ktpackage org.maplibre.android.testapp.activity.annotation\n\nimport android.app.ProgressDialog\nimport android.os.Bundle\nimport android.view.Menu\nimport android.view.View\nimport android.widget.AdapterView\nimport android.widget.AdapterView.OnItemSelectedListener\nimport android.widget.ArrayAdapter\nimport android.widget.Spinner\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.lifecycleScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.maplibre.android.annotations.MarkerOptions\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.MapView\nimport org.maplibre.android.maps.MapLibreMap\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\nimport org.maplibre.android.testapp.utils.GeoParseUtil\nimport timber.log.Timber\nimport java.io.IOException\nimport java.text.DecimalFormat\nimport java.util.*\nimport kotlin.math.min\n\n/**\n * Test activity showcasing adding a large amount of Markers.\n */\nclass BulkMarkerActivity : AppCompatActivity(), OnItemSelectedListener {\n private lateinit var maplibreMap: MapLibreMap\n private lateinit var mapView: MapView\n private var locations: List<LatLng>? = null\n private var progressDialog: ProgressDialog? = null\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_marker_bulk)\n mapView = findViewById(R.id.mapView)\n mapView.onCreate(savedInstanceState)\n mapView.getMapAsync { initMap(it) }\n }\n\n private fun initMap(maplibreMap: MapLibreMap) {\n this.maplibreMap = maplibreMap\n maplibreMap.setStyle(TestStyles.getPredefinedStyleWithFallback(\"Streets\"))\n }\n\n override fun onCreateOptionsMenu(menu: Menu): Boolean {\n val spinnerAdapter = ArrayAdapter.createFromResource(\n this,\n R.array.bulk_marker_list,\n android.R.layout.simple_spinner_item\n )\n spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)\n menuInflater.inflate(R.menu.menu_bulk_marker, menu)\n val item = menu.findItem(R.id.spinner)\n val spinner = item.actionView as Spinner\n spinner.adapter = spinnerAdapter\n spinner.onItemSelectedListener = this@BulkMarkerActivity\n return true\n }\n\n override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {\n val amount = Integer.valueOf(resources.getStringArray(R.array.bulk_marker_list)[position])\n if (locations == null) {\n progressDialog = ProgressDialog.show(this, \"Loading\", \"Fetching markers\", false)\n lifecycleScope.launch(Dispatchers.IO) {\n locations = loadLocationTask(this@BulkMarkerActivity)\n withContext(Dispatchers.Main) {\n onLatLngListLoaded(locations, amount)\n }\n }\n } else {\n showMarkers(amount)\n }\n }\n\n private fun onLatLngListLoaded(latLngs: List<LatLng>?, amount: Int) {\n progressDialog!!.hide()\n locations = latLngs\n showMarkers(amount)\n }\n\n private fun showMarkers(amount: Int) {\n if (!this::maplibreMap.isInitialized || locations == null || mapView.isDestroyed) {\n return\n }\n maplibreMap.clear()\n showGlMarkers(min(amount, locations!!.size))\n }\n\n private fun showGlMarkers(amount: Int) {\n val markerOptionsList: MutableList<MarkerOptions> = ArrayList()\n val formatter = DecimalFormat(\"#.#####\")\n val random = Random()\n var randomIndex: Int\n for (i in 0 until amount) {\n randomIndex = random.nextInt(locations!!.size)\n val latLng = locations!![randomIndex]\n markerOptionsList.add(\n MarkerOptions()\n .position(latLng)\n .title(i.toString())\n .snippet(formatter.format(latLng.latitude) + \"`, \" + formatter.format(latLng.longitude))\n )\n }\n maplibreMap.addMarkers(markerOptionsList)\n }\n\n override fun onNothingSelected(parent: AdapterView<*>?) {\n // nothing selected, nothing to do!\n }\n\n override fun onStart() {\n super.onStart()\n mapView.onStart()\n }\n\n override fun onResume() {\n super.onResume()\n mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n mapView.onPause()\n }\n\n override fun onStop() {\n super.onStop()\n mapView.onStop()\n }\n\n override fun onSaveInstanceState(outState: Bundle) {\n super.onSaveInstanceState(outState)\n mapView.onSaveInstanceState(outState)\n }\n\n override fun onDestroy() {\n super.onDestroy()\n if (progressDialog != null) {\n progressDialog!!.dismiss()\n }\n mapView.onDestroy()\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n mapView.onLowMemory()\n }\n\n private fun loadLocationTask(\n activity: BulkMarkerActivity,\n ) : List<LatLng>? {\n try {\n val json = GeoParseUtil.loadStringFromAssets(\n activity.applicationContext,\n \"points.geojson\"\n )\n return GeoParseUtil.parseGeoJsonCoordinates(json)\n } catch (exception: IOException) {\n Timber.e(exception, \"Could not add markers\")\n }\n return null\n }\n}\n
"},{"location":"annotations/marker-annotations/","title":"Annotation: Marker","text":"This guide will show you how to add Markers in the map.
Annotation
is an overlay on top of a Map. In package org.maplibre.android.annotations
, it has the following subclasses:
A Marker shows an icon image at a geographical location. By default, marker uses a provided image as its icon.
Or, the icon can be customized using IconFactory to generate an Icon using a provided image.
For more customization, please read the documentation about MarkerOptions.
In this showcase, we continue the code from the Quickstart, rename Activity into JsonApiActivity
, and pull the GeoJSON data from a free and public API. Then add markers to the map with GeoJSON:
In your module Gradle file (usually <project>/<app-module>/build.gradle
), add okhttp
to simplify code for making HTTP requests.
dependencies {\n ...\n implementation 'com.squareup.okhttp3:okhttp:4.10.0'\n ...\n}\n
Sync your Android project the with Gradle files.
In JsonApiActivity
we add a new variable for MapLibreMap
. It is used to add annotations to the map instance.
class JsonApiActivity : AppCompatActivity() {\n\n // Declare a variable for MapView\n private lateinit var mapView: MapView\n\n // Declare a variable for MapLibreMap\n private lateinit var maplibreMap: MapLibreMap\n
Call mapview.getMapSync()
in order to get a MapLibreMap
object. After maplibreMap
is assigned, call the getEarthQuakeDataFromUSGS()
method to make a HTTP request and transform data into the map annotations.
mapView.getMapAsync { map ->\n maplibreMap = map\n\n maplibreMap.setStyle(\"https://demotiles.maplibre.org/style.json\")\n\n // Fetch data from USGS\n getEarthQuakeDataFromUSGS()\n}\n
Define a function getEarthQuakeDataFromUSGS()
to fetch GeoJSON data from a public API. If we successfully get the response, call addMarkersToMap()
on the UI thread.
// Get Earthquake data from usgs.gov, read API doc at:\n// https://earthquake.usgs.gov/fdsnws/event/1/\nprivate fun getEarthQuakeDataFromUSGS() {\n val url = \"https://earthquake.usgs.gov/fdsnws/event/1/query\".toHttpUrl().newBuilder()\n .addQueryParameter(\"format\", \"geojson\")\n .addQueryParameter(\"starttime\", \"2022-01-01\")\n .addQueryParameter(\"endtime\", \"2022-12-31\")\n .addQueryParameter(\"minmagnitude\", \"5.8\")\n .addQueryParameter(\"latitude\", \"24\")\n .addQueryParameter(\"longitude\", \"121\")\n .addQueryParameter(\"maxradius\", \"1.5\")\n .build()\n val request: Request = Request.Builder().url(url).build()\n\n OkHttpClient().newCall(request).enqueue(object : Callback {\n override fun onFailure(call: Call, e: IOException) {\n Toast.makeText(this@JsonApiActivity, \"Fail to fetch data\", Toast.LENGTH_SHORT)\n .show()\n }\n\n override fun onResponse(call: Call, response: Response) {\n val featureCollection = response.body?.string()\n ?.let(FeatureCollection::fromJson)\n ?: return\n // If FeatureCollection in response is not null\n // Then add markers to map\n runOnUiThread { addMarkersToMap(featureCollection) }\n }\n })\n}\n
Now it is time to add markers into the map.
addMarkersToMap()
method, we define two types of bitmap for the marker icon.Finally, move the camera to the bounds of the newly added markers
private fun addMarkersToMap(data: FeatureCollection) {\n val bounds = mutableListOf<LatLng>()\n\n // Get bitmaps for marker icon\n val infoIconDrawable = ResourcesCompat.getDrawable(\n this.resources,\n // Intentionally specify package name\n // This makes copy from another project easier\n org.maplibre.android.R.drawable.maplibre_info_icon_default,\n theme\n )!!\n val bitmapBlue = infoIconDrawable.toBitmap()\n val bitmapRed = infoIconDrawable\n .mutate()\n .apply { setTint(Color.RED) }\n .toBitmap()\n\n // Add symbol for each point feature\n data.features()?.forEach { feature ->\n val geometry = feature.geometry()?.toJson() ?: return@forEach\n val point = Point.fromJson(geometry) ?: return@forEach\n val latLng = LatLng(point.latitude(), point.longitude())\n bounds.add(latLng)\n\n // Contents in InfoWindow of each marker\n val title = feature.getStringProperty(\"title\")\n val epochTime = feature.getNumberProperty(\"time\")\n val dateString = SimpleDateFormat(\"yyyy/MM/dd HH:mm\", Locale.TAIWAN).format(epochTime)\n\n // If magnitude > 6.0, show marker with red icon. If not, show blue icon instead\n val mag = feature.getNumberProperty(\"mag\")\n val icon = IconFactory.getInstance(this)\n .fromBitmap(if (mag.toFloat() > 6.0) bitmapRed else bitmapBlue)\n\n // Use MarkerOptions and addMarker() to add a new marker in map\n val markerOptions = MarkerOptions()\n .position(latLng)\n .title(dateString)\n .snippet(title)\n .icon(icon)\n maplibreMap.addMarker(markerOptions)\n }\n\n // Move camera to newly added annotations\n maplibreMap.getCameraForLatLngBounds(LatLngBounds.fromLatLngs(bounds))?.let {\n val newCameraPosition = CameraPosition.Builder()\n .target(it.target)\n .zoom(it.zoom - 0.5)\n .build()\n maplibreMap.cameraPosition = newCameraPosition\n }\n}\n
Here is the final result. For the full contents of JsonApiActivity
, please visit source code of our Test App.
Note
You can find the full source code of this example in CameraAnimationTypeActivity.kt
of the MapLibreAndroidTestApp.
This example showcases the different animation types.
MapLibreMap.moveCamera
method.MapLibreMap.easeCamera
method.MapLibreMap.animateCamera
method.The MapLibreMap.moveCamera
method jumps to the camera position provided.
val cameraPosition =\n CameraPosition.Builder()\n .target(nextLatLng)\n .zoom(14.0)\n .tilt(30.0)\n .tilt(0.0)\n .build()\nmaplibreMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))\n
"},{"location":"camera/animation-types/#ease","title":"Ease","text":"The MapLibreMap.moveCamera
eases to the camera position provided (with constant ground speed).
val cameraPosition =\n CameraPosition.Builder()\n .target(nextLatLng)\n .zoom(15.0)\n .bearing(180.0)\n .tilt(30.0)\n .build()\nmaplibreMap.easeCamera(\n CameraUpdateFactory.newCameraPosition(cameraPosition),\n 7500,\n callback\n)\n
"},{"location":"camera/animation-types/#animate","title":"Animate","text":"The MapLibreMap.animateCamera
uses a powered flight animation move to the camera position provided1.
val cameraPosition =\n CameraPosition.Builder().target(nextLatLng).bearing(270.0).tilt(20.0).build()\nmaplibreMap.animateCamera(\n CameraUpdateFactory.newCameraPosition(cameraPosition),\n 7500,\n callback\n)\n
"},{"location":"camera/animation-types/#animation-callbacks","title":"Animation Callbacks","text":"In the previous section a CancellableCallback
was passed to the last two animation methods. This callback shows a toast message when the animation is cancelled or when it is finished.
private val callback: CancelableCallback =\n object : CancelableCallback {\n override fun onCancel() {\n Timber.i(\"Duration onCancel Callback called.\")\n Toast.makeText(\n applicationContext,\n \"Ease onCancel Callback called.\",\n Toast.LENGTH_LONG\n )\n .show()\n }\n\n override fun onFinish() {\n Timber.i(\"Duration onFinish Callback called.\")\n Toast.makeText(\n applicationContext,\n \"Ease onFinish Callback called.\",\n Toast.LENGTH_LONG\n )\n .show()\n }\n }\n
The implementation is based on Van Wijk, Jarke J.; Nuij, Wim A. A. \u201cSmooth and efficient zooming and panning.\u201d INFOVIS \u201903. pp. 15\u201322. https://www.win.tue.nl/~vanwijk/zoompan.pdf#page=5 \u21a9
This example showcases how to use the Animator API to schedule a sequence of map animations.
CameraAnimatorActivity.ktpackage org.maplibre.android.testapp.activity.camera\n\nimport android.animation.Animator\nimport android.animation.AnimatorSet\nimport android.animation.TypeEvaluator\nimport android.animation.ValueAnimator\nimport android.os.Bundle\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.animation.AnticipateOvershootInterpolator\nimport android.view.animation.BounceInterpolator\nimport android.view.animation.Interpolator\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.collection.LongSparseArray\nimport androidx.core.view.animation.PathInterpolatorCompat\nimport androidx.interpolator.view.animation.FastOutLinearInInterpolator\nimport androidx.interpolator.view.animation.FastOutSlowInInterpolator\nimport org.maplibre.android.camera.CameraPosition\nimport org.maplibre.android.camera.CameraUpdateFactory\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.*\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\n\n/** Test activity showcasing using Android SDK animators to animate camera position changes. */\nclass CameraAnimatorActivity : AppCompatActivity(), OnMapReadyCallback {\n private val animators = LongSparseArray<Animator>()\n private lateinit var set: Animator\n private lateinit var mapView: MapView\n private lateinit var maplibreMap: MapLibreMap\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_camera_animator)\n mapView = findViewById<View>(R.id.mapView) as MapView\n if (::mapView.isInitialized) {\n mapView.onCreate(savedInstanceState)\n mapView.getMapAsync(this)\n }\n }\n\n override fun onMapReady(map: MapLibreMap) {\n maplibreMap = map\n map.setStyle(TestStyles.getPredefinedStyleWithFallback(\"Streets\"))\n initFab()\n }\n\n private fun initFab() {\n findViewById<View>(R.id.fab).setOnClickListener { view: View ->\n view.visibility = View.GONE\n val animatedPosition =\n CameraPosition.Builder()\n .target(LatLng(37.789992, -122.402214))\n .tilt(60.0)\n .zoom(14.5)\n .bearing(135.0)\n .build()\n set = createExampleAnimator(maplibreMap.cameraPosition, animatedPosition)\n set.start()\n }\n }\n\n //\n // Animator API used for the animation on the FAB\n //\n private fun createExampleAnimator(\n currentPosition: CameraPosition,\n targetPosition: CameraPosition\n ): Animator {\n val animatorSet = AnimatorSet()\n animatorSet.play(createLatLngAnimator(currentPosition.target!!, targetPosition.target!!))\n animatorSet.play(createZoomAnimator(currentPosition.zoom, targetPosition.zoom))\n animatorSet.play(createBearingAnimator(currentPosition.bearing, targetPosition.bearing))\n animatorSet.play(createTiltAnimator(currentPosition.tilt, targetPosition.tilt))\n return animatorSet\n }\n\n private fun createLatLngAnimator(currentPosition: LatLng, targetPosition: LatLng): Animator {\n val latLngAnimator =\n ValueAnimator.ofObject(LatLngEvaluator(), currentPosition, targetPosition)\n latLngAnimator.duration = (1000 * ANIMATION_DELAY_FACTOR).toLong()\n latLngAnimator.interpolator = FastOutSlowInInterpolator()\n latLngAnimator.addUpdateListener { animation: ValueAnimator ->\n maplibreMap.moveCamera(\n CameraUpdateFactory.newLatLng((animation.animatedValue as LatLng))\n )\n }\n return latLngAnimator\n }\n\n private fun createZoomAnimator(currentZoom: Double, targetZoom: Double): Animator {\n val zoomAnimator = ValueAnimator.ofFloat(currentZoom.toFloat(), targetZoom.toFloat())\n zoomAnimator.duration = (2200 * ANIMATION_DELAY_FACTOR).toLong()\n zoomAnimator.startDelay = (600 * ANIMATION_DELAY_FACTOR).toLong()\n zoomAnimator.interpolator = AnticipateOvershootInterpolator()\n zoomAnimator.addUpdateListener { animation: ValueAnimator ->\n maplibreMap.moveCamera(\n CameraUpdateFactory.zoomTo((animation.animatedValue as Float).toDouble())\n )\n }\n return zoomAnimator\n }\n\n private fun createBearingAnimator(currentBearing: Double, targetBearing: Double): Animator {\n val bearingAnimator =\n ValueAnimator.ofFloat(currentBearing.toFloat(), targetBearing.toFloat())\n bearingAnimator.duration = (1000 * ANIMATION_DELAY_FACTOR).toLong()\n bearingAnimator.startDelay = (1000 * ANIMATION_DELAY_FACTOR).toLong()\n bearingAnimator.interpolator = FastOutLinearInInterpolator()\n bearingAnimator.addUpdateListener { animation: ValueAnimator ->\n maplibreMap.moveCamera(\n CameraUpdateFactory.bearingTo((animation.animatedValue as Float).toDouble())\n )\n }\n return bearingAnimator\n }\n\n private fun createTiltAnimator(currentTilt: Double, targetTilt: Double): Animator {\n val tiltAnimator = ValueAnimator.ofFloat(currentTilt.toFloat(), targetTilt.toFloat())\n tiltAnimator.duration = (1000 * ANIMATION_DELAY_FACTOR).toLong()\n tiltAnimator.startDelay = (1500 * ANIMATION_DELAY_FACTOR).toLong()\n tiltAnimator.addUpdateListener { animation: ValueAnimator ->\n maplibreMap.moveCamera(\n CameraUpdateFactory.tiltTo((animation.animatedValue as Float).toDouble())\n )\n }\n return tiltAnimator\n }\n\n //\n // Interpolator examples\n //\n private fun obtainExampleInterpolator(menuItemId: Int): Animator? {\n return animators[menuItemId.toLong()]\n }\n\n override fun onCreateOptionsMenu(menu: Menu): Boolean {\n menuInflater.inflate(R.menu.menu_animator, menu)\n return true\n }\n\n override fun onOptionsItemSelected(item: MenuItem): Boolean {\n if (!::maplibreMap.isInitialized) {\n return false\n }\n if (item.itemId != android.R.id.home) {\n findViewById<View>(R.id.fab).visibility = View.GONE\n resetCameraPosition()\n playAnimation(item.itemId)\n }\n return super.onOptionsItemSelected(item)\n }\n\n private fun resetCameraPosition() {\n maplibreMap.moveCamera(\n CameraUpdateFactory.newCameraPosition(\n CameraPosition.Builder()\n .target(START_LAT_LNG)\n .zoom(11.0)\n .bearing(0.0)\n .tilt(0.0)\n .build()\n )\n )\n }\n\n private fun playAnimation(itemId: Int) {\n val animator = obtainExampleInterpolator(itemId)\n if (animator != null) {\n animator.cancel()\n animator.start()\n }\n }\n\n private fun obtainExampleInterpolator(interpolator: Interpolator, duration: Long): Animator {\n val zoomAnimator = ValueAnimator.ofFloat(11.0f, 16.0f)\n zoomAnimator.duration = (duration * ANIMATION_DELAY_FACTOR).toLong()\n zoomAnimator.interpolator = interpolator\n zoomAnimator.addUpdateListener { animation: ValueAnimator ->\n maplibreMap.moveCamera(\n CameraUpdateFactory.zoomTo((animation.animatedValue as Float).toDouble())\n )\n }\n return zoomAnimator\n }\n\n //\n // MapView lifecycle\n //\n override fun onStart() {\n super.onStart()\n mapView.onStart()\n }\n\n override fun onResume() {\n super.onResume()\n mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n mapView.onPause()\n }\n\n override fun onStop() {\n super.onStop()\n mapView.onStop()\n for (i in 0 until animators.size()) {\n animators[animators.keyAt(i)]!!.cancel()\n }\n if (this::set.isInitialized) {\n set.cancel()\n }\n }\n\n override fun onSaveInstanceState(outState: Bundle) {\n super.onSaveInstanceState(outState)\n mapView.onSaveInstanceState(outState)\n }\n\n override fun onDestroy() {\n super.onDestroy()\n if (::mapView.isInitialized) {\n mapView.onDestroy()\n }\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n if (::mapView.isInitialized) {\n mapView.onLowMemory()\n }\n }\n\n /** Helper class to evaluate LatLng objects with a ValueAnimator */\n private class LatLngEvaluator : TypeEvaluator<LatLng> {\n private val latLng = LatLng()\n override fun evaluate(fraction: Float, startValue: LatLng, endValue: LatLng): LatLng {\n latLng.latitude = startValue.latitude + (endValue.latitude - startValue.latitude) * fraction\n latLng.longitude = startValue.longitude + (endValue.longitude - startValue.longitude) * fraction\n return latLng\n }\n }\n\n companion object {\n private const val ANIMATION_DELAY_FACTOR = 1.5\n private val START_LAT_LNG = LatLng(37.787947, -122.407432)\n }\n\n init {\n val accelerateDecelerateAnimatorSet = AnimatorSet()\n accelerateDecelerateAnimatorSet.playTogether(\n createLatLngAnimator(START_LAT_LNG, LatLng(37.826715, -122.422795)),\n obtainExampleInterpolator(FastOutSlowInInterpolator(), 2500)\n )\n animators.put(\n R.id.menu_action_accelerate_decelerate_interpolator.toLong(),\n accelerateDecelerateAnimatorSet\n )\n val bounceAnimatorSet = AnimatorSet()\n bounceAnimatorSet.playTogether(\n createLatLngAnimator(START_LAT_LNG, LatLng(37.787947, -122.407432)),\n obtainExampleInterpolator(BounceInterpolator(), 3750)\n )\n animators.put(R.id.menu_action_bounce_interpolator.toLong(), bounceAnimatorSet)\n animators.put(\n R.id.menu_action_anticipate_overshoot_interpolator.toLong(),\n obtainExampleInterpolator(AnticipateOvershootInterpolator(), 2500)\n )\n animators.put(\n R.id.menu_action_path_interpolator.toLong(),\n obtainExampleInterpolator(\n PathInterpolatorCompat.create(.22f, .68f, 0f, 1.71f),\n 2500\n )\n )\n }\n}\n
"},{"location":"camera/cameraposition/","title":"CameraPosition Capabilities","text":"Note
You can find the full source code of this example in CameraPositionActivity.kt
of the MapLibreAndroidTestApp.
This example showcases how to listen to camera change events.
The camera animation is kicked off with this code:
val cameraPosition = CameraPosition.Builder().target(LatLng(latitude, longitude)).zoom(zoom).bearing(bearing).tilt(tilt).build()\n\nmaplibreMap?.animateCamera(\n CameraUpdateFactory.newCameraPosition(cameraPosition),\n 5000,\n object : CancelableCallback {\n override fun onCancel() {\n Timber.v(\"OnCancel called\")\n }\n\n override fun onFinish() {\n Timber.v(\"OnFinish called\")\n }\n }\n)\n
Notice how the color of the button in the bottom right changes color. Depending on the state of the camera.
We can listen for changes to the state of the camera by registering a OnCameraMoveListener
, OnCameraIdleListener
, OnCameraMoveCanceledListener
or OnCameraMoveStartedListener
with the MapLibreMap
. For example, the OnCameraMoveListener
is defined with:
private val moveListener = OnCameraMoveListener {\n Timber.e(\"OnCameraMove\")\n fab.setColorFilter(\n ContextCompat.getColor(this@CameraPositionActivity, android.R.color.holo_orange_dark)\n )\n}\n
And registered with:
maplibreMap.addOnCameraMoveListener(moveListener)\n
Refer to the full example to learn the methods to register the other types of camera change events.
"},{"location":"camera/gesture-detector/","title":"Gesture Detector","text":"The gesture detector of MapLibre Android is encapsulated in the maplibre-gestures-android
package.
You can add listeners for move, rotate, scale and shove gestures. For example, adding a move gesture listener with MapLibreMap.addOnRotateListener
:
maplibreMap.addOnMoveListener(\n object : OnMoveListener {\n override fun onMoveBegin(detector: MoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_START, \"MOVE START\")\n )\n }\n\n override fun onMove(detector: MoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_PROGRESS, \"MOVE PROGRESS\")\n )\n }\n\n override fun onMoveEnd(detector: MoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_END, \"MOVE END\")\n )\n recalculateFocalPoint()\n }\n }\n)\n
Refer to the full example below for examples of listeners for the other gesture types.
"},{"location":"camera/gesture-detector/#settings","title":"Settings","text":"You can access an UISettings
object via MapLibreMap.uiSettings
. Available settings include:
UiSettings.isQuickZoomGesturesEnabled
).UiSettings.isScaleVelocityAnimationEnabled
.uiSettings.isRotateGesturesEnabled
.uiSettings.isZoomGesturesEnabled
.package org.maplibre.android.testapp.activity.camera\n\nimport android.annotation.SuppressLint\nimport android.os.Bundle\nimport android.os.Handler\nimport android.os.Looper\nimport android.view.LayoutInflater\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.RelativeLayout\nimport android.widget.TextView\nimport androidx.annotation.ColorInt\nimport androidx.annotation.IntDef\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.content.ContextCompat\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport org.maplibre.android.gestures.AndroidGesturesManager\nimport org.maplibre.android.gestures.MoveGestureDetector\nimport org.maplibre.android.gestures.RotateGestureDetector\nimport org.maplibre.android.gestures.ShoveGestureDetector\nimport org.maplibre.android.gestures.StandardScaleGestureDetector\nimport org.maplibre.android.annotations.Marker\nimport org.maplibre.android.annotations.MarkerOptions\nimport org.maplibre.android.camera.CameraUpdateFactory\nimport org.maplibre.android.geometry.LatLng\nimport org.maplibre.android.maps.MapLibreMap\nimport org.maplibre.android.maps.MapLibreMap.CancelableCallback\nimport org.maplibre.android.maps.MapLibreMap.OnMoveListener\nimport org.maplibre.android.maps.MapLibreMap.OnRotateListener\nimport org.maplibre.android.maps.MapLibreMap.OnScaleListener\nimport org.maplibre.android.maps.MapLibreMap.OnShoveListener\nimport org.maplibre.android.maps.MapView\nimport org.maplibre.android.testapp.R\nimport org.maplibre.android.testapp.styles.TestStyles\nimport org.maplibre.android.testapp.utils.FontCache\nimport org.maplibre.android.testapp.utils.ResourceUtils\n\n/** Test activity showcasing APIs around gestures implementation. */\nclass GestureDetectorActivity : AppCompatActivity() {\n private lateinit var mapView: MapView\n private lateinit var maplibreMap: MapLibreMap\n private lateinit var recyclerView: RecyclerView\n private var gestureAlertsAdapter: GestureAlertsAdapter? = null\n private var gesturesManager: AndroidGesturesManager? = null\n private var marker: Marker? = null\n private var focalPointLatLng: LatLng? = null\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_gesture_detector)\n mapView = findViewById(R.id.mapView)\n mapView.onCreate(savedInstanceState)\n mapView.getMapAsync { map: MapLibreMap ->\n maplibreMap = map\n maplibreMap.setStyle(TestStyles.getPredefinedStyleWithFallback(\"Streets\"))\n initializeMap()\n }\n recyclerView = findViewById(R.id.alerts_recycler)\n recyclerView.setLayoutManager(LinearLayoutManager(this))\n gestureAlertsAdapter = GestureAlertsAdapter()\n recyclerView.setAdapter(gestureAlertsAdapter)\n }\n\n override fun onResume() {\n super.onResume()\n mapView.onResume()\n }\n\n override fun onPause() {\n super.onPause()\n gestureAlertsAdapter!!.cancelUpdates()\n mapView.onPause()\n }\n\n override fun onStart() {\n super.onStart()\n mapView.onStart()\n }\n\n override fun onStop() {\n super.onStop()\n mapView.onStop()\n }\n\n override fun onLowMemory() {\n super.onLowMemory()\n mapView.onLowMemory()\n }\n\n override fun onDestroy() {\n super.onDestroy()\n mapView.onDestroy()\n }\n\n override fun onSaveInstanceState(outState: Bundle) {\n super.onSaveInstanceState(outState)\n mapView.onSaveInstanceState(outState)\n }\n\n private fun initializeMap() {\n gesturesManager = maplibreMap.gesturesManager\n val layoutParams = recyclerView.layoutParams as RelativeLayout.LayoutParams\n layoutParams.height = (mapView.height / 1.75).toInt()\n layoutParams.width = mapView.width / 3\n recyclerView.layoutParams = layoutParams\n attachListeners()\n fixedFocalPointEnabled(maplibreMap.uiSettings.focalPoint != null)\n }\n\n fun attachListeners() {\n // # --8<-- [start:addOnMoveListener]\n maplibreMap.addOnMoveListener(\n object : OnMoveListener {\n override fun onMoveBegin(detector: MoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_START, \"MOVE START\")\n )\n }\n\n override fun onMove(detector: MoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_PROGRESS, \"MOVE PROGRESS\")\n )\n }\n\n override fun onMoveEnd(detector: MoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_END, \"MOVE END\")\n )\n recalculateFocalPoint()\n }\n }\n )\n // # --8<-- [end:addOnMoveListener]\n maplibreMap.addOnRotateListener(\n object : OnRotateListener {\n override fun onRotateBegin(detector: RotateGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_START, \"ROTATE START\")\n )\n }\n\n override fun onRotate(detector: RotateGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_PROGRESS, \"ROTATE PROGRESS\")\n )\n recalculateFocalPoint()\n }\n\n override fun onRotateEnd(detector: RotateGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_END, \"ROTATE END\")\n )\n }\n }\n )\n maplibreMap.addOnScaleListener(\n object : OnScaleListener {\n override fun onScaleBegin(detector: StandardScaleGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_START, \"SCALE START\")\n )\n if (focalPointLatLng != null) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(\n GestureAlert.TYPE_OTHER,\n \"INCREASING MOVE THRESHOLD\"\n )\n )\n gesturesManager!!.moveGestureDetector.moveThreshold =\n ResourceUtils.convertDpToPx(this@GestureDetectorActivity, 175f)\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(\n GestureAlert.TYPE_OTHER,\n \"MANUALLY INTERRUPTING MOVE\"\n )\n )\n gesturesManager!!.moveGestureDetector.interrupt()\n }\n recalculateFocalPoint()\n }\n\n override fun onScale(detector: StandardScaleGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_PROGRESS, \"SCALE PROGRESS\")\n )\n }\n\n override fun onScaleEnd(detector: StandardScaleGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_END, \"SCALE END\")\n )\n if (focalPointLatLng != null) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(\n GestureAlert.TYPE_OTHER,\n \"REVERTING MOVE THRESHOLD\"\n )\n )\n gesturesManager!!.moveGestureDetector.moveThreshold = 0f\n }\n }\n }\n )\n maplibreMap.addOnShoveListener(\n object : OnShoveListener {\n override fun onShoveBegin(detector: ShoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_START, \"SHOVE START\")\n )\n }\n\n override fun onShove(detector: ShoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_PROGRESS, \"SHOVE PROGRESS\")\n )\n }\n\n override fun onShoveEnd(detector: ShoveGestureDetector) {\n gestureAlertsAdapter!!.addAlert(\n GestureAlert(GestureAlert.TYPE_END, \"SHOVE END\")\n )\n }\n }\n )\n }\n\n override fun onCreateOptionsMenu(menu: Menu): Boolean {\n menuInflater.inflate(R.menu.menu_gestures, menu)\n return true\n }\n\n override fun onOptionsItemSelected(item: MenuItem): Boolean {\n val uiSettings = maplibreMap.uiSettings\n when (item.itemId) {\n R.id.menu_gesture_focus_point -> {\n fixedFocalPointEnabled(focalPointLatLng == null)\n return true\n }\n R.id.menu_gesture_animation -> {\n uiSettings.isScaleVelocityAnimationEnabled =\n !uiSettings.isScaleVelocityAnimationEnabled\n uiSettings.isRotateVelocityAnimationEnabled =\n !uiSettings.isRotateVelocityAnimationEnabled\n uiSettings.isFlingVelocityAnimationEnabled =\n !uiSettings.isFlingVelocityAnimationEnabled\n return true\n }\n R.id.menu_gesture_rotate -> {\n uiSettings.isRotateGesturesEnabled = !uiSettings.isRotateGesturesEnabled\n return true\n }\n R.id.menu_gesture_tilt -> {\n uiSettings.isTiltGesturesEnabled = !uiSettings.isTiltGesturesEnabled\n return true\n }\n R.id.menu_gesture_zoom -> {\n uiSettings.isZoomGesturesEnabled = !uiSettings.isZoomGesturesEnabled\n return true\n }\n R.id.menu_gesture_scroll -> {\n uiSettings.isScrollGesturesEnabled = !uiSettings.isScrollGesturesEnabled\n return true\n }\n R.id.menu_gesture_double_tap -> {\n uiSettings.isDoubleTapGesturesEnabled = !uiSettings.isDoubleTapGesturesEnabled\n return true\n }\n R.id.menu_gesture_quick_zoom -> {\n uiSettings.isQuickZoomGesturesEnabled = !uiSettings.isQuickZoomGesturesEnabled\n return true\n }\n R.id.menu_gesture_scroll_horizontal -> {\n uiSettings.isHorizontalScrollGesturesEnabled =\n !uiSettings.isHorizontalScrollGesturesEnabled\n return true\n }\n }\n return super.onOptionsItemSelected(item)\n }\n\n private fun fixedFocalPointEnabled(enabled: Boolean) {\n if (enabled) {\n focalPointLatLng = LatLng(51.50325, -0.12968)\n marker = maplibreMap.addMarker(MarkerOptions().position(focalPointLatLng))\n maplibreMap.easeCamera(\n CameraUpdateFactory.newLatLngZoom(focalPointLatLng!!, 16.0),\n object : CancelableCallback {\n override fun onCancel() {\n recalculateFocalPoint()\n }\n\n override fun onFinish() {\n recalculateFocalPoint()\n }\n }\n )\n } else {\n if (marker != null) {\n maplibreMap.removeMarker(marker!!)\n marker = null\n }\n focalPointLatLng = null\n maplibreMap.uiSettings.focalPoint = null\n }\n }\n\n private fun recalculateFocalPoint() {\n if (focalPointLatLng != null) {\n maplibreMap.uiSettings.focalPoint =\n maplibreMap.projection.toScreenLocation(focalPointLatLng!!)\n }\n }\n\n private class GestureAlertsAdapter : RecyclerView.Adapter<GestureAlertsAdapter.ViewHolder>() {\n private var isUpdating = false\n private val updateHandler = Handler(Looper.getMainLooper())\n private val alerts: MutableList<GestureAlert> = ArrayList()\n\n class ViewHolder internal constructor(view: View) : RecyclerView.ViewHolder(view) {\n var alertMessageTv: TextView\n\n init {\n val typeface = FontCache.get(\"Roboto-Regular.ttf\", view.context)\n alertMessageTv = view.findViewById(R.id.alert_message)\n alertMessageTv.typeface = typeface\n }\n }\n\n override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n val view =\n LayoutInflater.from(parent.context)\n .inflate(R.layout.item_gesture_alert, parent, false)\n return ViewHolder(view)\n }\n\n override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n val alert = alerts[position]\n holder.alertMessageTv.text = alert.message\n holder.alertMessageTv.setTextColor(\n ContextCompat.getColor(holder.alertMessageTv.context, alert.color)\n )\n }\n\n override fun getItemCount(): Int {\n return alerts.size\n }\n\n fun addAlert(alert: GestureAlert) {\n for (gestureAlert in alerts) {\n if (gestureAlert.alertType != GestureAlert.TYPE_PROGRESS) {\n break\n }\n if (alert.alertType == GestureAlert.TYPE_PROGRESS && gestureAlert == alert) {\n return\n }\n }\n if (itemCount >= MAX_NUMBER_OF_ALERTS) {\n alerts.removeAt(itemCount - 1)\n }\n alerts.add(0, alert)\n if (!isUpdating) {\n isUpdating = true\n updateHandler.postDelayed(updateRunnable, 250)\n }\n }\n\n @SuppressLint(\"NotifyDataSetChanged\")\n private val updateRunnable = Runnable {\n notifyDataSetChanged()\n isUpdating = false\n }\n\n fun cancelUpdates() {\n updateHandler.removeCallbacksAndMessages(null)\n }\n }\n\n private class GestureAlert(\n @field:Type @param:Type\n val alertType: Int,\n val message: String?\n ) {\n @Retention(AnnotationRetention.SOURCE)\n @IntDef(TYPE_NONE, TYPE_START, TYPE_PROGRESS, TYPE_END, TYPE_OTHER)\n annotation class Type\n\n @ColorInt var color = 0\n override fun equals(other: Any?): Boolean {\n if (this === other) {\n return true\n }\n if (other == null || javaClass != other.javaClass) {\n return false\n }\n val that = other as GestureAlert\n if (alertType != that.alertType) {\n return false\n }\n return if (message != null) message == that.message else that.message == null\n }\n\n override fun hashCode(): Int {\n var result = alertType\n result = 31 * result + (message?.hashCode() ?: 0)\n return result\n }\n\n companion object {\n const val TYPE_NONE = 0\n const val TYPE_START = 1\n const val TYPE_END = 2\n const val TYPE_PROGRESS = 3\n const val TYPE_OTHER = 4\n }\n\n init {\n when (alertType) {\n TYPE_NONE -> color = android.R.color.black\n TYPE_END -> color = android.R.color.holo_red_dark\n TYPE_OTHER -> color = android.R.color.holo_purple\n TYPE_PROGRESS -> color = android.R.color.holo_orange_dark\n TYPE_START -> color = android.R.color.holo_green_dark\n }\n }\n }\n\n companion object {\n private const val MAX_NUMBER_OF_ALERTS = 30\n }\n}\n
"},{"location":"camera/lat-lng-bounds/","title":"LatLngBounds API","text":"Note
You can find the full source code of this example in LatLngBoundsActivity.kt
of the MapLibreAndroidTestApp.
This example demonstrates setting the camera to some bounds defined by some features. It sets these bounds when the map is initialized and when the bottom sheet is opened or closed.
Here you can see how the feature collection is loaded and how MapLibreMap.getCameraForLatLngBounds
is used to set the bounds during map initialization:
val featureCollection: FeatureCollection =\n fromJson(GeoParseUtil.loadStringFromAssets(this, \"points-sf.geojson\"))\nbounds = createBounds(featureCollection)\n\nmap.getCameraForLatLngBounds(bounds, createPadding(peekHeight))?.let {\n map.cameraPosition = it\n}\n
The createBounds
function uses the LatLngBounds
API to include all points within the bounds:
private fun createBounds(featureCollection: FeatureCollection): LatLngBounds {\n val boundsBuilder = LatLngBounds.Builder()\n featureCollection.features()?.let {\n for (feature in it) {\n val point = feature.geometry() as Point\n boundsBuilder.include(LatLng(point.latitude(), point.longitude()))\n }\n }\n return boundsBuilder.build()\n}\n
"},{"location":"camera/max-min-zoom/","title":"Max/Min Zoom","text":"Note
You can find the full source code of this example in MaxMinZoomActivity.kt
of the MapLibreAndroidTestApp.
This example shows how to configure a maximum and a minimum zoom level.
maplibreMap.setMinZoomPreference(3.0)\nmaplibreMap.setMaxZoomPreference(5.0)\n
"},{"location":"camera/max-min-zoom/#bonus-add-click-listener","title":"Bonus: Add Click Listener","text":"As a bonus, this example also shows how you can define a click listener to the map.
maplibreMap.addOnMapClickListener {\n if (this::maplibreMap.isInitialized) {\n maplibreMap.setStyle(Style.Builder().fromUri(TestStyles.AMERICANA))\n }\n true\n}\n
You can remove a click listener again with MapLibreMap.removeOnMapClickListener
. To use this API you need to assign the click listener to a variable, since you need to pass the listener to that method.
Note
You can find the full source code of this example in ScrollByActivity.kt
of the MapLibreAndroidTestApp.
This example shows how you can move the map by x/y pixels.
maplibreMap.scrollBy(\n (seekBarX.progress * MULTIPLIER_PER_PIXEL).toFloat(),\n (seekBarY.progress * MULTIPLIER_PER_PIXEL).toFloat()\n)\n
"},{"location":"camera/zoom-methods/","title":"Zoom Methods","text":"Note
You can find the full source code of this example in ManualZoomActivity.kt
of the MapLibreAndroidTestApp.
This example shows different methods of zooming in.
Each method uses MapLibreMap.animateCamera
, but with a different CameraUpdateFactory
.
maplibreMap.animateCamera(CameraUpdateFactory.zoomIn())\n
"},{"location":"camera/zoom-methods/#zooming-out","title":"Zooming Out","text":"maplibreMap.animateCamera(CameraUpdateFactory.zoomOut())\n
"},{"location":"camera/zoom-methods/#zoom-by-some-amount-of-zoom-levels","title":"Zoom By Some Amount of Zoom Levels","text":"maplibreMap.animateCamera(CameraUpdateFactory.zoomBy(2.0))\n
"},{"location":"camera/zoom-methods/#zoom-to-a-zoom-level","title":"Zoom to a Zoom Level","text":"maplibreMap.animateCamera(CameraUpdateFactory.zoomTo(2.0))\n
"},{"location":"camera/zoom-methods/#zoom-to-a-point","title":"Zoom to a Point","text":"val view = window.decorView\nmaplibreMap.animateCamera(\n CameraUpdateFactory.zoomBy(\n 1.0,\n Point(view.measuredWidth / 4, view.measuredHeight / 4)\n )\n)\n
"},{"location":"styling/animated-image-source/","title":"Animated Image Source","text":"Note
You can find the full source code of this example in AnimatedImageSourceActivity.kt
of the MapLibreAndroidTestApp.
In this example we will see how we can animate an image source. This is the MapLibre Native equivalent of this MapLibre GL JS example.
Map data OpenStreetMap. \u00a9 OpenMapTiles.
We set up an image source in a particular quad. Then we kick of a runnable that periodically updates the image source.
Creating the image sourceval quad = LatLngQuad(\n LatLng(46.437, -80.425),\n LatLng(46.437, -71.516),\n LatLng(37.936, -71.516),\n LatLng(37.936, -80.425)\n)\nval imageSource = ImageSource(ID_IMAGE_SOURCE, quad, R.drawable.southeast_radar_0)\nval layer = RasterLayer(ID_IMAGE_LAYER, ID_IMAGE_SOURCE)\nmap.setStyle(\n Style.Builder()\n .fromUri(TestStyles.AMERICANA)\n .withSource(imageSource)\n .withLayer(layer)\n) { style: Style? ->\n runnable = RefreshImageRunnable(imageSource, handler)\n runnable?.let {\n handler.postDelayed(it, 100)\n }\n}\n
Updating the image sourceimageSource.setImage(drawables[drawableIndex++]!!)\nif (drawableIndex > 3) {\n drawableIndex = 0\n}\nhandler.postDelayed(this, 1000)\n
"},{"location":"styling/animated-symbol-layer/","title":"Animated SymbolLayer","text":"Note
You can find the full source code of this example in AnimatedSymbolLayerActivity.kt
of the MapLibreAndroidTestApp.
Map data OpenStreetMap. \u00a9 OpenMapTiles.
Notice that there are (red) cars randomly moving around, and a (yellow) taxi that is always heading to the passenger (indicated by the M symbol), which upon arrival hops to a different location again. We will focus on the passanger and the taxi, because the cars randomly moving around follow a similar pattern.
In a real application you would of course retrieve the locations from some sort of external API, but for the purposes of this example a random latitude longtitude pair within bounds of the currently visible screen will do.
Getter method to get a random location on the screenprivate val latLngInBounds: LatLng\n get() {\n val bounds = maplibreMap.projection.visibleRegion.latLngBounds\n val generator = Random()\n\n val randomLat = bounds.latitudeSouth + generator.nextDouble() * (bounds.latitudeNorth - bounds.latitudeSouth)\n val randomLon = bounds.longitudeWest + generator.nextDouble() * (bounds.longitudeEast - bounds.longitudeWest)\n\n return LatLng(randomLat, randomLon)\n }\n
Adding a passenger at a random location (on screen)private fun addPassenger(style: Style) {\n passenger = latLngInBounds\n val featureCollection = FeatureCollection.fromFeatures(\n arrayOf(\n Feature.fromGeometry(\n Point.fromLngLat(\n passenger!!.longitude,\n passenger!!.latitude\n )\n )\n )\n )\n style.addImage(\n PASSENGER,\n ResourcesCompat.getDrawable(resources, R.drawable.icon_burned, theme)!!\n )\n val geoJsonSource = GeoJsonSource(PASSENGER_SOURCE, featureCollection)\n style.addSource(geoJsonSource)\n val symbolLayer = SymbolLayer(PASSENGER_LAYER, PASSENGER_SOURCE)\n symbolLayer.withProperties(\n PropertyFactory.iconImage(PASSENGER),\n PropertyFactory.iconIgnorePlacement(true),\n PropertyFactory.iconAllowOverlap(true)\n )\n style.addLayerBelow(symbolLayer, RANDOM_CAR_LAYER)\n}\n
Adding the taxi on screen is done very similarly.
Adding the taxi with bearingprivate fun addTaxi(style: Style) {\n val latLng = latLngInBounds\n val properties = JsonObject()\n properties.addProperty(PROPERTY_BEARING, Car.getBearing(latLng, passenger))\n val feature = Feature.fromGeometry(\n Point.fromLngLat(\n latLng.longitude,\n latLng.latitude\n ),\n properties\n )\n val featureCollection = FeatureCollection.fromFeatures(arrayOf(feature))\n taxi = Car(feature, passenger, duration)\n style.addImage(\n TAXI,\n (ResourcesCompat.getDrawable(resources, R.drawable.ic_taxi_top, theme) as BitmapDrawable).bitmap\n )\n taxiSource = GeoJsonSource(TAXI_SOURCE, featureCollection)\n style.addSource(taxiSource!!)\n val symbolLayer = SymbolLayer(TAXI_LAYER, TAXI_SOURCE)\n symbolLayer.withProperties(\n PropertyFactory.iconImage(TAXI),\n PropertyFactory.iconRotate(Expression.get(PROPERTY_BEARING)),\n PropertyFactory.iconAllowOverlap(true),\n PropertyFactory.iconIgnorePlacement(true)\n )\n style.addLayer(symbolLayer)\n}\n
For animating the taxi we use a ValueAnimator
.
private fun animateTaxi(style: Style) {\n val valueAnimator = ValueAnimator.ofObject(LatLngEvaluator(), taxi!!.current, taxi!!.next)\n valueAnimator.addUpdateListener(object : AnimatorUpdateListener {\n private var latLng: LatLng? = null\n override fun onAnimationUpdate(animation: ValueAnimator) {\n latLng = animation.animatedValue as LatLng\n taxi!!.current = latLng\n updateTaxiSource()\n }\n })\n valueAnimator.addListener(object : AnimatorListenerAdapter() {\n override fun onAnimationEnd(animation: Animator) {\n super.onAnimationEnd(animation)\n updatePassenger(style)\n animateTaxi(style)\n }\n })\n valueAnimator.addListener(object : AnimatorListenerAdapter() {\n override fun onAnimationStart(animation: Animator) {\n super.onAnimationStart(animation)\n taxi!!.feature.properties()!!\n .addProperty(\"bearing\", Car.getBearing(taxi!!.current, taxi!!.next))\n }\n })\n valueAnimator.duration = (7 * taxi!!.current!!.distanceTo(taxi!!.next!!)).toLong()\n valueAnimator.interpolator = AccelerateDecelerateInterpolator()\n valueAnimator.start()\n animators.add(valueAnimator)\n}\n
"},{"location":"styling/building-layer/","title":"Building Layer","text":"Note
You can find the full source code of this example in BuildingFillExtrusionActivity.kt
of the MapLibreAndroidTestApp.
In this example will show how to add a Fill Extrusion layer to a style.
Map data OpenStreetMap. \u00a9 OpenMapTiles.
We use the OpenFreeMap Bright style which, unlike OpenFreeMap Libery, does not have a fill extrusion layer by default. However, if you inspect this style with Maputnik you will find that the multipolygons in the building
layer (of the openfreemap
source) each have render_min_height
and render_height
properties.
val fillExtrusionLayer = FillExtrusionLayer(\"building-3d\", \"openmaptiles\")\nfillExtrusionLayer.sourceLayer = \"building\"\nfillExtrusionLayer.setFilter(\n Expression.all(\n Expression.has(\"render_height\"),\n Expression.has(\"render_min_height\")\n )\n)\nfillExtrusionLayer.minZoom = 15f\nfillExtrusionLayer.setProperties(\n PropertyFactory.fillExtrusionColor(Color.LTGRAY),\n PropertyFactory.fillExtrusionHeight(Expression.get(\"render_height\")),\n PropertyFactory.fillExtrusionBase(Expression.get(\"render_min_height\")),\n PropertyFactory.fillExtrusionOpacity(0.9f)\n)\nstyle.addLayer(fillExtrusionLayer)\n
Changing the light directionisInitPosition = !isInitPosition\nif (isInitPosition) {\n light!!.position = Position(1.5f, 90f, 80f)\n} else {\n light!!.position = Position(1.15f, 210f, 30f)\n}\n
Changing the light colorisRedColor = !isRedColor\nlight!!.setColor(ColorUtils.colorToRgbaString(if (isRedColor) Color.RED else Color.BLUE))\n
"},{"location":"styling/circle-layer/","title":"Circle Layer (with Clustering)","text":"Note
You can find the full source code of this example in CircleLayerActivity.kt
of the MapLibreAndroidTestApp.
In this example we will add a circle layer for a GeoJSON source. We also show how you can use the cluster property of a GeoJSON source.
Create a GeoJsonSource
instance, pass a unique identifier for the source and the URL where the GeoJSON is available. Next add the source to the style.
try {\n source = GeoJsonSource(SOURCE_ID, URI(URL_BUS_ROUTES))\n} catch (exception: URISyntaxException) {\n Timber.e(exception, \"That's not an url... \")\n}\nstyle.addSource(source!!)\n
Now you can create a CircleLayer
, pass it a unique identifier for the layer and the source identifier of the GeoJSON source just created. You can use a PropertyFactory
to pass circle layer properties. Lastly add the layer to your style.
layer = CircleLayer(LAYER_ID, SOURCE_ID)\nlayer!!.setProperties(\n PropertyFactory.circleColor(Color.parseColor(\"#FF9800\")),\n PropertyFactory.circleRadius(2.0f)\n)\nstyle.addLayer(layer!!)\n
"},{"location":"styling/circle-layer/#clustering","title":"Clustering","text":"Next we will show you how you can use clustering. Create a GeoJsonSource
as before, but with some additional options to enable clustering.
style.addSource(\n GeoJsonSource(\n SOURCE_ID_CLUSTER,\n URI(URL_BUS_ROUTES),\n GeoJsonOptions()\n .withCluster(true)\n .withClusterMaxZoom(14)\n .withClusterRadius(50)\n )\n)\n
When enabling clustering some special attributes will be available to the points in the newly created layer. One is cluster
, which is true if the point indicates a cluster. We want to show a bus stop for points that are not clustered.
val unclustered = SymbolLayer(\"unclustered-points\", SOURCE_ID_CLUSTER)\nunclustered.setProperties(\n PropertyFactory.iconImage(\"bus-icon\"),\n)\nunclustered.setFilter(\n Expression.neq(Expression.get(\"cluster\"), true)\n)\nstyle.addLayer(unclustered)\n
Next we define which point amounts correspond to which colors. More than 150 points will get a red circle, clusters with 21-150 points will be green and clusters with 20 or less points will be green.
Define different colors for different point amountsval layers = arrayOf(\n 150 to ResourcesCompat.getColor(\n resources,\n R.color.redAccent,\n theme\n ),\n 20 to ResourcesCompat.getColor(resources, R.color.greenAccent, theme),\n 0 to ResourcesCompat.getColor(\n resources,\n R.color.blueAccent,\n theme\n )\n)\n
Lastly we iterate over the array of Pair
s to create a CircleLayer
for each element.
for (i in layers.indices) {\n // Add some nice circles\n val circles = CircleLayer(\"cluster-$i\", SOURCE_ID_CLUSTER)\n circles.setProperties(\n PropertyFactory.circleColor(layers[i].second),\n PropertyFactory.circleRadius(18f)\n )\n\n val pointCount = Expression.toNumber(Expression.get(\"point_count\"))\n circles.setFilter(\n if (i == 0) {\n Expression.all(\n Expression.has(\"point_count\"),\n Expression.gte(\n pointCount,\n Expression.literal(layers[i].first)\n )\n )\n } else {\n Expression.all(\n Expression.has(\"point_count\"),\n Expression.gt(\n pointCount,\n Expression.literal(layers[i].first)\n ),\n Expression.lt(\n pointCount,\n Expression.literal(layers[i - 1].first)\n )\n )\n }\n )\n\n style.addLayer(circles)\n}\n
"},{"location":"styling/custom-sprite/","title":"Add Custom Sprite","text":"Note
You can find the full source code of this example in CustomSpriteActivity.kt
of the MapLibreAndroidTestApp.
This example showcases adding a sprite image and using it in a Symbol Layer.
// Add an icon to reference later\nstyle.addImage(\n CUSTOM_ICON,\n BitmapFactory.decodeResource(\n resources,\n R.drawable.ic_car_top\n )\n)\n\n// Add a source with a geojson point\npoint = Point.fromLngLat(13.400972, 52.519003)\nsource = GeoJsonSource(\n \"point\",\n FeatureCollection.fromFeatures(arrayOf(Feature.fromGeometry(point)))\n)\nmaplibreMap.style!!.addSource(source!!)\n\n// Add a symbol layer that references that point source\nlayer = SymbolLayer(\"layer\", \"point\")\nlayer.setProperties( // Set the id of the sprite to use\n PropertyFactory.iconImage(CUSTOM_ICON),\n PropertyFactory.iconAllowOverlap(true),\n PropertyFactory.iconIgnorePlacement(true)\n)\n\n// lets add a circle below labels!\nmaplibreMap.style!!.addLayerBelow(layer, \"water-intermittent\")\nfab.setImageResource(R.drawable.ic_directions_car_black)\n
"},{"location":"styling/data-driven-style/","title":"Data Driven Style","text":"Note
You can find the full source code of this example in DataDrivenStyleActivity.kt
of the MapLibreAndroidTestApp.
In this example we will look at various types of data-driven styling.
The examples with 'Source' in the title apply data-driven styling the parks of Amsterdam. Those examples often are based on the somewhat arbitrary stroke-width
property part of the GeoJSON features. These examples are therefore most interesting to learn about the Kotlin API that can be used for data-driven styling.
Tip
Refer to the MapLibre Style Spec for more information about expressions such as interpolate
and step
.
layer.setProperties(\n PropertyFactory.fillColor(\n Expression.interpolate(\n Expression.exponential(0.5f),\n Expression.zoom(),\n Expression.stop(1, Expression.color(Color.RED)),\n Expression.stop(5, Expression.color(Color.BLUE)),\n Expression.stop(10, Expression.color(Color.GREEN))\n )\n )\n)\n
"},{"location":"styling/data-driven-style/#interval-zoom-function","title":"Interval Zoom Function","text":"layer.setProperties(\n PropertyFactory.fillColor(\n Expression.step(\n Expression.zoom(),\n Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1, Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(5, Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f)),\n Expression.stop(10, Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f))\n )\n )\n)\n
Equivalent JSON[\"step\",[\"zoom\"],[\"rgba\",0.0,255.0,255.0,1.0],1.0,[\"rgba\",255.0,0.0,0.0,1.0],5.0,[\"rgba\",0.0,0.0,255.0,1.0],10.0,[\"rgba\",0.0,255.0,0.0,1.0]]\n
"},{"location":"styling/data-driven-style/#exponential-source-function","title":"Exponential Source Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillColor(\n Expression.interpolate(\n Expression.exponential(0.5f),\n Expression.get(\"stroke-width\"),\n Expression.stop(1f, Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(5f, Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f)),\n Expression.stop(10f, Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f))\n )\n )\n)\n
"},{"location":"styling/data-driven-style/#categorical-source-function","title":"Categorical Source Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillColor(\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.literal(\"Jordaan\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.literal(\"Prinseneiland\"),\n Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f),\n Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f)\n )\n )\n)\n
"},{"location":"styling/data-driven-style/#identity-source-function","title":"Identity Source Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillOpacity(\n Expression.get(\"fill-opacity\")\n )\n)\n
"},{"location":"styling/data-driven-style/#interval-source-function","title":"Interval Source Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillColor(\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f))\n )\n )\n)\n
"},{"location":"styling/data-driven-style/#composite-exponential-function","title":"Composite Exponential Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillColor(\n Expression.interpolate(\n Expression.exponential(1f),\n Expression.zoom(),\n Expression.stop(\n 12,\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(0.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f))\n )\n ),\n Expression.stop(\n 15,\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(255.0f, 255.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(211.0f, 211.0f, 211.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f))\n )\n ),\n Expression.stop(\n 18,\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(0.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(128.0f, 128.0f, 128.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f))\n )\n )\n )\n )\n)\n
"},{"location":"styling/data-driven-style/#identity-source-function_1","title":"Identity Source Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillOpacity(\n Expression.get(\"fill-opacity\")\n )\n)\n
"},{"location":"styling/data-driven-style/#composite-interval-function","title":"Composite Interval Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillColor(\n Expression.interpolate(\n Expression.linear(),\n Expression.zoom(),\n Expression.stop(\n 12,\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(0.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f))\n )\n ),\n Expression.stop(\n 15,\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(255.0f, 255.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(211.0f, 211.0f, 211.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f))\n )\n ),\n Expression.stop(\n 18,\n Expression.step(\n Expression.get(\"stroke-width\"),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(1f, Expression.rgba(0.0f, 0.0f, 0.0f, 1.0f)),\n Expression.stop(2f, Expression.rgba(128.0f, 128.0f, 128.0f, 1.0f)),\n Expression.stop(3f, Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f))\n )\n )\n )\n )\n)\n
"},{"location":"styling/data-driven-style/#composite-categorical-function","title":"Composite Categorical Function","text":"val layer = maplibreMap.style!!.getLayerAs<FillLayer>(AMSTERDAM_PARKS_LAYER)!!\nlayer.setProperties(\n PropertyFactory.fillColor(\n Expression.step(\n Expression.zoom(),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f),\n Expression.stop(\n 7f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 8f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 9f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 10f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 11f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 12f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 13f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 14f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.literal(\"Jordaan\"),\n Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f),\n Expression.literal(\"PrinsenEiland\"),\n Expression.rgba(0.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 15f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 16f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 17f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 18f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.literal(\"Jordaan\"),\n Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 19f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 20f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 21f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n ),\n Expression.stop(\n 22f,\n Expression.match(\n Expression.get(\"name\"),\n Expression.literal(\"Westerpark\"),\n Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f),\n Expression.rgba(255.0f, 255.0f, 255.0f, 1.0f)\n )\n )\n )\n )\n)\n
"},{"location":"styling/distance-expression/","title":"Distance Expression","text":"Note
You can find the full source code of this example in DistanceExpressionActivity.kt
of the MapLibreAndroidTestApp.
This example shows how you can modify a style to only show certain features within a certain distance to a point. For this the distance expression is used.
Map data OpenStreetMap. \u00a9 OpenMapTiles.
First we add a fill layer and a GeoJSON source.
val center = Point.fromLngLat(lon, lat)\nval circle = TurfTransformation.circle(center, 150.0, TurfConstants.UNIT_METRES)\nmaplibreMap.setStyle(\n Style.Builder()\n .fromUri(TestStyles.OPENFREEMAP_BRIGHT)\n .withSources(\n GeoJsonSource(\n POINT_ID,\n Point.fromLngLat(lon, lat)\n ),\n GeoJsonSource(CIRCLE_ID, circle)\n )\n .withLayerBelow(\n FillLayer(CIRCLE_ID, CIRCLE_ID)\n .withProperties(\n fillOpacity(0.5f),\n fillColor(Color.parseColor(\"#3bb2d0\"))\n ),\n \"poi\"\n )\n
Next, we only show features from symbol layers that are less than a certain distance from the point. All symbol layers whose identifier does not start with poi
are completely hidden.
for (layer in style.layers) {\n if (layer is SymbolLayer) {\n if (layer.id.startsWith(\"poi\")) {\n layer.setFilter(lt(\n distance(\n Point.fromLngLat(lon, lat)\n ),\n 150\n ))\n } else {\n layer.setProperties(visibility(NONE))\n }\n }\n}\n
"},{"location":"styling/draggable-marker/","title":"Draggable Marker","text":"Note
You can find the full source code of this example in DraggableMarkerActivity.kt
of the MapLibreAndroidTestApp.
maplibreMap.addOnMapClickListener {\n // Adding a marker on map click\n val features = maplibreMap.queryRenderedSymbols(it, layerId)\n if (features.isEmpty()) {\n addMarker(it)\n } else {\n // Displaying marker info on marker click\n Snackbar.make(\n mapView,\n \"Marker's position: %.4f, %.4f\".format(it.latitude, it.longitude),\n Snackbar.LENGTH_LONG\n )\n .show()\n }\n\n false\n}\n
"},{"location":"styling/draggable-marker/#allowing-markers-to-be-dragged","title":"Allowing markers to be dragged","text":"This is slightly more involved, as we implement it by implementing a DraggableSymbolsManager
helper class.
This class is initialized and we pass a few callbacks when when markers are start or end being dragged.
draggableSymbolsManager = DraggableSymbolsManager(\n mapView,\n maplibreMap,\n featureCollection,\n source,\n layerId,\n actionBarHeight,\n 0\n)\n\n// Adding symbol drag listeners\ndraggableSymbolsManager?.addOnSymbolDragListener(object : DraggableSymbolsManager.OnSymbolDragListener {\n override fun onSymbolDragStarted(id: String) {\n binding.draggedMarkerPositionTv.visibility = View.VISIBLE\n Snackbar.make(\n mapView,\n \"Marker drag started (%s)\".format(id),\n Snackbar.LENGTH_SHORT\n )\n .show()\n }\n\n override fun onSymbolDrag(id: String) {\n val point = featureCollection.features()?.find {\n it.id() == id\n }?.geometry() as Point\n binding.draggedMarkerPositionTv.text = \"Dragged marker's position: %.4f, %.4f\".format(point.latitude(), point.longitude())\n }\n\n override fun onSymbolDragFinished(id: String) {\n binding.draggedMarkerPositionTv.visibility = View.GONE\n Snackbar.make(\n mapView,\n \"Marker drag finished (%s)\".format(id),\n Snackbar.LENGTH_SHORT\n )\n .show()\n }\n})\n
The implementation of DraggableSymbolsManager
follows. In its initializer we define a handler for when a user long taps on a marker. This then starts dragging that marker. It does this by temporarily suspending all other gestures.
We create a custom implementation of MoveGestureDetector.OnMoveGestureListener
and pass this to an instance of AndroidGesturesManager
linked to the map view.
Tip
See maplibre-gestures-android for the implementation details of the gestures library used by MapLibre Android.
/**\n * A manager, that allows dragging symbols after they are long clicked.\n * Since this manager lives outside of the Maps SDK, we need to intercept parent's motion events\n * and pass them with [DraggableSymbolsManager.onParentTouchEvent].\n * If we were to try and overwrite [AppCompatActivity.onTouchEvent], those events would've been\n * consumed by the map.\n *\n * We also need to setup a [DraggableSymbolsManager.androidGesturesManager],\n * because after disabling map's gestures and starting the drag process\n * we still need to listen for move gesture events which map won't be able to provide anymore.\n *\n * @param mapView the mapView\n * @param maplibreMap the maplibreMap\n * @param symbolsCollection the collection that contains all the symbols that we want to be draggable\n * @param symbolsSource the source that contains the [symbolsCollection]\n * @param symbolsLayerId the ID of the layer that the symbols are displayed on\n * @param touchAreaShiftX X-axis padding that is applied to the parent's window motion event,\n * as that window can be bigger than the [mapView].\n * @param touchAreaShiftY Y-axis padding that is applied to the parent's window motion event,\n * as that window can be bigger than the [mapView].\n * @param touchAreaMaxX maximum value of X-axis motion event\n * @param touchAreaMaxY maximum value of Y-axis motion event\n */\nclass DraggableSymbolsManager(\n mapView: MapView,\n private val maplibreMap: MapLibreMap,\n private val symbolsCollection: FeatureCollection,\n private val symbolsSource: GeoJsonSource,\n private val symbolsLayerId: String,\n private val touchAreaShiftY: Int = 0,\n private val touchAreaShiftX: Int = 0,\n private val touchAreaMaxX: Int = mapView.width,\n private val touchAreaMaxY: Int = mapView.height\n) {\n\n private val androidGesturesManager: AndroidGesturesManager = AndroidGesturesManager(mapView.context, false)\n private var draggedSymbolId: String? = null\n private val onSymbolDragListeners: MutableList<OnSymbolDragListener> = mutableListOf()\n\n init {\n maplibreMap.addOnMapLongClickListener {\n // Starting the drag process on long click\n draggedSymbolId = maplibreMap.queryRenderedSymbols(it, symbolsLayerId).firstOrNull()?.id()?.also { id ->\n maplibreMap.uiSettings.setAllGesturesEnabled(false)\n maplibreMap.gesturesManager.moveGestureDetector.interrupt()\n notifyOnSymbolDragListeners {\n onSymbolDragStarted(id)\n }\n }\n false\n }\n\n androidGesturesManager.setMoveGestureListener(MyMoveGestureListener())\n }\n\n inner class MyMoveGestureListener : MoveGestureDetector.OnMoveGestureListener {\n override fun onMoveBegin(detector: MoveGestureDetector): Boolean {\n return true\n }\n\n override fun onMove(detector: MoveGestureDetector, distanceX: Float, distanceY: Float): Boolean {\n if (detector.pointersCount > 1) {\n // Stopping the drag when we don't work with a simple, on-pointer move anymore\n stopDragging()\n return true\n }\n\n // Updating symbol's position\n draggedSymbolId?.also { draggedSymbolId ->\n val moveObject = detector.getMoveObject(0)\n val point = PointF(moveObject.currentX - touchAreaShiftX, moveObject.currentY - touchAreaShiftY)\n\n if (point.x < 0 || point.y < 0 || point.x > touchAreaMaxX || point.y > touchAreaMaxY) {\n stopDragging()\n }\n\n val latLng = maplibreMap.projection.fromScreenLocation(point)\n\n symbolsCollection.features()?.indexOfFirst {\n it.id() == draggedSymbolId\n }?.also { index ->\n symbolsCollection.features()?.get(index)?.also { oldFeature ->\n val properties = oldFeature.properties()\n val newFeature = Feature.fromGeometry(\n Point.fromLngLat(latLng.longitude, latLng.latitude),\n properties,\n draggedSymbolId\n )\n symbolsCollection.features()?.set(index, newFeature)\n symbolsSource.setGeoJson(symbolsCollection)\n notifyOnSymbolDragListeners {\n onSymbolDrag(draggedSymbolId)\n }\n return true\n }\n }\n }\n\n return false\n }\n\n override fun onMoveEnd(detector: MoveGestureDetector, velocityX: Float, velocityY: Float) {\n // Stopping the drag when move ends\n stopDragging()\n }\n }\n\n private fun stopDragging() {\n maplibreMap.uiSettings.setAllGesturesEnabled(true)\n draggedSymbolId?.let {\n notifyOnSymbolDragListeners {\n onSymbolDragFinished(it)\n }\n }\n draggedSymbolId = null\n }\n\n fun onParentTouchEvent(ev: MotionEvent?) {\n androidGesturesManager.onTouchEvent(ev)\n }\n\n private fun notifyOnSymbolDragListeners(action: OnSymbolDragListener.() -> Unit) {\n onSymbolDragListeners.forEach(action)\n }\n\n fun addOnSymbolDragListener(listener: OnSymbolDragListener) {\n onSymbolDragListeners.add(listener)\n }\n\n fun removeOnSymbolDragListener(listener: OnSymbolDragListener) {\n onSymbolDragListeners.remove(listener)\n }\n\n interface OnSymbolDragListener {\n fun onSymbolDragStarted(id: String)\n fun onSymbolDrag(id: String)\n fun onSymbolDragFinished(id: String)\n }\n}\n
"},{"location":"styling/live-realtime-data/","title":"Add live realtime data","text":"Note
You can find the full source code of this example in RealTimeGeoJsonActivity.kt
of the MapLibreAndroidTestApp.
In this example you will learn how to add a live GeoJSON source. We have set up a lambda function that returns a new GeoJSON point every time it is called.
First we will create a GeoJSONSource
.
try {\n style.addSource(GeoJsonSource(ID_GEOJSON_SOURCE, URI(URL_GEOJSON_SOURCE)))\n} catch (malformedUriException: URISyntaxException) {\n Timber.e(malformedUriException, \"Invalid URL\")\n}\n
Next we will create a SymbolLayer
that uses the source.
val layer = SymbolLayer(ID_GEOJSON_LAYER, ID_GEOJSON_SOURCE)\nlayer.setProperties(\n PropertyFactory.iconImage(\"plane\"),\n PropertyFactory.iconAllowOverlap(true)\n)\nstyle.addLayer(layer)\n
We use define a Runnable
and use android.os.Handler
with a android.os.Looper
to update the GeoJSON source every 2 seconds.
private inner class RefreshGeoJsonRunnable(\n private val maplibreMap: MapLibreMap,\n private val handler: Handler\n) : Runnable {\n override fun run() {\n val geoJsonSource = maplibreMap.style!!.getSource(ID_GEOJSON_SOURCE) as GeoJsonSource\n geoJsonSource.setUri(URL_GEOJSON_SOURCE)\n val features = geoJsonSource.querySourceFeatures(null)\n setIconRotation(features)\n handler.postDelayed(this, 2000)\n }\n}\n
"},{"location":"styling/live-realtime-data/#bonus-set-icon-rotation","title":"Bonus: set icon rotation","text":"You can set the icon rotation of the icon when ever the point is updated based on the last two points.
Defining a Runnable for updating the GeoJSON sourceif (features.size != 1) {\n Timber.e(\"Expected only one feature\")\n return\n}\n\nval feature = features[0]\nval geometry = feature.geometry()\nif (geometry !is Point) {\n Timber.e(\"Expected geometry to be a point\")\n return\n}\n\nif (lastLocation == null) {\n lastLocation = geometry\n return\n}\n\nmaplibreMap.style!!.getLayer(ID_GEOJSON_LAYER)!!.setProperties(\n PropertyFactory.iconRotate(calculateRotationAngle(lastLocation!!, geometry)),\n)\n
"}]}
\ No newline at end of file
diff --git a/android/examples/sitemap.xml b/android/examples/sitemap.xml
index bae9a0ffd2f..6eea748cfb7 100644
--- a/android/examples/sitemap.xml
+++ b/android/examples/sitemap.xml
@@ -2,102 +2,102 @@