Add feature state functionality to Android (#4219)

This commit is contained in:
Brandon Roeder
2026-07-13 13:53:52 -05:00
committed by GitHub
parent 764accc8f5
commit c58080b491
23 changed files with 797 additions and 7 deletions

View File

@@ -8,6 +8,7 @@ import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.JsonObject;
import org.maplibre.geojson.Feature;
import org.maplibre.geojson.Geometry;
import org.maplibre.android.annotations.Marker;
@@ -190,6 +191,21 @@ interface NativeMap {
@Nullable String[] layerIds,
@Nullable Expression filter);
void setFeatureState(@NonNull String sourceId,
@Nullable String sourceLayerId,
@NonNull String featureId,
@NonNull JsonObject state);
@Nullable
JsonObject getFeatureState(@NonNull String sourceId,
@Nullable String sourceLayerId,
@NonNull String featureId);
void removeFeatureState(@NonNull String sourceId,
@Nullable String sourceLayerId,
@Nullable String featureId,
@Nullable String stateKey);
//
// Projection API
//

View File

@@ -13,6 +13,7 @@ import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.JsonObject;
import org.maplibre.geojson.Feature;
import org.maplibre.geojson.Geometry;
import org.maplibre.android.LibraryLoader;
@@ -1123,6 +1124,39 @@ final class NativeMapView implements NativeMap {
return features != null ? Arrays.asList(features) : new ArrayList<Feature>();
}
@Override
public void setFeatureState(@NonNull String sourceId,
@Nullable String sourceLayerId,
@NonNull String featureId,
@NonNull JsonObject state) {
if (checkState("setFeatureState")) {
return;
}
nativeSetFeatureState(sourceId, sourceLayerId, featureId, state);
}
@Override
@Nullable
public JsonObject getFeatureState(@NonNull String sourceId,
@Nullable String sourceLayerId,
@NonNull String featureId) {
if (checkState("getFeatureState")) {
return null;
}
return nativeGetFeatureState(sourceId, sourceLayerId, featureId);
}
@Override
public void removeFeatureState(@NonNull String sourceId,
@Nullable String sourceLayerId,
@Nullable String featureId,
@Nullable String stateKey) {
if (checkState("removeFeatureState")) {
return;
}
nativeRemoveFeatureState(sourceId, sourceLayerId, featureId, stateKey);
}
@Override
public void setApiBaseUrl(String baseUrl) {
if (checkState("setApiBaseUrl")) {
@@ -1711,6 +1745,24 @@ final class NativeMapView implements NativeMap {
String[] layerIds,
Object[] filter);
@Keep
private native void nativeSetFeatureState(String sourceId,
String sourceLayerId,
String featureId,
JsonObject state);
@Nullable
@Keep
private native JsonObject nativeGetFeatureState(String sourceId,
String sourceLayerId,
String featureId);
@Keep
private native void nativeRemoveFeatureState(String sourceId,
String sourceLayerId,
String featureId,
String stateKey);
@NonNull
@Keep
private native Light nativeGetLight();

View File

@@ -1439,6 +1439,16 @@ public class Expression {
return new Expression("id");
}
/**
* Gets a value from feature state.
*
* @param property the state property to read
* @return the expression
*/
public static Expression featureState(@NonNull String property) {
return new Expression("feature-state", literal(property));
}
/**
* Gets the value of a cluster property accumulated so far. Can only be used in the clusterProperties
* option of a clustered GeoJSON source.

View File

@@ -2,6 +2,7 @@ package org.maplibre.android.style.sources
import androidx.annotation.Keep
import androidx.annotation.UiThread
import com.google.gson.JsonObject
import org.maplibre.geojson.Feature
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Geometry
@@ -404,6 +405,67 @@ class GeoJsonSource : Source {
return nativeGetUrl()
}
/**
* Sets the state of a feature in this source.
*
* Feature state can be read in style expressions through `["feature-state", key]`.
* The target feature must already have an id in the underlying source data.
*
* @param featureId the id of the feature whose state to set
* @param state a JSON object with the state key-value pairs to merge
* @return true if the source is attached to a map and the update was dispatched
*/
fun setFeatureState(featureId: String, state: JsonObject): Boolean {
checkThread()
return nativeSetFeatureState(null, featureId, state)
}
/**
* Gets the current state of a feature in this source.
*
* The target feature must already have an id in the underlying source data.
*
* @param featureId the id of the feature whose state to get
* @return the feature state, or null
*/
fun getFeatureState(featureId: String): JsonObject? {
checkThread()
return nativeGetFeatureState(null, featureId)
}
/**
* Removes state from a feature in this source, or from all features when [featureId] is null.
*
* @param featureId the id of the feature, or null to target all features
* @param stateKey the state key to remove, or null to remove all keys
* @return true if the source is attached to a map and the update was dispatched
*/
fun removeFeatureState(featureId: String?, stateKey: String?): Boolean {
checkThread()
return nativeRemoveFeatureState(null, featureId, stateKey)
}
/**
* Removes all state from a single feature in this source.
*
* @param featureId the id of the feature
* @return true if the source is attached to a map and the update was dispatched
*/
fun removeFeatureState(featureId: String): Boolean {
checkThread()
return nativeRemoveFeatureState(null, featureId, null)
}
/**
* Removes all feature state entries from this source.
*
* @return true if the source is attached to a map and the update was dispatched
*/
fun resetFeatureStates(): Boolean {
checkThread()
return nativeRemoveFeatureState(null, null, null)
}
/**
* Queries the source for features.
*

View File

@@ -4,6 +4,7 @@ import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.JsonObject;
import org.maplibre.android.LibraryLoader;
import org.maplibre.android.utils.ThreadUtils;
@@ -198,6 +199,21 @@ public abstract class Source {
@Keep
protected native Long nativeGetMinimumTileUpdateInterval();
@Keep
protected native boolean nativeSetFeatureState(@Nullable String sourceLayerId,
@NonNull String featureId,
@NonNull JsonObject state);
@Nullable
@Keep
protected native JsonObject nativeGetFeatureState(@Nullable String sourceLayerId,
@NonNull String featureId);
@Keep
protected native boolean nativeRemoveFeatureState(@Nullable String sourceLayerId,
@Nullable String featureId,
@Nullable String stateKey);
public void setDetached() {
detached = true;
}

View File

@@ -4,6 +4,7 @@ import android.net.Uri
import androidx.annotation.Keep
import androidx.annotation.Size
import androidx.annotation.UiThread
import com.google.gson.JsonObject
import org.maplibre.geojson.Feature
import org.maplibre.android.style.expressions.Expression
import java.net.URL
@@ -102,6 +103,73 @@ class VectorSource : Source {
initialize(id, tileSet.toValueObject())
}
/**
* Sets the state of a feature in this source.
*
* Feature state can be read in style expressions through `["feature-state", key]`.
* The target feature must already have an id in the underlying source data.
*
* @param sourceLayerId the source layer id
* @param featureId the id of the feature whose state to set
* @param state a JSON object with the state key-value pairs to merge
* @return true if the source is attached to a map and the update was dispatched
*/
fun setFeatureState(sourceLayerId: String, featureId: String, state: JsonObject): Boolean {
checkThread()
return nativeSetFeatureState(sourceLayerId, featureId, state)
}
/**
* Gets the current state of a feature in this source.
*
* The target feature must already have an id in the underlying source data.
*
* @param sourceLayerId the source layer id
* @param featureId the id of the feature whose state to get
* @return the feature state, or null
*/
fun getFeatureState(sourceLayerId: String, featureId: String): JsonObject? {
checkThread()
return nativeGetFeatureState(sourceLayerId, featureId)
}
/**
* Removes state from a feature in this source, or from all features in a source layer
* when [featureId] is null.
*
* @param sourceLayerId the source layer id
* @param featureId the id of the feature, or null to target all features
* @param stateKey the state key to remove, or null to remove all keys
* @return true if the source is attached to a map and the update was dispatched
*/
fun removeFeatureState(sourceLayerId: String, featureId: String?, stateKey: String?): Boolean {
checkThread()
return nativeRemoveFeatureState(sourceLayerId, featureId, stateKey)
}
/**
* Removes all state from a single feature in this source.
*
* @param sourceLayerId the source layer id
* @param featureId the id of the feature
* @return true if the source is attached to a map and the update was dispatched
*/
fun removeFeatureState(sourceLayerId: String, featureId: String): Boolean {
checkThread()
return nativeRemoveFeatureState(sourceLayerId, featureId, null)
}
/**
* Removes all feature state entries from the given source layer.
*
* @param sourceLayerId the source layer id
* @return true if the source is attached to a map and the update was dispatched
*/
fun resetFeatureStates(sourceLayerId: String): Boolean {
checkThread()
return nativeRemoveFeatureState(sourceLayerId, null, null)
}
/**
* Queries the source for features.
*