1/** 2 * Copyright (c) 2015-present, Facebook, Inc. 3 * All rights reserved. 4 * 5 * This source code is licensed under the BSD-style license found in the 6 * LICENSE file in the root directory of this source tree. An additional grant 7 * of patent rights can be found in the PATENTS file in the same directory. 8 */ 9 10#import "AIRMapManager.h" 11 12#import <React/RCTBridge.h> 13#import <React/RCTUIManager.h> 14#import <React/RCTConvert.h> 15#import <React/RCTConvert+CoreLocation.h> 16#import <React/RCTEventDispatcher.h> 17#import <React/RCTViewManager.h> 18#import <React/UIView+React.h> 19#import "AIRMap.h" 20#import "AIRMapMarker.h" 21#import "AIRMapPolyline.h" 22#import "AIRMapPolygon.h" 23#import "AIRMapCircle.h" 24#import "SMCalloutView.h" 25#import "AIRMapUrlTile.h" 26#import "AIRMapWMSTile.h" 27#import "AIRMapLocalTile.h" 28#import "AIRMapSnapshot.h" 29#import "RCTConvert+AirMap.h" 30#import "AIRMapOverlay.h" 31#import <MapKit/MapKit.h> 32 33static NSString *const RCTMapViewKey = @"MapView"; 34 35 36@interface AIRMapManager() <MKMapViewDelegate, UIGestureRecognizerDelegate> 37 38- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer; 39 40@end 41 42@implementation AIRMapManager{ 43 BOOL _hasObserver; 44} 45 46RCT_EXPORT_MODULE() 47 48- (UIView *)view 49{ 50 AIRMap *map = [AIRMap new]; 51 map.delegate = self; 52 53 map.isAccessibilityElement = NO; 54 map.accessibilityElementsHidden = NO; 55 56 // MKMapView doesn't report tap events, so we attach gesture recognizers to it 57 UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapTap:)]; 58 UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapDoubleTap:)]; 59 [doubleTap setNumberOfTapsRequired:2]; 60 [tap requireGestureRecognizerToFail:doubleTap]; 61 62 UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapLongPress:)]; 63 UIPanGestureRecognizer *drag = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapDrag:)]; 64 [drag setMinimumNumberOfTouches:1]; 65 // setting this to NO allows the parent MapView to continue receiving marker selection events 66 tap.cancelsTouchesInView = NO; 67 doubleTap.cancelsTouchesInView = NO; 68 longPress.cancelsTouchesInView = NO; 69 70 doubleTap.delegate = self; 71 72 // disable drag by default 73 drag.enabled = NO; 74 drag.delegate = self; 75 76 [map addGestureRecognizer:tap]; 77 [map addGestureRecognizer:doubleTap]; 78 [map addGestureRecognizer:longPress]; 79 [map addGestureRecognizer:drag]; 80 81 return map; 82} 83 84RCT_EXPORT_VIEW_PROPERTY(isAccessibilityElement, BOOL) 85RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier, NSString) 86RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL) 87RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) 88RCT_EXPORT_VIEW_PROPERTY(userLocationAnnotationTitle, NSString) 89RCT_EXPORT_VIEW_PROPERTY(userInterfaceStyle, NSString) 90RCT_EXPORT_VIEW_PROPERTY(followsUserLocation, BOOL) 91RCT_EXPORT_VIEW_PROPERTY(userLocationCalloutEnabled, BOOL) 92RCT_EXPORT_VIEW_PROPERTY(showsPointsOfInterest, BOOL) 93RCT_EXPORT_VIEW_PROPERTY(showsBuildings, BOOL) 94RCT_EXPORT_VIEW_PROPERTY(showsCompass, BOOL) 95RCT_EXPORT_VIEW_PROPERTY(showsScale, BOOL) 96RCT_EXPORT_VIEW_PROPERTY(showsTraffic, BOOL) 97RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL) 98RCT_EXPORT_VIEW_PROPERTY(kmlSrc, NSString) 99RCT_EXPORT_VIEW_PROPERTY(rotateEnabled, BOOL) 100RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL) 101RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL) 102RCT_EXPORT_VIEW_PROPERTY(cacheEnabled, BOOL) 103RCT_EXPORT_VIEW_PROPERTY(loadingEnabled, BOOL) 104RCT_EXPORT_VIEW_PROPERTY(loadingBackgroundColor, UIColor) 105RCT_EXPORT_VIEW_PROPERTY(loadingIndicatorColor, UIColor) 106RCT_EXPORT_VIEW_PROPERTY(handlePanDrag, BOOL) 107RCT_EXPORT_VIEW_PROPERTY(maxDelta, CGFloat) 108RCT_EXPORT_VIEW_PROPERTY(minDelta, CGFloat) 109RCT_EXPORT_VIEW_PROPERTY(compassOffset, CGPoint) 110RCT_EXPORT_VIEW_PROPERTY(legalLabelInsets, UIEdgeInsets) 111RCT_EXPORT_VIEW_PROPERTY(mapPadding, UIEdgeInsets) 112RCT_EXPORT_VIEW_PROPERTY(mapType, MKMapType) 113RCT_EXPORT_VIEW_PROPERTY(onMapReady, RCTBubblingEventBlock) 114RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) 115RCT_EXPORT_VIEW_PROPERTY(onPanDrag, RCTBubblingEventBlock) 116RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) 117RCT_EXPORT_VIEW_PROPERTY(onLongPress, RCTBubblingEventBlock) 118RCT_EXPORT_VIEW_PROPERTY(onDoublePress, RCTBubblingEventBlock) 119RCT_EXPORT_VIEW_PROPERTY(onMarkerPress, RCTDirectEventBlock) 120RCT_EXPORT_VIEW_PROPERTY(onMarkerSelect, RCTDirectEventBlock) 121RCT_EXPORT_VIEW_PROPERTY(onMarkerDeselect, RCTDirectEventBlock) 122RCT_EXPORT_VIEW_PROPERTY(onMarkerDragStart, RCTDirectEventBlock) 123RCT_EXPORT_VIEW_PROPERTY(onMarkerDrag, RCTDirectEventBlock) 124RCT_EXPORT_VIEW_PROPERTY(onMarkerDragEnd, RCTDirectEventBlock) 125RCT_EXPORT_VIEW_PROPERTY(onCalloutPress, RCTDirectEventBlock) 126RCT_EXPORT_VIEW_PROPERTY(onUserLocationChange, RCTBubblingEventBlock) 127RCT_CUSTOM_VIEW_PROPERTY(initialRegion, MKCoordinateRegion, AIRMap) 128{ 129 if (json == nil) return; 130 131 // don't emit region change events when we are setting the initialRegion 132 BOOL originalIgnore = view.ignoreRegionChanges; 133 view.ignoreRegionChanges = YES; 134 [view setInitialRegion:[RCTConvert MKCoordinateRegion:json]]; 135 view.ignoreRegionChanges = originalIgnore; 136} 137RCT_CUSTOM_VIEW_PROPERTY(initialCamera, MKMapCamera, AIRMap) 138{ 139 if (json == nil) return; 140 141 // don't emit region change events when we are setting the initialCamera 142 BOOL originalIgnore = view.ignoreRegionChanges; 143 view.ignoreRegionChanges = YES; 144 [view setInitialCamera:[RCTConvert MKMapCamera:json]]; 145 view.ignoreRegionChanges = originalIgnore; 146} 147 148 149RCT_EXPORT_VIEW_PROPERTY(minZoomLevel, CGFloat) 150RCT_EXPORT_VIEW_PROPERTY(maxZoomLevel, CGFloat) 151 152 153RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, AIRMap) 154{ 155 if (json == nil) return; 156 157 // don't emit region change events when we are setting the region 158 BOOL originalIgnore = view.ignoreRegionChanges; 159 view.ignoreRegionChanges = YES; 160 [view setRegion:[RCTConvert MKCoordinateRegion:json] animated:NO]; 161 view.ignoreRegionChanges = originalIgnore; 162} 163 164RCT_CUSTOM_VIEW_PROPERTY(camera, MKMapCamera*, AIRMap) 165{ 166 if (json == nil) return; 167 168 // don't emit region change events when we are setting the camera 169 BOOL originalIgnore = view.ignoreRegionChanges; 170 view.ignoreRegionChanges = YES; 171 [view setCamera:[RCTConvert MKMapCamera:json] animated:NO]; 172 view.ignoreRegionChanges = originalIgnore; 173} 174 175 176#pragma mark exported MapView methods 177 178RCT_EXPORT_METHOD(getMapBoundaries:(nonnull NSNumber *)reactTag 179 resolver:(RCTPromiseResolveBlock)resolve 180 rejecter:(RCTPromiseRejectBlock)reject) 181{ 182 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 183 id view = viewRegistry[reactTag]; 184 if (![view isKindOfClass:[AIRMap class]]) { 185 RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); 186 } else { 187 NSArray *boundingBox = [view getMapBoundaries]; 188 189 resolve(@{ 190 @"northEast" : @{ 191 @"longitude" : boundingBox[0][0], 192 @"latitude" : boundingBox[0][1] 193 }, 194 @"southWest" : @{ 195 @"longitude" : boundingBox[1][0], 196 @"latitude" : boundingBox[1][1] 197 } 198 }); 199 } 200 }]; 201} 202 203 204 205RCT_EXPORT_METHOD(getCamera:(nonnull NSNumber *)reactTag 206 resolver: (RCTPromiseResolveBlock)resolve 207 rejecter:(RCTPromiseRejectBlock)reject) 208{ 209 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 210 id view = viewRegistry[reactTag]; 211 AIRMap *mapView = (AIRMap *)view; 212 if (![view isKindOfClass:[AIRMap class]]) { 213 reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid view returned from registry, expecting AIRMap, got: %@", view], NULL); 214 } else { 215 MKMapCamera *camera = [mapView camera]; 216 resolve(@{ 217 @"center": @{ 218 @"latitude": @(camera.centerCoordinate.latitude), 219 @"longitude": @(camera.centerCoordinate.longitude), 220 }, 221 @"pitch": @(camera.pitch), 222 @"heading": @(camera.heading), 223 @"altitude": @(camera.altitude), 224 }); 225 } 226 }]; 227} 228 229 230RCT_EXPORT_METHOD(setCamera:(nonnull NSNumber *)reactTag 231 camera:(id)json) 232{ 233 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 234 id view = viewRegistry[reactTag]; 235 if (![view isKindOfClass:[AIRMap class]]) { 236 RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); 237 } else { 238 AIRMap *mapView = (AIRMap *)view; 239 240 // Merge the changes given with the current camera 241 MKMapCamera *camera = [RCTConvert MKMapCameraWithDefaults:json existingCamera:[mapView camera]]; 242 243 // don't emit region change events when we are setting the camera 244 BOOL originalIgnore = mapView.ignoreRegionChanges; 245 mapView.ignoreRegionChanges = YES; 246 [mapView setCamera:camera animated:NO]; 247 mapView.ignoreRegionChanges = originalIgnore; 248 } 249 }]; 250} 251 252 253RCT_EXPORT_METHOD(animateCamera:(nonnull NSNumber *)reactTag 254 withCamera:(id)json 255 withDuration:(CGFloat)duration) 256{ 257 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 258 id view = viewRegistry[reactTag]; 259 if (![view isKindOfClass:[AIRMap class]]) { 260 RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); 261 } else { 262 AIRMap *mapView = (AIRMap *)view; 263 264 // Merge the changes given with the current camera 265 MKMapCamera *camera = [RCTConvert MKMapCameraWithDefaults:json existingCamera:[mapView camera]]; 266 267 // don't emit region change events when we are setting the camera 268 BOOL originalIgnore = mapView.ignoreRegionChanges; 269 mapView.ignoreRegionChanges = YES; 270 [AIRMap animateWithDuration:duration/1000 animations:^{ 271 [mapView setCamera:camera animated:YES]; 272 } completion:^(BOOL finished){ 273 mapView.ignoreRegionChanges = originalIgnore; 274 }]; 275 } 276 }]; 277} 278 279RCT_EXPORT_METHOD(animateToRegion:(nonnull NSNumber *)reactTag 280 withRegion:(MKCoordinateRegion)region 281 withDuration:(CGFloat)duration) 282{ 283 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 284 id view = viewRegistry[reactTag]; 285 if (![view isKindOfClass:[AIRMap class]]) { 286 RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); 287 } else { 288 [AIRMap animateWithDuration:duration/1000 animations:^{ 289 [(AIRMap *)view setRegion:region animated:YES]; 290 }]; 291 } 292 }]; 293} 294 295RCT_EXPORT_METHOD(fitToElements:(nonnull NSNumber *)reactTag 296 edgePadding:(nonnull NSDictionary *)edgePadding 297 animated:(BOOL)animated) 298{ 299 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 300 id view = viewRegistry[reactTag]; 301 if (![view isKindOfClass:[AIRMap class]]) { 302 RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); 303 } else { 304 AIRMap *mapView = (AIRMap *)view; 305 // TODO(lmr): we potentially want to include overlays here... and could concat the two arrays together. 306 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 307 [mapView showAnnotations:mapView.annotations animated:animated]; 308 }); 309 } 310 }]; 311} 312 313RCT_EXPORT_METHOD(fitToSuppliedMarkers:(nonnull NSNumber *)reactTag 314 markers:(nonnull NSArray *)markers 315 edgePadding:(nonnull NSDictionary *)edgePadding 316 animated:(BOOL)animated) 317{ 318 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 319 id view = viewRegistry[reactTag]; 320 if (![view isKindOfClass:[AIRMap class]]) { 321 RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); 322 } else { 323 AIRMap *mapView = (AIRMap *)view; 324 // TODO(lmr): we potentially want to include overlays here... and could concat the two arrays together. 325 // id annotations = mapView.annotations; 326 327 NSPredicate *filterMarkers = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { 328 AIRMapMarker *marker = (AIRMapMarker *)evaluatedObject; 329 return [marker isKindOfClass:[AIRMapMarker class]] && [markers containsObject:marker.identifier]; 330 }]; 331 332 NSArray *filteredMarkers = [mapView.annotations filteredArrayUsingPredicate:filterMarkers]; 333 334 [mapView showAnnotations:filteredMarkers animated:animated]; 335 336 } 337 }]; 338} 339 340RCT_EXPORT_METHOD(fitToCoordinates:(nonnull NSNumber *)reactTag 341 coordinates:(nonnull NSArray<AIRMapCoordinate *> *)coordinates 342 edgePadding:(nonnull NSDictionary *)edgePadding 343 animated:(BOOL)animated) 344{ 345 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 346 id view = viewRegistry[reactTag]; 347 if (![view isKindOfClass:[AIRMap class]]) { 348 RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); 349 } else { 350 AIRMap *mapView = (AIRMap *)view; 351 352 // Create Polyline with coordinates 353 CLLocationCoordinate2D coords[coordinates.count]; 354 for(int i = 0; i < coordinates.count; i++) 355 { 356 coords[i] = coordinates[i].coordinate; 357 } 358 MKPolyline *polyline = [MKPolyline polylineWithCoordinates:coords count:coordinates.count]; 359 360 // Set Map viewport 361 CGFloat top = [RCTConvert CGFloat:edgePadding[@"top"]]; 362 CGFloat right = [RCTConvert CGFloat:edgePadding[@"right"]]; 363 CGFloat bottom = [RCTConvert CGFloat:edgePadding[@"bottom"]]; 364 CGFloat left = [RCTConvert CGFloat:edgePadding[@"left"]]; 365 366 [mapView setVisibleMapRect:[polyline boundingMapRect] edgePadding:UIEdgeInsetsMake(top, left, bottom, right) animated:animated]; 367 368 } 369 }]; 370} 371 372RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)reactTag 373 width:(nonnull NSNumber *)width 374 height:(nonnull NSNumber *)height 375 region:(MKCoordinateRegion)region 376 format:(nonnull NSString *)format 377 quality:(nonnull NSNumber *)quality 378 result:(nonnull NSString *)result 379 callback:(RCTResponseSenderBlock)callback) 380{ 381 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 382 id view = viewRegistry[reactTag]; 383 if (![view isKindOfClass:[AIRMap class]]) { 384 RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); 385 } else { 386 AIRMap *mapView = (AIRMap *)view; 387 MKMapSnapshotOptions *options = [[MKMapSnapshotOptions alloc] init]; 388 389 options.mapType = mapView.mapType; 390 options.region = (region.center.latitude && region.center.longitude) ? region : mapView.region; 391 options.size = CGSizeMake( 392 ([width floatValue] == 0) ? mapView.bounds.size.width : [width floatValue], 393 ([height floatValue] == 0) ? mapView.bounds.size.height : [height floatValue] 394 ); 395 options.scale = [[UIScreen mainScreen] scale]; 396 397 MKMapSnapshotter *snapshotter = [[MKMapSnapshotter alloc] initWithOptions:options]; 398 399 [self takeMapSnapshot:mapView 400 snapshotter:snapshotter 401 format:format 402 quality:quality.floatValue 403 result:result 404 callback:callback]; 405 } 406 }]; 407} 408 409RCT_EXPORT_METHOD(pointForCoordinate:(nonnull NSNumber *)reactTag 410 coordinate: (NSDictionary *)coordinate 411 resolver: (RCTPromiseResolveBlock)resolve 412 rejecter:(RCTPromiseRejectBlock)reject) 413{ 414 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 415 id view = viewRegistry[reactTag]; 416 AIRMap *mapView = (AIRMap *)view; 417 if (![view isKindOfClass:[AIRMap class]]) { 418 reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid view returned from registry, expecting AIRMap, got: %@", view], NULL); 419 } else { 420 CGPoint touchPoint = [mapView convertCoordinate: 421 CLLocationCoordinate2DMake( 422 [coordinate[@"latitude"] doubleValue], 423 [coordinate[@"longitude"] doubleValue] 424 ) 425 toPointToView:mapView]; 426 427 resolve(@{ 428 @"x": @(touchPoint.x), 429 @"y": @(touchPoint.y), 430 }); 431 } 432 }]; 433} 434 435RCT_EXPORT_METHOD(getMarkersFrames:(nonnull NSNumber *)reactTag 436 onlyVisible:(BOOL)onlyVisible 437 resolver: (RCTPromiseResolveBlock)resolve 438 rejecter:(RCTPromiseRejectBlock)reject) 439{ 440 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 441 id view = viewRegistry[reactTag]; 442 AIRMap *mapView = (AIRMap *)view; 443 if (![view isKindOfClass:[AIRMap class]]) { 444 reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid view returned from registry, expecting AIRMap, got: %@", view], NULL); 445 } else { 446 resolve([mapView getMarkersFramesWithOnlyVisible:onlyVisible]); 447 } 448 }]; 449} 450 451 452 453RCT_EXPORT_METHOD(coordinateForPoint:(nonnull NSNumber *)reactTag 454 point:(NSDictionary *)point 455 resolver: (RCTPromiseResolveBlock)resolve 456 rejecter:(RCTPromiseRejectBlock)reject) 457{ 458 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 459 id view = viewRegistry[reactTag]; 460 AIRMap *mapView = (AIRMap *)view; 461 if (![view isKindOfClass:[AIRMap class]]) { 462 reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid view returned from registry, expecting AIRMap, got: %@", view], NULL); 463 } else { 464 CLLocationCoordinate2D coordinate = [mapView convertPoint: 465 CGPointMake( 466 [point[@"x"] doubleValue], 467 [point[@"y"] doubleValue] 468 ) 469 toCoordinateFromView:mapView]; 470 471 resolve(@{ 472 @"latitude": @(coordinate.latitude), 473 @"longitude": @(coordinate.longitude), 474 }); 475 } 476 }]; 477} 478 479RCT_EXPORT_METHOD(getAddressFromCoordinates:(nonnull NSNumber *)reactTag 480 coordinate: (NSDictionary *)coordinate 481 resolver: (RCTPromiseResolveBlock)resolve 482 rejecter:(RCTPromiseRejectBlock)reject) 483{ 484 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 485 id view = viewRegistry[reactTag]; 486 if (![view isKindOfClass:[AIRMap class]]) { 487 reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid view returned from registry, expecting AIRMap, got: %@", view], NULL); 488 } else { 489 if (coordinate == nil || 490 ![[coordinate allKeys] containsObject:@"latitude"] || 491 ![[coordinate allKeys] containsObject:@"longitude"]) { 492 reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid coordinate format"], NULL); 493 } 494 CLLocation *location = [[CLLocation alloc] initWithLatitude:[coordinate[@"latitude"] doubleValue] 495 longitude:[coordinate[@"longitude"] doubleValue]]; 496 CLGeocoder *geoCoder = [[CLGeocoder alloc] init]; 497 [geoCoder reverseGeocodeLocation:location 498 completionHandler:^(NSArray *placemarks, NSError *error) { 499 if (error == nil && [placemarks count] > 0){ 500 CLPlacemark *placemark = placemarks[0]; 501 resolve(@{ 502 @"name" : [NSString stringWithFormat:@"%@", placemark.name], 503 @"thoroughfare" : [NSString stringWithFormat:@"%@", placemark.thoroughfare], 504 @"subThoroughfare" : [NSString stringWithFormat:@"%@", placemark.subThoroughfare], 505 @"locality" : [NSString stringWithFormat:@"%@", placemark.locality], 506 @"subLocality" : [NSString stringWithFormat:@"%@", placemark.subLocality], 507 @"administrativeArea" : [NSString stringWithFormat:@"%@", placemark.administrativeArea], 508 @"subAdministrativeArea" : [NSString stringWithFormat:@"%@", placemark.subAdministrativeArea], 509 @"postalCode" : [NSString stringWithFormat:@"%@", placemark.postalCode], 510 @"countryCode" : [NSString stringWithFormat:@"%@", placemark.ISOcountryCode], 511 @"country" : [NSString stringWithFormat:@"%@", placemark.country], 512 }); 513 } else { 514 reject(@"Invalid argument", [NSString stringWithFormat:@"Can not get address location"], NULL); 515 } 516 }]; 517 } 518 }]; 519} 520 521#pragma mark Take Snapshot 522- (void)takeMapSnapshot:(AIRMap *)mapView 523 snapshotter:(MKMapSnapshotter *) snapshotter 524 format:(NSString *)format 525 quality:(CGFloat) quality 526 result:(NSString *)result 527 callback:(RCTResponseSenderBlock) callback { 528 NSTimeInterval timeStamp = [[NSDate date] timeIntervalSince1970]; 529 NSString *pathComponent = [NSString stringWithFormat:@"Documents/snapshot-%.20lf.%@", timeStamp, format]; 530 NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent: pathComponent]; 531 532 [snapshotter startWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 533 completionHandler:^(MKMapSnapshot *snapshot, NSError *error) { 534 if (error) { 535 callback(@[error]); 536 return; 537 } 538 MKAnnotationView *pin = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:nil]; 539 540 UIImage *image = snapshot.image; 541 UIGraphicsBeginImageContextWithOptions(image.size, YES, image.scale); 542 { 543 [image drawAtPoint:CGPointMake(0.0f, 0.0f)]; 544 545 CGRect rect = CGRectMake(0.0f, 0.0f, image.size.width, image.size.height); 546 547 for (id <AIRMapSnapshot> overlay in mapView.overlays) { 548 if ([overlay respondsToSelector:@selector(drawToSnapshot:context:)]) { 549 [overlay drawToSnapshot:snapshot context:UIGraphicsGetCurrentContext()]; 550 } 551 } 552 553 for (id <MKAnnotation> annotation in mapView.annotations) { 554 CGPoint point = [snapshot pointForCoordinate:annotation.coordinate]; 555 556 MKAnnotationView* anView = [mapView viewForAnnotation: annotation]; 557 558 if (anView){ 559 pin = anView; 560 } 561 562 if (CGRectContainsPoint(rect, point)) { 563 point.x = point.x + pin.centerOffset.x - (pin.bounds.size.width / 2.0f); 564 point.y = point.y + pin.centerOffset.y - (pin.bounds.size.height / 2.0f); 565 if (pin.image) { 566 [pin.image drawAtPoint:point]; 567 } else { 568 CGRect pinRect = CGRectMake(point.x, point.y, pin.bounds.size.width, pin.bounds.size.height); 569 [pin drawViewHierarchyInRect:pinRect afterScreenUpdates:NO]; 570 } 571 } 572 } 573 574 UIImage *compositeImage = UIGraphicsGetImageFromCurrentImageContext(); 575 576 NSData *data; 577 if ([format isEqualToString:@"png"]) { 578 data = UIImagePNGRepresentation(compositeImage); 579 } 580 else if([format isEqualToString:@"jpg"]) { 581 data = UIImageJPEGRepresentation(compositeImage, quality); 582 } 583 584 if ([result isEqualToString:@"file"]) { 585 [data writeToFile:filePath atomically:YES]; 586 callback(@[[NSNull null], filePath]); 587 } 588 else if ([result isEqualToString:@"base64"]) { 589 callback(@[[NSNull null], [data base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn]]); 590 } 591 } 592 UIGraphicsEndImageContext(); 593 }]; 594} 595 596#pragma mark Gesture Recognizer Handlers 597 598#define MAX_DISTANCE_PX 10.0f 599- (void)handleMapTap:(UITapGestureRecognizer *)recognizer { 600 AIRMap *map = (AIRMap *)recognizer.view; 601 602 CGPoint tapPoint = [recognizer locationInView:map]; 603 CLLocationCoordinate2D tapCoordinate = [map convertPoint:tapPoint toCoordinateFromView:map]; 604 MKMapPoint mapPoint = MKMapPointForCoordinate(tapCoordinate); 605 CGPoint mapPointAsCGP = CGPointMake(mapPoint.x, mapPoint.y); 606 607 double maxMeters = [self metersFromPixel:MAX_DISTANCE_PX atPoint:tapPoint forMap:map]; 608 float nearestDistance = MAXFLOAT; 609 AIRMapPolyline *nearestPolyline = nil; 610 611 for (id<MKOverlay> overlay in map.overlays) { 612 if([overlay isKindOfClass:[AIRMapPolygon class]]){ 613 AIRMapPolygon *polygon = (AIRMapPolygon*) overlay; 614 if (polygon.onPress) { 615 CGMutablePathRef mpr = CGPathCreateMutable(); 616 617 for(int i = 0; i < polygon.coordinates.count; i++) { 618 AIRMapCoordinate *c = polygon.coordinates[i]; 619 MKMapPoint mp = MKMapPointForCoordinate(c.coordinate); 620 if (i == 0) { 621 CGPathMoveToPoint(mpr, NULL, mp.x, mp.y); 622 } else { 623 CGPathAddLineToPoint(mpr, NULL, mp.x, mp.y); 624 } 625 } 626 627 if (CGPathContainsPoint(mpr, NULL, mapPointAsCGP, FALSE)) { 628 id event = @{ 629 @"action": @"polygon-press", 630 @"coordinate": @{ 631 @"latitude": @(tapCoordinate.latitude), 632 @"longitude": @(tapCoordinate.longitude), 633 }, 634 }; 635 polygon.onPress(event); 636 } 637 638 CGPathRelease(mpr); 639 } 640 } 641 642 if([overlay isKindOfClass:[AIRMapPolyline class]]){ 643 AIRMapPolyline *polyline = (AIRMapPolyline*) overlay; 644 if (polyline.onPress) { 645 float distance = [self distanceOfPoint:MKMapPointForCoordinate(tapCoordinate) 646 toPoly:polyline]; 647 if (distance < nearestDistance) { 648 nearestDistance = distance; 649 nearestPolyline = polyline; 650 } 651 } 652 } 653 654 if ([overlay isKindOfClass:[AIRMapOverlay class]]) { 655 AIRMapOverlay *imageOverlay = (AIRMapOverlay*) overlay; 656 if (MKMapRectContainsPoint(imageOverlay.boundingMapRect, mapPoint)) { 657 if (imageOverlay.onPress) { 658 id event = @{ 659 @"action": @"image-overlay-press", 660 @"name": imageOverlay.name ?: @"unknown", 661 @"coordinate": @{ 662 @"latitude": @(imageOverlay.coordinate.latitude), 663 @"longitude": @(imageOverlay.coordinate.longitude) 664 } 665 }; 666 imageOverlay.onPress(event); 667 } 668 } 669 } 670 671 } 672 673 if (nearestDistance <= maxMeters) { 674 id event = @{ 675 @"action": @"polyline-press", 676 @"coordinate": @{ 677 @"latitude": @(tapCoordinate.latitude), 678 @"longitude": @(tapCoordinate.longitude) 679 } 680 }; 681 nearestPolyline.onPress(event); 682 } 683 684 if (!map.onPress) return; 685 map.onPress(@{ 686 @"coordinate": @{ 687 @"latitude": @(tapCoordinate.latitude), 688 @"longitude": @(tapCoordinate.longitude), 689 }, 690 @"position": @{ 691 @"x": @(tapPoint.x), 692 @"y": @(tapPoint.y), 693 }, 694 }); 695 696} 697 698- (void)handleMapDrag:(UIPanGestureRecognizer*)recognizer { 699 AIRMap *map = (AIRMap *)recognizer.view; 700 if (!map.onPanDrag) return; 701 702 CGPoint touchPoint = [recognizer locationInView:map]; 703 CLLocationCoordinate2D coord = [map convertPoint:touchPoint toCoordinateFromView:map]; 704 map.onPanDrag(@{ 705 @"coordinate": @{ 706 @"latitude": @(coord.latitude), 707 @"longitude": @(coord.longitude), 708 }, 709 @"position": @{ 710 @"x": @(touchPoint.x), 711 @"y": @(touchPoint.y), 712 }, 713 }); 714 715} 716 717- (void)handleMapDoubleTap:(UIPanGestureRecognizer*)recognizer { 718 AIRMap *map = (AIRMap *)recognizer.view; 719 if (!map.onDoublePress) return; 720 721 CGPoint touchPoint = [recognizer locationInView:map]; 722 CLLocationCoordinate2D coord = [map convertPoint:touchPoint toCoordinateFromView:map]; 723 map.onDoublePress(@{ 724 @"coordinate": @{ 725 @"latitude": @(coord.latitude), 726 @"longitude": @(coord.longitude), 727 }, 728 @"position": @{ 729 @"x": @(touchPoint.x), 730 @"y": @(touchPoint.y), 731 }, 732 }); 733 734} 735 736 737- (void)handleMapLongPress:(UITapGestureRecognizer *)recognizer { 738 739 // NOTE: android only does the equivalent of "began", so we only send in this case 740 if (recognizer.state != UIGestureRecognizerStateBegan) return; 741 742 AIRMap *map = (AIRMap *)recognizer.view; 743 if (!map.onLongPress) return; 744 745 CGPoint touchPoint = [recognizer locationInView:map]; 746 CLLocationCoordinate2D coord = [map convertPoint:touchPoint toCoordinateFromView:map]; 747 748 map.onLongPress(@{ 749 @"coordinate": @{ 750 @"latitude": @(coord.latitude), 751 @"longitude": @(coord.longitude), 752 }, 753 @"position": @{ 754 @"x": @(touchPoint.x), 755 @"y": @(touchPoint.y), 756 }, 757 }); 758} 759 760#pragma mark MKMapViewDelegate 761 762#pragma mark Polyline stuff 763 764- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id <MKOverlay>)overlay{ 765 if ([overlay isKindOfClass:[AIRMapPolyline class]]) { 766 return ((AIRMapPolyline *)overlay).renderer; 767 } else if ([overlay isKindOfClass:[AIRMapPolygon class]]) { 768 return ((AIRMapPolygon *)overlay).renderer; 769 } else if ([overlay isKindOfClass:[AIRMapCircle class]]) { 770 return ((AIRMapCircle *)overlay).renderer; 771 } else if ([overlay isKindOfClass:[AIRMapUrlTile class]]) { 772 return ((AIRMapUrlTile *)overlay).renderer; 773 } else if ([overlay isKindOfClass:[AIRMapWMSTile class]]) { 774 return ((AIRMapWMSTile *)overlay).renderer; 775 } else if ([overlay isKindOfClass:[AIRMapLocalTile class]]) { 776 return ((AIRMapLocalTile *)overlay).renderer; 777 } else if ([overlay isKindOfClass:[AIRMapOverlay class]]) { 778 return ((AIRMapOverlay *)overlay).renderer; 779 } else if([overlay isKindOfClass:[MKTileOverlay class]]) { 780 return [[MKTileOverlayRenderer alloc] initWithTileOverlay:overlay]; 781 } else { 782 return nil; 783 } 784} 785 786 787#pragma mark Annotation Stuff 788 789- (void)mapView:(AIRMap *)mapView didAddAnnotationViews:(NSArray<MKAnnotationView *> *)views 790{ 791 if(!mapView.userLocationCalloutEnabled){ 792 for(MKAnnotationView* view in views){ 793 if ([view.annotation isKindOfClass:[MKUserLocation class]]){ 794 [view setEnabled:NO]; 795 [view setCanShowCallout:NO]; 796 break; 797 } 798 } 799 } 800} 801 802 803- (void)mapView:(AIRMap *)mapView didSelectAnnotationView:(MKAnnotationView *)view 804{ 805 if ([view.annotation isKindOfClass:[AIRMapMarker class]]) { 806 [(AIRMapMarker *)view.annotation showCalloutView]; 807 } else if ([view.annotation isKindOfClass:[MKUserLocation class]] && mapView.userLocationAnnotationTitle != nil && view.annotation.title != mapView.userLocationAnnotationTitle) { 808 [(MKUserLocation*)view.annotation setTitle: mapView.userLocationAnnotationTitle]; 809 } 810 811} 812 813- (void)mapView:(AIRMap *)mapView didDeselectAnnotationView:(MKAnnotationView *)view { 814 if ([view.annotation isKindOfClass:[AIRMapMarker class]]) { 815 [(AIRMapMarker *)view.annotation hideCalloutView]; 816 } 817} 818 819- (MKAnnotationView *)mapView:(__unused AIRMap *)mapView viewForAnnotation:(AIRMapMarker *)marker 820{ 821 if (![marker isKindOfClass:[AIRMapMarker class]]) { 822 if ([marker isKindOfClass:[MKUserLocation class]] && mapView.userLocationAnnotationTitle != nil) { 823 [(MKUserLocation*)marker setTitle: mapView.userLocationAnnotationTitle]; 824 return nil; 825 } 826 return nil; 827 } 828 829 marker.map = mapView; 830 return [marker getAnnotationView]; 831} 832 833static int kDragCenterContext; 834 835- (void)mapView:(AIRMap *)mapView 836 annotationView:(MKAnnotationView *)view 837 didChangeDragState:(MKAnnotationViewDragState)newState 838 fromOldState:(MKAnnotationViewDragState)oldState 839{ 840 if (![view.annotation isKindOfClass:[AIRMapMarker class]]) return; 841 AIRMapMarker *marker = (AIRMapMarker *)view.annotation; 842 843 BOOL isPinView = [view isKindOfClass:[MKPinAnnotationView class]]; 844 845 id event = @{ 846 @"id": marker.identifier ?: @"unknown", 847 @"coordinate": @{ 848 @"latitude": @(marker.coordinate.latitude), 849 @"longitude": @(marker.coordinate.longitude) 850 } 851 }; 852 853 if (newState == MKAnnotationViewDragStateEnding || newState == MKAnnotationViewDragStateCanceling) { 854 if (!isPinView) { 855 [view setDragState:MKAnnotationViewDragStateNone animated:NO]; 856 } 857 if (mapView.onMarkerDragEnd) mapView.onMarkerDragEnd(event); 858 if (marker.onDragEnd) marker.onDragEnd(event); 859 860 if(_hasObserver) [view removeObserver:self forKeyPath:@"center"]; 861 _hasObserver = NO; 862 } else if (newState == MKAnnotationViewDragStateStarting) { 863 // MapKit doesn't emit continuous drag events. To get around this, we are going to use KVO. 864 [view addObserver:self forKeyPath:@"center" options:NSKeyValueObservingOptionNew context:&kDragCenterContext]; 865 _hasObserver = YES; 866 if (mapView.onMarkerDragStart) mapView.onMarkerDragStart(event); 867 if (marker.onDragStart) marker.onDragStart(event); 868 } 869} 870 871- (void)observeValueForKeyPath:(NSString *)keyPath 872 ofObject:(id)object 873 change:(NSDictionary *)change 874 context:(void *)context 875{ 876 if ([keyPath isEqualToString:@"center"] && [object isKindOfClass:[MKAnnotationView class]]) { 877 MKAnnotationView *view = (MKAnnotationView *)object; 878 AIRMapMarker *marker = (AIRMapMarker *)view.annotation; 879 880 // a marker we don't control might be getting dragged. Check just in case. 881 if (!marker) return; 882 883 AIRMap *map = marker.map; 884 885 // don't waste time calculating if there are no events to listen to it 886 if (!map.onMarkerDrag && !marker.onDrag) return; 887 888 CGPoint position = CGPointMake(view.center.x - view.centerOffset.x, view.center.y - view.centerOffset.y); 889 CLLocationCoordinate2D coordinate = [map convertPoint:position toCoordinateFromView:map]; 890 891 id event = @{ 892 @"id": marker.identifier ?: @"unknown", 893 @"position": @{ 894 @"x": @(position.x), 895 @"y": @(position.y), 896 }, 897 @"coordinate": @{ 898 @"latitude": @(coordinate.latitude), 899 @"longitude": @(coordinate.longitude), 900 } 901 }; 902 903 if (map.onMarkerDrag) map.onMarkerDrag(event); 904 if (marker.onDrag) marker.onDrag(event); 905 906 } else { 907 // This message is not for me; pass it on to super. 908 [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 909 } 910} 911 912- (void)mapView:(AIRMap *)mapView didUpdateUserLocation:(MKUserLocation *)location 913{ 914 id event = @{@"coordinate": @{ 915 @"latitude": @(location.coordinate.latitude), 916 @"longitude": @(location.coordinate.longitude), 917 @"altitude": @(location.location.altitude), 918 @"timestamp": @(location.location.timestamp.timeIntervalSinceReferenceDate * 1000), 919 @"accuracy": @(location.location.horizontalAccuracy), 920 @"altitudeAccuracy": @(location.location.verticalAccuracy), 921 @"speed": @(location.location.speed), 922 @"heading": @(location.location.course), 923 } 924 }; 925 926 if (mapView.onUserLocationChange) { 927 mapView.onUserLocationChange(event); 928 } 929 930 if (mapView.followUserLocation) { 931 [mapView setCenterCoordinate:location.coordinate animated:YES]; 932 } 933 934} 935 936- (void)mapViewDidChangeVisibleRegion:(AIRMap *)mapView 937{ 938 [self _regionChanged:mapView]; 939} 940 941- (void)mapView:(AIRMap *)mapView regionDidChangeAnimated:(__unused BOOL)animated 942{ 943 CGFloat zoomLevel = [self zoomLevel:mapView]; 944 945 // Don't send region did change events until map has 946 // started rendering, as these won't represent the final location 947 if(mapView.hasStartedRendering){ 948 [self _regionChanged:mapView]; 949 } 950 951 if (zoomLevel < mapView.minZoomLevel) { 952 [self setCenterCoordinate:[mapView centerCoordinate] zoomLevel:mapView.minZoomLevel animated:TRUE mapView:mapView]; 953 } 954 else if (zoomLevel > mapView.maxZoomLevel) { 955 [self setCenterCoordinate:[mapView centerCoordinate] zoomLevel:mapView.maxZoomLevel animated:TRUE mapView:mapView]; 956 } 957 958 // Don't send region did change events until map has 959 // started rendering, as these won't represent the final location 960 if (mapView.hasStartedRendering) { 961 [self _emitRegionChangeEvent:mapView continuous:NO]; 962 }; 963 964 mapView.pendingCenter = mapView.region.center; 965 mapView.pendingSpan = mapView.region.span; 966} 967 968- (void)mapViewWillStartRenderingMap:(AIRMap *)mapView 969{ 970 if (!mapView.hasStartedRendering) { 971 mapView.onMapReady(@{}); 972 mapView.hasStartedRendering = YES; 973 } 974 [mapView beginLoading]; 975} 976 977- (void)mapViewDidFinishRenderingMap:(AIRMap *)mapView fullyRendered:(BOOL)fullyRendered 978{ 979 [mapView finishLoading]; 980} 981 982#pragma mark Private 983 984- (void)_regionChanged:(AIRMap *)mapView 985{ 986 BOOL needZoom = NO; 987 CGFloat newLongitudeDelta = 0.0f; 988 MKCoordinateRegion region = mapView.region; 989 // On iOS 7, it's possible that we observe invalid locations during initialization of the map. 990 // Filter those out. 991 if (!CLLocationCoordinate2DIsValid(region.center)) { 992 return; 993 } 994 // Calculation on float is not 100% accurate. If user zoom to max/min and then move, it's likely the map will auto zoom to max/min from time to time. 995 // So let's try to make map zoom back to 99% max or 101% min so that there are some buffer that moving the map won't constantly hitting the max/min bound. 996 if (mapView.maxDelta > FLT_EPSILON && region.span.longitudeDelta > mapView.maxDelta) { 997 needZoom = YES; 998 newLongitudeDelta = mapView.maxDelta * (1 - AIRMapZoomBoundBuffer); 999 } else if (mapView.minDelta > FLT_EPSILON && region.span.longitudeDelta < mapView.minDelta) { 1000 needZoom = YES; 1001 newLongitudeDelta = mapView.minDelta * (1 + AIRMapZoomBoundBuffer); 1002 } 1003 if (needZoom) { 1004 region.span.latitudeDelta = region.span.latitudeDelta / region.span.longitudeDelta * newLongitudeDelta; 1005 region.span.longitudeDelta = newLongitudeDelta; 1006 mapView.region = region; 1007 } 1008 1009 // Continuously observe region changes 1010 [self _emitRegionChangeEvent:mapView continuous:YES]; 1011} 1012 1013- (void)_emitRegionChangeEvent:(AIRMap *)mapView continuous:(BOOL)continuous 1014{ 1015 if (!mapView.ignoreRegionChanges && mapView.onChange) { 1016 MKCoordinateRegion region = mapView.region; 1017 if (!CLLocationCoordinate2DIsValid(region.center)) { 1018 return; 1019 } 1020 1021#define FLUSH_NAN(value) (isnan(value) ? 0 : value) 1022 mapView.onChange(@{ 1023 @"continuous": @(continuous), 1024 @"region": @{ 1025 @"latitude": @(FLUSH_NAN(region.center.latitude)), 1026 @"longitude": @(FLUSH_NAN(region.center.longitude)), 1027 @"latitudeDelta": @(FLUSH_NAN(region.span.latitudeDelta)), 1028 @"longitudeDelta": @(FLUSH_NAN(region.span.longitudeDelta)), 1029 } 1030 }); 1031 } 1032} 1033 1034/** Returns the distance of |pt| to |poly| in meters 1035 * 1036 * 1037 */ 1038- (double)distanceOfPoint:(MKMapPoint)pt toPoly:(AIRMapPolyline *)poly 1039{ 1040 double distance = MAXFLOAT; 1041 for (int n = 0; n < poly.coordinates.count - 1; n++) { 1042 1043 MKMapPoint ptA = MKMapPointForCoordinate(poly.coordinates[n].coordinate); 1044 MKMapPoint ptB = MKMapPointForCoordinate(poly.coordinates[n + 1].coordinate); 1045 1046 double xDelta = ptB.x - ptA.x; 1047 double yDelta = ptB.y - ptA.y; 1048 1049 if (xDelta == 0.0 && yDelta == 0.0) { 1050 continue; 1051 } 1052 1053 double u = ((pt.x - ptA.x) * xDelta + (pt.y - ptA.y) * yDelta) / (xDelta * xDelta + yDelta * yDelta); 1054 MKMapPoint ptClosest; 1055 if (u < 0.0) { 1056 ptClosest = ptA; 1057 } 1058 else if (u > 1.0) { 1059 ptClosest = ptB; 1060 } 1061 else { 1062 ptClosest = MKMapPointMake(ptA.x + u * xDelta, ptA.y + u * yDelta); 1063 } 1064 1065 distance = MIN(distance, MKMetersBetweenMapPoints(ptClosest, pt)); 1066 } 1067 1068 return distance; 1069} 1070 1071 1072/** Converts |px| to meters at location |pt| */ 1073- (double)metersFromPixel:(NSUInteger)px atPoint:(CGPoint)pt forMap:(AIRMap *)mapView 1074{ 1075 CGPoint ptB = CGPointMake(pt.x + px, pt.y); 1076 1077 CLLocationCoordinate2D coordA = [mapView convertPoint:pt toCoordinateFromView:mapView]; 1078 CLLocationCoordinate2D coordB = [mapView convertPoint:ptB toCoordinateFromView:mapView]; 1079 1080 return MKMetersBetweenMapPoints(MKMapPointForCoordinate(coordA), MKMapPointForCoordinate(coordB)); 1081} 1082 1083+ (double)longitudeToPixelSpaceX:(double)longitude 1084{ 1085 return round(MERCATOR_OFFSET + MERCATOR_RADIUS * longitude * M_PI / 180.0); 1086} 1087 1088+ (double)latitudeToPixelSpaceY:(double)latitude 1089{ 1090 if (latitude == 90.0) { 1091 return 0; 1092 } else if (latitude == -90.0) { 1093 return MERCATOR_OFFSET * 2; 1094 } else { 1095 return round(MERCATOR_OFFSET - MERCATOR_RADIUS * logf((1 + sinf(latitude * M_PI / 180.0)) / (1 - sinf(latitude * M_PI / 180.0))) / 2.0); 1096 } 1097} 1098 1099+ (double)pixelSpaceXToLongitude:(double)pixelX 1100{ 1101 return ((round(pixelX) - MERCATOR_OFFSET) / MERCATOR_RADIUS) * 180.0 / M_PI; 1102} 1103 1104+ (double)pixelSpaceYToLatitude:(double)pixelY 1105{ 1106 return (M_PI / 2.0 - 2.0 * atan(exp((round(pixelY) - MERCATOR_OFFSET) / MERCATOR_RADIUS))) * 180.0 / M_PI; 1107} 1108 1109#pragma mark - 1110#pragma mark Helper methods 1111 1112- (MKCoordinateSpan)coordinateSpanWithMapView:(AIRMap *)mapView 1113 centerCoordinate:(CLLocationCoordinate2D)centerCoordinate 1114 andZoomLevel:(double)zoomLevel 1115{ 1116 // convert center coordiate to pixel space 1117 double centerPixelX = [AIRMapManager longitudeToPixelSpaceX:centerCoordinate.longitude]; 1118 double centerPixelY = [AIRMapManager latitudeToPixelSpaceY:centerCoordinate.latitude]; 1119 1120 // determine the scale value from the zoom level 1121 double zoomExponent = AIRMapMaxZoomLevel - zoomLevel; 1122 double zoomScale = pow(2, zoomExponent); 1123 1124 // scale the map’s size in pixel space 1125 CGSize mapSizeInPixels = mapView.bounds.size; 1126 double scaledMapWidth = mapSizeInPixels.width * zoomScale; 1127 double scaledMapHeight = mapSizeInPixels.height * zoomScale; 1128 1129 // figure out the position of the top-left pixel 1130 double topLeftPixelX = centerPixelX - (scaledMapWidth / 2); 1131 double topLeftPixelY = centerPixelY - (scaledMapHeight / 2); 1132 1133 // find delta between left and right longitudes 1134 CLLocationDegrees minLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX]; 1135 CLLocationDegrees maxLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX + scaledMapWidth]; 1136 CLLocationDegrees longitudeDelta = maxLng - minLng; 1137 1138 // find delta between top and bottom latitudes 1139 CLLocationDegrees minLat = [AIRMapManager pixelSpaceYToLatitude:topLeftPixelY]; 1140 CLLocationDegrees maxLat = [AIRMapManager pixelSpaceYToLatitude:topLeftPixelY + scaledMapHeight]; 1141 CLLocationDegrees latitudeDelta = -1 * (maxLat - minLat); 1142 1143 // create and return the lat/lng span 1144 MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta); 1145 return span; 1146} 1147 1148#pragma mark - 1149#pragma mark Public methods 1150 1151- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate 1152 zoomLevel:(double)zoomLevel 1153 animated:(BOOL)animated 1154 mapView:(AIRMap *)mapView 1155{ 1156 // clamp large numbers to 28 1157 zoomLevel = MIN(zoomLevel, AIRMapMaxZoomLevel); 1158 1159 // use the zoom level to compute the region 1160 MKCoordinateSpan span = [self coordinateSpanWithMapView:mapView centerCoordinate:centerCoordinate andZoomLevel:zoomLevel]; 1161 MKCoordinateRegion region = MKCoordinateRegionMake(centerCoordinate, span); 1162 1163 // set the region like normal 1164 [mapView setRegion:region animated:animated]; 1165} 1166 1167//KMapView cannot display tiles that cross the pole (as these would involve wrapping the map from top to bottom, something that a Mercator projection just cannot do). 1168-(MKCoordinateRegion)coordinateRegionWithMapView:(AIRMap *)mapView 1169 centerCoordinate:(CLLocationCoordinate2D)centerCoordinate 1170 andZoomLevel:(double)zoomLevel 1171{ 1172 // clamp lat/long values to appropriate ranges 1173 centerCoordinate.latitude = MIN(MAX(-90.0, centerCoordinate.latitude), 90.0); 1174 centerCoordinate.longitude = fmod(centerCoordinate.longitude, 180.0); 1175 1176 // convert center coordiate to pixel space 1177 double centerPixelX = [AIRMapManager longitudeToPixelSpaceX:centerCoordinate.longitude]; 1178 double centerPixelY = [AIRMapManager latitudeToPixelSpaceY:centerCoordinate.latitude]; 1179 1180 // determine the scale value from the zoom level 1181 double zoomExponent = AIRMapMaxZoomLevel - zoomLevel; 1182 double zoomScale = pow(2, zoomExponent); 1183 1184 // scale the map’s size in pixel space 1185 CGSize mapSizeInPixels = mapView.bounds.size; 1186 double scaledMapWidth = mapSizeInPixels.width * zoomScale; 1187 double scaledMapHeight = mapSizeInPixels.height * zoomScale; 1188 1189 // figure out the position of the left pixel 1190 double topLeftPixelX = centerPixelX - (scaledMapWidth / 2); 1191 1192 // find delta between left and right longitudes 1193 CLLocationDegrees minLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX]; 1194 CLLocationDegrees maxLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX + scaledMapWidth]; 1195 CLLocationDegrees longitudeDelta = maxLng - minLng; 1196 1197 // if we’re at a pole then calculate the distance from the pole towards the equator 1198 // as MKMapView doesn’t like drawing boxes over the poles 1199 double topPixelY = centerPixelY - (scaledMapHeight / 2); 1200 double bottomPixelY = centerPixelY + (scaledMapHeight / 2); 1201 BOOL adjustedCenterPoint = NO; 1202 if (topPixelY > MERCATOR_OFFSET * 2) { 1203 topPixelY = centerPixelY - scaledMapHeight; 1204 bottomPixelY = MERCATOR_OFFSET * 2; 1205 adjustedCenterPoint = YES; 1206 } 1207 1208 // find delta between top and bottom latitudes 1209 CLLocationDegrees minLat = [AIRMapManager pixelSpaceYToLatitude:topPixelY]; 1210 CLLocationDegrees maxLat = [AIRMapManager pixelSpaceYToLatitude:bottomPixelY]; 1211 CLLocationDegrees latitudeDelta = -1 * (maxLat - minLat); 1212 1213 // create and return the lat/lng span 1214 MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta); 1215 MKCoordinateRegion region = MKCoordinateRegionMake(centerCoordinate, span); 1216 // once again, MKMapView doesn’t like drawing boxes over the poles 1217 // so adjust the center coordinate to the center of the resulting region 1218 if (adjustedCenterPoint) { 1219 region.center.latitude = [AIRMapManager pixelSpaceYToLatitude:((bottomPixelY + topPixelY) / 2.0)]; 1220 } 1221 1222 return region; 1223} 1224 1225- (double) zoomLevel:(AIRMap *)mapView { 1226 MKCoordinateRegion region = mapView.region; 1227 1228 double centerPixelX = [AIRMapManager longitudeToPixelSpaceX: region.center.longitude]; 1229 double topLeftPixelX = [AIRMapManager longitudeToPixelSpaceX: region.center.longitude - region.span.longitudeDelta / 2]; 1230 1231 double scaledMapWidth = (centerPixelX - topLeftPixelX) * 2; 1232 CGSize mapSizeInPixels = mapView.bounds.size; 1233 double zoomScale = scaledMapWidth / mapSizeInPixels.width; 1234 double zoomExponent = log(zoomScale) / log(2); 1235 double zoomLevel = AIRMapMaxZoomLevel - zoomExponent; 1236 1237 return zoomLevel; 1238} 1239 1240#pragma mark MKMapViewDelegate - Tracking the User Location 1241 1242- (void)mapView:(AIRMap *)mapView didFailToLocateUserWithError:(NSError *)error { 1243 id event = @{@"error": @{ @"message": error.localizedDescription }}; 1244 if (mapView.onUserLocationChange) { 1245 mapView.onUserLocationChange(event); 1246 } 1247} 1248 1249- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { 1250 return YES; 1251} 1252 1253@end 1254