Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,18 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" />
</activity>
<activity
android:name=".activity.feature.FeatureStateVectorActivity"
android:description="@string/description_feature_state_vector"
android:exported="true"
android:label="@string/activity_feature_state_vector">
<meta-data
android:name="@string/category"
android:value="@string/category_features" />
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" />
</activity>
<activity
android:name=".activity.style.SymbolGeneratorActivity"
android:description="@string/description_symbol_generator"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package org.maplibre.android.testapp.activity.feature

import android.graphics.Color
import android.graphics.PointF
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.JsonObject
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.Style
import org.maplibre.android.style.expressions.Expression.*
import org.maplibre.android.style.layers.FillLayer
import org.maplibre.android.style.layers.LineLayer
import org.maplibre.android.style.layers.PropertyFactory.fillColor
import org.maplibre.android.style.layers.PropertyFactory.fillOpacity
import org.maplibre.android.style.layers.PropertyFactory.lineColor
import org.maplibre.android.style.layers.PropertyFactory.lineWidth
import org.maplibre.android.style.sources.VectorSource
import org.maplibre.android.testapp.R
import org.maplibre.android.testapp.styles.TestStyles

/**
* Test activity showcasing interactive styling with feature state on a vector
* tile source.
*
* This mirrors [FeatureStateActivity] (which uses a GeoJSON source) but shows the
* two things that differ for a vector source:
*
* 1. Every feature-state call takes a source-layer id, because vector tiles are
* organized into named source layers. (A GeoJSON source has a single implicit
* layer, so its API omits it.)
* 2. The target features must carry an id in the vector tile data — feature state
* is keyed by feature identifier. The MapLibre demo tiles used here assign one
* per country, so selection binds correctly.
*/
class FeatureStateVectorActivity : AppCompatActivity() {
companion object {
private const val COUNTRIES_SOURCE_ID = "countries-source"
// The source layer that holds the country polygons in the demo tiles.
private const val COUNTRIES_SOURCE_LAYER = "countries"
private const val FILL_LAYER_ID = "country-fills"
private const val LINE_LAYER_ID = "country-borders"
private const val TILES_URL = "https://demotiles.maplibre.org/tiles/tiles.json"
}

private lateinit var mapView: MapView
private lateinit var maplibreMap: MapLibreMap
private lateinit var countriesSource: VectorSource

private val mapClickListener = MapLibreMap.OnMapClickListener { point ->
handleMapClick(point)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_query_features_point)

mapView = findViewById<View>(R.id.mapView) as MapView
mapView.onCreate(savedInstanceState)
mapView.getMapAsync { map ->
maplibreMap = map
maplibreMap.moveCamera(
CameraUpdateFactory.newLatLngZoom(LatLng(20.0, 0.0), 1.0)
)
maplibreMap.setStyle(TestStyles.DEMOTILES) { style ->
addCountriesLayer(style)
maplibreMap.addOnMapClickListener(mapClickListener)
}
}
}

private fun addCountriesLayer(style: Style) {
// The MapLibre demo tiles are a world map of country polygons.
val source = VectorSource(COUNTRIES_SOURCE_ID, TILES_URL)
style.addSource(source)
countriesSource = source

val selectedFill = Color.parseColor("#d94d41")
val selectedBorder = Color.parseColor("#8f241f")
val defaultBorder = Color.parseColor("#888888")

// Fill layer: transparent by default so the basemap shows through, and
// highlighted only where the "selected" feature state is set.
style.addLayer(
FillLayer(FILL_LAYER_ID, COUNTRIES_SOURCE_ID)
// A vector style layer must be told which source layer to draw.
.withSourceLayer(COUNTRIES_SOURCE_LAYER)
.withProperties(
fillColor(color(selectedFill)),
fillOpacity(
switchCase(
toBool(featureState("selected")),
literal(0.5f),
literal(0.0f)
)
)
)
)

// Border layer: a thin border everywhere, thicker and red where selected.
style.addLayer(
LineLayer(LINE_LAYER_ID, COUNTRIES_SOURCE_ID)
.withSourceLayer(COUNTRIES_SOURCE_LAYER)
.withProperties(
lineColor(
switchCase(
toBool(featureState("selected")),
color(selectedBorder),
color(defaultBorder)
)
),
lineWidth(
switchCase(
toBool(featureState("selected")),
literal(2.0f),
literal(0.5f)
)
)
)
)
}

private fun handleMapClick(point: LatLng): Boolean {
val screenPoint: PointF = maplibreMap.projection.toScreenLocation(point)
val features = maplibreMap.queryRenderedFeatures(screenPoint, FILL_LAYER_ID)
val feature = features.firstOrNull() ?: return false
val featureId = feature.id() ?: return false

// Note the source-layer id argument, which is required for a vector source.
val currentState = countriesSource.getFeatureState(COUNTRIES_SOURCE_LAYER, featureId)
val isSelected = currentState
?.get("selected")
?.takeUnless { it.isJsonNull }
?.asBoolean
?: false

// Toggle the selection state of the tapped country. Selection is not
// exclusive: any number of countries can be selected at the same time.
val nextState = JsonObject().apply { addProperty("selected", !isSelected) }
countriesSource.setFeatureState(COUNTRIES_SOURCE_LAYER, featureId, nextState)

val countryName = feature.getStringProperty("NAME") ?: "Unknown"
Toast.makeText(
this,
"$countryName ${if (!isSelected) "selected" else "deselected"}",
Toast.LENGTH_SHORT
).show()
return true
}

override fun onStart() {
super.onStart()
mapView.onStart()
}

override fun onResume() {
super.onResume()
mapView.onResume()
}

override fun onPause() {
super.onPause()
mapView.onPause()
}

override fun onStop() {
super.onStop()
mapView.onStop()
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}

override fun onDestroy() {
super.onDestroy()
if (this::maplibreMap.isInitialized) {
maplibreMap.removeOnMapClickListener(mapClickListener)
}
mapView.onDestroy()
}

override fun onLowMemory() {
super.onLowMemory()
mapView.onLowMemory()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
<string name="description_query_rendered_features_box_highlight">Highlight buildings in box</string>
<string name="description_query_source_features">Query source for features</string>
<string name="description_feature_state">Toggle feature state and style features interactively</string>
<string name="description_feature_state_vector">Toggle feature state on a vector tile source</string>
<string name="description_simple_map">Shows a simple map</string>
<string name="description_map_change">Logs map change events to Logcat</string>
<string name="description_visibility_map">Changes visibility of map and view parent</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<string name="activity_query_rendered_features_box_highlight">Highlight features in box</string>
<string name="activity_query_source_features">Query source features</string>
<string name="activity_feature_state">Feature state</string>
<string name="activity_feature_state_vector">Feature state (vector)</string>
<string name="activity_symbol_layer">Symbols</string>
<string name="activity_add_sprite">Add Custom Sprite</string>
<string name="activity_simple_map">Simple Map</string>
Expand Down
83 changes: 83 additions & 0 deletions platform/darwin/src/MLNShapeSource.h
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,89 @@ MLN_EXPORT
*/
- (double)zoomLevelForExpandingCluster:(MLNPointFeatureCluster *)cluster;

// MARK: Managing Feature State

/**
Sets the state of a feature in this source.

A feature's state is a set of user-defined key-value pairs that are assigned
to a feature at runtime. The state object is merged with any existing
key-value pairs in the feature's state. Feature state can be read in style
expressions through the `feature-state` expression operator.

The target feature must have an identifier in the underlying source data:
either an `id` attribute in the original GeoJSON data, or an ``MLNFeature``
whose `identifier` is set.

This method can only be used while the source is attached to a map.

@param featureID The identifier of the feature whose state to set.
@param state A set of key-value pairs to merge into the feature's state. The
values should be valid JSON types.
@return `YES` if the source is attached to a map and the update was
dispatched.
*/
- (BOOL)setFeatureStateForFeatureID:(NSString *)featureID
state:(NSDictionary<NSString *, id> *)state
NS_SWIFT_NAME(setFeatureState(featureID:state:));

/**
Gets the current state of a feature in this source.

This method can only be used while the source is attached to a map.

@param featureID The identifier of the feature whose state to get.
@return A dictionary containing the current state of the feature, or `nil` if
the feature has no state or the source is not attached to a map.
*/
- (nullable NSDictionary<NSString *, id> *)featureStateForFeatureID:(NSString *)featureID
NS_SWIFT_NAME(featureState(featureID:));

/**
Removes state from a feature in this source, or from all features when
`featureID` is `nil`.

This method can only be used while the source is attached to a map.
Removals are applied on the next rendering frame: reading the state
back immediately afterwards may still return the removed entries.

@param featureID The identifier of the feature, or `nil` to target all
features.
@param stateKey The key of the state entry to remove, or `nil` to remove all
keys.
@return `YES` if the source is attached to a map and the update was
dispatched.
*/
- (BOOL)removeFeatureStateForFeatureID:(nullable NSString *)featureID
stateKey:(nullable NSString *)stateKey
NS_SWIFT_NAME(removeFeatureState(featureID:stateKey:));

/**
Removes all state from a single feature in this source.

This method can only be used while the source is attached to a map.
Removals are applied on the next rendering frame: reading the state
back immediately afterwards may still return the removed entries.

@param featureID The identifier of the feature.
@return `YES` if the source is attached to a map and the update was
dispatched.
*/
- (BOOL)removeFeatureStateForFeatureID:(NSString *)featureID
NS_SWIFT_NAME(removeFeatureState(featureID:));

/**
Removes all feature state entries from this source.

This method can only be used while the source is attached to a map.
Removals are applied on the next rendering frame: reading the state
back immediately afterwards may still return the removed entries.

@return `YES` if the source is attached to a map and the update was
dispatched.
*/
- (BOOL)resetFeatureStates;

@end

NS_ASSUME_NONNULL_END
24 changes: 24 additions & 0 deletions platform/darwin/src/MLNShapeSource.mm
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,30 @@ - (NSString *)description {
return MLNFeaturesFromMBGLFeatures(features);
}

// MARK: - Managing Feature State

- (BOOL)setFeatureStateForFeatureID:(NSString *)featureID
state:(NSDictionary<NSString *, id> *)state {
return [self mgl_setFeatureStateForSourceLayerID:nil featureID:featureID state:state];
}

- (nullable NSDictionary<NSString *, id> *)featureStateForFeatureID:(NSString *)featureID {
return [self mgl_featureStateForSourceLayerID:nil featureID:featureID];
}

- (BOOL)removeFeatureStateForFeatureID:(nullable NSString *)featureID
stateKey:(nullable NSString *)stateKey {
return [self mgl_removeFeatureStateForSourceLayerID:nil featureID:featureID stateKey:stateKey];
}

- (BOOL)removeFeatureStateForFeatureID:(NSString *)featureID {
return [self mgl_removeFeatureStateForSourceLayerID:nil featureID:featureID stateKey:nil];
}

- (BOOL)resetFeatureStates {
return [self mgl_removeFeatureStateForSourceLayerID:nil featureID:nil stateKey:nil];
}

// MARK: - MLNCluster management

- (std::optional<mbgl::FeatureExtensionValue>)
Expand Down
Loading
Loading