diff options
author | Minh Nguyễn <mxn@1ec5.org> | 2017-09-10 16:38:26 -0700 |
---|---|---|
committer | Minh Nguyễn <mxn@1ec5.org> | 2017-11-02 15:19:54 -0700 |
commit | 7a1fdd8fa6919d6f50e2b99448a9b5822c2c60a9 (patch) | |
tree | f5785f98e773584cb49064608325a57c773b8e21 | |
parent | de52d40ea6fbbe99f93eb9d16cba59e5dfb6c399 (diff) | |
download | qtlocation-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.strings | 6 | ||||
-rw-r--r-- | platform/ios/src/MGLMapAccessibilityElement.h | 4 | ||||
-rw-r--r-- | platform/ios/src/MGLMapAccessibilityElement.m | 74 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 128 |
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]; |