#import "Mapbox.h" #import "MBXViewController.h" #import "MBXAnnotationView.h" #import "MBXAppDelegate.h" #import "MBXCustomCalloutView.h" #import "MBXEmbeddedMapViewController.h" #import "MBXOfflinePacksTableViewController.h" #import "MBXOrnamentsViewController.h" #import "MBXSnapshotTestViewController.h" #import "MBXState.h" #import "MBXStateManager.h" #import "MBXUserLocationAnnotationView.h" #import "MLNSettings.h" #import "MLNMapView_Private.h" #include #include #include #include #include #include #include #include #import "CustomStyleLayerExample.h" #import "ExampleCustomDrawableStyleLayer.h" #import #import "MBXFrameTimeGraphView.h" #import "MLNMapView_Experimental.h" // Plug In Examples #if MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_OPENGL #import "MLNPluginStyleLayer.h" #import "PluginLayerExample.h" #if MLN_RENDER_BACKEND_METAL #import "PluginLayerExampleMetalRendering.h" #endif #endif static const CLLocationCoordinate2D WorldTourDestinations[] = { {.latitude = 38.8999418, .longitude = -77.033996}, {.latitude = 37.7884307, .longitude = -122.3998631}, {.latitude = 52.5003103, .longitude = 13.4197763}, {.latitude = 60.1712627, .longitude = 24.9378866}, {.latitude = 53.8948782, .longitude = 27.5558476}, }; static const MLNCoordinateBounds colorado = { .sw = {.latitude = 36.986207, .longitude = -109.049896}, .ne = {.latitude = 40.989329, .longitude = -102.062592}, }; static NSString *const MBXViewControllerAnnotationViewReuseIdentifer = @"MBXViewControllerAnnotationViewReuseIdentifer"; typedef NS_ENUM(NSInteger, MBXSettingsSections) { MBXSettingsDebugTools = 0, MBXSettingsAnnotations, MBXSettingsRuntimeStyling, MBXSettingsMiscellaneous, }; typedef NS_ENUM(NSInteger, MBXSettingsDebugToolsRows) { MBXSettingsDebugToolsResetPosition = 0, MBXSettingsDebugToolsTileBoundaries, MBXSettingsDebugToolsTileInfo, MBXSettingsDebugToolsTimestamps, MBXSettingsDebugToolsCollisionBoxes, MBXSettingsDebugToolsOverdrawVisualization, MBXSettingsDebugToolsShowZoomLevel, MBXSettingsDebugToolsShowFrameTimeGraph, MBXSettingsDebugToolsShowReuseQueueStats, MBXSettingsDebugToolsShowRenderingStats }; typedef NS_ENUM(NSInteger, MBXSettingsAnnotationsRows) { MBXSettingsAnnotations100Views = 0, MBXSettingsAnnotations1000Views, MBXSettingsAnnotations10000Views, MBXSettingsAnnotations100Sprites, MBXSettingsAnnotations1000Sprites, MBXSettingsAnnotations10000Sprites, MBXSettingsAnnotationAnimation, MBXSettingsAnnotationsTestShapes, MBXSettingsAnnotationsManyTestShapes, MBXSettingsAnnotationsCustomCallout, MBXSettingsAnnotationsQueryAnnotations, MBXSettingsAnnotationsCustomUserDot, MBXSettingsAnnotationsRemoveAnnotations, MBXSettingsAnnotationSelectRandomOffscreenPointAnnotation, MBXSettingsAnnotationCenterSelectedAnnotation, MBXSettingsAnnotationAddVisibleAreaPolyline }; typedef NS_ENUM(NSInteger, MBXSettingsRuntimeStylingRows) { MBXSettingsRuntimeStylingBuildingExtrusions = 0, MBXSettingsRuntimeStylingWater, MBXSettingsRuntimeStylingRoads, MBXSettingsRuntimeStylingRaster, MBXSettingsRuntimeStylingShape, MBXSettingsRuntimeStylingSymbols, MBXSettingsRuntimeStylingBuildings, MBXSettingsRuntimeStylingFerry, MBXSettingsRuntimeStylingParks, MBXSettingsRuntimeStylingFilteredFill, MBXSettingsRuntimeStylingFilteredLines, MBXSettingsRuntimeStylingNumericFilteredFill, MBXSettingsRuntimeStylingStyleQuery, MBXSettingsRuntimeStylingFeatureSource, MBXSettingsRuntimeStylingPointCollection, MBXSettingsRuntimeStylingUpdateShapeSourceData, MBXSettingsRuntimeStylingUpdateShapeSourceURL, MBXSettingsRuntimeStylingUpdateShapeSourceFeatures, MBXSettingsRuntimeStylingVectorTileSource, MBXSettingsRuntimeStylingRasterTileSource, MBXSettingsRuntimeStylingImageSource, MBXSettingsRuntimeStylingRouteLine, MBXSettingsRuntimeStylingAddCustomTriangleLayer, MBXSettingsRuntimeStylingDDSPolygon, MBXSettingsRuntimeStylingCustomLatLonGrid, MBXSettingsRuntimeStylingLineGradient, MBXSettingsRuntimeStylingCustomDrawableLayer, MBXSettingsRuntimeStylingAddFoursquarePOIsPMTiles, MBXSettingsRuntimeStylingHeatmapLayer, MBXSettingsRuntimeStylingParticleQbinV4Smoke }; typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { MBXSettingsMiscellaneousWorldTour, MBXSettingsMiscellaneousRandomTour, MBXSettingsMiscellaneousScrollView, MBXSettingsMiscellaneousToggleTwoMaps, MBXSettingsMiscellaneousLocalizeLabels, MBXSettingsMiscellaneousShowSnapshots, MBXSettingsMiscellaneousMissingIcon, MBXSettingsMiscellaneousShouldLimitCameraChanges, MBXSettingsMiscellaneousSetContentInsets, MBXSettingsMiscellaneousShowCustomLocationManager, MBXSettingsMiscellaneousOrnamentsPlacement, MBXSettingsMiscellaneousLatLngBoundsWithPadding, MBXSettingsMiscellaneousCycleTileLOD, MBXSettingsMiscellaneousLiveSnapshot, MBXSettingsMiscellaneousPrintLogFile, MBXSettingsMiscellaneousDeleteLogFile }; typedef NS_ENUM(NSInteger, MBXTileLodMode) { MBXTileLodModeNoLod = 0, MBXTileLodModeDefault, MBXTileLodModeReduced, MBXTileLodModeAggressive, MBXTileLodModeCount }; // Utility methods CLLocationCoordinate2D coordinateCentered(CLLocationCoordinate2D origin, CLLocationDegrees bearing, CLLocationDistance distance) { // Convert to radians double multiplier = M_PI / 180.0; double sourceLatitude = multiplier * origin.latitude; double sourceLongitude = multiplier * origin.longitude; bearing *= multiplier; distance /= 6378137.0; // Pulled from MLNRadianCoordinateAtDistanceFacingDirection: double latitude = asin((sin(sourceLatitude) * cos(distance)) + (cos(sourceLatitude) * sin(distance) * cos(bearing))); double longitude = sourceLongitude + atan2((sin(bearing) * sin(distance) * cos(sourceLatitude)), cos(distance) - (sin(sourceLatitude) * sin(latitude))); CLLocationCoordinate2D result; result.latitude = fmin(85.0, fmax(-85.0, (latitude / multiplier))); result.longitude = longitude / multiplier; return result; } CLLocationCoordinate2D randomWorldCoordinate(void) { static const struct { CLLocationCoordinate2D coordinate; CLLocationDistance radius; } landmasses[] = { // Rough land masses {{38.328531, 94.778736}, 4100000}, // Asia {{1.477244, 18.138111}, 4100000}, // Africa {{52.310059, 22.295425}, 2000000}, // Europe {{42.344216, -96.532700}, 3000000}, // N America {{-11.537273, -57.035181}, 2220000}, // S America {{-20.997030, 134.660541}, 2220000}, // Australia // A few cities {{51.504787, -0.106977}, 33000}, // London {{37.740186, -122.437086}, 8500}, // SF {{52.509978, 13.406510}, 12000}, // Berlin {{12.966246, 77.586505}, 19000} // Bengaluru }; NSInteger index = arc4random_uniform(sizeof(landmasses) / sizeof(landmasses[0])); CLLocationCoordinate2D coordinate = landmasses[index].coordinate; CLLocationDistance radius = landmasses[index].radius; // Now create a world coord CLLocationDegrees heading = (CLLocationDegrees)arc4random_uniform(360); CLLocationDistance distance = (CLLocationDistance)arc4random_uniform(radius); CLLocationCoordinate2D newLocation = coordinateCentered(coordinate, heading, distance); return newLocation; } @interface MBXDroppedPinAnnotation : MLNPointAnnotation @end @implementation MBXDroppedPinAnnotation @end @interface MBXCustomCalloutAnnotation : MLNPointAnnotation @property (nonatomic, assign) BOOL anchoredToAnnotation; @property (nonatomic, assign) BOOL dismissesAutomatically; @end @implementation MBXCustomCalloutAnnotation @end @interface MBXSpriteBackedAnnotation : MLNPointAnnotation @end @implementation MBXSpriteBackedAnnotation @end @interface MBXViewController () @property (nonatomic) IBOutlet MLNMapView *mapView; @property (nonatomic) MBXState *currentState; @property (weak, nonatomic) IBOutlet UIButton *hudLabel; @property (weak, nonatomic) IBOutlet MBXFrameTimeGraphView *frameTimeGraphView; @property (nonatomic) NSInteger styleIndex; @property (nonatomic) NSMutableArray *styleNames; @property (nonatomic) NSMutableArray *styleURLs; @property (nonatomic) BOOL customUserLocationAnnnotationEnabled; @property (nonatomic, getter=isLocalizingLabels) BOOL localizingLabels; @property (nonatomic) BOOL reuseQueueStatsEnabled; @property (nonatomic) BOOL frameTimeGraphEnabled; @property (nonatomic) BOOL renderingStatsEnabled; @property (nonatomic) BOOL shouldLimitCameraChanges; @property (nonatomic) BOOL randomWalk; @property (nonatomic) BOOL zoomLevelOrnamentEnabled; @property (nonatomic) NSMutableArray *helperWindows; @property (nonatomic) NSMutableArray *contentInsetsOverlays; @property (nonatomic, copy) void (^locationBlock)(void); - (void)showParticleQbinV4SmokeMenu; - (void)installParticleQbinV4SmokeAtTime:(uint64_t)requestedTime label:(NSString *)label; - (void)clearParticleQbinV4Smoke; @end @interface MLNMapView (MBXViewController) @property (nonatomic) NSDictionary *annotationViewReuseQueueByIdentifier; @end @implementation MBXViewController { BOOL _isTouringWorld; BOOL _contentInsetsEnabled; UIEdgeInsets _originalContentInsets; NSLayoutConstraint *_firstMapLayout; NSArray *_secondMapLayout; NSDictionary *_pointFeatures; NSLock *_loadLock; MBXTileLodMode _tileLodMode; } // MARK: - Setup & Teardown - (instancetype)init { if (self = [super init]) { _firstMapLayout = nil; _secondMapLayout = nil; _pointFeatures = nil; _loadLock = [NSLock new]; } return self; } // This will add the plug-in layers. This is a demo of how // extensible layers for the style can be added to the map view - (void)addPluginLayers { #if MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_OPENGL [self.mapView addPluginLayerType:[PluginLayerExample class]]; #if MLN_RENDER_BACKEND_METAL [self.mapView addPluginLayerType:[PluginLayerExampleMetalRendering class]]; #endif #endif } - (void)viewDidLoad { [super viewDidLoad]; [self addPluginLayers]; // Keep track of current map state and debug preferences, // saving and restoring when the application's state changes. self.currentState = [MBXStateManager sharedManager].currentState; if (!self.currentState) { // Create a new state with the below default values self.currentState = [[MBXState alloc] init]; self.mapView.showsUserHeadingIndicator = YES; self.mapView.showsScale = YES; self.mapView.showsLogoView = YES; self.mapView.showsCompassView = YES; self.mapView.showsAttributionButton = YES; self.zoomLevelOrnamentEnabled = NO; self.frameTimeGraphEnabled = NO; } else { // Revert to the previously saved state [self restoreMapState:nil]; } [self updateHUD]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveCurrentMapState:) name:UIApplicationDidEnterBackgroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(restoreMapState:) name:UIApplicationWillEnterForegroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveCurrentMapState:) name:UIApplicationWillTerminateNotification object:nil]; self.styleIndex = -1; [self setStyles]; [self cycleStyles:self]; self.mapView.experimental_enableFrameRateMeasurement = YES; self.hudLabel.titleLabel.font = [UIFont monospacedDigitSystemFontOfSize:10 weight:UIFontWeightRegular]; // Add fall-through single tap gesture recognizer. This will be called when // the map view's tap recognizers fail. UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)]; for (UIGestureRecognizer *gesture in self.mapView.gestureRecognizers) { if ([gesture isKindOfClass:[UITapGestureRecognizer class]]) { [singleTap requireGestureRecognizerToFail:gesture]; } } [self.mapView addGestureRecognizer:singleTap]; // Display a secondary map on any connected external display. // https://developer.apple.com/documentation/uikit/windows_and_screens/displaying_content_on_a_connected_screen?language=objc self.helperWindows = [NSMutableArray array]; [[NSNotificationCenter defaultCenter] addObserverForName:UIScreenDidConnectNotification object:nil queue:nil usingBlock:^(NSNotification *_Nonnull note) { UIScreen *helperScreen = note.object; UIWindow *helperWindow = [[UIWindow alloc] initWithFrame:helperScreen.bounds]; helperWindow.screen = helperScreen; UIViewController *helperViewController = [[UIViewController alloc] init]; MLNMapView *helperMapView = [[MLNMapView alloc] initWithFrame:helperWindow.bounds styleURL:[[MLNStyle predefinedStyle:@"Hybrid"] url]]; helperMapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; helperMapView.camera = self.mapView.camera; helperMapView.compassView.hidden = YES; helperViewController.view = helperMapView; helperWindow.rootViewController = helperViewController; helperWindow.hidden = NO; [self.helperWindows addObject:helperWindow]; }]; [[NSNotificationCenter defaultCenter] addObserverForName:UIScreenDidDisconnectNotification object:nil queue:nil usingBlock:^(NSNotification *_Nonnull note) { UIScreen *helperScreen = note.object; for (UIWindow *window in self.helperWindows) { if (window.screen == helperScreen) { [self.helperWindows removeObject:window]; } } }]; } - (UIInterfaceOrientationMask)supportedInterfaceOrientations { return UIInterfaceOrientationMaskAll; } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(__unused id)sender { if ([segue.identifier isEqualToString:@"ShowOfflinePacks"]) { MBXOfflinePacksTableViewController *controller = [segue destinationViewController]; controller.mapView = self.mapView; } } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } // MARK: - Debugging Interface - (IBAction)showSettings:(__unused id)sender { self.randomWalk = NO; UITableViewController *settingsViewController = [[UITableViewController alloc] initWithStyle:UITableViewStyleGrouped]; settingsViewController.tableView.delegate = self; settingsViewController.tableView.dataSource = self; settingsViewController.title = @"Debugging"; settingsViewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(dismissSettings:)]; UINavigationController *wrapper = [[UINavigationController alloc] initWithRootViewController:settingsViewController]; wrapper.navigationBar.tintColor = self.navigationController.navigationBar.tintColor; [self.navigationController presentViewController:wrapper animated:YES completion:nil]; } - (void)dismissSettings:(__unused id)sender { [self dismissViewControllerAnimated:YES completion:nil]; } - (NSArray *)settingsSectionTitles { return @[ @"Debug Tools", @"Annotations", @"Runtime Styling", @"Miscellaneous" ]; } - (NSArray *)settingsTitlesForSection:(NSInteger)section { NSMutableArray *settingsTitles = [NSMutableArray array]; MLNMapDebugMaskOptions debugMask = self.mapView.debugMask; switch (section) { case MBXSettingsDebugTools: [settingsTitles addObjectsFromArray:@[ @"Reset position", [NSString stringWithFormat:@"%@ tile boundaries", (debugMask & MLNMapDebugTileBoundariesMask ? @"Hide" : @"Show")], [NSString stringWithFormat:@"%@ tile info", (debugMask & MLNMapDebugTileInfoMask ? @"Hide" : @"Show")], [NSString stringWithFormat:@"%@ tile timestamps", (debugMask & MLNMapDebugTimestampsMask ? @"Hide" : @"Show")], [NSString stringWithFormat:@"%@ collision boxes", (debugMask & MLNMapDebugCollisionBoxesMask ? @"Hide" : @"Show")], [NSString stringWithFormat:@"%@ overdraw visualization", (debugMask & MLNMapDebugOverdrawVisualizationMask ? @"Hide" : @"Show")], [NSString stringWithFormat:@"%@ zoom level ornament", (self.zoomLevelOrnamentEnabled ? @"Hide" : @"Show")], [NSString stringWithFormat:@"%@ frame time graph", (self.frameTimeGraphEnabled ? @"Hide" : @"Show")], [NSString stringWithFormat:@"%@ reuse queue stats", (self.reuseQueueStatsEnabled ? @"Hide" : @"Show")], [NSString stringWithFormat:@"%@ rendering stats", (self.renderingStatsEnabled ? @"Hide" : @"Show")] ]]; break; case MBXSettingsAnnotations: [settingsTitles addObjectsFromArray:@[ @"Add 100 Views", @"Add 1,000 Views", @"Add 10,000 Views", @"Add 100 Sprites", @"Add 1,000 Sprites", @"Add 10,000 Sprites", @"Animate an Annotation View", @"Add Test Shapes", @"Add 10x Test Shapes", @"Add Point With Custom Callout", @"Query Annotations", [NSString stringWithFormat:@"%@ Custom User Dot", (_customUserLocationAnnnotationEnabled ? @"Disable" : @"Enable")], @"Remove Annotations", @"Select an offscreen point annotation", @"Center selected annotation", @"Add visible area polyline" ]]; break; case MBXSettingsRuntimeStyling: [settingsTitles addObjectsFromArray:@[ @"Add Building Extrusions", @"Style Water With Function", @"Style Roads With Function", @"Add Raster & Apply Function", @"Add Shapes & Apply Fill", @"Style Symbol Color", @"Style Building Fill Color", @"Style Ferry Line Color", @"Remove Parks", @"Style Fill With Filter", @"Style Lines With Filter", @"Style Fill With Numeric Filter", @"Query and Style Features", @"Style Feature", @"Style Dynamic Point Collection", @"Update Shape Source: Data", @"Update Shape Source: URL", @"Update Shape Source: Features", @"Style Vector Tile Source", @"Style Raster Tile Source", @"Style Image Source", @"Add Route Line", #if MLN_RENDER_BACKEND_METAL @"Add Custom Triangle Layer (Metal)", #else @"Add Custom Triangle Layer (OpenGL)", #endif @"Dynamically Style Polygon", @"Add Custom Lat/Lon Grid", @"Style Route line with gradient", @"Add Custom Drawable Layer", @"Add FourSquare POIs PMTiles Layer", @"Add Earthquake Heatmap", @"Particle / QBIN V4 Smoke (T0)" ]]; break; case MBXSettingsMiscellaneous: [settingsTitles addObjectsFromArray:@[ @"Start World Tour", @"Random Tour", @"Embedded Map View", [NSString stringWithFormat:@"%@ Second Map", ([self.view viewWithTag:2] == nil ? @"Show" : @"Hide")], [NSString stringWithFormat:@"Show Labels in %@", (_localizingLabels ? @"Default Language" : [[NSLocale currentLocale] displayNameForKey:NSLocaleIdentifier value:[self bestLanguageForUser]])], @"Show Snapshots", @"Missing Icon", [NSString stringWithFormat:@"%@ Camera Changes", (_shouldLimitCameraChanges ? @"Unlimit" : @"Limit")], [NSString stringWithFormat:@"Turn %@ Content Insets", (_contentInsetsEnabled ? @"Off" : @"On")], @"View Route Simulation", @"Ornaments Placement", @"Lat Long bounds with padding", [NSString stringWithFormat:@"Cycle tile LOD mode: %@", [self getTileLodModeName]], @"Live Snapshot" ]]; break; default: NSAssert(NO, @"All settings sections should be implemented"); break; } return settingsTitles; } - (void)performActionForSettingAtIndexPath:(NSIndexPath *)indexPath { switch (indexPath.section) { case MBXSettingsDebugTools: switch (indexPath.row) { case MBXSettingsDebugToolsResetPosition: [self.mapView resetPosition]; break; case MBXSettingsDebugToolsTileBoundaries: self.currentState.debugMask ^= MLNMapDebugTileBoundariesMask; break; case MBXSettingsDebugToolsTileInfo: self.currentState.debugMask ^= MLNMapDebugTileInfoMask; break; case MBXSettingsDebugToolsTimestamps: self.currentState.debugMask ^= MLNMapDebugTimestampsMask; break; case MBXSettingsDebugToolsCollisionBoxes: self.currentState.debugMask ^= MLNMapDebugCollisionBoxesMask; break; case MBXSettingsDebugToolsOverdrawVisualization: self.currentState.debugMask ^= MLNMapDebugOverdrawVisualizationMask; break; case MBXSettingsDebugToolsShowZoomLevel: { self.zoomLevelOrnamentEnabled = !self.zoomLevelOrnamentEnabled; self.currentState.showsZoomLevelOrnament = self.zoomLevelOrnamentEnabled; self.hudLabel.hidden = !self.zoomLevelOrnamentEnabled; self.reuseQueueStatsEnabled = NO; [self updateHUD]; break; } case MBXSettingsDebugToolsShowFrameTimeGraph: { self.frameTimeGraphEnabled = !self.frameTimeGraphEnabled; self.currentState.showsTimeFrameGraph = !self.currentState.showsTimeFrameGraph; self.frameTimeGraphView.hidden = !self.frameTimeGraphEnabled; [self updateHUD]; break; } case MBXSettingsDebugToolsShowReuseQueueStats: { self.reuseQueueStatsEnabled = !self.currentState.reuseQueueStatsEnabled; self.hudLabel.hidden = !self.currentState.reuseQueueStatsEnabled; self.zoomLevelOrnamentEnabled = NO; [self updateHUD]; break; } case MBXSettingsDebugToolsShowRenderingStats: { self.renderingStatsEnabled = !self.renderingStatsEnabled; [self.mapView enableRenderingStatsView:self.renderingStatsEnabled]; break; } default: NSAssert(NO, @"All debug tools setting rows should be implemented"); break; } self.mapView.debugMask = self.currentState.debugMask; break; case MBXSettingsAnnotations: switch (indexPath.row) { case MBXSettingsAnnotations100Views: [self parseFeaturesAddingCount:100 usingViews:YES]; break; case MBXSettingsAnnotations1000Views: [self parseFeaturesAddingCount:1000 usingViews:YES]; break; case MBXSettingsAnnotations10000Views: [self parseFeaturesAddingCount:10000 usingViews:YES]; break; case MBXSettingsAnnotations100Sprites: [self parseFeaturesAddingCount:100 usingViews:NO]; break; case MBXSettingsAnnotations1000Sprites: [self parseFeaturesAddingCount:1000 usingViews:NO]; break; case MBXSettingsAnnotations10000Sprites: [self parseFeaturesAddingCount:10000 usingViews:NO]; break; case MBXSettingsAnnotationAnimation: [self animateAnnotationView]; break; case MBXSettingsAnnotationsTestShapes: [self addTestShapes:1]; break; case MBXSettingsAnnotationsManyTestShapes: [self addTestShapes:10]; break; case MBXSettingsAnnotationsCustomCallout: [self addAnnotationWithCustomCallout]; break; case MBXSettingsAnnotationsQueryAnnotations: [self testQueryPointAnnotations]; break; case MBXSettingsAnnotationsCustomUserDot: [self toggleCustomUserDot]; break; case MBXSettingsAnnotationsRemoveAnnotations: [self.mapView removeAnnotations:self.mapView.annotations]; break; case MBXSettingsAnnotationSelectRandomOffscreenPointAnnotation: [self selectAnOffscreenPointAnnotation]; break; case MBXSettingsAnnotationCenterSelectedAnnotation: [self centerSelectedAnnotation]; break; case MBXSettingsAnnotationAddVisibleAreaPolyline: [self addVisibleAreaPolyline]; break; default: NSAssert(NO, @"All annotations setting rows should be implemented"); break; } break; case MBXSettingsRuntimeStyling: switch (indexPath.row) { case MBXSettingsRuntimeStylingBuildingExtrusions: [self styleBuildingExtrusions]; break; case MBXSettingsRuntimeStylingWater: [self styleWaterLayer]; break; case MBXSettingsRuntimeStylingRoads: [self styleRoadLayer]; break; case MBXSettingsRuntimeStylingRaster: [self styleRasterLayer]; break; case MBXSettingsRuntimeStylingShape: [self styleShapeSource]; break; case MBXSettingsRuntimeStylingSymbols: [self styleSymbolLayer]; break; case MBXSettingsRuntimeStylingBuildings: [self styleBuildingLayer]; break; case MBXSettingsRuntimeStylingFerry: [self styleFerryLayer]; break; case MBXSettingsRuntimeStylingParks: [self removeParkLayer]; break; case MBXSettingsRuntimeStylingFilteredFill: [self styleFilteredFill]; break; case MBXSettingsRuntimeStylingFilteredLines: [self styleFilteredLines]; break; case MBXSettingsRuntimeStylingNumericFilteredFill: [self styleNumericFilteredFills]; break; case MBXSettingsRuntimeStylingStyleQuery: [self styleQuery]; break; case MBXSettingsRuntimeStylingFeatureSource: [self styleFeature]; break; case MBXSettingsRuntimeStylingPointCollection: [self styleDynamicPointCollection]; break; case MBXSettingsRuntimeStylingUpdateShapeSourceURL: [self updateShapeSourceURL]; break; case MBXSettingsRuntimeStylingUpdateShapeSourceData: [self updateShapeSourceData]; break; case MBXSettingsRuntimeStylingUpdateShapeSourceFeatures: [self updateShapeSourceFeatures]; break; case MBXSettingsRuntimeStylingVectorTileSource: [self styleVectorTileSource]; break; case MBXSettingsRuntimeStylingRasterTileSource: [self styleRasterTileSource]; break; case MBXSettingsRuntimeStylingImageSource: [self styleImageSource]; break; case MBXSettingsRuntimeStylingRouteLine: [self styleRouteLine]; break; case MBXSettingsRuntimeStylingAddCustomTriangleLayer: [self styleAddCustomTriangleLayer]; break; case MBXSettingsRuntimeStylingDDSPolygon: [self stylePolygonWithDDS]; break; case MBXSettingsRuntimeStylingCustomLatLonGrid: [self addLatLonGrid]; break; case MBXSettingsRuntimeStylingLineGradient: [self styleLineGradient]; break; case MBXSettingsRuntimeStylingCustomDrawableLayer: [self addCustomDrawableLayer]; break; case MBXSettingsRuntimeStylingAddFoursquarePOIsPMTiles: [self addFoursquarePOIsPMTilesLayer]; break; case MBXSettingsRuntimeStylingHeatmapLayer: [self addEarthquakeHeatmap]; break; case MBXSettingsRuntimeStylingParticleQbinV4Smoke: [self showParticleQbinV4SmokeMenu]; break; default: NSAssert(NO, @"All runtime styling setting rows should be implemented"); break; } break; case MBXSettingsMiscellaneous: switch (indexPath.row) { case MBXSettingsMiscellaneousLocalizeLabels: [self toggleStyleLabelsLanguage]; break; case MBXSettingsMiscellaneousWorldTour: [self startWorldTour]; break; case MBXSettingsMiscellaneousRandomTour: [self randomWorldTour]; break; case MBXSettingsMiscellaneousScrollView: { UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil]; MBXEmbeddedMapViewController *embeddedMapViewController = (MBXEmbeddedMapViewController *)[storyboard instantiateViewControllerWithIdentifier:@"MBXEmbeddedMapViewController"]; [self.navigationController pushViewController:embeddedMapViewController animated:YES]; break; } case MBXSettingsMiscellaneousToggleTwoMaps: [self toggleSecondMapView]; break; case MBXSettingsMiscellaneousShowSnapshots: { [self performSegueWithIdentifier:@"ShowSnapshots" sender:nil]; break; } case MBXSettingsMiscellaneousMissingIcon: { [self loadMissingIcon]; break; } case MBXSettingsMiscellaneousShowCustomLocationManager: { [self performSegueWithIdentifier:@"ShowCustomLocationManger" sender:nil]; break; } case MBXSettingsMiscellaneousShouldLimitCameraChanges: { self.shouldLimitCameraChanges = !self.shouldLimitCameraChanges; if (self.shouldLimitCameraChanges) { [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(39.748947, -104.995882) zoomLevel:10 direction:0 animated:NO]; } break; } case MBXSettingsMiscellaneousSetContentInsets: { if (!_contentInsetsEnabled) { _originalContentInsets = [self.mapView contentInset]; } _contentInsetsEnabled = !_contentInsetsEnabled; _mapView.automaticallyAdjustsContentInset = !_contentInsetsEnabled; UIEdgeInsets contentInsets = self.mapView.bounds.size.width > self.mapView.bounds.size.height ? UIEdgeInsetsMake(_originalContentInsets.top, 0.5 * self.mapView.bounds.size.width, _originalContentInsets.bottom, 0.0) : UIEdgeInsetsMake(0.25 * self.mapView.bounds.size.height, 0.0, _originalContentInsets.bottom, 0.25 * self.mapView.bounds.size.width); if (_contentInsetsEnabled) { if (!self.contentInsetsOverlays) self.contentInsetsOverlays = [NSMutableArray array]; if (![self.contentInsetsOverlays count]) { UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.mapView.bounds.size.width, contentInsets.top)]; view.backgroundColor = [UIColor colorWithRed:0.0 green:0.3 blue:0.3 alpha:0.5]; [self.contentInsetsOverlays addObject:view]; [self.view addSubview:view]; view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, contentInsets.left, self.mapView.bounds.size.height)]; view.backgroundColor = [UIColor colorWithRed:0.0 green:0.3 blue:0.3 alpha:0.5]; [self.contentInsetsOverlays addObject:view]; [self.view addSubview:view]; view = [[UIView alloc] initWithFrame:CGRectMake(self.mapView.bounds.size.width - contentInsets.right, 0, contentInsets.right, self.mapView.bounds.size.height)]; view.backgroundColor = [UIColor colorWithRed:0.0 green:0.3 blue:0.3 alpha:0.5]; [self.contentInsetsOverlays addObject:view]; [self.view addSubview:view]; view = [[UIView alloc] initWithFrame:CGRectMake(0, self.mapView.bounds.size.height - contentInsets.bottom, self.mapView.bounds.size.width, self.mapView.bounds.size.height)]; view.backgroundColor = [UIColor colorWithRed:0.0 green:0.3 blue:0.3 alpha:0.5]; [self.contentInsetsOverlays addObject:view]; [self.view addSubview:view]; } [self.view bringSubviewToFront:self.contentInsetsOverlays[0]]; [self.view bringSubviewToFront:self.contentInsetsOverlays[1]]; [self.view bringSubviewToFront:self.contentInsetsOverlays[2]]; [self.view bringSubviewToFront:self.contentInsetsOverlays[3]]; // Denver streets parallel to cardinal directions help illustrate // viewport center offset when edge insets are set. MLNMapCamera *camera = [MLNMapCamera cameraLookingAtCenterCoordinate:CLLocationCoordinate2DMake(39.72707, -104.9986) acrossDistance:100 pitch:60 heading:0]; __weak MBXViewController *weakSelf = self; [self.mapView setCamera:camera withDuration:0.3 animationTimingFunction:nil completionHandler:^{ [weakSelf.mapView setContentInset:contentInsets animated:YES completionHandler:nil]; }]; } else { [self.view sendSubviewToBack:self.contentInsetsOverlays[0]]; [self.view sendSubviewToBack:self.contentInsetsOverlays[1]]; [self.view sendSubviewToBack:self.contentInsetsOverlays[2]]; [self.view sendSubviewToBack:self.contentInsetsOverlays[3]]; [self.mapView setContentInset:_originalContentInsets animated:YES completionHandler:nil]; } break; } case MBXSettingsMiscellaneousOrnamentsPlacement: { MBXOrnamentsViewController *ornamentsViewController = [[MBXOrnamentsViewController alloc] init]; [self.navigationController pushViewController:ornamentsViewController animated:YES]; break; } case MBXSettingsMiscellaneousLatLngBoundsWithPadding: [self flyToWithLatLngBoundsAndPadding]; break; case MBXSettingsMiscellaneousCycleTileLOD: [self cycleTileLodMode]; break; case MBXSettingsMiscellaneousLiveSnapshot: { MBXSnapshotTestViewController *vc = [[MBXSnapshotTestViewController alloc] init]; vc.sourceMapView = self.mapView; [self.navigationController pushViewController:vc animated:YES]; break; } default: NSAssert(NO, @"All miscellaneous setting rows should be implemented"); break; } break; default: NSAssert(NO, @"All settings sections should be implemented"); break; } } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [[self settingsSectionTitles] count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[self settingsTitlesForSection:section] count]; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { return [[self settingsSectionTitles] objectAtIndex:section]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; cell.textLabel.text = [[self settingsTitlesForSection:indexPath.section] objectAtIndex:indexPath.row]; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:NO]; [self dismissViewControllerAnimated:YES completion:^{ [self performActionForSettingAtIndexPath:indexPath]; }]; } // MARK: - Debugging Actions - (NSDictionary *)loadPointFeatures { [_loadLock lock]; if (!_pointFeatures) { NSData *featuresData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"points" ofType:@"geojson"]]; if (featuresData) { id features = [NSJSONSerialization JSONObjectWithData:featuresData options:0 error:nil]; if ([features isKindOfClass:[NSDictionary class]]) { _pointFeatures = (NSDictionary *)features; } } } [_loadLock unlock]; return _pointFeatures; } - (void)parseFeaturesAddingCount:(NSUInteger)featuresCount usingViews:(BOOL)useViews { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if (auto *features = [self loadPointFeatures]) { NSMutableArray *annotations = [NSMutableArray array]; for (NSDictionary *feature in features[@"features"]) { CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake([feature[@"geometry"][@"coordinates"][1] doubleValue], [feature[@"geometry"][@"coordinates"][0] doubleValue]); NSString *title = feature[@"properties"][@"NAME"]; MLNPointAnnotation *annotation = (useViews ? [MLNPointAnnotation new] : [MBXSpriteBackedAnnotation new]); annotation.coordinate = coordinate; annotation.title = title; [annotations addObject:annotation]; if (annotations.count == featuresCount) break; } dispatch_async(dispatch_get_main_queue(), ^{ [self.mapView removeAnnotations:self.mapView.annotations]; [self.mapView addAnnotations:annotations]; [self.mapView showAnnotations:annotations animated:YES]; }); } }); } - (void)animateAnnotationView { MLNPointAnnotation *annot = [[MLNPointAnnotation alloc] init]; annot.coordinate = self.mapView.centerCoordinate; [self.mapView addAnnotation:annot]; // Move the annotation to a point that is offscreen. CGPoint point = CGPointMake(self.view.frame.origin.x - 200, CGRectGetMidY(self.view.frame)); CLLocationCoordinate2D coord = [self.mapView convertPoint:point toCoordinateFromView:self.view]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [UIView animateWithDuration:10 animations:^{ annot.coordinate = coord; }]; }); }; - (void)addTestShapes:(NSUInteger)featuresCount { for (NSUInteger featureIndex = 0; featureIndex < featuresCount; ++featureIndex) { double deltaLongitude = featureIndex * 0.01; double deltaLatitude = -featureIndex * 0.01; // Pacific Northwest triangle // CLLocationCoordinate2D triangleCoordinates[3] = { CLLocationCoordinate2DMake(44 + deltaLatitude, -122 + deltaLongitude), CLLocationCoordinate2DMake(46 + deltaLatitude, -122 + deltaLongitude), CLLocationCoordinate2DMake(46 + deltaLatitude, -121 + deltaLongitude)}; MLNPolygon *triangle = [MLNPolygon polygonWithCoordinates:triangleCoordinates count:3]; [self.mapView addAnnotation:triangle]; // West coast polyline // CLLocationCoordinate2D lineCoordinates[4] = { CLLocationCoordinate2DMake(47.6025 + deltaLatitude, -122.3327 + deltaLongitude), CLLocationCoordinate2DMake(45.5189 + deltaLatitude, -122.6726 + deltaLongitude), CLLocationCoordinate2DMake(37.7790 + deltaLatitude, -122.4177 + deltaLongitude), CLLocationCoordinate2DMake(34.0532 + deltaLatitude, -118.2349 + deltaLongitude)}; MLNPolyline *line = [MLNPolyline polylineWithCoordinates:lineCoordinates count:4]; [self.mapView addAnnotation:line]; // Orcas Island, WA hike polyline // NSDictionary *hike = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"polyline" ofType:@"geojson"]] options:0 error:nil]; NSArray *hikeCoordinatePairs = hike[@"features"][0][@"geometry"][@"coordinates"]; CLLocationCoordinate2D *polylineCoordinates = (CLLocationCoordinate2D *)malloc( [hikeCoordinatePairs count] * sizeof(CLLocationCoordinate2D)); for (NSUInteger i = 0; i < [hikeCoordinatePairs count]; i++) { polylineCoordinates[i] = CLLocationCoordinate2DMake([hikeCoordinatePairs[i][1] doubleValue] + deltaLatitude, [hikeCoordinatePairs[i][0] doubleValue] + deltaLongitude); } MLNPolyline *polyline = [MLNPolyline polylineWithCoordinates:polylineCoordinates count:[hikeCoordinatePairs count]]; [self.mapView addAnnotation:polyline]; free(polylineCoordinates); // PA/NJ/DE polygons // NSDictionary *threestates = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"threestates" ofType:@"geojson"]] options:0 error:nil]; for (NSDictionary *feature in threestates[@"features"]) { NSArray *stateCoordinatePairs = feature[@"geometry"][@"coordinates"]; while ([stateCoordinatePairs count] == 1) stateCoordinatePairs = stateCoordinatePairs[0]; CLLocationCoordinate2D *polygonCoordinates = (CLLocationCoordinate2D *)malloc( [stateCoordinatePairs count] * sizeof(CLLocationCoordinate2D)); for (NSUInteger i = 0; i < [stateCoordinatePairs count]; i++) { polygonCoordinates[i] = CLLocationCoordinate2DMake([stateCoordinatePairs[i][1] doubleValue] + deltaLatitude, [stateCoordinatePairs[i][0] doubleValue] + deltaLongitude); } MLNPolygon *polygon = [MLNPolygon polygonWithCoordinates:polygonCoordinates count:[stateCoordinatePairs count]]; polygon.title = feature[@"properties"][@"NAME"]; [self.mapView addAnnotation:polygon]; free(polygonCoordinates); } // Null Island polygon with an interior hole // CLLocationCoordinate2D innerCoordinates[] = { CLLocationCoordinate2DMake(-5 + deltaLatitude, -5 + deltaLongitude), CLLocationCoordinate2DMake(-5 + deltaLatitude, 5 + deltaLongitude), CLLocationCoordinate2DMake(5 + deltaLatitude, 5 + deltaLongitude), CLLocationCoordinate2DMake(5 + deltaLatitude, -5 + deltaLongitude), }; MLNPolygon *innerPolygon = [MLNPolygon polygonWithCoordinates:innerCoordinates count:sizeof(innerCoordinates) / sizeof(innerCoordinates[0])]; CLLocationCoordinate2D outerCoordinates[] = { CLLocationCoordinate2DMake(-10 + deltaLatitude, -10 + deltaLongitude), CLLocationCoordinate2DMake(-10 + deltaLatitude, 10 + deltaLongitude), CLLocationCoordinate2DMake(10 + deltaLatitude, 10 + deltaLongitude), CLLocationCoordinate2DMake(10 + deltaLatitude, -10 + deltaLongitude), }; MLNPolygon *outerPolygon = [MLNPolygon polygonWithCoordinates:outerCoordinates count:sizeof(outerCoordinates) / sizeof(outerCoordinates[0]) interiorPolygons:@[ innerPolygon ]]; [self.mapView addAnnotation:outerPolygon]; } } - (void)addAnnotationWithCustomCallout { [self.mapView removeAnnotations:self.mapView.annotations]; MBXCustomCalloutAnnotation *firstAnnotation = [[MBXCustomCalloutAnnotation alloc] init]; firstAnnotation.coordinate = CLLocationCoordinate2DMake(48.8533940, 2.3775439); firstAnnotation.title = @"Open anchored to annotation"; firstAnnotation.anchoredToAnnotation = YES; firstAnnotation.dismissesAutomatically = NO; MBXCustomCalloutAnnotation *secondAnnotation = [[MBXCustomCalloutAnnotation alloc] init]; secondAnnotation.coordinate = CLLocationCoordinate2DMake(48.8543940, 2.3775439); secondAnnotation.title = @"Open not anchored to annotation"; secondAnnotation.anchoredToAnnotation = NO; secondAnnotation.dismissesAutomatically = NO; MBXCustomCalloutAnnotation *thirdAnnotation = [[MBXCustomCalloutAnnotation alloc] init]; thirdAnnotation.coordinate = CLLocationCoordinate2DMake(48.8553940, 2.3775439); thirdAnnotation.title = @"Dismisses automatically"; thirdAnnotation.anchoredToAnnotation = YES; thirdAnnotation.dismissesAutomatically = YES; NSArray *annotations = @[ firstAnnotation, secondAnnotation, thirdAnnotation ]; [self.mapView addAnnotations:annotations]; [self.mapView showAnnotations:annotations animated:YES]; } // MARK: - Foursquare PMTiles Implementation /* Add the new method that creates the PMTiles vector source and circle layer. */ - (void)addFoursquarePOIsPMTilesLayer { MLNVectorTileSource *foursquareSource = [[MLNVectorTileSource alloc] initWithIdentifier:@"foursquare-10M" configurationURLString: @"pmtiles://https://oliverwipfli.ch/data/foursquare-os-places-10M-2024-11-20.pmtiles"]; // Also works // MLNVectorTileSource *foursquareSource = // [[MLNVectorTileSource alloc] initWithIdentifier:@"foursquare-10M" // tileURLTemplates:@[@"pmtiles://https://oliverwipfli.ch/data/foursquare-os-places-10M-2024-11-20.pmtiles"] // options:nil]; [self.mapView.style addSource:foursquareSource]; MLNCircleStyleLayer *circleLayer = [[MLNCircleStyleLayer alloc] initWithIdentifier:@"foursquare-10M" source:foursquareSource]; circleLayer.sourceLayerIdentifier = @"place"; circleLayer.maximumZoomLevel = 11; circleLayer.circleColor = [NSExpression expressionForConstantValue:[UIColor colorWithRed:0.8 green:0.2 blue:0.2 alpha:1.0]]; circleLayer.circleOpacity = [NSExpression expressionWithMLNJSONObject:@[ @"interpolate", @[ @"linear" ], @[ @"zoom" ], @6, @0.5, @11, @0.5, @14, @1.0 ]]; circleLayer.circleRadius = [NSExpression expressionWithMLNJSONObject:@[ @"interpolate", @[ @"linear" ], @[ @"zoom" ], @6, @1, @11, @3, @16, @4, @18, @8 ]]; [self.mapView.style addLayer:circleLayer]; } // MARK: - STEP-081 Particle / QBIN V4 smoke (test app only) - (void)showParticleQbinV4SmokeMenu { UIAlertController *menu = [UIAlertController alertControllerWithTitle:@"Particle / QBIN V4 Smoke" message:@"Wind Golden QBIN T0/T1. This is a private Core test seam." preferredStyle:UIAlertControllerStyleAlert]; __weak MBXViewController *weakSelf = self; void (^selectTime)(uint64_t, NSString *) = ^(uint64_t timestamp, NSString *label) { [weakSelf installParticleQbinV4SmokeAtTime:timestamp label:label]; }; [menu addAction:[UIAlertAction actionWithTitle:@"Load T0" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) { selectTime(1789239600000ULL, @"T0"); }]]; [menu addAction:[UIAlertAction actionWithTitle:@"Load midpoint (mix 0.5)" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) { selectTime(1789241400000ULL, @"MIDPOINT"); }]]; [menu addAction:[UIAlertAction actionWithTitle:@"Load T1" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) { selectTime(1789243200000ULL, @"T1"); }]]; [menu addAction:[UIAlertAction actionWithTitle:@"Clear / release" style:UIAlertActionStyleDestructive handler:^(__unused UIAlertAction *a) { [weakSelf clearParticleQbinV4Smoke]; }]]; [menu addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]]; [self presentViewController:menu animated:YES completion:nil]; } - (void)installParticleQbinV4SmokeAtTime:(uint64_t)requestedTime label:(NSString *)label { [self clearParticleQbinV4Smoke]; NSString *(^fixturePath)(NSString *) = ^NSString *(NSString *name) { return [[NSBundle mainBundle] pathForResource:name ofType:@"qbin" inDirectory:@"ParticleSmokeFixtures"]; }; NSString *t0Path = fixturePath(@"T0"); NSString *t1Path = fixturePath(@"T1"); NSData *t0Data = t0Path ? [NSData dataWithContentsOfFile:t0Path] : nil; NSData *t1Data = t1Path ? [NSData dataWithContentsOfFile:t1Path] : nil; if (!t0Data || !t1Data) { NSLog(@"ParticleQbinV4Smoke: event=FIXTURE_MISSING expected=ParticleSmokeFixtures/T0.qbin,T1.qbin"); return; } auto& style = [self.mapView mbglMap].getStyle(); auto source = std::make_unique("particle-qbin-v4-smoke-source"); auto *sourcePtr = source.get(); mbgl::particle::ResolvedFieldProfileContract contract; contract.profileID = "wind"; contract.profileVersion = 1; contract.timeRelation = mbgl::particle::FieldTimeRelation::ValidGteReference; contract.requiredPlanes = {{1, mbgl::particle::FieldValueClass::VelocityComponent, 1, 1, mbgl::particle::FieldComponent::UEast, true}, {2, mbgl::particle::FieldValueClass::VelocityComponent, 1, 1, mbgl::particle::FieldComponent::VNorth, true}, {3, mbgl::particle::FieldValueClass::Validity, 0, 0, mbgl::particle::FieldComponent::None, true}}; const auto bytes = [](NSData *data) { const auto *begin = static_cast(data.bytes); return std::vector(begin, begin + data.length); }; const auto r0 = sourcePtr->submitEncodedField("fixture.wind", {"fixture-wind", 1, 1}, contract, bytes(t0Data)); const auto r1 = sourcePtr->submitEncodedField("fixture.wind", {"fixture-wind", 2, 2}, contract, bytes(t1Data)); sourcePtr->setColorBinding({"fixture.wind", {1, 1, mbgl::particle::FieldComponent::UEast}, mbgl::particle::FieldValueClass::VelocityComponent, 1}); sourcePtr->setParticleBinding({mbgl::particle::ParticleBindingMode::VectorComponents, "fixture.wind", {1, 1, mbgl::particle::FieldComponent::UEast}, {2, 1, mbgl::particle::FieldComponent::VNorth}, {}, 1}); sourcePtr->setRequestedForecastTime(requestedTime); if (r0 != mbgl::style::RasterArrayEncodedFieldSubmitResult::AcceptedInserted || r1 != mbgl::style::RasterArrayEncodedFieldSubmitResult::AcceptedInserted) { NSLog(@"ParticleQbinV4Smoke: event=FIELD_REJECTED T0=%d T1=%d", int(r0), int(r1)); return; } auto weather = std::make_unique("particle-qbin-v4-smoke-weather", "particle-qbin-v4-smoke-source"); weather->setOpacity(.82f); weather->setFieldValueSource(mbgl::style::FieldValueSource::VectorMagnitude); weather->setColorStops({{0.f, {0.05f, 0.18f, 0.65f, 1.f}}, {8.f, {0.05f, 0.75f, 0.78f, 1.f}}, {20.f, {0.98f, 0.22f, 0.10f, 1.f}}}); auto particles = std::make_unique("particle-qbin-v4-smoke-particles", "particle-qbin-v4-smoke-source"); particles->setParticleCount(4096); particles->setParticleOpacity(.95f); particles->setTrailOpacity(.75f); particles->setTrailFade(.94f); particles->setSpeedColorEnabled(true); style.addSource(std::move(source)); style.addLayer(std::move(weather)); style.addLayer(std::move(particles)); [self.mapView setNeedsRerender]; NSLog(@"ParticleQbinV4Smoke: event=FIELD_READY state=%@ requestedTime=%llu source=fixture.wind layers=weather-color,raster-particle", label, requestedTime); } - (void)clearParticleQbinV4Smoke { 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"); if (style.getSource("particle-qbin-v4-smoke-source")) style.removeSource("particle-qbin-v4-smoke-source"); [self.mapView setNeedsRerender]; NSLog(@"ParticleQbinV4Smoke: event=CLEAR_RELEASED"); } // Earthquake Heatmap - (void)addEarthquakeHeatmap { NSURL *earthquakesURL = [NSURL URLWithString:@"https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson"]; MLNShapeSource *source = [[MLNShapeSource alloc] initWithIdentifier:@"earthquakes" URL:earthquakesURL options:nil]; [self.mapView.style addSource:source]; // Heatmap layer - visible at low zoom, fades out by zoom 9 MLNHeatmapStyleLayer *heatmapLayer = [[MLNHeatmapStyleLayer alloc] initWithIdentifier:@"earthquakes-heat" source:source]; heatmapLayer.maximumZoomLevel = 9; // Weight by earthquake magnitude heatmapLayer.heatmapWeight = [NSExpression expressionWithMLNJSONObject:@[ @"interpolate", @[ @"linear" ], @[ @"get", @"mag" ], @0, @0, @6, @1 ]]; // Increase intensity with zoom heatmapLayer.heatmapIntensity = [NSExpression expressionWithMLNJSONObject:@[ @"interpolate", @[ @"linear" ], @[ @"zoom" ], @0, @1, @9, @3 ]]; // Color ramp from transparent blue to dark red heatmapLayer.heatmapColor = [NSExpression expressionWithMLNJSONObject:@[ @"interpolate", @[ @"linear" ], @[ @"heatmap-density" ], @0, @"rgba(33,102,172,0)", @0.2, @"rgb(103,169,207)", @0.4, @"rgb(209,229,240)", @0.6, @"rgb(253,219,199)", @0.8, @"rgb(239,138,98)", @1, @"rgb(178,24,43)" ]]; // Increase radius with zoom heatmapLayer.heatmapRadius = [NSExpression expressionWithMLNJSONObject:@[ @"interpolate", @[ @"linear" ], @[ @"zoom" ], @0, @2, @9, @20 ]]; // Fade out heatmap at high zoom heatmapLayer.heatmapOpacity = [NSExpression expressionWithMLNJSONObject:@[ @"interpolate", @[ @"linear" ], @[ @"zoom" ], @7, @1, @9, @0 ]]; [self.mapView.style addLayer:heatmapLayer]; // Circle layer - visible at high zoom, fades in from zoom 7 MLNCircleStyleLayer *circleLayer = [[MLNCircleStyleLayer alloc] initWithIdentifier:@"earthquakes-circle" source:source]; circleLayer.circleRadius = [NSExpression expressionWithMLNJSONObject:@[ @"interpolate", @[ @"linear" ], @[ @"zoom" ], @7, @[ @"interpolate", @[ @"linear" ], @[ @"get", @"mag" ], @1, @1, @6, @4 ], @16, @[ @"interpolate", @[ @"linear" ], @[ @"get", @"mag" ], @1, @5, @6, @50 ] ]]; circleLayer.circleColor = [NSExpression expressionWithMLNJSONObject:@[ @"interpolate", @[ @"linear" ], @[ @"get", @"mag" ], @1, @"rgba(33,102,172,0)", @2, @"rgb(103,169,207)", @3, @"rgb(209,229,240)", @4, @"rgb(253,219,199)", @5, @"rgb(239,138,98)", @6, @"rgb(178,24,43)" ]]; circleLayer.circleOpacity = [NSExpression expressionWithMLNJSONObject:@[ @"interpolate", @[ @"linear" ], @[ @"zoom" ], @7, @0, @8, @1 ]]; circleLayer.circleStrokeColor = [NSExpression expressionForConstantValue:[UIColor whiteColor]]; circleLayer.circleStrokeWidth = [NSExpression expressionForConstantValue:@1]; [self.mapView.style insertLayer:circleLayer belowLayer:heatmapLayer]; // Move camera to show earthquake data (Central America / Pacific region) [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(15.0, -94.0) zoomLevel:5 animated:YES]; } - (void)styleBuildingExtrusions { MLNSource *source = [self.mapView.style sourceWithIdentifier:@"composite"]; if (source) { MLNFillExtrusionStyleLayer *layer = [[MLNFillExtrusionStyleLayer alloc] initWithIdentifier:@"extrudedBuildings" source:source]; layer.sourceLayerIdentifier = @"building"; layer.predicate = [NSPredicate predicateWithFormat:@"extrude == 'true' AND CAST(height, 'NSNumber') > 0"]; layer.fillExtrusionBase = [NSExpression expressionForKeyPath:@"min_height"]; layer.fillExtrusionHeight = [NSExpression expressionForKeyPath:@"height"]; // Set the fill color to that of the existing building footprint layer, if it exists. MLNFillStyleLayer *buildingLayer = (MLNFillStyleLayer *)[self.mapView.style layerWithIdentifier:@"building"]; if (buildingLayer) { if (buildingLayer.fillColor) { layer.fillExtrusionColor = buildingLayer.fillColor; } else { layer.fillExtrusionColor = [NSExpression expressionForConstantValue:[UIColor whiteColor]]; } layer.fillExtrusionOpacity = [NSExpression expressionForConstantValue:@0.75]; } MLNStyleLayer *labelLayer = [self.mapView.style layerWithIdentifier:@"waterway-label"]; if (labelLayer) { [self.mapView.style insertLayer:layer belowLayer:labelLayer]; } else { [self.mapView.style addLayer:layer]; } } } - (void)styleWaterLayer { MLNFillStyleLayer *waterLayer = (MLNFillStyleLayer *)[self.mapView.style layerWithIdentifier:@"water"]; NSDictionary *waterColorStops = @{ @6.0f : [UIColor yellowColor], @8.0f : [UIColor blueColor], @10.0f : [UIColor redColor], @12.0f : [UIColor greenColor], @14.0f : [UIColor blueColor] }; NSExpression *fillColorExpression = [NSExpression mgl_expressionForInterpolatingExpression:NSExpression.zoomLevelVariableExpression withCurveType:MLNExpressionInterpolationModeLinear parameters:nil stops:[NSExpression expressionForConstantValue:waterColorStops]]; waterLayer.fillColor = fillColorExpression; NSDictionary *fillAntialiasedStops = @{@11 : @YES, @12 : @NO, @13 : @YES, @14 : @NO, @15 : @YES}; waterLayer.fillAntialiased = [NSExpression mgl_expressionForSteppingExpression:NSExpression.zoomLevelVariableExpression fromExpression:[NSExpression expressionForConstantValue:@NO] stops:[NSExpression expressionForConstantValue:fillAntialiasedStops]]; } - (void)styleRoadLayer { MLNLineStyleLayer *roadLayer = (MLNLineStyleLayer *)[self.mapView.style layerWithIdentifier:@"road-primary"]; roadLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor blackColor]]; NSDictionary *lineWidthStops = @{@5 : @5, @10 : @15, @15 : @30}; NSExpression *lineWidthExpression = [NSExpression expressionWithFormat: @"mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", lineWidthStops]; roadLayer.lineWidth = lineWidthExpression; roadLayer.lineGapWidth = lineWidthExpression; NSDictionary *roadLineColorStops = @{@10 : [UIColor purpleColor], @13 : [UIColor yellowColor], @16 : [UIColor cyanColor]}; roadLayer.lineColor = [NSExpression expressionWithFormat: @"mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", roadLineColorStops]; roadLayer.visible = YES; roadLayer.maximumZoomLevel = 15; roadLayer.minimumZoomLevel = 13; } - (void)styleRasterLayer { NSURL *rasterURL = [NSURL URLWithString:@"maptiler://sources/hybrid"]; MLNRasterTileSource *rasterTileSource = [[MLNRasterTileSource alloc] initWithIdentifier:@"my-raster-tile-source" configurationURL:rasterURL tileSize:512]; [self.mapView.style addSource:rasterTileSource]; MLNRasterStyleLayer *rasterLayer = [[MLNRasterStyleLayer alloc] initWithIdentifier:@"my-raster-layer" source:rasterTileSource]; NSDictionary *opacityStops = @{@20.0f : @1.0f, @5.0f : @0.0f}; rasterLayer.rasterOpacity = [NSExpression expressionWithFormat: @"mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", opacityStops]; [self.mapView.style addLayer:rasterLayer]; } - (void)styleShapeSource { NSString *filePath = [[NSBundle bundleForClass:self.class] pathForResource:@"amsterdam" ofType:@"geojson"]; NSURL *geoJSONURL = [NSURL fileURLWithPath:filePath]; MLNShapeSource *source = [[MLNShapeSource alloc] initWithIdentifier:@"ams" URL:geoJSONURL options:nil]; [self.mapView.style addSource:source]; MLNFillStyleLayer *fillLayer = [[MLNFillStyleLayer alloc] initWithIdentifier:@"test" source:source]; fillLayer.fillColor = [NSExpression expressionForConstantValue:[UIColor purpleColor]]; [self.mapView.style addLayer:fillLayer]; } - (void)styleSymbolLayer { if (auto *stateLayer = (MLNSymbolStyleLayer *)[self.mapView.style layerWithIdentifier:@"state-label-lg"]) { stateLayer.textColor = [NSExpression expressionForConstantValue:[UIColor redColor]]; } } - (void)styleBuildingLayer { MLNTransition transition = {5, 1}; self.mapView.style.transition = transition; if (auto *buildingLayer = (MLNFillStyleLayer *)[self.mapView.style layerWithIdentifier:@"building"]) { buildingLayer.fillColor = [NSExpression expressionForConstantValue:[UIColor purpleColor]]; } } - (void)styleFerryLayer { if (auto *ferryLineLayer = (MLNLineStyleLayer *)[self.mapView.style layerWithIdentifier:@"ferry"]) { ferryLineLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor redColor]]; } } - (void)removeParkLayer { if (auto *parkLayer = (MLNFillStyleLayer *)[self.mapView.style layerWithIdentifier:@"park"]) { [self.mapView.style removeLayer:parkLayer]; } } - (BOOL)loadStyleFromBundle:(NSString *)path { NSString *resourcePath = [[NSBundle mainBundle] pathForResource:path ofType:@"json"]; if (NSURL *url = resourcePath ? [NSURL fileURLWithPath:resourcePath] : nil) { [self.mapView setStyleURL:url]; return TRUE; } else { NSString *msg = [NSString stringWithFormat:@"Missing Style %@", path]; UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Style File" message:msg preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleCancel handler:nil]]; [self presentViewController:alertController animated:YES completion:nil]; return FALSE; } } - (void)styleFilteredFill { // set style and focus on Texas if (![self loadStyleFromBundle:@"fill_filter_style"]) { return; } [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(31, -100) zoomLevel:3 animated:NO]; // after slight delay, fill in Texas (atypical use; we want to clearly see the change for test // purposes) dispatch_after( dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ MLNFillStyleLayer *statesLayer = (MLNFillStyleLayer *)[self.mapView.style layerWithIdentifier:@"states"]; // filter statesLayer.predicate = [NSPredicate predicateWithFormat:@"name == 'Texas'"]; // paint properties statesLayer.fillColor = [NSExpression expressionForConstantValue:[UIColor redColor]]; statesLayer.fillOpacity = [NSExpression expressionForConstantValue:@0.25]; }); } - (void)styleFilteredLines { // set style and focus on lower 48 if (![self loadStyleFromBundle:@"line_filter_style"]) { return; } [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(40, -97) zoomLevel:5 animated:NO]; // after slight delay, change styling for all Washington-named counties (atypical use; we want to // clearly see the change for test purposes) dispatch_after( dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ MLNLineStyleLayer *countiesLayer = (MLNLineStyleLayer *)[self.mapView.style layerWithIdentifier:@"counties"]; // filter countiesLayer.predicate = [NSPredicate predicateWithFormat:@"NAME10 == 'Washington'"]; // paint properties countiesLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor redColor]]; countiesLayer.lineOpacity = [NSExpression expressionForConstantValue:@0.75]; countiesLayer.lineWidth = [NSExpression expressionForConstantValue:@5]; }); } - (void)styleNumericFilteredFills { // set style and focus on lower 48 if (![self loadStyleFromBundle:@"numeric_filter_style"]) { return; } [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(40, -97) zoomLevel:5 animated:NO]; // after slight delay, change styling for regions 200-299 (atypical use; we want to clearly see // the change for test purposes) dispatch_after( dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ MLNFillStyleLayer *regionsLayer = (MLNFillStyleLayer *)[self.mapView.style layerWithIdentifier:@"regions"]; // filter (testing both inline and format strings) regionsLayer.predicate = [NSPredicate predicateWithFormat: @"CAST(HRRNUM, 'NSNumber') >= %@ AND CAST(HRRNUM, 'NSNumber') < 300", @(200)]; // paint properties regionsLayer.fillColor = [NSExpression expressionForConstantValue:[UIColor blueColor]]; regionsLayer.fillOpacity = [NSExpression expressionForConstantValue:@0.5]; }); } - (void)styleQuery { CGRect queryRect = CGRectInset(self.mapView.bounds, 100, 200); NSArray *visibleFeatures = [self.mapView visibleFeaturesInRect:queryRect]; NSString *querySourceID = @"query-source-id"; NSString *queryLayerID = @"query-layer-id"; // RTE if you don't remove the layer first // RTE if you pass a nill layer to remove layer MLNStyleLayer *layer = [self.mapView.style layerWithIdentifier:queryLayerID]; if (layer) { [self.mapView.style removeLayer:layer]; } // RTE if you pass a nill source to remove source MLNSource *source = [self.mapView.style sourceWithIdentifier:querySourceID]; if (source) { [self.mapView.style removeSource:source]; } dispatch_async(dispatch_get_main_queue(), ^{ MLNShapeSource *shapeSource = [[MLNShapeSource alloc] initWithIdentifier:querySourceID features:visibleFeatures options:nil]; [self.mapView.style addSource:shapeSource]; MLNFillStyleLayer *fillLayer = [[MLNFillStyleLayer alloc] initWithIdentifier:queryLayerID source:shapeSource]; fillLayer.fillColor = [NSExpression expressionForConstantValue:[UIColor blueColor]]; fillLayer.fillOpacity = [NSExpression expressionForConstantValue:@0.5]; [self.mapView.style addLayer:fillLayer]; }); } - (void)styleFeature { self.mapView.zoomLevel = 10; self.mapView.centerCoordinate = CLLocationCoordinate2DMake(51.068585180672635, -114.06074523925781); CLLocationCoordinate2D leafCoords[] = { {50.9683733218221, -114.07035827636719}, {51.02325750523972, -114.06967163085938}, {51.009434536947786, -114.14245605468749}, {51.030599281184124, -114.12597656249999}, {51.060386316691016, -114.21043395996094}, {51.063838646941576, -114.17816162109375}, {51.08152779888779, -114.19876098632812}, {51.08066507029602, -114.16854858398438}, {51.09662294502995, -114.17472839355469}, {51.07764539352731, -114.114990234375}, {51.13670896949613, -114.12391662597656}, {51.13369295212583, -114.09576416015624}, {51.17546878815025, -114.07585144042969}, {51.140155605265896, -114.04632568359375}, {51.15049396880196, -114.01542663574219}, {51.088860342359965, -114.00924682617186}, {51.12205789681453, -113.94813537597656}, {51.106539930027225, -113.94882202148438}, {51.117747873223344, -113.92616271972656}, {51.10093493903458, -113.92616271972656}, {51.10697105503078, -113.90625}, {51.09144802136697, -113.9117431640625}, {51.04916446529361, -113.97010803222655}, {51.045279344649146, -113.9398956298828}, {51.022825599852496, -114.06211853027344}, {51.045279344649146, -113.9398956298828}, {51.022825599852496, -114.06211853027344}, {51.022825599852496, -114.06280517578125}, {50.968805734317804, -114.06280517578125}, {50.9683733218221, -114.07035827636719}, }; NSUInteger coordsCount = sizeof(leafCoords) / sizeof(leafCoords[0]); MLNPolygonFeature *feature = [MLNPolygonFeature polygonWithCoordinates:leafCoords count:coordsCount]; feature.identifier = @"leaf-feature"; feature.attributes = @{@"color" : @"red"}; MLNShapeSource *source = [[MLNShapeSource alloc] initWithIdentifier:@"leaf-source" shape:feature options:nil]; [self.mapView.style addSource:source]; MLNFillStyleLayer *layer = [[MLNFillStyleLayer alloc] initWithIdentifier:@"leaf-fill-layer" source:source]; layer.predicate = [NSPredicate predicateWithFormat:@"color = 'red'"]; layer.fillColor = [NSExpression expressionForConstantValue:[UIColor redColor]]; [self.mapView.style addLayer:layer]; NSString *geoJSON = @"{\"type\": \"Feature\", \"properties\": {\"color\": \"green\"}, \"geometry\": { \"type\": " @"\"Point\", \"coordinates\": [ -114.06847000122069, 51.050459433092655 ] }}"; NSData *data = [geoJSON dataUsingEncoding:NSUTF8StringEncoding]; MLNShape *shape = [MLNShape shapeWithData:data encoding:NSUTF8StringEncoding error:NULL]; MLNShapeSource *pointSource = [[MLNShapeSource alloc] initWithIdentifier:@"leaf-point-source" shape:shape options:nil]; [self.mapView.style addSource:pointSource]; MLNCircleStyleLayer *circleLayer = [[MLNCircleStyleLayer alloc] initWithIdentifier:@"leaf-circle-layer" source:pointSource]; circleLayer.circleColor = [NSExpression expressionForConstantValue:[UIColor greenColor]]; circleLayer.predicate = [NSPredicate predicateWithFormat:@"color = 'green'"]; [self.mapView.style addLayer:circleLayer]; CLLocationCoordinate2D squareCoords[] = { {51.056070541830934, -114.0274429321289}, {51.07937094724242, -114.0274429321289}, {51.07937094724242, -113.98761749267578}, {51.05607054183093, -113.98761749267578}, {51.056070541830934, -114.0274429321289}, }; MLNPolygon *polygon = [MLNPolygon polygonWithCoordinates:squareCoords count:sizeof(squareCoords) / sizeof(squareCoords[0])]; MLNShapeSource *plainShapeSource = [[MLNShapeSource alloc] initWithIdentifier:@"leaf-plain-shape-source" shape:polygon options:nil]; [self.mapView.style addSource:plainShapeSource]; MLNFillStyleLayer *plainFillLayer = [[MLNFillStyleLayer alloc] initWithIdentifier:@"leaf-plain-fill-layer" source:plainShapeSource]; plainFillLayer.fillColor = [NSExpression expressionForConstantValue:[UIColor yellowColor]]; [self.mapView.style addLayer:plainFillLayer]; } - (void)updateShapeSourceData { [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(40.329795743702064, -107.75390625) zoomLevel:11 animated:NO]; NSString *geoJSON = @"{\"type\": \"FeatureCollection\",\"features\": [{\"type\": \"Feature\",\"properties\": " @"{},\"geometry\": {\"type\": \"LineString\",\"coordinates\": " @"[[-107.75390625,40.329795743702064],[-104.34814453125,37.64903402157866]]}}]}"; NSData *data = [geoJSON dataUsingEncoding:NSUTF8StringEncoding]; MLNShape *shape = [MLNShape shapeWithData:data encoding:NSUTF8StringEncoding error:NULL]; MLNShapeSource *source = [[MLNShapeSource alloc] initWithIdentifier:@"mutable-data-source-id" shape:shape options:nil]; [self.mapView.style addSource:source]; MLNLineStyleLayer *layer = [[MLNLineStyleLayer alloc] initWithIdentifier:@"mutable-data-layer-id" source:source]; [self.mapView.style addLayer:layer]; dispatch_after( dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSString *updatedGeoJSON = @"{\"type\": \"FeatureCollection\",\"features\": [{\"type\": " @"\"Feature\",\"properties\": {},\"geometry\": {\"type\": " @"\"LineString\",\"coordinates\": " @"[[-107.75390625,40.329795743702064],[-109.34814453125,37.64903402157866]]}}]}"; NSData *updatedData = [updatedGeoJSON dataUsingEncoding:NSUTF8StringEncoding]; MLNShape *updatedShape = [MLNShape shapeWithData:updatedData encoding:NSUTF8StringEncoding error:NULL]; source.shape = updatedShape; }); } - (void)updateShapeSourceURL { [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(48.668731, -122.857151) zoomLevel:11 animated:NO]; NSString *filePath = [[NSBundle bundleForClass:self.class] pathForResource:@"polyline" ofType:@"geojson"]; NSURL *geoJSONURL = [NSURL fileURLWithPath:filePath]; MLNShapeSource *source = [[MLNShapeSource alloc] initWithIdentifier:@"mutable-data-source-url-id" URL:geoJSONURL options:nil]; [self.mapView.style addSource:source]; MLNLineStyleLayer *layer = [[MLNLineStyleLayer alloc] initWithIdentifier:@"mutable-data-layer-url-id" source:source]; [self.mapView.style addLayer:layer]; dispatch_after( dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(41.563986787078704, -75.04843935793578) zoomLevel:8 animated:NO]; NSString *threeStatesFilePath = [[NSBundle bundleForClass:self.class] pathForResource:@"threestates" ofType:@"geojson"]; NSURL *updatedGeoJSONURL = [NSURL fileURLWithPath:threeStatesFilePath]; source.URL = updatedGeoJSONURL; }); } - (void)updateShapeSourceFeatures { [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(-41.1520, 288.6592) zoomLevel:10 animated:NO]; CLLocationCoordinate2D smallBox[] = {{-41.14763798539186, 288.68019104003906}, {-41.140915920129665, 288.68019104003906}, {-41.140915920129665, 288.6887741088867}, {-41.14763798539186, 288.6887741088867}, {-41.14763798539186, 288.68019104003906}}; CLLocationCoordinate2D largeBox[] = {{-41.17710352162799, 288.67298126220703}, {-41.13962313627545, 288.67298126220703}, {-41.13962313627545, 288.7261962890625}, {-41.17710352162799, 288.7261962890625}, {-41.17710352162799, 288.67298126220703}}; MLNPolygonFeature *smallBoxFeature = [MLNPolygonFeature polygonWithCoordinates:smallBox count:sizeof(smallBox) / sizeof(smallBox[0])]; MLNPolygonFeature *largeBoxFeature = [MLNPolygonFeature polygonWithCoordinates:largeBox count:sizeof(largeBox) / sizeof(largeBox[0])]; MLNShapeSource *source = [[MLNShapeSource alloc] initWithIdentifier:@"mutable-data-source-features-id" shape:smallBoxFeature options:nil]; [self.mapView.style addSource:source]; MLNFillStyleLayer *layer = [[MLNFillStyleLayer alloc] initWithIdentifier:@"mutable-data-layer-features-id" source:source]; layer.fillColor = [NSExpression expressionForConstantValue:[UIColor redColor]]; [self.mapView.style addLayer:layer]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ source.shape = largeBoxFeature; }); } - (void)styleDynamicPointCollection { [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(36.9979, -109.0441) zoomLevel:14 animated:NO]; CLLocationCoordinate2D coordinates[] = { {37.00145594210082, -109.04960632324219}, {37.00173012609867, -109.0404224395752}, {36.99453246847359, -109.04960632324219}, {36.99508088541243, -109.04007911682129}, }; MLNPointCollectionFeature *feature = [MLNPointCollectionFeature pointCollectionWithCoordinates:coordinates count:4]; MLNShapeSource *source = [[MLNShapeSource alloc] initWithIdentifier:@"wiggle-source" shape:feature options:nil]; [self.mapView.style addSource:source]; MLNCircleStyleLayer *layer = [[MLNCircleStyleLayer alloc] initWithIdentifier:@"wiggle-layer" source:source]; [self.mapView.style addLayer:layer]; } - (void)styleVectorTileSource { NSURL *url = [[NSURL alloc] initWithString:@"maptiler://source/hillshade"]; MLNVectorTileSource *vectorTileSource = [[MLNVectorTileSource alloc] initWithIdentifier:@"style-vector-tile-source-id" configurationURL:url]; [self.mapView.style addSource:vectorTileSource]; MLNBackgroundStyleLayer *backgroundLayer = [[MLNBackgroundStyleLayer alloc] initWithIdentifier:@"style-vector-background-layer-id"]; backgroundLayer.backgroundColor = [NSExpression expressionForConstantValue:[UIColor blackColor]]; [self.mapView.style addLayer:backgroundLayer]; MLNLineStyleLayer *lineLayer = [[MLNLineStyleLayer alloc] initWithIdentifier:@"style-vector-line-layer-id" source:vectorTileSource]; lineLayer.sourceLayerIdentifier = @"contour"; lineLayer.lineJoin = [NSExpression expressionForConstantValue:@"round"]; lineLayer.lineCap = [NSExpression expressionForConstantValue:@"round"]; lineLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor greenColor]]; [self.mapView.style addLayer:lineLayer]; } - (void)styleRasterTileSource { NSString *tileURL = [NSString stringWithFormat: @"https://stamen-tiles.a.ssl.fastly.net/terrain-background/{z}/{x}/{y}%@.jpg", UIScreen.mainScreen.nativeScale > 1 ? @"@2x" : @""]; MLNRasterTileSource *rasterTileSource = [[MLNRasterTileSource alloc] initWithIdentifier:@"style-raster-tile-source-id" tileURLTemplates:@[ tileURL ] options:@{ MLNTileSourceOptionTileSize : @256, }]; [self.mapView.style addSource:rasterTileSource]; MLNRasterStyleLayer *rasterLayer = [[MLNRasterStyleLayer alloc] initWithIdentifier:@"style-raster-layer-id" source:rasterTileSource]; [self.mapView.style addLayer:rasterLayer]; } - (NSURL *)radarImageURL:(int)index { return [NSURL URLWithString: [NSString stringWithFormat:@"https://maplibre.org/maplibre-gl-js/docs/assets/radar%d.gif", index]]; } - (void)styleImageSource { MLNCoordinateQuad coordinateQuad = { {46.437, -80.425}, {37.936, -80.425}, {37.936, -71.516}, {46.437, -71.516}}; MLNImageSource *imageSource = [[MLNImageSource alloc] initWithIdentifier:@"style-image-source-id" coordinateQuad:coordinateQuad URL:[self radarImageURL:0]]; [self.mapView.style addSource:imageSource]; MLNRasterStyleLayer *rasterLayer = [[MLNRasterStyleLayer alloc] initWithIdentifier:@"style-raster-image-layer-id" source:imageSource]; [self.mapView.style addLayer:rasterLayer]; [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateAnimatedImageSource:) userInfo:imageSource repeats:YES]; const CGFloat maximumPadding = 50; const CGSize frameSize = self.mapView.frame.size; const CGFloat yPadding = (frameSize.height / 5 <= maximumPadding) ? (frameSize.height / 5) : maximumPadding; const CGFloat xPadding = (frameSize.width / 5 <= maximumPadding) ? (frameSize.width / 5) : maximumPadding; [self.mapView setVisibleCoordinateBounds:MLNCoordinateBoundsMake(coordinateQuad.bottomLeft, coordinateQuad.topRight) edgePadding:UIEdgeInsetsMake(yPadding, xPadding, yPadding, xPadding) animated:YES completionHandler:nil]; } - (void)updateAnimatedImageSource:(NSTimer *)timer { MLNImageSource *imageSource = (MLNImageSource *)timer.userInfo; if (![self.mapView.style sourceWithIdentifier:imageSource.identifier]) { // the source has been removed, probably by reloading the style, if we try to update // it now, we will crash with 'This source got invalidated after the style change' [timer invalidate]; return; } static int radarSuffix = 0; [imageSource setValue:[self radarImageURL:radarSuffix] forKey:@"URL"]; radarSuffix = (radarSuffix + 1) % 5; } - (void)toggleStyleLabelsLanguage { _localizingLabels = !_localizingLabels; [self.mapView.style localizeLabelsIntoLocale:_localizingLabels ? [NSLocale localeWithLocaleIdentifier:@"mul"] : nil]; } - (void)styleLineGradient { CLLocationCoordinate2D coords[] = { {43.84455590478528, 10.504238605499268}, {43.84385562343126, 10.504125952720642}, {43.84388657526694, 10.503299832344055}, {43.84332557075269, 10.503235459327698}, {43.843441641085036, 10.502264499664307}, {43.84396395478592, 10.50242006778717}, {43.84406067904351, 10.501744151115416}, {43.84422317544319, 10.501792430877686}}; NSInteger count = sizeof(coords) / sizeof(coords[0]); [self.mapView setCenterCoordinate:coords[0] zoomLevel:16 animated:YES]; MLNPolylineFeature *routeLine = [MLNPolylineFeature polylineWithCoordinates:coords count:count]; NSDictionary *sourceOptions = @{MLNShapeSourceOptionLineDistanceMetrics : @YES}; MLNShapeSource *routeSource = [[MLNShapeSource alloc] initWithIdentifier:@"style-route-source" shape:routeLine options:sourceOptions]; MLNLineStyleLayer *baseRouteLayer = [[MLNLineStyleLayer alloc] initWithIdentifier:@"style-base-route-layer" source:routeSource]; baseRouteLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor orangeColor]]; baseRouteLayer.lineWidth = [NSExpression expressionForConstantValue:@20]; baseRouteLayer.lineOpacity = [NSExpression expressionForConstantValue:@0.95]; baseRouteLayer.lineCap = [NSExpression expressionForConstantValue:@"round"]; baseRouteLayer.lineJoin = [NSExpression expressionForConstantValue:@"round"]; MLNLineStyleLayer *routeLayer = [[MLNLineStyleLayer alloc] initWithIdentifier:@"style-route-layer" source:routeSource]; routeLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor whiteColor]]; routeLayer.lineWidth = [NSExpression expressionForConstantValue:@15]; routeLayer.lineOpacity = [NSExpression expressionForConstantValue:@0.8]; routeLayer.lineCap = [NSExpression expressionForConstantValue:@"round"]; routeLayer.lineJoin = [NSExpression expressionForConstantValue:@"round"]; // Create stops dictionary NSDictionary *stops = @{ @0 : [UIColor blueColor], @0.1 : [UIColor colorWithRed:25 / 255.0 green:41 / 255.0 blue:88 / 255.0 alpha:1.0], @0.3 : [UIColor cyanColor], @0.5 : [UIColor greenColor], @0.7 : [UIColor yellowColor], @1 : [UIColor redColor], }; // Create an expression that will interpolate the color of the line // (mgl_interpolate:withCurveType:parameters:stops:($lineProgress, 'linear', nil, %@)) NSExpression *lineGradientExpression = [NSExpression expressionWithFormat: @"mgl_interpolate:withCurveType:parameters:stops:($lineProgress, 'linear', nil, %@)", stops]; routeLayer.lineGradient = lineGradientExpression; [self removeLayer:baseRouteLayer.identifier]; [self removeLayer:routeLayer.identifier]; [self removeSource:routeSource.identifier]; [self.mapView.style addSource:routeSource]; [self.mapView.style addLayer:baseRouteLayer]; [self.mapView.style addLayer:routeLayer]; } - (void)addCustomDrawableLayer { #if MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_OPENGL // Create a CustomLayer that uses the Drawable/Builder toolkit to generate and render geometry ExampleCustomDrawableStyleLayer *layer = [[ExampleCustomDrawableStyleLayer alloc] initWithIdentifier:@"custom-drawable-layer"]; if (layer) { [self.mapView.style addLayer:layer]; } #endif } - (void)removeSource:(NSString *)ident { if (MLNSource *source = [self.mapView.style sourceWithIdentifier:ident]) { [self.mapView.style removeSource:source]; } } - (void)removeLayer:(NSString *)ident { if (MLNStyleLayer *layer = [self.mapView.style layerWithIdentifier:ident]) { [self.mapView.style removeLayer:layer]; } } - (void)styleRouteLine { CLLocationCoordinate2D coords[] = { {43.84455590478528, 10.504238605499268}, {43.84385562343126, 10.504125952720642}, {43.84388657526694, 10.503299832344055}, {43.84332557075269, 10.503235459327698}, {43.843441641085036, 10.502264499664307}, {43.84396395478592, 10.50242006778717}, {43.84406067904351, 10.501744151115416}, {43.84422317544319, 10.501792430877686}}; NSInteger count = sizeof(coords) / sizeof(coords[0]); [self.mapView setCenterCoordinate:coords[0] zoomLevel:16 animated:YES]; MLNPolylineFeature *routeLine = [MLNPolylineFeature polylineWithCoordinates:coords count:count]; MLNShapeSource *routeSource = [[MLNShapeSource alloc] initWithIdentifier:@"style-route-source" shape:routeLine options:nil]; MLNLineStyleLayer *baseRouteLayer = [[MLNLineStyleLayer alloc] initWithIdentifier:@"style-base-route-layer" source:routeSource]; baseRouteLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor orangeColor]]; baseRouteLayer.lineWidth = [NSExpression expressionForConstantValue:@20]; baseRouteLayer.lineOpacity = [NSExpression expressionForConstantValue:@0.5]; baseRouteLayer.lineCap = [NSExpression expressionForConstantValue:@"round"]; baseRouteLayer.lineJoin = [NSExpression expressionForConstantValue:@"round"]; MLNLineStyleLayer *routeLayer = [[MLNLineStyleLayer alloc] initWithIdentifier:@"style-route-layer" source:routeSource]; routeLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor whiteColor]]; routeLayer.lineWidth = [NSExpression expressionForConstantValue:@15]; routeLayer.lineOpacity = [NSExpression expressionForConstantValue:@0.8]; routeLayer.lineCap = [NSExpression expressionForConstantValue:@"round"]; routeLayer.lineJoin = [NSExpression expressionForConstantValue:@"round"]; [self removeLayer:baseRouteLayer.identifier]; [self removeLayer:routeLayer.identifier]; [self removeSource:routeSource.identifier]; [self.mapView.style addSource:routeSource]; [self.mapView.style addLayer:baseRouteLayer]; [self.mapView.style addLayer:routeLayer]; } - (void)styleAddCustomTriangleLayer { #if MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_OPENGL if (CustomStyleLayerExample *layer = [[CustomStyleLayerExample alloc] initWithIdentifier:@"mbx-custom"]) { [self.mapView.style addLayer:layer]; } #endif } - (void)stylePolygonWithDDS { CLLocationCoordinate2D leftCoords[] = { {37.73081027834234, -122.49412536621094}, {37.7566013348511, -122.49412536621094}, {37.7566013348511, -122.46253967285156}, {37.73081027834234, -122.46253967285156}, {37.73081027834234, -122.49412536621094}, }; CLLocationCoordinate2D rightCoords[] = { {37.73135334055843, -122.44640350341795}, {37.75741564287944, -122.44640350341795}, {37.75741564287944, -122.41310119628906}, {37.73135334055843, -122.41310119628906}, {37.73135334055843, -122.44640350341795}, }; MLNPolygonFeature *leftFeature = [MLNPolygonFeature polygonWithCoordinates:leftCoords count:5]; leftFeature.attributes = @{@"fill" : @(YES)}; MLNPolygonFeature *rightFeature = [MLNPolygonFeature polygonWithCoordinates:rightCoords count:5]; rightFeature.attributes = @{@"opacity" : @(0.5)}; MLNShapeSource *shapeSource = [[MLNShapeSource alloc] initWithIdentifier:@"shape-source" features:@[ leftFeature, rightFeature ] options:nil]; [self.mapView.style addSource:shapeSource]; // source, categorical function that sets any feature with a "fill" attribute value of true to red // color and anything without to green MLNFillStyleLayer *fillStyleLayer = [[MLNFillStyleLayer alloc] initWithIdentifier:@"fill-layer" source:shapeSource]; fillStyleLayer.fillColor = [NSExpression mgl_expressionForConditional:[NSPredicate predicateWithFormat:@"fill == YES"] trueExpression:[NSExpression expressionForConstantValue:[UIColor greenColor]] falseExpresssion:[NSExpression expressionForConstantValue:[UIColor redColor]]]; // source, identity function that sets any feature with an "opacity" attribute to use that value // and anything without to 1.0 fillStyleLayer.fillOpacity = [NSExpression mgl_expressionForConditional:[NSPredicate predicateWithFormat:@"opacity != nil"] trueExpression:[NSExpression expressionForKeyPath:@"opacity"] falseExpresssion:[NSExpression expressionForConstantValue:@1.0]]; [self.mapView.style addLayer:fillStyleLayer]; } - (void)addLatLonGrid { MLNComputedShapeSource *source = [[MLNComputedShapeSource alloc] initWithIdentifier:@"latlon" options:@{MLNShapeSourceOptionMaximumZoomLevel : @14}]; source.dataSource = self; [self.mapView.style addSource:source]; MLNLineStyleLayer *lineLayer = [[MLNLineStyleLayer alloc] initWithIdentifier:@"latlonlines" source:source]; [self.mapView.style addLayer:lineLayer]; MLNSymbolStyleLayer *labelLayer = [[MLNSymbolStyleLayer alloc] initWithIdentifier:@"latlonlabels" source:source]; labelLayer.text = [NSExpression expressionForKeyPath:@"value"]; [self.mapView.style addLayer:labelLayer]; } - (NSString *)bestLanguageForUser { // https://www.mapbox.com/vector-tiles/mapbox-streets-v8/#name-text--name_lang-code-text NSArray *supportedLanguages = @[ @"ar", @"de", @"en", @"es", @"fr", @"ja", @"ko", @"pt", @"ru", @"zh", @"zh-Hans", @"zh-Hant" ]; NSArray *preferredLanguages = [NSBundle preferredLocalizationsFromArray:supportedLanguages forPreferences:[NSLocale preferredLanguages]]; NSString *mostSpecificLanguage; for (NSString *language in preferredLanguages) { if (language.length > mostSpecificLanguage.length) { mostSpecificLanguage = language; } } return mostSpecificLanguage ?: @"en"; } - (IBAction)startWorldTour { _isTouringWorld = YES; [self.mapView removeAnnotations:self.mapView.annotations]; NSUInteger numberOfAnnotations = sizeof(WorldTourDestinations) / sizeof(WorldTourDestinations[0]); NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:numberOfAnnotations]; for (NSUInteger i = 0; i < numberOfAnnotations; i++) { MBXDroppedPinAnnotation *annotation = [[MBXDroppedPinAnnotation alloc] init]; annotation.coordinate = WorldTourDestinations[i]; [annotations addObject:annotation]; } [self.mapView addAnnotations:annotations]; [self continueWorldTourWithRemainingAnnotations:annotations]; } - (void)continueWorldTourWithRemainingAnnotations: (NSMutableArray *)annotations { MLNPointAnnotation *nextAnnotation = annotations.firstObject; if (!nextAnnotation || !_isTouringWorld) { _isTouringWorld = NO; return; } [annotations removeObjectAtIndex:0]; MLNMapCamera *camera = [MLNMapCamera cameraLookingAtCenterCoordinate:nextAnnotation.coordinate acrossDistance:10 pitch:arc4random_uniform(60) heading:arc4random_uniform(360)]; __weak MBXViewController *weakSelf = self; [self.mapView flyToCamera:camera completionHandler:^{ MBXViewController *strongSelf = weakSelf; [strongSelf performSelector:@selector(continueWorldTourWithRemainingAnnotations:) withObject:annotations afterDelay:2]; }]; } - (void)toggleCustomUserDot { _customUserLocationAnnnotationEnabled = !_customUserLocationAnnnotationEnabled; self.mapView.showsUserLocation = NO; self.mapView.userTrackingMode = MLNUserTrackingModeFollow; } - (void)testQueryPointAnnotations { NSNumber *visibleAnnotationCount = @(self.mapView.visibleAnnotations.count); NSString *message; if ([visibleAnnotationCount integerValue] == 1) { message = [NSString stringWithFormat:@"There is %@ visible annotation.", visibleAnnotationCount]; } else { message = [NSString stringWithFormat:@"There are %@ visible annotations.", visibleAnnotationCount]; } UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Visible Annotations" message:message preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleCancel handler:nil]]; [self presentViewController:alertController animated:YES completion:nil]; } - (id)randomOffscreenPointAnnotation { NSPredicate *pointAnnotationPredicate = [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary *_Nullable bindings) { return [evaluatedObject isKindOfClass:[MLNPointAnnotation class]]; }]; NSArray *annotations = [self.mapView.annotations filteredArrayUsingPredicate:pointAnnotationPredicate]; if (annotations.count == 0) { return nil; } NSArray *visibleAnnotations = [self.mapView.visibleAnnotations filteredArrayUsingPredicate:pointAnnotationPredicate]; if (visibleAnnotations.count == annotations.count) { return nil; } NSMutableArray *invisibleAnnotations = [annotations mutableCopy]; if (visibleAnnotations.count > 0) { [invisibleAnnotations removeObjectsInArray:visibleAnnotations]; } // Now pick a random offscreen annotation. uint32_t index = arc4random_uniform((uint32_t)invisibleAnnotations.count); return invisibleAnnotations[index]; } - (void)selectAnOffscreenPointAnnotation { id annotation = [self randomOffscreenPointAnnotation]; if (annotation) { [self.mapView selectAnnotation:annotation animated:YES completionHandler:nil]; NSAssert(self.mapView.selectedAnnotations.firstObject, @"The annotation was not selected"); } } - (void)centerSelectedAnnotation { id annotation = self.mapView.selectedAnnotations.firstObject; if (!annotation) return; CGPoint point = [self.mapView convertCoordinate:annotation.coordinate toPointToView:self.mapView]; // Animate, so that point becomes the the center CLLocationCoordinate2D center = [self.mapView convertPoint:point toCoordinateFromView:self.mapView]; [self.mapView setCenterCoordinate:center animated:YES]; } - (void)addVisibleAreaPolyline { CGRect constrainedRect = UIEdgeInsetsInsetRect(self.mapView.bounds, self.mapView.contentInset); CLLocationCoordinate2D lineCoords[5]; lineCoords[0] = [self.mapView convertPoint:CGPointMake(CGRectGetMinX(constrainedRect), CGRectGetMinY(constrainedRect)) toCoordinateFromView:self.mapView]; lineCoords[1] = [self.mapView convertPoint:CGPointMake(CGRectGetMaxX(constrainedRect), CGRectGetMinY(constrainedRect)) toCoordinateFromView:self.mapView]; lineCoords[2] = [self.mapView convertPoint:CGPointMake(CGRectGetMaxX(constrainedRect), CGRectGetMaxY(constrainedRect)) toCoordinateFromView:self.mapView]; lineCoords[3] = [self.mapView convertPoint:CGPointMake(CGRectGetMinX(constrainedRect), CGRectGetMaxY(constrainedRect)) toCoordinateFromView:self.mapView]; lineCoords[4] = lineCoords[0]; MLNPolyline *line = [MLNPolyline polylineWithCoordinates:lineCoords count:sizeof(lineCoords) / sizeof(lineCoords[0])]; [self.mapView addAnnotation:line]; } - (void)loadMissingIcon { self.mapView.centerCoordinate = CLLocationCoordinate2DMake(0, 0); self.mapView.zoomLevel = 1; NSURL *customStyleJSON = [[NSBundle mainBundle] URLForResource:@"missing_icon" withExtension:@"json"]; [self.mapView setStyleURL:customStyleJSON]; } - (UIImage *)mapView:(MLNMapView *)mapView didFailToLoadImage:(NSString *)imageName { UIImage *backupImage = [UIImage imageNamed:@"MissingImage"]; return backupImage; } - (void)flyToWithLatLngBoundsAndPadding { [self.mapView removeAnnotations:self.mapView.annotations]; CLLocationCoordinate2D sw = CLLocationCoordinate2DMake(48, 11); CLLocationCoordinate2D ne = CLLocationCoordinate2DMake(50, 12); UIEdgeInsets padding = UIEdgeInsetsMake(200, 200, 0, 0); MLNMapCamera *cameraWithoutPadding = [self.mapView cameraThatFitsCoordinateBounds:MLNCoordinateBoundsMake(sw, ne) edgePadding:padding]; MLNPointAnnotation *annotation = [MLNPointAnnotation new]; annotation.coordinate = cameraWithoutPadding.centerCoordinate; annotation.title = @"Bounds center"; [self.mapView addAnnotation:annotation]; __weak MBXViewController *weakSelf = self; [self.mapView flyToCamera:cameraWithoutPadding edgePadding:padding withDuration:5 completionHandler:^{ [weakSelf.mapView flyToCamera:cameraWithoutPadding edgePadding:UIEdgeInsetsZero withDuration:5 completionHandler:^{ }]; }]; } - (void)cycleTileLodMode { // TileLodMode::Default parameters static const double defaultRadius = self.mapView.tileLodMinRadius; static const double defaultScale = self.mapView.tileLodScale; static const double defaultPitchThreshold = self.mapView.tileLodPitchThreshold; _tileLodMode = static_cast((static_cast(_tileLodMode) + 1) % static_cast(MBXTileLodModeCount)); switch (_tileLodMode) { case MBXTileLodModeDefault: self.mapView.tileLodMinRadius = defaultRadius; self.mapView.tileLodScale = defaultScale; self.mapView.tileLodPitchThreshold = defaultPitchThreshold; break; case MBXTileLodModeNoLod: // When LOD is off we set a maximum PitchThreshold self.mapView.tileLodPitchThreshold = M_PI; break; case MBXTileLodModeReduced: self.mapView.tileLodMinRadius = 2; self.mapView.tileLodScale = 1.5; self.mapView.tileLodPitchThreshold = M_PI / 4; break; case MBXTileLodModeAggressive: self.mapView.tileLodMinRadius = 1; self.mapView.tileLodScale = 2; self.mapView.tileLodPitchThreshold = 0; break; default: break; } // update UI static UISlider *zoomSlider = nil; if (zoomSlider && _tileLodMode == MBXTileLodModeNoLod) { [zoomSlider removeFromSuperview]; zoomSlider = nil; } if (!zoomSlider && _tileLodMode != MBXTileLodModeNoLod) { const float zoomShiftRange = 5.0f; zoomSlider = [[UISlider alloc] init]; zoomSlider.minimumValue = -zoomShiftRange / 2.0f; zoomSlider.maximumValue = zoomShiftRange / 2.0f; zoomSlider.value = self.mapView.tileLodZoomShift; zoomSlider.continuous = NO; [zoomSlider addTarget:self action:@selector(updateTileLodZoom:) forControlEvents:UIControlEventValueChanged]; [self.view addSubview:zoomSlider]; zoomSlider.translatesAutoresizingMaskIntoConstraints = NO; zoomSlider.frame = CGRectMake(0, 0, 100, 100); NSArray *layout = @[ [NSLayoutConstraint constraintWithItem:zoomSlider attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0], [NSLayoutConstraint constraintWithItem:zoomSlider attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeWidth multiplier:0.5 constant:0], [NSLayoutConstraint constraintWithItem:zoomSlider attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1 constant:0], [zoomSlider.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor] ]; [self.view addConstraints:layout]; } [self.mapView triggerRepaint]; } - (void)updateTileLodZoom:(id)sender { UISlider *slider = (UISlider *)sender; self.mapView.tileLodZoomShift = slider.value; [self.mapView triggerRepaint]; NSLog(@"Tile LOD zoom shift: %f", self.mapView.tileLodZoomShift); } - (NSString *)getTileLodModeName { switch (_tileLodMode) { case MBXTileLodModeDefault: return @"Default"; case MBXTileLodModeNoLod: return @"Disabled"; case MBXTileLodModeReduced: return @"Reduced"; case MBXTileLodModeAggressive: return @"Aggressive"; default: return @""; } } // MARK: - Random World Tour - (void)addAnnotations:(NSInteger)numAnnotations aroundCoordinate:(CLLocationCoordinate2D)coordinate radius:(CLLocationDistance)radius { NSMutableArray *annotations = [[NSMutableArray alloc] initWithCapacity:numAnnotations]; for (NSInteger i = 0; i < numAnnotations; i++) { CLLocationDegrees heading = (CLLocationDegrees)arc4random_uniform(360); CLLocationDistance distance = (CLLocationDistance)arc4random_uniform(radius); CLLocationCoordinate2D newLocation = coordinateCentered(coordinate, heading, distance); MBXDroppedPinAnnotation *annotation = [[MBXDroppedPinAnnotation alloc] init]; annotation.coordinate = newLocation; [annotations addObject:annotation]; } [self.mapView addAnnotations:annotations]; } - (void)randomWorldTour { // Consistent initial conditions (consider setting these by test params) srand48(0); [self.mapView removeAnnotations:self.mapView.annotations]; [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(31, -100) zoomLevel:3 animated:NO]; [self randomWorldTourInternal]; } - (void)randomWorldTourInternal { self.randomWalk = YES; // Remove all annotations NSTimeInterval duration = 16.0; __weak MBXViewController *weakSelf = self; // Remove old annotations, half-way through the flight. NSArray *annotationsToRemove = [self.mapView.annotations copy]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * 0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [weakSelf.mapView removeAnnotations:annotationsToRemove]; }); MBXDroppedPinAnnotation *annotation = [[MBXDroppedPinAnnotation alloc] init]; annotation.coordinate = randomWorldCoordinate(); [self.mapView addAnnotation:annotation]; // Add annotations around that coord [self addAnnotations:50 aroundCoordinate:annotation.coordinate radius:100000]; // 100km MLNMapCamera *camera = [MLNMapCamera cameraLookingAtCenterCoordinate:annotation.coordinate altitude:10000.0 pitch:(CLLocationDegrees)arc4random_uniform(60) heading:(CLLocationDegrees)arc4random_uniform(360)]; [self.mapView flyToCamera:camera withDuration:duration peakAltitude:2000000.0 completionHandler:^{ // This completion handler is currently called BEFORE the // region did change delegate method, and we don't have a "reason" // so we can't tell if the motion was cancelled. We use the delegate // for that, and set self.randomWalk. But since we want a delay // anyway, we can just check later. Not ideal though.. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ MBXViewController *strongSelf = weakSelf; if (strongSelf.randomWalk) { [strongSelf randomWorldTourInternal]; } }); }]; } - (void)toggleSecondMapView { if ([self.view viewWithTag:2] == nil) { MLNMapView *secondMapView = [[MLNMapView alloc] initWithFrame:CGRectMake(0, self.view.bounds.size.height / 2, self.view.bounds.size.width, self.view.bounds.size.height / 2)]; secondMapView.showsScale = YES; secondMapView.translatesAutoresizingMaskIntoConstraints = NO; secondMapView.tag = 2; // Remove the main map bottom constraint for (NSLayoutConstraint *constraint in self.view.constraints) { if ((constraint.firstItem == self.mapView && constraint.firstAttribute == NSLayoutAttributeBottom) || (constraint.secondItem == self.mapView && constraint.secondAttribute == NSLayoutAttributeBottom)) { _firstMapLayout = constraint; [self.view removeConstraint:constraint]; break; } } // Place the second map in the bottom half of the view _secondMapLayout = @[ [NSLayoutConstraint constraintWithItem:self.mapView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1 constant:0], [NSLayoutConstraint constraintWithItem:secondMapView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0], [NSLayoutConstraint constraintWithItem:secondMapView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeWidth multiplier:1 constant:0], [NSLayoutConstraint constraintWithItem:secondMapView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1 constant:0], [secondMapView.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor] ]; [self.view addSubview:secondMapView]; [self.view addConstraints:_secondMapLayout]; secondMapView.styleURL = _mapView.styleURL; __weak decltype(_mapView) weakMapView = _mapView; __weak decltype(secondMapView) weakSecondMap = secondMapView; auto moveComplete = ^{ weakSecondMap.camera = [_mapView cameraByTiltingToPitch:weakMapView.camera.pitch]; }; // Navigate the new map to the same view as the main one [secondMapView setCenterCoordinate:_mapView.centerCoordinate zoomLevel:_mapView.zoomLevel direction:_mapView.direction animated:YES completionHandler:moveComplete]; secondMapView.accessibilityIdentifier = @"Second Map"; } else { MLNMapView *secondMapView = (MLNMapView *)[self.view viewWithTag:2]; // Reset the layout to the original state [self.view removeConstraints:_secondMapLayout]; [self.view addConstraint:_firstMapLayout]; _firstMapLayout = nil; _secondMapLayout = nil; [secondMapView removeFromSuperview]; } } // MARK: - User Actions - (void)handleSingleTap:(UITapGestureRecognizer *)singleTap { [self.navigationController setNavigationBarHidden:!self.navigationController.navigationBarHidden animated:YES]; // This is how you'd get the coordinate for the point where the user tapped: // CGPoint tapPoint = [singleTap locationInView:self.mapView]; // CLLocationCoordinate2D tapCoordinate = [self.mapView convertPoint:tapPoint // toCoordinateFromView:nil]; } - (IBAction)handleLongPress:(UILongPressGestureRecognizer *)longPress { if (longPress.state == UIGestureRecognizerStateBegan) { CGPoint point = [longPress locationInView:longPress.view]; NSArray *features = [self.mapView visibleFeaturesAtPoint:point]; NSString *title; for (id feature in features) { if (!title) { title = [feature attributeForKey:@"name_en"] ?: [feature attributeForKey:@"name"]; } } MBXDroppedPinAnnotation *pin = [[MBXDroppedPinAnnotation alloc] init]; pin.coordinate = [self.mapView convertPoint:point toCoordinateFromView:self.mapView]; pin.title = title ?: @"Dropped Pin"; pin.subtitle = [[[MLNCoordinateFormatter alloc] init] stringFromCoordinate:pin.coordinate]; // Calling `addAnnotation:` on mapView is required here (since `selectAnnotation:animated` has // the side effect of adding the annotation if required, but returning an incorrect callout // positioning rect) [self.mapView addAnnotation:pin]; [self.mapView selectAnnotation:pin animated:YES completionHandler:nil]; } } // MARK: - User defined Styles - (void)setStyles { self.styleNames = [NSMutableArray array]; self.styleURLs = [NSMutableArray array]; /// Style that does not require an `apiKey` nor any further configuration [self.styleNames addObject:@"OpenFreeMap Liberty"]; [self.styleURLs addObject:[NSURL URLWithString:@"https://tiles.openfreemap.org/styles/liberty"]]; [self.styleNames addObject:@"MapLibre Basic"]; [self.styleURLs addObject:[NSURL URLWithString:@"https://demotiles.maplibre.org/style.json"]]; /// This is hte same style as above but copied locally and the three instances of the metal /// plug-in layer added to the style Look for "type": "plugin-layer-metal-rendering" in the /// PluginLayerTestStyle.json for an example of how the layer is defined NSURL *url = [[NSBundle mainBundle] URLForResource:@"PluginLayerTestStyle.json" withExtension:nil]; if (url) { [self.styleNames addObject:@"MapLibre Basic - Local With Plugin"]; [self.styleURLs addObject:url]; } /// Add MapLibre Styles if an `apiKey` exists NSString *apiKey = [MLNSettings apiKey]; if (apiKey.length) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ for (MLNDefaultStyle *predefinedStyle in [MLNStyle predefinedStyles]) { [self.styleNames addObject:predefinedStyle.name]; [self.styleURLs addObject:predefinedStyle.url]; } }); } NSAssert(self.styleNames.count == self.styleURLs.count, @"Style names and URLs don’t match."); } - (IBAction)cycleStyles:(__unused id)sender { self.styleIndex = (self.styleIndex + 1) % self.styleNames.count; self.mapView.styleURL = self.styleURLs[self.styleIndex]; UIButton *titleButton = (UIButton *)self.navigationItem.titleView; [titleButton setTitle:self.styleNames[self.styleIndex] forState:UIControlStateNormal]; } - (IBAction)locateUser:(id)sender { [self nextTrackingMode:sender]; } - (void)nextTrackingMode:(id)sender { MLNUserTrackingMode nextMode; NSString *nextAccessibilityValue; switch (self.mapView.userTrackingMode) { case MLNUserTrackingModeNone: nextMode = MLNUserTrackingModeFollow; nextAccessibilityValue = @"Follow location"; break; case MLNUserTrackingModeFollow: nextMode = MLNUserTrackingModeFollowWithHeading; nextAccessibilityValue = @"Follow location and heading"; break; case MLNUserTrackingModeFollowWithHeading: nextMode = MLNUserTrackingModeFollowWithCourse; nextAccessibilityValue = @"Follow course"; break; case MLNUserTrackingModeFollowWithCourse: nextMode = MLNUserTrackingModeNone; nextAccessibilityValue = @"Off"; break; } self.mapView.userTrackingMode = nextMode; [sender setAccessibilityValue:nextAccessibilityValue]; } // MARK: - UIViewDelegate - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; if (_contentInsetsEnabled) { _contentInsetsEnabled = NO; _mapView.automaticallyAdjustsContentInset = YES; [self.mapView setContentInset:UIEdgeInsetsZero]; } while (self.contentInsetsOverlays && [self.contentInsetsOverlays count]) { [[self.contentInsetsOverlays lastObject] removeFromSuperview]; [self.contentInsetsOverlays removeLastObject]; } } // MARK: - MLNMapViewDelegate - (void)mapView:(MLNMapView *)mapView sourceDidChange:(MLNSource *)source { NSLog(@"A source was updated: %@", source.identifier); } - (void)mapView:(MLNMapView *)mapView shaderWillCompile:(NSInteger)id backend:(NSInteger)backend defines:(NSString *)defines { NSLog( @"A new shader is being compiled - shaderID:%ld, backend type:%ld, program configuration:%@", id, backend, defines); } - (void)mapView:(MLNMapView *)mapView shaderDidCompile:(NSInteger)id backend:(NSInteger)backend defines:(NSString *)defines { NSLog(@"A shader has been compiled - shaderID:%ld, backend type:%ld, program configuration:%@", id, backend, defines); } - (void)mapView:(MLNMapView *)mapView glyphsWillLoad:(NSArray *)fontStack range:(NSRange)range { NSLog(@"Glyphs are being requested for the font stack %@, ranging from %ld to %ld", fontStack, range.location, range.location + range.length); } - (void)mapView:(MLNMapView *)mapView glyphsDidLoad:(NSArray *)fontStack range:(NSRange)range { NSLog(@"Glyphs have been loaded for the font stack %@, ranging from %ld to %ld", fontStack, range.location, range.location + range.length); } - (void)mapView:(MLNMapView *)mapView tileDidTriggerAction:(MLNTileOperation)operation x:(NSInteger)x y:(NSInteger)y z:(NSInteger)z wrap:(NSInteger)wrap overscaledZ:(NSInteger)overscaledZ sourceID:(NSString *)sourceID { NSString *tileStr = [NSString stringWithFormat:@"(x: %ld, y: %ld, z: %ld, wrap: %ld, overscaledZ: %ld, sourceID: %@)", x, y, z, wrap, overscaledZ, sourceID]; switch (operation) { case MLNTileOperationRequestedFromCache: NSLog(@"Requesting tile %@ from cache", tileStr); break; case MLNTileOperationRequestedFromNetwork: NSLog(@"Requesting tile %@ from network", tileStr); break; case MLNTileOperationLoadFromCache: NSLog(@"Loading tile %@, requested from the cache", tileStr); break; case MLNTileOperationLoadFromNetwork: NSLog(@"Loading tile %@, requested from the network", tileStr); break; case MLNTileOperationStartParse: NSLog(@"Parsing tile %@", tileStr); break; case MLNTileOperationEndParse: NSLog(@"Completed parsing tile %@", tileStr); break; case MLNTileOperationError: NSLog(@"An error occured during proccessing for tile %@", tileStr); break; case MLNTileOperationCancelled: NSLog(@"Pending work cancelled on tile %@", tileStr); break; case MLNTileOperationNullOp: NSLog(@"An unknown tile operation was emitted for tile %@", tileStr); break; } } - (void)mapView:(MLNMapView *)mapView spriteWillLoad:(NSString *)id url:(NSString *)url { NSLog(@"The sprite %@ has been requested from %@", id, url); } - (void)mapView:(MLNMapView *)mapView spriteDidLoad:(NSString *)id url:(NSString *)url { NSLog(@"The sprite %@ has been loaded from %@", id, url); } - (MLNAnnotationView *)mapView:(MLNMapView *)mapView viewForAnnotation:(id)annotation { if (annotation == mapView.userLocation) { if (_customUserLocationAnnnotationEnabled) { MBXUserLocationAnnotationView *annotationView = [[MBXUserLocationAnnotationView alloc] initWithFrame:CGRectZero]; annotationView.frame = CGRectMake(0, 0, annotationView.intrinsicContentSize.width, annotationView.intrinsicContentSize.height); return annotationView; } return nil; } // Use GL backed pins for dropped pin annotations if ([annotation isKindOfClass:[MBXDroppedPinAnnotation class]] || [annotation isKindOfClass:[MBXSpriteBackedAnnotation class]]) { return nil; } MBXAnnotationView *annotationView = (MBXAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:MBXViewControllerAnnotationViewReuseIdentifer]; if (!annotationView) { annotationView = [[MBXAnnotationView alloc] initWithReuseIdentifier:MBXViewControllerAnnotationViewReuseIdentifer]; annotationView.frame = CGRectMake(0, 0, 10, 10); annotationView.backgroundColor = [UIColor whiteColor]; // Note that having two long press gesture recognizers on overlapping // views (`self.view` & `annotationView`) will cause weird behavior. // Comment out the pin dropping functionality in the handleLongPress: // method in this class to make draggable annotation views play nice. annotationView.draggable = YES; } else { // orange indicates that the annotation view was reused annotationView.backgroundColor = [UIColor orangeColor]; } return annotationView; } - (MLNAnnotationImage *)mapView:(MLNMapView *__nonnull)mapView imageForAnnotation:(id __nonnull)annotation { if ([annotation isKindOfClass:[MBXDroppedPinAnnotation class]] || [annotation isKindOfClass:[MBXCustomCalloutAnnotation class]]) { return nil; // use default marker } NSAssert([annotation isKindOfClass:[MBXSpriteBackedAnnotation class]], @"Annotations should be sprite-backed."); NSString *title = [(MLNPointAnnotation *)annotation title]; if (!title.length) return nil; NSString *lastTwoCharacters = [title substringFromIndex:title.length - 2]; MLNAnnotationImage *annotationImage = [mapView dequeueReusableAnnotationImageWithIdentifier:lastTwoCharacters]; if (!annotationImage) { UIColor *color; // make every tenth annotation blue if ([lastTwoCharacters hasSuffix:@"0"]) { color = [UIColor blueColor]; } else { color = [UIColor redColor]; } UIImage *image = [self imageWithText:lastTwoCharacters backgroundColor:color]; annotationImage = [MLNAnnotationImage annotationImageWithImage:image reuseIdentifier:lastTwoCharacters]; // don't allow touches on blue annotations if ([color isEqual:[UIColor blueColor]]) annotationImage.enabled = NO; } return annotationImage; } - (UIImage *)imageWithText:(NSString *)text backgroundColor:(UIColor *)color { CGRect rect = CGRectMake(0, 0, 20, 15); UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]); CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextSetFillColorWithColor(ctx, [[color colorWithAlphaComponent:0.75] CGColor]); CGContextFillRect(ctx, rect); CGContextSetStrokeColorWithColor(ctx, [[UIColor blackColor] CGColor]); CGContextStrokeRectWithWidth(ctx, rect, 2); NSAttributedString *drawString = [[NSAttributedString alloc] initWithString:text attributes:@{ NSFontAttributeName : [UIFont fontWithName:@"Arial-BoldMT" size:12], NSForegroundColorAttributeName : [UIColor whiteColor], }]; CGSize stringSize = drawString.size; CGRect stringRect = CGRectMake((rect.size.width - stringSize.width) / 2, (rect.size.height - stringSize.height) / 2, stringSize.width, stringSize.height); [drawString drawInRect:stringRect]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } - (BOOL)mapView:(__unused MLNMapView *)mapView annotationCanShowCallout:(__unused id)annotation { return YES; } - (CGFloat)mapView:(__unused MLNMapView *)mapView alphaForShapeAnnotation:(MLNShape *)annotation { return ([annotation isKindOfClass:[MLNPolygon class]] ? 0.5 : 1.0); } - (UIColor *)mapView:(__unused MLNMapView *)mapView strokeColorForShapeAnnotation:(MLNShape *)annotation { UIColor *color = [annotation isKindOfClass:[MLNPolyline class]] ? [UIColor greenColor] : [UIColor blackColor]; return [color colorWithAlphaComponent:0.9]; } - (UIColor *)mapView:(__unused MLNMapView *)mapView fillColorForPolygonAnnotation:(__unused MLNPolygon *)annotation { UIColor *color = annotation.pointCount > 3 ? [UIColor greenColor] : [UIColor redColor]; return [color colorWithAlphaComponent:0.5]; } - (void)mapView:(__unused MLNMapView *)mapView didChangeUserTrackingMode:(MLNUserTrackingMode)mode animated:(__unused BOOL)animated { UIImage *newButtonImage; NSString *newButtonTitle; switch (mode) { case MLNUserTrackingModeNone: newButtonImage = [UIImage imageNamed:@"TrackingLocationOffMask.png"]; break; case MLNUserTrackingModeFollow: newButtonImage = [UIImage imageNamed:@"TrackingLocationMask.png"]; break; case MLNUserTrackingModeFollowWithHeading: newButtonImage = [UIImage imageNamed:@"TrackingHeadingMask.png"]; break; case MLNUserTrackingModeFollowWithCourse: newButtonImage = nil; newButtonTitle = @"Course"; break; } self.navigationItem.rightBarButtonItem.title = newButtonTitle; [UIView animateWithDuration:0.25 animations:^{ self.navigationItem.rightBarButtonItem.image = newButtonImage; }]; } - (nullable id)mapView:(__unused MLNMapView *)mapView calloutViewForAnnotation:(id)annotation { if ([annotation respondsToSelector:@selector(title)] && [annotation isKindOfClass:[MBXCustomCalloutAnnotation class]]) { MBXCustomCalloutAnnotation *customAnnotation = (MBXCustomCalloutAnnotation *)annotation; MBXCustomCalloutView *calloutView = [[MBXCustomCalloutView alloc] init]; calloutView.representedObject = annotation; calloutView.anchoredToAnnotation = customAnnotation.anchoredToAnnotation; calloutView.dismissesAutomatically = customAnnotation.dismissesAutomatically; return calloutView; } return nil; } - (UIView *)mapView:(__unused MLNMapView *)mapView leftCalloutAccessoryViewForAnnotation:(__unused id)annotation { UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; button.frame = CGRectZero; [button setTitle:@"Left" forState:UIControlStateNormal]; [button sizeToFit]; return button; } - (UIView *)mapView:(__unused MLNMapView *)mapView rightCalloutAccessoryViewForAnnotation:(__unused id)annotation { UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; button.frame = CGRectZero; [button setTitle:@"Right" forState:UIControlStateNormal]; [button sizeToFit]; return button; } - (void)mapView:(MLNMapView *)mapView tapOnCalloutForAnnotation:(id)annotation { if (![annotation isKindOfClass:[MLNPointAnnotation class]]) { return; } MLNPointAnnotation *point = (MLNPointAnnotation *)annotation; point.coordinate = [self.mapView convertPoint:self.mapView.center toCoordinateFromView:self.mapView]; } - (void)mapView:(MLNMapView *)mapView didFinishLoadingStyle:(MLNStyle *)style { // Default MapLibre styles use {name_en} as their label language, which means // that a device with an English-language locale is already effectively // using locale-based country labels. _localizingLabels = [[self bestLanguageForUser] isEqualToString:@"en"]; } - (BOOL)mapView:(MLNMapView *)mapView shouldChangeFromCamera:(MLNMapCamera *)oldCamera toCamera:(MLNMapCamera *)newCamera { if (_shouldLimitCameraChanges) { // Get the current camera to restore it after. MLNMapCamera *currentCamera = mapView.camera; // From the new camera obtain the center to test if it’s inside the boundaries. CLLocationCoordinate2D newCameraCenter = newCamera.centerCoordinate; // Set the map’s visible bounds to newCamera. mapView.camera = newCamera; MLNCoordinateBounds newVisibleCoordinates = mapView.visibleCoordinateBounds; // Revert the camera. mapView.camera = currentCamera; // Test if the newCameraCenter and newVisibleCoordinates are inside Colorado. BOOL inside = MLNCoordinateInCoordinateBounds(newCameraCenter, colorado); BOOL intersects = MLNCoordinateInCoordinateBounds(newVisibleCoordinates.ne, colorado) && MLNCoordinateInCoordinateBounds(newVisibleCoordinates.sw, colorado); return inside && intersects; } else { return YES; } } - (void)mapViewRegionIsChanging:(MLNMapView *)mapView { [self updateHUD]; [self updateHelperMapViews]; } - (void)mapView:(MLNMapView *)mapView regionDidChangeWithReason:(MLNCameraChangeReason)reason animated:(BOOL)animated { if (reason != MLNCameraChangeReasonProgrammatic) { self.randomWalk = NO; } [self updateHUD]; [self updateHelperMapViews]; } - (void)mapView:(MLNMapView *)mapView didUpdateUserLocation:(MLNUserLocation *)userLocation { [self updateHUD]; } - (void)updateHelperMapViews { for (UIWindow *window in self.helperWindows) { MLNMapView *mapView = (MLNMapView *)window.rootViewController.view; mapView.camera = self.mapView.camera; } } - (void)updateHUD { if (self.reuseQueueStatsEnabled == NO && self.zoomLevelOrnamentEnabled == NO) { return; } NSString *hudString; if (self.reuseQueueStatsEnabled) { NSUInteger queuedAnnotations = 0; for (NSArray *queue in self.mapView.annotationViewReuseQueueByIdentifier.allValues) { queuedAnnotations += queue.count; } hudString = [NSString stringWithFormat:@"Visible: %ld Queued: %ld", (unsigned long)self.mapView.visibleAnnotations.count, (unsigned long)queuedAnnotations]; } else if (self.zoomLevelOrnamentEnabled) { hudString = [NSString stringWithFormat:@"%.f FPS (%.1fms) ∕ %.2f ∕ ↕\U0000FE0E%.f° ∕ %.f°", roundf(self.mapView.averageFrameRate), self.mapView.averageFrameTime, self.mapView.zoomLevel, self.mapView.camera.pitch, self.mapView.direction]; } [self.hudLabel setTitle:hudString forState:UIControlStateNormal]; } // MARK: - MLNComputedShapeSourceDataSource - (NSArray> *)featuresInCoordinateBounds:(MLNCoordinateBounds)bounds zoomLevel:(NSUInteger)zoom { double gridSpacing; if (zoom >= 13) { gridSpacing = 0.01; } else if (zoom >= 11) { gridSpacing = 0.05; } else if (zoom == 10) { gridSpacing = .1; } else if (zoom == 9) { gridSpacing = 0.25; } else if (zoom == 8) { gridSpacing = 0.5; } else if (zoom >= 6) { gridSpacing = 1; } else if (zoom == 5) { gridSpacing = 2; } else if (zoom >= 4) { gridSpacing = 5; } else if (zoom == 2) { gridSpacing = 10; } else { gridSpacing = 20; } NSMutableArray> *features = [NSMutableArray array]; CLLocationCoordinate2D coords[2]; for (double y = ceil(bounds.ne.latitude / gridSpacing) * gridSpacing; y >= floor(bounds.sw.latitude / gridSpacing) * gridSpacing; y -= gridSpacing) { coords[0] = CLLocationCoordinate2DMake(y, bounds.sw.longitude); coords[1] = CLLocationCoordinate2DMake(y, bounds.ne.longitude); MLNPolylineFeature *feature = [MLNPolylineFeature polylineWithCoordinates:coords count:2]; feature.attributes = @{@"value" : @(y)}; [features addObject:feature]; } for (double x = floor(bounds.sw.longitude / gridSpacing) * gridSpacing; x <= ceil(bounds.ne.longitude / gridSpacing) * gridSpacing; x += gridSpacing) { coords[0] = CLLocationCoordinate2DMake(bounds.sw.latitude, x); coords[1] = CLLocationCoordinate2DMake(bounds.ne.latitude, x); MLNPolylineFeature *feature = [MLNPolylineFeature polylineWithCoordinates:coords count:2]; feature.attributes = @{@"value" : @(x)}; [features addObject:feature]; } return features; } - (void)mapViewDidFinishRenderingFrame:(MLNMapView *)mapView fullyRendered:(BOOL)fullyRendered renderingStats:(nonnull MLNRenderingStats *)renderingStats { if (self.frameTimeGraphEnabled) { [self.frameTimeGraphView updatePathWithFrameDuration:renderingStats.encodingTime]; } } - (void)mapView:(nonnull MLNMapView *)mapView didChangeLocationManagerAuthorization:(nonnull id)manager { if (@available(iOS 14, *)) { #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 if (manager.authorizationStatus == kCLAuthorizationStatusDenied || manager.accuracyAuthorization == CLAccuracyAuthorizationReducedAccuracy) { [self alertAccuracyChanges]; } #endif } } - (void)alertAccuracyChanges { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"MapLibre works best with your precise location." message:@"You'll get turn-by-turn directions." preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *settingsAction = [UIAlertAction actionWithTitle:@"Turn On in Settings" style:UIAlertActionStyleDefault handler:^(UIAlertAction *_Nonnull action) { [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:^(BOOL success) { if (success) { NSLog(@"Opened url"); } }]; }]; UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"Keep Precise Location Off" style:UIAlertActionStyleDefault handler:nil]; [alert addAction:settingsAction]; [alert addAction:defaultAction]; [self presentViewController:alert animated:YES completion:nil]; } - (BOOL)isUITesting { NSString *value = NSProcessInfo.processInfo.environment[@"UITesting"]; return value && [value isEqual:@"YES"]; } - (void)saveCurrentMapState:(__unused NSNotification *)notification { // saved changes to the settings can break UI tests, so always start from the defaults when // testing if ([self isUITesting]) { return; } // The following properties can change after the view loads so we need to save their // state before exiting the view controller. self.currentState.camera = self.mapView.camera; self.currentState.showsUserLocation = self.mapView.showsUserLocation; self.currentState.userTrackingMode = self.mapView.userTrackingMode; self.currentState.showsUserHeadingIndicator = self.mapView.showsUserHeadingIndicator; self.currentState.showsMapScale = self.mapView.showsScale; self.currentState.showsZoomLevelOrnament = self.zoomLevelOrnamentEnabled; self.currentState.showsTimeFrameGraph = self.frameTimeGraphEnabled; self.currentState.debugMask = self.mapView.debugMask; self.currentState.reuseQueueStatsEnabled = self.reuseQueueStatsEnabled; [[MBXStateManager sharedManager] saveState:self.currentState]; } - (void)restoreMapState:(__unused NSNotification *)notification { if ([self isUITesting]) { return; } MBXState *currentState = [MBXStateManager sharedManager].currentState; self.mapView.camera = currentState.camera; self.mapView.showsUserLocation = currentState.showsUserLocation; self.mapView.userTrackingMode = currentState.userTrackingMode; self.mapView.showsUserHeadingIndicator = currentState.showsUserHeadingIndicator; self.mapView.showsScale = currentState.showsMapScale; self.zoomLevelOrnamentEnabled = currentState.showsZoomLevelOrnament; self.frameTimeGraphEnabled = currentState.showsTimeFrameGraph; self.mapView.debugMask = currentState.debugMask; self.reuseQueueStatsEnabled = currentState.reuseQueueStatsEnabled; self.currentState = currentState; } @end