diff --git a/platform/ios/src/MLNParticleFieldRuntime.h b/platform/ios/src/MLNParticleFieldRuntime.h index 9542e4a2f..ee845126e 100644 --- a/platform/ios/src/MLNParticleFieldRuntime.h +++ b/platform/ios/src/MLNParticleFieldRuntime.h @@ -1,4 +1,5 @@ #import +#import #import "MLNFoundation.h" #import "MLNMapView.h" @@ -47,6 +48,60 @@ typedef NS_ENUM(NSInteger, MLNParticleEncodedParticleBindingMode) { MLNParticleEncodedParticleBindingModeDirectionAngle = 1, }; +/// V3 state is facade-owned control state; field windows and playback remain Core-owned. +typedef NS_ENUM(NSInteger, MLNParticleFieldRuntimeState) { + MLNParticleFieldRuntimeStateDisabled, + MLNParticleFieldRuntimeStateWaitingForFields, + MLNParticleFieldRuntimeStateReady, + MLNParticleFieldRuntimeStateError, +}; + +typedef NS_ENUM(NSInteger, MLNParticleFieldRuntimePlaybackState) { + MLNParticleFieldRuntimePlaybackStatePlay, + MLNParticleFieldRuntimePlaybackStatePause, +}; + +typedef NS_ENUM(NSInteger, MLNParticleFieldRuntimeTargetFps) { + MLNParticleFieldRuntimeTargetFpsAuto, + MLNParticleFieldRuntimeTargetFps30, + MLNParticleFieldRuntimeTargetFps60, +}; + +typedef NS_ENUM(NSInteger, MLNParticleFieldRuntimeCommandResult) { + MLNParticleFieldRuntimeCommandResultRejected, + MLNParticleFieldRuntimeCommandResultAccepted, +}; + +typedef NS_ENUM(NSInteger, MLNParticleHostFieldSubmitResult) { + /// Facade/lifecycle/argument errors are returned through NSError and use no Core result. + MLNParticleHostFieldSubmitResultNone = 0, + MLNParticleHostFieldSubmitResultRejectedSourceMode = -1, + MLNParticleHostFieldSubmitResultRejectedAdapter = -2, + MLNParticleHostFieldSubmitResultRejectedIncompatible = -3, + MLNParticleHostFieldSubmitResultRejectedNonMonotonic = -4, + MLNParticleHostFieldSubmitResultRejectedCapacity = -5, + MLNParticleHostFieldSubmitResultAcceptedPendingInitialWindow = 1, + MLNParticleHostFieldSubmitResultInitialWindowReady = 2, + MLNParticleHostFieldSubmitResultAcceptedNextSnapshot = 3, + MLNParticleHostFieldSubmitResultDuplicate = 4, +}; + +typedef NS_ENUM(NSInteger, MLNParticleFieldValueSource) { + MLNParticleFieldValueSourceScalar, + MLNParticleFieldValueSourceVectorMagnitude, +}; + +MLN_EXPORT +@interface MLNParticleWeatherColorStop : NSObject +@property (nonatomic, readonly) float value; +@property (nonatomic, readonly) float red; +@property (nonatomic, readonly) float green; +@property (nonatomic, readonly) float blue; +@property (nonatomic, readonly) float alpha; +- (instancetype)initWithValue:(float)value red:(float)red green:(float)green blue:(float)blue alpha:(float)alpha; +- (instancetype)init NS_UNAVAILABLE; +@end + /** Host-neutral identifiers for the existing style JSON raster-array and particle layers. */ MLN_EXPORT @interface MLNParticleFieldRuntimeConfiguration : NSObject @@ -71,10 +126,36 @@ MLN_EXPORT @property (nonatomic, copy, readonly) MLNParticleFieldRuntimeConfiguration *configuration; @property (nonatomic, readonly, getter=isDisposed) BOOL disposed; +@property (nonatomic, readonly) MLNParticleFieldRuntimeState state; +@property (nonatomic, readonly) MLNParticleFieldRuntimePlaybackState playbackState; +@property (nonatomic, readonly) MLNParticleFieldRuntimeTargetFps targetFps; - (instancetype)init NS_UNAVAILABLE; - (void)dispose; +- (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; +- (MLNParticleFieldRuntimeCommandResult)clearFieldsWithError:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setWeatherEnabled:(BOOL)enabled error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setWeatherColorVisible:(BOOL)visible error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setParticleVisible:(BOOL)visible error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setTargetFps:(MLNParticleFieldRuntimeTargetFps)target error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setForecastTime:(uint64_t)timestamp error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setPlaybackState:(MLNParticleFieldRuntimePlaybackState)playback error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setPlaybackSpeed:(float)speed error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setFieldValueSource:(MLNParticleFieldValueSource)source error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setWeatherColorStops:(NSArray *)stops error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setWeatherColorOpacity:(float)opacity error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setParticleCount:(uint32_t)count error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setParticleLineWidth:(float)value error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setParticleLength:(float)value error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setParticleOpacity:(float)value error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setTrailOpacity:(float)value error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setTrailFade:(float)value error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setParticleAdvectionScale:(float)value error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setParticleColorRed:(float)red green:(float)green blue:(float)blue alpha:(float)alpha error:(NSError * _Nullable * _Nullable)error; +- (MLNParticleFieldRuntimeCommandResult)setTrailColorRed:(float)red green:(float)green blue:(float)blue alpha:(float)alpha error:(NSError * _Nullable * _Nullable)error; + - (MLNParticleEncodedFieldSubmitResult)submitEncodedFieldWithFieldKey:(NSString *)fieldKey fieldID:(NSString *)fieldID generation:(uint64_t)generation diff --git a/platform/ios/src/MLNParticleFieldRuntime.mm b/platform/ios/src/MLNParticleFieldRuntime.mm index b7e88f7a1..82f5a016b 100644 --- a/platform/ios/src/MLNParticleFieldRuntime.mm +++ b/platform/ios/src/MLNParticleFieldRuntime.mm @@ -5,6 +5,7 @@ #import #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include +#include #include NSErrorDomain const MLNParticleFieldRuntimeErrorDomain = @"org.maplibre.MLNParticleFieldRuntime"; @@ -22,6 +24,7 @@ namespace { constexpr NSUInteger MaxEncodedIdentifierUTF8Bytes = 64; constexpr NSUInteger MaxContractUTF8Bytes = 256 * 1024; constexpr NSUInteger MaxQbinBytes = 64 * 1024 * 1024; +constexpr NSUInteger MaxMetadataUTF8Bytes = 1024 * 1024; void setError(NSError **error, MLNParticleFieldRuntimeErrorCode code, NSString *description) { if (error) *error = [NSError errorWithDomain:MLNParticleFieldRuntimeErrorDomain @@ -42,8 +45,27 @@ std::string stringFromNSString(NSString *value) { return utf8 ? std::string(utf8) : std::string(); } +MLNParticleFieldRuntimeCommandResult commandResult(bool accepted) { + return accepted ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected; +} + +bool validFloat(float value) { return std::isfinite(value); } + +std::vector charsFromData(NSData *data) { + std::vector value(data.length); + [data getBytes:value.data() length:value.size()]; + return value; +} + } // namespace +@implementation MLNParticleWeatherColorStop +- (instancetype)initWithValue:(float)value red:(float)red green:(float)green blue:(float)blue alpha:(float)alpha { + if ((self = [super init])) { _value = value; _red = red; _green = green; _blue = blue; _alpha = alpha; } + return self; +} +@end + @implementation MLNParticleFieldRuntimeConfiguration - (instancetype)initWithSourceIdentifier:(NSString *)sourceIdentifier @@ -74,6 +96,11 @@ std::string stringFromNSString(NSString *value) { @interface MLNParticleFieldRuntime () { __weak MLNMapView *_mapView; BOOL _disposed; + MLNParticleFieldRuntimeState _state; + MLNParticleFieldRuntimePlaybackState _playbackState; + MLNParticleFieldRuntimeTargetFps _targetFps; + BOOL _weatherColorVisible; + BOOL _particleVisible; } - (instancetype)initWithMapView:(MLNMapView *)mapView configuration:(MLNParticleFieldRuntimeConfiguration *)configuration @@ -88,19 +115,27 @@ std::string stringFromNSString(NSString *value) { if ((self = [super init])) { _mapView = mapView; _configuration = [configuration copy]; - if (![self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &) { return true; }]) return nil; + _state = MLNParticleFieldRuntimeStateWaitingForFields; + _playbackState = MLNParticleFieldRuntimePlaybackStatePause; + _targetFps = MLNParticleFieldRuntimeTargetFpsAuto; + _weatherColorVisible = YES; + _particleVisible = YES; + if (![self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { return true; }]) return nil; } return self; } - (BOOL)isDisposed { return _disposed; } +- (MLNParticleFieldRuntimeState)state { return _state; } +- (MLNParticleFieldRuntimePlaybackState)playbackState { return _playbackState; } +- (MLNParticleFieldRuntimeTargetFps)targetFps { return _targetFps; } - (void)dispose { _disposed = YES; _mapView = nil; } -- (BOOL)withCurrentSource:(NSError **)error action:(bool (^)(mbgl::style::RasterArraySource &))action { +- (BOOL)withCurrentSource:(NSError **)error action:(bool (^)(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &))action { if (_disposed) { setError(error, MLNParticleFieldRuntimeErrorDisposed, @"Particle field runtime has been disposed."); return NO; @@ -142,7 +177,7 @@ std::string stringFromNSString(NSString *value) { setError(error, MLNParticleFieldRuntimeErrorLayerSourceMismatch, @"Configured particle layers do not reference the configured raster-array source."); return NO; } - return action(*source); + return action(*source, *weather, *particle); } @catch (NSException *exception) { setError(error, MLNParticleFieldRuntimeErrorMapUnavailable, exception.reason ?: @"The underlying map is unavailable."); return NO; @@ -156,6 +191,10 @@ std::string stringFromNSString(NSString *value) { contractJSON:(NSString *)contractJSON qbin:(NSData *)qbin error:(NSError **)error { + if (_state == MLNParticleFieldRuntimeStateDisabled) { + setError(error, MLNParticleFieldRuntimeErrorDisposed, @"Particle field runtime is disabled; enable it before submitting fields."); + return MLNParticleEncodedFieldSubmitResultNone; + } if (!hasValidIdentifier(fieldKey) || !hasValidIdentifier(fieldID) || !hasValidContract(contractJSON) || generation == 0 || qbin.length == 0 || qbin.length > MaxQbinBytes) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Encoded field arguments violate the V4 transport contract."); return MLNParticleEncodedFieldSubmitResultNone; @@ -168,7 +207,7 @@ std::string stringFromNSString(NSString *value) { std::vector bytes(qbin.length); [qbin getBytes:bytes.data() length:bytes.size()]; __block MLNParticleEncodedFieldSubmitResult result = MLNParticleEncodedFieldSubmitResultNone; - const BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source) { + const BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { const auto coreResult = source.submitEncodedField(stringFromNSString(fieldKey), {stringFromNSString(fieldID), generation, dataVersion}, *contract, bytes); result = static_cast(coreResult); @@ -181,7 +220,7 @@ std::string stringFromNSString(NSString *value) { if (!hasValidIdentifier(fieldKey) || semanticID == 0 || expectedValueClass < MLNParticleFieldValueClassScalar || expectedValueClass > MLNParticleFieldValueClassDirectionAngle) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Invalid encoded color binding."); return NO; } - return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source) { + return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { source.setColorBinding({stringFromNSString(fieldKey), {semanticID, groupID}, static_cast(expectedValueClass), expectedUnitID}); return true; }]; @@ -192,7 +231,7 @@ std::string stringFromNSString(NSString *value) { const bool vector = mode == MLNParticleEncodedParticleBindingModeVectorComponents && uSemanticID != 0 && vSemanticID != 0 && directionSemanticID == 0 && directionGroupID == 0; const bool direction = mode == MLNParticleEncodedParticleBindingModeDirectionAngle && uSemanticID == 0 && uGroupID == 0 && vSemanticID == 0 && vGroupID == 0 && directionSemanticID != 0; if (!vector && !direction) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Inactive particle selectors must be exactly semanticID = 0 and groupID = 0."); return NO; } - return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source) { + return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { mbgl::particle::ParticleBinding binding; binding.fieldKey = stringFromNSString(fieldKey); binding.expectedUnitID = expectedUnitID; @@ -204,27 +243,104 @@ std::string stringFromNSString(NSString *value) { } - (BOOL)setEncodedForecastTime:(uint64_t)timestamp error:(NSError **)error { - return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source) { return source.setRequestedForecastTime(timestamp); }]; + return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { return source.setRequestedForecastTime(timestamp); }]; } - (BOOL)setEncodedForecastPlaybackState:(BOOL)playing error:(NSError **)error { - return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source) { source.setForecastPlaybackState(playing ? mbgl::particle::ForecastPlaybackState::Playing : mbgl::particle::ForecastPlaybackState::Paused); return true; }]; + return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { source.setForecastPlaybackState(playing ? mbgl::particle::ForecastPlaybackState::Playing : mbgl::particle::ForecastPlaybackState::Paused); return true; }]; } - (BOOL)setEncodedForecastPlaybackSpeed:(float)speed error:(NSError **)error { if (!std::isfinite(speed) || speed <= 0) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Forecast playback speed must be finite and greater than zero."); return NO; } - return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source) { return source.setForecastPlaybackSpeed(speed); }]; + return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { return source.setForecastPlaybackSpeed(speed); }]; } - (BOOL)clearEncodedFieldWithFieldKey:(NSString *)fieldKey error:(NSError **)error { if (!hasValidIdentifier(fieldKey)) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Invalid encoded field key."); return NO; } - return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source) { return source.clearEncodedField(stringFromNSString(fieldKey)); }]; + return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { return source.clearEncodedField(stringFromNSString(fieldKey)); }]; } - (BOOL)clearAllEncodedFieldsWithError:(NSError **)error { - return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source) { source.clearAllEncodedFields(); return true; }]; + return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { source.clearAllEncodedFields(); return true; }]; } +- (MLNParticleHostFieldSubmitResult)submitWindFieldWithMetadataJSON:(NSString *)metadataJSON payload:(NSData *)payload error:(NSError **)error { + if (_state == MLNParticleFieldRuntimeStateDisabled) { setError(error, MLNParticleFieldRuntimeErrorDisposed, @"Particle field runtime is disabled; enable it before submitting fields."); return MLNParticleHostFieldSubmitResultNone; } + if (!metadataJSON.length || [metadataJSON lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > MaxMetadataUTF8Bytes || !payload.length || payload.length > MaxQbinBytes) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Invalid V3 wind field payload."); return MLNParticleHostFieldSubmitResultNone; } + __block MLNParticleHostFieldSubmitResult result = MLNParticleHostFieldSubmitResultNone; + BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { + std::vector bytes = charsFromData(payload); + result = static_cast(source.submitHostField(stringFromNSString(metadataJSON), bytes)); return true; + }]; + if (completed && result > 0) _state = result == MLNParticleHostFieldSubmitResultAcceptedPendingInitialWindow ? MLNParticleFieldRuntimeStateWaitingForFields : (result == MLNParticleHostFieldSubmitResultDuplicate ? _state : MLNParticleFieldRuntimeStateReady); + else if (completed && result == MLNParticleHostFieldSubmitResultRejectedSourceMode) _state = MLNParticleFieldRuntimeStateError; + return completed ? result : MLNParticleHostFieldSubmitResultNone; +} + +- (MLNParticleHostFieldSubmitResult)submitFieldWithMetadataJSON:(NSString *)metadataJSON scalar:(NSData *)scalar vector:(NSData *)vector validity:(NSData *)validity error:(NSError **)error { + if (_state == MLNParticleFieldRuntimeStateDisabled) { setError(error, MLNParticleFieldRuntimeErrorDisposed, @"Particle field runtime is disabled; enable it before submitting fields."); return MLNParticleHostFieldSubmitResultNone; } + const NSUInteger scalarLength = scalar.length; + const NSUInteger vectorLength = vector.length; + const NSUInteger validityLength = validity.length; + if (!metadataJSON.length || [metadataJSON lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > MaxMetadataUTF8Bytes || !validityLength || (!scalar && !vector) || (scalar && !scalarLength) || (vector && !vectorLength) || scalarLength > MaxQbinBytes || vectorLength > MaxQbinBytes || validityLength > MaxQbinBytes || vectorLength > MaxQbinBytes - scalarLength || validityLength > MaxQbinBytes - scalarLength - vectorLength) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Invalid V3 field payload."); return MLNParticleHostFieldSubmitResultNone; } + __block MLNParticleHostFieldSubmitResult result = MLNParticleHostFieldSubmitResultNone; + BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { + mbgl::particle::ParticleFieldPayloadBundle payloads; + if (scalar) payloads.scalar = charsFromData(scalar); if (vector) payloads.vector = charsFromData(vector); payloads.validity = charsFromData(validity); + result = static_cast(source.submitHostField(stringFromNSString(metadataJSON), payloads)); return true; + }]; + if (completed && result > 0) _state = result == MLNParticleHostFieldSubmitResultAcceptedPendingInitialWindow ? MLNParticleFieldRuntimeStateWaitingForFields : (result == MLNParticleHostFieldSubmitResultDuplicate ? _state : MLNParticleFieldRuntimeStateReady); + else if (completed && result == MLNParticleHostFieldSubmitResultRejectedSourceMode) _state = MLNParticleFieldRuntimeStateError; + return completed ? result : MLNParticleHostFieldSubmitResultNone; +} + +- (MLNParticleFieldRuntimeCommandResult)clearFieldsWithError:(NSError **)error { + if (_disposed || _state == MLNParticleFieldRuntimeStateDisabled) return MLNParticleFieldRuntimeCommandResultRejected; + __block bool accepted = false; BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { accepted = source.clearHostFields(); return true; }]; + if (completed && accepted) _state = MLNParticleFieldRuntimeStateWaitingForFields; return commandResult(completed && accepted); +} + +- (MLNParticleFieldRuntimeCommandResult)setWeatherEnabled:(BOOL)enabled error:(NSError **)error { + 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 (completed && accepted) _state = enabled ? MLNParticleFieldRuntimeStateWaitingForFields : MLNParticleFieldRuntimeStateDisabled; else if (completed) _state = MLNParticleFieldRuntimeStateError; + return commandResult(completed && accepted); +} + +- (MLNParticleFieldRuntimeCommandResult)setWeatherColorVisible:(BOOL)visible error:(NSError **)error { if (_disposed) return MLNParticleFieldRuntimeCommandResultRejected; _weatherColorVisible = visible; return [self setVisibility:visible weather:YES error:error]; } +- (MLNParticleFieldRuntimeCommandResult)setParticleVisible:(BOOL)visible error:(NSError **)error { if (_disposed) return MLNParticleFieldRuntimeCommandResultRejected; _particleVisible = visible; return [self setVisibility:visible weather:NO error:error]; } +- (MLNParticleFieldRuntimeCommandResult)setVisibility:(BOOL)visible weather:(BOOL)weatherLayer error:(NSError **)error { + BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &weather, mbgl::style::RasterParticleLayer &particle) { (weatherLayer ? static_cast(weather) : static_cast(particle)).setVisibility(visible && _state != MLNParticleFieldRuntimeStateDisabled ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None); return true; }]; return commandResult(completed); +} + +- (MLNParticleFieldRuntimeCommandResult)setTargetFps:(MLNParticleFieldRuntimeTargetFps)target error:(NSError **)error { uint8_t value = target == MLNParticleFieldRuntimeTargetFps30 ? 30 : target == MLNParticleFieldRuntimeTargetFps60 ? 60 : target == MLNParticleFieldRuntimeTargetFpsAuto ? 0 : 255; if (value == 255) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Invalid target FPS."); return MLNParticleFieldRuntimeCommandResultRejected; } __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.setRuntimeTargetFps(value); return true; }]; if (done && ok) _targetFps = target; return commandResult(done && ok); } +- (MLNParticleFieldRuntimeCommandResult)setForecastTime:(uint64_t)timestamp error:(NSError **)error { if (_state == MLNParticleFieldRuntimeStateDisabled) return MLNParticleFieldRuntimeCommandResultRejected; __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.seekRuntimeTimelineToTimestamp(timestamp); return true; }]; return commandResult(done && ok); } +- (MLNParticleFieldRuntimeCommandResult)setPlaybackState:(MLNParticleFieldRuntimePlaybackState)playback error:(NSError **)error { if (_state == MLNParticleFieldRuntimeStateDisabled || (playback != MLNParticleFieldRuntimePlaybackStatePlay && playback != MLNParticleFieldRuntimePlaybackStatePause)) return MLNParticleFieldRuntimeCommandResultRejected; __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.controlRuntimeTimeline(playback == MLNParticleFieldRuntimePlaybackStatePlay ? 0 : 1); return true; }]; if (done && ok) _playbackState = playback; return commandResult(done && ok); } +- (MLNParticleFieldRuntimeCommandResult)setPlaybackSpeed:(float)speed error:(NSError **)error { if (!validFloat(speed) || _state == MLNParticleFieldRuntimeStateDisabled) return MLNParticleFieldRuntimeCommandResultRejected; __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.controlRuntimeTimeline(2, speed); return true; }]; return commandResult(done && ok); } +- (MLNParticleFieldRuntimeCommandResult)setFieldValueSource:(MLNParticleFieldValueSource)value error:(NSError **)error { if (_disposed || value > MLNParticleFieldValueSourceVectorMagnitude) return MLNParticleFieldRuntimeCommandResultRejected; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &weather, mbgl::style::RasterParticleLayer &) { weather.setFieldValueSource(static_cast(value)); return true; }]; return commandResult(done); } + +- (MLNParticleFieldRuntimeCommandResult)setWeatherColorStops:(NSArray *)stops error:(NSError **)error { + if (_disposed || !stops) return MLNParticleFieldRuntimeCommandResultRejected; + std::vector coreStops; coreStops.reserve(stops.count); + for (MLNParticleWeatherColorStop *stop in stops) coreStops.push_back({stop.value, {stop.red, stop.green, stop.blue, stop.alpha}}); + __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &weather, mbgl::style::RasterParticleLayer &) { ok = weather.setColorStops(std::move(coreStops)); return true; }]; return commandResult(done && ok); +} +- (MLNParticleFieldRuntimeCommandResult)setWeatherColorOpacity:(float)value error:(NSError **)error { if (_disposed || !validFloat(value)) return MLNParticleFieldRuntimeCommandResultRejected; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &weather, mbgl::style::RasterParticleLayer &) { weather.setOpacity(value); return true; }]; return commandResult(done); } +- (MLNParticleFieldRuntimeCommandResult)setParticleCount:(uint32_t)value error:(NSError **)error { if (_disposed) return MLNParticleFieldRuntimeCommandResultRejected; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &particle) { particle.setParticleCount(value); return particle.getParticleCount() == value; }]; return commandResult(done); } +- (MLNParticleFieldRuntimeCommandResult)setParticleLineWidth:(float)value error:(NSError **)error { if (_disposed || !validFloat(value)) return MLNParticleFieldRuntimeCommandResultRejected; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &particle) { particle.setLineWidth(value); return true; }]; return commandResult(done); } +- (MLNParticleFieldRuntimeCommandResult)setParticleLength:(float)value error:(NSError **)error { if (_disposed || !validFloat(value)) return MLNParticleFieldRuntimeCommandResultRejected; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &particle) { particle.setParticleLength(value); return true; }]; return commandResult(done); } +- (MLNParticleFieldRuntimeCommandResult)setParticleOpacity:(float)value error:(NSError **)error { if (_disposed || !validFloat(value)) return MLNParticleFieldRuntimeCommandResultRejected; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &particle) { particle.setParticleOpacity(value); return true; }]; return commandResult(done); } +- (MLNParticleFieldRuntimeCommandResult)setTrailOpacity:(float)value error:(NSError **)error { if (_disposed || !validFloat(value)) return MLNParticleFieldRuntimeCommandResultRejected; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &particle) { particle.setTrailOpacity(value); return true; }]; return commandResult(done); } +- (MLNParticleFieldRuntimeCommandResult)setTrailFade:(float)value error:(NSError **)error { if (_disposed || !validFloat(value)) return MLNParticleFieldRuntimeCommandResultRejected; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &particle) { particle.setTrailFade(value); return true; }]; return commandResult(done); } +- (MLNParticleFieldRuntimeCommandResult)setParticleAdvectionScale:(float)value error:(NSError **)error { if (_disposed || !validFloat(value) || value <= 0) return MLNParticleFieldRuntimeCommandResultRejected; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &particle) { particle.setAdvectionScale(value); return true; }]; return commandResult(done); } +- (MLNParticleFieldRuntimeCommandResult)setParticleColorRed:(float)red green:(float)green blue:(float)blue alpha:(float)alpha error:(NSError **)error { if (_disposed || !mbgl::style::validWeatherRGBA({red, green, blue, alpha})) return MLNParticleFieldRuntimeCommandResultRejected; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &particle) { particle.setParticleColor({red, green, blue, alpha}); return true; }]; return commandResult(done); } +- (MLNParticleFieldRuntimeCommandResult)setTrailColorRed:(float)red green:(float)green blue:(float)blue alpha:(float)alpha error:(NSError **)error { if (_disposed || !mbgl::style::validWeatherRGBA({red, green, blue, alpha})) return MLNParticleFieldRuntimeCommandResultRejected; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &particle) { particle.setTrailColor({red, green, blue, alpha}); return true; }]; return commandResult(done); } + @end namespace { const void *ParticleFieldRuntimeAssociationKey = &ParticleFieldRuntimeAssociationKey; }