From 0983893ca511a2bb157c41a7c173511c5a4a45f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sat, 9 Sep 2017 15:13:52 -0700 Subject: [PATCH 01/24] [ios] Summarize places, roads after zooming with VoiceOver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After zooming, MGLMapView’s accessibility value now indicates the number of visible roads and lists out a few places visible in the current viewport, starting with the features at the highest z-index (not necessarily the largest or the closest to the center of the view). Avoid saying that no annotations are visible. --- platform/darwin/src/MGLStyle.mm | 27 ++++++++- platform/darwin/src/MGLStyle_Private.h | 9 +++ .../resources/Base.lproj/Localizable.strings | 16 +++++- .../en.lproj/Localizable.stringsdict | 38 ++++++++++--- platform/ios/src/MGLMapView.mm | 57 ++++++++++++++++--- 5 files changed, 127 insertions(+), 20 deletions(-) diff --git a/platform/darwin/src/MGLStyle.mm b/platform/darwin/src/MGLStyle.mm index 52efc7a85aa..244fb94ef9b 100644 --- a/platform/darwin/src/MGLStyle.mm +++ b/platform/darwin/src/MGLStyle.mm @@ -638,7 +638,7 @@ - (NSString *)description self.URL ? [NSString stringWithFormat:@"\"%@\"", self.URL] : self.URL]; } -#pragma mark Style language preferences +#pragma mark Mapbox Streets source introspection - (void)setLocalizesLabels:(BOOL)localizesLabels { @@ -749,4 +749,29 @@ - (void)setLocalizesLabels:(BOOL)localizesLabels } } +- (NS_SET_OF(MGLVectorSource *) *)mapboxStreetsSources { + return [self.sources objectsPassingTest:^BOOL (__kindof MGLVectorSource * _Nonnull source, BOOL * _Nonnull stop) { + return [source isKindOfClass:[MGLVectorSource class]] && source.mapboxStreets; + }]; +} + +- (NS_ARRAY_OF(MGLStyleLayer *) *)placeStyleLayers { + NSSet *streetsSourceIdentifiers = [self.mapboxStreetsSources valueForKey:@"identifier"]; + + NSSet *placeSourceLayerIdentifiers = [NSSet setWithObjects:@"marine_label", @"country_label", @"state_label", @"place_label", @"water_label", @"poi_label", @"rail_station_label", @"mountain_peak_label", nil]; + NSPredicate *isPlacePredicate = [NSPredicate predicateWithBlock:^BOOL (MGLVectorStyleLayer * _Nullable layer, NSDictionary * _Nullable bindings) { + return [layer isKindOfClass:[MGLVectorStyleLayer class]] && [streetsSourceIdentifiers containsObject:layer.sourceIdentifier] && [placeSourceLayerIdentifiers containsObject:layer.sourceLayerIdentifier]; + }]; + return [self.layers filteredArrayUsingPredicate:isPlacePredicate]; +} + +- (NS_ARRAY_OF(MGLStyleLayer *) *)roadStyleLayers { + NSSet *streetsSourceIdentifiers = [self.mapboxStreetsSources valueForKey:@"identifier"]; + + NSPredicate *isPlacePredicate = [NSPredicate predicateWithBlock:^BOOL (MGLVectorStyleLayer * _Nullable layer, NSDictionary * _Nullable bindings) { + return [layer isKindOfClass:[MGLVectorStyleLayer class]] && [streetsSourceIdentifiers containsObject:layer.sourceIdentifier] && [layer.sourceLayerIdentifier isEqualToString:@"road_label"]; + }]; + return [self.layers filteredArrayUsingPredicate:isPlacePredicate]; +} + @end diff --git a/platform/darwin/src/MGLStyle_Private.h b/platform/darwin/src/MGLStyle_Private.h index 92b08e844bf..e5bd79dc025 100644 --- a/platform/darwin/src/MGLStyle_Private.h +++ b/platform/darwin/src/MGLStyle_Private.h @@ -14,6 +14,8 @@ namespace mbgl { @class MGLAttributionInfo; @class MGLMapView; @class MGLOpenGLStyleLayer; +@class MGLVectorSource; +@class MGLVectorStyleLayer; @interface MGLStyle (Private) @@ -30,4 +32,11 @@ namespace mbgl { @end +@interface MGLStyle (MGLStreetsAdditions) + +@property (nonatomic, readonly, copy) NS_ARRAY_OF(MGLVectorStyleLayer *) *placeStyleLayers; +@property (nonatomic, readonly, copy) NS_ARRAY_OF(MGLVectorStyleLayer *) *roadStyleLayers; + +@end + NS_ASSUME_NONNULL_END diff --git a/platform/ios/resources/Base.lproj/Localizable.strings b/platform/ios/resources/Base.lproj/Localizable.strings index 3f59262d716..815f7a34980 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,8 +49,17 @@ /* 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."; diff --git a/platform/ios/resources/en.lproj/Localizable.stringsdict b/platform/ios/resources/en.lproj/Localizable.stringsdict index e849318fe55..435b7bdfe8f 100644 --- a/platform/ios/resources/en.lproj/Localizable.stringsdict +++ b/platform/ios/resources/en.lproj/Localizable.stringsdict @@ -2,22 +2,26 @@ - MAP_A11Y_VALUE + MAP_A11Y_VALUE_ANNOTATIONS NSStringLocalizedFormatKey - %#@level@ -%#@count@ - level + %#@count@ + count NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey - d + ld one - Zoom %dx + %d annotation visible other - Zoom %dx + %d annotations visible + + MAP_A11Y_VALUE_ROADS + + NSStringLocalizedFormatKey + %#@count@ count NSStringFormatSpecTypeKey @@ -25,9 +29,25 @@ NSStringFormatValueTypeKey ld one - %d annotation visible + %d road visible other - %d annotations visible + %d roads visible + + + MAP_A11Y_VALUE_ZOOM + + NSStringLocalizedFormatKey + %#@level@ + level + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Zoom %dx + other + Zoom %dx diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index ed51754b0a0..630688f05ad 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -43,6 +43,7 @@ #import "MGLFoundation_Private.h" #import "MGLRendererFrontend.h" +#import "MGLVectorSource+MGLAdditions.h" #import "NSBundle+MGLAdditions.h" #import "NSDate+MGLAdditions.h" #import "NSException+MGLAdditions.h" @@ -2355,8 +2356,53 @@ - (void)setPitchEnabled:(BOOL)pitchEnabled - (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 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 ) *)visiblePlaceFeatures +{ + NSArray *placeStyleLayerIdentifiers = [self.style.placeStyleLayers valueForKey:@"identifier"]; + return [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:placeStyleLayerIdentifiers]]; +} + +- (NS_ARRAY_OF(id ) *)visibleRoadFeatures +{ + NSArray *roadStyleLayerIdentifiers = [self.style.roadStyleLayers valueForKey:@"identifier"]; + return [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:roadStyleLayerIdentifiers]]; } - (CGRect)accessibilityFrame @@ -2390,14 +2436,9 @@ - (NSInteger)accessibilityElementCount { 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 + 2 /* compass, attributionButton */; } - (NSInteger)accessibilityAnnotationCount From 6dd13241c3d77d5fe792b382fdbc064ac83aa9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 10 Sep 2017 11:37:01 -0700 Subject: [PATCH 02/24] [ios] Allow VoiceOver to navigate among place features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- platform/ios/ios.xcodeproj/project.pbxproj | 12 + platform/ios/src/MGLMapAccessibilityElement.h | 40 +++ platform/ios/src/MGLMapAccessibilityElement.m | 90 ++++++ platform/ios/src/MGLMapView.mm | 265 +++++++++++------- 4 files changed, 301 insertions(+), 106 deletions(-) create mode 100644 platform/ios/src/MGLMapAccessibilityElement.h create mode 100644 platform/ios/src/MGLMapAccessibilityElement.m diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index d67537a3cbe..88dadc8137c 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 = ""; }; DA704CBD1F63746E004B3F28 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.stringsdict"; sourceTree = ""; }; DA704CC71F6663A3004B3F28 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Foundation.strings; sourceTree = ""; }; + DA704CC01F65A475004B3F28 /* MGLMapAccessibilityElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapAccessibilityElement.h; sourceTree = ""; }; + DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLMapAccessibilityElement.m; sourceTree = ""; }; DA7262091DEEE3480043BB89 /* MGLOpenGLStyleLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLOpenGLStyleLayer.h; sourceTree = ""; }; DA72620A1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLOpenGLStyleLayer.mm; sourceTree = ""; }; DA737ADA1E59139D00AD2CDE /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = es; path = es.lproj/Foundation.stringsdict; sourceTree = ""; }; @@ -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 00000000000..9693c570f6a --- /dev/null +++ b/platform/ios/src/MGLMapAccessibilityElement.h @@ -0,0 +1,40 @@ +#import + +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 feature; + +- (instancetype)initWithAccessibilityContainer:(id)container feature:(id )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 00000000000..4a4569bb350 --- /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)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 630688f05ad..5018a18b3a7 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 #include @@ -140,9 +141,6 @@ typedef NS_ENUM(NSUInteger, MGLUserTrackingState) { 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 @@ typedef NS_ENUM(NSUInteger, MGLUserTrackingState) { 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 @@ - (void)accessibilityDecrement 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 () 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 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 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 @@ - (id)accessibilityElementAtIndex:(NSInteger)index 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 @@ - (id)accessibilityElementAtIndex:(NSInteger)index { 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 )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 @@ - (NSInteger)indexOfAccessibilityElement:(id)element 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 visibleAnnotations = [self annotationTagsInRect:self.bounds]; - + NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size()); MGLAnnotationTag tag = MGLAnnotationTagNotFound; if ([element isKindOfClass:[MGLAnnotationView class]]) { @@ -2576,22 +2614,36 @@ - (NSInteger)indexOfAccessibilityElement:(id)element { 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 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 @@ - (void)cameraDidChangeAnimated:(BOOL)animated { { if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) { + _accessibilityFeaturesByIdentifier = nil; UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); } [self.delegate mapView:self regionDidChangeAnimated:animated]; From 21dd42a15b2907cf8ec63c180507a8563689a546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 10 Sep 2017 11:37:30 -0700 Subject: [PATCH 03/24] [ios] Post layout change notification when fully rendered Post a layout change notification when fully finishing a map render. --- platform/ios/src/MGLMapView.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 5018a18b3a7..efe538aebc3 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -5325,6 +5325,8 @@ - (void)mapViewDidFinishRenderingMapFullyRendered:(BOOL)fullyRendered { if (!_mbglMap) { return; } + + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingMap:fullyRendered:)]) { From c17011f97a9f0a0a3ca90b858a486862d21657bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 10 Sep 2017 13:07:00 -0700 Subject: [PATCH 04/24] [ios, macos] Moved MGLVectorSource+MGLAdditions to more specific group --- platform/ios/ios.xcodeproj/project.pbxproj | 4 ++-- platform/macos/macos.xcodeproj/project.pbxproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index 88dadc8137c..1f628d51ffd 100644 --- a/platform/ios/ios.xcodeproj/project.pbxproj +++ b/platform/ios/ios.xcodeproj/project.pbxproj @@ -1125,6 +1125,8 @@ 35599DB81D46AD7F0048254D /* Categories */ = { isa = PBXGroup; children = ( + 1FDD9D6D1F26936400252B09 /* MGLVectorSource+MGLAdditions.h */, + 1FDD9D6E1F26936400252B09 /* MGLVectorSource+MGLAdditions.m */, 350098DA1D484E60004B2AF0 /* NSValue+MGLStyleAttributeAdditions.h */, 350098DB1D484E60004B2AF0 /* NSValue+MGLStyleAttributeAdditions.mm */, ); @@ -1628,8 +1630,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 */, diff --git a/platform/macos/macos.xcodeproj/project.pbxproj b/platform/macos/macos.xcodeproj/project.pbxproj index 14c8545094b..c839bfadd34 100644 --- a/platform/macos/macos.xcodeproj/project.pbxproj +++ b/platform/macos/macos.xcodeproj/project.pbxproj @@ -670,6 +670,8 @@ 352742791D4C235C00A1ECE6 /* Categories */ = { isa = PBXGroup; children = ( + 1FCDF1401F2A4F3600A46694 /* MGLVectorSource+MGLAdditions.h */, + 1FCDF1411F2A4F3600A46694 /* MGLVectorSource+MGLAdditions.m */, DA8F25A61D51CB270010E6B5 /* NSValue+MGLStyleAttributeAdditions.h */, DA8F25A71D51CB270010E6B5 /* NSValue+MGLStyleAttributeAdditions.mm */, ); @@ -959,8 +961,6 @@ DAD1657F1CF4CF50001FF4B9 /* Categories */ = { isa = PBXGroup; children = ( - 1FCDF1401F2A4F3600A46694 /* MGLVectorSource+MGLAdditions.h */, - 1FCDF1411F2A4F3600A46694 /* MGLVectorSource+MGLAdditions.m */, 408AA8601DAEED3300022900 /* MGLPolygon+MGLAdditions.h */, 408AA85C1DAEED3300022900 /* MGLPolygon+MGLAdditions.m */, 408AA8611DAEED3300022900 /* MGLPolyline+MGLAdditions.h */, From 226033a825bc6b7869c45572568bbd93d9d65055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 10 Sep 2017 13:13:01 -0700 Subject: [PATCH 05/24] [ios] Localize accessibility feature names --- platform/ios/src/MGLMapAccessibilityElement.m | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/platform/ios/src/MGLMapAccessibilityElement.m b/platform/ios/src/MGLMapAccessibilityElement.m index 4a4569bb350..b0170a85060 100644 --- a/platform/ios/src/MGLMapAccessibilityElement.m +++ b/platform/ios/src/MGLMapAccessibilityElement.m @@ -1,6 +1,7 @@ #import "MGLMapAccessibilityElement.h" #import "MGLDistanceFormatter.h" #import "MGLFeature.h" +#import "MGLVectorSource+MGLAdditions.h" #import "NSBundle+MGLAdditions.h" @@ -42,8 +43,9 @@ - (instancetype)initWithAccessibilityContainer:(id)container feature:(id Date: Sun, 10 Sep 2017 13:40:13 -0700 Subject: [PATCH 06/24] [ios] Find place feature accessibility elements by identifier --- platform/ios/src/MGLMapView.mm | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index efe538aebc3..c59eff65696 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -2633,6 +2633,16 @@ - (NSInteger)indexOfAccessibilityElement:(id)element { id feature = [(MGLFeatureAccessibilityElement *)element feature]; NSUInteger featureIndex = [visiblePlaceFeatures indexOfObject:feature]; + if (featureIndex == NSNotFound) + { + featureIndex = [visiblePlaceFeatures indexOfObjectPassingTest:^BOOL (id _Nonnull visibleFeature, NSUInteger idx, BOOL * _Nonnull stop) { + return [visibleFeature.identifier isEqual:feature.identifier]; + }]; + } + if (featureIndex == NSNotFound) + { + return NSNotFound; + } return visiblePlaceFeatureRange.location + featureIndex; } From 91d563574cca1ca57f6380c0ce4830dec6f62cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 10 Sep 2017 14:21:28 -0700 Subject: [PATCH 07/24] [ios] Refactored accessibility traits Also created a new MGLPlaceFeatureAccessibilityElement class. --- platform/ios/src/MGLMapAccessibilityElement.h | 4 +++ platform/ios/src/MGLMapAccessibilityElement.m | 30 ++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/platform/ios/src/MGLMapAccessibilityElement.h b/platform/ios/src/MGLMapAccessibilityElement.h index 9693c570f6a..e59cd628fbb 100644 --- a/platform/ios/src/MGLMapAccessibilityElement.h +++ b/platform/ios/src/MGLMapAccessibilityElement.h @@ -32,6 +32,10 @@ typedef uint32_t MGLAnnotationTag; @end +/** An accessibility element representing a place feature. */ +@interface MGLPlaceFeatureAccessibilityElement : 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 b0170a85060..2641dcdfc60 100644 --- a/platform/ios/src/MGLMapAccessibilityElement.m +++ b/platform/ios/src/MGLMapAccessibilityElement.m @@ -7,11 +7,8 @@ @implementation MGLMapAccessibilityElement -- (instancetype)initWithAccessibilityContainer:(id)container { - if (self = [super initWithAccessibilityContainer:container]) { - self.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitAdjustable; - } - return self; +- (UIAccessibilityTraits)accessibilityTraits { + return super.accessibilityTraits | UIAccessibilityTraitAdjustable; } - (void)accessibilityIncrement { @@ -34,6 +31,10 @@ - (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationT return self; } +- (UIAccessibilityTraits)accessibilityTraits { + return super.accessibilityTraits | UIAccessibilityTraitButton; +} + @end @implementation MGLFeatureAccessibilityElement @@ -42,11 +43,24 @@ - (instancetype)initWithAccessibilityContainer:(id)container feature:(id)feature { + if (self = [super initWithAccessibilityContainer:container feature:feature]) { + NSDictionary *attributes = feature.attributes; NSMutableArray *facts = [NSMutableArray array]; // Announce the kind of place or POI. From 77daa8fde2333c7783182c83008410ae07d9ce75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 10 Sep 2017 14:23:05 -0700 Subject: [PATCH 08/24] [ios] Sort accessibility elements by screen distance from center Sort annotation accessibility elements by screen distance, not the hypotenuse of coordinates, which can yield incorrect results when the map is rotated or tilted or when the user is located at high latitudes. Sort place feature accessibility elements by screen distance as well. --- platform/ios/src/MGLMapView.mm | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index c59eff65696..06228025020 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -2428,24 +2428,24 @@ - (id)accessibilityElementAtIndex:(NSInteger)index return self.userLocationAnnotationView; } + CGPoint centerPoint = self.contentCenter; + if (self.userTrackingMode != MGLUserTrackingModeNone) + { + centerPoint = self.userLocationAnnotationViewCenter; + } + // Visible annotations NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size()); if (NSLocationInRange(index, visibleAnnotationRange)) { 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); + 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; }); @@ -2459,6 +2459,14 @@ - (id)accessibilityElementAtIndex:(NSInteger)index NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count); if (NSLocationInRange(index, visiblePlaceFeatureRange)) { + visiblePlaceFeatures = [visiblePlaceFeatures sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull featureA, id _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 feature = visiblePlaceFeatures[index - visiblePlaceFeatureRange.location]; return [self accessibilityElementForFeature:feature withIdentifier:feature.identifier]; } From 539ec4de6d5d522f6948b251ae51c5ab455e8307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 10 Sep 2017 14:29:20 -0700 Subject: [PATCH 09/24] [ios] Create a place feature accessibility element, not an abstract feature accessibility element --- platform/ios/src/MGLMapView.mm | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 06228025020..00c80eba220 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -2411,9 +2411,6 @@ - (id)accessibilityElementAtIndex:(NSInteger)index return nil; } - std::vector visibleAnnotations = [self annotationTagsInRect:self.bounds]; - NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures; - // Compass NSUInteger compassIndex = 0; if (index == compassIndex) @@ -2435,6 +2432,7 @@ - (id)accessibilityElementAtIndex:(NSInteger)index } // Visible annotations + std::vector visibleAnnotations = [self annotationTagsInRect:self.bounds]; NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size()); if (NSLocationInRange(index, visibleAnnotationRange)) { @@ -2456,6 +2454,7 @@ - (id)accessibilityElementAtIndex:(NSInteger)index } // Visible place features + NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures; NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count); if (NSLocationInRange(index, visiblePlaceFeatureRange)) { @@ -2566,7 +2565,7 @@ - (id)accessibilityElementForFeature:(id )feature withIdentifier:(id } if (!element) { - element = [[MGLFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; + element = [[MGLPlaceFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; } if (!feature) { From de52d40ea6fbbe99f93eb9d16cba59e5dfb6c399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 10 Sep 2017 15:18:38 -0700 Subject: [PATCH 10/24] [ios] Only query for visible place features once per camera Improved accessibility performance after changing the map camera. MGLMapView no longer queries the map for place features once per place feature. --- platform/ios/src/MGLMapView.mm | 58 +++++++++++----------------------- 1 file changed, 19 insertions(+), 39 deletions(-) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 00c80eba220..6746ddf7a0e 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -274,7 +274,8 @@ @implementation MGLMapView BOOL _delegateHasLineWidthsForShapeAnnotations; MGLCompassDirectionFormatter *_accessibilityCompassFormatter; - NS_MUTABLE_DICTIONARY_OF(id, MGLFeatureAccessibilityElement *) *_accessibilityFeaturesByIdentifier; + NS_ARRAY_OF(id ) *_visiblePlaceFeatures; + NS_MUTABLE_SET_OF(MGLFeatureAccessibilityElement *) *_featureAccessibilityElements; MGLReachability *_reachability; } @@ -2342,8 +2343,12 @@ - (NSString *)accessibilityValue - (NS_ARRAY_OF(id ) *)visiblePlaceFeatures { - NSArray *placeStyleLayerIdentifiers = [self.style.placeStyleLayers valueForKey:@"identifier"]; - return [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:placeStyleLayerIdentifiers]]; + if (!_visiblePlaceFeatures) + { + NSArray *placeStyleLayerIdentifiers = [self.style.placeStyleLayers valueForKey:@"identifier"]; + _visiblePlaceFeatures = [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:placeStyleLayerIdentifiers]]; + } + return _visiblePlaceFeatures; } - (NS_ARRAY_OF(id ) *)visibleRoadFeatures @@ -2467,7 +2472,7 @@ - (id)accessibilityElementAtIndex:(NSInteger)index }]; id feature = visiblePlaceFeatures[index - visiblePlaceFeatureRange.location]; - return [self accessibilityElementForFeature:feature withIdentifier:feature.identifier]; + return [self accessibilityElementForFeature:feature]; } // Attribution button @@ -2534,54 +2539,28 @@ - (id)accessibilityElementForAnnotationWithTag:(MGLAnnotationTag)annotationTag /** 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. + @param feature The feature represented by the accessibility element. */ -- (id)accessibilityElementForFeature:(id )feature withIdentifier:(id)identifier +- (id)accessibilityElementForFeature:(id )feature { - if (!_accessibilityFeaturesByIdentifier) + if (!_featureAccessibilityElements) { - _accessibilityFeaturesByIdentifier = [NSMutableDictionary dictionary]; + _featureAccessibilityElements = [NSMutableSet set]; } - 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; - } + MGLFeatureAccessibilityElement *element = [_featureAccessibilityElements objectsPassingTest:^BOOL(MGLFeatureAccessibilityElement * _Nonnull element, BOOL * _Nonnull stop) { + return [element.feature.identifier isEqual:feature.identifier] || [element.feature isEqual:feature]; + }].anyObject; if (!element) { element = [[MGLPlaceFeatureAccessibilityElement 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; - } + [_featureAccessibilityElements addObject:element]; return element; } @@ -5254,7 +5233,8 @@ - (void)cameraDidChangeAnimated:(BOOL)animated { { if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) { - _accessibilityFeaturesByIdentifier = nil; + _featureAccessibilityElements = nil; + _visiblePlaceFeatures = nil; UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); } [self.delegate mapView:self regionDidChangeAnimated:animated]; From 7a1fdd8fa6919d6f50e2b99448a9b5822c2c60a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 10 Sep 2017 16:38:26 -0700 Subject: [PATCH 11/24] [ios] Made roads accessible Wrap visible road features in accessibility elements described by the road name, route number, and general direction of travel. --- .../resources/Base.lproj/Localizable.strings | 6 + platform/ios/src/MGLMapAccessibilityElement.h | 4 + platform/ios/src/MGLMapAccessibilityElement.m | 74 ++++++++++ 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 815f7a34980..d214ccb4d15 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 e59cd628fbb..efe077fac9f 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 2641dcdfc60..84f44090ac0 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 @@ - (instancetype)initWithAccessibilityContainer:(id)container feature:(id)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 6746ddf7a0e..d5ea8c7f90f 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -275,6 +275,7 @@ @implementation MGLMapView MGLCompassDirectionFormatter *_accessibilityCompassFormatter; NS_ARRAY_OF(id ) *_visiblePlaceFeatures; + NS_ARRAY_OF(id ) *_visibleRoadFeatures; NS_MUTABLE_SET_OF(MGLFeatureAccessibilityElement *) *_featureAccessibilityElements; MGLReachability *_reachability; @@ -2353,8 +2354,12 @@ - (NSString *)accessibilityValue - (NS_ARRAY_OF(id ) *)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 @@ - (NSInteger)accessibilityElementCount { 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 @@ - (id)accessibilityElementAtIndex:(NSInteger)index }]; id 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 _Nonnull featureA, id _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 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 @@ - (id)accessibilityElementForAnnotationWithTag:(MGLAnnotationTag)annotationTag } /** - 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 )feature +- (id)accessibilityElementForPlaceFeature:(id )feature { if (!_featureAccessibilityElements) { @@ -2549,7 +2573,7 @@ - (id)accessibilityElementForFeature:(id )feature } 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 @@ - (id)accessibilityElementForFeature:(id )feature 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 )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 @@ - (NSInteger)indexOfAccessibilityElement:(id)element if (featureIndex == NSNotFound) { featureIndex = [visiblePlaceFeatures indexOfObjectPassingTest:^BOOL (id _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 @@ - (NSInteger)indexOfAccessibilityElement:(id)element return visiblePlaceFeatureRange.location + featureIndex; } + // Visible road features + NSArray *visibleRoadFeatures = self.visibleRoadFeatures; + NSRange visibleRoadFeatureRange = NSMakeRange(NSMaxRange(visiblePlaceFeatureRange), visibleRoadFeatures.count); + if ([element isKindOfClass:[MGLFeatureAccessibilityElement class]]) + { + id feature = [(MGLFeatureAccessibilityElement *)element feature]; + NSUInteger featureIndex = [visibleRoadFeatures indexOfObject:feature]; + if (featureIndex == NSNotFound) + { + featureIndex = [visibleRoadFeatures indexOfObjectPassingTest:^BOOL (id _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 @@ - (void)cameraDidChangeAnimated:(BOOL)animated { { _featureAccessibilityElements = nil; _visiblePlaceFeatures = nil; + _visibleRoadFeatures = nil; UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); } [self.delegate mapView:self regionDidChangeAnimated:animated]; From 788482e1159c0a7cbd194485ca423d9b7bfcc0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 10 Sep 2017 16:47:58 -0700 Subject: [PATCH 12/24] [ios] Cleaned up radian conversions --- platform/ios/src/MGLMapAccessibilityElement.m | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/platform/ios/src/MGLMapAccessibilityElement.m b/platform/ios/src/MGLMapAccessibilityElement.m index 84f44090ac0..e2200303991 100644 --- a/platform/ios/src/MGLMapAccessibilityElement.m +++ b/platform/ios/src/MGLMapAccessibilityElement.m @@ -13,16 +13,23 @@ MGLLocationRadians longitude; } MGLRadianCoordinate2D; +MGLRadianCoordinate2D MGLRadianCoordinate2DMake(MGLLocationRadians latitude, MGLLocationRadians longitude) { + MGLRadianCoordinate2D radianCoordinate = { + .latitude = latitude, + .longitude = longitude, + }; + return radianCoordinate; +} + +MGLRadianCoordinate2D MGLRadianCoordinate2DFromLocationCoordinate2D(CLLocationCoordinate2D degreeCoordinate) { + return MGLRadianCoordinate2DMake(degreeCoordinate.latitude * M_PI / 180, degreeCoordinate.longitude * M_PI / 180); +} + /** 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, - }; + // Ported from https://github.com/mapbox/turf-swift/blob/857e2e8060678ef4a7a9169d4971b0788fdffc37/Turf/Turf.swift#L23-L31 + MGLRadianCoordinate2D firstRadianCoordinate = MGLRadianCoordinate2DFromLocationCoordinate2D(firstCoordinate); + MGLRadianCoordinate2D secondRadianCoordinate = MGLRadianCoordinate2DFromLocationCoordinate2D(secondCoordinate); CGFloat a = sin(secondRadianCoordinate.longitude - firstRadianCoordinate.longitude) * cos(secondRadianCoordinate.latitude); CGFloat b = (cos(firstRadianCoordinate.latitude) * sin(secondRadianCoordinate.latitude) From ac7ce2f6afccf395dd771f76534e8d114bedcf86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 10 Sep 2017 23:20:37 -0700 Subject: [PATCH 13/24] [ios] Thickened road accessibility elements --- platform/ios/src/MGLMapView.mm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index d5ea8c7f90f..0c5ff378dd9 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -2637,7 +2637,10 @@ - (id)accessibilityElementForRoadFeature:(id )feature [path moveToPoint:point]; } } - UIBezierPath *screenPath = UIAccessibilityConvertPathToScreenCoordinates(path, self); + 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; } From 0cf4b96bab568203fee54fb08ae520d7d2ce4ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 11 Sep 2017 00:36:52 -0700 Subject: [PATCH 14/24] [ios] Made unioned roads accessible --- platform/ios/src/MGLMapView.mm | 55 +++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 0c5ff378dd9..5b6b2612ff1 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -2609,10 +2609,7 @@ - (id)accessibilityElementForRoadFeature:(id )feature element = [[MGLRoadFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; } - if ([feature isKindOfClass:[MGLShapeCollectionFeature class]]) - { - feature = [(MGLShapeCollectionFeature *)feature shapes].firstObject; - } + UIBezierPath *path; if ([feature isKindOfClass:[MGLPointFeature class]]) { CGPoint center = [self convertCoordinate:feature.coordinate toPointToView:self]; @@ -2622,21 +2619,19 @@ - (id)accessibilityElementForRoadFeature:(id )feature } 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++) + path = [self pathOfPolyline:(MGLPolyline *)feature]; + } + else if ([feature isKindOfClass:[MGLMultiPolylineFeature class]]) + { + path = [UIBezierPath bezierPath]; + for (MGLPolyline *polyline in [(MGLMultiPolylineFeature *)feature polylines]) { - CGPoint point = [self convertCoordinate:coordinates[i] toPointToView:self]; - if (i) - { - [path addLineToPoint:point]; - } - else - { - [path moveToPoint:point]; - } + [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); @@ -2649,6 +2644,26 @@ - (id)accessibilityElementForRoadFeature:(id )feature 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) @@ -2671,6 +2686,12 @@ - (NSInteger)indexOfAccessibilityElement:(id)element return userLocationAnnotationRange.location; } + CGPoint centerPoint = self.contentCenter; + if (self.userTrackingMode != MGLUserTrackingModeNone) + { + centerPoint = self.userLocationAnnotationViewCenter; + } + // Visible annotations std::vector visibleAnnotations = [self annotationTagsInRect:self.bounds]; NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size()); From 04ba3e2de2f983629e84d5b2a05c942166eefb33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 11 Sep 2017 01:32:54 -0700 Subject: [PATCH 15/24] [ios] Consistently sort accessibility elements Also fixed an issue causing road feature accessibility elements to get treated like place feature accessibility elements. --- platform/ios/src/MGLMapView.mm | 42 +++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 5b6b2612ff1..cadfd74b6ef 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -2573,7 +2573,7 @@ - (id)accessibilityElementForPlaceFeature:(id )feature } 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]; + return element.feature.identifier && ![element.feature.identifier isEqual:@0] && [element.feature.identifier isEqual:feature.identifier]; }].anyObject; if (!element) { @@ -2602,7 +2602,7 @@ - (id)accessibilityElementForRoadFeature:(id )feature } 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]; + return element.feature.identifier && ![element.feature.identifier isEqual:@0] && [element.feature.identifier isEqual:feature.identifier]; }].anyObject; if (!element) { @@ -2709,6 +2709,16 @@ - (NSInteger)indexOfAccessibilityElement:(id)element 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()) { @@ -2720,14 +2730,22 @@ - (NSInteger)indexOfAccessibilityElement:(id)element // Visible place features NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures; NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count); - if ([element isKindOfClass:[MGLFeatureAccessibilityElement class]]) + if ([element isKindOfClass:[MGLPlaceFeatureAccessibilityElement class]]) { - id feature = [(MGLFeatureAccessibilityElement *)element feature]; + visiblePlaceFeatures = [visiblePlaceFeatures sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull featureA, id _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 feature = [(MGLPlaceFeatureAccessibilityElement *)element feature]; NSUInteger featureIndex = [visiblePlaceFeatures indexOfObject:feature]; if (featureIndex == NSNotFound) { featureIndex = [visiblePlaceFeatures indexOfObjectPassingTest:^BOOL (id _Nonnull visibleFeature, NSUInteger idx, BOOL * _Nonnull stop) { - return visibleFeature.identifier && [visibleFeature.identifier isEqual:feature.identifier]; + return visibleFeature.identifier && ![visibleFeature.identifier isEqual:@0] && [visibleFeature.identifier isEqual:feature.identifier]; }]; } if (featureIndex == NSNotFound) @@ -2740,14 +2758,22 @@ - (NSInteger)indexOfAccessibilityElement:(id)element // Visible road features NSArray *visibleRoadFeatures = self.visibleRoadFeatures; NSRange visibleRoadFeatureRange = NSMakeRange(NSMaxRange(visiblePlaceFeatureRange), visibleRoadFeatures.count); - if ([element isKindOfClass:[MGLFeatureAccessibilityElement class]]) + if ([element isKindOfClass:[MGLRoadFeatureAccessibilityElement class]]) { - id feature = [(MGLFeatureAccessibilityElement *)element feature]; + visibleRoadFeatures = [visibleRoadFeatures sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull featureA, id _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 feature = [(MGLRoadFeatureAccessibilityElement *)element feature]; NSUInteger featureIndex = [visibleRoadFeatures indexOfObject:feature]; if (featureIndex == NSNotFound) { featureIndex = [visibleRoadFeatures indexOfObjectPassingTest:^BOOL (id _Nonnull visibleFeature, NSUInteger idx, BOOL * _Nonnull stop) { - return visibleFeature.identifier && [visibleFeature.identifier isEqual:feature.identifier]; + return visibleFeature.identifier && ![visibleFeature.identifier isEqual:@0] && [visibleFeature.identifier isEqual:feature.identifier]; }]; } if (featureIndex == NSNotFound) From b475f4c282a8cd97f1cf04c1d2ae5cb4e99c9dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 11 Sep 2017 01:34:08 -0700 Subject: [PATCH 16/24] [ios] Announce direction of divided roads Announce the direction of a divided road based on the direction of its first polyline. --- platform/ios/src/MGLMapAccessibilityElement.m | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/platform/ios/src/MGLMapAccessibilityElement.m b/platform/ios/src/MGLMapAccessibilityElement.m index e2200303991..aa1912c4b63 100644 --- a/platform/ios/src/MGLMapAccessibilityElement.m +++ b/platform/ios/src/MGLMapAccessibilityElement.m @@ -143,16 +143,20 @@ - (instancetype)initWithAccessibilityContainer:(id)container feature:(id Date: Mon, 11 Sep 2017 02:23:14 -0700 Subject: [PATCH 17/24] [ios] Refined announced elevation units --- platform/ios/src/MGLMapAccessibilityElement.m | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/platform/ios/src/MGLMapAccessibilityElement.m b/platform/ios/src/MGLMapAccessibilityElement.m index aa1912c4b63..a592a9bcd88 100644 --- a/platform/ios/src/MGLMapAccessibilityElement.m +++ b/platform/ios/src/MGLMapAccessibilityElement.m @@ -111,11 +111,23 @@ - (instancetype)initWithAccessibilityContainer:(id)container feature:(id Date: Mon, 11 Sep 2017 22:57:35 -0700 Subject: [PATCH 18/24] [ios] Romanize feature names --- platform/ios/src/MGLMapAccessibilityElement.m | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/platform/ios/src/MGLMapAccessibilityElement.m b/platform/ios/src/MGLMapAccessibilityElement.m index a592a9bcd88..0fca75c0a5c 100644 --- a/platform/ios/src/MGLMapAccessibilityElement.m +++ b/platform/ios/src/MGLMapAccessibilityElement.m @@ -76,9 +76,20 @@ - (instancetype)initWithAccessibilityContainer:(id)container feature:(id Date: Mon, 18 Sep 2017 09:37:35 -0700 Subject: [PATCH 19/24] [ios] Updated changelog --- platform/ios/CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index c761761f27a..d449379ea6e 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -23,10 +23,9 @@ 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)) @@ -34,6 +33,11 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT * 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)) From 862f4fac231b8ae39dd3949f08888925b380e3a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 16 Oct 2017 13:20:52 -0700 Subject: [PATCH 20/24] [ios] Delay zoom announcement A 100-millisecond delay is enough for the post-zooming announcement to reflect the new zoom level rather than the previous zoom level. --- platform/ios/src/MGLMapView.mm | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index cadfd74b6ef..c960c60c789 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -277,6 +277,7 @@ @implementation MGLMapView NS_ARRAY_OF(id ) *_visiblePlaceFeatures; NS_ARRAY_OF(id ) *_visibleRoadFeatures; NS_MUTABLE_SET_OF(MGLFeatureAccessibilityElement *) *_featureAccessibilityElements; + BOOL _accessibilityValueAnnouncementIsPending; MGLReachability *_reachability; } @@ -2821,10 +2822,11 @@ - (void)accessibilityScaleBy:(double)scaleFactor { 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 - @@ -5387,12 +5389,23 @@ - (void)cameraDidChangeAnimated:(BOOL)animated { _featureAccessibilityElements = nil; _visiblePlaceFeatures = nil; _visibleRoadFeatures = nil; - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, 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; From b8e7ef5001b4a98e87d5886ed26fea9e8214c6df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Wed, 18 Oct 2017 12:24:18 -0700 Subject: [PATCH 21/24] [ios] Consolidated geometry functions Adopted MGLGeometry_Private.h in the accessibility code, forcing a conversion to Objective-C++. Avoid inlining some of the more complex geometric functions. --- platform/darwin/src/MGLGeometry.mm | 36 +++++++++++++++++ platform/darwin/src/MGLGeometry_Private.h | 40 +++++++------------ platform/ios/ios.xcodeproj/project.pbxproj | 18 ++++----- ...lement.m => MGLMapAccessibilityElement.mm} | 33 +-------------- 4 files changed, 60 insertions(+), 67 deletions(-) rename platform/ios/src/{MGLMapAccessibilityElement.m => MGLMapAccessibilityElement.mm} (82%) diff --git a/platform/darwin/src/MGLGeometry.mm b/platform/darwin/src/MGLGeometry.mm index 43bf74c4072..715a70f0b8d 100644 --- a/platform/darwin/src/MGLGeometry.mm +++ b/platform/darwin/src/MGLGeometry.mm @@ -62,6 +62,42 @@ CLLocationDistance MGLAltitudeForZoomLevel(double zoomLevel, CGFloat pitch, CLLo return ::log2(mapPixelWidthAtZoom / mbgl::util::tileSize); } +MGLRadianDistance MGLDistanceBetweenRadianCoordinates(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to) { + double a = pow(sin((to.latitude - from.latitude) / 2), 2) + + pow(sin((to.longitude - from.longitude) / 2), 2) * cos(from.latitude) * cos(to.latitude); + + return 2 * atan2(sqrt(a), sqrt(1 - a)); +} + +MGLRadianDirection MGLRadianCoordinatesDirection(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to) { + double a = sin(to.longitude - from.longitude) * cos(to.latitude); + double b = cos(from.latitude) * sin(to.latitude) + - sin(from.latitude) * cos(to.latitude) * cos(to.longitude - from.longitude); + return atan2(a, b); +} + +MGLRadianCoordinate2D MGLRadianCoordinateAtDistanceFacingDirection(MGLRadianCoordinate2D coordinate, + MGLRadianDistance distance, + MGLRadianDirection direction) { + double otherLatitude = asin(sin(coordinate.latitude) * cos(distance) + + cos(coordinate.latitude) * sin(distance) * cos(direction)); + double otherLongitude = coordinate.longitude + atan2(sin(direction) * sin(distance) * cos(coordinate.latitude), + cos(distance) - sin(coordinate.latitude) * sin(otherLatitude)); + return MGLRadianCoordinate2DMake(otherLatitude, otherLongitude); +} + +CLLocationDirection MGLDirectionBetweenCoordinates(CLLocationCoordinate2D firstCoordinate, CLLocationCoordinate2D secondCoordinate) { + // Ported from https://github.com/mapbox/turf-swift/blob/857e2e8060678ef4a7a9169d4971b0788fdffc37/Turf/Turf.swift#L23-L31 + MGLRadianCoordinate2D firstRadianCoordinate = MGLRadianCoordinateFromLocationCoordinate(firstCoordinate); + MGLRadianCoordinate2D secondRadianCoordinate = MGLRadianCoordinateFromLocationCoordinate(secondCoordinate); + + 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; +} + CGPoint MGLPointRounded(CGPoint point) { #if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR CGFloat scaleFactor = [UIScreen instancesRespondToSelector:@selector(nativeScale)] ? [UIScreen mainScreen].nativeScale : [UIScreen mainScreen].scale; diff --git a/platform/darwin/src/MGLGeometry_Private.h b/platform/darwin/src/MGLGeometry_Private.h index 87a19989c12..8b9c6c23277 100644 --- a/platform/darwin/src/MGLGeometry_Private.h +++ b/platform/darwin/src/MGLGeometry_Private.h @@ -105,38 +105,26 @@ NS_INLINE MGLRadianCoordinate2D MGLRadianCoordinateFromLocationCoordinate(CLLoca MGLRadiansFromDegrees(locationCoordinate.longitude)); } -/* +/** Returns the distance in radians given two coordinates. */ -NS_INLINE MGLRadianDistance MGLDistanceBetweenRadianCoordinates(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to) -{ - double a = pow(sin((to.latitude - from.latitude) / 2), 2) - + pow(sin((to.longitude - from.longitude) / 2), 2) * cos(from.latitude) * cos(to.latitude); - - return 2 * atan2(sqrt(a), sqrt(1 - a)); -} +MGLRadianDistance MGLDistanceBetweenRadianCoordinates(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to); -/* +/** Returns direction in radians given two coordinates. */ -NS_INLINE MGLRadianDirection MGLRadianCoordinatesDirection(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to) { - double a = sin(to.longitude - from.longitude) * cos(to.latitude); - double b = cos(from.latitude) * sin(to.latitude) - - sin(from.latitude) * cos(to.latitude) * cos(to.longitude - from.longitude); - return atan2(a, b); -} +MGLRadianDirection MGLRadianCoordinatesDirection(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to); -/* - Returns coordinate at a given distance and direction away from coordinate. +/** + Returns a coordinate at a given distance and direction away from coordinate. */ -NS_INLINE MGLRadianCoordinate2D MGLRadianCoordinateAtDistanceFacingDirection(MGLRadianCoordinate2D coordinate, - MGLRadianDistance distance, - MGLRadianDirection direction) { - double otherLatitude = asin(sin(coordinate.latitude) * cos(distance) - + cos(coordinate.latitude) * sin(distance) * cos(direction)); - double otherLongitude = coordinate.longitude + atan2(sin(direction) * sin(distance) * cos(coordinate.latitude), - cos(distance) - sin(coordinate.latitude) * sin(otherLatitude)); - return MGLRadianCoordinate2DMake(otherLatitude, otherLongitude); -} +MGLRadianCoordinate2D MGLRadianCoordinateAtDistanceFacingDirection(MGLRadianCoordinate2D coordinate, + MGLRadianDistance distance, + MGLRadianDirection direction); + +/** + Returns the direction from one coordinate to another. + */ +CLLocationDirection MGLDirectionBetweenCoordinates(CLLocationCoordinate2D firstCoordinate, CLLocationCoordinate2D secondCoordinate); CGPoint MGLPointRounded(CGPoint point); diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index 1f628d51ffd..29d02f6a6df 100644 --- a/platform/ios/ios.xcodeproj/project.pbxproj +++ b/platform/ios/ios.xcodeproj/project.pbxproj @@ -298,8 +298,8 @@ 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 */; }; + 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 */; }; @@ -824,9 +824,9 @@ DA704CBB1F637311004B3F28 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Foundation.strings; sourceTree = ""; }; DA704CBC1F637405004B3F28 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; DA704CBD1F63746E004B3F28 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.stringsdict"; sourceTree = ""; }; - DA704CC71F6663A3004B3F28 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Foundation.strings; sourceTree = ""; }; DA704CC01F65A475004B3F28 /* MGLMapAccessibilityElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapAccessibilityElement.h; sourceTree = ""; }; - DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLMapAccessibilityElement.m; sourceTree = ""; }; + DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLMapAccessibilityElement.mm; sourceTree = ""; }; + DA704CC71F6663A3004B3F28 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Foundation.strings; sourceTree = ""; }; DA7262091DEEE3480043BB89 /* MGLOpenGLStyleLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLOpenGLStyleLayer.h; sourceTree = ""; }; DA72620A1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLOpenGLStyleLayer.mm; sourceTree = ""; }; DA737ADA1E59139D00AD2CDE /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = es; path = es.lproj/Foundation.stringsdict; sourceTree = ""; }; @@ -1432,7 +1432,7 @@ DAD165841CF4D06B001FF4B9 /* Annotations */, DAD165851CF4D08B001FF4B9 /* Telemetry */, DA704CC01F65A475004B3F28 /* MGLMapAccessibilityElement.h */, - DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.m */, + DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.mm */, DA8848361CBAFB8500AB86E3 /* MGLMapView.h */, DA17BE2F1CC4BAC300402C41 /* MGLMapView_Private.h */, DA8848371CBAFB8500AB86E3 /* MGLMapView+IBAdditions.h */, @@ -2374,7 +2374,7 @@ DA88482A1CBAFA6200AB86E3 /* MGLTilePyramidOfflineRegion.mm in Sources */, 4049C29F1DB6CD6C00B3F799 /* MGLPointCollection.mm in Sources */, 35136D3F1D42273000C20EFD /* MGLLineStyleLayer.mm in Sources */, - DA704CC41F65A475004B3F28 /* MGLMapAccessibilityElement.m in Sources */, + DA704CC41F65A475004B3F28 /* MGLMapAccessibilityElement.mm in Sources */, DA72620D1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm in Sources */, DA88481A1CBAFA6200AB86E3 /* MGLAccountManager.m in Sources */, 3510FFFB1D6DCC4700F413B2 /* NSCompoundPredicate+MGLAdditions.mm in Sources */, @@ -2462,7 +2462,7 @@ DAA4E4211CBB730400178DFB /* MGLOfflineStorage.mm in Sources */, 4049C2A01DB6CD6C00B3F799 /* MGLPointCollection.mm in Sources */, 35136D401D42273000C20EFD /* MGLLineStyleLayer.mm in Sources */, - DA704CC51F65A475004B3F28 /* MGLMapAccessibilityElement.m in Sources */, + DA704CC51F65A475004B3F28 /* MGLMapAccessibilityElement.mm in Sources */, DA72620E1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm in Sources */, DAA4E42F1CBB730400178DFB /* MGLCompactCalloutView.m in Sources */, 3510FFFC1D6DCC4700F413B2 /* NSCompoundPredicate+MGLAdditions.mm in Sources */, @@ -2771,7 +2771,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/app/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -2785,7 +2785,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/app/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; diff --git a/platform/ios/src/MGLMapAccessibilityElement.m b/platform/ios/src/MGLMapAccessibilityElement.mm similarity index 82% rename from platform/ios/src/MGLMapAccessibilityElement.m rename to platform/ios/src/MGLMapAccessibilityElement.mm index 0fca75c0a5c..2baf4aea7c1 100644 --- a/platform/ios/src/MGLMapAccessibilityElement.m +++ b/platform/ios/src/MGLMapAccessibilityElement.mm @@ -5,38 +5,7 @@ #import "MGLVectorSource+MGLAdditions.h" #import "NSBundle+MGLAdditions.h" - -typedef CLLocationDegrees MGLLocationRadians; -typedef CLLocationDirection MGLRadianDirection; -typedef struct { - MGLLocationRadians latitude; - MGLLocationRadians longitude; -} MGLRadianCoordinate2D; - -MGLRadianCoordinate2D MGLRadianCoordinate2DMake(MGLLocationRadians latitude, MGLLocationRadians longitude) { - MGLRadianCoordinate2D radianCoordinate = { - .latitude = latitude, - .longitude = longitude, - }; - return radianCoordinate; -} - -MGLRadianCoordinate2D MGLRadianCoordinate2DFromLocationCoordinate2D(CLLocationCoordinate2D degreeCoordinate) { - return MGLRadianCoordinate2DMake(degreeCoordinate.latitude * M_PI / 180, degreeCoordinate.longitude * M_PI / 180); -} - -/** Returns the direction from one coordinate to another. */ -CLLocationDirection MGLDirectionBetweenCoordinates(CLLocationCoordinate2D firstCoordinate, CLLocationCoordinate2D secondCoordinate) { - // Ported from https://github.com/mapbox/turf-swift/blob/857e2e8060678ef4a7a9169d4971b0788fdffc37/Turf/Turf.swift#L23-L31 - MGLRadianCoordinate2D firstRadianCoordinate = MGLRadianCoordinate2DFromLocationCoordinate2D(firstCoordinate); - MGLRadianCoordinate2D secondRadianCoordinate = MGLRadianCoordinate2DFromLocationCoordinate2D(secondCoordinate); - - 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; -} +#import "MGLGeometry_Private.h" @implementation MGLMapAccessibilityElement From b585ccf86c8017679ff152e41ff1327851b026a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Thu, 2 Nov 2017 17:54:51 -0700 Subject: [PATCH 22/24] [ios] Fixed feature name romanization in accessibility labels NSLocale.scriptCode is only set when the locale identifier explicitly specifies a script. Use NSOrthography to identify the dominant orthography regardless of locale. Also added a unit test of feature accessibility element labels. --- platform/ios/ios.xcodeproj/project.pbxproj | 8 +++-- platform/ios/src/MGLMapAccessibilityElement.h | 8 ++++- .../ios/src/MGLMapAccessibilityElement.mm | 12 +++++-- .../test/MGLMapAccessibilityElementTests.m | 31 +++++++++++++++++++ 4 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 platform/ios/test/MGLMapAccessibilityElementTests.m diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index 29d02f6a6df..ad17e006738 100644 --- a/platform/ios/ios.xcodeproj/project.pbxproj +++ b/platform/ios/ios.xcodeproj/project.pbxproj @@ -292,6 +292,7 @@ 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 */; }; @@ -806,6 +807,7 @@ DA57D4AC1EBA922A00793288 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = vi; path = vi.lproj/Localizable.stringsdict; sourceTree = ""; }; DA5C09BA1EFC48550056B178 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; DA5C09BB1EFC486C0056B178 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + DA5DB1291FABF1EE001C2326 /* MGLMapAccessibilityElementTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MGLMapAccessibilityElementTests.m; sourceTree = ""; }; DA6023F11E4CE94300DBFF23 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Foundation.strings; sourceTree = ""; }; DA6023F21E4CE94800DBFF23 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sv; path = sv.lproj/Foundation.stringsdict; sourceTree = ""; }; DA618B111E68823600CB7F44 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1354,6 +1356,7 @@ 3598544C1E1D38AA00B29F84 /* MGLDistanceFormatterTests.m */, DA0CD58F1CF56F6A00A5F5A5 /* MGLFeatureTests.mm */, DA2E885C1CC0382C00F24E7B /* MGLGeometryTests.mm */, + DA5DB1291FABF1EE001C2326 /* MGLMapAccessibilityElementTests.m */, 35E208A61D24210F00EC9A46 /* MGLNSDataAdditionsTests.m */, 1F95931C1E6DE2E900D5B294 /* MGLNSDateAdditionsTests.mm */, DAE7DEC11E245455007505A6 /* MGLNSStringAdditionsTests.m */, @@ -2286,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 */, @@ -2771,7 +2775,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/app/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -2785,7 +2789,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/app/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; diff --git a/platform/ios/src/MGLMapAccessibilityElement.h b/platform/ios/src/MGLMapAccessibilityElement.h index efe077fac9f..952f6cbf2fb 100644 --- a/platform/ios/src/MGLMapAccessibilityElement.h +++ b/platform/ios/src/MGLMapAccessibilityElement.h @@ -1,5 +1,7 @@ #import +#import "MGLFoundation.h" + NS_ASSUME_NONNULL_BEGIN @protocol MGLFeature; @@ -8,6 +10,7 @@ NS_ASSUME_NONNULL_BEGIN typedef uint32_t MGLAnnotationTag; /** An accessibility element representing something that appears on the map. */ +MGL_EXPORT @interface MGLMapAccessibilityElement : UIAccessibilityElement @end @@ -23,6 +26,7 @@ typedef uint32_t MGLAnnotationTag; @end /** An accessibility element representing a map feature. */ +MGL_EXPORT @interface MGLFeatureAccessibilityElement : MGLMapAccessibilityElement /** The feature represented by this element. */ @@ -33,16 +37,18 @@ typedef uint32_t MGLAnnotationTag; @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 index 2baf4aea7c1..e36f35a60bf 100644 --- a/platform/ios/src/MGLMapAccessibilityElement.mm +++ b/platform/ios/src/MGLMapAccessibilityElement.mm @@ -53,8 +53,16 @@ - (instancetype)initWithAccessibilityContainer:(id)container feature:(id= 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]; } diff --git a/platform/ios/test/MGLMapAccessibilityElementTests.m b/platform/ios/test/MGLMapAccessibilityElementTests.m new file mode 100644 index 00000000000..67bbb087107 --- /dev/null +++ b/platform/ios/test/MGLMapAccessibilityElementTests.m @@ -0,0 +1,31 @@ +#import +#import + +#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."); +} + +@end From 096d48661f844ae94056cf41687f2e810be7a5e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Thu, 2 Nov 2017 18:15:31 -0700 Subject: [PATCH 23/24] [ios] Added tests for place, road accessibility values --- .../test/MGLMapAccessibilityElementTests.m | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/platform/ios/test/MGLMapAccessibilityElementTests.m b/platform/ios/test/MGLMapAccessibilityElementTests.m index 67bbb087107..02656e54521 100644 --- a/platform/ios/test/MGLMapAccessibilityElementTests.m +++ b/platform/ios/test/MGLMapAccessibilityElementTests.m @@ -28,4 +28,55 @@ - (void)testFeatureLabels { 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", + }; + MGLRoadFeatureAccessibilityElement *element = [[MGLRoadFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:roadFeature]; + XCTAssertEqualObjects(element.accessibilityValue, @"Route 42, southwest to northeast"); + + CLLocationCoordinate2D opposingCoordinates[] = { + CLLocationCoordinate2DMake(1, 0), + CLLocationCoordinate2DMake(2, 1), + }; + MGLPolylineFeature *opposingRoadFeature = [MGLPolylineFeature polylineWithCoordinates:opposingCoordinates count:sizeof(opposingCoordinates) / sizeof(opposingCoordinates[0])]; + 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 From 08ff14292a4f8dac4db86cd7f37faee07fbe618c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Thu, 2 Nov 2017 18:19:49 -0700 Subject: [PATCH 24/24] [ios] Announce one-way roads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A road feature’s accessibility value now indicates whether the road is a one-way road. --- platform/ios/resources/Base.lproj/Localizable.strings | 3 +++ platform/ios/src/MGLMapAccessibilityElement.mm | 5 +++++ platform/ios/test/MGLMapAccessibilityElementTests.m | 9 +++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/platform/ios/resources/Base.lproj/Localizable.strings b/platform/ios/resources/Base.lproj/Localizable.strings index d214ccb4d15..039ef4c4b19 100644 --- a/platform/ios/resources/Base.lproj/Localizable.strings +++ b/platform/ios/resources/Base.lproj/Localizable.strings @@ -67,6 +67,9 @@ /* 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 %@"; diff --git a/platform/ios/src/MGLMapAccessibilityElement.mm b/platform/ios/src/MGLMapAccessibilityElement.mm index e36f35a60bf..4e5f165fbf5 100644 --- a/platform/ios/src/MGLMapAccessibilityElement.mm +++ b/platform/ios/src/MGLMapAccessibilityElement.mm @@ -142,6 +142,11 @@ - (instancetype)initWithAccessibilityContainer:(id)container feature:(id