diff options
author | Tobrun <tobrun.van.nuland@gmail.com> | 2017-11-10 06:16:35 +0100 |
---|---|---|
committer | Tobrun <tobrun.van.nuland@gmail.com> | 2017-11-10 06:16:35 +0100 |
commit | 225fba8a0aad5ea52ff878443f58d65bb2221d74 (patch) | |
tree | 8fef9e5353ec33f2fc0cec378a77d7146f9c67d0 /platform/ios/src | |
parent | 6b5afaab7611cc59c86f749fd9130edb512b4945 (diff) | |
parent | 91dabd01dfcd52dc40aa1c1d1db9d3f48f7abc97 (diff) | |
download | qtlocation-mapboxgl-225fba8a0aad5ea52ff878443f58d65bb2221d74.tar.gz |
Merge branch 'release-agua' into tvn-merge-release
Diffstat (limited to 'platform/ios/src')
-rw-r--r-- | platform/ios/src/MGLCompactCalloutView.h | 2 | ||||
-rw-r--r-- | platform/ios/src/MGLFaux3DUserLocationAnnotationView.m | 18 | ||||
-rw-r--r-- | platform/ios/src/MGLMapAccessibilityElement.h | 54 | ||||
-rw-r--r-- | platform/ios/src/MGLMapAccessibilityElement.mm | 199 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 650 | ||||
-rw-r--r-- | platform/ios/src/MGLScaleBar.mm | 17 | ||||
-rw-r--r-- | platform/ios/src/MGLUserLocation.h | 3 |
7 files changed, 748 insertions, 195 deletions
diff --git a/platform/ios/src/MGLCompactCalloutView.h b/platform/ios/src/MGLCompactCalloutView.h index 56c48a99e5..5cecf37ff6 100644 --- a/platform/ios/src/MGLCompactCalloutView.h +++ b/platform/ios/src/MGLCompactCalloutView.h @@ -7,7 +7,7 @@ callout view displays the represented annotation’s title, subtitle, and accessory views in a compact, two-line layout. */ -@interface MGLCompactCalloutView : SMCalloutView <MGLCalloutView> +@interface MGLCompactCalloutView : MGLSMCalloutView <MGLCalloutView> + (instancetype)platformCalloutView; diff --git a/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m index 21f6aaa540..1ed3d86ad1 100644 --- a/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m +++ b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m @@ -10,7 +10,7 @@ const CGFloat MGLUserLocationAnnotationDotSize = 22.0; const CGFloat MGLUserLocationAnnotationHaloSize = 115.0; const CGFloat MGLUserLocationAnnotationPuckSize = 45.0; -const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuckSize * 0.6; +const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuckSize * 0.5; const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01; @@ -175,12 +175,16 @@ const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01; _puckArrow = [CAShapeLayer layer]; _puckArrow.path = [[self puckArrow] CGPath]; _puckArrow.fillColor = [self.mapView.tintColor CGColor]; - _puckArrow.bounds = CGRectMake(0, 0, MGLUserLocationAnnotationArrowSize, MGLUserLocationAnnotationArrowSize); - _puckArrow.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0); + _puckArrow.bounds = CGRectMake(0, 0, round(MGLUserLocationAnnotationArrowSize), round(MGLUserLocationAnnotationArrowSize)); + _puckArrow.position = CGPointMake(CGRectGetMidX(super.bounds), CGRectGetMidY(super.bounds)); _puckArrow.shouldRasterize = YES; _puckArrow.rasterizationScale = [UIScreen mainScreen].scale; _puckArrow.drawsAsynchronously = YES; + _puckArrow.lineJoin = @"round"; + _puckArrow.lineWidth = 1.f; + _puckArrow.strokeColor = _puckArrow.fillColor; + [self.layer addSublayer:_puckArrow]; } if (self.userLocation.location.course >= 0) @@ -302,7 +306,7 @@ const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01; [CATransaction setDisableActions:shouldDisableActions]; _accuracyRingLayer.bounds = CGRectMake(0, 0, accuracyRingSize, accuracyRingSize); - _accuracyRingLayer.cornerRadius = accuracyRingSize / 2; + _accuracyRingLayer.cornerRadius = accuracyRingSize / 2.0; // match the halo to the accuracy ring _haloLayer.bounds = _accuracyRingLayer.bounds; @@ -431,9 +435,11 @@ const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01; - (CALayer *)circleLayerWithSize:(CGFloat)layerSize { + layerSize = round(layerSize); + CALayer *circleLayer = [CALayer layer]; circleLayer.bounds = CGRectMake(0, 0, layerSize, layerSize); - circleLayer.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0); + circleLayer.position = CGPointMake(CGRectGetMidX(super.bounds), CGRectGetMidY(super.bounds)); circleLayer.cornerRadius = layerSize / 2.0; circleLayer.shouldRasterize = YES; circleLayer.rasterizationScale = [UIScreen mainScreen].scale; @@ -456,7 +462,7 @@ const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01; - (CGFloat)calculateAccuracyRingSize { // diameter in screen points - return self.userLocation.location.horizontalAccuracy / [self.mapView metersPerPointAtLatitude:self.userLocation.coordinate.latitude] * 2.0; + return round(self.userLocation.location.horizontalAccuracy / [self.mapView metersPerPointAtLatitude:self.userLocation.coordinate.latitude] * 2.0); } @end diff --git a/platform/ios/src/MGLMapAccessibilityElement.h b/platform/ios/src/MGLMapAccessibilityElement.h new file mode 100644 index 0000000000..952f6cbf2f --- /dev/null +++ b/platform/ios/src/MGLMapAccessibilityElement.h @@ -0,0 +1,54 @@ +#import <UIKit/UIKit.h> + +#import "MGLFoundation.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol MGLFeature; + +/// Unique identifier representing a single annotation in mbgl. +typedef uint32_t MGLAnnotationTag; + +/** An accessibility element representing something that appears on the map. */ +MGL_EXPORT +@interface MGLMapAccessibilityElement : UIAccessibilityElement + +@end + +/** An accessibility element representing a map annotation. */ +@interface MGLAnnotationAccessibilityElement : MGLMapAccessibilityElement + +/** The tag of the annotation represented by this element. */ +@property (nonatomic) MGLAnnotationTag tag; + +- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)identifier NS_DESIGNATED_INITIALIZER; + +@end + +/** An accessibility element representing a map feature. */ +MGL_EXPORT +@interface MGLFeatureAccessibilityElement : MGLMapAccessibilityElement + +/** The feature represented by this element. */ +@property (nonatomic, strong) id <MGLFeature> feature; + +- (instancetype)initWithAccessibilityContainer:(id)container feature:(id <MGLFeature>)feature NS_DESIGNATED_INITIALIZER; + +@end + +/** An accessibility element representing a place feature. */ +MGL_EXPORT +@interface MGLPlaceFeatureAccessibilityElement : MGLFeatureAccessibilityElement +@end + +/** An accessibility element representing a road feature. */ +MGL_EXPORT +@interface MGLRoadFeatureAccessibilityElement : MGLFeatureAccessibilityElement +@end + +/** An accessibility element representing the MGLMapView at large. */ +MGL_EXPORT +@interface MGLMapViewProxyAccessibilityElement : UIAccessibilityElement +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/ios/src/MGLMapAccessibilityElement.mm b/platform/ios/src/MGLMapAccessibilityElement.mm new file mode 100644 index 0000000000..4e5f165fbf --- /dev/null +++ b/platform/ios/src/MGLMapAccessibilityElement.mm @@ -0,0 +1,199 @@ +#import "MGLMapAccessibilityElement.h" +#import "MGLDistanceFormatter.h" +#import "MGLCompassDirectionFormatter.h" +#import "MGLFeature.h" +#import "MGLVectorSource+MGLAdditions.h" + +#import "NSBundle+MGLAdditions.h" +#import "MGLGeometry_Private.h" + +@implementation MGLMapAccessibilityElement + +- (UIAccessibilityTraits)accessibilityTraits { + return super.accessibilityTraits | UIAccessibilityTraitAdjustable; +} + +- (void)accessibilityIncrement { + [self.accessibilityContainer accessibilityIncrement]; +} + +- (void)accessibilityDecrement { + [self.accessibilityContainer accessibilityDecrement]; +} + +@end + +@implementation MGLAnnotationAccessibilityElement + +- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)tag { + if (self = [super initWithAccessibilityContainer:container]) { + _tag = tag; + self.accessibilityHint = NSLocalizedStringWithDefaultValue(@"ANNOTATION_A11Y_HINT", nil, nil, @"Shows more info", @"Accessibility hint"); + } + return self; +} + +- (UIAccessibilityTraits)accessibilityTraits { + return super.accessibilityTraits | UIAccessibilityTraitButton; +} + +@end + +@implementation MGLFeatureAccessibilityElement + +- (instancetype)initWithAccessibilityContainer:(id)container feature:(id<MGLFeature>)feature { + if (self = [super initWithAccessibilityContainer:container]) { + _feature = feature; + + NSString *languageCode = [MGLVectorSource preferredMapboxStreetsLanguage]; + NSString *nameAttribute = [NSString stringWithFormat:@"name_%@", languageCode]; + NSString *name = [feature attributeForKey:nameAttribute]; + + // If a feature hasn’t been translated into the preferred language, it + // may be in the local language, which may be written in another script. + // Romanize it. + NSLocale *locale = [NSLocale localeWithLocaleIdentifier:languageCode]; + NSOrthography *orthography; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + if ([NSOrthography respondsToSelector:@selector(defaultOrthographyForLanguage:)]) { + orthography = [NSOrthography defaultOrthographyForLanguage:locale.localeIdentifier]; + } +#pragma clang diagnostic pop +#endif + if ([orthography.dominantScript isEqualToString:@"Latn"]) { + name = [name stringByApplyingTransform:NSStringTransformToLatin reverse:NO]; + } + + self.accessibilityLabel = name; + } + return self; +} + +- (UIAccessibilityTraits)accessibilityTraits { + return super.accessibilityTraits | UIAccessibilityTraitStaticText; +} + +@end + +@implementation MGLPlaceFeatureAccessibilityElement + +- (instancetype)initWithAccessibilityContainer:(id)container feature:(id<MGLFeature>)feature { + if (self = [super initWithAccessibilityContainer:container feature:feature]) { + NSDictionary *attributes = feature.attributes; + NSMutableArray *facts = [NSMutableArray array]; + + // Announce the kind of place or POI. + if (attributes[@"type"]) { + // FIXME: Unfortunately, these types aren’t a closed set that can be + // localized, since they’re based on OpenStreetMap tags. + NSString *type = [attributes[@"type"] stringByReplacingOccurrencesOfString:@"_" + withString:@" "]; + [facts addObject:type]; + } + // Announce the kind of airport, rail station, or mountain based on its + // Maki image name. + else if (attributes[@"maki"]) { + // TODO: Localize Maki image names. + [facts addObject:attributes[@"maki"]]; + } + + // Announce the peak’s elevation in the preferred units. + if (attributes[@"elevation_m"] ?: attributes[@"elevation_ft"]) { + NSLengthFormatter *formatter = [[NSLengthFormatter alloc] init]; + formatter.unitStyle = NSFormattingUnitStyleLong; + + NSNumber *elevationValue; + NSLengthFormatterUnit unit; + BOOL usesMetricSystem = ![[formatter.numberFormatter.locale objectForKey:NSLocaleMeasurementSystem] + isEqualToString:@"U.S."]; + if (usesMetricSystem) { + elevationValue = attributes[@"elevation_m"]; + unit = NSLengthFormatterUnitMeter; + } else { + elevationValue = attributes[@"elevation_ft"]; + unit = NSLengthFormatterUnitFoot; + } + [facts addObject:[formatter stringFromValue:elevationValue.doubleValue unit:unit]]; + } + + if (facts.count) { + NSString *separator = NSLocalizedStringWithDefaultValue(@"LIST_SEPARATOR", nil, nil, @", ", @"List separator"); + self.accessibilityValue = [facts componentsJoinedByString:separator]; + } + } + return self; +} + +@end + +@implementation MGLRoadFeatureAccessibilityElement + +- (instancetype)initWithAccessibilityContainer:(id)container feature:(id<MGLFeature>)feature { + if (self = [super initWithAccessibilityContainer:container feature:feature]) { + NSDictionary *attributes = feature.attributes; + NSMutableArray *facts = [NSMutableArray array]; + + // Announce the route number. + if (attributes[@"ref"]) { + // TODO: Decorate the route number with the network name based on the shield attribute. + NSString *ref = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"ROAD_REF_A11Y_FMT", nil, nil, @"Route %@", @"String format for accessibility value for road feature; {route number}"), attributes[@"ref"]]; + [facts addObject:ref]; + } + + // Announce whether the road is a one-way road. + if ([attributes[@"oneway"] isEqualToString:@"true"]) { + [facts addObject:NSLocalizedStringWithDefaultValue(@"ROAD_ONEWAY_A11Y_VALUE", nil, nil, @"One way", @"Accessibility value indicating that a road is a one-way road")]; + } + + // Announce whether the road is a divided road. + MGLPolyline *polyline; + if ([feature isKindOfClass:[MGLMultiPolylineFeature class]]) { + [facts addObject:NSLocalizedStringWithDefaultValue(@"ROAD_DIVIDED_A11Y_VALUE", nil, nil, @"Divided road", @"Accessibility value indicating that a road is a divided road (dual carriageway)")]; + polyline = [(MGLMultiPolylineFeature *)feature polylines].firstObject; + } + + // Announce the road’s general direction. + if ([feature isKindOfClass:[MGLPolylineFeature class]]) { + polyline = (MGLPolylineFeature *)feature; + } + if (polyline) { + NSUInteger pointCount = polyline.pointCount; + if (pointCount) { + CLLocationCoordinate2D *coordinates = polyline.coordinates; + CLLocationDirection startDirection = MGLDirectionBetweenCoordinates(coordinates[pointCount - 1], coordinates[0]); + CLLocationDirection endDirection = MGLDirectionBetweenCoordinates(coordinates[0], coordinates[pointCount - 1]); + + MGLCompassDirectionFormatter *formatter = [[MGLCompassDirectionFormatter alloc] init]; + formatter.unitStyle = NSFormattingUnitStyleLong; + + NSString *startDirectionString = [formatter stringFromDirection:startDirection]; + NSString *endDirectionString = [formatter stringFromDirection:endDirection]; + NSString *directionString = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"ROAD_DIRECTION_A11Y_FMT", nil, nil, @"%@ to %@", @"String format for accessibility value for road feature; {starting compass direction}, {ending compass direction}"), startDirectionString, endDirectionString]; + [facts addObject:directionString]; + } + } + + if (facts.count) { + NSString *separator = NSLocalizedStringWithDefaultValue(@"LIST_SEPARATOR", nil, nil, @", ", @"List separator"); + self.accessibilityValue = [facts componentsJoinedByString:separator]; + } + } + return self; +} + +@end + +@implementation MGLMapViewProxyAccessibilityElement + +- (instancetype)initWithAccessibilityContainer:(id)container { + if (self = [super initWithAccessibilityContainer:container]) { + self.accessibilityTraits = UIAccessibilityTraitButton; + self.accessibilityLabel = [self.accessibilityContainer accessibilityLabel]; + self.accessibilityHint = NSLocalizedStringWithDefaultValue(@"CLOSE_CALLOUT_A11Y_HINT", nil, nil, @"Returns to the map", @"Accessibility hint for closing the selected annotation’s callout view and returning to the map"); + } + return self; +} + +@end diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index d5bfdbbc9f..ccab1fad36 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -44,6 +44,7 @@ #import "MGLFoundation_Private.h" #import "MGLRendererFrontend.h" +#import "MGLVectorSource+MGLAdditions.h" #import "NSBundle+MGLAdditions.h" #import "NSDate+MGLAdditions.h" #import "NSException+MGLAdditions.h" @@ -69,6 +70,7 @@ #import "MGLAnnotationContainerView.h" #import "MGLAnnotationContainerView_Private.h" #import "MGLAttributionInfo_Private.h" +#import "MGLMapAccessibilityElement.h" #include <algorithm> #include <cstdlib> @@ -140,9 +142,6 @@ const CGFloat MGLAnnotationImagePaddingForCallout = 1; const CGSize MGLAnnotationAccessibilityElementMinimumSize = CGSizeMake(10, 10); -/// Unique identifier representing a single annotation in mbgl. -typedef uint32_t MGLAnnotationTag; - /// An indication that the requested annotation was not found or is nonexistent. enum { MGLAnnotationTagNotFound = UINT32_MAX }; @@ -165,38 +164,6 @@ mbgl::util::UnitBezier MGLUnitBezierForMediaTimingFunction(CAMediaTimingFunction return { p1[0], p1[1], p2[0], p2[1] }; } -@interface MGLAnnotationAccessibilityElement : UIAccessibilityElement - -@property (nonatomic) MGLAnnotationTag tag; - -- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)identifier NS_DESIGNATED_INITIALIZER; - -@end - -@implementation MGLAnnotationAccessibilityElement - -- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)tag -{ - if (self = [super initWithAccessibilityContainer:container]) - { - _tag = tag; - self.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitAdjustable; - } - return self; -} - -- (void)accessibilityIncrement -{ - [self.accessibilityContainer accessibilityIncrement]; -} - -- (void)accessibilityDecrement -{ - [self.accessibilityContainer accessibilityDecrement]; -} - -@end - /// Lightweight container for metadata about an annotation, including the annotation itself. class MGLAnnotationContext { public: @@ -208,32 +175,12 @@ public: NSString *viewReuseIdentifier; }; -/** An accessibility element representing the MGLMapView at large. */ -@interface MGLMapViewProxyAccessibilityElement : UIAccessibilityElement - -@end - -@implementation MGLMapViewProxyAccessibilityElement - -- (instancetype)initWithAccessibilityContainer:(id)container -{ - if (self = [super initWithAccessibilityContainer:container]) - { - self.accessibilityTraits = UIAccessibilityTraitButton; - self.accessibilityLabel = [self.accessibilityContainer accessibilityLabel]; - self.accessibilityHint = NSLocalizedStringWithDefaultValue(@"CLOSE_CALLOUT_A11Y_HINT", nil, nil, @"Returns to the map", @"Accessibility hint for closing the selected annotation’s callout view and returning to the map"); - } - return self; -} - -@end - #pragma mark - Private - @interface MGLMapView () <UIGestureRecognizerDelegate, GLKViewDelegate, CLLocationManagerDelegate, - SMCalloutViewDelegate, + MGLSMCalloutViewDelegate, MGLCalloutViewDelegate, MGLMultiPointDelegate, MGLAnnotationImageDelegate> @@ -328,6 +275,10 @@ public: BOOL _delegateHasLineWidthsForShapeAnnotations; MGLCompassDirectionFormatter *_accessibilityCompassFormatter; + NS_ARRAY_OF(id <MGLFeature>) *_visiblePlaceFeatures; + NS_ARRAY_OF(id <MGLFeature>) *_visibleRoadFeatures; + NS_MUTABLE_SET_OF(MGLFeatureAccessibilityElement *) *_featureAccessibilityElements; + BOOL _accessibilityValueAnnouncementIsPending; MGLReachability *_reachability; } @@ -437,10 +388,9 @@ public: self.accessibilityTraits = UIAccessibilityTraitAllowsDirectInteraction | UIAccessibilityTraitAdjustable; _accessibilityCompassFormatter = [[MGLCompassDirectionFormatter alloc] init]; _accessibilityCompassFormatter.unitStyle = NSFormattingUnitStyleLong; - self.backgroundColor = [UIColor clearColor]; self.clipsToBounds = YES; - + if (@available(iOS 11.0, *)) { self.accessibilityIgnoresInvertColors = YES; } // setup mbgl view _mbglView = new MBGLView(self); @@ -491,9 +441,6 @@ public: _logoView.accessibilityTraits = UIAccessibilityTraitStaticText; _logoView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"LOGO_A11Y_LABEL", nil, nil, @"Mapbox", @"Accessibility label"); _logoView.translatesAutoresizingMaskIntoConstraints = NO; -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 - if ([_logoView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _logoView.accessibilityIgnoresInvertColors = YES; } -#endif [self addSubview:_logoView]; _logoViewConstraints = [NSMutableArray array]; @@ -502,9 +449,6 @@ public: _attributionButton = [UIButton buttonWithType:UIButtonTypeInfoLight]; _attributionButton.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"INFO_A11Y_LABEL", nil, nil, @"About this map", @"Accessibility label"); _attributionButton.accessibilityHint = NSLocalizedStringWithDefaultValue(@"INFO_A11Y_HINT", nil, nil, @"Shows credits, a feedback form, and more", @"Accessibility hint"); -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 - if ([_attributionButton respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _attributionButton.accessibilityIgnoresInvertColors = YES; } -#endif [_attributionButton addTarget:self action:@selector(showAttribution:) forControlEvents:UIControlEventTouchUpInside]; _attributionButton.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_attributionButton]; @@ -521,9 +465,6 @@ public: _compassView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_LABEL", nil, nil, @"Compass", @"Accessibility label"); _compassView.accessibilityHint = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_HINT", nil, nil, @"Rotates the map to face due north", @"Accessibility hint"); _compassView.translatesAutoresizingMaskIntoConstraints = NO; -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 - if ([_compassView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _compassView.accessibilityIgnoresInvertColors = YES; } -#endif [self addSubview:_compassView]; _compassViewConstraints = [NSMutableArray array]; @@ -531,9 +472,6 @@ public: // _scaleBar = [[MGLScaleBar alloc] init]; _scaleBar.translatesAutoresizingMaskIntoConstraints = NO; -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 - if ([_scaleBar respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _scaleBar.accessibilityIgnoresInvertColors = YES; } -#endif [self addSubview:_scaleBar]; _scaleBarConstraints = [NSMutableArray array]; @@ -648,9 +586,7 @@ public: _glView.contentScaleFactor = [UIScreen instancesRespondToSelector:@selector(nativeScale)] ? [[UIScreen mainScreen] nativeScale] : [[UIScreen mainScreen] scale]; _glView.layer.opaque = _opaque; _glView.delegate = self; -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 - if ([_glView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _glView.accessibilityIgnoresInvertColors = YES; } -#endif + [_glView bindDrawable]; [self insertSubview:_glView atIndex:0]; _glView.contentMode = UIViewContentModeCenter; @@ -1616,7 +1552,9 @@ public: id<MGLAnnotation>annotation = [self annotationForGestureRecognizer:singleTap persistingResults:YES]; if(annotation) { - [self selectAnnotation:annotation animated:YES]; + CGPoint calloutPoint = [singleTap locationInView:self]; + CGRect positionRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:calloutPoint]; + [self selectAnnotation:annotation animated:YES calloutPositioningRect:positionRect]; } else { @@ -1938,7 +1876,7 @@ public: return [self.delegate respondsToSelector:@selector(mapView:tapOnCalloutForAnnotation:)]; } -- (void)calloutViewClicked:(__unused SMCalloutView *)calloutView +- (void)calloutViewClicked:(__unused MGLSMCalloutView *)calloutView { if ([self.delegate respondsToSelector:@selector(mapView:tapOnCalloutForAnnotation:)]) { @@ -2008,14 +1946,14 @@ public: return ([validSimultaneousGestures containsObject:gestureRecognizer] && [validSimultaneousGestures containsObject:otherGestureRecognizer]); } - + - (CLLocationDegrees)angleBetweenPoints:(CGPoint)west east:(CGPoint)east { CGFloat slope = (west.y - east.y) / (west.x - east.x); - + CGFloat angle = atan(fabs(slope)); CLLocationDegrees degrees = MGLDegreesFromRadians(angle); - + return degrees; } @@ -2354,8 +2292,61 @@ public: - (NSString *)accessibilityValue { + NSMutableArray *facts = [NSMutableArray array]; + double zoomLevel = round(self.zoomLevel + 1); - return [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE", nil, nil, @"Zoom %dx\n%ld annotation(s) visible", @"Map accessibility value"), (int)zoomLevel, (long)self.accessibilityAnnotationCount]; + [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_ZOOM", nil, nil, @"Zoom %dx.", @"Map accessibility value; {zoom level}"), (int)zoomLevel]]; + + NSInteger annotationCount = self.accessibilityAnnotationCount; + if (annotationCount) { + [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_ANNOTATIONS", nil, nil, @"%ld annotation(s) visible.", @"Map accessibility value; {number of visible annotations}"), (long)self.accessibilityAnnotationCount]]; + } + + NSArray *placeFeatures = self.visiblePlaceFeatures; + if (placeFeatures.count) { + NSMutableArray *placesArray = [NSMutableArray arrayWithCapacity:placeFeatures.count]; + NSMutableSet *placesSet = [NSMutableSet setWithCapacity:placeFeatures.count]; + for (id <MGLFeature> placeFeature in placeFeatures.reverseObjectEnumerator) { + NSString *name = [placeFeature attributeForKey:@"name"]; + if (![placesSet containsObject:name]) { + [placesArray addObject:name]; + [placesSet addObject:name]; + } + if (placesArray.count >= 3) { + break; + } + } + NSString *placesString = [placesArray componentsJoinedByString:NSLocalizedStringWithDefaultValue(@"LIST_SEPARATOR", nil, nil, @", ", @"List separator")]; + [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_PLACES", nil, nil, @"Places visible: %@.", @"Map accessibility value; {list of visible places}"), placesString]]; + } + + NSArray *roadFeatures = self.visibleRoadFeatures; + if (roadFeatures.count) { + [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_ROADS", nil, nil, @"%ld road(s) visible.", @"Map accessibility value; {number of visible roads}"), roadFeatures.count]]; + } + + NSString *value = [facts componentsJoinedByString:@" "]; + return value; +} + +- (NS_ARRAY_OF(id <MGLFeature>) *)visiblePlaceFeatures +{ + if (!_visiblePlaceFeatures) + { + NSArray *placeStyleLayerIdentifiers = [self.style.placeStyleLayers valueForKey:@"identifier"]; + _visiblePlaceFeatures = [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:placeStyleLayerIdentifiers]]; + } + return _visiblePlaceFeatures; +} + +- (NS_ARRAY_OF(id <MGLFeature>) *)visibleRoadFeatures +{ + if (!_visibleRoadFeatures) + { + NSArray *roadStyleLayerIdentifiers = [self.style.roadStyleLayers valueForKey:@"identifier"]; + _visibleRoadFeatures = [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:roadStyleLayerIdentifiers]]; + } + return _visibleRoadFeatures; } - (CGRect)accessibilityFrame @@ -2389,14 +2380,9 @@ public: { if (self.calloutViewForSelectedAnnotation) { - return 2 /* selectedAnnotationCalloutView, mapViewProxyAccessibilityElement */; - } - NSInteger count = self.accessibilityAnnotationCount + 2 /* compass, attributionButton */; - if (self.userLocationAnnotationView) - { - count++; + return 2 /* calloutViewForSelectedAnnotation, mapViewProxyAccessibilityElement */; } - return count; + return !!self.userLocationAnnotationView + self.accessibilityAnnotationCount + self.visiblePlaceFeatures.count + self.visibleRoadFeatures.count + 2 /* compass, attributionButton */; } - (NSInteger)accessibilityAnnotationCount @@ -2421,67 +2407,123 @@ public: } return nil; } - std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds]; - - // Ornaments - if (index == 0) + + // Compass + NSUInteger compassIndex = 0; + if (index == compassIndex) { return self.compassView; } - if ( ! self.userLocationAnnotationView) - { - index++; - } - else if (index == 1) + + // User location annotation + NSRange userLocationAnnotationRange = NSMakeRange(compassIndex + 1, !!self.userLocationAnnotationView); + if (NSLocationInRange(index, userLocationAnnotationRange)) { return self.userLocationAnnotationView; } - if (index > 0 && (NSUInteger)index == visibleAnnotations.size() + 2 /* compass, userLocationAnnotationView */) - { - return self.attributionButton; - } - - std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); + CGPoint centerPoint = self.contentCenter; if (self.userTrackingMode != MGLUserTrackingModeNone) { centerPoint = self.userLocationAnnotationViewCenter; } - CLLocationCoordinate2D currentCoordinate = [self convertPoint:centerPoint toCoordinateFromView:self]; - std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { - CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; - CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; - CLLocationDegrees deltaA = hypot(coordinateA.latitude - currentCoordinate.latitude, - coordinateA.longitude - currentCoordinate.longitude); - CLLocationDegrees deltaB = hypot(coordinateB.latitude - currentCoordinate.latitude, - coordinateB.longitude - currentCoordinate.longitude); - return deltaA < deltaB; - }); - - NSUInteger annotationIndex = MGLAnnotationTagNotFound; - if (index >= 0 && (NSUInteger)(index - 2) < visibleAnnotations.size()) + + // Visible annotations + std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds]; + NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size()); + if (NSLocationInRange(index, visibleAnnotationRange)) + { + std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); + std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { + CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; + CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; + CGPoint pointA = [self convertCoordinate:coordinateA toPointToView:self]; + CGPoint pointB = [self convertCoordinate:coordinateB toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return deltaA < deltaB; + }); + + NSUInteger annotationIndex = index - visibleAnnotationRange.location; + MGLAnnotationTag annotationTag = visibleAnnotations[annotationIndex]; + NSAssert(annotationTag != MGLAnnotationTagNotFound, @"Can’t get accessibility element for nonexistent or invisible annotation at index %li.", (long)index); + return [self accessibilityElementForAnnotationWithTag:annotationTag]; + } + + // Visible place features + NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures; + NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count); + if (NSLocationInRange(index, visiblePlaceFeatureRange)) + { + visiblePlaceFeatures = [visiblePlaceFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) { + CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self]; + CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return [@(deltaA) compare:@(deltaB)]; + }]; + + id <MGLFeature> feature = visiblePlaceFeatures[index - visiblePlaceFeatureRange.location]; + return [self accessibilityElementForPlaceFeature:feature]; + } + + // Visible road features + NSArray *visibleRoadFeatures = self.visibleRoadFeatures; + NSRange visibleRoadFeatureRange = NSMakeRange(NSMaxRange(visiblePlaceFeatureRange), visibleRoadFeatures.count); + if (NSLocationInRange(index, visibleRoadFeatureRange)) + { + visibleRoadFeatures = [visibleRoadFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) { + CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self]; + CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return [@(deltaA) compare:@(deltaB)]; + }]; + + id <MGLFeature> feature = visibleRoadFeatures[index - visibleRoadFeatureRange.location]; + return [self accessibilityElementForRoadFeature:feature]; + } + + // Attribution button + NSUInteger attributionButtonIndex = NSMaxRange(visibleRoadFeatureRange); + if (index == attributionButtonIndex) { - annotationIndex = index - 2 /* compass, userLocationAnnotationView */; + return self.attributionButton; } - MGLAnnotationTag annotationTag = visibleAnnotations[annotationIndex]; - NSAssert(annotationTag != MGLAnnotationTagNotFound, @"Can’t get accessibility element for nonexistent or invisible annotation at index %li.", (long)index); + + NSAssert(NO, @"Index %ld not in recognized accessibility element ranges. " + @"User location annotation range: %@; visible annotation range: %@; " + @"visible place feature range: %@; visible road feature range: %@.", + (long)index, NSStringFromRange(userLocationAnnotationRange), + NSStringFromRange(visibleAnnotationRange), NSStringFromRange(visiblePlaceFeatureRange), + NSStringFromRange(visibleRoadFeatureRange)); + return nil; +} + +/** + Returns an accessibility element corresponding to a visible annotation with the given tag. + + @param annotationTag Tag of the annotation represented by the accessibility element to return. + */ +- (id)accessibilityElementForAnnotationWithTag:(MGLAnnotationTag)annotationTag +{ NSAssert(_annotationContextsByAnnotationTag.count(annotationTag), @"Missing annotation for tag %u.", annotationTag); MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); id <MGLAnnotation> annotation = annotationContext.annotation; - + // Let the annotation view serve as its own accessibility element. MGLAnnotationView *annotationView = annotationContext.annotationView; if (annotationView && annotationView.superview) { return annotationView; } - + // Lazily create an accessibility element for the found annotation. if ( ! annotationContext.accessibilityElement) { annotationContext.accessibilityElement = [[MGLAnnotationAccessibilityElement alloc] initWithAccessibilityContainer:self tag:annotationTag]; } - + // Update the accessibility element. MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag]; CGRect annotationFrame = [self frameOfImage:annotationImage.image centeredAtCoordinate:annotation.coordinate]; @@ -2492,8 +2534,7 @@ public: annotationFrame = CGRectUnion(annotationFrame, minimumFrame); CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self); annotationContext.accessibilityElement.accessibilityFrame = screenRect; - annotationContext.accessibilityElement.accessibilityHint = NSLocalizedStringWithDefaultValue(@"ANNOTATION_A11Y_HINT", nil, nil, @"Shows more info", @"Accessibility hint"); - + if ([annotation respondsToSelector:@selector(title)]) { annotationContext.accessibilityElement.accessibilityLabel = annotation.title; @@ -2502,10 +2543,114 @@ public: { annotationContext.accessibilityElement.accessibilityValue = annotation.subtitle; } - + return annotationContext.accessibilityElement; } +/** + Returns an accessibility element corresponding to the given place feature. + + @param feature The place feature represented by the accessibility element. + */ +- (id)accessibilityElementForPlaceFeature:(id <MGLFeature>)feature +{ + if (!_featureAccessibilityElements) + { + _featureAccessibilityElements = [NSMutableSet set]; + } + + MGLFeatureAccessibilityElement *element = [_featureAccessibilityElements objectsPassingTest:^BOOL(MGLFeatureAccessibilityElement * _Nonnull element, BOOL * _Nonnull stop) { + return element.feature.identifier && ![element.feature.identifier isEqual:@0] && [element.feature.identifier isEqual:feature.identifier]; + }].anyObject; + if (!element) + { + element = [[MGLPlaceFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; + } + CGPoint center = [self convertCoordinate:feature.coordinate toPointToView:self]; + CGRect annotationFrame = CGRectInset({center, CGSizeZero}, -MGLAnnotationAccessibilityElementMinimumSize.width / 2, -MGLAnnotationAccessibilityElementMinimumSize.width / 2); + CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self); + element.accessibilityFrame = screenRect; + + [_featureAccessibilityElements addObject:element]; + + return element; +} + +/** + Returns an accessibility element corresponding to the given road feature. + + @param feature The road feature represented by the accessibility element. + */ +- (id)accessibilityElementForRoadFeature:(id <MGLFeature>)feature +{ + if (!_featureAccessibilityElements) + { + _featureAccessibilityElements = [NSMutableSet set]; + } + + MGLFeatureAccessibilityElement *element = [_featureAccessibilityElements objectsPassingTest:^BOOL(MGLFeatureAccessibilityElement * _Nonnull element, BOOL * _Nonnull stop) { + return element.feature.identifier && ![element.feature.identifier isEqual:@0] && [element.feature.identifier isEqual:feature.identifier]; + }].anyObject; + if (!element) + { + element = [[MGLRoadFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; + } + + UIBezierPath *path; + if ([feature isKindOfClass:[MGLPointFeature class]]) + { + CGPoint center = [self convertCoordinate:feature.coordinate toPointToView:self]; + CGRect annotationFrame = CGRectInset({center, CGSizeZero}, -MGLAnnotationAccessibilityElementMinimumSize.width / 2, -MGLAnnotationAccessibilityElementMinimumSize.width / 2); + CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self); + element.accessibilityFrame = screenRect; + } + else if ([feature isKindOfClass:[MGLPolylineFeature class]]) + { + path = [self pathOfPolyline:(MGLPolyline *)feature]; + } + else if ([feature isKindOfClass:[MGLMultiPolylineFeature class]]) + { + path = [UIBezierPath bezierPath]; + for (MGLPolyline *polyline in [(MGLMultiPolylineFeature *)feature polylines]) + { + [path appendPath:[self pathOfPolyline:polyline]]; + } + } + + if (path) + { + CGPathRef strokedCGPath = CGPathCreateCopyByStrokingPath(path.CGPath, NULL, MGLAnnotationAccessibilityElementMinimumSize.width, kCGLineCapButt, kCGLineJoinMiter, 0); + UIBezierPath *strokedPath = [UIBezierPath bezierPathWithCGPath:strokedCGPath]; + CGPathRelease(strokedCGPath); + UIBezierPath *screenPath = UIAccessibilityConvertPathToScreenCoordinates(strokedPath, self); + element.accessibilityPath = screenPath; + } + + [_featureAccessibilityElements addObject:element]; + + return element; +} + +- (UIBezierPath *)pathOfPolyline:(MGLPolyline *)polyline +{ + CLLocationCoordinate2D *coordinates = polyline.coordinates; + NSUInteger pointCount = polyline.pointCount; + UIBezierPath *path = [UIBezierPath bezierPath]; + for (NSUInteger i = 0; i < pointCount; i++) + { + CGPoint point = [self convertCoordinate:coordinates[i] toPointToView:self]; + if (i) + { + [path addLineToPoint:point]; + } + else + { + [path moveToPoint:point]; + } + } + return path; +} + - (NSInteger)indexOfAccessibilityElement:(id)element { if (self.calloutViewForSelectedAnnotation) @@ -2513,17 +2658,30 @@ public: return [@[self.calloutViewForSelectedAnnotation, self.mapViewProxyAccessibilityElement] indexOfObject:element]; } + + // Compass + NSUInteger compassIndex = 0; if (element == self.compassView) { - return 0; + return compassIndex; } + + // User location annotation + NSRange userLocationAnnotationRange = NSMakeRange(compassIndex + 1, !!self.userLocationAnnotationView); if (element == self.userLocationAnnotationView) { - return 1; + return userLocationAnnotationRange.location; } - + + CGPoint centerPoint = self.contentCenter; + if (self.userTrackingMode != MGLUserTrackingModeNone) + { + centerPoint = self.userLocationAnnotationViewCenter; + } + + // Visible annotations std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds]; - + NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size()); MGLAnnotationTag tag = MGLAnnotationTagNotFound; if ([element isKindOfClass:[MGLAnnotationView class]]) { @@ -2534,22 +2692,92 @@ public: { tag = [(MGLAnnotationAccessibilityElement *)element tag]; } - else if (element == self.attributionButton) - { - return !!self.userLocationAnnotationView + visibleAnnotations.size(); + + if (tag != MGLAnnotationTagNotFound) + { + std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); + std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { + CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; + CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; + CGPoint pointA = [self convertCoordinate:coordinateA toPointToView:self]; + CGPoint pointB = [self convertCoordinate:coordinateB toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return deltaA < deltaB; + }); + + auto foundElement = std::find(visibleAnnotations.begin(), visibleAnnotations.end(), tag); + if (foundElement == visibleAnnotations.end()) + { + return NSNotFound; + } + return visibleAnnotationRange.location + std::distance(visibleAnnotations.begin(), foundElement); } - else - { - return NSNotFound; + + // Visible place features + NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures; + NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count); + if ([element isKindOfClass:[MGLPlaceFeatureAccessibilityElement class]]) + { + visiblePlaceFeatures = [visiblePlaceFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) { + CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self]; + CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return [@(deltaA) compare:@(deltaB)]; + }]; + + id <MGLFeature> feature = [(MGLPlaceFeatureAccessibilityElement *)element feature]; + NSUInteger featureIndex = [visiblePlaceFeatures indexOfObject:feature]; + if (featureIndex == NSNotFound) + { + featureIndex = [visiblePlaceFeatures indexOfObjectPassingTest:^BOOL (id <MGLFeature> _Nonnull visibleFeature, NSUInteger idx, BOOL * _Nonnull stop) { + return visibleFeature.identifier && ![visibleFeature.identifier isEqual:@0] && [visibleFeature.identifier isEqual:feature.identifier]; + }]; + } + if (featureIndex == NSNotFound) + { + return NSNotFound; + } + return visiblePlaceFeatureRange.location + featureIndex; } - - std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); - auto foundElement = std::find(visibleAnnotations.begin(), visibleAnnotations.end(), tag); - if (foundElement == visibleAnnotations.end()) + + // Visible road features + NSArray *visibleRoadFeatures = self.visibleRoadFeatures; + NSRange visibleRoadFeatureRange = NSMakeRange(NSMaxRange(visiblePlaceFeatureRange), visibleRoadFeatures.count); + if ([element isKindOfClass:[MGLRoadFeatureAccessibilityElement class]]) + { + visibleRoadFeatures = [visibleRoadFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) { + CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self]; + CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return [@(deltaA) compare:@(deltaB)]; + }]; + + id <MGLFeature> feature = [(MGLRoadFeatureAccessibilityElement *)element feature]; + NSUInteger featureIndex = [visibleRoadFeatures indexOfObject:feature]; + if (featureIndex == NSNotFound) + { + featureIndex = [visibleRoadFeatures indexOfObjectPassingTest:^BOOL (id <MGLFeature> _Nonnull visibleFeature, NSUInteger idx, BOOL * _Nonnull stop) { + return visibleFeature.identifier && ![visibleFeature.identifier isEqual:@0] && [visibleFeature.identifier isEqual:feature.identifier]; + }]; + } + if (featureIndex == NSNotFound) + { + return NSNotFound; + } + return visibleRoadFeatureRange.location + featureIndex; + } + + // Attribution button + NSUInteger attributionButtonIndex = NSMaxRange(visibleRoadFeatureRange); + if (element == self.attributionButton) { - return NSNotFound; + return attributionButtonIndex; } - return !!self.userLocationAnnotationView + std::distance(visibleAnnotations.begin(), foundElement) + 1 /* compass */; + + return NSNotFound; } - (MGLMapViewProxyAccessibilityElement *)mapViewProxyAccessibilityElement @@ -2580,10 +2808,11 @@ public: { centerPoint = self.userLocationAnnotationViewCenter; } - _mbglMap->setZoom(_mbglMap->getZoom() + log2(scaleFactor), mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); + double newZoom = round(self.zoomLevel) + log2(scaleFactor); + _mbglMap->setZoom(newZoom, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); [self unrotateIfNeededForGesture]; - UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.accessibilityValue); + _accessibilityValueAnnouncementIsPending = YES; } #pragma mark - Geography - @@ -3266,6 +3495,12 @@ public: } std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:rect]; + std::vector<MGLAnnotationTag> shapeAnnotationTags = [self shapeAnnotationTagsInRect:rect]; + + if (shapeAnnotationTags.size()) { + annotationTags.insert(annotationTags.end(), shapeAnnotationTags.begin(), shapeAnnotationTags.end()); + } + if (annotationTags.size()) { NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:annotationTags.size()]; @@ -3382,7 +3617,7 @@ public: { annotationViewsForAnnotation[annotationValue] = annotationView; annotationView.annotation = annotation; - annotationView.center = [self convertCoordinate:annotation.coordinate toPointToView:self]; + annotationView.center = MGLPointRounded([self convertCoordinate:annotation.coordinate toPointToView:self]); [newAnnotationViews addObject:annotationView]; MGLAnnotationImage *annotationImage = self.invisibleAnnotationImage; @@ -3771,6 +4006,11 @@ public: queryRect = CGRectInset(queryRect, -MGLAnnotationImagePaddingForHitTest, -MGLAnnotationImagePaddingForHitTest); std::vector<MGLAnnotationTag> nearbyAnnotations = [self annotationTagsInRect:queryRect]; + std::vector<MGLAnnotationTag> nearbyShapeAnnotations = [self shapeAnnotationTagsInRect:queryRect]; + + if (nearbyShapeAnnotations.size()) { + nearbyAnnotations.insert(nearbyAnnotations.end(), nearbyShapeAnnotations.begin(), nearbyShapeAnnotations.end()); + } if (nearbyAnnotations.size()) { @@ -3778,54 +4018,59 @@ public: CGRect hitRect = CGRectInset({ point, CGSizeZero }, -MGLAnnotationImagePaddingForHitTest, -MGLAnnotationImagePaddingForHitTest); - + // Filter out any annotation whose image or view is unselectable or for which // hit testing fails. - auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(), - [&](const MGLAnnotationTag annotationTag) - { + auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag annotationTag) { id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag]; NSAssert(annotation, @"Unknown annotation found nearby tap"); if ( ! annotation) { return true; } - + MGLAnnotationContext annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); CGRect annotationRect; - + MGLAnnotationView *annotationView = annotationContext.annotationView; + if (annotationView) { if ( ! annotationView.enabled) { return true; } - - CGPoint calloutAnchorPoint = [self convertCoordinate:annotation.coordinate toPointToView:self]; + + CGPoint calloutAnchorPoint = MGLPointRounded([self convertCoordinate:annotation.coordinate toPointToView:self]); CGRect frame = CGRectInset({ calloutAnchorPoint, CGSizeZero }, -CGRectGetWidth(annotationView.frame) / 2, -CGRectGetHeight(annotationView.frame) / 2); annotationRect = UIEdgeInsetsInsetRect(frame, annotationView.alignmentRectInsets); } else { + if ([annotation isKindOfClass:[MGLShape class]]) + { + return false; + } + MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag]; if ( ! annotationImage.enabled) { return true; } - + MGLAnnotationImage *fallbackAnnotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName]; UIImage *fallbackImage = fallbackAnnotationImage.image; - + annotationRect = [self frameOfImage:annotationImage.image ?: fallbackImage centeredAtCoordinate:annotation.coordinate]; } - + // Filter out the annotation if the fattened finger didn’t land // within the image’s alignment rect. return !!!CGRectIntersectsRect(annotationRect, hitRect); }); - + nearbyAnnotations.resize(std::distance(nearbyAnnotations.begin(), end)); + } MGLAnnotationTag hitAnnotationTag = MGLAnnotationTagNotFound; @@ -3908,6 +4153,14 @@ public: }); } +- (std::vector<MGLAnnotationTag>)shapeAnnotationTagsInRect:(CGRect)rect +{ + return _rendererFrontend->getRenderer()->queryShapeAnnotations({ + { CGRectGetMinX(rect), CGRectGetMinY(rect) }, + { CGRectGetMaxX(rect), CGRectGetMaxY(rect) }, + }); +} + - (id <MGLAnnotation>)selectedAnnotation { if (_userLocationAnnotationIsSelected) @@ -3957,9 +4210,13 @@ public: - (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated { - if ( ! annotation) return; + CGRect positioningRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:CGPointZero]; + [self selectAnnotation:annotation animated:animated calloutPositioningRect:positioningRect]; +} - if ([annotation isKindOfClass:[MGLMultiPoint class]]) return; +- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated calloutPositioningRect:(CGRect)calloutPositioningRect +{ + if ( ! annotation) return; if (annotation == self.selectedAnnotation) return; @@ -3974,9 +4231,6 @@ public: if (annotationTag == MGLAnnotationTagNotFound) return; } - // By default attempt to use the GL annotation image frame as the positioning rect. - CGRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag]; - MGLAnnotationView *annotationView = nil; if (annotation != self.userLocation) @@ -3984,21 +4238,12 @@ public: MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); annotationView = annotationContext.annotationView; if (annotationView && annotationView.enabled) { - { - // Annotations represented by views use the view frame as the positioning rect. - positioningRect = annotationView.frame; - [annotationView.superview bringSubviewToFront:annotationView]; - [annotationView setSelected:YES animated:animated]; + // Annotations represented by views use the view frame as the positioning rect. + calloutPositioningRect = annotationView.frame; + [annotationView.superview bringSubviewToFront:annotationView]; + [annotationView setSelected:YES animated:animated]; } } - } - - // The client can request that any annotation be selected (even ones that are offscreen). - // The annotation can’t be selected if no part of it is hittable. - if ( ! CGRectIntersectsRect(positioningRect, self.bounds) && annotation != self.userLocation) - { - return; - } self.selectedAnnotation = annotation; @@ -4028,7 +4273,7 @@ public: if (_userLocationAnnotationIsSelected) { - positioningRect = [self.userLocationAnnotationView.layer.presentationLayer frame]; + calloutPositioningRect = [self.userLocationAnnotationView.layer.presentationLayer frame]; CGRect implicitAnnotationFrame = [self.userLocationAnnotationView.layer.presentationLayer frame]; CGRect explicitAnnotationFrame = self.userLocationAnnotationView.frame; @@ -4044,7 +4289,7 @@ public: if ([calloutView.leftAccessoryView isKindOfClass:[UIControl class]]) { UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(handleCalloutAccessoryTapGesture:)]; + action:@selector(handleCalloutAccessoryTapGesture:)]; [calloutView.leftAccessoryView addGestureRecognizer:calloutAccessoryTap]; } @@ -4057,7 +4302,7 @@ public: if ([calloutView.rightAccessoryView isKindOfClass:[UIControl class]]) { UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(handleCalloutAccessoryTapGesture:)]; + action:@selector(handleCalloutAccessoryTapGesture:)]; [calloutView.rightAccessoryView addGestureRecognizer:calloutAccessoryTap]; } @@ -4067,7 +4312,7 @@ public: calloutView.delegate = self; // present popup - [calloutView presentCalloutFromRect:positioningRect + [calloutView presentCalloutFromRect:calloutPositioningRect inView:self.glView constrainedToView:self.glView animated:animated]; @@ -4097,6 +4342,27 @@ public: /// Returns the rectangle that represents the annotation image of the annotation /// with the given tag. This rectangle is fitted to the image’s alignment rect /// and is appropriate for positioning a popover. +/// If a shape annotation is visible but its centroid is not, and a default point is specified, +/// the callout view is anchored to the default callout point. +- (CGRect)positioningRectForAnnotation:(id <MGLAnnotation>)annotation defaultCalloutPoint:(CGPoint)calloutPoint +{ + MGLAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation]; + CGRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag]; + + // For annotations which `coordinate` falls offscreen it will use the current tap point as anchor instead. + if ( ! CGRectIntersectsRect(positioningRect, self.bounds) && annotation != self.userLocation) + { + if (!CGPointEqualToPoint(calloutPoint, CGPointZero)) { + positioningRect = CGRectMake(calloutPoint.x, calloutPoint.y, positioningRect.size.width, positioningRect.size.height); + } + } + + return positioningRect; +} + +/// Returns the rectangle that represents the annotation image of the annotation +/// with the given tag. This rectangle is fitted to the image’s alignment rect +/// and is appropriate for positioning a popover. - (CGRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annotationTag { id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag]; @@ -4104,6 +4370,13 @@ public: { return CGRectZero; } + + if ([annotation isKindOfClass:[MGLMultiPoint class]]) { + CLLocationCoordinate2D origin = annotation.coordinate; + CGPoint originPoint = [self convertCoordinate:origin toPointToView:self]; + return CGRectMake(originPoint.x, originPoint.y, MGLAnnotationImagePaddingForHitTest, MGLAnnotationImagePaddingForHitTest); + + } UIImage *image = [self imageOfAnnotationWithTag:annotationTag].image; if ( ! image) { @@ -4125,7 +4398,7 @@ public: /// image centered at the given coordinate. - (CGRect)frameOfImage:(UIImage *)image centeredAtCoordinate:(CLLocationCoordinate2D)coordinate { - CGPoint calloutAnchorPoint = [self convertCoordinate:coordinate toPointToView:self]; + CGPoint calloutAnchorPoint = MGLPointRounded([self convertCoordinate:coordinate toPointToView:self]); CGRect frame = CGRectInset({ calloutAnchorPoint, CGSizeZero }, -image.size.width / 2, -image.size.height / 2); return UIEdgeInsetsInsetRect(frame, image.alignmentRectInsets); } @@ -4564,7 +4837,6 @@ public: if (_showsUserHeadingIndicator) { self.showsUserLocation = YES; - } [self validateUserHeadingUpdating]; } @@ -5100,12 +5372,26 @@ public: { if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) { - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); + _featureAccessibilityElements = nil; + _visiblePlaceFeatures = nil; + _visibleRoadFeatures = nil; + if (_accessibilityValueAnnouncementIsPending) { + _accessibilityValueAnnouncementIsPending = NO; + [self performSelector:@selector(announceAccessibilityValue) withObject:nil afterDelay:0.1]; + } else { + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); + } } [self.delegate mapView:self regionDidChangeAnimated:animated]; } } +- (void)announceAccessibilityValue +{ + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.accessibilityValue); + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); +} + - (void)mapViewWillStartLoadingMap { if (!_mbglMap) { return; @@ -5187,6 +5473,8 @@ public: if (!_mbglMap) { return; } + + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingMap:fullyRendered:)]) { @@ -5270,7 +5558,7 @@ public: if (annotationView) { - annotationView.center = [self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self]; + annotationView.center = MGLPointRounded([self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self]); } } @@ -5388,7 +5676,7 @@ public: } else { - userPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self]; + userPoint = MGLPointRounded([self convertCoordinate:self.userLocation.coordinate toPointToView:self]); } if ( ! annotationView.superview) diff --git a/platform/ios/src/MGLScaleBar.mm b/platform/ios/src/MGLScaleBar.mm index 966555e46a..139dffdfab 100644 --- a/platform/ios/src/MGLScaleBar.mm +++ b/platform/ios/src/MGLScaleBar.mm @@ -175,10 +175,15 @@ static const CGFloat MGLFeetPerMeter = 3.28084; return [self usesMetricSystem] ? self.metersPerPoint : self.metersPerPoint * MGLFeetPerMeter; } -#pragma mark - Convenient methods +#pragma mark - Convenience methods - (BOOL)usesRightToLeftLayout { - return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.superview.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft; + // semanticContentAttribute is iOS 9+ + if ([self.superview respondsToSelector:@selector(semanticContentAttribute)]) { + return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.superview.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft; + } else { + return UIApplication.sharedApplication.userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; + } } - (BOOL)usesMetricSystem { @@ -241,7 +246,7 @@ static const CGFloat MGLFeetPerMeter = 3.28084; CGFloat alpha = maximumDistance > allowedDistance ? .0f : 1.0f; - if(self.alpha != alpha) { + if (self.alpha != alpha) { [UIView animateWithDuration:.2f delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ self.alpha = alpha; } completion:nil]; @@ -331,7 +336,7 @@ static const CGFloat MGLFeetPerMeter = 3.28084; } - (void)layoutBars { - CGFloat barWidth = (CGRectGetWidth(self.bounds) - self.borderWidth * 2.0f) / self.bars.count; + CGFloat barWidth = round((CGRectGetWidth(self.bounds) - self.borderWidth * 2.0f) / self.bars.count); NSUInteger i = 0; for (UIView *bar in self.bars) { @@ -354,11 +359,11 @@ static const CGFloat MGLFeetPerMeter = 3.28084; } - (void)layoutLabels { - CGFloat barWidth = self.bounds.size.width / self.bars.count; + CGFloat barWidth = round(self.bounds.size.width / self.bars.count); BOOL RTL = [self usesRightToLeftLayout]; NSUInteger i = RTL ? self.bars.count : 0; for (MGLScaleBarLabel *label in self.labels) { - CGFloat xPosition = barWidth * i - CGRectGetMidX(label.bounds) + self.borderWidth; + CGFloat xPosition = round(barWidth * i - CGRectGetMidX(label.bounds) + self.borderWidth); label.frame = CGRectMake(xPosition, 0, CGRectGetWidth(label.bounds), CGRectGetHeight(label.bounds)); diff --git a/platform/ios/src/MGLUserLocation.h b/platform/ios/src/MGLUserLocation.h index 91abadbcb7..4e01cf00c9 100644 --- a/platform/ios/src/MGLUserLocation.h +++ b/platform/ios/src/MGLUserLocation.h @@ -34,7 +34,8 @@ MGL_EXPORT The heading of the user location. (read-only) This property is `nil` if the user location tracking mode is not - `MGLUserTrackingModeFollowWithHeading`. + `MGLUserTrackingModeFollowWithHeading` or if + `MGLMapView.showsUserHeadingIndicator` is disabled. */ @property (nonatomic, readonly, nullable) CLHeading *heading; |