/* |
File: PhotoMapViewController.m |
Abstract: Primary map view controller. |
Version: 1.1 |
|
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
|
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
|
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
|
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
|
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
|
*/ |
|
#import "PhotoMapViewController.h" |
#import <ImageIO/ImageIO.h> |
|
#import "PhotoAnnotation.h" |
#import "PhotosViewController.h" |
#import "LoadingStatus.h" |
|
@interface PhotoMapViewController () |
|
@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)populateWorldWithAllPhotoAnnotations { |
|
// 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 mapview |
[self populateWorldWithAllPhotoAnnotations]; |
} |
|
- (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 |