Files
MapLibreParticleLab/platform/ios/app/MBXViewController.mm
tei 79c0ff89f1
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
feat: bring up particle renderer on ios metal
2026-09-17 09:27:35 +09:00

3248 lines
138 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 <mbgl/particle/field/field_binding.hpp>
#include <mbgl/map/map.hpp>
#include <mbgl/style/style.hpp>
#include <mbgl/style/layers/raster_particle_layer.hpp>
#include <mbgl/style/layers/weather_color_layer.hpp>
#include <mbgl/style/sources/raster_array_source.hpp>
#include <vector>
#include <memory>
#import "CustomStyleLayerExample.h"
#import "ExampleCustomDrawableStyleLayer.h"
#import <objc/runtime.h>
#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 () <UITableViewDelegate,
UITableViewDataSource,
MLNMapViewDelegate,
MLNComputedShapeSourceDataSource>
@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<UIWindow *> *helperWindows;
@property (nonatomic) NSMutableArray<UIView *> *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<NSLayoutConstraint *> *_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<NSString *> *)settingsSectionTitles {
return @[ @"Debug Tools", @"Annotations", @"Runtime Styling", @"Miscellaneous" ];
}
- (NSArray<NSString *> *)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 typeof(self) 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"];
};
const NSString *t0Path = fixturePath(@"T0");
const 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<mbgl::style::RasterArraySource>("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<const uint8_t *>(data.bytes);
return std::vector<uint8_t>(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<mbgl::style::WeatherColorLayer>("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<mbgl::style::RasterParticleLayer>("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<NSString *> *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<MLNPointAnnotation *> *)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<MLNAnnotation>)randomOffscreenPointAnnotation {
NSPredicate *pointAnnotationPredicate =
[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject,
NSDictionary<NSString *, id> *_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<MLNAnnotation> 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<MLNAnnotation> 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<MBXTileLodMode>((static_cast<int>(_tileLodMode) + 1) %
static_cast<int>(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<NSLayoutConstraint *> *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<MLNFeature> 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<UIViewControllerTransitionCoordinator>)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<NSString *> *)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<NSString *> *)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<MLNAnnotation>)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<MLNAnnotation> __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<MLNAnnotation>)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<MLNCalloutView>)mapView:(__unused MLNMapView *)mapView
calloutViewForAnnotation:(id<MLNAnnotation>)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<MLNAnnotation>)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<MLNAnnotation>)annotation {
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectZero;
[button setTitle:@"Right" forState:UIControlStateNormal];
[button sizeToFit];
return button;
}
- (void)mapView:(MLNMapView *)mapView tapOnCalloutForAnnotation:(id<MLNAnnotation>)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<id<MLNFeature>> *)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<id<MLNFeature>> *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<MLNLocationManager>)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