I am wanting to display and also cache map tiles for offline use from a chosen service such as Open Street Maps. This seems to be easy since iOS7
and it works using:
static NSString * const template = @"http://tile.openstreetmap.org/{z}/{x}/{y}.png";
self.tileOverlay = [[ZSSTileOverlay alloc] initWithURLTemplate:template];
self.tileOverlay.canReplaceMapContent = YES;
[self.mapView addOverlay:self.tileOverlay level:MKOverlayLevelAboveLabels];
In my MKTileOverLay
subclass I am overriding loadTileAtPath:result:
to download the tiles and save them to disk for offline use. Later I will implement a method for users to choose what part of the map to download:
- (void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *tileData, NSError *error))result {
NSString *url = [NSString stringWithFormat:@"http://tile.openstreetmap.org/%li/%li/%li.png", (long)path.z, (long)path.x, (long)path.y];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (data == nil) {
// Load a default tile here
} else {
UIImage *image = [[UIImage alloc] initWithData:data];
result(data, nil);
NSString *pathForWritting = [self pathToWriteImage:path];
NSString *fileName = [NSString stringWithFormat:@"%li", (long)path.y];
[self saveImage:image withFileName:fileName ofType:@"png" inDirectory:pathForWritting];
}
}];
}
- (void)saveImage:(UIImage *)image withFileName:(NSString *)imageName ofType:(NSString *)extension inDirectory:(NSString *)directoryPath {
// Create folder structure if it doesn't exist
if (![[NSFileManager defaultManager] fileExistsAtPath:directoryPath]) {
NSError *error = nil;
[[NSFileManager defaultManager] createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:&error];
if (error) {
NSLog(@"ERROR: %@", error.localizedDescription);
}
}
if ([[extension lowercaseString] isEqualToString:@"png"]) {
NSString *fullPath = [directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", imageName, @"png"]];
[UIImagePNGRepresentation(image) writeToFile:fullPath atomically:YES];
} else if ([[extension lowercaseString] isEqualToString:@"jpg"] || [[extension lowercaseString] isEqualToString:@"jpeg"]) {
[UIImageJPEGRepresentation(image, 1.0) writeToFile:[directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", imageName, @"jpg"]] options:NSAtomicWrite error:nil];
}
}
- (NSURL *)applicationDocumentsDirectory {
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask] lastObject];
}
- (NSString *)pathToWriteImage:(MKTileOverlayPath)path {
NSString *pathFolder = [NSString stringWithFormat:@"tiles/%li/%li", (long)path.z, (long)path.x];
return [NSString stringWithFormat:@"%@/%@", [self applicationDocumentsDirectory].path, pathFolder];
}
This works exactly how I would expect, and it saves the images into appropriately named folders:
Now, the real question here is threefold:
- Is saving the tiles to disk, and then loading them back when offline a good idea? Or should these be saved in something like an SQL Lite DB?
- It seems that tiles are automatically loaded by
MapKit
, why is it necessary to download them a second time inloadTitleAtPath:
? Is there no way to get the tiles thatMKMapView
is already loading? - What is a good method (ignoring how I have done it here, that was for testing) for allowing the user to choose what parts of the map they would like to download and what map levels? Current rect with all zoom levels higher than current one?