From eaca4f45a197a3cfdc797664f7750204b3aa0f6d Mon Sep 17 00:00:00 2001 From: Kiryl Dzehtsiarenka Date: Wed, 8 May 2024 10:57:13 +0300 Subject: [PATCH] [compose] Terrain support (#2427) * Compose Terrain support * PR fixes * Treat the terrain dem source fully as a layer --------- Co-authored-by: Ramon --- CHANGELOG.md | 1 + compose-app/src/main/AndroidManifest.xml | 10 ++ .../testapp/examples/style/TerrainActivity.kt | 121 +++++++++++++++ .../main/res/values/example_descriptions.xml | 1 + .../src/main/res/values/example_titles.xml | 1 + extension-compose/api/Release/metalava.txt | 66 ++++++++- extension-compose/api/extension-compose.api | 69 ++++++++- .../maps/extension/compose/style/Style.kt | 16 +- .../compose/style/internal/MapStyleNode.kt | 24 +++ .../compose/style/sources/SourceState.kt | 1 - .../style/standard/MapboxStandardStyle.kt | 10 +- .../style/terrain/TerrainStateApplier.kt | 138 +++++++++++++++++ .../terrain/generated/TerrainProperties.kt | 40 +++++ .../style/terrain/generated/TerrainState.kt | 140 ++++++++++++++++++ 14 files changed, 627 insertions(+), 11 deletions(-) create mode 100644 compose-app/src/main/java/com/mapbox/maps/compose/testapp/examples/style/TerrainActivity.kt create mode 100644 extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/terrain/TerrainStateApplier.kt create mode 100644 extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/terrain/generated/TerrainProperties.kt create mode 100644 extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/terrain/generated/TerrainState.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 6737296ed3..bf98b98930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Mapbox welcomes participation and contributions from everyone. ## Features ✨ and improvements 🏁 * [compose] Add `StyleImage` to construct following image layer properties: `IconImage`, `FillPattern`, `LinePattern`, `BearingImage`, `ShadowImage`, `TopImage`. * [compose] Add `ModelId` constructor to add model id and uri. +* [compose] Add `TerrainState` parameter to `GenericStyle`, `MapStyle` and `MapboxStandardStyle` composable functions. * Add experimental `RasterParticleLayer` in Style DSL and Compose. ## Bug fixes 🐞 diff --git a/compose-app/src/main/AndroidManifest.xml b/compose-app/src/main/AndroidManifest.xml index 14806b0c95..27e62c7dec 100644 --- a/compose-app/src/main/AndroidManifest.xml +++ b/compose-app/src/main/AndroidManifest.xml @@ -226,6 +226,16 @@ android:name="@string/category" android:value="@string/category_styles" /> + + + Showcase the usage of a 3D model layer. Use an image source to easily display images on the map. Load a raster image to a style using ImageSource and display it on a map as animated weather data using RasterLayer. + Add terrain on top of raster-dem source \ No newline at end of file diff --git a/compose-app/src/main/res/values/example_titles.xml b/compose-app/src/main/res/values/example_titles.xml index 3f0dc13674..b44f974437 100644 --- a/compose-app/src/main/res/values/example_titles.xml +++ b/compose-app/src/main/res/values/example_titles.xml @@ -20,4 +20,5 @@ Display 3D model in a model layer Use an image source Add animated weather data + Terrain diff --git a/extension-compose/api/Release/metalava.txt b/extension-compose/api/Release/metalava.txt index 5e323d5b54..9a2364d5c4 100644 --- a/extension-compose/api/Release/metalava.txt +++ b/extension-compose/api/Release/metalava.txt @@ -296,8 +296,8 @@ package com.mapbox.maps.extension.compose.style { } public final class StyleKt { - method @androidx.compose.runtime.Composable @com.mapbox.maps.MapboxExperimental @com.mapbox.maps.extension.compose.style.MapboxStyleComposable public static void GenericStyle(String style, com.mapbox.maps.extension.compose.style.SlotsContent slotsContent = com.mapbox.maps.extension.compose.style.SlotsContent(), com.mapbox.maps.extension.compose.style.LayerPositionedContent layerPositionedContent = com.mapbox.maps.extension.compose.style.LayerPositionedContent(), com.mapbox.maps.extension.compose.style.StyleImportsConfig styleImportsConfig = com.mapbox.maps.extension.compose.style.StyleImportsConfig(), com.mapbox.maps.extension.compose.style.projection.Projection projection = Projection.default, com.mapbox.maps.extension.compose.style.atmosphere.generated.AtmosphereState atmosphereState = AtmosphereState.default); - method @androidx.compose.runtime.Composable @com.mapbox.maps.MapboxExperimental @com.mapbox.maps.extension.compose.style.MapboxStyleComposable public static void MapStyle(String style, com.mapbox.maps.extension.compose.style.projection.Projection projection = Projection.default, com.mapbox.maps.extension.compose.style.atmosphere.generated.AtmosphereState atmosphereState = AtmosphereState.default); + method @androidx.compose.runtime.Composable @com.mapbox.maps.MapboxExperimental @com.mapbox.maps.extension.compose.style.MapboxStyleComposable public static void GenericStyle(String style, com.mapbox.maps.extension.compose.style.SlotsContent slotsContent = com.mapbox.maps.extension.compose.style.SlotsContent(), com.mapbox.maps.extension.compose.style.LayerPositionedContent layerPositionedContent = com.mapbox.maps.extension.compose.style.LayerPositionedContent(), com.mapbox.maps.extension.compose.style.StyleImportsConfig styleImportsConfig = com.mapbox.maps.extension.compose.style.StyleImportsConfig(), com.mapbox.maps.extension.compose.style.projection.Projection projection = Projection.default, com.mapbox.maps.extension.compose.style.atmosphere.generated.AtmosphereState atmosphereState = AtmosphereState.default, com.mapbox.maps.extension.compose.style.terrain.generated.TerrainState terrainState = TerrainState.initial); + method @androidx.compose.runtime.Composable @com.mapbox.maps.MapboxExperimental @com.mapbox.maps.extension.compose.style.MapboxStyleComposable public static void MapStyle(String style, com.mapbox.maps.extension.compose.style.projection.Projection projection = Projection.default, com.mapbox.maps.extension.compose.style.atmosphere.generated.AtmosphereState atmosphereState = AtmosphereState.default, com.mapbox.maps.extension.compose.style.terrain.generated.TerrainState terrainState = TerrainState.initial); method @com.mapbox.maps.MapboxExperimental public static com.mapbox.maps.extension.compose.style.LayerPositionedContent layerPositionedContent(kotlin.jvm.functions.Function1 init); method @com.mapbox.maps.MapboxExperimental public static com.mapbox.maps.extension.compose.style.SlotsContent slotsContent(kotlin.jvm.functions.Function1 init); method @com.mapbox.maps.MapboxExperimental public static com.mapbox.maps.extension.compose.style.StyleImportsConfig styleImportsConfig(kotlin.jvm.functions.Function1 init); @@ -4862,7 +4862,67 @@ package com.mapbox.maps.extension.compose.style.standard { } public final class MapboxStandardStyleKt { - method @androidx.compose.runtime.Composable @com.mapbox.maps.MapboxExperimental @com.mapbox.maps.extension.compose.style.MapboxStyleComposable public static void MapboxStandardStyle(kotlin.jvm.functions.Function0? topSlot = null, kotlin.jvm.functions.Function0? middleSlot = null, kotlin.jvm.functions.Function0? bottomSlot = null, com.mapbox.maps.extension.compose.style.standard.LightPreset lightPreset = LightPreset.default, com.mapbox.maps.extension.compose.style.projection.Projection projection = Projection.default, com.mapbox.maps.extension.compose.style.atmosphere.generated.AtmosphereState atmosphereState = AtmosphereState.default); + method @androidx.compose.runtime.Composable @com.mapbox.maps.MapboxExperimental @com.mapbox.maps.extension.compose.style.MapboxStyleComposable public static void MapboxStandardStyle(kotlin.jvm.functions.Function0? topSlot = null, kotlin.jvm.functions.Function0? middleSlot = null, kotlin.jvm.functions.Function0? bottomSlot = null, com.mapbox.maps.extension.compose.style.standard.LightPreset lightPreset = LightPreset.default, com.mapbox.maps.extension.compose.style.projection.Projection projection = Projection.default, com.mapbox.maps.extension.compose.style.atmosphere.generated.AtmosphereState atmosphereState = AtmosphereState.default, com.mapbox.maps.extension.compose.style.terrain.generated.TerrainState terrainState = TerrainState.initial); + } + +} + +package com.mapbox.maps.extension.compose.style.terrain { + + public final class TerrainStateApplierKt { + } + +} + +package com.mapbox.maps.extension.compose.style.terrain.generated { + + @androidx.compose.runtime.Immutable @com.mapbox.maps.MapboxExperimental public final class Exaggeration { + ctor public Exaggeration(com.mapbox.bindgen.Value value); + ctor public Exaggeration(double value); + ctor public Exaggeration(com.mapbox.maps.extension.style.expressions.generated.Expression expression); + method public com.mapbox.bindgen.Value component1(); + method public com.mapbox.maps.extension.compose.style.terrain.generated.Exaggeration copy(com.mapbox.bindgen.Value value); + method public com.mapbox.bindgen.Value getValue(); + property public final com.mapbox.bindgen.Value value; + field public static final com.mapbox.maps.extension.compose.style.terrain.generated.Exaggeration.Companion Companion; + } + + public static final class Exaggeration.Companion { + method public com.mapbox.maps.extension.compose.style.terrain.generated.Exaggeration getDefault(); + property public final com.mapbox.maps.extension.compose.style.terrain.generated.Exaggeration default; + } + + @com.mapbox.maps.MapboxExperimental public final class TerrainState { + ctor public TerrainState(com.mapbox.maps.extension.compose.style.sources.generated.RasterDemSourceState rasterDemSourceState, java.util.Map initialProperties = mapOf()); + method public com.mapbox.maps.extension.compose.style.terrain.generated.Exaggeration getExaggeration(); + method public void setExaggeration(com.mapbox.maps.extension.compose.style.terrain.generated.Exaggeration); + property public final com.mapbox.maps.extension.compose.style.terrain.generated.Exaggeration exaggeration; + field public static final com.mapbox.maps.extension.compose.style.terrain.generated.TerrainState.Companion Companion; + } + + public static final class TerrainState.Companion { + method public com.mapbox.maps.extension.compose.style.terrain.generated.TerrainState getDisabled(); + method public androidx.compose.runtime.saveable.Saver getSaver(); + property public final androidx.compose.runtime.saveable.Saver Saver; + property public final com.mapbox.maps.extension.compose.style.terrain.generated.TerrainState disabled; + } + + @com.mapbox.maps.MapboxExperimental @kotlinx.parcelize.Parcelize @kotlinx.parcelize.TypeParceler public static final class TerrainState.Holder implements android.os.Parcelable { + ctor public TerrainState.Holder(com.mapbox.maps.extension.compose.style.sources.SourceState.Holder? rasterDemSourceStateHolder, java.util.Map cachedProperties, boolean initial); + method public com.mapbox.maps.extension.compose.style.sources.SourceState.Holder? component1(); + method public java.util.Map component2(); + method public boolean component3(); + method public com.mapbox.maps.extension.compose.style.terrain.generated.TerrainState.Holder copy(com.mapbox.maps.extension.compose.style.sources.SourceState.Holder? rasterDemSourceStateHolder, java.util.Map cachedProperties, boolean initial); + method public java.util.Map getCachedProperties(); + method public boolean getInitial(); + method public com.mapbox.maps.extension.compose.style.sources.SourceState.Holder? getRasterDemSourceStateHolder(); + property public final java.util.Map cachedProperties; + property public final boolean initial; + property public final com.mapbox.maps.extension.compose.style.sources.SourceState.Holder? rasterDemSourceStateHolder; + } + + public final class TerrainStateKt { + method @androidx.compose.runtime.Composable @com.mapbox.maps.MapboxExperimental public static inline com.mapbox.maps.extension.compose.style.terrain.generated.TerrainState rememberTerrainState(com.mapbox.maps.extension.compose.style.sources.generated.RasterDemSourceState rasterDemSourceState, String? key = null, kotlin.jvm.functions.Function1 init = {}); } } diff --git a/extension-compose/api/extension-compose.api b/extension-compose/api/extension-compose.api index 55224bdbde..9681ac0c95 100644 --- a/extension-compose/api/extension-compose.api +++ b/extension-compose/api/extension-compose.api @@ -297,8 +297,8 @@ public final class com/mapbox/maps/extension/compose/style/StyleImportsConfig { } public final class com/mapbox/maps/extension/compose/style/StyleKt { - public static final fun GenericStyle (Ljava/lang/String;Lcom/mapbox/maps/extension/compose/style/SlotsContent;Lcom/mapbox/maps/extension/compose/style/LayerPositionedContent;Lcom/mapbox/maps/extension/compose/style/StyleImportsConfig;Lcom/mapbox/maps/extension/compose/style/projection/Projection;Lcom/mapbox/maps/extension/compose/style/atmosphere/generated/AtmosphereState;Landroidx/compose/runtime/Composer;II)V - public static final fun MapStyle (Ljava/lang/String;Lcom/mapbox/maps/extension/compose/style/projection/Projection;Lcom/mapbox/maps/extension/compose/style/atmosphere/generated/AtmosphereState;Landroidx/compose/runtime/Composer;II)V + public static final fun GenericStyle (Ljava/lang/String;Lcom/mapbox/maps/extension/compose/style/SlotsContent;Lcom/mapbox/maps/extension/compose/style/LayerPositionedContent;Lcom/mapbox/maps/extension/compose/style/StyleImportsConfig;Lcom/mapbox/maps/extension/compose/style/projection/Projection;Lcom/mapbox/maps/extension/compose/style/atmosphere/generated/AtmosphereState;Lcom/mapbox/maps/extension/compose/style/terrain/generated/TerrainState;Landroidx/compose/runtime/Composer;II)V + public static final fun MapStyle (Ljava/lang/String;Lcom/mapbox/maps/extension/compose/style/projection/Projection;Lcom/mapbox/maps/extension/compose/style/atmosphere/generated/AtmosphereState;Lcom/mapbox/maps/extension/compose/style/terrain/generated/TerrainState;Landroidx/compose/runtime/Composer;II)V public static final fun layerPositionedContent (Lkotlin/jvm/functions/Function1;)Lcom/mapbox/maps/extension/compose/style/LayerPositionedContent; public static final fun slotsContent (Lkotlin/jvm/functions/Function1;)Lcom/mapbox/maps/extension/compose/style/SlotsContent; public static final fun styleImportsConfig (Lkotlin/jvm/functions/Function1;)Lcom/mapbox/maps/extension/compose/style/StyleImportsConfig; @@ -5239,6 +5239,69 @@ public final class com/mapbox/maps/extension/compose/style/standard/LightPreset$ } public final class com/mapbox/maps/extension/compose/style/standard/MapboxStandardStyleKt { - public static final fun MapboxStandardStyle (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lcom/mapbox/maps/extension/compose/style/standard/LightPreset;Lcom/mapbox/maps/extension/compose/style/projection/Projection;Lcom/mapbox/maps/extension/compose/style/atmosphere/generated/AtmosphereState;Landroidx/compose/runtime/Composer;II)V + public static final fun MapboxStandardStyle (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lcom/mapbox/maps/extension/compose/style/standard/LightPreset;Lcom/mapbox/maps/extension/compose/style/projection/Projection;Lcom/mapbox/maps/extension/compose/style/atmosphere/generated/AtmosphereState;Lcom/mapbox/maps/extension/compose/style/terrain/generated/TerrainState;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/mapbox/maps/extension/compose/style/terrain/generated/Exaggeration { + public static final field Companion Lcom/mapbox/maps/extension/compose/style/terrain/generated/Exaggeration$Companion; + public fun (D)V + public fun (Lcom/mapbox/bindgen/Value;)V + public fun (Lcom/mapbox/maps/extension/style/expressions/generated/Expression;)V + public final fun component1 ()Lcom/mapbox/bindgen/Value; + public final fun copy (Lcom/mapbox/bindgen/Value;)Lcom/mapbox/maps/extension/compose/style/terrain/generated/Exaggeration; + public static synthetic fun copy$default (Lcom/mapbox/maps/extension/compose/style/terrain/generated/Exaggeration;Lcom/mapbox/bindgen/Value;ILjava/lang/Object;)Lcom/mapbox/maps/extension/compose/style/terrain/generated/Exaggeration; + public fun equals (Ljava/lang/Object;)Z + public final fun getValue ()Lcom/mapbox/bindgen/Value; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/mapbox/maps/extension/compose/style/terrain/generated/Exaggeration$Companion { + public final fun getDefault ()Lcom/mapbox/maps/extension/compose/style/terrain/generated/Exaggeration; +} + +public final class com/mapbox/maps/extension/compose/style/terrain/generated/TerrainState { + public static final field $stable I + public static final field Companion Lcom/mapbox/maps/extension/compose/style/terrain/generated/TerrainState$Companion; + public fun (Lcom/mapbox/maps/extension/compose/style/sources/generated/RasterDemSourceState;Ljava/util/Map;)V + public synthetic fun (Lcom/mapbox/maps/extension/compose/style/sources/generated/RasterDemSourceState;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getExaggeration ()Lcom/mapbox/maps/extension/compose/style/terrain/generated/Exaggeration; + public final fun setExaggeration (Lcom/mapbox/maps/extension/compose/style/terrain/generated/Exaggeration;)V +} + +public final class com/mapbox/maps/extension/compose/style/terrain/generated/TerrainState$Companion { + public final fun getDisabled ()Lcom/mapbox/maps/extension/compose/style/terrain/generated/TerrainState; + public final fun getSaver ()Landroidx/compose/runtime/saveable/Saver; +} + +public final class com/mapbox/maps/extension/compose/style/terrain/generated/TerrainState$Holder : android/os/Parcelable { + public static final field $stable I + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun (Lcom/mapbox/maps/extension/compose/style/sources/SourceState$Holder;Ljava/util/Map;Z)V + public final fun component1 ()Lcom/mapbox/maps/extension/compose/style/sources/SourceState$Holder; + public final fun component2 ()Ljava/util/Map; + public final fun component3 ()Z + public final fun copy (Lcom/mapbox/maps/extension/compose/style/sources/SourceState$Holder;Ljava/util/Map;Z)Lcom/mapbox/maps/extension/compose/style/terrain/generated/TerrainState$Holder; + public static synthetic fun copy$default (Lcom/mapbox/maps/extension/compose/style/terrain/generated/TerrainState$Holder;Lcom/mapbox/maps/extension/compose/style/sources/SourceState$Holder;Ljava/util/Map;ZILjava/lang/Object;)Lcom/mapbox/maps/extension/compose/style/terrain/generated/TerrainState$Holder; + public fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getCachedProperties ()Ljava/util/Map; + public final fun getInitial ()Z + public final fun getRasterDemSourceStateHolder ()Lcom/mapbox/maps/extension/compose/style/sources/SourceState$Holder; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class com/mapbox/maps/extension/compose/style/terrain/generated/TerrainState$Holder$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/mapbox/maps/extension/compose/style/terrain/generated/TerrainState$Holder; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/mapbox/maps/extension/compose/style/terrain/generated/TerrainState$Holder; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/mapbox/maps/extension/compose/style/terrain/generated/TerrainStateKt { + public static final fun rememberTerrainState (Lcom/mapbox/maps/extension/compose/style/sources/generated/RasterDemSourceState;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Lcom/mapbox/maps/extension/compose/style/terrain/generated/TerrainState; } diff --git a/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/Style.kt b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/Style.kt index 646c0d76cd..c52a69c22e 100644 --- a/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/Style.kt +++ b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/Style.kt @@ -16,6 +16,7 @@ import com.mapbox.maps.extension.compose.style.internal.StyleConfig import com.mapbox.maps.extension.compose.style.internal.StyleLayerPosition import com.mapbox.maps.extension.compose.style.internal.StyleSlot import com.mapbox.maps.extension.compose.style.projection.Projection +import com.mapbox.maps.extension.compose.style.terrain.generated.TerrainState /** * A simple composable function to set the style to the map without slots or import configs. @@ -23,6 +24,7 @@ import com.mapbox.maps.extension.compose.style.projection.Projection * @param style The Style JSON or Style Uri to be set to the map. * @param projection The projection to be set to the map. Defaults to [Projection.default] meaning that projection value is taken from the [style] definition. * @param atmosphereState The atmosphere to be set to the map. Defaults to [AtmosphereState.default] meaning that atmosphere is the default defined in [style] definition. + * @param terrainState The terrain to be set to the map. Defaults to initial state meaning no custom terrain is added, default value is taken from [style] definition. */ @Composable @MapboxStyleComposable @@ -31,8 +33,14 @@ public fun MapStyle( style: String, projection: Projection = Projection.default, atmosphereState: AtmosphereState = AtmosphereState.default, + terrainState: TerrainState = TerrainState.initial, ) { - GenericStyle(style = style, projection = projection, atmosphereState = atmosphereState) + GenericStyle( + style = style, + projection = projection, + atmosphereState = atmosphereState, + terrainState = terrainState + ) } /** @@ -205,6 +213,7 @@ public data class ImportConfig internal constructor( * @param styleImportsConfig The style import configurations for all the style imports in the style. You can use [styleImportsConfig] to create it. * @param projection The projection to be set to the map. Defaults to [Projection.default] meaning that projection value is taken from the [style] definition. * @param atmosphereState The atmosphere to be set to the map. Defaults to [AtmosphereState.default] meaning that atmosphere is the default defined in [style] definition. + * @param terrainState The terrain to be set to the map. Defaults to initial state meaning no custom terrain is added, default value is taken from [style] definition. */ @Composable @MapboxStyleComposable @@ -216,6 +225,7 @@ public fun GenericStyle( styleImportsConfig: StyleImportsConfig = StyleImportsConfig(), projection: Projection = Projection.default, atmosphereState: AtmosphereState = AtmosphereState.default, + terrainState: TerrainState = TerrainState.initial, ) { // When style is changed, we want to trigger the recompose of the whole style node key(style) { @@ -230,6 +240,7 @@ public fun GenericStyle( mapboxMap = mapApplier.mapView.mapboxMap, projection = projection, atmosphereState = atmosphereState, + terrainState = terrainState, ) }, update = { @@ -239,6 +250,9 @@ public fun GenericStyle( update(atmosphereState) { updateAtmosphere(atmosphereState) } + update(terrainState) { + updateTerrain(terrainState) + } } ) { slotsContent.entries.forEach { diff --git a/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/internal/MapStyleNode.kt b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/internal/MapStyleNode.kt index 972f1a3c53..3eee1feedc 100644 --- a/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/internal/MapStyleNode.kt +++ b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/internal/MapStyleNode.kt @@ -9,6 +9,7 @@ import com.mapbox.maps.coroutine.styleDataLoadedEvents import com.mapbox.maps.extension.compose.internal.MapNode import com.mapbox.maps.extension.compose.style.atmosphere.generated.AtmosphereState import com.mapbox.maps.extension.compose.style.projection.Projection +import com.mapbox.maps.extension.compose.style.terrain.generated.TerrainState import com.mapbox.maps.logD import com.mapbox.maps.logW import kotlinx.coroutines.CoroutineName @@ -29,6 +30,7 @@ internal class MapStyleNode( val mapboxMap: MapboxMap, private val projection: Projection, var atmosphereState: AtmosphereState, + var terrainState: TerrainState, ) : MapNode() { val coroutineScope = @@ -66,6 +68,7 @@ internal class MapStyleNode( updateStyle(style) updateProjection(projection) updateAtmosphere(atmosphereState) + updateTerrain(terrainState) } override fun onRemoved(parent: MapNode) { @@ -89,6 +92,7 @@ internal class MapStyleNode( override fun onClear() { super.onClear() atmosphereState.applier.detach() + terrainState.applier.detach() children.forEach { it.onClear() } } @@ -124,6 +128,26 @@ internal class MapStyleNode( } } + internal fun updateTerrain(terrainState: TerrainState) { + val previousTerrainState = this.terrainState + this.terrainState = terrainState + // we have to detach (in a sense of cancelling property collector jobs) the previous state + // before attaching the new state; otherwise the jobs will be duplicated + previousTerrainState.applier.detach() + coroutineScope.launch { + styleDataLoaded.collect { + // we have to treat terrain as some sort of persistent layer and attach / detach map accordingly + previousTerrainState.rasterDemSourceState?.let { + it.detachFromLayer("mapbox-terrain-${it.sourceId}", mapboxMap) + } + terrainState.rasterDemSourceState?.let { + it.attachToLayer("mapbox-terrain-${it.sourceId}", mapboxMap) + } + terrainState.applier.attachTo(mapboxMap) + } + } + } + override fun toString(): String { return "MapStyleNode(style=$style)" } diff --git a/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/sources/SourceState.kt b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/sources/SourceState.kt index dc6e96b1e3..12c1b8ddba 100644 --- a/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/sources/SourceState.kt +++ b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/sources/SourceState.kt @@ -45,7 +45,6 @@ private data class PropertyDetails( * * @param sourceId The id of the source state. * @param sourceType The type of the source in plain text. - * @param builderProperties The immutable properties of the source. * @param initialProperties The initial mutable properties of the source. * @param initialGeoJsonData The initial [GeoJSONData] of the source to be used for [GeoJsonSourceState]. */ diff --git a/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/standard/MapboxStandardStyle.kt b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/standard/MapboxStandardStyle.kt index cfc705b433..28f2af9944 100644 --- a/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/standard/MapboxStandardStyle.kt +++ b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/standard/MapboxStandardStyle.kt @@ -12,6 +12,7 @@ import com.mapbox.maps.extension.compose.style.atmosphere.generated.AtmosphereSt import com.mapbox.maps.extension.compose.style.projection.Projection import com.mapbox.maps.extension.compose.style.slotsContent import com.mapbox.maps.extension.compose.style.styleImportsConfig +import com.mapbox.maps.extension.compose.style.terrain.generated.TerrainState import com.mapbox.maps.extension.style.expressions.generated.Expression /** @@ -77,8 +78,9 @@ public data class LightPreset(public val value: Value) { * @param middleSlot The content to be set to the middle slot of the Mapbox Standard style. * @param bottomSlot The content to be set to the bottom slot of the Mapbox Standard style. * @param lightPreset The [LightPreset] settings of the Mapbox Standard Style, available lightPresets including "day", "night", "dawn", "dusk". - * @param projection The projection to be set to the map. Defaults to [Projection.default] meaning that projection value is taken from the [style] definition. - * @param atmosphereState The atmosphere to be set to the map. Defaults to [AtmosphereState.default] meaning that atmosphere is the default defined in [style] definition. + * @param projection The projection to be set to the map. Defaults to [Projection.default] meaning that projection value is taken from the Standard style definition. + * @param atmosphereState The atmosphere to be set to the map. Defaults to [AtmosphereState.default] meaning that atmosphere is the default defined in Standard style definition. + * @param terrainState The terrain to be set to the map. Defaults to initial state meaning no custom terrain is added, default value is taken from Standard style definition. */ @Composable @MapboxStyleComposable @@ -90,6 +92,7 @@ public fun MapboxStandardStyle( lightPreset: LightPreset = LightPreset.default, projection: Projection = Projection.default, atmosphereState: AtmosphereState = AtmosphereState.default, + terrainState: TerrainState = TerrainState.initial, ) { GenericStyle( style = Style.STANDARD, @@ -104,6 +107,7 @@ public fun MapboxStandardStyle( } }, projection = projection, - atmosphereState = atmosphereState + atmosphereState = atmosphereState, + terrainState = terrainState, ) } \ No newline at end of file diff --git a/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/terrain/TerrainStateApplier.kt b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/terrain/TerrainStateApplier.kt new file mode 100644 index 0000000000..053b264c99 --- /dev/null +++ b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/terrain/TerrainStateApplier.kt @@ -0,0 +1,138 @@ +package com.mapbox.maps.extension.compose.style.terrain + +import com.mapbox.bindgen.Value +import com.mapbox.maps.MapboxExperimental +import com.mapbox.maps.MapboxMap +import com.mapbox.maps.extension.compose.style.sources.generated.RasterDemSourceState +import com.mapbox.maps.extension.compose.style.terrain.generated.TerrainState +import com.mapbox.maps.logD +import com.mapbox.maps.logE +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +/** + * A [MutableStateFlow] to keep the latest value for a Terrain Property + */ +private typealias PropertyValueFlow = MutableStateFlow + +@OptIn(MapboxExperimental::class) +internal open class TerrainStateApplier internal constructor( + private val rasterDemSourceState: RasterDemSourceState?, + initialProperties: Map, + internal val initial: Boolean, + private val coroutineScope: CoroutineScope = CoroutineScope( + Dispatchers.Main.immediate + SupervisorJob() + CoroutineName( + "TerrainStateScope" + ) + ), +) { + private var propertiesUpdateJobs: MutableList = mutableListOf() + + /** + * A shared flow to keep track of each property own flow ([PropertyValueFlow]). + * Every time a new [Pair] is emitted in this flow we will start collecting its flow + * ([PropertyValueFlow]), see [startCollectingPropertyFlows]. + */ + private val propertiesFlowsToCollect = + MutableSharedFlow>(replay = Channel.UNLIMITED) + + init { + initialProperties.forEach { + setProperty(it.key, it.value) + } + } + + internal fun attachTo(mapboxMap: MapboxMap) { + if (initial) { + return + } + mapboxMap.setStyleTerrain( + properties = if (rasterDemSourceState != null) { + Value( + hashMapOf().also { map -> + map["source"] = Value(rasterDemSourceState.sourceId) + // Get the most recent list of properties and their values + map.putAll(propertiesFlowsToCollect.replayCache.associate { it.first to it.second.value }) + logD(TAG, "Setting all properties in one go: $map") + } + ).also { + logD(TAG, "Adding terrain: $this") + } + } else { + Value.nullValue().also { + logD(TAG, "Removing terrain: $this") + } + }, + ).onError { + logE(TAG, "Failed to add terrain: $it") + }.onValue { + if (rasterDemSourceState != null) { + logD(TAG, "Added terrain: $this") + } + } + startCollectingPropertyFlows(mapboxMap) + } + + private fun startCollectingPropertyFlows(mapboxMap: MapboxMap) { + val collectNewPropertiesJob = coroutineScope.launch { + propertiesFlowsToCollect.collect { (name: String, valueFlow: MutableStateFlow) -> + val updatePropertyJob = coroutineScope.launch { + valueFlow.collect { value -> + logD(TAG, "settingProperty: name=$name, value=$value ...") + mapboxMap.setStyleTerrainProperty(name, value).onValue { + logD(TAG, "settingProperty: name=$name, value=$value executed") + }.onError { error -> + logE(TAG, "Failed to set terrain property $name as $value: $error") + } + } + } + propertiesUpdateJobs.add(updatePropertyJob) + } + } + propertiesUpdateJobs.add(collectNewPropertiesJob) + } + + internal fun detach() { + // Stop any collect job that changes the source properties + propertiesUpdateJobs.forEach(Job::cancel) + propertiesUpdateJobs.clear() + } + + internal fun setProperty(name: String, value: Value) { + logD(TAG, "setProperty() called with: name = $name, value = $value") + val setOfFlows = propertiesFlowsToCollect.replayCache + val currentFlow: Pair>? = setOfFlows.firstOrNull { + it.first == name + } + if (currentFlow != null) { + currentFlow.second.value = value + } else { + logD(TAG, "setProperty: emitting new property to listen to: $name") + // Add the new property to the set of property flows we want to collect + propertiesFlowsToCollect.tryEmit(name to MutableStateFlow(value)) + } + } + + internal fun getProperty(name: String): Value? = + propertiesFlowsToCollect.replayCache.firstOrNull { + it.first == name + }?.second?.value + + @OptIn(MapboxExperimental::class) + internal fun save(): TerrainState.Holder = TerrainState.Holder( + rasterDemSourceState?.save(), + propertiesFlowsToCollect.replayCache.associate { it.first to it.second.value }, + initial + ) + + private companion object { + private const val TAG = "TerrainStateApplier" + } +} \ No newline at end of file diff --git a/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/terrain/generated/TerrainProperties.kt b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/terrain/generated/TerrainProperties.kt new file mode 100644 index 0000000000..8c5345ceb9 --- /dev/null +++ b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/terrain/generated/TerrainProperties.kt @@ -0,0 +1,40 @@ +// This file is generated. +@file:Suppress("ktlint:filename") +package com.mapbox.maps.extension.compose.style.terrain.generated + +import androidx.compose.runtime.Immutable +import com.mapbox.bindgen.Value +import com.mapbox.maps.MapboxExperimental +import com.mapbox.maps.extension.compose.style.internal.ComposeTypeUtils +import com.mapbox.maps.extension.style.expressions.generated.Expression + +/** + * Exaggerates the elevation of the terrain by multiplying the data from the DEM with this value. + * + * @param value the property wrapped in [Value] to be used with native renderer. + */ +@Immutable +@MapboxExperimental +public data class Exaggeration(public val value: Value) { + /** + * Construct the Exaggeration with [Double]. + */ + public constructor(value: Double) : this(ComposeTypeUtils.wrapToValue(value)) + /** + * Construct the Exaggeration with [Mapbox Expression](https://docs.mapbox.com/style-spec/reference/expressions/). + */ + public constructor(expression: Expression) : this(expression as Value) + + /** + * Public companion object. + */ + public companion object { + internal const val NAME: String = "exaggeration" + + /** + * Default value for [Exaggeration], setting default will result in restoring the property value defined in the style. + */ + public val default: Exaggeration = Exaggeration(Value.nullValue()) + } +} +// End of generated file. \ No newline at end of file diff --git a/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/terrain/generated/TerrainState.kt b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/terrain/generated/TerrainState.kt new file mode 100644 index 0000000000..97dd6c192c --- /dev/null +++ b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/terrain/generated/TerrainState.kt @@ -0,0 +1,140 @@ +// This file is generated. + +package com.mapbox.maps.extension.compose.style.terrain.generated + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import com.mapbox.bindgen.Value +import com.mapbox.maps.MapboxExperimental +import com.mapbox.maps.extension.compose.style.internal.ValueParceler +import com.mapbox.maps.extension.compose.style.sources.SourceState +import com.mapbox.maps.extension.compose.style.sources.generated.RasterDemSourceState +import com.mapbox.maps.extension.compose.style.terrain.TerrainStateApplier +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.TypeParceler + +/** + * Create and [rememberSaveable] a [TerrainState] using [TerrainState.Saver]. + * [init] will be called when the [TerrainState] is first created to configure its + * initial state. + * + * @param rasterDemSourceState Mandatory [RasterDemSourceState] to which terrain will be added. + * @param key An optional key to be used as a key for the saved value. If not provided we use the + * automatically generated by the Compose runtime which is unique for the every exact code location + * in the composition tree. + * @param init A function initialise this [TerrainState]. + */ +@Composable +@MapboxExperimental +public inline fun rememberTerrainState( + rasterDemSourceState: RasterDemSourceState, + key: String? = null, + crossinline init: TerrainState.() -> Unit = {} +): TerrainState = rememberSaveable(key = key, saver = TerrainState.Saver) { + TerrainState(rasterDemSourceState).apply(init) +} + +/** + * A global modifier that elevates layers and markers based on a DEM data source. + * + * @see [The online documentation](https://docs.mapbox.com/mapbox-gl-js/style-spec/terrain/) + */ +@MapboxExperimental +public class TerrainState { + + internal val rasterDemSourceState: RasterDemSourceState? + internal val applier: TerrainStateApplier + + public constructor( + /** + * The [RasterDemSourceState] that drives the terrain. + */ + rasterDemSourceState: RasterDemSourceState, + /** + * The initial mutable properties of the source. + */ + initialProperties: Map = mapOf() + ) { + this.rasterDemSourceState = rasterDemSourceState + this.applier = TerrainStateApplier( + rasterDemSourceState = rasterDemSourceState, + initialProperties = initialProperties, + initial = false + ) + } + + internal constructor(initial: Boolean) { + this.rasterDemSourceState = null + this.applier = TerrainStateApplier( + rasterDemSourceState = null, + initialProperties = emptyMap(), + initial = initial + ) + } + + /** + * Exaggerates the elevation of the terrain by multiplying the data from the DEM with this value. + */ + public var exaggeration: Exaggeration + get() = Exaggeration(applier.getProperty(Exaggeration.NAME) ?: Exaggeration.default.value) + set(value) { + applier.setProperty(Exaggeration.NAME, value.value) + } + + /** + * Terrain Holder class to be used within [Saver]. + */ + @MapboxExperimental + @Parcelize + @TypeParceler + public data class Holder( + /** + * Cached holder for [RasterDemSourceState]. + */ + val rasterDemSourceStateHolder: SourceState.Holder?, + /** + * Cached properties. + */ + val cachedProperties: Map, + /** + * If it is initial value meaning that no runtime terrain was set. + */ + val initial: Boolean, + ) : Parcelable + + /** + * Public companion object. + */ + public companion object { + /** + * The default saver implementation for [TerrainState] + */ + public val Saver: Saver = Saver( + save = { it.applier.save() }, + restore = { + it.rasterDemSourceStateHolder?.let { rasterDemSourceState -> + TerrainState( + RasterDemSourceState( + sourceId = rasterDemSourceState.sourcedId, + initialProperties = rasterDemSourceState.cachedProperties + ), + initialProperties = it.cachedProperties, + ) + } ?: TerrainState(it.initial) + } + ) + + /** + * Initial value for [TerrainState], meaning that no terrain is added. + */ + internal val initial: TerrainState = TerrainState(initial = true) + + /** + * Disabled value for [TerrainState], setting disabled will result in removing terrain. + */ + public val disabled: TerrainState = TerrainState(initial = false) + } +} +// End of generated file. \ No newline at end of file