summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMinh Nguyễn <mxn@1ec5.org>2017-09-10 11:37:01 -0700
committerMinh Nguyễn <mxn@1ec5.org>2017-11-02 15:19:53 -0700
commit6dd13241c3d77d5fe792b382fdbc064ac83aa9d9 (patch)
treecf124d3a627102e0701dd596f4f1b56fae37631e
parent0983893ca511a2bb157c41a7c173511c5a4a45f7 (diff)
downloadqtlocation-mapboxgl-6dd13241c3d77d5fe792b382fdbc064ac83aa9d9.tar.gz
[ios] Allow VoiceOver to navigate among place features
Split out a separate header for the various accessibility elements tied to MGLMapView. Wrap place features in accessibility elements and insert them into the narration order after the visible annotations but before the attribution button. Refactored MGLMapView’s accessibility code to rely on ranges to avoid off-by-one errors.
-rw-r--r--platform/ios/ios.xcodeproj/project.pbxproj12
-rw-r--r--platform/ios/src/MGLMapAccessibilityElement.h40
-rw-r--r--platform/ios/src/MGLMapAccessibilityElement.m90
-rw-r--r--platform/ios/src/MGLMapView.mm265
4 files changed, 301 insertions, 106 deletions
diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj
index d67537a3cb..88dadc8137 100644
--- a/platform/ios/ios.xcodeproj/project.pbxproj
+++ b/platform/ios/ios.xcodeproj/project.pbxproj
@@ -296,6 +296,10 @@
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.m in Sources */ = {isa = PBXBuildFile; fileRef = DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.m */; };
+ DA704CC51F65A475004B3F28 /* MGLMapAccessibilityElement.m in Sources */ = {isa = PBXBuildFile; fileRef = DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.m */; };
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 */; };
@@ -821,6 +825,8 @@
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>"; };
DA704CC71F6663A3004B3F28 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Foundation.strings; sourceTree = "<group>"; };
+ DA704CC01F65A475004B3F28 /* MGLMapAccessibilityElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapAccessibilityElement.h; sourceTree = "<group>"; };
+ DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLMapAccessibilityElement.m; 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>"; };
DA737ADA1E59139D00AD2CDE /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = es; path = es.lproj/Foundation.stringsdict; sourceTree = "<group>"; };
@@ -1423,6 +1429,8 @@
35CE617F1D4165C2004F2359 /* Categories */,
DAD165841CF4D06B001FF4B9 /* Annotations */,
DAD165851CF4D08B001FF4B9 /* Telemetry */,
+ DA704CC01F65A475004B3F28 /* MGLMapAccessibilityElement.h */,
+ DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.m */,
DA8848361CBAFB8500AB86E3 /* MGLMapView.h */,
DA17BE2F1CC4BAC300402C41 /* MGLMapView_Private.h */,
DA8848371CBAFB8500AB86E3 /* MGLMapView+IBAdditions.h */,
@@ -1762,6 +1770,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 */,
@@ -1897,6 +1906,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 */,
@@ -2364,6 +2374,7 @@
DA88482A1CBAFA6200AB86E3 /* MGLTilePyramidOfflineRegion.mm in Sources */,
4049C29F1DB6CD6C00B3F799 /* MGLPointCollection.mm in Sources */,
35136D3F1D42273000C20EFD /* MGLLineStyleLayer.mm in Sources */,
+ DA704CC41F65A475004B3F28 /* MGLMapAccessibilityElement.m in Sources */,
DA72620D1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm in Sources */,
DA88481A1CBAFA6200AB86E3 /* MGLAccountManager.m in Sources */,
3510FFFB1D6DCC4700F413B2 /* NSCompoundPredicate+MGLAdditions.mm in Sources */,
@@ -2451,6 +2462,7 @@
DAA4E4211CBB730400178DFB /* MGLOfflineStorage.mm in Sources */,
4049C2A01DB6CD6C00B3F799 /* MGLPointCollection.mm in Sources */,
35136D401D42273000C20EFD /* MGLLineStyleLayer.mm in Sources */,
+ DA704CC51F65A475004B3F28 /* MGLMapAccessibilityElement.m in Sources */,
DA72620E1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm in Sources */,
DAA4E42F1CBB730400178DFB /* MGLCompactCalloutView.m in Sources */,
3510FFFC1D6DCC4700F413B2 /* NSCompoundPredicate+MGLAdditions.mm in Sources */,
diff --git a/platform/ios/src/MGLMapAccessibilityElement.h b/platform/ios/src/MGLMapAccessibilityElement.h
new file mode 100644
index 0000000000..9693c570f6
--- /dev/null
+++ b/platform/ios/src/MGLMapAccessibilityElement.h
@@ -0,0 +1,40 @@
+#import <UIKit/UIKit.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. */
+@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. */
+@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 the MGLMapView at large. */
+@interface MGLMapViewProxyAccessibilityElement : UIAccessibilityElement
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/ios/src/MGLMapAccessibilityElement.m b/platform/ios/src/MGLMapAccessibilityElement.m
new file mode 100644
index 0000000000..4a4569bb35
--- /dev/null
+++ b/platform/ios/src/MGLMapAccessibilityElement.m
@@ -0,0 +1,90 @@
+#import "MGLMapAccessibilityElement.h"
+#import "MGLDistanceFormatter.h"
+#import "MGLFeature.h"
+
+#import "NSBundle+MGLAdditions.h"
+
+@implementation MGLMapAccessibilityElement
+
+- (instancetype)initWithAccessibilityContainer:(id)container {
+ if (self = [super initWithAccessibilityContainer:container]) {
+ self.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitAdjustable;
+ }
+ return self;
+}
+
+- (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;
+}
+
+@end
+
+@implementation MGLFeatureAccessibilityElement
+
+- (instancetype)initWithAccessibilityContainer:(id)container feature:(id<MGLFeature>)feature {
+ if (self = [super initWithAccessibilityContainer:container]) {
+ _feature = feature;
+
+ NSDictionary *attributes = feature.attributes;
+ // TODO: Localize the name.
+ self.accessibilityLabel = attributes[@"name"];
+
+ 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.
+ [facts addObject:[attributes[@"type"] stringByReplacingOccurrencesOfString:@"_" withString:@" "]];
+ }
+ // 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"]];
+ }
+
+ NSNumber *elevation = attributes[@"elevation_m"];
+ if (elevation) {
+ MGLDistanceFormatter *formatter = [[MGLDistanceFormatter alloc] init];
+ formatter.unitStyle = NSFormattingUnitStyleLong;
+ [facts addObject:[formatter stringFromDistance:elevation.doubleValue]];
+ }
+
+ if (facts.count) {
+ self.accessibilityValue = [facts componentsJoinedByString:NSLocalizedStringWithDefaultValue(@"LIST_SEPARATOR", nil, nil, @", ", @"List 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 630688f05a..5018a18b3a 100644
--- a/platform/ios/src/MGLMapView.mm
+++ b/platform/ios/src/MGLMapView.mm
@@ -69,6 +69,7 @@
#import "MGLAnnotationContainerView.h"
#import "MGLAnnotationContainerView_Private.h"
#import "MGLAttributionInfo_Private.h"
+#import "MGLMapAccessibilityElement.h"
#include <algorithm>
#include <cstdlib>
@@ -140,9 +141,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 +163,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,26 +174,6 @@ 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,
@@ -328,6 +274,7 @@ public:
BOOL _delegateHasLineWidthsForShapeAnnotations;
MGLCompassDirectionFormatter *_accessibilityCompassFormatter;
+ NS_MUTABLE_DICTIONARY_OF(id, MGLFeatureAccessibilityElement *) *_accessibilityFeaturesByIdentifier;
MGLReachability *_reachability;
}
@@ -2463,67 +2410,97 @@ public:
}
return nil;
}
+
std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds];
-
- // Ornaments
- if (index == 0)
+ NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures;
+
+ // 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 */)
+
+ // Visible annotations
+ NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size());
+ if (NSLocationInRange(index, visibleAnnotationRange))
{
- 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 = 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];
}
-
- std::sort(visibleAnnotations.begin(), visibleAnnotations.end());
- CGPoint centerPoint = self.contentCenter;
- if (self.userTrackingMode != MGLUserTrackingModeNone)
+
+ // Visible place features
+ NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count);
+ if (NSLocationInRange(index, visiblePlaceFeatureRange))
{
- centerPoint = self.userLocationAnnotationViewCenter;
+ id <MGLFeature> feature = visiblePlaceFeatures[index - visiblePlaceFeatureRange.location];
+ return [self accessibilityElementForFeature:feature withIdentifier:feature.identifier];
}
- 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())
+
+ // Attribution button
+ NSUInteger attributionButtonIndex = NSMaxRange(visiblePlaceFeatureRange);
+ 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: %@.",
+ (long)index, NSStringFromRange(userLocationAnnotationRange),
+ NSStringFromRange(visibleAnnotationRange), NSStringFromRange(visiblePlaceFeatureRange));
+ 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];
@@ -2534,8 +2511,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;
@@ -2544,10 +2520,65 @@ public:
{
annotationContext.accessibilityElement.accessibilityValue = annotation.subtitle;
}
-
+
return annotationContext.accessibilityElement;
}
+/**
+ Returns an accessibility element corresponding to the given feature.
+
+ @param feature An optional feature represented by the accessibility element.
+ @param identifier A feature identifier used as a fallback in case the feature
+ is unspecified and also used to cache the accessibility element.
+ */
+- (id)accessibilityElementForFeature:(id <MGLFeature>)feature withIdentifier:(id)identifier
+{
+ if (!_accessibilityFeaturesByIdentifier)
+ {
+ _accessibilityFeaturesByIdentifier = [NSMutableDictionary dictionary];
+ }
+
+ MGLFeatureAccessibilityElement *element = identifier ? _accessibilityFeaturesByIdentifier[identifier] : nil;
+ // It isn’t possible to check here whether feature is equal to
+ // element.feature, because various attributes and even the coordinate may
+ // change from one zoom level to the next. The only constant is the
+ // identifier.
+ if (!feature)
+ {
+ feature = element.feature;
+ }
+
+ // Lazily create an accessibility element for the found feature.
+ if (!feature)
+ {
+ NSArray *layerIdentifiers = [self.style.placeStyleLayers valueForKey:@"identifier"];
+ NSPredicate *identifierPredicate = [NSPredicate predicateWithFormat:@"%K == %@", @"$id", identifier];
+ NSArray *features = [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:layerIdentifiers] predicate:identifierPredicate];
+ feature = features.firstObject;
+ }
+ if (!element)
+ {
+ element = [[MGLFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature];
+ }
+ if (!feature)
+ {
+ return nil;
+ }
+
+ // Update the accessibility element.
+ 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;
+
+ if (identifier)
+ {
+ _accessibilityFeaturesByIdentifier[identifier] = element;
+ }
+
+ return element;
+}
+
- (NSInteger)indexOfAccessibilityElement:(id)element
{
if (self.calloutViewForSelectedAnnotation)
@@ -2555,17 +2586,24 @@ 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;
}
-
+
+ // 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]])
{
@@ -2576,22 +2614,36 @@ public:
{
tag = [(MGLAnnotationAccessibilityElement *)element tag];
}
- else if (element == self.attributionButton)
+
+ if (tag != MGLAnnotationTagNotFound)
{
- return !!self.userLocationAnnotationView + visibleAnnotations.size();
+ std::sort(visibleAnnotations.begin(), visibleAnnotations.end());
+ auto foundElement = std::find(visibleAnnotations.begin(), visibleAnnotations.end(), tag);
+ if (foundElement == visibleAnnotations.end())
+ {
+ return NSNotFound;
+ }
+ return visibleAnnotationRange.location + std::distance(visibleAnnotations.begin(), foundElement);
}
- else
+
+ // Visible place features
+ NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures;
+ NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count);
+ if ([element isKindOfClass:[MGLFeatureAccessibilityElement class]])
{
- return NSNotFound;
+ id <MGLFeature> feature = [(MGLFeatureAccessibilityElement *)element feature];
+ NSUInteger featureIndex = [visiblePlaceFeatures indexOfObject:feature];
+ return visiblePlaceFeatureRange.location + featureIndex;
}
-
- std::sort(visibleAnnotations.begin(), visibleAnnotations.end());
- auto foundElement = std::find(visibleAnnotations.begin(), visibleAnnotations.end(), tag);
- if (foundElement == visibleAnnotations.end())
+
+ // Attribution button
+ NSUInteger attributionButtonIndex = NSMaxRange(visiblePlaceFeatureRange);
+ if (element == self.attributionButton)
{
- return NSNotFound;
+ return attributionButtonIndex;
}
- return !!self.userLocationAnnotationView + std::distance(visibleAnnotations.begin(), foundElement) + 1 /* compass */;
+
+ return NSNotFound;
}
- (MGLMapViewProxyAccessibilityElement *)mapViewProxyAccessibilityElement
@@ -5185,6 +5237,7 @@ public:
{
if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive)
{
+ _accessibilityFeaturesByIdentifier = nil;
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
}
[self.delegate mapView:self regionDidChangeAnimated:animated];