diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/feature/QuerySourceFeaturesActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/feature/QuerySourceFeaturesActivity.kt index b4113a482bf..213994f3cbc 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/feature/QuerySourceFeaturesActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/feature/QuerySourceFeaturesActivity.kt @@ -43,6 +43,7 @@ class QuerySourceFeaturesActivity : AppCompatActivity() { } private fun initStyle(style: Style) { + // # --8<-- [start:JsonObject] val properties = JsonObject() properties.addProperty("key1", "value1") val source = GeoJsonSource( @@ -62,6 +63,7 @@ class QuerySourceFeaturesActivity : AppCompatActivity() { val layer = CircleLayer("test-layer", source.id) .withFilter(visible) style.addLayer(layer) + // # --8<-- [end:JsonObject] // Add a click listener maplibreMap.addOnMapClickListener { point: LatLng? -> diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CollectionUpdateOnStyleChange.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CollectionUpdateOnStyleChange.kt index cdabb1ffc10..589e6e623bf 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CollectionUpdateOnStyleChange.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CollectionUpdateOnStyleChange.kt @@ -56,6 +56,7 @@ class CollectionUpdateOnStyleChange : AppCompatActivity(), OnMapReadyCallback, S } private fun setupLayer(style: Style) { + // # --8<-- [start:setupLayer] val source = GeoJsonSource("source", featureCollection) val lineLayer = LineLayer("layer", "source") .withProperties( @@ -65,6 +66,7 @@ class CollectionUpdateOnStyleChange : AppCompatActivity(), OnMapReadyCallback, S style.addSource(source) style.addLayer(lineLayer) + // # --8<-- [end:setupLayer] } private fun setupStyleChangeView() { diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/HeatmapLayerActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/HeatmapLayerActivity.kt index dacb8d41cbe..117346e5694 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/HeatmapLayerActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/HeatmapLayerActivity.kt @@ -45,10 +45,11 @@ class HeatmapLayerActivity : AppCompatActivity() { } ) } - + // # --8<-- [start:createEarthquakeSource] private fun createEarthquakeSource(): GeoJsonSource { return GeoJsonSource(EARTHQUAKE_SOURCE_ID, URI(EARTHQUAKE_SOURCE_URL)) } + // # --8<-- [end:createEarthquakeSource] private fun createHeatmapLayer(): HeatmapLayer { val layer = HeatmapLayer(HEATMAP_LAYER_ID, EARTHQUAKE_SOURCE_ID) @@ -188,6 +189,7 @@ class HeatmapLayerActivity : AppCompatActivity() { mapView.onDestroy() } + // # --8<-- [start:constants] companion object { private const val EARTHQUAKE_SOURCE_URL = "https://maplibre.org/maplibre-gl-js-docs/assets/earthquakes.geojson" @@ -196,4 +198,5 @@ class HeatmapLayerActivity : AppCompatActivity() { private const val HEATMAP_LAYER_SOURCE = "earthquakes" private const val CIRCLE_LAYER_ID = "earthquakes-circle" } + // # --8<-- [end:constants] } diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/NoStyleActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/NoStyleActivity.kt index 43e16d7ffd6..3352d2a56e4 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/NoStyleActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/NoStyleActivity.kt @@ -29,7 +29,7 @@ class NoStyleActivity : AppCompatActivity() { super.onCreate(savedInstanceState) binding = ActivityMapSimpleBinding.inflate(layoutInflater) setContentView(binding.root) - + // # --8<-- [start:setup] binding.mapView.getMapAsync { map -> map.moveCamera(CameraUpdateFactory.newLatLngZoom(cameraTarget, cameraZoom)) map.setStyle( @@ -39,6 +39,7 @@ class NoStyleActivity : AppCompatActivity() { .withLayer(SymbolLayer(layerId, sourceId).withProperties(iconImage(imageId))) ) } + // # --8<-- [end:setup] } override fun onStart() { diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/RuntimeStyleActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/RuntimeStyleActivity.kt index 6df7788241a..71341d47405 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/RuntimeStyleActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/RuntimeStyleActivity.kt @@ -329,6 +329,7 @@ class RuntimeStyleActivity : AppCompatActivity() { private fun addParksLayer() { // Add a source + // # --8<-- [start:source] val source: Source = try { GeoJsonSource("amsterdam-spots", ResourceUtils.readRawResource(this, R.raw.amsterdam)) } catch (ioException: IOException) { @@ -347,6 +348,7 @@ class RuntimeStyleActivity : AppCompatActivity() { PropertyFactory.fillOpacity(0.3f), PropertyFactory.fillAntialias(true) ) + // # --8<-- [end:source] // Only show me parks (except westerpark with stroke-width == 3) layer.setFilter( diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/ZoomFunctionSymbolLayerActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/ZoomFunctionSymbolLayerActivity.kt index b7647dcdfc5..b2551676561 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/ZoomFunctionSymbolLayerActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/ZoomFunctionSymbolLayerActivity.kt @@ -61,6 +61,7 @@ class ZoomFunctionSymbolLayerActivity : AppCompatActivity() { } } + // # --8<-- [start:updateSource] private fun updateSource(style: Style?) { val featureCollection = createFeatureCollection() if (source != null) { @@ -70,6 +71,7 @@ class ZoomFunctionSymbolLayerActivity : AppCompatActivity() { style!!.addSource(source!!) } } + // # --8<-- [end:updateSource] private fun toggleSymbolLayerVisibility() { layer!!.setProperties( @@ -78,6 +80,7 @@ class ZoomFunctionSymbolLayerActivity : AppCompatActivity() { isShowingSymbolLayer = !isShowingSymbolLayer } + // # --8<-- [start:createFeatureCollection] private fun createFeatureCollection(): FeatureCollection { val point = if (isInitialPosition) { Point.fromLngLat(-74.01618140, 40.701745) @@ -89,6 +92,7 @@ class ZoomFunctionSymbolLayerActivity : AppCompatActivity() { val feature = Feature.fromGeometry(point, properties) return FeatureCollection.fromFeatures(arrayOf(feature)) } + // # --8<-- [end:createFeatureCollection] private fun addLayer(style: Style) { layer = SymbolLayer(LAYER_ID, SOURCE_ID) diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/turf/MapSnapshotterWithinExpression.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/turf/MapSnapshotterWithinExpression.kt index 7b5e6b4f707..ae7f447275f 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/turf/MapSnapshotterWithinExpression.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/turf/MapSnapshotterWithinExpression.kt @@ -187,9 +187,9 @@ class MapSnapshotterWithinExpression : AppCompatActivity() { super.onSaveInstanceState(outState, outPersistentState) binding.mapView.onSaveInstanceState(outState) } - - private fun bufferLineStringGeometry(): Polygon { + private fun bufferLineStringGeometry(): Polygon { // TODO replace static data by Turf#Buffer: mapbox-java/issues/987 + // # --8<-- [start:fromJson] return FeatureCollection.fromJson( """ { @@ -250,6 +250,7 @@ class MapSnapshotterWithinExpression : AppCompatActivity() { } """.trimIndent() ).features()!![0].geometry() as Polygon + // # --8<-- [end:fromJson] } companion object { diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/ResourceUtils.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/ResourceUtils.kt index cabe0ccdceb..5e9a28efe43 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/ResourceUtils.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/ResourceUtils.kt @@ -7,6 +7,7 @@ import java.io.* object ResourceUtils { @JvmStatic + // # --8<-- [start:readRawResource] fun readRawResource(context: Context?, @RawRes rawResource: Int): String { var json = "" if (context != null) { @@ -23,6 +24,7 @@ object ResourceUtils { } return json } + // # --8<-- [end:readRawResource] fun convertDpToPx(context: Context, dp: Float): Float { return TypedValue.applyDimension( diff --git a/platform/android/docs/geojson-guide.md b/platform/android/docs/geojson-guide.md new file mode 100644 index 00000000000..db7c0f1a943 --- /dev/null +++ b/platform/android/docs/geojson-guide.md @@ -0,0 +1,134 @@ +# Using a GeoJSON Source + +This guide will teach you how to use [`GeoJsonSource`](https://maplibre.org/maplibre-native/android/api/-map-libre%20-native%20-android/org.maplibre.android.style.sources/-geo-json-source/index.html) by deep diving into [GeoJSON](https://geojson.org/) file format. + +## Goals + +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 `GeoJsonSource`s. +4. Update data at runtime. + +## 1. Styles, Layers, and Data source + +- A style defines the visual representation of the map such as colors and appearance. +- Layers control how data should be presented to the user. +- Data sources hold actual data and provides layers with it. + +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. + +## 2. GeoJSON + +[GeoJSON](https://geojson.org/) is a JSON file for encoding various geographical data structures. +It defines several JSON objects to represent geospatial information. Typicalle the`.geojson` extension is used for GeoJSON files. +We define the most fundamental objects: + +- `Geometry` refers to a single geometric shape that contains one or more coordinates. These shapes are visual objects displayed on a map. A geometry can be one of the following six types: + - Point + - MultiPoint + - LineString + - MultilineString + - Polygon + - MultiPolygon +- `Feautue` is a compound object that combines a single geometry with user-defined attributes, such as name, color. +- `FeatureCollection` is set of features stored in an array. It is a root object that introduces all other features. + +A typical GeoJSON structure might look like: + +```json +{ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [125.6, 10.1] + }, + "properties": { + "name": "Dinagat Islands" + } +} +``` + +So far we learned describing geospatial data in GeoJSON files. We will start applying this knowledge into our map applications. + +## 3. GeoJsonSource + +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`: + +- Locally stored files such as assets and raw folders +- Remote services +- Raw string parsed into FeatureCollections objects +- Geometry, Feature, and FeatureCollection objects that map to GeoJSON Base builders + +A sample `GeoJsonSource`: + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/CollectionUpdateOnStyleChange.kt:setupLayer" +``` + +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. + +### Creating GeoJSON sources + +There are various ways you can create a `GeoJSONSource`. Some of the options are shown below. + +```kotlin title="Loading from local files with assets folder file" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/NoStyleActivity.kt:setup" +``` + +```kotlin title="Loading with raw folder file" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/RuntimeStyleActivity.kt:source" +``` + +```kotlin title="Parsing inline JSON" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/ResourceUtils.kt:readRawResource" +``` + +```kotlin title="Loading from remote services" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/HeatmapLayerActivity.kt:createEarthquakeSource" +``` + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/HeatmapLayerActivity.kt:constants" +``` + +```kotlin title="Parsing string with the fromJson method of FeatureCollection" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/turf/MapSnapshotterWithinExpression.kt:fromJson" +``` + +```kotlin title="Creating Geometry, Feature, and FeatureCollections from scratch" +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/feature/QuerySourceFeaturesActivity.kt:JsonObject" +``` + +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. + +## 4. Updating data at runtime + +The key feature of `GeoJsonSource`s is that once we add one, we can set another set of data. +We achieve this using `setGeoJson()` method. For instance, we create a source variable and check if we have not assigned it, then we create a new source object and add it to style; otherwise, we set a different data source: + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/ZoomFunctionSymbolLayerActivity.kt:createFeatureCollection" +``` + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/ZoomFunctionSymbolLayerActivity.kt:updateSource" +``` + +See [this guide](styling/animated-symbol-layer.md) for an advanced example that showcases random cars and a passenger on a map updating their positions with smooth animation. + +## Summary + +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’s better to use a remote data source. \ No newline at end of file