Compare commits

...

3 Commits

Author SHA1 Message Date
tei
e8b451fe70 release: finalize unified particle.17
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
2026-09-29 09:44:09 +09:00
tei
95f0a45d6d release: prepare unified particle.17 2026-09-28 21:40:56 +09:00
tei
baf9374318 fix: reset V17 weather runtime across Android and iOS
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
2026-09-28 21:37:16 +09:00
25 changed files with 320 additions and 58 deletions

View File

@@ -245,6 +245,10 @@ public:
particle::ParticleProjectionResult resolveParticleProjection() const;
bool clearEncodedField(const std::string& fieldKey);
void clearAllEncodedFields();
// Explicit terminal cleanup for an attached WeatherRuntime. Unlike an ordinary
// V4 window clear, this also drops bindings, forecast clock and V3 host ingress.
// Source and Style layers remain installed for the next runtime generation.
bool resetWeatherRuntimeState();
// Producer-facing timestamp seek. Core alone selects T0/T1 and MIX.
bool seekRuntimeTimelineToTimestamp(uint64_t timestamp);
// Low-frequency JNI commands for the Source-owned validation timeline.

View File

@@ -6,7 +6,7 @@ const marker = "// MapLibreParticleRNTestHost NavSea Particle release SDK";
module.exports = function withParticleNativeAndroid(config) {
return withProjectBuildGradle(config, (project) => {
if (project.modResults.contents.includes(marker)) return project;
project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.16")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
return project;
});
};

View File

@@ -2,6 +2,9 @@
## Unreleased
## 13.4.1-particle.17(Android / iOS 统一发行)
- Mac unified release pipeline 统一生成 Android Native、RN package 与 iOS SwiftPM/XCFramework;共享 Particle revision 17。
## 13.4.1-particle.16(Android / iOS 统一发行)
- Mac unified release pipeline 统一生成 Android Native、RN package 与 iOS SwiftPM/XCFramework;共享 Particle revision 16。

View File

@@ -1,6 +1,6 @@
# @navsea/maplibre-particle-rn
当前版本:`13.4.1-particle.16`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.16`。本包是 React Native 的薄控制面,不负责 Map 组件、provider、HTTP、manifest、下载、缓存或 NavSea 业务策略。
当前版本:`13.4.1-particle.17`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17`。本包是 React Native 的薄控制面,不负责 Map 组件、provider、HTTP、manifest、下载、缓存或 NavSea 业务策略。
## 当前推荐主链:QBIN V4
@@ -37,7 +37,7 @@ Podfile 的 `post_install do |installer|` 必须同时调用 `$MLRN.post_install
RN tgz:
```text
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.16.tgz
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.17.tgz
```
Native Maven repository:
@@ -49,7 +49,7 @@ https://pbf.jpcn.ccwu.cc/maven
Native coordinate:
```text
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.16
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17
```
Expo plugin 与 `android/build.gradle` 必须指向同一 `.16`。正常 Consumer 不复制 AAR、不依赖 MapLibreParticleLab 源码路径,也不以 Maven Local 作为公网验证主路径。完整 Linux 发布与 Win11 验证流程见 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。
Expo plugin 与 `android/build.gradle` 必须指向同一 `.17`。正常 Consumer 不复制 AAR、不依赖 MapLibreParticleLab 源码路径,也不以 Maven Local 作为公网验证主路径。完整 Linux 发布与 Win11 验证流程见 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。

View File

@@ -14,7 +14,7 @@ android {
dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.20"
implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.16"
implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17"
compileOnly project(":maplibre_maplibre-react-native")
testImplementation "junit:junit:4.13.2"
}

View File

@@ -46,7 +46,11 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
),
)
}
val runtime = map.replaceWeatherRuntime(WeatherRuntime.Config(WeatherIds.SOURCE, WeatherIds.COLOR_LAYER, WeatherIds.PARTICLE_LAYER))
val runtime = try {
map.replaceWeatherRuntime(WeatherRuntime.Config(WeatherIds.SOURCE, WeatherIds.COLOR_LAYER, WeatherIds.PARTICLE_LAYER))
} catch (error: IllegalStateException) {
return@withMapView reject(promise, "RUNTIME_CLEANUP_FAILED", error.message ?: "WeatherRuntime replacement cleanup failed")
}
if (!attachmentOwner(view).attach(map)) {
map.disposeWeatherRuntime()
return@withMapView reject(promise, "RUNTIME_DISPOSED", "The MapView attachment owner has been destroyed")
@@ -57,11 +61,18 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
@ReactMethod
override fun detach(reactTag: Double, promise: Promise) = withMapView(reactTag, promise) { view, map ->
val owner = view.getTag(R.id.weather_runtime_attachment_owner) as? WeatherRuntimeAttachmentOwner<*>
val hasRuntime = map.weatherRuntime != null
val detachedByOwner = (owner as? WeatherRuntimeAttachmentOwner<MapLibreMap>)?.detach(map) ?: false
if (!detachedByOwner) map.disposeWeatherRuntime()
if (!hasRuntime) reject(promise, "RUNTIME_DISPOSED", "No attached WeatherRuntime exists for this map")
else promise.resolve(null)
// Duplicate detach is idempotent; Core failures must not be reported as success.
try {
val detachedByOwner = (owner as? WeatherRuntimeAttachmentOwner<MapLibreMap>)?.detach(map) ?: false
if (!detachedByOwner) map.disposeWeatherRuntime()
if (map.weatherRuntime != null) {
reject(promise, "RUNTIME_CLEANUP_FAILED", "WeatherRuntime remains attached after detach")
} else {
promise.resolve(null)
}
} catch (error: IllegalStateException) {
reject(promise, "RUNTIME_CLEANUP_FAILED", error.message ?: "WeatherRuntime detach cleanup failed")
}
}
@ReactMethod override fun getState(reactTag: Double, promise: Promise) = withRuntime(reactTag, promise) { runtime -> promise.resolve(snapshot(runtime)) }

View File

@@ -20,7 +20,7 @@ const iosTag = "navsea-maplibre-particle-rn-maplibre-spm";
module.exports = function withNavSeaMapLibreParticle(config) {
config = withProjectBuildGradle(config, (project) => {
if (project.modResults.contents.includes(androidMarker)) return project;
project.modResults.contents += `\n${androidMarker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.16")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
project.modResults.contents += `\n${androidMarker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
return project;
});

View File

@@ -155,7 +155,16 @@ RCT_EXPORT_MODULE(WeatherRuntimeBridge)
}];
}
- (void)detach:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[self withMapForTag:reactTag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { [map disposeParticleFieldRuntime]; resolve(nil); }];
[self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) {
// Duplicate detach is a no-op, but an actual Source cleanup failure is not.
if (!runtime) { resolve(nil); return; }
NSError *error = nil;
if (![map disposeParticleFieldRuntimeWithError:&error]) {
reject(@"RUNTIME_CLEANUP_FAILED", error.localizedDescription ?: @"WeatherRuntime detach cleanup failed", error);
return;
}
resolve(nil);
}];
}
- (void)getState:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self withMapForTag:reactTag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { resolve(snapshot(runtime)); }]; }
- (void)setRetainedUnderlayEnabled:(double)reactTag enabled:(BOOL)enabled resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {

View File

@@ -1,6 +1,6 @@
{
"name": "@navsea/maplibre-particle-rn",
"version": "13.4.1-particle.16",
"version": "13.4.1-particle.17",
"description": "NavSea MapLibre Particle React Native pure Field Renderer API.",
"license": "BSD-2-Clause",
"main": "src/index.ts",

View File

@@ -1472,6 +1472,16 @@ jni::jboolean NativeMapView::clearRasterArraySourceHostFields(JNIEnv& env, const
return cleared;
}
jni::jboolean NativeMapView::resetRasterArraySourceWeatherRuntime(JNIEnv& env, const jni::String& sourceId) {
const auto id = jni::Make<std::string>(env, sourceId);
auto* source = dynamic_cast<mbgl::style::RasterArraySource*>(map->getStyle().getSource(id));
if (!source) return false;
const bool reset = source->resetWeatherRuntimeState();
mbgl::Log::Info(mbgl::Event::JNI, "WeatherRuntime: event=TERMINAL_RESET sourceId=" + id +
" result=" + (reset ? "SUCCESS" : "REJECTED"));
return reset;
}
jni::jboolean NativeMapView::seekRasterArraySourceTimelineToTimestamp(JNIEnv& env,
const jni::String& sourceId,
const jni::jlong timestamp) {
@@ -1920,6 +1930,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
METHOD(&NativeMapView::submitRasterArraySourceHostField, "nativeSubmitRasterArraySourceHostField"),
METHOD(&NativeMapView::submitRasterArraySourceHostFieldV2, "nativeSubmitRasterArraySourceHostFieldV2"),
METHOD(&NativeMapView::clearRasterArraySourceHostFields, "nativeClearRasterArraySourceHostFields"),
METHOD(&NativeMapView::resetRasterArraySourceWeatherRuntime, "nativeResetRasterArraySourceWeatherRuntime"),
METHOD(&NativeMapView::seekRasterArraySourceTimelineToTimestamp, "nativeSeekRasterArraySourceTimelineToTimestamp"),
METHOD(&NativeMapView::controlRasterArraySourceTimeline, "nativeControlRasterArraySourceTimeline"),
METHOD(&NativeMapView::submitRasterArraySourceEncodedField, "nativeSubmitRasterArraySourceEncodedField"),

View File

@@ -318,6 +318,7 @@ public:
jni::jint,
jni::jint);
jni::jboolean clearRasterArraySourceHostFields(JNIEnv&, const jni::String&);
jni::jboolean resetRasterArraySourceWeatherRuntime(JNIEnv&, const jni::String&);
jni::jboolean seekRasterArraySourceTimelineToTimestamp(JNIEnv&, const jni::String&, jni::jlong);
jni::jboolean controlRasterArraySourceTimeline(JNIEnv&, const jni::String&, jni::jint, jni::jfloat);
jni::jint submitRasterArraySourceEncodedField(JNIEnv&, const jni::String&, const jni::String&, const jni::String&, jni::jlong, jni::jlong, const jni::String&, const jni::Array<jbyte>&);

View File

@@ -491,6 +491,11 @@ public final class MapLibreMap {
return nativeMapView.clearRasterArraySourceHostFields(sourceId);
}
/** Internal terminal reset for V3+V4 state on the same existing Style source. */
boolean resetRasterArraySourceWeatherRuntime(String sourceId) {
return nativeMapView.resetRasterArraySourceWeatherRuntime(sourceId);
}
/** Requests a Core-owned T0/T1/MIX selection for a producer timestamp in milliseconds. */
public boolean seekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp) {
return nativeMapView.seekRasterArraySourceTimelineToTimestamp(sourceId, timestamp);

View File

@@ -172,6 +172,7 @@ interface NativeMap {
boolean vectorPresent, int vectorLength, int validityLength);
boolean clearRasterArraySourceHostFields(String sourceId);
boolean resetRasterArraySourceWeatherRuntime(String sourceId);
boolean seekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp);

View File

@@ -1070,6 +1070,12 @@ final class NativeMapView implements NativeMap {
&& nativeClearRasterArraySourceHostFields(sourceId);
}
@Override
public boolean resetRasterArraySourceWeatherRuntime(String sourceId) {
return !checkState("resetRasterArraySourceWeatherRuntime")
&& nativeResetRasterArraySourceWeatherRuntime(sourceId);
}
@Override
public boolean seekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp) {
return !checkState("seekRasterArraySourceTimelineToTimestamp")
@@ -1876,6 +1882,9 @@ final class NativeMapView implements NativeMap {
@Keep
private native boolean nativeClearRasterArraySourceHostFields(String sourceId);
@Keep
private native boolean nativeResetRasterArraySourceWeatherRuntime(String sourceId);
@Keep
private native boolean nativeSeekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp);

View File

@@ -81,7 +81,25 @@ public final class WeatherRuntime {
public Config getConfig() { return config; }
public boolean isDisposed() { return disposed; }
/** Ends this map/style-scoped facade. It is safe to call repeatedly. */
public void dispose() { if (disposed) return; disposed = true; cancelCadenceWake(); }
public void dispose() {
if (disposed) return;
cancelCadenceWake();
// If Style has already dropped the source, there is no Source-owned V4 store
// left to reset; otherwise disposal must not claim success before Core reset.
if (map.hasStyleSource(config.sourceId)) {
map.setStyleLayerVisibility(config.weatherColorLayerId, false);
map.setStyleLayerVisibility(config.particleLayerId, false);
if (!map.resetRasterArraySourceWeatherRuntime(config.sourceId)) {
throw new IllegalStateException("WeatherRuntime terminal Core reset failed");
}
}
submittedEncodedFieldKeys.clear();
v4ParticleBindingFieldKey = null;
v4ForecastPlaying = false;
playbackState = PlaybackState.PAUSE;
state = State.DISABLED;
disposed = true;
}
/** Core-owned capacity; Host never sees the shader's UBO layout. */
public int getWeatherColorStopCapacity() { return weatherColorStopCapacity; }
@@ -154,8 +172,14 @@ public final class WeatherRuntime {
map.controlRasterArraySourceTimeline(config.sourceId, 1, 0f);
boolean color = map.setStyleLayerVisibility(config.weatherColorLayerId, false);
boolean particle = map.setStyleLayerVisibility(config.particleLayerId, false);
boolean clear = map.clearRasterArraySourceHostFields(config.sourceId);
boolean clear = map.resetRasterArraySourceWeatherRuntime(config.sourceId);
state = stateAfterDisable(color && particle && clear);
if (clear) {
submittedEncodedFieldKeys.clear();
v4ParticleBindingFieldKey = null;
v4ForecastPlaying = false;
playbackState = PlaybackState.PAUSE;
}
if (state != State.READY) cancelCadenceWake();
return result(color && particle && clear);
}

View File

@@ -103,6 +103,40 @@ public class WeatherRuntimeStateTest {
Mockito.verify(map, Mockito.never()).submitRasterArraySourceHostField(Mockito.anyString(), Mockito.anyString(), Mockito.any(byte[].class));
}
@Test public void disableResetsV3AndV4ViaOneCoreCommandAndCanEnableAgain() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
Mockito.when(map.setStyleLayerVisibility(Mockito.anyString(), Mockito.anyBoolean())).thenReturn(true);
Mockito.when(map.resetRasterArraySourceWeatherRuntime("source")).thenReturn(true);
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setWeatherEnabled(false));
assertEquals(WeatherRuntime.State.DISABLED, runtime.getState());
Mockito.verify(map).resetRasterArraySourceWeatherRuntime("source");
Mockito.verify(map, Mockito.never()).clearRasterArraySourceHostFields(Mockito.anyString());
assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setWeatherEnabled(true));
assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, runtime.getState());
}
@Test public void terminalDisposeResetsSourceAndFailureDoesNotClaimDisposed() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
Mockito.when(map.hasStyleSource("source")).thenReturn(true);
Mockito.when(map.resetRasterArraySourceWeatherRuntime("source")).thenReturn(false).thenReturn(true);
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
try {
runtime.dispose();
org.junit.Assert.fail("Core reset failure must be reported");
} catch (IllegalStateException expected) {
assertEquals(false, runtime.isDisposed());
}
runtime.dispose();
assertEquals(true, runtime.isDisposed());
Mockito.verify(map, Mockito.times(2)).resetRasterArraySourceWeatherRuntime("source");
Mockito.clearInvocations(map);
runtime.dispose();
Mockito.verifyNoInteractions(map);
}
@Test public void disposeIsIdempotentAndRejectsFurtherControls() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);

View File

@@ -1 +1 @@
13.4.1-particle.16
13.4.1-particle.17

View File

@@ -131,7 +131,8 @@ MLN_EXPORT
@property (nonatomic, readonly) MLNParticleFieldRuntimeTargetFps targetFps;
- (instancetype)init NS_UNAVAILABLE;
- (void)dispose;
- (void)dispose; // Existing API; use the error-returning form for Bridge operations.
- (BOOL)disposeWithError:(NSError * _Nullable * _Nullable)error;
- (MLNParticleHostFieldSubmitResult)submitWindFieldWithMetadataJSON:(NSString *)metadataJSON payload:(NSData *)payload error:(NSError * _Nullable * _Nullable)error;
- (MLNParticleHostFieldSubmitResult)submitFieldWithMetadataJSON:(NSString *)metadataJSON scalar:(nullable NSData *)scalar vector:(nullable NSData *)vector validity:(NSData *)validity error:(NSError * _Nullable * _Nullable)error;
@@ -196,7 +197,11 @@ MLN_EXPORT
- (nullable MLNParticleFieldRuntime *)replaceParticleFieldRuntimeWithConfiguration:(MLNParticleFieldRuntimeConfiguration *)configuration
error:(NSError * _Nullable * _Nullable)error;
- (nullable MLNParticleFieldRuntime *)particleFieldRuntime;
// Legacy public selector retained for existing Native consumers.
- (void)disposeParticleFieldRuntime;
// Completes V3+V4 Source cleanup before detaching; a failure leaves the runtime
// associated so the caller may retry and must not be reported as success.
- (BOOL)disposeParticleFieldRuntimeWithError:(NSError * _Nullable * _Nullable)error;
@end

View File

@@ -138,11 +138,36 @@ std::vector<char> charsFromData(NSData *data) {
- (MLNParticleFieldRuntimePlaybackState)playbackState { return _playbackState; }
- (MLNParticleFieldRuntimeTargetFps)targetFps { return _targetFps; }
- (void)dispose {
- (BOOL)disposeWithError:(NSError **)error {
if (_disposed) return YES;
// A terminal dispose must clear the Source while the map and Style are valid.
// Otherwise the Source outlives this ObjC runtime and retains V4 field windows.
[_v4CadenceTimer invalidate];
_v4CadenceTimer = nil;
_v4ForecastPlaying = NO;
const BOOL completed = [self withCurrentSource:error action:^bool(
mbgl::style::RasterArraySource &source,
mbgl::style::WeatherColorLayer &weather,
mbgl::style::RasterParticleLayer &particle) {
weather.setVisibility(mbgl::style::VisibilityType::None);
particle.setVisibility(mbgl::style::VisibilityType::None);
return source.resetWeatherRuntimeState();
}];
if (!completed) return NO;
[_submittedEncodedFieldKeys removeAllObjects];
_v4ParticleBindingFieldKey = nil;
_playbackState = MLNParticleFieldRuntimePlaybackStatePause;
_state = MLNParticleFieldRuntimeStateDisabled;
_disposed = YES;
_mapView = nil;
return YES;
}
- (void)dispose {
NSError *error = nil;
if (![self disposeWithError:&error]) {
NSLog(@"WeatherRuntime: legacy dispose cleanup failed: %@", error.localizedDescription ?: @"unknown");
}
}
- (void)refreshV4CadenceWake {
@@ -355,11 +380,34 @@ std::vector<char> charsFromData(NSData *data) {
if (_disposed) return MLNParticleFieldRuntimeCommandResultRejected;
if (enabled && _state == MLNParticleFieldRuntimeStateError) return MLNParticleFieldRuntimeCommandResultRejected;
if (enabled && _state != MLNParticleFieldRuntimeStateDisabled) return MLNParticleFieldRuntimeCommandResultAccepted;
__block bool accepted = false; BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &weather, mbgl::style::RasterParticleLayer &particle) {
if (!enabled) { source.controlRuntimeTimeline(1); weather.setVisibility(mbgl::style::VisibilityType::None); particle.setVisibility(mbgl::style::VisibilityType::None); accepted = source.clearHostFields(); }
else { weather.setVisibility(_weatherColorVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None); particle.setVisibility(_particleVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None); accepted = true; } return true;
if (!enabled && _state == MLNParticleFieldRuntimeStateDisabled) return MLNParticleFieldRuntimeCommandResultAccepted;
__block bool accepted = false;
BOOL completed = [self withCurrentSource:error action:^bool(
mbgl::style::RasterArraySource &source,
mbgl::style::WeatherColorLayer &weather,
mbgl::style::RasterParticleLayer &particle) {
if (!enabled) {
weather.setVisibility(mbgl::style::VisibilityType::None);
particle.setVisibility(mbgl::style::VisibilityType::None);
accepted = source.resetWeatherRuntimeState();
} else {
weather.setVisibility(_weatherColorVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None);
particle.setVisibility(_particleVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None);
accepted = true;
}
return accepted;
}];
if (completed && accepted) _state = enabled ? MLNParticleFieldRuntimeStateWaitingForFields : MLNParticleFieldRuntimeStateDisabled; else if (completed) _state = MLNParticleFieldRuntimeStateError;
if (completed && accepted) {
_state = enabled ? MLNParticleFieldRuntimeStateWaitingForFields : MLNParticleFieldRuntimeStateDisabled;
if (!enabled) {
[_submittedEncodedFieldKeys removeAllObjects];
_v4ParticleBindingFieldKey = nil;
_v4ForecastPlaying = NO;
_playbackState = MLNParticleFieldRuntimePlaybackStatePause;
}
} else if (completed) {
_state = MLNParticleFieldRuntimeStateError;
}
[self refreshV4CadenceWake];
return commandResult(completed && accepted);
}
@@ -400,7 +448,7 @@ namespace { const void *ParticleFieldRuntimeAssociationKey = &ParticleFieldRunti
@implementation MLNMapView (ParticleFieldRuntime)
- (MLNParticleFieldRuntime *)replaceParticleFieldRuntimeWithConfiguration:(MLNParticleFieldRuntimeConfiguration *)configuration error:(NSError **)error {
[self disposeParticleFieldRuntime];
if (![self disposeParticleFieldRuntimeWithError:error]) return nil;
if (!configuration) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"A particle field runtime configuration is required."); return nil; }
MLNParticleFieldRuntime *runtime = [[MLNParticleFieldRuntime alloc] initWithMapView:self configuration:configuration error:error];
if (!runtime) return nil;
@@ -413,9 +461,18 @@ namespace { const void *ParticleFieldRuntimeAssociationKey = &ParticleFieldRunti
}
- (void)disposeParticleFieldRuntime {
NSError *error = nil;
if (![self disposeParticleFieldRuntimeWithError:&error]) {
NSLog(@"WeatherRuntime: legacy map dispose cleanup failed: %@", error.localizedDescription ?: @"unknown");
}
}
- (BOOL)disposeParticleFieldRuntimeWithError:(NSError **)error {
MLNParticleFieldRuntime *runtime = self.particleFieldRuntime;
[runtime dispose];
if (!runtime) return YES;
if (![runtime disposeWithError:error]) return NO;
objc_setAssociatedObject(self, ParticleFieldRuntimeAssociationKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return YES;
}
@end

View File

@@ -9,8 +9,8 @@ let package = Package(
targets: [
.binaryTarget(
name: "MapLibre",
url: "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.16/MapLibre.dynamic.xcframework.zip",
checksum: "88f5b8190f2c7479b54c4a4cb51de372b61902d2a074b9522ccaaefd4d27afb8"
url: "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.17/MapLibre.dynamic.xcframework.zip",
checksum: "5535345dd480f80c0f083811abe93f011b41b4d1a03588189eec5c50a36af0be"
),
]
)

View File

@@ -1,14 +1,14 @@
# MapLibre Particle iOS 发行
当前正式发行:6.28.0-particle.16
当前正式发行:6.28.0-particle.17
- Renderer:Metal
- Product / Module:MapLibre
- Binary:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.16/MapLibre.dynamic.xcframework.zip
- SHA-256:88f5b8190f2c7479b54c4a4cb51de372b61902d2a074b9522ccaaefd4d27afb8
- Binary:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.17/MapLibre.dynamic.xcframework.zip
- SHA-256:5535345dd480f80c0f083811abe93f011b41b4d1a03588189eec5c50a36af0be
- SwiftPM:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git
- Exact Version:6.28.0-particle.16
- Source Commit:e9f84a8ec6aa99a308af30c1957e83c31ec5f23f
- Exact Version:6.28.0-particle.17
- Source Commit:95f0a45d6d97c55f0fd417cf4d7a9a3746ab5aa3
构建命令固定为:

View File

@@ -1,19 +1,19 @@
{
"schemaVersion": 2,
"status": "PUBLISHED_PUBLIC_READBACK_PASS",
"distributionVersion": "6.28.0-particle.16",
"distributionVersion": "6.28.0-particle.17",
"moduleName": "MapLibre",
"artifact": {
"fileName": "MapLibre.dynamic.xcframework.zip",
"sizeBytes": 8442202,
"sha256": "88f5b8190f2c7479b54c4a4cb51de372b61902d2a074b9522ccaaefd4d27afb8",
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.16/MapLibre.dynamic.xcframework.zip"
"sizeBytes": 8443308,
"sha256": "5535345dd480f80c0f083811abe93f011b41b4d1a03588189eec5c50a36af0be",
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.17/MapLibre.dynamic.xcframework.zip"
},
"sourceCommit": "e9f84a8ec6aa99a308af30c1957e83c31ec5f23f",
"sourceCommit": "95f0a45d6d97c55f0fd417cf4d7a9a3746ab5aa3",
"swiftPackage": {
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git",
"requirement": "exactVersion",
"version": "6.28.0-particle.16",
"version": "6.28.0-particle.17",
"productName": "MapLibre"
},
"notes": "Metal renderer release built with bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic"

View File

@@ -1,48 +1,48 @@
{
"schemaVersion": 2,
"releaseState": "PUBLISHED_PUBLIC_READBACK_PASS",
"particleRevision": 16,
"sourceCommit": "e9f84a8ec6aa99a308af30c1957e83c31ec5f23f",
"generatedAtUtc": "2026-09-27T13:34:02.735946+00:00",
"particleRevision": 17,
"sourceCommit": "95f0a45d6d97c55f0fd417cf4d7a9a3746ab5aa3",
"generatedAtUtc": "2026-09-29T00:43:56.789316+00:00",
"reactNative": {
"name": "@navsea/maplibre-particle-rn",
"version": "13.4.1-particle.16",
"version": "13.4.1-particle.17",
"artifact": {
"fileName": "navsea-maplibre-particle-rn-13.4.1-particle.16.tgz",
"sizeBytes": 28295,
"sha256": "0016dd4719cbe02decaa940abbac2b2589448f0cc11f3944132d8e08627d627d",
"url": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.16.tgz"
"fileName": "navsea-maplibre-particle-rn-13.4.1-particle.17.tgz",
"sizeBytes": 28526,
"sha256": "7415a094da5351b86a3c6343a8bc2ef75b7a8ad49577a5f0efc81407d72a79ec",
"url": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.17.tgz"
},
"mapLibreReactNative": "11.3.6"
},
"android": {
"version": "13.4.1-particle.16",
"coordinate": "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.16",
"version": "13.4.1-particle.17",
"coordinate": "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17",
"renderer": "opengl",
"artifact": {
"fileName": "android-sdk-particle-opengl-13.4.1-particle.16.aar",
"sizeBytes": 170123246,
"sha256": "d39f8f4d0f96898874a0869823e02a51ecd985a877d88407f10f8b0130892380",
"url": "https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.16/android-sdk-particle-opengl-13.4.1-particle.16.aar"
"fileName": "android-sdk-particle-opengl-13.4.1-particle.17.aar",
"sizeBytes": 170158515,
"sha256": "6c5f10f3ddd8b81e1c14ed9c310f8d5e216dfdded32c3db55bfb0ba5f5d175d5",
"url": "https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.17/android-sdk-particle-opengl-13.4.1-particle.17.aar"
},
"ndk": "28.2.13676358",
"gradle": "9.5.1",
"jdk": "openjdk version \"17.0.20.1\" 2026-08-18"
},
"ios": {
"distributionVersion": "6.28.0-particle.16",
"distributionVersion": "6.28.0-particle.17",
"renderer": "metal",
"moduleName": "MapLibre",
"artifact": {
"fileName": "MapLibre.dynamic.xcframework.zip",
"sizeBytes": 8442202,
"sha256": "88f5b8190f2c7479b54c4a4cb51de372b61902d2a074b9522ccaaefd4d27afb8",
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.16/MapLibre.dynamic.xcframework.zip"
"sizeBytes": 8443308,
"sha256": "5535345dd480f80c0f083811abe93f011b41b4d1a03588189eec5c50a36af0be",
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.17/MapLibre.dynamic.xcframework.zip"
},
"swiftPackage": {
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git",
"requirement": "exactVersion",
"version": "6.28.0-particle.16",
"version": "6.28.0-particle.17",
"productName": "MapLibre"
},
"xcode": "Xcode 26.3 Build version 17C529 ",

View File

@@ -535,6 +535,37 @@ void RasterArraySource::clearAllEncodedFields() {
refreshV4RendererState();
}
bool RasterArraySource::resetWeatherRuntimeState() {
// V3 Host ingress and V4 QBIN can coexist on the same raster-array Source.
// Clearing the V3 window alone would leave the independent V4 store/projection.
// Pause first so Source notifications during cleanup cannot advance the clock.
forecastClock.pause();
lastForecastTick = {};
if (impl().hostFieldSubmission && !clearHostFields()) return false;
const auto oldFieldCount = encodedFieldStore.fieldCount();
const auto oldDecodedBytes = encodedFieldStore.decodedByteSize();
const auto hadColorProjection = static_cast<bool>(impl().v4RendererState.color);
const auto hadParticleProjection = static_cast<bool>(impl().v4RendererState.particle);
encodedFieldStore.clearAll();
colorBinding.reset();
particleBinding.reset();
forecastClock = particle::ForecastClock{};
lastForecastTick = {};
refreshV4RendererState();
Log::Info(Event::Render,
"WeatherRuntime: event=V4_RUNTIME_RESET sourceId=" + getID() +
" oldFieldCount=" + std::to_string(oldFieldCount) +
" oldDecodedBytes=" + std::to_string(oldDecodedBytes) +
" oldColorProjection=" + std::to_string(hadColorProjection) +
" oldParticleProjection=" + std::to_string(hadParticleProjection) +
" remainingFieldCount=" + std::to_string(encodedFieldStore.fieldCount()) +
" remainingDecodedBytes=" + std::to_string(encodedFieldStore.decodedByteSize()) +
" remainingColorProjection=" + std::to_string(static_cast<bool>(impl().v4RendererState.color)) +
" remainingParticleProjection=" + std::to_string(static_cast<bool>(impl().v4RendererState.particle)));
return true;
}
void RasterArraySource::refreshV4RendererState() {
Impl::V4RendererState next;
const auto& current = impl().v4RendererState;

View File

@@ -912,3 +912,60 @@ TEST(Source, RasterArrayV4MaterializerDecodesDirectionToUInt16LittleEndian) {
EXPECT_NEAR(materialized->t0->vector[7], 0.0f, 1e-5f);
EXPECT_EQ(materialized->t0->vectorSemantics, particle::VectorSemantics::Direction);
}
// The RN weather source uses Host V3 ingress and V4 QBIN on one Style source.
// A terminal detach must clear both, drop old bindings, and permit a new owner.
TEST(Source, RasterArrayV4TerminalResetClearsBothIngressesAndAllowsNewGeneration) {
style::RasterArraySource source{"v4-detach-reset"};
auto v3T0 = particle::OfflineFixtureParticleAdapter::load("uniform-east", 71);
auto v3T1 = particle::OfflineFixtureParticleAdapter::load("uniform-north", 72);
v3T1.validTime = v3T0.validTime + Hour;
ASSERT_EQ(source.submitInitialRuntimeFieldWindow(v3T0, v3T1),
style::RasterArrayRuntimeFieldUpdateResult::Applied);
ASSERT_TRUE(source.impl().field);
ASSERT_TRUE(source.impl().nextField);
submit(source, "wind", "wind", Base, 1);
submit(source, "wind", "wind", Base + Hour, 2);
source.setColorBinding({"wind", {1, 1, particle::FieldComponent::UEast},
particle::FieldValueClass::VelocityComponent, 1});
source.setParticleBinding({particle::ParticleBindingMode::VectorComponents,
"wind", {1, 1, particle::FieldComponent::UEast},
{2, 1, particle::FieldComponent::VNorth}, {}, 1});
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2));
ASSERT_TRUE(source.impl().v4RendererState.color);
ASSERT_TRUE(source.impl().v4RendererState.particle);
ASSERT_EQ(source.impl().v4RendererState.color->sampleState.mix, 0.5f);
EXPECT_EQ(source.impl().v4RendererState.particle->sampleState.mix, 0.5f);
source.setForecastPlaybackState(particle::ForecastPlaybackState::Playing);
ASSERT_EQ(source.impl().hostFieldSubmission, true);
ASSERT_TRUE(source.resetWeatherRuntimeState());
EXPECT_FALSE(source.impl().field);
EXPECT_FALSE(source.impl().nextField);
EXPECT_FALSE(source.impl().timeline);
EXPECT_EQ(source.impl().v4RendererState.color, nullptr);
EXPECT_EQ(source.impl().v4RendererState.particle, nullptr);
EXPECT_FALSE(source.impl().v4RendererState.colorInput);
EXPECT_FALSE(source.impl().v4RendererState.particleInput);
EXPECT_EQ(source.resolveColorProjection().error, particle::BindingProjectionError::InvalidBinding);
EXPECT_EQ(source.resolveParticleProjection().error, particle::BindingProjectionError::InvalidBinding);
EXPECT_EQ(source.impl().v4RendererState.color, nullptr);
EXPECT_EQ(source.impl().v4RendererState.particle, nullptr);
EXPECT_FALSE(source.tickForecastClock(std::chrono::steady_clock::now()));
EXPECT_TRUE(source.resetWeatherRuntimeState()); // Idempotent.
submit(source, "wind", "wind", Base, 3);
submit(source, "wind", "wind", Base + Hour, 4);
source.setColorBinding({"wind", {1, 1, particle::FieldComponent::UEast},
particle::FieldValueClass::VelocityComponent, 1});
source.setParticleBinding({particle::ParticleBindingMode::VectorComponents,
"wind", {1, 1, particle::FieldComponent::UEast},
{2, 1, particle::FieldComponent::VNorth}, {}, 1});
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2));
EXPECT_TRUE(source.resolveColorProjection().accepted());
EXPECT_TRUE(source.resolveParticleProjection().accepted());
EXPECT_TRUE(source.impl().v4RendererState.color);
EXPECT_TRUE(source.impl().v4RendererState.particle);
}