summaryrefslogtreecommitdiff
path: root/platform/ios
diff options
context:
space:
mode:
Diffstat (limited to 'platform/ios')
-rw-r--r--platform/ios/CHANGELOG.md20
-rw-r--r--platform/ios/Mapbox-iOS-SDK-nightly-dynamic.podspec2
-rw-r--r--platform/ios/Mapbox-iOS-SDK-symbols.podspec2
-rw-r--r--platform/ios/Mapbox-iOS-SDK.podspec2
-rw-r--r--platform/ios/app/MBXViewController.m5
-rw-r--r--platform/ios/app/bg.lproj/Localizable.strings0
-rw-r--r--platform/ios/bitrise.yml65
-rw-r--r--platform/ios/docs/guides/Adding Points to a Map.md4
-rw-r--r--platform/ios/framework/Settings.bundle/bg.lproj/Root.strings3
-rw-r--r--platform/ios/ios.xcodeproj/project.pbxproj37
-rw-r--r--platform/ios/resources/Base.lproj/Localizable.strings25
-rw-r--r--platform/ios/resources/bg.lproj/Localizable.strings93
-rw-r--r--platform/ios/resources/bg.lproj/Localizable.stringsdict34
-rw-r--r--platform/ios/resources/en.lproj/Localizable.stringsdict38
-rw-r--r--platform/ios/resources/pt-BR.lproj/Localizable.strings26
-rwxr-xr-xplatform/ios/scripts/deploy-packages.sh20
-rwxr-xr-xplatform/ios/scripts/document.sh2
-rw-r--r--platform/ios/src/MGLCompactCalloutView.h2
-rw-r--r--platform/ios/src/MGLFaux3DUserLocationAnnotationView.m18
-rw-r--r--platform/ios/src/MGLMapAccessibilityElement.h54
-rw-r--r--platform/ios/src/MGLMapAccessibilityElement.mm199
-rw-r--r--platform/ios/src/MGLMapView.mm650
-rw-r--r--platform/ios/src/MGLScaleBar.mm17
-rw-r--r--platform/ios/src/MGLUserLocation.h3
-rw-r--r--platform/ios/test/MGLMapAccessibilityElementTests.m87
m---------platform/ios/vendor/SMCalloutView0
-rwxr-xr-xplatform/ios/vendor/SMCalloutView/SMCalloutView.h205
-rwxr-xr-xplatform/ios/vendor/SMCalloutView/SMCalloutView.m851
28 files changed, 2202 insertions, 262 deletions
diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md
index 37a44549ba..404b4c7e2d 100644
--- a/platform/ios/CHANGELOG.md
+++ b/platform/ios/CHANGELOG.md
@@ -4,6 +4,12 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT
## master
+### Annotations and user interaction
+
+* Increased the default maximum zoom level from 20 to 22. ([#9835](https://github.com/mapbox/mapbox-gl-native/pull/9835))
+
+## 3.7.0
+
### Networking and storage
* Added a new `MGLMapSnapshotter` class for capturing rendered map images from an `MGLMapView`’s camera. ([#9891](https://github.com/mapbox/mapbox-gl-native/pull/9891))
@@ -23,19 +29,29 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT
* Fixed an issue that could cause antialiasing between polygons on the same layer to fail if the fill layers used data-driven styling for the fill color. ([#9699](https://github.com/mapbox/mapbox-gl-native/pull/9699))
* The previously deprecated support for style classes has been removed. For interface compatibility, the API methods remain, but they are now non-functional.
-### Annotations and user interaction
+### Annotations
* Fixed several bugs and performance issues related to the use of annotations backed by `MGLAnnotationImage`. The limits on the number and size of images and glyphs has been effectively eliminated and should now depend on hardware constraints. These fixes also apply to images used to represent icons in `MGLSymbolStyleLayer`. ([#9213](https://github.com/mapbox/mapbox-gl-native/pull/9213))
-* Increased the default maximum zoom level from 20 to 22. ([#9835](https://github.com/mapbox/mapbox-gl-native/pull/9835))
* Added an `overlays` property to `MGLMapView`. ([#8617](https://github.com/mapbox/mapbox-gl-native/pull/8617))
* Selecting an annotation no longer sets the user tracking mode to `MGLUserTrackingModeNone`. ([#10094](https://github.com/mapbox/mapbox-gl-native/pull/10094))
* Added `-[MGLMapView cameraThatFitsShape:direction:edgePadding:]` to get a camera with zoom level and center coordinate computed to fit a shape. ([#10107](https://github.com/mapbox/mapbox-gl-native/pull/10107))
+* Added support selection of shape and polyline annotations.([#9984](https://github.com/mapbox/mapbox-gl-native/pull/9984))
+* Fixed an issue where view annotations could be slightly misaligned. View annotation placement is now rounded to the nearest pixel. ([#10219](https://github.com/mapbox/mapbox-gl-native/pull/10219))
+* Fixed an issue where a shape annotation callout was not displayed if the centroid was not visible. ([#10255](https://github.com/mapbox/mapbox-gl-native/pull/10255))
+
+### User interaction
+
+* Users of VoiceOver can now swipe left and right to navigate among visible places, points of interest, and roads. ([#9950](https://github.com/mapbox/mapbox-gl-native/pull/9950))
+* Increased the default maximum zoom level from 20 to 22. ([#9835](https://github.com/mapbox/mapbox-gl-native/pull/9835))
### Other changes
+* Added a Bulgarian localization. ([#10309](https://github.com/mapbox/mapbox-gl-native/pull/10309))
* Fixed an issue that could cause line label rendering glitches when the line geometry is projected to a point behind the plane of the camera. ([#9865](https://github.com/mapbox/mapbox-gl-native/pull/9865))
* Fixed an issue that could cause a crash when using `-[MGLMapView flyToCamera:completionHandler:]` and related methods with zoom levels at or near the maximum value. ([#9381](https://github.com/mapbox/mapbox-gl-native/pull/9381))
* Added `-[MGLMapView showAttribution:]` to allow custom attribution buttons to show the default attribution interface. ([#10085](https://github.com/mapbox/mapbox-gl-native/pull/10085))
+* Fixed a conflict between multiple copies of SMCalloutView in a project. ([#10183](https://github.com/mapbox/mapbox-gl-native/pull/10183))
+* Fixed a crash when enabling the scale bar in iOS 8. ([#10241](https://github.com/mapbox/mapbox-gl-native/pull/10241))
## 3.6.4 - September 25, 2017
diff --git a/platform/ios/Mapbox-iOS-SDK-nightly-dynamic.podspec b/platform/ios/Mapbox-iOS-SDK-nightly-dynamic.podspec
index 1abcaf467c..b8b6687f0c 100644
--- a/platform/ios/Mapbox-iOS-SDK-nightly-dynamic.podspec
+++ b/platform/ios/Mapbox-iOS-SDK-nightly-dynamic.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |m|
- version = '3.7.0-alpha.1'
+ version = '3.7.0-beta.4'
m.name = 'Mapbox-iOS-SDK-nightly-dynamic'
m.version = "#{version}-nightly"
diff --git a/platform/ios/Mapbox-iOS-SDK-symbols.podspec b/platform/ios/Mapbox-iOS-SDK-symbols.podspec
index ceb7f21c47..e84255715d 100644
--- a/platform/ios/Mapbox-iOS-SDK-symbols.podspec
+++ b/platform/ios/Mapbox-iOS-SDK-symbols.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |m|
- version = '3.7.0-alpha.1'
+ version = '3.7.0-beta.4'
m.name = 'Mapbox-iOS-SDK-symbols'
m.version = "#{version}-symbols"
diff --git a/platform/ios/Mapbox-iOS-SDK.podspec b/platform/ios/Mapbox-iOS-SDK.podspec
index bcd37d0d06..bb6c5e0123 100644
--- a/platform/ios/Mapbox-iOS-SDK.podspec
+++ b/platform/ios/Mapbox-iOS-SDK.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |m|
- version = '3.7.0-alpha.1'
+ version = '3.7.0-beta.4'
m.name = 'Mapbox-iOS-SDK'
m.version = version
diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m
index 07838bc6bd..2c3d26b489 100644
--- a/platform/ios/app/MBXViewController.m
+++ b/platform/ios/app/MBXViewController.m
@@ -165,7 +165,9 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
self.mapView.scaleBar.hidden = NO;
self.mapView.showsUserHeadingIndicator = YES;
self.hudLabel.hidden = YES;
- self.hudLabel.titleLabel.font = [UIFont monospacedDigitSystemFontOfSize:10 weight:UIFontWeightRegular];
+ if ([UIFont respondsToSelector:@selector(monospacedDigitSystemFontOfSize:weight:)]) {
+ self.hudLabel.titleLabel.font = [UIFont monospacedDigitSystemFontOfSize:10 weight:UIFontWeightRegular];
+ }
if ([MGLAccountManager accessToken].length)
{
@@ -827,6 +829,7 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
}
MGLPolygon *polygon = [MGLPolygon polygonWithCoordinates:polygonCoordinates count:[stateCoordinatePairs count]];
+ polygon.title = feature[@"properties"][@"NAME"];
[self.mapView addAnnotation:polygon];
diff --git a/platform/ios/app/bg.lproj/Localizable.strings b/platform/ios/app/bg.lproj/Localizable.strings
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/platform/ios/app/bg.lproj/Localizable.strings
diff --git a/platform/ios/bitrise.yml b/platform/ios/bitrise.yml
index 9ca44e1d47..24bd054dbc 100644
--- a/platform/ios/bitrise.yml
+++ b/platform/ios/bitrise.yml
@@ -4,6 +4,8 @@ default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
trigger_map:
- pattern: nightly-release
workflow: nightly-release
+- pattern: release-from-tag
+ workflow: release-from-tag
- pattern: "*"
is_pull_request_allowed: true
workflow: primary
@@ -11,6 +13,12 @@ workflows:
primary:
steps:
- script:
+ title: Skip Workflow
+ inputs:
+ - content: echo "This workflow is obsolete — see CircleCi."
+ nightly-release:
+ steps:
+ - script:
title: Install Dependencies
inputs:
- content: |-
@@ -19,41 +27,40 @@ workflows:
brew install cmake
- is_debug: 'yes'
- script:
- title: Generate Workspace
+ title: Configure AWS-CLI
+ inputs:
+ - content: |-
+ #!/bin/bash
+ pip install awscli
+ - script:
+ title: Build package
inputs:
- content: |-
#!/bin/bash
set -eu -o pipefail
- export BUILDTYPE=Debug
- make iproj
+ export BUILDTYPE=Release
+ export BUILD_DEVICE=true
+ export FORMAT=dynamic
+ make ipackage-strip
+ CLOUDWATCH=true platform/ios/scripts/metrics.sh
+ platform/ios/scripts/deploy-nightly.sh
- is_debug: 'yes'
- - xcode-test:
- title: Run SDK Unit Tests
- inputs:
- - project_path: platform/ios/ios.xcworkspace
- - scheme: CI
- - deploy-to-bitrise-io:
- title: Deploy to Bitrise.io
- inputs:
- - notify_user_groups: none
- slack:
title: Post to Slack
inputs:
- webhook_url: "$SLACK_HOOK_URL"
- channel: "#gl-bots"
- - from_username: 'Bitrise iOS'
- - from_username_on_error: 'Bitrise iOS'
+ - from_username: 'Bitrise iOS Nightly 💤'
+ - from_username_on_error: 'Bitrise iOS Nightly 💤'
- message: '<${BITRISE_BUILD_URL}|Build #${BITRISE_BUILD_NUMBER}>
- for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}>
- by ${GIT_CLONE_COMMIT_COMMITER_NAME}
- passed'
+ for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}@%7B1day%7D...${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}>
+ completed successfully.'
- message_on_error: '<${BITRISE_BUILD_URL}|Build #${BITRISE_BUILD_NUMBER}>
- for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}>
- by ${GIT_CLONE_COMMIT_COMMITER_NAME}
- failed'
+ for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}@%7B1day%7D...${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}>
+ failed.'
- icon_url: https://bitrise-public-content-production.s3.amazonaws.com/slack/bitrise-slack-icon-128.png
- icon_url_on_error: https://bitrise-public-content-production.s3.amazonaws.com/slack/bitrise-slack-error-icon-128.png
- nightly-release:
+ release-from-tag:
steps:
- script:
title: Install Dependencies
@@ -71,25 +78,21 @@ workflows:
- content: |-
#!/bin/bash
set -eu -o pipefail
- export BUILDTYPE=Release
- export BUILD_DEVICE=true
- export FORMAT=dynamic
- make ipackage-strip
- CLOUDWATCH=true platform/ios/scripts/metrics.sh
- platform/ios/scripts/deploy-nightly.sh
+ export VERSION_TAG=${BITRISE_GIT_TAG}
+ platform/ios/scripts/deploy-packages.sh
- is_debug: 'yes'
- slack:
title: Post to Slack
inputs:
- webhook_url: "$SLACK_HOOK_URL"
- channel: "#gl-bots"
- - from_username: 'Bitrise iOS Nightly \U0001F31D'
- - from_username_on_error: 'Bitrise iOS Nightly \U0001F31D'
+ - from_username: 'Bitrise iOS Deploy'
+ - from_username_on_error: 'Bitrise iOS Deploy'
- message: '<${BITRISE_BUILD_URL}|Build #${BITRISE_BUILD_NUMBER}>
- for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}@%7B1day%7D...${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}>
+ for <https://github.com/mapbox/mapbox-gl-native/releases/tag/${BITRISE_GIT_TAG}|`${BITRISE_GIT_TAG}`>
completed successfully.'
- message_on_error: '<${BITRISE_BUILD_URL}|Build #${BITRISE_BUILD_NUMBER}>
- for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}@%7B1day%7D...${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}>
+ for <https://github.com/mapbox/mapbox-gl-native/releases/tag/${BITRISE_GIT_TAG}|`${BITRISE_GIT_TAG}`>
failed.'
- icon_url: https://bitrise-public-content-production.s3.amazonaws.com/slack/bitrise-slack-icon-128.png
- icon_url_on_error: https://bitrise-public-content-production.s3.amazonaws.com/slack/bitrise-slack-error-icon-128.png
diff --git a/platform/ios/docs/guides/Adding Points to a Map.md b/platform/ios/docs/guides/Adding Points to a Map.md
index 2698d5564f..2844075cc7 100644
--- a/platform/ios/docs/guides/Adding Points to a Map.md
+++ b/platform/ios/docs/guides/Adding Points to a Map.md
@@ -51,7 +51,7 @@ To use annotation views, implement `MGLMapViewDelegate` `-mapView:viewForAnnotat
* No limit on style or image size
* Full support for animations
* Relative control over z-ordering using the `zPosition` property on `CALayer`
-* [Familiar API for MapKit users](https://www.mapbox.com/help/switch-mapkit/#annotations-pins)
+* Familiar API for MapKit users
**Cons**
@@ -63,7 +63,7 @@ To use annotation views, implement `MGLMapViewDelegate` `-mapView:viewForAnnotat
For absolute full control of how points are displayed on a map, consider [runtime styling](runtime-styling.html).
-You can use `MGLPointFeature` or any of the other [style feature subclasses](Style%20Features.html) to add points and shapes to an `MGLShapeSource`.
+You can use `MGLPointFeature` or any other [style primitives](Style%20Primitives.html) to add points and shapes to an `MGLShapeSource`.
From there, you can create one or many `MGLSymbolStyleLayer` or `MGLCircleStyleLayer` layers to filter and style points for display on the map ([example](https://www.mapbox.com/ios-sdk/examples/runtime-multiple-annotations)).
diff --git a/platform/ios/framework/Settings.bundle/bg.lproj/Root.strings b/platform/ios/framework/Settings.bundle/bg.lproj/Root.strings
new file mode 100644
index 0000000000..c86decde32
--- /dev/null
+++ b/platform/ios/framework/Settings.bundle/bg.lproj/Root.strings
@@ -0,0 +1,3 @@
+"TELEMETRY_GROUP_TITLE" = "Настройки за поверителност";
+"TELEMETRY_SWITCH_TITLE" = "Mapbox Телеметрия";
+"TELEMETRY_GROUP_FOOTER" = "Тази настройка позволява на приложението да споделя анонимни локации и данни за използване с Mapbox.";
diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj
index a3ccc9337b..ad17e00673 100644
--- a/platform/ios/ios.xcodeproj/project.pbxproj
+++ b/platform/ios/ios.xcodeproj/project.pbxproj
@@ -292,10 +292,15 @@
DA35A2CB1CCAAAD200E826B2 /* NSValue+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = DA35A2C81CCAAAD200E826B2 /* NSValue+MGLAdditions.m */; };
DA35A2CC1CCAAAD200E826B2 /* NSValue+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = DA35A2C81CCAAAD200E826B2 /* NSValue+MGLAdditions.m */; };
DA35D0881E1A6309007DED41 /* one-liner.json in Resources */ = {isa = PBXBuildFile; fileRef = DA35D0871E1A6309007DED41 /* one-liner.json */; };
+ DA5DB12A1FABF1EE001C2326 /* MGLMapAccessibilityElementTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DA5DB1291FABF1EE001C2326 /* MGLMapAccessibilityElementTests.m */; };
DA6408DB1DA4E7D300908C90 /* MGLVectorStyleLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = DA6408D91DA4E7D300908C90 /* MGLVectorStyleLayer.h */; settings = {ATTRIBUTES = (Public, ); }; };
DA6408DC1DA4E7D300908C90 /* MGLVectorStyleLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = DA6408D91DA4E7D300908C90 /* MGLVectorStyleLayer.h */; settings = {ATTRIBUTES = (Public, ); }; };
DA6408DD1DA4E7D300908C90 /* MGLVectorStyleLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = DA6408DA1DA4E7D300908C90 /* MGLVectorStyleLayer.m */; };
DA6408DE1DA4E7D300908C90 /* MGLVectorStyleLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = DA6408DA1DA4E7D300908C90 /* MGLVectorStyleLayer.m */; };
+ DA704CC21F65A475004B3F28 /* MGLMapAccessibilityElement.h in Headers */ = {isa = PBXBuildFile; fileRef = DA704CC01F65A475004B3F28 /* MGLMapAccessibilityElement.h */; };
+ DA704CC31F65A475004B3F28 /* MGLMapAccessibilityElement.h in Headers */ = {isa = PBXBuildFile; fileRef = DA704CC01F65A475004B3F28 /* MGLMapAccessibilityElement.h */; };
+ DA704CC41F65A475004B3F28 /* MGLMapAccessibilityElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.mm */; };
+ DA704CC51F65A475004B3F28 /* MGLMapAccessibilityElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.mm */; };
DA72620B1DEEE3480043BB89 /* MGLOpenGLStyleLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = DA7262091DEEE3480043BB89 /* MGLOpenGLStyleLayer.h */; settings = {ATTRIBUTES = (Public, ); }; };
DA72620C1DEEE3480043BB89 /* MGLOpenGLStyleLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = DA7262091DEEE3480043BB89 /* MGLOpenGLStyleLayer.h */; settings = {ATTRIBUTES = (Public, ); }; };
DA72620D1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA72620A1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm */; };
@@ -774,6 +779,14 @@
DA2E885D1CC0382C00F24E7B /* MGLOfflinePackTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MGLOfflinePackTests.m; path = ../../darwin/test/MGLOfflinePackTests.m; sourceTree = "<group>"; };
DA2E885E1CC0382C00F24E7B /* MGLOfflineRegionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MGLOfflineRegionTests.m; path = ../../darwin/test/MGLOfflineRegionTests.m; sourceTree = "<group>"; };
DA2E88601CC0382C00F24E7B /* MGLStyleTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MGLStyleTests.mm; path = ../../darwin/test/MGLStyleTests.mm; sourceTree = "<group>"; };
+ DA33895F1FA3EAB7001EA329 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Foundation.strings"; sourceTree = "<group>"; };
+ DA3389651FA3EE1B001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
+ DA3389661FA3EE28001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Foundation.strings; sourceTree = "<group>"; };
+ DA3389671FA3EE2F001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Foundation.stringsdict; sourceTree = "<group>"; };
+ DA3389681FA3EE48001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
+ DA3389691FA3EE50001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+ DA33896A1FA3EE58001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Root.strings; sourceTree = "<group>"; };
+ DA33896B1FA3EF4A001EA329 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Foundation.stringsdict; sourceTree = "<group>"; };
DA35A29D1CC9E94C00E826B2 /* MGLCoordinateFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLCoordinateFormatter.h; sourceTree = "<group>"; };
DA35A2A01CC9E95F00E826B2 /* MGLCoordinateFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLCoordinateFormatter.m; sourceTree = "<group>"; };
DA35A2A91CCA058D00E826B2 /* MGLCoordinateFormatterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MGLCoordinateFormatterTests.m; path = ../../darwin/test/MGLCoordinateFormatterTests.m; sourceTree = "<group>"; };
@@ -794,6 +807,7 @@
DA57D4AC1EBA922A00793288 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = vi; path = vi.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
DA5C09BA1EFC48550056B178 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
DA5C09BB1EFC486C0056B178 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
+ DA5DB1291FABF1EE001C2326 /* MGLMapAccessibilityElementTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MGLMapAccessibilityElementTests.m; sourceTree = "<group>"; };
DA6023F11E4CE94300DBFF23 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Foundation.strings; sourceTree = "<group>"; };
DA6023F21E4CE94800DBFF23 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sv; path = sv.lproj/Foundation.stringsdict; sourceTree = "<group>"; };
DA618B111E68823600CB7F44 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -812,6 +826,8 @@
DA704CBB1F637311004B3F28 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Foundation.strings; sourceTree = "<group>"; };
DA704CBC1F637405004B3F28 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
DA704CBD1F63746E004B3F28 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
+ DA704CC01F65A475004B3F28 /* MGLMapAccessibilityElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapAccessibilityElement.h; sourceTree = "<group>"; };
+ DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLMapAccessibilityElement.mm; sourceTree = "<group>"; };
DA704CC71F6663A3004B3F28 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Foundation.strings; sourceTree = "<group>"; };
DA7262091DEEE3480043BB89 /* MGLOpenGLStyleLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLOpenGLStyleLayer.h; sourceTree = "<group>"; };
DA72620A1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLOpenGLStyleLayer.mm; sourceTree = "<group>"; };
@@ -1111,6 +1127,8 @@
35599DB81D46AD7F0048254D /* Categories */ = {
isa = PBXGroup;
children = (
+ 1FDD9D6D1F26936400252B09 /* MGLVectorSource+MGLAdditions.h */,
+ 1FDD9D6E1F26936400252B09 /* MGLVectorSource+MGLAdditions.m */,
350098DA1D484E60004B2AF0 /* NSValue+MGLStyleAttributeAdditions.h */,
350098DB1D484E60004B2AF0 /* NSValue+MGLStyleAttributeAdditions.mm */,
);
@@ -1338,6 +1356,7 @@
3598544C1E1D38AA00B29F84 /* MGLDistanceFormatterTests.m */,
DA0CD58F1CF56F6A00A5F5A5 /* MGLFeatureTests.mm */,
DA2E885C1CC0382C00F24E7B /* MGLGeometryTests.mm */,
+ DA5DB1291FABF1EE001C2326 /* MGLMapAccessibilityElementTests.m */,
35E208A61D24210F00EC9A46 /* MGLNSDataAdditionsTests.m */,
1F95931C1E6DE2E900D5B294 /* MGLNSDateAdditionsTests.mm */,
DAE7DEC11E245455007505A6 /* MGLNSStringAdditionsTests.m */,
@@ -1415,6 +1434,8 @@
35CE617F1D4165C2004F2359 /* Categories */,
DAD165841CF4D06B001FF4B9 /* Annotations */,
DAD165851CF4D08B001FF4B9 /* Telemetry */,
+ DA704CC01F65A475004B3F28 /* MGLMapAccessibilityElement.h */,
+ DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.mm */,
DA8848361CBAFB8500AB86E3 /* MGLMapView.h */,
DA17BE2F1CC4BAC300402C41 /* MGLMapView_Private.h */,
DA8848371CBAFB8500AB86E3 /* MGLMapView+IBAdditions.h */,
@@ -1612,8 +1633,6 @@
DAD165831CF4CFED001FF4B9 /* Categories */ = {
isa = PBXGroup;
children = (
- 1FDD9D6D1F26936400252B09 /* MGLVectorSource+MGLAdditions.h */,
- 1FDD9D6E1F26936400252B09 /* MGLVectorSource+MGLAdditions.m */,
7E016D821D9E890300A29A21 /* MGLPolygon+MGLAdditions.h */,
7E016D831D9E890300A29A21 /* MGLPolygon+MGLAdditions.m */,
7E016D7C1D9E86BE00A29A21 /* MGLPolyline+MGLAdditions.h */,
@@ -1754,6 +1773,7 @@
DA35A2C91CCAAAD200E826B2 /* NSValue+MGLAdditions.h in Headers */,
3510FFEA1D6D9C7A00F413B2 /* NSComparisonPredicate+MGLAdditions.h in Headers */,
DA6408DB1DA4E7D300908C90 /* MGLVectorStyleLayer.h in Headers */,
+ DA704CC21F65A475004B3F28 /* MGLMapAccessibilityElement.h in Headers */,
DD0902AB1DB192A800C5BDCE /* MGLNetworkConfiguration.h in Headers */,
DA8848571CBAFB9800AB86E3 /* MGLMapboxEvents.h in Headers */,
35D3A1E61E9BE7EB002B38EE /* MGLScaleBar.h in Headers */,
@@ -1889,6 +1909,7 @@
558DE7A11E5615E400C7916D /* MGLFoundation_Private.h in Headers */,
3538AA1E1D542239008EC33D /* MGLForegroundStyleLayer.h in Headers */,
30E578181DAA85520050F07E /* UIImage+MGLAdditions.h in Headers */,
+ DA704CC31F65A475004B3F28 /* MGLMapAccessibilityElement.h in Headers */,
40F887711D7A1E59008ECB67 /* MGLShapeSource_Private.h in Headers */,
DABFB8631CBE99E500D62B32 /* MGLOfflineRegion.h in Headers */,
DA35A2B21CCA141D00E826B2 /* MGLCompassDirectionFormatter.h in Headers */,
@@ -2109,6 +2130,7 @@
fi,
nl,
hu,
+ bg,
);
mainGroup = DA1DC9411CB6C1C2006E619F;
productRefGroup = DA1DC94B1CB6C1C2006E619F /* Products */;
@@ -2267,6 +2289,7 @@
DA2E88621CC0382C00F24E7B /* MGLOfflinePackTests.m in Sources */,
55E2AD131E5B125400E8C587 /* MGLOfflineStorageTests.mm in Sources */,
920A3E5D1E6F995200C16EFC /* MGLSourceQueryTests.m in Sources */,
+ DA5DB12A1FABF1EE001C2326 /* MGLMapAccessibilityElementTests.m in Sources */,
FAE1CDCB1E9D79CB00C40B5B /* MGLFillExtrusionStyleLayerTests.mm in Sources */,
DA35A2AA1CCA058D00E826B2 /* MGLCoordinateFormatterTests.m in Sources */,
357579831D502AE6000B822E /* MGLRasterStyleLayerTests.mm in Sources */,
@@ -2355,6 +2378,7 @@
DA88482A1CBAFA6200AB86E3 /* MGLTilePyramidOfflineRegion.mm in Sources */,
4049C29F1DB6CD6C00B3F799 /* MGLPointCollection.mm in Sources */,
35136D3F1D42273000C20EFD /* MGLLineStyleLayer.mm in Sources */,
+ DA704CC41F65A475004B3F28 /* MGLMapAccessibilityElement.mm in Sources */,
DA72620D1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm in Sources */,
DA88481A1CBAFA6200AB86E3 /* MGLAccountManager.m in Sources */,
3510FFFB1D6DCC4700F413B2 /* NSCompoundPredicate+MGLAdditions.mm in Sources */,
@@ -2442,6 +2466,7 @@
DAA4E4211CBB730400178DFB /* MGLOfflineStorage.mm in Sources */,
4049C2A01DB6CD6C00B3F799 /* MGLPointCollection.mm in Sources */,
35136D401D42273000C20EFD /* MGLLineStyleLayer.mm in Sources */,
+ DA704CC51F65A475004B3F28 /* MGLMapAccessibilityElement.mm in Sources */,
DA72620E1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm in Sources */,
DAA4E42F1CBB730400178DFB /* MGLCompactCalloutView.m in Sources */,
3510FFFC1D6DCC4700F413B2 /* NSCompoundPredicate+MGLAdditions.mm in Sources */,
@@ -2520,6 +2545,7 @@
DA618B2B1E68932D00CB7F44 /* fi */,
DAE8CCAD1E6E8C70009B5CB0 /* nl */,
DA5C09BA1EFC48550056B178 /* hu */,
+ DA3389651FA3EE1B001EA329 /* bg */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -2544,6 +2570,7 @@
DA618B2C1E68933600CB7F44 /* fi */,
DAE8CCAE1E6E8C76009B5CB0 /* nl */,
DACCD9C81F1F473700BB09A1 /* hu */,
+ DA33896A1FA3EE58001EA329 /* bg */,
);
name = Root.strings;
sourceTree = "<group>";
@@ -2566,6 +2593,7 @@
DA1AC01B1E5B8774006DF1D6 /* lt */,
DA618B1B1E68884E00CB7F44 /* ca */,
DA5C09BB1EFC486C0056B178 /* hu */,
+ DA3389681FA3EE48001EA329 /* bg */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -2585,6 +2613,8 @@
DAE9E0F11EB7BF1B001E8E8B /* es */,
DA704CBB1F637311004B3F28 /* ru */,
DA704CC71F6663A3004B3F28 /* uk */,
+ DA33895F1FA3EAB7001EA329 /* pt-BR */,
+ DA3389661FA3EE28001EA329 /* bg */,
);
name = Foundation.strings;
sourceTree = "<group>";
@@ -2604,6 +2634,8 @@
DA618B1D1E6888F500CB7F44 /* ca */,
DA618B261E68920D00CB7F44 /* lt */,
DACFE7981F66EA2100630DA8 /* vi */,
+ DA3389671FA3EE2F001EA329 /* bg */,
+ DA33896B1FA3EF4A001EA329 /* hu */,
);
name = Foundation.stringsdict;
sourceTree = "<group>";
@@ -2631,6 +2663,7 @@
DA57D4AC1EBA922A00793288 /* vi */,
DA704CBC1F637405004B3F28 /* uk */,
DA704CBD1F63746E004B3F28 /* zh-Hant */,
+ DA3389691FA3EE50001EA329 /* bg */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
diff --git a/platform/ios/resources/Base.lproj/Localizable.strings b/platform/ios/resources/Base.lproj/Localizable.strings
index 3f59262d71..039ef4c4b1 100644
--- a/platform/ios/resources/Base.lproj/Localizable.strings
+++ b/platform/ios/resources/Base.lproj/Localizable.strings
@@ -34,6 +34,9 @@
/* Accessibility label */
"INFO_A11Y_LABEL" = "About this map";
+/* List separator */
+"LIST_SEPARATOR" = ", ";
+
/* User-friendly error description */
"LOAD_MAP_FAILED_DESC" = "The map failed to load because an unknown error occurred.";
@@ -46,12 +49,30 @@
/* Accessibility label */
"MAP_A11Y_LABEL" = "Map";
-/* Map accessibility value */
-"MAP_A11Y_VALUE" = "Zoom %1$dx\n%2$ld annotation(s) visible";
+/* Map accessibility value; {number of visible annotations} */
+"MAP_A11Y_VALUE_ANNOTATIONS" = "%ld annotation(s) visible.";
+
+/* Map accessibility value; {list of visible places} */
+"MAP_A11Y_VALUE_PLACES" = "Places visible: %@.";
+
+/* Map accessibility value; {number of visible roads} */
+"MAP_A11Y_VALUE_ROADS" = "%ld road(s) visible.";
+
+/* Map accessibility value; {zoom level} */
+"MAP_A11Y_VALUE_ZOOM" = "Zoom %dx.";
/* User-friendly error description */
"PARSE_STYLE_FAILED_DESC" = "The map failed to load because the style is corrupted.";
+/* Accessibility value indicating that a road is a divided road (dual carriageway) */
+"ROAD_DIVIDED_A11Y_VALUE" = "Divided road";
+
+/* Accessibility value indicating that a road is a one-way road */
+"ROAD_ONEWAY_A11Y_VALUE" = "One way";
+
+/* String format for accessibility value for road feature; {route number} */
+"ROAD_REF_A11Y_FMT" = "Route %@";
+
/* Action sheet title */
"SDK_NAME" = "Mapbox iOS SDK";
diff --git a/platform/ios/resources/bg.lproj/Localizable.strings b/platform/ios/resources/bg.lproj/Localizable.strings
new file mode 100644
index 0000000000..7098589b6b
--- /dev/null
+++ b/platform/ios/resources/bg.lproj/Localizable.strings
@@ -0,0 +1,93 @@
+/* Accessibility hint */
+"ANNOTATION_A11Y_HINT" = "Показва повече инфо";
+
+/* No comment provided by engineer. */
+"API_CLIENT_400_DESC" = "Неуспешна сесия за данни. Оригинална заявка: %@";
+
+/* No comment provided by engineer. */
+"API_CLIENT_400_REASON" = "Статус кодът беше %ld";
+
+/* No comment provided by engineer. */
+"CANCEL" = "Отказ";
+
+/* Accessibility hint for closing the selected annotation’s callout view and returning to the map */
+"CLOSE_CALLOUT_A11Y_HINT" = "Връща към картата";
+
+/* Accessibility hint */
+"COMPASS_A11Y_HINT" = "Завърта картата в посока север";
+
+/* Accessibility label */
+"COMPASS_A11Y_LABEL" = "Компас";
+
+/* Compass abbreviation for north */
+"COMPASS_NORTH" = "С";
+
+/* Instructions in Interface Builder designable; {key}, {plist file name} */
+"DESIGNABLE" = "За да се показва Mapbox карта тук, добави %1$@ към твоя токен за достъп в %2$@\n\nЗа подробни инструкции, виж:";
+
+/* Setup documentation URL display string; keep as short as possible */
+"FIRST_STEPS_URL" = "mapbox.com/help/first-steps-ios-sdk";
+
+/* Accessibility hint */
+"INFO_A11Y_HINT" = "Показва кредитите, форма за връзка и още";
+
+/* Accessibility label */
+"INFO_A11Y_LABEL" = "За тази карта";
+
+/* User-friendly error description */
+"LOAD_MAP_FAILED_DESC" = "Картата не се зареди поради неизвестна грешка.";
+
+/* User-friendly error description */
+"LOAD_STYLE_FAILED_DESC" = "Картата не се зареди поради незареждане на стила.";
+
+/* Accessibility label */
+"LOGO_A11Y_LABEL" = "Mapbox";
+
+/* Accessibility label */
+"MAP_A11Y_LABEL" = "Карта";
+
+/* Map accessibility value */
+"MAP_A11Y_VALUE" = "Мащаб %1$dх\n@2$ld видима(и) анотация(и)";
+
+/* User-friendly error description */
+"PARSE_STYLE_FAILED_DESC" = "Картата не се зареди поради повреден стил.";
+
+/* Action sheet title */
+"SDK_NAME" = "Mapbox iOS SDK";
+
+/* Developer-only SDK update notification; {latest version, in format x.x.x} */
+"SDK_UPDATE_AVAILABLE" = "Сега е налична Mapbox iOS SDK версия %@:";
+
+/* User-friendly error description */
+"STYLE_NOT_FOUND_DESC" = "Картата не се зареди поради неоткрит или несъвместим стил.";
+
+/* Telemetry prompt message */
+"TELEMETRY_DISABLED_MSG" = "Можеш да помогнеш OpenStreetMap и Mapbox да станат по-добри, като предоставиш анонимни данни за потребление.";
+
+/* Telemetry prompt button */
+"TELEMETRY_DISABLED_OFF" = "Не участвам";
+
+/* Telemetry prompt button */
+"TELEMETRY_DISABLED_ON" = "Участвам";
+
+/* Telemetry prompt message */
+"TELEMETRY_ENABLED_MSG" = "Помагаш OpenStreetMap и Mapbox да станат по-добри, като предоставяш анонимни данни за потребление.";
+
+/* Telemetry prompt button */
+"TELEMETRY_ENABLED_OFF" = "Спирам участие";
+
+/* Telemetry prompt button */
+"TELEMETRY_ENABLED_ON" = "Продължавам да участвам";
+
+/* Telemetry prompt button */
+"TELEMETRY_MORE" = "Искам още инфо";
+
+/* Action in attribution sheet */
+"TELEMETRY_NAME" = "Mapbox Телеметрия";
+
+/* Telemetry prompt title */
+"TELEMETRY_TITLE" = "Направи Mapbox картите по-добри";
+
+/* Default user location annotation title */
+"USER_DOT_TITLE" = "Сега си тук";
+
diff --git a/platform/ios/resources/bg.lproj/Localizable.stringsdict b/platform/ios/resources/bg.lproj/Localizable.stringsdict
new file mode 100644
index 0000000000..f155a02acc
--- /dev/null
+++ b/platform/ios/resources/bg.lproj/Localizable.stringsdict
@@ -0,0 +1,34 @@
+<?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>MAP_A11Y_VALUE</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@level@
+%#@count@</string>
+ <key>level</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>one</key>
+ <string>Мащаб %dx</string>
+ <key>other</key>
+ <string>Мащаб %dx</string>
+ </dict>
+ <key>count</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>ld</string>
+ <key>one</key>
+ <string>%d видима анотация</string>
+ <key>other</key>
+ <string>%d видими анотации</string>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/platform/ios/resources/en.lproj/Localizable.stringsdict b/platform/ios/resources/en.lproj/Localizable.stringsdict
index e849318fe5..435b7bdfe8 100644
--- a/platform/ios/resources/en.lproj/Localizable.stringsdict
+++ b/platform/ios/resources/en.lproj/Localizable.stringsdict
@@ -2,22 +2,26 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
- <key>MAP_A11Y_VALUE</key>
+ <key>MAP_A11Y_VALUE_ANNOTATIONS</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
- <string>%#@level@
-%#@count@</string>
- <key>level</key>
+ <string>%#@count@</string>
+ <key>count</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
- <string>d</string>
+ <string>ld</string>
<key>one</key>
- <string>Zoom %dx</string>
+ <string>%d annotation visible</string>
<key>other</key>
- <string>Zoom %dx</string>
+ <string>%d annotations visible</string>
</dict>
+ </dict>
+ <key>MAP_A11Y_VALUE_ROADS</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@count@</string>
<key>count</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
@@ -25,9 +29,25 @@
<key>NSStringFormatValueTypeKey</key>
<string>ld</string>
<key>one</key>
- <string>%d annotation visible</string>
+ <string>%d road visible</string>
<key>other</key>
- <string>%d annotations visible</string>
+ <string>%d roads visible</string>
+ </dict>
+ </dict>
+ <key>MAP_A11Y_VALUE_ZOOM</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@level@</string>
+ <key>level</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>one</key>
+ <string>Zoom %dx</string>
+ <key>other</key>
+ <string>Zoom %dx</string>
</dict>
</dict>
</dict>
diff --git a/platform/ios/resources/pt-BR.lproj/Localizable.strings b/platform/ios/resources/pt-BR.lproj/Localizable.strings
index 56eaa7cf9f..53c3d3a12d 100644
--- a/platform/ios/resources/pt-BR.lproj/Localizable.strings
+++ b/platform/ios/resources/pt-BR.lproj/Localizable.strings
@@ -2,7 +2,7 @@
"ANNOTATION_A11Y_HINT" = "Mostrar mais informações";
/* No comment provided by engineer. */
-"API_CLIENT_400_DESC" = "The session data task failed. Original request was: %@";
+"API_CLIENT_400_DESC" = "Tarefa de dados da sessão falhou. Requisição original: %@";
/* No comment provided by engineer. */
"API_CLIENT_400_REASON" = "O código de status foi %ld";
@@ -10,6 +10,9 @@
/* No comment provided by engineer. */
"CANCEL" = "Cancelar";
+/* Accessibility hint for closing the selected annotation’s callout view and returning to the map */
+"CLOSE_CALLOUT_A11Y_HINT" = "Retornar ao mapa";
+
/* Accessibility hint */
"COMPASS_A11Y_HINT" = "Rotaciona o mapa com face ao norte";
@@ -20,7 +23,7 @@
"COMPASS_NORTH" = "N";
/* Instructions in Interface Builder designable; {key}, {plist file name} */
-"DESIGNABLE" = "To display a Mapbox-hosted map here, set %1$@ to your access token in %2$@\n\nFor detailed instructions, see:";
+"DESIGNABLE" = "Para exibir um mapa hospedado no Mapbox aqui, insira %1$@ para seu token de acesso %2$@\n\nPara maiores detalhes, veja:";
/* Setup documentation URL display string; keep as short as possible */
"FIRST_STEPS_URL" = "mapbox.com/help/first-steps-ios-sdk";
@@ -31,6 +34,12 @@
/* Accessibility label */
"INFO_A11Y_LABEL" = "Sobre este mapa";
+/* User-friendly error description */
+"LOAD_MAP_FAILED_DESC" = "Falha ao carregar mapa devido a um erro desconhecido";
+
+/* User-friendly error description */
+"LOAD_STYLE_FAILED_DESC" = "Falha ao carregar mapa porque o estilo não pode ser carregado";
+
/* Accessibility label */
"LOGO_A11Y_LABEL" = "Mapbox";
@@ -40,11 +49,20 @@
/* Map accessibility value */
"MAP_A11Y_VALUE" = "Zoom %1$dx\n%2$ld anotações visíveis";
+/* User-friendly error description */
+"PARSE_STYLE_FAILED_DESC" = "Falha ao carregar mapa porque o estilo está corrompido.";
+
/* Action sheet title */
"SDK_NAME" = "Mapbox iOS SDK";
+/* Developer-only SDK update notification; {latest version, in format x.x.x} */
+"SDK_UPDATE_AVAILABLE" = "SDK Mapbox para iOS versão %@ está disponível:";
+
+/* User-friendly error description */
+"STYLE_NOT_FOUND_DESC" = "Falha ao carregar mapa porque o estilo não pode ser encontrado ou é incompatível.";
+
/* Telemetry prompt message */
-"TELEMETRY_DISABLED_MSG" = "You can help make OpenStreetMap and Mapbox maps better by contributing anonymous usage data.";
+"TELEMETRY_DISABLED_MSG" = "Você pode ajudar a tornar o OpenStreetMap e Mapbox ainda melhor contribuindo anonimamente com seus dados de uso.";
/* Telemetry prompt button */
"TELEMETRY_DISABLED_OFF" = "Não Participar";
@@ -53,7 +71,7 @@
"TELEMETRY_DISABLED_ON" = "Participar";
/* Telemetry prompt message */
-"TELEMETRY_ENABLED_MSG" = "You are helping to make OpenStreetMap and Mapbox maps better by contributing anonymous usage data.";
+"TELEMETRY_ENABLED_MSG" = "Você está ajudando a tornar o OpenStreetMap e Mapbox ainda melhor contribuindo anonimamente com seus dados de uso.";
/* Telemetry prompt button */
"TELEMETRY_ENABLED_OFF" = "Parar de Participar";
diff --git a/platform/ios/scripts/deploy-packages.sh b/platform/ios/scripts/deploy-packages.sh
index bdc946497d..103e53768c 100755
--- a/platform/ios/scripts/deploy-packages.sh
+++ b/platform/ios/scripts/deploy-packages.sh
@@ -58,17 +58,17 @@ BINARY_DIRECTORY=${BINARY_DIRECTORY:-build/ios/deploy}
GITHUB_RELEASE=${GITHUB_RELEASE:-true}
PUBLISH_PRE_FLAG=''
+if [[ -z `which github-release` ]]; then
+ step "Installing github-release…"
+ brew install github-release
+ if [ -z `which github-release` ]; then
+ echo "Unable to install github-release. See: https://github.com/aktau/github-release"
+ exit 1
+ fi
+fi
+
if [[ ${GITHUB_RELEASE} = "true" ]]; then
GITHUB_RELEASE=true # Assign bool, not just a string
-
- if [[ -z `which github-release` ]]; then
- step "Installing github-release…"
- brew install github-release
- if [ -z `which github-release` ]; then
- echo "Unable to install github-release. See: https://github.com/aktau/github-release"
- exit 1
- fi
- fi
fi
if [[ -z ${VERSION_TAG} ]]; then
@@ -83,7 +83,7 @@ if [[ $( echo ${VERSION_TAG} | grep --invert-match ios-v ) ]]; then
exit 1
fi
-if [[ $( curl --head https://api.github.com/repos/${GITHUB_USER}/${GITHUB_REPO}/releases/tags/${VERSION_TAG} | head -n 1 | grep -c "404 Not Found") == 0 ]]; then
+if github-release info --tag ${VERSION_TAG} | grep --quiet "draft: ✗"; then
echo "Error: ${VERSION_TAG} has already been published on GitHub"
echo "See: https://github.com/${GITHUB_USER}/${GITHUB_REPO}/releases/tag/${VERSION_TAG}"
exit 1
diff --git a/platform/ios/scripts/document.sh b/platform/ios/scripts/document.sh
index 170debb625..57b596a4b9 100755
--- a/platform/ios/scripts/document.sh
+++ b/platform/ios/scripts/document.sh
@@ -6,7 +6,7 @@ set -u
if [ -z `which jazzy` ]; then
echo "Installing jazzy…"
- gem install jazzy
+ gem install jazzy --no-rdoc --no-ri
if [ -z `which jazzy` ]; then
echo "Unable to install jazzy. See https://github.com/mapbox/mapbox-gl-native/blob/master/platform/ios/INSTALL.md"
exit 1
diff --git a/platform/ios/src/MGLCompactCalloutView.h b/platform/ios/src/MGLCompactCalloutView.h
index 56c48a99e5..5cecf37ff6 100644
--- a/platform/ios/src/MGLCompactCalloutView.h
+++ b/platform/ios/src/MGLCompactCalloutView.h
@@ -7,7 +7,7 @@
callout view displays the represented annotation’s title, subtitle, and
accessory views in a compact, two-line layout.
*/
-@interface MGLCompactCalloutView : SMCalloutView <MGLCalloutView>
+@interface MGLCompactCalloutView : MGLSMCalloutView <MGLCalloutView>
+ (instancetype)platformCalloutView;
diff --git a/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m
index 21f6aaa540..1ed3d86ad1 100644
--- a/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m
+++ b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m
@@ -10,7 +10,7 @@ const CGFloat MGLUserLocationAnnotationDotSize = 22.0;
const CGFloat MGLUserLocationAnnotationHaloSize = 115.0;
const CGFloat MGLUserLocationAnnotationPuckSize = 45.0;
-const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuckSize * 0.6;
+const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuckSize * 0.5;
const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01;
@@ -175,12 +175,16 @@ const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01;
_puckArrow = [CAShapeLayer layer];
_puckArrow.path = [[self puckArrow] CGPath];
_puckArrow.fillColor = [self.mapView.tintColor CGColor];
- _puckArrow.bounds = CGRectMake(0, 0, MGLUserLocationAnnotationArrowSize, MGLUserLocationAnnotationArrowSize);
- _puckArrow.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0);
+ _puckArrow.bounds = CGRectMake(0, 0, round(MGLUserLocationAnnotationArrowSize), round(MGLUserLocationAnnotationArrowSize));
+ _puckArrow.position = CGPointMake(CGRectGetMidX(super.bounds), CGRectGetMidY(super.bounds));
_puckArrow.shouldRasterize = YES;
_puckArrow.rasterizationScale = [UIScreen mainScreen].scale;
_puckArrow.drawsAsynchronously = YES;
+ _puckArrow.lineJoin = @"round";
+ _puckArrow.lineWidth = 1.f;
+ _puckArrow.strokeColor = _puckArrow.fillColor;
+
[self.layer addSublayer:_puckArrow];
}
if (self.userLocation.location.course >= 0)
@@ -302,7 +306,7 @@ const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01;
[CATransaction setDisableActions:shouldDisableActions];
_accuracyRingLayer.bounds = CGRectMake(0, 0, accuracyRingSize, accuracyRingSize);
- _accuracyRingLayer.cornerRadius = accuracyRingSize / 2;
+ _accuracyRingLayer.cornerRadius = accuracyRingSize / 2.0;
// match the halo to the accuracy ring
_haloLayer.bounds = _accuracyRingLayer.bounds;
@@ -431,9 +435,11 @@ const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01;
- (CALayer *)circleLayerWithSize:(CGFloat)layerSize
{
+ layerSize = round(layerSize);
+
CALayer *circleLayer = [CALayer layer];
circleLayer.bounds = CGRectMake(0, 0, layerSize, layerSize);
- circleLayer.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0);
+ circleLayer.position = CGPointMake(CGRectGetMidX(super.bounds), CGRectGetMidY(super.bounds));
circleLayer.cornerRadius = layerSize / 2.0;
circleLayer.shouldRasterize = YES;
circleLayer.rasterizationScale = [UIScreen mainScreen].scale;
@@ -456,7 +462,7 @@ const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01;
- (CGFloat)calculateAccuracyRingSize
{
// diameter in screen points
- return self.userLocation.location.horizontalAccuracy / [self.mapView metersPerPointAtLatitude:self.userLocation.coordinate.latitude] * 2.0;
+ return round(self.userLocation.location.horizontalAccuracy / [self.mapView metersPerPointAtLatitude:self.userLocation.coordinate.latitude] * 2.0);
}
@end
diff --git a/platform/ios/src/MGLMapAccessibilityElement.h b/platform/ios/src/MGLMapAccessibilityElement.h
new file mode 100644
index 0000000000..952f6cbf2f
--- /dev/null
+++ b/platform/ios/src/MGLMapAccessibilityElement.h
@@ -0,0 +1,54 @@
+#import <UIKit/UIKit.h>
+
+#import "MGLFoundation.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol MGLFeature;
+
+/// Unique identifier representing a single annotation in mbgl.
+typedef uint32_t MGLAnnotationTag;
+
+/** An accessibility element representing something that appears on the map. */
+MGL_EXPORT
+@interface MGLMapAccessibilityElement : UIAccessibilityElement
+
+@end
+
+/** An accessibility element representing a map annotation. */
+@interface MGLAnnotationAccessibilityElement : MGLMapAccessibilityElement
+
+/** The tag of the annotation represented by this element. */
+@property (nonatomic) MGLAnnotationTag tag;
+
+- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)identifier NS_DESIGNATED_INITIALIZER;
+
+@end
+
+/** An accessibility element representing a map feature. */
+MGL_EXPORT
+@interface MGLFeatureAccessibilityElement : MGLMapAccessibilityElement
+
+/** The feature represented by this element. */
+@property (nonatomic, strong) id <MGLFeature> feature;
+
+- (instancetype)initWithAccessibilityContainer:(id)container feature:(id <MGLFeature>)feature NS_DESIGNATED_INITIALIZER;
+
+@end
+
+/** An accessibility element representing a place feature. */
+MGL_EXPORT
+@interface MGLPlaceFeatureAccessibilityElement : MGLFeatureAccessibilityElement
+@end
+
+/** An accessibility element representing a road feature. */
+MGL_EXPORT
+@interface MGLRoadFeatureAccessibilityElement : MGLFeatureAccessibilityElement
+@end
+
+/** An accessibility element representing the MGLMapView at large. */
+MGL_EXPORT
+@interface MGLMapViewProxyAccessibilityElement : UIAccessibilityElement
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/ios/src/MGLMapAccessibilityElement.mm b/platform/ios/src/MGLMapAccessibilityElement.mm
new file mode 100644
index 0000000000..4e5f165fbf
--- /dev/null
+++ b/platform/ios/src/MGLMapAccessibilityElement.mm
@@ -0,0 +1,199 @@
+#import "MGLMapAccessibilityElement.h"
+#import "MGLDistanceFormatter.h"
+#import "MGLCompassDirectionFormatter.h"
+#import "MGLFeature.h"
+#import "MGLVectorSource+MGLAdditions.h"
+
+#import "NSBundle+MGLAdditions.h"
+#import "MGLGeometry_Private.h"
+
+@implementation MGLMapAccessibilityElement
+
+- (UIAccessibilityTraits)accessibilityTraits {
+ return super.accessibilityTraits | UIAccessibilityTraitAdjustable;
+}
+
+- (void)accessibilityIncrement {
+ [self.accessibilityContainer accessibilityIncrement];
+}
+
+- (void)accessibilityDecrement {
+ [self.accessibilityContainer accessibilityDecrement];
+}
+
+@end
+
+@implementation MGLAnnotationAccessibilityElement
+
+- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)tag {
+ if (self = [super initWithAccessibilityContainer:container]) {
+ _tag = tag;
+ self.accessibilityHint = NSLocalizedStringWithDefaultValue(@"ANNOTATION_A11Y_HINT", nil, nil, @"Shows more info", @"Accessibility hint");
+ }
+ return self;
+}
+
+- (UIAccessibilityTraits)accessibilityTraits {
+ return super.accessibilityTraits | UIAccessibilityTraitButton;
+}
+
+@end
+
+@implementation MGLFeatureAccessibilityElement
+
+- (instancetype)initWithAccessibilityContainer:(id)container feature:(id<MGLFeature>)feature {
+ if (self = [super initWithAccessibilityContainer:container]) {
+ _feature = feature;
+
+ NSString *languageCode = [MGLVectorSource preferredMapboxStreetsLanguage];
+ NSString *nameAttribute = [NSString stringWithFormat:@"name_%@", languageCode];
+ NSString *name = [feature attributeForKey:nameAttribute];
+
+ // If a feature hasn’t been translated into the preferred language, it
+ // may be in the local language, which may be written in another script.
+ // Romanize it.
+ NSLocale *locale = [NSLocale localeWithLocaleIdentifier:languageCode];
+ NSOrthography *orthography;
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunguarded-availability-new"
+ if ([NSOrthography respondsToSelector:@selector(defaultOrthographyForLanguage:)]) {
+ orthography = [NSOrthography defaultOrthographyForLanguage:locale.localeIdentifier];
+ }
+#pragma clang diagnostic pop
+#endif
+ if ([orthography.dominantScript isEqualToString:@"Latn"]) {
+ name = [name stringByApplyingTransform:NSStringTransformToLatin reverse:NO];
+ }
+
+ self.accessibilityLabel = name;
+ }
+ return self;
+}
+
+- (UIAccessibilityTraits)accessibilityTraits {
+ return super.accessibilityTraits | UIAccessibilityTraitStaticText;
+}
+
+@end
+
+@implementation MGLPlaceFeatureAccessibilityElement
+
+- (instancetype)initWithAccessibilityContainer:(id)container feature:(id<MGLFeature>)feature {
+ if (self = [super initWithAccessibilityContainer:container feature:feature]) {
+ NSDictionary *attributes = feature.attributes;
+ NSMutableArray *facts = [NSMutableArray array];
+
+ // Announce the kind of place or POI.
+ if (attributes[@"type"]) {
+ // FIXME: Unfortunately, these types aren’t a closed set that can be
+ // localized, since they’re based on OpenStreetMap tags.
+ NSString *type = [attributes[@"type"] stringByReplacingOccurrencesOfString:@"_"
+ withString:@" "];
+ [facts addObject:type];
+ }
+ // Announce the kind of airport, rail station, or mountain based on its
+ // Maki image name.
+ else if (attributes[@"maki"]) {
+ // TODO: Localize Maki image names.
+ [facts addObject:attributes[@"maki"]];
+ }
+
+ // Announce the peak’s elevation in the preferred units.
+ if (attributes[@"elevation_m"] ?: attributes[@"elevation_ft"]) {
+ NSLengthFormatter *formatter = [[NSLengthFormatter alloc] init];
+ formatter.unitStyle = NSFormattingUnitStyleLong;
+
+ NSNumber *elevationValue;
+ NSLengthFormatterUnit unit;
+ BOOL usesMetricSystem = ![[formatter.numberFormatter.locale objectForKey:NSLocaleMeasurementSystem]
+ isEqualToString:@"U.S."];
+ if (usesMetricSystem) {
+ elevationValue = attributes[@"elevation_m"];
+ unit = NSLengthFormatterUnitMeter;
+ } else {
+ elevationValue = attributes[@"elevation_ft"];
+ unit = NSLengthFormatterUnitFoot;
+ }
+ [facts addObject:[formatter stringFromValue:elevationValue.doubleValue unit:unit]];
+ }
+
+ if (facts.count) {
+ NSString *separator = NSLocalizedStringWithDefaultValue(@"LIST_SEPARATOR", nil, nil, @", ", @"List separator");
+ self.accessibilityValue = [facts componentsJoinedByString:separator];
+ }
+ }
+ return self;
+}
+
+@end
+
+@implementation MGLRoadFeatureAccessibilityElement
+
+- (instancetype)initWithAccessibilityContainer:(id)container feature:(id<MGLFeature>)feature {
+ if (self = [super initWithAccessibilityContainer:container feature:feature]) {
+ NSDictionary *attributes = feature.attributes;
+ NSMutableArray *facts = [NSMutableArray array];
+
+ // Announce the route number.
+ if (attributes[@"ref"]) {
+ // TODO: Decorate the route number with the network name based on the shield attribute.
+ NSString *ref = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"ROAD_REF_A11Y_FMT", nil, nil, @"Route %@", @"String format for accessibility value for road feature; {route number}"), attributes[@"ref"]];
+ [facts addObject:ref];
+ }
+
+ // Announce whether the road is a one-way road.
+ if ([attributes[@"oneway"] isEqualToString:@"true"]) {
+ [facts addObject:NSLocalizedStringWithDefaultValue(@"ROAD_ONEWAY_A11Y_VALUE", nil, nil, @"One way", @"Accessibility value indicating that a road is a one-way road")];
+ }
+
+ // Announce whether the road is a divided road.
+ MGLPolyline *polyline;
+ if ([feature isKindOfClass:[MGLMultiPolylineFeature class]]) {
+ [facts addObject:NSLocalizedStringWithDefaultValue(@"ROAD_DIVIDED_A11Y_VALUE", nil, nil, @"Divided road", @"Accessibility value indicating that a road is a divided road (dual carriageway)")];
+ polyline = [(MGLMultiPolylineFeature *)feature polylines].firstObject;
+ }
+
+ // Announce the road’s general direction.
+ if ([feature isKindOfClass:[MGLPolylineFeature class]]) {
+ polyline = (MGLPolylineFeature *)feature;
+ }
+ if (polyline) {
+ NSUInteger pointCount = polyline.pointCount;
+ if (pointCount) {
+ CLLocationCoordinate2D *coordinates = polyline.coordinates;
+ CLLocationDirection startDirection = MGLDirectionBetweenCoordinates(coordinates[pointCount - 1], coordinates[0]);
+ CLLocationDirection endDirection = MGLDirectionBetweenCoordinates(coordinates[0], coordinates[pointCount - 1]);
+
+ MGLCompassDirectionFormatter *formatter = [[MGLCompassDirectionFormatter alloc] init];
+ formatter.unitStyle = NSFormattingUnitStyleLong;
+
+ NSString *startDirectionString = [formatter stringFromDirection:startDirection];
+ NSString *endDirectionString = [formatter stringFromDirection:endDirection];
+ NSString *directionString = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"ROAD_DIRECTION_A11Y_FMT", nil, nil, @"%@ to %@", @"String format for accessibility value for road feature; {starting compass direction}, {ending compass direction}"), startDirectionString, endDirectionString];
+ [facts addObject:directionString];
+ }
+ }
+
+ if (facts.count) {
+ NSString *separator = NSLocalizedStringWithDefaultValue(@"LIST_SEPARATOR", nil, nil, @", ", @"List separator");
+ self.accessibilityValue = [facts componentsJoinedByString:separator];
+ }
+ }
+ return self;
+}
+
+@end
+
+@implementation MGLMapViewProxyAccessibilityElement
+
+- (instancetype)initWithAccessibilityContainer:(id)container {
+ if (self = [super initWithAccessibilityContainer:container]) {
+ self.accessibilityTraits = UIAccessibilityTraitButton;
+ self.accessibilityLabel = [self.accessibilityContainer accessibilityLabel];
+ self.accessibilityHint = NSLocalizedStringWithDefaultValue(@"CLOSE_CALLOUT_A11Y_HINT", nil, nil, @"Returns to the map", @"Accessibility hint for closing the selected annotation’s callout view and returning to the map");
+ }
+ return self;
+}
+
+@end
diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm
index d5bfdbbc9f..ccab1fad36 100644
--- a/platform/ios/src/MGLMapView.mm
+++ b/platform/ios/src/MGLMapView.mm
@@ -44,6 +44,7 @@
#import "MGLFoundation_Private.h"
#import "MGLRendererFrontend.h"
+#import "MGLVectorSource+MGLAdditions.h"
#import "NSBundle+MGLAdditions.h"
#import "NSDate+MGLAdditions.h"
#import "NSException+MGLAdditions.h"
@@ -69,6 +70,7 @@
#import "MGLAnnotationContainerView.h"
#import "MGLAnnotationContainerView_Private.h"
#import "MGLAttributionInfo_Private.h"
+#import "MGLMapAccessibilityElement.h"
#include <algorithm>
#include <cstdlib>
@@ -140,9 +142,6 @@ const CGFloat MGLAnnotationImagePaddingForCallout = 1;
const CGSize MGLAnnotationAccessibilityElementMinimumSize = CGSizeMake(10, 10);
-/// Unique identifier representing a single annotation in mbgl.
-typedef uint32_t MGLAnnotationTag;
-
/// An indication that the requested annotation was not found or is nonexistent.
enum { MGLAnnotationTagNotFound = UINT32_MAX };
@@ -165,38 +164,6 @@ mbgl::util::UnitBezier MGLUnitBezierForMediaTimingFunction(CAMediaTimingFunction
return { p1[0], p1[1], p2[0], p2[1] };
}
-@interface MGLAnnotationAccessibilityElement : UIAccessibilityElement
-
-@property (nonatomic) MGLAnnotationTag tag;
-
-- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)identifier NS_DESIGNATED_INITIALIZER;
-
-@end
-
-@implementation MGLAnnotationAccessibilityElement
-
-- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)tag
-{
- if (self = [super initWithAccessibilityContainer:container])
- {
- _tag = tag;
- self.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitAdjustable;
- }
- return self;
-}
-
-- (void)accessibilityIncrement
-{
- [self.accessibilityContainer accessibilityIncrement];
-}
-
-- (void)accessibilityDecrement
-{
- [self.accessibilityContainer accessibilityDecrement];
-}
-
-@end
-
/// Lightweight container for metadata about an annotation, including the annotation itself.
class MGLAnnotationContext {
public:
@@ -208,32 +175,12 @@ public:
NSString *viewReuseIdentifier;
};
-/** An accessibility element representing the MGLMapView at large. */
-@interface MGLMapViewProxyAccessibilityElement : UIAccessibilityElement
-
-@end
-
-@implementation MGLMapViewProxyAccessibilityElement
-
-- (instancetype)initWithAccessibilityContainer:(id)container
-{
- if (self = [super initWithAccessibilityContainer:container])
- {
- self.accessibilityTraits = UIAccessibilityTraitButton;
- self.accessibilityLabel = [self.accessibilityContainer accessibilityLabel];
- self.accessibilityHint = NSLocalizedStringWithDefaultValue(@"CLOSE_CALLOUT_A11Y_HINT", nil, nil, @"Returns to the map", @"Accessibility hint for closing the selected annotation’s callout view and returning to the map");
- }
- return self;
-}
-
-@end
-
#pragma mark - Private -
@interface MGLMapView () <UIGestureRecognizerDelegate,
GLKViewDelegate,
CLLocationManagerDelegate,
- SMCalloutViewDelegate,
+ MGLSMCalloutViewDelegate,
MGLCalloutViewDelegate,
MGLMultiPointDelegate,
MGLAnnotationImageDelegate>
@@ -328,6 +275,10 @@ public:
BOOL _delegateHasLineWidthsForShapeAnnotations;
MGLCompassDirectionFormatter *_accessibilityCompassFormatter;
+ NS_ARRAY_OF(id <MGLFeature>) *_visiblePlaceFeatures;
+ NS_ARRAY_OF(id <MGLFeature>) *_visibleRoadFeatures;
+ NS_MUTABLE_SET_OF(MGLFeatureAccessibilityElement *) *_featureAccessibilityElements;
+ BOOL _accessibilityValueAnnouncementIsPending;
MGLReachability *_reachability;
}
@@ -437,10 +388,9 @@ public:
self.accessibilityTraits = UIAccessibilityTraitAllowsDirectInteraction | UIAccessibilityTraitAdjustable;
_accessibilityCompassFormatter = [[MGLCompassDirectionFormatter alloc] init];
_accessibilityCompassFormatter.unitStyle = NSFormattingUnitStyleLong;
-
self.backgroundColor = [UIColor clearColor];
self.clipsToBounds = YES;
-
+ if (@available(iOS 11.0, *)) { self.accessibilityIgnoresInvertColors = YES; }
// setup mbgl view
_mbglView = new MBGLView(self);
@@ -491,9 +441,6 @@ public:
_logoView.accessibilityTraits = UIAccessibilityTraitStaticText;
_logoView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"LOGO_A11Y_LABEL", nil, nil, @"Mapbox", @"Accessibility label");
_logoView.translatesAutoresizingMaskIntoConstraints = NO;
-#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
- if ([_logoView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _logoView.accessibilityIgnoresInvertColors = YES; }
-#endif
[self addSubview:_logoView];
_logoViewConstraints = [NSMutableArray array];
@@ -502,9 +449,6 @@ public:
_attributionButton = [UIButton buttonWithType:UIButtonTypeInfoLight];
_attributionButton.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"INFO_A11Y_LABEL", nil, nil, @"About this map", @"Accessibility label");
_attributionButton.accessibilityHint = NSLocalizedStringWithDefaultValue(@"INFO_A11Y_HINT", nil, nil, @"Shows credits, a feedback form, and more", @"Accessibility hint");
-#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
- if ([_attributionButton respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _attributionButton.accessibilityIgnoresInvertColors = YES; }
-#endif
[_attributionButton addTarget:self action:@selector(showAttribution:) forControlEvents:UIControlEventTouchUpInside];
_attributionButton.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_attributionButton];
@@ -521,9 +465,6 @@ public:
_compassView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_LABEL", nil, nil, @"Compass", @"Accessibility label");
_compassView.accessibilityHint = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_HINT", nil, nil, @"Rotates the map to face due north", @"Accessibility hint");
_compassView.translatesAutoresizingMaskIntoConstraints = NO;
-#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
- if ([_compassView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _compassView.accessibilityIgnoresInvertColors = YES; }
-#endif
[self addSubview:_compassView];
_compassViewConstraints = [NSMutableArray array];
@@ -531,9 +472,6 @@ public:
//
_scaleBar = [[MGLScaleBar alloc] init];
_scaleBar.translatesAutoresizingMaskIntoConstraints = NO;
-#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
- if ([_scaleBar respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _scaleBar.accessibilityIgnoresInvertColors = YES; }
-#endif
[self addSubview:_scaleBar];
_scaleBarConstraints = [NSMutableArray array];
@@ -648,9 +586,7 @@ public:
_glView.contentScaleFactor = [UIScreen instancesRespondToSelector:@selector(nativeScale)] ? [[UIScreen mainScreen] nativeScale] : [[UIScreen mainScreen] scale];
_glView.layer.opaque = _opaque;
_glView.delegate = self;
-#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
- if ([_glView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _glView.accessibilityIgnoresInvertColors = YES; }
-#endif
+
[_glView bindDrawable];
[self insertSubview:_glView atIndex:0];
_glView.contentMode = UIViewContentModeCenter;
@@ -1616,7 +1552,9 @@ public:
id<MGLAnnotation>annotation = [self annotationForGestureRecognizer:singleTap persistingResults:YES];
if(annotation)
{
- [self selectAnnotation:annotation animated:YES];
+ CGPoint calloutPoint = [singleTap locationInView:self];
+ CGRect positionRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:calloutPoint];
+ [self selectAnnotation:annotation animated:YES calloutPositioningRect:positionRect];
}
else
{
@@ -1938,7 +1876,7 @@ public:
return [self.delegate respondsToSelector:@selector(mapView:tapOnCalloutForAnnotation:)];
}
-- (void)calloutViewClicked:(__unused SMCalloutView *)calloutView
+- (void)calloutViewClicked:(__unused MGLSMCalloutView *)calloutView
{
if ([self.delegate respondsToSelector:@selector(mapView:tapOnCalloutForAnnotation:)])
{
@@ -2008,14 +1946,14 @@ public:
return ([validSimultaneousGestures containsObject:gestureRecognizer] && [validSimultaneousGestures containsObject:otherGestureRecognizer]);
}
-
+
- (CLLocationDegrees)angleBetweenPoints:(CGPoint)west east:(CGPoint)east
{
CGFloat slope = (west.y - east.y) / (west.x - east.x);
-
+
CGFloat angle = atan(fabs(slope));
CLLocationDegrees degrees = MGLDegreesFromRadians(angle);
-
+
return degrees;
}
@@ -2354,8 +2292,61 @@ public:
- (NSString *)accessibilityValue
{
+ NSMutableArray *facts = [NSMutableArray array];
+
double zoomLevel = round(self.zoomLevel + 1);
- return [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE", nil, nil, @"Zoom %dx\n%ld annotation(s) visible", @"Map accessibility value"), (int)zoomLevel, (long)self.accessibilityAnnotationCount];
+ [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_ZOOM", nil, nil, @"Zoom %dx.", @"Map accessibility value; {zoom level}"), (int)zoomLevel]];
+
+ NSInteger annotationCount = self.accessibilityAnnotationCount;
+ if (annotationCount) {
+ [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_ANNOTATIONS", nil, nil, @"%ld annotation(s) visible.", @"Map accessibility value; {number of visible annotations}"), (long)self.accessibilityAnnotationCount]];
+ }
+
+ NSArray *placeFeatures = self.visiblePlaceFeatures;
+ if (placeFeatures.count) {
+ NSMutableArray *placesArray = [NSMutableArray arrayWithCapacity:placeFeatures.count];
+ NSMutableSet *placesSet = [NSMutableSet setWithCapacity:placeFeatures.count];
+ for (id <MGLFeature> placeFeature in placeFeatures.reverseObjectEnumerator) {
+ NSString *name = [placeFeature attributeForKey:@"name"];
+ if (![placesSet containsObject:name]) {
+ [placesArray addObject:name];
+ [placesSet addObject:name];
+ }
+ if (placesArray.count >= 3) {
+ break;
+ }
+ }
+ NSString *placesString = [placesArray componentsJoinedByString:NSLocalizedStringWithDefaultValue(@"LIST_SEPARATOR", nil, nil, @", ", @"List separator")];
+ [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_PLACES", nil, nil, @"Places visible: %@.", @"Map accessibility value; {list of visible places}"), placesString]];
+ }
+
+ NSArray *roadFeatures = self.visibleRoadFeatures;
+ if (roadFeatures.count) {
+ [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_ROADS", nil, nil, @"%ld road(s) visible.", @"Map accessibility value; {number of visible roads}"), roadFeatures.count]];
+ }
+
+ NSString *value = [facts componentsJoinedByString:@" "];
+ return value;
+}
+
+- (NS_ARRAY_OF(id <MGLFeature>) *)visiblePlaceFeatures
+{
+ if (!_visiblePlaceFeatures)
+ {
+ NSArray *placeStyleLayerIdentifiers = [self.style.placeStyleLayers valueForKey:@"identifier"];
+ _visiblePlaceFeatures = [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:placeStyleLayerIdentifiers]];
+ }
+ return _visiblePlaceFeatures;
+}
+
+- (NS_ARRAY_OF(id <MGLFeature>) *)visibleRoadFeatures
+{
+ if (!_visibleRoadFeatures)
+ {
+ NSArray *roadStyleLayerIdentifiers = [self.style.roadStyleLayers valueForKey:@"identifier"];
+ _visibleRoadFeatures = [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:roadStyleLayerIdentifiers]];
+ }
+ return _visibleRoadFeatures;
}
- (CGRect)accessibilityFrame
@@ -2389,14 +2380,9 @@ public:
{
if (self.calloutViewForSelectedAnnotation)
{
- return 2 /* selectedAnnotationCalloutView, mapViewProxyAccessibilityElement */;
- }
- NSInteger count = self.accessibilityAnnotationCount + 2 /* compass, attributionButton */;
- if (self.userLocationAnnotationView)
- {
- count++;
+ return 2 /* calloutViewForSelectedAnnotation, mapViewProxyAccessibilityElement */;
}
- return count;
+ return !!self.userLocationAnnotationView + self.accessibilityAnnotationCount + self.visiblePlaceFeatures.count + self.visibleRoadFeatures.count + 2 /* compass, attributionButton */;
}
- (NSInteger)accessibilityAnnotationCount
@@ -2421,67 +2407,123 @@ public:
}
return nil;
}
- std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds];
-
- // Ornaments
- if (index == 0)
+
+ // Compass
+ NSUInteger compassIndex = 0;
+ if (index == compassIndex)
{
return self.compassView;
}
- if ( ! self.userLocationAnnotationView)
- {
- index++;
- }
- else if (index == 1)
+
+ // User location annotation
+ NSRange userLocationAnnotationRange = NSMakeRange(compassIndex + 1, !!self.userLocationAnnotationView);
+ if (NSLocationInRange(index, userLocationAnnotationRange))
{
return self.userLocationAnnotationView;
}
- if (index > 0 && (NSUInteger)index == visibleAnnotations.size() + 2 /* compass, userLocationAnnotationView */)
- {
- return self.attributionButton;
- }
-
- std::sort(visibleAnnotations.begin(), visibleAnnotations.end());
+
CGPoint centerPoint = self.contentCenter;
if (self.userTrackingMode != MGLUserTrackingModeNone)
{
centerPoint = self.userLocationAnnotationViewCenter;
}
- CLLocationCoordinate2D currentCoordinate = [self convertPoint:centerPoint toCoordinateFromView:self];
- std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) {
- CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate];
- CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate];
- CLLocationDegrees deltaA = hypot(coordinateA.latitude - currentCoordinate.latitude,
- coordinateA.longitude - currentCoordinate.longitude);
- CLLocationDegrees deltaB = hypot(coordinateB.latitude - currentCoordinate.latitude,
- coordinateB.longitude - currentCoordinate.longitude);
- return deltaA < deltaB;
- });
-
- NSUInteger annotationIndex = MGLAnnotationTagNotFound;
- if (index >= 0 && (NSUInteger)(index - 2) < visibleAnnotations.size())
+
+ // Visible annotations
+ std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds];
+ NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size());
+ if (NSLocationInRange(index, visibleAnnotationRange))
+ {
+ std::sort(visibleAnnotations.begin(), visibleAnnotations.end());
+ std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) {
+ CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate];
+ CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate];
+ CGPoint pointA = [self convertCoordinate:coordinateA toPointToView:self];
+ CGPoint pointB = [self convertCoordinate:coordinateB toPointToView:self];
+ CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y);
+ CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y);
+ return deltaA < deltaB;
+ });
+
+ NSUInteger annotationIndex = index - visibleAnnotationRange.location;
+ MGLAnnotationTag annotationTag = visibleAnnotations[annotationIndex];
+ NSAssert(annotationTag != MGLAnnotationTagNotFound, @"Can’t get accessibility element for nonexistent or invisible annotation at index %li.", (long)index);
+ return [self accessibilityElementForAnnotationWithTag:annotationTag];
+ }
+
+ // Visible place features
+ NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures;
+ NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count);
+ if (NSLocationInRange(index, visiblePlaceFeatureRange))
+ {
+ visiblePlaceFeatures = [visiblePlaceFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) {
+ CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self];
+ CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self];
+ CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y);
+ CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y);
+ return [@(deltaA) compare:@(deltaB)];
+ }];
+
+ id <MGLFeature> feature = visiblePlaceFeatures[index - visiblePlaceFeatureRange.location];
+ return [self accessibilityElementForPlaceFeature:feature];
+ }
+
+ // Visible road features
+ NSArray *visibleRoadFeatures = self.visibleRoadFeatures;
+ NSRange visibleRoadFeatureRange = NSMakeRange(NSMaxRange(visiblePlaceFeatureRange), visibleRoadFeatures.count);
+ if (NSLocationInRange(index, visibleRoadFeatureRange))
+ {
+ visibleRoadFeatures = [visibleRoadFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) {
+ CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self];
+ CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self];
+ CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y);
+ CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y);
+ return [@(deltaA) compare:@(deltaB)];
+ }];
+
+ id <MGLFeature> feature = visibleRoadFeatures[index - visibleRoadFeatureRange.location];
+ return [self accessibilityElementForRoadFeature:feature];
+ }
+
+ // Attribution button
+ NSUInteger attributionButtonIndex = NSMaxRange(visibleRoadFeatureRange);
+ if (index == attributionButtonIndex)
{
- annotationIndex = index - 2 /* compass, userLocationAnnotationView */;
+ return self.attributionButton;
}
- MGLAnnotationTag annotationTag = visibleAnnotations[annotationIndex];
- NSAssert(annotationTag != MGLAnnotationTagNotFound, @"Can’t get accessibility element for nonexistent or invisible annotation at index %li.", (long)index);
+
+ NSAssert(NO, @"Index %ld not in recognized accessibility element ranges. "
+ @"User location annotation range: %@; visible annotation range: %@; "
+ @"visible place feature range: %@; visible road feature range: %@.",
+ (long)index, NSStringFromRange(userLocationAnnotationRange),
+ NSStringFromRange(visibleAnnotationRange), NSStringFromRange(visiblePlaceFeatureRange),
+ NSStringFromRange(visibleRoadFeatureRange));
+ return nil;
+}
+
+/**
+ Returns an accessibility element corresponding to a visible annotation with the given tag.
+
+ @param annotationTag Tag of the annotation represented by the accessibility element to return.
+ */
+- (id)accessibilityElementForAnnotationWithTag:(MGLAnnotationTag)annotationTag
+{
NSAssert(_annotationContextsByAnnotationTag.count(annotationTag), @"Missing annotation for tag %u.", annotationTag);
MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
id <MGLAnnotation> annotation = annotationContext.annotation;
-
+
// Let the annotation view serve as its own accessibility element.
MGLAnnotationView *annotationView = annotationContext.annotationView;
if (annotationView && annotationView.superview)
{
return annotationView;
}
-
+
// Lazily create an accessibility element for the found annotation.
if ( ! annotationContext.accessibilityElement)
{
annotationContext.accessibilityElement = [[MGLAnnotationAccessibilityElement alloc] initWithAccessibilityContainer:self tag:annotationTag];
}
-
+
// Update the accessibility element.
MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
CGRect annotationFrame = [self frameOfImage:annotationImage.image centeredAtCoordinate:annotation.coordinate];
@@ -2492,8 +2534,7 @@ public:
annotationFrame = CGRectUnion(annotationFrame, minimumFrame);
CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self);
annotationContext.accessibilityElement.accessibilityFrame = screenRect;
- annotationContext.accessibilityElement.accessibilityHint = NSLocalizedStringWithDefaultValue(@"ANNOTATION_A11Y_HINT", nil, nil, @"Shows more info", @"Accessibility hint");
-
+
if ([annotation respondsToSelector:@selector(title)])
{
annotationContext.accessibilityElement.accessibilityLabel = annotation.title;
@@ -2502,10 +2543,114 @@ public:
{
annotationContext.accessibilityElement.accessibilityValue = annotation.subtitle;
}
-
+
return annotationContext.accessibilityElement;
}
+/**
+ Returns an accessibility element corresponding to the given place feature.
+
+ @param feature The place feature represented by the accessibility element.
+ */
+- (id)accessibilityElementForPlaceFeature:(id <MGLFeature>)feature
+{
+ if (!_featureAccessibilityElements)
+ {
+ _featureAccessibilityElements = [NSMutableSet set];
+ }
+
+ MGLFeatureAccessibilityElement *element = [_featureAccessibilityElements objectsPassingTest:^BOOL(MGLFeatureAccessibilityElement * _Nonnull element, BOOL * _Nonnull stop) {
+ return element.feature.identifier && ![element.feature.identifier isEqual:@0] && [element.feature.identifier isEqual:feature.identifier];
+ }].anyObject;
+ if (!element)
+ {
+ element = [[MGLPlaceFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature];
+ }
+ CGPoint center = [self convertCoordinate:feature.coordinate toPointToView:self];
+ CGRect annotationFrame = CGRectInset({center, CGSizeZero}, -MGLAnnotationAccessibilityElementMinimumSize.width / 2, -MGLAnnotationAccessibilityElementMinimumSize.width / 2);
+ CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self);
+ element.accessibilityFrame = screenRect;
+
+ [_featureAccessibilityElements addObject:element];
+
+ return element;
+}
+
+/**
+ Returns an accessibility element corresponding to the given road feature.
+
+ @param feature The road feature represented by the accessibility element.
+ */
+- (id)accessibilityElementForRoadFeature:(id <MGLFeature>)feature
+{
+ if (!_featureAccessibilityElements)
+ {
+ _featureAccessibilityElements = [NSMutableSet set];
+ }
+
+ MGLFeatureAccessibilityElement *element = [_featureAccessibilityElements objectsPassingTest:^BOOL(MGLFeatureAccessibilityElement * _Nonnull element, BOOL * _Nonnull stop) {
+ return element.feature.identifier && ![element.feature.identifier isEqual:@0] && [element.feature.identifier isEqual:feature.identifier];
+ }].anyObject;
+ if (!element)
+ {
+ element = [[MGLRoadFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature];
+ }
+
+ UIBezierPath *path;
+ if ([feature isKindOfClass:[MGLPointFeature class]])
+ {
+ CGPoint center = [self convertCoordinate:feature.coordinate toPointToView:self];
+ CGRect annotationFrame = CGRectInset({center, CGSizeZero}, -MGLAnnotationAccessibilityElementMinimumSize.width / 2, -MGLAnnotationAccessibilityElementMinimumSize.width / 2);
+ CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self);
+ element.accessibilityFrame = screenRect;
+ }
+ else if ([feature isKindOfClass:[MGLPolylineFeature class]])
+ {
+ path = [self pathOfPolyline:(MGLPolyline *)feature];
+ }
+ else if ([feature isKindOfClass:[MGLMultiPolylineFeature class]])
+ {
+ path = [UIBezierPath bezierPath];
+ for (MGLPolyline *polyline in [(MGLMultiPolylineFeature *)feature polylines])
+ {
+ [path appendPath:[self pathOfPolyline:polyline]];
+ }
+ }
+
+ if (path)
+ {
+ CGPathRef strokedCGPath = CGPathCreateCopyByStrokingPath(path.CGPath, NULL, MGLAnnotationAccessibilityElementMinimumSize.width, kCGLineCapButt, kCGLineJoinMiter, 0);
+ UIBezierPath *strokedPath = [UIBezierPath bezierPathWithCGPath:strokedCGPath];
+ CGPathRelease(strokedCGPath);
+ UIBezierPath *screenPath = UIAccessibilityConvertPathToScreenCoordinates(strokedPath, self);
+ element.accessibilityPath = screenPath;
+ }
+
+ [_featureAccessibilityElements addObject:element];
+
+ return element;
+}
+
+- (UIBezierPath *)pathOfPolyline:(MGLPolyline *)polyline
+{
+ CLLocationCoordinate2D *coordinates = polyline.coordinates;
+ NSUInteger pointCount = polyline.pointCount;
+ UIBezierPath *path = [UIBezierPath bezierPath];
+ for (NSUInteger i = 0; i < pointCount; i++)
+ {
+ CGPoint point = [self convertCoordinate:coordinates[i] toPointToView:self];
+ if (i)
+ {
+ [path addLineToPoint:point];
+ }
+ else
+ {
+ [path moveToPoint:point];
+ }
+ }
+ return path;
+}
+
- (NSInteger)indexOfAccessibilityElement:(id)element
{
if (self.calloutViewForSelectedAnnotation)
@@ -2513,17 +2658,30 @@ public:
return [@[self.calloutViewForSelectedAnnotation, self.mapViewProxyAccessibilityElement]
indexOfObject:element];
}
+
+ // Compass
+ NSUInteger compassIndex = 0;
if (element == self.compassView)
{
- return 0;
+ return compassIndex;
}
+
+ // User location annotation
+ NSRange userLocationAnnotationRange = NSMakeRange(compassIndex + 1, !!self.userLocationAnnotationView);
if (element == self.userLocationAnnotationView)
{
- return 1;
+ return userLocationAnnotationRange.location;
}
-
+
+ CGPoint centerPoint = self.contentCenter;
+ if (self.userTrackingMode != MGLUserTrackingModeNone)
+ {
+ centerPoint = self.userLocationAnnotationViewCenter;
+ }
+
+ // Visible annotations
std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds];
-
+ NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size());
MGLAnnotationTag tag = MGLAnnotationTagNotFound;
if ([element isKindOfClass:[MGLAnnotationView class]])
{
@@ -2534,22 +2692,92 @@ public:
{
tag = [(MGLAnnotationAccessibilityElement *)element tag];
}
- else if (element == self.attributionButton)
- {
- return !!self.userLocationAnnotationView + visibleAnnotations.size();
+
+ if (tag != MGLAnnotationTagNotFound)
+ {
+ std::sort(visibleAnnotations.begin(), visibleAnnotations.end());
+ std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) {
+ CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate];
+ CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate];
+ CGPoint pointA = [self convertCoordinate:coordinateA toPointToView:self];
+ CGPoint pointB = [self convertCoordinate:coordinateB toPointToView:self];
+ CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y);
+ CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y);
+ return deltaA < deltaB;
+ });
+
+ auto foundElement = std::find(visibleAnnotations.begin(), visibleAnnotations.end(), tag);
+ if (foundElement == visibleAnnotations.end())
+ {
+ return NSNotFound;
+ }
+ return visibleAnnotationRange.location + std::distance(visibleAnnotations.begin(), foundElement);
}
- else
- {
- return NSNotFound;
+
+ // Visible place features
+ NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures;
+ NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count);
+ if ([element isKindOfClass:[MGLPlaceFeatureAccessibilityElement class]])
+ {
+ visiblePlaceFeatures = [visiblePlaceFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) {
+ CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self];
+ CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self];
+ CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y);
+ CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y);
+ return [@(deltaA) compare:@(deltaB)];
+ }];
+
+ id <MGLFeature> feature = [(MGLPlaceFeatureAccessibilityElement *)element feature];
+ NSUInteger featureIndex = [visiblePlaceFeatures indexOfObject:feature];
+ if (featureIndex == NSNotFound)
+ {
+ featureIndex = [visiblePlaceFeatures indexOfObjectPassingTest:^BOOL (id <MGLFeature> _Nonnull visibleFeature, NSUInteger idx, BOOL * _Nonnull stop) {
+ return visibleFeature.identifier && ![visibleFeature.identifier isEqual:@0] && [visibleFeature.identifier isEqual:feature.identifier];
+ }];
+ }
+ if (featureIndex == NSNotFound)
+ {
+ return NSNotFound;
+ }
+ return visiblePlaceFeatureRange.location + featureIndex;
}
-
- std::sort(visibleAnnotations.begin(), visibleAnnotations.end());
- auto foundElement = std::find(visibleAnnotations.begin(), visibleAnnotations.end(), tag);
- if (foundElement == visibleAnnotations.end())
+
+ // Visible road features
+ NSArray *visibleRoadFeatures = self.visibleRoadFeatures;
+ NSRange visibleRoadFeatureRange = NSMakeRange(NSMaxRange(visiblePlaceFeatureRange), visibleRoadFeatures.count);
+ if ([element isKindOfClass:[MGLRoadFeatureAccessibilityElement class]])
+ {
+ visibleRoadFeatures = [visibleRoadFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) {
+ CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self];
+ CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self];
+ CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y);
+ CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y);
+ return [@(deltaA) compare:@(deltaB)];
+ }];
+
+ id <MGLFeature> feature = [(MGLRoadFeatureAccessibilityElement *)element feature];
+ NSUInteger featureIndex = [visibleRoadFeatures indexOfObject:feature];
+ if (featureIndex == NSNotFound)
+ {
+ featureIndex = [visibleRoadFeatures indexOfObjectPassingTest:^BOOL (id <MGLFeature> _Nonnull visibleFeature, NSUInteger idx, BOOL * _Nonnull stop) {
+ return visibleFeature.identifier && ![visibleFeature.identifier isEqual:@0] && [visibleFeature.identifier isEqual:feature.identifier];
+ }];
+ }
+ if (featureIndex == NSNotFound)
+ {
+ return NSNotFound;
+ }
+ return visibleRoadFeatureRange.location + featureIndex;
+ }
+
+ // Attribution button
+ NSUInteger attributionButtonIndex = NSMaxRange(visibleRoadFeatureRange);
+ if (element == self.attributionButton)
{
- return NSNotFound;
+ return attributionButtonIndex;
}
- return !!self.userLocationAnnotationView + std::distance(visibleAnnotations.begin(), foundElement) + 1 /* compass */;
+
+ return NSNotFound;
}
- (MGLMapViewProxyAccessibilityElement *)mapViewProxyAccessibilityElement
@@ -2580,10 +2808,11 @@ public:
{
centerPoint = self.userLocationAnnotationViewCenter;
}
- _mbglMap->setZoom(_mbglMap->getZoom() + log2(scaleFactor), mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y });
+ double newZoom = round(self.zoomLevel) + log2(scaleFactor);
+ _mbglMap->setZoom(newZoom, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y });
[self unrotateIfNeededForGesture];
- UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.accessibilityValue);
+ _accessibilityValueAnnouncementIsPending = YES;
}
#pragma mark - Geography -
@@ -3266,6 +3495,12 @@ public:
}
std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:rect];
+ std::vector<MGLAnnotationTag> shapeAnnotationTags = [self shapeAnnotationTagsInRect:rect];
+
+ if (shapeAnnotationTags.size()) {
+ annotationTags.insert(annotationTags.end(), shapeAnnotationTags.begin(), shapeAnnotationTags.end());
+ }
+
if (annotationTags.size())
{
NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:annotationTags.size()];
@@ -3382,7 +3617,7 @@ public:
{
annotationViewsForAnnotation[annotationValue] = annotationView;
annotationView.annotation = annotation;
- annotationView.center = [self convertCoordinate:annotation.coordinate toPointToView:self];
+ annotationView.center = MGLPointRounded([self convertCoordinate:annotation.coordinate toPointToView:self]);
[newAnnotationViews addObject:annotationView];
MGLAnnotationImage *annotationImage = self.invisibleAnnotationImage;
@@ -3771,6 +4006,11 @@ public:
queryRect = CGRectInset(queryRect, -MGLAnnotationImagePaddingForHitTest,
-MGLAnnotationImagePaddingForHitTest);
std::vector<MGLAnnotationTag> nearbyAnnotations = [self annotationTagsInRect:queryRect];
+ std::vector<MGLAnnotationTag> nearbyShapeAnnotations = [self shapeAnnotationTagsInRect:queryRect];
+
+ if (nearbyShapeAnnotations.size()) {
+ nearbyAnnotations.insert(nearbyAnnotations.end(), nearbyShapeAnnotations.begin(), nearbyShapeAnnotations.end());
+ }
if (nearbyAnnotations.size())
{
@@ -3778,54 +4018,59 @@ public:
CGRect hitRect = CGRectInset({ point, CGSizeZero },
-MGLAnnotationImagePaddingForHitTest,
-MGLAnnotationImagePaddingForHitTest);
-
+
// Filter out any annotation whose image or view is unselectable or for which
// hit testing fails.
- auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(),
- [&](const MGLAnnotationTag annotationTag)
- {
+ auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag annotationTag) {
id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
NSAssert(annotation, @"Unknown annotation found nearby tap");
if ( ! annotation)
{
return true;
}
-
+
MGLAnnotationContext annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
CGRect annotationRect;
-
+
MGLAnnotationView *annotationView = annotationContext.annotationView;
+
if (annotationView)
{
if ( ! annotationView.enabled)
{
return true;
}
-
- CGPoint calloutAnchorPoint = [self convertCoordinate:annotation.coordinate toPointToView:self];
+
+ CGPoint calloutAnchorPoint = MGLPointRounded([self convertCoordinate:annotation.coordinate toPointToView:self]);
CGRect frame = CGRectInset({ calloutAnchorPoint, CGSizeZero }, -CGRectGetWidth(annotationView.frame) / 2, -CGRectGetHeight(annotationView.frame) / 2);
annotationRect = UIEdgeInsetsInsetRect(frame, annotationView.alignmentRectInsets);
}
else
{
+ if ([annotation isKindOfClass:[MGLShape class]])
+ {
+ return false;
+ }
+
MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
if ( ! annotationImage.enabled)
{
return true;
}
-
+
MGLAnnotationImage *fallbackAnnotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName];
UIImage *fallbackImage = fallbackAnnotationImage.image;
-
+
annotationRect = [self frameOfImage:annotationImage.image ?: fallbackImage centeredAtCoordinate:annotation.coordinate];
}
-
+
// Filter out the annotation if the fattened finger didn’t land
// within the image’s alignment rect.
return !!!CGRectIntersectsRect(annotationRect, hitRect);
});
-
+
nearbyAnnotations.resize(std::distance(nearbyAnnotations.begin(), end));
+
}
MGLAnnotationTag hitAnnotationTag = MGLAnnotationTagNotFound;
@@ -3908,6 +4153,14 @@ public:
});
}
+- (std::vector<MGLAnnotationTag>)shapeAnnotationTagsInRect:(CGRect)rect
+{
+ return _rendererFrontend->getRenderer()->queryShapeAnnotations({
+ { CGRectGetMinX(rect), CGRectGetMinY(rect) },
+ { CGRectGetMaxX(rect), CGRectGetMaxY(rect) },
+ });
+}
+
- (id <MGLAnnotation>)selectedAnnotation
{
if (_userLocationAnnotationIsSelected)
@@ -3957,9 +4210,13 @@ public:
- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated
{
- if ( ! annotation) return;
+ CGRect positioningRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:CGPointZero];
+ [self selectAnnotation:annotation animated:animated calloutPositioningRect:positioningRect];
+}
- if ([annotation isKindOfClass:[MGLMultiPoint class]]) return;
+- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated calloutPositioningRect:(CGRect)calloutPositioningRect
+{
+ if ( ! annotation) return;
if (annotation == self.selectedAnnotation) return;
@@ -3974,9 +4231,6 @@ public:
if (annotationTag == MGLAnnotationTagNotFound) return;
}
- // By default attempt to use the GL annotation image frame as the positioning rect.
- CGRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag];
-
MGLAnnotationView *annotationView = nil;
if (annotation != self.userLocation)
@@ -3984,21 +4238,12 @@ public:
MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
annotationView = annotationContext.annotationView;
if (annotationView && annotationView.enabled) {
- {
- // Annotations represented by views use the view frame as the positioning rect.
- positioningRect = annotationView.frame;
- [annotationView.superview bringSubviewToFront:annotationView];
- [annotationView setSelected:YES animated:animated];
+ // Annotations represented by views use the view frame as the positioning rect.
+ calloutPositioningRect = annotationView.frame;
+ [annotationView.superview bringSubviewToFront:annotationView];
+ [annotationView setSelected:YES animated:animated];
}
}
- }
-
- // The client can request that any annotation be selected (even ones that are offscreen).
- // The annotation can’t be selected if no part of it is hittable.
- if ( ! CGRectIntersectsRect(positioningRect, self.bounds) && annotation != self.userLocation)
- {
- return;
- }
self.selectedAnnotation = annotation;
@@ -4028,7 +4273,7 @@ public:
if (_userLocationAnnotationIsSelected)
{
- positioningRect = [self.userLocationAnnotationView.layer.presentationLayer frame];
+ calloutPositioningRect = [self.userLocationAnnotationView.layer.presentationLayer frame];
CGRect implicitAnnotationFrame = [self.userLocationAnnotationView.layer.presentationLayer frame];
CGRect explicitAnnotationFrame = self.userLocationAnnotationView.frame;
@@ -4044,7 +4289,7 @@ public:
if ([calloutView.leftAccessoryView isKindOfClass:[UIControl class]])
{
UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self
- action:@selector(handleCalloutAccessoryTapGesture:)];
+ action:@selector(handleCalloutAccessoryTapGesture:)];
[calloutView.leftAccessoryView addGestureRecognizer:calloutAccessoryTap];
}
@@ -4057,7 +4302,7 @@ public:
if ([calloutView.rightAccessoryView isKindOfClass:[UIControl class]])
{
UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self
- action:@selector(handleCalloutAccessoryTapGesture:)];
+ action:@selector(handleCalloutAccessoryTapGesture:)];
[calloutView.rightAccessoryView addGestureRecognizer:calloutAccessoryTap];
}
@@ -4067,7 +4312,7 @@ public:
calloutView.delegate = self;
// present popup
- [calloutView presentCalloutFromRect:positioningRect
+ [calloutView presentCalloutFromRect:calloutPositioningRect
inView:self.glView
constrainedToView:self.glView
animated:animated];
@@ -4097,6 +4342,27 @@ public:
/// Returns the rectangle that represents the annotation image of the annotation
/// with the given tag. This rectangle is fitted to the image’s alignment rect
/// and is appropriate for positioning a popover.
+/// If a shape annotation is visible but its centroid is not, and a default point is specified,
+/// the callout view is anchored to the default callout point.
+- (CGRect)positioningRectForAnnotation:(id <MGLAnnotation>)annotation defaultCalloutPoint:(CGPoint)calloutPoint
+{
+ MGLAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
+ CGRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag];
+
+ // For annotations which `coordinate` falls offscreen it will use the current tap point as anchor instead.
+ if ( ! CGRectIntersectsRect(positioningRect, self.bounds) && annotation != self.userLocation)
+ {
+ if (!CGPointEqualToPoint(calloutPoint, CGPointZero)) {
+ positioningRect = CGRectMake(calloutPoint.x, calloutPoint.y, positioningRect.size.width, positioningRect.size.height);
+ }
+ }
+
+ return positioningRect;
+}
+
+/// Returns the rectangle that represents the annotation image of the annotation
+/// with the given tag. This rectangle is fitted to the image’s alignment rect
+/// and is appropriate for positioning a popover.
- (CGRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annotationTag
{
id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
@@ -4104,6 +4370,13 @@ public:
{
return CGRectZero;
}
+
+ if ([annotation isKindOfClass:[MGLMultiPoint class]]) {
+ CLLocationCoordinate2D origin = annotation.coordinate;
+ CGPoint originPoint = [self convertCoordinate:origin toPointToView:self];
+ return CGRectMake(originPoint.x, originPoint.y, MGLAnnotationImagePaddingForHitTest, MGLAnnotationImagePaddingForHitTest);
+
+ }
UIImage *image = [self imageOfAnnotationWithTag:annotationTag].image;
if ( ! image)
{
@@ -4125,7 +4398,7 @@ public:
/// image centered at the given coordinate.
- (CGRect)frameOfImage:(UIImage *)image centeredAtCoordinate:(CLLocationCoordinate2D)coordinate
{
- CGPoint calloutAnchorPoint = [self convertCoordinate:coordinate toPointToView:self];
+ CGPoint calloutAnchorPoint = MGLPointRounded([self convertCoordinate:coordinate toPointToView:self]);
CGRect frame = CGRectInset({ calloutAnchorPoint, CGSizeZero }, -image.size.width / 2, -image.size.height / 2);
return UIEdgeInsetsInsetRect(frame, image.alignmentRectInsets);
}
@@ -4564,7 +4837,6 @@ public:
if (_showsUserHeadingIndicator)
{
self.showsUserLocation = YES;
-
}
[self validateUserHeadingUpdating];
}
@@ -5100,12 +5372,26 @@ public:
{
if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive)
{
- UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
+ _featureAccessibilityElements = nil;
+ _visiblePlaceFeatures = nil;
+ _visibleRoadFeatures = nil;
+ if (_accessibilityValueAnnouncementIsPending) {
+ _accessibilityValueAnnouncementIsPending = NO;
+ [self performSelector:@selector(announceAccessibilityValue) withObject:nil afterDelay:0.1];
+ } else {
+ UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
+ }
}
[self.delegate mapView:self regionDidChangeAnimated:animated];
}
}
+- (void)announceAccessibilityValue
+{
+ UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.accessibilityValue);
+ UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
+}
+
- (void)mapViewWillStartLoadingMap {
if (!_mbglMap) {
return;
@@ -5187,6 +5473,8 @@ public:
if (!_mbglMap) {
return;
}
+
+ UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingMap:fullyRendered:)])
{
@@ -5270,7 +5558,7 @@ public:
if (annotationView)
{
- annotationView.center = [self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self];
+ annotationView.center = MGLPointRounded([self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self]);
}
}
@@ -5388,7 +5676,7 @@ public:
}
else
{
- userPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self];
+ userPoint = MGLPointRounded([self convertCoordinate:self.userLocation.coordinate toPointToView:self]);
}
if ( ! annotationView.superview)
diff --git a/platform/ios/src/MGLScaleBar.mm b/platform/ios/src/MGLScaleBar.mm
index 966555e46a..139dffdfab 100644
--- a/platform/ios/src/MGLScaleBar.mm
+++ b/platform/ios/src/MGLScaleBar.mm
@@ -175,10 +175,15 @@ static const CGFloat MGLFeetPerMeter = 3.28084;
return [self usesMetricSystem] ? self.metersPerPoint : self.metersPerPoint * MGLFeetPerMeter;
}
-#pragma mark - Convenient methods
+#pragma mark - Convenience methods
- (BOOL)usesRightToLeftLayout {
- return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.superview.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft;
+ // semanticContentAttribute is iOS 9+
+ if ([self.superview respondsToSelector:@selector(semanticContentAttribute)]) {
+ return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.superview.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft;
+ } else {
+ return UIApplication.sharedApplication.userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
+ }
}
- (BOOL)usesMetricSystem {
@@ -241,7 +246,7 @@ static const CGFloat MGLFeetPerMeter = 3.28084;
CGFloat alpha = maximumDistance > allowedDistance ? .0f : 1.0f;
- if(self.alpha != alpha) {
+ if (self.alpha != alpha) {
[UIView animateWithDuration:.2f delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
self.alpha = alpha;
} completion:nil];
@@ -331,7 +336,7 @@ static const CGFloat MGLFeetPerMeter = 3.28084;
}
- (void)layoutBars {
- CGFloat barWidth = (CGRectGetWidth(self.bounds) - self.borderWidth * 2.0f) / self.bars.count;
+ CGFloat barWidth = round((CGRectGetWidth(self.bounds) - self.borderWidth * 2.0f) / self.bars.count);
NSUInteger i = 0;
for (UIView *bar in self.bars) {
@@ -354,11 +359,11 @@ static const CGFloat MGLFeetPerMeter = 3.28084;
}
- (void)layoutLabels {
- CGFloat barWidth = self.bounds.size.width / self.bars.count;
+ CGFloat barWidth = round(self.bounds.size.width / self.bars.count);
BOOL RTL = [self usesRightToLeftLayout];
NSUInteger i = RTL ? self.bars.count : 0;
for (MGLScaleBarLabel *label in self.labels) {
- CGFloat xPosition = barWidth * i - CGRectGetMidX(label.bounds) + self.borderWidth;
+ CGFloat xPosition = round(barWidth * i - CGRectGetMidX(label.bounds) + self.borderWidth);
label.frame = CGRectMake(xPosition, 0,
CGRectGetWidth(label.bounds),
CGRectGetHeight(label.bounds));
diff --git a/platform/ios/src/MGLUserLocation.h b/platform/ios/src/MGLUserLocation.h
index 91abadbcb7..4e01cf00c9 100644
--- a/platform/ios/src/MGLUserLocation.h
+++ b/platform/ios/src/MGLUserLocation.h
@@ -34,7 +34,8 @@ MGL_EXPORT
The heading of the user location. (read-only)
This property is `nil` if the user location tracking mode is not
- `MGLUserTrackingModeFollowWithHeading`.
+ `MGLUserTrackingModeFollowWithHeading` or if
+ `MGLMapView.showsUserHeadingIndicator` is disabled.
*/
@property (nonatomic, readonly, nullable) CLHeading *heading;
diff --git a/platform/ios/test/MGLMapAccessibilityElementTests.m b/platform/ios/test/MGLMapAccessibilityElementTests.m
new file mode 100644
index 0000000000..5c79d85de1
--- /dev/null
+++ b/platform/ios/test/MGLMapAccessibilityElementTests.m
@@ -0,0 +1,87 @@
+#import <Mapbox/Mapbox.h>
+#import <XCTest/XCTest.h>
+
+#import "../../ios/src/MGLMapAccessibilityElement.h"
+
+@interface MGLMapAccessibilityElementTests : XCTestCase
+@end
+
+@implementation MGLMapAccessibilityElementTests
+
+- (void)testFeatureLabels {
+ MGLPointFeature *feature = [[MGLPointFeature alloc] init];
+ feature.attributes = @{
+ @"name": @"Local",
+ @"name_en": @"English",
+ @"name_es": @"Spanish",
+ @"name_fr": @"French",
+ @"name_tlh": @"Klingon",
+ };
+ MGLFeatureAccessibilityElement *element = [[MGLFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature];
+ XCTAssertEqualObjects(element.accessibilityLabel, @"English", @"Accessibility label should be localized.");
+
+ feature.attributes = @{
+ @"name": @"Цинциннати",
+ @"name_en": @"Цинциннати",
+ };
+ element = [[MGLFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature];
+ XCTAssertEqualObjects(element.accessibilityLabel, @"Cincinnati", @"Accessibility label should be romanized.");
+}
+
+- (void)testPlaceFeatureValues {
+ MGLPointFeature *feature = [[MGLPointFeature alloc] init];
+ feature.attributes = @{
+ @"type": @"village_green",
+ };
+ MGLPlaceFeatureAccessibilityElement *element = [[MGLPlaceFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature];
+ XCTAssertEqualObjects(element.accessibilityValue, @"village green");
+
+ feature = [[MGLPointFeature alloc] init];
+ feature.attributes = @{
+ @"maki": @"cat",
+ };
+ element = [[MGLPlaceFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature];
+ XCTAssertEqualObjects(element.accessibilityValue, @"cat");
+
+ feature = [[MGLPointFeature alloc] init];
+ feature.attributes = @{
+ @"elevation_ft": @31337,
+ @"elevation_m": @1337,
+ };
+ element = [[MGLPlaceFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature];
+ XCTAssertEqualObjects(element.accessibilityValue, @"31,337 feet");
+}
+
+- (void)testRoadFeatureValues {
+ CLLocationCoordinate2D coordinates[] = {
+ CLLocationCoordinate2DMake(0, 0),
+ CLLocationCoordinate2DMake(0, 1),
+ CLLocationCoordinate2DMake(1, 2),
+ CLLocationCoordinate2DMake(2, 2),
+ };
+ MGLPolylineFeature *roadFeature = [MGLPolylineFeature polylineWithCoordinates:coordinates count:sizeof(coordinates) / sizeof(coordinates[0])];
+ roadFeature.attributes = @{
+ @"ref": @"42",
+ @"oneway": @"true",
+ };
+ MGLRoadFeatureAccessibilityElement *element = [[MGLRoadFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:roadFeature];
+ XCTAssertEqualObjects(element.accessibilityValue, @"Route 42, One way, southwest to northeast");
+
+ CLLocationCoordinate2D opposingCoordinates[] = {
+ CLLocationCoordinate2DMake(2, 1),
+ CLLocationCoordinate2DMake(1, 0),
+ };
+ MGLPolylineFeature *opposingRoadFeature = [MGLPolylineFeature polylineWithCoordinates:opposingCoordinates count:sizeof(opposingCoordinates) / sizeof(opposingCoordinates[0])];
+ opposingRoadFeature.attributes = @{
+ @"ref": @"42",
+ @"oneway": @"true",
+ };
+ MGLMultiPolylineFeature *dividedRoadFeature = [MGLMultiPolylineFeature multiPolylineWithPolylines:@[roadFeature, opposingRoadFeature]];
+ dividedRoadFeature.attributes = @{
+ @"ref": @"42",
+ };
+ element = [[MGLRoadFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:dividedRoadFeature];
+ XCTAssertEqualObjects(element.accessibilityValue, @"Route 42, Divided road, southwest to northeast");
+}
+
+@end
diff --git a/platform/ios/vendor/SMCalloutView b/platform/ios/vendor/SMCalloutView
deleted file mode 160000
-Subproject d6ecaba377c9f963aef630faf86e3b8f8cdb88d
diff --git a/platform/ios/vendor/SMCalloutView/SMCalloutView.h b/platform/ios/vendor/SMCalloutView/SMCalloutView.h
new file mode 100755
index 0000000000..0b14913626
--- /dev/null
+++ b/platform/ios/vendor/SMCalloutView/SMCalloutView.h
@@ -0,0 +1,205 @@
+#import <UIKit/UIKit.h>
+#import <QuartzCore/QuartzCore.h>
+
+/*
+
+SMCalloutView
+-------------
+Created by Nick Farina (nfarina@gmail.com)
+Version 2.1.5
+
+*/
+
+/// options for which directions the callout is allowed to "point" in.
+typedef NS_OPTIONS(NSUInteger, MGLSMCalloutArrowDirection) {
+ MGLSMCalloutArrowDirectionUp = 1 << 0,
+ MGLSMCalloutArrowDirectionDown = 1 << 1,
+ MGLSMCalloutArrowDirectionAny = MGLSMCalloutArrowDirectionUp | MGLSMCalloutArrowDirectionDown
+};
+
+/// options for the callout present/dismiss animation
+typedef NS_ENUM(NSInteger, MGLSMCalloutAnimation) {
+ /// the "bounce" animation we all know and love from @c UIAlertView
+ MGLSMCalloutAnimationBounce,
+ /// a simple fade in or out
+ MGLSMCalloutAnimationFade,
+ /// grow or shrink linearly, like in the iPad Calendar app
+ MGLSMCalloutAnimationStretch
+};
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// when delaying our popup in order to scroll content into view, you can use this amount to match the
+/// animation duration of UIScrollView when using @c -setContentOffset:animated.
+extern NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView;
+
+@protocol MGLSMCalloutViewDelegate;
+@class MGLSMCalloutBackgroundView;
+
+//
+// Callout view.
+//
+
+// iOS 10+ expects CAAnimationDelegate to be set explicitly.
+#if __IPHONE_OS_VERSION_MAX_ALLOWED < 100000
+@interface MGLSMCalloutView : UIView
+#else
+@interface MGLSMCalloutView : UIView <CAAnimationDelegate>
+#endif
+
+@property (nonatomic, weak, nullable) id<MGLSMCalloutViewDelegate> delegate;
+/// title/titleView relationship mimics UINavigationBar.
+@property (nonatomic, copy, nullable) NSString *title;
+@property (nonatomic, copy, nullable) NSString *subtitle;
+
+/// Left accessory view for the call out
+@property (nonatomic, strong, nullable) UIView *leftAccessoryView;
+/// Right accessoty view for the call out
+@property (nonatomic, strong, nullable) UIView *rightAccessoryView;
+/// Default @c SMCalloutArrowDirectionDown
+@property (nonatomic, assign) MGLSMCalloutArrowDirection permittedArrowDirection;
+/// The current arrow direction
+@property (nonatomic, readonly) MGLSMCalloutArrowDirection currentArrowDirection;
+/// if the @c UIView you're constraining to has portions that are overlapped by nav bar, tab bar, etc. you'll need to tell us those insets.
+@property (nonatomic, assign) UIEdgeInsets constrainedInsets;
+/// default is @c SMCalloutMaskedBackgroundView, or @c SMCalloutDrawnBackgroundView when using @c SMClassicCalloutView
+@property (nonatomic, strong) MGLSMCalloutBackgroundView *backgroundView;
+
+/**
+ @brief Custom title view.
+
+ @disucssion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on titleView/subtitleView if defined, so your view
+ may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized.
+
+ @warning If this is set, the respective @c title property will be ignored.
+ */
+@property (nonatomic, strong, nullable) UIView *titleView;
+
+/**
+ @brief Custom subtitle view.
+
+ @discussion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on subtitleView if defined, so your view
+ may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized.
+
+ @warning If this is set, the respective @c subtitle property will be ignored.
+ */
+@property (nonatomic, strong, nullable) UIView *subtitleView;
+
+/// Custom "content" view that can be any width/height. If this is set, title/subtitle/titleView/subtitleView are all ignored.
+@property (nonatomic, retain, nullable) UIView *contentView;
+
+/// Custom content view margin
+@property (nonatomic, assign) UIEdgeInsets contentViewInset;
+
+/// calloutOffset is the offset in screen points from the top-middle of the target view, where the anchor of the callout should be shown.
+@property (nonatomic, assign) CGPoint calloutOffset;
+
+/// default SMCalloutAnimationBounce, SMCalloutAnimationFade respectively
+@property (nonatomic, assign) MGLSMCalloutAnimation presentAnimation, dismissAnimation;
+
+/// Returns a new instance of SMCalloutView if running on iOS 7 or better, otherwise a new instance of SMClassicCalloutView if available.
++ (MGLSMCalloutView *)platformCalloutView;
+
+/**
+ @brief Presents a callout view by adding it to "inView" and pointing at the given rect of inView's bounds.
+
+ @discussion Constrains the callout to the bounds of the given view. Optionally scrolls the given rect into view (plus margins)
+ if @c -delegate is set and responds to @c -delayForRepositionWithSize.
+
+ @param rect @c CGRect to present the view from
+ @param view view to 'constrain' the @c constrainedView to
+ @param constrainedView @c UIView to be constrainted in @c view
+ @param animated @c BOOL if presentation should be animated
+ */
+- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated;
+
+/**
+ @brief Present a callout layer in the `layer` and pointing at the given rect of the `layer` bounds
+
+ @discussion Same as the view-based presentation, but inserts the callout into a CALayer hierarchy instead.
+ @note Be aware that you'll have to direct your own touches to any accessory views, since CALayer doesn't relay touch events.
+
+ @param rect @c CGRect to present the view from
+ @param layer layer to 'constrain' the @c constrainedLayer to
+ @param constrainedLayer @c CALayer to be constrained in @c layer
+ @param animated @c BOOL if presentation should be animated
+ */
+- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated;
+
+/**
+ Dismiss the callout view
+
+ @param animated @c BOOL if dismissal should be animated
+ */
+- (void)dismissCalloutAnimated:(BOOL)animated;
+
+/// For subclassers. You can override this method to provide your own custom animation for presenting/dismissing the callout.
+- (CAAnimation *)animationWithType:(MGLSMCalloutAnimation)type presenting:(BOOL)presenting;
+
+@end
+
+//
+// Background view - default draws the iOS 7 system background style (translucent white with rounded arrow).
+//
+
+/// Abstract base class
+@interface MGLSMCalloutBackgroundView : UIView
+/// indicates where the tip of the arrow should be drawn, as a pixel offset
+@property (nonatomic, assign) CGPoint arrowPoint;
+/// will be set by the callout when the callout is in a highlighted state
+@property (nonatomic, assign) BOOL highlighted;
+/// returns an optional layer whose contents should mask the callout view's contents (not honored by @c SMClassicCalloutView )
+@property (nonatomic, assign) CALayer *contentMask;
+/// height of the callout "arrow"
+@property (nonatomic, assign) CGFloat anchorHeight;
+/// the smallest possible distance from the edge of our control to the "tip" of the anchor, from either left or right
+@property (nonatomic, assign) CGFloat anchorMargin;
+@end
+
+/// Default for iOS 7, this reproduces the "masked" behavior of the iOS 7-style callout view.
+/// Accessories are masked by the shape of the callout (including the arrow itself).
+@interface MGLSMCalloutMaskedBackgroundView : MGLSMCalloutBackgroundView
+@end
+
+//
+// Delegate methods
+//
+
+@protocol MGLSMCalloutViewDelegate <NSObject>
+@optional
+
+/// Controls whether the callout "highlights" when pressed. default YES. You must also respond to @c -calloutViewClicked below.
+/// Not honored by @c SMClassicCalloutView.
+- (BOOL)calloutViewShouldHighlight:(MGLSMCalloutView *)calloutView;
+
+/// Called when the callout view is clicked. Not honored by @c SMClassicCalloutView.
+- (void)calloutViewClicked:(MGLSMCalloutView *)calloutView;
+
+/**
+ Called when the callout view detects that it will be outside the constrained view when it appears,
+ or if the target rect was already outside the constrained view. You can implement this selector
+ to respond to this situation by repositioning your content first in order to make everything visible.
+ The @c CGSize passed is the calculated offset necessary to make everything visible (plus a nice margin).
+ It expects you to return the amount of time you need to reposition things so the popup can be delayed.
+ Typically you would return @c kSMCalloutViewRepositionDelayForUIScrollView if you're repositioning by calling @c [UIScrollView @c setContentOffset:animated:].
+
+ @param calloutView the @c SMCalloutView to reposition
+ @param offset caluclated offset necessary to make everything visible
+ @returns @c NSTimeInterval to delay the repositioning
+ */
+- (NSTimeInterval)calloutView:(MGLSMCalloutView *)calloutView delayForRepositionWithSize:(CGSize)offset;
+
+/// Called before the callout view appears on screen, or before the appearance animation will start.
+- (void)calloutViewWillAppear:(MGLSMCalloutView *)calloutView;
+
+/// Called after the callout view appears on screen, or after the appearance animation is complete.
+- (void)calloutViewDidAppear:(MGLSMCalloutView *)calloutView;
+
+/// Called before the callout view is removed from the screen, or before the disappearance animation is complete.
+- (void)calloutViewWillDisappear:(MGLSMCalloutView *)calloutView;
+
+/// Called after the callout view is removed from the screen, or after the disappearance animation is complete.
+- (void)calloutViewDidDisappear:(MGLSMCalloutView *)calloutView;
+
+NS_ASSUME_NONNULL_END
+@end
diff --git a/platform/ios/vendor/SMCalloutView/SMCalloutView.m b/platform/ios/vendor/SMCalloutView/SMCalloutView.m
new file mode 100755
index 0000000000..9631ca0367
--- /dev/null
+++ b/platform/ios/vendor/SMCalloutView/SMCalloutView.m
@@ -0,0 +1,851 @@
+#import "SMCalloutView.h"
+
+//
+// UIView frame helpers - we do a lot of UIView frame fiddling in this class; these functions help keep things readable.
+//
+
+@interface UIView (SMFrameAdditions)
+@property (nonatomic, assign) CGPoint frameOrigin;
+@property (nonatomic, assign) CGSize frameSize;
+@property (nonatomic, assign) CGFloat frameX, frameY, frameWidth, frameHeight; // normal rect properties
+@property (nonatomic, assign) CGFloat frameLeft, frameTop, frameRight, frameBottom; // these will stretch/shrink the rect
+@end
+
+//
+// Callout View.
+//
+
+#define CALLOUT_DEFAULT_CONTAINER_HEIGHT 44 // height of just the main portion without arrow
+#define CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT 52 // height of just the main portion without arrow (when subtitle is present)
+#define CALLOUT_MIN_WIDTH 61 // minimum width of system callout
+#define TITLE_HMARGIN 12 // the title/subtitle view's normal horizontal margin from the edges of our callout view or from the accessories
+#define TITLE_TOP 11 // the top of the title view when no subtitle is present
+#define TITLE_SUB_TOP 4 // the top of the title view when a subtitle IS present
+#define TITLE_HEIGHT 21 // title height, fixed
+#define SUBTITLE_TOP 28 // the top of the subtitle, when present
+#define SUBTITLE_HEIGHT 15 // subtitle height, fixed
+#define BETWEEN_ACCESSORIES_MARGIN 7 // margin between accessories when no title/subtitle is present
+#define TOP_ANCHOR_MARGIN 13 // all the above measurements assume a bottom anchor! if we're pointing "up" we'll need to add this top margin to everything.
+#define COMFORTABLE_MARGIN 10 // when we try to reposition content to be visible, we'll consider this margin around your target rect
+
+NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0;
+
+@interface MGLSMCalloutView ()
+@property (nonatomic, strong) UIButton *containerView; // for masking and interaction
+@property (nonatomic, strong) UILabel *titleLabel, *subtitleLabel;
+@property (nonatomic, assign) MGLSMCalloutArrowDirection currentArrowDirection;
+@property (nonatomic, assign) BOOL popupCancelled;
+@end
+
+@implementation MGLSMCalloutView
+
++ (MGLSMCalloutView *)platformCalloutView {
+ // MGL: Mapbox does not need or include the custom flavor, so this is modified to just use SMCalloutView.
+ return [MGLSMCalloutView new];
+}
+
+- (id)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ self.permittedArrowDirection = MGLSMCalloutArrowDirectionDown;
+ self.presentAnimation = MGLSMCalloutAnimationBounce;
+ self.dismissAnimation = MGLSMCalloutAnimationFade;
+ self.backgroundColor = [UIColor clearColor];
+ self.containerView = [UIButton new];
+ self.containerView.isAccessibilityElement = NO;
+ self.isAccessibilityElement = NO;
+ self.contentViewInset = UIEdgeInsetsMake(12, 12, 12, 12);
+
+ [self.containerView addTarget:self action:@selector(highlightIfNecessary) forControlEvents:UIControlEventTouchDown | UIControlEventTouchDragInside];
+ [self.containerView addTarget:self action:@selector(unhighlightIfNecessary) forControlEvents:UIControlEventTouchDragOutside | UIControlEventTouchCancel | UIControlEventTouchUpOutside | UIControlEventTouchUpInside];
+ [self.containerView addTarget:self action:@selector(calloutClicked) forControlEvents:UIControlEventTouchUpInside];
+ }
+ return self;
+}
+
+- (BOOL)supportsHighlighting {
+ if (![self.delegate respondsToSelector:@selector(calloutViewClicked:)])
+ return NO;
+ if ([self.delegate respondsToSelector:@selector(calloutViewShouldHighlight:)])
+ return [self.delegate calloutViewShouldHighlight:self];
+ return YES;
+}
+
+- (void)highlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = YES; }
+- (void)unhighlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = NO; }
+
+- (void)calloutClicked {
+ if ([self.delegate respondsToSelector:@selector(calloutViewClicked:)])
+ [self.delegate calloutViewClicked:self];
+}
+
+- (UIView *)titleViewOrDefault {
+ if (self.titleView)
+ // if you have a custom title view defined, return that.
+ return self.titleView;
+ else {
+ if (!self.titleLabel) {
+ // create a default titleView
+ self.titleLabel = [UILabel new];
+ self.titleLabel.frameHeight = TITLE_HEIGHT;
+ self.titleLabel.opaque = NO;
+ self.titleLabel.backgroundColor = [UIColor clearColor];
+ self.titleLabel.font = [UIFont systemFontOfSize:17];
+ self.titleLabel.textColor = [UIColor blackColor];
+ }
+ return self.titleLabel;
+ }
+}
+
+- (UIView *)subtitleViewOrDefault {
+ if (self.subtitleView)
+ // if you have a custom subtitle view defined, return that.
+ return self.subtitleView;
+ else {
+ if (!self.subtitleLabel) {
+ // create a default subtitleView
+ self.subtitleLabel = [UILabel new];
+ self.subtitleLabel.frameHeight = SUBTITLE_HEIGHT;
+ self.subtitleLabel.opaque = NO;
+ self.subtitleLabel.backgroundColor = [UIColor clearColor];
+ self.subtitleLabel.font = [UIFont systemFontOfSize:12];
+ self.subtitleLabel.textColor = [UIColor blackColor];
+ }
+ return self.subtitleLabel;
+ }
+}
+
+- (MGLSMCalloutBackgroundView *)backgroundView {
+ // create our default background on first access only if it's nil, since you might have set your own background anyway.
+ return _backgroundView ? _backgroundView : (_backgroundView = [self defaultBackgroundView]);
+}
+
+- (MGLSMCalloutBackgroundView *)defaultBackgroundView {
+ return [MGLSMCalloutMaskedBackgroundView new];
+}
+
+- (void)rebuildSubviews {
+ // remove and re-add our appropriate subviews in the appropriate order
+ [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
+ [self.containerView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
+ [self setNeedsDisplay];
+
+ [self addSubview:self.backgroundView];
+ [self addSubview:self.containerView];
+
+ if (self.contentView) {
+ [self.containerView addSubview:self.contentView];
+ }
+ else {
+ if (self.titleViewOrDefault) [self.containerView addSubview:self.titleViewOrDefault];
+ if (self.subtitleViewOrDefault) [self.containerView addSubview:self.subtitleViewOrDefault];
+ }
+ if (self.leftAccessoryView) [self.containerView addSubview:self.leftAccessoryView];
+ if (self.rightAccessoryView) [self.containerView addSubview:self.rightAccessoryView];
+}
+
+// Accessory margins. Accessories are centered vertically when shorter
+// than the callout, otherwise they grow from the upper corner.
+
+- (CGFloat)leftAccessoryVerticalMargin {
+ if (self.leftAccessoryView.frameHeight < self.calloutContainerHeight)
+ return roundf((self.calloutContainerHeight - self.leftAccessoryView.frameHeight) / 2);
+ else
+ return 0;
+}
+
+- (CGFloat)leftAccessoryHorizontalMargin {
+ return fminf(self.leftAccessoryVerticalMargin, TITLE_HMARGIN);
+}
+
+- (CGFloat)rightAccessoryVerticalMargin {
+ if (self.rightAccessoryView.frameHeight < self.calloutContainerHeight)
+ return roundf((self.calloutContainerHeight - self.rightAccessoryView.frameHeight) / 2);
+ else
+ return 0;
+}
+
+- (CGFloat)rightAccessoryHorizontalMargin {
+ return fminf(self.rightAccessoryVerticalMargin, TITLE_HMARGIN);
+}
+
+- (CGFloat)innerContentMarginLeft {
+ if (self.leftAccessoryView)
+ return self.leftAccessoryHorizontalMargin + self.leftAccessoryView.frameWidth + TITLE_HMARGIN;
+ else
+ return self.contentViewInset.left;
+}
+
+- (CGFloat)innerContentMarginRight {
+ if (self.rightAccessoryView)
+ return self.rightAccessoryHorizontalMargin + self.rightAccessoryView.frameWidth + TITLE_HMARGIN;
+ else
+ return self.contentViewInset.right;
+}
+
+- (CGFloat)calloutHeight {
+ return self.calloutContainerHeight + self.backgroundView.anchorHeight;
+}
+
+- (CGFloat)calloutContainerHeight {
+ if (self.contentView)
+ return self.contentView.frameHeight + self.contentViewInset.bottom + self.contentViewInset.top;
+ else if (self.subtitleView || self.subtitle.length > 0)
+ return CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT;
+ else
+ return CALLOUT_DEFAULT_CONTAINER_HEIGHT;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+
+ // calculate how much non-negotiable space we need to reserve for margin and accessories
+ CGFloat margin = self.innerContentMarginLeft + self.innerContentMarginRight;
+
+ // how much room is left for text?
+ CGFloat availableWidthForText = size.width - margin - 1;
+
+ // no room for text? then we'll have to squeeze into the given size somehow.
+ if (availableWidthForText < 0)
+ availableWidthForText = 0;
+
+ CGSize preferredTitleSize = [self.titleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, TITLE_HEIGHT)];
+ CGSize preferredSubtitleSize = [self.subtitleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, SUBTITLE_HEIGHT)];
+
+ // total width we'd like
+ CGFloat preferredWidth;
+
+ if (self.contentView) {
+
+ // if we have a content view, then take our preferred size directly from that
+ preferredWidth = self.contentView.frameWidth + margin;
+ }
+ else if (preferredTitleSize.width >= 0.000001 || preferredSubtitleSize.width >= 0.000001) {
+
+ // if we have a title or subtitle, then our assumed margins are valid, and we can apply them
+ preferredWidth = fmaxf(preferredTitleSize.width, preferredSubtitleSize.width) + margin;
+ }
+ else {
+ // ok we have no title or subtitle to speak of. In this case, the system callout would actually not display
+ // at all! But we can handle it.
+ preferredWidth = self.leftAccessoryView.frameWidth + self.rightAccessoryView.frameWidth + self.leftAccessoryHorizontalMargin + self.rightAccessoryHorizontalMargin;
+
+ if (self.leftAccessoryView && self.rightAccessoryView)
+ preferredWidth += BETWEEN_ACCESSORIES_MARGIN;
+ }
+
+ // ensure we're big enough to fit our graphics!
+ preferredWidth = fmaxf(preferredWidth, CALLOUT_MIN_WIDTH);
+
+ // ask to be smaller if we have space, otherwise we'll fit into what we have by truncating the title/subtitle.
+ return CGSizeMake(fminf(preferredWidth, size.width), self.calloutHeight);
+}
+
+- (CGSize)offsetToContainRect:(CGRect)innerRect inRect:(CGRect)outerRect {
+ CGFloat nudgeRight = fmaxf(0, CGRectGetMinX(outerRect) - CGRectGetMinX(innerRect));
+ CGFloat nudgeLeft = fminf(0, CGRectGetMaxX(outerRect) - CGRectGetMaxX(innerRect));
+ CGFloat nudgeTop = fmaxf(0, CGRectGetMinY(outerRect) - CGRectGetMinY(innerRect));
+ CGFloat nudgeBottom = fminf(0, CGRectGetMaxY(outerRect) - CGRectGetMaxY(innerRect));
+ return CGSizeMake(nudgeLeft ? nudgeLeft : nudgeRight, nudgeTop ? nudgeTop : nudgeBottom);
+}
+
+- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated {
+ [self presentCalloutFromRect:rect inLayer:view.layer ofView:view constrainedToLayer:constrainedView.layer animated:animated];
+}
+
+- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated {
+ [self presentCalloutFromRect:rect inLayer:layer ofView:nil constrainedToLayer:constrainedLayer animated:animated];
+}
+
+// this private method handles both CALayer and UIView parents depending on what's passed.
+- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer ofView:(UIView *)view constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated {
+
+ // Sanity check: dismiss this callout immediately if it's displayed somewhere
+ if (self.layer.superlayer) [self dismissCalloutAnimated:NO];
+
+ // cancel all animations that may be in progress
+ [self.layer removeAnimationForKey:@"present"];
+ [self.layer removeAnimationForKey:@"dismiss"];
+
+ // figure out the constrained view's rect in our popup view's coordinate system
+ CGRect constrainedRect = [constrainedLayer convertRect:constrainedLayer.bounds toLayer:layer];
+
+ // apply our edge constraints
+ constrainedRect = UIEdgeInsetsInsetRect(constrainedRect, self.constrainedInsets);
+
+ constrainedRect = CGRectInset(constrainedRect, COMFORTABLE_MARGIN, COMFORTABLE_MARGIN);
+
+ // form our subviews based on our content set so far
+ [self rebuildSubviews];
+
+ // apply title/subtitle (if present
+ self.titleLabel.text = self.title;
+ self.subtitleLabel.text = self.subtitle;
+
+ // size the callout to fit the width constraint as best as possible
+ self.frameSize = [self sizeThatFits:CGSizeMake(constrainedRect.size.width, self.calloutHeight)];
+
+ // how much room do we have in the constraint box, both above and below our target rect?
+ CGFloat topSpace = CGRectGetMinY(rect) - CGRectGetMinY(constrainedRect);
+ CGFloat bottomSpace = CGRectGetMaxY(constrainedRect) - CGRectGetMaxY(rect);
+
+ // we prefer to point our arrow down.
+ MGLSMCalloutArrowDirection bestDirection = MGLSMCalloutArrowDirectionDown;
+
+ // we'll point it up though if that's the only option you gave us.
+ if (self.permittedArrowDirection == MGLSMCalloutArrowDirectionUp)
+ bestDirection = MGLSMCalloutArrowDirectionUp;
+
+ // or, if we don't have enough space on the top and have more space on the bottom, and you
+ // gave us a choice, then pointing up is the better option.
+ if (self.permittedArrowDirection == MGLSMCalloutArrowDirectionAny && topSpace < self.calloutHeight && bottomSpace > topSpace)
+ bestDirection = MGLSMCalloutArrowDirectionUp;
+
+ self.currentArrowDirection = bestDirection;
+
+ // we want to point directly at the horizontal center of the given rect. calculate our "anchor point" in terms of our
+ // target view's coordinate system. make sure to offset the anchor point as requested if necessary.
+ CGFloat anchorX = self.calloutOffset.x + CGRectGetMidX(rect);
+ CGFloat anchorY = self.calloutOffset.y + (bestDirection == MGLSMCalloutArrowDirectionDown ? CGRectGetMinY(rect) : CGRectGetMaxY(rect));
+
+ // we prefer to sit centered directly above our anchor
+ CGFloat calloutX = roundf(anchorX - self.frameWidth / 2);
+
+ // but not if it's going to get too close to the edge of our constraints
+ if (calloutX < constrainedRect.origin.x)
+ calloutX = constrainedRect.origin.x;
+
+ if (calloutX > constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth)
+ calloutX = constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth;
+
+ // what's the farthest to the left and right that we could point to, given our background image constraints?
+ CGFloat minPointX = calloutX + self.backgroundView.anchorMargin;
+ CGFloat maxPointX = calloutX + self.frameWidth - self.backgroundView.anchorMargin;
+
+ // we may need to scoot over to the left or right to point at the correct spot
+ CGFloat adjustX = 0;
+ if (anchorX < minPointX) adjustX = anchorX - minPointX;
+ if (anchorX > maxPointX) adjustX = anchorX - maxPointX;
+
+ // add the callout to the given layer (or view if possible, to receive touch events)
+ if (view)
+ [view addSubview:self];
+ else
+ [layer addSublayer:self.layer];
+
+ CGPoint calloutOrigin = {
+ .x = calloutX + adjustX,
+ .y = bestDirection == MGLSMCalloutArrowDirectionDown ? (anchorY - self.calloutHeight) : anchorY
+ };
+
+ self.frameOrigin = calloutOrigin;
+
+ // now set the *actual* anchor point for our layer so that our "popup" animation starts from this point.
+ CGPoint anchorPoint = [layer convertPoint:CGPointMake(anchorX, anchorY) toLayer:self.layer];
+
+ // pass on the anchor point to our background view so it knows where to draw the arrow
+ self.backgroundView.arrowPoint = anchorPoint;
+
+ // adjust it to unit coordinates for the actual layer.anchorPoint property
+ anchorPoint.x /= self.frameWidth;
+ anchorPoint.y /= self.frameHeight;
+ self.layer.anchorPoint = anchorPoint;
+
+ // setting the anchor point moves the view a bit, so we need to reset
+ self.frameOrigin = calloutOrigin;
+
+ // make sure our frame is not on half-pixels or else we may be blurry!
+ CGFloat scale = [UIScreen mainScreen].scale;
+ self.frameX = floorf(self.frameX*scale)/scale;
+ self.frameY = floorf(self.frameY*scale)/scale;
+
+ // layout now so we can immediately start animating to the final position if needed
+ [self setNeedsLayout];
+ [self layoutIfNeeded];
+
+ // if we're outside the bounds of our constraint rect, we'll give our delegate an opportunity to shift us into position.
+ // consider both our size and the size of our target rect (which we'll assume to be the size of the content you want to scroll into view.
+ CGRect contentRect = CGRectUnion(self.frame, rect);
+ CGSize offset = [self offsetToContainRect:contentRect inRect:constrainedRect];
+
+ NSTimeInterval delay = 0;
+ self.popupCancelled = NO; // reset this before calling our delegate below
+
+ if ([self.delegate respondsToSelector:@selector(calloutView:delayForRepositionWithSize:)] && !CGSizeEqualToSize(offset, CGSizeZero))
+ delay = [self.delegate calloutView:(id)self delayForRepositionWithSize:offset];
+
+ // there's a chance that user code in the delegate method may have called -dismissCalloutAnimated to cancel things; if that
+ // happened then we need to bail!
+ if (self.popupCancelled) return;
+
+ // now we want to mask our contents to our background view (if requested) to match the iOS 7 style
+ self.containerView.layer.mask = self.backgroundView.contentMask;
+
+ // if we need to delay, we don't want to be visible while we're delaying, so hide us in preparation for our popup
+ self.hidden = YES;
+
+ // create the appropriate animation, even if we're not animated
+ CAAnimation *animation = [self animationWithType:self.presentAnimation presenting:YES];
+
+ // nuke the duration if no animation requested - we'll still need to "run" the animation to get delays and callbacks
+ if (!animated)
+ animation.duration = 0.0000001; // can't be zero or the animation won't "run"
+
+ animation.beginTime = CACurrentMediaTime() + delay;
+ animation.delegate = self;
+
+ [self.layer addAnimation:animation forKey:@"present"];
+}
+
+- (void)animationDidStart:(CAAnimation *)anim {
+ BOOL presenting = [[anim valueForKey:@"presenting"] boolValue];
+
+ if (presenting) {
+ if ([_delegate respondsToSelector:@selector(calloutViewWillAppear:)])
+ [_delegate calloutViewWillAppear:(id)self];
+
+ // ok, animation is on, let's make ourselves visible!
+ self.hidden = NO;
+ }
+ else if (!presenting) {
+ if ([_delegate respondsToSelector:@selector(calloutViewWillDisappear:)])
+ [_delegate calloutViewWillDisappear:(id)self];
+ }
+}
+
+- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)finished {
+ BOOL presenting = [[anim valueForKey:@"presenting"] boolValue];
+
+ if (presenting && finished) {
+ if ([_delegate respondsToSelector:@selector(calloutViewDidAppear:)])
+ [_delegate calloutViewDidAppear:(id)self];
+ }
+ else if (!presenting && finished) {
+
+ [self removeFromParent];
+ [self.layer removeAnimationForKey:@"dismiss"];
+
+ if ([_delegate respondsToSelector:@selector(calloutViewDidDisappear:)])
+ [_delegate calloutViewDidDisappear:(id)self];
+ }
+}
+
+- (void)dismissCalloutAnimated:(BOOL)animated {
+
+ // cancel all animations that may be in progress
+ [self.layer removeAnimationForKey:@"present"];
+ [self.layer removeAnimationForKey:@"dismiss"];
+
+ self.popupCancelled = YES;
+
+ if (animated) {
+ CAAnimation *animation = [self animationWithType:self.dismissAnimation presenting:NO];
+ animation.delegate = self;
+ [self.layer addAnimation:animation forKey:@"dismiss"];
+ }
+ else {
+ [self removeFromParent];
+ }
+}
+
+- (void)removeFromParent {
+ if (self.superview)
+ [self removeFromSuperview];
+ else {
+ // removing a layer from a superlayer causes an implicit fade-out animation that we wish to disable.
+ [CATransaction begin];
+ [CATransaction setDisableActions:YES];
+ [self.layer removeFromSuperlayer];
+ [CATransaction commit];
+ }
+}
+
+- (CAAnimation *)animationWithType:(MGLSMCalloutAnimation)type presenting:(BOOL)presenting {
+ CAAnimation *animation = nil;
+
+ if (type == MGLSMCalloutAnimationBounce) {
+
+ CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"];
+ fade.duration = 0.23;
+ fade.fromValue = presenting ? @0.0 : @1.0;
+ fade.toValue = presenting ? @1.0 : @0.0;
+ fade.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
+
+ CABasicAnimation *bounce = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
+ bounce.duration = 0.23;
+ bounce.fromValue = presenting ? @0.7 : @1.0;
+ bounce.toValue = presenting ? @1.0 : @0.7;
+ bounce.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.59367:0.12066:0.18878:1.5814];
+
+ CAAnimationGroup *group = [CAAnimationGroup animation];
+ group.animations = @[fade, bounce];
+ group.duration = 0.23;
+
+ animation = group;
+ }
+ else if (type == MGLSMCalloutAnimationFade) {
+ CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"];
+ fade.duration = 1.0/3.0;
+ fade.fromValue = presenting ? @0.0 : @1.0;
+ fade.toValue = presenting ? @1.0 : @0.0;
+ animation = fade;
+ }
+ else if (type == MGLSMCalloutAnimationStretch) {
+ CABasicAnimation *stretch = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
+ stretch.duration = 0.1;
+ stretch.fromValue = presenting ? @0.0 : @1.0;
+ stretch.toValue = presenting ? @1.0 : @0.0;
+ animation = stretch;
+ }
+
+ // CAAnimation is KVC compliant, so we can store whether we're presenting for lookup in our delegate methods
+ [animation setValue:@(presenting) forKey:@"presenting"];
+
+ animation.fillMode = kCAFillModeForwards;
+ animation.removedOnCompletion = NO;
+ return animation;
+}
+
+- (void)layoutSubviews {
+
+ self.containerView.frame = self.bounds;
+ self.backgroundView.frame = self.bounds;
+
+ // if we're pointing up, we'll need to push almost everything down a bit
+ CGFloat dy = self.currentArrowDirection == MGLSMCalloutArrowDirectionUp ? TOP_ANCHOR_MARGIN : 0;
+
+ self.titleViewOrDefault.frameX = self.innerContentMarginLeft;
+ self.titleViewOrDefault.frameY = (self.subtitleView || self.subtitle.length ? TITLE_SUB_TOP : TITLE_TOP) + dy;
+ self.titleViewOrDefault.frameWidth = self.frameWidth - self.innerContentMarginLeft - self.innerContentMarginRight;
+
+ self.subtitleViewOrDefault.frameX = self.titleViewOrDefault.frameX;
+ self.subtitleViewOrDefault.frameY = SUBTITLE_TOP + dy;
+ self.subtitleViewOrDefault.frameWidth = self.titleViewOrDefault.frameWidth;
+
+ self.leftAccessoryView.frameX = self.leftAccessoryHorizontalMargin;
+ self.leftAccessoryView.frameY = self.leftAccessoryVerticalMargin + dy;
+
+ self.rightAccessoryView.frameX = self.frameWidth - self.rightAccessoryHorizontalMargin - self.rightAccessoryView.frameWidth;
+ self.rightAccessoryView.frameY = self.rightAccessoryVerticalMargin + dy;
+
+ if (self.contentView) {
+ self.contentView.frameX = self.innerContentMarginLeft;
+ self.contentView.frameY = self.contentViewInset.top + dy;
+ }
+}
+
+#pragma mark - Accessibility
+
+- (NSInteger)accessibilityElementCount {
+ return (!!self.leftAccessoryView + !!self.titleViewOrDefault +
+ !!self.subtitleViewOrDefault + !!self.rightAccessoryView);
+}
+
+- (id)accessibilityElementAtIndex:(NSInteger)index {
+ if (index == 0) {
+ return self.leftAccessoryView ? self.leftAccessoryView : self.titleViewOrDefault;
+ }
+ if (index == 1) {
+ return self.leftAccessoryView ? self.titleViewOrDefault : self.subtitleViewOrDefault;
+ }
+ if (index == 2) {
+ return self.leftAccessoryView ? self.subtitleViewOrDefault : self.rightAccessoryView;
+ }
+ if (index == 3) {
+ return self.leftAccessoryView ? self.rightAccessoryView : nil;
+ }
+ return nil;
+}
+
+- (NSInteger)indexOfAccessibilityElement:(id)element {
+ if (element == nil) return NSNotFound;
+ if (element == self.leftAccessoryView) return 0;
+ if (element == self.titleViewOrDefault) {
+ return self.leftAccessoryView ? 1 : 0;
+ }
+ if (element == self.subtitleViewOrDefault) {
+ return self.leftAccessoryView ? 2 : 1;
+ }
+ if (element == self.rightAccessoryView) {
+ return self.leftAccessoryView ? 3 : 2;
+ }
+ return NSNotFound;
+}
+
+@end
+
+// import this known "private API" from SMCalloutBackgroundView
+@interface MGLSMCalloutBackgroundView (EmbeddedImages)
++ (UIImage *)embeddedImageNamed:(NSString *)name;
+@end
+
+//
+// Callout Background View.
+//
+
+@interface MGLSMCalloutMaskedBackgroundView ()
+@property (nonatomic, strong) UIView *containerView, *containerBorderView, *arrowView;
+@property (nonatomic, strong) UIImageView *arrowImageView, *arrowHighlightedImageView, *arrowBorderView;
+@end
+
+static UIImage *blackArrowImage = nil, *whiteArrowImage = nil, *grayArrowImage = nil;
+
+@implementation MGLSMCalloutMaskedBackgroundView
+
+- (id)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+
+ // Here we're mimicking the very particular (and odd) structure of the system callout view.
+ // The hierarchy and view/layer values were discovered by inspecting map kit using Reveal.app
+
+ self.containerView = [UIView new];
+ self.containerView.backgroundColor = [UIColor whiteColor];
+ self.containerView.alpha = 0.96;
+ self.containerView.layer.cornerRadius = 8;
+ self.containerView.layer.shadowRadius = 30;
+ self.containerView.layer.shadowOpacity = 0.1;
+
+ self.containerBorderView = [UIView new];
+ self.containerBorderView.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor;
+ self.containerBorderView.layer.borderWidth = 0.5;
+ self.containerBorderView.layer.cornerRadius = 8.5;
+
+ if (!blackArrowImage) {
+ blackArrowImage = [MGLSMCalloutBackgroundView embeddedImageNamed:@"CalloutArrow"];
+ whiteArrowImage = [self image:blackArrowImage withColor:[UIColor whiteColor]];
+ grayArrowImage = [self image:blackArrowImage withColor:[UIColor colorWithWhite:0.85 alpha:1]];
+ }
+
+ self.anchorHeight = 13;
+ self.anchorMargin = 27;
+
+ self.arrowView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, blackArrowImage.size.width, blackArrowImage.size.height)];
+ self.arrowView.alpha = 0.96;
+ self.arrowImageView = [[UIImageView alloc] initWithImage:whiteArrowImage];
+ self.arrowHighlightedImageView = [[UIImageView alloc] initWithImage:grayArrowImage];
+ self.arrowHighlightedImageView.hidden = YES;
+ self.arrowBorderView = [[UIImageView alloc] initWithImage:blackArrowImage];
+ self.arrowBorderView.alpha = 0.1;
+ self.arrowBorderView.frameY = 0.5;
+
+ [self addSubview:self.containerView];
+ [self.containerView addSubview:self.containerBorderView];
+ [self addSubview:self.arrowView];
+ [self.arrowView addSubview:self.arrowBorderView];
+ [self.arrowView addSubview:self.arrowImageView];
+ [self.arrowView addSubview:self.arrowHighlightedImageView];
+ }
+ return self;
+}
+
+// Make sure we relayout our images when our arrow point changes!
+- (void)setArrowPoint:(CGPoint)arrowPoint {
+ [super setArrowPoint:arrowPoint];
+ [self setNeedsLayout];
+}
+
+- (void)setHighlighted:(BOOL)highlighted {
+ [super setHighlighted:highlighted];
+ self.containerView.backgroundColor = highlighted ? [UIColor colorWithWhite:0.85 alpha:1] : [UIColor whiteColor];
+ self.arrowImageView.hidden = highlighted;
+ self.arrowHighlightedImageView.hidden = !highlighted;
+}
+
+- (UIImage *)image:(UIImage *)image withColor:(UIColor *)color {
+
+ UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);
+ CGRect imageRect = (CGRect){.size=image.size};
+ CGContextRef c = UIGraphicsGetCurrentContext();
+ CGContextTranslateCTM(c, 0, image.size.height);
+ CGContextScaleCTM(c, 1, -1);
+ CGContextClipToMask(c, imageRect, image.CGImage);
+ [color setFill];
+ CGContextFillRect(c, imageRect);
+ UIImage *whiteImage = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+ return whiteImage;
+}
+
+- (void)layoutSubviews {
+
+ BOOL pointingUp = self.arrowPoint.y < self.frameHeight/2;
+
+ // if we're pointing up, we'll need to push almost everything down a bit
+ CGFloat dy = pointingUp ? TOP_ANCHOR_MARGIN : 0;
+
+ self.containerView.frame = CGRectMake(0, dy, self.frameWidth, self.frameHeight - self.arrowView.frameHeight + 0.5);
+ self.containerBorderView.frame = CGRectInset(self.containerView.bounds, -0.5, -0.5);
+
+ self.arrowView.frameX = roundf(self.arrowPoint.x - self.arrowView.frameWidth / 2);
+
+ if (pointingUp) {
+ self.arrowView.frameY = 1;
+ self.arrowView.transform = CGAffineTransformMakeRotation(M_PI);
+ }
+ else {
+ self.arrowView.frameY = self.containerView.frameHeight - 0.5;
+ self.arrowView.transform = CGAffineTransformIdentity;
+ }
+}
+
+- (CALayer *)contentMask {
+
+ UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
+
+ [self.layer renderInContext:UIGraphicsGetCurrentContext()];
+
+ UIImage *maskImage = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+
+ CALayer *layer = [CALayer layer];
+ layer.frame = self.bounds;
+ layer.contents = (id)maskImage.CGImage;
+ return layer;
+}
+
+@end
+
+@implementation MGLSMCalloutBackgroundView
+
++ (NSData *)dataWithBase64EncodedString:(NSString *)string {
+ //
+ // NSData+Base64.m
+ //
+ // Version 1.0.2
+ //
+ // Created by Nick Lockwood on 12/01/2012.
+ // Copyright (C) 2012 Charcoal Design
+ //
+ // Distributed under the permissive zlib License
+ // Get the latest version from here:
+ //
+ // https://github.com/nicklockwood/Base64
+ //
+ // This software is provided 'as-is', without any express or implied
+ // warranty. In no event will the authors be held liable for any damages
+ // arising from the use of this software.
+ //
+ // Permission is granted to anyone to use this software for any purpose,
+ // including commercial applications, and to alter it and redistribute it
+ // freely, subject to the following restrictions:
+ //
+ // 1. The origin of this software must not be misrepresented; you must not
+ // claim that you wrote the original software. If you use this software
+ // in a product, an acknowledgment in the product documentation would be
+ // appreciated but is not required.
+ //
+ // 2. Altered source versions must be plainly marked as such, and must not be
+ // misrepresented as being the original software.
+ //
+ // 3. This notice may not be removed or altered from any source distribution.
+ //
+ const char lookup[] = {
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99,
+ 99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99,
+ 99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99
+ };
+
+ NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
+ long long inputLength = [inputData length];
+ const unsigned char *inputBytes = [inputData bytes];
+
+ long long maxOutputLength = (inputLength / 4 + 1) * 3;
+ NSMutableData *outputData = [NSMutableData dataWithLength:(NSUInteger)maxOutputLength];
+ unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes];
+
+ int accumulator = 0;
+ long long outputLength = 0;
+ unsigned char accumulated[] = {0, 0, 0, 0};
+ for (long long i = 0; i < inputLength; i++) {
+ unsigned char decoded = lookup[inputBytes[i] & 0x7F];
+ if (decoded != 99) {
+ accumulated[accumulator] = decoded;
+ if (accumulator == 3) {
+ outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4);
+ outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2);
+ outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3];
+ }
+ accumulator = (accumulator + 1) % 4;
+ }
+ }
+
+ //handle left-over data
+ if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4);
+ if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2);
+ if (accumulator > 2) outputLength++;
+
+ //truncate data to match actual output length
+ outputData.length = (NSUInteger)outputLength;
+ return outputLength? outputData: nil;
+}
+
++ (UIImage *)embeddedImageNamed:(NSString *)name {
+ CGFloat screenScale = [UIScreen mainScreen].scale;
+ if (screenScale > 1.0) {
+ name = [name stringByAppendingString:@"_2x"];
+ screenScale = 2.0;
+ }
+
+ SEL selector = NSSelectorFromString(name);
+
+ if (![(id)self respondsToSelector:selector]) {
+ NSLog(@"Could not find an embedded image. Ensure that you've added a class-level method named +%@", name);
+ return nil;
+ }
+
+ // We need to hush the compiler here - but we know what we're doing!
+ #pragma clang diagnostic push
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ NSString *base64String = [(id)self performSelector:selector];
+ #pragma clang diagnostic pop
+
+ UIImage *rawImage = [UIImage imageWithData:[self dataWithBase64EncodedString:base64String]];
+ return [UIImage imageWithCGImage:rawImage.CGImage scale:screenScale orientation:UIImageOrientationUp];
+}
+
++ (NSString *)CalloutArrow { return @"iVBORw0KGgoAAAANSUhEUgAAACcAAAANCAYAAAAqlHdlAAAAHGlET1QAAAACAAAAAAAAAAcAAAAoAAAABwAAAAYAAADJEgYpIwAAAJVJREFUOBFiYIAAdn5+fkFOTkE5Dg5eW05O3lJOTr6zQPyfDhhoD28pxF5BOZA7gE5ih7oLN8XJyR8MdNwrGjkQaC5/MG7biZDh4OBXBDruLpUdeBdkLhHWE1bCzs6nAnTcUyo58DnIPMK2kqAC6DALIP5JoQNB+i1IsJZ4pcBEm0iJ40D6ibeNDJVAx00k04ETSbUOAAAA//+SwicfAAAAe0lEQVRjYCAdMHNy8u7l5OT7Tzzm3Qu0hpl0q8jQwcPDIwp02B0iHXeHl5dXhAxryNfCzc2tC3TcJwIO/ARSR74tFOjk4uL1BzruHw4H/gPJU2A85Vq5uPjTgY77g+bAPyBxyk2nggkcHPxOnJz8B4AOfAGiQXwqGMsAACGK1kPPMHNBAAAAAElFTkSuQmCC"; }
+
++ (NSString *)CalloutArrow_2x { return @"iVBORw0KGgoAAAANSUhEUgAAAE4AAAAaCAYAAAAZtWr8AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAHGlET1QAAAACAAAAAAAAAA0AAAAoAAAADQAAAA0AAAFMRh0LGwAAARhJREFUWAnclbENwjAQRZ0mih2fDYgsQEVDxQZMgKjpWYAJkBANI8AGDIEoM0WkzBDRAf8klB44g0OkU1zE3/+9RIpS7VVY730/y/woTWlsjJ9iPcN9pbXfY85auyvm/qcDNmb0e2Z+sk/ZBTthN0oVttX12mJIWeaWEFf+kbySmZQa0msu3nzaGJprTXV3BVLNDG/if7bNOTeAvFP35NGJu39GL7Abb27bFXncVQBZLgJf3jp+ebSWIxZMgrxdvPJoJ4gqHpXgV36ITR46HUGaiNMKB6YQd4lI3gV8qTBjmDhrbQFxVQTyKu4ShjJQap7nE4hrfiiv4Q6B8MLGat1bQNztB/JwZm8Rli5wujFu821xfGZgLPUAAAD//4wvm4gAAAD7SURBVOWXMQ6CMBiFgaFpi6VyBEedXJy4hMQTeBSvRDgJEySegI3EQWOivkZnqUB/k0LyL7R9L++D9G+DwP0TCZGUqCdRlYgUuY9F4JCmqQa0hgBcY7wIItFZMLZYS5l0ruAZbXhs6BIROgmhcoB7OIAHTZUTRqG3wp9xmhqc0aRPQu8YAlwxIbwCEUL6GH9wfDcLXY2HpyvvmkHf9+BcrwCuHQGvNRp9Pl6OY0PPAO42AB7WqMxLKLahpFR7gLv/AA9zPe+gtvAMCIC7WMC7CqEPtrqzmBfHyy3A1V/g1Th27GYBY0BIxrk6Ap65254/VZp30GID9JwteQEZrVMWXqGn8gAAAABJRU5ErkJggg=="; }
+
+@end
+
+//
+// Our UIView frame helpers implementation
+//
+
+@implementation UIView (SMFrameAdditions)
+
+- (CGPoint)frameOrigin { return self.frame.origin; }
+- (void)setFrameOrigin:(CGPoint)origin { self.frame = (CGRect){ .origin=origin, .size=self.frame.size }; }
+
+- (CGFloat)frameX { return self.frame.origin.x; }
+- (void)setFrameX:(CGFloat)x { self.frame = (CGRect){ .origin.x=x, .origin.y=self.frame.origin.y, .size=self.frame.size }; }
+
+- (CGFloat)frameY { return self.frame.origin.y; }
+- (void)setFrameY:(CGFloat)y { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=y, .size=self.frame.size }; }
+
+- (CGSize)frameSize { return self.frame.size; }
+- (void)setFrameSize:(CGSize)size { self.frame = (CGRect){ .origin=self.frame.origin, .size=size }; }
+
+- (CGFloat)frameWidth { return self.frame.size.width; }
+- (void)setFrameWidth:(CGFloat)width { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=width, .size.height=self.frame.size.height }; }
+
+- (CGFloat)frameHeight { return self.frame.size.height; }
+- (void)setFrameHeight:(CGFloat)height { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=height }; }
+
+- (CGFloat)frameLeft { return self.frame.origin.x; }
+- (void)setFrameLeft:(CGFloat)left { self.frame = (CGRect){ .origin.x=left, .origin.y=self.frame.origin.y, .size.width=fmaxf(self.frame.origin.x+self.frame.size.width-left,0), .size.height=self.frame.size.height }; }
+
+- (CGFloat)frameTop { return self.frame.origin.y; }
+- (void)setFrameTop:(CGFloat)top { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=top, .size.width=self.frame.size.width, .size.height=fmaxf(self.frame.origin.y+self.frame.size.height-top,0) }; }
+
+- (CGFloat)frameRight { return self.frame.origin.x + self.frame.size.width; }
+- (void)setFrameRight:(CGFloat)right { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=fmaxf(right-self.frame.origin.x,0), .size.height=self.frame.size.height }; }
+
+- (CGFloat)frameBottom { return self.frame.origin.y + self.frame.size.height; }
+- (void)setFrameBottom:(CGFloat)bottom { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=fmaxf(bottom-self.frame.origin.y,0) }; }
+
+@end