diff options
86 files changed, 4424 insertions, 582 deletions
@@ -56,6 +56,10 @@ ipackage-sim: Xcode/ios ; @JOBS=$(JOBS) ./scripts/ios/package.sh sim ipackage-no-bitcode: Xcode/ios ; @JOBS=$(JOBS) ./scripts/ios/package.sh no-bitcode iframework: ipackage-strip ; ./scripts/ios/framework.sh itest: ipackage-sim ; ./scripts/ios/test.sh + +.PHONY: xpackage xpackage-strip +xpackage: Xcode/osx ; @JOBS=$(JOBS) ./scripts/osx/package.sh +xpackage-strip: Xcode/osx ; @JOBS=$(JOBS) ./scripts/osx/package.sh strip endif #### All platforms targets ##################################################### @@ -128,10 +132,12 @@ ifeq ($(BUILD), osx) if [ $$CUSTOM_DD ]; then \ echo clearing files in $$CUSTOM_DD older than one day; \ find $$CUSTOM_DD/mapboxgl-app-* -mtime +1 | xargs rm -rf; \ + find $$CUSTOM_DD/osxapp-* -mtime +1 | xargs rm -rf; \ fi; \ if [ -d ~/Library/Developer/Xcode/DerivedData/ ] && [ ! $$CUSTOM_DD ]; then \ - echo 'clearing files in ~/Library/Developer/Xcode/DerivedData/mapboxgl-app-* older than one day'; \ + echo 'clearing files in ~/Library/Developer/Xcode/DerivedData/{mapboxgl-app,osxapp}-* older than one day'; \ find ~/Library/Developer/Xcode/DerivedData/mapboxgl-app-* -mtime +1 | xargs rm -rf; \ + find ~/Library/Developer/Xcode/DerivedData/osxapp-* -mtime +1 | xargs rm -rf; \ fi endif @@ -19,7 +19,7 @@ If you want to use products _based on_ Mapbox GL, check out: ## Targets * Ubuntu Linux -* OS X 10.9+ +* OS X 10.10+ * iOS 7.0+ * iPhone 4S and above (5, 5c, 5s, 6, 6 Plus) * iPad 2 and above (3, 4, Mini, Air, Mini 2, Air 2) diff --git a/docs/BUILD_OSX b/docs/BUILD_OSX new file mode 100644 index 0000000000..6a43025201 --- /dev/null +++ b/docs/BUILD_OSX @@ -0,0 +1,92 @@ +# Building Mapbox GL Native for iOS + +This section is for people contributing to Mapbox GL directly in the context of their own app. + +### Build + +1. Install [appledoc](http://appledoc.gentlebytes.com/appledoc/) for API docs generation. + + ``` + curl -L -o appledoc.zip https://github.com/tomaz/appledoc/releases/download/v2.2-963/appledoc.zip + unzip appledoc.zip + cp appledoc /usr/local/bin + cp -Rf Templates/ ~/.appledoc + ``` + +1. Run `make ipackage`. The packaging script will produce the statically-linked `libMapbox.a`, `Mapbox.bundle` for resources, a `Headers` folder, and a `Docs` folder with HTML API documentation. + +### Access Tokens + +_The demo applications use Mapbox vector tiles, which require a Mapbox account and API access token. Obtain an access token on the [Mapbox account page](https://www.mapbox.com/account/apps/)._ + +Set up the access token by editing the scheme for the application target, then adding an environment variable with the name `MAPBOX_ACCESS_TOKEN`. + +![edit scheme](https://cloud.githubusercontent.com/assets/98601/5460702/c4610262-8519-11e4-873a-8597821da468.png) + +![setting access token in Xcode scheme](https://cloud.githubusercontent.com/assets/162976/5349358/0a086f00-7f8c-11e4-8433-bdbaccda2b58.png) + +### Test + +In the context of your own app, you can now either: + +#### CocoaPods + +Currently, until [#1437](https://github.com/mapbox/mapbox-gl-native/issues/1437) is completed, to install a _development version_ of Mapbox GL using CocoaPods you will need to build it from source manually per above. + +1. Zip up the build product. + + ``` + cd build/ios/pkg/static + ZIP=mapbox-ios-sdk.zip + rm -f ../${ZIP} + zip -r ../${ZIP} * + ``` + +1. Modify a custom `Mapbox-iOS-SDK.podspec` to download this zip file. + + ```rb + {...} + + m.source = { + :http => "http://{...}/mapbox-ios-sdk.zip", + :flatten => true + } + + {...} + ``` + +1. Update your app's `Podfile` to point to the `Mapbox-iOS-SDK.podspec`. + + ```rb + pod 'Mapbox-iOS-SDK', :podspec => 'http://{...}/Mapbox-iOS-SDK.podspec' + ``` + +1. Run `pod update` to grab the newly-built library. + +#### Binary + +1. Built from source manually per above. + +1. Copy the contents of `build/ios/pkg/static` into your project. It should happen automatically, but ensure that: + + - `Headers` is in your *Header Search Paths* (`HEADER_SEARCH_PATHS`) build setting. + - `Mapbox.bundle` is in your target's *Copy Bundle Resources* build phase. + - `libMapbox.a` is in your target's *Link Binary With Libraries* build phase. + +1. Add the following Cocoa framework dependencies to your target's *Link Binary With Libraries* build phase: + + - `GLKit.framework` + - `ImageIO.framework` + - `MobileCoreServices.framework` + - `QuartzCore.framework` + - `SystemConfiguration.framework` + - `libc++.dylib` + - `libsqlite3.dylib` + - `libz.dylib` + - `CoreTelephony.framework` (optional, telemetry-only) + +1. Add `-ObjC` to your target's "Other Linker Flags" build setting (`OTHER_LDFLAGS`). + +## Troubleshooting + +On OS X, you can also try clearing the Xcode cache with `make clear_xcode_cache`. diff --git a/docs/BUILD_OSX.md b/docs/BUILD_OSX.md new file mode 100644 index 0000000000..5b4f6e6327 --- /dev/null +++ b/docs/BUILD_OSX.md @@ -0,0 +1,32 @@ +# Building Mapbox GL Native for OS X + +This project provides an OS X SDK analogous to the Mapbox iOS SDK. Mapbox does not officially support it to the same extent as the iOS SDK; however, bug reports and pull requests are certainly welcome. This document explains how to build the OS X SDK and integrate it into your own Cocoa application. + +### Build + +1. Run `make xpackage`. The packaging script will produce the statically-linked `libMapbox.a`, `Mapbox.bundle` for resources, and a `Headers` folder. + +### Install + +1. Copy the contents of `build/osx/pkg/static` into your project. It should happen automatically, but ensure that: + + - `Headers` is in your *Header Search Paths* (`HEADER_SEARCH_PATHS`) build setting. + - `Mapbox.bundle` is in your target's *Copy Bundle Resources* build phase. + - `libMapbox.a` is in your target's *Link Binary With Libraries* build phase. + +1. Add the following Cocoa framework dependencies to your target's *Link Binary With Libraries* build phase: + + - `SystemConfiguration.framework` + - `libc++.tbd` + - `libsqlite3.tbd` + - `libz.tbd` + +1. Add `-ObjC` to your target's "Other Linker Flags" build setting (`OTHER_LDFLAGS`). + +1. Mapbox vector tiles require a Mapbox account and API access token. In the project editor, select the application target. In the Info tab, set `MGLMapboxAccessToken` to your access token. You can obtain one from the [Mapbox account page](https://www.mapbox.com/account/apps/). + +1. In a XIB or storyboard, add a Custom View and set its custom class to `MGLMapView`. If you need to manipulate the map view programmatically, import the `Mapbox` module (Swift) or `Mapbox.h` umbrella header (Objective-C). + +## Troubleshooting + +You can also try clearing the Xcode cache with `make clear_xcode_cache`. diff --git a/docs/DEVELOP_IOS_OSX.md b/docs/DEVELOP_IOS_OSX.md index 183fd15705..9cc27bc581 100644 --- a/docs/DEVELOP_IOS_OSX.md +++ b/docs/DEVELOP_IOS_OSX.md @@ -14,7 +14,7 @@ Run make iproj -Which which will create and open an Xcode project which can build the entire library from source, as well as an Objective-C test app. +Which will create and open an Xcode project that can build the entire library from source, as well as an Objective-C test app. If you don't have an Apple Developer account, change the destination from "My Mac" to a simulator such as "iPhone 6" before you run and build the app. diff --git a/docs/DEVELOP_OSX.md b/docs/DEVELOP_OSX.md index 02d0865726..3ca96ebf24 100644 --- a/docs/DEVELOP_OSX.md +++ b/docs/DEVELOP_OSX.md @@ -2,7 +2,7 @@ To create projects, you can run: -- `make xproj`: Creates an Xcode project with OS X-specific handlers for HTTP downloads and settings storage. It uses [GLFW](http://www.glfw.org) for window handling. +- `make xproj`: Creates an Xcode project with a native OS X application for testing changes to mapbox-gl-native on the desktop. - `make lproj`: Creates an Xcode project with platform-independent handlers for downloads and settings storage. This is what is also being built on Linux. - `make osx run-osx`: Builds and runs the OS X application on the command line with `xcodebuild`. @@ -10,14 +10,8 @@ Note that you can't have more than one project in Xcode open at a time because t ### Access Tokens -_The demo applications use Mapbox vector tiles, which require a Mapbox account and API access token. Obtain an access token on the [Mapbox account page](https://www.mapbox.com/studio/account/tokens/)._ - -Set up the access token by editing the scheme for the application target, then adding an environment variable with the name `MAPBOX_ACCESS_TOKEN`. - -![edit scheme](https://cloud.githubusercontent.com/assets/98601/5460702/c4610262-8519-11e4-873a-8597821da468.png) - -![setting access token in Xcode scheme](https://cloud.githubusercontent.com/assets/162976/5349358/0a086f00-7f8c-11e4-8433-bdbaccda2b58.png) +The demo applications use Mapbox vector tiles, which require a Mapbox account and API access token. Obtain an access token on the [Mapbox account page](https://www.mapbox.com/studio/account/tokens/). You will be prompted for this access token the first time you launch the demo application. ### Usage -Keyboard shortcuts for testing functionality are logged to the console when the test app is started. +Through the OS X SDK, the demo application supports a variety of standard gestures and keyboard shortcuts. For more details, open Mapbox GL Help from the Help menu. diff --git a/gyp/common.gypi b/gyp/common.gypi index b3834934d3..6b7f2aea7e 100644 --- a/gyp/common.gypi +++ b/gyp/common.gypi @@ -24,7 +24,7 @@ ], 'GCC_WARN_PEDANTIC': 'YES', 'GCC_WARN_UNINITIALIZED_AUTOS': 'YES_AGGRESSIVE', - 'MACOSX_DEPLOYMENT_TARGET': '10.9', + 'MACOSX_DEPLOYMENT_TARGET': '10.10', }, }, { 'cflags_cc': [ diff --git a/gyp/osx.gyp b/gyp/osx.gyp index 8810144a80..2df9d4818d 100644 --- a/gyp/osx.gyp +++ b/gyp/osx.gyp @@ -1,6 +1,6 @@ { 'includes': [ - '../platform/osx/mapboxgl-app.gypi', + '../platform/osx/app/mapboxgl-app.gypi', '../platform/linux/mapboxgl-app.gypi', '../test/test.gypi', '../bin/render.gypi', diff --git a/gyp/platform-ios.gypi b/gyp/platform-ios.gypi index 5dae54ad90..070dd55c31 100644 --- a/gyp/platform-ios.gypi +++ b/gyp/platform-ios.gypi @@ -20,6 +20,29 @@ '../platform/darwin/image.mm', '../platform/darwin/nsthread.mm', '../platform/darwin/reachability.m', + '../platform/darwin/NSException+MGLAdditions.h', + '../platform/darwin/NSString+MGLAdditions.h', + '../platform/darwin/NSString+MGLAdditions.m', + '../include/mbgl/darwin/MGLTypes.h', + '../platform/darwin/MGLTypes.m', + '../include/mbgl/darwin/MGLStyle.h', + '../platform/darwin/MGLStyle.mm', + '../include/mbgl/darwin/MGLGeometry.h', + '../platform/darwin/MGLGeometry_Private.h', + '../platform/darwin/MGLGeometry.m', + '../include/mbgl/darwin/MGLAnnotation.h', + '../include/mbgl/darwin/MGLShape.h', + '../platform/darwin/MGLShape.m', + '../include/mbgl/darwin/MGLMultiPoint.h', + '../platform/darwin/MGLMultiPoint_Private.h', + '../platform/darwin/MGLMultiPoint.mm', + '../include/mbgl/darwin/MGLOverlay.h', + '../include/mbgl/darwin/MGLPointAnnotation.h', + '../platform/darwin/MGLPointAnnotation.m', + '../include/mbgl/darwin/MGLPolyline.h', + '../platform/darwin/MGLPolyline.mm', + '../include/mbgl/darwin/MGLPolygon.h', + '../platform/darwin/MGLPolygon.mm', '../include/mbgl/ios/Mapbox.h', '../platform/ios/MGLMapboxEvents.h', '../platform/ios/MGLMapboxEvents.m', @@ -31,41 +54,19 @@ '../include/mbgl/ios/MGLAccountManager.h', '../platform/ios/MGLAccountManager_Private.h', '../platform/ios/MGLAccountManager.m', - '../include/mbgl/ios/MGLAnnotation.h', '../include/mbgl/ios/MGLUserLocation.h', '../platform/ios/MGLUserLocation_Private.h', '../platform/ios/MGLUserLocation.m', '../platform/ios/MGLUserLocationAnnotationView.h', '../platform/ios/MGLUserLocationAnnotationView.m', - '../include/mbgl/ios/MGLTypes.h', - '../platform/ios/MGLTypes.m', - '../include/mbgl/ios/MGLGeometry.h', - '../platform/ios/MGLGeometry.m', - '../include/mbgl/ios/MGLMultiPoint.h', - '../platform/ios/MGLMultiPoint_Private.h', - '../platform/ios/MGLMultiPoint.mm', - '../include/mbgl/ios/MGLOverlay.h', - '../include/mbgl/ios/MGLPointAnnotation.h', - '../platform/ios/MGLPointAnnotation.m', - '../include/mbgl/ios/MGLPolyline.h', - '../platform/ios/MGLPolyline.m', - '../include/mbgl/ios/MGLPolygon.h', - '../platform/ios/MGLPolygon.m', - '../include/mbgl/ios/MGLShape.h', - '../platform/ios/MGLShape.m', '../include/mbgl/ios/MGLAnnotationImage.h', '../platform/ios/MGLAnnotationImage.m', - '../include/mbgl/ios/MGLStyle.h', - '../platform/ios/MGLStyle.mm', '../platform/ios/MGLCategoryLoader.h', '../platform/ios/MGLCategoryLoader.m', '../platform/ios/NSBundle+MGLAdditions.h', '../platform/ios/NSBundle+MGLAdditions.m', - '../platform/ios/NSException+MGLAdditions.h', '../platform/ios/NSProcessInfo+MGLAdditions.h', '../platform/ios/NSProcessInfo+MGLAdditions.m', - '../platform/ios/NSString+MGLAdditions.h', - '../platform/ios/NSString+MGLAdditions.m', '../platform/ios/vendor/SMCalloutView/SMCalloutView.h', '../platform/ios/vendor/SMCalloutView/SMCalloutView.m', '../platform/ios/vendor/Fabric/FABAttributes.h', @@ -96,6 +97,8 @@ }, 'include_dirs': [ + '../include/mbgl/ios', + '../include/mbgl/darwin', '../include', '../src', ], @@ -115,6 +118,8 @@ 'direct_dependent_settings': { 'include_dirs': [ + '../include/mbgl/ios', + '../include/mbgl/darwin', '../include', ], 'mac_bundle_resources': [ diff --git a/gyp/platform-osx.gypi b/gyp/platform-osx.gypi index 1ccbd3d59b..1462d6e364 100644 --- a/gyp/platform-osx.gypi +++ b/gyp/platform-osx.gypi @@ -19,26 +19,76 @@ '../platform/darwin/asset_root.mm', '../platform/darwin/image.mm', '../platform/darwin/nsthread.mm', + '../platform/darwin/reachability.m', + '../platform/darwin/NSException+MGLAdditions.h', + '../platform/darwin/NSString+MGLAdditions.h', + '../platform/darwin/NSString+MGLAdditions.m', + '../include/mbgl/darwin/MGLTypes.h', + '../platform/darwin/MGLTypes.m', + '../include/mbgl/darwin/MGLStyle.h', + '../platform/darwin/MGLStyle.mm', + '../include/mbgl/darwin/MGLGeometry.h', + '../platform/darwin/MGLGeometry_Private.h', + '../platform/darwin/MGLGeometry.m', + '../include/mbgl/darwin/MGLAnnotation.h', + '../include/mbgl/darwin/MGLShape.h', + '../platform/darwin/MGLShape.m', + '../include/mbgl/darwin/MGLMultiPoint.h', + '../platform/darwin/MGLMultiPoint_Private.h', + '../platform/darwin/MGLMultiPoint.mm', + '../include/mbgl/darwin/MGLOverlay.h', + '../include/mbgl/darwin/MGLPointAnnotation.h', + '../platform/darwin/MGLPointAnnotation.m', + '../include/mbgl/darwin/MGLPolyline.h', + '../platform/darwin/MGLPolyline.mm', + '../include/mbgl/darwin/MGLPolygon.h', + '../platform/darwin/MGLPolygon.mm', + '../include/mbgl/osx/Mapbox.h', + '../include/mbgl/osx/MGLAccountManager.h', + '../platform/osx/sdk/MGLAccountManager_Private.h', + '../platform/osx/sdk/MGLAccountManager.m', + '../include/mbgl/osx/MGLMapView.h', + '../platform/osx/sdk/MGLMapView_Private.h', + '../platform/osx/sdk/MGLMapView.mm', + '../include/mbgl/osx/MGLMapView+IBAdditions.h', + '../platform/osx/sdk/MGLMapView+IBAdditions.m', + '../include/mbgl/osx/MGLMapViewDelegate.h', + '../platform/osx/sdk/MGLOpenGLLayer.h', + '../platform/osx/sdk/MGLOpenGLLayer.mm', + '../platform/osx/sdk/MGLCompassCell.h', + '../platform/osx/sdk/MGLCompassCell.m', + '../platform/osx/sdk/MGLAttributionButton.h', + '../platform/osx/sdk/MGLAttributionButton.m', + '../include/mbgl/osx/MGLAnnotationImage.h', + '../platform/osx/sdk/MGLAnnotationImage.m', + '../platform/osx/sdk/NSBundle+MGLAdditions.h', + '../platform/osx/sdk/NSBundle+MGLAdditions.m', + '../platform/osx/sdk/NSProcessInfo+MGLAdditions.h', + '../platform/osx/sdk/NSProcessInfo+MGLAdditions.m', + '../platform/osx/sdk/resources/', ], 'variables': { 'cflags_cc': [ '<@(libuv_cflags)', '<@(boost_cflags)', + '<@(variant_cflags)', ], 'libraries': [ '<@(libuv_static_libs)', ], 'ldflags': [ - '-framework Foundation', - '-framework ImageIO', - '-framework CoreServices', + '-framework Cocoa', + '-framework CoreLocation', '-framework OpenGL', - '-framework ApplicationServices', + '-framework QuartzCore', + '-framework SystemConfiguration', ], }, 'include_dirs': [ + '../include/mbgl/osx', + '../include/mbgl/darwin', '../include', '../src', ], @@ -58,8 +108,13 @@ 'direct_dependent_settings': { 'include_dirs': [ + '../include/mbgl/osx', + '../include/mbgl/darwin', '../include', ], + 'mac_bundle_resources': [ + '<!@(find ../platform/osx/sdk/resources -type f \! -name "README")', + ], }, }, ], diff --git a/include/mbgl/ios/MGLAnnotation.h b/include/mbgl/darwin/MGLAnnotation.h index aef21ecbc6..aef21ecbc6 100644 --- a/include/mbgl/ios/MGLAnnotation.h +++ b/include/mbgl/darwin/MGLAnnotation.h diff --git a/include/mbgl/ios/MGLGeometry.h b/include/mbgl/darwin/MGLGeometry.h index a2ae9c991c..5337dca6b8 100644 --- a/include/mbgl/ios/MGLGeometry.h +++ b/include/mbgl/darwin/MGLGeometry.h @@ -83,14 +83,12 @@ NS_INLINE NSString *MGLStringFromCoordinateBounds(MGLCoordinateBounds bounds) { } /** Returns radians, converted from degrees. */ -NS_INLINE CGFloat MGLRadiansFromDegrees(CLLocationDegrees degrees) -{ +NS_INLINE CGFloat MGLRadiansFromDegrees(CLLocationDegrees degrees) { return (CGFloat)(degrees * M_PI) / 180; } /** Returns degrees, converted from radians. */ -NS_INLINE CLLocationDegrees MGLDegreesFromRadians(CGFloat radians) -{ +NS_INLINE CLLocationDegrees MGLDegreesFromRadians(CGFloat radians) { return radians * 180 / M_PI; } diff --git a/include/mbgl/ios/MGLMultiPoint.h b/include/mbgl/darwin/MGLMultiPoint.h index 5c58016df4..5c58016df4 100644 --- a/include/mbgl/ios/MGLMultiPoint.h +++ b/include/mbgl/darwin/MGLMultiPoint.h diff --git a/include/mbgl/ios/MGLOverlay.h b/include/mbgl/darwin/MGLOverlay.h index ab827c0d22..ab827c0d22 100644 --- a/include/mbgl/ios/MGLOverlay.h +++ b/include/mbgl/darwin/MGLOverlay.h diff --git a/include/mbgl/ios/MGLPointAnnotation.h b/include/mbgl/darwin/MGLPointAnnotation.h index d186cbff18..d186cbff18 100644 --- a/include/mbgl/ios/MGLPointAnnotation.h +++ b/include/mbgl/darwin/MGLPointAnnotation.h diff --git a/include/mbgl/ios/MGLPolygon.h b/include/mbgl/darwin/MGLPolygon.h index a19b984f1c..a19b984f1c 100644 --- a/include/mbgl/ios/MGLPolygon.h +++ b/include/mbgl/darwin/MGLPolygon.h diff --git a/include/mbgl/ios/MGLPolyline.h b/include/mbgl/darwin/MGLPolyline.h index c849bc9876..c849bc9876 100644 --- a/include/mbgl/ios/MGLPolyline.h +++ b/include/mbgl/darwin/MGLPolyline.h diff --git a/include/mbgl/ios/MGLShape.h b/include/mbgl/darwin/MGLShape.h index f54d4bfb74..f54d4bfb74 100644 --- a/include/mbgl/ios/MGLShape.h +++ b/include/mbgl/darwin/MGLShape.h diff --git a/include/mbgl/ios/MGLStyle.h b/include/mbgl/darwin/MGLStyle.h index 88cb8aaa83..88cb8aaa83 100644 --- a/include/mbgl/ios/MGLStyle.h +++ b/include/mbgl/darwin/MGLStyle.h diff --git a/include/mbgl/darwin/MGLTypes.h b/include/mbgl/darwin/MGLTypes.h new file mode 100644 index 0000000000..b86ecc35e6 --- /dev/null +++ b/include/mbgl/darwin/MGLTypes.h @@ -0,0 +1,52 @@ +#import <Foundation/Foundation.h> + +#pragma once + +#if !__has_feature(nullability) + #define NS_ASSUME_NONNULL_BEGIN + #define NS_ASSUME_NONNULL_END + #define nullable + #define nonnull + #define null_resettable +#endif + +NS_ASSUME_NONNULL_BEGIN + +extern NSString * const MGLErrorDomain; + +/** The mode used to track the user location on the map. */ +typedef NS_ENUM(NSUInteger, MGLUserTrackingMode) { + /** The map does not follow the user location. */ + MGLUserTrackingModeNone = 0, + /** The map follows the user location. */ + MGLUserTrackingModeFollow, + /** The map follows the user location and rotates when the heading changes. */ + MGLUserTrackingModeFollowWithHeading, + /** The map follows the user location and rotates when the course changes. */ + MGLUserTrackingModeFollowWithCourse, +}; + +NS_ASSUME_NONNULL_END + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wvariadic-macros" + #ifndef NS_ARRAY_OF + // Foundation collection classes adopted lightweight generics in iOS 9.0 and OS X 10.11 SDKs. + #if __has_feature(objc_generics) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 || __MAC_OS_X_VERSION_MAX_ALLOWED >= 101100) + /** Inserts a type specifier for a pointer to a lightweight generic with the given collection and object classes. Use a `*` for any non-`id` object classes but no `*` for the collection class. */ + #define NS_ARRAY_OF(ObjectClass...) NSArray <ObjectClass> + #define NS_MUTABLE_ARRAY_OF(ObjectClass...) NSMutableArray <ObjectClass> + #define NS_SET_OF(ObjectClass...) NSSet <ObjectClass> + #define NS_MUTABLE_SET_OF(ObjectClass...) NSMutableSet <ObjectClass> + #define NS_DICTIONARY_OF(ObjectClass...) NSDictionary <ObjectClass> + #define NS_MUTABLE_DICTIONARY_OF(ObjectClass...) NSMutableDictionary <ObjectClass> + #else + #define NS_ARRAY_OF(ObjectClass...) NSArray + #define NS_MUTABLE_ARRAY_OF(ObjectClass...) NSMutableArray + #define NS_SET_OF(ObjectClass...) NSSet + #define NS_MUTABLE_SET_OF(ObjectClass...) NSMutableSet + #define NS_DICTIONARY_OF(ObjectClass...) NSDictionary + #define NS_MUTABLE_DICTIONARY_OF(ObjectClass...) NSMutableDictionary + #endif + #endif +#pragma clang diagnostic pop diff --git a/include/mbgl/ios/MGLTypes.h b/include/mbgl/ios/MGLTypes.h deleted file mode 100644 index f36fc3f44e..0000000000 --- a/include/mbgl/ios/MGLTypes.h +++ /dev/null @@ -1,49 +0,0 @@ -#import <Foundation/Foundation.h> - -#pragma once - -#if !__has_feature(nullability) - #define NS_ASSUME_NONNULL_BEGIN - #define NS_ASSUME_NONNULL_END - #define nullable - #define nonnull - #define null_resettable -#endif - -NS_ASSUME_NONNULL_BEGIN - -extern NSString * const MGLErrorDomain; - -/** The mode used to track the user location on the map. */ -typedef NS_ENUM(NSUInteger, MGLUserTrackingMode) { - /** The map does not follow the user location. */ - MGLUserTrackingModeNone = 0, - /** The map follows the user location. */ - MGLUserTrackingModeFollow, - /** The map follows the user location and rotates when the heading changes. */ - MGLUserTrackingModeFollowWithHeading, - /** The map follows the user location and rotates when the course changes. */ - MGLUserTrackingModeFollowWithCourse, -}; - -NS_ASSUME_NONNULL_END - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wvariadic-macros" - #if __has_feature(objc_generics) - /** Inserts a type specifier for a pointer to a lightweight generic with the given collection and object classes. Use a `*` for any non-`id` object classes but no `*` for the collection class. */ - #define NS_ARRAY_OF(ObjectClass...) NSArray <ObjectClass> - #define NS_MUTABLE_ARRAY_OF(ObjectClass...) NSMutableArray <ObjectClass> - #define NS_SET_OF(ObjectClass...) NSSet <ObjectClass> - #define NS_MUTABLE_SET_OF(ObjectClass...) NSMutableSet <ObjectClass> - #define NS_DICTIONARY_OF(ObjectClass...) NSDictionary <ObjectClass> - #define NS_MUTABLE_DICTIONARY_OF(ObjectClass...) NSMutableDictionary <ObjectClass> - #else - #define NS_ARRAY_OF(ObjectClass...) NSArray - #define NS_MUTABLE_ARRAY_OF(ObjectClass...) NSMutableArray - #define NS_SET_OF(ObjectClass...) NSSet - #define NS_MUTABLE_SET_OF(ObjectClass...) NSMutableSet - #define NS_DICTIONARY_OF(ObjectClass...) NSDictionary - #define NS_MUTABLE_DICTIONARY_OF(ObjectClass...) NSMutableDictionary - #endif -#pragma clang diagnostic pop diff --git a/include/mbgl/osx/MGLAccountManager.h b/include/mbgl/osx/MGLAccountManager.h new file mode 100644 index 0000000000..c185f29b2e --- /dev/null +++ b/include/mbgl/osx/MGLAccountManager.h @@ -0,0 +1,26 @@ +#import <Foundation/Foundation.h> + +#import "MGLTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +/** The MGLAccountManager object provides a global way to set a Mapbox API access token, as well as other settings used framework-wide. */ +@interface MGLAccountManager : NSObject + +/** @name Authorizing Access */ + +/** Set the Mapbox API access token for the framework. +* +* You can set an access token on MGLAccountManager or on an individual map view. The same token is used throughout the framework. +* @param accessToken The Mapbox API access token. */ ++ (void)setAccessToken:(nullable NSString *)accessToken; + +/** Retreive the Mapbox API access token for the framework. +* +* You can set an access token on MGLAccountManager or on an individual map view. The same token is used throughout the framework. +* @return accessToken The Mapbox API access token. */ ++ (nullable NSString *)accessToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/include/mbgl/osx/MGLAnnotationImage.h b/include/mbgl/osx/MGLAnnotationImage.h new file mode 100644 index 0000000000..e42e062022 --- /dev/null +++ b/include/mbgl/osx/MGLAnnotationImage.h @@ -0,0 +1,17 @@ +#import <AppKit/AppKit.h> + +#import "MGLTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MGLAnnotationImage : NSObject + ++ (instancetype)annotationImageWithImage:(NSImage *)image reuseIdentifier:(NSString *)reuseIdentifier; + +@property (nonatomic, readonly) NSImage *image; +@property (nonatomic, readonly) NSString *reuseIdentifier; +@property (nonatomic, getter=isSelectable) BOOL selectable; + +@end + +NS_ASSUME_NONNULL_END diff --git a/include/mbgl/osx/MGLMapView+IBAdditions.h b/include/mbgl/osx/MGLMapView+IBAdditions.h new file mode 100644 index 0000000000..8dacd5ec7b --- /dev/null +++ b/include/mbgl/osx/MGLMapView+IBAdditions.h @@ -0,0 +1,44 @@ +#import <Foundation/Foundation.h> + +#import "MGLMapView.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MGLMapView (IBAdditions) + +// Core properties that can be manipulated in the Attributes inspector in +// Interface Builder. These redeclarations merely add the IBInspectable keyword. +// They appear here to ensure that they appear above the convenience properties; +// inspectables declared in MGLMapView.h are always sorted before those in +// MGLMapView+IBAdditions.h, due to ASCII sort order. + +#if TARGET_INTERFACE_BUILDER +// We want this property to look like a URL bar in the Attributes inspector, but +// just calling it styleURL would violate Cocoa naming conventions and conflict +// with the existing NSURL property. Fortunately, IB strips out the two +// underscores for display. +@property (nonatomic, nullable) IBInspectable NSString *styleURL__; +#endif + +// Convenience properties related to the initial viewport. These properties +// are not meant to be used outside of Interface Builder. latitude and longitude +// are backed by properties of type CLLocationDegrees, but these declarations +// must use the type double because Interface Builder is unaware that +// CLLocationDegrees is a typedef for double. + +@property (nonatomic) IBInspectable double latitude; +@property (nonatomic) IBInspectable double longitude; +@property (nonatomic) IBInspectable double zoomLevel; + +// Renamed properties. Interface Builder derives the display name of each +// inspectable from the runtime name, but runtime names don’t always make sense +// in UI. + +@property (nonatomic) IBInspectable BOOL allowsZooming; +@property (nonatomic) IBInspectable BOOL allowsScrolling; +@property (nonatomic) IBInspectable BOOL allowsRotating; +@property (nonatomic) IBInspectable BOOL allowsTilting; + +@end + +NS_ASSUME_NONNULL_END diff --git a/include/mbgl/osx/MGLMapView.h b/include/mbgl/osx/MGLMapView.h new file mode 100644 index 0000000000..27c67bd38e --- /dev/null +++ b/include/mbgl/osx/MGLMapView.h @@ -0,0 +1,85 @@ +#import <Cocoa/Cocoa.h> +#import <CoreLocation/CoreLocation.h> + +#import "MGLGeometry.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_OPTIONS(NSUInteger, MGLMapDebugMaskOptions) { + MGLMapDebugTileBoundariesMask = 1 << 1, + MGLMapDebugTileInfoMask = 1 << 2, + MGLMapDebugTimestampsMask = 1 << 3, + MGLMapDebugCollisionBoxesMask = 1 << 4, +}; + +@class MGLAnnotationImage; + +@protocol MGLAnnotation; +@protocol MGLMapViewDelegate; +@protocol MGLOverlay; + +IB_DESIGNABLE +@interface MGLMapView : NSView + +- (instancetype)initWithFrame:(CGRect)frame styleURL:(nullable NSURL *)styleURL; + +@property (nonatomic, weak, nullable) IBOutlet id <MGLMapViewDelegate> delegate; + +@property (nonatomic, null_resettable) NSURL *styleURL; + +- (IBAction)reloadStyle:(id)sender; + +@property (nonatomic, readonly) NSSegmentedControl *zoomControls; +@property (nonatomic, readonly) NSSlider *compass; +@property (nonatomic, readonly) NSImageView *logoView; +@property (nonatomic, readonly) NSView *attributionView; + +@property (nonatomic) CLLocationCoordinate2D centerCoordinate; + +- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated; + +@property (nonatomic) double zoomLevel; +@property (nonatomic, readonly) double maximumZoomLevel; +@property (nonatomic, readonly) double minimumZoomLevel; + +- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated; + +@property (nonatomic) CLLocationDirection direction; + +- (void)setDirection:(CLLocationDirection)direction animated:(BOOL)animated; + +@property (nonatomic) MGLCoordinateBounds visibleCoordinateBounds; + +@property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled; +@property (nonatomic, getter=isZoomEnabled) BOOL zoomEnabled; +@property (nonatomic, getter=isRotateEnabled) BOOL rotateEnabled; +@property (nonatomic, getter=isPitchEnabled) BOOL pitchEnabled; + +@property (nonatomic, readonly, nullable) NS_ARRAY_OF(id <MGLAnnotation>) *annotations; + +- (void)addAnnotation:(id <MGLAnnotation>)annotation; +- (void)addAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations; +- (void)removeAnnotation:(id <MGLAnnotation>)annotation; +- (void)removeAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations; + +- (nullable MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier; + +@property (nonatomic, copy) NS_ARRAY_OF(id <MGLAnnotation>) *selectedAnnotations; + +- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated; +- (void)deselectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated; + +- (void)addOverlay:(id <MGLOverlay>)overlay; +- (void)addOverlays:(NS_ARRAY_OF(id <MGLOverlay>) *)overlays; +- (void)removeOverlay:(id <MGLOverlay>)overlay; +- (void)removeOverlays:(NS_ARRAY_OF(id <MGLOverlay>) *)overlays; + +- (CLLocationCoordinate2D)convertPoint:(NSPoint)point toCoordinateFromView:(nullable NSView *)view; +- (NSPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate toPointToView:(nullable NSView *)view; +- (CLLocationDistance)metersPerPixelAtLatitude:(CLLocationDegrees)latitude; + +@property (nonatomic) MGLMapDebugMaskOptions debugMask; + +@end + +NS_ASSUME_NONNULL_END diff --git a/include/mbgl/osx/MGLMapViewDelegate.h b/include/mbgl/osx/MGLMapViewDelegate.h new file mode 100644 index 0000000000..c2de6ec52c --- /dev/null +++ b/include/mbgl/osx/MGLMapViewDelegate.h @@ -0,0 +1,42 @@ +#import <Foundation/Foundation.h> + +#import "MGLTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +@class MGLMapView; +@class MGLAnnotationImage; +@class MGLPolygon; +@class MGLPolyline; +@class MGLShape; + +@protocol MGLMapViewDelegate <NSObject> + +@optional + +- (void)mapView:(MGLMapView *)mapView regionWillChangeAnimated:(BOOL)animated; +- (void)mapViewRegionIsChanging:(MGLMapView *)mapView; +- (void)mapView:(MGLMapView *)mapView regionDidChangeAnimated:(BOOL)animated; + +- (void)mapViewWillStartLoadingMap:(MGLMapView *)mapView; +- (void)mapViewDidFinishLoadingMap:(MGLMapView *)mapView; + +- (void)mapViewWillStartRenderingMap:(MGLMapView *)mapView; +- (void)mapViewDidFinishRenderingMap:(MGLMapView *)mapView fullyRendered:(BOOL)fullyRendered; +- (void)mapViewWillStartRenderingFrame:(MGLMapView *)mapView; +- (void)mapViewDidFinishRenderingFrame:(MGLMapView *)mapView fullyRendered:(BOOL)fullyRendered; + +- (nullable MGLAnnotationImage *)mapView:(MGLMapView *)mapView imageForAnnotation:(id <MGLAnnotation>)annotation; +- (CGFloat)mapView:(MGLMapView *)mapView alphaForShapeAnnotation:(MGLShape *)annotation; +- (NSColor *)mapView:(MGLMapView *)mapView strokeColorForShapeAnnotation:(MGLShape *)annotation; +- (NSColor *)mapView:(MGLMapView *)mapView fillColorForPolygonAnnotation:(MGLPolygon *)annotation; +- (CGFloat)mapView:(MGLMapView *)mapView lineWidthForPolylineAnnotation:(MGLPolyline *)annotation; + +- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id <MGLAnnotation>)annotation; +- (nullable NSViewController *)mapView:(MGLMapView *)mapView calloutViewControllerForAnnotation:(id <MGLAnnotation>)annotation; +- (void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id <MGLAnnotation>)annotation; +- (void)mapView:(MGLMapView *)mapView didDeselectAnnotation:(id <MGLAnnotation>)annotation; + +@end + +NS_ASSUME_NONNULL_END diff --git a/include/mbgl/osx/Mapbox.h b/include/mbgl/osx/Mapbox.h new file mode 100644 index 0000000000..57d11bcaa9 --- /dev/null +++ b/include/mbgl/osx/Mapbox.h @@ -0,0 +1,15 @@ +#import "MGLAccountManager.h" +#import "MGLAnnotation.h" +#import "MGLAnnotationImage.h" +#import "MGLGeometry.h" +#import "MGLMapView.h" +#import "MGLMapView+IBAdditions.h" +#import "MGLMapViewDelegate.h" +#import "MGLMultiPoint.h" +#import "MGLOverlay.h" +#import "MGLPointAnnotation.h" +#import "MGLPolygon.h" +#import "MGLPolyline.h" +#import "MGLShape.h" +#import "MGLStyle.h" +#import "MGLTypes.h" diff --git a/ios/app/app-info.plist b/ios/app/app-info.plist index d497d09e76..309702d990 100644 --- a/ios/app/app-info.plist +++ b/ios/app/app-info.plist @@ -39,8 +39,10 @@ </dict> <key>NSHumanReadableCopyright</key> <string>(c) 2014 Mapbox</string> + <key>NSLocationAlwaysUsageDescription</key> + <string>The map will ALWAYS display the user's location.</string> <key>NSLocationWhenInUseUsageDescription</key> - <string>The map will display the user's location.</string> + <string>The map will display the user's location.</string> <key>UILaunchStoryboardName</key> <string>Default</string> <key>UIRequiredDeviceCapabilities</key> @@ -60,7 +62,5 @@ <string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeRight</string> </array> - <key>NSLocationAlwaysUsageDescription</key> - <string>The map will ALWAYS display the user's location.</string> </dict> </plist> diff --git a/platform/ios/MGLGeometry.m b/platform/darwin/MGLGeometry.m index 9eab5565fa..9eab5565fa 100644 --- a/platform/ios/MGLGeometry.m +++ b/platform/darwin/MGLGeometry.m diff --git a/platform/darwin/MGLGeometry_Private.h b/platform/darwin/MGLGeometry_Private.h new file mode 100644 index 0000000000..25f222e3ce --- /dev/null +++ b/platform/darwin/MGLGeometry_Private.h @@ -0,0 +1,26 @@ +#import "MGLGeometry.h" + +#include <mbgl/util/geo.hpp> + +NS_INLINE mbgl::LatLng MGLLatLngFromLocationCoordinate2D(CLLocationCoordinate2D coordinate) { + return mbgl::LatLng(coordinate.latitude, coordinate.longitude); +} + +NS_INLINE CLLocationCoordinate2D MGLLocationCoordinate2DFromLatLng(mbgl::LatLng latLng) { + return CLLocationCoordinate2DMake(latLng.latitude, latLng.longitude); +} + +NS_INLINE MGLCoordinateBounds MGLCoordinateBoundsFromLatLngBounds(mbgl::LatLngBounds latLngBounds) { + return MGLCoordinateBoundsMake(MGLLocationCoordinate2DFromLatLng(latLngBounds.sw), + MGLLocationCoordinate2DFromLatLng(latLngBounds.ne)); +} + +NS_INLINE mbgl::LatLngBounds MGLLatLngBoundsFromCoordinateBounds(MGLCoordinateBounds coordinateBounds) { + return mbgl::LatLngBounds(MGLLatLngFromLocationCoordinate2D(coordinateBounds.sw), + MGLLatLngFromLocationCoordinate2D(coordinateBounds.ne)); +} + +NS_INLINE BOOL MGLCoordinateInCoordinateBounds(CLLocationCoordinate2D coordinate, MGLCoordinateBounds coordinateBounds) { + mbgl::LatLngBounds bounds = MGLLatLngBoundsFromCoordinateBounds(coordinateBounds); + return bounds.contains(MGLLatLngFromLocationCoordinate2D(coordinate)); +} diff --git a/platform/ios/MGLMultiPoint.mm b/platform/darwin/MGLMultiPoint.mm index 702932f066..fd27cf7819 100644 --- a/platform/ios/MGLMultiPoint.mm +++ b/platform/darwin/MGLMultiPoint.mm @@ -1,8 +1,17 @@ -#import "MGLMultiPoint.h" -#import "MGLGeometry.h" +#import "MGLMultiPoint_Private.h" +#import "MGLGeometry_Private.h" #import <mbgl/util/geo.hpp> +mbgl::Color MGLColorObjectFromCGColorRef(CGColorRef cgColor) { + if (!cgColor) { + return {{ 0, 0, 0, 0 }}; + } + NSCAssert(CGColorGetNumberOfComponents(cgColor) >= 4, @"Color must have at least 4 components"); + const CGFloat *components = CGColorGetComponents(cgColor); + return {{ (float)components[0], (float)components[1], (float)components[2], (float)components[3] }}; +} + @implementation MGLMultiPoint { CLLocationCoordinate2D *_coords; @@ -100,4 +109,28 @@ return _bounds.intersects(area); } +- (void)addShapeAnnotationObjectToCollection:(std::vector<mbgl::ShapeAnnotation> &)shapes withDelegate:(id <MGLMultiPointDelegate>)delegate { + NSUInteger count = self.pointCount; + if (count == 0) { + return; + } + + CLLocationCoordinate2D *coordinates = (CLLocationCoordinate2D *)malloc(count * sizeof(CLLocationCoordinate2D)); + NSAssert(coordinates, @"Unable to allocate annotation with %lu points", (unsigned long)count); + [self getCoordinates:coordinates range:NSMakeRange(0, count)]; + + mbgl::AnnotationSegment segment; + segment.reserve(count); + for (NSUInteger i = 0; i < count; i++) { + segment.push_back(MGLLatLngFromLocationCoordinate2D(coordinates[i])); + } + free(coordinates); + shapes.emplace_back(mbgl::AnnotationSegments {{ segment }}, + [self shapeAnnotationPropertiesObjectWithDelegate:delegate]); +} + +- (mbgl::ShapeAnnotation::Properties)shapeAnnotationPropertiesObjectWithDelegate:(__unused id <MGLMultiPointDelegate>)delegate { + return mbgl::ShapeAnnotation::Properties(); +} + @end diff --git a/platform/darwin/MGLMultiPoint_Private.h b/platform/darwin/MGLMultiPoint_Private.h new file mode 100644 index 0000000000..e0d875d88a --- /dev/null +++ b/platform/darwin/MGLMultiPoint_Private.h @@ -0,0 +1,38 @@ +#import "MGLMultiPoint.h" + +#import "MGLGeometry.h" +#import "MGLTypes.h" + +#import <mbgl/annotation/shape_annotation.hpp> +#import <vector> + +#import <CoreGraphics/CoreGraphics.h> +#import <CoreLocation/CoreLocation.h> + +NS_ASSUME_NONNULL_BEGIN + +@class MGLPolygon; +@class MGLPolyline; + +@protocol MGLMultiPointDelegate; + +@interface MGLMultiPoint (Private) + +- (instancetype)initWithCoordinates:(CLLocationCoordinate2D *)coords count:(NSUInteger)count; +- (BOOL)intersectsOverlayBounds:(MGLCoordinateBounds)overlayBounds; + +- (void)addShapeAnnotationObjectToCollection:(std::vector<mbgl::ShapeAnnotation> &)shapes withDelegate:(id <MGLMultiPointDelegate>)delegate; +- (mbgl::ShapeAnnotation::Properties)shapeAnnotationPropertiesObjectWithDelegate:(id <MGLMultiPointDelegate>)delegate; + +@end + +@protocol MGLMultiPointDelegate <NSObject> + +- (double)alphaForShapeAnnotation:(MGLShape *)annotation; +- (mbgl::Color)strokeColorForShapeAnnotation:(MGLShape *)annotation; +- (mbgl::Color)fillColorForPolygonAnnotation:(MGLPolygon *)annotation; +- (CGFloat)lineWidthForPolylineAnnotation:(MGLPolyline *)annotation; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/ios/MGLPointAnnotation.m b/platform/darwin/MGLPointAnnotation.m index 13fbba1083..13fbba1083 100644 --- a/platform/ios/MGLPointAnnotation.m +++ b/platform/darwin/MGLPointAnnotation.m diff --git a/platform/darwin/MGLPolygon.mm b/platform/darwin/MGLPolygon.mm new file mode 100644 index 0000000000..5019385cb2 --- /dev/null +++ b/platform/darwin/MGLPolygon.mm @@ -0,0 +1,28 @@ +#import "MGLPolygon.h" + +#import "MGLMultiPoint_Private.h" + +@implementation MGLPolygon + +@dynamic overlayBounds; + ++ (instancetype)polygonWithCoordinates:(CLLocationCoordinate2D *)coords + count:(NSUInteger)count +{ + return [[self alloc] initWithCoordinates:coords count:count]; +} + +- (mbgl::ShapeAnnotation::Properties)shapeAnnotationPropertiesObjectWithDelegate:(id <MGLMultiPointDelegate>)delegate { + mbgl::ShapeAnnotation::Properties shapeProperties = [super shapeAnnotationPropertiesObjectWithDelegate:delegate]; + + mbgl::FillAnnotationProperties fillProperties; + fillProperties.opacity = [delegate alphaForShapeAnnotation:self]; + fillProperties.outlineColor = [delegate strokeColorForShapeAnnotation:self]; + fillProperties.color = [delegate fillColorForPolygonAnnotation:self]; + + shapeProperties.set<mbgl::FillAnnotationProperties>(fillProperties); + + return shapeProperties; +} + +@end diff --git a/platform/darwin/MGLPolyline.mm b/platform/darwin/MGLPolyline.mm new file mode 100644 index 0000000000..f560a571bc --- /dev/null +++ b/platform/darwin/MGLPolyline.mm @@ -0,0 +1,28 @@ +#import "MGLPolyline.h" + +#import "MGLMultiPoint_Private.h" + +@implementation MGLPolyline + +@dynamic overlayBounds; + ++ (instancetype)polylineWithCoordinates:(CLLocationCoordinate2D *)coords + count:(NSUInteger)count +{ + return [[self alloc] initWithCoordinates:coords count:count]; +} + +- (mbgl::ShapeAnnotation::Properties)shapeAnnotationPropertiesObjectWithDelegate:(id <MGLMultiPointDelegate>)delegate { + mbgl::ShapeAnnotation::Properties shapeProperties = [super shapeAnnotationPropertiesObjectWithDelegate:delegate]; + + mbgl::LineAnnotationProperties lineProperties; + lineProperties.opacity = [delegate alphaForShapeAnnotation:self]; + lineProperties.color = [delegate strokeColorForShapeAnnotation:self]; + lineProperties.width = [delegate lineWidthForPolylineAnnotation:self]; + + shapeProperties.set<mbgl::LineAnnotationProperties>(lineProperties); + + return shapeProperties; +} + +@end diff --git a/platform/ios/MGLShape.m b/platform/darwin/MGLShape.m index e3d92c38c8..e3d92c38c8 100644 --- a/platform/ios/MGLShape.m +++ b/platform/darwin/MGLShape.m diff --git a/platform/ios/MGLStyle.mm b/platform/darwin/MGLStyle.mm index 15a25db9e3..15a25db9e3 100644 --- a/platform/ios/MGLStyle.mm +++ b/platform/darwin/MGLStyle.mm diff --git a/platform/ios/MGLTypes.m b/platform/darwin/MGLTypes.m index 01e9a1467c..01e9a1467c 100644 --- a/platform/ios/MGLTypes.m +++ b/platform/darwin/MGLTypes.m diff --git a/platform/ios/NSException+MGLAdditions.h b/platform/darwin/NSException+MGLAdditions.h index f75b54c15c..f75b54c15c 100644 --- a/platform/ios/NSException+MGLAdditions.h +++ b/platform/darwin/NSException+MGLAdditions.h diff --git a/platform/ios/NSString+MGLAdditions.h b/platform/darwin/NSString+MGLAdditions.h index 6064f8b40f..6064f8b40f 100644 --- a/platform/ios/NSString+MGLAdditions.h +++ b/platform/darwin/NSString+MGLAdditions.h diff --git a/platform/ios/NSString+MGLAdditions.m b/platform/darwin/NSString+MGLAdditions.m index 284c63f5a9..b94a5f0198 100644 --- a/platform/ios/NSString+MGLAdditions.m +++ b/platform/darwin/NSString+MGLAdditions.m @@ -1,8 +1,8 @@ #import "NSString+MGLAdditions.h" -@implementation NSString (MGLAdditions) +void mgl_linkStringCategory() {} -void mgl_linkStringCategory(){} +@implementation NSString (MGLAdditions) - (nullable NSString *)mgl_stringOrNilIfEmpty { diff --git a/platform/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm index 092750cc16..63cca9210e 100644 --- a/platform/ios/MGLMapView.mm +++ b/platform/ios/MGLMapView.mm @@ -26,6 +26,8 @@ #include <mbgl/util/default_styles.hpp> #import "Mapbox.h" +#import "../darwin/MGLGeometry_Private.h" +#import "../darwin/MGLMultiPoint_Private.h" #import "NSBundle+MGLAdditions.h" #import "NSString+MGLAdditions.h" @@ -79,34 +81,6 @@ enum { MGLAnnotationTagNotFound = UINT32_MAX }; /// the annotation itself. typedef std::map<MGLAnnotationTag, MGLAnnotationContext> MGLAnnotationContextMap; -mbgl::LatLng MGLLatLngFromLocationCoordinate2D(CLLocationCoordinate2D coordinate) -{ - return mbgl::LatLng(coordinate.latitude, coordinate.longitude); -} - -CLLocationCoordinate2D MGLLocationCoordinate2DFromLatLng(mbgl::LatLng latLng) -{ - return CLLocationCoordinate2DMake(latLng.latitude, latLng.longitude); -} - -MGLCoordinateBounds MGLCoordinateBoundsFromLatLngBounds(mbgl::LatLngBounds latLngBounds) -{ - return MGLCoordinateBoundsMake(MGLLocationCoordinate2DFromLatLng(latLngBounds.sw), - MGLLocationCoordinate2DFromLatLng(latLngBounds.ne)); -} - -mbgl::LatLngBounds MGLLatLngBoundsFromCoordinateBounds(MGLCoordinateBounds coordinateBounds) -{ - return mbgl::LatLngBounds(MGLLatLngFromLocationCoordinate2D(coordinateBounds.sw), - MGLLatLngFromLocationCoordinate2D(coordinateBounds.ne)); -} - -BOOL MGLCoordinateInCoordinateBounds(CLLocationCoordinate2D coordinate, MGLCoordinateBounds coordinateBounds) -{ - mbgl::LatLngBounds bounds = MGLLatLngBoundsFromCoordinateBounds(coordinateBounds); - return bounds.contains(MGLLatLngFromLocationCoordinate2D(coordinate)); -} - mbgl::util::UnitBezier MGLUnitBezierForMediaTimingFunction(CAMediaTimingFunction *function) { if ( ! function) @@ -119,6 +93,17 @@ mbgl::util::UnitBezier MGLUnitBezierForMediaTimingFunction(CAMediaTimingFunction return { p1[0], p1[1], p2[0], p2[1] }; } +mbgl::Color MGLColorObjectFromUIColor(UIColor *color) +{ + if (!color) + { + return {{ 0, 0, 0, 0 }}; + } + CGFloat r, g, b, a; + [color getRed:&r green:&g blue:&b alpha:&a]; + return {{ (float)r, (float)g, (float)b, (float)a }}; +} + /// Lightweight container for metadata about an annotation, including the annotation itself. class MGLAnnotationContext { public: @@ -134,7 +119,8 @@ public: GLKViewDelegate, CLLocationManagerDelegate, UIActionSheetDelegate, - SMCalloutViewDelegate> + SMCalloutViewDelegate, + MGLMultiPointDelegate> @property (nonatomic) EAGLContext *context; @property (nonatomic) GLKView *glView; @@ -192,13 +178,18 @@ public: BOOL _needsDisplayRefresh; NSUInteger _changeDelimiterSuppressionDepth; + + BOOL _delegateHasAlphasForShapeAnnotations; + BOOL _delegateHasStrokeColorsForShapeAnnotations; + BOOL _delegateHasFillColorsForShapeAnnotations; + BOOL _delegateHasLineWidthsForShapeAnnotations; } #pragma mark - Setup & Teardown - @dynamic debugActive; -std::chrono::steady_clock::duration durationInSeconds(float duration) +std::chrono::steady_clock::duration MGLDurationInSeconds(float duration) { return std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<float, std::chrono::seconds::period>(duration)); } @@ -538,6 +529,16 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) @"Implement -[%@ mapView:imageForAnnotation:] instead.", NSStringFromClass([delegate class]), NSStringFromClass([delegate class])]; } + + _delegateHasAlphasForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:alphaForShapeAnnotation:)]; + _delegateHasStrokeColorsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:strokeColorForShapeAnnotation:)]; + _delegateHasFillColorsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:fillColorForPolygonAnnotation:)]; + _delegateHasLineWidthsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:lineWidthForPolylineAnnotation:)]; +} + +- (void)didReceiveMemoryWarning +{ + _mbglMap->onLowMemory(); } #pragma mark - Layout - @@ -949,7 +950,7 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) if (drift) { CGPoint offset = CGPointMake(velocity.x * duration / 4, velocity.y * duration / 4); - _mbglMap->moveBy({ offset.x, offset.y }, durationInSeconds(duration)); + _mbglMap->moveBy({ offset.x, offset.y }, MGLDurationInSeconds(duration)); } [self notifyGestureDidEndWithDrift:drift]; @@ -1031,7 +1032,7 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) { CGPoint pinchCenter = [pinch locationInView:pinch.view]; mbgl::PrecisionPoint center(pinchCenter.x, pinchCenter.y); - _mbglMap->setScale(newScale, center, durationInSeconds(duration)); + _mbglMap->setScale(newScale, center, MGLDurationInSeconds(duration)); } [self notifyGestureDidEndWithDrift:velocity]; @@ -1085,7 +1086,7 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) CGFloat newRadians = radians + velocity * duration * 0.1; CGFloat newDegrees = MGLDegreesFromRadians(newRadians) * -1; - _mbglMap->setBearing(newDegrees, durationInSeconds(duration)); + _mbglMap->setBearing(newDegrees, MGLDurationInSeconds(duration)); [self notifyGestureDidEndWithDrift:YES]; @@ -1178,7 +1179,7 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) } mbgl::PrecisionPoint center(zoomInPoint.x, zoomInPoint.y); - _mbglMap->scaleBy(2, center, durationInSeconds(MGLAnimationDuration)); + _mbglMap->scaleBy(2, center, MGLDurationInSeconds(MGLAnimationDuration)); __weak MGLMapView *weakSelf = self; @@ -1217,7 +1218,7 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) } mbgl::PrecisionPoint center(zoomOutPoint.x, zoomOutPoint.y); - _mbglMap->scaleBy(0.5, center, durationInSeconds(MGLAnimationDuration)); + _mbglMap->scaleBy(0.5, center, MGLDurationInSeconds(MGLAnimationDuration)); __weak MGLMapView *weakSelf = self; @@ -1538,7 +1539,7 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) } if (animated) { - options.duration = durationInSeconds(duration); + options.duration = MGLDurationInSeconds(duration); options.easing = MGLUnitBezierForMediaTimingFunction(nil); } if (completion) @@ -1655,7 +1656,7 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) } if (duration > 0) { - options.duration = durationInSeconds(duration); + options.duration = MGLDurationInSeconds(duration); options.easing = MGLUnitBezierForMediaTimingFunction(function); } if (completion) @@ -1702,7 +1703,7 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) CGFloat duration = animated ? MGLAnimationDuration : 0; - _mbglMap->setBearing(direction, durationInSeconds(duration)); + _mbglMap->setBearing(direction, MGLDurationInSeconds(duration)); } - (void)setDirection:(CLLocationDirection)direction @@ -1856,7 +1857,7 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) } if (duration > 0) { - options.duration = durationInSeconds(duration); + options.duration = MGLDurationInSeconds(duration); options.easing = MGLUnitBezierForMediaTimingFunction(function); } if (completion) @@ -2016,7 +2017,7 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) newAppliedClasses.insert(newAppliedClasses.end(), [appliedClass UTF8String]); } - _mbglMap->setDefaultTransitionDuration(durationInSeconds(transitionDuration)); + _mbglMap->setDefaultTransitionDuration(MGLDurationInSeconds(transitionDuration)); _mbglMap->setClasses(newAppliedClasses); } @@ -2111,10 +2112,6 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) std::vector<mbgl::ShapeAnnotation> shapes; BOOL delegateImplementsImageForPoint = [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)]; - BOOL delegateImplementsAlphaForShape = [self.delegate respondsToSelector:@selector(mapView:alphaForShapeAnnotation:)]; - BOOL delegateImplementsStrokeColorForShape = [self.delegate respondsToSelector:@selector(mapView:strokeColorForShapeAnnotation:)]; - BOOL delegateImplementsFillColorForPolygon = [self.delegate respondsToSelector:@selector(mapView:fillColorForPolygonAnnotation:)]; - BOOL delegateImplementsLineWidthForPolyline = [self.delegate respondsToSelector:@selector(mapView:lineWidthForPolylineAnnotation:)]; for (id <MGLAnnotation> annotation in annotations) { @@ -2122,77 +2119,7 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) if ([annotation isKindOfClass:[MGLMultiPoint class]]) { - NSUInteger count = [(MGLMultiPoint *)annotation pointCount]; - - if (count == 0) break; - - CGFloat alpha = (delegateImplementsAlphaForShape ? - [self.delegate mapView:self alphaForShapeAnnotation:annotation] : - 1.0); - - UIColor *strokeColor = (delegateImplementsStrokeColorForShape ? - [self.delegate mapView:self strokeColorForShapeAnnotation:annotation] : - [UIColor blackColor]); - - assert(strokeColor); - - CGFloat r,g,b,a; - [strokeColor getRed:&r green:&g blue:&b alpha:&a]; - mbgl::Color strokeNativeColor({{ (float)r, (float)g, (float)b, (float)a }}); - - mbgl::ShapeAnnotation::Properties shapeProperties; - - if ([annotation isKindOfClass:[MGLPolyline class]]) - { - CGFloat lineWidth = (delegateImplementsLineWidthForPolyline ? - [self.delegate mapView:self lineWidthForPolylineAnnotation:(MGLPolyline *)annotation] : - 3.0); - - mbgl::LineAnnotationProperties lineProperties; - lineProperties.opacity = alpha; - lineProperties.color = strokeNativeColor; - lineProperties.width = lineWidth; - shapeProperties.set<mbgl::LineAnnotationProperties>(lineProperties); - - } - else if ([annotation isKindOfClass:[MGLPolygon class]]) - { - UIColor *fillColor = (delegateImplementsFillColorForPolygon ? - [self.delegate mapView:self fillColorForPolygonAnnotation:(MGLPolygon *)annotation] : - [UIColor blueColor]); - - assert(fillColor); - - [fillColor getRed:&r green:&g blue:&b alpha:&a]; - mbgl::Color fillNativeColor({{ (float)r, (float)g, (float)b, (float)a }}); - - mbgl::FillAnnotationProperties fillProperties; - fillProperties.opacity = alpha; - fillProperties.outlineColor = strokeNativeColor; - fillProperties.color = fillNativeColor; - shapeProperties.set<mbgl::FillAnnotationProperties>(fillProperties); - } - else - { - [[NSException exceptionWithName:@"MGLUnknownShapeClassException" - reason:[NSString stringWithFormat:@"%@ is an unknown shape class", [annotation class]] - userInfo:nil] raise]; - } - - CLLocationCoordinate2D *coordinates = (CLLocationCoordinate2D *)malloc(count * sizeof(CLLocationCoordinate2D)); - [(MGLMultiPoint *)annotation getCoordinates:coordinates range:NSMakeRange(0, count)]; - - mbgl::AnnotationSegment segment; - segment.reserve(count); - - for (NSUInteger i = 0; i < count; i++) - { - segment.push_back(mbgl::LatLng(coordinates[i].latitude, coordinates[i].longitude)); - } - - free(coordinates); - - shapes.emplace_back(mbgl::AnnotationSegments {{ segment }}, shapeProperties); + [(MGLMultiPoint *)annotation addShapeAnnotationObjectToCollection:shapes withDelegate:self]; } else { @@ -2253,6 +2180,40 @@ std::chrono::steady_clock::duration durationInSeconds(float duration) [self didChangeValueForKey:@"annotations"]; } +- (double)alphaForShapeAnnotation:(MGLShape *)annotation +{ + if (_delegateHasAlphasForShapeAnnotations) + { + return [self.delegate mapView:self alphaForShapeAnnotation:annotation]; + } + return 1.0; +} + +- (mbgl::Color)strokeColorForShapeAnnotation:(MGLShape *)annotation +{ + UIColor *color = (_delegateHasStrokeColorsForShapeAnnotations + ? [self.delegate mapView:self strokeColorForShapeAnnotation:annotation] + : [UIColor blackColor]); + return MGLColorObjectFromUIColor(color); +} + +- (mbgl::Color)fillColorForPolygonAnnotation:(MGLPolygon *)annotation +{ + UIColor *color = (_delegateHasFillColorsForShapeAnnotations + ? [self.delegate mapView:self fillColorForPolygonAnnotation:annotation] + : [UIColor blueColor]); + return MGLColorObjectFromUIColor(color); +} + +- (CGFloat)lineWidthForPolylineAnnotation:(MGLPolyline *)annotation +{ + if (_delegateHasLineWidthsForShapeAnnotations) + { + return [self.delegate mapView:self lineWidthForPolylineAnnotation:(MGLPolyline *)annotation]; + } + return 3.0; +} + - (void)installAnnotationImage:(MGLAnnotationImage *)annotationImage { // retrieve pixels @@ -3612,9 +3573,4 @@ class MBGLView : public mbgl::View self.pitchEnabled = allowsTilting; } -- (void)didReceiveMemoryWarning -{ - _mbglMap->onLowMemory(); -} - @end diff --git a/platform/ios/MGLMultiPoint_Private.h b/platform/ios/MGLMultiPoint_Private.h deleted file mode 100644 index fe57064a90..0000000000 --- a/platform/ios/MGLMultiPoint_Private.h +++ /dev/null @@ -1,10 +0,0 @@ -#import "MGLMultiPoint.h" - -#import <CoreLocation/CoreLocation.h> - -@interface MGLMultiPoint (Private) - -- (instancetype)initWithCoordinates:(CLLocationCoordinate2D *)coords count:(NSUInteger)count; -- (BOOL)intersectsOverlayBounds:(MGLCoordinateBounds)overlayBounds; - -@end diff --git a/platform/ios/MGLPolygon.m b/platform/ios/MGLPolygon.m deleted file mode 100644 index 770a2a4b95..0000000000 --- a/platform/ios/MGLPolygon.m +++ /dev/null @@ -1,15 +0,0 @@ -#import "MGLPolygon.h" - -#import "MGLMultiPoint_Private.h" - -@implementation MGLPolygon - -@dynamic overlayBounds; - -+ (instancetype)polygonWithCoordinates:(CLLocationCoordinate2D *)coords - count:(NSUInteger)count -{ - return [[self alloc] initWithCoordinates:coords count:count]; -} - -@end diff --git a/platform/ios/MGLPolyline.m b/platform/ios/MGLPolyline.m deleted file mode 100644 index bcd652e0d3..0000000000 --- a/platform/ios/MGLPolyline.m +++ /dev/null @@ -1,15 +0,0 @@ -#import "MGLPolyline.h" - -#import "MGLMultiPoint_Private.h" - -@implementation MGLPolyline - -@dynamic overlayBounds; - -+ (instancetype)polylineWithCoordinates:(CLLocationCoordinate2D *)coords - count:(NSUInteger)count -{ - return [[self alloc] initWithCoordinates:coords count:count]; -} - -@end diff --git a/platform/osx/Info.plist b/platform/osx/Info.plist deleted file mode 100644 index 514db118ea..0000000000 --- a/platform/osx/Info.plist +++ /dev/null @@ -1,47 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> -<plist version="1.0"> -<dict> - <key>CFBundleDevelopmentRegion</key> - <string>English</string> - <key>CFBundleExecutable</key> - <string>${EXECUTABLE_NAME}</string> - <key>CFBundleGetInfoString</key> - <string>0.0.1</string> - <key>CFBundleIconFile</key> - <string>Icon.icns</string> - <key>CFBundleIdentifier</key> - <string>com.mapbox.${PRODUCT_NAME:rfc1034identifier}</string> - <key>CFBundleInfoDictionaryVersion</key> - <string>6.0</string> - <key>CFBundleLongVersionString</key> - <string>0.0.1</string> - <key>CFBundleName</key> - <string>${PRODUCT_NAME}</string> - <key>CFBundlePackageType</key> - <string>APPL</string> - <key>CFBundleShortVersionString</key> - <string>0.0.1</string> - <key>CFBundleSignature</key> - <string>MBGL</string> - <key>CFBundleVersion</key> - <string>0.0.1</string> - <key>CSResourcesFileMapped</key> - <true/> - <key>NSHumanReadableCopyright</key> - <string>0.0.1</string> - <key>NSHighResolutionCapable</key> - <true/> - <key>CFBundleURLTypes</key> - <array> - <dict> - <key>CFBundleURLName</key> - <string>${PRODUCT_NAME}</string> - <key>CFBundleURLSchemes</key> - <array> - <string>mapboxgl</string> - </array> - </dict> - </array> -</dict> -</plist>
\ No newline at end of file diff --git a/platform/osx/app/AppDelegate.h b/platform/osx/app/AppDelegate.h new file mode 100644 index 0000000000..69b6e0f13a --- /dev/null +++ b/platform/osx/app/AppDelegate.h @@ -0,0 +1,7 @@ +#import <Cocoa/Cocoa.h> + +@interface AppDelegate : NSObject <NSApplicationDelegate> + + +@end + diff --git a/platform/osx/app/AppDelegate.m b/platform/osx/app/AppDelegate.m new file mode 100644 index 0000000000..4c8ce92ab7 --- /dev/null +++ b/platform/osx/app/AppDelegate.m @@ -0,0 +1,462 @@ +#import "AppDelegate.h" + +#import "DroppedPinAnnotation.h" +#import "LocationCoordinate2DTransformer.h" +#import "NSValue+Additions.h" + +#import <mbgl/osx/Mapbox.h> + +static NSString * const MGLMapboxAccessTokenDefaultsKey = @"MGLMapboxAccessToken"; + +@interface AppDelegate () <NSApplicationDelegate, NSSharingServicePickerDelegate, NSMenuDelegate, MGLMapViewDelegate> + +@property (weak) IBOutlet NSWindow *window; +@property (weak) IBOutlet MGLMapView *mapView; +@property (weak) IBOutlet NSMenu *mapViewContextMenu; + +@property (weak) IBOutlet NSWindow *preferencesWindow; + +@end + +@implementation AppDelegate { + NSPoint _mouseLocationForMapViewContextMenu; +} + +#pragma mark Lifecycle + ++ (void)load { + // Set access token, unless MGLAccountManager already read it in from Info.plist. + if (![MGLAccountManager accessToken]) { + NSString *accessToken = [NSProcessInfo processInfo].environment[@"MAPBOX_ACCESS_TOKEN"]; + if (accessToken) { + // Store to preferences so that we can launch the app later on without having to specify + // token. + [[NSUserDefaults standardUserDefaults] setObject:accessToken forKey:MGLMapboxAccessTokenDefaultsKey]; + } else { + // Try to retrieve from preferences, maybe we've stored them there previously and can reuse + // the token. + accessToken = [[NSUserDefaults standardUserDefaults] stringForKey:MGLMapboxAccessTokenDefaultsKey]; + } + [MGLAccountManager setAccessToken:accessToken]; + } +} + +- (void)applicationWillFinishLaunching:(NSNotification *)notification { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(userDefaultsDidChange:) + name:NSUserDefaultsDidChangeNotification + object:nil]; + + [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self + andSelector:@selector(handleGetURLEvent:withReplyEvent:) + forEventClass:kInternetEventClass + andEventID:kAEGetURL]; +} + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + // Set access token, unless MGLAccountManager already read it in from Info.plist. + if (![MGLAccountManager accessToken]) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.messageText = @"Access token required"; + alert.informativeText = @"To load Mapbox-hosted tiles and styles, enter your Mapbox access token in Preferences."; + [alert addButtonWithTitle:@"Open Preferences"]; + [alert runModal]; + [self showPreferences:nil]; + } + + NSPressGestureRecognizer *pressGestureRecognizer = [[NSPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePressGesture:)]; + [self.mapView addGestureRecognizer:pressGestureRecognizer]; +} + +- (void)applicationWillTerminate:(NSNotification *)notification { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)userDefaultsDidChange:(NSNotification *)notification { + NSUserDefaults *userDefaults = notification.object; + NSString *accessToken = [userDefaults stringForKey:MGLMapboxAccessTokenDefaultsKey]; + if (![accessToken isEqualToString:[MGLAccountManager accessToken]]) { + [MGLAccountManager setAccessToken:accessToken]; + [self reload:self]; + } +} + +#pragma mark Services + +- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { + NSURL *url = [NSURL URLWithString:[event paramDescriptorForKeyword:keyDirectObject].stringValue]; + NS_MUTABLE_DICTIONARY_OF(NSString *, NSString *) *params = [[NSMutableDictionary alloc] init]; + for (NSString *param in [url.query componentsSeparatedByString:@"&"]) { + NSArray *parts = [param componentsSeparatedByString:@"="]; + if (parts.count >= 2) { + params[parts[0]] = [parts[1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + } + } + + NSString *centerString = params[@"center"]; + if (centerString) { + NSArray *coordinateValues = [centerString componentsSeparatedByString:@","]; + if (coordinateValues.count == 2) { + self.mapView.centerCoordinate = CLLocationCoordinate2DMake([coordinateValues[0] doubleValue], + [coordinateValues[1] doubleValue]); + } + } + + NSString *zoomLevelString = params[@"zoom"]; + if (zoomLevelString.length) { + self.mapView.zoomLevel = zoomLevelString.doubleValue; + } + + NSString *directionString = params[@"bearing"]; + if (directionString.length) { + self.mapView.direction = directionString.doubleValue; + } +} + +- (IBAction)showShareMenu:(id)sender { + NSSharingServicePicker *picker = [[NSSharingServicePicker alloc] initWithItems:@[self.shareURL]]; + picker.delegate = self; + [picker showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMinYEdge]; +} + +- (NSURL *)shareURL { + NSArray *components = self.mapView.styleURL.pathComponents; + CLLocationCoordinate2D centerCoordinate = self.mapView.centerCoordinate; + return [NSURL URLWithString: + [NSString stringWithFormat:@"https://api.mapbox.com/styles/v1/%@/%@.html?access_token=%@#%.2f/%.5f/%.5f/%.f", + components[1], components[2], [MGLAccountManager accessToken], + self.mapView.zoomLevel, centerCoordinate.latitude, centerCoordinate.longitude, self.mapView.direction]]; +} + +#pragma mark View methods + +- (IBAction)setStyle:(id)sender { + NSInteger tag; + if ([sender isKindOfClass:[NSMenuItem class]]) { + tag = [sender tag]; + } else if ([sender isKindOfClass:[NSPopUpButton class]]) { + tag = [sender selectedTag]; + } + NSURL *styleURL; + switch (tag) { + case 1: + styleURL = [MGLStyle streetsStyleURL]; + break; + case 2: + styleURL = [MGLStyle emeraldStyleURL]; + break; + case 3: + styleURL = [MGLStyle lightStyleURL]; + break; + case 4: + styleURL = [MGLStyle darkStyleURL]; + break; + case 5: + styleURL = [MGLStyle satelliteStyleURL]; + break; + case 6: + styleURL = [MGLStyle hybridStyleURL]; + break; + default: + NSAssert(NO, @"Cannot set style from control with tag %li", (long)tag); + break; + } + self.mapView.styleURL = styleURL; + [self.window.toolbar validateVisibleItems]; +} + +- (IBAction)chooseCustomStyle:(id)sender { + NSAlert *alert = [[NSAlert alloc] init]; + alert.messageText = @"Apply custom style"; + alert.informativeText = @"Enter the URL to a JSON file that conforms to the Mapbox GL style specification:"; + NSTextField *textField = [[NSTextField alloc] initWithFrame:NSZeroRect]; + [textField sizeToFit]; + NSRect textFieldFrame = textField.frame; + textFieldFrame.size.width = 300; + textField.frame = textFieldFrame; + NSString *savedURLString = [[NSUserDefaults standardUserDefaults] stringForKey:@"MBXCustomStyleURL"]; + if (savedURLString) { + textField.stringValue = savedURLString; + } + alert.accessoryView = textField; + [alert addButtonWithTitle:@"Apply"]; + [alert addButtonWithTitle:@"Cancel"]; + if ([alert runModal] == NSAlertFirstButtonReturn) { + [[NSUserDefaults standardUserDefaults] setObject:textField.stringValue forKey:@"MBXCustomStyleURL"]; + self.mapView.styleURL = [NSURL URLWithString:textField.stringValue]; + [self.window.toolbar validateVisibleItems]; + } +} + +- (IBAction)zoomIn:(id)sender { + [self.mapView setZoomLevel:self.mapView.zoomLevel + 1 animated:YES]; +} + +- (IBAction)zoomOut:(id)sender { + [self.mapView setZoomLevel:self.mapView.zoomLevel - 1 animated:YES]; +} + +- (IBAction)snapToNorth:(id)sender { + [self.mapView setDirection:0 animated:YES]; +} + +- (IBAction)reload:(id)sender { + [self.mapView reloadStyle:sender]; +} + +- (IBAction)toggleTileBoundaries:(id)sender { + self.mapView.debugMask ^= MGLMapDebugTileBoundariesMask; +} + +- (IBAction)toggleTileInfo:(id)sender { + self.mapView.debugMask ^= MGLMapDebugTileInfoMask; +} + +- (IBAction)toggleTileTimestamps:(id)sender { + self.mapView.debugMask ^= MGLMapDebugTimestampsMask; +} + +- (IBAction)toggleCollisionBoxes:(id)sender { + self.mapView.debugMask ^= MGLMapDebugCollisionBoxesMask; +} + +#pragma mark Help methods + +- (IBAction)showShortcuts:(id)sender { + NSAlert *alert = [[NSAlert alloc] init]; + alert.messageText = @"Mapbox GL Help"; + alert.informativeText = @"\ +• To scroll, swipe with two fingers, drag the cursor, or press the arrow keys.\n\ +• To zoom, pinch with two fingers, or hold down Shift while dragging the cursor up and down, or hold down Option while pressing the up and down arrow keys.\n\ +• To rotate, move two fingers opposite each other in a circle, or hold down Option while dragging the cursor left and right, or hold down Option while pressing the left and right arrow keys.\n\ +• To tilt, hold down Option while dragging the cursor up and down.\ +• To drop a pin, click and hold.\ +"; + [alert runModal]; +} + +- (IBAction)giveFeedback:(id)sender { + CLLocationCoordinate2D centerCoordinate = self.mapView.centerCoordinate; + NSURL *feedbackURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://www.mapbox.com/map-feedback/#/%.5f/%.5f/%.0f", + centerCoordinate.longitude, centerCoordinate.latitude, round(self.mapView.zoomLevel)]]; + [[NSWorkspace sharedWorkspace] openURL:feedbackURL]; +} + +- (IBAction)showPreferences:(id)sender { + [self.preferencesWindow makeKeyAndOrderFront:sender]; +} + +- (IBAction)openAccessTokenManager:(id)sender { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://www.mapbox.com/studio/account/tokens/"]]; +} + +#pragma mark Mouse events + +- (void)handlePressGesture:(NSPressGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer.state == NSGestureRecognizerStateBegan) { + NSPoint location = [gestureRecognizer locationInView:self.mapView]; + [self dropPinAtPoint:location]; + } +} + +- (IBAction)dropPin:(NSMenuItem *)sender { + [self dropPinAtPoint:_mouseLocationForMapViewContextMenu]; +} + +- (void)dropPinAtPoint:(NSPoint)point { + DroppedPinAnnotation *annotation = [[DroppedPinAnnotation alloc] init]; + annotation.coordinate = [self.mapView convertPoint:point toCoordinateFromView:self.mapView]; + annotation.title = @"Dropped Pin"; + [self.mapView addAnnotation:annotation]; + [self.mapView selectAnnotation:annotation animated:YES]; +} + +#pragma mark User interface validation + +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem { + if (menuItem.action == @selector(setStyle:)) { + NSURL *styleURL = self.mapView.styleURL; + NSCellStateValue state; + switch (menuItem.tag) { + case 1: + state = [styleURL isEqual:[MGLStyle streetsStyleURL]]; + break; + case 2: + state = [styleURL isEqual:[MGLStyle emeraldStyleURL]]; + break; + case 3: + state = [styleURL isEqual:[MGLStyle lightStyleURL]]; + break; + case 4: + state = [styleURL isEqual:[MGLStyle darkStyleURL]]; + break; + case 5: + state = [styleURL isEqual:[MGLStyle satelliteStyleURL]]; + break; + case 6: + state = [styleURL isEqual:[MGLStyle hybridStyleURL]]; + break; + default: + return NO; + } + menuItem.state = state; + return YES; + } + if (menuItem.action == @selector(chooseCustomStyle:)) { + menuItem.state = self.indexOfStyleInToolbarItem == NSNotFound; + return YES; + } + if (menuItem.action == @selector(zoomIn:)) { + return self.mapView.zoomLevel < self.mapView.maximumZoomLevel; + } + if (menuItem.action == @selector(zoomOut:)) { + return self.mapView.zoomLevel > self.mapView.minimumZoomLevel; + } + if (menuItem.action == @selector(snapToNorth:)) { + return self.mapView.direction != 0; + } + if (menuItem.action == @selector(reload:)) { + return YES; + } + if (menuItem.action == @selector(dropPin:)) { + return YES; + } + if (menuItem.action == @selector(toggleTileBoundaries:)) { + BOOL isShown = self.mapView.debugMask & MGLMapDebugTileBoundariesMask; + menuItem.title = isShown ? @"Hide Tile Boundaries" : @"Show Tile Boundaries"; + return YES; + } + if (menuItem.action == @selector(toggleTileInfo:)) { + BOOL isShown = self.mapView.debugMask & MGLMapDebugTileInfoMask; + menuItem.title = isShown ? @"Hide Tile Info" : @"Show Tile Info"; + return YES; + } + if (menuItem.action == @selector(toggleTileTimestamps:)) { + BOOL isShown = self.mapView.debugMask & MGLMapDebugTimestampsMask; + menuItem.title = isShown ? @"Hide Tile Timestamps" : @"Show Tile Timestamps"; + return YES; + } + if (menuItem.action == @selector(toggleCollisionBoxes:)) { + BOOL isShown = self.mapView.debugMask & MGLMapDebugCollisionBoxesMask; + menuItem.title = isShown ? @"Hide Collision Boxes" : @"Show Collision Boxes"; + return YES; + } + if (menuItem.action == @selector(showShortcuts:)) { + return YES; + } + if (menuItem.action == @selector(giveFeedback:)) { + return YES; + } + if (menuItem.action == @selector(showPreferences:)) { + return YES; + } + return NO; +} + +- (NSUInteger)indexOfStyleInToolbarItem { + if (![MGLAccountManager accessToken]) { + return NSNotFound; + } + + NSArray *styleURLs = @[ + [MGLStyle streetsStyleURL], + [MGLStyle emeraldStyleURL], + [MGLStyle lightStyleURL], + [MGLStyle darkStyleURL], + [MGLStyle satelliteStyleURL], + [MGLStyle hybridStyleURL], + ]; + return [styleURLs indexOfObject:self.mapView.styleURL]; +} + +- (BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem { + if (!self.mapView) { + return NO; + } + + if (toolbarItem.action == @selector(showShareMenu:)) { + [(NSButton *)toolbarItem.view sendActionOn:NSLeftMouseDownMask]; + return ([MGLAccountManager accessToken] + && [self.mapView.styleURL.scheme isEqualToString:@"mapbox"] + && [self.mapView.styleURL.pathComponents.firstObject isEqualToString:@"styles"]); + } + if (toolbarItem.action == @selector(setStyle:)) { + NSPopUpButton *popUpButton = (NSPopUpButton *)toolbarItem.view; + NSUInteger index = self.indexOfStyleInToolbarItem; + if (index == NSNotFound) { + [popUpButton addItemWithTitle:@"Custom"]; + index = [popUpButton numberOfItems] - 1; + } + [popUpButton selectItemAtIndex:index]; + } + return NO; +} + +#pragma mark NSApplicationDelegate methods + +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { + return YES; +} + +#pragma mark NSSharingServicePickerDelegate methods + +- (NS_ARRAY_OF(NSSharingService *) *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items proposedSharingServices:(NS_ARRAY_OF(NSSharingService *) *)proposedServices { + NSURL *shareURL = self.shareURL; + NSURL *browserURL = [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:shareURL]; + NSImage *browserIcon = [[NSWorkspace sharedWorkspace] iconForFile:browserURL.path]; + NSString *browserName = [[NSFileManager defaultManager] displayNameAtPath:browserURL.path]; + NSString *browserServiceName = [NSString stringWithFormat:@"Open in %@", browserName]; + + NSSharingService *browserService = [[NSSharingService alloc] initWithTitle:browserServiceName + image:browserIcon + alternateImage:nil + handler:^{ + [[NSWorkspace sharedWorkspace] openURL:self.shareURL]; + }]; + + NSMutableArray *sharingServices = [proposedServices mutableCopy]; + [sharingServices insertObject:browserService atIndex:0]; + return sharingServices; +} + +#pragma mark NSMenuDelegate methods + +- (void)menuWillOpen:(NSMenu *)menu { + if (menu == self.mapViewContextMenu) { + _mouseLocationForMapViewContextMenu = self.window.mouseLocationOutsideOfEventStream; + } +} + +#pragma mark MGLMapViewDelegate methods + +- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id <MGLAnnotation>)annotation { + return YES; +} + +- (void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id <MGLAnnotation>)annotation { + if ([annotation isKindOfClass:[DroppedPinAnnotation class]]) { + DroppedPinAnnotation *droppedPin = annotation; + [droppedPin resume]; + } +} + +- (void)mapView:(MGLMapView *)mapView didDeselectAnnotation:(id<MGLAnnotation>)annotation { + if ([annotation isKindOfClass:[DroppedPinAnnotation class]]) { + DroppedPinAnnotation *droppedPin = annotation; + [droppedPin pause]; + } +} + +@end + +@interface ValidatedToolbarItem : NSToolbarItem + +@end + +@implementation ValidatedToolbarItem + +- (void)validate { + [(AppDelegate *)self.toolbar.delegate validateToolbarItem:self]; +} + +@end diff --git a/platform/osx/app/Credits.rtf b/platform/osx/app/Credits.rtf new file mode 100644 index 0000000000..6b17eb34b2 --- /dev/null +++ b/platform/osx/app/Credits.rtf @@ -0,0 +1,9 @@ +{\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf130 +{\fonttbl\f0\fnil\fcharset0 SFUIText-Regular;} +{\colortbl;\red255\green255\blue255;} +\margl1440\margr1440\vieww10800\viewh8400\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 + +\f0\fs20 \cf0 Copyright \'a9 {\field{\*\fldinst{HYPERLINK "https://www.mapbox.com/about/maps/"}}{\fldrslt Mapbox}}.\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 +\cf0 Copyright \'a9 {\field{\*\fldinst{HYPERLINK "http://www.openstreetmap.org/about/"}}{\fldrslt OpenStreetMap contributors}}.}
\ No newline at end of file diff --git a/platform/osx/app/DroppedPinAnnotation.h b/platform/osx/app/DroppedPinAnnotation.h new file mode 100644 index 0000000000..69bb453f28 --- /dev/null +++ b/platform/osx/app/DroppedPinAnnotation.h @@ -0,0 +1,10 @@ +#import "MGLPointAnnotation.h" + +@interface DroppedPinAnnotation : MGLPointAnnotation + +@property (nonatomic, readonly) NSTimeInterval elapsedShownTime; + +- (void)resume; +- (void)pause; + +@end diff --git a/platform/osx/app/DroppedPinAnnotation.m b/platform/osx/app/DroppedPinAnnotation.m new file mode 100644 index 0000000000..a103ec923c --- /dev/null +++ b/platform/osx/app/DroppedPinAnnotation.m @@ -0,0 +1,63 @@ +#import "DroppedPinAnnotation.h" + +#import "LocationCoordinate2DTransformer.h" +#import "TimeIntervalTransformer.h" +#import "NSValue+Additions.h" + +@implementation DroppedPinAnnotation { + NSTimer *_timer; + NSTimeInterval _priorShownTimeInterval; + NSDate *_dateShown; + + NSValueTransformer *_coordinateTransformer; + NSValueTransformer *_timeIntervalTransformer; +} + +- (instancetype)init { + if (self = [super init]) { + _coordinateTransformer = [NSValueTransformer valueTransformerForName: + NSStringFromClass([LocationCoordinate2DTransformer class])]; + _timeIntervalTransformer = [NSValueTransformer valueTransformerForName: + NSStringFromClass([TimeIntervalTransformer class])]; + [self update:nil]; + } + return self; +} + +- (void)dealloc { + [self pause]; +} + +- (void)setCoordinate:(CLLocationCoordinate2D)coordinate { + super.coordinate = coordinate; + [self update:nil]; +} + +- (NSTimeInterval)elapsedShownTime { + return _priorShownTimeInterval - _dateShown.timeIntervalSinceNow; +} + +- (void)resume { + _dateShown = [NSDate date]; + _timer = [NSTimer scheduledTimerWithTimeInterval:1 + target:self + selector:@selector(update:) + userInfo:nil + repeats:YES]; +} + +- (void)pause { + [_timer invalidate]; + _timer = nil; + _priorShownTimeInterval -= _dateShown.timeIntervalSinceNow; + _dateShown = nil; +} + +- (void)update:(NSTimer *)timer { + NSString *coordinate = [_coordinateTransformer transformedValue: + [NSValue valueWithCLLocationCoordinate2D:self.coordinate]]; + NSString *elapsedTime = [_timeIntervalTransformer transformedValue:@(self.elapsedShownTime)]; + self.subtitle = [NSString stringWithFormat:@"%@\nSelected for %@", coordinate, elapsedTime]; +} + +@end diff --git a/platform/osx/Icon.icns b/platform/osx/app/Icon.icns Binary files differindex 64a5e57cf8..64a5e57cf8 100644 --- a/platform/osx/Icon.icns +++ b/platform/osx/app/Icon.icns diff --git a/platform/osx/app/Info.plist b/platform/osx/app/Info.plist new file mode 100644 index 0000000000..0d178d4b9e --- /dev/null +++ b/platform/osx/app/Info.plist @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleGetInfoString</key> + <string>0.1.0</string> + <key>CFBundleIconFile</key> + <string>Icon.icns</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleLongVersionString</key> + <string>0.1.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>0.1.0</string> + <key>CFBundleSignature</key> + <string>MBGL</string> + <key>CFBundleURLTypes</key> + <array> + <dict> + <key>CFBundleURLName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundleURLSchemes</key> + <array> + <string>mapboxgl</string> + </array> + </dict> + </array> + <key>CFBundleVersion</key> + <string>0.1.0</string> + <key>CSResourcesFileMapped</key> + <true/> + <key>NSHighResolutionCapable</key> + <true/> + <key>NSMainNibFile</key> + <string>MainMenu</string> + <key>NSPrincipalClass</key> + <string>NSApplication</string> +</dict> +</plist> diff --git a/platform/osx/app/LocationCoordinate2DTransformer.h b/platform/osx/app/LocationCoordinate2DTransformer.h new file mode 100644 index 0000000000..162325fbad --- /dev/null +++ b/platform/osx/app/LocationCoordinate2DTransformer.h @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> + +@interface LocationCoordinate2DTransformer : NSValueTransformer + +@end diff --git a/platform/osx/app/LocationCoordinate2DTransformer.m b/platform/osx/app/LocationCoordinate2DTransformer.m new file mode 100644 index 0000000000..d9c35b2a1f --- /dev/null +++ b/platform/osx/app/LocationCoordinate2DTransformer.m @@ -0,0 +1,42 @@ +#import "LocationCoordinate2DTransformer.h" + +#import "NSValue+Additions.h" + +NSString *StringFromDegrees(CLLocationDegrees degrees, char positiveDirection, char negativeDirection) { + double minutes = (degrees - floor(degrees)) * 60; + double seconds = (minutes - floor(minutes)) * 60; + + NSMutableString *string = [NSMutableString stringWithFormat:@"%.0f°", fabs(degrees)]; + if (floor(minutes) || floor(seconds)) { + [string appendFormat:@"%.0f′", minutes]; + } + if (floor(seconds)) { + [string appendFormat:@"%.0f″", seconds]; + } + if (degrees) { + [string appendFormat:@"%c", degrees > 0 ? positiveDirection : negativeDirection]; + } + return string; +} + +@implementation LocationCoordinate2DTransformer + ++ (Class)transformedValueClass { + return [NSString class]; +} + ++ (BOOL)allowsReverseTransformation { + return NO; +} + +- (id)transformedValue:(id)value { + if (![value isKindOfClass:[NSValue class]]) { + return nil; + } + CLLocationCoordinate2D coordinate = [value CLLocationCoordinate2DValue]; + return [NSString stringWithFormat:@"%@, %@", + StringFromDegrees(coordinate.latitude, 'N', 'S'), + StringFromDegrees(coordinate.longitude, 'E', 'W')]; +} + +@end diff --git a/platform/osx/app/MainMenu.xib b/platform/osx/app/MainMenu.xib new file mode 100644 index 0000000000..c96169b094 --- /dev/null +++ b/platform/osx/app/MainMenu.xib @@ -0,0 +1,683 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9531" systemVersion="15B42" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> + <dependencies> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="9531"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> + <connections> + <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application"> + <connections> + <outlet property="delegate" destination="Voe-Tx-rLC" id="z9N-Fm-MUP"/> + </connections> + </customObject> + <customObject id="Voe-Tx-rLC" customClass="AppDelegate"> + <connections> + <outlet property="mapView" destination="v1Z-oA-oHU" id="70S-xO-QIP"/> + <outlet property="mapViewContextMenu" destination="6rZ-M1-uTn" id="wl2-03-9Z8"/> + <outlet property="preferencesWindow" destination="UWc-yQ-qda" id="Ota-aT-Mz2"/> + <outlet property="window" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/> + </connections> + </customObject> + <menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6"> + <items> + <menuItem title="Mapbox GL" id="1Xt-HY-uBw"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Mapbox GL" systemMenu="apple" id="uQy-DD-JDr"> + <items> + <menuItem title="About Mapbox GL" id="5kV-Vb-QxS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/> + <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"> + <connections> + <action selector="showPreferences:" target="-1" id="Llx-Uy-HTS"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/> + <menuItem title="Services" id="NMo-om-nkz"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/> + </menuItem> + <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/> + <menuItem title="Hide Mapbox GL" keyEquivalent="h" id="Olw-nP-bQN"> + <connections> + <action selector="hide:" target="-1" id="PnN-Uc-m68"/> + </connections> + </menuItem> + <menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/> + </connections> + </menuItem> + <menuItem title="Show All" id="Kd2-mp-pUS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/> + <menuItem title="Quit Mapbox GL" keyEquivalent="q" id="4sb-4s-VLi"> + <connections> + <action selector="terminate:" target="-1" id="Te7-pn-YzF"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="File" id="dMs-cI-mzQ"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="File" id="bib-Uj-vzu"> + <items> + <menuItem title="New" keyEquivalent="n" id="Was-JA-tGl"> + <connections> + <action selector="newDocument:" target="-1" id="4Si-XN-c54"/> + </connections> + </menuItem> + <menuItem title="Open…" keyEquivalent="o" id="IAo-SY-fd9"> + <connections> + <action selector="openDocument:" target="-1" id="bVn-NM-KNZ"/> + </connections> + </menuItem> + <menuItem title="Open Recent" id="tXI-mr-wws"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="oas-Oc-fiZ"> + <items> + <menuItem title="Clear Menu" id="vNY-rz-j42"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="clearRecentDocuments:" target="-1" id="Daa-9d-B3U"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem isSeparatorItem="YES" id="m54-Is-iLE"/> + <menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG"> + <connections> + <action selector="performClose:" target="-1" id="HmO-Ls-i7Q"/> + </connections> + </menuItem> + <menuItem title="Save…" keyEquivalent="s" id="pxx-59-PXV"> + <connections> + <action selector="saveDocument:" target="-1" id="teZ-XB-qJY"/> + </connections> + </menuItem> + <menuItem title="Save As…" keyEquivalent="S" id="Bw7-FT-i3A"> + <connections> + <action selector="saveDocumentAs:" target="-1" id="mDf-zr-I0C"/> + </connections> + </menuItem> + <menuItem title="Revert to Saved" id="KaW-ft-85H"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="revertDocumentToSaved:" target="-1" id="iJ3-Pv-kwq"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="aJh-i4-bef"/> + <menuItem title="Page Setup…" keyEquivalent="P" id="qIS-W8-SiK"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + <connections> + <action selector="runPageLayout:" target="-1" id="Din-rz-gC5"/> + </connections> + </menuItem> + <menuItem title="Print…" keyEquivalent="p" id="aTl-1u-JFS"> + <connections> + <action selector="print:" target="-1" id="qaZ-4w-aoO"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Edit" id="5QF-Oa-p0T"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Edit" id="W48-6f-4Dl"> + <items> + <menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg"> + <connections> + <action selector="undo:" target="-1" id="M6e-cu-g7V"/> + </connections> + </menuItem> + <menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam"> + <connections> + <action selector="redo:" target="-1" id="oIA-Rs-6OD"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/> + <menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG"> + <connections> + <action selector="cut:" target="-1" id="YJe-68-I9s"/> + </connections> + </menuItem> + <menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU"> + <connections> + <action selector="copy:" target="-1" id="G1f-GL-Joy"/> + </connections> + </menuItem> + <menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL"> + <connections> + <action selector="paste:" target="-1" id="UvS-8e-Qdg"/> + </connections> + </menuItem> + <menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/> + </connections> + </menuItem> + <menuItem title="Delete" id="pa3-QI-u2k"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="delete:" target="-1" id="0Mk-Ml-PaM"/> + </connections> + </menuItem> + <menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m"> + <connections> + <action selector="selectAll:" target="-1" id="VNm-Mi-diN"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/> + <menuItem title="Find" id="4EN-yA-p0u"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Find" id="1b7-l0-nxx"> + <items> + <menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/> + </connections> + </menuItem> + <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/> + </connections> + </menuItem> + <menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/> + </connections> + </menuItem> + <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/> + </connections> + </menuItem> + <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/> + </connections> + </menuItem> + <menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd"> + <connections> + <action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Spelling and Grammar" id="Dv1-io-Yv7"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Spelling" id="3IN-sU-3Bg"> + <items> + <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI"> + <connections> + <action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/> + </connections> + </menuItem> + <menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7"> + <connections> + <action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="bNw-od-mp5"/> + <menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/> + </connections> + </menuItem> + <menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/> + </connections> + </menuItem> + <menuItem title="Correct Spelling Automatically" id="78Y-hA-62v"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Substitutions" id="9ic-FL-obx"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Substitutions" id="FeM-D8-WVr"> + <items> + <menuItem title="Show Substitutions" id="z6F-FW-3nz"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/> + <menuItem title="Smart Copy/Paste" id="9yt-4B-nSM"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/> + </connections> + </menuItem> + <menuItem title="Smart Quotes" id="hQb-2v-fYv"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/> + </connections> + </menuItem> + <menuItem title="Smart Dashes" id="rgM-f4-ycn"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/> + </connections> + </menuItem> + <menuItem title="Smart Links" id="cwL-P1-jid"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/> + </connections> + </menuItem> + <menuItem title="Data Detectors" id="tRr-pd-1PS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/> + </connections> + </menuItem> + <menuItem title="Text Replacement" id="HFQ-gK-NFA"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Transformations" id="2oI-Rn-ZJC"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Transformations" id="c8a-y6-VQd"> + <items> + <menuItem title="Make Upper Case" id="vmV-6d-7jI"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/> + </connections> + </menuItem> + <menuItem title="Make Lower Case" id="d9M-CD-aMd"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/> + </connections> + </menuItem> + <menuItem title="Capitalize" id="UEZ-Bs-lqG"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Speech" id="xrE-MZ-jX0"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Speech" id="3rS-ZA-NoH"> + <items> + <menuItem title="Start Speaking" id="Ynk-f8-cLZ"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/> + </connections> + </menuItem> + <menuItem title="Stop Speaking" id="Oyz-dy-DGm"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="View" id="H8h-7b-M4v"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="View" id="HyV-fh-RgO"> + <items> + <menuItem title="Streets" state="on" tag="1" keyEquivalent="1" id="17N-yz-NNo"> + <connections> + <action selector="setStyle:" target="-1" id="I4L-Wx-UXA"/> + </connections> + </menuItem> + <menuItem title="Emerald" tag="2" keyEquivalent="2" id="BBa-Qa-SQr"> + <connections> + <action selector="setStyle:" target="-1" id="rM1-yG-t5u"/> + </connections> + </menuItem> + <menuItem title="Light" tag="3" keyEquivalent="3" id="HWe-7u-UVJ"> + <connections> + <action selector="setStyle:" target="-1" id="Q9V-O1-oRz"/> + </connections> + </menuItem> + <menuItem title="Dark" tag="4" keyEquivalent="4" id="6HI-q6-AeV"> + <connections> + <action selector="setStyle:" target="-1" id="YfH-1I-G50"/> + </connections> + </menuItem> + <menuItem title="Satellite" tag="5" keyEquivalent="5" id="h0J-5X-kgF"> + <connections> + <action selector="setStyle:" target="-1" id="GXt-oK-Hy1"/> + </connections> + </menuItem> + <menuItem title="Hybrid" tag="6" keyEquivalent="6" id="9BL-00-HFt"> + <connections> + <action selector="setStyle:" target="-1" id="oL4-AC-waq"/> + </connections> + </menuItem> + <menuItem title="Custom Style…" id="L0h-86-2cU"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="chooseCustomStyle:" target="-1" id="QJF-fM-Ty3"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="BMF-ml-0Bd"/> + <menuItem title="Zoom In" keyEquivalent="+" id="W82-WO-xvB"> + <connections> + <action selector="zoomIn:" target="-1" id="g33-vK-zUu"/> + </connections> + </menuItem> + <menuItem title="Zoom Out" keyEquivalent="-" id="j7h-PY-edM"> + <connections> + <action selector="zoomOut:" target="-1" id="0pP-tO-9ex"/> + </connections> + </menuItem> + <menuItem title="Snap to North" keyEquivalent="" id="Zss-3w-wkz"> + <connections> + <action selector="snapToNorth:" target="-1" id="Ayq-GE-Lb5"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="mkP-YN-G0w"/> + <menuItem title="Reload" keyEquivalent="r" id="JvI-nv-KaE"> + <connections> + <action selector="reload:" target="-1" id="xkh-9F-mOe"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="CyM-Wv-Bnc"/> + <menuItem title="Show Toolbar" keyEquivalent="t" id="snW-S8-Cw5"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="toggleToolbarShown:" target="-1" id="BXY-wc-z0C"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Debug" id="ZNC-5r-eBw"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Debug" id="McE-ka-r79"> + <items> + <menuItem title="Show Tile Boundaries" id="rDE-dG-rTR"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleTileBoundaries:" target="-1" id="LAO-88-F7h"/> + </connections> + </menuItem> + <menuItem title="Show Tile Info" id="LoH-qD-kb0"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleTileInfo:" target="-1" id="KCn-0G-V87"/> + </connections> + </menuItem> + <menuItem title="Show Tile Timestamps" id="bY0-2E-LZ7"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleTileTimestamps:" target="-1" id="tBs-2N-KEG"/> + </connections> + </menuItem> + <menuItem title="Show Collision Boxes" id="Y0b-3K-mJE"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleCollisionBoxes:" target="-1" id="EYa-7n-iWZ"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Window" id="aUF-d1-5bR"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo"> + <items> + <menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV"> + <connections> + <action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/> + </connections> + </menuItem> + <menuItem title="Zoom" keyEquivalent="z" id="R4o-n2-Eq4"> + <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/> + <connections> + <action selector="performZoom:" target="-1" id="DIl-cC-cCs"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/> + <menuItem title="Bring All to Front" id="LE2-aR-0XJ"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Help" id="wpr-3q-Mcd"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ"> + <items> + <menuItem title="Mapbox GL Help" keyEquivalent="?" id="FKE-Sm-Kum"> + <connections> + <action selector="showShortcuts:" target="-1" id="hNZ-sm-X2q"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="EpY-wQ-SjH"/> + <menuItem title="Improve This Map" id="xu5-WN-qYK"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="giveFeedback:" target="-1" id="cil-i9-r39"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + </items> + </menu> + <window title="Mapbox GL" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" frameAutosaveName="Mapbox" animationBehavior="default" id="QvC-M9-y7g"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES" fullSizeContentView="YES"/> + <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> + <rect key="contentRect" x="388" y="211" width="512" height="360"/> + <rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/> + <view key="contentView" id="EiT-Mj-1SZ"> + <rect key="frame" x="0.0" y="0.0" width="512" height="360"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <customView translatesAutoresizingMaskIntoConstraints="NO" id="v1Z-oA-oHU" customClass="MGLMapView"> + <rect key="frame" x="0.0" y="0.0" width="512" height="360"/> + <connections> + <outlet property="delegate" destination="Voe-Tx-rLC" id="qJ7-fL-iw1"/> + <outlet property="menu" destination="6rZ-M1-uTn" id="4sB-UN-zuc"/> + </connections> + </customView> + </subviews> + <constraints> + <constraint firstItem="v1Z-oA-oHU" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" id="Z3O-xb-fah"/> + <constraint firstAttribute="trailing" secondItem="v1Z-oA-oHU" secondAttribute="trailing" id="oYw-bk-P2l"/> + <constraint firstItem="v1Z-oA-oHU" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" id="r4Z-4m-bgA"/> + <constraint firstAttribute="bottom" secondItem="v1Z-oA-oHU" secondAttribute="bottom" id="w3h-H1-baQ"/> + </constraints> + </view> + <toolbar key="toolbar" implicitIdentifier="A3AC6577-4712-4628-813D-113498171A84" allowsUserCustomization="NO" displayMode="iconOnly" sizeMode="regular" id="8nY-8B-Om5"> + <allowedToolbarItems> + <toolbarItem implicitItemIdentifier="NSToolbarSpaceItem" id="zzK-Oz-JV8"/> + <toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="SMs-8a-x2h"/> + <toolbarItem implicitItemIdentifier="2CB58C0A-7B95-4233-8DD3-F94BFE7D3061" label="Share" paletteLabel="Share" image="NSShareTemplate" id="HtT-hs-jWY" customClass="ValidatedToolbarItem"> + <nil key="toolTip"/> + <size key="minSize" width="40" height="32"/> + <size key="maxSize" width="40" height="32"/> + <button key="view" verticalHuggingPriority="750" id="l0U-ql-5hn"> + <rect key="frame" x="0.0" y="14" width="48" height="32"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <buttonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="NSShareTemplate" imagePosition="only" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="vxZ-hj-hV2"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + </button> + <connections> + <action selector="showShareMenu:" target="-1" id="vDU-9Y-DkS"/> + </connections> + </toolbarItem> + <toolbarItem implicitItemIdentifier="BA3542AF-D63A-4893-9CC7-8F67EF2E82B0" label="Style" paletteLabel="Style" id="ge5-0H-wyr" customClass="ValidatedToolbarItem"> + <nil key="toolTip"/> + <size key="minSize" width="100" height="26"/> + <size key="maxSize" width="100" height="26"/> + <popUpButton key="view" verticalHuggingPriority="750" id="5Cd-Lw-cWm"> + <rect key="frame" x="0.0" y="14" width="100" height="26"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <popUpButtonCell key="cell" type="roundTextured" title="Streets" bezelStyle="texturedRounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="border" tag="1" imageScaling="proportionallyDown" inset="2" selectedItem="ZnV-nK-Bri" id="esr-2z-V4Y"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="menu"/> + <menu key="menu" id="uxq-U6-EQW"> + <items> + <menuItem title="Streets" state="on" tag="1" id="ZnV-nK-Bri"/> + <menuItem title="Emerald" tag="2" id="LLa-jX-Dox"/> + <menuItem title="Light" tag="3" id="jDf-NE-hMn"/> + <menuItem title="Dark" tag="4" id="etJ-y7-M5R"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Satellite" tag="5" id="xYa-J8-bLe"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Hybrid" tag="6" id="1Uq-Nl-FSZ"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + </items> + </menu> + </popUpButtonCell> + </popUpButton> + <connections> + <action selector="setStyle:" target="-1" id="qb2-E8-7pE"/> + </connections> + </toolbarItem> + </allowedToolbarItems> + <defaultToolbarItems> + <toolbarItem reference="HtT-hs-jWY"/> + <toolbarItem reference="SMs-8a-x2h"/> + <toolbarItem reference="ge5-0H-wyr"/> + </defaultToolbarItems> + <connections> + <outlet property="delegate" destination="Voe-Tx-rLC" id="ciT-Nf-Dtm"/> + </connections> + </toolbar> + <connections> + <binding destination="Voe-Tx-rLC" name="title" keyPath="mapView.centerCoordinate" id="XFo-sp-PtR"> + <dictionary key="options"> + <string key="NSValueTransformerName">LocationCoordinate2DTransformer</string> + </dictionary> + </binding> + </connections> + </window> + <window title="Preferences" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="Preferences" animationBehavior="default" id="UWc-yQ-qda"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/> + <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> + <rect key="contentRect" x="109" y="131" width="350" height="62"/> + <rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/> + <view key="contentView" id="eA4-n3-qPe"> + <rect key="frame" x="0.0" y="0.0" width="350" height="62"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="0IK-AW-Gg3"> + <rect key="frame" x="18" y="23" width="89" height="17"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Access token:" id="Ptd-FI-M5A"> + <font key="font" metaFont="system"/> + <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <connections> + <accessibilityConnection property="link" destination="7sb-sf-oJU" id="U0t-jC-oQ7"/> + </connections> + </textField> + <textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7sb-sf-oJU"> + <rect key="frame" x="113" y="20" width="197" height="22"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="jlV-TC-NUv"> + <font key="font" metaFont="system"/> + <color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <connections> + <binding destination="45S-yT-WUN" name="value" keyPath="values.MGLMapboxAccessToken" id="iJE-S2-ALY"/> + </connections> + </textField> + <button translatesAutoresizingMaskIntoConstraints="NO" id="c3S-LC-PoX"> + <rect key="frame" x="318" y="25" width="12" height="12"/> + <constraints> + <constraint firstAttribute="width" constant="12" id="M3J-pU-gKn"/> + </constraints> + <buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSFollowLinkFreestandingTemplate" imagePosition="only" alignment="center" controlSize="small" imageScaling="proportionallyUpOrDown" inset="2" id="38x-37-Ay0"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="smallSystem"/> + </buttonCell> + <connections> + <action selector="openAccessTokenManager:" target="-1" id="1LX-4G-roC"/> + </connections> + </button> + </subviews> + <constraints> + <constraint firstAttribute="trailing" secondItem="c3S-LC-PoX" secondAttribute="trailing" constant="20" symbolic="YES" id="7QU-Jd-Rg6"/> + <constraint firstItem="c3S-LC-PoX" firstAttribute="top" secondItem="eA4-n3-qPe" secondAttribute="top" constant="25" id="JOS-HU-27c"/> + <constraint firstItem="7sb-sf-oJU" firstAttribute="leading" secondItem="0IK-AW-Gg3" secondAttribute="trailing" constant="8" symbolic="YES" id="SS6-VQ-sLK"/> + <constraint firstItem="0IK-AW-Gg3" firstAttribute="leading" secondItem="eA4-n3-qPe" secondAttribute="leading" constant="20" symbolic="YES" id="TYG-io-qfV"/> + <constraint firstItem="7sb-sf-oJU" firstAttribute="top" secondItem="eA4-n3-qPe" secondAttribute="top" constant="20" symbolic="YES" id="Vzb-q8-ecP"/> + <constraint firstItem="c3S-LC-PoX" firstAttribute="leading" secondItem="7sb-sf-oJU" secondAttribute="trailing" constant="8" symbolic="YES" id="pjl-9u-IgM"/> + <constraint firstItem="7sb-sf-oJU" firstAttribute="baseline" secondItem="0IK-AW-Gg3" secondAttribute="baseline" id="qIY-Jr-9Ws"/> + <constraint firstItem="7sb-sf-oJU" firstAttribute="centerY" secondItem="c3S-LC-PoX" secondAttribute="centerY" id="zej-gw-fC0"/> + </constraints> + </view> + <connections> + <outlet property="initialFirstResponder" destination="7sb-sf-oJU" id="UZe-di-dnA"/> + </connections> + <point key="canvasLocation" x="754" y="210"/> + </window> + <userDefaultsController representsSharedInstance="YES" id="45S-yT-WUN"/> + <menu title="Map View" id="6rZ-M1-uTn"> + <items> + <menuItem title="Drop Pin" id="fxQ-3P-E7l"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="dropPin:" target="Voe-Tx-rLC" id="eDR-iw-JUG"/> + </connections> + </menuItem> + </items> + <connections> + <outlet property="delegate" destination="Voe-Tx-rLC" id="MsV-YU-f4X"/> + </connections> + <point key="canvasLocation" x="820" y="254.5"/> + </menu> + </objects> + <resources> + <image name="NSFollowLinkFreestandingTemplate" width="14" height="14"/> + <image name="NSShareTemplate" width="11" height="16"/> + </resources> +</document> diff --git a/platform/osx/app/NSValue+Additions.h b/platform/osx/app/NSValue+Additions.h new file mode 100644 index 0000000000..050dbc8132 --- /dev/null +++ b/platform/osx/app/NSValue+Additions.h @@ -0,0 +1,10 @@ +#import <Foundation/Foundation.h> +#import <CoreLocation/CoreLocation.h> + +@interface NSValue (Additions) + ++ (instancetype)valueWithCLLocationCoordinate2D:(CLLocationCoordinate2D)coordinate; + +@property (readonly) CLLocationCoordinate2D CLLocationCoordinate2DValue; + +@end diff --git a/platform/osx/app/NSValue+Additions.m b/platform/osx/app/NSValue+Additions.m new file mode 100644 index 0000000000..c060602f43 --- /dev/null +++ b/platform/osx/app/NSValue+Additions.m @@ -0,0 +1,15 @@ +#import "NSValue+Additions.h" + +@implementation NSValue (Additions) + ++ (instancetype)valueWithCLLocationCoordinate2D:(CLLocationCoordinate2D)coordinate { + return [self valueWithBytes:&coordinate objCType:@encode(CLLocationCoordinate2D)]; +} + +- (CLLocationCoordinate2D)CLLocationCoordinate2DValue { + CLLocationCoordinate2D coordinate; + [self getValue:&coordinate]; + return coordinate; +} + +@end diff --git a/platform/osx/app/TimeIntervalTransformer.h b/platform/osx/app/TimeIntervalTransformer.h new file mode 100644 index 0000000000..ca88ad2cd1 --- /dev/null +++ b/platform/osx/app/TimeIntervalTransformer.h @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> + +@interface TimeIntervalTransformer : NSValueTransformer + +@end diff --git a/platform/osx/app/TimeIntervalTransformer.m b/platform/osx/app/TimeIntervalTransformer.m new file mode 100644 index 0000000000..e6d7333d25 --- /dev/null +++ b/platform/osx/app/TimeIntervalTransformer.m @@ -0,0 +1,55 @@ +#import "TimeIntervalTransformer.h" + +#import "NSValue+Additions.h" + +@implementation TimeIntervalTransformer + ++ (Class)transformedValueClass { + return [NSString class]; +} + ++ (BOOL)allowsReverseTransformation { + return NO; +} + +NSString *NumberAndUnitString(NSInteger quantity, NSString *singular, NSString *plural) { + return [NSString stringWithFormat:@"%ld %@", quantity, quantity == 1 ? singular : plural]; +} + +- (id)transformedValue:(id)value { + if (![value isKindOfClass:[NSValue class]]) { + return nil; + } + + NSTimeInterval timeInterval = [value doubleValue]; + NSInteger seconds = floor(timeInterval); + NSInteger minutes = floor(seconds / 60); + seconds -= minutes * 60; + NSInteger hours = floor(minutes / 60); + minutes -= hours * 60; + NSInteger days = floor(hours / 24); + hours -= days * 24; + NSInteger weeks = floor(days) / 7; + days -= weeks * 7; + + NSMutableArray *components = [NSMutableArray array]; + if (seconds || timeInterval < 60) { + [components addObject:NumberAndUnitString(seconds, @"second", @"seconds")]; + } + if (minutes) { + [components insertObject:NumberAndUnitString(minutes, @"minute", @"minutes") atIndex:0]; + } + if (hours) { + [components insertObject:NumberAndUnitString(hours, @"hour", @"hours") atIndex:0]; + } + if (days) { + [components insertObject:NumberAndUnitString(days, @"day", @"days") atIndex:0]; + } + if (weeks) { + [components insertObject:NumberAndUnitString(weeks, @"week", @"weeks") atIndex:0]; + } + + return [components componentsJoinedByString:@", "]; +} + +@end diff --git a/platform/osx/app/main.m b/platform/osx/app/main.m new file mode 100644 index 0000000000..8a6799b414 --- /dev/null +++ b/platform/osx/app/main.m @@ -0,0 +1,5 @@ +#import <Cocoa/Cocoa.h> + +int main(int argc, const char * argv[]) { + return NSApplicationMain(argc, argv); +} diff --git a/platform/osx/app/mapboxgl-app.gypi b/platform/osx/app/mapboxgl-app.gypi new file mode 100644 index 0000000000..8ab0320d31 --- /dev/null +++ b/platform/osx/app/mapboxgl-app.gypi @@ -0,0 +1,48 @@ +{ + 'includes': [ + '../../../gyp/common.gypi', + ], + 'targets': [ + { 'target_name': 'osxapp', + 'product_name': 'Mapbox GL', + 'type': 'executable', + 'product_extension': 'app', + 'mac_bundle': 1, + 'mac_bundle_resources': [ + 'Credits.rtf', + 'Icon.icns', + 'MainMenu.xib', + ], + + 'dependencies': [ + 'mbgl.gyp:core', + 'mbgl.gyp:platform-<(platform_lib)', + 'mbgl.gyp:http-<(http_lib)', + 'mbgl.gyp:asset-<(asset_lib)', + 'mbgl.gyp:cache-<(cache_lib)', + ], + + 'sources': [ + './AppDelegate.h', + './AppDelegate.m', + './DroppedPinAnnotation.h', + './DroppedPinAnnotation.m', + './LocationCoordinate2DTransformer.h', + './LocationCoordinate2DTransformer.m', + './TimeIntervalTransformer.h', + './TimeIntervalTransformer.m', + './NSValue+Additions.h', + './NSValue+Additions.m', + './main.m', + ], + + 'xcode_settings': { + 'SDKROOT': 'macosx', + 'SUPPORTED_PLATFORMS':'macosx', + 'OTHER_LDFLAGS': [ '-stdlib=libc++', '-lstdc++' ], + 'INFOPLIST_FILE': '../platform/osx/app/Info.plist', + 'CLANG_ENABLE_OBJC_ARC': 'YES', + }, + } + ] +} diff --git a/platform/osx/main.mm b/platform/osx/main.mm deleted file mode 100644 index 6b8aef3814..0000000000 --- a/platform/osx/main.mm +++ /dev/null @@ -1,212 +0,0 @@ -#include <mbgl/platform/log.hpp> -#include <mbgl/platform/platform.hpp> -#include <mbgl/platform/darwin/settings_nsuserdefaults.hpp> -#include <mbgl/platform/darwin/reachability.h> -#include <mbgl/platform/default/glfw_view.hpp> -#include <mbgl/storage/default_file_source.hpp> -#include <mbgl/storage/sqlite_cache.hpp> -#include <mbgl/storage/network_status.hpp> - -#include <mbgl/util/geo.hpp> -#include <mbgl/util/default_styles.hpp> - -#import <Foundation/Foundation.h> - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunknown-pragmas" -#pragma GCC diagnostic ignored "-Wunused-local-typedefs" -#pragma GCC diagnostic ignored "-Wshadow" -#include <boost/program_options.hpp> -#pragma GCC diagnostic pop - -#include <iostream> - -namespace po = boost::program_options; - - -@interface URLHandler : NSObject -@property (nonatomic) mbgl::Map *map; - -- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event - withReplyEvent:(NSAppleEventDescriptor *)replyEvent; -@end - -@implementation URLHandler - -- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event - withReplyEvent:(NSAppleEventDescriptor *)replyEvent { - (void)replyEvent; - - NSString* urlString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; - NSURL *url = [NSURL URLWithString:urlString]; - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - for (NSString *param in [[url query] componentsSeparatedByString:@"&"]) { - NSArray *parts = [param componentsSeparatedByString:@"="]; - if([parts count] < 2) continue; - [params setObject:[parts objectAtIndex:1] forKey:[parts objectAtIndex:0]]; - } - - mbgl::LatLng latLng = mbgl::LatLng(0, 0); - double zoom = 0, bearing = 0; - bool hasCenter = false, hasZoom = false, hasBearing = false; - - NSString *centerString = [params objectForKey:@"center"]; - if (centerString) { - NSArray *latLngValues = [centerString componentsSeparatedByString:@","]; - if ([latLngValues count] == 2) { - latLng.latitude = [latLngValues[0] doubleValue]; - latLng.longitude = [latLngValues[1] doubleValue]; - hasCenter = true; - } - } - - NSString *zoomString = [params objectForKey:@"zoom"]; - if (zoomString) { - zoom = [zoomString doubleValue]; - hasZoom = true; - } - - NSString *bearingString = [params objectForKey:@"bearing"]; - if (bearingString) { - bearing = [bearingString doubleValue]; - hasBearing = true; - } - - if ([self map]) { - if (hasCenter && hasZoom) { - [self map]->setLatLngZoom(latLng, zoom); - } else if (hasCenter) { - [self map]->setLatLng(latLng); - } else if (hasZoom) { - [self map]->setZoom(zoom); - } - - if (hasBearing) { - [self map]->setBearing(bearing); - } - } -} -@end - -// Returns the path to the default cache database on this system. -const std::string &defaultCacheDatabase() { - static const std::string path = []() -> std::string { - NSArray *paths = NSSearchPathForDirectoriesInDomains( - NSApplicationSupportDirectory, NSUserDomainMask, YES); - if ([paths count] == 0) { - // Disable the cache if we don't have a location to write. - return ""; - } - - NSString *p = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"Mapbox GL"]; - - if (![[NSFileManager defaultManager] createDirectoryAtPath:p - withIntermediateDirectories:YES - attributes:nil - error:nil]) { - // Disable the cache if we couldn't create the directory. - return ""; - } - - return [[p stringByAppendingPathComponent:@"cache.db"] UTF8String]; - }(); - return path; -} - -int main(int argc, char* argv[]) { - bool fullscreen = false; - bool benchmark = false; - std::string style; - - po::options_description desc("Allowed options"); - desc.add_options() - ("fullscreen,f", po::bool_switch(&fullscreen)->default_value(fullscreen), "Fullscreen mode") - ("style,s", po::value(&style)->value_name("json"), "Map stylesheet") - ("benchmark,b", po::bool_switch(&benchmark)->default_value(benchmark), "Benchmark mode") - ; - - try { - auto parsed = po::command_line_parser(argc, argv).options(desc).allow_unregistered().run(); - po::variables_map vm; - po::store(parsed, vm); - po::notify(vm); - } catch(std::exception& e) { - std::cout << "Error: " << e.what() << std::endl << desc; - exit(1); - } - - if (benchmark) { - mbgl::Log::Info(mbgl::Event::General, "BENCHMARK MODE: Some optimizations are disabled."); - } - - GLFWView view(fullscreen, benchmark); - - mbgl::SQLiteCache cache(defaultCacheDatabase()); - mbgl::DefaultFileSource fileSource(&cache); - - // Set access token if present - NSString *accessToken = [[NSProcessInfo processInfo] environment][@"MAPBOX_ACCESS_TOKEN"]; - if (!accessToken) mbgl::Log::Warning(mbgl::Event::Setup, "No access token set. Mapbox vector tiles won't work."); - if (accessToken) fileSource.setAccessToken([accessToken cStringUsingEncoding:[NSString defaultCStringEncoding]]); - - mbgl::Map map(view, fileSource); - - URLHandler *handler = [[URLHandler alloc] init]; - [handler setMap:&map]; - NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; - [appleEventManager setEventHandler:handler andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; - - // Notify map object when network reachability status changes. - MGLReachability* reachability = [MGLReachability reachabilityForInternetConnection]; - reachability.reachableBlock = ^(MGLReachability *) { - mbgl::NetworkStatus::Reachable(); - }; - [reachability startNotifier]; - - // Load settings - mbgl::Settings_NSUserDefaults settings; - map.setLatLngZoom(mbgl::LatLng(settings.latitude, settings.longitude), settings.zoom); - map.setBearing(settings.bearing); - map.setPitch(settings.pitch); - map.setDebug(mbgl::MapDebugOptions(settings.debug)); - - view.setChangeStyleCallback([&map, &view] () { - static uint8_t currentStyleIndex; - - if (++currentStyleIndex == mbgl::util::default_styles::numOrderedStyles) { - currentStyleIndex = 0; - } - - mbgl::util::default_styles::DefaultStyle newStyle = mbgl::util::default_styles::orderedStyles[currentStyleIndex]; - map.setStyleURL(newStyle.url); - view.setWindowTitle(newStyle.name); - - mbgl::Log::Info(mbgl::Event::Setup, "Changed style to: %s", newStyle.name); - }); - - - // Load style - if (style.empty()) { - mbgl::util::default_styles::DefaultStyle newStyle = mbgl::util::default_styles::orderedStyles[0]; - style = newStyle.url; - view.setWindowTitle(newStyle.name); - } - - map.setStyleURL(style); - - view.run(); - - [reachability stopNotifier]; - - // Save settings - mbgl::LatLng latLng = map.getLatLng(); - settings.latitude = latLng.latitude; - settings.longitude = latLng.longitude; - settings.zoom = map.getZoom(); - settings.bearing = map.getBearing(); - settings.pitch = map.getPitch(); - settings.debug = uint32_t(map.getDebug()); - settings.save(); - - return 0; -} diff --git a/platform/osx/mapboxgl-app.gypi b/platform/osx/mapboxgl-app.gypi deleted file mode 100644 index 2bb893af30..0000000000 --- a/platform/osx/mapboxgl-app.gypi +++ /dev/null @@ -1,63 +0,0 @@ -{ - 'includes': [ - '../../gyp/common.gypi', - ], - 'targets': [ - { 'target_name': 'osxapp', - 'product_name': 'Mapbox GL', - 'type': 'executable', - 'product_extension': 'app', - 'mac_bundle': 1, - 'mac_bundle_resources': [ - 'Icon.icns' - ], - - 'dependencies': [ - 'mbgl.gyp:core', - 'mbgl.gyp:platform-<(platform_lib)', - 'mbgl.gyp:http-<(http_lib)', - 'mbgl.gyp:asset-<(asset_lib)', - 'mbgl.gyp:cache-<(cache_lib)', - ], - - 'sources': [ - './main.mm', - '../darwin/settings_nsuserdefaults.hpp', - '../darwin/settings_nsuserdefaults.mm', - '../darwin/reachability.m', - '../default/glfw_view.hpp', - '../default/glfw_view.cpp', - ], - - 'variables' : { - 'cflags_cc': [ - '<@(boost_cflags)', - '<@(glfw_cflags)', - '<@(variant_cflags)', - ], - 'ldflags': [ - '-framework SystemConfiguration', # For NSUserDefaults and Reachability - '<@(glfw_ldflags)', - ], - 'libraries': [ - '<@(glfw_static_libs)', - '<@(boost_libprogram_options_static_libs)' - ], - }, - - 'libraries': [ - '<@(libraries)', - ], - - 'xcode_settings': { - 'SDKROOT': 'macosx', - 'SUPPORTED_PLATFORMS':'macosx', - 'OTHER_CPLUSPLUSFLAGS': [ '<@(cflags_cc)' ], - 'OTHER_LDFLAGS': [ '<@(ldflags)' ], - 'SDKROOT': 'macosx', - 'INFOPLIST_FILE': '../platform/osx/Info.plist', - 'CLANG_ENABLE_OBJC_ARC': 'YES' - }, - } - ] -} diff --git a/platform/osx/sdk/MGLAccountManager.m b/platform/osx/sdk/MGLAccountManager.m new file mode 100644 index 0000000000..d6bc82defe --- /dev/null +++ b/platform/osx/sdk/MGLAccountManager.m @@ -0,0 +1,66 @@ +#import "MGLAccountManager_Private.h" + +#import <mbgl/osx/MGLMapView.h> + +#import "NSBundle+MGLAdditions.h" +#import "NSProcessInfo+MGLAdditions.h" +#import "../../darwin/NSString+MGLAdditions.h" + +@interface MGLAccountManager () + +@property (atomic) NSString *accessToken; + +@end + +@implementation MGLAccountManager + +#pragma mark - Internal + ++ (void)load { + mgl_linkBundleCategory(); + mgl_linkStringCategory(); + mgl_linkProcessInfoCategory(); + + [MGLMapView restorableStateKeyPaths]; + + // Read the initial configuration from Info.plist. + NSString *accessToken = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"MGLMapboxAccessToken"]; + if (accessToken.length) { + self.accessToken = accessToken; + } +} + +// Can be called from any thread. +// ++ (instancetype)sharedManager { + static dispatch_once_t onceToken; + static MGLAccountManager *_sharedManager; + void (^setupBlock)() = ^{ + dispatch_once(&onceToken, ^{ + _sharedManager = [[self alloc] init]; + }); + }; + if (![[NSThread currentThread] isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + setupBlock(); + }); + } + else { + setupBlock(); + } + return _sharedManager; +} + ++ (void)setAccessToken:(NSString *)accessToken { + accessToken = [accessToken stringByTrimmingCharactersInSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if (![accessToken length]) return; + + [MGLAccountManager sharedManager].accessToken = accessToken; +} + ++ (NSString *)accessToken { + return [MGLAccountManager sharedManager].accessToken; +} + +@end diff --git a/platform/osx/sdk/MGLAccountManager_Private.h b/platform/osx/sdk/MGLAccountManager_Private.h new file mode 100644 index 0000000000..c0b8d2666a --- /dev/null +++ b/platform/osx/sdk/MGLAccountManager_Private.h @@ -0,0 +1,10 @@ +#import <mbgl/osx/MGLAccountManager.h> + +@interface MGLAccountManager (Private) + +/** Returns the shared instance of the `MGLAccountManager` class. */ ++ (instancetype)sharedManager; + +@property (atomic) NSString *accessToken; + +@end diff --git a/platform/osx/sdk/MGLAnnotationImage.m b/platform/osx/sdk/MGLAnnotationImage.m new file mode 100644 index 0000000000..855105fded --- /dev/null +++ b/platform/osx/sdk/MGLAnnotationImage.m @@ -0,0 +1,25 @@ +#import <mbgl/osx/MGLAnnotationImage.h> + +@interface MGLAnnotationImage () + +@property (nonatomic) NSImage *image; +@property (nonatomic) NSString *reuseIdentifier; + +@end + +@implementation MGLAnnotationImage + ++ (instancetype)annotationImageWithImage:(NSImage *)image reuseIdentifier:(NSString *)reuseIdentifier { + return [[self alloc] initWithImage:image reuseIdentifier:reuseIdentifier]; +} + +- (instancetype)initWithImage:(NSImage *)image reuseIdentifier:(NSString *)reuseIdentifier { + if (self = [super init]) { + _image = image; + _reuseIdentifier = [reuseIdentifier copy]; + _selectable = YES; + } + return self; +} + +@end diff --git a/platform/osx/sdk/MGLAttributionButton.h b/platform/osx/sdk/MGLAttributionButton.h new file mode 100644 index 0000000000..7bc0fa5357 --- /dev/null +++ b/platform/osx/sdk/MGLAttributionButton.h @@ -0,0 +1,11 @@ +#import <Cocoa/Cocoa.h> + +@interface MGLAttributionButton : NSButton + +- (instancetype)initWithTitle:(NSString *)title URL:(NSURL *)url; + +@property (nonatomic) NSURL *URL; + +- (IBAction)openURL:(id)sender; + +@end diff --git a/platform/osx/sdk/MGLAttributionButton.m b/platform/osx/sdk/MGLAttributionButton.m new file mode 100644 index 0000000000..642f71f608 --- /dev/null +++ b/platform/osx/sdk/MGLAttributionButton.m @@ -0,0 +1,46 @@ +#import "MGLAttributionButton.h" + +@implementation MGLAttributionButton { + NSTrackingRectTag _trackingAreaTag; +} + +- (instancetype)initWithTitle:(NSString *)title URL:(NSURL *)url { + if (self = [super initWithFrame:NSZeroRect]) { + self.bordered = NO; + self.bezelStyle = NSRegularSquareBezelStyle; + + NSMutableAttributedString *attributedTitle = [[NSMutableAttributedString alloc] initWithString:@"© " + attributes:@{ + NSFontAttributeName: [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSMiniControlSize]], + }]; + [attributedTitle appendAttributedString: + [[NSAttributedString alloc] initWithString:title + attributes:@{ + NSFontAttributeName: [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSMiniControlSize]], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle), + }]]; + self.attributedTitle = attributedTitle; + [self sizeToFit]; + + _URL = url; + self.toolTip = _URL.absoluteString; + + self.target = self; + self.action = @selector(openURL:); + } + return self; +} + +- (BOOL)wantsLayer { + return YES; +} + +- (void)resetCursorRects { + [self addCursorRect:self.bounds cursor:[NSCursor pointingHandCursor]]; +} + +- (IBAction)openURL:(__unused id)sender { + [[NSWorkspace sharedWorkspace] openURL:self.URL]; +} + +@end diff --git a/platform/osx/sdk/MGLCompassCell.h b/platform/osx/sdk/MGLCompassCell.h new file mode 100644 index 0000000000..8c89b43e18 --- /dev/null +++ b/platform/osx/sdk/MGLCompassCell.h @@ -0,0 +1,5 @@ +#import <Cocoa/Cocoa.h> + +@interface MGLCompassCell : NSSliderCell + +@end diff --git a/platform/osx/sdk/MGLCompassCell.m b/platform/osx/sdk/MGLCompassCell.m new file mode 100644 index 0000000000..88911de2ff --- /dev/null +++ b/platform/osx/sdk/MGLCompassCell.m @@ -0,0 +1,31 @@ +#import "MGLCompassCell.h" + +@implementation MGLCompassCell + +- (instancetype)init { + if (self = [super init]) { + self.sliderType = NSCircularSlider; + self.numberOfTickMarks = 4; + self.minValue = -360; + self.maxValue = 0; + } + return self; +} + +- (void)drawKnob:(NSRect)knobRect { + NSBezierPath *trianglePath = [NSBezierPath bezierPath]; + [trianglePath moveToPoint:NSMakePoint(NSMinX(knobRect), NSMaxY(knobRect))]; + [trianglePath lineToPoint:NSMakePoint(NSMaxX(knobRect), NSMaxY(knobRect))]; + [trianglePath lineToPoint:NSMakePoint(NSMidX(knobRect), NSMinY(knobRect))]; + [trianglePath closePath]; + NSAffineTransform *transform = [NSAffineTransform transform]; + [transform translateXBy:NSMidX(knobRect) yBy:NSMidY(knobRect)]; + [transform scaleBy:0.8]; + [transform rotateByDegrees:self.doubleValue]; + [transform translateXBy:-NSMidX(knobRect) yBy:-NSMidY(knobRect)]; + [trianglePath transformUsingAffineTransform:transform]; + [[NSColor redColor] setFill]; + [trianglePath fill]; +} + +@end diff --git a/platform/osx/sdk/MGLMapView+IBAdditions.m b/platform/osx/sdk/MGLMapView+IBAdditions.m new file mode 100644 index 0000000000..504bc789de --- /dev/null +++ b/platform/osx/sdk/MGLMapView+IBAdditions.m @@ -0,0 +1,114 @@ +#import <mbgl/osx/MGLMapView+IBAdditions.h> + +#import "MGLMapView_Private.h" + +@implementation MGLMapView (IBAdditions) + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingStyleURL__ { + return [NSSet setWithObject:@"styleURL"]; +} + +- (nullable NSString *)styleURL__ { + return self.styleURL.absoluteString; +} + +- (void)setStyleURL__:(nullable NSString *)URLString { + URLString = [URLString stringByTrimmingCharactersInSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSURL *url = URLString.length ? [NSURL URLWithString:URLString] : nil; + if (URLString.length && !url) { + [NSException raise:@"Invalid style URL" + format:@"“%@” is not a valid style URL.", URLString]; + } + self.styleURL = url; +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingLatitude { + return [NSSet setWithObjects:@"centerCoordinate", nil]; +} + +- (double)latitude { + return self.centerCoordinate.latitude; +} + +- (void)setLatitude:(double)latitude { + if (!isnan(self.pendingLongitude)) { + self.centerCoordinate = CLLocationCoordinate2DMake(latitude, self.pendingLongitude); + self.pendingLatitude = NAN; + self.pendingLongitude = NAN; + } else { + // Not enough info to make a valid center coordinate yet. Stash this + // latitude away until the longitude is set too. + self.pendingLatitude = latitude; + } +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingLongitude { + return [NSSet setWithObjects:@"centerCoordinate", nil]; +} + +- (double)longitude { + return self.centerCoordinate.longitude; +} + +- (void)setLongitude:(double)longitude { + if (!isnan(self.pendingLatitude)) { + self.centerCoordinate = CLLocationCoordinate2DMake(self.pendingLatitude, longitude); + self.pendingLatitude = NAN; + self.pendingLongitude = NAN; + } else { + // Not enough info to make a valid center coordinate yet. Stash this + // longitude away until the latitude is set too. + self.pendingLongitude = longitude; + } +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingAllowsZooming { + return [NSSet setWithObject:@"zoomEnabled"]; +} + +- (BOOL)allowsZooming { + return self.zoomEnabled; +} + +- (void)setAllowsZooming:(BOOL)allowsZooming { + self.zoomEnabled = allowsZooming; +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingAllowsScrolling { + return [NSSet setWithObject:@"scrollEnabled"]; +} + +- (BOOL)allowsScrolling { + return self.scrollEnabled; +} + +- (void)setAllowsScrolling:(BOOL)allowsScrolling { + self.scrollEnabled = allowsScrolling; +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingAllowsRotating { + return [NSSet setWithObject:@"rotateEnabled"]; +} + +- (BOOL)allowsRotating { + return self.rotateEnabled; +} + +- (void)setAllowsRotating:(BOOL)allowsRotating { + self.rotateEnabled = allowsRotating; +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingAllowsTilting { + return [NSSet setWithObject:@"pitchEnabled"]; +} + +- (BOOL)allowsTilting { + return self.pitchEnabled; +} + +- (void)setAllowsTilting:(BOOL)allowsTilting { + self.pitchEnabled = allowsTilting; +} + +@end diff --git a/platform/osx/sdk/MGLMapView.mm b/platform/osx/sdk/MGLMapView.mm new file mode 100644 index 0000000000..c6be45c5f7 --- /dev/null +++ b/platform/osx/sdk/MGLMapView.mm @@ -0,0 +1,1636 @@ +#import "MGLMapView_Private.h" +#import "MGLAccountManager_Private.h" +#import "MGLAttributionButton.h" +#import "MGLCompassCell.h" +#import "MGLOpenGLLayer.h" +#import "MGLStyle.h" + +#import "../../darwin/MGLGeometry_Private.h" +#import "../../darwin/MGLMultiPoint_Private.h" + +#import <mbgl/darwin/MGLPolygon.h> +#import <mbgl/darwin/MGLPolyline.h> +#import <mbgl/osx/MGLAnnotationImage.h> +#import <mbgl/osx/MGLMapViewDelegate.h> + +#import <mbgl/mbgl.hpp> +#import <mbgl/annotation/point_annotation.hpp> +#import <mbgl/map/camera.hpp> +#import <mbgl/platform/darwin/reachability.h> +#import <mbgl/platform/gl.hpp> +#import <mbgl/sprite/sprite_image.hpp> +#import <mbgl/storage/default_file_source.hpp> +#import <mbgl/storage/network_status.hpp> +#import <mbgl/storage/sqlite_cache.hpp> +#import <mbgl/util/constants.hpp> +#import <mbgl/util/math.hpp> +#import <mbgl/util/std.hpp> + +#import <map> +#import <unordered_set> + +#import "NSBundle+MGLAdditions.h" +#import "NSProcessInfo+MGLAdditions.h" +#import "../../darwin/NSException+MGLAdditions.h" +#import "../../darwin/NSString+MGLAdditions.h" + +#import <QuartzCore/QuartzCore.h> + +class MGLMapViewImpl; +class MGLAnnotationContext; + +const CGFloat MGLOrnamentPadding = 12; + +const NSTimeInterval MGLAnimationDuration = 0.3; +const CGFloat MGLKeyPanningIncrement = 150; +const CLLocationDegrees MGLKeyRotationIncrement = 25; + +static NSString * const MGLVendorDirectoryName = @"com.mapbox.MapboxGL"; + +static NSString * const MGLDefaultStyleMarkerSymbolName = @"default_marker"; +static NSString * const MGLAnnotationSpritePrefix = @"com.mapbox.sprites."; +const CGFloat MGLAnnotationImagePaddingForHitTest = 4; +const CGFloat MGLAnnotationImagePaddingForCallout = 4; + +struct MGLAttribution { + NSString *title; + NSString *urlString; +} MGLAttributions[] = { + { @"Mapbox", @"https://www.mapbox.com/about/maps/" }, + { @"OpenStreetMap", @"http://www.openstreetmap.org/about/" }, +}; + +typedef uint32_t MGLAnnotationID; +enum { MGLAnnotationNotFound = UINT32_MAX }; +typedef std::map<MGLAnnotationID, MGLAnnotationContext> MGLAnnotationContextMap; + +NSImage *MGLDefaultMarkerImage() { + NSString *path = [[NSBundle mgl_resourceBundle] pathForResource:MGLDefaultStyleMarkerSymbolName + ofType:@"pdf"]; + return [[NSImage alloc] initWithContentsOfFile:path]; +} + +std::chrono::steady_clock::duration MGLDurationInSeconds(float duration) { + return std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<float, std::chrono::seconds::period>(duration)); +} + +mbgl::Color MGLColorObjectFromNSColor(NSColor *color) { + if (!color) { + return {{ 0, 0, 0, 0 }}; + } + CGFloat r, g, b, a; + [[color colorUsingColorSpaceName:NSCalibratedRGBColorSpace] getRed:&r green:&g blue:&b alpha:&a]; + return {{ (float)r, (float)g, (float)b, (float)a }}; +} + +class MGLAnnotationContext { +public: + id <MGLAnnotation> annotation; + NSString *symbolIdentifier; +}; + +@interface MGLMapView () <NSPopoverDelegate, MGLMultiPointDelegate> + +@property (nonatomic, readwrite) NSSegmentedControl *zoomControls; +@property (nonatomic, readwrite) NSSlider *compass; +@property (nonatomic, readwrite) NSImageView *logoView; +@property (nonatomic, readwrite) NSView *attributionView; + +@property (nonatomic) NS_MUTABLE_DICTIONARY_OF(NSString *, MGLAnnotationImage *) *annotationImagesByIdentifier; +@property (nonatomic) NSPopover *calloutForSelectedAnnotation; + +@property (nonatomic, readwrite, getter=isDormant) BOOL dormant; + +@end + +@implementation MGLMapView { + mbgl::Map *_mbglMap; + MGLMapViewImpl *_mbglView; + std::shared_ptr<mbgl::SQLiteCache> _mbglFileCache; + mbgl::DefaultFileSource *_mbglFileSource; + + NSMagnificationGestureRecognizer *_magnificationGestureRecognizer; + NSRotationGestureRecognizer *_rotationGestureRecognizer; + double _scaleAtBeginningOfGesture; + CLLocationDirection _directionAtBeginningOfGesture; + CGFloat _pitchAtBeginningOfGesture; + + MGLAnnotationContextMap _annotationContextsByAnnotationID; + MGLAnnotationID _selectedAnnotationID; + NSRect _unionedAnnotationImageAlignmentRect; + std::vector<MGLAnnotationID> _annotationsNearbyLastClick; + BOOL _delegateHasAlphasForShapeAnnotations; + BOOL _delegateHasStrokeColorsForShapeAnnotations; + BOOL _delegateHasFillColorsForShapeAnnotations; + BOOL _delegateHasLineWidthsForShapeAnnotations; + + BOOL _isTargetingInterfaceBuilder; + CLLocationDegrees _pendingLatitude; + CLLocationDegrees _pendingLongitude; +} + +#pragma mark Lifecycle + +- (instancetype)initWithFrame:(NSRect)frameRect { + if (self = [super initWithFrame:frameRect]) { + [self commonInit]; + self.styleURL = nil; + } + return self; +} + +- (instancetype)initWithFrame:(CGRect)frame styleURL:(nullable NSURL *)styleURL { + if (self = [super initWithFrame:frame]) { + [self commonInit]; + self.styleURL = styleURL; + } + return self; +} + +- (instancetype)initWithCoder:(nonnull NSCoder *)decoder { + if (self = [super initWithCoder:decoder]) { + [self commonInit]; + } + return self; +} + +- (void)awakeFromNib { + [super awakeFromNib]; + + self.styleURL = nil; +} + ++ (NSArray *)restorableStateKeyPaths { + return @[@"zoomLevel", @"direction"]; +} + +- (void)commonInit { + _isTargetingInterfaceBuilder = NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent; + + _mbglView = new MGLMapViewImpl(self, [NSScreen mainScreen].backingScaleFactor); + + // Place the cache in a location that can be shared among all the + // applications that embed the Mapbox OS X SDK. + NSURL *cacheDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory + inDomain:NSUserDomainMask + appropriateForURL:nil + create:YES + error:nil]; + cacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent:MGLVendorDirectoryName]; + [[NSFileManager defaultManager] createDirectoryAtURL:cacheDirectoryURL + withIntermediateDirectories:YES + attributes:nil + error:nil]; + NSURL *cacheURL = [cacheDirectoryURL URLByAppendingPathComponent:@"cache.db"]; + NSString *cachePath = cacheURL ? cacheURL.path : @""; + _mbglFileCache = mbgl::SharedSQLiteCache::get(cachePath.UTF8String); + _mbglFileSource = new mbgl::DefaultFileSource(_mbglFileCache.get()); + + _mbglMap = new mbgl::Map(*_mbglView, *_mbglFileSource, mbgl::MapMode::Continuous); + + self.layer = _isTargetingInterfaceBuilder ? [CALayer layer] : [MGLOpenGLLayer layer]; + + // Observe for changes to the global access token (and find out the current one). + [[MGLAccountManager sharedManager] addObserver:self + forKeyPath:@"accessToken" + options:(NSKeyValueObservingOptionInitial | + NSKeyValueObservingOptionNew) + context:NULL]; + + // Notify map object when network reachability status changes. + MGLReachability *reachability = [MGLReachability reachabilityForInternetConnection]; + reachability.reachableBlock = ^(MGLReachability *) { + mbgl::NetworkStatus::Reachable(); + }; + [reachability startNotifier]; + + [self installZoomControls]; + [self installCompass]; + [self installLogoView]; + [self installAttributionView]; + [self installGestureRecognizers]; + + _annotationImagesByIdentifier = [NSMutableDictionary dictionary]; + _annotationContextsByAnnotationID = {}; + _selectedAnnotationID = MGLAnnotationNotFound; + _unionedAnnotationImageAlignmentRect = NSZeroRect; + + mbgl::CameraOptions options; + options.center = mbgl::LatLng(0, 0); + options.zoom = _mbglMap->getMinZoom(); + _mbglMap->jumpTo(options); +} + +- (void)installZoomControls { + _zoomControls = [[NSSegmentedControl alloc] initWithFrame:NSZeroRect]; + _zoomControls.wantsLayer = YES; + _zoomControls.layer.opacity = 0.9; + [(NSSegmentedCell *)_zoomControls.cell setTrackingMode:NSSegmentSwitchTrackingMomentary]; + _zoomControls.continuous = YES; + _zoomControls.segmentCount = 2; + [_zoomControls setLabel:@"−" forSegment:0]; + [(NSSegmentedCell *)_zoomControls.cell setTag:0 forSegment:0]; + [(NSSegmentedCell *)_zoomControls.cell setToolTip:@"Zoom Out" forSegment:0]; + [_zoomControls setLabel:@"+" forSegment:1]; + [(NSSegmentedCell *)_zoomControls.cell setTag:1 forSegment:1]; + [(NSSegmentedCell *)_zoomControls.cell setToolTip:@"Zoom In" forSegment:1]; + _zoomControls.target = self; + _zoomControls.action = @selector(zoomInOrOut:); + _zoomControls.controlSize = NSRegularControlSize; + [_zoomControls sizeToFit]; + _zoomControls.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:_zoomControls]; +} + +- (void)installCompass { + _compass = [[NSSlider alloc] initWithFrame:NSZeroRect]; + _compass.wantsLayer = YES; + _compass.layer.opacity = 0.9; + _compass.cell = [[MGLCompassCell alloc] init]; + _compass.continuous = YES; + _compass.target = self; + _compass.action = @selector(rotate:); + [_compass sizeToFit]; + _compass.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:_compass]; +} + +- (void)installLogoView { + _logoView = [[NSImageView alloc] initWithFrame:NSZeroRect]; + _logoView.wantsLayer = YES; + NSImage *logoImage = [[NSImage alloc] initWithContentsOfFile: + [[NSBundle mgl_resourceBundle] pathForResource:@"mapbox" ofType:@"pdf"]]; + logoImage.alignmentRect = NSInsetRect(logoImage.alignmentRect, 3, 3); + _logoView.image = logoImage; + _logoView.translatesAutoresizingMaskIntoConstraints = NO; + _logoView.accessibilityTitle = @"Mapbox"; + [self addSubview:_logoView]; +} + +- (void)installAttributionView { + _attributionView = [[NSView alloc] initWithFrame:NSZeroRect]; + _attributionView.wantsLayer = YES; + _attributionView.layer.opacity = 0.6; + CIFilter *attributionBlurFilter = [CIFilter filterWithName:@"CIGaussianBlur"]; + [attributionBlurFilter setDefaults]; + CIFilter *attributionColorFilter = [CIFilter filterWithName:@"CIColorControls"]; + [attributionColorFilter setDefaults]; + [attributionColorFilter setValue:@(0.1) forKey:kCIInputBrightnessKey]; + _attributionView.backgroundFilters = @[attributionColorFilter, attributionBlurFilter]; + _attributionView.layer.cornerRadius = 4; + _attributionView.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:_attributionView]; + [self updateAttributionView]; +} + +- (void)installGestureRecognizers { + self.acceptsTouchEvents = YES; + _scrollEnabled = YES; + _zoomEnabled = YES; + _rotateEnabled = YES; + _pitchEnabled = YES; + + NSPanGestureRecognizer *panGestureRecognizer = [[NSPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)]; + panGestureRecognizer.delaysKeyEvents = YES; + [self addGestureRecognizer:panGestureRecognizer]; + + NSClickGestureRecognizer *clickGestureRecognizer = [[NSClickGestureRecognizer alloc] initWithTarget:self action:@selector(handleClickGesture:)]; + clickGestureRecognizer.delaysPrimaryMouseButtonEvents = NO; + [self addGestureRecognizer:clickGestureRecognizer]; + + NSClickGestureRecognizer *secondaryClickGestureRecognizer = [[NSClickGestureRecognizer alloc] initWithTarget:self action:@selector(handleSecondaryClickGesture:)]; + secondaryClickGestureRecognizer.buttonMask = 0x2; + [self addGestureRecognizer:secondaryClickGestureRecognizer]; + + NSClickGestureRecognizer *doubleClickGestureRecognizer = [[NSClickGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleClickGesture:)]; + doubleClickGestureRecognizer.numberOfClicksRequired = 2; + doubleClickGestureRecognizer.delaysPrimaryMouseButtonEvents = NO; + [self addGestureRecognizer:doubleClickGestureRecognizer]; + + _magnificationGestureRecognizer = [[NSMagnificationGestureRecognizer alloc] initWithTarget:self action:@selector(handleMagnificationGesture:)]; + [self addGestureRecognizer:_magnificationGestureRecognizer]; + + _rotationGestureRecognizer = [[NSRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotationGesture:)]; + [self addGestureRecognizer:_rotationGestureRecognizer]; +} + +- (void)updateAttributionView { + self.attributionView.subviews = @[]; + + for (NSUInteger i = 0; i < sizeof(MGLAttributions) / sizeof(MGLAttributions[0]); i++) { + NSURL *url = [NSURL URLWithString:MGLAttributions[i].urlString]; + NSButton *button = [[MGLAttributionButton alloc] initWithTitle:MGLAttributions[i].title URL:url]; + button.controlSize = NSMiniControlSize; + button.translatesAutoresizingMaskIntoConstraints = NO; + + NSView *previousView = self.attributionView.subviews.lastObject; + [self.attributionView addSubview:button]; + + [_attributionView addConstraint: + [NSLayoutConstraint constraintWithItem:button + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:_attributionView + attribute:NSLayoutAttributeBottom + multiplier:1 + constant:0]]; + [_attributionView addConstraint: + [NSLayoutConstraint constraintWithItem:button + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:previousView ? previousView : _attributionView + attribute:previousView ? NSLayoutAttributeTrailing : NSLayoutAttributeLeading + multiplier:1 + constant:8]]; + } +} + +- (void)dealloc { + [[MGLAccountManager sharedManager] removeObserver:self forKeyPath:@"accessToken"]; + + [self.calloutForSelectedAnnotation close]; + self.calloutForSelectedAnnotation = nil; + + if (_mbglMap) { + delete _mbglMap; + _mbglMap = nullptr; + } + if (_mbglFileSource) { + delete _mbglFileSource; + _mbglFileSource = nullptr; + } + if (_mbglView) { + delete _mbglView; + _mbglView = nullptr; + } +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(__unused void *)context { + // Synchronize mbgl::Map’s access token with the global one in MGLAccountManager. + if ([keyPath isEqualToString:@"accessToken"] && object == [MGLAccountManager sharedManager]) { + NSString *accessToken = change[NSKeyValueChangeNewKey]; + if (![accessToken isKindOfClass:[NSNull class]]) { + _mbglFileSource->setAccessToken((std::string)[accessToken UTF8String]); + } + } +} + +- (void)setDelegate:(id<MGLMapViewDelegate>)delegate { + _delegate = delegate; + + _delegateHasAlphasForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:alphaForShapeAnnotation:)]; + _delegateHasStrokeColorsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:strokeColorForShapeAnnotation:)]; + _delegateHasFillColorsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:fillColorForPolygonAnnotation:)]; + _delegateHasLineWidthsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:lineWidthForPolylineAnnotation:)]; +} + +#pragma mark Style + +- (nonnull NSURL *)styleURL { + NSString *styleURLString = @(_mbglMap->getStyleURL().c_str()).mgl_stringOrNilIfEmpty; + return styleURLString ? [NSURL URLWithString:styleURLString] : [MGLStyle streetsStyleURL]; +} + +- (void)setStyleURL:(nullable NSURL *)styleURL { + if (_isTargetingInterfaceBuilder) { + return; + } + + if (!styleURL) { + if (![MGLAccountManager accessToken]) { + return; + } + styleURL = [MGLStyle streetsStyleURL]; + } + + if (![styleURL scheme]) { + // Assume a relative path into the application’s resource folder. + styleURL = [NSURL URLWithString:[@"asset://" stringByAppendingString:[styleURL absoluteString]]]; + } + + _mbglMap->setStyleURL([[styleURL absoluteString] UTF8String]); +} + +- (IBAction)reloadStyle:(__unused id)sender { + NSURL *styleURL = self.styleURL; + _mbglMap->setStyleURL(""); + self.styleURL = styleURL; +} + +#pragma mark View hierarchy and drawing + +- (void)viewWillMoveToWindow:(NSWindow *)newWindow { + [self deselectAnnotation:self.selectedAnnotation animated:NO]; + if (!self.dormant && !newWindow) { + self.dormant = YES; + _mbglMap->pause(); + } +} + +- (void)viewDidMoveToWindow { + if (self.dormant && self.window) { + _mbglMap->resume(); + self.dormant = NO; + } +} + +- (BOOL)wantsLayer { + return YES; +} + +- (BOOL)wantsBestResolutionOpenGLSurface { + return !_isTargetingInterfaceBuilder; +} + +- (void)setFrame:(NSRect)frame { + super.frame = frame; + if (!_isTargetingInterfaceBuilder) { + _mbglMap->update(mbgl::Update::Dimensions); + } +} + +- (void)updateConstraints { + [self addConstraint: + [NSLayoutConstraint constraintWithItem:self + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:_zoomControls + attribute:NSLayoutAttributeBottom + multiplier:1 + constant:MGLOrnamentPadding]]; + [self addConstraint: + [NSLayoutConstraint constraintWithItem:self + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:_zoomControls + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:MGLOrnamentPadding]]; + + [self addConstraint: + [NSLayoutConstraint constraintWithItem:_compass + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:_zoomControls + attribute:NSLayoutAttributeCenterX + multiplier:1 + constant:0]]; + [self addConstraint: + [NSLayoutConstraint constraintWithItem:_zoomControls + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:_compass + attribute:NSLayoutAttributeBottom + multiplier:1 + constant:8]]; + + [self addConstraint: + [NSLayoutConstraint constraintWithItem:self + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:_logoView + attribute:NSLayoutAttributeBottom + multiplier:1 + constant:MGLOrnamentPadding - _logoView.image.alignmentRect.origin.y]]; + [self addConstraint: + [NSLayoutConstraint constraintWithItem:_logoView + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeLeading + multiplier:1 + constant:MGLOrnamentPadding - _logoView.image.alignmentRect.origin.x]]; + + [self addConstraint:[NSLayoutConstraint constraintWithItem:_logoView + attribute:NSLayoutAttributeBaseline + relatedBy:NSLayoutRelationEqual + toItem:_attributionView + attribute:NSLayoutAttributeBaseline + multiplier:1 + constant:_logoView.image.alignmentRect.origin.y]]; + [self addConstraint:[NSLayoutConstraint constraintWithItem:_attributionView + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:_logoView + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:8]]; + [self addConstraint:[NSLayoutConstraint constraintWithItem:_attributionView.subviews.firstObject + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:_attributionView + attribute:NSLayoutAttributeTop + multiplier:1 + constant:0]]; + [self addConstraint:[NSLayoutConstraint constraintWithItem:_attributionView + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:_attributionView.subviews.lastObject + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:8]]; + + [super updateConstraints]; +} + +- (void)renderSync { + if (!self.dormant) { + CGFloat zoomFactor = _mbglMap->getMaxZoom() - _mbglMap->getMinZoom() + 1; + CGFloat cpuFactor = (CGFloat)[NSProcessInfo processInfo].processorCount; + CGFloat memoryFactor = (CGFloat)[NSProcessInfo processInfo].physicalMemory / 1000 / 1000 / 1000; + CGFloat sizeFactor = ((CGFloat)_mbglMap->getWidth() / mbgl::util::tileSize) * ((CGFloat)_mbglMap->getHeight() / mbgl::util::tileSize); + + NSUInteger cacheSize = zoomFactor * cpuFactor * memoryFactor * sizeFactor * 0.5; + + _mbglMap->setSourceTileCacheSize(cacheSize); + _mbglMap->renderSync(); + +// [self updateUserLocationAnnotationView]; + } +} + +- (void)invalidate { + MGLAssertIsMainThread(); + + [self.layer setNeedsDisplay]; +} + +- (void)notifyMapChange:(mbgl::MapChange)change { + // Ignore map updates when the Map object isn't set. + if (!_mbglMap) { + return; + } + + switch (change) { + case mbgl::MapChangeRegionWillChange: + case mbgl::MapChangeRegionWillChangeAnimated: + { + if ([self.delegate respondsToSelector:@selector(mapView:regionWillChangeAnimated:)]) { + BOOL animated = change == mbgl::MapChangeRegionWillChangeAnimated; + [self.delegate mapView:self regionWillChangeAnimated:animated]; + } + break; + } + case mbgl::MapChangeRegionIsChanging: + { + [self updateCompass]; + [self updateAnnotationCallouts]; + + if ([self.delegate respondsToSelector:@selector(mapViewRegionIsChanging:)]) { + [self.delegate mapViewRegionIsChanging:self]; + } + break; + } + case mbgl::MapChangeRegionDidChange: + case mbgl::MapChangeRegionDidChangeAnimated: + { + [self updateZoomControls]; + [self updateCompass]; + [self updateAnnotationCallouts]; + + if ([self.delegate respondsToSelector:@selector(mapView:regionDidChangeAnimated:)]) { + BOOL animated = change == mbgl::MapChangeRegionDidChangeAnimated; + [self.delegate mapView:self regionDidChangeAnimated:animated]; + } + break; + } + case mbgl::MapChangeWillStartLoadingMap: + { + if ([self.delegate respondsToSelector:@selector(mapViewWillStartLoadingMap:)]) { + [self.delegate mapViewWillStartLoadingMap:self]; + } + break; + } + case mbgl::MapChangeDidFinishLoadingMap: + { + if ([self.delegate respondsToSelector:@selector(mapViewDidFinishLoadingMap:)]) { + [self.delegate mapViewDidFinishLoadingMap:self]; + } + break; + } + case mbgl::MapChangeDidFailLoadingMap: + { + // Not yet implemented. + break; + } + case mbgl::MapChangeWillStartRenderingMap: + { + if ([self.delegate respondsToSelector:@selector(mapViewWillStartRenderingMap:)]) { + [self.delegate mapViewWillStartRenderingMap:self]; + } + break; + } + case mbgl::MapChangeDidFinishRenderingMap: + case mbgl::MapChangeDidFinishRenderingMapFullyRendered: + { + if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingMap:fullyRendered:)]) { + BOOL fullyRendered = change == mbgl::MapChangeDidFinishRenderingMapFullyRendered; + [self.delegate mapViewDidFinishRenderingMap:self fullyRendered:fullyRendered]; + } + break; + } + case mbgl::MapChangeWillStartRenderingFrame: + { + if ([self.delegate respondsToSelector:@selector(mapViewWillStartRenderingFrame:)]) { + [self.delegate mapViewWillStartRenderingFrame:self]; + } + break; + } + case mbgl::MapChangeDidFinishRenderingFrame: + case mbgl::MapChangeDidFinishRenderingFrameFullyRendered: + { + if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingFrame:fullyRendered:)]) { + BOOL fullyRendered = change == mbgl::MapChangeDidFinishRenderingFrameFullyRendered; + [self.delegate mapViewDidFinishRenderingFrame:self fullyRendered:fullyRendered]; + } + break; + } + } +} + +#pragma mark Viewport + +- (CLLocationCoordinate2D)centerCoordinate { + return MGLLocationCoordinate2DFromLatLng(_mbglMap->getLatLng()); +} + +- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate { + [self setCenterCoordinate:centerCoordinate animated:NO]; +} + +- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate animated:(BOOL)animated { + [self willChangeValueForKey:@"centerCoordinate"]; + _mbglMap->setLatLng(MGLLatLngFromLocationCoordinate2D(centerCoordinate), + MGLDurationInSeconds(animated ? MGLAnimationDuration : 0)); + [self didChangeValueForKey:@"centerCoordinate"]; +} + +- (void)offsetCenterCoordinateBy:(NSPoint)delta animated:(BOOL)animated { + [self willChangeValueForKey:@"centerCoordinate"]; + _mbglMap->cancelTransitions(); + _mbglMap->moveBy({ delta.x, delta.y }, + MGLDurationInSeconds(animated ? MGLAnimationDuration : 0)); + [self didChangeValueForKey:@"centerCoordinate"]; +} + +- (double)zoomLevel { + return _mbglMap->getZoom(); +} + +- (void)setZoomLevel:(double)zoomLevel { + [self setZoomLevel:zoomLevel animated:NO]; +} + +- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated { + _mbglMap->setZoom(zoomLevel, MGLDurationInSeconds(animated ? MGLAnimationDuration : 0)); +} + +- (void)scaleBy:(double)scaleFactor atPoint:(NSPoint)point animated:(BOOL)animated { + [self willChangeValueForKey:@"zoomLevel"]; + mbgl::PrecisionPoint center(point.x, point.y); + _mbglMap->scaleBy(scaleFactor, center, MGLDurationInSeconds(animated ? MGLAnimationDuration : 0)); + [self didChangeValueForKey:@"zoomLevel"]; +} + +- (double)maximumZoomLevel { + return _mbglMap->getMaxZoom(); +} + +- (double)minimumZoomLevel { + return _mbglMap->getMinZoom(); +} + +- (IBAction)zoomInOrOut:(NSSegmentedControl *)sender { + switch (sender.selectedSegment) { + case 0: + [self moveToEndOfParagraph:sender]; + break; + case 1: + [self moveToBeginningOfParagraph:sender]; + break; + default: + break; + } +} + +- (CLLocationDirection)direction { + return mbgl::util::wrap(_mbglMap->getBearing(), 0., 360.); +} + +- (void)setDirection:(CLLocationDirection)direction { + [self setDirection:direction animated:NO]; +} + +- (void)setDirection:(CLLocationDirection)direction animated:(BOOL)animated { + [self willChangeValueForKey:@"direction"]; + _mbglMap->setBearing(direction, MGLDurationInSeconds(animated ? MGLAnimationDuration : 0)); + [self didChangeValueForKey:@"direction"]; +} + +- (void)offsetDirectionBy:(CLLocationDegrees)delta animated:(BOOL)animated { + [self willChangeValueForKey:@"direction"]; + _mbglMap->cancelTransitions(); + _mbglMap->setBearing(_mbglMap->getBearing() + delta, MGLDurationInSeconds(animated ? MGLAnimationDuration : 0)); + [self didChangeValueForKey:@"direction"]; +} + ++ (NSSet *)keyPathsForValuesAffectingVisibleCoordinateBounds { + return [NSSet setWithObjects:@"centerCoordinate", @"zoomLevel", @"direction", @"bounds", nil]; +} + +- (MGLCoordinateBounds)visibleCoordinateBounds { + return [self convertRectToCoordinateBounds:self.bounds]; +} + +- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds { + [self setVisibleCoordinateBounds:bounds animated:NO]; +} + +- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds animated:(BOOL)animated { + [self setVisibleCoordinateBounds:bounds edgePadding:NSEdgeInsetsZero animated:animated]; +} + +- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated { + _mbglMap->cancelTransitions(); + + mbgl::EdgeInsets mbglInsets = {insets.top, insets.left, insets.bottom, insets.right}; + mbgl::CameraOptions options = _mbglMap->cameraForLatLngBounds(MGLLatLngBoundsFromCoordinateBounds(bounds), mbglInsets); + if (animated) { + options.duration = MGLDurationInSeconds(MGLAnimationDuration); + } + + [self willChangeValueForKey:@"visibleCoordinateBounds"]; + options.transitionFinishFn = ^() { + [self didChangeValueForKey:@"visibleCoordinateBounds"]; + }; + _mbglMap->easeTo(options); +} + +#pragma mark Mouse events and gestures + +- (BOOL)acceptsFirstResponder { + return YES; +} + +- (void)handlePanGesture:(NSPanGestureRecognizer *)gestureRecognizer { + NSPoint delta = [gestureRecognizer translationInView:self]; + NSPoint endPoint = [gestureRecognizer locationInView:self]; + NSPoint startPoint = NSMakePoint(endPoint.x - delta.x, self.bounds.size.height - (endPoint.y - delta.y)); + + NSEventModifierFlags flags = [NSApp currentEvent].modifierFlags; + if (flags & NSShiftKeyMask) { + if (!self.zoomEnabled) { + return; + } + + _mbglMap->cancelTransitions(); + + if (gestureRecognizer.state == NSGestureRecognizerStateBegan) { + _mbglMap->setGestureInProgress(true); + _scaleAtBeginningOfGesture = _mbglMap->getScale(); + } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) { + CGFloat newZoomLevel = log2f(_scaleAtBeginningOfGesture) - delta.y / 75; + [self scaleBy:powf(2, newZoomLevel) / _mbglMap->getScale() atPoint:startPoint animated:NO]; + } else if (gestureRecognizer.state == NSGestureRecognizerStateEnded + || gestureRecognizer.state == NSGestureRecognizerStateCancelled) { + _mbglMap->setGestureInProgress(false); + // Maps.app locks the cursor to the start point, but that would + // interfere with the pan gesture recognizer. Just move the cursor + // back at the end of the gesture. + CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, startPoint); + } + } else if (flags & NSAlternateKeyMask) { + _mbglMap->cancelTransitions(); + + if (gestureRecognizer.state == NSGestureRecognizerStateBegan) { + _mbglMap->setGestureInProgress(true); + _directionAtBeginningOfGesture = self.direction; + _pitchAtBeginningOfGesture = _mbglMap->getPitch(); + } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) { + mbgl::PrecisionPoint center(startPoint.x, startPoint.y); + if (self.rotateEnabled) { + CLLocationDirection newDirection = _directionAtBeginningOfGesture - delta.x / 10; + [self willChangeValueForKey:@"direction"]; + _mbglMap->setBearing(newDirection, center); + [self didChangeValueForKey:@"direction"]; + } + if (self.pitchEnabled) { + _mbglMap->setPitch(_pitchAtBeginningOfGesture + delta.y / 5); + } + } else if (gestureRecognizer.state == NSGestureRecognizerStateEnded + || gestureRecognizer.state == NSGestureRecognizerStateCancelled) { + _mbglMap->setGestureInProgress(false); + } + } else if (self.scrollEnabled) { + _mbglMap->cancelTransitions(); + + if (gestureRecognizer.state == NSGestureRecognizerStateBegan) { + [[NSCursor closedHandCursor] push]; + _mbglMap->setGestureInProgress(true); + } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) { + delta.y *= -1; + [self offsetCenterCoordinateBy:delta animated:NO]; + [gestureRecognizer setTranslation:NSZeroPoint inView:self]; + } else if (gestureRecognizer.state == NSGestureRecognizerStateEnded + || gestureRecognizer.state == NSGestureRecognizerStateCancelled) { + _mbglMap->setGestureInProgress(false); + [[NSCursor arrowCursor] pop]; + } + } +} + +- (void)handleMagnificationGesture:(NSMagnificationGestureRecognizer *)gestureRecognizer { + if (!self.zoomEnabled) { + return; + } + + _mbglMap->cancelTransitions(); + + if (gestureRecognizer.state == NSGestureRecognizerStateBegan) { + _mbglMap->setGestureInProgress(true); + _scaleAtBeginningOfGesture = _mbglMap->getScale(); + } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) { + NSPoint zoomInPoint = [gestureRecognizer locationInView:self]; + mbgl::PrecisionPoint center(zoomInPoint.x, self.bounds.size.height - zoomInPoint.y); + if (gestureRecognizer.magnification > -1) { + [self willChangeValueForKey:@"zoomLevel"]; + [self willChangeValueForKey:@"centerCoordinate"]; + _mbglMap->setScale(_scaleAtBeginningOfGesture * (1 + gestureRecognizer.magnification), center); + [self didChangeValueForKey:@"centerCoordinate"]; + [self didChangeValueForKey:@"zoomLevel"]; + } + } else if (gestureRecognizer.state == NSGestureRecognizerStateEnded + || gestureRecognizer.state == NSGestureRecognizerStateCancelled) { + _mbglMap->setGestureInProgress(false); + } +} + +- (void)handleClickGesture:(NSClickGestureRecognizer *)gestureRecognizer { + NSPoint gesturePoint = [gestureRecognizer locationInView:self]; + NSRect hitRect = NSOffsetRect(_unionedAnnotationImageAlignmentRect, + gesturePoint.x, gesturePoint.y); + hitRect = NSInsetRect(hitRect, -MGLAnnotationImagePaddingForHitTest, + -MGLAnnotationImagePaddingForHitTest); + mbgl::LatLngBounds hitBounds = [self convertRectToLatLngBounds:hitRect]; + std::vector<MGLAnnotationID> nearbyAnnotations = _mbglMap->getPointAnnotationsInBounds(hitBounds); + + if (nearbyAnnotations.size()) { + mbgl::util::erase_if(nearbyAnnotations, [&](const MGLAnnotationID annotationID) { + NSAssert(_annotationContextsByAnnotationID.count(annotationID) != 0, @"Unknown annotation found nearby click"); + NSString *customSymbol = _annotationContextsByAnnotationID[annotationID].symbolIdentifier; + NSString *symbolName = customSymbol.length ? customSymbol : MGLDefaultStyleMarkerSymbolName; + MGLAnnotationImage *annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:symbolName]; + return !annotationImage.selectable; + }); + } + + MGLAnnotationID hitAnnotationID = MGLAnnotationNotFound; + if (nearbyAnnotations.size()) { + std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end()); + + if (nearbyAnnotations == _annotationsNearbyLastClick) { + if (_selectedAnnotationID == _annotationsNearbyLastClick.back() + || _selectedAnnotationID == MGLAnnotationNotFound) { + hitAnnotationID = _annotationsNearbyLastClick.front(); + } else { + auto result = std::find(_annotationsNearbyLastClick.begin(), + _annotationsNearbyLastClick.end(), + _selectedAnnotationID); + auto distance = std::distance(_annotationsNearbyLastClick.begin(), result); + hitAnnotationID = _annotationsNearbyLastClick[distance + 1]; + } + } else { + _annotationsNearbyLastClick = nearbyAnnotations; + hitAnnotationID = _annotationsNearbyLastClick.front(); + } + } + + if (hitAnnotationID != MGLAnnotationNotFound) { + if (hitAnnotationID != _selectedAnnotationID) { + id <MGLAnnotation> annotation = [self annotationWithID:hitAnnotationID]; + NSAssert(annotation, @"Cannot select nonexistent annotation with ID %i", hitAnnotationID); + [self selectAnnotation:annotation animated:YES]; + } + } else { + [self deselectAnnotation:self.selectedAnnotation animated:YES]; + } +} + +- (void)handleSecondaryClickGesture:(NSClickGestureRecognizer *)gestureRecognizer { + if (!self.zoomEnabled) { + return; + } + + _mbglMap->cancelTransitions(); + + NSPoint gesturePoint = [gestureRecognizer locationInView:self]; + [self scaleBy:0.5 atPoint:NSMakePoint(gesturePoint.x, self.bounds.size.height - gesturePoint.y) animated:YES]; +} + +- (void)handleDoubleClickGesture:(NSClickGestureRecognizer *)gestureRecognizer { + if (!self.zoomEnabled) { + return; + } + + _mbglMap->cancelTransitions(); + + NSPoint gesturePoint = [gestureRecognizer locationInView:self]; + [self scaleBy:2 atPoint:NSMakePoint(gesturePoint.x, self.bounds.size.height - gesturePoint.y) animated:YES]; +} + +- (void)handleRotationGesture:(NSRotationGestureRecognizer *)gestureRecognizer { + if (!self.rotateEnabled) { + return; + } + + _mbglMap->cancelTransitions(); + + if (gestureRecognizer.state == NSGestureRecognizerStateBegan) { + _mbglMap->setGestureInProgress(true); + _directionAtBeginningOfGesture = self.direction; + } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) { + NSPoint rotationPoint = [gestureRecognizer locationInView:self]; + mbgl::PrecisionPoint center(rotationPoint.x, rotationPoint.y); + _mbglMap->setBearing(_directionAtBeginningOfGesture + gestureRecognizer.rotationInDegrees, center); + } else if (gestureRecognizer.state == NSGestureRecognizerStateEnded + || gestureRecognizer.state == NSGestureRecognizerStateCancelled) { + _mbglMap->setGestureInProgress(false); + } +} + +- (BOOL)wantsScrollEventsForSwipeTrackingOnAxis:(__unused NSEventGestureAxis)axis { + return YES; +} + +- (void)scrollWheel:(NSEvent *)event { + // https://developer.apple.com/library/mac/releasenotes/AppKit/RN-AppKitOlderNotes/#10_7Dragging + if (event.phase == NSEventPhaseNone && event.momentumPhase == NSEventPhaseNone) { + // A traditional, vertical scroll wheel zooms instead of panning. + if (self.zoomEnabled && std::abs(event.scrollingDeltaX) < std::abs(event.scrollingDeltaY)) { + _mbglMap->cancelTransitions(); + + [self willChangeValueForKey:@"zoomLevel"]; + [self willChangeValueForKey:@"centerCoordinate"]; + NSPoint gesturePoint = [self convertPoint:event.locationInWindow fromView:nil]; + mbgl::PrecisionPoint center(gesturePoint.x, self.bounds.size.height - gesturePoint.y); + _mbglMap->scaleBy(exp2(event.scrollingDeltaY / 20), center); + [self didChangeValueForKey:@"centerCoordinate"]; + [self didChangeValueForKey:@"zoomLevel"]; + } + } else if (self.scrollEnabled + && _magnificationGestureRecognizer.state == NSGestureRecognizerStatePossible + && _rotationGestureRecognizer.state == NSGestureRecognizerStatePossible) { + _mbglMap->cancelTransitions(); + + CGFloat x = event.scrollingDeltaX; + CGFloat y = event.scrollingDeltaY; + if (x || y) { + [self offsetCenterCoordinateBy:NSMakePoint(x, y) animated:NO]; + } + + if (event.momentumPhase != NSEventPhaseNone) { + [self offsetCenterCoordinateBy:NSMakePoint(x, y) animated:NO]; + } + } +} + +#pragma mark Keyboard events + +- (void)keyDown:(NSEvent *)event { + if (event.modifierFlags & NSNumericPadKeyMask) { + [self interpretKeyEvents:@[event]]; + } else { + [super keyDown:event]; + } +} + +- (IBAction)moveUp:(__unused id)sender { + [self offsetCenterCoordinateBy:NSMakePoint(0, MGLKeyPanningIncrement) animated:YES]; +} + +- (IBAction)moveDown:(__unused id)sender { + [self offsetCenterCoordinateBy:NSMakePoint(0, -MGLKeyPanningIncrement) animated:YES]; +} + +- (IBAction)moveLeft:(__unused id)sender { + [self offsetCenterCoordinateBy:NSMakePoint(MGLKeyPanningIncrement, 0) animated:YES]; +} + +- (IBAction)moveRight:(__unused id)sender { + [self offsetCenterCoordinateBy:NSMakePoint(-MGLKeyPanningIncrement, 0) animated:YES]; +} + +- (IBAction)moveToBeginningOfParagraph:(__unused id)sender { + if (self.zoomEnabled) { + [self scaleBy:2 atPoint:NSZeroPoint animated:YES]; + } +} + +- (IBAction)moveToEndOfParagraph:(__unused id)sender { + if (self.zoomEnabled) { + [self scaleBy:0.5 atPoint:NSZeroPoint animated:YES]; + } +} + +- (IBAction)moveWordLeft:(__unused id)sender { + if (self.rotateEnabled) { + [self offsetDirectionBy:MGLKeyRotationIncrement animated:YES]; + } +} + +- (IBAction)moveWordRight:(__unused id)sender { + if (self.rotateEnabled) { + [self offsetDirectionBy:-MGLKeyRotationIncrement animated:YES]; + } +} + +- (void)setZoomEnabled:(BOOL)zoomEnabled { + _zoomEnabled = zoomEnabled; + _zoomControls.enabled = zoomEnabled; + _zoomControls.hidden = !zoomEnabled; +} + +- (void)setRotateEnabled:(BOOL)rotateEnabled { + _rotateEnabled = rotateEnabled; + _compass.enabled = rotateEnabled; + _compass.hidden = !rotateEnabled; +} + +#pragma mark Ornaments + +- (void)updateZoomControls { + [_zoomControls setEnabled:self.zoomLevel > self.minimumZoomLevel forSegment:0]; + [_zoomControls setEnabled:self.zoomLevel < self.maximumZoomLevel forSegment:1]; +} + +- (void)updateCompass { + _compass.doubleValue = -self.direction; +} + +- (IBAction)rotate:(NSSlider *)sender { + [self setDirection:-sender.doubleValue animated:YES]; +} + +#pragma mark Annotations + +- (nullable NS_ARRAY_OF(id <MGLAnnotation>) *)annotations { + if (_annotationContextsByAnnotationID.empty()) { + return nil; + } + + std::vector<id <MGLAnnotation> > annotations; + std::transform(_annotationContextsByAnnotationID.begin(), + _annotationContextsByAnnotationID.end(), + std::back_inserter(annotations), + ^ id <MGLAnnotation> (const std::pair<MGLAnnotationID, MGLAnnotationContext> &pair) { + return pair.second.annotation; + }); + return [NSArray arrayWithObjects:&annotations[0] count:annotations.size()]; +} + +- (id <MGLAnnotation>)annotationWithID:(MGLAnnotationID)annotationID { + if (!_annotationContextsByAnnotationID.count(annotationID)) { + return nil; + } + + MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationID[annotationID]; + return annotationContext.annotation; +} + +- (MGLAnnotationID)annotationIDForAnnotation:(id <MGLAnnotation>)annotation { + if (!annotation) { + return MGLAnnotationNotFound; + } + + for (auto &pair : _annotationContextsByAnnotationID) { + if (pair.second.annotation == annotation) { + return pair.first; + } + } + return MGLAnnotationNotFound; +} + +- (void)addAnnotation:(id <MGLAnnotation>)annotation { + if (annotation) { + [self addAnnotations:@[annotation]]; + } +} + +- (void)addAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations { + if (!annotations) { + return; + } + + BOOL delegateHasImagesForAnnotations = [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)]; + + std::vector<mbgl::PointAnnotation> points; + std::vector<mbgl::ShapeAnnotation> shapes; + + for (id <MGLAnnotation> annotation in annotations) { + NSAssert([annotation conformsToProtocol:@protocol(MGLAnnotation)], @"Annotation does not conform to MGLAnnotation"); + + if ([annotation isKindOfClass:[MGLMultiPoint class]]) { + [(MGLMultiPoint *)annotation addShapeAnnotationObjectToCollection:shapes withDelegate:self]; + } else { + MGLAnnotationImage *annotationImage = nil; + if (delegateHasImagesForAnnotations) { + annotationImage = [self.delegate mapView:self imageForAnnotation:annotation]; + } + if (!annotationImage) { + annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName]; + } + if (!annotationImage) { + NSImage *image = MGLDefaultMarkerImage(); + NSRect alignmentRect = image.alignmentRect; + alignmentRect.size.height /= 2; + image.alignmentRect = alignmentRect; + annotationImage = [MGLAnnotationImage annotationImageWithImage:image + reuseIdentifier:MGLDefaultStyleMarkerSymbolName]; + } + + if (!self.annotationImagesByIdentifier[annotationImage.reuseIdentifier]) { + self.annotationImagesByIdentifier[annotationImage.reuseIdentifier] = annotationImage; + [self installAnnotationImage:annotationImage]; + } + + NSString *symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier]; + points.emplace_back(MGLLatLngFromLocationCoordinate2D(annotation.coordinate), symbolName ? [symbolName UTF8String] : ""); + } + } + + if (points.size()) { + std::vector<MGLAnnotationID> pointAnnotationIDs = _mbglMap->addPointAnnotations(points); + + for (size_t i = 0; i < pointAnnotationIDs.size(); ++i) { + MGLAnnotationContext context; + context.annotation = annotations[i]; + context.symbolIdentifier = @(points[i].icon.c_str()); + _annotationContextsByAnnotationID[pointAnnotationIDs[i]] = context; + } + } + + if (shapes.size()) { + std::vector<MGLAnnotationID> shapeAnnotationIDs = _mbglMap->addShapeAnnotations(shapes); + + for (size_t i = 0; i < shapeAnnotationIDs.size(); ++i) { + MGLAnnotationContext context; + context.annotation = annotations[i]; + _annotationContextsByAnnotationID[shapeAnnotationIDs[i]] = context; + } + } +} + +- (void)installAnnotationImage:(MGLAnnotationImage *)annotationImage { + NSImage *image = annotationImage.image; + NSSize size = image.size; + if (size.width < 1 || size.height < 1 || !image.valid) { + return; + } + + // http://www.cocoabuilder.com/archive/cocoa/82430-nsimage-getting-raw-bitmap-data.html#82431 + [image lockFocus]; + NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:{ NSZeroPoint, size }]; + [image unlockFocus]; + + std::string pixelString((const char *)rep.bitmapData, rep.pixelsWide * rep.pixelsHigh * 4 /* RGBA */); + auto cSpriteImage = std::make_shared<mbgl::SpriteImage>((uint16_t)rep.size.width, + (uint16_t)rep.size.height, + (float)(rep.pixelsWide / size.width), + std::move(pixelString)); + NSString *symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier]; + _mbglMap->addAnnotationIcon(symbolName.UTF8String, cSpriteImage); + + // Center the alignment rect around what will be the center of the + // annotation, then union it with all existing alignment rects. + NSRect alignmentRect = image.alignmentRect; + alignmentRect = NSOffsetRect(alignmentRect, -size.width / 2, -size.height / 2); + _unionedAnnotationImageAlignmentRect = NSUnionRect(_unionedAnnotationImageAlignmentRect, + alignmentRect); +} + +- (void)removeAnnotation:(id <MGLAnnotation>)annotation { + if (annotation) { + [self removeAnnotations:@[annotation]]; + } +} + +- (void)removeAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations { + if (!annotations) { + return; + } + + std::vector<MGLAnnotationID> annotationIDsToRemove; + annotationIDsToRemove.reserve(annotations.count); + + for (id <MGLAnnotation> annotation in annotations) { + NSAssert([annotation conformsToProtocol:@protocol(MGLAnnotation)], @"Annotation does not conform to MGLAnnotation"); + + MGLAnnotationID annotationID = [self annotationIDForAnnotation:annotation]; + NSAssert(annotationID != MGLAnnotationNotFound, @"No ID for annotation %@", annotation); + annotationIDsToRemove.push_back(annotationID); + + _annotationContextsByAnnotationID.erase(annotationID); + + if (annotationID == _selectedAnnotationID) { + [self deselectAnnotation:annotation animated:NO]; + } + } + + _mbglMap->removeAnnotations(annotationIDsToRemove); +} + +- (id <MGLAnnotation>)selectedAnnotation { + if (!_annotationContextsByAnnotationID.count(_selectedAnnotationID)) { + return nil; + } + MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationID.at(_selectedAnnotationID); + return annotationContext.annotation; +} + +- (nullable MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier { + if ([identifier hasPrefix:MGLAnnotationSpritePrefix]) { + identifier = [identifier substringFromIndex:MGLAnnotationSpritePrefix.length]; + } + return self.annotationImagesByIdentifier[identifier]; +} + +- (NS_ARRAY_OF(id <MGLAnnotation>) *)selectedAnnotations { + id <MGLAnnotation> selectedAnnotation = self.selectedAnnotation; + return selectedAnnotation ? @[selectedAnnotation] : @[]; +} + +- (void)setSelectedAnnotation:(id <MGLAnnotation>)selectedAnnotation { + _selectedAnnotationID = [self annotationIDForAnnotation:selectedAnnotation]; +} + +- (void)setSelectedAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)selectedAnnotations { + if (!selectedAnnotations.count) { + return; + } + + id <MGLAnnotation> firstAnnotation = selectedAnnotations[0]; + NSAssert([firstAnnotation conformsToProtocol:@protocol(MGLAnnotation)], @"Annotation does not conform to MGLAnnotation"); + if ([firstAnnotation isKindOfClass:[MGLMultiPoint class]]) { + return; + } + + if (MGLCoordinateInCoordinateBounds(firstAnnotation.coordinate, self.visibleCoordinateBounds)) { + [self selectAnnotation:firstAnnotation animated:NO]; + } +} + +- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated +{ + if (!annotation + || [annotation isKindOfClass:[MGLMultiPoint class]] + || !MGLCoordinateInCoordinateBounds(annotation.coordinate, self.visibleCoordinateBounds)) { + return; + } + + id <MGLAnnotation> selectedAnnotation = self.selectedAnnotation; + if (annotation == selectedAnnotation) { + return; + } + + [self deselectAnnotation:selectedAnnotation animated:NO]; + + MGLAnnotationID annotationID = [self annotationIDForAnnotation:annotation]; + if (annotationID == MGLAnnotationNotFound) { + [self addAnnotation:annotation]; + } + _selectedAnnotationID = annotationID; + + if ([annotation respondsToSelector:@selector(title)] + && annotation.title + && !self.calloutForSelectedAnnotation.shown + && [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)] + && [self.delegate mapView:self annotationCanShowCallout:annotation]) { + NSPopover *callout = [self calloutForAnnotation:annotation]; + callout.animates = animated; + + callout.delegate = self; + self.calloutForSelectedAnnotation = callout; + NSRect positioningRect = [self positioningRectForCalloutForAnnotationWithID:annotationID]; + NSRectEdge edge = (self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft + ? NSMinXEdge + : NSMaxXEdge); + [callout showRelativeToRect:positioningRect ofView:self preferredEdge:edge]; + } +} + +- (NSPopover *)calloutForAnnotation:(id <MGLAnnotation>)annotation { + NSPopover *callout = [[NSPopover alloc] init]; + callout.behavior = NSPopoverBehaviorTransient; + + NSViewController *viewController; + if ([self.delegate respondsToSelector:@selector(mapView:calloutViewControllerForAnnotation:)]) { + viewController = [self.delegate mapView:self calloutViewControllerForAnnotation:annotation]; + } + if (!viewController) { + viewController = [[NSViewController alloc] initWithNibName:@"MGLAnnotationCallout" + bundle:[NSBundle mgl_resourceBundle]]; + } + NSAssert(viewController, @"Unable to load MGLAnnotationCallout view controller"); + viewController.representedObject = annotation; + callout.contentViewController = viewController; + + return callout; +} + +- (NSRect)positioningRectForCalloutForAnnotationWithID:(MGLAnnotationID)annotationID { + id <MGLAnnotation> annotation = [self annotationWithID:annotationID]; + if (!annotation) { + return NSZeroRect; + } + + NSString *customSymbol = _annotationContextsByAnnotationID.at(annotationID).symbolIdentifier; + NSString *symbolName = customSymbol.length ? customSymbol : MGLDefaultStyleMarkerSymbolName; + + NSPoint calloutAnchorPoint = [self convertCoordinate:annotation.coordinate toPointToView:self]; + + MGLAnnotationImage *annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:symbolName]; + NSSize annotationSize = annotationImage.image.size; + NSRect annotationRect = NSMakeRect(calloutAnchorPoint.x - annotationSize.width / 2, calloutAnchorPoint.y, + annotationSize.width, annotationSize.height / 2); + annotationRect = NSOffsetRect(annotationImage.image.alignmentRect, + annotationRect.origin.x, annotationRect.origin.y); + return NSInsetRect(annotationRect, -MGLAnnotationImagePaddingForCallout, 0); +} + +- (void)deselectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated { + if (!annotation || self.selectedAnnotation != annotation) { + return; + } + + NSPopover *callout = self.calloutForSelectedAnnotation; + callout.animates = animated; + [callout performClose:self]; + + self.selectedAnnotation = nil; +} + +- (void)updateAnnotationCallouts { + NSPopover *callout = self.calloutForSelectedAnnotation; + if (callout) { + callout.positioningRect = [self positioningRectForCalloutForAnnotationWithID:_selectedAnnotationID]; + } +} + +#pragma mark MGLMultiPointDelegate methods + +- (double)alphaForShapeAnnotation:(MGLShape *)annotation { + if (_delegateHasAlphasForShapeAnnotations) { + return [self.delegate mapView:self alphaForShapeAnnotation:annotation]; + } + return 1.0; +} + +- (mbgl::Color)strokeColorForShapeAnnotation:(MGLShape *)annotation { + NSColor *color = (_delegateHasStrokeColorsForShapeAnnotations + ? [self.delegate mapView:self strokeColorForShapeAnnotation:annotation] + : [NSColor blackColor]); + return MGLColorObjectFromNSColor(color); +} + +- (mbgl::Color)fillColorForPolygonAnnotation:(MGLPolygon *)annotation { + NSColor *color = (_delegateHasFillColorsForShapeAnnotations + ? [self.delegate mapView:self fillColorForPolygonAnnotation:annotation] + : [NSColor blueColor]); + return MGLColorObjectFromNSColor(color); +} + +- (CGFloat)lineWidthForPolylineAnnotation:(MGLPolyline *)annotation { + if (_delegateHasLineWidthsForShapeAnnotations) { + return [self.delegate mapView:self lineWidthForPolylineAnnotation:(MGLPolyline *)annotation]; + } + return 3.0; +} + +#pragma mark MGLPopoverDelegate methods + +- (void)popoverDidShow:(__unused NSNotification *)notification { + id <MGLAnnotation> annotation = self.selectedAnnotation; + if ([self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)]) { + [self.delegate mapView:self didSelectAnnotation:annotation]; + } +} + +- (void)popoverDidClose:(__unused NSNotification *)notification { + id <MGLAnnotation> annotation = self.calloutForSelectedAnnotation.contentViewController.representedObject; + self.calloutForSelectedAnnotation = nil; + self.selectedAnnotation = nil; + + if ([self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotation:)]) { + [self.delegate mapView:self didDeselectAnnotation:annotation]; + } +} + +#pragma mark Overlays + +- (void)addOverlay:(id <MGLOverlay>)overlay { + [self addOverlays:@[overlay]]; +} + +- (void)addOverlays:(NS_ARRAY_OF(id <MGLOverlay>) *)overlays +{ + for (id <MGLOverlay> overlay in overlays) { + NSAssert([overlay conformsToProtocol:@protocol(MGLOverlay)], @"Overlay does not conform to MGLOverlay"); + } + [self addAnnotations:overlays]; +} + +- (void)removeOverlay:(id <MGLOverlay>)overlay { + [self removeOverlays:@[overlay]]; +} + +- (void)removeOverlays:(NS_ARRAY_OF(id <MGLOverlay>) *)overlays { + for (id <MGLOverlay> overlay in overlays) { + NSAssert([overlay conformsToProtocol:@protocol(MGLOverlay)], @"Overlay does not conform to MGLOverlay"); + } + [self removeAnnotations:overlays]; +} + +#pragma mark Interface Builder methods + +- (void)prepareForInterfaceBuilder { + [super prepareForInterfaceBuilder]; + + self.layer.borderColor = [NSColor colorWithRed:59/255. + green:178/255. + blue:208/255. + alpha:0.8].CGColor; + self.layer.borderWidth = 2; + self.layer.backgroundColor = [NSColor colorWithRed:59/255. + green:178/255. + blue:208/255. + alpha:0.6].CGColor; + + self.layer.contents = MGLDefaultMarkerImage(); + self.layer.contentsGravity = kCAGravityCenter; + self.layer.contentsScale = [NSScreen mainScreen].backingScaleFactor; +} + +#pragma mark Geometric methods + +- (CLLocationCoordinate2D)convertPoint:(NSPoint)point toCoordinateFromView:(nullable NSView *)view { + return MGLLocationCoordinate2DFromLatLng([self convertPoint:point toLatLngFromView:view]); +} + +- (mbgl::LatLng)convertPoint:(NSPoint)point toLatLngFromView:(nullable NSView *)view { + NSPoint convertedPoint = [self convertPoint:point fromView:view]; + return _mbglMap->latLngForPixel(mbgl::PrecisionPoint(convertedPoint.x, convertedPoint.y)); +} + +- (NSPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate toPointToView:(nullable NSView *)view { + return [self convertLatLng:MGLLatLngFromLocationCoordinate2D(coordinate) toPointToView:view]; +} + +- (NSPoint)convertLatLng:(mbgl::LatLng)latLng toPointToView:(nullable NSView *)view { + mbgl::vec2<double> pixel = _mbglMap->pixelForLatLng(latLng); + return [self convertPoint:NSMakePoint(pixel.x, pixel.y) toView:view]; +} + +- (MGLCoordinateBounds)convertRectToCoordinateBounds:(NSRect)rect { + return MGLCoordinateBoundsFromLatLngBounds([self convertRectToLatLngBounds:rect]); +} + +- (mbgl::LatLngBounds)convertRectToLatLngBounds:(NSRect)rect { + mbgl::LatLngBounds bounds = mbgl::LatLngBounds::getExtendable(); + bounds.extend([self convertPoint:{ NSMinX(rect), NSMinY(rect) } toLatLngFromView:self]); + bounds.extend([self convertPoint:{ NSMaxX(rect), NSMinY(rect) } toLatLngFromView:self]); + bounds.extend([self convertPoint:{ NSMaxX(rect), NSMaxY(rect) } toLatLngFromView:self]); + bounds.extend([self convertPoint:{ NSMinX(rect), NSMaxY(rect) } toLatLngFromView:self]); + + // If the world is wrapping, extend the bounds to cover all longitudes. + mbgl::LatLng outsideLatLng; + if (bounds.sw.longitude > -180) { + outsideLatLng = { + (bounds.sw.latitude + bounds.ne.latitude) / 2, + bounds.sw.longitude - 1, + }; + } else if (bounds.ne.longitude < 180) { + outsideLatLng = { + (bounds.sw.latitude + bounds.ne.latitude) / 2, + bounds.ne.longitude + 1, + }; + } + if (NSPointInRect([self convertLatLng:outsideLatLng toPointToView:self], rect)) { + bounds.sw.longitude = -180; + bounds.ne.longitude = 180; + } + + return bounds; +} + +- (CLLocationDistance)metersPerPixelAtLatitude:(CLLocationDegrees)latitude { + return _mbglMap->getMetersPerPixelAtLatitude(latitude, self.zoomLevel); +} + +#pragma mark Debugging + +- (MGLMapDebugMaskOptions)debugMask { + mbgl::MapDebugOptions options = _mbglMap->getDebug(); + MGLMapDebugMaskOptions mask = 0; + if (options & mbgl::MapDebugOptions::TileBorders) { + mask |= MGLMapDebugTileBoundariesMask; + } + if (options & mbgl::MapDebugOptions::ParseStatus) { + mask |= MGLMapDebugTileInfoMask; + } + if (options & mbgl::MapDebugOptions::Timestamps) { + mask |= MGLMapDebugTimestampsMask; + } + if (options & mbgl::MapDebugOptions::Collision) { + mask |= MGLMapDebugCollisionBoxesMask; + } + return mask; +} + +- (void)setDebugMask:(MGLMapDebugMaskOptions)debugMask { + mbgl::MapDebugOptions options = mbgl::MapDebugOptions::NoDebug; + if (debugMask & MGLMapDebugTileBoundariesMask) { + options |= mbgl::MapDebugOptions::TileBorders; + } + if (debugMask & MGLMapDebugTileInfoMask) { + options |= mbgl::MapDebugOptions::ParseStatus; + } + if (debugMask & MGLMapDebugTimestampsMask) { + options |= mbgl::MapDebugOptions::Timestamps; + } + if (debugMask & MGLMapDebugCollisionBoxesMask) { + options |= mbgl::MapDebugOptions::Collision; + } + _mbglMap->setDebug(options); +} + +class MGLMapViewImpl : public mbgl::View { +public: + MGLMapViewImpl(MGLMapView *nativeView_, const float scaleFactor_) + : nativeView(nativeView_), scaleFactor(scaleFactor_) {} + virtual ~MGLMapViewImpl() {} + + + float getPixelRatio() const override { + return scaleFactor; + } + + std::array<uint16_t, 2> getSize() const override { + return {{ static_cast<uint16_t>(nativeView.bounds.size.width), + static_cast<uint16_t>(nativeView.bounds.size.height) }}; + } + + std::array<uint16_t, 2> getFramebufferSize() const override { + NSRect bounds = [nativeView convertRectToBacking:nativeView.bounds]; + return {{ static_cast<uint16_t>(bounds.size.width), + static_cast<uint16_t>(bounds.size.height) }}; + } + + void notify() override {} + + void notifyMapChange(mbgl::MapChange change) override { + assert([[NSThread currentThread] isMainThread]); + [nativeView notifyMapChange:change]; + } + + void activate() override { + MGLOpenGLLayer *layer = (MGLOpenGLLayer *)nativeView.layer; + if ([NSOpenGLContext currentContext] != layer.openGLContext) { + [layer.openGLContext makeCurrentContext]; + + mbgl::gl::InitializeExtensions([](const char *name) { + static CFBundleRef framework = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.opengl")); + if (!framework) { + throw std::runtime_error("Failed to load OpenGL framework."); + } + + CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, name, kCFStringEncodingASCII); + void *symbol = CFBundleGetFunctionPointerForName(framework, str); + CFRelease(str); + + return reinterpret_cast<mbgl::gl::glProc>(symbol); + }); + } + } + + void deactivate() override { + [NSOpenGLContext clearCurrentContext]; + } + + void invalidate() override { + [nativeView performSelectorOnMainThread:@selector(invalidate) + withObject:nil + waitUntilDone:NO]; + } + + void beforeRender() override { + activate(); + } + + void afterRender() override {} + +private: + __weak MGLMapView *nativeView = nullptr; + const float scaleFactor; +}; + +@end diff --git a/platform/osx/sdk/MGLMapView_Private.h b/platform/osx/sdk/MGLMapView_Private.h new file mode 100644 index 0000000000..3b37ce24b4 --- /dev/null +++ b/platform/osx/sdk/MGLMapView_Private.h @@ -0,0 +1,12 @@ +#import <mbgl/osx/MGLMapView.h> + +@interface MGLMapView (Private) + +@property (nonatomic, readonly, getter=isDormant) BOOL dormant; + +@property (nonatomic) CLLocationDegrees pendingLatitude; +@property (nonatomic) CLLocationDegrees pendingLongitude; + +- (void)renderSync; + +@end diff --git a/platform/osx/sdk/MGLOpenGLLayer.h b/platform/osx/sdk/MGLOpenGLLayer.h new file mode 100644 index 0000000000..9429b01bb5 --- /dev/null +++ b/platform/osx/sdk/MGLOpenGLLayer.h @@ -0,0 +1,10 @@ +#import <Cocoa/Cocoa.h> + +#import "MGLTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MGLOpenGLLayer : NSOpenGLLayer +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/osx/sdk/MGLOpenGLLayer.mm b/platform/osx/sdk/MGLOpenGLLayer.mm new file mode 100644 index 0000000000..06a7be961c --- /dev/null +++ b/platform/osx/sdk/MGLOpenGLLayer.mm @@ -0,0 +1,49 @@ +#import "MGLOpenGLLayer.h" + +#import "MGLMapView_Private.h" + +#import <mbgl/platform/gl.hpp> + +@implementation MGLOpenGLLayer + +- (MGLMapView *)mapView { + return (MGLMapView *)super.view; +} + +//- (BOOL)isAsynchronous { +// return YES; +//} + +- (BOOL)needsDisplayOnBoundsChange { + return YES; +} + +- (CGRect)frame { + return self.view.bounds; +} + +- (NSOpenGLPixelFormat *)openGLPixelFormatForDisplayMask:(uint32_t)mask { + NSOpenGLPixelFormatAttribute pfas[] = { + NSOpenGLPFAAccelerated, + NSOpenGLPFAClosestPolicy, + NSOpenGLPFAAccumSize, 32, + NSOpenGLPFAColorSize, 24, + NSOpenGLPFAAlphaSize, 8, + NSOpenGLPFADepthSize, 16, + NSOpenGLPFAStencilSize, 8, + NSOpenGLPFAScreenMask, mask, + 0 + }; + return [[NSOpenGLPixelFormat alloc] initWithAttributes:pfas]; +} + +- (BOOL)canDrawInOpenGLContext:(__unused NSOpenGLContext *)context pixelFormat:(__unused NSOpenGLPixelFormat *)pixelFormat forLayerTime:(__unused CFTimeInterval)t displayTime:(__unused const CVTimeStamp *)ts { + return !self.mapView.dormant; +} + +- (void)drawInOpenGLContext:(NSOpenGLContext *)context pixelFormat:(NSOpenGLPixelFormat *)pixelFormat forLayerTime:(CFTimeInterval)t displayTime:(const CVTimeStamp *)ts { + [self.mapView renderSync]; + [super drawInOpenGLContext:context pixelFormat:pixelFormat forLayerTime:t displayTime:ts]; +} + +@end diff --git a/platform/osx/sdk/NSBundle+MGLAdditions.h b/platform/osx/sdk/NSBundle+MGLAdditions.h new file mode 100644 index 0000000000..c09ba38a5d --- /dev/null +++ b/platform/osx/sdk/NSBundle+MGLAdditions.h @@ -0,0 +1,16 @@ +#import <Foundation/Foundation.h> + +#import "MGLTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +void mgl_linkBundleCategory(); + +@interface NSBundle (MGLAdditions) + ++ (instancetype)mgl_resourceBundle; ++ (NSString *)mgl_resourceBundlePath; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/osx/sdk/NSBundle+MGLAdditions.m b/platform/osx/sdk/NSBundle+MGLAdditions.m new file mode 100644 index 0000000000..a53802c343 --- /dev/null +++ b/platform/osx/sdk/NSBundle+MGLAdditions.m @@ -0,0 +1,22 @@ +#import "NSBundle+MGLAdditions.h" + +#import "MGLAccountManager.h" + +void mgl_linkBundleCategory() {} + +@implementation NSBundle (MGLAdditions) + ++ (instancetype)mgl_resourceBundle { + return [self bundleWithPath:[self mgl_resourceBundlePath]]; +} + ++ (NSString *)mgl_resourceBundlePath { + NSString *resourceBundlePath = [[self bundleForClass:[MGLAccountManager class]] + pathForResource:@"Mapbox" ofType:@"bundle"]; + if (!resourceBundlePath) { + resourceBundlePath = [[self mainBundle] bundlePath]; + } + return resourceBundlePath; +} + +@end diff --git a/platform/osx/sdk/NSProcessInfo+MGLAdditions.h b/platform/osx/sdk/NSProcessInfo+MGLAdditions.h new file mode 100644 index 0000000000..6b34f54756 --- /dev/null +++ b/platform/osx/sdk/NSProcessInfo+MGLAdditions.h @@ -0,0 +1,9 @@ +#import <Foundation/Foundation.h> + +void mgl_linkProcessInfoCategory(); + +@interface NSProcessInfo (MGLAdditions) + +- (BOOL)mgl_isInterfaceBuilderDesignablesAgent; + +@end diff --git a/platform/osx/sdk/NSProcessInfo+MGLAdditions.m b/platform/osx/sdk/NSProcessInfo+MGLAdditions.m new file mode 100644 index 0000000000..16f869703c --- /dev/null +++ b/platform/osx/sdk/NSProcessInfo+MGLAdditions.m @@ -0,0 +1,11 @@ +#import "NSProcessInfo+MGLAdditions.h" + +void mgl_linkProcessInfoCategory() {} + +@implementation NSProcessInfo (MGLAdditions) + +- (BOOL)mgl_isInterfaceBuilderDesignablesAgent { + return [self.processName isEqualToString:@"IBDesignablesAgent"]; +} + +@end diff --git a/platform/osx/sdk/resources/MGLAnnotationCallout.xib b/platform/osx/sdk/resources/MGLAnnotationCallout.xib new file mode 100644 index 0000000000..edf84a26a7 --- /dev/null +++ b/platform/osx/sdk/resources/MGLAnnotationCallout.xib @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9531" systemVersion="15B42" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> + <dependencies> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="9531"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="NSViewController"> + <connections> + <outlet property="view" destination="c22-O7-iKe" id="QAM-0O-WIj"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application" customClass="NSObject"/> + <customView id="c22-O7-iKe"> + <rect key="frame" x="0.0" y="0.0" width="270" height="50"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <subviews> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="k5x-ao-Pz3"> + <rect key="frame" x="18" y="25" width="234" height="17"/> + <textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" title="Title" id="nVE-Zi-KcG"> + <font key="font" metaFont="system"/> + <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <connections> + <binding destination="-2" name="value" keyPath="representedObject.title" id="3nD-YS-gzq"/> + </connections> + </textField> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="e9C-Ve-ccw"> + <rect key="frame" x="18" y="8" width="234" height="14"/> + <textFieldCell key="cell" controlSize="small" selectable="YES" sendsActionOnEndEditing="YES" title="Subtitle" id="eKw-tQ-dw8"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <connections> + <binding destination="-2" name="value" keyPath="representedObject.subtitle" id="RQf-48-DyH"/> + </connections> + </textField> + </subviews> + <constraints> + <constraint firstItem="e9C-Ve-ccw" firstAttribute="leading" secondItem="k5x-ao-Pz3" secondAttribute="leading" id="ApT-ew-CYb"/> + <constraint firstAttribute="bottom" secondItem="e9C-Ve-ccw" secondAttribute="bottom" constant="8" id="CWV-Dd-8oi"/> + <constraint firstItem="k5x-ao-Pz3" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="20" id="UUL-GB-Jtv"/> + <constraint firstItem="e9C-Ve-ccw" firstAttribute="top" secondItem="k5x-ao-Pz3" secondAttribute="bottom" constant="3" id="Urc-wn-m8X"/> + <constraint firstItem="e9C-Ve-ccw" firstAttribute="trailing" secondItem="k5x-ao-Pz3" secondAttribute="trailing" id="gss-6G-9GF"/> + <constraint firstAttribute="trailing" secondItem="k5x-ao-Pz3" secondAttribute="trailing" constant="20" id="xCZ-s9-HaP"/> + <constraint firstItem="k5x-ao-Pz3" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="8" id="xcm-oY-jjy"/> + </constraints> + <point key="canvasLocation" x="257" y="355"/> + </customView> + </objects> +</document> diff --git a/platform/osx/sdk/resources/default_marker.pdf b/platform/osx/sdk/resources/default_marker.pdf Binary files differnew file mode 100644 index 0000000000..4e2e332301 --- /dev/null +++ b/platform/osx/sdk/resources/default_marker.pdf diff --git a/platform/osx/sdk/resources/mapbox.pdf b/platform/osx/sdk/resources/mapbox.pdf Binary files differnew file mode 100644 index 0000000000..c08a0e3135 --- /dev/null +++ b/platform/osx/sdk/resources/mapbox.pdf diff --git a/scripts/ios/package.sh b/scripts/ios/package.sh index 7f0fe447d1..049aa7434d 100755 --- a/scripts/ios/package.sh +++ b/scripts/ios/package.sh @@ -99,6 +99,9 @@ echo "Created ${OUTPUT}/static/lib${NAME}.a" step "Copying Headers..." mkdir -p "${OUTPUT}/static/Headers" +for i in `ls -R include/mbgl/darwin | grep -vi private`; do + cp -pv include/mbgl/darwin/$i "${OUTPUT}/static/Headers" +done for i in `ls -R include/mbgl/ios | grep -vi private`; do cp -pv include/mbgl/ios/$i "${OUTPUT}/static/Headers" done diff --git a/scripts/osx/package.sh b/scripts/osx/package.sh new file mode 100755 index 0000000000..eef9fcd919 --- /dev/null +++ b/scripts/osx/package.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -e +set -o pipefail +set -u + +NAME=Mapbox +OUTPUT=build/osx/pkg +OSX_SDK_VERSION=`xcrun --sdk macosx --show-sdk-version` +LIBUV_VERSION=1.7.5 + +if [[ ${#} -eq 0 ]]; then # e.g. "make ipackage" + BUILDTYPE="Release" + GCC_GENERATE_DEBUGGING_SYMBOLS="YES" +else # e.g. "make ipackage-strip" + BUILDTYPE="Release" + GCC_GENERATE_DEBUGGING_SYMBOLS="NO" +fi + +function step { >&2 echo -e "\033[1m\033[36m* $@\033[0m"; } +function finish { >&2 echo -en "\033[0m"; } +trap finish EXIT + + +rm -rf ${OUTPUT} +mkdir -p "${OUTPUT}"/static + + +step "Recording library version..." +VERSION="${OUTPUT}"/static/version.txt +echo -n "https://github.com/mapbox/mapbox-gl-native/commit/" > ${VERSION} +HASH=`git log | head -1 | awk '{ print $2 }' | cut -c 1-10` && true +echo -n "mapbox-gl-native " +echo ${HASH} +echo ${HASH} >> ${VERSION} + + +step "Creating build files..." +export MASON_PLATFORM=osx +export BUILDTYPE=${BUILDTYPE:-Release} +export HOST=osx +make Xcode/osx + +step "Building OS X targets..." +xcodebuild -sdk macosx${OSX_SDK_VERSION} \ + ARCHS="x86_64" \ + ONLY_ACTIVE_ARCH=NO \ + GCC_GENERATE_DEBUGGING_SYMBOLS=${GCC_GENERATE_DEBUGGING_SYMBOLS} \ + -project ./build/osx-x86_64/gyp/mbgl.xcodeproj \ + -configuration ${BUILDTYPE} \ + -target everything \ + -jobs ${JOBS} + + +step "Building static library..." +LIBS=(core.a platform-osx.a asset-fs.a cache-sqlite.a http-nsurl.a) +libtool -static -no_warning_for_no_symbols \ + `find mason_packages/osx-${OSX_SDK_VERSION} -type f -name libuv.a` \ + `find mason_packages/osx-${OSX_SDK_VERSION} -type f -name libgeojsonvt.a` \ + -o ${OUTPUT}/static/lib${NAME}.a \ + ${LIBS[@]/#/gyp/build/${BUILDTYPE}/libmbgl-} +echo "Created ${OUTPUT}/static/lib${NAME}.a" + + +step "Copying Headers..." +mkdir -p "${OUTPUT}/static/Headers" +for i in `ls -R include/mbgl/darwin | grep -vi private`; do + cp -pv include/mbgl/darwin/$i "${OUTPUT}/static/Headers" +done +for i in `ls -R include/mbgl/osx | grep -vi private`; do + cp -pv include/mbgl/osx/$i "${OUTPUT}/static/Headers" +done + +step "Copying Resources..." +cp -pv LICENSE.md "${OUTPUT}/static" +mkdir -p "${OUTPUT}/static/${NAME}.bundle" +cp -pv platform/osx/resources/* "${OUTPUT}/static/${NAME}.bundle" + diff --git a/scripts/osx/run.sh b/scripts/osx/run.sh index 28c1adba1b..0f7841b4a3 100755 --- a/scripts/osx/run.sh +++ b/scripts/osx/run.sh @@ -11,6 +11,12 @@ BUILDTYPE=${BUILDTYPE:-Release} # Build ################################################################################ +mapbox_time "package_osx_symbols" \ +make xpackage + +mapbox_time "package_osx_stripped" \ +make xpackage-strip + mapbox_time "compile_program" \ make xosx -j${JOBS} BUILDTYPE=${BUILDTYPE} |