From 0dd475c22fa5617dd20975703f7ca56d0be063a1 Mon Sep 17 00:00:00 2001 From: tei Date: Thu, 24 Sep 2026 15:28:29 +0900 Subject: [PATCH] feat: add STEP-093 iPad validation menu --- platform/ios/app/MBXViewController.mm | 504 +++++++++++++++++++++++++- 1 file changed, 502 insertions(+), 2 deletions(-) diff --git a/platform/ios/app/MBXViewController.mm b/platform/ios/app/MBXViewController.mm index b198c78eb..608a21696 100644 --- a/platform/ios/app/MBXViewController.mm +++ b/platform/ios/app/MBXViewController.mm @@ -59,6 +59,19 @@ static const MLNCoordinateBounds colorado = { static NSString *const MBXViewControllerAnnotationViewReuseIdentifer = @"MBXViewControllerAnnotationViewReuseIdentifer"; +static NSString *const MBXStep093SourceBottom = @"step093-bottom-source"; +static NSString *const MBXStep093SourceTopSemi = @"step093-top-semi-source"; +static NSString *const MBXStep093SourceTopOpaque = @"step093-top-opaque-source"; +static NSString *const MBXStep093SourceTopLine = @"step093-top-line-source"; +static NSString *const MBXStep093SourceCross = @"step093-cross-source"; +static NSString *const MBXStep093LayerBottom = @"step093-bottom-fill"; +static NSString *const MBXStep093LayerTopSemi = @"step093-top-semi"; +static NSString *const MBXStep093LayerTopOpaque = @"step093-top-opaque"; +static NSString *const MBXStep093LayerTopLine = @"step093-top-line"; +static NSString *const MBXStep093LayerCross = @"step093-cross-layer"; +static NSString *const MBXStep093WeatherLayer = @"particle-qbin-v4-smoke-weather"; +static NSString *const MBXStep093ParticleLayer = @"particle-qbin-v4-smoke-particles"; + typedef NS_ENUM(NSInteger, MBXSettingsSections) { MBXSettingsDebugTools = 0, MBXSettingsAnnotations, @@ -129,7 +142,22 @@ typedef NS_ENUM(NSInteger, MBXSettingsRuntimeStylingRows) { MBXSettingsRuntimeStylingAddFoursquarePOIsPMTiles, MBXSettingsRuntimeStylingHeatmapLayer, MBXSettingsRuntimeStylingParticleCoreValidation, - MBXSettingsRuntimeStylingParticleQbinV4Smoke + MBXSettingsRuntimeStylingParticleQbinV4Smoke, + MBXSettingsRuntimeStylingStep093 +}; + +typedef NS_ENUM(NSInteger, MBXStep093Scenario) { + MBXStep093ScenarioBottomContinuousTop = 0, + MBXStep093ScenarioBottomContinuous, + MBXStep093ScenarioContinuousTop, + MBXStep093ScenarioContinuousOnly, + MBXStep093ScenarioSingleLayer +}; + +typedef NS_ENUM(NSInteger, MBXStep093CrossPosition) { + MBXStep093CrossPositionBottom = 0, + MBXStep093CrossPositionContinuous, + MBXStep093CrossPositionTop }; typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { @@ -262,6 +290,27 @@ CLLocationCoordinate2D randomWorldCoordinate(void) { - (void)showParticleQbinV4SmokeMenu; - (void)installParticleQbinV4SmokeAtTime:(uint64_t)requestedTime label:(NSString *)label; - (void)clearParticleQbinV4Smoke; +- (void)showStep093Menu; +- (void)showStep093InvalidRangeMenu; +- (void)showStep093EndpointMenu; +- (void)showStep093MutationMenu; +- (void)installStep093Scenario:(MBXStep093Scenario)scenario; +- (void)installStep093OrdinarySources; +- (void)removeStep093BaseStyleLayers; +- (void)clearStep093OrdinaryLayers; +- (void)setStep093RangeStart:(NSString *)start end:(NSString *)end label:(NSString *)label; +- (void)deleteStep093Endpoint:(NSString *)identifier; +- (void)toggleStep093EndpointVisibility:(NSString *)identifier; +- (void)moveStep093CrossLayer:(MBXStep093CrossPosition)position; +- (void)showStep093Diagnostics; +- (MLNFillStyleLayer *)step093FillLayer:(NSString *)identifier + source:(NSString *)sourceIdentifier + color:(UIColor *)color + opacity:(CGFloat)opacity; +- (MLNLineStyleLayer *)step093LineLayer:(NSString *)identifier + source:(NSString *)sourceIdentifier + color:(UIColor *)color + width:(CGFloat)width; @end @interface MLNMapView (MBXViewController) @@ -523,7 +572,8 @@ CLLocationCoordinate2D randomWorldCoordinate(void) { @"Add FourSquare POIs PMTiles Layer", @"Add Earthquake Heatmap", @"Particle Core Validation (Fixtures)", - @"Particle / QBIN V4 Smoke (T0)" + @"Particle / QBIN V4 Smoke (T0)", + @"STEP-093 Continuous Range" ]]; break; case MBXSettingsMiscellaneous: @@ -763,6 +813,9 @@ CLLocationCoordinate2D randomWorldCoordinate(void) { case MBXSettingsRuntimeStylingParticleQbinV4Smoke: [self showParticleQbinV4SmokeMenu]; break; + case MBXSettingsRuntimeStylingStep093: + [self showStep093Menu]; + break; default: NSAssert(NO, @"All runtime styling setting rows should be implemented"); break; @@ -1391,6 +1444,7 @@ CLLocationCoordinate2D randomWorldCoordinate(void) { } - (void)clearParticleQbinV4Smoke { + [self clearStep093OrdinaryLayers]; auto& style = [self.mapView mbglMap].getStyle(); if (style.getLayer("particle-qbin-v4-smoke-particles")) style.removeLayer("particle-qbin-v4-smoke-particles"); if (style.getLayer("particle-qbin-v4-smoke-weather")) style.removeLayer("particle-qbin-v4-smoke-weather"); @@ -1399,6 +1453,452 @@ CLLocationCoordinate2D randomWorldCoordinate(void) { NSLog(@"ParticleQbinV4Smoke: event=CLEAR_RELEASED"); } +// MARK: - STEP-093 Continuous Range runtime validation (test app only) + +- (void)showStep093Menu { + UIAlertController *menu = + [UIAlertController alertControllerWithTitle:@"STEP-093 Continuous Range" + message:@"Style order remains the only Z-order. Select a topology, then use the submenus to mutate it and refresh diagnostics." + preferredStyle:UIAlertControllerStyleAlert]; + __weak MBXViewController *weakSelf = self; + void (^scenario)(NSString *, MBXStep093Scenario) = ^(NSString *title, + MBXStep093Scenario value) { + [menu addAction:[UIAlertAction actionWithTitle:title + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf installStep093Scenario:value]; + }]]; + }; + scenario(@"A Bottom + Continuous + Top", MBXStep093ScenarioBottomContinuousTop); + scenario(@"B Bottom + Continuous", MBXStep093ScenarioBottomContinuous); + scenario(@"C Continuous + Top", MBXStep093ScenarioContinuousTop); + scenario(@"D Continuous-only", MBXStep093ScenarioContinuousOnly); + scenario(@"E Single-layer Continuous", MBXStep093ScenarioSingleLayer); + [menu addAction:[UIAlertAction actionWithTitle:@"Invalid / Missing / Reversed" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf showStep093InvalidRangeMenu]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Endpoint / Visibility" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf showStep093EndpointMenu]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Mutation / Cross-boundary" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf showStep093MutationMenu]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Diagnostics / Refresh" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf showStep093Diagnostics]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Restore Default A" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf installStep093Scenario: + MBXStep093ScenarioBottomContinuousTop]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:menu animated:YES completion:nil]; +} + +- (void)showStep093InvalidRangeMenu { + UIAlertController *menu = + [UIAlertController alertControllerWithTitle:@"STEP-093 Invalid Range" + message:@"Each action first restores default A when its endpoints are unavailable." + preferredStyle:UIAlertControllerStyleAlert]; + __weak MBXViewController *weakSelf = self; + void (^ensureDefault)(void) = ^{ + if (![weakSelf.mapView.style layerWithIdentifier:MBXStep093WeatherLayer] || + ![weakSelf.mapView.style layerWithIdentifier:MBXStep093ParticleLayer]) { + [weakSelf installStep093Scenario:MBXStep093ScenarioBottomContinuousTop]; + } + }; + [menu addAction:[UIAlertAction actionWithTitle:@"Missing start" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + ensureDefault(); + [weakSelf setStep093RangeStart:@"step093-missing-start" + end:MBXStep093ParticleLayer + label:@"MISSING_START"]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Missing end" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + ensureDefault(); + [weakSelf setStep093RangeStart:MBXStep093WeatherLayer + end:@"step093-missing-end" + label:@"MISSING_END"]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Reversed endpoints" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + ensureDefault(); + [weakSelf setStep093RangeStart:MBXStep093ParticleLayer + end:MBXStep093WeatherLayer + label:@"REVERSED"]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Empty start ID (API reject)" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + ensureDefault(); + [weakSelf setStep093RangeStart:@"" + end:MBXStep093ParticleLayer + label:@"EMPTY_START"]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Restore valid A" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf installStep093Scenario: + MBXStep093ScenarioBottomContinuousTop]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Back" + style:UIAlertActionStyleCancel + handler:^(__unused UIAlertAction *action) { + [weakSelf showStep093Menu]; + }]]; + [self presentViewController:menu animated:YES completion:nil]; +} + +- (void)showStep093EndpointMenu { + UIAlertController *menu = + [UIAlertController alertControllerWithTitle:@"STEP-093 Endpoints" + message:@"Visibility changes content only; deletion must invalidate the configured endpoint. Restore rebuilds default A without replacing MLNMapView." + preferredStyle:UIAlertControllerStyleAlert]; + __weak MBXViewController *weakSelf = self; + [menu addAction:[UIAlertAction actionWithTitle:@"Toggle start visibility" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf toggleStep093EndpointVisibility: + MBXStep093WeatherLayer]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Toggle end visibility" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf toggleStep093EndpointVisibility: + MBXStep093ParticleLayer]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Delete start endpoint" + style:UIAlertActionStyleDestructive + handler:^(__unused UIAlertAction *action) { + [weakSelf deleteStep093Endpoint:MBXStep093WeatherLayer]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Restore start (default A)" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf installStep093Scenario: + MBXStep093ScenarioBottomContinuousTop]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Delete end endpoint" + style:UIAlertActionStyleDestructive + handler:^(__unused UIAlertAction *action) { + [weakSelf deleteStep093Endpoint:MBXStep093ParticleLayer]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Restore end (default A)" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf installStep093Scenario: + MBXStep093ScenarioBottomContinuousTop]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Back" + style:UIAlertActionStyleCancel + handler:^(__unused UIAlertAction *action) { + [weakSelf showStep093Menu]; + }]]; + [self presentViewController:menu animated:YES completion:nil]; +} + +- (void)showStep093MutationMenu { + UIAlertController *menu = + [UIAlertController alertControllerWithTitle:@"STEP-093 Cross-boundary" + message:@"The same ordinary fill layer ID is removed and re-added at three Style-order positions." + preferredStyle:UIAlertControllerStyleAlert]; + __weak MBXViewController *weakSelf = self; + void (^move)(NSString *, MBXStep093CrossPosition) = ^(NSString *title, + MBXStep093CrossPosition position) { + [menu addAction:[UIAlertAction actionWithTitle:title + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf moveStep093CrossLayer:position]; + }]]; + }; + move(@"Move cross layer → Bottom", MBXStep093CrossPositionBottom); + move(@"Move cross layer → Continuous", MBXStep093CrossPositionContinuous); + move(@"Move cross layer → Top", MBXStep093CrossPositionTop); + [menu addAction:[UIAlertAction actionWithTitle:@"Remove cross layer" + style:UIAlertActionStyleDestructive + handler:^(__unused UIAlertAction *action) { + [weakSelf removeLayer:MBXStep093LayerCross]; + [weakSelf.mapView setNeedsRerender]; + NSLog(@"STEP093_IOS event=MUTATION cross=REMOVED"); + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Restore Default A" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf installStep093Scenario: + MBXStep093ScenarioBottomContinuousTop]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Back" + style:UIAlertActionStyleCancel + handler:^(__unused UIAlertAction *action) { + [weakSelf showStep093Menu]; + }]]; + [self presentViewController:menu animated:YES completion:nil]; +} + +- (void)installStep093Scenario:(MBXStep093Scenario)scenario { + [self clearStep093OrdinaryLayers]; + [self installParticleQbinV4SmokeAtTime:1789241400000ULL label:@"STEP093_MIDPOINT"]; + if (![self.mapView.style layerWithIdentifier:MBXStep093WeatherLayer] || + ![self.mapView.style layerWithIdentifier:MBXStep093ParticleLayer]) { + NSLog(@"STEP093_IOS event=SCENE_FAILED reason=QBIN_ENDPOINTS_UNAVAILABLE"); + return; + } + + [self removeStep093BaseStyleLayers]; + [self installStep093OrdinarySources]; + MLNStyle *style = self.mapView.style; + MLNStyleLayer *weather = [style layerWithIdentifier:MBXStep093WeatherLayer]; + MLNStyleLayer *particle = [style layerWithIdentifier:MBXStep093ParticleLayer]; + const BOOL hasBottom = scenario == MBXStep093ScenarioBottomContinuousTop || + scenario == MBXStep093ScenarioBottomContinuous || + scenario == MBXStep093ScenarioSingleLayer; + const BOOL hasTop = scenario == MBXStep093ScenarioBottomContinuousTop || + scenario == MBXStep093ScenarioContinuousTop || + scenario == MBXStep093ScenarioSingleLayer; + + if (hasBottom) { + MLNFillStyleLayer *bottom = + [self step093FillLayer:MBXStep093LayerBottom + source:MBXStep093SourceBottom + color:[UIColor colorWithRed:0.05 green:0.48 blue:0.58 alpha:1.0] + opacity:0.72]; + [style insertLayer:bottom belowLayer:weather]; + } + if (hasTop) { + [style addLayer:[self step093FillLayer:MBXStep093LayerTopSemi + source:MBXStep093SourceTopSemi + color:[UIColor colorWithRed:1.0 green:0.82 blue:0.27 alpha:1.0] + opacity:0.42]]; + [style addLayer:[self step093FillLayer:MBXStep093LayerTopOpaque + source:MBXStep093SourceTopOpaque + color:[UIColor colorWithRed:0.94 green:0.20 blue:0.34 alpha:1.0] + opacity:1.0]]; + [style addLayer:[self step093LineLayer:MBXStep093LayerTopLine + source:MBXStep093SourceTopLine + color:UIColor.whiteColor + width:6.0]]; + } + + NSString *start = MBXStep093WeatherLayer; + NSString *end = MBXStep093ParticleLayer; + if (scenario == MBXStep093ScenarioSingleLayer) { + start = MBXStep093ParticleLayer; + end = MBXStep093ParticleLayer; + } + + self.mapView.retainedUnderlayEnabled = YES; + NSString *labels[] = {@"BOTTOM_CONTINUOUS_TOP", @"BOTTOM_CONTINUOUS", + @"CONTINUOUS_TOP", @"CONTINUOUS_ONLY", @"SINGLE_LAYER"}; + [self setStep093RangeStart:start end:end label:labels[scenario]]; + [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(27.8882865, 135.0) + zoomLevel:3.0 + animated:NO]; + [self.mapView setNeedsRerender]; +} + +- (void)installStep093OrdinarySources { + MLNStyle *style = self.mapView.style; + MLNShapeSource *(^polygonSource)(NSString *, CLLocationCoordinate2D *, NSUInteger) = + ^MLNShapeSource *(NSString *identifier, CLLocationCoordinate2D *coordinates, + NSUInteger count) { + MLNPolygon *polygon = [MLNPolygon polygonWithCoordinates:coordinates count:count]; + return [[MLNShapeSource alloc] initWithIdentifier:identifier shape:polygon options:nil]; + }; + + CLLocationCoordinate2D bottom[] = {{4.0, 114.0}, {4.0, 157.0}, {53.0, 157.0}, + {53.0, 114.0}, {4.0, 114.0}}; + CLLocationCoordinate2D semi[] = {{8.0, 136.0}, {8.0, 152.0}, {49.0, 152.0}, + {49.0, 136.0}, {8.0, 136.0}}; + CLLocationCoordinate2D opaque[] = {{18.0, 124.0}, {18.0, 136.0}, {42.0, 136.0}, + {42.0, 124.0}, {18.0, 124.0}}; + CLLocationCoordinate2D cross[] = {{14.0, 128.0}, {14.0, 145.0}, {37.0, 145.0}, + {37.0, 128.0}, {14.0, 128.0}}; + CLLocationCoordinate2D line[] = {{6.0, 116.0}, {50.0, 154.0}, {6.0, 154.0}, + {50.0, 116.0}}; + [style addSource:polygonSource(MBXStep093SourceBottom, bottom, 5)]; + [style addSource:polygonSource(MBXStep093SourceTopSemi, semi, 5)]; + [style addSource:polygonSource(MBXStep093SourceTopOpaque, opaque, 5)]; + [style addSource:[[MLNShapeSource alloc] + initWithIdentifier:MBXStep093SourceTopLine + shape:[MLNPolyline polylineWithCoordinates:line count:4] + options:nil]]; + [style addSource:polygonSource(MBXStep093SourceCross, cross, 5)]; +} + +- (void)removeStep093BaseStyleLayers { + // The demo style normally contains many pre-existing layers. Remove them from this private + // validation scene so A/B/C/D describe the complete Style topology rather than only a suffix. + // The QBIN source and the two existing dynamic endpoints remain untouched. + for (MLNStyleLayer *layer in [self.mapView.style.layers copy]) { + if (![layer.identifier isEqualToString:MBXStep093WeatherLayer] && + ![layer.identifier isEqualToString:MBXStep093ParticleLayer]) { + [self.mapView.style removeLayer:layer]; + } + } +} + +- (void)clearStep093OrdinaryLayers { + for (NSString *identifier in @[ MBXStep093LayerCross, MBXStep093LayerTopLine, + MBXStep093LayerTopOpaque, MBXStep093LayerTopSemi, + MBXStep093LayerBottom ]) { + [self removeLayer:identifier]; + } + for (NSString *identifier in @[ MBXStep093SourceCross, MBXStep093SourceTopLine, + MBXStep093SourceTopOpaque, MBXStep093SourceTopSemi, + MBXStep093SourceBottom ]) { + [self removeSource:identifier]; + } +} + +- (void)setStep093RangeStart:(NSString *)start end:(NSString *)end label:(NSString *)label { + BOOL accepted = [self.mapView setContinuousLayerRangeFromLayerIdentifier:start + toLayerIdentifier:end]; + [self.mapView setNeedsRerender]; + NSLog(@"STEP093_IOS event=RANGE label=%@ accepted=%d start=%@ end=%@", label, accepted, + start, end); +} + +- (void)deleteStep093Endpoint:(NSString *)identifier { + if (![self.mapView.style layerWithIdentifier:MBXStep093WeatherLayer] || + ![self.mapView.style layerWithIdentifier:MBXStep093ParticleLayer]) { + [self installStep093Scenario:MBXStep093ScenarioBottomContinuousTop]; + } + [self removeLayer:identifier]; + [self.mapView setNeedsRerender]; + NSLog(@"STEP093_IOS event=ENDPOINT_DELETE id=%@", identifier); +} + +- (void)toggleStep093EndpointVisibility:(NSString *)identifier { + MLNStyleLayer *layer = [self.mapView.style layerWithIdentifier:identifier]; + if (!layer) { + [self installStep093Scenario:MBXStep093ScenarioBottomContinuousTop]; + layer = [self.mapView.style layerWithIdentifier:identifier]; + } + layer.visible = !layer.visible; + [self.mapView setNeedsRerender]; + NSLog(@"STEP093_IOS event=ENDPOINT_VISIBILITY id=%@ visible=%d", identifier, + layer.visible); +} + +- (void)moveStep093CrossLayer:(MBXStep093CrossPosition)position { + if (![self.mapView.style sourceWithIdentifier:MBXStep093SourceCross] || + ![self.mapView.style layerWithIdentifier:MBXStep093WeatherLayer] || + ![self.mapView.style layerWithIdentifier:MBXStep093ParticleLayer]) { + [self installStep093Scenario:MBXStep093ScenarioBottomContinuousTop]; + } + [self removeLayer:MBXStep093LayerCross]; + MLNFillStyleLayer *cross = + [self step093FillLayer:MBXStep093LayerCross + source:MBXStep093SourceCross + color:[UIColor colorWithRed:0.62 green:0.25 blue:0.78 alpha:1.0] + opacity:0.28]; + MLNStyle *style = self.mapView.style; + MLNStyleLayer *weather = [style layerWithIdentifier:MBXStep093WeatherLayer]; + MLNStyleLayer *particle = [style layerWithIdentifier:MBXStep093ParticleLayer]; + NSString *label = nil; + switch (position) { + case MBXStep093CrossPositionBottom: + [style insertLayer:cross belowLayer:weather]; + label = @"BOTTOM"; + break; + case MBXStep093CrossPositionContinuous: + [style insertLayer:cross belowLayer:particle]; + label = @"CONTINUOUS"; + break; + case MBXStep093CrossPositionTop: + [style insertLayer:cross aboveLayer:particle]; + label = @"TOP"; + break; + } + [self.mapView setNeedsRerender]; + NSLog(@"STEP093_IOS event=MUTATION cross=%@", label); +} + +- (void)showStep093Diagnostics { + MLNRetainedUnderlayDiagnostics *value = self.mapView.retainedUnderlayDiagnostics; + NSUInteger cacheCount = (value.bottomPresent ? 1 : 0) + (value.topPresent ? 1 : 0); + NSString *message = [NSString + stringWithFormat: + @"Range configured=%d valid=%d fallback=%d\n%@[%ld] … %@[%ld]\nContinuous=[%ld,%ld) caches=%lu\nreason=%@\n\nBottom present=%d state=%@\nb/r/i=%llu/%llu/%llu\ntex/comp/release=%llu/%llu/%llu\nlast=%@\n\nTop present=%d state=%@\nb/r/i=%llu/%llu/%llu\ntex/comp/release=%llu/%llu/%llu\nlast=%@\n\nFast eligible=%d active=%d\nhit/miss/full=%llu/%llu/%llu\nreason=%@", + value.rangeConfigured, value.rangeValid, value.rangeFallback, value.startLayerId, + (long)value.startIndex, value.endLayerId, (long)value.endIndex, + (long)value.continuousBegin, (long)value.continuousEndExclusive, + (unsigned long)cacheCount, value.lastReason, value.bottomPresent, + value.bottomCacheState, (unsigned long long)value.bottomBuild, + (unsigned long long)value.bottomReuse, (unsigned long long)value.bottomInvalidation, + (unsigned long long)value.bottomTextureCreate, + (unsigned long long)value.bottomCompositeCreate, + (unsigned long long)value.bottomResourceRelease, value.bottomLastInvalidationReason, + value.topPresent, value.topCacheState, (unsigned long long)value.topBuild, + (unsigned long long)value.topReuse, (unsigned long long)value.topInvalidation, + (unsigned long long)value.topTextureCreate, + (unsigned long long)value.topCompositeCreate, + (unsigned long long)value.topResourceRelease, value.topLastInvalidationReason, + value.fastPathEligible, value.fastPathActive, (unsigned long long)value.fastPathHit, + (unsigned long long)value.fastPathMiss, (unsigned long long)value.fullCpuFrame, + value.fastPathFallbackReason]; + NSLog(@"STEP093_IOS_DIAGNOSTICS %@", [message stringByReplacingOccurrencesOfString:@"\n" + withString:@" "]); + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"STEP-093 Diagnostics" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + __weak MBXViewController *weakSelf = self; + [alert addAction:[UIAlertAction actionWithTitle:@"Refresh" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf showStep093Diagnostics]; + }]]; + [alert addAction:[UIAlertAction actionWithTitle:@"Back to STEP-093" + style:UIAlertActionStyleDefault + handler:^(__unused UIAlertAction *action) { + [weakSelf showStep093Menu]; + }]]; + [alert addAction:[UIAlertAction actionWithTitle:@"Close" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:alert animated:YES completion:nil]; +} + +- (MLNFillStyleLayer *)step093FillLayer:(NSString *)identifier + source:(NSString *)sourceIdentifier + color:(UIColor *)color + opacity:(CGFloat)opacity { + MLNSource *source = [self.mapView.style sourceWithIdentifier:sourceIdentifier]; + MLNFillStyleLayer *layer = [[MLNFillStyleLayer alloc] initWithIdentifier:identifier + source:source]; + layer.fillColor = [NSExpression expressionForConstantValue:color]; + layer.fillOpacity = [NSExpression expressionForConstantValue:@(opacity)]; + return layer; +} + +- (MLNLineStyleLayer *)step093LineLayer:(NSString *)identifier + source:(NSString *)sourceIdentifier + color:(UIColor *)color + width:(CGFloat)width { + MLNSource *source = [self.mapView.style sourceWithIdentifier:sourceIdentifier]; + MLNLineStyleLayer *layer = [[MLNLineStyleLayer alloc] initWithIdentifier:identifier + source:source]; + layer.lineColor = [NSExpression expressionForConstantValue:color]; + layer.lineWidth = [NSExpression expressionForConstantValue:@(width)]; + layer.lineOpacity = [NSExpression expressionForConstantValue:@0.95]; + return layer; +} + // Earthquake Heatmap - (void)addEarthquakeHeatmap {