fix: reset V17 weather runtime across Android and iOS
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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>&);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user