1// 2// AIRMapUrlTileCachedOverlay.m 3// Airmaps 4// 5// Created by Markus Suomi on 10/04/2021. 6// 7 8#import "AIRMapUrlTileCachedOverlay.h" 9 10@interface AIRMapUrlTileCachedOverlay () 11 12@end 13 14@implementation AIRMapUrlTileCachedOverlay { 15 CIContext *_ciContext; 16 CGColorSpaceRef _colorspace; 17 NSURLSession *_urlSession; 18} 19 20- (void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result 21{ 22 if (!result) return; 23 24 NSInteger maximumZ = self.maximumNativeZ ? self.maximumNativeZ : path.z; 25 [self scaleIfNeededLowerZoomTile:path maximumZ:maximumZ result:^(NSData *image, NSError *error) { 26 if (!image && self.offlineMode && self.tileCachePath) { 27 NSInteger zoomLevelToStart = (path.z > maximumZ) ? maximumZ - 1 : path.z - 1; 28 NSInteger minimumZoomToSearch = self.minimumZ >= zoomLevelToStart - 3 ? self.minimumZ : zoomLevelToStart - 3; 29 [self findLowerZoomTileAndScale:path tryZ:zoomLevelToStart minZ:minimumZoomToSearch result:result]; 30 } else { 31 result(image, error); 32 } 33 }]; 34} 35 36- (void)scaleIfNeededLowerZoomTile:(MKTileOverlayPath)path maximumZ:(NSInteger)maximumZ result:(void (^)(NSData *, NSError *))result 37{ 38 NSInteger overZoomLevel = path.z - maximumZ; 39 if (overZoomLevel <= 0) { 40 [self getTileImage:path result:result]; 41 return; 42 } 43 44 NSInteger zoomFactor = 1 << overZoomLevel; 45 46 MKTileOverlayPath parentTile; 47 parentTile.x = path.x >> overZoomLevel; 48 parentTile.y = path.y >> overZoomLevel; 49 parentTile.z = path.z - overZoomLevel; 50 parentTile.contentScaleFactor = path.contentScaleFactor; 51 52 NSInteger xOffset = path.x % zoomFactor; 53 NSInteger yOffset = path.y % zoomFactor; 54 NSInteger subTileSize = self.tileSize.width / zoomFactor; 55 56 if (!_ciContext) _ciContext = [CIContext context]; 57 if (!_colorspace) _colorspace = CGColorSpaceCreateDeviceRGB(); 58 59 [self getTileImage:parentTile result:^(NSData *image, NSError *error) { 60 if (!image) { 61 result(nil, nil); 62 return; 63 } 64 65 CIImage* originalCIImage = [CIImage imageWithData:image]; 66 67 CGRect rect; 68 rect.origin.x = xOffset * subTileSize; 69 rect.origin.y = self.tileSize.width - (yOffset + 1) * subTileSize; 70 rect.size.width = subTileSize; 71 rect.size.height = subTileSize; 72 CIVector *inputRect = [CIVector vectorWithCGRect:rect]; 73 CIFilter* cropFilter = [CIFilter filterWithName:@"CICrop"]; 74 [cropFilter setValue:originalCIImage forKey:@"inputImage"]; 75 [cropFilter setValue:inputRect forKey:@"inputRectangle"]; 76 77 CGAffineTransform trans = CGAffineTransformMakeScale(zoomFactor, zoomFactor); 78 CIImage* scaledCIImage = [cropFilter.outputImage imageByApplyingTransform:trans]; 79 80 NSData *finalImage = [_ciContext PNGRepresentationOfImage:scaledCIImage format:kCIFormatABGR8 colorSpace:_colorspace options:nil]; 81 result(finalImage, nil); 82 }]; 83} 84 85- (void)findLowerZoomTileAndScale:(MKTileOverlayPath)path tryZ:(NSInteger)tryZ minZ:(NSInteger)minZ result:(void (^)(NSData *, NSError *))result 86{ 87 [self scaleIfNeededLowerZoomTile:path maximumZ:tryZ result:^(NSData *image, NSError *error) { 88 if (image) { 89 result(image, error); 90 } else if (tryZ >= minZ) { 91 [self findLowerZoomTileAndScale:path tryZ:tryZ - 1 minZ:minZ result:result]; 92 } else { 93 result(nil, nil); 94 } 95 }]; 96} 97 98- (void)getTileImage:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result 99{ 100 NSData *image; 101 NSURL *tileCacheFileDirectory = [NSURL URLWithString:[NSString stringWithFormat:@"%d/%d/", (int)path.z, (int)path.x] relativeToURL:self.tileCachePath]; 102 NSURL *tileCacheFilePath = [NSURL URLWithString:[NSString stringWithFormat:@"%d", (int)path.y] relativeToURL:tileCacheFileDirectory]; 103 104 if (self.tileCachePath) { 105 image = [self readTileImage:path fromFilePath:tileCacheFilePath]; 106 if (image) { 107 result(image, nil); 108 if (!self.offlineMode && self.tileCacheMaxAge) { 109 [self checkForRefresh:path fromFilePath:tileCacheFilePath]; 110 } 111 } 112 } 113 114 if (!image) { 115 if (!self.offlineMode) { 116 [self fetchTile:path result:^(NSData *image, NSError *error) { 117 result(image, error); 118 if (image && self.tileCachePath) { 119 [self writeTileImage:tileCacheFileDirectory withTileCacheFilePath:tileCacheFilePath withTileData:image]; 120 } 121 }]; 122 } else { 123 result(nil, nil); 124 } 125 } 126} 127 128- (NSData *)readTileImage:(MKTileOverlayPath)path fromFilePath:(NSURL *)tileCacheFilePath 129{ 130 NSError *error; 131 132 if ([[NSFileManager defaultManager] fileExistsAtPath:[tileCacheFilePath path]]) { 133 if (!self.tileCacheMaxAge) { 134 [[NSFileManager defaultManager] setAttributes:@{NSFileModificationDate:[NSDate date]} 135 ofItemAtPath:[tileCacheFilePath path] 136 error:&error]; 137 } 138 139 NSData *tile = [NSData dataWithContentsOfFile:[tileCacheFilePath path]]; 140 NSLog(@"tileCache HIT for %d_%d_%d", (int)path.z, (int)path.x, (int)path.y); 141 NSLog(@"tileCache HIT, with max age set at %d", self.tileCacheMaxAge); 142 return tile; 143 } else { 144 NSLog(@"tileCache MISS for %d_%d_%d", (int)path.z, (int)path.x, (int)path.y); 145 return nil; 146 } 147} 148 149- (void)fetchTile:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result 150{ 151 if (!_urlSession) [self createURLSession]; 152 153 [[_urlSession dataTaskWithURL:[self URLForTilePath:path] 154 completionHandler:^(NSData *data, 155 NSURLResponse *response, 156 NSError *error) { 157 result(data, error); 158 }] resume]; 159} 160 161- (void)writeTileImage:(NSURL *)tileCacheFileDirectory withTileCacheFilePath:(NSURL *)tileCacheFilePath withTileData:(NSData *)data 162{ 163 NSError *error; 164 165 if (![[NSFileManager defaultManager] fileExistsAtPath:[tileCacheFileDirectory path]]) { 166 [[NSFileManager defaultManager] createDirectoryAtPath:[tileCacheFileDirectory path] withIntermediateDirectories:YES attributes:nil error:&error]; 167 if (error) { 168 NSLog(@"Error: %@", error); 169 return; 170 } 171 } 172 173 [[NSFileManager defaultManager] createFileAtPath:[tileCacheFilePath path] contents:data attributes:nil]; 174 NSLog(@"tileCache SAVED tile %@", [tileCacheFilePath path]); 175} 176 177- (void)createTileCacheDirectory 178{ 179 NSError *error; 180 NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 181 NSString *documentsDirectory = [paths objectAtIndex:0]; 182 NSString *tileCacheBaseDirectory = [NSString stringWithFormat:@"%@/tileCache", documentsDirectory]; 183 self.tileCachePath = [NSURL fileURLWithPath:tileCacheBaseDirectory isDirectory:YES]; 184 185 if (![[NSFileManager defaultManager] fileExistsAtPath:[self.tileCachePath path]]) 186 [[NSFileManager defaultManager] createDirectoryAtPath:[self.tileCachePath path] withIntermediateDirectories:NO attributes:nil error:&error]; 187} 188 189- (void)createURLSession 190{ 191 if (!_urlSession) { 192 _urlSession = [NSURLSession sharedSession]; 193 } 194} 195 196- (void)checkForRefresh:(MKTileOverlayPath)path fromFilePath:(NSURL *)tileCacheFilePath 197{ 198 if ([self doesFileNeedRefresh:path fromFilePath:tileCacheFilePath withMaxAge:self.tileCacheMaxAge]) { 199 dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^ { 200 // This code runs asynchronously! 201 if ([self doesFileNeedRefresh:path fromFilePath:tileCacheFilePath withMaxAge:self.tileCacheMaxAge]) { 202 if (!_urlSession) [self createURLSession]; 203 204 [[_urlSession dataTaskWithURL:[self URLForTilePath:path] 205 completionHandler:^(NSData *data, 206 NSURLResponse *response, 207 NSError *error) { 208 if (!error) { 209 [[NSFileManager defaultManager] createFileAtPath:[tileCacheFilePath path] contents:data attributes:nil]; 210 NSLog(@"tileCache File refreshed at %@", [tileCacheFilePath path]); 211 } 212 }] resume]; 213 } 214 }); 215 } 216} 217 218- (BOOL)doesFileNeedRefresh:(MKTileOverlayPath)path fromFilePath:(NSURL *)tileCacheFilePath withMaxAge:(NSInteger)tileCacheMaxAge 219{ 220 NSError *error; 221 NSDictionary<NSFileAttributeKey, id> *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[tileCacheFilePath path] error:&error]; 222 223 if (fileAttributes) { 224 NSDate *modificationDate = fileAttributes[@"NSFileModificationDate"]; 225 if (modificationDate) { 226 if (-1 * (int)modificationDate.timeIntervalSinceNow > tileCacheMaxAge) { 227 return YES; 228 } 229 } 230 } 231 232 return NO; 233} 234 235@end 236