summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMinh Nguyễn <mxn@1ec5.org>2017-09-10 16:38:26 -0700
committerMinh Nguyễn <mxn@1ec5.org>2017-11-02 15:19:54 -0700
commit7a1fdd8fa6919d6f50e2b99448a9b5822c2c60a9 (patch)
treef5785f98e773584cb49064608325a57c773b8e21
parentde52d40ea6fbbe99f93eb9d16cba59e5dfb6c399 (diff)
downloadqtlocation-mapboxgl-7a1fdd8fa6919d6f50e2b99448a9b5822c2c60a9.tar.gz
[ios] Made roads accessible
Wrap visible road features in accessibility elements described by the road name, route number, and general direction of travel.
-rw-r--r--platform/ios/resources/Base.lproj/Localizable.strings6
-rw-r--r--platform/ios/src/MGLMapAccessibilityElement.h4
-rw-r--r--platform/ios/src/MGLMapAccessibilityElement.m74
-rw-r--r--platform/ios/src/MGLMapView.mm128
4 files changed, 199 insertions, 13 deletions
diff --git a/platform/ios/resources/Base.lproj/Localizable.strings b/platform/ios/resources/Base.lproj/Localizable.strings
index 815f7a3498..d214ccb4d1 100644
--- a/platform/ios/resources/Base.lproj/Localizable.strings
+++ b/platform/ios/resources/Base.lproj/Localizable.strings
@@ -64,6 +64,12 @@
/* 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";
+
+/* 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/src/MGLMapAccessibilityElement.h b/platform/ios/src/MGLMapAccessibilityElement.h
index e59cd628fb..efe077fac9 100644
--- a/platform/ios/src/MGLMapAccessibilityElement.h
+++ b/platform/ios/src/MGLMapAccessibilityElement.h
@@ -36,6 +36,10 @@ typedef uint32_t MGLAnnotationTag;
@interface MGLPlaceFeatureAccessibilityElement : MGLFeatureAccessibilityElement
@end
+/** An accessibility element representing a road feature. */
+@interface MGLRoadFeatureAccessibilityElement : MGLFeatureAccessibilityElement
+@end
+
/** An accessibility element representing the MGLMapView at large. */
@interface MGLMapViewProxyAccessibilityElement : UIAccessibilityElement
diff --git a/platform/ios/src/MGLMapAccessibilityElement.m b/platform/ios/src/MGLMapAccessibilityElement.m
index 2641dcdfc6..84f44090ac 100644
--- a/platform/ios/src/MGLMapAccessibilityElement.m
+++ b/platform/ios/src/MGLMapAccessibilityElement.m
@@ -1,10 +1,36 @@
#import "MGLMapAccessibilityElement.h"
#import "MGLDistanceFormatter.h"
+#import "MGLCompassDirectionFormatter.h"
#import "MGLFeature.h"
#import "MGLVectorSource+MGLAdditions.h"
#import "NSBundle+MGLAdditions.h"
+typedef CLLocationDegrees MGLLocationRadians;
+typedef CLLocationDirection MGLRadianDirection;
+typedef struct {
+ MGLLocationRadians latitude;
+ MGLLocationRadians longitude;
+} MGLRadianCoordinate2D;
+
+/** Returns the direction from one coordinate to another. */
+CLLocationDirection MGLDirectionBetweenCoordinates(CLLocationCoordinate2D firstCoordinate, CLLocationCoordinate2D secondCoordinate) {
+ MGLRadianCoordinate2D firstRadianCoordinate = {
+ firstCoordinate.latitude * M_PI / 180,
+ firstCoordinate.longitude * M_PI / 180,
+ };
+ MGLRadianCoordinate2D secondRadianCoordinate = {
+ secondCoordinate.latitude * M_PI / 180,
+ secondCoordinate.longitude * M_PI / 180,
+ };
+
+ CGFloat a = sin(secondRadianCoordinate.longitude - firstRadianCoordinate.longitude) * cos(secondRadianCoordinate.latitude);
+ CGFloat b = (cos(firstRadianCoordinate.latitude) * sin(secondRadianCoordinate.latitude)
+ - sin(firstRadianCoordinate.latitude) * cos(secondRadianCoordinate.latitude) * cos(secondRadianCoordinate.longitude - firstRadianCoordinate.longitude));
+ MGLRadianDirection radianDirection = atan2(a, b);
+ return radianDirection * 180 / M_PI;
+}
+
@implementation MGLMapAccessibilityElement
- (UIAccessibilityTraits)accessibilityTraits {
@@ -95,6 +121,54 @@
@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 divided road.
+ if ([feature isKindOfClass:[MGLShapeCollectionFeature class]]) {
+ [facts addObject:NSLocalizedStringWithDefaultValue(@"ROAD_DIVIDED_A11Y_VALUE", nil, nil, @"Divided road", @"Accessibility value indicating that a road is a divided road (dual carriageway)")];
+ feature = [(MGLShapeCollectionFeature *)feature shapes].firstObject;
+ }
+
+ // Announce the road’s general direction.
+ if ([feature isKindOfClass:[MGLPolylineFeature class]]) {
+ NSUInteger pointCount = [(MGLPolylineFeature *)feature pointCount];
+ if (pointCount) {
+ CLLocationCoordinate2D *coordinates = [(MGLPolyline *)feature 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 {
diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm
index 6746ddf7a0..d5ea8c7f90 100644
--- a/platform/ios/src/MGLMapView.mm
+++ b/platform/ios/src/MGLMapView.mm
@@ -275,6 +275,7 @@ public:
MGLCompassDirectionFormatter *_accessibilityCompassFormatter;
NS_ARRAY_OF(id <MGLFeature>) *_visiblePlaceFeatures;
+ NS_ARRAY_OF(id <MGLFeature>) *_visibleRoadFeatures;
NS_MUTABLE_SET_OF(MGLFeatureAccessibilityElement *) *_featureAccessibilityElements;
MGLReachability *_reachability;
@@ -2353,8 +2354,12 @@ public:
- (NS_ARRAY_OF(id <MGLFeature>) *)visibleRoadFeatures
{
- NSArray *roadStyleLayerIdentifiers = [self.style.roadStyleLayers valueForKey:@"identifier"];
- return [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:roadStyleLayerIdentifiers]];
+ if (!_visibleRoadFeatures)
+ {
+ NSArray *roadStyleLayerIdentifiers = [self.style.roadStyleLayers valueForKey:@"identifier"];
+ _visibleRoadFeatures = [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:roadStyleLayerIdentifiers]];
+ }
+ return _visibleRoadFeatures;
}
- (CGRect)accessibilityFrame
@@ -2390,7 +2395,7 @@ public:
{
return 2 /* calloutViewForSelectedAnnotation, mapViewProxyAccessibilityElement */;
}
- return !!self.userLocationAnnotationView + self.accessibilityAnnotationCount + self.visiblePlaceFeatures.count + 2 /* compass, attributionButton */;
+ return !!self.userLocationAnnotationView + self.accessibilityAnnotationCount + self.visiblePlaceFeatures.count + self.visibleRoadFeatures.count + 2 /* compass, attributionButton */;
}
- (NSInteger)accessibilityAnnotationCount
@@ -2472,20 +2477,39 @@ public:
}];
id <MGLFeature> feature = visiblePlaceFeatures[index - visiblePlaceFeatureRange.location];
- return [self accessibilityElementForFeature:feature];
+ 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(visiblePlaceFeatureRange);
+ NSUInteger attributionButtonIndex = NSMaxRange(visibleRoadFeatureRange);
if (index == attributionButtonIndex)
{
return self.attributionButton;
}
NSAssert(NO, @"Index %ld not in recognized accessibility element ranges. "
- @"User location annotation range: %@; visible annotation range: %@; visible place feature range: %@.",
+ @"User location annotation range: %@; visible annotation range: %@; "
+ @"visible place feature range: %@; visible road feature range: %@.",
(long)index, NSStringFromRange(userLocationAnnotationRange),
- NSStringFromRange(visibleAnnotationRange), NSStringFromRange(visiblePlaceFeatureRange));
+ NSStringFromRange(visibleAnnotationRange), NSStringFromRange(visiblePlaceFeatureRange),
+ NSStringFromRange(visibleRoadFeatureRange));
return nil;
}
@@ -2537,11 +2561,11 @@ public:
}
/**
- Returns an accessibility element corresponding to the given feature.
+ Returns an accessibility element corresponding to the given place feature.
- @param feature The feature represented by the accessibility element.
+ @param feature The place feature represented by the accessibility element.
*/
-- (id)accessibilityElementForFeature:(id <MGLFeature>)feature
+- (id)accessibilityElementForPlaceFeature:(id <MGLFeature>)feature
{
if (!_featureAccessibilityElements)
{
@@ -2549,7 +2573,7 @@ public:
}
MGLFeatureAccessibilityElement *element = [_featureAccessibilityElements objectsPassingTest:^BOOL(MGLFeatureAccessibilityElement * _Nonnull element, BOOL * _Nonnull stop) {
- return [element.feature.identifier isEqual:feature.identifier] || [element.feature isEqual:feature];
+ return (element.feature.identifier && [element.feature.identifier isEqual:feature.identifier]) || [element.feature isEqual:feature];
}].anyObject;
if (!element)
{
@@ -2565,6 +2589,63 @@ public:
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:feature.identifier]) || [element.feature isEqual:feature];
+ }].anyObject;
+ if (!element)
+ {
+ element = [[MGLRoadFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature];
+ }
+
+ if ([feature isKindOfClass:[MGLShapeCollectionFeature class]])
+ {
+ feature = [(MGLShapeCollectionFeature *)feature shapes].firstObject;
+ }
+ 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]])
+ {
+ CLLocationCoordinate2D *coordinates = [(MGLPolylineFeature *)feature coordinates];
+ NSUInteger pointCount = [(MGLPolylineFeature *)feature 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];
+ }
+ }
+ UIBezierPath *screenPath = UIAccessibilityConvertPathToScreenCoordinates(path, self);
+ element.accessibilityPath = screenPath;
+ }
+
+ [_featureAccessibilityElements addObject:element];
+
+ return element;
+}
+
- (NSInteger)indexOfAccessibilityElement:(id)element
{
if (self.calloutViewForSelectedAnnotation)
@@ -2622,7 +2703,7 @@ public:
if (featureIndex == NSNotFound)
{
featureIndex = [visiblePlaceFeatures indexOfObjectPassingTest:^BOOL (id <MGLFeature> _Nonnull visibleFeature, NSUInteger idx, BOOL * _Nonnull stop) {
- return [visibleFeature.identifier isEqual:feature.identifier];
+ return visibleFeature.identifier && [visibleFeature.identifier isEqual:feature.identifier];
}];
}
if (featureIndex == NSNotFound)
@@ -2632,8 +2713,28 @@ public:
return visiblePlaceFeatureRange.location + featureIndex;
}
+ // Visible road features
+ NSArray *visibleRoadFeatures = self.visibleRoadFeatures;
+ NSRange visibleRoadFeatureRange = NSMakeRange(NSMaxRange(visiblePlaceFeatureRange), visibleRoadFeatures.count);
+ if ([element isKindOfClass:[MGLFeatureAccessibilityElement class]])
+ {
+ id <MGLFeature> feature = [(MGLFeatureAccessibilityElement *)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:feature.identifier];
+ }];
+ }
+ if (featureIndex == NSNotFound)
+ {
+ return NSNotFound;
+ }
+ return visibleRoadFeatureRange.location + featureIndex;
+ }
+
// Attribution button
- NSUInteger attributionButtonIndex = NSMaxRange(visiblePlaceFeatureRange);
+ NSUInteger attributionButtonIndex = NSMaxRange(visibleRoadFeatureRange);
if (element == self.attributionButton)
{
return attributionButtonIndex;
@@ -5235,6 +5336,7 @@ public:
{
_featureAccessibilityElements = nil;
_visiblePlaceFeatures = nil;
+ _visibleRoadFeatures = nil;
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
}
[self.delegate mapView:self regionDidChangeAnimated:animated];