Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
7537 lines
288 KiB
Plaintext
7537 lines
288 KiB
Plaintext
#import "MLNMapView_Private.h"
|
||
#import "MLNMapView+Impl.h"
|
||
|
||
#include <mbgl/map/map.hpp>
|
||
#include <mbgl/map/map_options.hpp>
|
||
#include <mbgl/annotation/annotation.hpp>
|
||
#include <mbgl/map/camera.hpp>
|
||
#include <mbgl/map/mode.hpp>
|
||
#include <mbgl/util/platform.hpp>
|
||
#include <mbgl/storage/resource_options.hpp>
|
||
#include <mbgl/storage/network_status.hpp>
|
||
#include <mbgl/style/style.hpp>
|
||
#include <mbgl/style/image.hpp>
|
||
#include <mbgl/style/transition_options.hpp>
|
||
#include <mbgl/style/layers/custom_layer.hpp>
|
||
#include <mbgl/renderer/renderer.hpp>
|
||
#include <mbgl/math/wrap.hpp>
|
||
#include <mbgl/util/client_options.hpp>
|
||
#include <mbgl/util/exception.hpp>
|
||
#include <mbgl/util/geo.hpp>
|
||
#include <mbgl/util/constants.hpp>
|
||
#include <mbgl/util/image.hpp>
|
||
#include <mbgl/util/projection.hpp>
|
||
#include <mbgl/util/chrono.hpp>
|
||
#include <mbgl/util/run_loop.hpp>
|
||
#include <mbgl/util/string.hpp>
|
||
#include <mbgl/util/projection.hpp>
|
||
|
||
#import "Mapbox.h"
|
||
#import "MLNShape_Private.h"
|
||
#import "MLNFeature_Private.h"
|
||
#import "MLNGeometry_Private.h"
|
||
#import "MLNMultiPoint_Private.h"
|
||
#import "MLNOfflineStorage_Private.h"
|
||
#import "MLNVectorTileSource_Private.h"
|
||
#import "MLNFoundation_Private.h"
|
||
#import "MLNRendererFrontend.h"
|
||
#import "MLNRendererConfiguration.h"
|
||
|
||
#import "NSBundle+MLNAdditions.h"
|
||
#import "NSDate+MLNAdditions.h"
|
||
#import "NSException+MLNAdditions.h"
|
||
#import "NSPredicate+MLNPrivateAdditions.h"
|
||
#import "NSString+MLNAdditions.h"
|
||
#import "NSURL+MLNAdditions.h"
|
||
#import "UIDevice+MLNAdditions.h"
|
||
#import "UIImage+MLNAdditions.h"
|
||
#import "UIViewController+MLNAdditions.h"
|
||
#import "UIView+MLNAdditions.h"
|
||
|
||
#import "MLNFaux3DUserLocationAnnotationView.h"
|
||
#import "MLNUserLocationAnnotationView.h"
|
||
#import "MLNUserLocationAnnotationView_Private.h"
|
||
#import "MLNUserLocation_Private.h"
|
||
#import "MLNAnnotationImage_Private.h"
|
||
#import "MLNAnnotationView_Private.h"
|
||
#import "MLNCompassButton_Private.h"
|
||
#import "MLNScaleBar.h"
|
||
#import "MLNStyle_Private.h"
|
||
#import "MLNStyleLayer_Private.h"
|
||
#import "MLNCompactCalloutView.h"
|
||
#import "MLNAnnotationContainerView.h"
|
||
#import "MLNAnnotationContainerView_Private.h"
|
||
#import "MLNAttributionInfo_Private.h"
|
||
#import "MLNMapAccessibilityElement.h"
|
||
#import "MLNLocationManager_Private.h"
|
||
#import "MLNLoggingConfiguration_Private.h"
|
||
#import "MLNNetworkConfiguration_Private.h"
|
||
#import "MLNReachability.h"
|
||
#import "MLNSettings_Private.h"
|
||
#import "MLNMapProjection.h"
|
||
|
||
#include <algorithm>
|
||
#include <cstdlib>
|
||
#include <map>
|
||
#include <unordered_set>
|
||
#if TARGET_IPHONE_SIMULATOR & (TARGET_CPU_X86 | TARGET_CPU_X86_64)
|
||
#include <sys/sysctl.h>
|
||
|
||
// The m1 simulator's gesture's velocity is wrong and here is a workaround to fix it.
|
||
// These custom gestures will calculate the correct velocity.
|
||
// This may because the system time is incorrect.
|
||
|
||
@interface MBGLPanGesture : UIPanGestureRecognizer
|
||
@end
|
||
|
||
@interface MBGLPinchGesture : UIPinchGestureRecognizer
|
||
@end
|
||
|
||
@interface MBGLRotationGesture : UIRotationGestureRecognizer
|
||
@end
|
||
|
||
@implementation MBGLPanGesture {
|
||
bool customVelocity;
|
||
NSTimeInterval _lastTime;
|
||
CGPoint _lastTouch;
|
||
CGPoint __velocity;
|
||
}
|
||
|
||
- (CGPoint)velocityInView:(nullable UIView *)view {
|
||
if (customVelocity) {
|
||
return __velocity;
|
||
}
|
||
|
||
return [super velocityInView:view];
|
||
}
|
||
|
||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||
_lastTime = NSProcessInfo.processInfo.systemUptime;
|
||
|
||
UITouch *touch = [touches anyObject];
|
||
_lastTouch = [touch locationInView:self.view];
|
||
customVelocity = true;
|
||
|
||
[super touchesBegan:touches withEvent:event];
|
||
}
|
||
|
||
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||
if (customVelocity && touches.count) {
|
||
CFTimeInterval now = NSProcessInfo.processInfo.systemUptime;
|
||
UITouch *touch = [touches anyObject];
|
||
CGPoint touchLoc = [touch locationInView:self.view];
|
||
auto delta = now - _lastTime;
|
||
if (delta > 0) {
|
||
__velocity.x = ((touchLoc.x - _lastTouch.x) / delta) * 0.9 + __velocity.x * 0.1;
|
||
__velocity.y = ((touchLoc.y - _lastTouch.y) / delta) * 0.9 + __velocity.y * 0.1;
|
||
}
|
||
_lastTouch = touchLoc;
|
||
_lastTime = now;
|
||
}
|
||
[super touchesMoved:touches withEvent:event];
|
||
|
||
}
|
||
@end
|
||
|
||
@implementation MBGLPinchGesture {
|
||
bool customVelocity;
|
||
NSTimeInterval _lastTime;
|
||
CGFloat _lastScale;
|
||
CGFloat __velocity;
|
||
}
|
||
|
||
- (CGFloat)velocity {
|
||
if (customVelocity) {
|
||
return __velocity;
|
||
}
|
||
|
||
return [super velocity];
|
||
}
|
||
|
||
|
||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||
_lastTime = NSProcessInfo.processInfo.systemUptime;
|
||
|
||
_lastScale = 0;
|
||
customVelocity = true;
|
||
|
||
[super touchesBegan:touches withEvent:event];
|
||
}
|
||
|
||
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||
[super touchesMoved:touches withEvent:event];
|
||
|
||
if (customVelocity && touches.count) {
|
||
CFTimeInterval now = NSProcessInfo.processInfo.systemUptime;
|
||
CGFloat scale = [super scale];
|
||
auto delta = now - _lastTime;
|
||
if (delta > 0) {
|
||
__velocity = ((scale - _lastScale) / delta) * 0.9 + __velocity * 0.1;
|
||
}
|
||
_lastScale = scale;
|
||
_lastTime = now;
|
||
}
|
||
}
|
||
@end
|
||
|
||
@implementation MBGLRotationGesture {
|
||
bool customVelocity;
|
||
NSTimeInterval _lastTime;
|
||
CGFloat _lastRotation;
|
||
CGFloat __velocity;
|
||
}
|
||
|
||
- (CGFloat)velocity {
|
||
if (customVelocity) {
|
||
return __velocity;
|
||
}
|
||
|
||
return [super velocity];
|
||
}
|
||
|
||
|
||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||
_lastTime = NSProcessInfo.processInfo.systemUptime;
|
||
|
||
_lastRotation = 0;
|
||
customVelocity = true;
|
||
|
||
[super touchesBegan:touches withEvent:event];
|
||
}
|
||
|
||
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||
[super touchesMoved:touches withEvent:event];
|
||
|
||
if (customVelocity && touches.count) {
|
||
CFTimeInterval now = NSProcessInfo.processInfo.systemUptime;
|
||
CGFloat rotation = [super rotation];
|
||
auto delta = now - _lastTime;
|
||
if (delta > 0) {
|
||
__velocity = ((rotation - _lastRotation) / delta) * 0.9 + __velocity * 0.1;
|
||
}
|
||
_lastRotation = rotation;
|
||
_lastTime = now;
|
||
}
|
||
}
|
||
@end
|
||
|
||
int processIsTranslated() {
|
||
int ret = 0;
|
||
size_t size = sizeof(ret);
|
||
|
||
// Call the sysctl and if successful return the result
|
||
if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) != -1)
|
||
return ret;
|
||
// If "sysctl.proc_translated" is not present then must be native
|
||
if (errno == ENOENT)
|
||
return 0;
|
||
return -1;
|
||
}
|
||
|
||
#endif
|
||
|
||
class MLNAnnotationContext;
|
||
|
||
const MLNMapViewDecelerationRate MLNMapViewDecelerationRateNormal = UIScrollViewDecelerationRateNormal;
|
||
const MLNMapViewDecelerationRate MLNMapViewDecelerationRateFast = UIScrollViewDecelerationRateFast;
|
||
const MLNMapViewDecelerationRate MLNMapViewDecelerationRateImmediate = 0.0;
|
||
|
||
const MLNMapViewPreferredFramesPerSecond MLNMapViewPreferredFramesPerSecondDefault = -1;
|
||
const MLNMapViewPreferredFramesPerSecond MLNMapViewPreferredFramesPerSecondLowPower = 30;
|
||
const MLNMapViewPreferredFramesPerSecond MLNMapViewPreferredFramesPerSecondMaximum = 0;
|
||
|
||
const MLNExceptionName MLNMissingLocationServicesUsageDescriptionException = @"MLNMissingLocationServicesUsageDescriptionException";
|
||
const MLNExceptionName MLNUserLocationAnnotationTypeException = @"MLNUserLocationAnnotationTypeException";
|
||
const MLNExceptionName MLNUnderlyingMapUnavailableException = @"MLNUnderlyingMapUnavailableException";
|
||
|
||
const CGPoint MLNOrnamentDefaultPositionOffset = CGPointMake(8, 8);
|
||
|
||
/// Indicates the manner in which the map view is tracking the user location.
|
||
typedef NS_ENUM(NSUInteger, MLNUserTrackingState) {
|
||
/// The map view is not yet tracking the user location.
|
||
MLNUserTrackingStatePossible = 0,
|
||
/// The map view has begun to move to the first reported user location.
|
||
MLNUserTrackingStateBegan,
|
||
/// The map view begins a significant transition.
|
||
MLNUserTrackingStateBeginSignificantTransition,
|
||
/// The map view has finished moving to the first reported user location.
|
||
MLNUserTrackingStateChanged,
|
||
};
|
||
|
||
const NSTimeInterval MLNAnimationDuration = 0.3;
|
||
|
||
/// Duration of an animation due to a user location update, typically chosen to
|
||
/// match a typical interval between user location updates.
|
||
const NSTimeInterval MLNUserLocationAnimationDuration = 1.0;
|
||
|
||
/// Distance between the map view’s edge and that of the user location
|
||
/// annotation view.
|
||
const UIEdgeInsets MLNUserLocationAnnotationViewInset = UIEdgeInsetsMake(50, 0, 50, 0);
|
||
|
||
const CGSize MLNAnnotationUpdateViewportOutset = {150, 150};
|
||
const CGFloat MLNMinimumZoom = 3;
|
||
|
||
/// Minimum initial zoom level when entering user tracking mode.
|
||
const double MLNMinimumZoomLevelForUserTracking = 10.5;
|
||
|
||
/// Initial zoom level when entering user tracking mode from a low zoom level.
|
||
const double MLNDefaultZoomLevelForUserTracking = 14.0;
|
||
|
||
/// Tolerance for snapping to true north, measured in degrees in either direction.
|
||
const CLLocationDirection MLNToleranceForSnappingToNorth = 7;
|
||
|
||
/// Distance threshold to stop the camera while animating.
|
||
const CLLocationDistance MLNDistanceThresholdForCameraPause = 500;
|
||
|
||
/// Rotation threshold while a pinch gesture is occurring.
|
||
static NSString * const MLNRotationThresholdWhileZoomingKey = @"MLNRotationThresholdWhileZooming";
|
||
|
||
/// Reuse identifier and file name of the default point annotation image.
|
||
static NSString * const MLNDefaultStyleMarkerSymbolName = @"default_marker";
|
||
|
||
/// Reuse identifier and file name of the invisible point annotation image used
|
||
/// by annotations that are visually backed by MLNAnnotationView objects
|
||
static NSString * const MLNInvisibleStyleMarkerSymbolName = @"invisible_marker";
|
||
|
||
/// Prefix that denotes a sprite installed by MLNMapView, to avoid collisions
|
||
/// with style-defined sprites.
|
||
NSString * const MLNAnnotationSpritePrefix = @"org.maplibre.sprites.";
|
||
|
||
/// Slop area around the hit testing point, allowing for imprecise annotation selection.
|
||
const CGFloat MLNAnnotationImagePaddingForHitTest = 5;
|
||
|
||
/// Distance from the callout’s anchor point to the annotation it points to.
|
||
const CGFloat MLNAnnotationImagePaddingForCallout = 1;
|
||
|
||
const CGSize MLNAnnotationAccessibilityElementMinimumSize = CGSizeMake(10, 10);
|
||
|
||
/// The number of view annotations (excluding the user location view) that must
|
||
/// be descendents of ``MLNMapView`` before presentsWithTransaction is enabled.
|
||
static const NSUInteger MLNPresentsWithTransactionAnnotationCount = 0;
|
||
|
||
/// An indication that the requested annotation was not found or is nonexistent.
|
||
enum { MLNAnnotationTagNotFound = UINT32_MAX };
|
||
|
||
/// The threshold used to consider when a tilt gesture should start.
|
||
const CLLocationDegrees MLNHorizontalTiltToleranceDegrees = 45.0;
|
||
|
||
/// Mapping from an annotation tag to metadata about that annotation, including
|
||
/// the annotation itself.
|
||
typedef std::unordered_map<MLNAnnotationTag, MLNAnnotationContext> MLNAnnotationTagContextMap;
|
||
|
||
/// Mapping from an annotation object to an annotation tag.
|
||
typedef std::map<id<MLNAnnotation>, MLNAnnotationTag> MLNAnnotationObjectTagMap;
|
||
|
||
mbgl::util::UnitBezier MLNUnitBezierForMediaTimingFunction(CAMediaTimingFunction *function)
|
||
{
|
||
if ( ! function)
|
||
{
|
||
function = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
|
||
}
|
||
float p1[2], p2[2];
|
||
[function getControlPointAtIndex:0 values:p1];
|
||
[function getControlPointAtIndex:1 values:p2];
|
||
return { p1[0], p1[1], p2[0], p2[1] };
|
||
}
|
||
|
||
/// Lightweight container for metadata about an annotation, including the annotation itself.
|
||
class MLNAnnotationContext {
|
||
public:
|
||
id <MLNAnnotation> annotation;
|
||
/// The annotation’s image’s reuse identifier.
|
||
NSString *imageReuseIdentifier;
|
||
MLNAnnotationAccessibilityElement *accessibilityElement;
|
||
MLNAnnotationView *annotationView;
|
||
NSString *viewReuseIdentifier;
|
||
};
|
||
|
||
// MARK: - Private -
|
||
|
||
@interface MLNMapView () <UIGestureRecognizerDelegate,
|
||
MLNLocationManagerDelegate,
|
||
MLNSMCalloutViewDelegate,
|
||
MLNCalloutViewDelegate,
|
||
MLNMultiPointDelegate,
|
||
MLNAnnotationImageDelegate>
|
||
|
||
@property (nonatomic) NSMutableArray<NSLayoutConstraint *> *scaleBarConstraints;
|
||
@property (nonatomic, readwrite) MLNScaleBar *scaleBar;
|
||
@property (nonatomic, readwrite) MLNCompassButton *compassView;
|
||
@property (nonatomic) NSMutableArray<NSLayoutConstraint *> *compassViewConstraints;
|
||
@property (nonatomic, readwrite) UIImageView *logoView;
|
||
@property (nonatomic) NSMutableArray<NSLayoutConstraint *> *logoViewConstraints;
|
||
@property (nonatomic, readwrite) UIButton *attributionButton;
|
||
@property (nonatomic) NSMutableArray<NSLayoutConstraint *> *attributionButtonConstraints;
|
||
@property (nonatomic, weak) UIAlertController *attributionController;
|
||
|
||
@property (nonatomic, readwrite) MLNStyle *style;
|
||
|
||
@property (nonatomic) UITapGestureRecognizer *singleTapGestureRecognizer;
|
||
@property (nonatomic) UITapGestureRecognizer *doubleTap;
|
||
@property (nonatomic) UITapGestureRecognizer *twoFingerTap;
|
||
@property (nonatomic) UIPanGestureRecognizer *pan;
|
||
@property (nonatomic) UIPinchGestureRecognizer *pinch;
|
||
@property (nonatomic) UIRotationGestureRecognizer *rotate;
|
||
@property (nonatomic) UILongPressGestureRecognizer *quickZoom;
|
||
@property (nonatomic) UIPanGestureRecognizer *twoFingerDrag;
|
||
|
||
@property (nonatomic) UIInterfaceOrientation currentOrientation;
|
||
@property (nonatomic) UIInterfaceOrientationMask applicationSupportedInterfaceOrientations;
|
||
|
||
@property (nonatomic) MLNCameraChangeReason cameraChangeReasonBitmask;
|
||
|
||
/// Mapping from reusable identifiers to annotation images.
|
||
@property (nonatomic) NSMutableDictionary<NSString *, MLNAnnotationImage *> *annotationImagesByIdentifier;
|
||
|
||
/// Currently shown popover representing the selected annotation.
|
||
@property (nonatomic) UIView<MLNCalloutView> *calloutViewForSelectedAnnotation;
|
||
|
||
/// Anchor coordinate from which to present callout views (for example, for shapes this
|
||
/// could be the touch point rather than its centroid)
|
||
@property (nonatomic) CLLocationCoordinate2D anchorCoordinateForSelectedAnnotation;
|
||
|
||
@property (nonatomic) MLNUserLocationAnnotationView *userLocationAnnotationView;
|
||
|
||
/// Indicates how thoroughly the map view is tracking the user location.
|
||
@property (nonatomic) MLNUserTrackingState userTrackingState;
|
||
@property (nonatomic) CGFloat scale;
|
||
@property (nonatomic) CGFloat angle;
|
||
@property (nonatomic) CGFloat quickZoomStart;
|
||
/// Dormant means there is no underlying GL view (typically in the background)
|
||
@property (nonatomic, getter=isDormant) BOOL dormant;
|
||
@property (nonatomic, readonly, getter=isDisplayLinkActive) BOOL displayLinkActive;
|
||
@property (nonatomic, readonly, getter=isRotationAllowed) BOOL rotationAllowed;
|
||
@property (nonatomic) CGFloat rotationThresholdWhileZooming;
|
||
@property (nonatomic) CGFloat rotationBeforeThresholdMet;
|
||
@property (nonatomic) BOOL isZooming;
|
||
@property (nonatomic) BOOL isRotating;
|
||
@property (nonatomic) BOOL shouldTriggerHapticFeedbackForCompass;
|
||
@property (nonatomic) MLNMapViewProxyAccessibilityElement *mapViewProxyAccessibilityElement;
|
||
@property (nonatomic) MLNAnnotationContainerView *annotationContainerView;
|
||
@property (nonatomic) MLNUserLocation *userLocation;
|
||
@property (nonatomic) NSMutableDictionary<NSString *, NSMutableArray<MLNAnnotationView *> *> *annotationViewReuseQueueByIdentifier;
|
||
@property (nonatomic, readonly) BOOL enablePresentsWithTransaction;
|
||
@property (nonatomic) NSMutableArray *pendingCompletionBlocks;
|
||
|
||
/// Experimental rendering performance measurement.
|
||
@property (nonatomic) BOOL experimental_enableFrameRateMeasurement;
|
||
@property (nonatomic) CGFloat averageFrameRate;
|
||
@property (nonatomic) CFTimeInterval frameTime;
|
||
@property (nonatomic) CFTimeInterval averageFrameTime;
|
||
|
||
/// Residual properties (saved on app termination)
|
||
@property (nonatomic) BOOL terminated;
|
||
@property (nonatomic, copy) MLNMapCamera *residualCamera;
|
||
@property (nonatomic) MLNMapDebugMaskOptions residualDebugMask;
|
||
@property (nonatomic, copy) NSURL *residualStyleURL;
|
||
|
||
/// Tilt gesture recognizer helper
|
||
@property (nonatomic, assign) CGPoint dragGestureMiddlePoint;
|
||
|
||
/// This property is used to keep track of the view's safe edge insets
|
||
/// and calculate the ornament's position
|
||
@property (nonatomic, assign) UIEdgeInsets safeMapViewContentInsets;
|
||
@property (nonatomic, strong) NSNumber *automaticallyAdjustContentInsetHolder;
|
||
|
||
// Display Link
|
||
@property (nonatomic, weak) UIScreen *displayLinkScreen;
|
||
@property (nonatomic) CADisplayLink *displayLink;
|
||
@property (nonatomic, assign) BOOL needsDisplayRefresh;
|
||
|
||
@end
|
||
|
||
@implementation MLNMapView
|
||
{
|
||
std::unique_ptr<mbgl::Map> _mbglMap;
|
||
std::unique_ptr<MLNMapViewImpl> _mbglView;
|
||
std::unique_ptr<MLNRenderFrontend> _rendererFrontend;
|
||
|
||
BOOL _opaque;
|
||
|
||
MLNAnnotationTagContextMap _annotationContextsByAnnotationTag;
|
||
MLNAnnotationObjectTagMap _annotationTagsByAnnotation;
|
||
|
||
/// Tag of the selected annotation. If the user location annotation is selected, this ivar is set to ``MLNAnnotationTagNotFound``.
|
||
MLNAnnotationTag _selectedAnnotationTag;
|
||
|
||
BOOL _userLocationAnnotationIsSelected;
|
||
/// Size of the rectangle formed by unioning the maximum slop area around every annotation image and annotation image view.
|
||
CGSize _unionedAnnotationRepresentationSize;
|
||
CGSize _largestAnnotationViewSize;
|
||
std::vector<MLNAnnotationTag> _annotationsNearbyLastTap;
|
||
CGPoint _initialImplicitCalloutViewOffset;
|
||
NSDate *_userLocationAnimationCompletionDate;
|
||
/// True if a willChange notification has been issued for shape annotation layers and a didChange notification is pending.
|
||
BOOL _isChangingAnnotationLayers;
|
||
BOOL _isWaitingForRedundantReachableNotification;
|
||
|
||
CLLocationDegrees _pendingLatitude;
|
||
CLLocationDegrees _pendingLongitude;
|
||
|
||
NSInteger _changeDelimiterSuppressionDepth;
|
||
|
||
/// Center of the pinch gesture on the previous iteration of the gesture.
|
||
CGPoint _previousPinchCenterPoint;
|
||
NSUInteger _previousPinchNumberOfTouches;
|
||
|
||
CLLocationDistance _distanceFromOldUserLocation;
|
||
|
||
BOOL _delegateHasAlphasForShapeAnnotations;
|
||
BOOL _delegateHasStrokeColorsForShapeAnnotations;
|
||
BOOL _delegateHasFillColorsForShapeAnnotations;
|
||
BOOL _delegateHasLineWidthsForShapeAnnotations;
|
||
|
||
NSArray<id <MLNFeature>> *_visiblePlaceFeatures;
|
||
NSArray<id <MLNFeature>> *_visibleRoadFeatures;
|
||
NSMutableSet<MLNFeatureAccessibilityElement *> *_featureAccessibilityElements;
|
||
BOOL _accessibilityValueAnnouncementIsPending;
|
||
|
||
MLNReachability *_reachability;
|
||
|
||
/// Experimental rendering performance measurement.
|
||
CFTimeInterval _frameCounterStartTime;
|
||
NSInteger _frameCount;
|
||
CFTimeInterval _frameDurations;
|
||
}
|
||
|
||
// MARK: - Setup & Teardown -
|
||
|
||
- (instancetype)initWithFrame:(CGRect)frame
|
||
{
|
||
if (self = [super initWithFrame:frame])
|
||
{
|
||
MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class]));
|
||
MLNLogDebug(@"Initializing frame: %@", NSStringFromCGRect(frame));
|
||
[self commonInit];
|
||
self.styleURL = nil;
|
||
MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class]));
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (instancetype)initWithFrame:(CGRect)frame styleURL:(nullable NSURL *)styleURL
|
||
{
|
||
if (self = [super initWithFrame:frame])
|
||
{
|
||
MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class]));
|
||
MLNLogDebug(@"Initializing frame: %@ styleURL: %@", NSStringFromCGRect(frame), styleURL);
|
||
[self commonInit];
|
||
self.styleURL = styleURL;
|
||
MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class]));
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (instancetype)initWithCoder:(nonnull NSCoder *)decoder
|
||
{
|
||
if (self = [super initWithCoder:decoder])
|
||
{
|
||
MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class]));
|
||
[self commonInit];
|
||
self.styleURL = nil;
|
||
MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class]));
|
||
}
|
||
return self;
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingStyle
|
||
{
|
||
return [NSSet setWithObject:@"styleURL"];
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingStyleURL
|
||
{
|
||
return [NSSet setWithObjects:@"styleURL__", nil];
|
||
}
|
||
|
||
- (nonnull NSURL *)styleURL
|
||
{
|
||
if (!_mbglMap)
|
||
{
|
||
NSAssert(self.terminated, @"_mbglMap should only be unavailable during app termination");
|
||
return self.residualStyleURL;
|
||
}
|
||
|
||
NSString *styleURLString = @(self.mbglMap.getStyle().getURL().c_str()).mgl_stringOrNilIfEmpty;
|
||
MLNAssert(styleURLString, @"Invalid style URL string %@", styleURLString);
|
||
return styleURLString ? [NSURL URLWithString:styleURLString] : nil;
|
||
}
|
||
|
||
- (void)setStyleURL:(nullable NSURL *)styleURL
|
||
{
|
||
if ( ! styleURL)
|
||
{
|
||
styleURL = [MLNStyle defaultStyleURL];
|
||
}
|
||
MLNLogDebug(@"Setting styleURL: %@", styleURL);
|
||
styleURL = styleURL.mgl_URLByStandardizingScheme;
|
||
self.style = nil;
|
||
self.mbglMap.getStyle().loadURL([[styleURL absoluteString] UTF8String]);
|
||
}
|
||
|
||
- (IBAction)reloadStyle:(__unused id)sender {
|
||
MLNLogInfo(@"Reloading style.");
|
||
NSURL *styleURL = self.styleURL;
|
||
self.mbglMap.getStyle().loadURL("");
|
||
self.styleURL = styleURL;
|
||
}
|
||
|
||
- (mbgl::Map &)mbglMap
|
||
{
|
||
if (!_mbglMap)
|
||
{
|
||
[NSException raise:MLNUnderlyingMapUnavailableException
|
||
format:@"The underlying map is not available - this happens during app termination"];
|
||
}
|
||
return *_mbglMap;
|
||
}
|
||
|
||
- (mbgl::Renderer *)renderer
|
||
{
|
||
return _rendererFrontend->getRenderer();
|
||
}
|
||
|
||
- (void)commonInit
|
||
{
|
||
_opaque = NO;
|
||
|
||
// setup accessibility
|
||
// self.isAccessibilityElement = YES;
|
||
|
||
// Ensure network configuration is set up (connect gl-native networking to
|
||
// platform SDK via delegation). Calling `resetNativeNetworkManagerDelegate`
|
||
// is not necessary here, since the shared manager already calls it.
|
||
[MLNNetworkConfiguration sharedManager];
|
||
|
||
self.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"MAP_A11Y_LABEL", nil, nil, @"Map", @"Accessibility label");
|
||
self.accessibilityTraits = UIAccessibilityTraitAllowsDirectInteraction | UIAccessibilityTraitAdjustable;
|
||
self.backgroundColor = [UIColor clearColor];
|
||
self.clipsToBounds = YES;
|
||
if (@available(iOS 11.0, *)) { self.accessibilityIgnoresInvertColors = YES; }
|
||
|
||
self.preferredFramesPerSecond = MLNMapViewPreferredFramesPerSecondDefault;
|
||
|
||
// setup mbgl view
|
||
_mbglView = MLNMapViewImpl::Create(self);
|
||
|
||
BOOL background = [UIApplication sharedApplication].applicationState == UIApplicationStateBackground;
|
||
if (!background)
|
||
{
|
||
_mbglView->createView();
|
||
}
|
||
// Delete the pre-offline ambient cache at ~/Library/Caches/cache.db.
|
||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||
NSString *fileCachePath = [paths.firstObject stringByAppendingPathComponent:@"cache.db"];
|
||
[[NSFileManager defaultManager] removeItemAtPath:fileCachePath error:NULL];
|
||
|
||
// setup mbgl map
|
||
MLNRendererConfiguration *config = [MLNRendererConfiguration currentConfiguration];
|
||
|
||
std::optional<std::string> localFontFamilyName = config.localFontFamilyName ? std::optional<std::string>(std::string(config.localFontFamilyName.UTF8String)) : std::nullopt;
|
||
auto renderer = std::make_unique<mbgl::Renderer>(_mbglView->getRendererBackend(), config.scaleFactor, localFontFamilyName);
|
||
BOOL enableCrossSourceCollisions = !config.perSourceCollisions;
|
||
_rendererFrontend = std::make_unique<MLNRenderFrontend>(std::move(renderer), self, _mbglView->getRendererBackend());
|
||
|
||
mbgl::MapOptions mapOptions;
|
||
mapOptions.withMapMode(mbgl::MapMode::Continuous)
|
||
.withSize(self.size)
|
||
.withPixelRatio(config.scaleFactor)
|
||
.withConstrainMode(mbgl::ConstrainMode::None)
|
||
.withViewportMode(mbgl::ViewportMode::Default)
|
||
.withCrossSourceCollisions(enableCrossSourceCollisions);
|
||
|
||
|
||
mbgl::TileServerOptions* tileServerOptions = [[MLNSettings sharedSettings] tileServerOptionsInternal];
|
||
mbgl::ResourceOptions resourceOptions;
|
||
resourceOptions.withCachePath(MLNOfflineStorage.sharedOfflineStorage.databasePath.UTF8String)
|
||
.withAssetPath([NSBundle mainBundle].resourceURL.path.UTF8String)
|
||
.withTileServerOptions(*tileServerOptions);
|
||
mbgl::ClientOptions clientOptions;
|
||
|
||
auto apiKey = [[MLNSettings sharedSettings] apiKey];
|
||
if (apiKey) {
|
||
resourceOptions.withApiKey([apiKey UTF8String]);
|
||
}
|
||
|
||
NSAssert(!_mbglMap, @"_mbglMap should be NULL");
|
||
_mbglMap = std::make_unique<mbgl::Map>(*_rendererFrontend, *_mbglView, mapOptions, resourceOptions, clientOptions);
|
||
|
||
// start paused if launch into the background
|
||
if (background) {
|
||
self.dormant = YES;
|
||
}
|
||
|
||
// Notify map object when network reachability status changes.
|
||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||
selector:@selector(reachabilityChanged:)
|
||
name:kMLNReachabilityChangedNotification
|
||
object:nil];
|
||
|
||
_reachability = [MLNReachability reachabilityForInternetConnection];
|
||
if ([_reachability isReachable])
|
||
{
|
||
_isWaitingForRedundantReachableNotification = YES;
|
||
}
|
||
[_reachability startNotifier];
|
||
|
||
// setup default location manager
|
||
self.locationManager = nil;
|
||
|
||
// Set up annotation management and selection state.
|
||
_annotationImagesByIdentifier = [NSMutableDictionary dictionary];
|
||
_annotationContextsByAnnotationTag = {};
|
||
_annotationTagsByAnnotation = {};
|
||
_annotationViewReuseQueueByIdentifier = [NSMutableDictionary dictionary];
|
||
_selectedAnnotationTag = MLNAnnotationTagNotFound;
|
||
_annotationsNearbyLastTap = {};
|
||
|
||
// TODO: This warning should be removed when automaticallyAdjustsScrollViewInsets is removed from
|
||
// the UIViewController api.
|
||
static dispatch_once_t onceToken;
|
||
dispatch_once(&onceToken, ^{
|
||
NSLog(@"%@ WARNING UIViewController.automaticallyAdjustsScrollViewInsets is deprecated use MLNMapView.automaticallyAdjustContentInset instead.",
|
||
NSStringFromClass(self.class));
|
||
});
|
||
|
||
// setup logo
|
||
//
|
||
UIImage *logo = [UIImage mgl_resourceImageNamed:@"maplibre-logo-stroke-gray"];
|
||
_logoView = [[UIImageView alloc] initWithImage:logo];
|
||
_logoView.accessibilityTraits = UIAccessibilityTraitStaticText;
|
||
_logoView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"LOGO_A11Y_LABEL", nil, nil, @"Mapbox", @"Accessibility label");
|
||
_logoView.translatesAutoresizingMaskIntoConstraints = NO;
|
||
[self addSubview:_logoView];
|
||
_logoViewConstraints = [NSMutableArray array];
|
||
_logoViewPosition = MLNOrnamentPositionBottomLeft;
|
||
_logoViewMargins = MLNOrnamentDefaultPositionOffset;
|
||
|
||
// setup attribution
|
||
//
|
||
_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");
|
||
[_attributionButton addTarget:self action:@selector(showAttribution:) forControlEvents:UIControlEventTouchUpInside];
|
||
_attributionButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||
[self addSubview:_attributionButton];
|
||
_attributionButtonConstraints = [NSMutableArray array];
|
||
|
||
UILongPressGestureRecognizer *attributionLongPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(showAttribution:)];
|
||
[_attributionButton addGestureRecognizer:attributionLongPress];
|
||
_attributionButtonPosition = MLNOrnamentPositionBottomRight;
|
||
_attributionButtonMargins = MLNOrnamentDefaultPositionOffset;
|
||
|
||
// setup compass
|
||
//
|
||
_compassView = [MLNCompassButton compassButtonWithMapView:self];
|
||
[self addSubview:_compassView];
|
||
_compassViewConstraints = [NSMutableArray array];
|
||
_compassViewPosition = MLNOrnamentPositionTopRight;
|
||
_compassViewMargins = MLNOrnamentDefaultPositionOffset;
|
||
|
||
// setup scale control
|
||
//
|
||
_scaleBar = [[MLNScaleBar alloc] init];
|
||
_scaleBar.translatesAutoresizingMaskIntoConstraints = NO;
|
||
[self addSubview:_scaleBar];
|
||
_scaleBarConstraints = [NSMutableArray array];
|
||
_scaleBarPosition = MLNOrnamentPositionTopLeft;
|
||
_scaleBarMargins = MLNOrnamentDefaultPositionOffset;
|
||
|
||
[self installConstraints];
|
||
|
||
// setup interaction
|
||
//
|
||
|
||
self.anchorRotateOrZoomGesturesToCenterCoordinate = NO;
|
||
|
||
#if TARGET_IPHONE_SIMULATOR & (TARGET_CPU_X86 | TARGET_CPU_X86_64)
|
||
bool isM1Simulator = processIsTranslated() > 0;
|
||
if (isM1Simulator) {
|
||
_pan = [[MBGLPanGesture alloc] initWithTarget:self action:@selector(handlePanGesture:)];
|
||
} else {
|
||
_pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
|
||
}
|
||
#else
|
||
_pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
|
||
#endif
|
||
_pan.delegate = self;
|
||
_pan.maximumNumberOfTouches = 1;
|
||
[self addGestureRecognizer:_pan];
|
||
_scrollEnabled = YES;
|
||
_panScrollingMode = MLNPanScrollingModeDefault;
|
||
|
||
#if TARGET_IPHONE_SIMULATOR & (TARGET_CPU_X86 | TARGET_CPU_X86_64)
|
||
if (isM1Simulator) {
|
||
_pinch = [[MBGLPinchGesture alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
|
||
} else {
|
||
_pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
|
||
}
|
||
#else
|
||
_pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
|
||
#endif
|
||
_pinch.delegate = self;
|
||
[self addGestureRecognizer:_pinch];
|
||
_zoomEnabled = YES;
|
||
|
||
#if TARGET_IPHONE_SIMULATOR & (TARGET_CPU_X86 | TARGET_CPU_X86_64)
|
||
if (isM1Simulator) {
|
||
_rotate = [[MBGLRotationGesture alloc] initWithTarget:self action:@selector(handleRotateGesture:)];
|
||
} else {
|
||
_rotate = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotateGesture:)];
|
||
}
|
||
#else
|
||
_rotate = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotateGesture:)];
|
||
#endif
|
||
_rotate.delegate = self;
|
||
[self addGestureRecognizer:_rotate];
|
||
_rotateEnabled = YES;
|
||
_rotationThresholdWhileZooming = 3;
|
||
|
||
_doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTapGesture:)];
|
||
_doubleTap.numberOfTapsRequired = 2;
|
||
[self addGestureRecognizer:_doubleTap];
|
||
|
||
_twoFingerDrag = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleTwoFingerDragGesture:)];
|
||
_twoFingerDrag.minimumNumberOfTouches = 2;
|
||
_twoFingerDrag.maximumNumberOfTouches = 2;
|
||
_twoFingerDrag.delegate = self;
|
||
[_twoFingerDrag requireGestureRecognizerToFail:_pan];
|
||
[self addGestureRecognizer:_twoFingerDrag];
|
||
_pitchEnabled = YES;
|
||
|
||
_twoFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTwoFingerTapGesture:)];
|
||
_twoFingerTap.numberOfTouchesRequired = 2;
|
||
[_twoFingerTap requireGestureRecognizerToFail:_pinch];
|
||
[_twoFingerTap requireGestureRecognizerToFail:_rotate];
|
||
[_twoFingerTap requireGestureRecognizerToFail:_twoFingerDrag];
|
||
[self addGestureRecognizer:_twoFingerTap];
|
||
|
||
_hapticFeedbackEnabled = YES;
|
||
|
||
_decelerationRate = MLNMapViewDecelerationRateNormal;
|
||
|
||
_quickZoom = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleQuickZoomGesture:)];
|
||
_quickZoom.numberOfTapsRequired = 1;
|
||
_quickZoom.minimumPressDuration = 0;
|
||
[_quickZoom requireGestureRecognizerToFail:_doubleTap];
|
||
[self addGestureRecognizer:_quickZoom];
|
||
|
||
_singleTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTapGesture:)];
|
||
[_singleTapGestureRecognizer requireGestureRecognizerToFail:_doubleTap];
|
||
_singleTapGestureRecognizer.delegate = self;
|
||
[_singleTapGestureRecognizer requireGestureRecognizerToFail:_quickZoom];
|
||
[self addGestureRecognizer:_singleTapGestureRecognizer];
|
||
|
||
// observe app activity
|
||
//
|
||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willTerminate) name:UIApplicationWillTerminateNotification object:nil];
|
||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
|
||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
|
||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
|
||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
|
||
|
||
// Pending completion blocks are called *after* annotation views have been updated
|
||
// in updateFromDisplayLink.
|
||
_pendingCompletionBlocks = [NSMutableArray array];
|
||
|
||
|
||
// As of 3.7.5, we intentionally do not listen for `UIApplicationWillResignActiveNotification` or call `pauseRendering:` in response to it, as doing
|
||
// so causes a loop when asking for location permission. See: https://github.com/mapbox/mapbox-gl-native/issues/11225
|
||
|
||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
||
|
||
// Device orientation management
|
||
self.currentOrientation = UIInterfaceOrientationUnknown;
|
||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceOrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil];
|
||
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
|
||
|
||
// set initial position
|
||
//
|
||
mbgl::CameraOptions options;
|
||
options.center = mbgl::LatLng(0, 0);
|
||
mbgl::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(self.contentInset);
|
||
options.padding = padding;
|
||
options.zoom = 0;
|
||
|
||
_cameraChangeReasonBitmask = MLNCameraChangeReasonNone;
|
||
|
||
_mbglMap->jumpTo(options);
|
||
_pendingLatitude = NAN;
|
||
_pendingLongitude = NAN;
|
||
_targetCoordinate = kCLLocationCoordinate2DInvalid;
|
||
|
||
_shouldRequestAuthorizationToUseLocationServices = YES;
|
||
}
|
||
|
||
- (mbgl::Size)size
|
||
{
|
||
// check for minimum texture size supported by OpenGL ES 2.0
|
||
//
|
||
CGSize size = CGSizeMake(MAX(self.bounds.size.width, 64), MAX(self.bounds.size.height, 64));
|
||
return { static_cast<uint32_t>(size.width),
|
||
static_cast<uint32_t>(size.height) };
|
||
}
|
||
|
||
- (void)reachabilityChanged:(NSNotification *)notification
|
||
{
|
||
MLNAssertIsMainThread();
|
||
|
||
MLNReachability *reachability = [notification object];
|
||
if ( ! _isWaitingForRedundantReachableNotification && [reachability isReachable])
|
||
{
|
||
mbgl::NetworkStatus::Reachable();
|
||
}
|
||
_isWaitingForRedundantReachableNotification = NO;
|
||
}
|
||
|
||
|
||
- (void)destroyCoreObjects {
|
||
// Record the current state. Currently only saving a limited set of properties.
|
||
self.terminated = YES;
|
||
self.residualCamera = self.camera;
|
||
self.residualDebugMask = self.debugMask;
|
||
self.residualStyleURL = self.styleURL;
|
||
|
||
// Tear down C++ objects, insuring worker threads correctly terminate.
|
||
// Because of how _mbglMap is constructed, we need to destroy it first.
|
||
_mbglMap.reset();
|
||
|
||
_mbglView.reset();
|
||
|
||
_rendererFrontend.reset();
|
||
}
|
||
|
||
- (void)dealloc
|
||
{
|
||
MLNLogInfo(@"Deallocating MLNMapView.");
|
||
[_reachability stopNotifier];
|
||
|
||
[[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
|
||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||
|
||
// Removing the annotations unregisters any outstanding KVO observers.
|
||
NSArray *annotations = self.annotations;
|
||
if (annotations)
|
||
{
|
||
[self removeAnnotations:annotations];
|
||
}
|
||
|
||
[self destroyDisplayLink];
|
||
|
||
[self destroyCoreObjects];
|
||
|
||
[self.compassViewConstraints removeAllObjects];
|
||
self.compassViewConstraints = nil;
|
||
|
||
[self.scaleBarConstraints removeAllObjects];
|
||
self.scaleBarConstraints = nil;
|
||
|
||
[self.logoViewConstraints removeAllObjects];
|
||
self.logoViewConstraints = nil;
|
||
|
||
[self.attributionButtonConstraints removeAllObjects];
|
||
self.attributionButtonConstraints = nil;
|
||
|
||
[_locationManager stopUpdatingLocation];
|
||
[_locationManager stopUpdatingHeading];
|
||
_locationManager.delegate = nil;
|
||
}
|
||
|
||
- (void)setDelegate:(nullable id<MLNMapViewDelegate>)delegate
|
||
{
|
||
MLNLogDebug(@"Setting delegate: %@", delegate);
|
||
if (_delegate == delegate) return;
|
||
|
||
_delegate = delegate;
|
||
|
||
_delegateHasAlphasForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:alphaForShapeAnnotation:)];
|
||
_delegateHasStrokeColorsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:strokeColorForShapeAnnotation:)];
|
||
_delegateHasFillColorsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:fillColorForPolygonAnnotation:)];
|
||
_delegateHasLineWidthsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:lineWidthForPolylineAnnotation:)];
|
||
}
|
||
|
||
- (void)didReceiveMemoryWarning
|
||
{
|
||
MLNAssertIsMainThread();
|
||
|
||
if ( ! self.dormant && _rendererFrontend)
|
||
{
|
||
_rendererFrontend->reduceMemoryUse();
|
||
}
|
||
}
|
||
|
||
- (MLNMapViewImpl *)viewImpl
|
||
{
|
||
return _mbglView.get();
|
||
}
|
||
|
||
// MARK: - Layout -
|
||
|
||
+ (BOOL)requiresConstraintBasedLayout
|
||
{
|
||
return YES;
|
||
}
|
||
|
||
- (void)setScaleBarPosition:(MLNOrnamentPosition)scaleBarPosition {
|
||
MLNLogDebug(@"Setting scaleBarPosition: %lu", scaleBarPosition);
|
||
_scaleBarPosition = scaleBarPosition;
|
||
[self installScaleBarConstraints];
|
||
}
|
||
|
||
- (void)setScaleBarMargins:(CGPoint)scaleBarMargins {
|
||
MLNLogDebug(@"Setting scaleBarMargins: (x:%f, y:%f)", scaleBarMargins.x, scaleBarMargins.y);
|
||
_scaleBarMargins = scaleBarMargins;
|
||
[self installScaleBarConstraints];
|
||
}
|
||
|
||
- (void)setCompassViewPosition:(MLNOrnamentPosition)compassViewPosition {
|
||
MLNLogDebug(@"Setting compassViewPosition: %lu", compassViewPosition);
|
||
_compassViewPosition = compassViewPosition;
|
||
[self installCompassViewConstraints];
|
||
}
|
||
|
||
- (void)setCompassViewMargins:(CGPoint)compassViewMargins {
|
||
MLNLogDebug(@"Setting compassViewOffset: (x:%f, y:%f)", compassViewMargins.x, compassViewMargins.y);
|
||
_compassViewMargins = compassViewMargins;
|
||
[self installCompassViewConstraints];
|
||
}
|
||
|
||
- (void)setLogoViewPosition:(MLNOrnamentPosition)logoViewPosition {
|
||
MLNLogDebug(@"Setting logoViewPosition: %lu", logoViewPosition);
|
||
_logoViewPosition = logoViewPosition;
|
||
[self installLogoViewConstraints];
|
||
}
|
||
|
||
- (void)setLogoViewMargins:(CGPoint)logoViewMargins {
|
||
MLNLogDebug(@"Setting logoViewMargins: (x:%f, y:%f)", logoViewMargins.x, logoViewMargins.y);
|
||
_logoViewMargins = logoViewMargins;
|
||
[self installLogoViewConstraints];
|
||
}
|
||
|
||
- (void)setAttributionButtonPosition:(MLNOrnamentPosition)attributionButtonPosition {
|
||
MLNLogDebug(@"Setting attributionButtonPosition: %lu", attributionButtonPosition);
|
||
_attributionButtonPosition = attributionButtonPosition;
|
||
[self installAttributionButtonConstraints];
|
||
}
|
||
|
||
- (void)setAttributionButtonMargins:(CGPoint)attributionButtonMargins {
|
||
MLNLogDebug(@"Setting attributionButtonMargins: (x:%f, y:%f)", attributionButtonMargins.x, attributionButtonMargins.y);
|
||
_attributionButtonMargins = attributionButtonMargins;
|
||
[self installAttributionButtonConstraints];
|
||
}
|
||
|
||
- (void)updateConstraintsForOrnament:(UIView *)view
|
||
constraints:(NSMutableArray *)constraints
|
||
position:(MLNOrnamentPosition)position
|
||
size:(CGSize)size
|
||
margins:(CGPoint)margins {
|
||
NSMutableArray *updatedConstraints = [NSMutableArray array];
|
||
UIEdgeInsets inset = UIEdgeInsetsZero;
|
||
|
||
BOOL automaticallyAdjustContentInset;
|
||
if (_automaticallyAdjustContentInsetHolder) {
|
||
automaticallyAdjustContentInset = _automaticallyAdjustContentInsetHolder.boolValue;
|
||
} else {
|
||
UIViewController *viewController = [self rootViewController];
|
||
#pragma clang diagnostic push
|
||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||
automaticallyAdjustContentInset = viewController.automaticallyAdjustsScrollViewInsets;
|
||
#pragma clang diagnostic pop
|
||
}
|
||
|
||
if (! automaticallyAdjustContentInset) {
|
||
inset = UIEdgeInsetsMake(self.contentInset.top - self.safeMapViewContentInsets.top,
|
||
self.contentInset.left - self.safeMapViewContentInsets.left,
|
||
self.contentInset.bottom - self.safeMapViewContentInsets.bottom,
|
||
self.contentInset.right - self.safeMapViewContentInsets.right);
|
||
|
||
// makes sure the insets don't have negative values that could hide the ornaments
|
||
// thus violating our ToS
|
||
inset = UIEdgeInsetsMake(fmaxf(inset.top, 0),
|
||
fmaxf(inset.left, 0),
|
||
fmaxf(inset.bottom, 0),
|
||
fmaxf(inset.right, 0));
|
||
}
|
||
|
||
switch (position) {
|
||
case MLNOrnamentPositionTopLeft:
|
||
[updatedConstraints addObject:[view.topAnchor constraintEqualToAnchor:self.mgl_safeTopAnchor constant:margins.y + inset.top]];
|
||
[updatedConstraints addObject:[view.leadingAnchor constraintEqualToAnchor:self.mgl_safeLeadingAnchor constant:margins.x + inset.left]];
|
||
break;
|
||
case MLNOrnamentPositionTopRight:
|
||
[updatedConstraints addObject:[view.topAnchor constraintEqualToAnchor:self.mgl_safeTopAnchor constant:margins.y + inset.top]];
|
||
[updatedConstraints addObject:[self.mgl_safeTrailingAnchor constraintEqualToAnchor:view.trailingAnchor constant:margins.x + inset.right]];
|
||
break;
|
||
case MLNOrnamentPositionBottomLeft:
|
||
[updatedConstraints addObject:[self.mgl_safeBottomAnchor constraintEqualToAnchor:view.bottomAnchor constant:margins.y + inset.bottom]];
|
||
[updatedConstraints addObject:[view.leadingAnchor constraintEqualToAnchor:self.mgl_safeLeadingAnchor constant:margins.x + inset.left]];
|
||
break;
|
||
case MLNOrnamentPositionBottomRight:
|
||
[updatedConstraints addObject:[self.mgl_safeBottomAnchor constraintEqualToAnchor:view.bottomAnchor constant:margins.y + inset.bottom]];
|
||
[updatedConstraints addObject: [self.mgl_safeTrailingAnchor constraintEqualToAnchor:view.trailingAnchor constant:margins.x + inset.right]];
|
||
break;
|
||
}
|
||
|
||
if (!CGSizeEqualToSize(size, CGSizeZero)) {
|
||
NSLayoutConstraint *widthConstraint = [view.widthAnchor constraintEqualToConstant:size.width];
|
||
widthConstraint.identifier = @"width";
|
||
NSLayoutConstraint *heightConstraint = [view.heightAnchor constraintEqualToConstant:size.height];
|
||
heightConstraint.identifier = @"height";
|
||
[updatedConstraints addObjectsFromArray:@[widthConstraint,heightConstraint]];
|
||
}
|
||
|
||
[NSLayoutConstraint deactivateConstraints:constraints];
|
||
[constraints removeAllObjects];
|
||
[NSLayoutConstraint activateConstraints:updatedConstraints];
|
||
[constraints addObjectsFromArray:updatedConstraints];
|
||
}
|
||
|
||
- (void)installConstraints
|
||
{
|
||
[self installCompassViewConstraints];
|
||
[self installScaleBarConstraints];
|
||
[self installLogoViewConstraints];
|
||
[self installAttributionButtonConstraints];
|
||
}
|
||
|
||
- (void)installCompassViewConstraints {
|
||
// compass view
|
||
[self updateConstraintsForOrnament:self.compassView
|
||
constraints:self.compassViewConstraints
|
||
position:self.compassViewPosition
|
||
size:[self sizeForOrnament:self.compassView constraints:self.compassViewConstraints]
|
||
margins:self.compassViewMargins];
|
||
}
|
||
|
||
- (void)installScaleBarConstraints {
|
||
// scale bar view
|
||
[self updateConstraintsForOrnament:self.scaleBar
|
||
constraints:self.scaleBarConstraints
|
||
position:self.scaleBarPosition
|
||
size:CGSizeZero
|
||
margins:self.scaleBarMargins];
|
||
}
|
||
|
||
- (void)installLogoViewConstraints {
|
||
// logo view
|
||
[self updateConstraintsForOrnament:self.logoView
|
||
constraints:self.logoViewConstraints
|
||
position:self.logoViewPosition
|
||
size:[self sizeForOrnament:self.logoView constraints:self.logoViewConstraints]
|
||
margins:self.logoViewMargins];
|
||
}
|
||
|
||
- (void)installAttributionButtonConstraints {
|
||
// attribution button
|
||
[self updateConstraintsForOrnament:self.attributionButton
|
||
constraints:self.attributionButtonConstraints
|
||
position:self.attributionButtonPosition
|
||
size:[self sizeForOrnament:self.attributionButton constraints:self.attributionButtonConstraints]
|
||
margins:self.attributionButtonMargins];
|
||
}
|
||
|
||
- (CGSize)sizeForOrnament:(UIView *)view
|
||
constraints:(NSMutableArray *)constraints {
|
||
// avoid regenerating size constraints
|
||
CGSize size = view.bounds.size;
|
||
if(constraints && constraints.count > 0) {
|
||
for (NSLayoutConstraint * constraint in constraints) {
|
||
if([constraint.identifier isEqualToString:@"width"]) {
|
||
size.width = constraint.constant;
|
||
}
|
||
else if ([constraint.identifier isEqualToString:@"height"]) {
|
||
size.height = constraint.constant;
|
||
}
|
||
}
|
||
}
|
||
|
||
return size;
|
||
}
|
||
|
||
- (BOOL)isOpaque
|
||
{
|
||
return _opaque;
|
||
}
|
||
|
||
- (void)setOpaque:(BOOL)opaque
|
||
{
|
||
_opaque = opaque;
|
||
if (_mbglView) {
|
||
_mbglView->setOpaque(opaque);
|
||
}
|
||
}
|
||
|
||
- (void)updateViewsWithCurrentUpdateParameters {
|
||
// Update UIKit elements, prior to rendering
|
||
[self updateUserLocationAnnotationView];
|
||
[self updateAnnotationViews];
|
||
[self updateCalloutView];
|
||
}
|
||
|
||
- (BOOL)renderSync
|
||
{
|
||
BOOL hasPendingBlocks = (self.pendingCompletionBlocks.count > 0);
|
||
|
||
if (!self.needsDisplayRefresh && !hasPendingBlocks) {
|
||
return NO;
|
||
}
|
||
|
||
BOOL needsRender = self.needsDisplayRefresh;
|
||
|
||
self.needsDisplayRefresh = NO;
|
||
|
||
if (!self.dormant && needsRender)
|
||
{
|
||
// It's important to call this *before* `_rendererFrontend->render()`, as
|
||
// that function saves the current `updateParameters` before rendering. If this
|
||
// occurs after then the views will be a frame behind.
|
||
//
|
||
// The update parameters will have been updated earlier, for example by
|
||
// calls to easeTo, flyTo, called from gesture handlers.
|
||
|
||
[self updateViewsWithCurrentUpdateParameters];
|
||
|
||
if (_rendererFrontend) {
|
||
|
||
_rendererFrontend->render();
|
||
|
||
}
|
||
|
||
}
|
||
|
||
if (hasPendingBlocks) {
|
||
// Call any pending completion blocks. This is primarily to ensure
|
||
// that annotations are in the expected position after core rendering
|
||
// and map update.
|
||
//
|
||
// TODO: Consider using this same mechanism for delegate callbacks.
|
||
[self processPendingBlocks];
|
||
}
|
||
|
||
return YES;
|
||
}
|
||
|
||
// This gets called when the view dimension changes, e.g. because the device is being rotated.
|
||
- (void)layoutSubviews
|
||
{
|
||
[super layoutSubviews];
|
||
|
||
// Calling this here instead of in the scale bar itself because if this is done in the
|
||
// scale bar instance, it triggers a call to this `layoutSubviews` method that calls
|
||
// `_mbglMap->setSize()` just below that triggers rendering update which triggers
|
||
// another scale bar update which causes a rendering update loop and a major performace
|
||
// degradation.
|
||
[self.scaleBar invalidateIntrinsicContentSize];
|
||
|
||
[self adjustContentInset];
|
||
|
||
if (_mbglView) {
|
||
_mbglView->layoutChanged();
|
||
}
|
||
|
||
if (_mbglMap) {
|
||
self.mbglMap.setSize([self size]);
|
||
}
|
||
|
||
if (self.compassView.alpha)
|
||
{
|
||
[self updateCompass];
|
||
}
|
||
|
||
if (self.compassView.alpha || self.showsUserHeadingIndicator)
|
||
{
|
||
[self updateHeadingForDeviceOrientation];
|
||
}
|
||
|
||
[self updateUserLocationAnnotationView];
|
||
|
||
[self updateAttributionAlertView];
|
||
|
||
MLNAssert(self.attributionButton.isHidden || CGRectContainsRect(self.bounds, self.attributionButton.mgl_frameForIdentifyTransform),
|
||
@"The attribution is not in the visible area of the mapview. Please check your position and offset settings");
|
||
MLNAssert(self.scaleBar.isHidden || CGRectContainsRect(self.bounds, self.scaleBar.mgl_frameForIdentifyTransform),
|
||
@"The scaleBar is not in the visible area of the mapview. Please check your position and offset settings");
|
||
MLNAssert(self.compassView.isHidden || self.compassView.compassVisibility == MLNOrnamentVisibilityHidden || CGRectContainsRect(self.bounds, self.compassView.mgl_frameForIdentifyTransform),
|
||
@"The compassView is not in the visible area of the mapview. Please check your position and offset settings");
|
||
MLNAssert(self.logoView.isHidden || CGRectContainsRect(self.bounds, self.logoView.mgl_frameForIdentifyTransform),
|
||
@"The logoView is not in the visible area of the mapview. Please check your position and offset settings");
|
||
}
|
||
|
||
/// Updates `contentInset` to reflect the current window geometry.
|
||
- (void)adjustContentInset
|
||
{
|
||
UIEdgeInsets adjustedContentInsets = UIEdgeInsetsZero;
|
||
UIViewController *viewController = [self rootViewController];
|
||
BOOL automaticallyAdjustContentInset;
|
||
adjustedContentInsets = self.safeAreaInsets;
|
||
|
||
if (_automaticallyAdjustContentInsetHolder) {
|
||
automaticallyAdjustContentInset = _automaticallyAdjustContentInsetHolder.boolValue;
|
||
} else {
|
||
#pragma clang diagnostic push
|
||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||
automaticallyAdjustContentInset = viewController.automaticallyAdjustsScrollViewInsets;
|
||
#pragma clang diagnostic pop
|
||
}
|
||
|
||
self.safeMapViewContentInsets = adjustedContentInsets;
|
||
if ( ! automaticallyAdjustContentInset)
|
||
{
|
||
return;
|
||
}
|
||
|
||
self.contentInset = adjustedContentInsets;
|
||
}
|
||
|
||
- (UIViewController *)rootViewController {
|
||
// We could crawl all the way up the responder chain using
|
||
// -viewControllerForLayoutGuides, but an intervening view means that any
|
||
// manual contentInset should not be overridden; something other than the
|
||
// top and bottom bars may be influencing the manual inset.
|
||
UIViewController *viewController;
|
||
if ([self.nextResponder isKindOfClass:[UIViewController class]])
|
||
{
|
||
// This map view is the content view of a view controller.
|
||
viewController = (UIViewController *)self.nextResponder;
|
||
}
|
||
else if ([self.superview.nextResponder isKindOfClass:[UIViewController class]])
|
||
{
|
||
// This map view is an immediate child of a view controller’s content view.
|
||
viewController = (UIViewController *)self.superview.nextResponder;
|
||
}
|
||
return viewController;
|
||
}
|
||
|
||
- (void)setAutomaticallyAdjustsContentInset:(BOOL)automaticallyAdjustsContentInset {
|
||
MLNLogDebug(@"Setting automaticallyAdjustsContentInset: %@", MLNStringFromBOOL(automaticallyAdjustsContentInset));
|
||
_automaticallyAdjustContentInsetHolder = [NSNumber numberWithBool:automaticallyAdjustsContentInset];
|
||
}
|
||
|
||
- (BOOL)automaticallyAdjustsContentInset {
|
||
return _automaticallyAdjustContentInsetHolder.boolValue;
|
||
}
|
||
|
||
- (void)setContentInset:(UIEdgeInsets)contentInset
|
||
{
|
||
[self setContentInset:contentInset animated:NO completionHandler:nil];
|
||
}
|
||
|
||
- (void)setContentInset:(UIEdgeInsets)contentInset animated:(BOOL)animated
|
||
{
|
||
[self setContentInset:contentInset animated:animated completionHandler:nil];
|
||
}
|
||
|
||
- (void)setContentInset:(UIEdgeInsets)contentInset animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
MLNLogDebug(@"Setting contentInset: %@ animated:", NSStringFromUIEdgeInsets(contentInset), MLNStringFromBOOL(animated));
|
||
if (UIEdgeInsetsEqualToEdgeInsets(contentInset, self.contentInset))
|
||
{
|
||
if (completion) {
|
||
completion();
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (self.userTrackingMode == MLNUserTrackingModeNone)
|
||
{
|
||
// Don’t call -setCenterCoordinate:, which resets the user tracking mode.
|
||
[self _setCenterCoordinate:self.centerCoordinate edgePadding:contentInset zoomLevel:self.zoomLevel direction:self.direction duration:animated ? MLNAnimationDuration : 0 animationTimingFunction:nil completionHandler:completion];
|
||
_contentInset = contentInset;
|
||
}
|
||
else
|
||
{
|
||
_contentInset = contentInset;
|
||
[self didUpdateLocationWithUserTrackingAnimated:animated completionHandler:completion];
|
||
}
|
||
|
||
// Compass, logo and attribution button constraints needs to be updated.z
|
||
[self installConstraints];
|
||
}
|
||
|
||
/// Returns the frame of inset content within the map view.
|
||
- (CGRect)contentFrame
|
||
{
|
||
return UIEdgeInsetsInsetRect(self.bounds, self.contentInset);
|
||
}
|
||
|
||
/// Returns the center point of the inset content within the map view.
|
||
- (CGPoint)contentCenter
|
||
{
|
||
CGRect contentFrame = self.contentFrame;
|
||
return CGPointMake(CGRectGetMidX(contentFrame), CGRectGetMidY(contentFrame));
|
||
}
|
||
|
||
// MARK: - Pending completion blocks
|
||
|
||
- (void)processPendingBlocks
|
||
{
|
||
NSArray *blocks = self.pendingCompletionBlocks;
|
||
self.pendingCompletionBlocks = [NSMutableArray array];
|
||
|
||
for (dispatch_block_t block in blocks)
|
||
{
|
||
block();
|
||
}
|
||
}
|
||
|
||
- (BOOL)scheduleTransitionCompletion:(dispatch_block_t)block
|
||
{
|
||
// Only add a block if the display link (that calls processPendingBlocks) is
|
||
// running, otherwise fall back to calling immediately.
|
||
if (self.isDisplayLinkActive)
|
||
{
|
||
[self willChangeValueForKey:@"pendingCompletionBlocks"];
|
||
[self.pendingCompletionBlocks addObject:block];
|
||
[self didChangeValueForKey:@"pendingCompletionBlocks"];
|
||
return YES;
|
||
}
|
||
|
||
return NO;
|
||
}
|
||
|
||
// MARK: - Life Cycle -
|
||
|
||
- (void)updateFromDisplayLink:(CADisplayLink *)displayLink
|
||
{
|
||
// CADisplayLink's call interval closely matches the that defined by,
|
||
// preferredFramesPerSecond, however it is NOT called on the vsync and
|
||
// can fire some time after the vsync, and the duration can often exceed
|
||
// the expected period.
|
||
//
|
||
// The `timestamp` property should represent (or be very close to) the vsync,
|
||
// so for any kind of frame rate measurement, it can be important to record
|
||
// the time upon entry to this method.
|
||
//
|
||
// This start time, coupled with the `targetTimestamp` gives you a measure
|
||
// of how long you have to do work before the next vsync.
|
||
//
|
||
// Note that CADisplayLink's duration property is interval between vsyncs at
|
||
// the device's natural frequency (60, 120). Instead, for the duration of a
|
||
// frame, use the two timestamps instead. This is especially important if
|
||
// you have set preferredFramesPerSecond to something other than the default.
|
||
//
|
||
// │ remaining duration ┃
|
||
// │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▶┃
|
||
// ┌ ─ ─ ─ ─ ─ ┼───────────────────────╋───────────────────────────────────┳───────
|
||
// │ ┃ ┃
|
||
// │ │ ┃ ┃
|
||
// │ ┃ ┃
|
||
// ▼ │ ▼ ▼
|
||
// timestamp │ target
|
||
// (vsync?) │ timestamp
|
||
// │
|
||
// ▼
|
||
// display link
|
||
// start time
|
||
|
||
MLNAssertIsMainThread();
|
||
|
||
// Not "visible" - this isn't a full definition of visibility, but if
|
||
// the map view doesn't have a window then it *cannot* be visible.
|
||
if (!self.window) {
|
||
return;
|
||
}
|
||
|
||
// Mismatched display link
|
||
if (displayLink && displayLink != _displayLink) {
|
||
return;
|
||
}
|
||
|
||
// Check to ensure rendering doesn't occur in the background
|
||
if (([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) &&
|
||
![self supportsBackgroundRendering])
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (self.needsDisplayRefresh || (self.pendingCompletionBlocks.count > 0))
|
||
{
|
||
// UIView update logic has moved into `renderSync` above, which now gets
|
||
// triggered by a call to setNeedsDisplay.
|
||
// See MLNMapViewOpenGLImpl::display() for more details
|
||
_mbglView->display();
|
||
}
|
||
|
||
// TODO: Fix
|
||
if (self.experimental_enableFrameRateMeasurement)
|
||
{
|
||
CFTimeInterval now = CACurrentMediaTime();
|
||
|
||
self.frameTime = now - _displayLink.timestamp;
|
||
_frameDurations += self.frameTime;
|
||
|
||
_frameCount++;
|
||
|
||
CFTimeInterval elapsed = now - _frameCounterStartTime;
|
||
|
||
if (elapsed >= 1.0) {
|
||
self.averageFrameRate = _frameCount / elapsed;
|
||
self.averageFrameTime = (_frameDurations / _frameCount) * 1000;
|
||
|
||
_frameCount = 0;
|
||
_frameDurations = 0;
|
||
_frameCounterStartTime = now;
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)setNeedsRerender
|
||
{
|
||
MLNAssertIsMainThread();
|
||
|
||
self.needsDisplayRefresh = YES;
|
||
}
|
||
|
||
- (void)willTerminate
|
||
{
|
||
MLNAssertIsMainThread();
|
||
|
||
if ( ! self.dormant)
|
||
{
|
||
[self.displayLink invalidate];
|
||
self.displayLink = nil;
|
||
|
||
self.dormant = YES;
|
||
|
||
if (_rendererFrontend) {
|
||
_rendererFrontend->reduceMemoryUse();
|
||
}
|
||
|
||
_mbglView->deleteView();
|
||
}
|
||
|
||
[self destroyCoreObjects];
|
||
}
|
||
|
||
- (UIScreen *)windowScreen {
|
||
UIScreen *screen;
|
||
|
||
#ifdef SUPPORT_UIWINDOWSCENE
|
||
if (@available(iOS 13.0, *)) {
|
||
if (self.window.windowScene) {
|
||
screen = self.window.windowScene.screen;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
// Fallback if there's no windowScene
|
||
if (!screen) {
|
||
screen = self.window.screen;
|
||
}
|
||
|
||
return screen;
|
||
}
|
||
|
||
- (BOOL)isVisible
|
||
{
|
||
// "Visible" is not strictly true here - for example, the view hierarchy is not
|
||
// currently observed (e.g. looking at a parent's or the window's hidden
|
||
// status.
|
||
// This does NOT take application state into account
|
||
UIScreen *screen = [self windowScreen];
|
||
return (!self.isHidden && screen);
|
||
}
|
||
|
||
- (void)validateDisplayLink
|
||
{
|
||
BOOL isVisible = self.superview && self.window;
|
||
if (isVisible && ! _displayLink)
|
||
{
|
||
if (_mbglMap && self.mbglMap.getMapOptions().constrainMode() == mbgl::ConstrainMode::None)
|
||
{
|
||
self.mbglMap.setConstrainMode(mbgl::ConstrainMode::HeightOnly);
|
||
}
|
||
|
||
_displayLink = [self.window.screen displayLinkWithTarget:self selector:@selector(updateFromDisplayLink:)];
|
||
[self updateDisplayLinkPreferredFramesPerSecond];
|
||
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
||
self.needsDisplayRefresh = YES;
|
||
[self updateFromDisplayLink:_displayLink];
|
||
}
|
||
else if ( ! isVisible && _displayLink)
|
||
{
|
||
[_displayLink invalidate];
|
||
_displayLink = nil;
|
||
[self processPendingBlocks];
|
||
}
|
||
}
|
||
|
||
- (void)updateDisplayLinkPreferredFramesPerSecond
|
||
{
|
||
if (!_displayLink)
|
||
{
|
||
return;
|
||
}
|
||
|
||
NSInteger newFrameRate;
|
||
if (_preferredFramesPerSecond == MLNMapViewPreferredFramesPerSecondDefault)
|
||
{
|
||
// On legacy devices that cannot maintain a reasonable frame rate, set
|
||
// a lower limit to avoid jank.
|
||
newFrameRate = UIDevice.currentDevice.mgl_isLegacyDevice ? MLNMapViewPreferredFramesPerSecondLowPower : MLNMapViewPreferredFramesPerSecondMaximum;
|
||
}
|
||
else
|
||
{
|
||
newFrameRate = _preferredFramesPerSecond;
|
||
}
|
||
|
||
if (@available(iOS 10.0, *))
|
||
{
|
||
_displayLink.preferredFramesPerSecond = newFrameRate;
|
||
}
|
||
else
|
||
{
|
||
// CADisplayLink.frameInterval does not support more than 60 FPS (and
|
||
// no device that supports >60 FPS ever supported iOS 9).
|
||
NSInteger maximumFrameRate = 60;
|
||
|
||
// `0` is an alias for maximum frame rate.
|
||
newFrameRate = newFrameRate ?: maximumFrameRate;
|
||
|
||
_displayLink.preferredFramesPerSecond = maximumFrameRate / MIN(newFrameRate, maximumFrameRate);
|
||
}
|
||
}
|
||
|
||
- (void)setPreferredFramesPerSecond:(MLNMapViewPreferredFramesPerSecond)preferredFramesPerSecond
|
||
{
|
||
MLNLogDebug(@"Setting preferredFramesPerSecond: %ld", preferredFramesPerSecond);
|
||
if (_preferredFramesPerSecond == preferredFramesPerSecond)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_preferredFramesPerSecond = preferredFramesPerSecond;
|
||
[self updateDisplayLinkPreferredFramesPerSecond];
|
||
}
|
||
|
||
- (void)updatePresentsWithTransaction
|
||
{
|
||
BOOL hasEnoughViewAnnotations = (self.annotationContainerView.annotationViews.count > MLNPresentsWithTransactionAnnotationCount);
|
||
BOOL hasAnAnchoredCallout = [self hasAnAnchoredAnnotationCalloutView];
|
||
|
||
_enablePresentsWithTransaction = (hasEnoughViewAnnotations || hasAnAnchoredCallout);
|
||
|
||
// If the map is visible, change the layer property too
|
||
if (self.window) {
|
||
_mbglView->setPresentsWithTransaction(_enablePresentsWithTransaction);
|
||
}
|
||
}
|
||
|
||
- (void)willMoveToWindow:(UIWindow *)newWindow {
|
||
[super willMoveToWindow:newWindow];
|
||
[self refreshSupportedInterfaceOrientationsWithWindow:newWindow];
|
||
|
||
if (!newWindow)
|
||
{
|
||
// See https://github.com/mapbox/mapbox-gl-native/issues/14232
|
||
// In iOS 12.2, CAEAGLLayer.presentsWithTransaction can cause dramatic
|
||
// slow down. The exact cause of this is unknown, but this work around
|
||
// appears to lessen the effects.
|
||
_mbglView->setPresentsWithTransaction(NO);
|
||
}
|
||
|
||
// Changing windows regardless of whether it's a new one, or the map is being
|
||
// removed from the hierarchy
|
||
[self destroyDisplayLink];
|
||
|
||
if (self.window) {
|
||
#ifdef SUPPORT_UIWINDOWSCENE
|
||
if (@available(iOS 13.0, *))
|
||
{
|
||
[self.window removeObserver:self forKeyPath:@"windowScene" context:windowScreenContext];
|
||
}
|
||
else
|
||
#endif
|
||
{
|
||
[self.window removeObserver:self forKeyPath:@"screen" context:windowScreenContext];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)didMoveToWindow
|
||
{
|
||
[super didMoveToWindow];
|
||
MLNLogDebug(@"[%p] window=%p", self, self.window);
|
||
|
||
if (self.window)
|
||
{
|
||
// See above comment
|
||
[self resumeRenderingIfNecessary];
|
||
[self updatePresentsWithTransaction];
|
||
|
||
#ifdef SUPPORT_UIWINDOWSCENE
|
||
if (@available(iOS 13.0, *))
|
||
{
|
||
[self.window addObserver:self forKeyPath:@"windowScene" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:windowScreenContext];
|
||
}
|
||
else
|
||
#endif
|
||
{
|
||
[self.window addObserver:self forKeyPath:@"screen" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:windowScreenContext];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)didMoveToSuperview
|
||
{
|
||
[self validateDisplayLink];
|
||
if (self.superview)
|
||
{
|
||
[self installConstraints];
|
||
}
|
||
[super didMoveToSuperview];
|
||
}
|
||
|
||
- (void)refreshSupportedInterfaceOrientationsWithWindow:(UIWindow *)window {
|
||
|
||
// "The system intersects the view controller's supported orientations with
|
||
// the app's supported orientations (as determined by the Info.plist file or
|
||
// the app delegate's application:supportedInterfaceOrientationsForWindow:
|
||
// method) and the device's supported orientations to determine whether to rotate.
|
||
|
||
UIApplication *application = [UIApplication sharedApplication];
|
||
|
||
if (window && [application.delegate respondsToSelector:@selector(application:supportedInterfaceOrientationsForWindow:)]) {
|
||
self.applicationSupportedInterfaceOrientations = [application.delegate application:application supportedInterfaceOrientationsForWindow:window];
|
||
return;
|
||
}
|
||
|
||
// If no delegate method, check the application's plist.
|
||
static UIInterfaceOrientationMask orientationMask = UIInterfaceOrientationMaskAll;
|
||
|
||
static dispatch_once_t onceToken;
|
||
dispatch_once(&onceToken, ^{
|
||
// No application delegate
|
||
NSArray *orientations = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UISupportedInterfaceOrientations"];
|
||
|
||
// Application's info plist provided supported orientations.
|
||
if (orientations.count > 0) {
|
||
orientationMask = 0;
|
||
|
||
NSDictionary *lookup =
|
||
@{
|
||
@"UIInterfaceOrientationPortrait" : @(UIInterfaceOrientationMaskPortrait),
|
||
@"UIInterfaceOrientationPortraitUpsideDown" : @(UIInterfaceOrientationMaskPortraitUpsideDown),
|
||
@"UIInterfaceOrientationLandscapeLeft" : @(UIInterfaceOrientationMaskLandscapeLeft),
|
||
@"UIInterfaceOrientationLandscapeRight" : @(UIInterfaceOrientationMaskLandscapeRight)
|
||
};
|
||
|
||
for (NSString *orientation in orientations) {
|
||
UIInterfaceOrientationMask mask = ((NSNumber*)lookup[orientation]).unsignedIntegerValue;
|
||
orientationMask |= mask;
|
||
}
|
||
}
|
||
});
|
||
|
||
self.applicationSupportedInterfaceOrientations = orientationMask;
|
||
}
|
||
|
||
- (void)deviceOrientationDidChange:(__unused NSNotification *)notification
|
||
{
|
||
UIDeviceOrientation deviceOrientation = [[UIDevice currentDevice] orientation];
|
||
|
||
// The docs for `UIViewController.supportedInterfaceOrientations` states:
|
||
//
|
||
// When the user changes the device orientation, the system calls this method
|
||
// on the root view controller or the topmost presented view controller that
|
||
// fills the window. If the view controller supports the new orientation, the
|
||
// window and view controller are rotated to the new orientation. This method
|
||
// is only called if the view controller's shouldAutorotate method returns YES.
|
||
//
|
||
// We want to match similar behaviour. However, it may be preferable to look
|
||
// at the owning view controller (in cases where the map view may be covered
|
||
// by another view.
|
||
|
||
UIViewController *viewController = [self.window.rootViewController mgl_topMostViewController];
|
||
|
||
if (![viewController shouldAutorotate]) {
|
||
return;
|
||
}
|
||
|
||
if ((self.currentOrientation == (UIInterfaceOrientation)deviceOrientation) &&
|
||
(self.currentOrientation != UIInterfaceOrientationUnknown)) {
|
||
return;
|
||
}
|
||
|
||
// "The system intersects the view controller's supported orientations with
|
||
// the app's supported orientations (as determined by the Info.plist file or
|
||
// the app delegate's application:supportedInterfaceOrientationsForWindow:
|
||
// method) and the device's supported orientations to determine whether to rotate.
|
||
|
||
UIInterfaceOrientationMask supportedOrientations = viewController.supportedInterfaceOrientations;
|
||
supportedOrientations &= self.applicationSupportedInterfaceOrientations;
|
||
|
||
// Interface orientations are defined by device orientations
|
||
UIInterfaceOrientationMask interfaceOrientation = 1 << deviceOrientation;
|
||
UIInterfaceOrientationMask validOrientation = interfaceOrientation & UIInterfaceOrientationMaskAll;
|
||
|
||
if (!(validOrientation & supportedOrientations)) {
|
||
return;
|
||
}
|
||
|
||
self.currentOrientation = (UIInterfaceOrientation)deviceOrientation;
|
||
|
||
// Q. Do we need to re-layout if we're just going from Portrait -> Portrait
|
||
// Upside Down (or from Left to Right)?
|
||
[self setNeedsLayout];
|
||
}
|
||
|
||
- (void)stopDisplayLink
|
||
{
|
||
MLNLogDebug(@"[%p]", self);
|
||
self.displayLink.paused = YES;
|
||
self.needsDisplayRefresh = NO;
|
||
[self processPendingBlocks];
|
||
}
|
||
|
||
- (void)createDisplayLink
|
||
{
|
||
MLNLogDebug(@"[%p]", self);
|
||
|
||
// Create and start the display link in a *paused* state
|
||
MLNAssert(!self.displayLinkScreen, @"");
|
||
MLNAssert(!self.displayLink, @"");
|
||
MLNAssert(self.window, @"");
|
||
MLNAssert(self.window.screen, @"");
|
||
|
||
self.displayLinkScreen = self.window.screen;
|
||
self.displayLink = [self.window.screen displayLinkWithTarget:self selector:@selector(updateFromDisplayLink:)];
|
||
self.displayLink.paused = YES;
|
||
|
||
[self updateDisplayLinkPreferredFramesPerSecond];
|
||
|
||
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
||
|
||
if (_mbglMap && self.mbglMap.getMapOptions().constrainMode() == mbgl::ConstrainMode::None)
|
||
{
|
||
self.mbglMap.setConstrainMode(mbgl::ConstrainMode::HeightOnly);
|
||
}
|
||
}
|
||
|
||
- (void)destroyDisplayLink
|
||
{
|
||
MLNLogDebug(@"[%p]", self);
|
||
[self.displayLink invalidate];
|
||
self.displayLink = nil;
|
||
self.displayLinkScreen = nil;
|
||
self.needsDisplayRefresh = NO;
|
||
[self processPendingBlocks];
|
||
}
|
||
|
||
- (void)startDisplayLink
|
||
{
|
||
MLNLogDebug(@"[%p]", self);
|
||
MLNAssert(self.displayLink, @"");
|
||
MLNAssert([self isVisible], @"Display link should only be started when allowed");
|
||
|
||
self.displayLink.paused = NO;
|
||
[self setNeedsRerender];
|
||
[self updateFromDisplayLink:self.displayLink];
|
||
}
|
||
|
||
// MARK: - Application lifecycle
|
||
- (void)willResignActive:(NSNotification *)notification
|
||
{
|
||
MLNAssertIsMainThread();
|
||
MLNLogDebug(@"[%p]", self);
|
||
|
||
// Going from active to inactive states. This could be because a system dialog
|
||
// has been displayed, control center, or the app is headed into the background
|
||
|
||
if (self.supportsBackgroundRendering) {
|
||
return;
|
||
}
|
||
|
||
// We want to pause the rendering
|
||
[self stopDisplayLink];
|
||
|
||
// For OpenGL this calls glFinish as recommended in
|
||
// https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/ImplementingaMultitasking-awareOpenGLESApplication/ImplementingaMultitasking-awareOpenGLESApplication.html#//apple_ref/doc/uid/TP40008793-CH5-SW1
|
||
// reduceMemoryUse(), calls performCleanup(), which calls glFinish
|
||
if (_rendererFrontend)
|
||
{
|
||
_rendererFrontend->reduceMemoryUse();
|
||
}
|
||
}
|
||
|
||
- (void)didEnterBackground:(NSNotification *)notification
|
||
{
|
||
MLNAssertIsMainThread();
|
||
MLNAssert(!self.dormant, @"Should not be dormant heading into background");
|
||
MLNLogDebug(@"[%p] dormant=%d", self, self.dormant);
|
||
|
||
// See comment in `supportsBackgroundRendering` above.
|
||
if (self.supportsBackgroundRendering) {
|
||
return;
|
||
}
|
||
|
||
// We now want to stop rendering.
|
||
// We want to pause the rendering
|
||
[self stopDisplayLink];
|
||
|
||
// For OpenGL this calls glFinish as recommended in
|
||
// https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/ImplementingaMultitasking-awareOpenGLESApplication/ImplementingaMultitasking-awareOpenGLESApplication.html#//apple_ref/doc/uid/TP40008793-CH5-SW1
|
||
// reduceMemoryUse(), calls performCleanup(), which calls glFinish
|
||
if (_rendererFrontend)
|
||
{
|
||
_rendererFrontend->reduceMemoryUse();
|
||
}
|
||
|
||
// We now completely remove the display link, and the renderable resource.
|
||
// Although the method below is called `deleteView` this does NOT delete the
|
||
// GLKView, instead releasing the memory hungry resources.
|
||
[self destroyDisplayLink];
|
||
[self processPendingBlocks];
|
||
_mbglView->deleteView();
|
||
|
||
self.dormant = YES;
|
||
|
||
// Handle non-rendering issues.
|
||
[self validateLocationServices];
|
||
}
|
||
|
||
- (void)willEnterForeground:(NSNotification *)notification
|
||
{
|
||
MLNLogDebug(@"[%p] dormant=%d", self, self.dormant);
|
||
|
||
// We're transitioning from Background to Inactive states
|
||
|
||
if (self.supportsBackgroundRendering) {
|
||
return;
|
||
}
|
||
|
||
// Reverse the process of going into the background
|
||
_mbglView->createView();
|
||
|
||
// A display link needs the window's screen, so create it if we can
|
||
UIScreen *screen = [self windowScreen];
|
||
|
||
if (screen) {
|
||
[self createDisplayLink];
|
||
|
||
// If we can render during the inactive state, start the display link now
|
||
if (self.isVisible) {
|
||
[self startDisplayLink];
|
||
}
|
||
}
|
||
self.dormant = NO;
|
||
|
||
[self validateLocationServices];
|
||
}
|
||
|
||
- (void)didBecomeActive:(NSNotification *)notification
|
||
{
|
||
MLNLogDebug(@"[%p] DL.paused=<%p>.paused=%d", self, self.displayLink, self.displayLink.paused);
|
||
|
||
// Most times, we should already have a display link created at this point,
|
||
// which may or may not be running. However, at the start of the application,
|
||
// it's possible to have a situation where the display link hasn't been created.
|
||
[self resumeRenderingIfNecessary];
|
||
}
|
||
|
||
// MARK: - GL / display link wake/sleep
|
||
|
||
- (EAGLContext *)context {
|
||
return _mbglView->getEAGLContext();
|
||
}
|
||
|
||
- (BOOL)supportsBackgroundRendering
|
||
{
|
||
// Note: The following comment may be out of date with Metal enabled builds where
|
||
// OpenGL is being emulated with Metal.
|
||
//
|
||
// If this view targets an external display, such as AirPlay or CarPlay, we
|
||
// can safely continue to render OpenGL content without tripping
|
||
// gpus_ReturnNotPermittedKillClient in libGPUSupportMercury, because the
|
||
// external connection keeps the application from truly receding to the
|
||
// background.
|
||
UIScreen *screen = [self windowScreen];
|
||
|
||
BOOL supportsBackgroundRendering = (screen && (screen != [UIScreen mainScreen]));
|
||
MLNLogDebug(@"supportsBackgroundRendering=%d",supportsBackgroundRendering);
|
||
return supportsBackgroundRendering;
|
||
}
|
||
|
||
- (void)resumeRenderingIfNecessary {
|
||
MLNLogDebug(@"[%p] DL.paused=<%p>.paused=%d", self, self.displayLink, self.displayLink.paused);
|
||
|
||
// Most times, we should already have a display link created at this point,
|
||
// which may or may not be running. However, at the start of the application,
|
||
// it's possible to have a situation where the display link hasn't been created.
|
||
|
||
// Reverse the process of going into the background
|
||
if ([UIApplication sharedApplication].applicationState != UIApplicationStateBackground) {
|
||
if (self.dormant) {
|
||
_mbglView->createView();
|
||
self.dormant = NO;
|
||
}
|
||
|
||
// Check display link, if necessary
|
||
if (!self.displayLink) {
|
||
if ([self windowScreen]) {
|
||
[self createDisplayLink];
|
||
}
|
||
}
|
||
}
|
||
|
||
// Start the display link if we need to
|
||
if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive || [UIApplication sharedApplication].applicationState == UIApplicationStateInactive) {
|
||
|
||
BOOL mapViewVisible = self.isVisible;
|
||
if (self.displayLink) {
|
||
if (mapViewVisible && self.displayLink.isPaused) {
|
||
[self startDisplayLink];
|
||
}
|
||
else if (!mapViewVisible && !self.displayLink.isPaused) {
|
||
// Unlikely scenario
|
||
[self stopDisplayLink];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
- (BOOL)isDisplayLinkActive {
|
||
MLNLogDebug(@"[%p]", self);
|
||
return (self.displayLink && !self.displayLink.isPaused);
|
||
}
|
||
|
||
- (void)setHidden:(BOOL)hidden
|
||
{
|
||
super.hidden = hidden;
|
||
_displayLink.paused = ![self isVisible];
|
||
|
||
if (hidden)
|
||
{
|
||
[self processPendingBlocks];
|
||
}
|
||
}
|
||
|
||
- (void)tintColorDidChange
|
||
{
|
||
for (UIView *subview in self.subviews) [self updateTintColorForView:subview];
|
||
}
|
||
|
||
- (void)updateTintColorForView:(UIView *)view
|
||
{
|
||
// Don't update:
|
||
// - annotation views
|
||
// - attribution button (handled automatically)
|
||
if ([view isEqual:self.annotationContainerView] || [view isEqual:self.attributionButton]) return;
|
||
|
||
if ([view respondsToSelector:@selector(setTintColor:)]) view.tintColor = self.tintColor;
|
||
|
||
for (UIView *subview in view.subviews) [self updateTintColorForView:subview];
|
||
}
|
||
|
||
- (BOOL)canBecomeFirstResponder {
|
||
return YES;
|
||
}
|
||
|
||
// MARK: - Gestures -
|
||
|
||
- (void)touchesBegan:(__unused NSSet<UITouch *> *)touches withEvent:(__unused UIEvent *)event
|
||
{
|
||
if (!self.zoomEnabled && !self.pitchEnabled && !self.rotateEnabled && !self.scrollEnabled)
|
||
{
|
||
return;
|
||
};
|
||
|
||
self.mbglMap.setGestureInProgress(false);
|
||
if (self.userTrackingState == MLNUserTrackingStateBegan)
|
||
{
|
||
[self setUserTrackingMode:MLNUserTrackingModeNone animated:NO completionHandler:nil];
|
||
}
|
||
|
||
[self cancelTransitions];
|
||
}
|
||
|
||
- (void)notifyGestureDidBegin {
|
||
BOOL animated = NO;
|
||
|
||
[self cameraWillChangeAnimated:animated];
|
||
self.mbglMap.setGestureInProgress(true);
|
||
_changeDelimiterSuppressionDepth++;
|
||
}
|
||
|
||
- (void)notifyGestureDidEndWithDrift:(BOOL)drift {
|
||
_changeDelimiterSuppressionDepth--;
|
||
MLNAssert(_changeDelimiterSuppressionDepth >= 0,
|
||
@"Unbalanced change delimiter suppression/unsuppression");
|
||
if (_changeDelimiterSuppressionDepth == 0) {
|
||
self.mbglMap.setGestureInProgress(false);
|
||
}
|
||
if ( ! drift)
|
||
{
|
||
BOOL animated = NO;
|
||
[self cameraDidChangeAnimated:animated];
|
||
}
|
||
}
|
||
|
||
- (BOOL)isSuppressingChangeDelimiters {
|
||
return _changeDelimiterSuppressionDepth > 0;
|
||
}
|
||
|
||
- (BOOL)_shouldChangeFromCamera:(nonnull MLNMapCamera *)oldCamera toCamera:(nonnull MLNMapCamera *)newCamera
|
||
{
|
||
// Check delegates first
|
||
if ([self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:reason:)])
|
||
{
|
||
return [self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:newCamera reason:self.cameraChangeReasonBitmask];
|
||
}
|
||
else if ([self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)])
|
||
{
|
||
return [self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:newCamera];
|
||
}
|
||
else
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
|
||
- (void)handlePanGesture:(UIPanGestureRecognizer *)pan
|
||
{
|
||
if ( ! self.isScrollEnabled) return;
|
||
|
||
[self cancelTransitions];
|
||
|
||
MLNMapCamera *oldCamera = self.camera;
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonGesturePan;
|
||
|
||
if (pan.state == UIGestureRecognizerStateBegan)
|
||
{
|
||
self.userTrackingMode = MLNUserTrackingModeNone;
|
||
|
||
[self notifyGestureDidBegin];
|
||
}
|
||
else if (pan.state == UIGestureRecognizerStateChanged)
|
||
{
|
||
CGPoint delta = [pan translationInView:pan.view];
|
||
MLNMapCamera *toCamera = [self cameraByPanningWithTranslation:delta panGesture:pan];
|
||
|
||
if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera])
|
||
{
|
||
switch(self.panScrollingMode){
|
||
case MLNPanScrollingModeVertical:
|
||
self.mbglMap.moveBy({ 0, delta.y });
|
||
break;
|
||
case MLNPanScrollingModeHorizontal:
|
||
self.mbglMap.moveBy({ delta.x, 0 });
|
||
break;
|
||
default:
|
||
self.mbglMap.moveBy({ delta.x, delta.y });
|
||
}
|
||
|
||
[pan setTranslation:CGPointZero inView:pan.view];
|
||
}
|
||
|
||
[self cameraIsChanging];
|
||
}
|
||
else if (pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateCancelled)
|
||
{
|
||
CGPoint velocity = [pan velocityInView:pan.view];
|
||
if (self.decelerationRate == MLNMapViewDecelerationRateImmediate || sqrtf(velocity.x * velocity.x + velocity.y * velocity.y) < 100)
|
||
{
|
||
// Not enough velocity to overcome friction
|
||
velocity = CGPointZero;
|
||
}
|
||
|
||
BOOL drift = ! CGPointEqualToPoint(velocity, CGPointZero);
|
||
if (drift)
|
||
{
|
||
CGPoint offset = CGPointMake(velocity.x * self.decelerationRate / 4, velocity.y * self.decelerationRate / 4);
|
||
MLNMapCamera *toCamera = [self cameraByPanningWithTranslation:offset panGesture:pan];
|
||
|
||
if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera])
|
||
{
|
||
switch(self.panScrollingMode){
|
||
case MLNPanScrollingModeVertical:
|
||
self.mbglMap.moveBy({ 0, offset.y }, MLNDurationFromTimeInterval(self.decelerationRate));
|
||
break;
|
||
case MLNPanScrollingModeHorizontal:
|
||
self.mbglMap.moveBy({ offset.x, 0 }, MLNDurationFromTimeInterval(self.decelerationRate));
|
||
break;
|
||
default:
|
||
self.mbglMap.moveBy({ offset.x, offset.y }, MLNDurationFromTimeInterval(self.decelerationRate));
|
||
}
|
||
}
|
||
}
|
||
|
||
[self notifyGestureDidEndWithDrift:drift];
|
||
}
|
||
|
||
}
|
||
|
||
- (void)handlePinchGesture:(UIPinchGestureRecognizer *)pinch
|
||
{
|
||
if ( ! self.isZoomEnabled) return;
|
||
|
||
[self cancelTransitions];
|
||
|
||
CGPoint centerPoint = [self anchorPointForGesture:pinch];
|
||
if (self.anchorRotateOrZoomGesturesToCenterCoordinate) {
|
||
if (pinch.numberOfTouches != 1 || pinch.state == UIGestureRecognizerStateEnded) {
|
||
centerPoint = [self contentCenter];
|
||
}
|
||
}
|
||
MLNMapCamera *oldCamera = self.camera;
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonGesturePinch;
|
||
|
||
if (pinch.state == UIGestureRecognizerStateBegan)
|
||
{
|
||
self.scale = powf(2, [self zoomLevel]);
|
||
|
||
if (abs(pinch.velocity) > abs(self.rotate.velocity)) {
|
||
self.isZooming = YES;
|
||
}
|
||
[self notifyGestureDidBegin];
|
||
}
|
||
else if (pinch.state == UIGestureRecognizerStateChanged)
|
||
{
|
||
// Zoom limiting happens at the core level.
|
||
CGFloat newScale = self.scale * pinch.scale;
|
||
double newZoom = log2(newScale);
|
||
|
||
// Calculates the final camera zoom, has no effect within current map camera.
|
||
MLNMapCamera *toCamera = [self cameraByZoomingToZoomLevel:newZoom aroundAnchorPoint:centerPoint];
|
||
|
||
if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera])
|
||
{
|
||
self.mbglMap.jumpTo(mbgl::CameraOptions()
|
||
.withZoom(newZoom)
|
||
.withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }));
|
||
|
||
// The gesture recognizer only reports the gesture’s current center
|
||
// point, so use the previous center point to anchor the transition.
|
||
// If the number of touches has changed, the remembered center point is
|
||
// meaningless.
|
||
if (self.userTrackingMode == MLNUserTrackingModeNone && pinch.numberOfTouches == _previousPinchNumberOfTouches)
|
||
{
|
||
self.mbglMap.moveBy({centerPoint.x - _previousPinchCenterPoint.x, centerPoint.y - _previousPinchCenterPoint.y});
|
||
}
|
||
}
|
||
[self cameraIsChanging];
|
||
}
|
||
else if (pinch.state == UIGestureRecognizerStateEnded || pinch.state == UIGestureRecognizerStateCancelled)
|
||
{
|
||
CGFloat velocity = pinch.velocity;
|
||
if (isnan(velocity))
|
||
{
|
||
// UIPinchGestureRecognizer sometimes returns NaN for the velocity
|
||
velocity = 0;
|
||
}
|
||
if (velocity > -0.5 && velocity < 3)
|
||
{
|
||
velocity = 0;
|
||
}
|
||
|
||
NSTimeInterval duration = (velocity > 0 ? 1 : 0.25) * self.decelerationRate;
|
||
|
||
CGFloat scale = self.scale * pinch.scale;
|
||
CGFloat newScale = scale;
|
||
if (velocity >= 0)
|
||
{
|
||
newScale += scale * velocity * duration * 0.1;
|
||
}
|
||
else
|
||
{
|
||
newScale += scale / (velocity * duration) * 0.1;
|
||
}
|
||
|
||
if (newScale <= 0 || log2(newScale) < *self.mbglMap.getBounds().minZoom)
|
||
{
|
||
velocity = 0;
|
||
}
|
||
|
||
BOOL drift = velocity && duration;
|
||
|
||
// Calculates the final camera zoom, this has no effect within current map camera.
|
||
double zoom = log2(newScale);
|
||
MLNMapCamera *toCamera = [self cameraByZoomingToZoomLevel:zoom aroundAnchorPoint:centerPoint];
|
||
|
||
if ( ! [self _shouldChangeFromCamera:oldCamera toCamera:toCamera])
|
||
{
|
||
drift = NO;
|
||
}
|
||
else
|
||
{
|
||
if (drift)
|
||
{
|
||
self.mbglMap.easeTo(mbgl::CameraOptions()
|
||
.withZoom(zoom)
|
||
.withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }),
|
||
MLNDurationFromTimeInterval(duration));
|
||
}
|
||
}
|
||
|
||
self.isZooming = NO;
|
||
[self notifyGestureDidEndWithDrift:drift];
|
||
[self unrotateIfNeededForGesture];
|
||
}
|
||
|
||
_previousPinchCenterPoint = centerPoint;
|
||
_previousPinchNumberOfTouches = pinch.numberOfTouches;
|
||
}
|
||
|
||
- (void)handleRotateGesture:(UIRotationGestureRecognizer *)rotate
|
||
{
|
||
if ( ! self.isRotateEnabled) return;
|
||
|
||
[self cancelTransitions];
|
||
|
||
CGPoint centerPoint = [self anchorPointForGesture:rotate];
|
||
if (self.anchorRotateOrZoomGesturesToCenterCoordinate) {
|
||
centerPoint = [self contentCenter];
|
||
}
|
||
MLNMapCamera *oldCamera = self.camera;
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonGestureRotate;
|
||
|
||
if ([[NSUserDefaults standardUserDefaults] objectForKey:MLNRotationThresholdWhileZoomingKey]) {
|
||
self.rotationThresholdWhileZooming = [[[NSUserDefaults standardUserDefaults] objectForKey:MLNRotationThresholdWhileZoomingKey] floatValue];
|
||
}
|
||
// Check whether a zoom triggered by a pinch gesture is occurring and if the rotation threshold has been met.
|
||
if (MLNDegreesFromRadians(self.rotationBeforeThresholdMet) < self.rotationThresholdWhileZooming && self.isZooming && !self.isRotating) {
|
||
self.rotationBeforeThresholdMet += fabs(rotate.rotation);
|
||
if (self.anchorRotateOrZoomGesturesToCenterCoordinate) {
|
||
self.rotationBeforeThresholdMet = 0;
|
||
}
|
||
rotate.rotation = 0;
|
||
return;
|
||
}
|
||
|
||
if (rotate.state == UIGestureRecognizerStateBegan || ! self.isRotating)
|
||
{
|
||
self.angle = MLNRadiansFromDegrees(*self.mbglMap.getCameraOptions().bearing) * -1;
|
||
|
||
self.isRotating = YES;
|
||
if (self.userTrackingMode != MLNUserTrackingModeNone)
|
||
{
|
||
self.userTrackingMode = MLNUserTrackingModeFollow;
|
||
}
|
||
|
||
self.shouldTriggerHapticFeedbackForCompass = NO;
|
||
[self notifyGestureDidBegin];
|
||
}
|
||
if (rotate.state == UIGestureRecognizerStateChanged)
|
||
{
|
||
CGFloat newDegrees = MLNDegreesFromRadians(self.angle + rotate.rotation) * -1;
|
||
|
||
// constrain to +/-30 degrees when merely rotating like Apple does
|
||
//
|
||
if ( ! self.isRotationAllowed && std::abs(self.pinch.scale) < 10)
|
||
{
|
||
newDegrees = fminf(newDegrees, 30);
|
||
newDegrees = fmaxf(newDegrees, -30);
|
||
}
|
||
|
||
MLNMapCamera *toCamera = [self cameraByRotatingToDirection:newDegrees aroundAnchorPoint:centerPoint];
|
||
|
||
if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera])
|
||
{
|
||
self.mbglMap.jumpTo(mbgl::CameraOptions()
|
||
.withBearing(newDegrees)
|
||
.withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y}));
|
||
}
|
||
|
||
[self cameraIsChanging];
|
||
|
||
// Trigger a light haptic feedback event when the user rotates to due north.
|
||
if (@available(iOS 10.0, *))
|
||
{
|
||
if (self.isHapticFeedbackEnabled && fabs(newDegrees) <= 1 && self.shouldTriggerHapticFeedbackForCompass)
|
||
{
|
||
UIImpactFeedbackGenerator *hapticFeedback = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
|
||
[hapticFeedback impactOccurred];
|
||
|
||
self.shouldTriggerHapticFeedbackForCompass = NO;
|
||
}
|
||
else if (fabs(newDegrees) > 1)
|
||
{
|
||
self.shouldTriggerHapticFeedbackForCompass = YES;
|
||
}
|
||
}
|
||
}
|
||
else if ((rotate.state == UIGestureRecognizerStateEnded || rotate.state == UIGestureRecognizerStateCancelled))
|
||
{
|
||
self.rotationBeforeThresholdMet = 0;
|
||
if (! self.isRotating) { return; }
|
||
self.isRotating = NO;
|
||
|
||
CGFloat velocity = rotate.velocity;
|
||
CGFloat decelerationRate = self.decelerationRate;
|
||
if (decelerationRate != MLNMapViewDecelerationRateImmediate && fabs(velocity) > 3)
|
||
{
|
||
CGFloat radians = self.angle + rotate.rotation;
|
||
CGFloat newRadians = radians + velocity * decelerationRate * 0.1;
|
||
CGFloat newDegrees = MLNDegreesFromRadians(newRadians) * -1;
|
||
|
||
MLNMapCamera *toCamera = [self cameraByRotatingToDirection:newDegrees aroundAnchorPoint:centerPoint];
|
||
|
||
if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera])
|
||
{
|
||
self.mbglMap.easeTo(mbgl::CameraOptions()
|
||
.withBearing(newDegrees)
|
||
.withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }),
|
||
MLNDurationFromTimeInterval(decelerationRate));
|
||
|
||
[self notifyGestureDidEndWithDrift:YES];
|
||
__weak MLNMapView *weakSelf = self;
|
||
|
||
[self animateWithDelay:decelerationRate animations:^
|
||
{
|
||
[weakSelf unrotateIfNeededForGesture];
|
||
}];
|
||
}
|
||
}
|
||
else
|
||
{
|
||
[self notifyGestureDidEndWithDrift:NO];
|
||
[self unrotateIfNeededForGesture];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)handleSingleTapGesture:(UITapGestureRecognizer *)singleTap
|
||
{
|
||
if (singleTap.state != UIGestureRecognizerStateRecognized) return;
|
||
|
||
if (self.mapViewProxyAccessibilityElement.accessibilityElementIsFocused)
|
||
{
|
||
id nextElement;
|
||
if (_userLocationAnnotationIsSelected)
|
||
{
|
||
nextElement = self.userLocationAnnotationView;
|
||
}
|
||
else
|
||
{
|
||
if (_selectedAnnotationTag != MLNAnnotationTagNotFound) {
|
||
nextElement = _annotationContextsByAnnotationTag.at(_selectedAnnotationTag).accessibilityElement;
|
||
}
|
||
}
|
||
[self deselectAnnotation:self.selectedAnnotation animated:YES];
|
||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nextElement);
|
||
|
||
return;
|
||
}
|
||
|
||
id<MLNAnnotation> annotation = [self annotationForGestureRecognizer:singleTap persistingResults:YES];
|
||
if (annotation)
|
||
{
|
||
CGPoint calloutPoint = [singleTap locationInView:self];
|
||
CGRect positionRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:calloutPoint];
|
||
[self selectAnnotation:annotation moveIntoView:YES animateSelection:YES calloutPositioningRect:positionRect completionHandler:nil];
|
||
}
|
||
else if (self.selectedAnnotation)
|
||
{
|
||
[self deselectAnnotation:self.selectedAnnotation animated:YES];
|
||
}
|
||
}
|
||
|
||
/**
|
||
Returns the annotation that would be selected by a tap gesture recognizer.
|
||
|
||
This is used when a gesture is recognized, and to check if the gesture should be recognized.
|
||
|
||
@param singleTap An in progress tap gesture recognizer.
|
||
@param persist True to remember the cycleable set of annotations. @see annotationTagAtPoint:persistingResults
|
||
*/
|
||
- (nullable id <MLNAnnotation>)annotationForGestureRecognizer:(UITapGestureRecognizer*)singleTap persistingResults:(BOOL)persist
|
||
{
|
||
CGPoint tapPoint = [singleTap locationInView:self];
|
||
|
||
if (self.userLocationVisible)
|
||
{
|
||
CGPoint tapPointForUserLocation;
|
||
if (self.userLocationAnnotationView.hitTestLayer == self.userLocationAnnotationView.layer.presentationLayer)
|
||
{
|
||
tapPointForUserLocation = tapPoint;
|
||
}
|
||
else
|
||
{
|
||
// Get the tap point within the custom hit test layer.
|
||
tapPointForUserLocation = [singleTap locationInView:self.userLocationAnnotationView];
|
||
}
|
||
|
||
CALayer *hitLayer = [self.userLocationAnnotationView.hitTestLayer hitTest:tapPointForUserLocation];
|
||
|
||
if (hitLayer)
|
||
{
|
||
if ( ! _userLocationAnnotationIsSelected)
|
||
{
|
||
return self.userLocation;
|
||
}
|
||
return nil;
|
||
}
|
||
}
|
||
|
||
MLNAnnotationTag hitAnnotationTag = [self annotationTagAtPoint:tapPoint persistingResults:persist];
|
||
if (hitAnnotationTag != MLNAnnotationTagNotFound)
|
||
{
|
||
if (hitAnnotationTag != _selectedAnnotationTag)
|
||
{
|
||
id <MLNAnnotation> annotation = [self annotationWithTag:hitAnnotationTag];
|
||
MLNAssert(annotation, @"Cannot select nonexistent annotation with tag %llu", hitAnnotationTag);
|
||
return annotation;
|
||
}
|
||
}
|
||
|
||
return nil;
|
||
}
|
||
|
||
- (void)handleDoubleTapGesture:(UITapGestureRecognizer *)doubleTap
|
||
{
|
||
if (doubleTap.state != UIGestureRecognizerStateRecognized) return;
|
||
|
||
if ( ! self.isZoomEnabled) return;
|
||
|
||
[self cancelTransitions];
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonGestureZoomIn;
|
||
|
||
MLNMapCamera *oldCamera = self.camera;
|
||
|
||
double newZoom = round(self.zoomLevel) + 1.0;
|
||
|
||
CGPoint gesturePoint = [self anchorPointForGesture:doubleTap];
|
||
|
||
MLNMapCamera *toCamera = [self cameraByZoomingToZoomLevel:newZoom aroundAnchorPoint:gesturePoint];
|
||
|
||
if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera])
|
||
{
|
||
mbgl::ScreenCoordinate center(gesturePoint.x, gesturePoint.y);
|
||
self.mbglMap.easeTo(mbgl::CameraOptions()
|
||
.withZoom(newZoom)
|
||
.withAnchor(center),
|
||
MLNDurationFromTimeInterval(MLNAnimationDuration));
|
||
|
||
__weak MLNMapView *weakSelf = self;
|
||
|
||
[self animateWithDelay:MLNAnimationDuration animations:^
|
||
{
|
||
[weakSelf unrotateIfNeededForGesture];
|
||
}];
|
||
}
|
||
else
|
||
{
|
||
[self unrotateIfNeededForGesture];
|
||
}
|
||
}
|
||
|
||
- (void)handleTwoFingerTapGesture:(UITapGestureRecognizer *)twoFingerTap
|
||
{
|
||
if (twoFingerTap.state != UIGestureRecognizerStateRecognized) return;
|
||
|
||
if ( ! self.isZoomEnabled) return;
|
||
|
||
if ([self zoomLevel] == *self.mbglMap.getBounds().minZoom) return;
|
||
|
||
[self cancelTransitions];
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonGestureZoomOut;
|
||
|
||
MLNMapCamera *oldCamera = self.camera;
|
||
|
||
double newZoom = round(self.zoomLevel) - 1.0;
|
||
|
||
CGPoint gesturePoint = [self anchorPointForGesture:twoFingerTap];
|
||
|
||
MLNMapCamera *toCamera = [self cameraByZoomingToZoomLevel:newZoom aroundAnchorPoint:gesturePoint];
|
||
|
||
if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera])
|
||
{
|
||
mbgl::ScreenCoordinate center(gesturePoint.x, gesturePoint.y);
|
||
self.mbglMap.easeTo(mbgl::CameraOptions()
|
||
.withZoom(newZoom)
|
||
.withAnchor(center),
|
||
MLNDurationFromTimeInterval(MLNAnimationDuration));
|
||
|
||
__weak MLNMapView *weakSelf = self;
|
||
|
||
[self animateWithDelay:MLNAnimationDuration animations:^
|
||
{
|
||
[weakSelf unrotateIfNeededForGesture];
|
||
}];
|
||
}
|
||
}
|
||
|
||
- (void)handleQuickZoomGesture:(UILongPressGestureRecognizer *)quickZoom
|
||
{
|
||
if ( ! self.isZoomEnabled) return;
|
||
|
||
[self cancelTransitions];
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonGestureOneFingerZoom;
|
||
|
||
if (quickZoom.state == UIGestureRecognizerStateBegan)
|
||
{
|
||
self.scale = powf(2, [self zoomLevel]);
|
||
|
||
self.quickZoomStart = [quickZoom locationInView:quickZoom.view].y;
|
||
|
||
[self notifyGestureDidBegin];
|
||
}
|
||
else if (quickZoom.state == UIGestureRecognizerStateChanged)
|
||
{
|
||
CGFloat distance = [quickZoom locationInView:quickZoom.view].y - self.quickZoomStart;
|
||
|
||
CGFloat newZoom = MAX(log2f(self.scale) + (distance / 75), *self.mbglMap.getBounds().minZoom);
|
||
|
||
if ([self zoomLevel] == newZoom) return;
|
||
|
||
CGPoint centerPoint = [self anchorPointForGesture:quickZoom];
|
||
|
||
MLNMapCamera *oldCamera = self.camera;
|
||
MLNMapCamera *toCamera = [self cameraByZoomingToZoomLevel:newZoom aroundAnchorPoint:centerPoint];
|
||
|
||
if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera])
|
||
{
|
||
self.mbglMap.jumpTo(mbgl::CameraOptions()
|
||
.withZoom(newZoom)
|
||
.withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }));
|
||
}
|
||
|
||
[self cameraIsChanging];
|
||
}
|
||
else if (quickZoom.state == UIGestureRecognizerStateEnded || quickZoom.state == UIGestureRecognizerStateCancelled)
|
||
{
|
||
[self notifyGestureDidEndWithDrift:NO];
|
||
[self unrotateIfNeededForGesture];
|
||
}
|
||
}
|
||
|
||
- (void)handleTwoFingerDragGesture:(UIPanGestureRecognizer *)twoFingerDrag
|
||
{
|
||
if ( ! self.isPitchEnabled) return;
|
||
|
||
[self cancelTransitions];
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonGestureTilt;
|
||
static CGFloat initialPitch;
|
||
|
||
if (twoFingerDrag.state == UIGestureRecognizerStateBegan)
|
||
{
|
||
CGPoint midPoint = [twoFingerDrag translationInView:twoFingerDrag.view];
|
||
// In the following if and for the first execution middlePoint
|
||
// will be equal to dragGestureMiddlePoint and the resulting
|
||
// gestureSlopeAngle will be 0º causing a small delay,
|
||
// initializing dragGestureMiddlePoint with the current midPoint
|
||
// but substracting one point from 'y' forces an initial 90º angle
|
||
// making the gesture avoid the delay
|
||
self.dragGestureMiddlePoint = CGPointMake(midPoint.x, midPoint.y-1);
|
||
initialPitch = *self.mbglMap.getCameraOptions().pitch;
|
||
[self notifyGestureDidBegin];
|
||
}
|
||
|
||
if (twoFingerDrag.state == UIGestureRecognizerStateBegan || twoFingerDrag.state == UIGestureRecognizerStateChanged)
|
||
{
|
||
if (twoFingerDrag.numberOfTouches != 2)
|
||
{
|
||
twoFingerDrag.state = UIGestureRecognizerStateEnded;
|
||
return;
|
||
}
|
||
|
||
CGPoint leftTouchPoint = [twoFingerDrag locationOfTouch:0 inView:twoFingerDrag.view];
|
||
CGPoint rightTouchPoint = [twoFingerDrag locationOfTouch:1 inView:twoFingerDrag.view];
|
||
CLLocationDegrees fingerSlopeAngle = [self angleBetweenPoints:leftTouchPoint endPoint:rightTouchPoint];
|
||
|
||
CGPoint middlePoint = [twoFingerDrag translationInView:twoFingerDrag.view];
|
||
|
||
CLLocationDegrees gestureSlopeAngle = [self angleBetweenPoints:self.dragGestureMiddlePoint endPoint:middlePoint];
|
||
self.dragGestureMiddlePoint = middlePoint;
|
||
if (fabs(fingerSlopeAngle) < MLNHorizontalTiltToleranceDegrees && fabs(gestureSlopeAngle) > 60.0 ) {
|
||
|
||
CGFloat gestureDistance = middlePoint.y;
|
||
CGFloat slowdown = 2.0;
|
||
|
||
CGFloat pitchNew = initialPitch - (gestureDistance / slowdown);
|
||
|
||
CGPoint centerPoint = [self anchorPointForGesture:twoFingerDrag];
|
||
|
||
MLNMapCamera *oldCamera = self.camera;
|
||
MLNMapCamera *toCamera = [self cameraByTiltingToPitch:pitchNew];
|
||
|
||
if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera])
|
||
{
|
||
self.mbglMap.jumpTo(mbgl::CameraOptions()
|
||
.withPitch(pitchNew)
|
||
.withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }));
|
||
}
|
||
|
||
[self cameraIsChanging];
|
||
|
||
}
|
||
|
||
|
||
}
|
||
else if (twoFingerDrag.state == UIGestureRecognizerStateEnded || twoFingerDrag.state == UIGestureRecognizerStateCancelled)
|
||
{
|
||
[self notifyGestureDidEndWithDrift:NO];
|
||
[self unrotateIfNeededForGesture];
|
||
self.dragGestureMiddlePoint = CGPointZero;
|
||
}
|
||
|
||
}
|
||
|
||
- (MLNMapCamera *)cameraByPanningWithTranslation:(CGPoint)endPoint panGesture:(UIPanGestureRecognizer *)pan
|
||
{
|
||
MLNMapCamera *panCamera = [self.camera copy];
|
||
|
||
CGPoint centerPoint = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
|
||
CGPoint endCameraPoint = CGPointMake(centerPoint.x - endPoint.x, centerPoint.y - endPoint.y);
|
||
CLLocationCoordinate2D panCoordinate = [self convertPoint:endCameraPoint toCoordinateFromView:pan.view];
|
||
|
||
panCamera.centerCoordinate = panCoordinate;
|
||
|
||
return panCamera;
|
||
}
|
||
|
||
- (MLNMapCamera *)cameraByZoomingToZoomLevel:(double)zoom aroundAnchorPoint:(CGPoint)anchorPoint
|
||
{
|
||
mbgl::ScreenCoordinate anchor = mbgl::ScreenCoordinate { anchorPoint.x, anchorPoint.y };
|
||
mbgl::EdgeInsets padding = mbgl::EdgeInsets(anchor.y, anchor.x, self.size.height - anchor.y, self.size.width - anchor.x);
|
||
mbgl::CameraOptions currentCameraOptions = self.mbglMap.getCameraOptions(padding);
|
||
|
||
currentCameraOptions.zoom = mbgl::util::clamp(zoom, self.minimumZoomLevel, self.maximumZoomLevel);
|
||
currentCameraOptions.anchor = anchor;
|
||
MLNCoordinateBounds bounds = MLNCoordinateBoundsFromLatLngBounds(self.mbglMap.latLngBoundsForCamera(currentCameraOptions));
|
||
|
||
return [self cameraThatFitsCoordinateBounds:bounds];
|
||
}
|
||
|
||
- (MLNMapCamera *)cameraByRotatingToDirection:(CLLocationDirection)degrees aroundAnchorPoint:(CGPoint)anchorPoint
|
||
{
|
||
mbgl::CameraOptions currentCameraOptions = self.mbglMap.getCameraOptions();
|
||
|
||
MLNMapCamera *camera;
|
||
|
||
mbgl::ScreenCoordinate anchor = mbgl::ScreenCoordinate { anchorPoint.x, anchorPoint.y };
|
||
currentCameraOptions.bearing = degrees;
|
||
currentCameraOptions.anchor = anchor;
|
||
camera = [self cameraForCameraOptions:currentCameraOptions];
|
||
|
||
return camera;
|
||
}
|
||
|
||
- (MLNMapCamera *)cameraByTiltingToPitch:(CGFloat)pitch
|
||
{
|
||
mbgl::CameraOptions currentCameraOptions = self.mbglMap.getCameraOptions();
|
||
|
||
MLNMapCamera *camera;
|
||
|
||
currentCameraOptions.pitch = pitch;
|
||
camera = [self cameraForCameraOptions:currentCameraOptions];
|
||
|
||
return camera;
|
||
}
|
||
|
||
- (CGPoint)anchorPointForGesture:(UIGestureRecognizer *)gesture {
|
||
if (self.userTrackingMode != MLNUserTrackingModeNone)
|
||
{
|
||
return self.userLocationAnnotationViewCenter;
|
||
}
|
||
|
||
// Special case for two-finger drag and quickzoom
|
||
if ([gesture isKindOfClass:[UIPanGestureRecognizer class]] || [gesture isKindOfClass:[UILongPressGestureRecognizer class]])
|
||
{
|
||
return self.contentCenter;
|
||
}
|
||
|
||
return [gesture locationInView:gesture.view];
|
||
}
|
||
|
||
- (void)handleCalloutAccessoryTapGesture:(UITapGestureRecognizer *)tap
|
||
{
|
||
if ([self.delegate respondsToSelector:@selector(mapView:annotation:calloutAccessoryControlTapped:)])
|
||
{
|
||
MLNAssert([tap.view isKindOfClass:[UIControl class]], @"Tapped view %@ is not a UIControl", tap.view);
|
||
id <MLNAnnotation> selectedAnnotation = self.selectedAnnotation;
|
||
MLNAssert(selectedAnnotation, @"Selected annotation should not be nil.");
|
||
[self.delegate mapView:self annotation:selectedAnnotation
|
||
calloutAccessoryControlTapped:(UIControl *)tap.view];
|
||
}
|
||
}
|
||
|
||
- (BOOL)calloutViewShouldHighlight:(__unused MLNCompactCalloutView *)calloutView
|
||
{
|
||
return [self.delegate respondsToSelector:@selector(mapView:tapOnCalloutForAnnotation:)];
|
||
}
|
||
|
||
- (void)calloutViewClicked:(__unused MLNSMCalloutView *)calloutView
|
||
{
|
||
if ([self.delegate respondsToSelector:@selector(mapView:tapOnCalloutForAnnotation:)])
|
||
{
|
||
id <MLNAnnotation> selectedAnnotation = self.selectedAnnotation;
|
||
MLNAssert(selectedAnnotation, @"Selected annotation should not be nil.");
|
||
[self.delegate mapView:self tapOnCalloutForAnnotation:selectedAnnotation];
|
||
}
|
||
}
|
||
|
||
- (void)calloutViewTapped:(__unused MLNCompactCalloutView *)calloutView
|
||
{
|
||
if ([self.delegate respondsToSelector:@selector(mapView:tapOnCalloutForAnnotation:)])
|
||
{
|
||
id <MLNAnnotation> selectedAnnotation = self.selectedAnnotation;
|
||
MLNAssert(selectedAnnotation, @"Selected annotation should not be nil.");
|
||
[self.delegate mapView:self tapOnCalloutForAnnotation:selectedAnnotation];
|
||
}
|
||
}
|
||
|
||
- (void)calloutViewDidAppear:(UIView<MLNCalloutView> *)calloutView
|
||
{
|
||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
|
||
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, calloutView);
|
||
|
||
[self updatePresentsWithTransaction];
|
||
|
||
// TODO: Add sibling disappear method
|
||
}
|
||
|
||
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
|
||
{
|
||
if (gestureRecognizer == _twoFingerDrag)
|
||
{
|
||
UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)gestureRecognizer;
|
||
|
||
if (panGesture.minimumNumberOfTouches == 2)
|
||
{
|
||
CGPoint leftTouchPoint = [panGesture locationOfTouch:0 inView:panGesture.view];
|
||
CGPoint rightTouchPoint = [panGesture locationOfTouch:1 inView:panGesture.view];
|
||
|
||
CLLocationDegrees degrees = [self angleBetweenPoints:leftTouchPoint endPoint:rightTouchPoint];
|
||
if (fabs(degrees) > MLNHorizontalTiltToleranceDegrees) {
|
||
return NO;
|
||
}
|
||
}
|
||
}
|
||
else if (gestureRecognizer == _singleTapGestureRecognizer)
|
||
{
|
||
// Gesture will be recognized if it could deselect an annotation
|
||
if(!self.selectedAnnotation)
|
||
{
|
||
id<MLNAnnotation> annotation = [self annotationForGestureRecognizer:(UITapGestureRecognizer*)gestureRecognizer persistingResults:NO];
|
||
if (!annotation) {
|
||
return NO;
|
||
}
|
||
}
|
||
}
|
||
else if (gestureRecognizer == _pan)
|
||
{
|
||
if (self.anchorRotateOrZoomGesturesToCenterCoordinate) {
|
||
if (self.isZooming || self.isRotating) {
|
||
return NO;
|
||
}
|
||
}
|
||
}
|
||
return YES;
|
||
}
|
||
|
||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
||
{
|
||
NSArray *validSimultaneousGestures = @[ self.pan, self.pinch, self.rotate ];
|
||
return ([validSimultaneousGestures containsObject:gestureRecognizer] && [validSimultaneousGestures containsObject:otherGestureRecognizer]);
|
||
}
|
||
|
||
- (CLLocationDegrees)angleBetweenPoints:(CGPoint)originPoint endPoint:(CGPoint)endPoint
|
||
{
|
||
if (originPoint.x > endPoint.x) {
|
||
CGPoint swap = originPoint;
|
||
originPoint = endPoint;
|
||
endPoint = swap;
|
||
}
|
||
|
||
CGFloat x = (endPoint.x - originPoint.x);
|
||
CGFloat y = (endPoint.y - originPoint.y);
|
||
|
||
CGFloat angleInRadians = atan2(y, x);
|
||
CLLocationDegrees angleInDegrees = MLNDegreesFromRadians(angleInRadians);
|
||
|
||
return angleInDegrees;
|
||
}
|
||
|
||
// MARK: - Attribution -
|
||
|
||
- (void)showAttribution:(id)sender
|
||
{
|
||
BOOL shouldShowVersion = [sender isKindOfClass:[UILongPressGestureRecognizer class]];
|
||
if (shouldShowVersion)
|
||
{
|
||
UILongPressGestureRecognizer *longPress = (UILongPressGestureRecognizer *)sender;
|
||
if (longPress.state != UIGestureRecognizerStateBegan)
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
|
||
NSString *actionSheetTitle = NSLocalizedStringWithDefaultValue(@"SDK_NAME", nil, nil, @"MapLibre Native iOS", @"Action sheet title");
|
||
UIAlertController *attributionController = [UIAlertController alertControllerWithTitle:actionSheetTitle
|
||
message:nil
|
||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||
NSString *version = [NSBundle mgl_frameworkInfoDictionary][@"CFBundleShortVersionString"];
|
||
if (shouldShowVersion && version != nil && ![version isEqualToString:@""])
|
||
{
|
||
attributionController.title = [actionSheetTitle stringByAppendingFormat:@" %@", version];
|
||
}
|
||
|
||
NSArray *attributionInfos = [self.style attributionInfosWithFontSize:[UIFont buttonFontSize] linkColor:nil];
|
||
for (MLNAttributionInfo *attributionInfo in attributionInfos)
|
||
{
|
||
UIAlertAction *action = [UIAlertAction actionWithTitle:[attributionInfo.title.string mgl_titleCasedStringWithLocale:[NSLocale currentLocale]]
|
||
style:UIAlertActionStyleDefault
|
||
handler:^(UIAlertAction * _Nonnull actionBlock) {
|
||
NSURL *url = attributionInfo.URL;
|
||
if (url)
|
||
{
|
||
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
|
||
}
|
||
}];
|
||
[attributionController addAction:action];
|
||
}
|
||
|
||
NSString *cancelTitle = NSLocalizedStringWithDefaultValue(@"CANCEL", nil, nil, @"Cancel", @"Title of button for dismissing attribution action sheet");
|
||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:cancelTitle
|
||
style:UIAlertActionStyleCancel
|
||
handler:NULL];
|
||
[attributionController addAction:cancelAction];
|
||
|
||
attributionController.popoverPresentationController.sourceView = self;
|
||
attributionController.popoverPresentationController.sourceRect = self.attributionButton.frame;
|
||
|
||
UIViewController *viewController = [self.window.rootViewController mgl_topMostViewController];
|
||
[viewController presentViewController:attributionController animated:YES completion:NULL];
|
||
self.attributionController = attributionController;
|
||
}
|
||
|
||
// MARK: - Properties -
|
||
|
||
static void *windowScreenContext = &windowScreenContext;
|
||
|
||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
||
{
|
||
if ([keyPath isEqualToString:@"coordinate"] && [object conformsToProtocol:@protocol(MLNAnnotation)] && ![object isKindOfClass:[MLNMultiPoint class]])
|
||
{
|
||
id <MLNAnnotation> annotation = object;
|
||
MLNAnnotationTag annotationTag = (MLNAnnotationTag)(NSUInteger)context;
|
||
// We can get here because a subclass registered itself as an observer
|
||
// of the coordinate key path of a non-multipoint annotation but failed
|
||
// to handle the change. This check deters us from treating the
|
||
// subclass’s context as an annotation tag. If the context happens to
|
||
// match a valid annotation tag, the annotation will be unnecessarily
|
||
// but safely updated.
|
||
if (annotation == [self annotationWithTag:annotationTag])
|
||
{
|
||
const mbgl::Point<double> point = MLNPointFromLocationCoordinate2D(annotation.coordinate);
|
||
|
||
if (annotationTag != MLNAnnotationTagNotFound) {
|
||
MLNAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
|
||
if (annotationContext.annotationView)
|
||
{
|
||
// Redundantly move the associated annotation view outside the scope of the animation-less transaction block in -updateAnnotationViews.
|
||
annotationContext.annotationView.center = [self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self];
|
||
}
|
||
|
||
MLNAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
|
||
NSString *symbolName = annotationImage.styleIconIdentifier;
|
||
|
||
// Update the annotation’s backing geometry to match the annotation model object. Any associated annotation view is also moved by side effect. However, -updateAnnotationViews disables the view’s animation actions, because it can’t distinguish between moves due to the viewport changing and moves due to the annotation’s coordinate changing.
|
||
self.mbglMap.updateAnnotation(annotationTag, mbgl::SymbolAnnotation { point, symbolName.UTF8String });
|
||
[self updateCalloutView];
|
||
}
|
||
}
|
||
}
|
||
else if ([keyPath isEqualToString:@"coordinates"] && [object isKindOfClass:[MLNMultiPoint class]])
|
||
{
|
||
MLNMultiPoint *annotation = object;
|
||
MLNAnnotationTag annotationTag = (MLNAnnotationTag)(NSUInteger)context;
|
||
// We can get here because a subclass registered itself as an observer
|
||
// of the coordinates key path of a multipoint annotation but failed
|
||
// to handle the change. This check deters us from treating the
|
||
// subclass’s context as an annotation tag. If the context happens to
|
||
// match a valid annotation tag, the annotation will be unnecessarily
|
||
// but safely updated.
|
||
if (annotation == [self annotationWithTag:annotationTag])
|
||
{
|
||
// Update the annotation’s backing geometry to match the annotation model object.
|
||
self.mbglMap.updateAnnotation(annotationTag, [annotation annotationObjectWithDelegate:self]);
|
||
[self updateCalloutView];
|
||
}
|
||
}
|
||
else if (context == windowScreenContext)
|
||
{
|
||
if ([keyPath isEqualToString:@"screen"] ||
|
||
[keyPath isEqualToString:@"windowScene"]) {
|
||
[self destroyDisplayLink];
|
||
[self didBecomeActive:nil];
|
||
}
|
||
}
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingZoomEnabled
|
||
{
|
||
return [NSSet setWithObject:@"allowsZooming"];
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingScrollEnabled
|
||
{
|
||
return [NSSet setWithObject:@"allowsScrolling"];
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingRotateEnabled
|
||
{
|
||
return [NSSet setWithObject:@"allowsRotating"];
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingPitchEnabled
|
||
{
|
||
return [NSSet setWithObject:@"allowsTilting"];
|
||
}
|
||
|
||
- (MLNMapDebugMaskOptions)debugMask
|
||
{
|
||
if (!_mbglMap)
|
||
{
|
||
NSAssert(self.terminated, @"_mbglMap should only be unavailable during app termination");
|
||
return self.residualDebugMask;
|
||
}
|
||
|
||
mbgl::MapDebugOptions options = self.mbglMap.getDebug();
|
||
MLNMapDebugMaskOptions mask = 0;
|
||
if (options & mbgl::MapDebugOptions::TileBorders)
|
||
{
|
||
mask |= MLNMapDebugTileBoundariesMask;
|
||
}
|
||
if (options & mbgl::MapDebugOptions::ParseStatus)
|
||
{
|
||
mask |= MLNMapDebugTileInfoMask;
|
||
}
|
||
if (options & mbgl::MapDebugOptions::Timestamps)
|
||
{
|
||
mask |= MLNMapDebugTimestampsMask;
|
||
}
|
||
if (options & mbgl::MapDebugOptions::Collision)
|
||
{
|
||
mask |= MLNMapDebugCollisionBoxesMask;
|
||
}
|
||
if (options & mbgl::MapDebugOptions::Overdraw)
|
||
{
|
||
mask |= MLNMapDebugOverdrawVisualizationMask;
|
||
}
|
||
return mask;
|
||
}
|
||
|
||
- (void)setDebugMask:(MLNMapDebugMaskOptions)debugMask
|
||
{
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
|
||
mbgl::MapDebugOptions options = mbgl::MapDebugOptions::NoDebug;
|
||
if (debugMask & MLNMapDebugTileBoundariesMask)
|
||
{
|
||
options |= mbgl::MapDebugOptions::TileBorders;
|
||
}
|
||
if (debugMask & MLNMapDebugTileInfoMask)
|
||
{
|
||
options |= mbgl::MapDebugOptions::ParseStatus;
|
||
}
|
||
if (debugMask & MLNMapDebugTimestampsMask)
|
||
{
|
||
options |= mbgl::MapDebugOptions::Timestamps;
|
||
}
|
||
if (debugMask & MLNMapDebugCollisionBoxesMask)
|
||
{
|
||
options |= mbgl::MapDebugOptions::Collision;
|
||
}
|
||
if (debugMask & MLNMapDebugOverdrawVisualizationMask)
|
||
{
|
||
options |= mbgl::MapDebugOptions::Overdraw;
|
||
}
|
||
self.mbglMap.setDebug(options);
|
||
}
|
||
|
||
- (void)resetNorth
|
||
{
|
||
MLNLogInfo(@"Resetting the map rotation to a northern heading — a direction of 0 degrees.");
|
||
[self resetNorthAnimated:YES];
|
||
}
|
||
|
||
- (void)resetNorthAnimated:(BOOL)animated
|
||
{
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonResetNorth;
|
||
|
||
[self setDirection:0 animated:animated];
|
||
}
|
||
|
||
- (void)resetPosition
|
||
{
|
||
MLNLogInfo(@"Resetting the map to the current style’s default viewport.");
|
||
auto camera = self.mbglMap.getStyle().getDefaultCamera();
|
||
|
||
double pitch = camera.pitch ? *camera.pitch : 0.0;
|
||
double bearing = camera.bearing ? *camera.bearing : 0.0;
|
||
double zoom = camera.zoom ? *camera.zoom : 0.0;
|
||
mbgl::LatLng center = camera.center ? *camera.center : mbgl::LatLng();
|
||
|
||
CLLocationDirection heading = mbgl::util::wrap(bearing, 0., 360.);
|
||
CLLocationDistance altitude = MLNAltitudeForZoomLevel(zoom, pitch, 0, self.frame.size);
|
||
self.camera = [MLNMapCamera cameraLookingAtCenterCoordinate:MLNLocationCoordinate2DFromLatLng(center)
|
||
altitude:altitude
|
||
pitch:pitch
|
||
heading:heading];
|
||
}
|
||
|
||
- (void)setZoomEnabled:(BOOL)zoomEnabled
|
||
{
|
||
MLNLogDebug(@"Setting zoomEnabled: %@", MLNStringFromBOOL(zoomEnabled));
|
||
_zoomEnabled = zoomEnabled;
|
||
self.pinch.enabled = zoomEnabled;
|
||
self.doubleTap.enabled = zoomEnabled;
|
||
self.quickZoom.enabled = zoomEnabled;
|
||
self.twoFingerTap.enabled = zoomEnabled;
|
||
}
|
||
|
||
- (void)setScrollEnabled:(BOOL)scrollEnabled
|
||
{
|
||
MLNLogDebug(@"Setting scrollEnabled: %@", MLNStringFromBOOL(scrollEnabled));
|
||
_scrollEnabled = scrollEnabled;
|
||
self.pan.enabled = scrollEnabled;
|
||
}
|
||
|
||
- (void)setRotateEnabled:(BOOL)rotateEnabled
|
||
{
|
||
MLNLogDebug(@"Setting rotateEnabled: %@", MLNStringFromBOOL(rotateEnabled));
|
||
_rotateEnabled = rotateEnabled;
|
||
self.rotate.enabled = rotateEnabled;
|
||
}
|
||
|
||
- (void)setPitchEnabled:(BOOL)pitchEnabled
|
||
{
|
||
MLNLogDebug(@"Setting pitchEnabled: %@", MLNStringFromBOOL(pitchEnabled));
|
||
_pitchEnabled = pitchEnabled;
|
||
self.twoFingerDrag.enabled = pitchEnabled;
|
||
}
|
||
|
||
- (void)setShowsScale:(BOOL)showsScale
|
||
{
|
||
MLNLogDebug(@"Setting showsScale: %@", MLNStringFromBOOL(showsScale));
|
||
_showsScale = showsScale;
|
||
self.scaleBar.hidden = !showsScale;
|
||
|
||
if (showsScale)
|
||
{
|
||
[self updateScaleBar];
|
||
}
|
||
}
|
||
|
||
- (void)setScaleBarShouldShowDarkStyles:(BOOL)scaleBarShouldShowDarkStyles {
|
||
|
||
_scaleBarShouldShowDarkStyles = scaleBarShouldShowDarkStyles;
|
||
[(MLNScaleBar *)self.scaleBar setShouldShowDarkStyles:scaleBarShouldShowDarkStyles];
|
||
|
||
}
|
||
|
||
- (void)setScaleBarUsesMetricSystem:(BOOL)scaleBarUsesMetricSystem {
|
||
|
||
_scaleBarUsesMetricSystem = scaleBarUsesMetricSystem;
|
||
[(MLNScaleBar *)self.scaleBar setUsesMetricSystem:scaleBarUsesMetricSystem];
|
||
|
||
}
|
||
|
||
- (void)setPrefetchesTiles:(BOOL)prefetchesTiles
|
||
{
|
||
self.mbglMap.setPrefetchZoomDelta(prefetchesTiles ? mbgl::util::DEFAULT_PREFETCH_ZOOM_DELTA : 0);
|
||
}
|
||
|
||
- (BOOL)prefetchesTiles
|
||
{
|
||
return self.mbglMap.getPrefetchZoomDelta() > 0 ? YES : NO;
|
||
}
|
||
|
||
- (void)setTileCacheEnabled:(BOOL)enabled
|
||
{
|
||
_rendererFrontend->setTileCacheEnabled(enabled);
|
||
}
|
||
|
||
- (BOOL)tileCacheEnabled
|
||
{
|
||
return _rendererFrontend->getTileCacheEnabled();
|
||
}
|
||
|
||
// MARK: - Accessibility -
|
||
|
||
- (NSString *)accessibilityValue
|
||
{
|
||
NSMutableArray *facts = [NSMutableArray array];
|
||
|
||
double zoomLevel = round(self.zoomLevel + 1);
|
||
[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 <MLNFeature> 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;
|
||
}
|
||
|
||
- (NSArray<id <MLNFeature>> *)visiblePlaceFeatures
|
||
{
|
||
if (!_visiblePlaceFeatures)
|
||
{
|
||
NSArray *placeStyleLayerIdentifiers = [self.style.placeStyleLayers valueForKey:@"identifier"];
|
||
_visiblePlaceFeatures = [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:placeStyleLayerIdentifiers]];
|
||
}
|
||
return _visiblePlaceFeatures;
|
||
}
|
||
|
||
- (NSArray<id <MLNFeature>> *)visibleRoadFeatures
|
||
{
|
||
if (!_visibleRoadFeatures)
|
||
{
|
||
NSArray *roadStyleLayerIdentifiers = [self.style.roadStyleLayers valueForKey:@"identifier"];
|
||
_visibleRoadFeatures = [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:roadStyleLayerIdentifiers]];
|
||
}
|
||
return _visibleRoadFeatures;
|
||
}
|
||
|
||
- (CGRect)accessibilityFrame
|
||
{
|
||
CGRect frame = [super accessibilityFrame];
|
||
UIViewController *viewController = self.mgl_viewControllerForLayoutGuides;
|
||
if (viewController)
|
||
{
|
||
#pragma clang diagnostic push
|
||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||
CGFloat topInset = viewController.topLayoutGuide.length;
|
||
frame.origin.y += topInset;
|
||
frame.size.height -= topInset + viewController.bottomLayoutGuide.length;
|
||
#pragma clang diagnostic pop
|
||
}
|
||
return frame;
|
||
}
|
||
|
||
- (UIBezierPath *)accessibilityPath
|
||
{
|
||
UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.accessibilityFrame];
|
||
|
||
// Exclude any visible annotation callout view.
|
||
if (self.calloutViewForSelectedAnnotation)
|
||
{
|
||
UIBezierPath *calloutViewPath = [UIBezierPath bezierPathWithRect:self.calloutViewForSelectedAnnotation.frame];
|
||
[path appendPath:calloutViewPath];
|
||
}
|
||
|
||
return path;
|
||
}
|
||
|
||
- (NSInteger)accessibilityElementCount
|
||
{
|
||
if (self.calloutViewForSelectedAnnotation)
|
||
{
|
||
return 2 /* calloutViewForSelectedAnnotation, mapViewProxyAccessibilityElement */;
|
||
}
|
||
return !!self.userLocationAnnotationView + self.accessibilityAnnotationCount + self.visiblePlaceFeatures.count + self.visibleRoadFeatures.count + 2 /* compass, attributionButton */;
|
||
}
|
||
|
||
- (NSInteger)accessibilityAnnotationCount
|
||
{
|
||
std::vector<MLNAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds];
|
||
return visibleAnnotations.size();
|
||
}
|
||
|
||
- (id)accessibilityElementAtIndex:(NSInteger)index
|
||
{
|
||
if (self.calloutViewForSelectedAnnotation)
|
||
{
|
||
if (index == 0)
|
||
{
|
||
return self.calloutViewForSelectedAnnotation;
|
||
}
|
||
if (index == 1)
|
||
{
|
||
self.mapViewProxyAccessibilityElement.accessibilityFrame = self.accessibilityFrame;
|
||
self.mapViewProxyAccessibilityElement.accessibilityPath = self.accessibilityPath;
|
||
return self.mapViewProxyAccessibilityElement;
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
// Compass
|
||
NSInteger compassIndex = 0;
|
||
if (index == compassIndex)
|
||
{
|
||
return self.compassView;
|
||
}
|
||
|
||
// User location annotation
|
||
NSRange userLocationAnnotationRange = NSMakeRange(compassIndex + 1, !!self.userLocationAnnotationView);
|
||
if (NSLocationInRange(index, userLocationAnnotationRange))
|
||
{
|
||
return self.userLocationAnnotationView;
|
||
}
|
||
|
||
CGPoint centerPoint = self.contentCenter;
|
||
if (self.userTrackingMode != MLNUserTrackingModeNone)
|
||
{
|
||
centerPoint = self.userLocationAnnotationViewCenter;
|
||
}
|
||
|
||
// Visible annotations
|
||
std::vector<MLNAnnotationTag> 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 MLNAnnotationTag tagA, const MLNAnnotationTag 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;
|
||
MLNAnnotationTag annotationTag = visibleAnnotations[annotationIndex];
|
||
MLNAssert(annotationTag != MLNAnnotationTagNotFound, @"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 <MLNFeature> _Nonnull featureA, id <MLNFeature> _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 <MLNFeature> 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 <MLNFeature> _Nonnull featureA, id <MLNFeature> _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 <MLNFeature> feature = visibleRoadFeatures[index - visibleRoadFeatureRange.location];
|
||
return [self accessibilityElementForRoadFeature:feature];
|
||
}
|
||
|
||
// Attribution button
|
||
NSInteger attributionButtonIndex = NSMaxRange(visibleRoadFeatureRange);
|
||
if (index == attributionButtonIndex)
|
||
{
|
||
return self.attributionButton;
|
||
}
|
||
|
||
MLNAssert(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:(MLNAnnotationTag)annotationTag
|
||
{
|
||
MLNAssert(_annotationContextsByAnnotationTag.count(annotationTag), @"Missing annotation for tag %llu.", annotationTag);
|
||
MLNAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
|
||
id <MLNAnnotation> annotation = annotationContext.annotation;
|
||
|
||
// Let the annotation view serve as its own accessibility element.
|
||
MLNAnnotationView *annotationView = annotationContext.annotationView;
|
||
if (annotationView && annotationView.superview)
|
||
{
|
||
return annotationView;
|
||
}
|
||
|
||
// Lazily create an accessibility element for the found annotation.
|
||
if ( ! annotationContext.accessibilityElement)
|
||
{
|
||
annotationContext.accessibilityElement = [[MLNAnnotationAccessibilityElement alloc] initWithAccessibilityContainer:self tag:annotationTag];
|
||
}
|
||
|
||
// Update the accessibility element.
|
||
MLNAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
|
||
CGRect annotationFrame = [self frameOfImage:annotationImage.image centeredAtCoordinate:annotation.coordinate];
|
||
CGPoint annotationFrameCenter = CGPointMake(CGRectGetMidX(annotationFrame), CGRectGetMidY(annotationFrame));
|
||
CGRect minimumFrame = CGRectInset({ annotationFrameCenter, CGSizeZero },
|
||
-MLNAnnotationAccessibilityElementMinimumSize.width / 2,
|
||
-MLNAnnotationAccessibilityElementMinimumSize.height / 2);
|
||
annotationFrame = CGRectUnion(annotationFrame, minimumFrame);
|
||
CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self);
|
||
annotationContext.accessibilityElement.accessibilityFrame = screenRect;
|
||
|
||
if ([annotation respondsToSelector:@selector(title)])
|
||
{
|
||
annotationContext.accessibilityElement.accessibilityLabel = annotation.title;
|
||
}
|
||
if ([annotation respondsToSelector:@selector(subtitle)])
|
||
{
|
||
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 <MLNFeature>)feature
|
||
{
|
||
if (!_featureAccessibilityElements)
|
||
{
|
||
_featureAccessibilityElements = [NSMutableSet set];
|
||
}
|
||
|
||
MLNFeatureAccessibilityElement *element = [_featureAccessibilityElements objectsPassingTest:^BOOL(MLNFeatureAccessibilityElement * _Nonnull testElement, BOOL * _Nonnull stop) {
|
||
return testElement.feature.identifier && ![testElement.feature.identifier isEqual:@0] && [testElement.feature.identifier isEqual:feature.identifier];
|
||
}].anyObject;
|
||
if (!element)
|
||
{
|
||
element = [[MLNPlaceFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature];
|
||
}
|
||
CGPoint center = [self convertCoordinate:feature.coordinate toPointToView:self];
|
||
CGRect annotationFrame = CGRectInset({center, CGSizeZero}, -MLNAnnotationAccessibilityElementMinimumSize.width / 2, -MLNAnnotationAccessibilityElementMinimumSize.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 <MLNFeature>)feature
|
||
{
|
||
if (!_featureAccessibilityElements)
|
||
{
|
||
_featureAccessibilityElements = [NSMutableSet set];
|
||
}
|
||
|
||
MLNFeatureAccessibilityElement *element = [_featureAccessibilityElements objectsPassingTest:^BOOL(MLNFeatureAccessibilityElement * _Nonnull testElement, BOOL * _Nonnull stop) {
|
||
return testElement.feature.identifier && ![testElement.feature.identifier isEqual:@0] && [testElement.feature.identifier isEqual:feature.identifier];
|
||
}].anyObject;
|
||
if (!element)
|
||
{
|
||
element = [[MLNRoadFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature];
|
||
}
|
||
|
||
UIBezierPath *path;
|
||
if ([feature isKindOfClass:[MLNPointFeature class]])
|
||
{
|
||
CGPoint center = [self convertCoordinate:feature.coordinate toPointToView:self];
|
||
CGRect annotationFrame = CGRectInset({center, CGSizeZero}, -MLNAnnotationAccessibilityElementMinimumSize.width / 2, -MLNAnnotationAccessibilityElementMinimumSize.width / 2);
|
||
CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self);
|
||
element.accessibilityFrame = screenRect;
|
||
}
|
||
else if ([feature isKindOfClass:[MLNPolylineFeature class]])
|
||
{
|
||
path = [self pathOfPolyline:(MLNPolyline *)feature];
|
||
}
|
||
else if ([feature isKindOfClass:[MLNMultiPolylineFeature class]])
|
||
{
|
||
path = [UIBezierPath bezierPath];
|
||
for (MLNPolyline *polyline in [(MLNMultiPolylineFeature *)feature polylines])
|
||
{
|
||
[path appendPath:[self pathOfPolyline:polyline]];
|
||
}
|
||
}
|
||
|
||
if (path)
|
||
{
|
||
CGPathRef strokedCGPath = CGPathCreateCopyByStrokingPath(path.CGPath, NULL, MLNAnnotationAccessibilityElementMinimumSize.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:(MLNPolyline *)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)
|
||
{
|
||
return [@[self.calloutViewForSelectedAnnotation, self.mapViewProxyAccessibilityElement]
|
||
indexOfObject:element];
|
||
}
|
||
|
||
// Compass
|
||
NSUInteger compassIndex = 0;
|
||
if (element == self.compassView)
|
||
{
|
||
return compassIndex;
|
||
}
|
||
|
||
// User location annotation
|
||
NSRange userLocationAnnotationRange = NSMakeRange(compassIndex + 1, !!self.userLocationAnnotationView);
|
||
if (element == self.userLocationAnnotationView)
|
||
{
|
||
return userLocationAnnotationRange.location;
|
||
}
|
||
|
||
CGPoint centerPoint = self.contentCenter;
|
||
if (self.userTrackingMode != MLNUserTrackingModeNone)
|
||
{
|
||
centerPoint = self.userLocationAnnotationViewCenter;
|
||
}
|
||
|
||
// Visible annotations
|
||
std::vector<MLNAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds];
|
||
NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size());
|
||
MLNAnnotationTag tag = MLNAnnotationTagNotFound;
|
||
if ([element isKindOfClass:[MLNAnnotationView class]])
|
||
{
|
||
id <MLNAnnotation> annotation = [(MLNAnnotationView *)element annotation];
|
||
tag = [self annotationTagForAnnotation:annotation];
|
||
}
|
||
else if ([element isKindOfClass:[MLNAnnotationAccessibilityElement class]])
|
||
{
|
||
tag = [(MLNAnnotationAccessibilityElement *)element tag];
|
||
}
|
||
|
||
if (tag != MLNAnnotationTagNotFound)
|
||
{
|
||
std::sort(visibleAnnotations.begin(), visibleAnnotations.end());
|
||
std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MLNAnnotationTag tagA, const MLNAnnotationTag 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);
|
||
}
|
||
|
||
// Visible place features
|
||
NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures;
|
||
NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count);
|
||
if ([element isKindOfClass:[MLNPlaceFeatureAccessibilityElement class]])
|
||
{
|
||
visiblePlaceFeatures = [visiblePlaceFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MLNFeature> _Nonnull featureA, id <MLNFeature> _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 <MLNFeature> feature = [(MLNPlaceFeatureAccessibilityElement *)element feature];
|
||
NSUInteger featureIndex = [visiblePlaceFeatures indexOfObject:feature];
|
||
if (featureIndex == NSNotFound)
|
||
{
|
||
featureIndex = [visiblePlaceFeatures indexOfObjectPassingTest:^BOOL (id <MLNFeature> _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;
|
||
}
|
||
|
||
// Visible road features
|
||
NSArray *visibleRoadFeatures = self.visibleRoadFeatures;
|
||
NSRange visibleRoadFeatureRange = NSMakeRange(NSMaxRange(visiblePlaceFeatureRange), visibleRoadFeatures.count);
|
||
if ([element isKindOfClass:[MLNRoadFeatureAccessibilityElement class]])
|
||
{
|
||
visibleRoadFeatures = [visibleRoadFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MLNFeature> _Nonnull featureA, id <MLNFeature> _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 <MLNFeature> feature = [(MLNRoadFeatureAccessibilityElement *)element feature];
|
||
NSUInteger featureIndex = [visibleRoadFeatures indexOfObject:feature];
|
||
if (featureIndex == NSNotFound)
|
||
{
|
||
featureIndex = [visibleRoadFeatures indexOfObjectPassingTest:^BOOL (id <MLNFeature> _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 attributionButtonIndex;
|
||
}
|
||
|
||
return NSNotFound;
|
||
}
|
||
|
||
- (MLNMapViewProxyAccessibilityElement *)mapViewProxyAccessibilityElement
|
||
{
|
||
if ( ! _mapViewProxyAccessibilityElement)
|
||
{
|
||
_mapViewProxyAccessibilityElement = [[MLNMapViewProxyAccessibilityElement alloc] initWithAccessibilityContainer:self];
|
||
}
|
||
return _mapViewProxyAccessibilityElement;
|
||
}
|
||
|
||
- (void)accessibilityIncrement
|
||
{
|
||
// Swipe up to zoom out.
|
||
[self accessibilityScaleBy:0.5];
|
||
}
|
||
|
||
- (void)accessibilityDecrement
|
||
{
|
||
// Swipe down to zoom in.
|
||
[self accessibilityScaleBy:2];
|
||
}
|
||
|
||
- (void)accessibilityScaleBy:(double)scaleFactor
|
||
{
|
||
CGPoint centerPoint = self.contentCenter;
|
||
if (self.userTrackingMode != MLNUserTrackingModeNone)
|
||
{
|
||
centerPoint = self.userLocationAnnotationViewCenter;
|
||
}
|
||
double newZoom = round(self.zoomLevel) + log2(scaleFactor);
|
||
self.mbglMap.jumpTo(mbgl::CameraOptions()
|
||
.withZoom(newZoom)
|
||
.withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }));
|
||
[self unrotateIfNeededForGesture];
|
||
|
||
_accessibilityValueAnnouncementIsPending = YES;
|
||
}
|
||
|
||
// MARK: - Geography -
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingCenterCoordinate
|
||
{
|
||
return [NSSet setWithObjects:@"latitude", @"longitude", @"camera", nil];
|
||
}
|
||
|
||
- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated
|
||
{
|
||
MLNLogDebug(@"Setting centerCoordinate: %@ animated: %@", MLNStringFromCLLocationCoordinate2D(coordinate), MLNStringFromBOOL(animated));
|
||
[self setCenterCoordinate:coordinate zoomLevel:self.zoomLevel animated:animated];
|
||
}
|
||
|
||
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
|
||
{
|
||
MLNLogDebug(@"Setting centerCoordinate: %@", MLNStringFromCLLocationCoordinate2D(centerCoordinate));
|
||
[self setCenterCoordinate:centerCoordinate animated:NO];
|
||
}
|
||
|
||
- (CLLocationCoordinate2D)centerCoordinate
|
||
{
|
||
return MLNLocationCoordinate2DFromLatLng(*self.mbglMap.getCameraOptions().center);
|
||
}
|
||
|
||
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel animated:(BOOL)animated
|
||
{
|
||
MLNLogDebug(@"Setting centerCoordinate: %@ zoomLevel: %f animated: %@",
|
||
MLNStringFromCLLocationCoordinate2D(centerCoordinate),
|
||
zoomLevel,
|
||
MLNStringFromBOOL(animated));
|
||
[self setCenterCoordinate:centerCoordinate zoomLevel:zoomLevel direction:self.direction animated:animated];
|
||
}
|
||
|
||
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel direction:(CLLocationDirection)direction animated:(BOOL)animated
|
||
{
|
||
MLNLogDebug(@"Setting centerCoordinate: %@ zoomLevel: %f direction: %f animated: %@",
|
||
MLNStringFromCLLocationCoordinate2D(centerCoordinate),
|
||
zoomLevel,
|
||
direction,
|
||
MLNStringFromBOOL(animated));
|
||
[self setCenterCoordinate:centerCoordinate zoomLevel:zoomLevel direction:direction animated:animated completionHandler:nil];
|
||
}
|
||
|
||
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel direction:(CLLocationDirection)direction animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
MLNLogDebug(@"Setting centerCoordinate: %@ zoomLevel: %f direction: %f animated: %@ completionHandler: %@",
|
||
MLNStringFromCLLocationCoordinate2D(centerCoordinate),
|
||
zoomLevel,
|
||
direction,
|
||
MLNStringFromBOOL(animated),
|
||
completion);
|
||
self.userTrackingMode = MLNUserTrackingModeNone;
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonProgrammatic;
|
||
|
||
[self _setCenterCoordinate:centerCoordinate edgePadding:self.contentInset zoomLevel:zoomLevel direction:direction duration:animated ? MLNAnimationDuration : 0 animationTimingFunction:nil completionHandler:completion];
|
||
}
|
||
|
||
- (void)_setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate edgePadding:(UIEdgeInsets)insets zoomLevel:(double)zoomLevel direction:(CLLocationDirection)direction duration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
if (!_mbglMap)
|
||
{
|
||
if (completion)
|
||
{
|
||
completion();
|
||
}
|
||
return;
|
||
}
|
||
|
||
mbgl::CameraOptions cameraOptions;
|
||
cameraOptions.center = MLNLatLngFromLocationCoordinate2D(centerCoordinate);
|
||
cameraOptions.padding = MLNEdgeInsetsFromNSEdgeInsets(insets);
|
||
cameraOptions.zoom = zoomLevel;
|
||
if (direction >= 0)
|
||
{
|
||
cameraOptions.bearing = direction;
|
||
}
|
||
|
||
mbgl::AnimationOptions animationOptions;
|
||
if (duration)
|
||
{
|
||
animationOptions.duration.emplace(MLNDurationFromTimeInterval(duration));
|
||
animationOptions.easing.emplace(MLNUnitBezierForMediaTimingFunction(function));
|
||
}
|
||
|
||
dispatch_block_t pendingCompletion;
|
||
|
||
if (completion)
|
||
{
|
||
__weak __typeof__(self) weakSelf = self;
|
||
|
||
pendingCompletion = ^{
|
||
if (![weakSelf scheduleTransitionCompletion:completion])
|
||
{
|
||
completion();
|
||
}
|
||
};
|
||
|
||
animationOptions.transitionFinishFn = [pendingCompletion]() {
|
||
// Must run asynchronously after the transition is completely over.
|
||
// Otherwise, a call to -setCenterCoordinate: within the completion
|
||
// handler would reenter the completion handler’s caller.
|
||
|
||
dispatch_async(dispatch_get_main_queue(), pendingCompletion);
|
||
};
|
||
}
|
||
|
||
MLNMapCamera *camera = [self cameraForCameraOptions:cameraOptions];
|
||
if ([self.camera isEqualToMapCamera:camera] && UIEdgeInsetsEqualToEdgeInsets(_contentInset, insets))
|
||
{
|
||
if (pendingCompletion)
|
||
{
|
||
[self animateWithDelay:duration animations:pendingCompletion];
|
||
}
|
||
return;
|
||
}
|
||
|
||
[self cancelTransitions];
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonProgrammatic;
|
||
|
||
self.mbglMap.easeTo(cameraOptions, animationOptions);
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingZoomLevel
|
||
{
|
||
return [NSSet setWithObject:@"camera"];
|
||
}
|
||
|
||
- (double)zoomLevel
|
||
{
|
||
return *self.mbglMap.getCameraOptions().zoom;
|
||
}
|
||
|
||
- (void)setZoomLevel:(double)zoomLevel
|
||
{
|
||
MLNLogDebug(@"Setting zoomLevel: %f", zoomLevel);
|
||
[self setZoomLevel:zoomLevel animated:NO];
|
||
}
|
||
|
||
- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated
|
||
{
|
||
MLNLogDebug(@"Setting zoomLevel: %f animated: %@", zoomLevel, MLNStringFromBOOL(animated));
|
||
if (zoomLevel == self.zoomLevel) return;
|
||
[self cancelTransitions];
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonProgrammatic;
|
||
|
||
CGFloat duration = animated ? MLNAnimationDuration : 0;
|
||
|
||
self.mbglMap.easeTo(mbgl::CameraOptions()
|
||
.withZoom(zoomLevel), MLNDurationFromTimeInterval(duration));
|
||
}
|
||
|
||
- (void)setMinimumZoomLevel:(double)minimumZoomLevel
|
||
{
|
||
MLNLogDebug(@"Setting minimumZoomLevel: %f", minimumZoomLevel);
|
||
self.mbglMap.setBounds(mbgl::BoundOptions().withMinZoom(minimumZoomLevel));
|
||
}
|
||
|
||
- (double)minimumZoomLevel
|
||
{
|
||
return *self.mbglMap.getBounds().minZoom;
|
||
}
|
||
|
||
- (void)setMaximumZoomLevel:(double)maximumZoomLevel
|
||
{
|
||
MLNLogDebug(@"Setting maximumZoomLevel: %f", maximumZoomLevel);
|
||
self.mbglMap.setBounds(mbgl::BoundOptions().withMaxZoom(maximumZoomLevel));
|
||
}
|
||
|
||
- (double)maximumZoomLevel
|
||
{
|
||
return *self.mbglMap.getBounds().maxZoom;
|
||
}
|
||
|
||
- (void)setMaximumScreenBounds:(MLNCoordinateBounds)maximumScreenBounds
|
||
{
|
||
mbgl::LatLng sw = {maximumScreenBounds.sw.latitude, maximumScreenBounds.sw.longitude};
|
||
mbgl::LatLng ne = {maximumScreenBounds.ne.latitude, maximumScreenBounds.ne.longitude};
|
||
mbgl::BoundOptions newBounds = mbgl::BoundOptions().withLatLngBounds(mbgl::LatLngBounds::hull(sw, ne));
|
||
|
||
self.mbglMap.setBounds(newBounds);
|
||
self.mbglMap.setConstrainMode(mbgl::ConstrainMode::Screen);
|
||
}
|
||
|
||
- (MLNCoordinateBounds)maximumScreenBounds
|
||
{
|
||
return MLNCoordinateBoundsFromLatLngBounds(*self.mbglMap.getBounds().bounds);;
|
||
}
|
||
|
||
- (CGFloat)minimumPitch
|
||
{
|
||
return *self.mbglMap.getBounds().minPitch;
|
||
}
|
||
|
||
- (void)setMinimumPitch:(CGFloat)minimumPitch
|
||
{
|
||
MLNLogDebug(@"Setting minimumPitch: %f", minimumPitch);
|
||
self.mbglMap.setBounds(mbgl::BoundOptions().withMinPitch(minimumPitch));
|
||
}
|
||
|
||
- (CGFloat)maximumPitch
|
||
{
|
||
return *self.mbglMap.getBounds().maxPitch;
|
||
}
|
||
|
||
- (void)setMaximumPitch:(CGFloat)maximumPitch
|
||
{
|
||
MLNLogDebug(@"Setting maximumPitch: %f", maximumPitch);
|
||
self.mbglMap.setBounds(mbgl::BoundOptions().withMaxPitch(maximumPitch));
|
||
}
|
||
|
||
- (MLNCoordinateBounds)visibleCoordinateBounds
|
||
{
|
||
return [self convertRect:self.bounds toCoordinateBoundsFromView:self];
|
||
}
|
||
|
||
- (void)setVisibleCoordinateBounds:(MLNCoordinateBounds)bounds
|
||
{
|
||
[self setVisibleCoordinateBounds:bounds animated:NO];
|
||
}
|
||
|
||
- (void)setVisibleCoordinateBounds:(MLNCoordinateBounds)bounds animated:(BOOL)animated
|
||
{
|
||
[self setVisibleCoordinateBounds:bounds edgePadding:UIEdgeInsetsZero animated:animated completionHandler:nil];
|
||
}
|
||
|
||
- (void)setVisibleCoordinateBounds:(MLNCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated
|
||
{
|
||
[self setVisibleCoordinateBounds:bounds edgePadding:insets animated:animated completionHandler:nil];
|
||
}
|
||
|
||
- (void)setVisibleCoordinateBounds:(MLNCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
MLNLogDebug(@"Setting visibleCoordinateBounds: %@ edgePadding: %@ animated: %@",
|
||
MLNStringFromCoordinateBounds(bounds),
|
||
NSStringFromUIEdgeInsets(insets),
|
||
MLNStringFromBOOL(animated));
|
||
CLLocationCoordinate2D coordinates[] = {
|
||
{bounds.ne.latitude, bounds.sw.longitude},
|
||
bounds.sw,
|
||
{bounds.sw.latitude, bounds.ne.longitude},
|
||
bounds.ne,
|
||
};
|
||
[self setVisibleCoordinates:coordinates
|
||
count:sizeof(coordinates) / sizeof(coordinates[0])
|
||
edgePadding:insets
|
||
direction:self.direction
|
||
duration:animated ? MLNAnimationDuration : 0
|
||
animationTimingFunction:nil
|
||
completionHandler:completion];
|
||
}
|
||
|
||
- (void)setVisibleCoordinates:(const CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated
|
||
{
|
||
MLNLogDebug(@"Setting: %lu coordinates edgePadding: %@ animated: %@",
|
||
count,
|
||
NSStringFromUIEdgeInsets(insets),
|
||
MLNStringFromBOOL(animated));
|
||
[self setVisibleCoordinates:coordinates count:count edgePadding:insets direction:self.direction duration:animated ? MLNAnimationDuration : 0 animationTimingFunction:nil];
|
||
}
|
||
|
||
- (void)setVisibleCoordinates:(const CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction duration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function {
|
||
MLNLogDebug(@"Setting: %lu coordinates edgePadding: %@ direction: %f duration: %f animationTimingFunction: %@",
|
||
count,
|
||
NSStringFromUIEdgeInsets(insets),
|
||
direction,
|
||
duration,
|
||
function);
|
||
[self setVisibleCoordinates:coordinates count:count edgePadding:insets direction:direction duration:duration animationTimingFunction:function completionHandler:NULL];
|
||
}
|
||
|
||
- (void)setVisibleCoordinates:(const CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction duration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
MLNLogDebug(@"Setting: %lu coordinates edgePadding: %@ direction: %f duration: %f animationTimingFunction: %@ completionHandler: %@", count, NSStringFromUIEdgeInsets(insets), direction, duration, function, completion);
|
||
self.userTrackingMode = MLNUserTrackingModeNone;
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonProgrammatic;
|
||
|
||
[self _setVisibleCoordinates:coordinates count:count edgePadding:insets direction:direction duration:duration animationTimingFunction:function completionHandler:completion];
|
||
}
|
||
|
||
- (void)_setVisibleCoordinates:(const CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction duration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
if (!_mbglMap)
|
||
{
|
||
if (completion)
|
||
{
|
||
completion();
|
||
}
|
||
return;
|
||
}
|
||
|
||
mbgl::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(insets);
|
||
padding += MLNEdgeInsetsFromNSEdgeInsets(self.contentInset);
|
||
std::vector<mbgl::LatLng> latLngs;
|
||
latLngs.reserve(count);
|
||
for (NSUInteger i = 0; i < count; i++)
|
||
{
|
||
latLngs.push_back({coordinates[i].latitude, coordinates[i].longitude});
|
||
}
|
||
|
||
CLLocationDirection cameraDirection = direction >= 0 ? direction : self.direction;
|
||
|
||
mbgl::CameraOptions cameraOptions = self.mbglMap.cameraForLatLngs(latLngs, padding, cameraDirection);
|
||
|
||
mbgl::AnimationOptions animationOptions;
|
||
if (duration > 0)
|
||
{
|
||
animationOptions.duration.emplace(MLNDurationFromTimeInterval(duration));
|
||
animationOptions.easing.emplace(MLNUnitBezierForMediaTimingFunction(function));
|
||
}
|
||
|
||
dispatch_block_t pendingCompletion;
|
||
|
||
if (completion)
|
||
{
|
||
__weak __typeof__(self) weakSelf = self;
|
||
|
||
pendingCompletion = ^{
|
||
if (![weakSelf scheduleTransitionCompletion:completion])
|
||
{
|
||
completion();
|
||
}
|
||
};
|
||
|
||
animationOptions.transitionFinishFn = [pendingCompletion]() {
|
||
dispatch_async(dispatch_get_main_queue(), pendingCompletion);
|
||
};
|
||
}
|
||
|
||
// Padding is baked in adjusted camera center (in cameraForLatLngs) and
|
||
// cameraOptions.padding at this point is (0, 0, 0, 0) and we don't need to
|
||
// check if cameraOptions.contentInsets are equal to contentInsets.
|
||
MLNMapCamera *camera = [self cameraForCameraOptions:cameraOptions];
|
||
if ([self.camera isEqualToMapCamera:camera])
|
||
{
|
||
if (pendingCompletion)
|
||
{
|
||
[self animateWithDelay:duration animations:pendingCompletion];
|
||
}
|
||
return;
|
||
}
|
||
|
||
[self willChangeValueForKey:@"visibleCoordinateBounds"];
|
||
[self cancelTransitions];
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonProgrammatic;
|
||
|
||
self.mbglMap.easeTo(cameraOptions, animationOptions);
|
||
[self didChangeValueForKey:@"visibleCoordinateBounds"];
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDirection
|
||
{
|
||
return [NSSet setWithObject:@"camera"];
|
||
}
|
||
|
||
- (CLLocationDirection)direction
|
||
{
|
||
return mbgl::util::wrap(*self.mbglMap.getCameraOptions().bearing, 0., 360.);
|
||
}
|
||
|
||
- (void)setDirection:(CLLocationDirection)direction animated:(BOOL)animated
|
||
{
|
||
MLNLogDebug(@"Setting direction: %f animated: %@", direction, MLNStringFromBOOL(animated));
|
||
if ( ! animated && ! self.rotationAllowed) return;
|
||
|
||
if (self.userTrackingMode == MLNUserTrackingModeFollowWithHeading ||
|
||
self.userTrackingMode == MLNUserTrackingModeFollowWithCourse)
|
||
{
|
||
self.userTrackingMode = MLNUserTrackingModeFollow;
|
||
}
|
||
|
||
[self _setDirection:direction animated:animated];
|
||
}
|
||
|
||
- (void)_setDirection:(CLLocationDirection)direction animated:(BOOL)animated
|
||
{
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (direction == self.direction) return;
|
||
[self cancelTransitions];
|
||
|
||
CGFloat duration = animated ? MLNAnimationDuration : 0;
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonProgrammatic;
|
||
|
||
if (self.userTrackingMode == MLNUserTrackingModeNone)
|
||
{
|
||
self.mbglMap.easeTo(mbgl::CameraOptions().withBearing(direction),
|
||
MLNDurationFromTimeInterval(duration));
|
||
}
|
||
else
|
||
{
|
||
CGPoint anchor = self.userLocationAnnotationViewCenter;
|
||
self.mbglMap.easeTo(mbgl::CameraOptions()
|
||
.withBearing(direction)
|
||
.withAnchor(mbgl::ScreenCoordinate { anchor.x, anchor.y }),
|
||
MLNDurationFromTimeInterval(duration));
|
||
}
|
||
}
|
||
|
||
- (void)setDirection:(CLLocationDirection)direction
|
||
{
|
||
MLNLogDebug(@"Setting direction: %f", direction);
|
||
[self setDirection:direction animated:NO];
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingPitch
|
||
{
|
||
return [NSSet setWithObject:@"camera"];
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingCamera
|
||
{
|
||
return [NSSet setWithObjects:@"longitude", @"latitude", @"centerCoordinate", @"zoomLevel", @"direction", nil];
|
||
}
|
||
|
||
- (UIEdgeInsets)cameraEdgeInsets {
|
||
mbgl::CameraOptions cameraOptions = self.mbglMap.getCameraOptions();
|
||
return NSEdgeInsetsFromMLNEdgeInsets(cameraOptions.padding.value_or(mbgl::EdgeInsets()));
|
||
}
|
||
|
||
- (MLNMapCamera *)camera
|
||
{
|
||
if (!_mbglMap)
|
||
{
|
||
NSAssert(self.terminated, @"_mbglMap should only be unavailable during app termination");
|
||
return self.residualCamera;
|
||
}
|
||
|
||
return [self cameraForCameraOptions:self.mbglMap.getCameraOptions()];
|
||
}
|
||
|
||
- (void)setCamera:(MLNMapCamera *)camera
|
||
{
|
||
MLNLogDebug(@"Setting camera: %@", camera);
|
||
[self setCamera:camera animated:NO];
|
||
}
|
||
|
||
- (void)setCamera:(MLNMapCamera *)camera animated:(BOOL)animated
|
||
{
|
||
MLNLogDebug(@"Setting camera: %@ animated: %@", camera, MLNStringFromBOOL(animated));
|
||
[self setCamera:camera withDuration:animated ? MLNAnimationDuration : 0 animationTimingFunction:nil];
|
||
}
|
||
|
||
- (void)setCamera:(MLNMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function
|
||
{
|
||
MLNLogDebug(@"Setting camera: %@ duration: %f animationTimingFunction: %@", camera, duration, function);
|
||
[self setCamera:camera withDuration:duration animationTimingFunction:function completionHandler:nil];
|
||
}
|
||
|
||
- (void)setCamera:(MLNMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
MLNLogDebug(@"Setting camera: %@ duration: %f animationTimingFunction: %@ completionHandler: %@", camera, duration, function, completion);
|
||
[self setCamera:camera withDuration:duration animationTimingFunction:function edgePadding:UIEdgeInsetsZero completionHandler:completion];
|
||
}
|
||
|
||
- (void)setCamera:(MLNMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function edgePadding:(UIEdgeInsets)edgePadding completionHandler:(nullable void (^)(void))completion {
|
||
if (!_mbglMap)
|
||
{
|
||
if (completion)
|
||
{
|
||
completion();
|
||
}
|
||
return;
|
||
}
|
||
|
||
MLNLogDebug(@"Setting camera: %@ duration: %f animationTimingFunction: %@ edgePadding: %@ completionHandler: %@", camera, duration, function, NSStringFromUIEdgeInsets(edgePadding), completion);
|
||
|
||
edgePadding = MLNEdgeInsetsInsetEdgeInset(edgePadding, self.contentInset);
|
||
|
||
mbgl::AnimationOptions animationOptions;
|
||
if (duration > 0)
|
||
{
|
||
animationOptions.duration.emplace(MLNDurationFromTimeInterval(duration));
|
||
animationOptions.easing.emplace(MLNUnitBezierForMediaTimingFunction(function));
|
||
}
|
||
|
||
dispatch_block_t pendingCompletion;
|
||
|
||
if (completion)
|
||
{
|
||
__weak __typeof__(self) weakSelf = self;
|
||
|
||
pendingCompletion = ^{
|
||
if (![weakSelf scheduleTransitionCompletion:completion])
|
||
{
|
||
completion();
|
||
}
|
||
};
|
||
|
||
animationOptions.transitionFinishFn = [pendingCompletion]() {
|
||
dispatch_async(dispatch_get_main_queue(), pendingCompletion);
|
||
};
|
||
}
|
||
|
||
if ([self.camera isEqualToMapCamera:camera] && UIEdgeInsetsEqualToEdgeInsets(_contentInset, edgePadding))
|
||
{
|
||
if (pendingCompletion)
|
||
{
|
||
[self animateWithDelay:duration animations:pendingCompletion];
|
||
}
|
||
return;
|
||
}
|
||
|
||
[self willChangeValueForKey:@"camera"];
|
||
[self cancelTransitions];
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonProgrammatic;
|
||
|
||
mbgl::CameraOptions cameraOptions = [self cameraOptionsObjectForAnimatingToCamera:camera edgePadding:edgePadding];
|
||
self.mbglMap.easeTo(cameraOptions, animationOptions);
|
||
[self didChangeValueForKey:@"camera"];
|
||
}
|
||
|
||
- (void)flyToCamera:(MLNMapCamera *)camera completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
MLNLogDebug(@"Setting flyToCamera: %@ completionHandler: %@", camera, completion);
|
||
[self flyToCamera:camera withDuration:-1 completionHandler:completion];
|
||
}
|
||
|
||
- (void)flyToCamera:(MLNMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
MLNLogDebug(@"Setting flyToCamera: %@ withDuration: %f completionHandler: %@", camera, duration, completion);
|
||
[self flyToCamera:camera withDuration:duration peakAltitude:-1 completionHandler:completion];
|
||
}
|
||
|
||
- (void)flyToCamera:(MLNMapCamera *)camera withDuration:(NSTimeInterval)duration peakAltitude:(CLLocationDistance)peakAltitude completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
MLNLogDebug(@"Setting flyToCamera: %@ withDuration: %f peakAltitude: %f completionHandler: %@", camera, duration, peakAltitude, completion);
|
||
[self _flyToCamera:camera edgePadding:self.contentInset withDuration:duration peakAltitude:peakAltitude completionHandler:completion];
|
||
}
|
||
|
||
- (void)flyToCamera:(MLNMapCamera *)camera edgePadding:(UIEdgeInsets)insets withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion {
|
||
UIEdgeInsets finalEdgeInsets = UIEdgeInsetsMake(self.contentInset.top + insets.top,
|
||
self.contentInset.left + insets.left,
|
||
self.contentInset.bottom + insets.bottom,
|
||
self.contentInset.right + insets.right);
|
||
[self _flyToCamera:camera edgePadding:finalEdgeInsets withDuration:duration peakAltitude:-1 completionHandler:completion];
|
||
}
|
||
|
||
- (void)_flyToCamera:(MLNMapCamera *)camera edgePadding:(UIEdgeInsets)insets withDuration:(NSTimeInterval)duration peakAltitude:(CLLocationDistance)peakAltitude completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
if (!_mbglMap)
|
||
{
|
||
if (completion)
|
||
{
|
||
completion();
|
||
}
|
||
return;
|
||
}
|
||
|
||
mbgl::AnimationOptions animationOptions;
|
||
if (duration >= 0)
|
||
{
|
||
animationOptions.duration = MLNDurationFromTimeInterval(duration);
|
||
}
|
||
if (peakAltitude >= 0)
|
||
{
|
||
CLLocationDegrees peakLatitude = (self.centerCoordinate.latitude + camera.centerCoordinate.latitude) / 2;
|
||
CLLocationDegrees peakPitch = (self.camera.pitch + camera.pitch) / 2;
|
||
animationOptions.minZoom = MLNZoomLevelForAltitude(peakAltitude, peakPitch,
|
||
peakLatitude, self.frame.size);
|
||
}
|
||
|
||
dispatch_block_t pendingCompletion;
|
||
|
||
if (completion)
|
||
{
|
||
__weak __typeof__(self) weakSelf = self;
|
||
|
||
pendingCompletion = ^{
|
||
if (![weakSelf scheduleTransitionCompletion:completion])
|
||
{
|
||
completion();
|
||
}
|
||
};
|
||
|
||
animationOptions.transitionFinishFn = [pendingCompletion]() {
|
||
dispatch_async(dispatch_get_main_queue(), pendingCompletion);
|
||
};
|
||
}
|
||
if ([self.camera isEqualToMapCamera:camera] && UIEdgeInsetsEqualToEdgeInsets(_contentInset, insets))
|
||
{
|
||
if (pendingCompletion)
|
||
{
|
||
[self animateWithDelay:duration animations:pendingCompletion];
|
||
}
|
||
return;
|
||
}
|
||
|
||
[self willChangeValueForKey:@"camera"];
|
||
[self cancelTransitions];
|
||
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonProgrammatic;
|
||
|
||
mbgl::CameraOptions cameraOptions = [self cameraOptionsObjectForAnimatingToCamera:camera edgePadding:insets];
|
||
self.mbglMap.flyTo(cameraOptions, animationOptions);
|
||
[self didChangeValueForKey:@"camera"];
|
||
}
|
||
|
||
- (void)cancelTransitions {
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
self.cameraChangeReasonBitmask |= MLNCameraChangeReasonTransitionCancelled;
|
||
self.mbglMap.cancelTransitions();
|
||
self.cameraChangeReasonBitmask &= ~MLNCameraChangeReasonTransitionCancelled;
|
||
}
|
||
|
||
- (MLNMapCamera *)cameraThatFitsCoordinateBounds:(MLNCoordinateBounds)bounds
|
||
{
|
||
return [self cameraThatFitsCoordinateBounds:bounds edgePadding:UIEdgeInsetsZero];
|
||
}
|
||
|
||
- (MLNMapCamera *)cameraThatFitsCoordinateBounds:(MLNCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets
|
||
{
|
||
if (!_mbglMap)
|
||
{
|
||
return self.residualCamera;
|
||
}
|
||
|
||
mbgl::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(insets);
|
||
padding += MLNEdgeInsetsFromNSEdgeInsets(self.contentInset);
|
||
mbgl::CameraOptions cameraOptions = self.mbglMap.cameraForLatLngBounds(MLNLatLngBoundsFromCoordinateBounds(bounds), padding);
|
||
return [self cameraForCameraOptions:cameraOptions];
|
||
}
|
||
|
||
- (MLNMapCamera *)camera:(MLNMapCamera *)camera fittingCoordinateBounds:(MLNCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets
|
||
{
|
||
if (!_mbglMap)
|
||
{
|
||
return self.residualCamera;
|
||
}
|
||
|
||
mbgl::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(insets);
|
||
padding += MLNEdgeInsetsFromNSEdgeInsets(self.contentInset);
|
||
|
||
MLNMapCamera *currentCamera = self.camera;
|
||
CGFloat pitch = camera.pitch < 0 ? currentCamera.pitch : camera.pitch;
|
||
CLLocationDirection direction = camera.heading < 0 ? currentCamera.heading : camera.heading;
|
||
|
||
mbgl::CameraOptions cameraOptions = self.mbglMap.cameraForLatLngBounds(MLNLatLngBoundsFromCoordinateBounds(bounds), padding, direction, pitch);
|
||
return [self cameraForCameraOptions:cameraOptions];
|
||
}
|
||
|
||
- (MLNMapCamera *)camera:(MLNMapCamera *)camera fittingShape:(MLNShape *)shape edgePadding:(UIEdgeInsets)insets {
|
||
if (!_mbglMap)
|
||
{
|
||
return self.residualCamera;
|
||
}
|
||
|
||
mbgl::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(insets);
|
||
padding += MLNEdgeInsetsFromNSEdgeInsets(self.contentInset);
|
||
|
||
MLNMapCamera *currentCamera = self.camera;
|
||
CGFloat pitch = camera.pitch < 0 ? currentCamera.pitch : camera.pitch;
|
||
CLLocationDirection direction = camera.heading < 0 ? currentCamera.heading : camera.heading;
|
||
|
||
mbgl::CameraOptions cameraOptions = self.mbglMap.cameraForGeometry([shape geometryObject], padding, direction, pitch);
|
||
|
||
return [self cameraForCameraOptions: cameraOptions];
|
||
}
|
||
|
||
- (MLNMapCamera *)cameraThatFitsShape:(MLNShape *)shape direction:(CLLocationDirection)direction edgePadding:(UIEdgeInsets)insets {
|
||
if (!_mbglMap)
|
||
{
|
||
return self.residualCamera;
|
||
}
|
||
|
||
mbgl::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(insets);
|
||
padding += MLNEdgeInsetsFromNSEdgeInsets(self.contentInset);
|
||
|
||
mbgl::CameraOptions cameraOptions = self.mbglMap.cameraForGeometry([shape geometryObject], padding, direction);
|
||
|
||
return [self cameraForCameraOptions:cameraOptions];
|
||
}
|
||
|
||
- (MLNMapCamera *)cameraForCameraOptions:(const mbgl::CameraOptions &)cameraOptions
|
||
{
|
||
if (!_mbglMap)
|
||
{
|
||
return self.residualCamera;
|
||
}
|
||
|
||
mbgl::CameraOptions mapCamera = self.mbglMap.getCameraOptions();
|
||
CLLocationCoordinate2D centerCoordinate = MLNLocationCoordinate2DFromLatLng(cameraOptions.center ? *cameraOptions.center : *mapCamera.center);
|
||
double zoomLevel = cameraOptions.zoom ? *cameraOptions.zoom : self.zoomLevel;
|
||
CLLocationDirection direction = cameraOptions.bearing ? mbgl::util::wrap(*cameraOptions.bearing, 0., 360.) : self.direction;
|
||
CGFloat pitch = cameraOptions.pitch ? *cameraOptions.pitch : *mapCamera.pitch;
|
||
CLLocationDistance altitude = MLNAltitudeForZoomLevel(zoomLevel, pitch, centerCoordinate.latitude, self.frame.size);
|
||
return [MLNMapCamera cameraLookingAtCenterCoordinate:centerCoordinate altitude:altitude pitch:pitch heading:direction];
|
||
}
|
||
|
||
/// Returns a CameraOptions object that specifies parameters for animating to
|
||
/// the given camera.
|
||
- (mbgl::CameraOptions)cameraOptionsObjectForAnimatingToCamera:(MLNMapCamera *)camera edgePadding:(UIEdgeInsets)insets
|
||
{
|
||
mbgl::CameraOptions options;
|
||
if (CLLocationCoordinate2DIsValid(camera.centerCoordinate))
|
||
{
|
||
options.center = MLNLatLngFromLocationCoordinate2D(camera.centerCoordinate);
|
||
}
|
||
options.padding = MLNEdgeInsetsFromNSEdgeInsets(insets);
|
||
options.zoom = MLNZoomLevelForAltitude(camera.altitude, camera.pitch,
|
||
camera.centerCoordinate.latitude,
|
||
self.frame.size);
|
||
if (camera.heading >= 0)
|
||
{
|
||
options.bearing = camera.heading;
|
||
}
|
||
if (camera.pitch >= 0)
|
||
{
|
||
options.pitch = camera.pitch;
|
||
}
|
||
return options;
|
||
}
|
||
|
||
- (CLLocationCoordinate2D)convertPoint:(CGPoint)point toCoordinateFromView:(nullable UIView *)view
|
||
{
|
||
return MLNLocationCoordinate2DFromLatLng([self convertPoint:point toLatLngFromView:view]);
|
||
}
|
||
|
||
/// Converts a point in the view’s coordinate system to a geographic coordinate.
|
||
- (mbgl::LatLng)convertPoint:(CGPoint)point toLatLngFromView:(nullable UIView *)view
|
||
{
|
||
CGPoint convertedPoint = [self convertPoint:point fromView:view];
|
||
return self.mbglMap.latLngForPixel(mbgl::ScreenCoordinate(convertedPoint.x, convertedPoint.y)).wrapped();
|
||
}
|
||
|
||
- (CGPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate toPointToView:(nullable UIView *)view
|
||
{
|
||
if ( ! CLLocationCoordinate2DIsValid(coordinate))
|
||
{
|
||
return CGPointMake(NAN, NAN);
|
||
}
|
||
return [self convertLatLng:MLNLatLngFromLocationCoordinate2D(coordinate) toPointToView:view];
|
||
}
|
||
|
||
/// Converts a geographic coordinate to a point in the view’s coordinate system.
|
||
- (CGPoint)convertLatLng:(mbgl::LatLng)latLng toPointToView:(nullable UIView *)view
|
||
{
|
||
mbgl::ScreenCoordinate pixel = self.mbglMap.pixelForLatLng(latLng);
|
||
return [self convertPoint:CGPointMake(pixel.x, pixel.y) toView:view];
|
||
}
|
||
|
||
- (MLNCoordinateBounds)convertRect:(CGRect)rect toCoordinateBoundsFromView:(nullable UIView *)view
|
||
{
|
||
return MLNCoordinateBoundsFromLatLngBounds([self convertRect:rect toLatLngBoundsFromView:view]);
|
||
}
|
||
|
||
- (CGRect)convertCoordinateBounds:(MLNCoordinateBounds)bounds toRectToView:(nullable UIView *)view
|
||
{
|
||
return [self convertLatLngBounds:MLNLatLngBoundsFromCoordinateBounds(bounds) toRectToView:view];
|
||
}
|
||
|
||
/// Converts a geographic bounding box to a rectangle in the view’s coordinate
|
||
/// system.
|
||
- (CGRect)convertLatLngBounds:(mbgl::LatLngBounds)bounds toRectToView:(nullable UIView *)view {
|
||
auto northwest = bounds.northwest();
|
||
auto northeast = bounds.northeast();
|
||
auto southwest = bounds.southwest();
|
||
auto southeast = bounds.southeast();
|
||
|
||
auto center = [self convertPoint:{ CGRectGetMidX(view.bounds), CGRectGetMidY(view.bounds) } toLatLngFromView:view];
|
||
|
||
// Extend bounds to account for the antimeridian
|
||
northwest.unwrapForShortestPath(center);
|
||
northeast.unwrapForShortestPath(center);
|
||
southwest.unwrapForShortestPath(center);
|
||
southeast.unwrapForShortestPath(center);
|
||
|
||
auto correctedLatLngBounds = mbgl::LatLngBounds::empty();
|
||
correctedLatLngBounds.extend(northwest);
|
||
correctedLatLngBounds.extend(northeast);
|
||
correctedLatLngBounds.extend(southwest);
|
||
correctedLatLngBounds.extend(southeast);
|
||
|
||
CGRect rect = { [self convertLatLng:correctedLatLngBounds.southwest() toPointToView:view], CGSizeZero };
|
||
rect = MLNExtendRect(rect, [self convertLatLng:correctedLatLngBounds.northeast() toPointToView:view]);
|
||
return rect;
|
||
}
|
||
|
||
/// Converts a rectangle in the given view’s coordinate system to a geographic
|
||
/// bounding box.
|
||
- (mbgl::LatLngBounds)convertRect:(CGRect)rect toLatLngBoundsFromView:(nullable UIView *)view
|
||
{
|
||
auto bounds = mbgl::LatLngBounds::empty();
|
||
auto topLeft = [self convertPoint:{ CGRectGetMinX(rect), CGRectGetMinY(rect) } toLatLngFromView:view];
|
||
auto topRight = [self convertPoint:{ CGRectGetMaxX(rect), CGRectGetMinY(rect) } toLatLngFromView:view];
|
||
auto bottomRight = [self convertPoint:{ CGRectGetMaxX(rect), CGRectGetMaxY(rect) } toLatLngFromView:view];
|
||
auto bottomLeft = [self convertPoint:{ CGRectGetMinX(rect), CGRectGetMaxY(rect) } toLatLngFromView:view];
|
||
|
||
// If the bounds straddles the antimeridian, unwrap it so that one side
|
||
// extends beyond ±180° longitude.
|
||
auto center = [self convertPoint:{ CGRectGetMidX(rect), CGRectGetMidY(rect) } toLatLngFromView:view];
|
||
topLeft.unwrapForShortestPath(center);
|
||
topRight.unwrapForShortestPath(center);
|
||
bottomRight.unwrapForShortestPath(center);
|
||
bottomLeft.unwrapForShortestPath(center);
|
||
|
||
bounds.extend(topLeft);
|
||
bounds.extend(topRight);
|
||
bounds.extend(bottomRight);
|
||
bounds.extend(bottomLeft);
|
||
|
||
return bounds;
|
||
}
|
||
|
||
- (CLLocationDistance)metersPerPointAtLatitude:(CLLocationDegrees)latitude
|
||
{
|
||
return mbgl::Projection::getMetersPerPixelAtLatitude(latitude, self.zoomLevel);
|
||
}
|
||
|
||
- (CLLocationDistance)metersPerPointAtLatitude:(CLLocationDegrees)latitude zoomLevel:(double)zoomLevel
|
||
{
|
||
return mbgl::Projection::getMetersPerPixelAtLatitude(latitude, zoomLevel);
|
||
}
|
||
|
||
- (MLNMapProjection*)mapProjection {
|
||
return [[MLNMapProjection alloc] initWithMapView:self];
|
||
}
|
||
|
||
// MARK: - Camera Change Reason -
|
||
|
||
- (void)resetCameraChangeReason
|
||
{
|
||
self.cameraChangeReasonBitmask = MLNCameraChangeReasonNone;
|
||
}
|
||
|
||
// MARK: - Annotations -
|
||
|
||
- (nullable NSArray<id <MLNAnnotation>> *)annotations
|
||
{
|
||
if (_annotationContextsByAnnotationTag.empty())
|
||
{
|
||
return nil;
|
||
}
|
||
|
||
// Map all the annotation tags to the annotations themselves.
|
||
std::vector<id <MLNAnnotation>> annotations;
|
||
std::transform(_annotationContextsByAnnotationTag.begin(),
|
||
_annotationContextsByAnnotationTag.end(),
|
||
std::back_inserter(annotations),
|
||
^ id <MLNAnnotation> (const std::pair<MLNAnnotationTag, MLNAnnotationContext> &pair)
|
||
{
|
||
return pair.second.annotation;
|
||
});
|
||
|
||
annotations.erase(std::remove_if(annotations.begin(), annotations.end(),
|
||
[](const id <MLNAnnotation> annotation) { return annotation == nullptr; }),
|
||
annotations.end());
|
||
|
||
return [NSArray arrayWithObjects:&annotations[0] count:annotations.size()];
|
||
}
|
||
|
||
- (nullable NSArray<id <MLNAnnotation>> *)visibleAnnotations
|
||
{
|
||
return [self visibleAnnotationsInRect:self.bounds];
|
||
}
|
||
|
||
- (nullable NSArray<id <MLNAnnotation>> *)visibleAnnotationsInRect:(CGRect)rect
|
||
{
|
||
if (_annotationContextsByAnnotationTag.empty())
|
||
{
|
||
return nil;
|
||
}
|
||
|
||
std::vector<MLNAnnotationTag> annotationTags = [self annotationTagsInRect:rect];
|
||
std::vector<MLNAnnotationTag> shapeAnnotationTags = [self shapeAnnotationTagsInRect:rect];
|
||
|
||
if (shapeAnnotationTags.size()) {
|
||
annotationTags.insert(annotationTags.end(), shapeAnnotationTags.begin(), shapeAnnotationTags.end());
|
||
}
|
||
|
||
if (annotationTags.size())
|
||
{
|
||
NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:annotationTags.size()];
|
||
|
||
for (auto const& annotationTag: annotationTags)
|
||
{
|
||
if (!_annotationContextsByAnnotationTag.count(annotationTag) ||
|
||
annotationTag == MLNAnnotationTagNotFound)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
MLNAnnotationContext annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
|
||
MLNAssert(annotationContext.annotation, @"Missing annotation for tag %llu.", annotationTag);
|
||
if (annotationContext.annotation)
|
||
{
|
||
[annotations addObject:annotationContext.annotation];
|
||
}
|
||
}
|
||
|
||
return [annotations copy];
|
||
}
|
||
|
||
return nil;
|
||
}
|
||
|
||
/// Returns the annotation assigned the given tag. Cheap.
|
||
- (id <MLNAnnotation>)annotationWithTag:(MLNAnnotationTag)tag
|
||
{
|
||
if ( ! _annotationContextsByAnnotationTag.count(tag) ||
|
||
tag == MLNAnnotationTagNotFound) {
|
||
return nil;
|
||
}
|
||
|
||
MLNAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(tag);
|
||
return annotationContext.annotation;
|
||
}
|
||
|
||
/// Returns the annotation tag assigned to the given annotation.
|
||
- (MLNAnnotationTag)annotationTagForAnnotation:(id <MLNAnnotation>)annotation
|
||
{
|
||
if ( ! annotation || annotation == self.userLocation
|
||
|| _annotationTagsByAnnotation.count(annotation) == 0)
|
||
{
|
||
return MLNAnnotationTagNotFound;
|
||
}
|
||
|
||
return _annotationTagsByAnnotation.at(annotation);
|
||
}
|
||
|
||
- (void)addAnnotation:(id <MLNAnnotation>)annotation
|
||
{
|
||
MLNLogDebug(@"Adding annotation: %@", annotation);
|
||
if ( ! annotation) return;
|
||
|
||
// The core bulk add API is efficient with respect to indexing and
|
||
// screen refreshes, thus we should defer to it even for individual adds.
|
||
//
|
||
[self addAnnotations:@[ annotation ]];
|
||
}
|
||
|
||
- (void)addAnnotations:(NSArray<id <MLNAnnotation>> *)annotations
|
||
{
|
||
MLNLogDebug(@"Adding: %lu annotations", annotations.count);
|
||
if ( ! annotations) return;
|
||
[self willChangeValueForKey:@"annotations"];
|
||
|
||
NSMutableDictionary *annotationImagesForAnnotation = [NSMutableDictionary dictionary];
|
||
NSMutableDictionary *annotationViewsForAnnotation = [NSMutableDictionary dictionary];
|
||
|
||
BOOL delegateImplementsViewForAnnotation = [self.delegate respondsToSelector:@selector(mapView:viewForAnnotation:)];
|
||
BOOL delegateImplementsImageForPoint = [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)];
|
||
|
||
NSMutableArray *newAnnotationViews = [[NSMutableArray alloc] initWithCapacity:annotations.count];
|
||
|
||
for (id <MLNAnnotation> annotation in annotations)
|
||
{
|
||
MLNAssert([annotation conformsToProtocol:@protocol(MLNAnnotation)], @"annotation should conform to MLNAnnotation");
|
||
|
||
// adding the same annotation object twice is a no-op
|
||
if (_annotationTagsByAnnotation.count(annotation) != 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if ([annotation isKindOfClass:[MLNMultiPoint class]])
|
||
{
|
||
// The polyline or polygon knows how to style itself (with the map view’s help).
|
||
MLNMultiPoint *multiPoint = (MLNMultiPoint *)annotation;
|
||
if (!multiPoint.pointCount) {
|
||
continue;
|
||
}
|
||
|
||
_isChangingAnnotationLayers = YES;
|
||
MLNAnnotationTag annotationTag = self.mbglMap.addAnnotation([multiPoint annotationObjectWithDelegate:self]);
|
||
MLNAnnotationContext context;
|
||
context.annotation = annotation;
|
||
_annotationContextsByAnnotationTag[annotationTag] = context;
|
||
_annotationTagsByAnnotation[annotation] = annotationTag;
|
||
|
||
[(NSObject *)annotation addObserver:self forKeyPath:@"coordinates" options:0 context:(void *)(NSUInteger)annotationTag];
|
||
}
|
||
else if ( ! [annotation isKindOfClass:[MLNMultiPolyline class]]
|
||
&& ![annotation isKindOfClass:[MLNMultiPolygon class]]
|
||
&& ![annotation isKindOfClass:[MLNShapeCollection class]]
|
||
&& ![annotation isKindOfClass:[MLNPointCollection class]])
|
||
{
|
||
MLNAnnotationView *annotationView;
|
||
NSString *symbolName;
|
||
NSValue *annotationValue = [NSValue valueWithNonretainedObject:annotation];
|
||
|
||
if (delegateImplementsViewForAnnotation)
|
||
{
|
||
annotationView = [self annotationViewForAnnotation:annotation];
|
||
if (annotationView)
|
||
{
|
||
annotationViewsForAnnotation[annotationValue] = annotationView;
|
||
annotationView.annotation = annotation;
|
||
annotationView.center = MLNPointRounded([self convertCoordinate:annotation.coordinate toPointToView:self]);
|
||
[newAnnotationViews addObject:annotationView];
|
||
|
||
MLNAnnotationImage *annotationImage = self.invisibleAnnotationImage;
|
||
symbolName = annotationImage.styleIconIdentifier;
|
||
annotationImagesForAnnotation[annotationValue] = annotationImage;
|
||
if ( ! self.annotationImagesByIdentifier[annotationImage.reuseIdentifier])
|
||
{
|
||
[self installAnnotationImage:annotationImage];
|
||
}
|
||
}
|
||
}
|
||
|
||
if ( ! annotationView) {
|
||
MLNAnnotationImage *annotationImage;
|
||
|
||
if (delegateImplementsImageForPoint)
|
||
{
|
||
annotationImage = [self.delegate mapView:self imageForAnnotation:annotation];
|
||
}
|
||
if ( ! annotationImage)
|
||
{
|
||
annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MLNDefaultStyleMarkerSymbolName];
|
||
}
|
||
if ( ! annotationImage)
|
||
{
|
||
annotationImage = self.defaultAnnotationImage;
|
||
}
|
||
|
||
symbolName = annotationImage.styleIconIdentifier;
|
||
|
||
if ( ! symbolName)
|
||
{
|
||
symbolName = [MLNAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier];
|
||
annotationImage.styleIconIdentifier = symbolName;
|
||
}
|
||
if ( ! self.annotationImagesByIdentifier[annotationImage.reuseIdentifier])
|
||
{
|
||
[self installAnnotationImage:annotationImage];
|
||
}
|
||
|
||
annotationImagesForAnnotation[annotationValue] = annotationImage;
|
||
}
|
||
|
||
MLNAnnotationTag annotationTag = self.mbglMap.addAnnotation(mbgl::SymbolAnnotation {
|
||
MLNPointFromLocationCoordinate2D(annotation.coordinate),
|
||
symbolName.UTF8String
|
||
});
|
||
|
||
MLNAnnotationContext context;
|
||
context.annotation = annotation;
|
||
MLNAnnotationImage *annotationImage = annotationImagesForAnnotation[annotationValue];
|
||
context.imageReuseIdentifier = annotationImage.reuseIdentifier;
|
||
|
||
if (annotationView) {
|
||
context.annotationView = annotationView;
|
||
context.viewReuseIdentifier = annotationView.reuseIdentifier;
|
||
}
|
||
|
||
_annotationTagsByAnnotation[annotation] = annotationTag;
|
||
_annotationContextsByAnnotationTag[annotationTag] = context;
|
||
|
||
if ([annotation isKindOfClass:[NSObject class]]) {
|
||
MLNAssert(![annotation isKindOfClass:[MLNMultiPoint class]], @"Point annotation should not be MLNMultiPoint.");
|
||
[(NSObject *)annotation addObserver:self forKeyPath:@"coordinate" options:0 context:(void *)(NSUInteger)annotationTag];
|
||
}
|
||
}
|
||
}
|
||
|
||
[self updateAnnotationContainerViewWithAnnotationViews:newAnnotationViews];
|
||
|
||
[self didChangeValueForKey:@"annotations"];
|
||
if (_isChangingAnnotationLayers)
|
||
{
|
||
[self.style willChangeValueForKey:@"layers"];
|
||
}
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapView:didAddAnnotationViews:)])
|
||
{
|
||
[self.delegate mapView:self didAddAnnotationViews:newAnnotationViews];
|
||
}
|
||
|
||
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
|
||
}
|
||
|
||
- (void)updateAnnotationContainerViewWithAnnotationViews:(NSArray<MLNAnnotationView *> *)annotationViews
|
||
{
|
||
if (annotationViews.count == 0) return;
|
||
|
||
MLNAnnotationContainerView *newAnnotationContainerView;
|
||
if (self.annotationContainerView)
|
||
{
|
||
// reload any previously added views
|
||
newAnnotationContainerView = [MLNAnnotationContainerView annotationContainerViewWithAnnotationContainerView:self.annotationContainerView];
|
||
[self.annotationContainerView removeFromSuperview];
|
||
}
|
||
else
|
||
{
|
||
newAnnotationContainerView = [[MLNAnnotationContainerView alloc] initWithFrame:self.bounds];
|
||
}
|
||
newAnnotationContainerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||
newAnnotationContainerView.contentMode = UIViewContentModeCenter;
|
||
[newAnnotationContainerView addSubviews:annotationViews];
|
||
[_mbglView->getView() insertSubview:newAnnotationContainerView atIndex:0];
|
||
self.annotationContainerView = newAnnotationContainerView;
|
||
|
||
[self updatePresentsWithTransaction];
|
||
}
|
||
|
||
/// Initialize and return a default annotation image that depicts a round pin
|
||
/// rising from the center, with a shadow slightly below center. The alignment
|
||
/// rect therefore excludes the bottom half.
|
||
- (MLNAnnotationImage *)defaultAnnotationImage
|
||
{
|
||
UIImage *image = [UIImage mgl_resourceImageNamed:MLNDefaultStyleMarkerSymbolName];
|
||
image = [image imageWithAlignmentRectInsets:
|
||
UIEdgeInsetsMake(0, 0, image.size.height / 2, 0)];
|
||
MLNAnnotationImage *annotationImage = [MLNAnnotationImage annotationImageWithImage:image
|
||
reuseIdentifier:MLNDefaultStyleMarkerSymbolName];
|
||
annotationImage.styleIconIdentifier = [MLNAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier];
|
||
return annotationImage;
|
||
}
|
||
|
||
- (MLNAnnotationImage *)invisibleAnnotationImage
|
||
{
|
||
MLNAnnotationImage *annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MLNInvisibleStyleMarkerSymbolName];
|
||
|
||
if (!annotationImage)
|
||
{
|
||
UIGraphicsBeginImageContext(CGSizeMake(1, 1));
|
||
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
||
UIGraphicsEndImageContext();
|
||
annotationImage = [MLNAnnotationImage annotationImageWithImage:image
|
||
reuseIdentifier:MLNInvisibleStyleMarkerSymbolName];
|
||
annotationImage.styleIconIdentifier = [MLNAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier];
|
||
}
|
||
|
||
return annotationImage;
|
||
}
|
||
|
||
- (MLNAnnotationView *)annotationViewForAnnotation:(id<MLNAnnotation>)annotation
|
||
{
|
||
MLNAnnotationView *annotationView = [self.delegate mapView:self viewForAnnotation:annotation];
|
||
|
||
if (annotationView)
|
||
{
|
||
// Make sure that the annotation views are selected/deselected correctly because
|
||
// annotations are not dismissed when they move out of the visible bounds
|
||
BOOL isViewForSelectedAnnotation = self.selectedAnnotation == annotation;
|
||
[annotationView setSelected:isViewForSelectedAnnotation];
|
||
|
||
annotationView.annotation = annotation;
|
||
annotationView.mapView = self;
|
||
CGRect bounds = UIEdgeInsetsInsetRect({ CGPointZero, annotationView.frame.size }, annotationView.alignmentRectInsets);
|
||
|
||
// Take any offset into consideration
|
||
CGFloat adjustedAnnotationWidth = CGRectGetWidth(bounds) + fabs(annotationView.centerOffset.dx);
|
||
CGFloat adjustedAnnotationHeight = CGRectGetHeight(bounds) + fabs(annotationView.centerOffset.dx);
|
||
|
||
_largestAnnotationViewSize = CGSizeMake(MAX(_largestAnnotationViewSize.width, adjustedAnnotationWidth),
|
||
MAX(_largestAnnotationViewSize.height, adjustedAnnotationHeight));
|
||
|
||
_unionedAnnotationRepresentationSize = CGSizeMake(MAX(_unionedAnnotationRepresentationSize.width, _largestAnnotationViewSize.width),
|
||
MAX(_unionedAnnotationRepresentationSize.height, _largestAnnotationViewSize.height));
|
||
}
|
||
|
||
return annotationView;
|
||
}
|
||
|
||
- (nullable MLNAnnotationView *)viewForAnnotation:(id<MLNAnnotation>)annotation
|
||
{
|
||
MLNLogDebug(@"Retrieving the annotation view for: %@", annotation);
|
||
if (annotation == self.userLocation)
|
||
{
|
||
return self.userLocationAnnotationView;
|
||
}
|
||
MLNAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
|
||
if (annotationTag == MLNAnnotationTagNotFound) {
|
||
return nil;
|
||
}
|
||
MLNAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
|
||
return annotationContext.annotationView;
|
||
}
|
||
|
||
- (double)alphaForShapeAnnotation:(MLNShape *)annotation
|
||
{
|
||
if (_delegateHasAlphasForShapeAnnotations)
|
||
{
|
||
return [self.delegate mapView:self alphaForShapeAnnotation:annotation];
|
||
}
|
||
return 1.0;
|
||
}
|
||
|
||
- (mbgl::Color)strokeColorForShapeAnnotation:(MLNShape *)annotation
|
||
{
|
||
UIColor *color = (_delegateHasStrokeColorsForShapeAnnotations
|
||
? [self.delegate mapView:self strokeColorForShapeAnnotation:annotation]
|
||
: self.tintColor);
|
||
return color.mgl_color;
|
||
}
|
||
|
||
- (mbgl::Color)fillColorForPolygonAnnotation:(MLNPolygon *)annotation
|
||
{
|
||
UIColor *color = (_delegateHasFillColorsForShapeAnnotations
|
||
? [self.delegate mapView:self fillColorForPolygonAnnotation:annotation]
|
||
: self.tintColor);
|
||
return color.mgl_color;
|
||
}
|
||
|
||
- (CGFloat)lineWidthForPolylineAnnotation:(MLNPolyline *)annotation
|
||
{
|
||
if (_delegateHasLineWidthsForShapeAnnotations)
|
||
{
|
||
return [self.delegate mapView:self lineWidthForPolylineAnnotation:(MLNPolyline *)annotation];
|
||
}
|
||
return 3.0;
|
||
}
|
||
|
||
- (void)installAnnotationImage:(MLNAnnotationImage *)annotationImage
|
||
{
|
||
NSString *iconIdentifier = annotationImage.styleIconIdentifier;
|
||
self.annotationImagesByIdentifier[annotationImage.reuseIdentifier] = annotationImage;
|
||
annotationImage.delegate = self;
|
||
|
||
// add sprite
|
||
self.mbglMap.addAnnotationImage([annotationImage.image mgl_styleImageWithIdentifier:iconIdentifier]);
|
||
|
||
// Create a slop area with a “radius” equal in size to the annotation
|
||
// image’s alignment rect, allowing the eventual tap to be on any point
|
||
// within this image. Union this slop area with any existing slop areas.
|
||
CGRect bounds = UIEdgeInsetsInsetRect({ CGPointZero, annotationImage.image.size },
|
||
annotationImage.image.alignmentRectInsets);
|
||
_unionedAnnotationRepresentationSize = CGSizeMake(MAX(_unionedAnnotationRepresentationSize.width, bounds.size.width),
|
||
MAX(_unionedAnnotationRepresentationSize.height, bounds.size.height));
|
||
}
|
||
|
||
- (void)removeAnnotation:(id <MLNAnnotation>)annotation
|
||
{
|
||
MLNLogDebug(@"Removing annotation: %@", annotation);
|
||
if ( ! annotation) return;
|
||
|
||
// The core bulk deletion API is efficient with respect to indexing
|
||
// and screen refreshes, thus we should defer to it even for
|
||
// individual deletes.
|
||
//
|
||
[self removeAnnotations:@[ annotation ]];
|
||
}
|
||
|
||
- (void)removeAnnotations:(NSArray<id <MLNAnnotation>> *)annotations
|
||
{
|
||
MLNLogDebug(@"Removing: %lu annotations", annotations.count);
|
||
if ( ! annotations) return;
|
||
|
||
[self willChangeValueForKey:@"annotations"];
|
||
|
||
for (id <MLNAnnotation> annotation in annotations)
|
||
{
|
||
MLNAssert([annotation conformsToProtocol:@protocol(MLNAnnotation)], @"annotation should conform to MLNAnnotation");
|
||
|
||
MLNAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
|
||
if (annotationTag == MLNAnnotationTagNotFound)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
MLNAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
|
||
MLNAnnotationView *annotationView = annotationContext.annotationView;
|
||
|
||
if (annotationContext.viewReuseIdentifier)
|
||
{
|
||
NSMutableArray *annotationViewReuseQueue = [self annotationViewReuseQueueForIdentifier:annotationContext.viewReuseIdentifier];
|
||
if (![annotationViewReuseQueue containsObject:annotationView])
|
||
{
|
||
[annotationViewReuseQueue removeObject:annotationView];
|
||
}
|
||
}
|
||
|
||
annotationView.annotation = nil;
|
||
[annotationView removeFromSuperview];
|
||
[self.annotationContainerView.annotationViews removeObject:annotationView];
|
||
|
||
if (annotationTag == _selectedAnnotationTag)
|
||
{
|
||
[self deselectAnnotation:annotation animated:NO];
|
||
}
|
||
|
||
_annotationContextsByAnnotationTag.erase(annotationTag);
|
||
_annotationTagsByAnnotation.erase(annotation);
|
||
|
||
if ([annotation isKindOfClass:[NSObject class]] && ![annotation isKindOfClass:[MLNMultiPoint class]])
|
||
{
|
||
[(NSObject *)annotation removeObserver:self forKeyPath:@"coordinate" context:(void *)(NSUInteger)annotationTag];
|
||
}
|
||
else if ([annotation isKindOfClass:[MLNMultiPoint class]])
|
||
{
|
||
[(NSObject *)annotation removeObserver:self forKeyPath:@"coordinates" context:(void *)(NSUInteger)annotationTag];
|
||
}
|
||
|
||
_isChangingAnnotationLayers = YES;
|
||
// If the underlying map is gone, there’s nothing to remove, but still
|
||
// continue to unregister KVO and other annotation resources.
|
||
if (_mbglMap)
|
||
{
|
||
self.mbglMap.removeAnnotation(annotationTag);
|
||
}
|
||
}
|
||
|
||
[self updatePresentsWithTransaction];
|
||
|
||
[self didChangeValueForKey:@"annotations"];
|
||
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
|
||
if (_isChangingAnnotationLayers)
|
||
{
|
||
[self.style willChangeValueForKey:@"layers"];
|
||
}
|
||
}
|
||
|
||
- (nonnull NSArray<id <MLNOverlay>> *)overlays
|
||
{
|
||
if (self.annotations == nil) { return @[]; }
|
||
|
||
NSMutableArray<id <MLNOverlay>> *mutableOverlays = [NSMutableArray array];
|
||
|
||
[self.annotations enumerateObjectsUsingBlock:^(id<MLNAnnotation> _Nonnull annotation, NSUInteger idx, BOOL * _Nonnull stop) {
|
||
if ([annotation conformsToProtocol:@protocol(MLNOverlay)])
|
||
{
|
||
[mutableOverlays addObject:(id<MLNOverlay>)annotation];
|
||
}
|
||
}];
|
||
|
||
return [NSArray arrayWithArray:mutableOverlays];
|
||
}
|
||
|
||
- (void)addOverlay:(id <MLNOverlay>)overlay
|
||
{
|
||
MLNLogDebug(@"Adding overlay: %@", overlay);
|
||
[self addOverlays:@[ overlay ]];
|
||
}
|
||
|
||
- (void)addOverlays:(NSArray<id <MLNOverlay>> *)overlays
|
||
{
|
||
MLNLogDebug(@"Adding: %lu overlays", overlays.count);
|
||
#if DEBUG
|
||
for (id <MLNOverlay> overlay in overlays)
|
||
{
|
||
MLNAssert([overlay conformsToProtocol:@protocol(MLNOverlay)], @"overlay should conform to MLNOverlay");
|
||
}
|
||
#endif
|
||
|
||
[self addAnnotations:overlays];
|
||
}
|
||
|
||
- (void)removeOverlay:(id <MLNOverlay>)overlay
|
||
{
|
||
MLNLogDebug(@"Removing overlay: %@", overlay);
|
||
[self removeOverlays:@[ overlay ]];
|
||
}
|
||
|
||
- (void)removeOverlays:(NSArray<id <MLNOverlay>> *)overlays
|
||
{
|
||
MLNLogDebug(@"Removing: %lu overlays", overlays.count);
|
||
#if DEBUG
|
||
for (id <MLNOverlay> overlay in overlays)
|
||
{
|
||
MLNAssert([overlay conformsToProtocol:@protocol(MLNOverlay)], @"overlay should conform to MLNOverlay");
|
||
}
|
||
#endif
|
||
|
||
[self removeAnnotations:overlays];
|
||
}
|
||
|
||
- (nullable MLNAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier
|
||
{
|
||
return self.annotationImagesByIdentifier[identifier];
|
||
}
|
||
|
||
- (nullable MLNAnnotationView *)dequeueReusableAnnotationViewWithIdentifier:(NSString *)identifier
|
||
{
|
||
NSMutableArray *annotationViewReuseQueue = [self annotationViewReuseQueueForIdentifier:identifier];
|
||
MLNAnnotationView *reusableView = annotationViewReuseQueue.firstObject;
|
||
[reusableView prepareForReuse];
|
||
[annotationViewReuseQueue removeObject:reusableView];
|
||
|
||
return reusableView;
|
||
}
|
||
|
||
/**
|
||
Returns the tag of the annotation at the given point in the view.
|
||
|
||
This is more involved than it sounds: if multiple point annotations overlap
|
||
near the point, this method cycles through them so that each of them is
|
||
accessible to the user at some point.
|
||
|
||
@param persist True to remember the cycleable set of annotations, so that a
|
||
different annotation is returned the next time this method is called
|
||
with the same point. Setting this parameter to false is useful for
|
||
asking “what if?”
|
||
*/
|
||
- (MLNAnnotationTag)annotationTagAtPoint:(CGPoint)point persistingResults:(BOOL)persist
|
||
{
|
||
// Look for any annotation near the tap. An annotation is “near” if the
|
||
// distance between its center and the tap is less than the maximum height
|
||
// or width of an installed annotation image or annotation view.
|
||
CGRect queryRect = CGRectInset({ point, CGSizeZero },
|
||
-_unionedAnnotationRepresentationSize.width,
|
||
-_unionedAnnotationRepresentationSize.height);
|
||
queryRect = CGRectInset(queryRect, -MLNAnnotationImagePaddingForHitTest,
|
||
-MLNAnnotationImagePaddingForHitTest);
|
||
std::vector<MLNAnnotationTag> nearbyAnnotations = [self annotationTagsInRect:queryRect];
|
||
std::vector<MLNAnnotationTag> nearbyShapeAnnotations = [self shapeAnnotationTagsInRect:queryRect];
|
||
|
||
if (nearbyShapeAnnotations.size()) {
|
||
nearbyAnnotations.insert(nearbyAnnotations.end(), nearbyShapeAnnotations.begin(), nearbyShapeAnnotations.end());
|
||
}
|
||
|
||
if (nearbyAnnotations.size())
|
||
{
|
||
// Assume that the user is fat-fingering an annotation.
|
||
CGRect hitRect = CGRectInset({ point, CGSizeZero },
|
||
-MLNAnnotationImagePaddingForHitTest,
|
||
-MLNAnnotationImagePaddingForHitTest);
|
||
|
||
// 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 MLNAnnotationTag annotationTag) {
|
||
id <MLNAnnotation> annotation = [self annotationWithTag:annotationTag];
|
||
MLNAssert(annotation, @"Unknown annotation found nearby tap");
|
||
if ( ! annotation)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
MLNAnnotationContext annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
|
||
CGRect annotationRect;
|
||
|
||
MLNAnnotationView *annotationView = annotationContext.annotationView;
|
||
|
||
if (annotationView)
|
||
{
|
||
if ( ! annotationView.enabled)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
CGPoint calloutAnchorPoint = MLNPointRounded([self convertCoordinate:annotation.coordinate toPointToView:self]);
|
||
CGRect frame = CGRectInset({ calloutAnchorPoint, CGSizeZero }, -CGRectGetWidth(annotationView.frame) / 2, -CGRectGetHeight(annotationView.frame) / 2);
|
||
|
||
// We need to take any offset into consideration. Note that a large offset will result in a
|
||
// large value for `_unionedAnnotationRepresentationSize` (and thus a larger feature query rect).
|
||
// Aim to keep the offset as small as possible.
|
||
frame = CGRectOffset(frame, annotationView.centerOffset.dx, annotationView.centerOffset.dy);
|
||
|
||
annotationRect = UIEdgeInsetsInsetRect(frame, annotationView.alignmentRectInsets);
|
||
}
|
||
else
|
||
{
|
||
if ([annotation isKindOfClass:[MLNMultiPoint class]])
|
||
{
|
||
if ([self.delegate respondsToSelector:@selector(mapView:shapeAnnotationIsEnabled:)]) {
|
||
return !!(![self.delegate mapView:self shapeAnnotationIsEnabled:(MLNMultiPoint *)annotation]);
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
MLNAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
|
||
if ( ! annotationImage.enabled)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
MLNAnnotationImage *fallbackAnnotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MLNDefaultStyleMarkerSymbolName];
|
||
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));
|
||
|
||
}
|
||
|
||
MLNAnnotationTag hitAnnotationTag = MLNAnnotationTagNotFound;
|
||
if (nearbyAnnotations.size())
|
||
{
|
||
// The first selection in the cycle should be the one nearest to the
|
||
// tap. Also the annotation tags need to be stable in order to compare them with
|
||
// the remembered tags _annotationsNearbyLastTap.
|
||
CLLocationCoordinate2D currentCoordinate = [self convertPoint:point toCoordinateFromView:self];
|
||
std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MLNAnnotationTag tagA, const MLNAnnotationTag 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;
|
||
});
|
||
|
||
if (nearbyAnnotations == _annotationsNearbyLastTap)
|
||
{
|
||
// The last time we persisted a set of annotations, we had the same
|
||
// set of annotations as we do now. Cycle through them.
|
||
if (_selectedAnnotationTag == MLNAnnotationTagNotFound
|
||
|| _selectedAnnotationTag == nearbyAnnotations.back())
|
||
{
|
||
// Either no annotation is selected or the last annotation in
|
||
// the set was selected. Wrap around to the first annotation in
|
||
// the set.
|
||
hitAnnotationTag = nearbyAnnotations.front();
|
||
}
|
||
else
|
||
{
|
||
auto result = std::find(nearbyAnnotations.begin(),
|
||
nearbyAnnotations.end(),
|
||
_selectedAnnotationTag);
|
||
if (result == nearbyAnnotations.end())
|
||
{
|
||
// An annotation from this set hasn’t been selected before.
|
||
// Select the first (nearest) one.
|
||
hitAnnotationTag = nearbyAnnotations.front();
|
||
}
|
||
else
|
||
{
|
||
// Step to the next annotation in the set.
|
||
auto distance = std::distance(nearbyAnnotations.begin(), result);
|
||
hitAnnotationTag = nearbyAnnotations[distance + 1];
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Remember the nearby annotations for the next time this method is
|
||
// called.
|
||
if (persist)
|
||
{
|
||
_annotationsNearbyLastTap = nearbyAnnotations;
|
||
}
|
||
|
||
// Choose the first nearby annotation.
|
||
if (nearbyAnnotations.size())
|
||
{
|
||
hitAnnotationTag = nearbyAnnotations.front();
|
||
}
|
||
}
|
||
}
|
||
|
||
return hitAnnotationTag;
|
||
}
|
||
|
||
/// Returns the tags of the annotations coincident with the given rectangle.
|
||
- (std::vector<MLNAnnotationTag>)annotationTagsInRect:(CGRect)rect
|
||
{
|
||
return _rendererFrontend->getRenderer()->queryPointAnnotations({
|
||
{ CGRectGetMinX(rect), CGRectGetMinY(rect) },
|
||
{ CGRectGetMaxX(rect), CGRectGetMaxY(rect) },
|
||
});
|
||
}
|
||
|
||
- (std::vector<MLNAnnotationTag>)shapeAnnotationTagsInRect:(CGRect)rect
|
||
{
|
||
return _rendererFrontend->getRenderer()->queryShapeAnnotations({
|
||
{ CGRectGetMinX(rect), CGRectGetMinY(rect) },
|
||
{ CGRectGetMaxX(rect), CGRectGetMaxY(rect) },
|
||
});
|
||
}
|
||
|
||
|
||
- (BOOL)isMovingAnnotationIntoViewSupportedForAnnotation:(id<MLNAnnotation>)annotation animated:(BOOL)animated {
|
||
// Consider delegating
|
||
return [annotation isKindOfClass:[MLNPointAnnotation class]];
|
||
}
|
||
|
||
- (id <MLNAnnotation>)selectedAnnotation
|
||
{
|
||
if (_userLocationAnnotationIsSelected)
|
||
{
|
||
return self.userLocation;
|
||
}
|
||
|
||
if ( ! _annotationContextsByAnnotationTag.count(_selectedAnnotationTag) ||
|
||
_selectedAnnotationTag == MLNAnnotationTagNotFound) {
|
||
return nil;
|
||
}
|
||
|
||
MLNAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(_selectedAnnotationTag);
|
||
return annotationContext.annotation;
|
||
}
|
||
|
||
- (void)setSelectedAnnotation:(id <MLNAnnotation>)annotation
|
||
{
|
||
MLNLogDebug(@"Selecting annotation: %@", annotation);
|
||
[self willChangeValueForKey:@"selectedAnnotations"];
|
||
_selectedAnnotationTag = [self annotationTagForAnnotation:annotation];
|
||
_userLocationAnnotationIsSelected = annotation && annotation == self.userLocation;
|
||
[self didChangeValueForKey:@"selectedAnnotations"];
|
||
}
|
||
|
||
- (NSArray<id <MLNAnnotation>> *)selectedAnnotations
|
||
{
|
||
id <MLNAnnotation> selectedAnnotation = self.selectedAnnotation;
|
||
return (selectedAnnotation ? @[ selectedAnnotation ] : @[]);
|
||
}
|
||
|
||
- (void)setSelectedAnnotations:(NSArray<id <MLNAnnotation>> *)selectedAnnotations
|
||
{
|
||
if ( ! selectedAnnotations.count) return;
|
||
|
||
id <MLNAnnotation> firstAnnotation = selectedAnnotations[0];
|
||
|
||
MLNAssert([firstAnnotation conformsToProtocol:@protocol(MLNAnnotation)], @"annotation should conform to MLNAnnotation");
|
||
|
||
if ([firstAnnotation isKindOfClass:[MLNMultiPoint class]]) return;
|
||
|
||
[self selectAnnotation:firstAnnotation animated:YES completionHandler:nil];
|
||
}
|
||
|
||
- (void)selectAnnotation:(id <MLNAnnotation>)annotation animated:(BOOL)animated
|
||
{
|
||
[self selectAnnotation:annotation animated:animated completionHandler:nil];
|
||
}
|
||
|
||
- (void)selectAnnotation:(id <MLNAnnotation>)annotation animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
[self selectAnnotation:annotation moveIntoView:animated animateSelection:animated completionHandler:completion];
|
||
}
|
||
|
||
- (void)selectAnnotation:(id <MLNAnnotation>)annotation moveIntoView:(BOOL)moveIntoView animateSelection:(BOOL)animateSelection completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
MLNLogDebug(@"Selecting annotation: %@ moveIntoView: %@ animateSelection: %@", annotation, MLNStringFromBOOL(moveIntoView), MLNStringFromBOOL(animateSelection));
|
||
CGRect positioningRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:CGPointZero];
|
||
[self selectAnnotation:annotation moveIntoView:moveIntoView animateSelection:animateSelection calloutPositioningRect:positioningRect completionHandler:completion];
|
||
}
|
||
|
||
- (void)selectAnnotation:(id <MLNAnnotation>)annotation moveIntoView:(BOOL)moveIntoView animateSelection:(BOOL)animateSelection calloutPositioningRect:(CGRect)calloutPositioningRect completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
if ( ! annotation) return;
|
||
|
||
if (annotation == self.selectedAnnotation) return;
|
||
|
||
[self deselectAnnotation:self.selectedAnnotation animated:NO];
|
||
|
||
// Add the annotation to the map if it hasn’t been added yet.
|
||
MLNAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
|
||
if (annotationTag == MLNAnnotationTagNotFound && annotation != self.userLocation)
|
||
{
|
||
[self addAnnotation:annotation];
|
||
annotationTag = [self annotationTagForAnnotation:annotation];
|
||
if (annotationTag == MLNAnnotationTagNotFound) return;
|
||
}
|
||
|
||
MLNAnnotationView *annotationView = nil;
|
||
|
||
if (annotation != self.userLocation)
|
||
if (annotationTag != MLNAnnotationTagNotFound) {
|
||
MLNAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
|
||
annotationView = annotationContext.annotationView;
|
||
if (annotationView && annotationView.enabled) {
|
||
// Annotations represented by views use the view frame as the positioning rect.
|
||
calloutPositioningRect = annotationView.frame;
|
||
[annotationView.superview bringSubviewToFront:annotationView];
|
||
|
||
[annotationView setSelected:YES animated:animateSelection];
|
||
}
|
||
}
|
||
|
||
self.selectedAnnotation = annotation;
|
||
|
||
// Determine if we're allowed to move this offscreen annotation on screen, even though we've asked it to
|
||
if (moveIntoView) {
|
||
moveIntoView = [self isMovingAnnotationIntoViewSupportedForAnnotation:annotation animated:animateSelection];
|
||
}
|
||
|
||
// If we have an invalid positioning rect, we need to provide a suitable default.
|
||
// This (currently) happens if you select an annotation that has NOT yet been
|
||
// added. See https://github.com/mapbox/mapbox-gl-native/issues/11476
|
||
if (CGRectIsNull(calloutPositioningRect)) {
|
||
CLLocationCoordinate2D origin = annotation.coordinate;
|
||
CGPoint originPoint = [self convertCoordinate:origin toPointToView:self];
|
||
calloutPositioningRect = { .origin = originPoint, .size = CGSizeZero };
|
||
}
|
||
|
||
CGRect expandedPositioningRect = calloutPositioningRect;
|
||
|
||
// Used for callout positioning, and moving offscreen annotations onscreen.
|
||
CGRect constrainedRect = self.contentFrame;
|
||
CGRect bounds = constrainedRect;
|
||
|
||
BOOL expandedPositioningRectToMoveCalloutIntoViewWithMargins = NO;
|
||
|
||
UIView <MLNCalloutView> *calloutView = nil;
|
||
|
||
if ([annotation respondsToSelector:@selector(title)] &&
|
||
annotation.title &&
|
||
[self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)] &&
|
||
[self.delegate mapView:self annotationCanShowCallout:annotation])
|
||
{
|
||
// build the callout
|
||
if ([self.delegate respondsToSelector:@selector(mapView:calloutViewForAnnotation:)])
|
||
{
|
||
id providedCalloutView = [self.delegate mapView:self calloutViewForAnnotation:annotation];
|
||
if (providedCalloutView) {
|
||
if (![providedCalloutView isKindOfClass:[UIView class]]) {
|
||
[NSException raise:NSInvalidArgumentException format:@"Callout view must be a kind of UIView"];
|
||
}
|
||
MLNAssert([providedCalloutView conformsToProtocol:@protocol(MLNCalloutView)], @"callout view must conform to MLNCalloutView");
|
||
calloutView = providedCalloutView;
|
||
}
|
||
}
|
||
if (!calloutView)
|
||
{
|
||
calloutView = [self calloutViewForAnnotation:annotation];
|
||
}
|
||
self.calloutViewForSelectedAnnotation = calloutView;
|
||
|
||
if (_userLocationAnnotationIsSelected)
|
||
{
|
||
calloutPositioningRect = [self.userLocationAnnotationView.layer.presentationLayer frame];
|
||
|
||
CGRect implicitAnnotationFrame = [self.userLocationAnnotationView.layer.presentationLayer frame];
|
||
CGRect explicitAnnotationFrame = self.userLocationAnnotationView.frame;
|
||
_initialImplicitCalloutViewOffset = CGPointMake(CGRectGetMinX(explicitAnnotationFrame) - CGRectGetMinX(implicitAnnotationFrame),
|
||
CGRectGetMinY(explicitAnnotationFrame) - CGRectGetMinY(implicitAnnotationFrame));
|
||
}
|
||
|
||
// consult delegate for left and/or right accessory views
|
||
if ([self.delegate respondsToSelector:@selector(mapView:leftCalloutAccessoryViewForAnnotation:)])
|
||
{
|
||
calloutView.leftAccessoryView = [self.delegate mapView:self leftCalloutAccessoryViewForAnnotation:annotation];
|
||
|
||
if ([calloutView.leftAccessoryView isKindOfClass:[UIControl class]])
|
||
{
|
||
UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self
|
||
action:@selector(handleCalloutAccessoryTapGesture:)];
|
||
|
||
[calloutView.leftAccessoryView addGestureRecognizer:calloutAccessoryTap];
|
||
}
|
||
}
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapView:rightCalloutAccessoryViewForAnnotation:)])
|
||
{
|
||
calloutView.rightAccessoryView = [self.delegate mapView:self rightCalloutAccessoryViewForAnnotation:annotation];
|
||
|
||
if ([calloutView.rightAccessoryView isKindOfClass:[UIControl class]])
|
||
{
|
||
UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self
|
||
action:@selector(handleCalloutAccessoryTapGesture:)];
|
||
|
||
[calloutView.rightAccessoryView addGestureRecognizer:calloutAccessoryTap];
|
||
}
|
||
}
|
||
|
||
// set annotation delegate to handle taps on the callout view
|
||
calloutView.delegate = self;
|
||
|
||
// If the callout view provides inset (outset) information, we can use it to expand our positioning
|
||
// rect, which we then use to help move the annotation on-screen if want need to.
|
||
if (moveIntoView && [calloutView respondsToSelector:@selector(marginInsetsHintForPresentationFromRect:)]) {
|
||
UIEdgeInsets margins = [calloutView marginInsetsHintForPresentationFromRect:calloutPositioningRect];
|
||
expandedPositioningRect = UIEdgeInsetsInsetRect(expandedPositioningRect, margins);
|
||
expandedPositioningRectToMoveCalloutIntoViewWithMargins = YES;
|
||
}
|
||
}
|
||
|
||
if (!expandedPositioningRectToMoveCalloutIntoViewWithMargins)
|
||
{
|
||
// We don't have a callout (OR our callout didn't implement
|
||
// marginInsetsHintForPresentationFromRect: - in this case we need to
|
||
// ensure that partially off-screen annotations are NOT moved into view.
|
||
//
|
||
// We may want to create (and fallback to) an ``MLNMapViewDelegate`` version
|
||
// of the `-[MLNCalloutView marginInsetsHintForPresentationFromRect:]
|
||
// protocol method.
|
||
bounds = CGRectInset(bounds, -calloutPositioningRect.size.width, -calloutPositioningRect.size.height);
|
||
}
|
||
|
||
if (moveIntoView)
|
||
{
|
||
moveIntoView = NO;
|
||
|
||
// Any one of these cases should trigger a move onscreen
|
||
CGFloat minX = CGRectGetMinX(expandedPositioningRect);
|
||
|
||
if (minX < CGRectGetMinX(bounds)) {
|
||
constrainedRect.origin.x = minX;
|
||
moveIntoView = YES;
|
||
}
|
||
else {
|
||
CGFloat maxX = CGRectGetMaxX(expandedPositioningRect);
|
||
|
||
if (maxX > CGRectGetMaxX(bounds)) {
|
||
constrainedRect.origin.x = maxX - CGRectGetWidth(constrainedRect);
|
||
moveIntoView = YES;
|
||
}
|
||
}
|
||
|
||
CGFloat minY = CGRectGetMinY(expandedPositioningRect);
|
||
|
||
if (minY < CGRectGetMinY(bounds)) {
|
||
constrainedRect.origin.y = minY;
|
||
moveIntoView = YES;
|
||
}
|
||
else {
|
||
CGFloat maxY = CGRectGetMaxY(expandedPositioningRect);
|
||
|
||
if (maxY > CGRectGetMaxY(bounds)) {
|
||
constrainedRect.origin.y = maxY - CGRectGetHeight(constrainedRect);
|
||
moveIntoView = YES;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Remember, calloutView can be nil here.
|
||
[calloutView presentCalloutFromRect:calloutPositioningRect
|
||
inView:_mbglView->getView()
|
||
constrainedToRect:constrainedRect
|
||
animated:animateSelection];
|
||
|
||
// Save the anchor coordinate
|
||
if ([annotation isKindOfClass:[MLNPointAnnotation class]]) {
|
||
self.anchorCoordinateForSelectedAnnotation = annotation.coordinate;
|
||
}
|
||
else {
|
||
// This is used for features like polygons, so that if the map is dragged
|
||
// the callout doesn't ping to its coordinate.
|
||
CGPoint anchorPoint = CGPointMake(CGRectGetMidX(calloutPositioningRect), CGRectGetMidY(calloutPositioningRect));
|
||
self.anchorCoordinateForSelectedAnnotation = [self convertPoint:anchorPoint toCoordinateFromView:self];
|
||
}
|
||
|
||
// notify delegate
|
||
if ([self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)])
|
||
{
|
||
[self.delegate mapView:self didSelectAnnotation:annotation];
|
||
}
|
||
|
||
if (annotationView && [self.delegate respondsToSelector:@selector(mapView:didSelectAnnotationView:)])
|
||
{
|
||
[self.delegate mapView:self didSelectAnnotationView:annotationView];
|
||
}
|
||
|
||
if (moveIntoView)
|
||
{
|
||
CGPoint center = CGPointMake(CGRectGetMidX(constrainedRect), CGRectGetMidY(constrainedRect));
|
||
CLLocationCoordinate2D centerCoord = [self convertPoint:center toCoordinateFromView:self];
|
||
[self setCenterCoordinate:centerCoord zoomLevel:self.zoomLevel direction:self.direction animated:animateSelection completionHandler:completion];
|
||
}
|
||
else if (completion)
|
||
{
|
||
completion();
|
||
}
|
||
}
|
||
|
||
- (MLNCompactCalloutView *)calloutViewForAnnotation:(id <MLNAnnotation>)annotation
|
||
{
|
||
MLNCompactCalloutView *calloutView = [MLNCompactCalloutView platformCalloutView];
|
||
calloutView.representedObject = annotation;
|
||
calloutView.tintColor = self.tintColor;
|
||
|
||
return calloutView;
|
||
}
|
||
|
||
/// 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 <MLNAnnotation>)annotation defaultCalloutPoint:(CGPoint)calloutPoint
|
||
{
|
||
MLNAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
|
||
CGRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag];
|
||
|
||
if (CGRectIsNull(positioningRect)) {
|
||
return positioningRect;
|
||
}
|
||
|
||
// 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:(MLNAnnotationTag)annotationTag
|
||
{
|
||
id <MLNAnnotation> annotation = [self annotationWithTag:annotationTag];
|
||
if ( ! annotation)
|
||
{
|
||
return CGRectNull;
|
||
}
|
||
|
||
CLLocationCoordinate2D coordinate;
|
||
|
||
if ((annotation == self.selectedAnnotation) &&
|
||
CLLocationCoordinate2DIsValid(self.anchorCoordinateForSelectedAnnotation)) {
|
||
coordinate = self.anchorCoordinateForSelectedAnnotation;
|
||
}
|
||
else {
|
||
coordinate = annotation.coordinate;
|
||
}
|
||
|
||
if ([annotation isKindOfClass:[MLNMultiPoint class]]) {
|
||
CLLocationCoordinate2D origin = coordinate;
|
||
CGPoint originPoint = [self convertCoordinate:origin toPointToView:self];
|
||
return CGRectMake(originPoint.x, originPoint.y, MLNAnnotationImagePaddingForHitTest, MLNAnnotationImagePaddingForHitTest);
|
||
}
|
||
|
||
UIImage *image = [self imageOfAnnotationWithTag:annotationTag].image;
|
||
if ( ! image)
|
||
{
|
||
image = [self dequeueReusableAnnotationImageWithIdentifier:MLNDefaultStyleMarkerSymbolName].image;
|
||
}
|
||
if ( ! image)
|
||
{
|
||
return CGRectZero;
|
||
}
|
||
|
||
CGRect positioningRect = [self frameOfImage:image centeredAtCoordinate:coordinate];
|
||
positioningRect.origin.x -= 0.5;
|
||
|
||
return CGRectInset(positioningRect, -MLNAnnotationImagePaddingForCallout,
|
||
-MLNAnnotationImagePaddingForCallout);
|
||
}
|
||
|
||
/// Returns the rectangle relative to the viewport that represents the given
|
||
/// image centered at the given coordinate.
|
||
- (CGRect)frameOfImage:(UIImage *)image centeredAtCoordinate:(CLLocationCoordinate2D)coordinate
|
||
{
|
||
CGPoint calloutAnchorPoint = MLNPointRounded([self convertCoordinate:coordinate toPointToView:self]);
|
||
CGRect frame = CGRectInset({ calloutAnchorPoint, CGSizeZero }, -image.size.width / 2, -image.size.height / 2);
|
||
return UIEdgeInsetsInsetRect(frame, image.alignmentRectInsets);
|
||
}
|
||
|
||
/// Returns the annotation image assigned to the annotation with the given tag.
|
||
- (MLNAnnotationImage *)imageOfAnnotationWithTag:(MLNAnnotationTag)annotationTag
|
||
{
|
||
if (annotationTag == MLNAnnotationTagNotFound
|
||
|| _annotationContextsByAnnotationTag.count(annotationTag) == 0)
|
||
{
|
||
return nil;
|
||
}
|
||
|
||
NSString *customSymbol = _annotationContextsByAnnotationTag.at(annotationTag).imageReuseIdentifier;
|
||
NSString *symbolName = customSymbol.length ? customSymbol : MLNDefaultStyleMarkerSymbolName;
|
||
|
||
return [self dequeueReusableAnnotationImageWithIdentifier:symbolName];
|
||
}
|
||
|
||
- (void)deselectAnnotation:(id <MLNAnnotation>)annotation animated:(BOOL)animated
|
||
{
|
||
if ( ! annotation) return;
|
||
|
||
if (self.selectedAnnotation == annotation)
|
||
{
|
||
MLNLogDebug(@"Deselecting annotation: %@ animated: %@", annotation, MLNStringFromBOOL(animated));
|
||
// dismiss popup
|
||
[self.calloutViewForSelectedAnnotation dismissCalloutAnimated:animated];
|
||
|
||
// deselect annotation view
|
||
MLNAnnotationView *annotationView = nil;
|
||
MLNAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
|
||
|
||
if (annotationTag != MLNAnnotationTagNotFound)
|
||
{
|
||
MLNAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
|
||
annotationView = annotationContext.annotationView;
|
||
[annotationView setSelected:NO animated:animated];
|
||
}
|
||
|
||
// clean up
|
||
self.calloutViewForSelectedAnnotation = nil;
|
||
self.selectedAnnotation = nil;
|
||
self.anchorCoordinateForSelectedAnnotation = kCLLocationCoordinate2DInvalid;
|
||
|
||
// notify delegate
|
||
if ([self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotation:)])
|
||
{
|
||
[self.delegate mapView:self didDeselectAnnotation:annotation];
|
||
}
|
||
|
||
if (annotationView && [self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotationView:)])
|
||
{
|
||
[self.delegate mapView:self didDeselectAnnotationView:annotationView];
|
||
}
|
||
|
||
[self updatePresentsWithTransaction];
|
||
}
|
||
}
|
||
|
||
- (void)calloutViewWillAppear:(UIView <MLNCalloutView> *)calloutView
|
||
{
|
||
if (_userLocationAnnotationIsSelected ||
|
||
CGPointEqualToPoint(_initialImplicitCalloutViewOffset, CGPointZero))
|
||
{
|
||
return;
|
||
}
|
||
|
||
__weak __typeof__(self) weakSelf = self;
|
||
|
||
// The user location callout view initially points to the user location
|
||
// annotation’s implicit (visual) frame, which is offset from the
|
||
// annotation’s explicit frame. Now the callout view needs to rendezvous
|
||
// with the explicit frame. Then,
|
||
// -updateUserLocationAnnotationViewAnimatedWithDuration: will take over the
|
||
// next time an updated location arrives.
|
||
[UIView animateWithDuration:_userLocationAnimationCompletionDate.timeIntervalSinceNow
|
||
delay:0
|
||
options:(UIViewAnimationOptionCurveLinear |
|
||
UIViewAnimationOptionAllowUserInteraction |
|
||
UIViewAnimationOptionBeginFromCurrentState)
|
||
animations:^
|
||
{
|
||
__typeof__(self) strongSelf = weakSelf;
|
||
if ( ! strongSelf)
|
||
{
|
||
return;
|
||
}
|
||
|
||
calloutView.frame = CGRectOffset(calloutView.frame,
|
||
strongSelf->_initialImplicitCalloutViewOffset.x,
|
||
strongSelf->_initialImplicitCalloutViewOffset.y);
|
||
strongSelf->_initialImplicitCalloutViewOffset = CGPointZero;
|
||
}
|
||
completion:NULL];
|
||
}
|
||
|
||
- (void)showAnnotations:(NSArray<id <MLNAnnotation>> *)annotations animated:(BOOL)animated
|
||
{
|
||
CGFloat maximumPadding = 100;
|
||
CGFloat yPadding = (self.frame.size.height / 5 <= maximumPadding) ? (self.frame.size.height / 5) : maximumPadding;
|
||
CGFloat xPadding = (self.frame.size.width / 5 <= maximumPadding) ? (self.frame.size.width / 5) : maximumPadding;
|
||
|
||
UIEdgeInsets edgeInsets = UIEdgeInsetsMake(yPadding, xPadding, yPadding, xPadding);
|
||
|
||
[self showAnnotations:annotations edgePadding:edgeInsets animated:animated completionHandler:nil];
|
||
}
|
||
|
||
- (void)showAnnotations:(NSArray<id <MLNAnnotation>> *)annotations edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated
|
||
{
|
||
[self showAnnotations:annotations edgePadding:insets animated:animated completionHandler:nil];
|
||
}
|
||
|
||
- (void)showAnnotations:(NSArray<id <MLNAnnotation>> *)annotations edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
MLNLogDebug(@"Showing: %lu annotations edgePadding: %@ animated: %@", annotations.count, NSStringFromUIEdgeInsets(insets), MLNStringFromBOOL(animated));
|
||
if ( ! annotations.count)
|
||
{
|
||
if (completion) {
|
||
completion();
|
||
}
|
||
return;
|
||
}
|
||
|
||
mbgl::LatLngBounds bounds = mbgl::LatLngBounds::empty();
|
||
|
||
for (id <MLNAnnotation> annotation in annotations)
|
||
{
|
||
if ([annotation conformsToProtocol:@protocol(MLNOverlay)])
|
||
{
|
||
bounds.extend(MLNLatLngBoundsFromCoordinateBounds(((id <MLNOverlay>)annotation).overlayBounds));
|
||
}
|
||
else
|
||
{
|
||
bounds.extend(MLNLatLngFromLocationCoordinate2D(annotation.coordinate));
|
||
}
|
||
}
|
||
|
||
[self setVisibleCoordinateBounds:MLNCoordinateBoundsFromLatLngBounds(bounds)
|
||
edgePadding:insets
|
||
animated:animated
|
||
completionHandler:completion];
|
||
}
|
||
|
||
|
||
// MARK: Annotation Image Delegate
|
||
|
||
- (void)annotationImageNeedsRedisplay:(MLNAnnotationImage *)annotationImage
|
||
{
|
||
NSString *reuseIdentifier = annotationImage.reuseIdentifier;
|
||
NSString *iconIdentifier = annotationImage.styleIconIdentifier;
|
||
NSString *fallbackReuseIdentifier = MLNDefaultStyleMarkerSymbolName;
|
||
NSString *fallbackIconIdentifier = [MLNAnnotationSpritePrefix stringByAppendingString:fallbackReuseIdentifier];
|
||
|
||
if (annotationImage.image)
|
||
{
|
||
// Add the new icon to the style.
|
||
NSString *updatedIconIdentifier = [MLNAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier];
|
||
annotationImage.styleIconIdentifier = updatedIconIdentifier;
|
||
[self installAnnotationImage:annotationImage];
|
||
|
||
if ([iconIdentifier isEqualToString:fallbackIconIdentifier])
|
||
{
|
||
// Update any annotations associated with the annotation image.
|
||
[self applyIconIdentifier:updatedIconIdentifier toAnnotationsWithImageReuseIdentifier:reuseIdentifier];
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Add the default icon to the style if necessary.
|
||
annotationImage.styleIconIdentifier = fallbackIconIdentifier;
|
||
if ( ! [self dequeueReusableAnnotationImageWithIdentifier:MLNDefaultStyleMarkerSymbolName])
|
||
{
|
||
[self installAnnotationImage:self.defaultAnnotationImage];
|
||
}
|
||
|
||
// Update any annotations associated with the annotation image.
|
||
[self applyIconIdentifier:fallbackIconIdentifier toAnnotationsWithImageReuseIdentifier:reuseIdentifier];
|
||
}
|
||
}
|
||
|
||
- (void)applyIconIdentifier:(NSString *)iconIdentifier toAnnotationsWithImageReuseIdentifier:(NSString *)reuseIdentifier
|
||
{
|
||
for (auto &pair : _annotationContextsByAnnotationTag)
|
||
{
|
||
if ([pair.second.imageReuseIdentifier isEqualToString:reuseIdentifier])
|
||
{
|
||
const mbgl::Point<double> point = MLNPointFromLocationCoordinate2D(pair.second.annotation.coordinate);
|
||
self.mbglMap.updateAnnotation(pair.first, mbgl::SymbolAnnotation { point, iconIdentifier.UTF8String ?: "" });
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - User Location -
|
||
|
||
- (void)setLocationManager:(nullable id<MLNLocationManager>)locationManager
|
||
{
|
||
MLNLogDebug(@"Setting locationManager: %@", locationManager);
|
||
if (!locationManager) {
|
||
locationManager = [[MLNCLLocationManager alloc] init];
|
||
}
|
||
[_locationManager stopUpdatingLocation];
|
||
[_locationManager stopUpdatingHeading];
|
||
_locationManager.delegate = nil;
|
||
|
||
_locationManager = locationManager;
|
||
_locationManager.delegate = self;
|
||
}
|
||
|
||
- (void)validateLocationServices
|
||
{
|
||
BOOL shouldEnableLocationServices = self.showsUserLocation && !self.dormant;
|
||
|
||
if (shouldEnableLocationServices)
|
||
{
|
||
if (self.shouldRequestAuthorizationToUseLocationServices && self.locationManager.authorizationStatus == kCLAuthorizationStatusNotDetermined) {
|
||
BOOL hasWhenInUseUsageDescription = !![[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"];
|
||
|
||
if (@available(iOS 11.0, *)) {
|
||
// A WhenInUse string is required in iOS 11+ and the map never has any need for Always, so it's enough to just ask for WhenInUse.
|
||
if (hasWhenInUseUsageDescription) {
|
||
[self.locationManager requestWhenInUseAuthorization];
|
||
} else {
|
||
[NSException raise:MLNMissingLocationServicesUsageDescriptionException
|
||
format:@"To use location services this app must have a NSLocationWhenInUseUsageDescription string in its Info.plist."];
|
||
}
|
||
} else {
|
||
// We might have to ask for Always if the app does not provide a WhenInUse string.
|
||
BOOL hasAlwaysUsageDescription = !![[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysUsageDescription"];
|
||
|
||
if (hasWhenInUseUsageDescription) {
|
||
[self.locationManager requestWhenInUseAuthorization];
|
||
} else if (hasAlwaysUsageDescription) {
|
||
[self.locationManager requestAlwaysAuthorization];
|
||
} else {
|
||
[NSException raise:MLNMissingLocationServicesUsageDescriptionException
|
||
format:@"To use location services this app must have a NSLocationWhenInUseUsageDescription and/or NSLocationAlwaysUsageDescription string in its Info.plist."];
|
||
}
|
||
}
|
||
}
|
||
|
||
[self.locationManager startUpdatingLocation];
|
||
|
||
[self validateUserHeadingUpdating];
|
||
}
|
||
else if ( ! shouldEnableLocationServices && self.locationManager)
|
||
{
|
||
[self.locationManager stopUpdatingLocation];
|
||
[self.locationManager stopUpdatingHeading];
|
||
}
|
||
}
|
||
|
||
- (NSString *)accuracyDescriptionString {
|
||
NSDictionary *dictionary = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationTemporaryUsageDescriptionDictionary"];
|
||
return dictionary[@"MLNAccuracyAuthorizationDescription"];
|
||
}
|
||
|
||
- (void)setShowsUserLocation:(BOOL)showsUserLocation
|
||
{
|
||
MLNLogDebug(@"Setting showsUserLocation: %@", MLNStringFromBOOL(showsUserLocation));
|
||
if (showsUserLocation == _showsUserLocation) return;
|
||
|
||
_showsUserLocation = showsUserLocation;
|
||
|
||
if (showsUserLocation)
|
||
{
|
||
if ([self.delegate respondsToSelector:@selector(mapViewWillStartLocatingUser:)])
|
||
{
|
||
[self.delegate mapViewWillStartLocatingUser:self];
|
||
}
|
||
|
||
self.userLocation = [[MLNUserLocation alloc] initWithMapView:self];
|
||
|
||
MLNUserLocationAnnotationView *userLocationAnnotationView;
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapView:viewForAnnotation:)])
|
||
{
|
||
userLocationAnnotationView = (MLNUserLocationAnnotationView *)[self.delegate mapView:self viewForAnnotation:self.userLocation];
|
||
if (userLocationAnnotationView && ! [userLocationAnnotationView isKindOfClass:MLNUserLocationAnnotationView.class])
|
||
{
|
||
[NSException raise:MLNUserLocationAnnotationTypeException
|
||
format:@"User location annotation view must be a kind of MLNUserLocationAnnotationView. %@", userLocationAnnotationView.debugDescription];
|
||
}
|
||
}
|
||
|
||
self.userLocationAnnotationView = userLocationAnnotationView ?: [[MLNFaux3DUserLocationAnnotationView alloc] init];
|
||
self.userLocationAnnotationView.mapView = self;
|
||
self.userLocationAnnotationView.userLocation = self.userLocation;
|
||
|
||
self.userLocationAnnotationView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin |
|
||
UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
|
||
|
||
[self validateLocationServices];
|
||
}
|
||
else
|
||
{
|
||
[self validateLocationServices];
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapViewDidStopLocatingUser:)])
|
||
{
|
||
[self.delegate mapViewDidStopLocatingUser:self];
|
||
}
|
||
|
||
[self setUserTrackingMode:MLNUserTrackingModeNone animated:YES completionHandler:nil];
|
||
|
||
[self.userLocationAnnotationView removeFromSuperview];
|
||
self.userLocationAnnotationView = nil;
|
||
}
|
||
}
|
||
|
||
- (void)setUserLocationAnnotationView:(MLNUserLocationAnnotationView *)newAnnotationView
|
||
{
|
||
if ( ! [newAnnotationView isEqual:_userLocationAnnotationView])
|
||
{
|
||
_userLocationAnnotationView = newAnnotationView;
|
||
[self updateUserLocationAnnotationView];
|
||
}
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingUserLocation
|
||
{
|
||
return [NSSet setWithObject:@"userLocationAnnotationView"];
|
||
}
|
||
|
||
- (BOOL)isUserLocationVisible
|
||
{
|
||
if (self.userLocationAnnotationView)
|
||
{
|
||
CGPoint locationPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self];
|
||
|
||
CGRect locationRect = CGRectMake(locationPoint.x - self.userLocation.location.horizontalAccuracy,
|
||
locationPoint.y - self.userLocation.location.horizontalAccuracy,
|
||
self.userLocation.location.horizontalAccuracy * 2,
|
||
self.userLocation.location.horizontalAccuracy * 2);
|
||
|
||
return CGRectIntersectsRect([self bounds], locationRect);
|
||
}
|
||
|
||
return NO;
|
||
}
|
||
|
||
- (void)setUserTrackingMode:(MLNUserTrackingMode)mode
|
||
{
|
||
[self setUserTrackingMode:mode animated:YES completionHandler:nil];
|
||
}
|
||
|
||
- (void)setUserTrackingMode:(MLNUserTrackingMode)mode animated:(BOOL)animated
|
||
{
|
||
[self setUserTrackingMode:mode animated:animated completionHandler:nil];
|
||
}
|
||
|
||
- (void)setUserTrackingMode:(MLNUserTrackingMode)mode animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
MLNLogDebug(@"Setting userTrackingMode: %lu animated: %@", mode, MLNStringFromBOOL(animated));
|
||
if (mode == _userTrackingMode)
|
||
{
|
||
if (completion)
|
||
{
|
||
completion();
|
||
}
|
||
return;
|
||
}
|
||
|
||
MLNUserTrackingMode oldMode = _userTrackingMode;
|
||
[self willChangeValueForKey:@"userTrackingMode"];
|
||
_userTrackingMode = mode;
|
||
[self didChangeValueForKey:@"userTrackingMode"];
|
||
|
||
switch (_userTrackingMode)
|
||
{
|
||
case MLNUserTrackingModeNone:
|
||
{
|
||
self.userTrackingState = MLNUserTrackingStatePossible;
|
||
|
||
// Immediately update the annotation view; other cases update inside
|
||
// the locationManager:didUpdateLocations: method.
|
||
[self updateUserLocationAnnotationView];
|
||
|
||
break;
|
||
}
|
||
case MLNUserTrackingModeFollow:
|
||
case MLNUserTrackingModeFollowWithCourse:
|
||
{
|
||
self.userTrackingState = animated ? MLNUserTrackingStatePossible : MLNUserTrackingStateChanged;
|
||
self.showsUserLocation = YES;
|
||
|
||
break;
|
||
}
|
||
case MLNUserTrackingModeFollowWithHeading:
|
||
{
|
||
if (oldMode == MLNUserTrackingModeNone)
|
||
{
|
||
self.userTrackingState = animated ? MLNUserTrackingStatePossible : MLNUserTrackingStateChanged;
|
||
}
|
||
|
||
self.showsUserLocation = YES;
|
||
|
||
if (self.zoomLevel < self.currentMinimumZoom)
|
||
{
|
||
[self setZoomLevel:self.currentMinimumZoom animated:YES];
|
||
}
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
CLLocation *location;
|
||
if (_userTrackingMode != MLNUserTrackingModeNone && (location = self.userLocation.location) && self.userLocationAnnotationView)
|
||
{
|
||
[self locationManager:self.locationManager didUpdateLocations:@[location] animated:animated completionHandler:completion];
|
||
}
|
||
else if (completion)
|
||
{
|
||
completion();
|
||
}
|
||
|
||
[self validateUserHeadingUpdating];
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapView:didChangeUserTrackingMode:animated:)])
|
||
{
|
||
[self.delegate mapView:self didChangeUserTrackingMode:_userTrackingMode animated:animated];
|
||
}
|
||
}
|
||
|
||
- (void)setUserLocationVerticalAlignment:(MLNAnnotationVerticalAlignment)alignment
|
||
{
|
||
[self setUserLocationVerticalAlignment:alignment animated:YES];
|
||
}
|
||
|
||
- (void)setUserLocationVerticalAlignment:(MLNAnnotationVerticalAlignment)alignment animated:(BOOL)animated
|
||
{
|
||
_userLocationVerticalAlignment = alignment;
|
||
if (self.userTrackingMode != MLNUserTrackingModeNone)
|
||
{
|
||
CLLocation *location = self.userLocation.location;
|
||
if (location)
|
||
{
|
||
[self locationManager:self.locationManager didUpdateLocations:@[location] animated:animated completionHandler:nil];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate
|
||
{
|
||
[self setTargetCoordinate:targetCoordinate animated:YES completionHandler:nil];
|
||
}
|
||
|
||
- (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate animated:(BOOL)animated
|
||
{
|
||
[self setTargetCoordinate:targetCoordinate animated:animated completionHandler:nil];
|
||
}
|
||
|
||
- (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
MLNLogDebug(@"Setting targetCoordinate: %@ animated: %@", MLNStringFromCLLocationCoordinate2D(targetCoordinate), MLNStringFromBOOL(animated));
|
||
BOOL isSynchronous = YES;
|
||
if (targetCoordinate.latitude != self.targetCoordinate.latitude
|
||
|| targetCoordinate.longitude != self.targetCoordinate.longitude)
|
||
{
|
||
_targetCoordinate = targetCoordinate;
|
||
if (self.userTrackingMode == MLNUserTrackingModeFollowWithCourse)
|
||
{
|
||
self.userTrackingState = MLNUserTrackingStatePossible;
|
||
|
||
if (CLLocation *location = self.userLocation.location)
|
||
{
|
||
isSynchronous = NO;
|
||
[self locationManager:self.locationManager didUpdateLocations:@[location] animated:animated completionHandler:completion];
|
||
}
|
||
}
|
||
}
|
||
if (isSynchronous && completion)
|
||
{
|
||
completion();
|
||
}
|
||
}
|
||
|
||
- (void)setShowsUserHeadingIndicator:(BOOL)showsUserHeadingIndicator
|
||
{
|
||
MLNLogDebug(@"Setting showsUserHeadingIndicator: %@", MLNStringFromBOOL(showsUserHeadingIndicator));
|
||
_showsUserHeadingIndicator = showsUserHeadingIndicator;
|
||
|
||
if (_showsUserHeadingIndicator)
|
||
{
|
||
self.showsUserLocation = YES;
|
||
}
|
||
[self validateUserHeadingUpdating];
|
||
}
|
||
|
||
- (void)validateUserHeadingUpdating
|
||
{
|
||
BOOL canShowPermanentHeadingIndicator = self.showsUserHeadingIndicator && self.userTrackingMode != MLNUserTrackingModeFollowWithCourse;
|
||
|
||
if (canShowPermanentHeadingIndicator || self.userTrackingMode == MLNUserTrackingModeFollowWithHeading)
|
||
{
|
||
[self updateHeadingForDeviceOrientation];
|
||
[self.locationManager startUpdatingHeading];
|
||
}
|
||
else
|
||
{
|
||
[self.locationManager stopUpdatingHeading];
|
||
}
|
||
}
|
||
|
||
- (void)locationManager:(id<MLNLocationManager>)manager didUpdateLocations:(NSArray *)locations
|
||
{
|
||
[self locationManager:manager didUpdateLocations:locations animated:YES completionHandler:nil];
|
||
}
|
||
|
||
- (void)locationManager:(__unused id<MLNLocationManager>)manager didUpdateLocations:(NSArray *)locations animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
CLLocation *oldLocation = self.userLocation.location;
|
||
CLLocation *newLocation = locations.lastObject;
|
||
_distanceFromOldUserLocation = [newLocation distanceFromLocation:oldLocation];
|
||
|
||
if ( ! _showsUserLocation || ! newLocation || ! CLLocationCoordinate2DIsValid(newLocation.coordinate)) return;
|
||
|
||
if (! oldLocation || ! CLLocationCoordinate2DIsValid(oldLocation.coordinate) || [newLocation distanceFromLocation:oldLocation]
|
||
|| oldLocation.course != newLocation.course)
|
||
{
|
||
if ( ! oldLocation || ! CLLocationCoordinate2DIsValid(oldLocation.coordinate) || self.userTrackingState != MLNUserTrackingStateBegan)
|
||
{
|
||
self.userLocation.location = newLocation;
|
||
}
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapView:didUpdateUserLocation:)])
|
||
{
|
||
[self.delegate mapView:self didUpdateUserLocation:self.userLocation];
|
||
}
|
||
}
|
||
|
||
[self didUpdateLocationWithUserTrackingAnimated:animated completionHandler:completion];
|
||
|
||
NSTimeInterval duration = MLNAnimationDuration;
|
||
if (oldLocation && ! CGPointEqualToPoint(self.userLocationAnnotationView.center, CGPointZero))
|
||
{
|
||
duration = MIN([newLocation.timestamp timeIntervalSinceDate:oldLocation.timestamp], MLNUserLocationAnimationDuration);
|
||
}
|
||
[self updateUserLocationAnnotationViewAnimatedWithDuration:duration];
|
||
|
||
if (self.userTrackingMode == MLNUserTrackingModeNone &&
|
||
self.userLocationAnnotationView.accessibilityElementIsFocused &&
|
||
[UIApplication sharedApplication].applicationState == UIApplicationStateActive)
|
||
{
|
||
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self.userLocationAnnotationView);
|
||
}
|
||
}
|
||
|
||
- (void)didUpdateLocationWithUserTrackingAnimated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
CLLocation *location = self.userLocation.location;
|
||
if ( ! _showsUserLocation || ! location
|
||
|| ! CLLocationCoordinate2DIsValid(location.coordinate)
|
||
|| self.userTrackingMode == MLNUserTrackingModeNone)
|
||
{
|
||
if (completion)
|
||
{
|
||
completion();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// If the user location annotation is already where it’s supposed to be,
|
||
// don’t change the viewport.
|
||
CGPoint correctPoint = self.userLocationAnnotationViewCenter;
|
||
CGPoint currentPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self];
|
||
if (std::abs(currentPoint.x - correctPoint.x) <= 1.0 && std::abs(currentPoint.y - correctPoint.y) <= 1.0
|
||
&& self.userTrackingMode != MLNUserTrackingModeFollowWithCourse)
|
||
{
|
||
if (completion)
|
||
{
|
||
completion();
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (self.userTrackingMode == MLNUserTrackingModeFollowWithCourse
|
||
&& CLLocationCoordinate2DIsValid(self.targetCoordinate))
|
||
{
|
||
if (self.userTrackingState != MLNUserTrackingStateBegan)
|
||
{
|
||
// Keep both the user and the destination in view.
|
||
[self didUpdateLocationWithTargetAnimated:animated completionHandler:completion];
|
||
}
|
||
}
|
||
else if (self.userTrackingState == MLNUserTrackingStatePossible)
|
||
{
|
||
// The first location update is often a great distance away from the
|
||
// current viewport, so fly there to provide additional context.
|
||
[self didUpdateLocationSignificantlyAnimated:animated completionHandler:completion];
|
||
}
|
||
else if (self.userTrackingState == MLNUserTrackingStateChanged)
|
||
{
|
||
// Subsequent updates get a more subtle animation.
|
||
[self didUpdateLocationIncrementallyAnimated:animated completionHandler:completion];
|
||
}
|
||
[self unrotateIfNeededAnimated:YES];
|
||
}
|
||
|
||
/// Changes the viewport based on an incremental location update.
|
||
- (void)didUpdateLocationIncrementallyAnimated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
[self _setCenterCoordinate:self.userLocation.location.coordinate
|
||
edgePadding:self.edgePaddingForFollowing
|
||
zoomLevel:self.zoomLevel
|
||
direction:self.directionByFollowingWithCourse
|
||
duration:animated ? MLNUserLocationAnimationDuration : 0
|
||
animationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]
|
||
completionHandler:completion];
|
||
}
|
||
|
||
/// Changes the viewport based on a significant location update, such as the
|
||
/// first location update.
|
||
- (void)didUpdateLocationSignificantlyAnimated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
|
||
if (_distanceFromOldUserLocation >= MLNDistanceThresholdForCameraPause) {
|
||
self.userTrackingState = MLNUserTrackingStateBeginSignificantTransition;
|
||
} else {
|
||
self.userTrackingState = MLNUserTrackingStateBegan;
|
||
}
|
||
|
||
MLNMapCamera *camera = self.camera;
|
||
camera.centerCoordinate = self.userLocation.location.coordinate;
|
||
camera.heading = self.directionByFollowingWithCourse;
|
||
if (self.zoomLevel < MLNMinimumZoomLevelForUserTracking)
|
||
{
|
||
camera.altitude = MLNAltitudeForZoomLevel(MLNDefaultZoomLevelForUserTracking,
|
||
camera.pitch,
|
||
camera.centerCoordinate.latitude,
|
||
self.frame.size);
|
||
}
|
||
|
||
__weak MLNMapView *weakSelf = self;
|
||
[self _flyToCamera:camera
|
||
edgePadding:self.edgePaddingForFollowing
|
||
withDuration:animated ? -1 : 0
|
||
peakAltitude:-1
|
||
completionHandler:^{
|
||
MLNMapView *strongSelf = weakSelf;
|
||
if (strongSelf.userTrackingState == MLNUserTrackingStateBegan ||
|
||
strongSelf.userTrackingState == MLNDistanceThresholdForCameraPause)
|
||
{
|
||
strongSelf.userTrackingState = MLNUserTrackingStateChanged;
|
||
}
|
||
if (completion)
|
||
{
|
||
completion();
|
||
}
|
||
}];
|
||
}
|
||
|
||
/// Changes the viewport based on a location update in the presence of a target
|
||
/// coordinate that must also be displayed on the map concurrently.
|
||
- (void)didUpdateLocationWithTargetAnimated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
|
||
{
|
||
BOOL firstUpdate = self.userTrackingState == MLNUserTrackingStatePossible;
|
||
void (^animationCompletion)(void);
|
||
if (animated && firstUpdate)
|
||
{
|
||
self.userTrackingState = MLNUserTrackingStateBegan;
|
||
__weak MLNMapView *weakSelf = self;
|
||
animationCompletion = ^{
|
||
MLNMapView *strongSelf = weakSelf;
|
||
if (strongSelf.userTrackingState == MLNUserTrackingStateBegan)
|
||
{
|
||
strongSelf.userTrackingState = MLNUserTrackingStateChanged;
|
||
}
|
||
if (completion)
|
||
{
|
||
completion();
|
||
}
|
||
};
|
||
}
|
||
|
||
CLLocationCoordinate2D foci[] = {
|
||
self.userLocation.location.coordinate,
|
||
self.targetCoordinate,
|
||
};
|
||
UIEdgeInsets inset = self.edgePaddingForFollowingWithCourse;
|
||
|
||
#pragma clang diagnostic push
|
||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||
if (self.userLocationVerticalAlignment == MLNAnnotationVerticalAlignmentCenter)
|
||
{
|
||
inset.bottom = CGRectGetMaxY(self.bounds) - CGRectGetMidY(self.contentFrame);
|
||
}
|
||
#pragma clang diagnostic pop
|
||
|
||
[self _setVisibleCoordinates:foci
|
||
count:sizeof(foci) / sizeof(foci[0])
|
||
edgePadding:inset
|
||
direction:self.directionByFollowingWithCourse
|
||
duration:animated ? MLNUserLocationAnimationDuration : 0
|
||
animationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]
|
||
completionHandler:animationCompletion];
|
||
}
|
||
|
||
/// Returns the edge padding to apply when moving the map to a tracked location.
|
||
- (UIEdgeInsets)edgePaddingForFollowing
|
||
{
|
||
// Center on user location unless we're already centered there (or very close).
|
||
CGPoint correctPoint = self.userLocationAnnotationViewCenter;
|
||
|
||
// Shift the entire frame upward or downward to accommodate a shifted user
|
||
// location annotation view.
|
||
CGRect bounds = self.bounds;
|
||
CGRect boundsAroundCorrectPoint = CGRectOffset(bounds,
|
||
correctPoint.x - CGRectGetMidX(bounds),
|
||
correctPoint.y - CGRectGetMidY(bounds));
|
||
return UIEdgeInsetsMake(CGRectGetMinY(boundsAroundCorrectPoint) - CGRectGetMinY(bounds),
|
||
CGRectGetMaxX(boundsAroundCorrectPoint) - CGRectGetMaxX(bounds),
|
||
CGRectGetMaxY(bounds) - CGRectGetMaxY(boundsAroundCorrectPoint),
|
||
CGRectGetMaxX(bounds) - CGRectGetMaxX(boundsAroundCorrectPoint));
|
||
}
|
||
|
||
/// Returns the edge padding to apply during bifocal course tracking.
|
||
- (UIEdgeInsets)edgePaddingForFollowingWithCourse
|
||
{
|
||
UIEdgeInsets inset = MLNUserLocationAnnotationViewInset;
|
||
inset.top += CGRectGetHeight(self.userLocationAnnotationView.frame);
|
||
inset.bottom += CGRectGetHeight(self.userLocationAnnotationView.frame);
|
||
return inset;
|
||
}
|
||
|
||
/// Returns the direction the map should be turned to due to course tracking.
|
||
- (CLLocationDirection)directionByFollowingWithCourse
|
||
{
|
||
CLLocationDirection direction = -1;
|
||
if (self.userTrackingMode == MLNUserTrackingModeFollowWithCourse)
|
||
{
|
||
if (CLLocationCoordinate2DIsValid(self.targetCoordinate))
|
||
{
|
||
mbgl::LatLng userLatLng = MLNLatLngFromLocationCoordinate2D(self.userLocation.coordinate);
|
||
mbgl::LatLng targetLatLng = MLNLatLngFromLocationCoordinate2D(self.targetCoordinate);
|
||
mbgl::ProjectedMeters userMeters = mbgl::Projection::projectedMetersForLatLng(userLatLng);
|
||
mbgl::ProjectedMeters targetMeters = mbgl::Projection::projectedMetersForLatLng(targetLatLng);
|
||
double angle = atan2(targetMeters.easting() - userMeters.easting(),
|
||
targetMeters.northing() - userMeters.northing());
|
||
direction = mbgl::util::wrap(MLNDegreesFromRadians(angle), 0., 360.);
|
||
}
|
||
else
|
||
{
|
||
direction = self.userLocation.location.course;
|
||
}
|
||
|
||
if (direction >= 0)
|
||
{
|
||
#pragma clang diagnostic push
|
||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||
if (self.userLocationVerticalAlignment == MLNAnnotationVerticalAlignmentTop)
|
||
{
|
||
direction += 180;
|
||
}
|
||
#pragma clang diagnostic pop
|
||
}
|
||
}
|
||
return direction;
|
||
}
|
||
|
||
- (BOOL)locationManagerShouldDisplayHeadingCalibration:(id<MLNLocationManager>)manager
|
||
{
|
||
if (self.displayHeadingCalibration) [self performSelector:@selector(dismissHeadingCalibrationDisplay:)
|
||
withObject:manager
|
||
afterDelay:10.0];
|
||
|
||
return self.displayHeadingCalibration;
|
||
}
|
||
|
||
- (void)dismissHeadingCalibrationDisplay:(id<MLNLocationManager>)manager
|
||
{
|
||
[manager dismissHeadingCalibrationDisplay];
|
||
}
|
||
|
||
- (void)locationManager:(__unused id<MLNLocationManager>)manager didUpdateHeading:(CLHeading *)newHeading
|
||
{
|
||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||
if ( ! self->_showsUserLocation || self.pan.state == UIGestureRecognizerStateBegan || newHeading.headingAccuracy < 0) return;
|
||
|
||
self.userLocation.heading = newHeading;
|
||
|
||
if (self.showsUserHeadingIndicator || self.userTrackingMode == MLNUserTrackingModeFollowWithHeading)
|
||
{
|
||
[self updateUserLocationAnnotationView];
|
||
}
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapView:didUpdateUserLocation:)])
|
||
{
|
||
[self.delegate mapView:self didUpdateUserLocation:self.userLocation];
|
||
|
||
if (!self->_showsUserLocation) return;
|
||
}
|
||
|
||
CLLocationDirection headingDirection = (newHeading.trueHeading >= 0 ? newHeading.trueHeading : newHeading.magneticHeading);
|
||
|
||
if (headingDirection >= 0 && self.userTrackingMode == MLNUserTrackingModeFollowWithHeading
|
||
&& self.userTrackingState != MLNUserTrackingStateBegan)
|
||
{
|
||
[self _setDirection:headingDirection animated:YES];
|
||
[self updateUserLocationAnnotationView];
|
||
}
|
||
});
|
||
}
|
||
|
||
- (void)locationManager:(__unused id<MLNLocationManager>)manager didFailWithError:(NSError *)error
|
||
{
|
||
if ([error code] == kCLErrorDenied)
|
||
{
|
||
self.userTrackingMode = MLNUserTrackingModeNone;
|
||
self.showsUserLocation = NO;
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapView:didFailToLocateUserWithError:)])
|
||
{
|
||
[self.delegate mapView:self didFailToLocateUserWithError:error];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)locationManagerDidChangeAuthorization:(id<MLNLocationManager>)manager
|
||
{
|
||
if (![self shouldShowLocationDotBasedOnCurrentLocationPermissions])
|
||
{
|
||
[self.userLocationAnnotationView removeFromSuperview];
|
||
[self.locationManager stopUpdatingLocation];
|
||
[self.locationManager stopUpdatingHeading];
|
||
} else {
|
||
if (@available(iOS 14, *)) {
|
||
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
|
||
if (self.userTrackingMode != MLNUserTrackingModeNone &&
|
||
[manager respondsToSelector:@selector(authorizationStatus)] &&
|
||
(manager.authorizationStatus != kCLAuthorizationStatusRestricted ||
|
||
manager.authorizationStatus != kCLAuthorizationStatusAuthorizedAlways ||
|
||
manager.authorizationStatus != kCLAuthorizationStatusAuthorizedWhenInUse) &&
|
||
[manager respondsToSelector:@selector(accuracyAuthorization)] &&
|
||
manager.accuracyAuthorization == CLAccuracyAuthorizationReducedAccuracy &&
|
||
[self accuracyDescriptionString] != nil ) {
|
||
[self.locationManager requestTemporaryFullAccuracyAuthorizationWithPurposeKey:@"MLNAccuracyAuthorizationDescription"];
|
||
} else {
|
||
[self validateLocationServices];
|
||
}
|
||
#endif
|
||
} else {
|
||
[self validateLocationServices];
|
||
}
|
||
}
|
||
|
||
if (@available(iOS 14, *)) {
|
||
if ([self.delegate respondsToSelector:@selector(mapView:didChangeLocationManagerAuthorization:)]) {
|
||
[self.delegate mapView:self didChangeLocationManagerAuthorization:manager];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (BOOL)shouldShowLocationDotBasedOnCurrentLocationPermissions
|
||
{
|
||
return self.locationManager && (self.locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedAlways
|
||
|| self.locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse);
|
||
}
|
||
|
||
- (void)updateHeadingForDeviceOrientation
|
||
{
|
||
if (self.locationManager)
|
||
{
|
||
// note that right/left device and interface orientations are opposites (see UIApplication.h)
|
||
//
|
||
CLDeviceOrientation orientation;
|
||
switch ([[UIApplication sharedApplication] statusBarOrientation])
|
||
{
|
||
case (UIInterfaceOrientationLandscapeLeft):
|
||
{
|
||
orientation = CLDeviceOrientationLandscapeRight;
|
||
break;
|
||
}
|
||
case (UIInterfaceOrientationLandscapeRight):
|
||
{
|
||
orientation = CLDeviceOrientationLandscapeLeft;
|
||
break;
|
||
}
|
||
case (UIInterfaceOrientationPortraitUpsideDown):
|
||
{
|
||
orientation = CLDeviceOrientationPortraitUpsideDown;
|
||
break;
|
||
}
|
||
case (UIInterfaceOrientationPortrait):
|
||
default:
|
||
{
|
||
orientation = CLDeviceOrientationPortrait;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Setting the location manager's heading orientation causes it to send
|
||
// a heading event, which in turn makes us redraw, which kicks off a
|
||
// loop... so don't do that. rdar://34059173
|
||
if (self.locationManager.headingOrientation != orientation)
|
||
{
|
||
self.locationManager.headingOrientation = orientation;
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: Data
|
||
|
||
- (NSArray<id <MLNFeature>> *)visibleFeaturesAtPoint:(CGPoint)point
|
||
{
|
||
MLNLogDebug(@"Querying visibleFeaturesAtPoint: %@", NSStringFromCGPoint(point));
|
||
return [self visibleFeaturesAtPoint:point inStyleLayersWithIdentifiers:nil];
|
||
}
|
||
|
||
- (NSArray<id <MLNFeature>> *)visibleFeaturesAtPoint:(CGPoint)point inStyleLayersWithIdentifiers:(NSSet<NSString *> *)styleLayerIdentifiers {
|
||
MLNLogDebug(@"Querying visibleFeaturesAtPoint: %@ inStyleLayersWithIdentifiers: %@", NSStringFromCGPoint(point), styleLayerIdentifiers);
|
||
return [self visibleFeaturesAtPoint:point inStyleLayersWithIdentifiers:styleLayerIdentifiers predicate:nil];
|
||
}
|
||
|
||
- (NSArray<id <MLNFeature>> *)visibleFeaturesAtPoint:(CGPoint)point inStyleLayersWithIdentifiers:(NSSet<NSString *> *)styleLayerIdentifiers predicate:(NSPredicate *)predicate
|
||
{
|
||
MLNLogDebug(@"Querying visibleFeaturesAtPoint: %@ inStyleLayersWithIdentifiers: %@ predicate: %@", NSStringFromCGPoint(point), styleLayerIdentifiers, predicate);
|
||
mbgl::ScreenCoordinate screenCoordinate = { point.x, point.y };
|
||
|
||
std::optional<std::vector<std::string>> optionalLayerIDs;
|
||
if (styleLayerIdentifiers)
|
||
{
|
||
__block std::vector<std::string> layerIDs;
|
||
layerIDs.reserve(styleLayerIdentifiers.count);
|
||
[styleLayerIdentifiers enumerateObjectsUsingBlock:^(NSString * _Nonnull identifier, BOOL * _Nonnull stop)
|
||
{
|
||
layerIDs.push_back(identifier.UTF8String);
|
||
}];
|
||
optionalLayerIDs = layerIDs;
|
||
}
|
||
|
||
std::optional<mbgl::style::Filter> optionalFilter;
|
||
if (predicate) {
|
||
optionalFilter = predicate.mgl_filter;
|
||
}
|
||
|
||
std::vector<mbgl::Feature> features = _rendererFrontend->getRenderer()->queryRenderedFeatures(screenCoordinate, { optionalLayerIDs, optionalFilter });
|
||
return MLNFeaturesFromMBGLFeatures(features);
|
||
}
|
||
|
||
- (NSArray<id <MLNFeature>> *)visibleFeaturesInRect:(CGRect)rect {
|
||
MLNLogDebug(@"Querying visibleFeaturesInRect: %@", NSStringFromCGRect(rect));
|
||
return [self visibleFeaturesInRect:rect inStyleLayersWithIdentifiers:nil];
|
||
}
|
||
|
||
- (NSArray<id <MLNFeature>> *)visibleFeaturesInRect:(CGRect)rect inStyleLayersWithIdentifiers:(NSSet<NSString *> *)styleLayerIdentifiers {
|
||
MLNLogDebug(@"Querying visibleFeaturesInRect: %@ inStyleLayersWithIdentifiers: %@", NSStringFromCGRect(rect), styleLayerIdentifiers);
|
||
return [self visibleFeaturesInRect:rect inStyleLayersWithIdentifiers:styleLayerIdentifiers predicate:nil];
|
||
}
|
||
|
||
- (NSArray<id <MLNFeature>> *)visibleFeaturesInRect:(CGRect)rect inStyleLayersWithIdentifiers:(NSSet<NSString *> *)styleLayerIdentifiers predicate:(NSPredicate *)predicate {
|
||
MLNLogDebug(@"Querying visibleFeaturesInRect: %@ inStyleLayersWithIdentifiers: %@ predicate: %@", NSStringFromCGRect(rect), styleLayerIdentifiers, predicate);
|
||
mbgl::ScreenBox screenBox = {
|
||
{ CGRectGetMinX(rect), CGRectGetMinY(rect) },
|
||
{ CGRectGetMaxX(rect), CGRectGetMaxY(rect) },
|
||
};
|
||
|
||
std::optional<std::vector<std::string>> optionalLayerIDs;
|
||
if (styleLayerIdentifiers) {
|
||
__block std::vector<std::string> layerIDs;
|
||
layerIDs.reserve(styleLayerIdentifiers.count);
|
||
[styleLayerIdentifiers enumerateObjectsUsingBlock:^(NSString * _Nonnull identifier, BOOL * _Nonnull stop) {
|
||
layerIDs.push_back(identifier.UTF8String);
|
||
}];
|
||
optionalLayerIDs = layerIDs;
|
||
}
|
||
|
||
std::optional<mbgl::style::Filter> optionalFilter;
|
||
if (predicate) {
|
||
optionalFilter = predicate.mgl_filter;
|
||
}
|
||
|
||
std::vector<mbgl::Feature> features = _rendererFrontend->getRenderer()->queryRenderedFeatures(screenBox, { optionalLayerIDs, optionalFilter });
|
||
return MLNFeaturesFromMBGLFeatures(features);
|
||
}
|
||
|
||
// MARK: - Utility -
|
||
|
||
- (void)animateWithDelay:(NSTimeInterval)delay animations:(void (^)(void))animations
|
||
{
|
||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), animations);
|
||
}
|
||
|
||
- (CGFloat)currentMinimumZoom
|
||
{
|
||
return fmaxf(*self.mbglMap.getBounds().minZoom, MLNMinimumZoom);
|
||
}
|
||
|
||
- (BOOL)isRotationAllowed
|
||
{
|
||
return (self.zoomLevel >= self.currentMinimumZoom);
|
||
}
|
||
|
||
- (void)unrotateIfNeededForGesture
|
||
{
|
||
// Avoid contention with in-progress gestures.
|
||
UIGestureRecognizerState state = self.pinch.state;
|
||
if (self.direction != 0
|
||
&& state != UIGestureRecognizerStateBegan
|
||
&& state != UIGestureRecognizerStateChanged)
|
||
{
|
||
[self unrotateIfNeededAnimated:YES];
|
||
|
||
// Snap to north.
|
||
if ((self.direction < MLNToleranceForSnappingToNorth
|
||
|| self.direction > 360 - MLNToleranceForSnappingToNorth)
|
||
&& self.userTrackingMode != MLNUserTrackingModeFollowWithHeading
|
||
&& self.userTrackingMode != MLNUserTrackingModeFollowWithCourse)
|
||
{
|
||
[self resetNorthAnimated:YES];
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Rotate back to true north if the map view is zoomed too far out.
|
||
- (void)unrotateIfNeededAnimated:(BOOL)animated
|
||
{
|
||
if (self.direction != 0 && ! self.isRotationAllowed
|
||
&& self.userTrackingState != MLNUserTrackingStateBegan)
|
||
{
|
||
if (animated)
|
||
{
|
||
self.userInteractionEnabled = NO;
|
||
|
||
__weak MLNMapView *weakSelf = self;
|
||
|
||
[self animateWithDelay:0.1 animations:^
|
||
{
|
||
[weakSelf resetNorthAnimated:YES];
|
||
|
||
[self animateWithDelay:MLNAnimationDuration animations:^
|
||
{
|
||
weakSelf.userInteractionEnabled = YES;
|
||
}];
|
||
|
||
}];
|
||
}
|
||
else
|
||
{
|
||
[self resetNorthAnimated:NO];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)cameraWillChangeAnimated:(BOOL)animated {
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if ( ! _userLocationAnnotationIsSelected
|
||
|| self.userTrackingMode == MLNUserTrackingModeNone
|
||
|| self.userTrackingState != MLNUserTrackingStateChanged)
|
||
{
|
||
UIView<MLNCalloutView> *calloutView = self.calloutViewForSelectedAnnotation;
|
||
BOOL dismissesAutomatically = (calloutView
|
||
&& [calloutView respondsToSelector:@selector(dismissesAutomatically)]
|
||
&& calloutView.dismissesAutomatically);
|
||
// dismissesAutomatically is an optional property and we want to dismiss
|
||
// the callout view if it's unimplemented.
|
||
if (dismissesAutomatically || (calloutView && ![calloutView respondsToSelector:@selector(dismissesAutomatically)]))
|
||
{
|
||
[self deselectAnnotation:self.selectedAnnotation animated:NO];
|
||
}
|
||
}
|
||
|
||
if ( ! [self isSuppressingChangeDelimiters] )
|
||
{
|
||
if ([self.delegate respondsToSelector:@selector(mapView:regionWillChangeWithReason:animated:)])
|
||
{
|
||
[self.delegate mapView:self regionWillChangeWithReason:self.cameraChangeReasonBitmask animated:animated];
|
||
}
|
||
else if ([self.delegate respondsToSelector:@selector(mapView:regionWillChangeAnimated:)])
|
||
{
|
||
[self.delegate mapView:self regionWillChangeAnimated:animated];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)cameraIsChanging {
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
|
||
[self updateCompass];
|
||
[self updateScaleBar];
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapView:regionIsChangingWithReason:)])
|
||
{
|
||
[self.delegate mapView:self regionIsChangingWithReason:self.cameraChangeReasonBitmask];
|
||
}
|
||
else if ([self.delegate respondsToSelector:@selector(mapViewRegionIsChanging:)])
|
||
{
|
||
[self.delegate mapViewRegionIsChanging:self];
|
||
}
|
||
}
|
||
|
||
- (void)cameraDidChangeAnimated:(BOOL)animated {
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
|
||
[self updateCompass];
|
||
[self updateScaleBar];
|
||
|
||
if ( ! [self isSuppressingChangeDelimiters])
|
||
{
|
||
BOOL respondsToSelector = [self.delegate respondsToSelector:@selector(mapView:regionDidChangeAnimated:)];
|
||
BOOL respondsToSelectorWithReason = [self.delegate respondsToSelector:@selector(mapView:regionDidChangeWithReason:animated:)];
|
||
|
||
if ((respondsToSelector || respondsToSelectorWithReason) &&
|
||
([UIApplication sharedApplication].applicationState == UIApplicationStateActive))
|
||
{
|
||
_featureAccessibilityElements = nil;
|
||
_visiblePlaceFeatures = nil;
|
||
_visibleRoadFeatures = nil;
|
||
if (_accessibilityValueAnnouncementIsPending) {
|
||
_accessibilityValueAnnouncementIsPending = NO;
|
||
[self performSelector:@selector(announceAccessibilityValue) withObject:nil afterDelay:0.1];
|
||
} else {
|
||
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
|
||
}
|
||
}
|
||
|
||
if (respondsToSelectorWithReason)
|
||
{
|
||
[self.delegate mapView:self regionDidChangeWithReason:self.cameraChangeReasonBitmask animated:animated];
|
||
}
|
||
else if (respondsToSelector)
|
||
{
|
||
[self.delegate mapView:self regionDidChangeAnimated:animated];
|
||
}
|
||
|
||
[self resetCameraChangeReason];
|
||
}
|
||
}
|
||
|
||
- (void)announceAccessibilityValue
|
||
{
|
||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.accessibilityValue);
|
||
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
|
||
}
|
||
|
||
- (void)mapViewWillStartLoadingMap {
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapViewWillStartLoadingMap:)])
|
||
{
|
||
[self.delegate mapViewWillStartLoadingMap:self];
|
||
}
|
||
}
|
||
|
||
- (void)mapViewDidFinishLoadingMap {
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
|
||
[self.style willChangeValueForKey:@"sources"];
|
||
[self.style didChangeValueForKey:@"sources"];
|
||
[self.style willChangeValueForKey:@"layers"];
|
||
[self.style didChangeValueForKey:@"layers"];
|
||
if ([self.delegate respondsToSelector:@selector(mapViewDidFinishLoadingMap:)])
|
||
{
|
||
[self.delegate mapViewDidFinishLoadingMap:self];
|
||
}
|
||
}
|
||
|
||
- (void)mapViewDidFailLoadingMapWithError:(NSError *)error {
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapViewDidFailLoadingMap:withError:)])
|
||
{
|
||
[self.delegate mapViewDidFailLoadingMap:self withError:error];
|
||
}
|
||
}
|
||
|
||
- (void)mapViewWillStartRenderingFrame {
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapViewWillStartRenderingFrame:)])
|
||
{
|
||
[self.delegate mapViewWillStartRenderingFrame:self];
|
||
}
|
||
}
|
||
|
||
- (void)mapViewDidFinishRenderingFrameFullyRendered:(BOOL)fullyRendered
|
||
frameEncodingTime:(double)frameEncodingTime
|
||
frameRenderingTime:(double)frameRenderingTime {
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (_isChangingAnnotationLayers)
|
||
{
|
||
_isChangingAnnotationLayers = NO;
|
||
[self.style didChangeValueForKey:@"layers"];
|
||
}
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingFrame:fullyRendered:frameEncodingTime:frameRenderingTime:)])
|
||
{
|
||
[self.delegate mapViewDidFinishRenderingFrame:self fullyRendered:fullyRendered frameEncodingTime:frameEncodingTime frameRenderingTime:frameRenderingTime];
|
||
}
|
||
else if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingFrame:fullyRendered:)])
|
||
{
|
||
[self.delegate mapViewDidFinishRenderingFrame:self fullyRendered:fullyRendered];
|
||
}
|
||
}
|
||
|
||
- (void)mapViewWillStartRenderingMap {
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapViewWillStartRenderingMap:)])
|
||
{
|
||
[self.delegate mapViewWillStartRenderingMap:self];
|
||
}
|
||
}
|
||
|
||
- (void)mapViewDidFinishRenderingMapFullyRendered:(BOOL)fullyRendered {
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingMap:fullyRendered:)])
|
||
{
|
||
[self.delegate mapViewDidFinishRenderingMap:self fullyRendered:fullyRendered];
|
||
}
|
||
}
|
||
|
||
- (void)mapViewDidBecomeIdle {
|
||
if (!_mbglMap) {
|
||
return;
|
||
}
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapViewDidBecomeIdle:)]) {
|
||
[self.delegate mapViewDidBecomeIdle:self];
|
||
}
|
||
}
|
||
|
||
- (void)mapViewDidFinishLoadingStyle {
|
||
if (!_mbglMap)
|
||
{
|
||
return;
|
||
}
|
||
|
||
self.style = [[MLNStyle alloc] initWithRawStyle:&self.mbglMap.getStyle() stylable:self];
|
||
if ([self.delegate respondsToSelector:@selector(mapView:didFinishLoadingStyle:)])
|
||
{
|
||
[self.delegate mapView:self didFinishLoadingStyle:self.style];
|
||
}
|
||
}
|
||
|
||
- (void)sourceDidChange:(MLNSource *)source {
|
||
// no-op: we only show attribution after tapping the info button, so there's no
|
||
// interactive update needed.
|
||
}
|
||
|
||
- (void)didFailToLoadImage:(NSString *)imageName {
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapView:didFailToLoadImage:)]) {
|
||
MLNImage *imageToLoad = [self.delegate mapView:self didFailToLoadImage:imageName];
|
||
if (imageToLoad) {
|
||
auto image = [imageToLoad mgl_styleImageWithIdentifier:imageName];
|
||
self.mbglMap.getStyle().addImage(std::move(image));
|
||
}
|
||
}
|
||
}
|
||
|
||
- (BOOL)shouldRemoveStyleImage:(NSString *)imageName {
|
||
if ([self.delegate respondsToSelector:@selector(mapView:shouldRemoveStyleImage:)]) {
|
||
return [self.delegate mapView:self shouldRemoveStyleImage:imageName];
|
||
}
|
||
|
||
return YES;
|
||
}
|
||
|
||
- (void)updateUserLocationAnnotationView
|
||
{
|
||
[self updateUserLocationAnnotationViewAnimatedWithDuration:0];
|
||
}
|
||
|
||
- (void)updateAnnotationViews
|
||
{
|
||
BOOL delegateImplementsViewForAnnotation = [self.delegate respondsToSelector:@selector(mapView:viewForAnnotation:)];
|
||
|
||
if (!delegateImplementsViewForAnnotation)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// If the map is pitched consider the viewport to be exactly the same as the bounds.
|
||
// Otherwise, add a small buffer.
|
||
CGFloat largestWidth = MAX(_largestAnnotationViewSize.width, CGRectGetWidth(self.frame));
|
||
CGFloat largestHeight = MAX(_largestAnnotationViewSize.height, CGRectGetHeight(self.frame));
|
||
CGFloat widthAdjustment = self.camera.pitch > 0.0 ? 0.0 : -largestWidth * 2.0;
|
||
CGFloat heightAdjustment = self.camera.pitch > 0.0 ? 0.0 : -largestHeight * 2.0;
|
||
CGRect viewPort = CGRectInset(self.bounds, widthAdjustment, heightAdjustment);
|
||
|
||
NSArray *visibleAnnotations = [self visibleAnnotationsInRect:viewPort];
|
||
NSMutableArray *offscreenAnnotations = [self.annotations mutableCopy];
|
||
[offscreenAnnotations removeObjectsInArray:visibleAnnotations];
|
||
|
||
// Update the center of visible annotation views
|
||
for (id<MLNAnnotation> annotation in visibleAnnotations)
|
||
{
|
||
// Defer to the shape/polygon styling delegate methods
|
||
if ([annotation isKindOfClass:[MLNMultiPoint class]])
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// Get the annotation tag then use it to get the context.
|
||
MLNAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
|
||
MLNAssert(annotationTag != MLNAnnotationTagNotFound, @"-visibleAnnotationsInRect: returned unrecognized annotation");
|
||
MLNAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
|
||
|
||
MLNAnnotationView *annotationView = annotationContext.annotationView;
|
||
if (!annotationView)
|
||
{
|
||
// This will dequeue views if the delegate implements the dequeue call
|
||
annotationView = [self annotationViewForAnnotation:annotationContext.annotation];
|
||
|
||
if (annotationView)
|
||
{
|
||
annotationView.mapView = self;
|
||
annotationContext.annotationView = annotationView;
|
||
|
||
// New annotation (created because there is nothing to dequeue) may not have been added to the
|
||
// container view yet. Add them here.
|
||
if (!annotationView.superview) {
|
||
[self.annotationContainerView insertSubview:annotationView atIndex:0];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (annotationView)
|
||
{
|
||
annotationView.center = MLNPointRounded([self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self]);
|
||
}
|
||
}
|
||
|
||
MLNCoordinateBounds coordinateBounds = [self convertRect:viewPort toCoordinateBoundsFromView:self];
|
||
|
||
// Enqueue (and move if required) offscreen annotation views
|
||
for (id<MLNAnnotation> annotation in offscreenAnnotations)
|
||
{
|
||
// Defer to the shape/polygon styling delegate methods
|
||
if ([annotation isKindOfClass:[MLNMultiPoint class]])
|
||
{
|
||
continue;
|
||
}
|
||
|
||
MLNAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
|
||
MLNAssert(annotationTag != MLNAnnotationTagNotFound, @"-visibleAnnotationsInRect: returned unrecognized annotation");
|
||
MLNAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
|
||
UIView *annotationView = annotationContext.annotationView;
|
||
|
||
if (annotationView)
|
||
{
|
||
CLLocationCoordinate2D coordinate = annotation.coordinate;
|
||
|
||
// Every so often (1 out of 1000 frames?) the mbgl query mechanism fails. This logic spot checks the
|
||
// offscreenAnnotations values -- if they are actually still on screen then the view center is
|
||
// moved and the enqueue operation is avoided. This allows us to keep the performance benefit of
|
||
// using the mbgl query result. It also forces views that have just gone offscreen to be cleared
|
||
// fully from view.
|
||
if (MLNCoordinateInCoordinateBounds(coordinate, coordinateBounds))
|
||
{
|
||
annotationView.center = [self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self];
|
||
}
|
||
else
|
||
{
|
||
if (annotationView.layer.animationKeys.count > 0) {
|
||
continue;
|
||
}
|
||
|
||
// Move the annotation view far out of view to the left
|
||
CGPoint adjustedCenter = annotationView.center;
|
||
adjustedCenter.x = -CGRectGetWidth(self.frame) * 10.0;
|
||
annotationView.center = adjustedCenter;
|
||
|
||
[self enqueueAnnotationViewForAnnotationContext:annotationContext];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
- (BOOL)hasAnAnchoredAnnotationCalloutView
|
||
{
|
||
// TODO: Remove duplicate code.
|
||
UIView <MLNCalloutView> *calloutView = self.calloutViewForSelectedAnnotation;
|
||
id <MLNAnnotation> annotation = calloutView.representedObject;
|
||
|
||
BOOL isAnchoredToAnnotation = (calloutView
|
||
&& annotation
|
||
&& [calloutView respondsToSelector:@selector(isAnchoredToAnnotation)]
|
||
&& calloutView.isAnchoredToAnnotation);
|
||
return isAnchoredToAnnotation;
|
||
}
|
||
|
||
- (void)updateCalloutView
|
||
{
|
||
UIView <MLNCalloutView> *calloutView = self.calloutViewForSelectedAnnotation;
|
||
id <MLNAnnotation> annotation = calloutView.representedObject;
|
||
|
||
BOOL isAnchoredToAnnotation = (calloutView
|
||
&& annotation
|
||
&& [calloutView respondsToSelector:@selector(isAnchoredToAnnotation)]
|
||
&& calloutView.isAnchoredToAnnotation);
|
||
|
||
if (isAnchoredToAnnotation)
|
||
{
|
||
MLNAnnotationTag tag = [self annotationTagForAnnotation:annotation];
|
||
MLNAnnotationView *annotationView = nil;
|
||
|
||
if (tag != MLNAnnotationTagNotFound) {
|
||
MLNAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(tag);
|
||
annotationView = annotationContext.annotationView;
|
||
} else if (annotation == self.userLocation) {
|
||
annotationView = self.userLocationAnnotationView;
|
||
}
|
||
|
||
CGRect positioningRect = annotationView ?
|
||
annotationView.frame :
|
||
[self positioningRectForCalloutForAnnotationWithTag:tag];
|
||
|
||
MLNAssert( ! CGRectIsNull(positioningRect), @"Positioning rect should not be CGRectNull by this point");
|
||
|
||
CGPoint centerPoint = CGPointMake(CGRectGetMidX(positioningRect), CGRectGetMinY(positioningRect));
|
||
|
||
if ( ! CGPointEqualToPoint(calloutView.center, centerPoint)) {
|
||
calloutView.center = centerPoint;
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)updateAttributionAlertView {
|
||
if (self.attributionController.presentingViewController) {
|
||
self.attributionController.popoverPresentationController.sourceRect = self.attributionButton.frame;
|
||
switch (self.attributionButtonPosition) {
|
||
case MLNOrnamentPositionTopLeft:
|
||
case MLNOrnamentPositionTopRight:
|
||
[self.attributionController.popoverPresentationController setPermittedArrowDirections:UIMenuControllerArrowUp];
|
||
break;
|
||
case MLNOrnamentPositionBottomLeft:
|
||
case MLNOrnamentPositionBottomRight:
|
||
[self.attributionController.popoverPresentationController setPermittedArrowDirections:UIMenuControllerArrowDown];
|
||
break;
|
||
}
|
||
[self.attributionController.popoverPresentationController.containerView setNeedsLayout];
|
||
}
|
||
}
|
||
|
||
- (void)enqueueAnnotationViewForAnnotationContext:(MLNAnnotationContext &)annotationContext
|
||
{
|
||
MLNAnnotationView *annotationView = annotationContext.annotationView;
|
||
|
||
if (!annotationView) return;
|
||
|
||
if (annotationContext.viewReuseIdentifier)
|
||
{
|
||
annotationView.annotation = nil;
|
||
NSMutableArray *annotationViewReuseQueue = [self annotationViewReuseQueueForIdentifier:annotationContext.viewReuseIdentifier];
|
||
if (![annotationViewReuseQueue containsObject:annotationView])
|
||
{
|
||
[annotationViewReuseQueue addObject:annotationView];
|
||
annotationContext.annotationView = nil;
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)updateUserLocationAnnotationViewAnimatedWithDuration:(NSTimeInterval)duration
|
||
{
|
||
MLNUserLocationAnnotationView *annotationView = self.userLocationAnnotationView;
|
||
if ( ! CLLocationCoordinate2DIsValid(self.userLocation.coordinate)) {
|
||
annotationView.hidden = YES;
|
||
return;
|
||
}
|
||
|
||
CGPoint userPoint;
|
||
if (self.userTrackingMode != MLNUserTrackingModeNone
|
||
&& self.userTrackingState == MLNUserTrackingStateChanged)
|
||
{
|
||
userPoint = self.userLocationAnnotationViewCenter;
|
||
}
|
||
else
|
||
{
|
||
userPoint = MLNPointRounded([self convertCoordinate:self.userLocation.coordinate toPointToView:self]);
|
||
}
|
||
|
||
if ( ! annotationView.superview)
|
||
{
|
||
[_mbglView->getView() addSubview:annotationView];
|
||
// Prevents the view from sliding in from the origin.
|
||
annotationView.center = userPoint;
|
||
}
|
||
|
||
if (CGRectContainsPoint(CGRectInset(self.bounds, -MLNAnnotationUpdateViewportOutset.width,
|
||
-MLNAnnotationUpdateViewportOutset.height), userPoint))
|
||
{
|
||
// Smoothly move the user location annotation view and callout view to
|
||
// the new location.
|
||
|
||
dispatch_block_t animation = ^{
|
||
if (self.selectedAnnotation == self.userLocation)
|
||
{
|
||
UIView <MLNCalloutView> *calloutView = self.calloutViewForSelectedAnnotation;
|
||
calloutView.frame = CGRectOffset(calloutView.frame,
|
||
userPoint.x - annotationView.center.x,
|
||
userPoint.y - annotationView.center.y);
|
||
}
|
||
annotationView.center = userPoint;
|
||
};
|
||
|
||
if (duration > 0) {
|
||
[UIView animateWithDuration:duration
|
||
delay:0
|
||
options:(UIViewAnimationOptionCurveLinear |
|
||
UIViewAnimationOptionAllowUserInteraction |
|
||
UIViewAnimationOptionBeginFromCurrentState)
|
||
animations:animation
|
||
completion:NULL];
|
||
}
|
||
else {
|
||
animation();
|
||
}
|
||
_userLocationAnimationCompletionDate = [NSDate dateWithTimeIntervalSinceNow:duration];
|
||
|
||
annotationView.hidden = NO;
|
||
[annotationView update];
|
||
}
|
||
else
|
||
{
|
||
// User has moved far enough outside of the viewport that showing it or
|
||
// its callout would be useless.
|
||
annotationView.hidden = YES;
|
||
|
||
if (_userLocationAnnotationIsSelected)
|
||
{
|
||
[self deselectAnnotation:self.selectedAnnotation animated:YES];
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Intended center point of the user location annotation view with respect to
|
||
/// the overall map view (but respecting the content inset).
|
||
- (CGPoint)userLocationAnnotationViewCenter
|
||
{
|
||
if ([self.delegate respondsToSelector:@selector(mapViewUserLocationAnchorPoint:)])
|
||
{
|
||
CGPoint anchorPoint = [self.delegate mapViewUserLocationAnchorPoint:self];
|
||
return CGPointMake(anchorPoint.x + self.contentInset.left, anchorPoint.y + self.contentInset.top);
|
||
}
|
||
|
||
CGRect contentFrame = UIEdgeInsetsInsetRect(self.contentFrame, self.edgePaddingForFollowingWithCourse);
|
||
|
||
if (CGRectIsEmpty(contentFrame))
|
||
{
|
||
contentFrame = self.contentFrame;
|
||
}
|
||
|
||
CGPoint center = CGPointMake(CGRectGetMidX(contentFrame), CGRectGetMidY(contentFrame));
|
||
|
||
#pragma clang diagnostic push
|
||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||
switch (self.userLocationVerticalAlignment) {
|
||
case MLNAnnotationVerticalAlignmentCenter:
|
||
break;
|
||
case MLNAnnotationVerticalAlignmentTop:
|
||
center.y = CGRectGetMinY(contentFrame);
|
||
break;
|
||
case MLNAnnotationVerticalAlignmentBottom:
|
||
center.y = CGRectGetMaxY(contentFrame);
|
||
break;
|
||
}
|
||
#pragma clang diagnostic pop
|
||
|
||
return center;
|
||
}
|
||
|
||
- (void)updateCompass
|
||
{
|
||
[self.compassView updateCompass];
|
||
}
|
||
|
||
- (void)updateScaleBar
|
||
{
|
||
// Use the `hidden` property (instead of `self.showsScale`) so that we don't
|
||
// break developers who still rely on the <4.0.0 approach of directly
|
||
// setting this property.
|
||
if ( ! self.scaleBar.hidden)
|
||
{
|
||
[(MLNScaleBar *)self.scaleBar setMetersPerPoint:[self metersPerPointAtLatitude:self.centerCoordinate.latitude]];
|
||
}
|
||
}
|
||
|
||
- (BOOL)isFullyLoaded
|
||
{
|
||
return self.mbglMap.isFullyLoaded();
|
||
}
|
||
|
||
- (void)prepareForInterfaceBuilder
|
||
{
|
||
[super prepareForInterfaceBuilder];
|
||
|
||
self.layer.borderColor = [UIColor colorWithRed:59/255.
|
||
green:178/255.
|
||
blue:208/255.
|
||
alpha:0.8].CGColor;
|
||
self.layer.borderWidth = 4;
|
||
self.layer.backgroundColor = [UIColor whiteColor].CGColor;
|
||
|
||
UIView *diagnosticView = [[UIView alloc] init];
|
||
diagnosticView.translatesAutoresizingMaskIntoConstraints = NO;
|
||
[self addSubview:diagnosticView];
|
||
|
||
// Headline
|
||
UILabel *headlineLabel = [[UILabel alloc] init];
|
||
headlineLabel.text = NSStringFromClass([self class]);
|
||
headlineLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
|
||
headlineLabel.textAlignment = NSTextAlignmentCenter;
|
||
headlineLabel.numberOfLines = 1;
|
||
headlineLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||
[headlineLabel setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
|
||
forAxis:UILayoutConstraintAxisHorizontal];
|
||
[diagnosticView addSubview:headlineLabel];
|
||
|
||
// Explanation
|
||
UILabel *explanationLabel = [[UILabel alloc] init];
|
||
explanationLabel.text = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"DESIGNABLE", nil, nil, @"To display a map which requires authoriaztion here, set %@ to your API key in %@", @"Instructions in Interface Builder designable; {key}, {plist file name}"), @"MLNApiKey", @"Info.plist"];
|
||
explanationLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
|
||
explanationLabel.numberOfLines = 0;
|
||
explanationLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||
[explanationLabel setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
|
||
forAxis:UILayoutConstraintAxisHorizontal];
|
||
[diagnosticView addSubview:explanationLabel];
|
||
|
||
// Link
|
||
UIButton *linkButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||
[linkButton setTitle:NSLocalizedStringWithDefaultValue(@"FIRST_STEPS_URL", nil, nil, @"docs.mapbox.com/help/tutorials/first-steps-ios-sdk", @"Setup documentation URL display string; keep as short as possible") forState:UIControlStateNormal];
|
||
linkButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||
linkButton.titleLabel.numberOfLines = 0;
|
||
[linkButton setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
|
||
forAxis:UILayoutConstraintAxisHorizontal];
|
||
[diagnosticView addSubview:linkButton];
|
||
|
||
// Constraints
|
||
NSDictionary *views = @{
|
||
@"container": diagnosticView,
|
||
@"headline": headlineLabel,
|
||
@"explanation": explanationLabel,
|
||
@"link": linkButton,
|
||
};
|
||
[self addConstraint:
|
||
[NSLayoutConstraint constraintWithItem:diagnosticView
|
||
attribute:NSLayoutAttributeCenterYWithinMargins
|
||
relatedBy:NSLayoutRelationEqual
|
||
toItem:self
|
||
attribute:NSLayoutAttributeCenterYWithinMargins
|
||
multiplier:1
|
||
constant:0]];
|
||
[self addConstraint:
|
||
[NSLayoutConstraint constraintWithItem:diagnosticView
|
||
attribute:NSLayoutAttributeTopMargin
|
||
relatedBy:NSLayoutRelationGreaterThanOrEqual
|
||
toItem:self
|
||
attribute:NSLayoutAttributeTopMargin
|
||
multiplier:1
|
||
constant:8]];
|
||
[self addConstraint:
|
||
[NSLayoutConstraint constraintWithItem:self
|
||
attribute:NSLayoutAttributeBottomMargin
|
||
relatedBy:NSLayoutRelationGreaterThanOrEqual
|
||
toItem:diagnosticView
|
||
attribute:NSLayoutAttributeBottomMargin
|
||
multiplier:1
|
||
constant:8]];
|
||
[self addConstraints:
|
||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[container(20@20)]-|"
|
||
options:NSLayoutFormatAlignAllCenterY
|
||
metrics:nil
|
||
views:views]];
|
||
[self addConstraints:
|
||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[headline]-[explanation]-[link]|"
|
||
options:0
|
||
metrics:nil
|
||
views:views]];
|
||
[self addConstraints:
|
||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[headline]|"
|
||
options:0
|
||
metrics:nil
|
||
views:views]];
|
||
[self addConstraints:
|
||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[explanation]|"
|
||
options:0
|
||
metrics:nil
|
||
views:views]];
|
||
[self addConstraints:
|
||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[link]|"
|
||
options:0
|
||
metrics:nil
|
||
views:views]];
|
||
}
|
||
|
||
- (NSMutableArray<MLNAnnotationView *> *)annotationViewReuseQueueForIdentifier:(NSString *)identifier {
|
||
if (!_annotationViewReuseQueueByIdentifier[identifier])
|
||
{
|
||
_annotationViewReuseQueueByIdentifier[identifier] = [NSMutableArray array];
|
||
}
|
||
|
||
return _annotationViewReuseQueueByIdentifier[identifier];
|
||
}
|
||
|
||
- (void)triggerRepaint
|
||
{
|
||
_mbglMap->triggerRepaint();
|
||
}
|
||
|
||
- (MLNBackendResource *)backendResource {
|
||
return _mbglView->getObject();
|
||
}
|
||
|
||
@end
|
||
|
||
// MARK: - IBAdditions methods
|
||
|
||
@implementation MLNMapView (IBAdditions)
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingStyleURL__
|
||
{
|
||
return [NSSet setWithObject:@"styleURL"];
|
||
}
|
||
|
||
- (nullable NSString *)styleURL__
|
||
{
|
||
return self.styleURL.absoluteString;
|
||
}
|
||
|
||
- (void)setStyleURL__:(nullable NSString *)URLString
|
||
{
|
||
URLString = [URLString stringByTrimmingCharactersInSet:
|
||
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||
NSURL *url = URLString.length ? [NSURL URLWithString:URLString] : nil;
|
||
if (URLString.length && !url)
|
||
{
|
||
[NSException raise:MLNInvalidStyleURLException
|
||
format:@"“%@” is not a valid style URL.", URLString];
|
||
}
|
||
self.styleURL = url;
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingLatitude
|
||
{
|
||
return [NSSet setWithObjects:@"centerCoordinate", @"camera", nil];
|
||
}
|
||
|
||
- (double)latitude
|
||
{
|
||
return self.centerCoordinate.latitude;
|
||
}
|
||
|
||
- (void)setLatitude:(double)latitude
|
||
{
|
||
if ( ! isnan(_pendingLongitude))
|
||
{
|
||
self.centerCoordinate = CLLocationCoordinate2DMake(latitude, _pendingLongitude);
|
||
_pendingLatitude = NAN;
|
||
_pendingLongitude = NAN;
|
||
}
|
||
else
|
||
{
|
||
// Not enough info to make a valid center coordinate yet. Stash this
|
||
// latitude away until the longitude is set too.
|
||
_pendingLatitude = latitude;
|
||
}
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingLongitude
|
||
{
|
||
return [NSSet setWithObjects:@"centerCoordinate", @"camera", nil];
|
||
}
|
||
|
||
- (double)longitude
|
||
{
|
||
return self.centerCoordinate.longitude;
|
||
}
|
||
|
||
- (void)setLongitude:(double)longitude
|
||
{
|
||
if ( ! isnan(_pendingLatitude))
|
||
{
|
||
self.centerCoordinate = CLLocationCoordinate2DMake(_pendingLatitude, longitude);
|
||
_pendingLatitude = NAN;
|
||
_pendingLongitude = NAN;
|
||
}
|
||
else
|
||
{
|
||
// Not enough info to make a valid center coordinate yet. Stash this
|
||
// longitude away until the latitude is set too.
|
||
_pendingLongitude = longitude;
|
||
}
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingAllowsZooming
|
||
{
|
||
return [NSSet setWithObject:@"zoomEnabled"];
|
||
}
|
||
|
||
- (BOOL)allowsZooming
|
||
{
|
||
return self.zoomEnabled;
|
||
}
|
||
|
||
- (void)setAllowsZooming:(BOOL)allowsZooming
|
||
{
|
||
MLNLogDebug(@"Setting allowsZooming: %@", MLNStringFromBOOL(allowsZooming));
|
||
self.zoomEnabled = allowsZooming;
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingAllowsScrolling
|
||
{
|
||
return [NSSet setWithObject:@"scrollEnabled"];
|
||
}
|
||
|
||
- (BOOL)allowsScrolling
|
||
{
|
||
return self.scrollEnabled;
|
||
}
|
||
|
||
- (void)setAllowsScrolling:(BOOL)allowsScrolling
|
||
{
|
||
MLNLogDebug(@"Setting allowsScrolling: %@", MLNStringFromBOOL(allowsScrolling));
|
||
self.scrollEnabled = allowsScrolling;
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingAllowsRotating
|
||
{
|
||
return [NSSet setWithObject:@"rotateEnabled"];
|
||
}
|
||
|
||
- (BOOL)allowsRotating
|
||
{
|
||
return self.rotateEnabled;
|
||
}
|
||
|
||
- (void)setAllowsRotating:(BOOL)allowsRotating
|
||
{
|
||
MLNLogDebug(@"Setting allowsRotating: %@", MLNStringFromBOOL(allowsRotating));
|
||
self.rotateEnabled = allowsRotating;
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingAllowsTilting
|
||
{
|
||
return [NSSet setWithObject:@"pitchEnabled"];
|
||
}
|
||
|
||
- (BOOL)allowsTilting
|
||
{
|
||
return self.pitchEnabled;
|
||
}
|
||
|
||
- (void)setAllowsTilting:(BOOL)allowsTilting
|
||
{
|
||
MLNLogDebug(@"Setting allowsTilting: %@", MLNStringFromBOOL(allowsTilting));
|
||
self.pitchEnabled = allowsTilting;
|
||
}
|
||
|
||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingShowsHeading
|
||
{
|
||
return [NSSet setWithObject:@"showsUserHeadingIndicator"];
|
||
}
|
||
|
||
- (BOOL)showsHeading
|
||
{
|
||
return self.showsUserHeadingIndicator;
|
||
}
|
||
|
||
- (void)setShowsHeading:(BOOL)showsHeading
|
||
{
|
||
MLNLogDebug(@"Setting showsHeading: %@", MLNStringFromBOOL(showsHeading));
|
||
self.showsUserHeadingIndicator = showsHeading;
|
||
}
|
||
|
||
|
||
|
||
@end
|