diff --git a/packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt b/packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt index 39135ef40..56bb72c5e 100644 --- a/packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt +++ b/packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt @@ -118,7 +118,7 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) : @ReactMethod override fun setEncodedForecastPlaybackSpeed(reactTag: Double, speed: Double, promise: Promise) { if(!speed.isFinite() || speed <= 0 || speed > Float.MAX_VALUE) return reject(promise,"INVALID_ARGUMENT","invalid encoded playback speed"); command(reactTag,promise) { it.setEncodedForecastPlaybackSpeed(speed.toFloat()) } } @ReactMethod override fun setEncodedColorBinding(reactTag: Double, fieldKey: String, semanticID: Double, groupID: Double, expectedValueClass: String, expectedUnitID: Double, promise: Promise) { if (!validEncodedId(fieldKey) || !validUInt16(semanticID, true) || !validUInt16(groupID, false) || !validUInt16(expectedUnitID, false)) return reject(promise,"INVALID_ARGUMENT","invalid encoded color binding selector") - val valueClass = when (expectedValueClass) { "SCALAR" -> 0; "DIRECTION_ANGLE" -> 2; else -> return reject(promise,"INVALID_ARGUMENT","invalid encoded color binding valueClass") } + val valueClass = when (expectedValueClass) { "SCALAR" -> 0; "VELOCITY_COMPONENT" -> 1; "DIRECTION_ANGLE" -> 2; else -> return reject(promise,"INVALID_ARGUMENT","invalid encoded color binding valueClass") } command(reactTag,promise) { it.setEncodedColorBinding(fieldKey, semanticID.toInt(), groupID.toInt(), valueClass, expectedUnitID.toInt()) } } @ReactMethod override fun setEncodedParticleBinding(reactTag: Double, fieldKey: String, mode: String, uSemanticID: Double, uGroupID: Double, vSemanticID: Double, vGroupID: Double, directionSemanticID: Double, directionGroupID: Double, expectedUnitID: Double, promise: Promise) { diff --git a/packages/maplibre-particle-rn/src/FieldRuntime.ts b/packages/maplibre-particle-rn/src/FieldRuntime.ts index edabc07a8..659e0a8b8 100644 --- a/packages/maplibre-particle-rn/src/FieldRuntime.ts +++ b/packages/maplibre-particle-rn/src/FieldRuntime.ts @@ -29,7 +29,7 @@ export const FieldRuntime = { payloadRefs.vectorFileUri ?? "", ), submitEncodedField: (reactTag: number, fieldKey: string, metadata: EncodedFieldRuntimeMetadata, contract: ResolvedFieldProfileContractInput, qbinFileUri: string): Promise => { encodedString(fieldKey, "fieldKey"); encodedString(metadata.fieldID, "fieldID"); uint(metadata.generation, "generation", MAX_SAFE, true); uint(metadata.dataVersion, "dataVersion"); if (typeof qbinFileUri !== "string" || !(qbinFileUri.startsWith("/") || qbinFileUri.startsWith("file://"))) return Promise.reject(new Error("qbinFileUri must be an absolute local path or file:// URI")); return nativeBridge().submitEncodedField(reactTag, fieldKey, metadata.fieldID, metadata.generation, metadata.dataVersion, encodedContractJson(contract), qbinFileUri); }, - setEncodedColorBinding: (reactTag: number, binding: EncodedColorBindingInput): Promise => { encodedString(binding.fieldKey, "fieldKey"); uint(binding.semanticID, "semanticID", 0xffff, true); uint(binding.groupID, "groupID", 0xffff); uint(binding.expectedUnitID, "expectedUnitID", 0xffff); if (!["SCALAR", "DIRECTION_ANGLE"].includes(binding.expectedValueClass)) return Promise.reject(new Error("invalid encoded color binding valueClass")); return nativeBridge().setEncodedColorBinding(reactTag, binding.fieldKey, binding.semanticID, binding.groupID, binding.expectedValueClass, binding.expectedUnitID); }, + setEncodedColorBinding: (reactTag: number, binding: EncodedColorBindingInput): Promise => { encodedString(binding.fieldKey, "fieldKey"); uint(binding.semanticID, "semanticID", 0xffff, true); uint(binding.groupID, "groupID", 0xffff); uint(binding.expectedUnitID, "expectedUnitID", 0xffff); if (!["SCALAR", "VELOCITY_COMPONENT", "DIRECTION_ANGLE"].includes(binding.expectedValueClass)) return Promise.reject(new Error("invalid encoded color binding valueClass")); return nativeBridge().setEncodedColorBinding(reactTag, binding.fieldKey, binding.semanticID, binding.groupID, binding.expectedValueClass, binding.expectedUnitID); }, setEncodedParticleBinding: (reactTag: number, binding: EncodedParticleBindingInput): Promise => { encodedString(binding.fieldKey, "fieldKey"); uint(binding.expectedUnitID, "expectedUnitID", 0xffff); const inactive = { semanticID: 0, groupID: 0 }; const valid = (plane: { semanticID: number; groupID: number }) => { uint(plane.semanticID, "selector semanticID", 0xffff, true); uint(plane.groupID, "selector groupID", 0xffff); }; let u = inactive, v = inactive, direction = inactive; if (binding.mode === "VECTOR_COMPONENTS" && binding.uPlane && binding.vPlane && !binding.directionPlane) { u = binding.uPlane; v = binding.vPlane; valid(u); valid(v); } else if (binding.mode === "DIRECTION_ANGLE" && binding.directionPlane && !binding.uPlane && !binding.vPlane) { direction = binding.directionPlane; valid(direction); } else return Promise.reject(new Error("encoded particle binding requires exactly U/V or direction selector; inactive ABI slots are zero")); return nativeBridge().setEncodedParticleBinding(reactTag, binding.fieldKey, binding.mode, u.semanticID, u.groupID, v.semanticID, v.groupID, direction.semanticID, direction.groupID, binding.expectedUnitID); }, setEncodedForecastTime: (reactTag: number, timestampMs: number): Promise => { uint(timestampMs, "forecast timestamp"); return nativeBridge().setEncodedForecastTime(reactTag, timestampMs); }, setEncodedForecastPlaybackState: (reactTag: number, playback: "PLAY" | "PAUSE"): Promise => nativeBridge().setEncodedForecastPlaybackState(reactTag, playback), diff --git a/packages/maplibre-particle-rn/src/types.ts b/packages/maplibre-particle-rn/src/types.ts index de7b5bb82..047645074 100644 --- a/packages/maplibre-particle-rn/src/types.ts +++ b/packages/maplibre-particle-rn/src/types.ts @@ -9,7 +9,7 @@ export type EncodedFieldRuntimeMetadata = Readonly<{ fieldID: string; generation export type RequiredPlaneContractInput = Readonly<{ semanticID: number; valueClass: "SCALAR" | "VELOCITY_COMPONENT" | "DIRECTION_ANGLE" | "VALIDITY"; unitID: number; groupID: number; component: "NONE" | "U_EAST" | "V_NORTH" | "W_UP"; requiredFlag: boolean }>; /** Frozen V4 JSON field names match ResolvedFieldProfileContract exactly. */ export type ResolvedFieldProfileContractInput = Readonly<{ contractSchemaVersion: number; profileID: string; profileVersion: number; allowAdditionalPlanes: boolean; sharedValidityRequired: boolean; timeRelation: "ANY" | "VALID_GTE_REFERENCE" | "VALID_EQ_REFERENCE"; requiredPlanes: readonly RequiredPlaneContractInput[] }>; -export type EncodedColorBindingInput = Readonly<{ fieldKey: string; semanticID: number; groupID: number; expectedValueClass: "SCALAR" | "DIRECTION_ANGLE"; expectedUnitID: number }>; +export type EncodedColorBindingInput = Readonly<{ fieldKey: string; semanticID: number; groupID: number; expectedValueClass: "SCALAR" | "VELOCITY_COMPONENT" | "DIRECTION_ANGLE"; expectedUnitID: number }>; export type EncodedParticleBindingInput = Readonly<{ fieldKey: string; mode: "VECTOR_COMPONENTS" | "DIRECTION_ANGLE"; uPlane?: Readonly<{ semanticID: number; groupID: number }>; vPlane?: Readonly<{ semanticID: number; groupID: number }>; directionPlane?: Readonly<{ semanticID: number; groupID: number }>; expectedUnitID: number }>; export type FieldPayloadRefs = Readonly< { validityFileUri: string } & ( diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp index e14ea8b82..d5e42ff55 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp @@ -1435,7 +1435,7 @@ jni::jint NativeMapView::submitRasterArraySourceEncodedField(JNIEnv& env, const } jni::jint NativeMapView::clearRasterArraySourceEncodedField(JNIEnv& env,const jni::String& sourceId,const jni::String& fieldKey) { auto* s=dynamic_cast(map->getStyle().getSource(jni::Make(env,sourceId))); if(!s)return 0; return s->clearEncodedField(jni::Make(env,fieldKey)) ? 1 : -1; } jni::jint NativeMapView::clearAllRasterArraySourceEncodedFields(JNIEnv& env,const jni::String& sourceId) { auto* s=dynamic_cast(map->getStyle().getSource(jni::Make(env,sourceId))); if(!s)return 0; s->clearAllEncodedFields(); return 1; } -jni::jint NativeMapView::setRasterArraySourceEncodedColorBinding(JNIEnv& env, const jni::String& sourceId, const jni::String& fieldKey, jni::jint semanticID, jni::jint groupID, jni::jint valueClass, jni::jint unitID) { auto* s=dynamic_cast(map->getStyle().getSource(jni::Make(env,sourceId))); if(!s)return 0; if(semanticID<=0||semanticID>UINT16_MAX||groupID<0||groupID>UINT16_MAX||unitID<0||unitID>UINT16_MAX||(valueClass!=0&&valueClass!=2))return -1; s->setColorBinding({jni::Make(env,fieldKey),{static_cast(semanticID),static_cast(groupID)},static_cast(valueClass),static_cast(unitID)}); return 1; } +jni::jint NativeMapView::setRasterArraySourceEncodedColorBinding(JNIEnv& env, const jni::String& sourceId, const jni::String& fieldKey, jni::jint semanticID, jni::jint groupID, jni::jint valueClass, jni::jint unitID) { auto* s=dynamic_cast(map->getStyle().getSource(jni::Make(env,sourceId))); if(!s)return 0; if(semanticID<=0||semanticID>UINT16_MAX||groupID<0||groupID>UINT16_MAX||unitID<0||unitID>UINT16_MAX||(valueClass<0||valueClass>2))return -1; s->setColorBinding({jni::Make(env,fieldKey),{static_cast(semanticID),static_cast(groupID)},static_cast(valueClass),static_cast(unitID)}); return 1; } jni::jint NativeMapView::setRasterArraySourceEncodedParticleBinding(JNIEnv& env, const jni::String& sourceId, const jni::String& fieldKey, jni::jint mode, jni::jint uSemanticID, jni::jint uGroupID, jni::jint vSemanticID, jni::jint vGroupID, jni::jint directionSemanticID, jni::jint directionGroupID, jni::jint unitID) { auto* s=dynamic_cast(map->getStyle().getSource(jni::Make(env,sourceId))); if(!s)return 0; const auto valid=[](jni::jint semantic,jni::jint group){return semantic>0&&semantic<=UINT16_MAX&&group>=0&&group<=UINT16_MAX;}; const auto inactive=[](jni::jint semantic,jni::jint group){return semantic==0&&group==0;}; if(unitID<0||unitID>UINT16_MAX)return -1; mbgl::particle::ParticleBinding binding; binding.fieldKey=jni::Make(env,fieldKey); binding.expectedUnitID=static_cast(unitID); if(mode==0&&valid(uSemanticID,uGroupID)&&valid(vSemanticID,vGroupID)&&inactive(directionSemanticID,directionGroupID)){binding.mode=mbgl::particle::ParticleBindingMode::VectorComponents;binding.uPlane={static_cast(uSemanticID),static_cast(uGroupID)};binding.vPlane={static_cast(vSemanticID),static_cast(vGroupID)};} else if(mode==1&&inactive(uSemanticID,uGroupID)&&inactive(vSemanticID,vGroupID)&&valid(directionSemanticID,directionGroupID)){binding.mode=mbgl::particle::ParticleBindingMode::DirectionAngle;binding.directionPlane={static_cast(directionSemanticID),static_cast(directionGroupID)};} else return -1; s->setParticleBinding(std::move(binding)); return 1; } jni::jint NativeMapView::setRasterArraySourceEncodedForecastTime(JNIEnv& env,const jni::String& sourceId,jni::jlong timestamp) { auto* s=dynamic_cast(map->getStyle().getSource(jni::Make(env,sourceId))); if(!s)return 0; return timestamp>=0&&s->setRequestedForecastTime(static_cast(timestamp))?1:-1; } jni::jint NativeMapView::setRasterArraySourceEncodedForecastPlaybackState(JNIEnv& env,const jni::String& sourceId,jni::jboolean playing) { auto* s=dynamic_cast(map->getStyle().getSource(jni::Make(env,sourceId))); if(!s)return 0; s->setForecastPlaybackState(playing ? mbgl::particle::ForecastPlaybackState::Playing : mbgl::particle::ForecastPlaybackState::Paused); return 1; } diff --git a/test/particle/field_binding.test.cpp b/test/particle/field_binding.test.cpp index 62a34443b..48b284297 100644 --- a/test/particle/field_binding.test.cpp +++ b/test/particle/field_binding.test.cpp @@ -209,6 +209,17 @@ TEST(ParticleFieldBinding, FailsClosedAndSupportsDirectionAngle) { particle::EncodedFieldStore store; submit(store, "weather.wind", makeFrame("wind", Base, 1, "0")); submit(store, "weather.wind", makeFrame("wind", Base + Hour, 1, "1")); + const particle::ColorBinding velocityComponent{"weather.wind", {1, 7, particle::FieldComponent::UEast}, + particle::FieldValueClass::VelocityComponent, + particle::FieldUnitMetersPerSecond}; + const auto velocityProjection = particle::BindingProjectionResolver::resolve(store, velocityComponent, Base); + ASSERT_TRUE(velocityProjection.accepted()); + EXPECT_EQ(velocityProjection.projection->t0PlaneStorage, + velocityProjection.projection->t0Frame->planes.front().storage); + const particle::ColorBinding validity{"weather.wind", {900, 0, particle::FieldComponent::None}, + particle::FieldValueClass::Validity, particle::FieldUnitNone}; + EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, validity, Base).error, + particle::BindingProjectionError::InvalidBinding); EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, color(""), Base).error, particle::BindingProjectionError::InvalidBinding); EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, color("missing"), Base).error, particle::BindingProjectionError::FieldSeriesMissing); EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, color("weather.wind"), Base + 2 * Hour).error, particle::BindingProjectionError::WindowUnavailable);