Retired Document
Important: This document may not represent best practices for current development. Links to downloads and other resources may no longer be valid.
PhotoMap/PhotoMapViewController.m
/* |
Copyright (C) 2018 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
Primary map view controller. |
*/ |
#import "PhotoMapViewController.h" |
#import "PhotoAnnotation.h" |
#import "PhotosViewController.h" |
#import "LoadingStatus.h" |
@import MapKit; |
@import ImageIO; |
@interface PhotoMapViewController () <MKMapViewDelegate> |
@property (nonatomic, strong) NSArray *photos; |
@property (nonatomic, strong) MKMapView *allAnnotationsMapView; |
@property (nonatomic, strong) IBOutlet MKMapView *mapView; |
@end |
#pragma mark - |
@implementation PhotoMapViewController |
- (NSArray *)photoSetFromPath:(NSString *)path { |
NSMutableArray *photos = [[NSMutableArray alloc] init]; |
// The bulk of our work here is going to be loading the files and looking up metadata |
// Thus, we see a major speed improvement by loading multiple photos simultaneously |
// |
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; |
[queue setMaxConcurrentOperationCount: 8]; |
NSArray *photoPaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"jpg" inDirectory:path]; |
for (NSString *photoPath in photoPaths) { |
[queue addOperationWithBlock:^{ |
NSData *imageData = [NSData dataWithContentsOfFile:photoPath]; |
CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((CFDataRef)imageData); |
CGImageSourceRef imageSource = CGImageSourceCreateWithDataProvider(dataProvider, NULL); |
NSDictionary *imageProperties = (NSDictionary *)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(imageSource,0, NULL)); |
// check if the image is geotagged |
NSDictionary *gpsInfo = imageProperties[(NSString *)kCGImagePropertyGPSDictionary]; |
if (gpsInfo) { |
CLLocationCoordinate2D coord; |
coord.latitude = [gpsInfo[(NSString *)kCGImagePropertyGPSLatitude] doubleValue]; |
coord.longitude = [gpsInfo[(NSString *)kCGImagePropertyGPSLongitude] doubleValue]; |
if ([gpsInfo[(NSString *)kCGImagePropertyGPSLatitudeRef] isEqualToString:@"S"]) |
coord.latitude = coord.latitude * -1; |
if ([gpsInfo[(NSString *)kCGImagePropertyGPSLongitudeRef] isEqualToString:@"W"]) |
coord.longitude = coord.longitude * -1; |
NSString *fileName = [[photoPath lastPathComponent] stringByDeletingPathExtension]; |
PhotoAnnotation *photo = [[PhotoAnnotation alloc] initWithImagePath:photoPath title:fileName coordinate:coord]; |
@synchronized(photos) { |
[photos addObject:photo]; |
} |
} |
if (imageSource) |
CFRelease(imageSource); |
if (imageProperties) |
CFRelease(CFBridgingRetain(imageProperties)); |
if (dataProvider) |
CFRelease(dataProvider); |
}]; |
} |
[queue waitUntilAllOperationsAreFinished]; |
return photos; |
} |
- (void)populateMapWithAllPhotoAnnotations { |
// add a temporary loading view |
LoadingStatus *loadingStatus = [LoadingStatus defaultLoadingStatusWithWidth:CGRectGetWidth(self.view.frame)]; |
[self.view addSubview:loadingStatus]; |
// loading/processing photos might take a while -- do it asynchronously |
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
NSArray *photos = [self photoSetFromPath:@"PhotoSet"]; |
NSAssert(photos != nil, @"No photos found"); |
_photos = photos; |
dispatch_async(dispatch_get_main_queue(), ^{ |
[_allAnnotationsMapView addAnnotations:_photos]; |
[self updateVisibleAnnotations]; |
[loadingStatus removeFromSuperviewWithFade]; |
}); |
}); |
} |
- (id<MKAnnotation>)annotationInGrid:(MKMapRect)gridMapRect usingAnnotations:(NSSet *)annotations { |
// first, see if one of the annotations we were already showing is in this mapRect |
NSSet *visibleAnnotationsInBucket = [self.mapView annotationsInMapRect:gridMapRect]; |
NSSet *annotationsForGridSet = [annotations objectsPassingTest:^BOOL(id obj, BOOL *stop) { |
BOOL returnValue = ([visibleAnnotationsInBucket containsObject:obj]); |
if (returnValue) |
{ |
*stop = YES; |
} |
return returnValue; |
}]; |
if (annotationsForGridSet.count != 0) { |
return [annotationsForGridSet anyObject]; |
} |
// otherwise, sort the annotations based on their distance from the center of the grid square, |
// then choose the one closest to the center to show |
MKMapPoint centerMapPoint = MKMapPointMake(MKMapRectGetMidX(gridMapRect), MKMapRectGetMidY(gridMapRect)); |
NSArray *sortedAnnotations = [[annotations allObjects] sortedArrayUsingComparator:^(id obj1, id obj2) { |
MKMapPoint mapPoint1 = MKMapPointForCoordinate(((id<MKAnnotation>)obj1).coordinate); |
MKMapPoint mapPoint2 = MKMapPointForCoordinate(((id<MKAnnotation>)obj2).coordinate); |
CLLocationDistance distance1 = MKMetersBetweenMapPoints(mapPoint1, centerMapPoint); |
CLLocationDistance distance2 = MKMetersBetweenMapPoints(mapPoint2, centerMapPoint); |
if (distance1 < distance2) { |
return NSOrderedAscending; |
} else if (distance1 > distance2) { |
return NSOrderedDescending; |
} |
return NSOrderedSame; |
}]; |
return sortedAnnotations[0]; |
} |
- (void)updateVisibleAnnotations { |
// This value to controls the number of off screen annotations are displayed. |
// A bigger number means more annotations, less chance of seeing annotation views pop in but decreased performance. |
// A smaller number means fewer annotations, more chance of seeing annotation views pop in but better performance. |
static float marginFactor = 2.0; |
// Adjust this roughly based on the dimensions of your annotations views. |
// Bigger numbers more aggressively coalesce annotations (fewer annotations displayed but better performance). |
// Numbers too small result in overlapping annotations views and too many annotations on screen. |
static float bucketSize = 60.0; |
// find all the annotations in the visible area + a wide margin to avoid popping annotation views in and out while panning the map. |
MKMapRect visibleMapRect = [self.mapView visibleMapRect]; |
MKMapRect adjustedVisibleMapRect = MKMapRectInset(visibleMapRect, -marginFactor * visibleMapRect.size.width, -marginFactor * visibleMapRect.size.height); |
// determine how wide each bucket will be, as a MKMapRect square |
CLLocationCoordinate2D leftCoordinate = [self.mapView convertPoint:CGPointZero toCoordinateFromView:self.view]; |
CLLocationCoordinate2D rightCoordinate = [self.mapView convertPoint:CGPointMake(bucketSize, 0) toCoordinateFromView:self.view]; |
double gridSize = MKMapPointForCoordinate(rightCoordinate).x - MKMapPointForCoordinate(leftCoordinate).x; |
MKMapRect gridMapRect = MKMapRectMake(0, 0, gridSize, gridSize); |
// condense annotations, with a padding of two squares, around the visibleMapRect |
double startX = floor(MKMapRectGetMinX(adjustedVisibleMapRect) / gridSize) * gridSize; |
double startY = floor(MKMapRectGetMinY(adjustedVisibleMapRect) / gridSize) * gridSize; |
double endX = floor(MKMapRectGetMaxX(adjustedVisibleMapRect) / gridSize) * gridSize; |
double endY = floor(MKMapRectGetMaxY(adjustedVisibleMapRect) / gridSize) * gridSize; |
// for each square in our grid, pick one annotation to show |
gridMapRect.origin.y = startY; |
while (MKMapRectGetMinY(gridMapRect) <= endY) { |
gridMapRect.origin.x = startX; |
while (MKMapRectGetMinX(gridMapRect) <= endX) { |
NSSet *allAnnotationsInBucket = [self.allAnnotationsMapView annotationsInMapRect:gridMapRect]; |
NSSet *visibleAnnotationsInBucket = [self.mapView annotationsInMapRect:gridMapRect]; |
// we only care about PhotoAnnotations |
NSMutableSet *filteredAnnotationsInBucket = [[allAnnotationsInBucket objectsPassingTest:^BOOL(id obj, BOOL *stop) { |
return ([obj isKindOfClass:[PhotoAnnotation class]]); |
}] mutableCopy]; |
if (filteredAnnotationsInBucket.count > 0) { |
PhotoAnnotation *annotationForGrid = (PhotoAnnotation *)[self annotationInGrid:gridMapRect usingAnnotations:filteredAnnotationsInBucket]; |
[filteredAnnotationsInBucket removeObject:annotationForGrid]; |
// give the annotationForGrid a reference to all the annotations it will represent |
annotationForGrid.containedAnnotations = [filteredAnnotationsInBucket allObjects]; |
[self.mapView addAnnotation:annotationForGrid]; |
for (PhotoAnnotation *annotation in filteredAnnotationsInBucket) { |
// give all the other annotations a reference to the one which is representing them |
annotation.clusterAnnotation = annotationForGrid; |
annotation.containedAnnotations = nil; |
// remove annotations which we've decided to cluster |
if ([visibleAnnotationsInBucket containsObject:annotation]) { |
CLLocationCoordinate2D actualCoordinate = annotation.coordinate; |
[UIView animateWithDuration:0.3 animations:^{ |
annotation.coordinate = annotation.clusterAnnotation.coordinate; |
} completion:^(BOOL finished) { |
annotation.coordinate = actualCoordinate; |
[self.mapView removeAnnotation:annotation]; |
}]; |
} |
} |
} |
gridMapRect.origin.x += gridSize; |
} |
gridMapRect.origin.y += gridSize; |
} |
} |
#pragma mark - UIViewController |
static const CLLocationCoordinate2D CherryLakeLocation = {38.002493, -119.9078987}; |
- (void)viewDidLoad { |
[super viewDidLoad]; |
// center to Cherry Lake, but zoomed outward |
MKCoordinateRegion newRegion; |
newRegion.center = CherryLakeLocation; |
newRegion.span.latitudeDelta = 5.0; |
newRegion.span.longitudeDelta = 5.0; |
self.mapView.region = newRegion; |
_allAnnotationsMapView = [[MKMapView alloc] initWithFrame:CGRectZero]; |
// now load all photos from Resources and add them as annotations to the map view |
[self populateMapWithAllPhotoAnnotations]; |
} |
- (IBAction)zoomToCherryLake:(id)sender { |
// clear any annotations in preparation for zooming |
[self.mapView removeAnnotations:[self.mapView annotations]]; |
// center to Cherry Lake to see the rest of the annotations |
MKCoordinateRegion newRegion; |
newRegion.center = CherryLakeLocation; |
newRegion.span.latitudeDelta = 0.05; |
newRegion.span.longitudeDelta = 0.05; |
[self.mapView setRegion:newRegion animated:YES]; |
} |
#pragma mark - MKMapViewDelegate |
- (void)mapView:(MKMapView *)aMapView regionDidChangeAnimated:(BOOL)animated { |
[self updateVisibleAnnotations]; |
} |
- (void)mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views { |
for (MKAnnotationView *annotationView in views) { |
if (![annotationView.annotation isKindOfClass:[PhotoAnnotation class]]) { |
continue; |
} |
PhotoAnnotation *annotation = (PhotoAnnotation *)annotationView.annotation; |
if (annotation.clusterAnnotation != nil) { |
// animate the annotation from it's old container's coordinate, to its actual coordinate |
CLLocationCoordinate2D actualCoordinate = annotation.coordinate; |
CLLocationCoordinate2D containerCoordinate = annotation.clusterAnnotation.coordinate; |
// since it's displayed on the map, it is no longer contained by another annotation, |
// (We couldn't reset this in -updateVisibleAnnotations because we needed the reference to it here |
// to get the containerCoordinate) |
annotation.clusterAnnotation = nil; |
annotation.coordinate = containerCoordinate; |
[UIView animateWithDuration:0.3 animations:^{ |
annotation.coordinate = actualCoordinate; |
}]; |
} |
} |
} |
- (MKAnnotationView *)mapView:(MKMapView *)aMapView viewForAnnotation:(id <MKAnnotation>)annotation { |
static NSString *annotationIdentifier = @"Photo"; |
if (aMapView != self.mapView) |
return nil; |
if ([annotation isKindOfClass:[PhotoAnnotation class]]) { |
MKPinAnnotationView *annotationView = (MKPinAnnotationView *)[self.mapView dequeueReusableAnnotationViewWithIdentifier:annotationIdentifier]; |
if (annotationView == nil) |
annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:annotationIdentifier]; |
annotationView.canShowCallout = YES; |
UIButton *disclosureButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure]; |
annotationView.rightCalloutAccessoryView = disclosureButton; |
return annotationView; |
} |
return nil; |
} |
// user tapped the call out accessory 'i' button |
- (void)mapView:(MKMapView *)aMapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control { |
PhotoAnnotation *annotation = (PhotoAnnotation *)view.annotation; |
NSMutableArray *photosToShow = [NSMutableArray arrayWithObject:annotation]; |
[photosToShow addObjectsFromArray:annotation.containedAnnotations]; |
PhotosViewController *viewController = [[PhotosViewController alloc] init]; |
viewController.edgesForExtendedLayout = UIRectEdgeNone; |
viewController.photosToShow = photosToShow; |
[self.navigationController pushViewController:viewController animated:YES]; |
} |
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view { |
if ([view.annotation isKindOfClass:[PhotoAnnotation class]]) |
{ |
PhotoAnnotation *annotation = (PhotoAnnotation *)view.annotation; |
[annotation updateSubtitleIfNeeded]; |
} |
} |
@end |
Copyright © 2018 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2018-04-26