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 @@

Bonus: Add Click Listener

- .openmaptiles_caption at 0x7fe58089fb00> + .openmaptiles_caption at 0x7f5d7ca77b00> diff --git a/android/examples/search/search_index.json b/android/examples/search/search_index.json index 37e1e368735..727133594cd 100644 --- a/android/examples/search/search_index.json +++ b/android/examples/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"MapLibre Android Examples","text":"

Welcome to the examples documentation of MapLibre Android.

"},{"location":"#open-source-apps-using-maplibre-android","title":"Open-Source Apps Using MapLibre Android","text":"

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:

"},{"location":"#see-also","title":"See Also","text":""},{"location":"configuration/","title":"Configuration","text":"

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:

  1. Using existing XML namespace tags forMapView in the layout.
  2. Creating MapLibreMapOptions and passing builder function values into the MapView.
  3. Creating a 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.

MapOptionsXmlActivity.kt
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.

"},{"location":"configuration/#mapview-configuration-with-maplibremapoptions","title":"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:

MapOptionsRuntimeActivity.kt
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.

"},{"location":"configuration/#supportmapfragment-with-the-help-of-maplibremapoptions","title":"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.

"},{"location":"geojson-guide/","title":"Using a GeoJSON Source","text":"

This guide will teach you how to use GeoJsonSource by deep diving into GeoJSON file format.

"},{"location":"geojson-guide/#goals","title":"Goals","text":"

After finishing this documentation you should be able to:

  1. Understand how Style, Layer, and Source interact with each other.
  2. Explore building blocks of GeoJSON data.
  3. Use GeoJSON files in constructing GeoJsonSources.
  4. Update data at runtime.
"},{"location":"geojson-guide/#1-styles-layers-and-data-source","title":"1. Styles, Layers, and Data source","text":"

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:

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.

Loading from local files with assets folder file
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 file
val 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 JSON
fun 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 services
private 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 FeatureCollection
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
Creating Geometry, Feature, and FeatureCollections from scratch
val 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 GeoJsonSources 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":"
  1. 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
  2. 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
  3. Sync your Android project with Gradle files.

  4. 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
  5. 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
  6. Build and run the app. If you run the app successfully, a map will be displayed as seen in the screenshot below.

"},{"location":"location-component/","title":"LocationComponent","text":"

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:

/**\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\")\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.

  1. 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

"},{"location":"snapshotter/","title":"Using the Snapshotter","text":"

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.kt
package 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.kt
package 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
  1. See App resources overview for this and other ways you can provide resources to your app.\u00a0\u21a9

"},{"location":"annotations/add-markers/","title":"Add Markers in Bulk","text":"

This example demonstrates how you can add markers in bulk.

BulkMarkerActivity.kt
package 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:

  1. Marker
  2. Polyline
  3. Polygon

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:

  1. 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

  2. Sync your Android project the with Gradle files.

  3. 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

  4. 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

  5. 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

  6. Now it is time to add markers into the map.

  7. In the addMarkersToMap() method, we define two types of bitmap for the marker icon.
  8. For each feature in the GeoJSON, add a marker with a snippet about earthquake details.
  9. If the magnitude of an earthquake is bigger than 6.0, we use the red icon. Otherwise, we use the blue one.
  10. 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

  11. Here is the final result. For the full contents of JsonApiActivity, please visit source code of our Test App.

"},{"location":"camera/animation-types/","title":"Animation Types","text":"

Note

You can find the full source code of this example in CameraAnimationTypeActivity.kt of the MapLibreAndroidTestApp.

This example showcases the different animation types.

"},{"location":"camera/animation-types/#move","title":"Move","text":"

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
  1. 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

"},{"location":"camera/animator-animation/","title":"Animator Animation","text":"

This example showcases how to use the Animator API to schedule a sequence of map animations.

CameraAnimatorActivity.kt
package 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.

"},{"location":"camera/gesture-detector/#gesture-listeners","title":"Gesture Listeners","text":"

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:

"},{"location":"camera/gesture-detector/#full-example-activity","title":"Full Example Activity","text":"GestureDetectorActivity.kt
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.

.openmaptiles_caption at 0x7fe58089fb00>"},{"location":"camera/move-map-pixels/","title":"Scroll by Method","text":"

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.

"},{"location":"camera/zoom-methods/#zooming-in","title":"Zooming In","text":"
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 source
val 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 source
imageSource.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 screen
private 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 bearing
private 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.

Animate the taxi driving towards the passenger
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.

Setting up the fill extrusion layer
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 direction
isInitPosition = !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 color
isRedColor = !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.

Setting up the GeoJSON source
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.

Create circle layer a small orange circle for each bus stop
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.

Setting up the clustered GeoJSON source
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.

Add a symbol layers 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 amounts
val 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 Pairs to create a CircleLayer for each element.

Add different circle layers for clusters of different point amounts
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.

"},{"location":"styling/data-driven-style/#exponential-zoom-function","title":"Exponential Zoom Function","text":"
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.

"},{"location":"styling/draggable-marker/#adding-a-marker-on-tap","title":"Adding a marker on tap","text":"Adding a tap listener to the map to add a marker on tap
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.

Adding GeoJSON source
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.

Adding a SymbolLayer 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.

Defining a Runnable for updating the GeoJSON source
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 source
if (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.

"},{"location":"#open-source-apps-using-maplibre-android","title":"Open-Source Apps Using MapLibre Android","text":"

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:

"},{"location":"#see-also","title":"See Also","text":""},{"location":"configuration/","title":"Configuration","text":"

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:

  1. Using existing XML namespace tags forMapView in the layout.
  2. Creating MapLibreMapOptions and passing builder function values into the MapView.
  3. Creating a 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.

MapOptionsXmlActivity.kt
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.

"},{"location":"configuration/#mapview-configuration-with-maplibremapoptions","title":"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:

MapOptionsRuntimeActivity.kt
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.

"},{"location":"configuration/#supportmapfragment-with-the-help-of-maplibremapoptions","title":"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.

"},{"location":"geojson-guide/","title":"Using a GeoJSON Source","text":"

This guide will teach you how to use GeoJsonSource by deep diving into GeoJSON file format.

"},{"location":"geojson-guide/#goals","title":"Goals","text":"

After finishing this documentation you should be able to:

  1. Understand how Style, Layer, and Source interact with each other.
  2. Explore building blocks of GeoJSON data.
  3. Use GeoJSON files in constructing GeoJsonSources.
  4. Update data at runtime.
"},{"location":"geojson-guide/#1-styles-layers-and-data-source","title":"1. Styles, Layers, and Data source","text":"

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:

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.

Loading from local files with assets folder file
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 file
val 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 JSON
fun 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 services
private 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 FeatureCollection
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
Creating Geometry, Feature, and FeatureCollections from scratch
val 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 GeoJsonSources 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":"
  1. 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
  2. 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
  3. Sync your Android project with Gradle files.

  4. 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
  5. 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
  6. Build and run the app. If you run the app successfully, a map will be displayed as seen in the screenshot below.

"},{"location":"location-component/","title":"LocationComponent","text":"

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:

/**\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\")\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.

  1. 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

"},{"location":"snapshotter/","title":"Using the Snapshotter","text":"

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.kt
package 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.kt
package 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
  1. See App resources overview for this and other ways you can provide resources to your app.\u00a0\u21a9

"},{"location":"annotations/add-markers/","title":"Add Markers in Bulk","text":"

This example demonstrates how you can add markers in bulk.

BulkMarkerActivity.kt
package 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:

  1. Marker
  2. Polyline
  3. Polygon

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:

  1. 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

  2. Sync your Android project the with Gradle files.

  3. 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

  4. 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

  5. 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

  6. Now it is time to add markers into the map.

  7. In the addMarkersToMap() method, we define two types of bitmap for the marker icon.
  8. For each feature in the GeoJSON, add a marker with a snippet about earthquake details.
  9. If the magnitude of an earthquake is bigger than 6.0, we use the red icon. Otherwise, we use the blue one.
  10. 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

  11. Here is the final result. For the full contents of JsonApiActivity, please visit source code of our Test App.

"},{"location":"camera/animation-types/","title":"Animation Types","text":"

Note

You can find the full source code of this example in CameraAnimationTypeActivity.kt of the MapLibreAndroidTestApp.

This example showcases the different animation types.

"},{"location":"camera/animation-types/#move","title":"Move","text":"

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
  1. 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

"},{"location":"camera/animator-animation/","title":"Animator Animation","text":"

This example showcases how to use the Animator API to schedule a sequence of map animations.

CameraAnimatorActivity.kt
package 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.

"},{"location":"camera/gesture-detector/#gesture-listeners","title":"Gesture Listeners","text":"

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:

"},{"location":"camera/gesture-detector/#full-example-activity","title":"Full Example Activity","text":"GestureDetectorActivity.kt
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.

.openmaptiles_caption at 0x7f5d7ca77b00>"},{"location":"camera/move-map-pixels/","title":"Scroll by Method","text":"

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.

"},{"location":"camera/zoom-methods/#zooming-in","title":"Zooming In","text":"
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 source
val 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 source
imageSource.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 screen
private 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 bearing
private 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.

Animate the taxi driving towards the passenger
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.

Setting up the fill extrusion layer
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 direction
isInitPosition = !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 color
isRedColor = !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.

Setting up the GeoJSON source
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.

Create circle layer a small orange circle for each bus stop
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.

Setting up the clustered GeoJSON source
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.

Add a symbol layers 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 amounts
val 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 Pairs to create a CircleLayer for each element.

Add different circle layers for clusters of different point amounts
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.

"},{"location":"styling/data-driven-style/#exponential-zoom-function","title":"Exponential Zoom Function","text":"
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.

"},{"location":"styling/draggable-marker/#adding-a-marker-on-tap","title":"Adding a marker on tap","text":"Adding a tap listener to the map to add a marker on tap
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.

Adding GeoJSON source
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.

Adding a SymbolLayer 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.

Defining a Runnable for updating the GeoJSON source
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 source
if (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 @@ https://www.maplibre.org/maplibre-native/android/examples/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/configuration/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/geojson-guide/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/getting-started/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/location-component/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/snapshotter/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/annotations/add-markers/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/annotations/marker-annotations/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/camera/animation-types/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/camera/animator-animation/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/camera/cameraposition/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/camera/gesture-detector/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/camera/lat-lng-bounds/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/camera/max-min-zoom/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/camera/move-map-pixels/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/camera/zoom-methods/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/styling/animated-image-source/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/styling/animated-symbol-layer/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/styling/building-layer/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/styling/circle-layer/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/styling/custom-sprite/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/styling/data-driven-style/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/styling/distance-expression/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/styling/draggable-marker/ - 2024-12-18 + 2024-12-20 https://www.maplibre.org/maplibre-native/android/examples/styling/live-realtime-data/ - 2024-12-18 + 2024-12-20 \ No newline at end of file diff --git a/android/examples/sitemap.xml.gz b/android/examples/sitemap.xml.gz index e247497e810..b214482a002 100644 Binary files a/android/examples/sitemap.xml.gz and b/android/examples/sitemap.xml.gz differ