1+ package com.mapbox.maps.compose.testapp.examples.style
2+
3+ import android.graphics.BitmapFactory
4+ import android.os.Bundle
5+ import android.util.Log
6+ import androidx.activity.ComponentActivity
7+ import androidx.activity.compose.setContent
8+ import androidx.compose.foundation.layout.fillMaxSize
9+ import androidx.compose.runtime.getValue
10+ import androidx.compose.runtime.mutableStateOf
11+ import androidx.compose.runtime.remember
12+ import androidx.compose.runtime.setValue
13+ import androidx.compose.ui.Modifier
14+ import com.mapbox.bindgen.Value
15+ import com.mapbox.geojson.Point
16+ import com.mapbox.maps.LayerPosition
17+ import com.mapbox.maps.compose.testapp.ExampleScaffold
18+ import com.mapbox.maps.compose.testapp.R
19+ import com.mapbox.maps.compose.testapp.ui.theme.MapboxMapComposeTheme
20+ import com.mapbox.maps.extension.compose.MapEffect
21+ import com.mapbox.maps.extension.compose.MapboxMap
22+ import com.mapbox.maps.extension.compose.animation.viewport.rememberMapViewportState
23+ import com.mapbox.maps.extension.compose.rememberMapState
24+ import com.mapbox.maps.extension.compose.style.standard.MapboxStandardStyle
25+ import com.mapbox.maps.extension.compose.style.standard.rememberStandardStyleState
26+ import com.mapbox.maps.interactions.FeatureState
27+ import com.mapbox.maps.interactions.FeaturesetFeature
28+
29+ /* *
30+ * Example demonstrating the experimental Appearances API for dynamic icon states.
31+ * Shows how to use appearances with feature-state to change icon images based on user interaction.
32+ * - Default: hotel icon
33+ * - Currently Selected: hotel-active icon
34+ * - Previously Clicked: hotel-clicked icon
35+ */
36+ public class AppearancesActivity : ComponentActivity () {
37+
38+ override fun onCreate (savedInstanceState : Bundle ? ) {
39+ super .onCreate(savedInstanceState)
40+
41+ val hotelBitmap = BitmapFactory .decodeResource(resources, R .drawable.hotel)
42+ val hotelActiveBitmap = BitmapFactory .decodeResource(resources, R .drawable.hotel_active)
43+ val hotelClickedBitmap = BitmapFactory .decodeResource(resources, R .drawable.hotel_clicked)
44+ setContent {
45+ var selectedFeature by remember { mutableStateOf<FeaturesetFeature <FeatureState >? > (null ) }
46+ val clickedFeatures = remember { mutableSetOf<FeaturesetFeature <FeatureState >>() }
47+
48+ MapboxMapComposeTheme {
49+ ExampleScaffold {
50+ MapboxMap (
51+ modifier = Modifier .fillMaxSize(),
52+ mapViewportState = rememberMapViewportState {
53+ setCameraOptions {
54+ center(Point .fromLngLat(1.8447281852 , 42.10025506 ))
55+ zoom(15.5 )
56+ pitch(0.0 )
57+ bearing(0.0 )
58+ }
59+ },
60+ mapState = rememberMapState(),
61+ style = {
62+ MapboxStandardStyle (
63+ standardStyleState = rememberStandardStyleState {
64+ // When a hotel icon is clicked, set the currentlySelected feature state to true,
65+ // unselect the previous one if any, and store this feature both as the selected
66+ // feature and in the list of features that have been clicked
67+ interactionsState.onLayerClicked(" points" ) { feature, _ ->
68+ // Clear the currently selected feature by resetting its feature state
69+ selectedFeature?.setFeatureState(
70+ FeatureState { addBooleanState(CURRENTLY_SELECTED_KEY , false ) }
71+ ) {}
72+
73+ // Store this feature as the currently selected feature and in the list
74+ // of features that have been clicked
75+ clickedFeatures.add(feature)
76+ feature.setFeatureState(
77+ FeatureState {
78+ addBooleanState(CURRENTLY_SELECTED_KEY , true )
79+ addBooleanState(HAS_BEEN_CLICKED_KEY , true )
80+ }
81+ ) {}
82+ selectedFeature = feature
83+ true
84+ }
85+
86+ // When the map is clicked outside of any feature, unselect the currently selected
87+ // feature if there's any, or remove all features from the list of features that
88+ // have been clicked to get back to the initial state
89+ interactionsState.onMapClicked {
90+ if (selectedFeature != null ) {
91+ // Unselect the currently selected feature
92+ selectedFeature?.setFeatureState(
93+ FeatureState { addBooleanState(CURRENTLY_SELECTED_KEY , false ) }
94+ ) {}
95+ selectedFeature = null
96+ } else {
97+ // Reset the state of all features to the default one
98+ clickedFeatures.forEach { clickedFeature ->
99+ clickedFeature.setFeatureState(
100+ FeatureState { addBooleanState(HAS_BEEN_CLICKED_KEY , false ) }
101+ ) {}
102+ }
103+ clickedFeatures.clear()
104+ }
105+ true
106+ }
107+ }
108+ )
109+ }
110+ ) {
111+ // Add images, source, and layer after the style has loaded
112+ MapEffect (Unit ) { mapView ->
113+ mapView.mapboxMap.subscribeStyleLoaded {
114+ mapView.mapboxMap.style?.let { style ->
115+
116+ // Load an image for every feature state
117+ style.addImage(" hotel" , hotelBitmap)
118+ style.addImage(" hotel-active" , hotelActiveBitmap)
119+ style.addImage(" hotel-clicked" , hotelClickedBitmap)
120+
121+ // Add a GeoJSON source with hotel locations
122+ style.addStyleSource(
123+ " points" ,
124+ Value .fromJson(
125+ """
126+ {
127+ "type": "geojson",
128+ "data": $HOTEL_GEOJSON
129+ }
130+ """ .trimIndent()
131+ ).value!!
132+ )
133+
134+ // Add a layer to show an icon on every point with appearances
135+ // - When currentlySelected feature state is true: use "hotel-active" icon
136+ // - When hasBeenClicked feature state is true and currentlySelected is not: use "hotel-clicked" icon
137+ // - Otherwise: use the default "hotel" icon defined in layout
138+ // Appearances are experimental and subject to change in future versions
139+ try {
140+ style.addStyleLayer(
141+ Value .fromJson(POINTS_LAYER_JSON ).value!! ,
142+ LayerPosition (null , null , null )
143+ )
144+ } catch (e: Exception ) {
145+ Log .e(" Appearances" , " Error adding layer" , e)
146+ }
147+ }
148+ }
149+ }
150+ }
151+ }
152+ }
153+ }
154+ }
155+
156+ private companion object {
157+ private const val CURRENTLY_SELECTED_KEY = " currentlySelected"
158+ private const val HAS_BEEN_CLICKED_KEY = " hasBeenClicked"
159+ }
160+ }
161+
162+ private const val POINTS_LAYER_JSON = """
163+ {
164+ "id": "points",
165+ "type": "symbol",
166+ "source": "points",
167+ "layout": {
168+ "icon-allow-overlap": true,
169+ "icon-image": "hotel",
170+ "icon-size": 1.0,
171+ "icon-anchor": "center"
172+ },
173+ "appearances": [
174+ {
175+ "name": "currently-selected",
176+ "condition": ["boolean", ["feature-state", "currentlySelected"], false],
177+ "properties": {
178+ "icon-image": "hotel-active"
179+ }
180+ },
181+ {
182+ "name": "has-been-clicked",
183+ "condition": ["boolean", ["feature-state", "hasBeenClicked"], false],
184+ "properties": {
185+ "icon-image": "hotel-clicked"
186+ }
187+ }
188+ ]
189+ }
190+ """
191+
192+ private const val HOTEL_GEOJSON = """
193+ {
194+ "type": "FeatureCollection",
195+ "features": [
196+ {"type": "Feature", "id": "1", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8452993238082342, 42.100164223399275]}},
197+ {"type": "Feature", "id": "2", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8438590191857145, 42.1004178052402]}},
198+ {"type": "Feature", "id": "3", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.844225198327564, 42.10130533369667]}},
199+ {"type": "Feature", "id": "4", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8443594640122, 42.0990955459275]}},
200+ {"type": "Feature", "id": "5", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8449697625811154, 42.09869705141318]}},
201+ {"type": "Feature", "id": "6", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8471058075726603, 42.09978384873651]}},
202+ {"type": "Feature", "id": "7", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8455739474818813, 42.10182152060625]}},
203+ {"type": "Feature", "id": "8", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8427787800360136, 42.10039061289771]}},
204+ {"type": "Feature", "id": "9", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8433280487479635, 42.0994396753579]}}
205+ ]
206+ }
207+ """
0 commit comments