Skip to content

Commit

Permalink
Guide to using GeoJsonDataSource (#2977)
Browse files Browse the repository at this point in the history
Co-authored-by: Bart Louwers <[email protected]>
  • Loading branch information
jDilshodbek and louwers authored Nov 28, 2024
1 parent 4878005 commit 8b32c9a
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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? ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -65,6 +66,7 @@ class CollectionUpdateOnStyleChange : AppCompatActivity(), OnMapReadyCallback, S

style.addSource(source)
style.addLayer(lineLayer)
// # --8<-- [end:setupLayer]
}

private fun setupStyleChangeView() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand All @@ -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]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -39,6 +39,7 @@ class NoStyleActivity : AppCompatActivity() {
.withLayer(SymbolLayer(layerId, sourceId).withProperties(iconImage(imageId)))
)
}
// # --8<-- [end:setup]
}

override fun onStart() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class ZoomFunctionSymbolLayerActivity : AppCompatActivity() {
}
}

// # --8<-- [start:updateSource]
private fun updateSource(style: Style?) {
val featureCollection = createFeatureCollection()
if (source != null) {
Expand All @@ -70,6 +71,7 @@ class ZoomFunctionSymbolLayerActivity : AppCompatActivity() {
style!!.addSource(source!!)
}
}
// # --8<-- [end:updateSource]

private fun toggleSymbolLayerVisibility() {
layer!!.setProperties(
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
"""
{
Expand Down Expand Up @@ -250,6 +250,7 @@ class MapSnapshotterWithinExpression : AppCompatActivity() {
}
""".trimIndent()
).features()!![0].geometry() as Polygon
// # --8<-- [end:fromJson]
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -23,6 +24,7 @@ object ResourceUtils {
}
return json
}
// # --8<-- [end:readRawResource]

fun convertDpToPx(context: Context, dp: Float): Float {
return TypedValue.applyDimension(
Expand Down
134 changes: 134 additions & 0 deletions platform/android/docs/geojson-guide.md
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 8b32c9a

Please sign in to comment.