Add feature state functionality to Android (#4219)
This commit is contained in:
@@ -81,6 +81,10 @@ public:
|
||||
const std::optional<std::string>& sourceLayerID,
|
||||
const std::string& featureID) const;
|
||||
|
||||
FeatureState getFeatureState(const std::string& sourceID,
|
||||
const std::optional<std::string>& sourceLayerID,
|
||||
const std::string& featureID) const;
|
||||
|
||||
void removeFeatureState(const std::string& sourceID,
|
||||
const std::optional<std::string>& sourceLayerID,
|
||||
const std::optional<std::string>& featureID,
|
||||
|
||||
@@ -170,6 +170,28 @@ std::vector<Feature> AndroidRendererFrontend::querySourceFeatures(const std::str
|
||||
return mapRenderer.actor().ask(&Renderer::querySourceFeatures, sourceID, options).get();
|
||||
}
|
||||
|
||||
void AndroidRendererFrontend::setFeatureState(const std::string& sourceID,
|
||||
const std::optional<std::string>& sourceLayerID,
|
||||
const std::string& featureID,
|
||||
const FeatureState& state) const {
|
||||
mapRenderer.actor().invoke(&Renderer::setFeatureState, sourceID, sourceLayerID, featureID, state);
|
||||
}
|
||||
|
||||
FeatureState AndroidRendererFrontend::getFeatureState(const std::string& sourceID,
|
||||
const std::optional<std::string>& sourceLayerID,
|
||||
const std::string& featureID) const {
|
||||
auto getFeatureState = static_cast<FeatureState (Renderer::*)(
|
||||
const std::string&, const std::optional<std::string>&, const std::string&) const>(&Renderer::getFeatureState);
|
||||
return mapRenderer.actor().ask(getFeatureState, sourceID, sourceLayerID, featureID).get();
|
||||
}
|
||||
|
||||
void AndroidRendererFrontend::removeFeatureState(const std::string& sourceID,
|
||||
const std::optional<std::string>& sourceLayerID,
|
||||
const std::optional<std::string>& featureID,
|
||||
const std::optional<std::string>& stateKey) const {
|
||||
mapRenderer.actor().invoke(&Renderer::removeFeatureState, sourceID, sourceLayerID, featureID, stateKey);
|
||||
}
|
||||
|
||||
std::vector<Feature> AndroidRendererFrontend::queryRenderedFeatures(const ScreenBox& box,
|
||||
const RenderedQueryOptions& options) const {
|
||||
// Select the right overloaded method
|
||||
|
||||
@@ -52,6 +52,17 @@ public:
|
||||
std::vector<Feature> queryRenderedFeatures(const ScreenCoordinate&, const RenderedQueryOptions&) const;
|
||||
std::vector<Feature> queryRenderedFeatures(const ScreenBox&, const RenderedQueryOptions&) const;
|
||||
std::vector<Feature> querySourceFeatures(const std::string& sourceID, const SourceQueryOptions&) const;
|
||||
void setFeatureState(const std::string& sourceID,
|
||||
const std::optional<std::string>& sourceLayerID,
|
||||
const std::string& featureID,
|
||||
const FeatureState& state) const;
|
||||
FeatureState getFeatureState(const std::string& sourceID,
|
||||
const std::optional<std::string>& sourceLayerID,
|
||||
const std::string& featureID) const;
|
||||
void removeFeatureState(const std::string& sourceID,
|
||||
const std::optional<std::string>& sourceLayerID,
|
||||
const std::optional<std::string>& featureID,
|
||||
const std::optional<std::string>& stateKey) const;
|
||||
AnnotationIDs queryPointAnnotations(const ScreenBox& box, const std::chrono::milliseconds& timeout) const;
|
||||
AnnotationIDs queryShapeAnnotations(const ScreenBox& box, const std::chrono::milliseconds& timeout) const;
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "conversion/collection.hpp"
|
||||
#include "style/conversion/filter.hpp"
|
||||
#include "geojson/feature.hpp"
|
||||
#include "gson/json_object.hpp"
|
||||
#include "rendering_stats.hpp"
|
||||
|
||||
#include "android_renderer_frontend.hpp"
|
||||
@@ -1015,6 +1016,59 @@ jni::Local<jni::Array<jni::Object<geojson::Feature>>> NativeMapView::queryRender
|
||||
return Feature::convert(env, rendererFrontend->queryRenderedFeatures(box, {layers, toFilter(env, jfilter)}));
|
||||
}
|
||||
|
||||
void NativeMapView::setFeatureState(JNIEnv& env,
|
||||
const jni::String& sourceId,
|
||||
const jni::String& sourceLayerId,
|
||||
const jni::String& featureId,
|
||||
const jni::Object<gson::JsonObject>& state) {
|
||||
if (!sourceId || !featureId || !state) {
|
||||
return;
|
||||
}
|
||||
|
||||
rendererFrontend->setFeatureState(
|
||||
jni::Make<std::string>(env, sourceId),
|
||||
sourceLayerId ? std::optional<std::string>(jni::Make<std::string>(env, sourceLayerId)) : std::nullopt,
|
||||
jni::Make<std::string>(env, featureId),
|
||||
gson::JsonObject::convert(env, state));
|
||||
map->triggerRepaint();
|
||||
}
|
||||
|
||||
jni::Local<jni::Object<gson::JsonObject>> NativeMapView::getFeatureState(JNIEnv& env,
|
||||
const jni::String& sourceId,
|
||||
const jni::String& sourceLayerId,
|
||||
const jni::String& featureId) {
|
||||
if (!sourceId || !featureId) {
|
||||
return jni::Local<jni::Object<gson::JsonObject>>();
|
||||
}
|
||||
|
||||
const auto state = rendererFrontend->getFeatureState(
|
||||
jni::Make<std::string>(env, sourceId),
|
||||
sourceLayerId ? std::optional<std::string>(jni::Make<std::string>(env, sourceLayerId)) : std::nullopt,
|
||||
jni::Make<std::string>(env, featureId));
|
||||
if (state.empty()) {
|
||||
return jni::Local<jni::Object<gson::JsonObject>>();
|
||||
}
|
||||
|
||||
return gson::JsonObject::New(env, state);
|
||||
}
|
||||
|
||||
void NativeMapView::removeFeatureState(JNIEnv& env,
|
||||
const jni::String& sourceId,
|
||||
const jni::String& sourceLayerId,
|
||||
const jni::String& featureId,
|
||||
const jni::String& stateKey) {
|
||||
if (!sourceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
rendererFrontend->removeFeatureState(
|
||||
jni::Make<std::string>(env, sourceId),
|
||||
sourceLayerId ? std::optional<std::string>(jni::Make<std::string>(env, sourceLayerId)) : std::nullopt,
|
||||
featureId ? std::optional<std::string>(jni::Make<std::string>(env, featureId)) : std::nullopt,
|
||||
stateKey ? std::optional<std::string>(jni::Make<std::string>(env, stateKey)) : std::nullopt);
|
||||
map->triggerRepaint();
|
||||
}
|
||||
|
||||
jni::Local<jni::Object<Light>> NativeMapView::getLight(JNIEnv& env) {
|
||||
mbgl::style::Light* light = map->getStyle().getLight();
|
||||
if (light) {
|
||||
@@ -1183,7 +1237,7 @@ jni::Local<jni::Array<jni::Object<Source>>> NativeMapView::getSources(JNIEnv& en
|
||||
auto jSources = jni::Array<jni::Object<Source>>::New(env, sources.size());
|
||||
int index = 0;
|
||||
for (auto source : sources) {
|
||||
jSources.Set(env, index, Source::peerForCoreSource(env, *source, *rendererFrontend));
|
||||
jSources.Set(env, index, Source::peerForCoreSource(env, *source, *rendererFrontend, *map));
|
||||
index++;
|
||||
}
|
||||
|
||||
@@ -1199,7 +1253,7 @@ jni::Local<jni::Object<Source>> NativeMapView::getSource(JNIEnv& env, const jni:
|
||||
}
|
||||
|
||||
// Create and return the source's native peer
|
||||
return jni::NewLocal(env, Source::peerForCoreSource(env, *coreSource, *rendererFrontend));
|
||||
return jni::NewLocal(env, Source::peerForCoreSource(env, *coreSource, *rendererFrontend, *map));
|
||||
}
|
||||
|
||||
void NativeMapView::addSource(JNIEnv& env, const jni::Object<Source>& obj, jlong sourcePtr) {
|
||||
@@ -1428,6 +1482,9 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
|
||||
METHOD(&NativeMapView::queryShapeAnnotations, "nativeQueryShapeAnnotations"),
|
||||
METHOD(&NativeMapView::queryRenderedFeaturesForPoint, "nativeQueryRenderedFeaturesForPoint"),
|
||||
METHOD(&NativeMapView::queryRenderedFeaturesForBox, "nativeQueryRenderedFeaturesForBox"),
|
||||
METHOD(&NativeMapView::setFeatureState, "nativeSetFeatureState"),
|
||||
METHOD(&NativeMapView::getFeatureState, "nativeGetFeatureState"),
|
||||
METHOD(&NativeMapView::removeFeatureState, "nativeRemoveFeatureState"),
|
||||
METHOD(&NativeMapView::getLight, "nativeGetLight"),
|
||||
METHOD(&NativeMapView::getLayers, "nativeGetLayers"),
|
||||
METHOD(&NativeMapView::getLayer, "nativeGetLayer"),
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "graphics/rectf.hpp"
|
||||
#include "geojson/feature.hpp"
|
||||
#include "geojson/geometry.hpp"
|
||||
#include "gson/json_object.hpp"
|
||||
#include "geometry/lat_lng.hpp"
|
||||
#include "geometry/projected_meters.hpp"
|
||||
#include "style/layers/layer_manager.hpp"
|
||||
@@ -258,6 +259,23 @@ public:
|
||||
const jni::Array<jni::String>&,
|
||||
const jni::Array<jni::Object<>>& jfilter);
|
||||
|
||||
void setFeatureState(JNIEnv&,
|
||||
const jni::String& sourceId,
|
||||
const jni::String& sourceLayerId,
|
||||
const jni::String& featureId,
|
||||
const jni::Object<gson::JsonObject>& state);
|
||||
|
||||
jni::Local<jni::Object<gson::JsonObject>> getFeatureState(JNIEnv&,
|
||||
const jni::String& sourceId,
|
||||
const jni::String& sourceLayerId,
|
||||
const jni::String& featureId);
|
||||
|
||||
void removeFeatureState(JNIEnv&,
|
||||
const jni::String& sourceId,
|
||||
const jni::String& sourceLayerId,
|
||||
const jni::String& featureId,
|
||||
const jni::String& stateKey);
|
||||
|
||||
jni::Local<jni::Object<Light>> getLight(JNIEnv&);
|
||||
|
||||
jni::Local<jni::Array<jni::Object<Layer>>> getLayers(JNIEnv&);
|
||||
|
||||
@@ -52,11 +52,14 @@ static std::unique_ptr<Source> createSourcePeer(jni::JNIEnv& env,
|
||||
|
||||
const jni::Object<Source>& Source::peerForCoreSource(jni::JNIEnv& env,
|
||||
mbgl::style::Source& coreSource,
|
||||
AndroidRendererFrontend& frontend) {
|
||||
AndroidRendererFrontend& frontend,
|
||||
mbgl::Map& map) {
|
||||
if (!coreSource.peer.has_value()) {
|
||||
coreSource.peer = createSourcePeer(env, coreSource, &frontend);
|
||||
}
|
||||
return coreSource.peer.get<std::unique_ptr<Source>>()->javaPeer;
|
||||
auto* peer = coreSource.peer.get<std::unique_ptr<Source>>().get();
|
||||
peer->bindToMap(frontend, map);
|
||||
return peer->javaPeer;
|
||||
}
|
||||
|
||||
const jni::Object<Source>& Source::peerForCoreSource(jni::JNIEnv& env, mbgl::style::Source& coreSource) {
|
||||
@@ -172,7 +175,7 @@ void Source::addToMap(JNIEnv& env, const jni::Object<Source>& obj, mbgl::Map& ma
|
||||
// Add strong reference to java source
|
||||
javaPeer = jni::NewGlobal(env, obj);
|
||||
|
||||
rendererFrontend = &frontend;
|
||||
bindToMap(frontend, map);
|
||||
}
|
||||
|
||||
bool Source::removeFromMap(JNIEnv&, const jni::Object<Source>&, mbgl::Map& map) {
|
||||
@@ -204,6 +207,67 @@ jni::Local<jni::Long> Source::getMinimumTileUpdateInterval(JNIEnv& env) {
|
||||
return jni::Box(env, jni::jlong(source.getMinimumTileUpdateInterval().count() / 1000000));
|
||||
}
|
||||
|
||||
jni::jboolean Source::setFeatureState(JNIEnv& env,
|
||||
const jni::String& sourceLayerId,
|
||||
const jni::String& featureId,
|
||||
const jni::Object<gson::JsonObject>& state) {
|
||||
if (!rendererFrontend || !featureId || !state) {
|
||||
return jni::jni_false;
|
||||
}
|
||||
|
||||
rendererFrontend->setFeatureState(
|
||||
source.getID(),
|
||||
sourceLayerId ? std::optional<std::string>(jni::Make<std::string>(env, sourceLayerId)) : std::nullopt,
|
||||
jni::Make<std::string>(env, featureId),
|
||||
gson::JsonObject::convert(env, state));
|
||||
if (map) {
|
||||
map->triggerRepaint();
|
||||
}
|
||||
return jni::jni_true;
|
||||
}
|
||||
|
||||
jni::Local<jni::Object<gson::JsonObject>> Source::getFeatureState(JNIEnv& env,
|
||||
const jni::String& sourceLayerId,
|
||||
const jni::String& featureId) {
|
||||
if (!rendererFrontend || !featureId) {
|
||||
return jni::Local<jni::Object<gson::JsonObject>>();
|
||||
}
|
||||
|
||||
const auto state = rendererFrontend->getFeatureState(
|
||||
source.getID(),
|
||||
sourceLayerId ? std::optional<std::string>(jni::Make<std::string>(env, sourceLayerId)) : std::nullopt,
|
||||
jni::Make<std::string>(env, featureId));
|
||||
if (state.empty()) {
|
||||
return jni::Local<jni::Object<gson::JsonObject>>();
|
||||
}
|
||||
|
||||
return gson::JsonObject::New(env, state);
|
||||
}
|
||||
|
||||
jni::jboolean Source::removeFeatureState(JNIEnv& env,
|
||||
const jni::String& sourceLayerId,
|
||||
const jni::String& featureId,
|
||||
const jni::String& stateKey) {
|
||||
if (!rendererFrontend) {
|
||||
return jni::jni_false;
|
||||
}
|
||||
|
||||
rendererFrontend->removeFeatureState(
|
||||
source.getID(),
|
||||
sourceLayerId ? std::optional<std::string>(jni::Make<std::string>(env, sourceLayerId)) : std::nullopt,
|
||||
featureId ? std::optional<std::string>(jni::Make<std::string>(env, featureId)) : std::nullopt,
|
||||
stateKey ? std::optional<std::string>(jni::Make<std::string>(env, stateKey)) : std::nullopt);
|
||||
if (map) {
|
||||
map->triggerRepaint();
|
||||
}
|
||||
return jni::jni_true;
|
||||
}
|
||||
|
||||
void Source::bindToMap(AndroidRendererFrontend& frontend, mbgl::Map& map) {
|
||||
rendererFrontend = &frontend;
|
||||
this->map = ↦
|
||||
}
|
||||
|
||||
void Source::releaseJavaPeer() {
|
||||
// We can't release the peer if the source was not removed from the map
|
||||
if (!ownedSource) {
|
||||
@@ -220,6 +284,7 @@ void Source::releaseJavaPeer() {
|
||||
javaPeer.reset();
|
||||
|
||||
rendererFrontend = nullptr;
|
||||
map = nullptr;
|
||||
}
|
||||
|
||||
void Source::registerNative(jni::JNIEnv& env) {
|
||||
@@ -242,7 +307,10 @@ void Source::registerNative(jni::JNIEnv& env) {
|
||||
METHOD(&Source::isVolatile, "nativeIsVolatile"),
|
||||
METHOD(&Source::setVolatile, "nativeSetVolatile"),
|
||||
METHOD(&Source::setMinimumTileUpdateInterval, "nativeSetMinimumTileUpdateInterval"),
|
||||
METHOD(&Source::getMinimumTileUpdateInterval, "nativeGetMinimumTileUpdateInterval"));
|
||||
METHOD(&Source::getMinimumTileUpdateInterval, "nativeGetMinimumTileUpdateInterval"),
|
||||
METHOD(&Source::setFeatureState, "nativeSetFeatureState"),
|
||||
METHOD(&Source::getFeatureState, "nativeGetFeatureState"),
|
||||
METHOD(&Source::removeFeatureState, "nativeRemoveFeatureState"));
|
||||
|
||||
// Register subclasses
|
||||
GeoJSONSource::registerNative(env);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "../value.hpp"
|
||||
#include "../../android_renderer_frontend.hpp"
|
||||
#include "../../gson/json_object.hpp"
|
||||
|
||||
#include <jni/jni.hpp>
|
||||
|
||||
@@ -19,7 +20,10 @@ public:
|
||||
static void registerNative(jni::JNIEnv&);
|
||||
|
||||
static const jni::Object<Source>& peerForCoreSource(jni::JNIEnv&, mbgl::style::Source&);
|
||||
static const jni::Object<Source>& peerForCoreSource(jni::JNIEnv&, mbgl::style::Source&, AndroidRendererFrontend&);
|
||||
static const jni::Object<Source>& peerForCoreSource(jni::JNIEnv&,
|
||||
mbgl::style::Source&,
|
||||
AndroidRendererFrontend&,
|
||||
mbgl::Map&);
|
||||
|
||||
/*
|
||||
* Called when a Java object is created for a core source that belongs to a map.
|
||||
@@ -62,6 +66,22 @@ public:
|
||||
|
||||
jni::Local<jni::Long> getMinimumTileUpdateInterval(JNIEnv&);
|
||||
|
||||
jni::jboolean setFeatureState(JNIEnv&,
|
||||
const jni::String& sourceLayerId,
|
||||
const jni::String& featureId,
|
||||
const jni::Object<gson::JsonObject>& state);
|
||||
|
||||
jni::Local<jni::Object<gson::JsonObject>> getFeatureState(JNIEnv&,
|
||||
const jni::String& sourceLayerId,
|
||||
const jni::String& featureId);
|
||||
|
||||
jni::jboolean removeFeatureState(JNIEnv&,
|
||||
const jni::String& sourceLayerId,
|
||||
const jni::String& featureId,
|
||||
const jni::String& stateKey);
|
||||
|
||||
void bindToMap(AndroidRendererFrontend&, mbgl::Map&);
|
||||
|
||||
protected:
|
||||
// Set on newly created sources until added to the map.
|
||||
std::unique_ptr<mbgl::style::Source> ownedSource;
|
||||
@@ -74,6 +94,9 @@ protected:
|
||||
|
||||
// RendererFrontend pointer is valid only when added to the map.
|
||||
AndroidRendererFrontend* rendererFrontend{nullptr};
|
||||
|
||||
// Map pointer is valid only when added to the map.
|
||||
mbgl::Map* map{nullptr};
|
||||
};
|
||||
|
||||
} // namespace android
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -261,4 +261,5 @@ class MapLibreMapTest : BaseTest() {
|
||||
verify { developerAnimationListener.onDeveloperAnimationStarted() }
|
||||
verify { nativeMapView.setZoom(2.0, target, 0) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ class SequentialActivityTest(private val activity: Class<out Activity>) {
|
||||
org.maplibre.android.testapp.activity.camera.CameraAnimatorActivity::class.java,
|
||||
org.maplibre.android.testapp.activity.customlayer.CustomLayerActivity::class.java,
|
||||
org.maplibre.android.testapp.activity.events.ObserverActivity::class.java,
|
||||
org.maplibre.android.testapp.activity.feature.FeatureStateActivity::class.java,
|
||||
org.maplibre.android.testapp.activity.feature.QuerySourceFeaturesActivity::class.java,
|
||||
org.maplibre.android.testapp.activity.fragment.MapFragmentActivity::class.java,
|
||||
org.maplibre.android.testapp.activity.imagegenerator.SnapshotActivity::class.java,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.maplibre.android.testapp.style;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.maplibre.android.style.sources.CannotAddSourceException;
|
||||
import org.maplibre.android.style.sources.GeoJsonOptions;
|
||||
@@ -7,6 +9,7 @@ import org.maplibre.geojson.Feature;
|
||||
import org.maplibre.geojson.FeatureCollection;
|
||||
import org.maplibre.geojson.Point;
|
||||
import org.maplibre.android.geometry.LatLng;
|
||||
import org.maplibre.android.maps.Style;
|
||||
import org.maplibre.android.style.layers.CircleLayer;
|
||||
import org.maplibre.android.style.layers.Layer;
|
||||
import org.maplibre.android.style.sources.GeoJsonSource;
|
||||
@@ -112,6 +115,77 @@ public class GeoJsonSourceTests extends EspressoTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFeatureStateUpdatesStyleOwnedSourceImmediately() {
|
||||
validateTestSetup();
|
||||
MapLibreMapAction.invoke(maplibreMap, (uiController, maplibreMap) -> {
|
||||
boolean[] styleLoaded = {false};
|
||||
String styleJson = ResourceUtils.readRawResource(rule.getActivity(), R.raw.test_feature_state_style);
|
||||
maplibreMap.setStyle(new Style.Builder().fromJson(styleJson), style -> styleLoaded[0] = true);
|
||||
|
||||
while (!styleLoaded[0]) {
|
||||
uiController.loopMainThreadForAtLeast(100);
|
||||
}
|
||||
|
||||
TestingAsyncUtils.INSTANCE.waitForLayer(uiController, mapView);
|
||||
|
||||
GeoJsonSource source = maplibreMap.getStyle().getSourceAs("style-source");
|
||||
assertEquals(0, queryFeatureCount(maplibreMap));
|
||||
|
||||
JsonObject state = new JsonObject();
|
||||
state.addProperty("selected", true);
|
||||
source.setFeatureState("feature-1", state);
|
||||
|
||||
// The feature-state update is applied on a subsequent render pass, so
|
||||
// poll until the filter re-evaluates rather than relying on a single
|
||||
// didBecomeIdle callback (which can be missed if the repaint completes
|
||||
// before the listener is attached).
|
||||
assertEquals(1, pollFeatureCount(uiController, maplibreMap, 1));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFeatureStateKeyRemovalAcrossSource() {
|
||||
validateTestSetup();
|
||||
MapLibreMapAction.invoke(maplibreMap, (uiController, maplibreMap) -> {
|
||||
boolean[] styleLoaded = {false};
|
||||
String styleJson = ResourceUtils.readRawResource(rule.getActivity(), R.raw.test_feature_state_style);
|
||||
maplibreMap.setStyle(new Style.Builder().fromJson(styleJson), style -> styleLoaded[0] = true);
|
||||
|
||||
while (!styleLoaded[0]) {
|
||||
uiController.loopMainThreadForAtLeast(100);
|
||||
}
|
||||
|
||||
TestingAsyncUtils.INSTANCE.waitForLayer(uiController, mapView);
|
||||
|
||||
GeoJsonSource source = maplibreMap.getStyle().getSourceAs("style-source");
|
||||
JsonObject state = new JsonObject();
|
||||
state.addProperty("selected", true);
|
||||
source.setFeatureState("feature-1", state);
|
||||
assertEquals(1, pollFeatureCount(uiController, maplibreMap, 1));
|
||||
|
||||
source.removeFeatureState(null, "selected");
|
||||
assertEquals(0, pollFeatureCount(uiController, maplibreMap, 0));
|
||||
});
|
||||
}
|
||||
|
||||
private int queryFeatureCount(org.maplibre.android.maps.MapLibreMap maplibreMap) {
|
||||
return maplibreMap.queryRenderedFeatures(
|
||||
maplibreMap.getProjection().toScreenLocation(new LatLng(0.0, 0.0)), "selected-layer").size();
|
||||
}
|
||||
|
||||
private int pollFeatureCount(androidx.test.espresso.UiController uiController,
|
||||
org.maplibre.android.maps.MapLibreMap maplibreMap,
|
||||
int expected) {
|
||||
long deadline = System.nanoTime() / 1_000_000L + 5_000L;
|
||||
int count = queryFeatureCount(maplibreMap);
|
||||
while (count != expected && System.nanoTime() / 1_000_000L < deadline) {
|
||||
uiController.loopMainThreadForAtLeast(100);
|
||||
count = queryFeatureCount(maplibreMap);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClearCollectionDuringConversion() {
|
||||
// https://github.com/mapbox/mapbox-gl-native/issues/14565
|
||||
|
||||
@@ -831,6 +831,18 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activity.FeatureOverviewActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.feature.FeatureStateActivity"
|
||||
android:description="@string/description_feature_state"
|
||||
android:exported="true"
|
||||
android:label="@string/activity_feature_state">
|
||||
<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"
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
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.GeoJsonSource
|
||||
import org.maplibre.android.testapp.R
|
||||
import org.maplibre.android.testapp.styles.TestStyles
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* Test activity showcasing interactive styling with feature state.
|
||||
*
|
||||
* Loads US states from a remote GeoJSON source and uses feature-state
|
||||
* expressions to highlight tapped states.
|
||||
*/
|
||||
class FeatureStateActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private const val STATES_SOURCE_ID = "states"
|
||||
private const val FILL_LAYER_ID = "state-fills"
|
||||
private const val LINE_LAYER_ID = "state-borders"
|
||||
private const val STATES_URL =
|
||||
"https://maplibre.org/maplibre-gl-js/docs/assets/us_states.geojson"
|
||||
}
|
||||
|
||||
private lateinit var mapView: MapView
|
||||
private lateinit var maplibreMap: MapLibreMap
|
||||
private lateinit var statesSource: GeoJsonSource
|
||||
|
||||
private val mapClickListener = MapLibreMap.OnMapClickListener { point ->
|
||||
handleMapClick(point)
|
||||
}
|
||||
|
||||
// # --8<-- [start:onCreate]
|
||||
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(42.619626, -103.523181), 3.0)
|
||||
)
|
||||
maplibreMap.setStyle(TestStyles.getPredefinedStyleWithFallback("Streets")) { style ->
|
||||
addStatesLayer(style)
|
||||
maplibreMap.addOnMapClickListener(mapClickListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
// # --8<-- [end:onCreate]
|
||||
|
||||
// # --8<-- [start:addStatesLayer]
|
||||
private fun addStatesLayer(style: Style) {
|
||||
val source = GeoJsonSource(STATES_SOURCE_ID, URI(STATES_URL))
|
||||
style.addSource(source)
|
||||
statesSource = source
|
||||
|
||||
val selectedFill = Color.parseColor("#d94d41")
|
||||
val defaultFill = Color.parseColor("#2f7de1")
|
||||
val selectedBorder = Color.parseColor("#8f241f")
|
||||
val defaultBorder = Color.parseColor("#2f7de1")
|
||||
|
||||
style.addLayer(
|
||||
FillLayer(FILL_LAYER_ID, STATES_SOURCE_ID).withProperties(
|
||||
fillColor(
|
||||
switchCase(
|
||||
toBool(featureState("selected")),
|
||||
color(selectedFill),
|
||||
color(defaultFill)
|
||||
)
|
||||
),
|
||||
fillOpacity(
|
||||
switchCase(
|
||||
toBool(featureState("selected")),
|
||||
literal(0.7f),
|
||||
literal(0.5f)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
style.addLayer(
|
||||
LineLayer(LINE_LAYER_ID, STATES_SOURCE_ID).withProperties(
|
||||
lineColor(
|
||||
switchCase(
|
||||
toBool(featureState("selected")),
|
||||
color(selectedBorder),
|
||||
color(defaultBorder)
|
||||
)
|
||||
),
|
||||
lineWidth(
|
||||
switchCase(
|
||||
toBool(featureState("selected")),
|
||||
literal(2.0f),
|
||||
literal(1.0f)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
// # --8<-- [end:addStatesLayer]
|
||||
|
||||
// # --8<-- [start:handleMapClick]
|
||||
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
|
||||
|
||||
val currentState = statesSource.getFeatureState(featureId)
|
||||
val isSelected = currentState
|
||||
?.get("selected")
|
||||
?.takeUnless { it.isJsonNull }
|
||||
?.asBoolean
|
||||
?: false
|
||||
|
||||
val nextState = JsonObject().apply { addProperty("selected", !isSelected) }
|
||||
statesSource.setFeatureState(featureId, nextState)
|
||||
|
||||
val stateName = feature.getStringProperty("STATE_NAME") ?: "Unknown"
|
||||
Toast.makeText(
|
||||
this,
|
||||
"$stateName ${if (!isSelected) "selected" else "deselected"}",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return true
|
||||
}
|
||||
// # --8<-- [end:handleMapClick]
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"version": 8,
|
||||
"center": [0, 0],
|
||||
"zoom": 3,
|
||||
"sources": {
|
||||
"style-source": {
|
||||
"type": "geojson",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"id": "feature-1",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [0, 0]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": "#ffffff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "selected-layer",
|
||||
"type": "circle",
|
||||
"source": "style-source",
|
||||
"filter": ["==", ["feature-state", "selected"], true],
|
||||
"paint": {
|
||||
"circle-radius": 12,
|
||||
"circle-color": "#ff0000"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -50,6 +50,7 @@
|
||||
<string name="description_query_rendered_features_box_symbol_count">Count all rendered symbols in box</string>
|
||||
<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_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>
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
<string name="activity_query_rendered_features_box_symbol_count">Count symbols in box</string>
|
||||
<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_symbol_layer">Symbols</string>
|
||||
<string name="activity_add_sprite">Add Custom Sprite</string>
|
||||
<string name="activity_simple_map">Simple Map</string>
|
||||
|
||||
37
platform/android/docs/styling/feature-state.md
Normal file
37
platform/android/docs/styling/feature-state.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Feature State
|
||||
|
||||
{{ activity_source_note("FeatureStateActivity.kt") }}
|
||||
|
||||
This example shows how to use [`feature-state`](https://maplibre.org/maplibre-style-spec/expressions/#feature-state) for interactive styling on Android.
|
||||
|
||||
Feature state lets you attach runtime JSON values to individual features, then read those values back inside style expressions. This is useful for interaction-driven UI such as selection, highlighting, or temporary analysis state.
|
||||
|
||||
## Load US states from a remote GeoJSON source
|
||||
|
||||
The example loads the same US states GeoJSON used by the GLFW and iOS demos:
|
||||
|
||||
```kotlin
|
||||
--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/feature/FeatureStateActivity.kt:addStatesLayer"
|
||||
```
|
||||
|
||||
## Style layers with feature-state expressions
|
||||
|
||||
The fill color/opacity and border color/width respond to a `selected` boolean stored in feature state:
|
||||
|
||||
- **Selected** — red fill, higher opacity, thicker red border
|
||||
- **Default** — blue fill, semi-transparent, thin blue border
|
||||
|
||||
## Toggle selection on tap
|
||||
|
||||
Tapping a state toggles its `selected` feature-state key and shows a toast:
|
||||
|
||||
```kotlin
|
||||
--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/feature/FeatureStateActivity.kt:handleMapClick"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Use `GeoJsonSource.setFeatureState()` or `VectorSource.setFeatureState()` to merge new keys into a feature's state.
|
||||
- Use `getFeatureState()` on the source to inspect the current state for one feature.
|
||||
- Use `removeFeatureState()` or `resetFeatureStates()` on the source to clear state.
|
||||
- For `VectorSource`, all feature-state methods require a `sourceLayerId` parameter.
|
||||
@@ -124,6 +124,14 @@ void Renderer::getFeatureState(FeatureState& state,
|
||||
impl->orchestrator.getFeatureState(state, sourceID, sourceLayerID, featureID);
|
||||
}
|
||||
|
||||
FeatureState Renderer::getFeatureState(const std::string& sourceID,
|
||||
const std::optional<std::string>& sourceLayerID,
|
||||
const std::string& featureID) const {
|
||||
FeatureState state;
|
||||
getFeatureState(state, sourceID, sourceLayerID, featureID);
|
||||
return state;
|
||||
}
|
||||
|
||||
void Renderer::removeFeatureState(const std::string& sourceID,
|
||||
const std::optional<std::string>& sourceLayerID,
|
||||
const std::optional<std::string>& featureID,
|
||||
|
||||
Reference in New Issue
Block a user