chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
+83
View File
@@ -0,0 +1,83 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import "SMCalloutView.h"
#import "RCTConvert+AirMap.h"
#import "AIRMapCalloutSubview.h"
@class AIRMapMarker;
extern const NSTimeInterval AIRMapRegionChangeObserveInterval;
extern const CGFloat AIRMapZoomBoundBuffer;
extern const NSInteger AIRMapMaxZoomLevel;
@interface AIRMap: MKMapView<SMCalloutViewDelegate>
@property (nonatomic, strong) SMCalloutView *calloutView;
@property (nonatomic, strong) UIImageView *cacheImageView;
@property (nonatomic, strong) UIView *loadingView;
@property (nonatomic, copy) NSString *userLocationAnnotationTitle;
@property (nonatomic, assign) BOOL followUserLocation;
@property (nonatomic, assign) BOOL userLocationCalloutEnabled;
@property (nonatomic, assign) BOOL hasStartedRendering;
@property (nonatomic, assign) BOOL cacheEnabled;
@property (nonatomic, assign) BOOL loadingEnabled;
@property (nonatomic, assign) BOOL legacyZoomConstraintsEnabled;
@property (nonatomic, strong) UIColor *loadingBackgroundColor;
@property (nonatomic, strong) UIColor *loadingIndicatorColor;
@property (nonatomic, assign) BOOL hasShownInitialLoading;
@property (nonatomic, assign) CGFloat minDelta;
@property (nonatomic, assign) CGFloat maxDelta;
@property (nonatomic, assign) UIEdgeInsets legalLabelInsets;
@property (nonatomic, assign) MKCoordinateRegion initialRegion;
@property (nonatomic, assign) MKMapCamera *initialCamera;
@property (nonatomic, assign) CGFloat minZoomLevel;
@property (nonatomic, assign) CGFloat maxZoomLevel;
@property (nonatomic, assign) CGPoint compassOffset;
@property (nonatomic, assign) UIEdgeInsets mapPadding;
@property (nonatomic, assign) CLLocationCoordinate2D pendingCenter;
@property (nonatomic, assign) MKCoordinateSpan pendingSpan;
@property (nonatomic, assign) BOOL ignoreRegionChanges;
@property (nonatomic, copy) RCTBubblingEventBlock onMapReady;
@property (nonatomic, copy) RCTBubblingEventBlock onChange;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@property (nonatomic, copy) RCTBubblingEventBlock onPanDrag;
@property (nonatomic, copy) RCTBubblingEventBlock onDoublePress;
@property (nonatomic, copy) RCTBubblingEventBlock onLongPress;
@property (nonatomic, copy) RCTDirectEventBlock onMarkerPress;
@property (nonatomic, copy) RCTDirectEventBlock onMarkerSelect;
@property (nonatomic, copy) RCTDirectEventBlock onMarkerDeselect;
@property (nonatomic, copy) RCTDirectEventBlock onMarkerDragStart;
@property (nonatomic, copy) RCTDirectEventBlock onMarkerDrag;
@property (nonatomic, copy) RCTDirectEventBlock onMarkerDragEnd;
@property (nonatomic, copy) RCTDirectEventBlock onCalloutPress;
@property (nonatomic, copy) RCTDirectEventBlock onRegionChangeStart;
@property (nonatomic, copy) RCTDirectEventBlock onRegionChange;
@property (nonatomic, copy) RCTBubblingEventBlock onUserLocationChange;
- (void)cacheViewIfNeeded;
- (void)beginLoading;
- (void)finishLoading;
- (double)getZoomLevel;
- (NSArray *)getMapBoundaries;
- (AIRMapMarker*) markerAtPoint:(CGPoint)point;
- (NSDictionary*) getMarkersFramesWithOnlyVisible:(BOOL)onlyVisible;
@end
+756
View File
@@ -0,0 +1,756 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMap.h"
#import <React/RCTEventDispatcher.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
#import "AIRMapPolyline.h"
#import "AIRMapPolygon.h"
#import "AIRMapCircle.h"
#import <QuartzCore/QuartzCore.h>
#import "AIRMapUrlTile.h"
#import "AIRMapWMSTile.h"
#import "AIRMapLocalTile.h"
#import "AIRMapOverlay.h"
const NSTimeInterval AIRMapRegionChangeObserveInterval = 0.1;
const CGFloat AIRMapZoomBoundBuffer = 0.01;
const NSInteger AIRMapMaxZoomLevel = 20;
@interface MKMapView (UIGestureRecognizer)
// this tells the compiler that MKMapView actually implements this method
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
@end
@interface AIRMap ()
@property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView;
@property (nonatomic, assign) NSNumber *shouldZoomEnabled;
@property (nonatomic, assign) NSNumber *shouldScrollEnabled;
@property (nonatomic, strong) MKCompassButton *defaultCompassButton;
@property (nonatomic, strong) MKCompassButton *overlayCompassButton;
- (void)updateScrollEnabled;
- (void)updateZoomEnabled;
@end
@implementation AIRMap
{
UIView *_legalLabel;
BOOL _initialRegionSet;
BOOL _initialCameraSet;
// Array to manually track RN subviews
//
// AIRMap implicitly creates subviews that aren't regular RN children
// (SMCalloutView injects an overlay subview), which otherwise confuses RN
// during component re-renders:
// https://github.com/facebook/react-native/blob/v0.16.0/React/Modules/RCTUIManager.m#L657
//
// Implementation based on RCTTextField, another component with indirect children
// https://github.com/facebook/react-native/blob/v0.16.0/Libraries/Text/RCTTextField.m#L20
NSMutableArray<UIView *> *_reactSubviews;
}
- (instancetype)init
{
if ((self = [super init])) {
_hasStartedRendering = NO;
_reactSubviews = [NSMutableArray new];
// Find Apple link label
for (UIView *subview in self.subviews) {
if ([NSStringFromClass(subview.class) isEqualToString:@"MKAttributionLabel"]) {
// This check is super hacky, but the whole premise of moving around
// Apple's internal subviews is super hacky
_legalLabel = subview;
break;
}
}
// 3rd-party callout view for MapKit that has more options than the built-in. It's painstakingly built to
// be identical to the built-in callout view (which has a private API)
self.calloutView = [SMCalloutView platformCalloutView];
self.calloutView.delegate = self;
self.minZoomLevel = 0;
self.maxZoomLevel = AIRMapMaxZoomLevel;
self.compassOffset = CGPointMake(0, 0);
self.legacyZoomConstraintsEnabled = YES;
}
return self;
}
-(void)addSubview:(UIView *)view {
if([view isKindOfClass:[AIRMapMarker class]]) {
[self addAnnotation:(id <MKAnnotation>)view];
} else {
[super addSubview:view];
}
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
- (void)insertReactSubview:(id<RCTComponent>)subview atIndex:(NSInteger)atIndex {
// Our desired API is to pass up markers/overlays as children to the mapview component.
// This is where we intercept them and do the appropriate underlying mapview action.
if ([subview isKindOfClass:[AIRMapMarker class]]) {
[self addAnnotation:(id <MKAnnotation>) subview];
} else if ([subview isKindOfClass:[AIRMapPolyline class]]) {
((AIRMapPolyline *)subview).map = self;
[self addOverlay:(id<MKOverlay>)subview];
} else if ([subview isKindOfClass:[AIRMapPolygon class]]) {
((AIRMapPolygon *)subview).map = self;
[self addOverlay:(id<MKOverlay>)subview];
} else if ([subview isKindOfClass:[AIRMapCircle class]]) {
((AIRMapCircle *)subview).map = self;
[self addOverlay:(id<MKOverlay>)subview];
} else if ([subview isKindOfClass:[AIRMapUrlTile class]]) {
((AIRMapUrlTile *)subview).map = self;
[self addOverlay:(id<MKOverlay>)subview];
}else if ([subview isKindOfClass:[AIRMapWMSTile class]]) {
((AIRMapWMSTile *)subview).map = self;
[self addOverlay:(id<MKOverlay>)subview];
} else if ([subview isKindOfClass:[AIRMapLocalTile class]]) {
((AIRMapLocalTile *)subview).map = self;
[self addOverlay:(id<MKOverlay>)subview];
} else if ([subview isKindOfClass:[AIRMapOverlay class]]) {
((AIRMapOverlay *)subview).map = self;
[self addOverlay:(id<MKOverlay>)subview];
} else {
NSArray<id<RCTComponent>> *childSubviews = [subview reactSubviews];
for (int i = 0; i < childSubviews.count; i++) {
[self insertReactSubview:(UIView *)childSubviews[i] atIndex:atIndex];
}
}
[_reactSubviews insertObject:(UIView *)subview atIndex:(NSUInteger) atIndex];
}
#pragma clang diagnostic pop
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
- (void)removeReactSubview:(id<RCTComponent>)subview {
// similarly, when the children are being removed we have to do the appropriate
// underlying mapview action here.
if ([subview isKindOfClass:[AIRMapMarker class]]) {
[self removeAnnotation:(id<MKAnnotation>)subview];
} else if ([subview isKindOfClass:[AIRMapPolyline class]]) {
[self removeOverlay:(id <MKOverlay>) subview];
} else if ([subview isKindOfClass:[AIRMapPolygon class]]) {
[self removeOverlay:(id <MKOverlay>) subview];
} else if ([subview isKindOfClass:[AIRMapCircle class]]) {
[self removeOverlay:(id <MKOverlay>) subview];
} else if ([subview isKindOfClass:[AIRMapUrlTile class]]) {
[self removeOverlay:(id <MKOverlay>) subview];
} else if ([subview isKindOfClass:[AIRMapWMSTile class]]) {
[self removeOverlay:(id <MKOverlay>) subview];
} else if ([subview isKindOfClass:[AIRMapLocalTile class]]) {
[self removeOverlay:(id <MKOverlay>) subview];
} else if ([subview isKindOfClass:[AIRMapOverlay class]]) {
[self removeOverlay:(id <MKOverlay>) subview];
} else {
NSArray<id<RCTComponent>> *childSubviews = [subview reactSubviews];
for (int i = 0; i < childSubviews.count; i++) {
[self removeReactSubview:(UIView *)childSubviews[i]];
}
}
[_reactSubviews removeObject:(UIView *)subview];
}
#pragma clang diagnostic pop
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
- (NSArray<id<RCTComponent>> *)reactSubviews {
return _reactSubviews;
}
#pragma clang diagnostic pop
#pragma mark Utils
- (NSArray*) markers {
NSPredicate *filterMarkers = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
AIRMapMarker *marker = (AIRMapMarker *)evaluatedObject;
return [marker isKindOfClass:[AIRMapMarker class]];
}];
NSArray *filteredMarkers = [self.annotations filteredArrayUsingPredicate:filterMarkers];
return filteredMarkers;
}
- (AIRMapMarker*) markerForCallout:(AIRMapCallout*)callout {
AIRMapMarker* marker = nil;
NSArray* markers = [self markers];
for (AIRMapMarker* mrk in markers) {
if (mrk.calloutView == callout) {
marker = mrk;
break;
}
}
return marker;
}
- (CGRect) frameForMarker:(AIRMapMarker*) mrkAnn {
MKAnnotationView* mrkView = [self viewForAnnotation: mrkAnn];
CGRect mrkFrame = mrkView.frame;
return mrkFrame;
}
- (NSDictionary*) getMarkersFramesWithOnlyVisible:(BOOL)onlyVisible {
NSMutableDictionary* markersFrames = [NSMutableDictionary new];
for (AIRMapMarker* mrkAnn in self.markers) {
CGRect frame = [self frameForMarker:mrkAnn];
CGPoint point = [self convertCoordinate:mrkAnn.coordinate toPointToView:self];
NSDictionary* frameDict = @{
@"x": @(frame.origin.x),
@"y": @(frame.origin.y),
@"width": @(frame.size.width),
@"height": @(frame.size.height)
};
NSDictionary* pointDict = @{
@"x": @(point.x),
@"y": @(point.y)
};
NSString* k = mrkAnn.identifier;
BOOL isVisible = CGRectIntersectsRect(self.bounds, frame);
if (k != nil && (!onlyVisible || isVisible)) {
[markersFrames setObject:@{ @"frame": frameDict, @"point": pointDict } forKey:k];
}
}
return markersFrames;
}
- (AIRMapMarker*) markerAtPoint:(CGPoint)point {
AIRMapMarker* mrk = nil;
for (AIRMapMarker* mrkAnn in self.markers) {
CGRect frame = [self frameForMarker:mrkAnn];
if (CGRectContainsPoint(frame, point)) {
mrk = mrkAnn;
break;
}
}
return mrk;
}
#pragma mark Overrides for Callout behavior
// override UIGestureRecognizer's delegate method so we can prevent MKMapView's recognizer from firing
// when we interact with UIControl subclasses inside our callout view.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if ([touch.view isDescendantOfView:self.calloutView])
return NO;
else
return [super gestureRecognizer:gestureRecognizer shouldReceiveTouch:touch];
}
// Allow touches to be sent to our calloutview.
// See this for some discussion of why we need to override this: https://github.com/nfarina/calloutview/pull/9
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGPoint touchPoint = [self.calloutView convertPoint:point fromView:self];
UIView *touchedView = [self.calloutView hitTest:touchPoint withEvent:event];
if (touchedView) {
UIWindow* win = [[[UIApplication sharedApplication] windows] firstObject];
AIRMapCalloutSubview* calloutSubview = nil;
AIRMapCallout* callout = nil;
AIRMapMarker* marker = nil;
UIView* tmp = touchedView;
while (tmp && tmp != win && tmp != self.calloutView) {
if ([tmp respondsToSelector:@selector(onPress)]) {
calloutSubview = (AIRMapCalloutSubview*) tmp;
}
if ([tmp isKindOfClass:[AIRMapCallout class]]) {
callout = (AIRMapCallout*) tmp;
break;
}
tmp = tmp.superview;
}
if (callout) {
marker = [self markerForCallout:callout];
if (marker) {
CGPoint touchPointReal = [marker.calloutView convertPoint:point fromView:self];
if (![callout isPointInside:touchPointReal]) {
return [super hitTest:point withEvent:event];
}
}
}
return calloutSubview ? calloutSubview : touchedView;
}
return [super hitTest:point withEvent:event];
}
#pragma mark SMCalloutViewDelegate
- (NSTimeInterval)calloutView:(SMCalloutView *)calloutView delayForRepositionWithSize:(CGSize)offset {
// When the callout is being asked to present in a way where it or its target will be partially offscreen, it asks us
// if we'd like to reposition our surface first so the callout is completely visible. Here we scroll the map into view,
// but it takes some math because we have to deal in lon/lat instead of the given offset in pixels.
CLLocationCoordinate2D coordinate = self.region.center;
// where's the center coordinate in terms of our view?
CGPoint center = [self convertCoordinate:coordinate toPointToView:self];
// move it by the requested offset
center.x -= offset.width;
center.y -= offset.height;
// and translate it back into map coordinates
coordinate = [self convertPoint:center toCoordinateFromView:self];
// move the map!
[self setCenterCoordinate:coordinate animated:YES];
// tell the callout to wait for a while while we scroll (we assume the scroll delay for MKMapView matches UIScrollView)
return kSMCalloutViewRepositionDelayForUIScrollView;
}
#pragma mark Accessors
- (NSArray *)getMapBoundaries
{
MKMapRect mapRect = self.visibleMapRect;
CLLocationCoordinate2D northEast = MKCoordinateForMapPoint(MKMapPointMake(MKMapRectGetMaxX(mapRect), mapRect.origin.y));
CLLocationCoordinate2D southWest = MKCoordinateForMapPoint(MKMapPointMake(mapRect.origin.x, MKMapRectGetMaxY(mapRect)));
return @[
@[
[NSNumber numberWithDouble:northEast.longitude],
[NSNumber numberWithDouble:northEast.latitude]
],
@[
[NSNumber numberWithDouble:southWest.longitude],
[NSNumber numberWithDouble:southWest.latitude]
]
];
}
- (void)setShowsUserLocation:(BOOL)showsUserLocation
{
if (self.showsUserLocation != showsUserLocation) {
super.showsUserLocation = showsUserLocation;
}
}
- (void)setUserInterfaceStyle:(NSString*)userInterfaceStyle
{
if (@available(iOS 13.0, *)) {
if([userInterfaceStyle isEqualToString:@"light"]) {
self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
} else if([userInterfaceStyle isEqualToString:@"dark"]) {
self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
} else {
self.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified;
}
} else {
NSLog(@"UserInterfaceStyle not supported below iOS 13");
}
}
- (void)setTintColor:(UIColor *)tintColor
{
super.tintColor = tintColor;
}
- (void)setFollowsUserLocation:(BOOL)followsUserLocation
{
_followUserLocation = followsUserLocation;
}
- (void)setUserLocationCalloutEnabled:(BOOL)calloutEnabled
{
_userLocationCalloutEnabled = calloutEnabled;
}
- (void)setHandlePanDrag:(BOOL)handleMapDrag {
for (UIGestureRecognizer *recognizer in [self gestureRecognizers]) {
if ([recognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
recognizer.enabled = handleMapDrag;
break;
}
}
}
- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated
{
// If location is invalid, abort
if (!CLLocationCoordinate2DIsValid(region.center)) {
return;
}
// If new span values are nil, use old values instead
if (!region.span.latitudeDelta) {
region.span.latitudeDelta = self.region.span.latitudeDelta;
}
if (!region.span.longitudeDelta) {
region.span.longitudeDelta = self.region.span.longitudeDelta;
}
// Animate/move to new position
[super setRegion:region animated:animated];
}
- (void)setInitialRegion:(MKCoordinateRegion)initialRegion {
if (!_initialRegionSet) {
_initialRegionSet = YES;
[self setRegion:initialRegion animated:NO];
}
}
- (void)setCamera:(MKMapCamera*)camera animated:(BOOL)animated
{
[super setCamera:camera animated:animated];
}
- (void)setInitialCamera:(MKMapCamera*)initialCamera {
if (!_initialCameraSet) {
_initialCameraSet = YES;
[self setCamera:initialCamera animated:NO];
}
}
- (void)setCacheEnabled:(BOOL)cacheEnabled {
_cacheEnabled = cacheEnabled;
if (self.cacheEnabled && self.cacheImageView.image == nil) {
self.loadingView.hidden = NO;
[self.activityIndicatorView startAnimating];
}
else {
if (_loadingView != nil) {
self.loadingView.hidden = YES;
}
}
}
- (void)setLoadingEnabled:(BOOL)loadingEnabled {
_loadingEnabled = loadingEnabled;
if (!self.hasShownInitialLoading) {
self.loadingView.hidden = !self.loadingEnabled;
}
else {
if (_loadingView != nil) {
self.loadingView.hidden = YES;
}
}
}
- (UIColor *)loadingBackgroundColor {
return self.loadingView.backgroundColor;
}
- (void)setLoadingBackgroundColor:(UIColor *)loadingBackgroundColor {
self.loadingView.backgroundColor = loadingBackgroundColor;
}
- (UIColor *)loadingIndicatorColor {
return self.activityIndicatorView.color;
}
- (void)setLoadingIndicatorColor:(UIColor *)loadingIndicatorColor {
self.activityIndicatorView.color = loadingIndicatorColor;
}
- (void)setCameraZoomRange:(NSDictionary *)cameraZoomRange {
if (@available(iOS 13.0, *)) {
if (cameraZoomRange == nil) {
cameraZoomRange = @{};
}
NSNumber *minValue = cameraZoomRange[@"minCenterCoordinateDistance"];
NSNumber *maxValue = cameraZoomRange[@"maxCenterCoordinateDistance"];
if (minValue == nil && maxValue == nil) {
self.legacyZoomConstraintsEnabled = YES;
MKMapCameraZoomRange *defaultZoomRange = [[MKMapCameraZoomRange alloc] initWithMinCenterCoordinateDistance:MKMapCameraZoomDefault maxCenterCoordinateDistance:MKMapCameraZoomDefault];
[super setCameraZoomRange:defaultZoomRange animated:NO];
return;
}
MKMapCameraZoomRange *zoomRange = nil;
if (minValue != nil && maxValue != nil) {
zoomRange = [[MKMapCameraZoomRange alloc] initWithMinCenterCoordinateDistance:[minValue doubleValue] maxCenterCoordinateDistance:[maxValue doubleValue]];
} else if (minValue != nil) {
zoomRange = [[MKMapCameraZoomRange alloc] initWithMinCenterCoordinateDistance:[minValue doubleValue]];
} else if (maxValue != nil) {
zoomRange = [[MKMapCameraZoomRange alloc] initWithMaxCenterCoordinateDistance:[maxValue doubleValue]];
}
BOOL animated = [cameraZoomRange[@"animated"] boolValue];
self.legacyZoomConstraintsEnabled = NO;
[super setCameraZoomRange:zoomRange animated:animated];
}
}
// Include properties of MKMapView which are only available on iOS 9+
// and check if their selector is available before calling super method.
- (void)setShowsCompass:(BOOL)showsCompass {
if (self.overlayCompassButton != nil) {
self.overlayCompassButton.compassVisibility = showsCompass ? MKFeatureVisibilityAdaptive : MKFeatureVisibilityHidden;
}
if ([MKMapView instancesRespondToSelector:@selector(setShowsCompass:)]) {
[super setShowsCompass:showsCompass];
}
}
- (BOOL)showsCompass {
if ([MKMapView instancesRespondToSelector:@selector(showsCompass)]) {
return [super showsCompass];
} else {
return NO;
}
}
- (void)setShowsScale:(BOOL)showsScale {
if ([MKMapView instancesRespondToSelector:@selector(setShowsScale:)]) {
[super setShowsScale:showsScale];
}
}
- (BOOL)showsScale {
if ([MKMapView instancesRespondToSelector:@selector(showsScale)]) {
return [super showsScale];
} else {
return NO;
}
}
- (void)setShowsTraffic:(BOOL)showsTraffic {
if ([MKMapView instancesRespondToSelector:@selector(setShowsTraffic:)]) {
[super setShowsTraffic:showsTraffic];
}
}
- (BOOL)showsTraffic {
if ([MKMapView instancesRespondToSelector:@selector(showsTraffic)]) {
return [super showsTraffic];
} else {
return NO;
}
}
- (void)setScrollEnabled:(BOOL)scrollEnabled {
self.shouldScrollEnabled = [NSNumber numberWithBool:scrollEnabled];
[self updateScrollEnabled];
}
- (void)updateScrollEnabled {
if (self.cacheEnabled) {
[super setScrollEnabled:NO];
}
else if (self.shouldScrollEnabled != nil) {
[super setScrollEnabled:[self.shouldScrollEnabled boolValue]];
}
}
- (void)setZoomEnabled:(BOOL)zoomEnabled {
self.shouldZoomEnabled = [NSNumber numberWithBool:zoomEnabled];
[self updateZoomEnabled];
}
- (void)updateZoomEnabled {
if (self.cacheEnabled) {
[super setZoomEnabled: NO];
}
else if (self.shouldZoomEnabled != nil) {
[super setZoomEnabled:[self.shouldZoomEnabled boolValue]];
}
}
- (void)cacheViewIfNeeded {
// https://github.com/react-native-maps/react-native-maps/issues/3100
// Do nothing if app is not active
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) {
return;
}
if (self.hasShownInitialLoading) {
if (!self.cacheEnabled) {
if (_cacheImageView != nil) {
self.cacheImageView.hidden = YES;
self.cacheImageView.image = nil;
}
}
else {
self.cacheImageView.image = nil;
self.cacheImageView.hidden = YES;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.cacheImageView.image = nil;
self.cacheImageView.hidden = YES;
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, 0.0);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.cacheImageView.image = image;
self.cacheImageView.hidden = NO;
});
}
[self updateScrollEnabled];
[self updateZoomEnabled];
[self updateLegalLabelInsets];
}
}
- (void)updateLegalLabelInsets {
if (_legalLabel) {
dispatch_async(dispatch_get_main_queue(), ^{
CGRect frame = self->_legalLabel.frame;
if (self->_legalLabelInsets.left) {
frame.origin.x = self->_legalLabelInsets.left;
} else if (self->_legalLabelInsets.right) {
frame.origin.x = self.frame.size.width - self->_legalLabelInsets.right - frame.size.width;
}
if (self->_legalLabelInsets.top) {
frame.origin.y = self->_legalLabelInsets.top;
} else if (self->_legalLabelInsets.bottom) {
frame.origin.y = self.frame.size.height - self->_legalLabelInsets.bottom - frame.size.height;
}
self->_legalLabel.frame = frame;
});
}
}
- (void)setLegalLabelInsets:(UIEdgeInsets)legalLabelInsets {
_legalLabelInsets = legalLabelInsets;
[self updateLegalLabelInsets];
}
- (void)setMapPadding:(UIEdgeInsets)mapPadding {
self.layoutMargins = mapPadding;
}
- (UIEdgeInsets)mapPadding {
return self.layoutMargins;
}
- (void)beginLoading {
if ((!self.hasShownInitialLoading && self.loadingEnabled) || (self.cacheEnabled && self.cacheImageView.image == nil)) {
self.loadingView.hidden = NO;
[self.activityIndicatorView startAnimating];
}
else {
if (_loadingView != nil) {
self.loadingView.hidden = YES;
}
}
}
- (void)finishLoading {
self.hasShownInitialLoading = YES;
if (_loadingView != nil) {
self.loadingView.hidden = YES;
}
[self cacheViewIfNeeded];
}
- (UIActivityIndicatorView *)activityIndicatorView {
if (_activityIndicatorView == nil) {
_activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
_activityIndicatorView.center = self.loadingView.center;
_activityIndicatorView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
_activityIndicatorView.color = [UIColor colorWithRed:96.f/255.f green:96.f/255.f blue:96.f/255.f alpha:1.f]; // defaults to #606060
}
[self.loadingView addSubview:_activityIndicatorView];
return _activityIndicatorView;
}
- (UIView *)loadingView {
if (_loadingView == nil) {
_loadingView = [[UIView alloc] initWithFrame:self.bounds];
_loadingView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
_loadingView.backgroundColor = [UIColor whiteColor]; // defaults to #FFFFFF
[self addSubview:_loadingView];
_loadingView.hidden = NO;
}
return _loadingView;
}
- (UIImageView *)cacheImageView {
if (_cacheImageView == nil) {
_cacheImageView = [[UIImageView alloc] initWithFrame:self.bounds];
_cacheImageView.contentMode = UIViewContentModeCenter;
_cacheImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
[self addSubview:self.cacheImageView];
_cacheImageView.hidden = YES;
}
return _cacheImageView;
}
- (void)layoutSubviews {
[super layoutSubviews];
[self cacheViewIfNeeded];
if (self.overlayCompassButton == nil) {
for (UIView *subview in self.subviews) {
if (![NSStringFromClass(subview.class) isEqualToString:@"MKPassThroughStackView"]) continue;
self.overlayCompassButton = [MKCompassButton compassButtonWithMapView:self];
[subview addSubview:self.overlayCompassButton];
self.overlayCompassButton.frame = CGRectMake(self.overlayCompassButton.frame.origin.x + _compassOffset.x, self.overlayCompassButton.frame.origin.y + _compassOffset.y, self.overlayCompassButton.frame.size.width, self.overlayCompassButton.frame.size.height);
break;
}
}
if (self.defaultCompassButton == nil) {
for (UIView *subview in self.subviews) {
if (![NSStringFromClass(subview.class) isEqualToString:@"MKPassThroughStackView"]) continue;
for (UIView *subview2 in subview.subviews) {
if (![NSStringFromClass(subview2.class) isEqualToString:@"MKCompassView"]) continue;
self.defaultCompassButton = subview2;
self.defaultCompassButton.hidden = YES;
}
break;
}
}
}
// based on https://medium.com/@dmytrobabych/getting-actual-rotation-and-zoom-level-for-mapkit-mkmapview-e7f03f430aa9
- (CGFloat)getZoomLevel {
CGFloat cameraAngle = self.camera.heading;
if (cameraAngle > 270) {
cameraAngle = 360 - cameraAngle;
} else if (cameraAngle > 90) {
cameraAngle = fabs(cameraAngle - 180);
}
CGFloat angleRad = M_PI * cameraAngle / 180; // map rotation in radians
CGFloat width = self.frame.size.width;
CGFloat height = self.frame.size.height;
CGFloat heightOffset = 20; // the offset (status bar height) which is taken by MapKit into consideration to calculate visible area height
// calculating Longitude span corresponding to normal (non-rotated) width
CGFloat spanStraight = width * self.region.span.longitudeDelta / (width * cos(angleRad) + (height - heightOffset) * sin(angleRad));
int normalizingFactor = 512;
return log2(360 * ((width / normalizingFactor) / spanStraight));
}
@end
@@ -0,0 +1,18 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <React/RCTView.h>
@interface AIRMapCallout : RCTView
@property (nonatomic, assign) BOOL tooltip;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@property (nonatomic, assign) BOOL alphaHitTest;
- (BOOL) isPointInside:(CGPoint)pointInCallout;
@end
@@ -0,0 +1,30 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import "AIRMapCallout.h"
@implementation AIRMapCallout
- (BOOL) isPointInside:(CGPoint)pointInCallout {
if (!self.alphaHitTest)
return TRUE;
CGFloat alpha = [self alphaOfPoint:pointInCallout];
return alpha >= 0.01;
}
- (CGFloat) alphaOfPoint:(CGPoint)point {
unsigned char pixel[4] = {0};
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pixel, 1, 1, 8, 4, colorSpace, kCGBitmapAlphaInfoMask & kCGImageAlphaPremultipliedLast);
CGContextTranslateCTM(context, -point.x, -point.y);
[self.layer renderInContext:context];
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
return pixel[3]/255.0;
}
@end
@@ -0,0 +1,10 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapCalloutManager : RCTViewManager
@end
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapCalloutManager.h"
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
#import "AIRMapCallout.h"
@interface AIRMapCalloutManager()
@end
@implementation AIRMapCalloutManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [AIRMapCallout new];
}
RCT_EXPORT_VIEW_PROPERTY(tooltip, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(alphaHitTest, BOOL)
@end
@@ -0,0 +1,15 @@
//
// AIRMapCalloutSubview.h
// AirMaps
//
// Created by Denis Oblogin on 10/8/18.
//
//
#import <UIKit/UIKit.h>
#import <React/RCTView.h>
@interface AIRMapCalloutSubview : UIView
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@end
@@ -0,0 +1,15 @@
//
// AIRMapCalloutSubview.m
// AirMaps
//
// Created by Denis Oblogin on 10/8/18.
//
//
#import "AIRMapCalloutSubview.h"
#import <React/RCTUtils.h>
#import <React/RCTView.h>
#import <React/RCTBridge.h>
@implementation AIRMapCalloutSubview
@end
@@ -0,0 +1,14 @@
//
// AIRMapCalloutSubviewManager.h
// AirMaps
//
// Created by Denis Oblogin on 10/8/18.
//
//
#import <React/RCTViewManager.h>
@interface AIRMapCalloutSubviewManager : RCTViewManager
@end
@@ -0,0 +1,24 @@
//
// AIRMapCalloutSubviewManager.m
// AirMaps
//
// Created by Denis Oblogin on 10/8/18.
//
//
#import "AIRMapCalloutSubviewManager.h"
#import "AIRMapCalloutSubview.h"
#import <React/RCTView.h>
@implementation AIRMapCalloutSubviewManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapCalloutSubview *calloutSubview = [AIRMapCalloutSubview new];
return calloutSubview;
}
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
@end
@@ -0,0 +1,44 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import <React/RCTView.h>
#import "AIRMapCoordinate.h"
#import "AIRMap.h"
#import "RCTConvert+AirMap.h"
@interface AIRMapCircle: MKAnnotationView <MKOverlay>
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, strong) MKCircle *circle;
@property (nonatomic, strong) MKCircleRenderer *renderer;
@property (nonatomic, assign) CLLocationCoordinate2D centerCoordinate;
@property (nonatomic, assign) CLLocationDistance radius;
@property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, strong) UIColor *strokeColor;
@property (nonatomic, assign) CGFloat strokeWidth;
@property (nonatomic, assign) CGFloat miterLimit;
@property (nonatomic, assign) CGLineCap lineCap;
@property (nonatomic, assign) CGLineJoin lineJoin;
@property (nonatomic, assign) CGFloat lineDashPhase;
@property (nonatomic, strong) NSArray <NSNumber *> *lineDashPattern;
#pragma mark MKOverlay protocol
@property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(nonatomic, readonly) MKMapRect boundingMapRect;
- (BOOL)intersectsMapRect:(MKMapRect)mapRect;
- (BOOL)canReplaceMapContent;
@end
@@ -0,0 +1,117 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import "AIRMapCircle.h"
#import <React/UIView+React.h>
@implementation AIRMapCircle {
BOOL _radiusSet;
BOOL _centerSet;
}
- (void)setFillColor:(UIColor *)fillColor {
_fillColor = fillColor;
[self update];
}
- (void)setStrokeColor:(UIColor *)strokeColor {
_strokeColor = strokeColor;
[self update];
}
- (void)setStrokeWidth:(CGFloat)strokeWidth {
_strokeWidth = strokeWidth;
[self update];
}
- (void)setLineJoin:(CGLineJoin)lineJoin {
_lineJoin = lineJoin;
[self update];
}
- (void)setLineCap:(CGLineCap)lineCap {
_lineCap = lineCap;
[self update];
}
- (void)setMiterLimit:(CGFloat)miterLimit {
_miterLimit = miterLimit;
[self update];
}
- (void)setLineDashPhase:(CGFloat)lineDashPhase {
_lineDashPhase = lineDashPhase;
[self update];
}
- (void)setLineDashPattern:(NSArray <NSNumber *> *)lineDashPattern {
_lineDashPattern = lineDashPattern;
[self update];
}
- (void)setRadius:(CLLocationDistance)radius {
_radius = radius;
_radiusSet = YES;
[self createCircleAndRendererIfPossible];
[self update];
}
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate{
_centerCoordinate = centerCoordinate;
_centerSet = YES;
[self createCircleAndRendererIfPossible];
[self update];
}
- (void) createCircleAndRendererIfPossible
{
if (!_centerSet || !_radiusSet) return;
self.circle = [MKCircle circleWithCenterCoordinate:_centerCoordinate radius:_radius];
self.renderer = [[MKCircleRenderer alloc] initWithCircle:self.circle];
}
- (void) update
{
if (!_renderer) return;
_renderer.fillColor = _fillColor;
_renderer.strokeColor = _strokeColor;
_renderer.lineWidth = _strokeWidth;
_renderer.lineCap = _lineCap;
_renderer.lineJoin = _lineJoin;
_renderer.miterLimit = _miterLimit;
_renderer.lineDashPhase = _lineDashPhase;
_renderer.lineDashPattern = _lineDashPattern;
if (_map == nil) return;
[_map removeOverlay:self];
[_map addOverlay:self];
}
#pragma mark MKOverlay implementation
- (CLLocationCoordinate2D) coordinate
{
return self.circle.coordinate;
}
- (MKMapRect) boundingMapRect
{
return self.circle.boundingMapRect;
}
- (BOOL)intersectsMapRect:(MKMapRect)mapRect
{
BOOL answer = [self.circle intersectsMapRect:mapRect];
return answer;
}
- (BOOL)canReplaceMapContent
{
return NO;
}
@end
@@ -0,0 +1,10 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapCircleManager : RCTViewManager
@end
@@ -0,0 +1,51 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapCircleManager.h"
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
#import "AIRMapCircle.h"
@interface AIRMapCircleManager()
@end
@implementation AIRMapCircleManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapCircle *circle = [AIRMapCircle new];
return circle;
}
RCT_REMAP_VIEW_PROPERTY(center, centerCoordinate, CLLocationCoordinate2D)
RCT_EXPORT_VIEW_PROPERTY(radius, CLLocationDistance)
RCT_EXPORT_VIEW_PROPERTY(fillColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineCap, CGLineCap)
RCT_EXPORT_VIEW_PROPERTY(lineJoin, CGLineJoin)
RCT_EXPORT_VIEW_PROPERTY(miterLimit, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineDashPhase, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineDashPattern, NSArray)
// NOTE(lmr):
// for now, onPress events for overlays will be left unimplemented. Seems it is possible with some work, but
// it is difficult to achieve in both ios and android so I decided to leave it out.
//RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
@end
@@ -0,0 +1,13 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
@interface AIRMapCoordinate : NSObject
@property (nonatomic, assign) CLLocationCoordinate2D coordinate;
@end
@@ -0,0 +1,10 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import "AIRMapCoordinate.h"
@implementation AIRMapCoordinate
@end
@@ -0,0 +1,36 @@
//
// AIRMapLocalTile.h
// AirMaps
//
// Created by Peter Zavadsky on 01/12/2017.
// Copyright © 2017 Christopher. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import <React/RCTView.h>
#import "AIRMapCoordinate.h"
#import "AIRMap.h"
#import "RCTConvert+AirMap.h"
@interface AIRMapLocalTile : MKAnnotationView <MKOverlay>
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, strong) MKTileOverlay *tileOverlay;
@property (nonatomic, strong) MKTileOverlayRenderer *renderer;
@property (nonatomic, copy) NSString *pathTemplate;
@property (nonatomic, assign) CGFloat tileSize;
#pragma mark MKOverlay protocol
@property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(nonatomic, readonly) MKMapRect boundingMapRect;
//- (BOOL)intersectsMapRect:(MKMapRect)mapRect;
- (BOOL)canReplaceMapContent;
@end
@@ -0,0 +1,68 @@
//
// AIRMapLocalTile.m
// AirMaps
//
// Created by Peter Zavadsky on 01/12/2017.
// Copyright © 2017 Christopher. All rights reserved.
//
#import "AIRMapLocalTile.h"
#import <React/UIView+React.h>
#import "AIRMapLocalTileOverlay.h"
@implementation AIRMapLocalTile {
BOOL _pathTemplateSet;
BOOL _tileSizeSet;
}
- (void)setPathTemplate:(NSString *)pathTemplate{
_pathTemplate = pathTemplate;
_pathTemplateSet = YES;
[self createTileOverlayAndRendererIfPossible];
[self update];
}
- (void)setTileSize:(CGFloat)tileSize{
_tileSize = tileSize;
_tileSizeSet = YES;
[self createTileOverlayAndRendererIfPossible];
[self update];
}
- (void) createTileOverlayAndRendererIfPossible
{
if (!_pathTemplateSet || !_tileSizeSet) return;
self.tileOverlay = [[AIRMapLocalTileOverlay alloc] initWithURLTemplate:self.pathTemplate];
self.tileOverlay.canReplaceMapContent = YES;
self.tileOverlay.tileSize = CGSizeMake(_tileSize, _tileSize);
self.renderer = [[MKTileOverlayRenderer alloc] initWithTileOverlay:self.tileOverlay];
}
- (void) update
{
if (!_renderer) return;
if (_map == nil) return;
[_map removeOverlay:self];
[_map addOverlay:self level:MKOverlayLevelAboveLabels];
}
#pragma mark MKOverlay implementation
- (CLLocationCoordinate2D) coordinate
{
return self.tileOverlay.coordinate;
}
- (MKMapRect) boundingMapRect
{
return self.tileOverlay.boundingMapRect;
}
- (BOOL)canReplaceMapContent
{
return self.tileOverlay.canReplaceMapContent;
}
@end
@@ -0,0 +1,13 @@
//
// AIRMapLocalTileManager.h
// AirMaps
//
// Created by Peter Zavadsky on 01/12/2017.
// Copyright © 2017 Christopher. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapLocalTileManager : RCTViewManager
@end
@@ -0,0 +1,38 @@
//
// AIRMapLocalTileManager.m
// AirMaps
//
// Created by Peter Zavadsky on 01/12/2017.
// Copyright © 2017 Christopher. All rights reserved.
//
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
#import "AIRMapLocalTile.h"
#import "AIRMapLocalTileManager.h"
@interface AIRMapLocalTileManager()
@end
@implementation AIRMapLocalTileManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapLocalTile *tile = [AIRMapLocalTile new];
return tile;
}
RCT_EXPORT_VIEW_PROPERTY(pathTemplate, NSString)
RCT_EXPORT_VIEW_PROPERTY(tileSize, CGFloat)
@end
@@ -0,0 +1,12 @@
//
// AIRMapLocalTileOverlay.h
// Pods
//
// Created by Peter Zavadsky on 04/12/2017.
//
#import <MapKit/MapKit.h>
@interface AIRMapLocalTileOverlay : MKTileOverlay
@end
@@ -0,0 +1,31 @@
//
// AIRMapLocalTileOverlay.m
// Pods-AirMapsExplorer
//
// Created by Peter Zavadsky on 04/12/2017.
//
#import "AIRMapLocalTileOverlay.h"
@interface AIRMapLocalTileOverlay ()
@end
@implementation AIRMapLocalTileOverlay
-(void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result {
NSMutableString *tileFilePath = [self.URLTemplate mutableCopy];
[tileFilePath replaceOccurrencesOfString: @"{x}" withString:[NSString stringWithFormat:@"%li", (long)path.x] options:0 range:NSMakeRange(0, tileFilePath.length)];
[tileFilePath replaceOccurrencesOfString:@"{y}" withString:[NSString stringWithFormat:@"%li", (long)path.y] options:0 range:NSMakeRange(0, tileFilePath.length)];
[tileFilePath replaceOccurrencesOfString:@"{z}" withString:[NSString stringWithFormat:@"%li", (long)path.z] options:0 range:NSMakeRange(0, tileFilePath.length)];
if ([[NSFileManager defaultManager] fileExistsAtPath:tileFilePath]) {
NSData* tile = [NSData dataWithContentsOfFile:tileFilePath];
result(tile,nil);
} else {
result(nil, nil);
}
}
@end
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <React/RCTViewManager.h>
#import "AIRMap.h"
#define MERCATOR_RADIUS 85445659.44705395
#define MERCATOR_OFFSET 268435456
@interface AIRMapManager : RCTViewManager
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
zoomLevel:(double)zoomLevel
animated:(BOOL)animated
mapView:(AIRMap *)mapView;
- (MKCoordinateRegion)coordinateRegionWithMapView:(AIRMap *)mapView
centerCoordinate:(CLLocationCoordinate2D)centerCoordinate
andZoomLevel:(double)zoomLevel;
- (double) zoomLevel:(AIRMap *)mapView;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapMarker.h"
#import "AIRMapCallout.h"
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import "AIRMap.h"
#import "SMCalloutView.h"
#import "RCTConvert+AirMap.h"
@class RCTBridge;
@interface AIRMapMarker : MKAnnotationView <MKAnnotation>
@property (nonatomic, strong) AIRMapCallout *calloutView;
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, weak) RCTBridge *bridge;
@property (nonatomic, strong) NSString *identifier;
@property (nonatomic, copy) NSString *imageSrc;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *subtitle;
@property (nonatomic, assign) CLLocationCoordinate2D coordinate;
@property (nonatomic, strong) UIColor *pinColor;
@property (nonatomic, assign) NSInteger zIndex;
@property (nonatomic, assign) double opacity;
@property (nonatomic, assign) BOOL isPreselected;
@property (nonatomic, assign) MKFeatureVisibility titleVisibility;
@property (nonatomic, assign) MKFeatureVisibility subtitleVisibility;
@property (nonatomic, assign) BOOL useLegacyPinView;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@property (nonatomic, copy) RCTDirectEventBlock onSelect;
@property (nonatomic, copy) RCTDirectEventBlock onDeselect;
@property (nonatomic, copy) RCTDirectEventBlock onCalloutPress;
@property (nonatomic, copy) RCTDirectEventBlock onDragStart;
@property (nonatomic, copy) RCTDirectEventBlock onDrag;
@property (nonatomic, copy) RCTDirectEventBlock onDragEnd;
- (MKAnnotationView *)getAnnotationView;
- (void)fillCalloutView:(SMCalloutView *)calloutView;
- (BOOL)shouldShowCalloutView;
- (void)showCalloutView;
- (void)hideCalloutView;
- (void)addTapGestureRecognizer;
- (void)setUseLegacyPinView:(BOOL)value;
@end
@interface AIREmptyCalloutBackgroundView : SMCalloutBackgroundView
@end
@@ -0,0 +1,393 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapMarker.h"
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTImageLoaderProtocol.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
NSInteger const AIR_CALLOUT_OPEN_ZINDEX_BASELINE = 999;
@implementation AIREmptyCalloutBackgroundView
@end
@implementation AIRMapMarker {
BOOL _hasSetCalloutOffset;
RCTImageLoaderCancellationBlock _reloadImageCancellationBlock;
MKMarkerAnnotationView *_markerView;
MKPinAnnotationView *_pinView;
BOOL _calloutIsOpen;
NSInteger _zIndexBeforeOpen;
BOOL _useLegacyPinView;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self.layer addObserver:self forKeyPath:@"zPosition" options:NSKeyValueObservingOptionNew context:nil];
}
return self;
}
- (void)reactSetFrame:(CGRect)frame
{
// Make sure we use the image size when available
CGSize size = self.image ? self.image.size : frame.size;
CGRect bounds = {CGPointZero, size};
// The MapView is basically in charge of figuring out the center position of the marker view. If the view changed in
// height though, we need to compensate in such a way that the bottom of the marker stays at the same spot on the
// map.
CGFloat dy = (bounds.size.height - self.bounds.size.height) / 2;
CGPoint center = (CGPoint){ self.center.x, self.center.y - dy };
// Avoid crashes due to nan coords
if (isnan(center.x) || isnan(center.y) ||
isnan(bounds.origin.x) || isnan(bounds.origin.y) ||
isnan(bounds.size.width) || isnan(bounds.size.height)) {
RCTLogError(@"Invalid layout for (%@)%@. position: %@. bounds: %@",
self.reactTag, self, NSStringFromCGPoint(center), NSStringFromCGRect(bounds));
return;
}
self.center = center;
self.bounds = bounds;
}
- (void)insertReactSubview:(id<RCTComponent>)subview atIndex:(NSInteger)atIndex {
if ([subview isKindOfClass:[AIRMapCallout class]]) {
self.calloutView = (AIRMapCallout *)subview;
} else {
[super insertReactSubview:(UIView *)subview atIndex:atIndex];
}
}
- (void)removeReactSubview:(id<RCTComponent>)subview {
if ([subview isKindOfClass:[AIRMapCallout class]] && self.calloutView == subview) {
self.calloutView = nil;
} else {
[super removeReactSubview:(UIView *)subview];
}
}
- (MKAnnotationView *)getAnnotationView
{
if ([self shouldUsePinView]) {
// In this case, we want to render a platform "default" legacy marker.
if (_pinView == nil && _useLegacyPinView) {
_pinView = [[MKPinAnnotationView alloc] initWithAnnotation:self reuseIdentifier: nil];
[self addGestureRecognizerToView:_pinView];
_pinView.annotation = self;
if ([_pinView respondsToSelector:@selector(setPinTintColor:)]) {
_pinView.pinTintColor = self.pinColor;
}
_pinView.draggable = self.draggable;
_pinView.layer.zPosition = self.zIndex;
return _pinView;
}
if (_markerView == nil && !_useLegacyPinView) {
_markerView = [[MKMarkerAnnotationView alloc] initWithAnnotation:self reuseIdentifier: nil];
[self addGestureRecognizerToView:_markerView];
_markerView.annotation = self;
_markerView.draggable = self.draggable;
_markerView.layer.zPosition = self.zIndex;
_markerView.markerTintColor = self.pinColor;
_markerView.titleVisibility = self.titleVisibility ?: MKFeatureVisibilityHidden;
_markerView.subtitleVisibility = self.subtitleVisibility ?: MKFeatureVisibilityHidden;
}
return _markerView ?: _pinView;
} else {
// If it has subviews, it means we are wanting to render a custom marker with arbitrary react views.
// if it has a non-null image, it means we want to render a custom marker with the image.
// In either case, we want to return the AIRMapMarker since it is both an MKAnnotation and an
// MKAnnotationView all at the same time.
self.layer.zPosition = self.zIndex;
return self;
}
}
- (void)fillCalloutView:(SMCalloutView *)calloutView
{
// Set everything necessary on the calloutView before it becomes visible.
// Apply the MKAnnotationView's desired calloutOffset (from the top-middle of the view)
if ([self shouldUsePinView] && !_hasSetCalloutOffset && _useLegacyPinView) {
calloutView.calloutOffset = CGPointMake(-8,0);
} else {
calloutView.calloutOffset = self.calloutOffset;
}
if (self.calloutView) {
calloutView.title = nil;
calloutView.subtitle = nil;
if (self.calloutView.tooltip) {
// if tooltip is true, then the user wants their react view to be the "tooltip" as wwell, so we set
// the background view to something empty/transparent
calloutView.backgroundView = [AIREmptyCalloutBackgroundView new];
} else {
// the default tooltip look is wanted, and the user is just filling the content with their react subviews.
// as a result, we use the default "masked" background view.
calloutView.backgroundView = [SMCalloutMaskedBackgroundView new];
}
// when this is set, the callout's content will be whatever react views the user has put as the callout's
// children.
calloutView.contentView = self.calloutView;
} else {
// if there is no calloutView, it means the user wants to use the default callout behavior with title/subtitle
// pairs.
calloutView.title = self.title;
calloutView.subtitle = self.subtitle;
calloutView.contentView = nil;
calloutView.backgroundView = [SMCalloutMaskedBackgroundView new];
}
}
- (void)showCalloutView
{
_calloutIsOpen = YES;
[self setZIndex:_zIndexBeforeOpen];
MKAnnotationView *annotationView = [self getAnnotationView];
[self setSelected:YES animated:NO];
[self.map selectAnnotation:self animated:NO];
id event = @{
@"action": @"marker-select",
@"id": self.identifier ?: @"unknown",
@"coordinate": @{
@"latitude": @(self.coordinate.latitude),
@"longitude": @(self.coordinate.longitude)
}
};
if (self.map.onMarkerSelect) self.map.onMarkerSelect(event);
if (self.onSelect) self.onSelect(event);
if (![self shouldShowCalloutView]) {
// no callout to show
return;
}
[self fillCalloutView:self.map.calloutView];
// This is where we present our custom callout view... MapKit's built-in callout doesn't have the flexibility
// we need, but a lot of work was done by Nick Farina to make this identical to MapKit's built-in.
[self.map.calloutView presentCalloutFromRect:annotationView.bounds
inView:annotationView
constrainedToView:self.map
animated:YES];
}
#pragma mark - Tap Gesture & Events.
- (void)addTapGestureRecognizer {
[self addGestureRecognizerToView:nil];
}
- (void)addGestureRecognizerToView:(UIView *)view {
if (!view) {
view = self;
}
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_handleTap:)];
// setting this to NO allows the parent MapView to continue receiving marker selection events
tapGestureRecognizer.cancelsTouchesInView = NO;
[view addGestureRecognizer:tapGestureRecognizer];
}
- (void)_handleTap:(UITapGestureRecognizer *)recognizer {
AIRMapMarker *marker = self;
if (!marker) return;
if (marker.selected) {
CGPoint touchPoint = [recognizer locationInView:marker.map.calloutView];
CGRect bubbleFrame = [self.calloutView convertRect:marker.map.calloutView.bounds toView:marker.map];
CGPoint touchPointReal = [recognizer locationInView:self.calloutView];
UIView *calloutView = [marker.map.calloutView hitTest:touchPoint withEvent:nil];
if (calloutView) {
// the callout (or its subview) got clicked, not the marker
UIWindow* win = [[[UIApplication sharedApplication] windows] firstObject];
AIRMapCalloutSubview* calloutSubview = nil;
UIView* tmp = calloutView;
while (tmp && tmp != win && tmp != self.calloutView && tmp != self.map) {
if ([tmp respondsToSelector:@selector(onPress)]) {
calloutSubview = (AIRMapCalloutSubview*) tmp;
break;
}
tmp = tmp.superview;
}
id event = @{
@"action": calloutSubview ? @"callout-inside-press" : @"callout-press",
@"id": marker.identifier ?: @"unknown",
@"point": @{
@"x": @(touchPointReal.x),
@"y": @(touchPointReal.y),
},
@"frame": @{
@"x": @(bubbleFrame.origin.x),
@"y": @(bubbleFrame.origin.y),
@"width": @(bubbleFrame.size.width),
@"height": @(bubbleFrame.size.height),
}
};
if (calloutSubview) calloutSubview.onPress(event);
if (marker.onCalloutPress) marker.onCalloutPress(event);
if (marker.calloutView && marker.calloutView.onPress) marker.calloutView.onPress(event);
if (marker.map.onCalloutPress) marker.map.onCalloutPress(event);
return;
}
}
// the actual marker got clicked
CGPoint touchPointReal = [recognizer locationInView:self.calloutView];
id event = @{
@"action": @"marker-press",
@"id": marker.identifier ?: @"unknown",
@"coordinate": @{
@"latitude": @(marker.coordinate.latitude),
@"longitude": @(marker.coordinate.longitude)
},
@"position": @{
@"x": @(touchPointReal.x),
@"y": @(touchPointReal.y),
}
};
if (marker.onPress) marker.onPress(event);
if (marker.map.onMarkerPress) marker.map.onMarkerPress(event);
[marker.map selectAnnotation:marker animated:NO];
}
- (void)hideCalloutView
{
_calloutIsOpen = NO;
[self setZIndex:_zIndexBeforeOpen];
// hide the callout view
[self.map.calloutView dismissCalloutAnimated:YES];
[self setSelected:NO animated:NO];
[self.map deselectAnnotation:self animated:NO];
id event = @{
@"action": @"marker-deselect",
@"id": self.identifier ?: @"unknown",
@"coordinate": @{
@"latitude": @(self.coordinate.latitude),
@"longitude": @(self.coordinate.longitude)
}
};
if (self.map.onMarkerDeselect) self.map.onMarkerDeselect(event);
if (self.onDeselect) self.onDeselect(event);
}
- (void)setCalloutOffset:(CGPoint)calloutOffset
{
_hasSetCalloutOffset = YES;
[super setCalloutOffset:calloutOffset];
}
- (BOOL)shouldShowCalloutView
{
return self.calloutView != nil || self.title != nil || self.subtitle != nil;
}
- (BOOL)shouldUsePinView
{
return self.reactSubviews.count == 0 && !self.imageSrc;
}
- (void)setOpacity:(double)opacity
{
[self setAlpha:opacity];
}
- (void)setImageSrc:(NSString *)imageSrc
{
_imageSrc = imageSrc;
if (_reloadImageCancellationBlock) {
_reloadImageCancellationBlock();
_reloadImageCancellationBlock = nil;
}
_reloadImageCancellationBlock = [[_bridge moduleForName:@"ImageLoader"] loadImageWithURLRequest:[RCTConvert NSURLRequest:_imageSrc]
size:self.bounds.size
scale:RCTScreenScale()
clipped:YES
resizeMode:RCTResizeModeCenter
progressBlock:nil
partialLoadBlock:nil
completionBlock:^(NSError *error, UIImage *image) {
if (error) {
// TODO(lmr): do something with the error?
NSLog(@"%@", error);
}
dispatch_async(dispatch_get_main_queue(), ^{
self.image = image;
});
}];
}
- (void)setPinColor:(UIColor *)pinColor
{
_pinColor = pinColor;
if(_useLegacyPinView && [_pinView respondsToSelector:@selector(setPinTintColor:)]) {
_pinView.pinTintColor = _pinColor;
} else {
_markerView.markerTintColor = _pinColor;
}
}
- (void)setZIndex:(NSInteger)zIndex
{
_zIndexBeforeOpen = zIndex;
_zIndex = _calloutIsOpen ? zIndex + AIR_CALLOUT_OPEN_ZINDEX_BASELINE : zIndex;
self.layer.zPosition = zIndex;
}
- (BOOL)isSelected {
return _isPreselected || [super isSelected];
}
- (void)dealloc {
[self.layer removeObserver:self forKeyPath:@"zPosition"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"zPosition"]) {
self.layer.zPosition = _zIndex;
}
}
- (void)setUseLegacyPinView:(BOOL)value {
_useLegacyPinView = value;
}
@end
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <React/RCTViewManager.h>
@interface AIRMapMarkerManager : RCTViewManager
@end
@@ -0,0 +1,98 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapMarkerManager.h"
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTUIManager.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
@interface AIRMapMarkerManager () <MKMapViewDelegate>
@end
@implementation AIRMapMarkerManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapMarker *marker = [AIRMapMarker new];
[marker addTapGestureRecognizer];
marker.bridge = self.bridge;
marker.isAccessibilityElement = YES;
marker.accessibilityElementsHidden = NO;
return marker;
}
RCT_EXPORT_VIEW_PROPERTY(identifier, NSString)
RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier, NSString)
//RCT_EXPORT_VIEW_PROPERTY(reuseIdentifier, NSString)
RCT_EXPORT_VIEW_PROPERTY(title, NSString)
RCT_REMAP_VIEW_PROPERTY(description, subtitle, NSString)
RCT_EXPORT_VIEW_PROPERTY(coordinate, CLLocationCoordinate2D)
RCT_EXPORT_VIEW_PROPERTY(centerOffset, CGPoint)
RCT_EXPORT_VIEW_PROPERTY(calloutOffset, CGPoint)
RCT_REMAP_VIEW_PROPERTY(image, imageSrc, NSString)
RCT_EXPORT_VIEW_PROPERTY(pinColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(draggable, BOOL)
RCT_EXPORT_VIEW_PROPERTY(zIndex, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(opacity, double)
RCT_EXPORT_VIEW_PROPERTY(isPreselected, BOOL)
RCT_EXPORT_VIEW_PROPERTY(titleVisibility, NSString)
RCT_EXPORT_VIEW_PROPERTY(subtitleVisibility, NSString)
RCT_EXPORT_VIEW_PROPERTY(useLegacyPinView, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSelect, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDeselect, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onCalloutPress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDragStart, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDrag, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDragEnd, RCTDirectEventBlock)
RCT_EXPORT_METHOD(showCallout:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRMapMarker class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
[(AIRMapMarker *) view showCalloutView];
}
}];
}
RCT_EXPORT_METHOD(hideCallout:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRMapMarker class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
[(AIRMapMarker *) view hideCalloutView];
}
}];
}
RCT_EXPORT_METHOD(redrawCallout:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRMapMarker class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
//no need to do anything here
}
}];
}
@end
@@ -0,0 +1,36 @@
#import "AIRMapCallout.h"
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import "RCTConvert+AirMap.h"
#import <React/RCTComponent.h>
#import "AIRMap.h"
#import "AIRMapOverlayRenderer.h"
@class RCTBridge;
@interface AIRMapOverlay : UIView <MKOverlay>
@property (nonatomic, strong) AIRMapOverlayRenderer *renderer;
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, weak) RCTBridge *bridge;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) NSString *imageSrc;
@property (nonatomic, strong, readonly) UIImage *overlayImage;
@property (nonatomic, copy) NSArray *boundsRect;
@property (nonatomic, assign) NSInteger rotation;
@property (nonatomic, assign) CGFloat transparency;
@property (nonatomic, assign) NSInteger zIndex;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
#pragma mark MKOverlay protocol
@property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(nonatomic, readonly) MKMapRect boundingMapRect;
- (BOOL)intersectsMapRect:(MKMapRect)mapRect;
- (BOOL)canReplaceMapContent;
@end
@@ -0,0 +1,103 @@
#import "AIRMapOverlay.h"
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTImageLoaderProtocol.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
@interface AIRMapOverlay()
@property (nonatomic, strong, readwrite) UIImage *overlayImage;
@end
@implementation AIRMapOverlay {
RCTImageLoaderCancellationBlock _reloadImageCancellationBlock;
CLLocationCoordinate2D _southWest;
CLLocationCoordinate2D _northEast;
MKMapRect _mapRect;
}
- (void)setImageSrc:(NSString *)imageSrc
{
NSLog(@">>> SET IMAGESRC: %@", imageSrc);
_imageSrc = imageSrc;
if (_reloadImageCancellationBlock) {
_reloadImageCancellationBlock();
_reloadImageCancellationBlock = nil;
}
__weak typeof(self) weakSelf = self;
_reloadImageCancellationBlock = [[_bridge moduleForName:@"ImageLoader"] loadImageWithURLRequest:[RCTConvert NSURLRequest:_imageSrc]
size:weakSelf.bounds.size
scale:RCTScreenScale()
clipped:YES
resizeMode:RCTResizeModeCenter
progressBlock:nil
partialLoadBlock:nil
completionBlock:^(NSError *error, UIImage *image) {
if (error) {
NSLog(@"%@", error);
}
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@">>> IMAGE: %@", image);
weakSelf.overlayImage = image;
[weakSelf createOverlayRendererIfPossible];
[weakSelf update];
});
}];
}
- (void)setBoundsRect:(NSArray *)boundsRect {
_boundsRect = boundsRect;
_southWest = CLLocationCoordinate2DMake([boundsRect[0][0] doubleValue], [boundsRect[0][1] doubleValue]);
_northEast = CLLocationCoordinate2DMake([boundsRect[1][0] doubleValue], [boundsRect[1][1] doubleValue]);
MKMapPoint southWest = MKMapPointForCoordinate(_southWest);
MKMapPoint northEast = MKMapPointForCoordinate(_northEast);
_mapRect = MKMapRectMake(southWest.x, northEast.y, ABS(northEast.x - southWest.x), ABS(northEast.y - southWest.y));
[self update];
}
- (void)createOverlayRendererIfPossible
{
if (MKMapRectIsEmpty(_mapRect) || !self.overlayImage) return;
__weak typeof(self) weakSelf = self;
self.renderer = [[AIRMapOverlayRenderer alloc] initWithOverlay:weakSelf];
}
- (void)update
{
if (!_renderer) return;
if (_map == nil) return;
[_map removeOverlay:self];
[_map addOverlay:self];
}
#pragma mark MKOverlay implementation
- (CLLocationCoordinate2D)coordinate
{
return MKCoordinateForMapPoint(MKMapPointMake(MKMapRectGetMidX(_mapRect), MKMapRectGetMidY(_mapRect)));
}
- (MKMapRect)boundingMapRect
{
return _mapRect;
}
- (BOOL)intersectsMapRect:(MKMapRect)mapRect
{
return MKMapRectIntersectsRect(_mapRect, mapRect);
}
- (BOOL)canReplaceMapContent
{
return NO;
}
@end
@@ -0,0 +1,5 @@
#import <React/RCTViewManager.h>
@interface AIRMapOverlayManager : RCTViewManager
@end
@@ -0,0 +1,27 @@
#import "AIRMapOverlayManager.h"
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTUIManager.h>
#import <React/UIView+React.h>
#import "AIRMapOverlay.h"
@interface AIRMapOverlayManager () <MKMapViewDelegate>
@end
@implementation AIRMapOverlayManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapOverlay *overlay = [AIRMapOverlay new];
overlay.bridge = self.bridge;
return overlay;
}
RCT_REMAP_VIEW_PROPERTY(bounds, boundsRect, NSArray)
RCT_REMAP_VIEW_PROPERTY(image, imageSrc, NSString)
@end
@@ -0,0 +1,8 @@
#import <MapKit/MapKit.h>
@interface AIRMapOverlayRenderer : MKOverlayRenderer
@property (nonatomic, assign) NSInteger rotation;
@property (nonatomic, assign) CGFloat transparency;
@end
@@ -0,0 +1,30 @@
#import "AIRMapOverlayRenderer.h"
#import "AIRMapOverlay.h"
@implementation AIRMapOverlayRenderer
- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context {
UIImage *image = [(AIRMapOverlay *)self.overlay overlayImage];
CGContextSaveGState(context);
CGImageRef imageReference = image.CGImage;
MKMapRect theMapRect = [self.overlay boundingMapRect];
CGRect theRect = [self rectForMapRect:theMapRect];
CGContextRotateCTM(context, M_PI);
CGContextScaleCTM(context, -1.0, 1.0);
CGContextTranslateCTM(context, 0.0, -theRect.size.height);
CGContextAddRect(context, theRect);
CGContextDrawImage(context, theRect, imageReference);
CGContextRestoreGState(context);
}
- (BOOL)canDrawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale {
return [(AIRMapOverlay *)self.overlay overlayImage] != nil;
}
@end
@@ -0,0 +1,46 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import <React/RCTView.h>
#import "AIRMapCoordinate.h"
#import "AIRMap.h"
#import "RCTConvert+AirMap.h"
@interface AIRMapPolygon: MKAnnotationView <MKOverlay>
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, strong) MKPolygon *polygon;
@property (nonatomic, strong) MKPolygonRenderer *renderer;
@property (nonatomic, strong) NSArray<MKPolygon *> *interiorPolygons;
@property (nonatomic, strong) NSArray<AIRMapCoordinate *> *coordinates;
@property (nonatomic, strong) NSArray<NSArray<AIRMapCoordinate *> *> *holes;
@property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, strong) UIColor *strokeColor;
@property (nonatomic, assign) CGFloat strokeWidth;
@property (nonatomic, assign) CGFloat miterLimit;
@property (nonatomic, assign) CGLineCap lineCap;
@property (nonatomic, assign) CGLineJoin lineJoin;
@property (nonatomic, assign) CGFloat lineDashPhase;
@property (nonatomic, strong) NSArray <NSNumber *> *lineDashPattern;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
#pragma mark MKOverlay protocol
@property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(nonatomic, readonly) MKMapRect boundingMapRect;
- (BOOL)intersectsMapRect:(MKMapRect)mapRect;
- (BOOL)canReplaceMapContent;
@end
@@ -0,0 +1,126 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import "AIRMapPolygon.h"
#import <React/UIView+React.h>
@implementation AIRMapPolygon {
}
- (void)setFillColor:(UIColor *)fillColor {
_fillColor = fillColor;
[self update];
}
- (void)setStrokeColor:(UIColor *)strokeColor {
_strokeColor = strokeColor;
[self update];
}
- (void)setStrokeWidth:(CGFloat)strokeWidth {
_strokeWidth = strokeWidth;
[self update];
}
- (void)setLineJoin:(CGLineJoin)lineJoin {
_lineJoin = lineJoin;
[self update];
}
- (void)setLineCap:(CGLineCap)lineCap {
_lineCap = lineCap;
[self update];
}
- (void)setMiterLimit:(CGFloat)miterLimit {
_miterLimit = miterLimit;
[self update];
}
- (void)setLineDashPhase:(CGFloat)lineDashPhase {
_lineDashPhase = lineDashPhase;
[self update];
}
- (void)setLineDashPattern:(NSArray <NSNumber *> *)lineDashPattern {
_lineDashPattern = lineDashPattern;
[self update];
}
- (void)setCoordinates:(NSArray<AIRMapCoordinate *> *)coordinates {
_coordinates = coordinates;
CLLocationCoordinate2D coords[coordinates.count];
for(int i = 0; i < coordinates.count; i++)
{
coords[i] = coordinates[i].coordinate;
}
self.polygon = [MKPolygon polygonWithCoordinates:coords count:coordinates.count interiorPolygons:_interiorPolygons];
// TODO: we could lazy-initialize the polygon, since we don't need it until the
// polygon is in view.
self.renderer = [[MKPolygonRenderer alloc] initWithPolygon:self.polygon];
[self update];
}
- (void)setHoles:(NSArray<NSArray<AIRMapCoordinate *> *> *)holes {
_holes = holes;
if (holes.count)
{
NSMutableArray<MKPolygon *> *polygons = [NSMutableArray array];
for(int h = 0; h < holes.count; h++)
{
CLLocationCoordinate2D coords[holes[h].count];
for(int i = 0; i < holes[h].count; i++)
{
coords[i] = holes[h][i].coordinate;
}
[polygons addObject:[MKPolygon polygonWithCoordinates:coords count:holes[h].count]];
}
_interiorPolygons = polygons;
}
}
- (void) update
{
if (!_renderer) return;
_renderer.fillColor = _fillColor;
_renderer.strokeColor = _strokeColor;
_renderer.lineWidth = _strokeWidth;
_renderer.lineCap = _lineCap;
_renderer.lineJoin = _lineJoin;
_renderer.miterLimit = _miterLimit;
_renderer.lineDashPhase = _lineDashPhase;
_renderer.lineDashPattern = _lineDashPattern;
if (_map == nil) return;
[_map removeOverlay:self];
[_map addOverlay:self];
}
#pragma mark MKOverlay implementation
- (CLLocationCoordinate2D) coordinate
{
return self.polygon.coordinate;
}
- (MKMapRect) boundingMapRect
{
return self.polygon.boundingMapRect;
}
- (BOOL)intersectsMapRect:(MKMapRect)mapRect
{
BOOL answer = [self.polygon intersectsMapRect:mapRect];
return answer;
}
- (BOOL)canReplaceMapContent
{
return NO;
}
@end
@@ -0,0 +1,10 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapPolygonManager : RCTViewManager
@end
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapPolygonManager.h"
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "RCTConvert+AirMap.h"
#import "AIRMapMarker.h"
#import "AIRMapPolygon.h"
@interface AIRMapPolygonManager()
@end
@implementation AIRMapPolygonManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapPolygon *polygon = [AIRMapPolygon new];
return polygon;
}
RCT_EXPORT_VIEW_PROPERTY(coordinates, AIRMapCoordinateArray)
RCT_EXPORT_VIEW_PROPERTY(holes, AIRMapCoordinateArrayArray)
RCT_EXPORT_VIEW_PROPERTY(fillColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineCap, CGLineCap)
RCT_EXPORT_VIEW_PROPERTY(lineJoin, CGLineJoin)
RCT_EXPORT_VIEW_PROPERTY(miterLimit, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineDashPhase, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineDashPattern, NSArray)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
@end
@@ -0,0 +1,45 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import <React/RCTView.h>
#import "AIRMapCoordinate.h"
#import "AIRMap.h"
#import "RCTConvert+AirMap.h"
@interface AIRMapPolyline: MKAnnotationView <MKOverlay>
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, strong) MKPolyline *polyline;
@property (nonatomic, strong) MKOverlayPathRenderer *renderer;
@property (nonatomic, strong) NSArray<AIRMapCoordinate *> *coordinates;
@property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, strong) UIColor *strokeColor;
@property (nonatomic, strong) NSArray<UIColor *> *strokeColors;
@property (nonatomic, assign) CGFloat strokeWidth;
@property (nonatomic, assign) CGFloat miterLimit;
@property (nonatomic, assign) CGLineCap lineCap;
@property (nonatomic, assign) CGLineJoin lineJoin;
@property (nonatomic, assign) CGFloat lineDashPhase;
@property (nonatomic, strong) NSArray <NSNumber *> *lineDashPattern;
@property (nonatomic, assign) BOOL geodesic;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
#pragma mark MKOverlay protocol
@property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(nonatomic, readonly) MKMapRect boundingMapRect;
- (BOOL)intersectsMapRect:(MKMapRect)mapRect;
- (BOOL)canReplaceMapContent;
@end
@@ -0,0 +1,161 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import "AIRMapPolyline.h"
#import "AIRMapPolylineRenderer.h"
#import <React/UIView+React.h>
@implementation AIRMapPolyline {
}
- (void)setFillColor:(UIColor *)fillColor {
_fillColor = fillColor;
[self update];
}
- (void)setStrokeColor:(UIColor *)strokeColor {
_strokeColor = strokeColor;
[self update];
}
- (void)setStrokeColors:(NSArray<UIColor *> *)strokeColors {
_strokeColors = strokeColors;
if ((self.renderer != nil) && ![_renderer isKindOfClass:[AIRMapPolylineRenderer class]]) {
self.renderer = [self createRenderer];
}
[self update];
}
- (void)setStrokeWidth:(CGFloat)strokeWidth {
_strokeWidth = strokeWidth;
[self update];
}
- (void)setLineJoin:(CGLineJoin)lineJoin {
_lineJoin = lineJoin;
[self update];
}
- (void)setLineCap:(CGLineCap)lineCap {
_lineCap = lineCap;
[self update];
}
- (void)setMiterLimit:(CGFloat)miterLimit {
_miterLimit = miterLimit;
[self update];
}
- (void)setLineDashPhase:(CGFloat)lineDashPhase {
_lineDashPhase = lineDashPhase;
[self update];
}
- (void)setLineDashPattern:(NSArray <NSNumber *> *)lineDashPattern {
_lineDashPattern = lineDashPattern;
[self update];
}
-(void)setGeodesic:(BOOL)geodesic
{
_geodesic = geodesic;
if(_coordinates){
[self setCoordinates:_coordinates];
}
}
- (void)setCoordinates:(NSArray<AIRMapCoordinate *> *)coordinates {
_coordinates = coordinates;
CLLocationCoordinate2D *coords = calloc(coordinates.count, sizeof(CLLocationCoordinate2D));
for(int i = 0; i < coordinates.count; i++)
{
coords[i] = coordinates[i].coordinate;
}
if(_geodesic){
self.polyline = [MKGeodesicPolyline polylineWithCoordinates:coords count:coordinates.count];
} else {
self.polyline = [MKPolyline polylineWithCoordinates:coords count:coordinates.count];
}
free(coords);
self.renderer = [self createRenderer];
[self update];
}
- (MKOverlayPathRenderer*)createRenderer {
if (self.polyline == nil) return nil;
if (self.strokeColors == nil) {
// Use the default renderer when no array of stroke-colors is defined.
// This behaviour may be changed in the future if we permanently want to
// use the custom renderer, because it can add funtionality that is not
// supported by the default renderer.
return [[MKPolylineRenderer alloc] initWithPolyline:self.polyline];
}
else {
return [[AIRMapPolylineRenderer alloc] initWithOverlay:self polyline:self.polyline];
}
}
- (void) update
{
if (!_renderer) return;
[self updateRenderer:_renderer];
if (_map == nil) return;
[_map removeOverlay:self];
[_map addOverlay:self];
}
- (void) updateRenderer:(MKOverlayPathRenderer*)renderer {
renderer.fillColor = _fillColor;
renderer.strokeColor = _strokeColor;
renderer.lineWidth = _strokeWidth;
renderer.lineCap = _lineCap;
renderer.lineJoin = _lineJoin;
renderer.miterLimit = _miterLimit;
renderer.lineDashPhase = _lineDashPhase;
renderer.lineDashPattern = _lineDashPattern;
if ([renderer isKindOfClass:[AIRMapPolylineRenderer class]]) {
((AIRMapPolylineRenderer*)renderer).strokeColors = _strokeColors;
}
}
#pragma mark MKOverlay implementation
- (CLLocationCoordinate2D) coordinate
{
return self.polyline.coordinate;
}
- (MKMapRect) boundingMapRect
{
return self.polyline.boundingMapRect;
}
- (BOOL)intersectsMapRect:(MKMapRect)mapRect
{
BOOL answer = [self.polyline intersectsMapRect:mapRect];
return answer;
}
- (BOOL)canReplaceMapContent
{
return NO;
}
#pragma mark AIRMapSnapshot implementation
- (void) drawToSnapshot:(MKMapSnapshot *) snapshot context:(CGContextRef) context
{
AIRMapPolylineRenderer* renderer = [[AIRMapPolylineRenderer alloc] initWithSnapshot:snapshot overlay:self polyline:self.polyline];
[self updateRenderer:renderer];
[renderer drawWithZoomScale:2 inContext:context];
}
@end
@@ -0,0 +1,10 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapPolylineManager : RCTViewManager
@end
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapPolylineManager.h"
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "RCTConvert+AirMap.h"
#import "AIRMapMarker.h"
#import "AIRMapPolyline.h"
@interface AIRMapPolylineManager()
@end
@implementation AIRMapPolylineManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapPolyline *polyline = [AIRMapPolyline new];
return polyline;
}
RCT_EXPORT_VIEW_PROPERTY(coordinates, AIRMapCoordinateArray)
RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeColors, UIColorArray)
RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineCap, CGLineCap)
RCT_EXPORT_VIEW_PROPERTY(lineJoin, CGLineJoin)
RCT_EXPORT_VIEW_PROPERTY(miterLimit, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineDashPhase, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineDashPattern, NSArray)
RCT_EXPORT_VIEW_PROPERTY(geodesic, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
@end
@@ -0,0 +1,19 @@
//
// AIRMapPolylineRenderer.h
// mapDemo
//
// Created by IjzerenHein on 13-11-21.
// Copyright (c) 2017 IjzerenHein. All rights reserved.
//
#import <MapKit/MapKit.h>
@interface AIRMapPolylineRenderer : MKOverlayPathRenderer
-(id)initWithOverlay:(id<MKOverlay>)overlay polyline:(MKPolyline*)polyline;
-(id)initWithSnapshot:(MKMapSnapshot*)snapshot overlay:(id<MKOverlay>)overlay polyline:(MKPolyline*)polyline;
-(void)drawWithZoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context;
@property (nonatomic, strong) NSArray<UIColor *> *strokeColors;
@end
@@ -0,0 +1,240 @@
//
// AIRMapPolylineRenderer.h
// mapDemo
//
// Created by IjzerenHein on 13-11-21.
// Copyright (c) 2017 IjzerenHein. All rights reserved.
//
#import "AIRMapPolylineRenderer.h"
@interface AIRMapPolylineRendererSegment : NSObject
- (id)initWithPoint:(CGPoint)point color:(UIColor*)color;
- (void) addPoint:(CGPoint)point color:(UIColor*)color;
@property CGMutablePathRef path;
@property UIColor *startColor;
@property UIColor *endColor;
@property CGPoint startPoint;
@property CGPoint endPoint;
@end
@implementation AIRMapPolylineRendererSegment
- (id)initWithPoint:(CGPoint)point color:(UIColor*)color
{
self = [super init];
if (self){
self.path = CGPathCreateMutable();
self.startColor = color;
self.startPoint = point;
self.endPoint = point;
CGPathMoveToPoint(self.path, nil, point.x, point.y);
}
return self;
}
- (void) addPoint:(CGPoint)point color:(UIColor*)color
{
self.endPoint = point;
self.endColor = color;
CGPathAddLineToPoint(self.path, nil, point.x, point.y);
}
@end
@implementation AIRMapPolylineRenderer {
MKPolyline* _polyline;
NSArray<UIColor *> *_strokeColors;
MKMapSnapshot* _snapshot;
CLLocationCoordinate2D* _coordinates;
}
@synthesize strokeColors;
- (id)initWithOverlay:(id<MKOverlay>)overlay polyline:(MKPolyline*)polyline
{
self = [super initWithOverlay:overlay];
if (self){
_polyline = polyline;
[self createPath];
}
return self;
}
- (id)initWithSnapshot:(MKMapSnapshot*)snapshot overlay:(id<MKOverlay>)overlay polyline:(MKPolyline*)polyline
{
self = [super initWithOverlay:overlay];
if (self){
_snapshot = snapshot;
_polyline = polyline;
_coordinates = malloc(sizeof(CLLocationCoordinate2D) * [_polyline pointCount]);
[_polyline getCoordinates:_coordinates range:NSMakeRange(0, [_polyline pointCount])];
}
return self;
}
- (void) dealloc
{
if (_coordinates) free(_coordinates);
}
- (CGPoint) pointForIndex:(NSUInteger)index
{
if (_snapshot != nil) {
return [_snapshot pointForCoordinate:_coordinates[index]];
}
else {
return [self pointForMapPoint:_polyline.points[index]];
}
}
- (UIColor*) colorForIndex:(NSUInteger)index
{
if ((_strokeColors == nil) || !_strokeColors.count) return self.strokeColor;
index = MIN(index, _strokeColors.count - 1);
UIColor* color = _strokeColors[index];
CGFloat pc_r,pc_g,pc_b,pc_a;
[color getRed:&pc_r green:&pc_g blue:&pc_b alpha:&pc_a];
return (pc_a == 0) ? nil : color;
}
- (void) createPath
{
CGMutablePathRef path = CGPathCreateMutable();
BOOL first = YES;
for (NSUInteger i = 0, n = _polyline.pointCount; i < n; i++){
CGPoint point = [self pointForIndex:i];
if (first) {
CGPathMoveToPoint(path, nil, point.x, point.y);
first = NO;
} else {
CGPathAddLineToPoint(path, nil, point.x, point.y);
}
}
self.path = path;
}
- (NSArray*) createSegments
{
NSMutableArray* segments = [NSMutableArray new];
if (_polyline.pointCount <= 1) return segments;
AIRMapPolylineRendererSegment* segment = nil;
for (NSUInteger i = 0, n = _polyline.pointCount; i < n; i++){
CGPoint point = [self pointForIndex:i];
UIColor* color = [self colorForIndex:i];
if (segment == nil) {
// Start new segment
segment = [[AIRMapPolylineRendererSegment alloc] initWithPoint:point color:color];
[segments addObject:segment];
}
else if (((color == nil) && (segment.endColor == nil)) ||
((color != nil) && [segment.startColor isEqual:color])) {
// Append point to segment
[segment addPoint:point color: color];
}
else {
// Close the last segment if needed
if (segment.endColor == nil) {
[segment addPoint:point color:color];
}
else {
// Add transition gradient
segment = [[AIRMapPolylineRendererSegment alloc] initWithPoint:segment.endPoint color:segment.endColor];
[segment addPoint:point color:color];
[segments addObject:segment];
}
// Start new segment
if (i < (n - 1)) {
segment = [[AIRMapPolylineRendererSegment alloc] initWithPoint:point color:color];
[segments addObject:segment];
}
}
}
// Remove last segment in case it only contains a single path point
if ((segment != nil) && (segment.endColor == nil)) {
[segments removeLastObject];
}
return segments;
}
- (void) setStrokeColors:(NSArray<UIColor *> *)strokeColors
{
if (_strokeColors != strokeColors) {
_strokeColors = strokeColors;
}
}
- (void) setStrokeColor:(UIColor *)strokeColor
{
if (super.strokeColor != strokeColor) {
super.strokeColor = strokeColor;
}
}
- (void) drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
{
CGRect pointsRect = CGPathGetBoundingBox(self.path);
CGRect mapRectCG = [self rectForMapRect:mapRect];
if (!CGRectIntersectsRect(pointsRect, mapRectCG)) return;
[self drawWithZoomScale:zoomScale inContext:context];
}
- (void) drawWithZoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
{
CGFloat lineWidth = (self.lineWidth / zoomScale) * 2.0;
CGContextSetLineWidth(context, lineWidth);
CGContextSetLineCap(context, self.lineCap);
CGContextSetLineJoin(context, self.lineJoin);
CGContextSetFillColorWithColor(context, self.fillColor.CGColor);
CGContextSetMiterLimit(context, self.miterLimit);
CGFloat dashes[self.lineDashPattern.count];
for (NSUInteger i = 0; i < self.lineDashPattern.count; i++) {
dashes[i] = self.lineDashPattern[i].floatValue;
}
CGContextSetLineDash(context, self.lineDashPhase, dashes, self.lineDashPattern.count);
NSArray* segments = [self createSegments];
for (NSUInteger i = 0, n = segments.count; i < n; i++) {
AIRMapPolylineRendererSegment* segment = segments[i];
CGContextBeginPath(context);
CGContextAddPath(context, segment.path);
// When segment has two colors, draw it as a gradient
if (![segment.startColor isEqual:segment.endColor]) {
CGFloat pc_r,pc_g,pc_b,pc_a,
cc_r,cc_g,cc_b,cc_a;
[segment.startColor getRed:&pc_r green:&pc_g blue:&pc_b alpha:&pc_a];
[segment.endColor getRed:&cc_r green:&cc_g blue:&cc_b alpha:&cc_a];
CGFloat gradientColors[8] = {pc_r,pc_g,pc_b,pc_a,
cc_r,cc_g,cc_b,cc_a};
CGFloat gradientLocation[2] = {0,1};
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradientColors, gradientLocation, 2);
CGColorSpaceRelease(colorSpace);
CGContextReplacePathWithStrokedPath(context);
CGContextClip(context);
CGContextDrawLinearGradient(
context,
gradient,
segment.startPoint,
segment.endPoint,
kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation
);
CGGradientRelease(gradient);
CGContextResetClip(context);
}
else {
CGContextSetStrokeColorWithColor(context, segment.startColor.CGColor);
CGContextStrokePath(context);
}
}
}
@end
@@ -0,0 +1,17 @@
//
// AIRMapSnapshot.h
// AirMaps
//
// Created by Hein Rutjes on 26/09/16.
// Copyright © 2016 Christopher. All rights reserved.
//
#ifndef AIRMapSnapshot_h
#define AIRMapSnapshot_h
@protocol AIRMapSnapshot <NSObject>
@optional
- (void) drawToSnapshot:(MKMapSnapshot *) snapshot context:(CGContextRef) context;
@end
#endif /* AIRMapSnapshot_h */
@@ -0,0 +1,57 @@
//
// AIRUrlTileOverlay.h
// AirMaps
//
// Created by cascadian on 3/19/16.
// Copyright © 2016. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import <React/RCTView.h>
#import "AIRMapCoordinate.h"
#import "AIRMap.h"
#import "RCTConvert+AirMap.h"
#import "AIRMapUrlTileCachedOverlay.h"
@interface AIRMapUrlTile : MKAnnotationView <MKOverlay> {
BOOL _urlTemplateSet;
BOOL _tileSizeSet;
BOOL _flipYSet;
BOOL _tileCachePathSet;
BOOL _tileCacheMaxAgeSet;
BOOL _maximumNativeZSet;
BOOL _cachedOverlayCreated;
BOOL _opacitySet;
}
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, strong) AIRMapUrlTileCachedOverlay *tileOverlay;
@property (nonatomic, strong) MKTileOverlayRenderer *renderer;
@property (nonatomic, copy) NSString *urlTemplate;
@property NSInteger maximumZ;
@property NSInteger maximumNativeZ;
@property NSInteger minimumZ;
@property BOOL flipY;
@property BOOL shouldReplaceMapContent;
@property NSInteger tileSize;
@property (nonatomic, copy) NSString *tileCachePath;
@property NSInteger tileCacheMaxAge;
@property BOOL offlineMode;
@property CGFloat opacity;
- (void)updateProperties;
- (void)update;
#pragma mark MKOverlay protocol
@property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(nonatomic, readonly) MKMapRect boundingMapRect;
//- (BOOL)intersectsMapRect:(MKMapRect)mapRect;
- (BOOL)canReplaceMapContent;
@end
@@ -0,0 +1,211 @@
//
// AIRUrlTileOverlay.m
// AirMaps
//
// Created by cascadian on 3/19/16.
// Copyright © 2016. All rights reserved.
//
#import "AIRMapUrlTile.h"
#import <React/UIView+React.h>
#import "AIRMapUrlTileCachedOverlay.h"
@implementation AIRMapUrlTile
- (void)setShouldReplaceMapContent:(BOOL)shouldReplaceMapContent
{
_shouldReplaceMapContent = shouldReplaceMapContent;
if (self.tileOverlay) {
self.tileOverlay.canReplaceMapContent = _shouldReplaceMapContent;
}
[self update];
}
- (void)setMaximumZ:(NSInteger)maximumZ
{
_maximumZ = maximumZ;
if (self.tileOverlay) {
self.tileOverlay.maximumZ = _maximumZ;
}
[self update];
}
- (void)setMaximumNativeZ:(NSInteger)maximumNativeZ
{
_maximumNativeZ = maximumNativeZ;
_maximumNativeZSet = YES;
if (_cachedOverlayCreated) {
self.tileOverlay.maximumNativeZ = _maximumNativeZ;
} else {
[self createTileOverlayAndRendererIfPossible];
}
[self update];
}
- (void)setMinimumZ:(NSInteger)minimumZ
{
_minimumZ = minimumZ;
if (self.tileOverlay) {
self.tileOverlay.minimumZ = _minimumZ;
}
[self update];
}
- (void)setFlipY:(BOOL)flipY
{
_flipY = flipY;
_flipYSet = YES;
if (self.tileOverlay) {
self.tileOverlay.geometryFlipped = _flipY;
}
[self update];
}
- (void)setUrlTemplate:(NSString *)urlTemplate
{
_urlTemplate = urlTemplate;
_urlTemplateSet = YES;
[self createTileOverlayAndRendererIfPossible];
[self update];
}
- (void)setTileSize:(NSInteger)tileSize
{
_tileSize = tileSize;
_tileSizeSet = YES;
[self createTileOverlayAndRendererIfPossible];
[self update];
}
- (void)setTileCachePath:(NSString *)tileCachePath{
if (!tileCachePath) return;
_tileCachePath = tileCachePath;
_tileCachePathSet = YES;
[self createTileOverlayAndRendererIfPossible];
[self update];
}
- (void)setTileCacheMaxAge:(NSInteger)tileCacheMaxAge{
_tileCacheMaxAge = tileCacheMaxAge;
_tileCacheMaxAgeSet = YES;
if (_cachedOverlayCreated) {
self.tileOverlay.tileCacheMaxAge = _tileCacheMaxAge;
} else {
[self createTileOverlayAndRendererIfPossible];
}
[self update];
}
- (void)setOfflineMode:(BOOL)offlineMode
{
_offlineMode = offlineMode;
if (_cachedOverlayCreated) {
self.tileOverlay.offlineMode = _offlineMode;
}
if (self.renderer) [self.renderer reloadData];
}
- (void)setOpacity:(CGFloat)opacity
{
_opacity = opacity;
_opacitySet = YES;
if (self.renderer) {
self.renderer.alpha = opacity;
} else {
[self createTileOverlayAndRendererIfPossible];
}
[self update];
}
- (void)createTileOverlayAndRendererIfPossible
{
if (!_urlTemplateSet) return;
if (_tileCachePathSet || _maximumNativeZSet) {
NSLog(@"tileCache dir %@", _tileCachePath);
self.tileOverlay = [[AIRMapUrlTileCachedOverlay alloc] initWithURLTemplate:self.urlTemplate];
_cachedOverlayCreated = YES;
if (_tileCachePathSet) {
NSURL *urlPath = [NSURL URLWithString:[self.tileCachePath stringByAppendingString:@"/"]];
if (urlPath.fileURL) {
self.tileOverlay.tileCachePath = urlPath;
} else {
NSURL *filePath = [NSURL fileURLWithPath:self.tileCachePath isDirectory:YES];
self.tileOverlay.tileCachePath = filePath;
}
if (_tileCacheMaxAgeSet) {
self.tileOverlay.tileCacheMaxAge = self.tileCacheMaxAge;
}
}
} else {
NSLog(@"tileCache normal overlay");
self.tileOverlay = [[MKTileOverlay alloc] initWithURLTemplate:self.urlTemplate];
_cachedOverlayCreated = NO;
}
[self updateProperties];
self.renderer = [[MKTileOverlayRenderer alloc] initWithTileOverlay:self.tileOverlay];
if (_opacitySet) {
self.renderer.alpha = self.opacity;
}
}
- (void)updateProperties
{
self.tileOverlay.canReplaceMapContent = self.shouldReplaceMapContent;
if(self.minimumZ) {
self.tileOverlay.minimumZ = self.minimumZ;
}
if (self.maximumZ) {
self.tileOverlay.maximumZ = self.maximumZ;
}
if (_cachedOverlayCreated && self.maximumNativeZ) {
self.tileOverlay.maximumNativeZ = self.maximumNativeZ;
}
if (_flipYSet) {
self.tileOverlay.geometryFlipped = self.flipY;
}
if (_tileSizeSet) {
self.tileOverlay.tileSize = CGSizeMake(self.tileSize, self.tileSize);
}
if (_cachedOverlayCreated && self.offlineMode) {
self.tileOverlay.offlineMode = self.offlineMode;
}
}
- (void)update
{
if (!_renderer) return;
if (_map == nil) return;
[_map removeOverlay:self];
[_map addOverlay:self level:MKOverlayLevelAboveLabels];
for (id<MKOverlay> overlay in _map.overlays) {
if ([overlay isKindOfClass:[AIRMapUrlTile class]]) {
continue;
}
[_map removeOverlay:overlay];
[_map addOverlay:overlay];
}
}
#pragma mark MKOverlay implementation
- (CLLocationCoordinate2D)coordinate
{
return self.tileOverlay.coordinate;
}
- (MKMapRect)boundingMapRect
{
return self.tileOverlay.boundingMapRect;
}
- (BOOL)canReplaceMapContent
{
return self.tileOverlay.canReplaceMapContent;
}
@end
@@ -0,0 +1,17 @@
//
// AIRMapUrlTileCachedOverlay.h
// Airmaps
//
// Created by Markus Suomi on 10/04/2021.
//
#import <MapKit/MapKit.h>
@interface AIRMapUrlTileCachedOverlay : MKTileOverlay
@property NSInteger maximumNativeZ;
@property (nonatomic, copy) NSURL *tileCachePath;
@property NSInteger tileCacheMaxAge;
@property BOOL offlineMode;
@end
@@ -0,0 +1,235 @@
//
// AIRMapUrlTileCachedOverlay.m
// Airmaps
//
// Created by Markus Suomi on 10/04/2021.
//
#import "AIRMapUrlTileCachedOverlay.h"
@interface AIRMapUrlTileCachedOverlay ()
@end
@implementation AIRMapUrlTileCachedOverlay {
CIContext *_ciContext;
CGColorSpaceRef _colorspace;
NSURLSession *_urlSession;
}
- (void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result
{
if (!result) return;
NSInteger maximumZ = self.maximumNativeZ ? self.maximumNativeZ : path.z;
[self scaleIfNeededLowerZoomTile:path maximumZ:maximumZ result:^(NSData *image, NSError *error) {
if (!image && self.offlineMode && self.tileCachePath) {
NSInteger zoomLevelToStart = (path.z > maximumZ) ? maximumZ - 1 : path.z - 1;
NSInteger minimumZoomToSearch = self.minimumZ >= zoomLevelToStart - 3 ? self.minimumZ : zoomLevelToStart - 3;
[self findLowerZoomTileAndScale:path tryZ:zoomLevelToStart minZ:minimumZoomToSearch result:result];
} else {
result(image, error);
}
}];
}
- (void)scaleIfNeededLowerZoomTile:(MKTileOverlayPath)path maximumZ:(NSInteger)maximumZ result:(void (^)(NSData *, NSError *))result
{
NSInteger overZoomLevel = path.z - maximumZ;
if (overZoomLevel <= 0) {
[self getTileImage:path result:result];
return;
}
NSInteger zoomFactor = 1 << overZoomLevel;
MKTileOverlayPath parentTile;
parentTile.x = path.x >> overZoomLevel;
parentTile.y = path.y >> overZoomLevel;
parentTile.z = path.z - overZoomLevel;
parentTile.contentScaleFactor = path.contentScaleFactor;
NSInteger xOffset = path.x % zoomFactor;
NSInteger yOffset = path.y % zoomFactor;
NSInteger subTileSize = self.tileSize.width / zoomFactor;
if (!_ciContext) _ciContext = [CIContext context];
if (!_colorspace) _colorspace = CGColorSpaceCreateDeviceRGB();
[self getTileImage:parentTile result:^(NSData *image, NSError *error) {
if (!image) {
result(nil, nil);
return;
}
CIImage* originalCIImage = [CIImage imageWithData:image];
CGRect rect;
rect.origin.x = xOffset * subTileSize;
rect.origin.y = self.tileSize.width - (yOffset + 1) * subTileSize;
rect.size.width = subTileSize;
rect.size.height = subTileSize;
CIVector *inputRect = [CIVector vectorWithCGRect:rect];
CIFilter* cropFilter = [CIFilter filterWithName:@"CICrop"];
[cropFilter setValue:originalCIImage forKey:@"inputImage"];
[cropFilter setValue:inputRect forKey:@"inputRectangle"];
CGAffineTransform trans = CGAffineTransformMakeScale(zoomFactor, zoomFactor);
CIImage* scaledCIImage = [cropFilter.outputImage imageByApplyingTransform:trans];
NSData *finalImage = [_ciContext PNGRepresentationOfImage:scaledCIImage format:kCIFormatABGR8 colorSpace:_colorspace options:nil];
result(finalImage, nil);
}];
}
- (void)findLowerZoomTileAndScale:(MKTileOverlayPath)path tryZ:(NSInteger)tryZ minZ:(NSInteger)minZ result:(void (^)(NSData *, NSError *))result
{
[self scaleIfNeededLowerZoomTile:path maximumZ:tryZ result:^(NSData *image, NSError *error) {
if (image) {
result(image, error);
} else if (tryZ >= minZ) {
[self findLowerZoomTileAndScale:path tryZ:tryZ - 1 minZ:minZ result:result];
} else {
result(nil, nil);
}
}];
}
- (void)getTileImage:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result
{
NSData *image;
NSURL *tileCacheFileDirectory = [NSURL URLWithString:[NSString stringWithFormat:@"%d/%d/", (int)path.z, (int)path.x] relativeToURL:self.tileCachePath];
NSURL *tileCacheFilePath = [NSURL URLWithString:[NSString stringWithFormat:@"%d", (int)path.y] relativeToURL:tileCacheFileDirectory];
if (self.tileCachePath) {
image = [self readTileImage:path fromFilePath:tileCacheFilePath];
if (image) {
result(image, nil);
if (!self.offlineMode && self.tileCacheMaxAge) {
[self checkForRefresh:path fromFilePath:tileCacheFilePath];
}
}
}
if (!image) {
if (!self.offlineMode) {
[self fetchTile:path result:^(NSData *image, NSError *error) {
result(image, error);
if (image && self.tileCachePath) {
[self writeTileImage:tileCacheFileDirectory withTileCacheFilePath:tileCacheFilePath withTileData:image];
}
}];
} else {
result(nil, nil);
}
}
}
- (NSData *)readTileImage:(MKTileOverlayPath)path fromFilePath:(NSURL *)tileCacheFilePath
{
NSError *error;
if ([[NSFileManager defaultManager] fileExistsAtPath:[tileCacheFilePath path]]) {
if (!self.tileCacheMaxAge) {
[[NSFileManager defaultManager] setAttributes:@{NSFileModificationDate:[NSDate date]}
ofItemAtPath:[tileCacheFilePath path]
error:&error];
}
NSData *tile = [NSData dataWithContentsOfFile:[tileCacheFilePath path]];
NSLog(@"tileCache HIT for %d_%d_%d", (int)path.z, (int)path.x, (int)path.y);
NSLog(@"tileCache HIT, with max age set at %d", self.tileCacheMaxAge);
return tile;
} else {
NSLog(@"tileCache MISS for %d_%d_%d", (int)path.z, (int)path.x, (int)path.y);
return nil;
}
}
- (void)fetchTile:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result
{
if (!_urlSession) [self createURLSession];
[[_urlSession dataTaskWithURL:[self URLForTilePath:path]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
result(data, error);
}] resume];
}
- (void)writeTileImage:(NSURL *)tileCacheFileDirectory withTileCacheFilePath:(NSURL *)tileCacheFilePath withTileData:(NSData *)data
{
NSError *error;
if (![[NSFileManager defaultManager] fileExistsAtPath:[tileCacheFileDirectory path]]) {
[[NSFileManager defaultManager] createDirectoryAtPath:[tileCacheFileDirectory path] withIntermediateDirectories:YES attributes:nil error:&error];
if (error) {
NSLog(@"Error: %@", error);
return;
}
}
[[NSFileManager defaultManager] createFileAtPath:[tileCacheFilePath path] contents:data attributes:nil];
NSLog(@"tileCache SAVED tile %@", [tileCacheFilePath path]);
}
- (void)createTileCacheDirectory
{
NSError *error;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *tileCacheBaseDirectory = [NSString stringWithFormat:@"%@/tileCache", documentsDirectory];
self.tileCachePath = [NSURL fileURLWithPath:tileCacheBaseDirectory isDirectory:YES];
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.tileCachePath path]])
[[NSFileManager defaultManager] createDirectoryAtPath:[self.tileCachePath path] withIntermediateDirectories:NO attributes:nil error:&error];
}
- (void)createURLSession
{
if (!_urlSession) {
_urlSession = [NSURLSession sharedSession];
}
}
- (void)checkForRefresh:(MKTileOverlayPath)path fromFilePath:(NSURL *)tileCacheFilePath
{
if ([self doesFileNeedRefresh:path fromFilePath:tileCacheFilePath withMaxAge:self.tileCacheMaxAge]) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^ {
// This code runs asynchronously!
if ([self doesFileNeedRefresh:path fromFilePath:tileCacheFilePath withMaxAge:self.tileCacheMaxAge]) {
if (!_urlSession) [self createURLSession];
[[_urlSession dataTaskWithURL:[self URLForTilePath:path]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
if (!error) {
[[NSFileManager defaultManager] createFileAtPath:[tileCacheFilePath path] contents:data attributes:nil];
NSLog(@"tileCache File refreshed at %@", [tileCacheFilePath path]);
}
}] resume];
}
});
}
}
- (BOOL)doesFileNeedRefresh:(MKTileOverlayPath)path fromFilePath:(NSURL *)tileCacheFilePath withMaxAge:(NSInteger)tileCacheMaxAge
{
NSError *error;
NSDictionary<NSFileAttributeKey, id> *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[tileCacheFilePath path] error:&error];
if (fileAttributes) {
NSDate *modificationDate = fileAttributes[@"NSFileModificationDate"];
if (modificationDate) {
if (-1 * (int)modificationDate.timeIntervalSinceNow > tileCacheMaxAge) {
return YES;
}
}
}
return NO;
}
@end
@@ -0,0 +1,14 @@
//
// AIRMapUrlTileManager.h
// AirMaps
//
// Created by cascadian on 3/19/16.
// Copyright © 2016. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapUrlTileManager : RCTViewManager
@end
@@ -0,0 +1,47 @@
//
// AIRMapUrlTileManager.m
// AirMaps
//
// Created by cascadian on 3/19/16.
// Copyright © 2016. All rights reserved.
//
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
#import "AIRMapUrlTile.h"
#import "AIRMapUrlTileManager.h"
@interface AIRMapUrlTileManager()
@end
@implementation AIRMapUrlTileManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapUrlTile *tile = [AIRMapUrlTile new];
return tile;
}
RCT_EXPORT_VIEW_PROPERTY(urlTemplate, NSString)
RCT_EXPORT_VIEW_PROPERTY(maximumZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(maximumNativeZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(minimumZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(flipY, BOOL)
RCT_EXPORT_VIEW_PROPERTY(shouldReplaceMapContent, BOOL)
RCT_EXPORT_VIEW_PROPERTY(tileSize, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(tileCachePath, NSString)
RCT_EXPORT_VIEW_PROPERTY(tileCacheMaxAge, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(offlineMode, BOOL)
RCT_EXPORT_VIEW_PROPERTY(opacity, CGFloat)
@end
@@ -0,0 +1,35 @@
//
// AIRMapWMSTile.h
// AirMaps
//
// Created by nizam on 10/28/18.
// Copyright © 2018. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import <React/RCTView.h>
#import "AIRMapCoordinate.h"
#import "AIRMap.h"
#import "RCTConvert+AirMap.h"
#import "AIRMapUrlTile.h"
#import "AIRMapUrlTileCachedOverlay.h"
@interface AIRMapWMSTile : AIRMapUrlTile <MKOverlay>
@end
@interface AIRMapWMSTileOverlay : MKTileOverlay
@end
@interface AIRMapWMSTileCachedOverlay : AIRMapUrlTileCachedOverlay
@end
@interface AIRMapWMSTileHelper : NSObject
+ (NSURL *)URLForTilePath:(MKTileOverlayPath)path withURLTemplate:(NSString *)URLTemplate withTileSize:(NSInteger)tileSize;
@end
@@ -0,0 +1,116 @@
//
// AIRMapWMSTile.m
// AirMaps
//
// Created by nizam on 10/28/18.
// Copyright © 2018. All rights reserved.
//
#import "AIRMapWMSTile.h"
#import <React/UIView+React.h>
@implementation AIRMapWMSTile
- (void)createTileOverlayAndRendererIfPossible
{
if (!_urlTemplateSet) return;
if (_tileCachePathSet || _maximumNativeZSet) {
NSLog(@"tileCache new overlay dir %@", self.tileCachePath);
NSLog(@"tileCache %d", _tileCachePathSet);
NSLog(@"tileCache %d", _maximumNativeZSet);
self.tileOverlay = [[AIRMapWMSTileCachedOverlay alloc] initWithURLTemplate:self.urlTemplate];
_cachedOverlayCreated = YES;
if (_tileCachePathSet) {
NSURL *urlPath = [NSURL URLWithString:[self.tileCachePath stringByAppendingString:@"/"]];
if (urlPath.fileURL) {
self.tileOverlay.tileCachePath = urlPath;
} else {
NSURL *filePath = [NSURL fileURLWithPath:self.tileCachePath isDirectory:YES];
self.tileOverlay.tileCachePath = filePath;
}
if (_tileCacheMaxAgeSet) {
self.tileOverlay.tileCacheMaxAge = self.tileCacheMaxAge;
}
}
} else {
NSLog(@"tileCache normal overlay");
self.tileOverlay = [[AIRMapWMSTileOverlay alloc] initWithURLTemplate:self.urlTemplate];
_cachedOverlayCreated = NO;
}
[self updateProperties];
self.renderer = [[MKTileOverlayRenderer alloc] initWithTileOverlay:self.tileOverlay];
if (_opacitySet) {
self.renderer.alpha = self.opacity;
}
}
@end
@implementation AIRMapWMSTileOverlay
- (id)initWithURLTemplate:(NSString *)URLTemplate
{
self = [super initWithURLTemplate:URLTemplate];
return self;
}
- (NSURL *)URLForTilePath:(MKTileOverlayPath)path
{
return [AIRMapWMSTileHelper URLForTilePath:path withURLTemplate:self.URLTemplate withTileSize:self.tileSize.width];
}
@end
@implementation AIRMapWMSTileCachedOverlay
- (id)initWithURLTemplate:(NSString *)URLTemplate
{
self = [super initWithURLTemplate:URLTemplate];
return self;
}
- (NSURL *)URLForTilePath:(MKTileOverlayPath)path
{
return [AIRMapWMSTileHelper URLForTilePath:path withURLTemplate:self.URLTemplate withTileSize:self.tileSize.width];
}
@end
@implementation AIRMapWMSTileHelper
+ (NSURL *)URLForTilePath:(MKTileOverlayPath)path withURLTemplate:(NSString *)URLTemplate withTileSize:(NSInteger)tileSize
{
NSArray *bb = [self getBoundBox:path.x yAxis:path.y zoom:path.z];
NSMutableString *url = [URLTemplate mutableCopy];
[url replaceOccurrencesOfString: @"{minX}" withString:[NSString stringWithFormat:@"%@", bb[0]] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{minY}" withString:[NSString stringWithFormat:@"%@", bb[1]] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{maxX}" withString:[NSString stringWithFormat:@"%@", bb[2]] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{maxY}" withString:[NSString stringWithFormat:@"%@", bb[3]] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{width}" withString:[NSString stringWithFormat:@"%d", (int)tileSize] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{height}" withString:[NSString stringWithFormat:@"%d", (int)tileSize] options:0 range:NSMakeRange(0, url.length)];
return [NSURL URLWithString:url];
}
+ (NSArray *)getBoundBox:(NSInteger)x yAxis:(NSInteger)y zoom:(NSInteger)zoom
{
double MapX = -20037508.34789244;
double MapY = 20037508.34789244;
double FULL = 20037508.34789244 * 2;
double tile = FULL / pow(2.0, (double)zoom);
NSArray *result =[[NSArray alloc] initWithObjects:
[NSNumber numberWithDouble:MapX + (double)x * tile],
[NSNumber numberWithDouble:MapY - (double)(y + 1) * tile],
[NSNumber numberWithDouble:MapX + (double)(x + 1) * tile],
[NSNumber numberWithDouble:MapY - (double)y * tile],
nil];
return result;
}
@end
@@ -0,0 +1,14 @@
//
// AIRMapWMSTileManager.h
// AirMaps
//
// Created by nizam on 10/28/18.
// Copyright © 2018. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapWMSTileManager : RCTViewManager
@end
@@ -0,0 +1,47 @@
//
// AIRMapWMSTileManager.m
// AirMaps
//
// Created by nizam on 10/28/18.
// Copyright © 2018. All rights reserved.
//
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
#import "AIRMapWMSTile.h"
#import "AIRMapWMSTileManager.h"
@interface AIRMapWMSTileManager()
@end
@implementation AIRMapWMSTileManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapWMSTile *tile = [AIRMapWMSTile new];
return tile;
}
RCT_EXPORT_VIEW_PROPERTY(urlTemplate, NSString)
RCT_EXPORT_VIEW_PROPERTY(maximumZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(maximumNativeZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(minimumZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(shouldReplaceMapContent, BOOL)
RCT_EXPORT_VIEW_PROPERTY(tileSize, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(tileCachePath, NSString)
RCT_EXPORT_VIEW_PROPERTY(tileCacheMaxAge, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(offlineMode, BOOL)
RCT_EXPORT_VIEW_PROPERTY(opacity, CGFloat)
@end
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
#import "AIRMap.h"
NS_ASSUME_NONNULL_BEGIN
@interface AIRWeakMapReference : NSObject
@property (nonatomic, weak) AIRMap *mapView;
- (instancetype)initWithMapView:(AIRMap *)mapView;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRWeakMapReference.h"
@implementation AIRWeakMapReference
- (instancetype)initWithMapView:(AIRMap *)mapView {
self = [super init];
if (self) {
_mapView = mapView;
}
return self;
}
@end
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AIRWeakTimerReference : NSObject
- (instancetype)initWithTarget:(id)target andSelector:(SEL)selector;
- (void)timerDidFire:(NSTimer *)timer;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRWeakTimerReference.h"
@implementation AIRWeakTimerReference
{
__weak NSObject *_target;
SEL _selector;
}
- (instancetype)initWithTarget:(id)target andSelector:(SEL)selector {
self = [super init];
if (self) {
_target = target;
_selector = selector;
}
return self;
}
- (void)timerDidFire:(NSTimer *)timer
{
if(_target)
{
[_target performSelector:_selector withObject:timer];
}
else
{
[timer invalidate];
}
}
@end
@@ -0,0 +1,204 @@
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
/*
SMCalloutView
-------------
Created by Nick Farina (nfarina@gmail.com)
Version 2.1.2
*/
/// options for which directions the callout is allowed to "point" in.
typedef NS_OPTIONS(NSUInteger, SMCalloutArrowDirection) {
SMCalloutArrowDirectionUp = 1 << 0,
SMCalloutArrowDirectionDown = 1 << 1,
SMCalloutArrowDirectionAny = SMCalloutArrowDirectionUp | SMCalloutArrowDirectionDown
};
/// options for the callout present/dismiss animation
typedef NS_ENUM(NSInteger, SMCalloutAnimation) {
/// the "bounce" animation we all know and love from @c UIAlertView
SMCalloutAnimationBounce,
/// a simple fade in or out
SMCalloutAnimationFade,
/// grow or shrink linearly, like in the iPad Calendar app
SMCalloutAnimationStretch
};
NS_ASSUME_NONNULL_BEGIN
/// when delaying our popup in order to scroll content into view, you can use this amount to match the
/// animation duration of UIScrollView when using @c -setContentOffset:animated.
extern NSTimeInterval const kSMCalloutViewRepositionDelayForUIScrollView;
@protocol SMCalloutViewDelegate;
@class SMCalloutBackgroundView;
//
// Callout view.
//
#if __IPHONE_OS_VERSION_MAX_ALLOWED < 100000
@interface SMCalloutView : UIView
#else
@interface SMCalloutView : UIView <CAAnimationDelegate>
#endif
@property (nonatomic, weak, nullable) id<SMCalloutViewDelegate> delegate;
/// title/titleView relationship mimics UINavigationBar.
@property (nonatomic, copy, nullable) NSString *title;
@property (nonatomic, copy, nullable) NSString *subtitle;
/// Left accessory view for the call out
@property (nonatomic, strong, nullable) UIView *leftAccessoryView;
/// Right accessoty view for the call out
@property (nonatomic, strong, nullable) UIView *rightAccessoryView;
/// Default @c SMCalloutArrowDirectionDown
@property (nonatomic, assign) SMCalloutArrowDirection permittedArrowDirection;
/// The current arrow direction
@property (nonatomic, readonly) SMCalloutArrowDirection currentArrowDirection;
/// if the @c UIView you're constraining to has portions that are overlapped by nav bar, tab bar, etc. you'll need to tell us those insets.
@property (nonatomic, assign) UIEdgeInsets constrainedInsets;
/// default is @c SMCalloutMaskedBackgroundView, or @c SMCalloutDrawnBackgroundView when using @c SMClassicCalloutView
@property (nonatomic, strong) SMCalloutBackgroundView *backgroundView;
/**
@brief Custom title view.
@disucssion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on titleView/subtitleView if defined, so your view
may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized.
@warning If this is set, the respective @c title property will be ignored.
*/
@property (nonatomic, strong, nullable) UIView *titleView;
/**
@brief Custom subtitle view.
@discussion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on subtitleView if defined, so your view
may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized.
@warning If this is set, the respective @c subtitle property will be ignored.
*/
@property (nonatomic, strong, nullable) UIView *subtitleView;
/// Custom "content" view that can be any width/height. If this is set, title/subtitle/titleView/subtitleView are all ignored.
@property (nonatomic, retain, nullable) UIView *contentView;
/// Custom content view margin
@property (nonatomic, assign) UIEdgeInsets contentViewInset;
/// calloutOffset is the offset in screen points from the top-middle of the target view, where the anchor of the callout should be shown.
@property (nonatomic, assign) CGPoint calloutOffset;
/// default SMCalloutAnimationBounce, SMCalloutAnimationFade respectively
@property (nonatomic, assign) SMCalloutAnimation presentAnimation, dismissAnimation;
/// Returns a new instance of SMCalloutView if running on iOS 7 or better, otherwise a new instance of SMClassicCalloutView if available.
+ (SMCalloutView *)platformCalloutView;
/**
@brief Presents a callout view by adding it to "inView" and pointing at the given rect of inView's bounds.
@discussion Constrains the callout to the bounds of the given view. Optionally scrolls the given rect into view (plus margins)
if @c -delegate is set and responds to @c -delayForRepositionWithSize.
@param rect @c CGRect to present the view from
@param view view to 'constrain' the @c constrainedView to
@param constrainedView @c UIView to be constrainted in @c view
@param animated @c BOOL if presentation should be animated
*/
- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated;
/**
@brief Present a callout layer in the `layer` and pointing at the given rect of the `layer` bounds
@discussion Same as the view-based presentation, but inserts the callout into a CALayer hierarchy instead.
@note Be aware that you'll have to direct your own touches to any accessory views, since CALayer doesn't relay touch events.
@param rect @c CGRect to present the view from
@param layer layer to 'constrain' the @c constrainedLayer to
@param constrainedLayer @c CALayer to be constrained in @c layer
@param animated @c BOOL if presentation should be animated
*/
- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated;
/**
Dismiss the callout view
@param animated @c BOOL if dismissal should be animated
*/
- (void)dismissCalloutAnimated:(BOOL)animated;
/// For subclassers. You can override this method to provide your own custom animation for presenting/dismissing the callout.
- (CAAnimation *)animationWithType:(SMCalloutAnimation)type presenting:(BOOL)presenting;
@end
//
// Background view - default draws the iOS 7 system background style (translucent white with rounded arrow).
//
/// Abstract base class
@interface SMCalloutBackgroundView : UIView
/// indicates where the tip of the arrow should be drawn, as a pixel offset
@property (nonatomic, assign) CGPoint arrowPoint;
/// will be set by the callout when the callout is in a highlighted state
@property (nonatomic, assign) BOOL highlighted;
/// returns an optional layer whose contents should mask the callout view's contents (not honored by @c SMClassicCalloutView )
@property (nonatomic, assign) CALayer *contentMask;
/// height of the callout "arrow"
@property (nonatomic, assign) CGFloat anchorHeight;
/// the smallest possible distance from the edge of our control to the "tip" of the anchor, from either left or right
@property (nonatomic, assign) CGFloat anchorMargin;
@end
/// Default for iOS 7, this reproduces the "masked" behavior of the iOS 7-style callout view.
/// Accessories are masked by the shape of the callout (including the arrow itself).
@interface SMCalloutMaskedBackgroundView : SMCalloutBackgroundView
@end
//
// Delegate methods
//
@protocol SMCalloutViewDelegate <NSObject>
@optional
/// Controls whether the callout "highlights" when pressed. default YES. You must also respond to @c -calloutViewClicked below.
/// Not honored by @c SMClassicCalloutView.
- (BOOL)calloutViewShouldHighlight:(SMCalloutView *)calloutView;
/// Called when the callout view is clicked. Not honored by @c SMClassicCalloutView.
- (void)calloutViewClicked:(SMCalloutView *)calloutView;
/**
Called when the callout view detects that it will be outside the constrained view when it appears,
or if the target rect was already outside the constrained view. You can implement this selector
to respond to this situation by repositioning your content first in order to make everything visible.
The @c CGSize passed is the calculated offset necessary to make everything visible (plus a nice margin).
It expects you to return the amount of time you need to reposition things so the popup can be delayed.
Typically you would return @c kSMCalloutViewRepositionDelayForUIScrollView if you're repositioning by calling @c [UIScrollView @c setContentOffset:animated:].
@param calloutView the @c SMCalloutView to reposition
@param offset calculated offset necessary to make everything visible
@returns @c NSTimeInterval to delay the repositioning
*/
- (NSTimeInterval)calloutView:(SMCalloutView *)calloutView delayForRepositionWithSize:(CGSize)offset;
/// Called before the callout view appears on screen, or before the appearance animation will start.
- (void)calloutViewWillAppear:(SMCalloutView *)calloutView;
/// Called after the callout view appears on screen, or after the appearance animation is complete.
- (void)calloutViewDidAppear:(SMCalloutView *)calloutView;
/// Called before the callout view is removed from the screen, or before the disappearance animation is complete.
- (void)calloutViewWillDisappear:(SMCalloutView *)calloutView;
/// Called after the callout view is removed from the screen, or after the disappearance animation is complete.
- (void)calloutViewDidDisappear:(SMCalloutView *)calloutView;
NS_ASSUME_NONNULL_END
@end
@@ -0,0 +1,858 @@
#import "SMCalloutView.h"
//
// UIView frame helpers - we do a lot of UIView frame fiddling in this class; these functions help keep things readable.
//
@interface UIView (SMFrameAdditions)
@property (nonatomic, assign) CGPoint frameOrigin;
@property (nonatomic, assign) CGSize frameSize;
@property (nonatomic, assign) CGFloat frameX, frameY, frameWidth, frameHeight; // normal rect properties
@property (nonatomic, assign) CGFloat frameLeft, frameTop, frameRight, frameBottom; // these will stretch/shrink the rect
@end
//
// Callout View.
//
#define CALLOUT_DEFAULT_CONTAINER_HEIGHT 44 // height of just the main portion without arrow
#define CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT 52 // height of just the main portion without arrow (when subtitle is present)
#define CALLOUT_MIN_WIDTH 61 // minimum width of system callout
#define TITLE_HMARGIN 12 // the title/subtitle view's normal horizontal margin from the edges of our callout view or from the accessories
#define TITLE_TOP 11 // the top of the title view when no subtitle is present
#define TITLE_SUB_TOP 4 // the top of the title view when a subtitle IS present
#define TITLE_HEIGHT 21 // title height, fixed
#define SUBTITLE_TOP 28 // the top of the subtitle, when present
#define SUBTITLE_HEIGHT 15 // subtitle height, fixed
#define BETWEEN_ACCESSORIES_MARGIN 7 // margin between accessories when no title/subtitle is present
#define TOP_ANCHOR_MARGIN 13 // all the above measurements assume a bottom anchor! if we're pointing "up" we'll need to add this top margin to everything.
#define COMFORTABLE_MARGIN 10 // when we try to reposition content to be visible, we'll consider this margin around your target rect
NSTimeInterval const kSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0;
@interface SMCalloutView ()
@property (nonatomic, strong) UIButton *containerView; // for masking and interaction
@property (nonatomic, strong) UILabel *titleLabel, *subtitleLabel;
@property (nonatomic, assign) SMCalloutArrowDirection currentArrowDirection;
@property (nonatomic, assign) BOOL popupCancelled;
@end
@implementation SMCalloutView
+ (SMCalloutView *)platformCalloutView {
// if you haven't compiled SMClassicCalloutView into your app, then we can't possibly create an instance of it!
if (!NSClassFromString(@"SMClassicCalloutView"))
return [SMCalloutView new];
// ok we have both - so choose the best one based on current platform
if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1)
return [SMCalloutView new]; // iOS 7+
else
return [NSClassFromString(@"SMClassicCalloutView") new];
}
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.permittedArrowDirection = SMCalloutArrowDirectionDown;
self.presentAnimation = SMCalloutAnimationBounce;
self.dismissAnimation = SMCalloutAnimationFade;
self.backgroundColor = [UIColor clearColor];
self.containerView = [UIButton new];
self.containerView.isAccessibilityElement = NO;
self.isAccessibilityElement = NO;
self.contentViewInset = UIEdgeInsetsMake(12, 12, 12, 12);
[self.containerView addTarget:self action:@selector(highlightIfNecessary) forControlEvents:UIControlEventTouchDown | UIControlEventTouchDragInside];
[self.containerView addTarget:self action:@selector(unhighlightIfNecessary) forControlEvents:UIControlEventTouchDragOutside | UIControlEventTouchCancel | UIControlEventTouchUpOutside | UIControlEventTouchUpInside];
[self.containerView addTarget:self action:@selector(calloutClicked) forControlEvents:UIControlEventTouchUpInside];
}
return self;
}
- (BOOL)supportsHighlighting {
if (![self.delegate respondsToSelector:@selector(calloutViewClicked:)])
return NO;
if ([self.delegate respondsToSelector:@selector(calloutViewShouldHighlight:)])
return [self.delegate calloutViewShouldHighlight:self];
return YES;
}
- (void)highlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = YES; }
- (void)unhighlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = NO; }
- (void)calloutClicked {
if ([self.delegate respondsToSelector:@selector(calloutViewClicked:)])
[self.delegate calloutViewClicked:self];
}
- (UIView *)titleViewOrDefault {
if (self.titleView)
// if you have a custom title view defined, return that.
return self.titleView;
else {
if (!self.titleLabel) {
// create a default titleView
self.titleLabel = [UILabel new];
self.titleLabel.frameHeight = TITLE_HEIGHT;
self.titleLabel.opaque = NO;
self.titleLabel.backgroundColor = [UIColor clearColor];
self.titleLabel.font = [UIFont systemFontOfSize:17];
self.titleLabel.textColor = [UIColor blackColor];
}
return self.titleLabel;
}
}
- (UIView *)subtitleViewOrDefault {
if (self.subtitleView)
// if you have a custom subtitle view defined, return that.
return self.subtitleView;
else {
if (!self.subtitleLabel) {
// create a default subtitleView
self.subtitleLabel = [UILabel new];
self.subtitleLabel.frameHeight = SUBTITLE_HEIGHT;
self.subtitleLabel.opaque = NO;
self.subtitleLabel.backgroundColor = [UIColor clearColor];
self.subtitleLabel.font = [UIFont systemFontOfSize:12];
self.subtitleLabel.textColor = [UIColor blackColor];
}
return self.subtitleLabel;
}
}
- (SMCalloutBackgroundView *)backgroundView {
// create our default background on first access only if it's nil, since you might have set your own background anyway.
return _backgroundView ? _backgroundView : (_backgroundView = [self defaultBackgroundView]);
}
- (SMCalloutBackgroundView *)defaultBackgroundView {
return [SMCalloutMaskedBackgroundView new];
}
- (void)rebuildSubviews {
// remove and re-add our appropriate subviews in the appropriate order
[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self.containerView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self setNeedsDisplay];
[self addSubview:self.backgroundView];
[self addSubview:self.containerView];
if (self.contentView) {
[self.containerView addSubview:self.contentView];
}
else {
if (self.titleViewOrDefault) [self.containerView addSubview:self.titleViewOrDefault];
if (self.subtitleViewOrDefault) [self.containerView addSubview:self.subtitleViewOrDefault];
}
if (self.leftAccessoryView) [self.containerView addSubview:self.leftAccessoryView];
if (self.rightAccessoryView) [self.containerView addSubview:self.rightAccessoryView];
}
// Accessory margins. Accessories are centered vertically when shorter
// than the callout, otherwise they grow from the upper corner.
- (CGFloat)leftAccessoryVerticalMargin {
if (self.leftAccessoryView.frameHeight < self.calloutContainerHeight)
return roundf((self.calloutContainerHeight - self.leftAccessoryView.frameHeight) / 2);
else
return 0;
}
- (CGFloat)leftAccessoryHorizontalMargin {
return fminf(self.leftAccessoryVerticalMargin, TITLE_HMARGIN);
}
- (CGFloat)rightAccessoryVerticalMargin {
if (self.rightAccessoryView.frameHeight < self.calloutContainerHeight)
return roundf((self.calloutContainerHeight - self.rightAccessoryView.frameHeight) / 2);
else
return 0;
}
- (CGFloat)rightAccessoryHorizontalMargin {
return fminf(self.rightAccessoryVerticalMargin, TITLE_HMARGIN);
}
- (CGFloat)innerContentMarginLeft {
if (self.leftAccessoryView)
return self.leftAccessoryHorizontalMargin + self.leftAccessoryView.frameWidth + TITLE_HMARGIN;
else
return self.contentViewInset.left;
}
- (CGFloat)innerContentMarginRight {
if (self.rightAccessoryView)
return self.rightAccessoryHorizontalMargin + self.rightAccessoryView.frameWidth + TITLE_HMARGIN;
else
return self.contentViewInset.right;
}
- (CGFloat)calloutHeight {
return self.calloutContainerHeight + self.backgroundView.anchorHeight;
}
- (CGFloat)calloutContainerHeight {
if (self.contentView)
return self.contentView.frameHeight + self.contentViewInset.bottom + self.contentViewInset.top;
else if (self.subtitleView || self.subtitle.length > 0)
return CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT;
else
return CALLOUT_DEFAULT_CONTAINER_HEIGHT;
}
- (CGSize)sizeThatFits:(CGSize)size {
// calculate how much non-negotiable space we need to reserve for margin and accessories
CGFloat margin = self.innerContentMarginLeft + self.innerContentMarginRight;
// how much room is left for text?
CGFloat availableWidthForText = size.width - margin - 1;
// no room for text? then we'll have to squeeze into the given size somehow.
if (availableWidthForText < 0)
availableWidthForText = 0;
CGSize preferredTitleSize = [self.titleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, TITLE_HEIGHT)];
CGSize preferredSubtitleSize = [self.subtitleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, SUBTITLE_HEIGHT)];
// total width we'd like
CGFloat preferredWidth;
if (self.contentView) {
// if we have a content view, then take our preferred size directly from that
preferredWidth = self.contentView.frameWidth + margin;
}
else if (preferredTitleSize.width >= 0.000001 || preferredSubtitleSize.width >= 0.000001) {
// if we have a title or subtitle, then our assumed margins are valid, and we can apply them
preferredWidth = fmaxf(preferredTitleSize.width, preferredSubtitleSize.width) + margin;
}
else {
// ok we have no title or subtitle to speak of. In this case, the system callout would actually not display
// at all! But we can handle it.
preferredWidth = self.leftAccessoryView.frameWidth + self.rightAccessoryView.frameWidth + self.leftAccessoryHorizontalMargin + self.rightAccessoryHorizontalMargin;
if (self.leftAccessoryView && self.rightAccessoryView)
preferredWidth += BETWEEN_ACCESSORIES_MARGIN;
}
// ensure we're big enough to fit our graphics!
preferredWidth = fmaxf(preferredWidth, CALLOUT_MIN_WIDTH);
// ask to be smaller if we have space, otherwise we'll fit into what we have by truncating the title/subtitle.
return CGSizeMake(fminf(preferredWidth, size.width), self.calloutHeight);
}
- (CGSize)offsetToContainRect:(CGRect)innerRect inRect:(CGRect)outerRect {
CGFloat nudgeRight = fmaxf(0, CGRectGetMinX(outerRect) - CGRectGetMinX(innerRect));
CGFloat nudgeLeft = fminf(0, CGRectGetMaxX(outerRect) - CGRectGetMaxX(innerRect));
CGFloat nudgeTop = fmaxf(0, CGRectGetMinY(outerRect) - CGRectGetMinY(innerRect));
CGFloat nudgeBottom = fminf(0, CGRectGetMaxY(outerRect) - CGRectGetMaxY(innerRect));
return CGSizeMake(nudgeLeft ? nudgeLeft : nudgeRight, nudgeTop ? nudgeTop : nudgeBottom);
}
- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated {
[self presentCalloutFromRect:rect inLayer:view.layer ofView:view constrainedToLayer:constrainedView.layer animated:animated];
}
- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated {
[self presentCalloutFromRect:rect inLayer:layer ofView:nil constrainedToLayer:constrainedLayer animated:animated];
}
// this private method handles both CALayer and UIView parents depending on what's passed.
- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer ofView:(UIView *)view constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated {
// Sanity check: dismiss this callout immediately if it's displayed somewhere
if (self.layer.superlayer) [self dismissCalloutAnimated:NO];
// cancel any presenting animation that may be in progress
[self.layer removeAnimationForKey:@"present"];
// figure out the constrained view's rect in our popup view's coordinate system
CGRect constrainedRect = [constrainedLayer convertRect:constrainedLayer.bounds toLayer:layer];
// apply our edge constraints
constrainedRect = UIEdgeInsetsInsetRect(constrainedRect, self.constrainedInsets);
constrainedRect = CGRectInset(constrainedRect, COMFORTABLE_MARGIN, COMFORTABLE_MARGIN);
// form our subviews based on our content set so far
[self rebuildSubviews];
// apply title/subtitle (if present
self.titleLabel.text = self.title;
self.subtitleLabel.text = self.subtitle;
// size the callout to fit the width constraint as best as possible
self.frameSize = [self sizeThatFits:CGSizeMake(constrainedRect.size.width, self.calloutHeight)];
// how much room do we have in the constraint box, both above and below our target rect?
CGFloat topSpace = CGRectGetMinY(rect) - CGRectGetMinY(constrainedRect);
CGFloat bottomSpace = CGRectGetMaxY(constrainedRect) - CGRectGetMaxY(rect);
// we prefer to point our arrow down.
SMCalloutArrowDirection bestDirection = SMCalloutArrowDirectionDown;
// we'll point it up though if that's the only option you gave us.
if (self.permittedArrowDirection == SMCalloutArrowDirectionUp)
bestDirection = SMCalloutArrowDirectionUp;
// or, if we don't have enough space on the top and have more space on the bottom, and you
// gave us a choice, then pointing up is the better option.
if (self.permittedArrowDirection == SMCalloutArrowDirectionAny && topSpace < self.calloutHeight && bottomSpace > topSpace)
bestDirection = SMCalloutArrowDirectionUp;
self.currentArrowDirection = bestDirection;
// we want to point directly at the horizontal center of the given rect. calculate our "anchor point" in terms of our
// target view's coordinate system. make sure to offset the anchor point as requested if necessary.
CGFloat anchorX = self.calloutOffset.x + CGRectGetMidX(rect);
CGFloat anchorY = self.calloutOffset.y + (bestDirection == SMCalloutArrowDirectionDown ? CGRectGetMinY(rect) : CGRectGetMaxY(rect));
// we prefer to sit centered directly above our anchor
CGFloat calloutX = roundf(anchorX - self.frameWidth / 2);
// but not if it's going to get too close to the edge of our constraints
if (calloutX < constrainedRect.origin.x)
calloutX = constrainedRect.origin.x;
if (calloutX > constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth)
calloutX = constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth;
// what's the farthest to the left and right that we could point to, given our background image constraints?
CGFloat minPointX = calloutX + self.backgroundView.anchorMargin;
CGFloat maxPointX = calloutX + self.frameWidth - self.backgroundView.anchorMargin;
// we may need to scoot over to the left or right to point at the correct spot
CGFloat adjustX = 0;
if (anchorX < minPointX) adjustX = anchorX - minPointX;
if (anchorX > maxPointX) adjustX = anchorX - maxPointX;
// add the callout to the given layer (or view if possible, to receive touch events)
if (view)
[view addSubview:self];
else
[layer addSublayer:self.layer];
CGPoint calloutOrigin = {
.x = calloutX + adjustX,
.y = bestDirection == SMCalloutArrowDirectionDown ? (anchorY - self.calloutHeight) : anchorY
};
self.frameOrigin = calloutOrigin;
// now set the *actual* anchor point for our layer so that our "popup" animation starts from this point.
CGPoint anchorPoint = [layer convertPoint:CGPointMake(anchorX, anchorY) toLayer:self.layer];
// pass on the anchor point to our background view so it knows where to draw the arrow
self.backgroundView.arrowPoint = anchorPoint;
// adjust it to unit coordinates for the actual layer.anchorPoint property
anchorPoint.x /= self.frameWidth;
anchorPoint.y /= self.frameHeight;
self.layer.anchorPoint = anchorPoint;
// setting the anchor point moves the view a bit, so we need to reset
self.frameOrigin = calloutOrigin;
// make sure our frame is not on half-pixels or else we may be blurry!
CGFloat scale = [UIScreen mainScreen].scale;
self.frameX = floorf(self.frameX*scale)/scale;
self.frameY = floorf(self.frameY*scale)/scale;
// layout now so we can immediately start animating to the final position if needed
[self setNeedsLayout];
[self layoutIfNeeded];
// if we're outside the bounds of our constraint rect, we'll give our delegate an opportunity to shift us into position.
// consider both our size and the size of our target rect (which we'll assume to be the size of the content you want to scroll into view.
CGRect contentRect = CGRectUnion(self.frame, rect);
CGSize offset = [self offsetToContainRect:contentRect inRect:constrainedRect];
NSTimeInterval delay = 0;
self.popupCancelled = NO; // reset this before calling our delegate below
if ([self.delegate respondsToSelector:@selector(calloutView:delayForRepositionWithSize:)] && !CGSizeEqualToSize(offset, CGSizeZero))
delay = [self.delegate calloutView:(id)self delayForRepositionWithSize:offset];
// there's a chance that user code in the delegate method may have called -dismissCalloutAnimated to cancel things; if that
// happened then we need to bail!
if (self.popupCancelled) return;
// now we want to mask our contents to our background view (if requested) to match the iOS 7 style
self.containerView.layer.mask = self.backgroundView.contentMask;
// if we need to delay, we don't want to be visible while we're delaying, so hide us in preparation for our popup
self.hidden = YES;
// create the appropriate animation, even if we're not animated
CAAnimation *animation = [self animationWithType:self.presentAnimation presenting:YES];
// nuke the duration if no animation requested - we'll still need to "run" the animation to get delays and callbacks
if (!animated)
animation.duration = 0.0000001; // can't be zero or the animation won't "run"
animation.beginTime = CACurrentMediaTime() + delay;
animation.delegate = self;
[self.layer addAnimation:animation forKey:@"present"];
}
- (void)animationDidStart:(CAAnimation *)anim {
BOOL presenting = [[anim valueForKey:@"presenting"] boolValue];
if (presenting) {
if ([_delegate respondsToSelector:@selector(calloutViewWillAppear:)])
[_delegate calloutViewWillAppear:(id)self];
// ok, animation is on, let's make ourselves visible!
self.hidden = NO;
}
else if (!presenting) {
if ([_delegate respondsToSelector:@selector(calloutViewWillDisappear:)])
[_delegate calloutViewWillDisappear:(id)self];
}
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)finished {
BOOL presenting = [[anim valueForKey:@"presenting"] boolValue];
if (presenting && finished) {
if ([_delegate respondsToSelector:@selector(calloutViewDidAppear:)])
[_delegate calloutViewDidAppear:(id)self];
}
else if (!presenting && finished) {
[self removeFromParent];
[self.layer removeAnimationForKey:@"dismiss"];
if ([_delegate respondsToSelector:@selector(calloutViewDidDisappear:)])
[_delegate calloutViewDidDisappear:(id)self];
}
}
- (void)dismissCalloutAnimated:(BOOL)animated {
// cancel all animations that may be in progress
[self.layer removeAnimationForKey:@"present"];
[self.layer removeAnimationForKey:@"dismiss"];
self.popupCancelled = YES;
if (animated) {
CAAnimation *animation = [self animationWithType:self.dismissAnimation presenting:NO];
animation.delegate = self;
[self.layer addAnimation:animation forKey:@"dismiss"];
}
else {
[self removeFromParent];
}
}
- (void)removeFromParent {
if (self.superview)
[self removeFromSuperview];
else {
// removing a layer from a superlayer causes an implicit fade-out animation that we wish to disable.
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self.layer removeFromSuperlayer];
[CATransaction commit];
}
}
- (CAAnimation *)animationWithType:(SMCalloutAnimation)type presenting:(BOOL)presenting {
CAAnimation *animation = nil;
if (type == SMCalloutAnimationBounce) {
CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"];
fade.duration = 0.23;
fade.fromValue = presenting ? @0.0 : @1.0;
fade.toValue = presenting ? @1.0 : @0.0;
fade.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
CABasicAnimation *bounce = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
bounce.duration = 0.23;
bounce.fromValue = presenting ? @0.7 : @1.0;
bounce.toValue = presenting ? @1.0 : @0.7;
bounce.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.59367:0.12066:0.18878:1.5814];
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = @[fade, bounce];
group.duration = 0.23;
animation = group;
}
else if (type == SMCalloutAnimationFade) {
CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"];
fade.duration = 1.0/3.0;
fade.fromValue = presenting ? @0.0 : @1.0;
fade.toValue = presenting ? @1.0 : @0.0;
animation = fade;
}
else if (type == SMCalloutAnimationStretch) {
CABasicAnimation *stretch = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
stretch.duration = 0.1;
stretch.fromValue = presenting ? @0.0 : @1.0;
stretch.toValue = presenting ? @1.0 : @0.0;
animation = stretch;
}
// CAAnimation is KVC compliant, so we can store whether we're presenting for lookup in our delegate methods
[animation setValue:@(presenting) forKey:@"presenting"];
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
return animation;
}
- (void)layoutSubviews {
self.containerView.frame = self.bounds;
self.backgroundView.frame = self.bounds;
// if we're pointing up, we'll need to push almost everything down a bit
CGFloat dy = self.currentArrowDirection == SMCalloutArrowDirectionUp ? TOP_ANCHOR_MARGIN : 0;
self.titleViewOrDefault.frameX = self.innerContentMarginLeft;
self.titleViewOrDefault.frameY = (self.subtitleView || self.subtitle.length ? TITLE_SUB_TOP : TITLE_TOP) + dy;
self.titleViewOrDefault.frameWidth = self.frameWidth - self.innerContentMarginLeft - self.innerContentMarginRight;
self.subtitleViewOrDefault.frameX = self.titleViewOrDefault.frameX;
self.subtitleViewOrDefault.frameY = SUBTITLE_TOP + dy;
self.subtitleViewOrDefault.frameWidth = self.titleViewOrDefault.frameWidth;
self.leftAccessoryView.frameX = self.leftAccessoryHorizontalMargin;
self.leftAccessoryView.frameY = self.leftAccessoryVerticalMargin + dy;
self.rightAccessoryView.frameX = self.frameWidth - self.rightAccessoryHorizontalMargin - self.rightAccessoryView.frameWidth;
self.rightAccessoryView.frameY = self.rightAccessoryVerticalMargin + dy;
if (self.contentView) {
self.contentView.frameX = self.innerContentMarginLeft;
self.contentView.frameY = self.contentViewInset.top + dy;
}
}
#pragma mark - Accessibility
- (NSInteger)accessibilityElementCount {
return (!!self.leftAccessoryView + !!self.titleViewOrDefault +
!!self.subtitleViewOrDefault + !!self.rightAccessoryView);
}
- (id)accessibilityElementAtIndex:(NSInteger)index {
if (index == 0) {
return self.leftAccessoryView ? self.leftAccessoryView : self.titleViewOrDefault;
}
if (index == 1) {
return self.leftAccessoryView ? self.titleViewOrDefault : self.subtitleViewOrDefault;
}
if (index == 2) {
return self.leftAccessoryView ? self.subtitleViewOrDefault : self.rightAccessoryView;
}
if (index == 3) {
return self.leftAccessoryView ? self.rightAccessoryView : nil;
}
return nil;
}
- (NSInteger)indexOfAccessibilityElement:(id)element {
if (element == nil) return NSNotFound;
if (element == self.leftAccessoryView) return 0;
if (element == self.titleViewOrDefault) {
return self.leftAccessoryView ? 1 : 0;
}
if (element == self.subtitleViewOrDefault) {
return self.leftAccessoryView ? 2 : 1;
}
if (element == self.rightAccessoryView) {
return self.leftAccessoryView ? 3 : 2;
}
return NSNotFound;
}
@end
// import this known "private API" from SMCalloutBackgroundView
@interface SMCalloutBackgroundView (EmbeddedImages)
+ (UIImage *)embeddedImageNamed:(NSString *)name;
@end
//
// Callout Background View.
//
@interface SMCalloutMaskedBackgroundView ()
@property (nonatomic, strong) UIView *containerView, *containerBorderView, *arrowView;
@property (nonatomic, strong) UIImageView *arrowImageView, *arrowHighlightedImageView, *arrowBorderView;
@end
static UIImage *blackArrowImage = nil, *whiteArrowImage = nil, *grayArrowImage = nil;
@implementation SMCalloutMaskedBackgroundView
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// Here we're mimicking the very particular (and odd) structure of the system callout view.
// The hierarchy and view/layer values were discovered by inspecting map kit using Reveal.app
self.containerView = [UIView new];
self.containerView.backgroundColor = [UIColor whiteColor];
self.containerView.alpha = 0.96;
self.containerView.layer.cornerRadius = 8;
self.containerView.layer.shadowRadius = 30;
self.containerView.layer.shadowOpacity = 0.1;
self.containerBorderView = [UIView new];
self.containerBorderView.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor;
self.containerBorderView.layer.borderWidth = 0.5;
self.containerBorderView.layer.cornerRadius = 8.5;
if (!blackArrowImage) {
blackArrowImage = [SMCalloutBackgroundView embeddedImageNamed:@"CalloutArrow"];
whiteArrowImage = [self image:blackArrowImage withColor:[UIColor whiteColor]];
grayArrowImage = [self image:blackArrowImage withColor:[UIColor colorWithWhite:0.85 alpha:1]];
}
self.anchorHeight = 13;
self.anchorMargin = 27;
self.arrowView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, blackArrowImage.size.width, blackArrowImage.size.height)];
self.arrowView.alpha = 0.96;
self.arrowImageView = [[UIImageView alloc] initWithImage:whiteArrowImage];
self.arrowHighlightedImageView = [[UIImageView alloc] initWithImage:grayArrowImage];
self.arrowHighlightedImageView.hidden = YES;
self.arrowBorderView = [[UIImageView alloc] initWithImage:blackArrowImage];
self.arrowBorderView.alpha = 0.1;
self.arrowBorderView.frameY = 0.5;
[self addSubview:self.containerView];
[self.containerView addSubview:self.containerBorderView];
[self addSubview:self.arrowView];
[self.arrowView addSubview:self.arrowBorderView];
[self.arrowView addSubview:self.arrowImageView];
[self.arrowView addSubview:self.arrowHighlightedImageView];
}
return self;
}
// Make sure we relayout our images when our arrow point changes!
- (void)setArrowPoint:(CGPoint)arrowPoint {
[super setArrowPoint:arrowPoint];
[self setNeedsLayout];
}
- (void)setHighlighted:(BOOL)highlighted {
[super setHighlighted:highlighted];
self.containerView.backgroundColor = highlighted ? [UIColor colorWithWhite:0.85 alpha:1] : [UIColor whiteColor];
self.arrowImageView.hidden = highlighted;
self.arrowHighlightedImageView.hidden = !highlighted;
}
- (UIImage *)image:(UIImage *)image withColor:(UIColor *)color {
UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);
CGRect imageRect = (CGRect){.size=image.size};
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(c, 0, image.size.height);
CGContextScaleCTM(c, 1, -1);
CGContextClipToMask(c, imageRect, image.CGImage);
[color setFill];
CGContextFillRect(c, imageRect);
UIImage *whiteImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return whiteImage;
}
- (void)layoutSubviews {
BOOL pointingUp = self.arrowPoint.y < self.frameHeight/2;
// if we're pointing up, we'll need to push almost everything down a bit
CGFloat dy = pointingUp ? TOP_ANCHOR_MARGIN : 0;
self.containerView.frame = CGRectMake(0, dy, self.frameWidth, self.frameHeight - self.arrowView.frameHeight + 0.5);
self.containerBorderView.frame = CGRectInset(self.containerView.bounds, -0.5, -0.5);
self.arrowView.frameX = roundf(self.arrowPoint.x - self.arrowView.frameWidth / 2);
if (pointingUp) {
self.arrowView.frameY = 1;
self.arrowView.transform = CGAffineTransformMakeRotation(M_PI);
}
else {
self.arrowView.frameY = self.containerView.frameHeight - 0.5;
self.arrowView.transform = CGAffineTransformIdentity;
}
}
- (CALayer *)contentMask {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *maskImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CALayer *layer = [CALayer layer];
layer.frame = self.bounds;
layer.contents = (id)maskImage.CGImage;
return layer;
}
@end
@implementation SMCalloutBackgroundView
+ (NSData *)dataWithBase64EncodedString:(NSString *)string {
//
// NSData+Base64.m
//
// Version 1.0.2
//
// Created by Nick Lockwood on 12/01/2012.
// Copyright (C) 2012 Charcoal Design
//
// Distributed under the permissive zlib License
// Get the latest version from here:
//
// https://github.com/nicklockwood/Base64
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
const char lookup[] = {
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99,
99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99,
99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99
};
NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
long long inputLength = [inputData length];
const unsigned char *inputBytes = [inputData bytes];
long long maxOutputLength = (inputLength / 4 + 1) * 3;
NSMutableData *outputData = [NSMutableData dataWithLength:(NSUInteger)maxOutputLength];
unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes];
int accumulator = 0;
long long outputLength = 0;
unsigned char accumulated[] = {0, 0, 0, 0};
for (long long i = 0; i < inputLength; i++) {
unsigned char decoded = lookup[inputBytes[i] & 0x7F];
if (decoded != 99) {
accumulated[accumulator] = decoded;
if (accumulator == 3) {
outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4);
outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2);
outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3];
}
accumulator = (accumulator + 1) % 4;
}
}
//handle left-over data
if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4);
if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2);
if (accumulator > 2) outputLength++;
//truncate data to match actual output length
outputData.length = (NSUInteger)outputLength;
return outputLength? outputData: nil;
}
+ (UIImage *)embeddedImageNamed:(NSString *)name {
CGFloat screenScale = [UIScreen mainScreen].scale;
if (screenScale > 1.0) {
name = [name stringByAppendingString:@"_2x"];
screenScale = 2.0;
}
SEL selector = NSSelectorFromString(name);
if (![(id)self respondsToSelector:selector]) {
NSLog(@"Could not find an embedded image. Ensure that you've added a class-level method named +%@", name);
return nil;
}
// We need to hush the compiler here - but we know what we're doing!
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
NSString *base64String = [(id)self performSelector:selector];
#pragma clang diagnostic pop
UIImage *rawImage = [UIImage imageWithData:[self dataWithBase64EncodedString:base64String]];
return [UIImage imageWithCGImage:rawImage.CGImage scale:screenScale orientation:UIImageOrientationUp];
}
+ (NSString *)CalloutArrow { return @"iVBORw0KGgoAAAANSUhEUgAAACcAAAANCAYAAAAqlHdlAAAAHGlET1QAAAACAAAAAAAAAAcAAAAoAAAABwAAAAYAAADJEgYpIwAAAJVJREFUOBFiYIAAdn5+fkFOTkE5Dg5eW05O3lJOTr6zQPyfDhhoD28pxF5BOZA7gE5ih7oLN8XJyR8MdNwrGjkQaC5/MG7biZDh4OBXBDruLpUdeBdkLhHWE1bCzs6nAnTcUyo58DnIPMK2kqAC6DALIP5JoQNB+i1IsJZ4pcBEm0iJ40D6ibeNDJVAx00k04ETSbUOAAAA//+SwicfAAAAe0lEQVRjYCAdMHNy8u7l5OT7Tzzm3Qu0hpl0q8jQwcPDIwp02B0iHXeHl5dXhAxryNfCzc2tC3TcJwIO/ARSR74tFOjk4uL1BzruHw4H/gPJU2A85Vq5uPjTgY77g+bAPyBxyk2nggkcHPxOnJz8B4AOfAGiQXwqGMsAACGK1kPPMHNBAAAAAElFTkSuQmCC"; }
+ (NSString *)CalloutArrow_2x { return @"iVBORw0KGgoAAAANSUhEUgAAAE4AAAAaCAYAAAAZtWr8AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAHGlET1QAAAACAAAAAAAAAA0AAAAoAAAADQAAAA0AAAFMRh0LGwAAARhJREFUWAnclbENwjAQRZ0mih2fDYgsQEVDxQZMgKjpWYAJkBANI8AGDIEoM0WkzBDRAf8klB44g0OkU1zE3/+9RIpS7VVY730/y/woTWlsjJ9iPcN9pbXfY85auyvm/qcDNmb0e2Z+sk/ZBTthN0oVttX12mJIWeaWEFf+kbySmZQa0msu3nzaGJprTXV3BVLNDG/if7bNOTeAvFP35NGJu39GL7Abb27bFXncVQBZLgJf3jp+ebSWIxZMgrxdvPJoJ4gqHpXgV36ITR46HUGaiNMKB6YQd4lI3gV8qTBjmDhrbQFxVQTyKu4ShjJQap7nE4hrfiiv4Q6B8MLGat1bQNztB/JwZm8Rli5wujFu821xfGZgLPUAAAD//4wvm4gAAAD7SURBVOWXMQ6CMBiFgaFpi6VyBEedXJy4hMQTeBSvRDgJEySegI3EQWOivkZnqUB/k0LyL7R9L++D9G+DwP0TCZGUqCdRlYgUuY9F4JCmqQa0hgBcY7wIItFZMLZYS5l0ruAZbXhs6BIROgmhcoB7OIAHTZUTRqG3wp9xmhqc0aRPQu8YAlwxIbwCEUL6GH9wfDcLXY2HpyvvmkHf9+BcrwCuHQGvNRp9Pl6OY0PPAO42AB7WqMxLKLahpFR7gLv/AA9zPe+gtvAMCIC7WMC7CqEPtrqzmBfHyy3A1V/g1Th27GYBY0BIxrk6Ap65254/VZp30GID9JwteQEZrVMWXqGn8gAAAABJRU5ErkJggg=="; }
@end
//
// Our UIView frame helpers implementation
//
@implementation UIView (SMFrameAdditions)
- (CGPoint)frameOrigin { return self.frame.origin; }
- (void)setFrameOrigin:(CGPoint)origin { self.frame = (CGRect){ .origin=origin, .size=self.frame.size }; }
- (CGFloat)frameX { return self.frame.origin.x; }
- (void)setFrameX:(CGFloat)x { self.frame = (CGRect){ .origin.x=x, .origin.y=self.frame.origin.y, .size=self.frame.size }; }
- (CGFloat)frameY { return self.frame.origin.y; }
- (void)setFrameY:(CGFloat)y { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=y, .size=self.frame.size }; }
- (CGSize)frameSize { return self.frame.size; }
- (void)setFrameSize:(CGSize)size { self.frame = (CGRect){ .origin=self.frame.origin, .size=size }; }
- (CGFloat)frameWidth { return self.frame.size.width; }
- (void)setFrameWidth:(CGFloat)width { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=width, .size.height=self.frame.size.height }; }
- (CGFloat)frameHeight { return self.frame.size.height; }
- (void)setFrameHeight:(CGFloat)height { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=height }; }
- (CGFloat)frameLeft { return self.frame.origin.x; }
- (void)setFrameLeft:(CGFloat)left { self.frame = (CGRect){ .origin.x=left, .origin.y=self.frame.origin.y, .size.width=fmaxf(self.frame.origin.x+self.frame.size.width-left,0), .size.height=self.frame.size.height }; }
- (CGFloat)frameTop { return self.frame.origin.y; }
- (void)setFrameTop:(CGFloat)top { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=top, .size.width=self.frame.size.width, .size.height=fmaxf(self.frame.origin.y+self.frame.size.height-top,0) }; }
- (CGFloat)frameRight { return self.frame.origin.x + self.frame.size.width; }
- (void)setFrameRight:(CGFloat)right { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=fmaxf(right-self.frame.origin.x,0), .size.height=self.frame.size.height }; }
- (CGFloat)frameBottom { return self.frame.origin.y + self.frame.size.height; }
- (void)setFrameBottom:(CGFloat)bottom { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=fmaxf(bottom-self.frame.origin.y,0) }; }
@end
@@ -0,0 +1,13 @@
//
// RCTComponentData+Maps.h
// AirMaps
//
// Created by Salah Ghanim on 24.12.23.
// Copyright © 2023 Christopher. All rights reserved.
//
#import <React/RCTComponentData.h>
@interface RCTComponentData (Maps)
@end
@@ -0,0 +1,55 @@
//
// RCTComponentData+Maps.m
// AirMaps
//
// Created by Salah Ghanim on 24.12.23.
// Copyright © 2023 Christopher. All rights reserved.
//
#import "RCTComponentData+Maps.h"
#import <objc/runtime.h>
#import <Foundation/NSObjCRuntime.h>
@implementation RCTComponentData (Maps)
- (void) myCustom_setProps:(NSDictionary<NSString *, id> *)props forShadowView:(RCTShadowView *)shadowView{
// Pass initialProps to any manager that supports initialProps
id manager = [self manager];
if ([manager respondsToSelector:@selector(setInitialProps:)]) {
[manager performSelector:@selector(setInitialProps:) withObject:props];
}
// Call the original method
[self myCustom_setProps:props forShadowView:shadowView];
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [RCTComponentData class]; // Or the class where the method is defined
SEL originalSelector = @selector(setProps:forShadowView:);
SEL swizzledSelector = @selector(myCustom_setProps:forShadowView:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
@end
@@ -0,0 +1,18 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <CoreLocation/CoreLocation.h>
#import <MapKit/MapKit.h>
#import <React/RCTConvert.h>
@interface RCTConvert (AirMap)
+ (MKCoordinateSpan)MKCoordinateSpan:(id)json;
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json;
+ (MKMapCamera*)MKMapCamera:(id)json;
+ (MKMapCamera*)MKMapCameraWithDefaults:(id)json existingCamera:(MKMapCamera*)camera;
+ (MKMapType)MKMapType:(id)json;
@end
@@ -0,0 +1,88 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import "RCTConvert+AirMap.h"
#import <React/RCTConvert+CoreLocation.h>
#import "AIRMapCoordinate.h"
@implementation RCTConvert (AirMap)
+ (MKCoordinateSpan)MKCoordinateSpan:(id)json
{
json = [self NSDictionary:json];
return (MKCoordinateSpan){
[self CLLocationDegrees:json[@"latitudeDelta"]],
[self CLLocationDegrees:json[@"longitudeDelta"]]
};
}
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json
{
return (MKCoordinateRegion){
[self CLLocationCoordinate2D:json],
[self MKCoordinateSpan:json]
};
}
+ (MKMapCamera*)MKMapCamera:(id)json
{
json = [self NSDictionary:json];
return [RCTConvert MKMapCameraWithDefaults:json existingCamera:nil];
}
+ (MKMapCamera*)MKMapCameraWithDefaults:(id)json existingCamera:(MKMapCamera*)camera
{
json = [self NSDictionary:json];
if (camera == nil) {
camera = [[MKMapCamera alloc] init];
} else {
camera = [camera copy];
}
if (json[@"center"]) {
camera.centerCoordinate = [self CLLocationCoordinate2D:json[@"center"]];
}
if (json[@"pitch"]) {
camera.pitch = [self double:json[@"pitch"]];
}
if (json[@"altitude"]) {
camera.altitude = [self double:json[@"altitude"]];
}
if (json[@"heading"]) {
camera.heading = [self double:json[@"heading"]];
}
return camera;
}
RCT_ENUM_CONVERTER(MKMapType, (@{
@"standard": @(MKMapTypeStandard),
@"satellite": @(MKMapTypeSatellite),
@"hybrid": @(MKMapTypeHybrid),
@"satelliteFlyover": @(MKMapTypeSatelliteFlyover),
@"hybridFlyover": @(MKMapTypeHybridFlyover),
@"mutedStandard": @(MKMapTypeMutedStandard)
}), MKMapTypeStandard, integerValue)
// NOTE(lmr):
// This is a bit of a hack, but I'm using this class to simply wrap
// around a `CLLocationCoordinate2D`, since I was unable to figure out
// how to handle an array of structs like CLLocationCoordinate2D. Would love
// to get rid of this if someone can show me how...
+ (AIRMapCoordinate *)AIRMapCoordinate:(id)json
{
AIRMapCoordinate *coord = [AIRMapCoordinate new];
coord.coordinate = [self CLLocationCoordinate2D:json];
return coord;
}
RCT_ARRAY_CONVERTER(AIRMapCoordinate)
+ (NSArray<NSArray<AIRMapCoordinate *> *> *)AIRMapCoordinateArrayArray:(id)json
{
return RCTConvertArrayValue(@selector(AIRMapCoordinateArray:), json);
}
@end