diff options
author | Minh Nguyễn <mxn@1ec5.org> | 2015-11-22 18:07:06 -0800 |
---|---|---|
committer | Minh Nguyễn <mxn@1ec5.org> | 2015-12-13 17:17:35 -0800 |
commit | 0cab7809eb59675be6c3a87a665a2bf0b9a28d75 (patch) | |
tree | 490eb9f86113a71be274781f4465c8d32b92dabb | |
parent | b59f087ea8bd451d7e822bb27d3612974fa5ac77 (diff) | |
download | qtlocation-mapboxgl-0cab7809eb59675be6c3a87a665a2bf0b9a28d75.tar.gz |
[osx] Rewrote platform-osx and osxapp
platform-osx now vends a real NSView subclass, MGLMapView, that is readily embedded inside a Cocoa application for OS X. MGLMapView is backed by an NSOpenGLLayer for optimal performance and integration with other layer-backed views. It supports keyboard shortcuts and several gestures and hosts attribution, zooming, and rotation controls as subviews. osxapp is now a bona fide Cocoa application that embeds MGLMapView inside a XIB. osxapp has preferences and a share button for tight integration with custom styles.
Enabling asynchronous rendering would be more consistent with iOS but increases CPU usage so much, even when idle, that it isn’t worth any performance gain. The bigger issue is that VAOs aren’t being used.
make xpackage creates a static library similar to the one created by make ipackage. make clean cleans additional places where build output ends up.
The OS X minimum deployment target has been increased from 10.9 to 10.10. osxapp’s window has a full size content view, which requires 10.10. Lightweight generics require iOS 9+ and OS X 10.11 regardless, because it was only in that release that Foundation collection classes started adopting lightweight generics.
Shuffled files around and refactored annotations so that iOS and OS X share a good chunk of the annotations code, which now takes advantage of polymorphism. MGLMapView can now display annotations but cannot yet select them. In osxapp, a long press drops a pin, and so does the map view’s context menu.
Annotations have NSPopovers as callouts, and their view controllers can be customized. Annotation image alignment rects are respected for hit testing purposes and for positioning the callout anchor. Callouts in osxapp demonstrate the use of bindings to keep callouts in sync with underlying model objects.
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} |