From 34c104b8e222d588d351f7d0f5f558914ee4ab29 Mon Sep 17 00:00:00 2001 From: Bart Louwers Date: Fri, 22 Nov 2024 21:10:40 +0100 Subject: [PATCH] Add more examples to MapLibre Andoid Example Documentation (#3038) --- platform/android/DEVELOPING.md | 12 +- platform/android/Makefile | 2 + .../style/AnimatedImageSourceActivity.kt | 8 +- .../style/AnimatedSymbolLayerActivity.kt | 81 +++--- .../style/BuildingFillExtrusionActivity.kt | 20 +- .../activity/style/CircleLayerActivity.kt | 234 +++++++++--------- .../activity/style/CustomSpriteActivity.kt | 6 +- .../activity/style/DataDrivenStyleActivity.kt | 80 ++++-- .../style/DistanceExpressionActivity.kt | 41 +-- .../activity/style/DraggableMarkerActivity.kt | 13 +- .../activity/style/RealTimeGeoJsonActivity.kt | 75 +++++- .../android/testapp/styles/TestStyles.kt | 6 +- platform/android/docs/Dockerfile | 3 + platform/android/docs/README.md | 57 ++--- platform/android/docs/assets/extra.css | 4 + .../android/docs/camera/animation-types.md | 16 +- .../android/docs/camera/cameraposition.md | 8 +- .../android/docs/camera/lat-lng-bounds.md | 9 +- .../{min-max-zoom.md => max-min-zoom.md} | 11 +- .../android/docs/camera/move-map-pixels.md | 4 +- platform/android/docs/camera/zoom-methods.md | 8 +- platform/android/docs/configuration.md | 6 +- platform/android/docs/index.md | 48 ++++ platform/android/docs/location-component.md | 5 +- platform/android/docs/main.py | 37 +++ platform/android/docs/snapshotter.md | 26 +- .../docs/styling/animated-image-source.md | 22 ++ .../docs/styling/animated-symbol-layer.md | 35 +++ .../android/docs/styling/building-layer.md | 25 ++ platform/android/docs/styling/circle-layer.md | 49 ++++ .../android/docs/styling/custom-sprite.md | 9 + .../android/docs/styling/data-driven-style.md | 88 +++++++ .../docs/styling/distance-expression.md | 22 ++ .../android/docs/styling/draggable-marker.md | 36 +++ .../docs/styling/live-realtime-data.md | 37 +++ platform/android/mkdocs.yaml | 5 + 36 files changed, 833 insertions(+), 315 deletions(-) create mode 100644 platform/android/docs/Dockerfile rename platform/android/docs/camera/{min-max-zoom.md => max-min-zoom.md} (55%) create mode 100644 platform/android/docs/index.md create mode 100644 platform/android/docs/main.py create mode 100644 platform/android/docs/styling/animated-image-source.md create mode 100644 platform/android/docs/styling/animated-symbol-layer.md create mode 100644 platform/android/docs/styling/building-layer.md create mode 100644 platform/android/docs/styling/circle-layer.md create mode 100644 platform/android/docs/styling/custom-sprite.md create mode 100644 platform/android/docs/styling/data-driven-style.md create mode 100644 platform/android/docs/styling/distance-expression.md create mode 100644 platform/android/docs/styling/draggable-marker.md create mode 100644 platform/android/docs/styling/live-realtime-data.md diff --git a/platform/android/DEVELOPING.md b/platform/android/DEVELOPING.md index 9d60a6e6c..e8e19dd3e 100644 --- a/platform/android/DEVELOPING.md +++ b/platform/android/DEVELOPING.md @@ -94,14 +94,8 @@ To run the benchmarks (for Android) include the following line on a PR comment: [maplibre-native/docs/mdbook](https://maplibre.org/maplibre-native/docs/book/) describes how Tracy can be used for profiling. -## Examples Documentation +## Documentation -To build the Examples Documentation you need to have Docker installed. +We use Dokka for the API documentation. -From `platform/android`, run: - -``` -make mkdocs -``` - -Next, visit http://localhost:8000/maplibre-native/android/examples/ \ No newline at end of file +The documentation site with examples uses MkDocs along with Material for MkDocs. For more information on how to work on the examples, see [`docs/README.md`](./docs/REAME.md`). \ No newline at end of file diff --git a/platform/android/Makefile b/platform/android/Makefile index 9da3307c7..c1621751f 100644 --- a/platform/android/Makefile +++ b/platform/android/Makefile @@ -316,8 +316,10 @@ clean: .PHONY: mkdocs mkdocs: + docker build -t squidfunk/mkdocs-material docs docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material .PHONY: mkdocs-build mkdocs-build: + docker build -t squidfunk/mkdocs-material docs docker run --rm -v ${PWD}:/docs squidfunk/mkdocs-material build --strict \ No newline at end of file diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedImageSourceActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedImageSourceActivity.kt index 82ec7d5ae..b68d8bd9b 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedImageSourceActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedImageSourceActivity.kt @@ -24,7 +24,7 @@ import org.maplibre.android.utils.BitmapUtils * with an ImageSource * * - * GL-native equivalent of https://maplibre.org/maplibre-gl-js-docs/example/animate-images/ + * MapLibre Native equivalent of https://maplibre.org/maplibre-gl-js/docs/examples/animate-images/ * */ class AnimatedImageSourceActivity : AppCompatActivity(), OnMapReadyCallback { @@ -40,6 +40,7 @@ class AnimatedImageSourceActivity : AppCompatActivity(), OnMapReadyCallback { } override fun onMapReady(map: MapLibreMap) { + // --8<-- [start:onMapReady] val quad = LatLngQuad( LatLng(46.437, -80.425), LatLng(46.437, -71.516), @@ -50,7 +51,7 @@ class AnimatedImageSourceActivity : AppCompatActivity(), OnMapReadyCallback { val layer = RasterLayer(ID_IMAGE_LAYER, ID_IMAGE_SOURCE) map.setStyle( Style.Builder() - .fromUri(TestStyles.getPredefinedStyleWithFallback("Streets")) + .fromUri(TestStyles.AMERICANA) .withSource(imageSource) .withLayer(layer) ) { style: Style? -> @@ -59,6 +60,7 @@ class AnimatedImageSourceActivity : AppCompatActivity(), OnMapReadyCallback { handler.postDelayed(it, 100) } } + // --8<-- [end:onMapReady] } override fun onStart() { @@ -110,11 +112,13 @@ class AnimatedImageSourceActivity : AppCompatActivity(), OnMapReadyCallback { } override fun run() { + // --8<-- [start:setImage] imageSource.setImage(drawables[drawableIndex++]!!) if (drawableIndex > 3) { drawableIndex = 0 } handler.postDelayed(this, 1000) + // --8<-- [end:setImage] } init { diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedSymbolLayerActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedSymbolLayerActivity.kt index 84a053d23..553ca733b 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedSymbolLayerActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedSymbolLayerActivity.kt @@ -35,7 +35,6 @@ class AnimatedSymbolLayerActivity : AppCompatActivity() { private val random = Random() private lateinit var mapView: MapView private lateinit var maplibreMap: MapLibreMap - private var style: Style? = null private val randomCars: MutableList = ArrayList() private var randomCarSource: GeoJsonSource? = null private var taxi: Car? = null @@ -49,22 +48,21 @@ class AnimatedSymbolLayerActivity : AppCompatActivity() { mapView.onCreate(savedInstanceState) mapView.getMapAsync { maplibreMap = it - it.setStyle(TestStyles.getPredefinedStyleWithFallback("Streets")) { style: Style? -> - this.style = style - setupCars() - animateRandomRoutes() - animateTaxi() + it.setStyle(TestStyles.OPENFREEMAP_LIBERY) { style -> + setupCars(style) + animateRandomRoutes(style) + animateTaxi(style) } } } - private fun setupCars() { - addRandomCars() - addPassenger() - addMainCar() + private fun setupCars(style: Style) { + addRandomCars(style) + addPassenger(style) + addTaxi(style) } - private fun animateRandomRoutes() { + private fun animateRandomRoutes(style: Style) { val longestDrive = longestDrive val random = Random() for (car in randomCars) { @@ -85,7 +83,7 @@ class AnimatedSymbolLayerActivity : AppCompatActivity() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) updateRandomDestinations() - animateRandomRoutes() + animateRandomRoutes(style) } }) } @@ -105,7 +103,8 @@ class AnimatedSymbolLayerActivity : AppCompatActivity() { } } - private fun animateTaxi() { + // --8<-- [start:animateTaxi] + private fun animateTaxi(style: Style) { val valueAnimator = ValueAnimator.ofObject(LatLngEvaluator(), taxi!!.current, taxi!!.next) valueAnimator.addUpdateListener(object : AnimatorUpdateListener { private var latLng: LatLng? = null @@ -118,8 +117,8 @@ class AnimatedSymbolLayerActivity : AppCompatActivity() { valueAnimator.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) - updatePassenger() - animateTaxi() + updatePassenger(style) + animateTaxi(style) } }) valueAnimator.addListener(object : AnimatorListenerAdapter() { @@ -134,15 +133,16 @@ class AnimatedSymbolLayerActivity : AppCompatActivity() { valueAnimator.start() animators.add(valueAnimator) } + // --8<-- [end:animateTaxi] - private fun updatePassenger() { + private fun updatePassenger(style: Style) { passenger = latLngInBounds - updatePassengerSource() + updatePassengerSource(style) taxi!!.setNext(passenger) } - private fun updatePassengerSource() { - val source = style!!.getSourceAs(PASSENGER_SOURCE) + private fun updatePassengerSource(style: Style) { + val source = style.getSourceAs(PASSENGER_SOURCE) val featureCollection = FeatureCollection.fromFeatures( arrayOf( Feature.fromGeometry( @@ -198,7 +198,7 @@ class AnimatedSymbolLayerActivity : AppCompatActivity() { private val duration: Long get() = (random.nextInt(DURATION_RANDOM_MAX) + DURATION_BASE).toLong() - private fun addRandomCars() { + private fun addRandomCars(style: Style) { var latLng: LatLng var next: LatLng for (i in 0..9) { @@ -216,12 +216,13 @@ class AnimatedSymbolLayerActivity : AppCompatActivity() { randomCars.add( Car(feature, next, duration) ) + } randomCarSource = GeoJsonSource(RANDOM_CAR_SOURCE, featuresFromRoutes()) - style!!.addSource(randomCarSource!!) - style!!.addImage( + style.addSource(randomCarSource!!) + style.addImage( RANDOM_CAR_IMAGE_ID, - (ResourcesCompat.getDrawable(resources, R.drawable.ic_car_top, theme) as BitmapDrawable).bitmap + ResourcesCompat.getDrawable(resources, R.drawable.ic_car_top, theme)!! ) val symbolLayer = SymbolLayer(RANDOM_CAR_LAYER, RANDOM_CAR_SOURCE) symbolLayer.withProperties( @@ -230,10 +231,12 @@ class AnimatedSymbolLayerActivity : AppCompatActivity() { PropertyFactory.iconRotate(Expression.get(PROPERTY_BEARING)), PropertyFactory.iconIgnorePlacement(true) ) - style!!.addLayerBelow(symbolLayer, WATERWAY_LAYER_ID) + + style.addLayerBelow(symbolLayer, "label_country_1") } - private fun addPassenger() { + // --8<-- [start:addPassenger] + private fun addPassenger(style: Style) { passenger = latLngInBounds val featureCollection = FeatureCollection.fromFeatures( arrayOf( @@ -245,22 +248,24 @@ class AnimatedSymbolLayerActivity : AppCompatActivity() { ) ) ) - style!!.addImage( + style.addImage( PASSENGER, - (ResourcesCompat.getDrawable(resources, R.drawable.icon_burned, theme) as BitmapDrawable).bitmap + ResourcesCompat.getDrawable(resources, R.drawable.icon_burned, theme)!! ) val geoJsonSource = GeoJsonSource(PASSENGER_SOURCE, featureCollection) - style!!.addSource(geoJsonSource) + style.addSource(geoJsonSource) val symbolLayer = SymbolLayer(PASSENGER_LAYER, PASSENGER_SOURCE) symbolLayer.withProperties( PropertyFactory.iconImage(PASSENGER), PropertyFactory.iconIgnorePlacement(true), PropertyFactory.iconAllowOverlap(true) ) - style!!.addLayerBelow(symbolLayer, RANDOM_CAR_LAYER) + style.addLayerBelow(symbolLayer, RANDOM_CAR_LAYER) } + // --8<-- [end:addPassenger] - private fun addMainCar() { + // --8<-- [start:addTaxi] + private fun addTaxi(style: Style) { val latLng = latLngInBounds val properties = JsonObject() properties.addProperty(PROPERTY_BEARING, Car.getBearing(latLng, passenger)) @@ -273,12 +278,12 @@ class AnimatedSymbolLayerActivity : AppCompatActivity() { ) val featureCollection = FeatureCollection.fromFeatures(arrayOf(feature)) taxi = Car(feature, passenger, duration) - style!!.addImage( + style.addImage( TAXI, (ResourcesCompat.getDrawable(resources, R.drawable.ic_taxi_top, theme) as BitmapDrawable).bitmap ) taxiSource = GeoJsonSource(TAXI_SOURCE, featureCollection) - style!!.addSource(taxiSource!!) + style.addSource(taxiSource!!) val symbolLayer = SymbolLayer(TAXI_LAYER, TAXI_SOURCE) symbolLayer.withProperties( PropertyFactory.iconImage(TAXI), @@ -286,17 +291,23 @@ class AnimatedSymbolLayerActivity : AppCompatActivity() { PropertyFactory.iconAllowOverlap(true), PropertyFactory.iconIgnorePlacement(true) ) - style!!.addLayer(symbolLayer) + style.addLayer(symbolLayer) } + // --8<-- [end:addTaxi] + + // --8<-- [start:latLngInBounds] private val latLngInBounds: LatLng get() { val bounds = maplibreMap.projection.visibleRegion.latLngBounds val generator = Random() - val randomLat = bounds.latitudeSouth + generator.nextDouble() * bounds.latitudeNorth - bounds.latitudeSouth - val randomLon = bounds.longitudeWest + generator.nextDouble() * bounds.longitudeEast - bounds.longitudeWest + + val randomLat = bounds.latitudeSouth + generator.nextDouble() * (bounds.latitudeNorth - bounds.latitudeSouth) + val randomLon = bounds.longitudeWest + generator.nextDouble() * (bounds.longitudeEast - bounds.longitudeWest) + return LatLng(randomLat, randomLon) } + // --8<-- [end:latLngInBounds] override fun onStart() { super.onStart() diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/BuildingFillExtrusionActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/BuildingFillExtrusionActivity.kt index ce0d0eeae..7b8b4f7e2 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/BuildingFillExtrusionActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/BuildingFillExtrusionActivity.kt @@ -41,7 +41,7 @@ class BuildingFillExtrusionActivity : AppCompatActivity() { if (map != null) { maplibreMap = map } - maplibreMap.setStyle(TestStyles.getPredefinedStyleWithFallback("Streets")) { style: Style -> + maplibreMap.setStyle(TestStyles.OPENFREEMAP_BRIGHT) { style: Style -> setupBuildings(style) setupLight() } @@ -50,37 +50,43 @@ class BuildingFillExtrusionActivity : AppCompatActivity() { } private fun setupBuildings(style: Style) { - val fillExtrusionLayer = FillExtrusionLayer("3d-buildings", "composite") + // --8<-- [start:setupBuildings] + val fillExtrusionLayer = FillExtrusionLayer("building-3d", "openmaptiles") fillExtrusionLayer.sourceLayer = "building" fillExtrusionLayer.setFilter( - Expression.eq( - Expression.get("extrude"), - Expression.literal("true") + Expression.all( + Expression.has("render_height"), + Expression.has("render_min_height") ) ) fillExtrusionLayer.minZoom = 15f fillExtrusionLayer.setProperties( PropertyFactory.fillExtrusionColor(Color.LTGRAY), - PropertyFactory.fillExtrusionHeight(Expression.get("height")), - PropertyFactory.fillExtrusionBase(Expression.get("min_height")), + PropertyFactory.fillExtrusionHeight(Expression.get("render_height")), + PropertyFactory.fillExtrusionBase(Expression.get("render_min_height")), PropertyFactory.fillExtrusionOpacity(0.9f) ) style.addLayer(fillExtrusionLayer) + // --8<-- [end:setupBuildings] } private fun setupLight() { light = maplibreMap.style!!.light findViewById(R.id.fabLightPosition).setOnClickListener { v: View? -> + // --8<-- [start:lightPosition] isInitPosition = !isInitPosition if (isInitPosition) { light!!.position = Position(1.5f, 90f, 80f) } else { light!!.position = Position(1.15f, 210f, 30f) } + // --8<-- [end:lightPosition] } findViewById(R.id.fabLightColor).setOnClickListener { v: View? -> + // --8<-- [start:lightColor] isRedColor = !isRedColor light!!.setColor(ColorUtils.colorToRgbaString(if (isRedColor) Color.RED else Color.BLUE)) + // --8<-- [end:lightColor] } } diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CircleLayerActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CircleLayerActivity.kt index 4a21d9aa1..f9bd1cce0 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CircleLayerActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CircleLayerActivity.kt @@ -4,13 +4,12 @@ import android.graphics.Color import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import com.google.android.material.floatingactionbutton.FloatingActionButton import org.maplibre.android.maps.MapView -import org.maplibre.android.maps.MapView.OnDidFinishLoadingStyleListener import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.maps.OnMapReadyCallback import org.maplibre.android.maps.Style import org.maplibre.android.style.expressions.Expression import org.maplibre.android.style.layers.CircleLayer @@ -42,52 +41,59 @@ class CircleLayerActivity : AppCompatActivity(), View.OnClickListener { private var isLoadingStyle = true public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContentView(R.layout.activity_circle_layer) mapView = findViewById(R.id.mapView) mapView.onCreate(savedInstanceState) - mapView.getMapAsync( - OnMapReadyCallback { map: MapLibreMap? -> - if (map != null) { - maplibreMap = map - } - maplibreMap.setStyle(TestStyles.getPredefinedStyleWithFallback("Satellite Hybrid")) - mapView.addOnDidFinishLoadingStyleListener( - OnDidFinishLoadingStyleListener { - val style = maplibreMap.style - addBusStopSource(style) - addBusStopCircleLayer(style) - initFloatingActionButtons() - isLoadingStyle = false - } - ) + mapView.getMapAsync { map: MapLibreMap? -> + if (map != null) { + maplibreMap = map } - ) + maplibreMap.setStyle(TestStyles.PROTOMAPS_WHITE) { + maplibreMap.getStyle { style -> + addBusStopSource(style) + addBusStopCircleLayer(style) + initFloatingActionButtons() + isLoadingStyle = false + + style.addImage( + "bus-icon", + AppCompatResources.getDrawable(this, R.drawable.ic_directions_bus_black)!! + ) + } + } + } } - private fun addBusStopSource(style: Style?) { + private fun addBusStopSource(style: Style) { + // --8<-- [start:addBusStopSource] try { source = GeoJsonSource(SOURCE_ID, URI(URL_BUS_ROUTES)) } catch (exception: URISyntaxException) { Timber.e(exception, "That's not an url... ") } - style!!.addSource(source!!) + style.addSource(source!!) + // --8<-- [end:addBusStopSource] } - private fun addBusStopCircleLayer(style: Style?) { + private fun addBusStopCircleLayer(style: Style) { + // --8<-- [start:addBusStopCircleLayer] layer = CircleLayer(LAYER_ID, SOURCE_ID) layer!!.setProperties( PropertyFactory.circleColor(Color.parseColor("#FF9800")), PropertyFactory.circleRadius(2.0f) ) - style!!.addLayerBelow(layer!!, "water_intermittent") + style.addLayer(layer!!) + // --8<-- [end:addBusStopCircleLayer] + } private fun initFloatingActionButtons() { routeFab = findViewById(R.id.fab_route) - routeFab.setColorFilter(ContextCompat.getColor(this@CircleLayerActivity, R.color.primary)) - routeFab.setOnClickListener(this@CircleLayerActivity) + routeFab.setColorFilter(ContextCompat.getColor(this, R.color.primary)) + routeFab.setOnClickListener(this) styleFab = findViewById(R.id.fab_style) - styleFab.setOnClickListener(this@CircleLayerActivity) + styleFab.setOnClickListener(this) } override fun onClick(view: View) { @@ -113,102 +119,107 @@ class CircleLayerActivity : AppCompatActivity(), View.OnClickListener { } private fun addClusteredSource() { - try { - maplibreMap.style!!.addSource( - GeoJsonSource( - SOURCE_ID_CLUSTER, - URI(URL_BUS_ROUTES), - GeoJsonOptions() - .withCluster(true) - .withClusterMaxZoom(14) - .withClusterRadius(50) + maplibreMap.getStyle { style -> + try { + // --8<-- [start:addClusteredSource] + style.addSource( + GeoJsonSource( + SOURCE_ID_CLUSTER, + URI(URL_BUS_ROUTES), + GeoJsonOptions() + .withCluster(true) + .withClusterMaxZoom(14) + .withClusterRadius(50) + ) ) - ) - } catch (malformedUrlException: URISyntaxException) { - Timber.e(malformedUrlException, "That's not an url... ") - } + // --8<-- [end:addClusteredSource] + } catch (malformedUrlException: URISyntaxException) { + Timber.e(malformedUrlException, "That's not an url... ") + } - // Add unclustered layer - val layers = arrayOf( - intArrayOf( - 150, - ResourcesCompat.getColor( + // --8<-- [start:unclusteredLayer] + val unclustered = SymbolLayer("unclustered-points", SOURCE_ID_CLUSTER) + unclustered.setProperties( + PropertyFactory.iconImage("bus-icon"), + ) + unclustered.setFilter( + Expression.neq(Expression.get("cluster"), true) + ) + style.addLayer(unclustered) + // --8<-- [end:unclusteredLayer] + + // --8<-- [start:clusteredCircleLayers] + val layers = arrayOf( + 150 to ResourcesCompat.getColor( resources, R.color.redAccent, theme - ) - ), - intArrayOf(20, ResourcesCompat.getColor(resources, R.color.greenAccent, theme)), - intArrayOf( - 0, - ResourcesCompat.getColor( + ), + 20 to ResourcesCompat.getColor(resources, R.color.greenAccent, theme), + 0 to ResourcesCompat.getColor( resources, R.color.blueAccent, theme ) ) - ) - val unclustered = SymbolLayer("unclustered-points", SOURCE_ID_CLUSTER) - unclustered.setProperties( - PropertyFactory.iconImage("bus-15") - ) - maplibreMap.style!!.addLayer(unclustered) - for (i in layers.indices) { - // Add some nice circles - val circles = CircleLayer("cluster-$i", SOURCE_ID_CLUSTER) - circles.setProperties( - PropertyFactory.circleColor(layers[i][1]), - PropertyFactory.circleRadius(18f) - ) - val pointCount = Expression.toNumber(Expression.get("point_count")) - circles.setFilter( - if (i == 0) { - Expression.all( - Expression.has("point_count"), - Expression.gte( - pointCount, - Expression.literal( - layers[i][0] - ) - ) + // --8<-- [end:clusteredCircleLayers] - ) - } else { - Expression.all( - Expression.has("point_count"), - Expression.gt( - pointCount, - Expression.literal( - layers[i][0] - ) - ), - Expression.lt( - pointCount, - Expression.literal( - layers[i - 1][0] + // --8<-- [start:clusteredCircleLayersLoop] + for (i in layers.indices) { + // Add some nice circles + val circles = CircleLayer("cluster-$i", SOURCE_ID_CLUSTER) + circles.setProperties( + PropertyFactory.circleColor(layers[i].second), + PropertyFactory.circleRadius(18f) + ) + + val pointCount = Expression.toNumber(Expression.get("point_count")) + circles.setFilter( + if (i == 0) { + Expression.all( + Expression.has("point_count"), + Expression.gte( + pointCount, + Expression.literal(layers[i].first) ) ) - ) - } + } else { + Expression.all( + Expression.has("point_count"), + Expression.gt( + pointCount, + Expression.literal(layers[i].first) + ), + Expression.lt( + pointCount, + Expression.literal(layers[i - 1].first) + ) + ) + } + ) + + style.addLayer(circles) + } + // --8<-- [end:clusteredCircleLayersLoop] + + // --8<-- [start:countLabels] + val count = SymbolLayer("count", SOURCE_ID_CLUSTER) + count.setProperties( + PropertyFactory.textField(Expression.toString(Expression.get("point_count"))), + PropertyFactory.textFont(arrayOf("Noto Sans Medium")), + PropertyFactory.textSize(12f), + PropertyFactory.textColor(Color.WHITE), + PropertyFactory.textIgnorePlacement(true), + PropertyFactory.textAllowOverlap(true) ) - maplibreMap.style!!.addLayer(circles) + style.addLayer(count) + // --8<-- [end:countLabels] } - - // Add the count labels - val count = SymbolLayer("count", SOURCE_ID_CLUSTER) - count.setProperties( - PropertyFactory.textField(Expression.toString(Expression.get("point_count"))), - PropertyFactory.textSize(12f), - PropertyFactory.textColor(Color.WHITE), - PropertyFactory.textIgnorePlacement(true), - PropertyFactory.textAllowOverlap(true) - ) - maplibreMap.style!!.addLayer(count) } private fun removeFabs() { - routeFab!!.visibility = View.GONE - styleFab!!.visibility = View.GONE + routeFab.visibility = View.GONE + styleFab.visibility = View.GONE } private fun changeMapStyle() { @@ -226,13 +237,8 @@ class CircleLayerActivity : AppCompatActivity(), View.OnClickListener { maplibreMap.setStyle(Style.Builder().fromUri(nextStyle)) } - private fun addBusStop() { - maplibreMap.style!!.addLayer(layer!!) - maplibreMap.style!!.addSource(source!!) - } - private val nextStyle: String - private get() { + get() { currentStyleIndex++ if (currentStyleIndex == Data.STYLES.size) { currentStyleIndex = 0 @@ -277,12 +283,8 @@ class CircleLayerActivity : AppCompatActivity(), View.OnClickListener { private object Data { val STYLES = arrayOf( - TestStyles.getPredefinedStyleWithFallback("Streets"), - TestStyles.getPredefinedStyleWithFallback("Outdoor"), - TestStyles.getPredefinedStyleWithFallback("Bright"), - TestStyles.getPredefinedStyleWithFallback("Pastel"), - TestStyles.getPredefinedStyleWithFallback("Satellite Hybrid"), - TestStyles.getPredefinedStyleWithFallback("Satellite Hybrid") + TestStyles.PROTOMAPS_WHITE, + TestStyles.PROTOMAPS_LIGHT ) } @@ -290,7 +292,7 @@ class CircleLayerActivity : AppCompatActivity(), View.OnClickListener { const val SOURCE_ID = "bus_stop" const val SOURCE_ID_CLUSTER = "bus_stop_cluster" const val URL_BUS_ROUTES = - "https://raw.githubusercontent.com/cheeaun/busrouter-sg/master/data/2/bus-stops.geojson" + "https://s3.eu-central-1.amazonaws.com/maplibre-native/android-documentation-resources/bus-stops.geojson" const val LAYER_ID = "stops_layer" } } diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CustomSpriteActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CustomSpriteActivity.kt index 9a89c55c2..0b7e04c76 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CustomSpriteActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CustomSpriteActivity.kt @@ -37,7 +37,7 @@ class CustomSpriteActivity : AppCompatActivity() { mapView.onCreate(savedInstanceState) mapView.getMapAsync { maplibreMap = it - it.setStyle(TestStyles.getPredefinedStyleWithFallback("Streets")) { style: Style -> + it.setStyle(TestStyles.OPENFREEMAP_LIBERY) { style: Style -> val fab = findViewById(R.id.fab) fab.setColorFilter( ContextCompat.getColor( @@ -50,6 +50,7 @@ class CustomSpriteActivity : AppCompatActivity() { override fun onClick(view: View) { if (!this::point.isInitialized) { Timber.i("First click -> Car") + // --8<-- [start:addImage] // Add an icon to reference later style.addImage( CUSTOM_ICON, @@ -76,8 +77,9 @@ class CustomSpriteActivity : AppCompatActivity() { ) // lets add a circle below labels! - maplibreMap.style!!.addLayerBelow(layer, "water_intermittent") + maplibreMap.style!!.addLayerBelow(layer, "water-intermittent") fab.setImageResource(R.drawable.ic_directions_car_black) + // --8<-- [end:addImage] } else { // Update point point = Point.fromLngLat( diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt index feba5ae95..0c34f1f50 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt @@ -42,7 +42,7 @@ class DataDrivenStyleActivity : AppCompatActivity() { mapView.getMapAsync { // Store for later maplibreMap = it - it.setStyle(TestStyles.getPredefinedStyleWithFallback("Streets")) { style: Style? -> + it.setStyle(TestStyles.VERSATILES) { style: Style? -> // Add a parks layer addParksLayer() @@ -155,42 +155,56 @@ class DataDrivenStyleActivity : AppCompatActivity() { } } + private fun addExponentialZoomFunction() { Timber.i("Add exponential zoom function") - val layer = maplibreMap.style!!.getLayerAs("water")!! - layer.setProperties( - PropertyFactory.fillColor( - Expression.interpolate( - Expression.exponential(0.5f), - Expression.zoom(), - Expression.stop(1, Expression.color(Color.RED)), - Expression.stop(5, Expression.color(Color.BLUE)), - Expression.stop(10, Expression.color(Color.GREEN)) - ) - ) - ) - Timber.i("Fill color: %s", layer.fillColor) + maplibreMap.getStyle { style -> + style.layers.filter { it.id.startsWith("water") }.filterIsInstance() + .forEach { layer -> + // --8<-- [start:addExponentialZoomFunction] + layer.setProperties( + PropertyFactory.fillColor( + Expression.interpolate( + Expression.exponential(0.5f), + Expression.zoom(), + Expression.stop(1, Expression.color(Color.RED)), + Expression.stop(5, Expression.color(Color.BLUE)), + Expression.stop(10, Expression.color(Color.GREEN)) + ) + ) + ) + // --8<-- [end:addExponentialZoomFunction] + Timber.i("Fill color: %s", layer.fillColor) + } + } } private fun addIntervalZoomFunction() { Timber.i("Add interval zoom function") - val layer = maplibreMap.style!!.getLayerAs("water")!! - layer.setProperties( - PropertyFactory.fillColor( - Expression.step( - Expression.zoom(), - Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f), - Expression.stop(1, Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f)), - Expression.stop(5, Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f)), - Expression.stop(10, Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f)) + maplibreMap.getStyle { style -> + style.layers.filter { it.id.startsWith("water") }.filterIsInstance().forEach { layer -> + // --8<-- [start:addIntervalZoomFunction] + layer.setProperties( + PropertyFactory.fillColor( + Expression.step( + Expression.zoom(), + Expression.rgba(0.0f, 255.0f, 255.0f, 1.0f), + Expression.stop(1, Expression.rgba(255.0f, 0.0f, 0.0f, 1.0f)), + Expression.stop(5, Expression.rgba(0.0f, 0.0f, 255.0f, 1.0f)), + Expression.stop(10, Expression.rgba(0.0f, 255.0f, 0.0f, 1.0f)) + ) + ) ) - ) - ) - Timber.i("Fill color: %s", layer.fillColor) + // --8<-- [end:addIntervalZoomFunction] + + Timber.i("Fill color: %s", layer.fillColor) + } + } } private fun addExponentialSourceFunction() { Timber.i("Add exponential source function") + // --8<-- [start:addExponentialSourceFunction] val layer = maplibreMap.style!!.getLayerAs(AMSTERDAM_PARKS_LAYER)!! layer.setProperties( PropertyFactory.fillColor( @@ -203,11 +217,13 @@ class DataDrivenStyleActivity : AppCompatActivity() { ) ) ) + // --8<-- [end:addExponentialSourceFunction] Timber.i("Fill color: %s", layer.fillColor) } private fun addCategoricalSourceFunction() { Timber.i("Add categorical source function") + // --8<-- [start:addCategoricalSourceFunction] val layer = maplibreMap.style!!.getLayerAs(AMSTERDAM_PARKS_LAYER)!! layer.setProperties( PropertyFactory.fillColor( @@ -223,22 +239,27 @@ class DataDrivenStyleActivity : AppCompatActivity() { ) ) ) + // --8<-- [end:addCategoricalSourceFunction] + Timber.i("Fill color: %s", layer.fillColor) } private fun addIdentitySourceFunction() { Timber.i("Add identity source function") + // --8<-- [start:addIdentitySourceFunction] val layer = maplibreMap.style!!.getLayerAs(AMSTERDAM_PARKS_LAYER)!! layer.setProperties( PropertyFactory.fillOpacity( Expression.get("fill-opacity") ) ) + // --8<-- [end:addIdentitySourceFunction] Timber.i("Fill opacity: %s", layer.fillOpacity) } private fun addIntervalSourceFunction() { Timber.i("Add interval source function") + // --8<-- [start:addIntervalSourceFunction] val layer = maplibreMap.style!!.getLayerAs(AMSTERDAM_PARKS_LAYER)!! layer.setProperties( PropertyFactory.fillColor( @@ -251,11 +272,13 @@ class DataDrivenStyleActivity : AppCompatActivity() { ) ) ) + // --8<-- [end:addIntervalSourceFunction] Timber.i("Fill color: %s", layer.fillColor) } private fun addCompositeExponentialFunction() { Timber.i("Add composite exponential function") + // --8<-- [start:addCompositeExponentialFunction] val layer = maplibreMap.style!!.getLayerAs(AMSTERDAM_PARKS_LAYER)!! layer.setProperties( PropertyFactory.fillColor( @@ -295,11 +318,13 @@ class DataDrivenStyleActivity : AppCompatActivity() { ) ) ) + // --8<-- [end:addCompositeExponentialFunction] Timber.i("Fill color: %s", layer.fillColor) } private fun addCompositeIntervalFunction() { Timber.i("Add composite interval function") + // --8<-- [start:addCompositeIntervalFunction] val layer = maplibreMap.style!!.getLayerAs(AMSTERDAM_PARKS_LAYER)!! layer.setProperties( PropertyFactory.fillColor( @@ -339,11 +364,13 @@ class DataDrivenStyleActivity : AppCompatActivity() { ) ) ) + // --8<-- [end:addCompositeIntervalFunction] Timber.i("Fill color: %s", layer.fillColor) } private fun addCompositeCategoricalFunction() { Timber.i("Add composite categorical function") + // --8<-- [start:addCompositeCategoricalFunction] val layer = maplibreMap.style!!.getLayerAs(AMSTERDAM_PARKS_LAYER)!! layer.setProperties( PropertyFactory.fillColor( @@ -503,6 +530,7 @@ class DataDrivenStyleActivity : AppCompatActivity() { ) ) ) + // --8<-- [end:addCompositeCategoricalFunction] Timber.i("Fill color: %s", layer.fillColor) } diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DistanceExpressionActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DistanceExpressionActivity.kt index df50d3a86..e43978d66 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DistanceExpressionActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DistanceExpressionActivity.kt @@ -51,13 +51,12 @@ class DistanceExpressionActivity : AppCompatActivity() { } private fun setupStyle() { + // --8<-- [start:FillLayer] val center = Point.fromLngLat(lon, lat) val circle = TurfTransformation.circle(center, 150.0, TurfConstants.UNIT_METRES) - // Setup style with additional layers, - // using Streets as a base style maplibreMap.setStyle( Style.Builder() - .fromUri(TestStyles.getPredefinedStyleWithFallback("Streets")) + .fromUri(TestStyles.OPENFREEMAP_BRIGHT) .withSources( GeoJsonSource( POINT_ID, @@ -71,24 +70,26 @@ class DistanceExpressionActivity : AppCompatActivity() { fillOpacity(0.5f), fillColor(Color.parseColor("#3bb2d0")) ), - "poi-label" + "poi" ) + // --8<-- [end:FillLayer] ) { style -> - // Show only POI labels inside circle radius using distance expression - val symbolLayer = style.getLayer("poi_z16") as SymbolLayer - symbolLayer.setFilter( - lt( - distance( - Point.fromLngLat(lon, lat) - ), - 150 - ) - ) - - // Hide other types of labels to highlight POI labels - (style.getLayer("road_label") as SymbolLayer?)?.setProperties(visibility(NONE)) - (style.getLayer("airport-label-major") as SymbolLayer?)?.setProperties(visibility(NONE)) - (style.getLayer("poi_transit") as SymbolLayer?)?.setProperties(visibility(NONE)) + // --8<-- [start:distanceExpression] + for (layer in style.layers) { + if (layer is SymbolLayer) { + if (layer.id.startsWith("poi")) { + layer.setFilter(lt( + distance( + Point.fromLngLat(lon, lat) + ), + 150 + )) + } else { + layer.setProperties(visibility(NONE)) + } + } + } + // --8<-- [end:distanceExpression] } } @@ -124,7 +125,7 @@ class DistanceExpressionActivity : AppCompatActivity() { override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) { super.onSaveInstanceState(outState, outPersistentState) - outState?.let { + outState.let { binding.mapView.onSaveInstanceState(it) } } diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DraggableMarkerActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DraggableMarkerActivity.kt index de24495f3..902fab2dd 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DraggableMarkerActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DraggableMarkerActivity.kt @@ -72,7 +72,7 @@ class DraggableMarkerActivity : AppCompatActivity() { maplibreMap.setStyle( Style.Builder() - .fromUri(TestStyles.getPredefinedStyleWithFallback("Streets")) + .fromUri(TestStyles.PROTOMAPS_LIGHT) .withImage(markerImageId, IconFactory.getInstance(this).defaultMarker().bitmap) .withSource(source) .withLayer(layer) @@ -91,6 +91,7 @@ class DraggableMarkerActivity : AppCompatActivity() { ) ) + // --8<-- [start:addOnMapClickListener] maplibreMap.addOnMapClickListener { // Adding a marker on map click val features = maplibreMap.queryRenderedSymbols(it, layerId) @@ -108,7 +109,9 @@ class DraggableMarkerActivity : AppCompatActivity() { false } + // --8<-- [end:addOnMapClickListener] + // --8<-- [start:draggableSymbolsManager] draggableSymbolsManager = DraggableSymbolsManager( mapView, maplibreMap, @@ -148,22 +151,25 @@ class DraggableMarkerActivity : AppCompatActivity() { .show() } }) + // --8<-- [end:draggableSymbolsManager] } } + // --8<-- [start:addMarker] private fun addMarker(latLng: LatLng) { featureCollection.features()?.add( Feature.fromGeometry(Point.fromLngLat(latLng.longitude, latLng.latitude), null, generateMarkerId()) ) source.setGeoJson(featureCollection) } + // --8<-- [end:addMarker] override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { // Dispatching parent's touch events to the manager draggableSymbolsManager?.onParentTouchEvent(ev) return super.dispatchTouchEvent(ev) } - + // --8<-- [start:DraggableSymbolsManager] /** * A manager, that allows dragging symbols after they are long clicked. * Since this manager lives outside of the Maps SDK, we need to intercept parent's motion events @@ -201,7 +207,7 @@ class DraggableMarkerActivity : AppCompatActivity() { private val androidGesturesManager: AndroidGesturesManager = AndroidGesturesManager(mapView.context, false) private var draggedSymbolId: String? = null - private val onSymbolDragListeners: MutableList = mutableListOf() + private val onSymbolDragListeners: MutableList = mutableListOf() init { maplibreMap.addOnMapLongClickListener { @@ -303,6 +309,7 @@ class DraggableMarkerActivity : AppCompatActivity() { fun onSymbolDragFinished(id: String) } } + // --8<-- [end:DraggableSymbolsManager] override fun onStart() { super.onStart() diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/RealTimeGeoJsonActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/RealTimeGeoJsonActivity.kt index b6804303a..a8550fc23 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/RealTimeGeoJsonActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/RealTimeGeoJsonActivity.kt @@ -4,15 +4,32 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.view.View +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import org.maplibre.android.camera.CameraPosition +import org.maplibre.android.geometry.LatLng import org.maplibre.android.maps.* import org.maplibre.android.style.layers.* import org.maplibre.android.style.sources.GeoJsonSource import org.maplibre.android.testapp.R import org.maplibre.android.testapp.styles.TestStyles +import org.maplibre.geojson.Feature +import org.maplibre.geojson.Point import timber.log.Timber import java.net.URI import java.net.URISyntaxException +import kotlin.math.atan2 + +fun calculateRotationAngle(from: Point, to: Point): Float { + val longitudeDiff = to.longitude() - from.longitude() + val latitudeDiff = to.latitude() - from.latitude() + + val angleRadians = atan2(longitudeDiff, latitudeDiff) + val angleDegrees = Math.toDegrees(angleRadians) + + return ((angleDegrees + 360) % 360).toFloat() // Normalize to 0-360 degrees +} /** * Test activity showcasing using realtime GeoJSON to move a symbol on your map @@ -26,8 +43,16 @@ class RealTimeGeoJsonActivity : AppCompatActivity(), OnMapReadyCallback { private lateinit var maplibreMap: MapLibreMap private var handler: Handler? = null private var runnable: Runnable? = null + + private var lastLocation: Point? = null + + private val netherlands = LatLng(52.646396, 5.723877) + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) + + supportActionBar?.hide() // Hides the action bar if present setContentView(R.layout.activity_default) mapView = findViewById(R.id.mapView) as MapView mapView.onCreate(savedInstanceState) @@ -36,21 +61,31 @@ class RealTimeGeoJsonActivity : AppCompatActivity(), OnMapReadyCallback { override fun onMapReady(map: MapLibreMap) { maplibreMap = map - maplibreMap.setStyle(TestStyles.getPredefinedStyleWithFallback("Streets")) { style -> // add source + maplibreMap.cameraPosition = CameraPosition.Builder().target(netherlands).zoom(6.0).build() + maplibreMap.setStyle(TestStyles.PROTOMAPS_WHITE) { style -> // add source + ResourcesCompat.getDrawable(resources, R.drawable.ic_airplanemode_active_black, theme) + ?.let { style.addImage("plane", it) } + // --8<-- [start:addSource] try { style.addSource(GeoJsonSource(ID_GEOJSON_SOURCE, URI(URL_GEOJSON_SOURCE))) } catch (malformedUriException: URISyntaxException) { Timber.e(malformedUriException, "Invalid URL") } + // --8<-- [end:addSource] - // add layer + // --8<-- [start:addLayer] val layer = SymbolLayer(ID_GEOJSON_LAYER, ID_GEOJSON_SOURCE) - layer.setProperties(PropertyFactory.iconImage("rocket_15")) + layer.setProperties( + PropertyFactory.iconImage("plane"), + PropertyFactory.iconAllowOverlap(true) + ) style.addLayer(layer) + // --8<-- [end:addLayer] + // loop refresh geojson handler = Handler(Looper.getMainLooper()) - runnable = RefreshGeoJsonRunnable(maplibreMap!!, handler!!) + runnable = RefreshGeoJsonRunnable(maplibreMap, handler!!) runnable?.let { handler!!.postDelayed(it, 2000) } @@ -90,16 +125,46 @@ class RealTimeGeoJsonActivity : AppCompatActivity(), OnMapReadyCallback { mapView.onSaveInstanceState(outState) } - private class RefreshGeoJsonRunnable( + private fun setIconRotation(features: List) { + // --8<-- [start:setIconRotation] + if (features.size != 1) { + Timber.e("Expected only one feature") + return + } + + val feature = features[0] + val geometry = feature.geometry() + if (geometry !is Point) { + Timber.e("Expected geometry to be a point") + return + } + + if (lastLocation == null) { + lastLocation = geometry + return + } + + maplibreMap.style!!.getLayer(ID_GEOJSON_LAYER)!!.setProperties( + PropertyFactory.iconRotate(calculateRotationAngle(lastLocation!!, geometry)), + ) + // --8<-- [end:setIconRotation] + lastLocation = geometry + } + + // --8<-- [start:Runnable] + private inner class RefreshGeoJsonRunnable( private val maplibreMap: MapLibreMap, private val handler: Handler ) : Runnable { override fun run() { val geoJsonSource = maplibreMap.style!!.getSource(ID_GEOJSON_SOURCE) as GeoJsonSource geoJsonSource.setUri(URL_GEOJSON_SOURCE) + val features = geoJsonSource.querySourceFeatures(null) + setIconRotation(features) handler.postDelayed(this, 2000) } } + // --8<-- [end:Runnable] companion object { private const val ID_GEOJSON_LAYER = "border" diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/styles/TestStyles.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/styles/TestStyles.kt index eb8a50d9e..3e3435c73 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/styles/TestStyles.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/styles/TestStyles.kt @@ -11,6 +11,8 @@ object TestStyles { const val OPENFREEMAP_LIBERY = "https://tiles.openfreemap.org/styles/liberty" + const val OPENFREEMAP_BRIGHT = "https://tiles.openfreemap.org/styles/bright" + const val AWS_OPEN_DATA_STANDARD_LIGHT = "https://maps.geo.us-east-2.amazonaws.com/maps/v0/maps/OpenDataStyle/style-descriptor?key=v1.public.eyJqdGkiOiI1NjY5ZTU4My0yNWQwLTQ5MjctODhkMS03OGUxOTY4Y2RhMzgifR_7GLT66TNRXhZJ4KyJ-GK1TPYD9DaWuc5o6YyVmlikVwMaLvEs_iqkCIydspe_vjmgUVsIQstkGoInXV_nd5CcmqRMMa-_wb66SxDdbeRDvmmkpy2Ow_LX9GJDgL2bbiCws0wupJPFDwWCWFLwpK9ICmzGvNcrPbX5uczOQL0N8V9iUvziA52a1WWkZucIf6MUViFRf3XoFkyAT15Ll0NDypAzY63Bnj8_zS8bOaCvJaQqcXM9lrbTusy8Ftq8cEbbK5aMFapXRjug7qcrzUiQ5sr0g23qdMvnKJQFfo7JuQn8vwAksxrQm6A0ByceEXSfyaBoVpFcTzEclxUomhY.NjAyMWJkZWUtMGMyOS00NmRkLThjZTMtODEyOTkzZTUyMTBi" private fun protomaps(style: String): String { @@ -23,7 +25,9 @@ object TestStyles { val PROTOMAPS_GRAYSCALE = protomaps("grayscale") val PROTOMAPS_WHITE = protomaps("white") - + + val PROTOMAPS_BLACK = protomaps("black") + fun getPredefinedStyleWithFallback(name: String): String { try { val style = Style.getPredefinedStyle(name) diff --git a/platform/android/docs/Dockerfile b/platform/android/docs/Dockerfile new file mode 100644 index 000000000..0219ccace --- /dev/null +++ b/platform/android/docs/Dockerfile @@ -0,0 +1,3 @@ +FROM squidfunk/mkdocs-material + +RUN pip install mkdocs-macros-plugin diff --git a/platform/android/docs/README.md b/platform/android/docs/README.md index ee549ca44..9ca8049a6 100644 --- a/platform/android/docs/README.md +++ b/platform/android/docs/README.md @@ -1,48 +1,39 @@ # MapLibre Android Examples -Welcome to the examples documentation of MapLibre Android. +To build the Examples Documentation you need to have Docker installed. -
+From `platform/android`, run: -- :material-clock-fast:{ .lg .middle } __Quickstart__ +``` +make mkdocs +``` - --- +Next, visit http://localhost:8000/maplibre-native/android/examples/ - Learn how to include MapLibre Android in your project +## Snippets - [:octicons-arrow-right-24: Getting started](getting-started.md) +We use [a Markdown extension for snippets](https://facelessuser.github.io/pymdown-extensions/extensions/snippets/#snippet-sections). This way code can be referenced instead of copy pasted into the documentation. This avoids code examples from becoming out of date or failing to compile. The syntax is as follows: -- :fontawesome-brands-slack:{ .lg .middle } __Find us on Slack__ +````kotlin +// --8<-- [start:fun] +fun double(x: Int): Int { + return 2 * x +} +// --8<-- [end:fun] +```` - --- +Next, you'll be able to reference that piece of code in Markdown like so: - Discuss the project and ask questions in the `#maplibre-android` channel +``` +--8<-- "example.kt:fun" +``` - [:octicons-arrow-right-24: Slack](https://osmus.slack.com/archives/C01G3D28DAB) +Where `example.kt` is the path to the file. -- :fontawesome-solid-code:{ .lg .middle } __Contribute to these Docs__ +## Static Assets - --- +Static assets are ideally uploaded to the [MapLibre Native S3 Bucket](https://maplibre-native.s3.eu-central-1.amazonaws.com/index.html#android-documentation-resources/). - Share your own examples with the community! +Please open an issue with the ARN of your AWS account to get upload privileges. - [:octicons-arrow-right-24: Documentation on GitHub](https://github.com/maplibre/maplibre-native/tree/main/platform/android/docs) - -
- - -## Open-Source Apps Using MapLibre Android - -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: - -- [Streetcomplete](https://github.com/streetcomplete/StreetComplete) ([source code](https://github.com/search?q=repo%3Astreetcomplete%2FStreetComplete%20maplibre&type=code)) -- [The official Wikipedia app for Android](https://github.com/wikimedia/apps-android-wikipedia) ([source code](https://github.com/search?q=repo%3Awikimedia%2Fapps-android-wikipedia%20maplibre&type=code)). -- [MapLibreAndroidTestApp](https://github.com/maplibre/maplibre-native/tree/main/platform/android/MapLibreAndroidTestApp). This app is part of the MapLibre Native repository and is used for (automated) testing. Many of the examples in this documentation site come directly from this app. - -## See Also - -- [MapLibre Android API Documentation](https://maplibre.org/maplibre-native/android/api/) -- [Source code on GitHub](https://github.com/maplibre/maplibre-native/tree/main/platform/android) -- [Latest releases](https://github.com/maplibre/maplibre-native/releases?q=android-v11&expanded=true) -- [GitHub Discussions](https://github.com/maplibre/maplibre-native/discussions/categories/q-a?discussions_q=is%3Aopen+category%3AQ%26A+label%3Aandroid) -- [MapLibre on Slack](https://slack.openstreetmap.us). Join the `#maplibre-native` and `#maplibre-android` channels. \ No newline at end of file +Use the CDN URL `https://dwxvn1oqw6mkc.cloudfront.net/` instead of linking to the S3 bucket directly. \ No newline at end of file diff --git a/platform/android/docs/assets/extra.css b/platform/android/docs/assets/extra.css index e3654f0f5..ed9769cce 100644 --- a/platform/android/docs/assets/extra.css +++ b/platform/android/docs/assets/extra.css @@ -20,4 +20,8 @@ .md-copyright__highlight { color: inherit; +} + +.inline-attribution { + font-size: smaller; } \ No newline at end of file diff --git a/platform/android/docs/camera/animation-types.md b/platform/android/docs/camera/animation-types.md index d09781578..24922a85e 100644 --- a/platform/android/docs/camera/animation-types.md +++ b/platform/android/docs/camera/animation-types.md @@ -1,8 +1,6 @@ # Animation Types -!!! note - - You can find the full source code of this example in [`CameraAnimationTypeActivity.kt`](https://github.com/maplibre/maplibre-native/blob/main/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/activity/camera/CameraAnimationTypeActivity.kt) of the MapLibreAndroidTestApp. +{{ activity_source_note("CameraAnimationTypeActivity.kt") }} This example showcases the different animation types. @@ -19,8 +17,8 @@ The `MapLibreMap.moveCamera` method jumps to the camera position provided. ```
-
@@ -33,8 +31,8 @@ The `MapLibreMap.moveCamera` eases to the camera position provided (with constan ```
-
@@ -50,8 +48,8 @@ The `MapLibreMap.animateCamera` uses a powered flight animation move to the came ```
-
diff --git a/platform/android/docs/camera/cameraposition.md b/platform/android/docs/camera/cameraposition.md index ab3525fad..d8b58fa3b 100644 --- a/platform/android/docs/camera/cameraposition.md +++ b/platform/android/docs/camera/cameraposition.md @@ -1,14 +1,12 @@ # CameraPosition Capabilities -!!! note - - You can find the full source code of this example in [`CameraPositionActivity.kt`](https://github.com/maplibre/maplibre-native/blob/main/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/camera/CameraPositionActivity.kt) of the MapLibreAndroidTestApp. +{{ activity_source_note("CameraPositionActivity.kt") }} This example showcases how to listen to camera change events.
-
diff --git a/platform/android/docs/camera/lat-lng-bounds.md b/platform/android/docs/camera/lat-lng-bounds.md index bfbba0566..3b6e99d0d 100644 --- a/platform/android/docs/camera/lat-lng-bounds.md +++ b/platform/android/docs/camera/lat-lng-bounds.md @@ -1,15 +1,12 @@ # LatLngBounds API - -!!! note - - You can find the full source code of this example in [`LatLngBoundsActivity.kt`](https://github.com/maplibre/maplibre-native/blob/main/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/camera/LatLngBoundsActivity.kt) of the MapLibreAndroidTestApp. +{{ activity_source_note("LatLngBoundsActivity.kt") }} 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](https://m2.material.io/components/sheets-bottom) is opened or closed.
-
diff --git a/platform/android/docs/camera/min-max-zoom.md b/platform/android/docs/camera/max-min-zoom.md similarity index 55% rename from platform/android/docs/camera/min-max-zoom.md rename to platform/android/docs/camera/max-min-zoom.md index f7122b997..6c3297785 100644 --- a/platform/android/docs/camera/min-max-zoom.md +++ b/platform/android/docs/camera/max-min-zoom.md @@ -1,8 +1,6 @@ -# Min/Max Zoom +# Max/Min Zoom -!!! note - - You can find the full source code of this example in [`MaxMinZoomActivity.kt`](https://github.com/maplibre/maplibre-native/blob/main/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/camera/MaxMinZoomActivity.kt) of the MapLibreAndroidTestApp. +{{ activity_source_note("MaxMinZoomActivity.kt") }} This example shows how to configure a maximum and a minimum zoom level. @@ -21,8 +19,9 @@ As a bonus, this example also shows how you can define a click listener to the m 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.
-
diff --git a/platform/android/docs/camera/move-map-pixels.md b/platform/android/docs/camera/move-map-pixels.md index 0d2c746cb..7b9cd3035 100644 --- a/platform/android/docs/camera/move-map-pixels.md +++ b/platform/android/docs/camera/move-map-pixels.md @@ -1,8 +1,6 @@ # Scroll by Method -!!! note - - You can find the full source code of this example in [`ScrollByActivity.kt`](https://github.com/maplibre/maplibre-native/blob/main/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/camera/ScrollByActivity.kt) of the MapLibreAndroidTestApp. +{{ activity_source_note("ScrollByActivity.kt") }} This example shows how you can move the map by x/y pixels. diff --git a/platform/android/docs/camera/zoom-methods.md b/platform/android/docs/camera/zoom-methods.md index e9638aa2f..ca1e19c96 100644 --- a/platform/android/docs/camera/zoom-methods.md +++ b/platform/android/docs/camera/zoom-methods.md @@ -1,14 +1,12 @@ # Zoom Methods -!!! note - - You can find the full source code of this example in [`ManualZoomActivity.kt`](https://github.com/maplibre/maplibre-native/blob/main/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/camera/ManualZoomActivity.kt) of the MapLibreAndroidTestApp. +{{ activity_source_note("ManualZoomActivity.kt") }} This example shows different methods of zooming in.
-
diff --git a/platform/android/docs/configuration.md b/platform/android/docs/configuration.md index 01126e8cf..1055d8555 100644 --- a/platform/android/docs/configuration.md +++ b/platform/android/docs/configuration.md @@ -38,7 +38,7 @@ This can be found in [`activity_map_options_xml.xml`](https://github.com/maplibr 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. -```kotlin +```kotlin title="MapOptionsXmlActivity.kt" --8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/options/MapOptionsXmlActivity.kt" ``` @@ -55,8 +55,8 @@ This can be found in [`activity_map_options_runtime.xml`](https://github.com/map A `MapLibreMapOptions` object must be created and passed to the MapView constructor. All setup is done in the Activity code: -```kotlin ---8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/options/MapOptionsRuntimeActivity.ktl" +```kotlin title="MapOptionsRuntimeActivity.kt" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/options/MapOptionsRuntimeActivity.kt" ``` This can be found in [`MapOptionsRuntimeActivity.kt`](https://github.com/maplibre/maplibre-native/blob/main/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/options/MapOptionsRuntimeActivity.kt). diff --git a/platform/android/docs/index.md b/platform/android/docs/index.md new file mode 100644 index 000000000..ee549ca44 --- /dev/null +++ b/platform/android/docs/index.md @@ -0,0 +1,48 @@ +# MapLibre Android Examples + +Welcome to the examples documentation of MapLibre Android. + +
+ +- :material-clock-fast:{ .lg .middle } __Quickstart__ + + --- + + Learn how to include MapLibre Android in your project + + [:octicons-arrow-right-24: Getting started](getting-started.md) + +- :fontawesome-brands-slack:{ .lg .middle } __Find us on Slack__ + + --- + + Discuss the project and ask questions in the `#maplibre-android` channel + + [:octicons-arrow-right-24: Slack](https://osmus.slack.com/archives/C01G3D28DAB) + +- :fontawesome-solid-code:{ .lg .middle } __Contribute to these Docs__ + + --- + + Share your own examples with the community! + + [:octicons-arrow-right-24: Documentation on GitHub](https://github.com/maplibre/maplibre-native/tree/main/platform/android/docs) + +
+ + +## Open-Source Apps Using MapLibre Android + +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: + +- [Streetcomplete](https://github.com/streetcomplete/StreetComplete) ([source code](https://github.com/search?q=repo%3Astreetcomplete%2FStreetComplete%20maplibre&type=code)) +- [The official Wikipedia app for Android](https://github.com/wikimedia/apps-android-wikipedia) ([source code](https://github.com/search?q=repo%3Awikimedia%2Fapps-android-wikipedia%20maplibre&type=code)). +- [MapLibreAndroidTestApp](https://github.com/maplibre/maplibre-native/tree/main/platform/android/MapLibreAndroidTestApp). This app is part of the MapLibre Native repository and is used for (automated) testing. Many of the examples in this documentation site come directly from this app. + +## See Also + +- [MapLibre Android API Documentation](https://maplibre.org/maplibre-native/android/api/) +- [Source code on GitHub](https://github.com/maplibre/maplibre-native/tree/main/platform/android) +- [Latest releases](https://github.com/maplibre/maplibre-native/releases?q=android-v11&expanded=true) +- [GitHub Discussions](https://github.com/maplibre/maplibre-native/discussions/categories/q-a?discussions_q=is%3Aopen+category%3AQ%26A+label%3Aandroid) +- [MapLibre on Slack](https://slack.openstreetmap.us). Join the `#maplibre-native` and `#maplibre-android` channels. \ No newline at end of file diff --git a/platform/android/docs/location-component.md b/platform/android/docs/location-component.md index bedaa2440..2dc7b7595 100644 --- a/platform/android/docs/location-component.md +++ b/platform/android/docs/location-component.md @@ -76,7 +76,10 @@ val locationComponentOptions = 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]. -![result](https://github.com/maplibre/maplibre-native/assets/19887090/03dfc87b-111b-4dd0-b4a3-d89e30ed6b63) +
+ ![result](https://github.com/maplibre/maplibre-native/assets/19887090/03dfc87b-111b-4dd0-b4a3-d89e30ed6b63) + {{ openmaptiles_caption() }} +
[^1]: A variety of [camera modes] determine how the camera will track the user location. diff --git a/platform/android/docs/main.py b/platform/android/docs/main.py new file mode 100644 index 000000000..4f449afc8 --- /dev/null +++ b/platform/android/docs/main.py @@ -0,0 +1,37 @@ +import os +from pathlib import Path + +def find_file(start_directory, target_filename): + """Recursively search for a file by name starting from start_directory.""" + for root, dirs, files in os.walk(start_directory): + if target_filename in files: + path = Path(os.path.join(root, target_filename)) + return path.relative_to("/docs") + raise FileNotFoundError(f"File '{target_filename}' could not be found in {start_directory}") + +def define_env(env): + """ + This is the hook for the variables, macros and filters. + """ + + @env.macro + def activity_source_note(filename): + file_path = find_file(f"{os.getcwd()}/MapLibreAndroidTestApp", filename) + + return f""" +!!! note + + You can find the full source code of this example in [`{filename}`](https://github.com/maplibre/maplibre-native/blob/main/platform/android/{file_path}) of the MapLibreAndroidTestApp. +""" + + @env.macro + def s3_url(filename): + return f"https://dwxvn1oqw6mkc.cloudfront.net/android-documentation-resources/{filename}" + + @env.macro + def openmaptiles_caption(): + return f""" + +

Map data [OpenStreetMap](https://www.openstreetmap.org/copyright). © [OpenMapTiles](https://openmaptiles.org/).

+ +""" \ No newline at end of file diff --git a/platform/android/docs/snapshotter.md b/platform/android/docs/snapshotter.md index 89a17dbe2..152c488d2 100644 --- a/platform/android/docs/snapshotter.md +++ b/platform/android/docs/snapshotter.md @@ -4,9 +4,7 @@ This guide will help you walk through how to use [MapSnapshotter](https://maplib ## Map Snapshot with Local Style -!!! note - - You can find the full source code of this example in [`MapSnapshotterLocalStyleActivity.kt`](https://github.com/maplibre/maplibre-native/blob/main/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/snapshot/MapSnapshotterLocalStyleActivity.kt) of the MapLibreAndroidTestApp. +{{ activity_source_note("MapSnapshotterLocalStyleActivity.kt") }} To get started we will show how to use the map snapshotter with a local style. @@ -36,14 +34,12 @@ Lastly we use the `.start()` method to create the snapshot, and pass callbacks f ## Show a Grid of Snapshots -!!! note - - You can find the full source code of this example in [`MapSnapshotterActivity.kt`](https://github.com/maplibre/maplibre-native/blob/main/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/snapshot/MapSnapshotterActivity.kt) of the MapLibreAndroidTestApp. +{{ activity_source_note("MapSnapshotterActivity.kt") }} 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.
- ![Map Snapshotter](https://maplibre-native.s3.eu-central-1.amazonaws.com/android-documentation-resources/map_snapshotter.png){ width="300" } + ![Map Snapshotter](https://dwxvn1oqw6mkc.cloudfront.net/android-documentation-resources/map_snapshotter.png){ width="300" }
First we create a [`GridLayout`](https://developer.android.com/reference/kotlin/android/widget/GridLayout) and a list of `MapSnapshotter` instances. We create a `Style.Builder` with a different style for each cell in the grid. @@ -78,14 +74,12 @@ In the last column of the first row we add two bitmaps. See the next example for ## Map Snapshot with Bitmap Overlay -!!! note - - You can also find this code in [`MapSnapshotterBitMapOverlayActivity.kt`](https://github.com/maplibre/maplibre-native/blob/main/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/snapshot/MapSnapshotterBitMapOverlayActivity.kt) of the MapLibreAndroidTestApp. +{{ activity_source_note("MapSnapshotterBitMapOverlayActivity.kt") }} This example adds a bitmap on top of the snapshot. It also demonstrates how you can add a click listener to a snapshot.
- ![Screenshot of Map Snapshot with Bitmap Overlay](https://maplibre-native.s3.eu-central-1.amazonaws.com/android-documentation-resources/map_snapshot_with_bitmap_overlay.png){ width="300" } + ![Screenshot of Map Snapshot with Bitmap Overlay](https://dwxvn1oqw6mkc.cloudfront.net/android-documentation-resources/map_snapshot_with_bitmap_overlay.png){ width="300" }
@@ -95,14 +89,12 @@ This example adds a bitmap on top of the snapshot. It also demonstrates how you ## Map Snapshotter with Heatmap Layer -!!! note - - You can find the full source code of this example in [`MapSnapshotterHeatMapActivity.kt`](https://github.com/maplibre/maplibre-native/blob/main/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/snapshot/MapSnapshotterHeatMapActivity.kt) of the MapLibreAndroidTestApp. +{{ activity_source_note("MapSnapshotterHeatMapActivity.kt") }} 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.
- ![Screenshot of Snapshotter with Heatmap](https://maplibre-native.s3.eu-central-1.amazonaws.com/android-documentation-resources/snapshotter_headmap_screenshot.png){ width="300" } + ![Screenshot of Snapshotter with Heatmap](https://dwxvn1oqw6mkc.cloudfront.net/android-documentation-resources/snapshotter_headmap_screenshot.png){ width="300" }
First, we create the `MapSnapshotterHeatMapActivity` class, which extends `AppCompatActivity` and implements `MapSnapshotter.SnapshotReadyCallback` to receive the snapshot once it's ready. @@ -148,9 +140,7 @@ Finally, we ensure to cancel the snapshotter in the `onStop` method to free up r ## Map Snapshotter with Expression -!!! note - - You can find the full source code of this example in [`MapSnapshotterWithinExpression.kt`](https://github.com/maplibre/maplibre-native/blob/main/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/turf/MapSnapshotterWithinExpression.kt) of the MapLibreAndroidTestApp. +{{ activity_source_note("MapSnapshotterWithinExpression.kt") }} 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](https://maplibre.org/maplibre-style-spec/expressions/#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. diff --git a/platform/android/docs/styling/animated-image-source.md b/platform/android/docs/styling/animated-image-source.md new file mode 100644 index 000000000..bd63c6d38 --- /dev/null +++ b/platform/android/docs/styling/animated-image-source.md @@ -0,0 +1,22 @@ +# Animated Image Source + +{{ activity_source_note("AnimatedImageSourceActivity.kt") }} + +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](https://maplibre.org/maplibre-gl-js/docs/examples/animate-images/). + +
+ + {{ openmaptiles_caption() }} +
+ +We set up an [image source](https://maplibre.org/maplibre-style-spec/sources/#image) in a particular quad. Then we kick of a runnable that periodically updates the image source. + +```kotlin title="Creating the image source" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedImageSourceActivity.kt:onMapReady" +``` + +```kotlin title="Updating the image source" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedImageSourceActivity.kt:setImage" +``` \ No newline at end of file diff --git a/platform/android/docs/styling/animated-symbol-layer.md b/platform/android/docs/styling/animated-symbol-layer.md new file mode 100644 index 000000000..204da05f4 --- /dev/null +++ b/platform/android/docs/styling/animated-symbol-layer.md @@ -0,0 +1,35 @@ +# Animated SymbolLayer + +{{ activity_source_note("AnimatedSymbolLayerActivity.kt") }} + +
+ + {{ openmaptiles_caption() }} +
+ + +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. + +```kotlin title="Getter method to get a random location on the screen" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedSymbolLayerActivity.kt:latLngInBounds" +``` + +```kotlin title="Adding a passenger at a random location (on screen)" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedSymbolLayerActivity.kt:addPassenger" +``` + +Adding the taxi on screen is done very similarly. + +```kotlin title="Adding the taxi with bearing" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedSymbolLayerActivity.kt:addTaxi" +``` + +For animating the taxi we use a [`ValueAnimator`](https://developer.android.com/reference/android/animation/ValueAnimator). + +```kotlin title="Animate the taxi driving towards the passenger" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/AnimatedSymbolLayerActivity.kt:animateTaxi" +``` \ No newline at end of file diff --git a/platform/android/docs/styling/building-layer.md b/platform/android/docs/styling/building-layer.md new file mode 100644 index 000000000..9f9cb152d --- /dev/null +++ b/platform/android/docs/styling/building-layer.md @@ -0,0 +1,25 @@ +# Building Layer + +{{ activity_source_note("BuildingFillExtrusionActivity.kt") }} + +In this example will show how to add a [Fill Extrusion](https://maplibre.org/maplibre-style-spec/layers/#fill-extrusion) layer to a style. +
+ + {{ openmaptiles_caption() }} +
+ +We use the [OpenFreeMap Bright](https://openfreemap.org/quick_start/) style which, unlike OpenFreeMap Libery, does not have a fill extrusion layer by default. However, if you inspect this style with [Maputnik](https://maplibre.org/maputnik) you will find that the multipolygons in the `building` layer (of the `openfreemap` source) each have `render_min_height` and `render_height` properties. + +```kotlin title="Setting up the fill extrusion layer" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/BuildingFillExtrusionActivity.kt:setupBuildings" +``` + +```kotlin title="Changing the light direction" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/BuildingFillExtrusionActivity.kt:lightPosition" +``` + +```kotlin title="Changing the light color" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/BuildingFillExtrusionActivity.kt:lightColor" +``` \ No newline at end of file diff --git a/platform/android/docs/styling/circle-layer.md b/platform/android/docs/styling/circle-layer.md new file mode 100644 index 000000000..90503bd53 --- /dev/null +++ b/platform/android/docs/styling/circle-layer.md @@ -0,0 +1,49 @@ +# Circle Layer (with Clustering) + +{{ activity_source_note("CircleLayerActivity.kt") }} + +In this example we will add a circle layer for a GeoJSON source. We also show how you can use the [cluster](https://maplibre.org/maplibre-style-spec/sources/#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. + +```kotlin title="Setting up the GeoJSON source" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CircleLayerActivity.kt:addBusStopSource" +``` + +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](https://maplibre.org/maplibre-style-spec/layers/#circle). Lastly add the layer to your style. + +```kotlin title="Create circle layer a small orange circle for each bus stop" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CircleLayerActivity.kt:addBusStopCircleLayer" +``` + +## Clustering + +Next we will show you how you can use clustering. Create a `GeoJsonSource` as before, but with some additional options to enable clustering. + +```kotlin title="Setting up the clustered GeoJSON source" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CircleLayerActivity.kt:addClusteredSource" +``` + +When enabling clustering some [special attributes](https://maplibre.org/maplibre-style-spec/sources/#cluster) 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. + +```kotlin title="Add a symbol layers for points that are not clustered" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CircleLayerActivity.kt:unclusteredLayer" +``` + +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. + +```kotlin title="Define different colors for different point amounts" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CircleLayerActivity.kt:clusteredCircleLayers" +``` + +Lastly we iterate over the array of `Pair`s to create a `CircleLayer` for each element. + +```kotlin title="Add different circle layers for clusters of different point amounts" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CircleLayerActivity.kt:clusteredCircleLayersLoop" +``` \ No newline at end of file diff --git a/platform/android/docs/styling/custom-sprite.md b/platform/android/docs/styling/custom-sprite.md new file mode 100644 index 000000000..ce603960b --- /dev/null +++ b/platform/android/docs/styling/custom-sprite.md @@ -0,0 +1,9 @@ +# Add Custom Sprite + +{{ activity_source_note("CustomSpriteActivity.kt") }} + +This example showcases adding a sprite image and using it in a Symbol Layer. + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CustomSpriteActivity.kt:addImage" +``` \ No newline at end of file diff --git a/platform/android/docs/styling/data-driven-style.md b/platform/android/docs/styling/data-driven-style.md new file mode 100644 index 000000000..7aa7d499f --- /dev/null +++ b/platform/android/docs/styling/data-driven-style.md @@ -0,0 +1,88 @@ +# Data Driven Style + +{{ activity_source_note("DataDrivenStyleActivity.kt") }} + +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](https://github.com/maplibre/maplibre-native/blob/main/platform/android/MapLibreAndroidTestApp/src/main/res/raw/amsterdam.geojson). 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](https://maplibre.org/maplibre-style-spec/) for more information about [expressions](https://maplibre.org/maplibre-style-spec/expressions/) such as [`interpolate`](https://maplibre.org/maplibre-style-spec/expressions/#interpolate) and [`step`](https://maplibre.org/maplibre-style-spec/expressions/#step). + + +## Exponential Zoom Function + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt:addExponentialZoomFunction" +``` + +
+ +
+ + +## Interval Zoom Function + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt:addIntervalZoomFunction" +``` + +
+ +
+ +```json title="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]] +``` + +## Exponential Source Function + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt:addExponentialSourceFunction" +``` + +## Categorical Source Function + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt:addCategoricalSourceFunction" +``` + +## Identity Source Function + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt:addIdentitySourceFunction" +``` + +## Interval Source Function + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt:addIntervalSourceFunction" +``` + +## Composite Exponential Function + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt:addCompositeExponentialFunction" +``` + +## Identity Source Function + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt:addIdentitySourceFunction" +``` + +## Composite Interval Function + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt:addCompositeIntervalFunction" +``` + +## Composite Categorical Function + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DataDrivenStyleActivity.kt:addCompositeCategoricalFunction" +``` diff --git a/platform/android/docs/styling/distance-expression.md b/platform/android/docs/styling/distance-expression.md new file mode 100644 index 000000000..774efdd03 --- /dev/null +++ b/platform/android/docs/styling/distance-expression.md @@ -0,0 +1,22 @@ +# Distance Expression + +{{ activity_source_note("DistanceExpressionActivity.kt") }} + +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](https://maplibre.org/maplibre-style-spec/expressions/#within) is used. + +
+ ![Screenshot of map where only labels inside some circular area are shown]({{ s3_url("distance_expression_activity.png") }}){ width="400" } + {{ openmaptiles_caption() }} +
+ +First we add a [fill layer](https://maplibre.org/maplibre-style-spec/layers/#fill) and a GeoJSON source. + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DistanceExpressionActivity.kt:FillLayer" +``` + +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. + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DistanceExpressionActivity.kt:distanceExpression" +``` \ No newline at end of file diff --git a/platform/android/docs/styling/draggable-marker.md b/platform/android/docs/styling/draggable-marker.md new file mode 100644 index 000000000..d19452719 --- /dev/null +++ b/platform/android/docs/styling/draggable-marker.md @@ -0,0 +1,36 @@ +# Draggable Marker + +{{ activity_source_note("DraggableMarkerActivity.kt") }} + +
+ +
+ +## Adding a marker on tap + +```kotlin title="Adding a tap listener to the map to add a marker on tap" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DraggableMarkerActivity.kt:addOnMapClickListener" +``` + +## Allowing markers to be dragged + +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. + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DraggableMarkerActivity.kt:draggableSymbolsManager" +``` + +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](https://github.com/maplibre/maplibre-gestures-android) for the implementation details of the gestures library used by MapLibre Android. + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/DraggableMarkerActivity.kt:DraggableSymbolsManager" +``` \ No newline at end of file diff --git a/platform/android/docs/styling/live-realtime-data.md b/platform/android/docs/styling/live-realtime-data.md new file mode 100644 index 000000000..3ca725a9a --- /dev/null +++ b/platform/android/docs/styling/live-realtime-data.md @@ -0,0 +1,37 @@ +# Add live realtime data + +{{ activity_source_note("RealTimeGeoJsonActivity.kt") }} + +In this example you will learn how to add a live GeoJSON source. We have set up a [lambda function](https://m6rgfvqjp34nnwqcdm4cmmy3cm0dtupu.lambda-url.us-east-1.on.aws/) that returns a new GeoJSON point every time it is called. + +
+ +
+ +First we will create a `GeoJSONSource`. + +```kotlin title="Adding GeoJSON source" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/RealTimeGeoJsonActivity.kt:addSource" +``` + +Next we will create a `SymbolLayer` that uses the source. + +```kotlin title="Adding a SymbolLayer source" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/RealTimeGeoJsonActivity.kt:addLayer" +``` + +We use define a `Runnable` and use `android.os.Handler` with a `android.os.Looper` to update the GeoJSON source every 2 seconds. + +```kotlin title="Defining a Runnable for updating the GeoJSON source" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/RealTimeGeoJsonActivity.kt:Runnable" +``` + +## Bonus: set icon rotation + +You can set the icon rotation of the icon when ever the point is updated based on the last two points. + +```kotlin title="Defining a Runnable for updating the GeoJSON source" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/RealTimeGeoJsonActivity.kt:setIconRotation" +``` diff --git a/platform/android/mkdocs.yaml b/platform/android/mkdocs.yaml index 8b3aab8c7..43b9dbd9d 100644 --- a/platform/android/mkdocs.yaml +++ b/platform/android/mkdocs.yaml @@ -4,6 +4,7 @@ repo_url: https://github.com/maplibre/maplibre-native site_description: MapLibre Android Examples edit_uri: edit/main/platform/android/docs copyright: Map data from OpenStreetMap +exclude_docs: README.md extra_css: - assets/extra.css theme: @@ -40,6 +41,7 @@ markdown_extensions: - pymdownx.inlinehilite - pymdownx.snippets: dedent_subsections: true + check_paths: true - pymdownx.superfences - pymdownx.escapeall: hardbreak: True @@ -66,6 +68,9 @@ extra: link: https://github.com/maplibre plugins: - search + - macros: + module_name: docs/main + on_undefined: strict - social: cards_layout_options: background_color: '#295DAA'