[ios] Introduce MGLAnnotationView and support for view annotations

Add an UIView subclass that can be used as the base class for all
client provided UIViews for annotations.

Teach MGLMapView to be able to display annotation views over the map if provided by the client delegate. For now, if the delegate provides a UIView then it will be used. If not, the map view will fall back to the old strategy of using GL annotations with an image provided by the delegate or a default image if not.
The map keeps a reuse queue and will store annotation views that are panned
offscreen in the queue if the application developer supplied a reuse queue identifer. The views in the queue are reused when more annotation
views are required. This view reuse provides a performance gain when many
annotations are shown and most of them are offscreen.

iosapp now implements the new delegate method to supply
a native view.

Add a playground to the workspace to facilitate experimentation
with new features. A playground is capable of importing frameworks if it
exists in the same workspace that builds the imported framework. The
initial playground demonstrates annotation views.

This also fixes a crash due to nullptr in annotations array If the `annotations` method is called while the user dot's callout view was showing, the userdot annotation is represented as null in the annotation context map. This
caused a crash when the null pointer was attempted to be converted
into an NSArray via C array. This protects against this bug by filtering out such a null annotation.
This commit is contained in:
Jesse Bounds
2016-04-22 07:43:55 -06:00
parent a6ca18f23c
commit 39669243b7
15 changed files with 555 additions and 34 deletions

1
.gitignore vendored
View File

@@ -23,3 +23,4 @@ xcuserdata
/node_modules
/platform/ios/benchmark/assets/glyphs/DIN*
/platform/ios/benchmark/assets/tiles/mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v6
**/token

View File

@@ -0,0 +1,133 @@
import UIKit
import XCPlayground
import Mapbox
let width: CGFloat = 700
let height: CGFloat = 800
//: A control panel
let panelWidth: CGFloat = 200
let panel = UIView(frame: CGRect(x: width - panelWidth, y: 0, width: 200, height: 100))
panel.alpha = 0.8
panel.backgroundColor = UIColor.whiteColor()
let deleteSwitchLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 30))
deleteSwitchLabel.adjustsFontSizeToFitWidth = true
deleteSwitchLabel.text = "Delete Markers"
let deleteMarkerSwitchView = UISwitch(frame: CGRect(x: panelWidth - panelWidth / 2.0, y:0, width: 100, height: 50))
panel.addSubview(deleteSwitchLabel)
panel.addSubview(deleteMarkerSwitchView)
let hideSwitchLabel = UILabel(frame: CGRect(x: 0, y: 30, width: 100, height: 30))
hideSwitchLabel.adjustsFontSizeToFitWidth = true
hideSwitchLabel.text = "Hide Markers"
let hideMarkerSwitchView = UISwitch(frame: CGRect(x: panelWidth - panelWidth / 2.0, y: 30, width: 100, height: 50))
panel.addSubview(hideSwitchLabel)
panel.addSubview(hideMarkerSwitchView)
//: # Mapbox Maps
/*:
Put your access token into a plain text file called `token`. Then select the “token” placeholder below, go to Editor ‣ Insert File Literal, and select the `token` file.
*/
var accessToken = try String(contentsOfURL: <#token#>)
MGLAccountManager.setAccessToken(accessToken)
class PlaygroundAnnotationView: MGLAnnotationView {
override func prepareForReuse() {
hidden = hideMarkerSwitchView.on
}
}
//: Define a map delegate
class MapDelegate: NSObject, MGLMapViewDelegate {
var annotationViewByAnnotation = [MGLPointAnnotation: PlaygroundAnnotationView]()
func mapView(mapView: MGLMapView, viewForAnnotation annotation: MGLAnnotation) -> MGLAnnotationView? {
var annotationView = mapView.dequeueReusableAnnotationViewWithIdentifier("annotation") as? PlaygroundAnnotationView
if (annotationView == nil) {
let av = PlaygroundAnnotationView(reuseIdentifier: "annotation")
av.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
let centerView = UIView(frame: CGRectInset(av.bounds, 3, 3))
centerView.backgroundColor = UIColor.whiteColor()
av.addSubview(centerView)
av.backgroundColor = UIColor.purpleColor()
annotationView = av
} else {
annotationView!.subviews.first?.backgroundColor = UIColor.greenColor()
}
annotationViewByAnnotation[annotation as! MGLPointAnnotation] = annotationView
return annotationView
}
func mapView(mapView: MGLMapView, didSelectAnnotation annotation: MGLAnnotation) {
let pointAnnotation = annotation as! MGLPointAnnotation
let annotationView: PlaygroundAnnotationView = annotationViewByAnnotation[pointAnnotation]!
for view in annotationViewByAnnotation.values {
view.layer.zPosition = -1
}
annotationView.layer.zPosition = 1
UIView.animateWithDuration(1.25, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.6, options: .CurveEaseOut, animations: {
annotationView.transform = CGAffineTransformMakeScale(1.8, 1.8)
}) { _ in
annotationView.transform = CGAffineTransformMakeScale(1, 1)
if deleteMarkerSwitchView.on {
mapView.removeAnnotation(pointAnnotation)
return
}
if hideMarkerSwitchView.on {
annotationView.hidden = true
}
}
}
func handleTap(press: UILongPressGestureRecognizer) {
let mapView: MGLMapView = press.view as! MGLMapView
if (press.state == .Recognized) {
let coordiante: CLLocationCoordinate2D = mapView.convertPoint(press.locationInView(mapView), toCoordinateFromView: mapView)
let annotation = MGLPointAnnotation()
annotation.title = "Dropped Marker"
annotation.coordinate = coordiante
mapView.addAnnotation(annotation)
mapView.showAnnotations([annotation], animated: true)
}
}
}
//: Create a map and its delegate
let lat: CLLocationDegrees = 37.174057
let lng: CLLocationDegrees = -104.490984
let centerCoordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng)
let mapView = MGLMapView(frame: CGRect(x: 0, y: 0, width: width, height: height))
mapView.frame = CGRect(x: 0, y: 0, width: width, height: height)
XCPlaygroundPage.currentPage.liveView = mapView
let mapDelegate = MapDelegate()
mapView.delegate = mapDelegate
let tapGesture = UILongPressGestureRecognizer(target: mapDelegate, action: #selector(mapDelegate.handleTap))
mapView.addGestureRecognizer(tapGesture)
//: Zoom in to a location
mapView.setCenterCoordinate(centerCoordinate, zoomLevel: 12, animated: false)
//: Add control panel
mapView.addSubview(panel)

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='5.0' target-platform='ios' display-mode='raw'>
<timeline fileName='timeline.xctimeline'/>
</playground>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Timeline
version = "3.0">
<TimelineItems>
</TimelineItems>
</Timeline>

View File

@@ -0,0 +1,7 @@
#import <Mapbox/Mapbox.h>
@interface MBXAnnotationView : MGLAnnotationView
@property (nonatomic) UIColor *centerColor;
@end

View File

@@ -0,0 +1,28 @@
#import "MBXAnnotationView.h"
@interface MBXAnnotationView ()
@property (nonatomic) UIView *centerView;
@end
@implementation MBXAnnotationView
- (void)layoutSubviews {
[super layoutSubviews];
if (!self.centerView) {
self.backgroundColor = [UIColor blueColor];
self.centerView = [[UIView alloc] initWithFrame:CGRectInset(self.bounds, 5.0, 5.0)];
self.centerView.backgroundColor = self.centerColor;
[self addSubview:self.centerView];
}
}
- (void)setCenterColor:(UIColor *)centerColor {
if (![_centerColor isEqual:centerColor]) {
_centerColor = centerColor;
self.centerView.backgroundColor = centerColor;
}
}
@end

View File

@@ -3,6 +3,7 @@
#import "MBXAppDelegate.h"
#import "MBXCustomCalloutView.h"
#import "MBXOfflinePacksTableViewController.h"
#import "MBXAnnotationView.h"
#import <Mapbox/Mapbox.h>
@@ -17,6 +18,8 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = {
{ .latitude = -13.15589555, .longitude = -74.2178961777998 },
};
static NSString * const MBXViewControllerAnnotationViewReuseIdentifer = @"MBXViewControllerAnnotationViewReuseIdentifer";
@interface MBXDroppedPinAnnotation : MGLPointAnnotation
@end
@@ -461,7 +464,7 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = {
toCoordinateFromView:self.mapView];
point.title = @"Dropped Pin";
point.subtitle = [[[MGLCoordinateFormatter alloc] init] stringFromCoordinate:point.coordinate];
[self.mapView addAnnotation:point];
// Calling `addAnnotation:` on mapView is not required since `selectAnnotation:animated` has the side effect of adding the annotation if required
[self.mapView selectAnnotation:point animated:YES];
}
}
@@ -608,6 +611,21 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = {
#pragma mark - MGLMapViewDelegate
- (MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id<MGLAnnotation>)annotation
{
MBXAnnotationView *annotationView = (MBXAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:MBXViewControllerAnnotationViewReuseIdentifer];
if (!annotationView)
{
annotationView = [[MBXAnnotationView alloc] initWithReuseIdentifier:MBXViewControllerAnnotationViewReuseIdentifer];
annotationView.frame = CGRectMake(0, 0, 40, 40);
annotationView.centerColor = [UIColor whiteColor];
} else {
// orange indicates that the annotation view was reused
annotationView.centerColor = [UIColor orangeColor];
}
return annotationView;
}
- (MGLAnnotationImage *)mapView:(MGLMapView * __nonnull)mapView imageForAnnotation:(id <MGLAnnotation> __nonnull)annotation
{
if ([annotation isKindOfClass:[MBXDroppedPinAnnotation class]]

View File

@@ -7,6 +7,12 @@
objects = {
/* Begin PBXBuildFile section */
4018B1C71CDC287F00F666AF /* MGLAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4018B1C41CDC277F00F666AF /* MGLAnnotationView.m */; };
4018B1C81CDC287F00F666AF /* MGLAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4018B1C41CDC277F00F666AF /* MGLAnnotationView.m */; };
4018B1C91CDC288A00F666AF /* MGLAnnotationView_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 4018B1C31CDC277F00F666AF /* MGLAnnotationView_Private.h */; };
4018B1CA1CDC288E00F666AF /* MGLAnnotationView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4018B1C51CDC277F00F666AF /* MGLAnnotationView.h */; settings = {ATTRIBUTES = (Public, ); }; };
4018B1CB1CDC288E00F666AF /* MGLAnnotationView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4018B1C51CDC277F00F666AF /* MGLAnnotationView.h */; settings = {ATTRIBUTES = (Public, ); }; };
40FDA76B1CCAAA6800442548 /* MBXAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 40FDA76A1CCAAA6800442548 /* MBXAnnotationView.m */; };
DA17BE301CC4BAC300402C41 /* MGLMapView_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = DA17BE2F1CC4BAC300402C41 /* MGLMapView_Internal.h */; };
DA17BE311CC4BDAA00402C41 /* MGLMapView_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = DA17BE2F1CC4BAC300402C41 /* MGLMapView_Internal.h */; };
DA1DC96A1CB6C6B7006E619F /* MBXCustomCalloutView.m in Sources */ = {isa = PBXBuildFile; fileRef = DA1DC9671CB6C6B7006E619F /* MBXCustomCalloutView.m */; };
@@ -304,6 +310,12 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
4018B1C31CDC277F00F666AF /* MGLAnnotationView_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLAnnotationView_Private.h; sourceTree = "<group>"; };
4018B1C41CDC277F00F666AF /* MGLAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLAnnotationView.m; sourceTree = "<group>"; };
4018B1C51CDC277F00F666AF /* MGLAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLAnnotationView.h; sourceTree = "<group>"; };
402E9DE01CD2C76200FD4519 /* Mapbox.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Mapbox.playground; sourceTree = "<group>"; };
40FDA7691CCAAA6800442548 /* MBXAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MBXAnnotationView.h; sourceTree = "<group>"; };
40FDA76A1CCAAA6800442548 /* MBXAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MBXAnnotationView.m; sourceTree = "<group>"; };
DA17BE2F1CC4BAC300402C41 /* MGLMapView_Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapView_Internal.h; sourceTree = "<group>"; };
DA1DC94A1CB6C1C2006E619F /* Mapbox GL.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Mapbox GL.app"; sourceTree = BUILT_PRODUCTS_DIR; };
DA1DC9501CB6C1C2006E619F /* MBXAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBXAppDelegate.h; sourceTree = "<group>"; };
@@ -511,9 +523,18 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
402E9DE21CD3A56500FD4519 /* Playground */ = {
isa = PBXGroup;
children = (
402E9DE01CD2C76200FD4519 /* Mapbox.playground */,
);
name = Playground;
sourceTree = "<group>";
};
DA1DC9411CB6C1C2006E619F = {
isa = PBXGroup;
children = (
402E9DE21CD3A56500FD4519 /* Playground */,
DA1DC94C1CB6C1C2006E619F /* Demo App */,
DABCABA91CB80692000A7C39 /* Benchmarking App */,
DA8847D31CBAF91600AB86E3 /* SDK */,
@@ -543,6 +564,8 @@
children = (
DA1DC9501CB6C1C2006E619F /* MBXAppDelegate.h */,
DA1DC9981CB6E054006E619F /* MBXAppDelegate.m */,
40FDA7691CCAAA6800442548 /* MBXAnnotationView.h */,
40FDA76A1CCAAA6800442548 /* MBXAnnotationView.m */,
DA1DC9661CB6C6B7006E619F /* MBXCustomCalloutView.h */,
DA1DC9671CB6C6B7006E619F /* MBXCustomCalloutView.m */,
DA1DC9681CB6C6B7006E619F /* MBXOfflinePacksTableViewController.h */,
@@ -690,6 +713,9 @@
DA8848331CBAFB2A00AB86E3 /* Kit */ = {
isa = PBXGroup;
children = (
4018B1C31CDC277F00F666AF /* MGLAnnotationView_Private.h */,
4018B1C41CDC277F00F666AF /* MGLAnnotationView.m */,
4018B1C51CDC277F00F666AF /* MGLAnnotationView.h */,
DA8848341CBAFB8500AB86E3 /* MGLAnnotationImage.h */,
DA8848401CBAFB9800AB86E3 /* MGLAnnotationImage_Private.h */,
DA8848411CBAFB9800AB86E3 /* MGLAnnotationImage.m */,
@@ -844,6 +870,7 @@
DA8848861CBB033F00AB86E3 /* Fabric+FABKits.h in Headers */,
DA8848201CBAFA6200AB86E3 /* MGLOfflinePack_Private.h in Headers */,
DA8847FA1CBAFA5100AB86E3 /* MGLPolyline.h in Headers */,
4018B1C91CDC288A00F666AF /* MGLAnnotationView_Private.h in Headers */,
DA88482C1CBAFA6200AB86E3 /* NSBundle+MGLAdditions.h in Headers */,
DA88488E1CBB047F00AB86E3 /* reachability.h in Headers */,
DA8848231CBAFA6200AB86E3 /* MGLOfflineStorage_Private.h in Headers */,
@@ -855,6 +882,7 @@
DA8847FC1CBAFA5100AB86E3 /* MGLStyle.h in Headers */,
DA8847F01CBAFA5100AB86E3 /* MGLAnnotation.h in Headers */,
DA88483E1CBAFB8500AB86E3 /* MGLMapView+MGLCustomStyleLayerAdditions.h in Headers */,
4018B1CA1CDC288E00F666AF /* MGLAnnotationView.h in Headers */,
DA8847EF1CBAFA5100AB86E3 /* MGLAccountManager.h in Headers */,
DA8848511CBAFB9800AB86E3 /* MGLAPIClient.h in Headers */,
DA35A2C91CCAAAD200E826B2 /* NSValue+MGLAdditions.h in Headers */,
@@ -919,6 +947,7 @@
DA35A2B21CCA141D00E826B2 /* MGLCompassDirectionFormatter.h in Headers */,
DABFB8731CBE9A9900D62B32 /* Mapbox.h in Headers */,
DABFB86B1CBE99E500D62B32 /* MGLTilePyramidOfflineRegion.h in Headers */,
4018B1CB1CDC288E00F666AF /* MGLAnnotationView.h in Headers */,
DABFB85F1CBE99E500D62B32 /* MGLGeometry.h in Headers */,
DABFB85D1CBE99E500D62B32 /* MGLAccountManager.h in Headers */,
DA35A2BC1CCA9A6900E826B2 /* MGLClockDirectionFormatter.h in Headers */,
@@ -1217,6 +1246,7 @@
DA1DC96B1CB6C6B7006E619F /* MBXOfflinePacksTableViewController.m in Sources */,
DA1DC96A1CB6C6B7006E619F /* MBXCustomCalloutView.m in Sources */,
DA1DC99B1CB6E064006E619F /* MBXViewController.m in Sources */,
40FDA76B1CCAAA6800442548 /* MBXAnnotationView.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1259,6 +1289,7 @@
DA88481C1CBAFA6200AB86E3 /* MGLGeometry.mm in Sources */,
DA88481F1CBAFA6200AB86E3 /* MGLMultiPoint.mm in Sources */,
DA88482B1CBAFA6200AB86E3 /* MGLTypes.m in Sources */,
4018B1C71CDC287F00F666AF /* MGLAnnotationView.m in Sources */,
DA88481D1CBAFA6200AB86E3 /* MGLMapCamera.mm in Sources */,
DA8848261CBAFA6200AB86E3 /* MGLPolygon.mm in Sources */,
DA8848521CBAFB9800AB86E3 /* MGLAPIClient.m in Sources */,
@@ -1296,6 +1327,7 @@
DAA4E4301CBB730400178DFB /* MGLLocationManager.m in Sources */,
DAA4E4321CBB730400178DFB /* MGLMapView.mm in Sources */,
DAA4E41E1CBB730400178DFB /* MGLMapCamera.mm in Sources */,
4018B1C81CDC287F00F666AF /* MGLAnnotationView.m in Sources */,
DAA4E4341CBB730400178DFB /* MGLUserLocationAnnotationView.m in Sources */,
DAA4E42C1CBB730400178DFB /* reachability.m in Sources */,
DAA4E4311CBB730400178DFB /* MGLMapboxEvents.m in Sources */,

View File

@@ -70,7 +70,8 @@
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA1DC9491CB6C1C2006E619F"
@@ -78,7 +79,7 @@
BlueprintName = "iosapp"
ReferencedContainer = "container:ios.xcodeproj">
</BuildableReference>
</MacroExpansion>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">

View File

@@ -0,0 +1,20 @@
#import <UIKit/UIKit.h>
#import "MGLTypes.h"
NS_ASSUME_NONNULL_BEGIN
@interface MGLAnnotationView : UIView
- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier;
/**
The string that identifies that this annotation view is reusable. (read-only)
*/
@property (nonatomic, readonly, nullable) NSString *reuseIdentifier;
- (void)prepareForReuse;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,40 @@
#import "MGLAnnotationView.h"
#import "MGLAnnotationView_Private.h"
@interface MGLAnnotationView ()
@property (nonatomic) id<MGLAnnotation> annotation;
@property (nonatomic, readwrite, nullable) NSString *reuseIdentifier;
@end
@implementation MGLAnnotationView
- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier
{
self = [super init];
if (self)
{
_reuseIdentifier = [reuseIdentifier copy];
}
return self;
}
- (void)prepareForReuse
{
// Intentionally left blank. The default implementation of this method does nothing.
}
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
// Allow mbgl to drive animation of this view’s bounds.
if ([event isEqualToString:@"bounds"])
{
return [NSNull null];
}
return [super actionForLayer:layer forKey:event];
}
@end

View File

@@ -0,0 +1,13 @@
#import "MGLAnnotationView.h"
#import "MGLAnnotation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MGLAnnotationView (Private)
@property (nonatomic) id<MGLAnnotation> annotation;
@property (nonatomic, readwrite, nullable) NSString *reuseIdentifier;
@end
NS_ASSUME_NONNULL_END

View File

@@ -8,6 +8,7 @@
NS_ASSUME_NONNULL_BEGIN
@class MGLAnnotationView;
@class MGLAnnotationImage;
@class MGLUserLocation;
@class MGLPolyline;
@@ -931,6 +932,8 @@ IB_DESIGNABLE
*/
- (nullable MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier;
- (nullable MGLAnnotationView *)dequeueReusableAnnotationViewWithIdentifier:(NSString *)identifier;
#pragma mark Managing Annotation Selections
/**
@@ -1151,6 +1154,8 @@ IB_DESIGNABLE
#pragma mark Managing the Display of Annotations
- (nullable MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id <MGLAnnotation>)annotation;
/**
Returns an image object to use for the marker for the specified point annotation object.

View File

@@ -37,6 +37,7 @@
#import "MGLUserLocationAnnotationView.h"
#import "MGLUserLocation_Private.h"
#import "MGLAnnotationImage_Private.h"
#import "MGLAnnotationView_Private.h"
#import "MGLMapboxEvents.h"
#import "MGLCompactCalloutView.h"
@@ -167,6 +168,8 @@ public:
/// The annotation’s image’s reuse identifier.
NSString *imageReuseIdentifier;
MGLAnnotationAccessibilityElement *accessibilityElement;
MGLAnnotationView *annotationView;
NSString *viewReuseIdentifier;
};
/** An accessibility element representing the MGLMapView at large. */
@@ -240,10 +243,12 @@ public:
BOOL _opaque;
NS_MUTABLE_ARRAY_OF(NSURL *) *_bundledStyleURLs;
MGLAnnotationContextMap _annotationContextsByAnnotationTag;
/// Tag of the selected annotation. If the user location annotation is selected, this ivar is set to `MGLAnnotationTagNotFound`.
MGLAnnotationTag _selectedAnnotationTag;
NS_MUTABLE_DICTIONARY_OF(NSString *, NS_MUTABLE_ARRAY_OF(MGLAnnotationView *) *) *_annotationViewReuseQueueByIdentifier;
BOOL _userLocationAnnotationIsSelected;
/// Size of the rectangle formed by unioning the maximum slop area around every annotation image.
CGSize _unionedAnnotationImageSize;
@@ -272,6 +277,8 @@ public:
BOOL _delegateHasLineWidthsForShapeAnnotations;
MGLCompassDirectionFormatter *_accessibilityCompassFormatter;
CGSize _largestAnnotationViewSize;
}
#pragma mark - Setup & Teardown -
@@ -404,6 +411,7 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
// Set up annotation management and selection state.
_annotationImagesByIdentifier = [NSMutableDictionary dictionary];
_annotationContextsByAnnotationTag = {};
_annotationViewReuseQueueByIdentifier = [NSMutableDictionary dictionary];
_selectedAnnotationTag = MGLAnnotationTagNotFound;
_annotationsNearbyLastTap = {};
@@ -1404,6 +1412,13 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
}
return;
}
MGLAnnotationView *hitAnnotationView = [self annotationViewAtPoint:tapPoint];
if (hitAnnotationView)
{
[self selectAnnotation:hitAnnotationView.annotation animated:YES];
return;
}
MGLAnnotationTag hitAnnotationTag = [self annotationTagAtPoint:tapPoint persistingResults:YES];
if (hitAnnotationTag != MGLAnnotationTagNotFound)
@@ -2713,6 +2728,11 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
{
return pair.second.annotation;
});
annotations.erase(std::remove_if(annotations.begin(), annotations.end(),
[](const id <MGLAnnotation> annotation) { return annotation == nullptr; }),
annotations.end());
return [NSArray arrayWithObjects:&annotations[0] count:annotations.size()];
}
@@ -2763,8 +2783,11 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
std::vector<mbgl::PointAnnotation> points;
std::vector<mbgl::ShapeAnnotation> shapes;
NSMutableArray *annotationImages = [NSMutableArray arrayWithCapacity:annotations.count];
NSMutableDictionary *annotationImagesForAnnotation = [NSMutableDictionary dictionary];
NSMutableDictionary *annotationViewsForAnnotation = [NSMutableDictionary dictionary];
BOOL delegateImplementsViewForAnnotation = [self.delegate respondsToSelector:@selector(mapView:viewForAnnotation:)];
BOOL delegateImplementsImageForPoint = [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)];
for (id <MGLAnnotation> annotation in annotations)
@@ -2777,32 +2800,51 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
}
else
{
MGLAnnotationImage *annotationImage;
if (delegateImplementsImageForPoint)
MGLAnnotationView *annotationView;
NSString *symbolName;
NSValue *annotationValue = [NSValue valueWithNonretainedObject:annotation];
if (delegateImplementsViewForAnnotation)
{
annotationImage = [self.delegate mapView:self imageForAnnotation:annotation];
}
if ( ! annotationImage)
{
annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName];
}
if ( ! annotationImage)
{
annotationImage = self.defaultAnnotationImage;
annotationView = [self annotationViewForAnnotation:annotation];
if (annotationView)
{
annotationViewsForAnnotation[annotationValue] = annotationView;
annotationView.center = [self convertCoordinate:annotation.coordinate toPointToView:self];
[self.glView addSubview:annotationView];
}
}
NSString *symbolName = annotationImage.styleIconIdentifier;
if ( ! symbolName)
{
symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier];
annotationImage.styleIconIdentifier = symbolName;
if ( ! annotationView) {
MGLAnnotationImage *annotationImage;
if (delegateImplementsImageForPoint)
{
annotationImage = [self.delegate mapView:self imageForAnnotation:annotation];
}
if ( ! annotationImage)
{
annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName];
}
if ( ! annotationImage)
{
annotationImage = self.defaultAnnotationImage;
}
symbolName = annotationImage.styleIconIdentifier;
if ( ! symbolName)
{
symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier];
annotationImage.styleIconIdentifier = symbolName;
}
if ( ! self.annotationImagesByIdentifier[annotationImage.reuseIdentifier])
{
[self installAnnotationImage:annotationImage];
}
annotationImagesForAnnotation[annotationValue] = annotationImage;
}
if ( ! self.annotationImagesByIdentifier[annotationImage.reuseIdentifier])
{
[self installAnnotationImage:annotationImage];
}
[annotationImages addObject:annotationImage];
points.emplace_back(MGLLatLngFromLocationCoordinate2D(annotation.coordinate), symbolName.UTF8String ?: "");
}
@@ -2810,20 +2852,30 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
if (points.size())
{
// refactor this to build contexts above and just associate with tags here
std::vector<MGLAnnotationTag> annotationTags = _mbglMap->addPointAnnotations(points);
for (size_t i = 0; i < annotationTags.size(); ++i)
{
MGLAnnotationTag annotationTag = annotationTags[i];
MGLAnnotationImage *annotationImage = annotationImages[i];
annotationImage.styleIconIdentifier = @(points[i].icon.c_str());
id <MGLAnnotation> annotation = annotations[i];
id<MGLAnnotation> annotation = annotations[i];
NSValue *annotationValue = [NSValue valueWithNonretainedObject:annotation];
MGLAnnotationContext context;
context.annotation = annotation;
context.imageReuseIdentifier = annotationImage.reuseIdentifier;
_annotationContextsByAnnotationTag[annotationTag] = context;
MGLAnnotationImage *annotationImage = annotationImagesForAnnotation[annotationValue];
if (annotationImage) {
context.imageReuseIdentifier = annotationImage.reuseIdentifier;
}
MGLAnnotationView *annotationView = annotationViewsForAnnotation[annotationValue];
if (annotationView) {
context.annotationView = annotationView;
context.viewReuseIdentifier = annotationView.reuseIdentifier;
}
MGLAnnotationTag annotationTag = annotationTags[i];
_annotationContextsByAnnotationTag[annotationTag] = context;
if ([annotation isKindOfClass:[NSObject class]]) {
NSAssert(![annotation isKindOfClass:[MGLMultiPoint class]], @"Point annotation should not be MGLMultiPoint.");
[(NSObject *)annotation addObserver:self forKeyPath:@"coordinate" options:0 context:(void *)(NSUInteger)annotationTag];
@@ -2864,6 +2916,20 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
return annotationImage;
}
- (MGLAnnotationView *)annotationViewForAnnotation:(id<MGLAnnotation>)annotation
{
MGLAnnotationView *annotationView = [self.delegate mapView:self viewForAnnotation:annotation];
if (annotationView)
{
annotationView.annotation = annotation;
CGRect bounds = UIEdgeInsetsInsetRect({ CGPointZero, annotationView.frame.size }, annotationView.alignmentRectInsets);
_largestAnnotationViewSize = CGSizeMake(bounds.size.width / 2.0, bounds.size.height / 2.0);
}
return annotationView;
}
- (double)alphaForShapeAnnotation:(MGLShape *)annotation
{
if (_delegateHasAlphasForShapeAnnotations)
@@ -2962,6 +3028,11 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
{
continue;
}
MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
MGLAnnotationView *annotationView = annotationContext.annotationView;
[annotationView removeFromSuperview];
annotationTagsToRemove.push_back(annotationTag);
if (annotationTag == _selectedAnnotationTag)
@@ -3025,6 +3096,35 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
return self.annotationImagesByIdentifier[identifier];
}
- (nullable MGLAnnotationView *)dequeueReusableAnnotationViewWithIdentifier:(NSString *)identifier
{
NSMutableArray *annotationViewReuseQueue = [self annotationViewReuseQueueForIdentifier:identifier];
MGLAnnotationView *reusableView = annotationViewReuseQueue.firstObject;
[reusableView prepareForReuse];
[annotationViewReuseQueue removeObject:reusableView];
return reusableView;
}
- (MGLAnnotationView *)annotationViewAtPoint:(CGPoint)point
{
std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:self.bounds];
for(auto const& annotationTag: annotationTags)
{
auto &annotationContext = _annotationContextsByAnnotationTag[annotationTag];
MGLAnnotationView *annotationView = annotationContext.annotationView;
CGPoint convertedPoint = [self convertPoint:point toView:annotationView];
if ([annotationView pointInside:convertedPoint withEvent:nil])
{
return annotationView;
}
}
return nil;
}
/**
Returns the tag of the annotation at the given point in the view.
@@ -3067,6 +3167,8 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
NSAssert(annotation, @"Unknown annotation found nearby tap");
MGLAnnotationContext annotationContext = _annotationContextsByAnnotationTag[annotationTag];
MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
if ( ! annotationImage.enabled)
{
@@ -3076,8 +3178,10 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
// Filter out the annotation if the fattened finger didn’t land
// within the image’s alignment rect.
CGRect annotationRect = [self frameOfImage:annotationImage.image ?: fallbackImage centeredAtCoordinate:annotation.coordinate];
return !!!CGRectIntersectsRect(annotationRect, hitRect);
});
nearbyAnnotations.resize(std::distance(nearbyAnnotations.begin(), end));
}
@@ -3224,10 +3328,26 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
if (annotationTag == MGLAnnotationTagNotFound && annotation != self.userLocation)
{
[self addAnnotation:annotation];
annotationTag = [self annotationTagForAnnotation:annotation];
if (annotationTag == MGLAnnotationTagNotFound) return;
}
// The annotation can’t be selected if no part of it is hittable.
// By default attempt to use the GL annotation image frame as the positioning rect.
CGRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag];
if (annotation != self.userLocation)
{
MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
if (annotationContext.annotationView)
{
// Annotations represented by views use the view frame as the positioning rect.
positioningRect = annotationContext.annotationView.frame;
}
}
// 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;
@@ -3320,6 +3440,8 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
/// and is appropriate for positioning a popover.
- (CGRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annotationTag
{
MGLAnnotationContext annotationContext = _annotationContextsByAnnotationTag[annotationTag];
id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
if ( ! annotation)
{
@@ -3337,6 +3459,7 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
CGRect positioningRect = [self frameOfImage:image centeredAtCoordinate:annotation.coordinate];
positioningRect.origin.x -= 0.5;
return CGRectInset(positioningRect, -MGLAnnotationImagePaddingForCallout,
-MGLAnnotationImagePaddingForCallout);
}
@@ -4221,6 +4344,7 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
{
[self.delegate mapViewDidFinishRenderingFrame:self fullyRendered:(change == mbgl::MapChangeDidFinishRenderingFrameFullyRendered)];
}
[self updateAnnotationViews];
break;
}
}
@@ -4231,6 +4355,85 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
[self updateUserLocationAnnotationViewAnimatedWithDuration:0];
}
- (void)updateAnnotationViews
{
BOOL delegateImplementsViewForAnnotation = [self.delegate respondsToSelector:@selector(mapView:viewForAnnotation:)];
if (!delegateImplementsViewForAnnotation)
{
return;
}
// Update all visible annotation views
std::set<MGLAnnotationTag> visibleTags;
std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:CGRectInset(self.bounds, -_largestAnnotationViewSize.width - MGLAnnotationUpdateViewportOutset.width, -_largestAnnotationViewSize.height - MGLAnnotationUpdateViewportOutset.width)];
for(auto const& annotationTag: annotationTags)
{
auto &annotationContext = _annotationContextsByAnnotationTag[annotationTag];
id<MGLAnnotation> annotation = annotationContext.annotation;
// If there is no annotation view at this point, it means the context's view was reused by some
// other context so we need to reuse or make a new view.
if (!annotationContext.annotationView)
{
MGLAnnotationView *annotationView = [self annotationViewForAnnotation:annotation];
if (annotationView)
{
// If the annotation view has no superview it means it was never used before so add it
if (!annotationView.superview)
{
[self.glView addSubview:annotationView];
}
annotationContext.annotationView = annotationView;
}
}
annotationContext.annotationView.center = [self convertCoordinate:annotation.coordinate toPointToView:self];;
visibleTags.insert(annotationTag);
}
// Hide and add offscreen annotation views to reuse queue
for (auto &pair : _annotationContextsByAnnotationTag)
{
MGLAnnotationTag annotationTag = pair.first;
MGLAnnotationContext &annotationContext = pair.second;
MGLAnnotationView *annotationView = annotationContext.annotationView;
const bool tagIsNotVisible = visibleTags.find(annotationTag) == visibleTags.end();
// The call to `annotationTagsInRect:` (above) does not return the correct result when the
// map is tilted and the user is scrolling quickly. So, some annotation views get stuck in
// a limbo state where they are onscreen and put on the reuse queue. Hiding the views hides
// the bug until we fix the result of `annotationTagsInRect:`.
annotationView.hidden = tagIsNotVisible;
if (annotationView && annotationView.reuseIdentifier && tagIsNotVisible)
{
[self enqueueAnnotationViewForAnnotationContext:annotationContext];
}
}
}
- (void)enqueueAnnotationViewForAnnotationContext:(MGLAnnotationContext &)annotationContext
{
MGLAnnotationView *annotationView = annotationContext.annotationView;
if (!annotationView) return;
if (annotationContext.viewReuseIdentifier)
{
NSMutableArray *annotationViewReuseQueue = [self annotationViewReuseQueueForIdentifier:annotationContext.viewReuseIdentifier];
if (![annotationViewReuseQueue containsObject:annotationView])
{
[annotationViewReuseQueue addObject:annotationView];
annotationContext.annotationView = nil;
}
}
}
- (void)updateUserLocationAnnotationViewAnimatedWithDuration:(NSTimeInterval)duration
{
MGLUserLocationAnnotationView *annotationView = self.userLocationAnnotationView;
@@ -4492,6 +4695,15 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
views:views]];
}
- (NS_MUTABLE_ARRAY_OF(MGLAnnotationView *) *)annotationViewReuseQueueForIdentifier:(NSString *)identifier {
if (!_annotationViewReuseQueueByIdentifier[identifier])
{
_annotationViewReuseQueueByIdentifier[identifier] = [NSMutableArray array];
}
return _annotationViewReuseQueueByIdentifier[identifier];
}
class MBGLView : public mbgl::View
{
public:

View File

@@ -6,6 +6,7 @@ FOUNDATION_EXPORT double MapboxVersionNumber;
/// Project version string for Mapbox.
FOUNDATION_EXPORT const unsigned char MapboxVersionString[];
#import "MGLAnnotationView.h"
#import "MGLAccountManager.h"
#import "MGLAnnotation.h"
#import "MGLAnnotationImage.h"