Authored by Tracy Harton

Handle HTTP errors gracefully. Retry some, send error notification on others

... ... @@ -98,13 +98,8 @@
// image.screenLocation.size.width, image.screenLocation.size.height);
// RMLog(@"tileAdded");
[image makeLayer];
CALayer *sublayer = [image layer];
sublayer.delegate = self;
[layer addSublayer:sublayer];
[layer addSublayer:[image layer]];
}
-(void) tileRemoved: (RMTile) tile
... ...
... ... @@ -35,6 +35,7 @@ typedef NSImage UIImage;
#import "RMFoundation.h"
#import "RMTile.h"
#import "RMTileProxy.h"
@class RMTileImage;
@class NSData;
... ... @@ -69,16 +70,13 @@ extern NSString * const RMMapImageLoadingCancelledNotification;
+ (RMTileImage*) dummyTile: (RMTile)tile;
//- (id) increaseLoadingPriority;
//- (id) decreaseLoadingPriority;
- (void)drawInRect:(CGRect)rect;
- (void)draw;
+ (RMTileImage*)imageForTile: (RMTile) tile withURL: (NSString*)url;
+ (RMTileImage*)imageForTile: (RMTile) tile fromFile: (NSString*)filename;
+ (RMTileImage*)imageForTile: (RMTile) tile withData: (NSData*)data;
- (void)drawInRect:(CGRect)rect;
- (void)draw;
- (void)moveBy: (CGSize) delta;
- (void)zoomByFactor: (float) zoomFactor near:(CGPoint) center;
... ... @@ -92,6 +90,8 @@ extern NSString * const RMMapImageLoadingCancelledNotification;
- (BOOL)isLoaded;
- (void) displayProxy:(UIImage*)img;
@property (readwrite, assign) CGRect screenLocation;
@property (readonly, assign) RMTile tile;
@property (readonly) CALayer *layer;
... ...
... ... @@ -53,12 +53,14 @@ NSString * const RMMapImageLoadingCancelledNotification = @"MapImageLoadingCance
lastUsedTime = nil;
dataPending = nil;
screenLocation = CGRectZero;
[self makeLayer];
[self touch];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(tileRemovedFromScreen:)
name:RMMapImageRemovedFromScreenNotification object:self];
selector:@selector(tileRemovedFromScreen:)
name:RMMapImageRemovedFromScreenNotification object:self];
return self;
}
... ... @@ -86,9 +88,6 @@ NSString * const RMMapImageLoadingCancelledNotification = @"MapImageLoadingCance
[[NSNotificationCenter defaultCenter] removeObserver:self];
// if (image)
// CGImageRelease(image);
[image release]; image = nil;
[layer release]; layer = nil;
[dataPending release]; dataPending = nil;
... ... @@ -96,36 +95,15 @@ NSString * const RMMapImageLoadingCancelledNotification = @"MapImageLoadingCance
[super dealloc];
}
/*
- (id) increaseLoadingPriority
{
loadingPriorityCount++;
return self;
}
- (id) decreaseLoadingPriority
{
loadingPriorityCount--;
if (loadingPriorityCount == 0)
[self cancelLoading];
return self;
}*/
- (void)drawInRect:(CGRect)rect
{
[image drawInRect:rect];
/* if (image != NULL)
{
CGContextRef context = UIGraphicsGetCurrentContext();
RMLog(@"image width = %f", CGImageGetWidth(image));
// CGContextClipToRect(context, rect);
CGContextDrawImage(context, rect, image);
}*/
[image drawInRect:rect];
}
-(void)draw
{
[self drawInRect:screenLocation];
[self drawInRect:screenLocation];
}
+ (RMTileImage*)imageForTile:(RMTile) _tile withURL: (NSString*)url
... ... @@ -135,7 +113,7 @@ NSString * const RMMapImageLoadingCancelledNotification = @"MapImageLoadingCance
+ (RMTileImage*)imageForTile:(RMTile) _tile fromFile: (NSString*)filename
{
return [[[RMFileTileImage alloc] initWithTile: _tile FromFile:filename] autorelease];
return [[[RMFileTileImage alloc] initWithTile:_tile FromFile:filename] autorelease];
}
+ (RMTileImage*)imageForTile:(RMTile) tile withData: (NSData*)data
... ... @@ -192,9 +170,7 @@ NSString * const RMMapImageLoadingCancelledNotification = @"MapImageLoadingCance
[tileImage release];
NSDictionary *d = [NSDictionary dictionaryWithObject:data forKey:@"data"];
[[NSNotificationCenter defaultCenter] postNotificationName:RMMapImageLoadedNotification
object:self
userInfo:d];
[[NSNotificationCenter defaultCenter] postNotificationName:RMMapImageLoadedNotification object:self userInfo:d];
}
- (BOOL)isLoaded
... ... @@ -249,10 +225,6 @@ NSString * const RMMapImageLoadingCancelledNotification = @"MapImageLoadingCance
layer.actions=customActions;
layer.edgeAntialiasingMask = 0;
// RMLog(@"location %f %f", screenLocation.origin.x, screenLocation.origin.y);
// RMLog(@"layer made");
}
if (image != nil)
... ... @@ -260,7 +232,6 @@ NSString * const RMMapImageLoadingCancelledNotification = @"MapImageLoadingCance
layer.contents = (id)[image CGImage];
[image release];
image = nil;
// RMLog(@"layer contents set");
}
}
... ... @@ -286,7 +257,7 @@ NSString * const RMMapImageLoadingCancelledNotification = @"MapImageLoadingCance
if (layer != nil)
{
// layer.frame = screenLocation;
// layer.frame = screenLocation;
layer.position = screenLocation.origin;
layer.bounds = CGRectMake(0, 0, screenLocation.size.width, screenLocation.size.height);
}
... ... @@ -294,4 +265,9 @@ NSString * const RMMapImageLoadingCancelledNotification = @"MapImageLoadingCance
[self touch];
}
- (void) displayProxy:(UIImage*) img
{
layer.contents = (id)[img CGImage];
}
@end
... ...
... ... @@ -42,6 +42,7 @@ extern NSString * const RMResumeExpensiveOperations;
extern NSString * const RMTileRetrieved;
extern NSString * const RMTileRequested;
extern NSString * const RMTileError;
@protocol RMTileSource;
... ...
... ... @@ -45,6 +45,7 @@ NSString* const RMResumeExpensiveOperations = @"RMResumeExpensiveOperations";
NSString* const RMTileRetrieved = @"RMTileRetrieved";
NSString* const RMTileRequested = @"RMTileRequested";
NSString* const RMTileError = @"RMTileError";
@implementation RMTileLoader
... ...
... ... @@ -35,9 +35,7 @@
}
/// \deprecated hardcoded to return nil
+(RMTileImage*) bestProxyFor: (RMTile) t;
+(RMTileImage*) errorTile;
+(RMTileImage*) loadingTile;
+(UIImage*) errorTile;
+(UIImage*) loadingTile;
@end
... ...
... ... @@ -26,34 +26,26 @@
// POSSIBILITY OF SUCH DAMAGE.
#import "RMTileProxy.h"
#import "RMTileImage.h"
@implementation RMTileProxy
+(RMTileImage*) bestProxyFor: (RMTile) t
{
WarnDeprecated();
return nil;
}
//static TileImage *_errorTile = nil;
static RMTileImage *_loadingTile = nil;
static UIImage *_errorTile = nil;
static UIImage *_loadingTile = nil;
+(RMTileImage*) errorTile
+ (UIImage*) errorTile
{
return nil;
if (_errorTile) return _errorTile;
_errorTile = [[UIImage imageNamed:@"error.png"] retain];
return _errorTile;
}
+(RMTileImage*) loadingTile
+ (UIImage*) loadingTile
{
if (_loadingTile != nil)
return _loadingTile;
if (_loadingTile) return _loadingTile;
RMTile t = RMTileDummy();
/// \bug magic string literals
NSString* file = [[NSBundle mainBundle] pathForResource:@"loading" ofType:@"png"];
_loadingTile = [[RMTileImage imageForTile:t fromFile:file] retain];
_loadingTile = [[UIImage imageNamed:@"loading.png"] retain];
return _loadingTile;
// return nil;
}
@end
... ...
... ... @@ -28,19 +28,21 @@
#import <Foundation/Foundation.h>
#import "RMTileImage.h"
static const NSUInteger kWebTileRetries = 30;
/// RMTileImage subclass: a tile image loaded from a URL.
@interface RMWebTileImage : RMTileImage {
/// Before image is completely loaded a proxy image can be used.
/// This will typically be a boilerplate image or a zoomed in or zoomed out version of another image.
RMTileImage *proxy;
NSUInteger retries;
NSInteger retryCode;
NSURL *url;
NSURLConnection *connection;
/// Data accumulator during loading.
NSMutableData *data;
}
@property (assign, nonatomic) RMTileImage *proxy;
- (id) initWithTile: (RMTile)tile FromURL:(NSString*)url;
- (void) requestTile;
- (void) startLoading:(NSTimer *)timer;
@end
... ...
... ... @@ -26,7 +26,6 @@
// POSSIBILITY OF SUCH DAMAGE.
#import "RMWebTileImage.h"
#import "RMTileProxy.h"
#import <QuartzCore/CALayer.h>
#import "RMMapContents.h"
... ... @@ -34,115 +33,95 @@
@implementation RMWebTileImage
@synthesize proxy;
- (id) initWithTile: (RMTile)_tile FromURL:(NSString*)urlStr
{
if (![super initWithTile:_tile])
return nil;
// RMLog(@"Loading image from URL %@ ...", urlStr);
NSURL *url = [NSURL URLWithString: urlStr];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSCachedURLResponse *cachedData = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
self.proxy = [RMTileProxy loadingTile];
[super displayProxy:[RMTileProxy loadingTile]];
// NSURLCache *cache = [NSURLCache sharedURLCache];
// RMLog(@"Cache mem size: %d / %d disk size: %d / %d", [cache currentMemoryUsage], [cache memoryCapacity], [cache currentDiskUsage], [cache diskCapacity]);
if (cachedData != nil)
{
// RMLog(@"Using cached image");
[self updateImageUsingData:[cachedData data]];
}
else
{
BOOL startImmediately = [RMMapContents performExpensiveOperations];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:startImmediately];
url = [[NSURL alloc] initWithString:urlStr];
if (connection == nil)
{
RMLog(@"Error: Connection is nil ?!?");
self.proxy = [RMTileProxy errorTile];
}
else
{
//Notify whatever is interested that we have requested a tile
[[NSNotificationCenter defaultCenter] postNotificationName:RMTileRequested object:nil];
}
connection = nil;
if (startImmediately == NO)
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startLoadingImage:) name:RMResumeExpensiveOperations object:nil];
}
}
data =[[NSMutableData alloc] initWithCapacity:0];
retries = kWebTileRetries;
[[NSNotificationCenter defaultCenter] postNotificationName:RMTileRequested object:nil];
// RMLog(@"... done. data size = %d", [imageData length]);
[self requestTile];
return self;
}
- (void) startLoadingImage: (NSNotification*)notification
{
if (connection != nil)
{
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[connection start];
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:RMResumeExpensiveOperations object:nil];
}
-(void) dealloc
- (void) dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self cancelLoading];
// RMLog(@"Image dealloced");
// we never retain so don't ever release it. The error and loading tiles are singletons
// [proxy release];
[data release];
data = nil;
// RMLog(@"loading cancelled because image dealloced");
[self cancelLoading];
[url release];
url = nil;
[super dealloc];
}
-(void) cancelLoading
- (void) requestTile
{
if (connection == nil)
return;
//RMLog(@"fetching: %@", url);
if(connection) // re-request
{
//RMLog(@"Refetching: %@: %d", url, retries);
// RMLog(@"Image loading cancelled");
[[NSNotificationCenter defaultCenter] postNotificationName:RMTileRetrieved object:nil];
[connection cancel];
[connection release];
connection = nil;
[connection release];
connection = nil;
[data release];
data = nil;
[super cancelLoading];
if(retries == 0) // No more retries
{
[super displayProxy:[RMTileProxy errorTile]];
[[NSNotificationCenter defaultCenter] postNotificationName:RMTileRetrieved object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:RMTileError object:[NSNumber numberWithInteger:retryCode]];
return;
}
retries--;
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(startLoading:) userInfo:nil repeats:NO];
}
else
{
[self startLoading:nil];
}
}
- (void)makeLayer
- (void) startLoading:(NSTimer *)timer
{
[super makeLayer];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:30.0];
if (image == nil
&& layer != nil
&& layer.contents == nil)
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
if (!connection)
{
layer.contents = (id)[[proxy image] CGImage];
[super displayProxy:[RMTileProxy errorTile]];
[[NSNotificationCenter defaultCenter] postNotificationName:RMTileRetrieved object:nil];
}
}
- (void)drawInRect:(CGRect)rect
- (void) cancelLoading
{
if (image)
[super drawInRect:rect];
if (!connection)
return;
[[NSNotificationCenter defaultCenter] postNotificationName:RMTileRetrieved object:nil];
[connection cancel];
[connection release];
connection = nil;
[super cancelLoading];
}
#pragma mark URL loading functions
... ... @@ -159,32 +138,50 @@
//– connection:didFailWithError: delegate method
//– connectionDidFinishLoading: delegate method
// Do clean up when download fails
- (void)failCleanUp
{
self.proxy = [RMTileProxy errorTile];
[data release];
data = nil;
//If the tile failed, we still need to notify that this connection is done
[[NSNotificationCenter defaultCenter] postNotificationName:RMTileRetrieved object:nil];
}
- (void)connection:(NSURLConnection *)_connection didReceiveResponse:(NSURLResponse *)response
{
if([(NSHTTPURLResponse *)response statusCode] == 404){
RMLog(@"Tile could not be loaded: RMWebTileImage received a 404 status code");
[self failCleanUp];
[_connection cancel];
return;
}
if (data != nil)
[data release];
/// \bug magic number
int statusCode = 600; // unknown
if([response isKindOfClass:[NSHTTPURLResponse class]])
statusCode = [(NSHTTPURLResponse*)response statusCode];
[data setLength:0];
NSInteger contentLength = [response expectedContentLength];
if (contentLength < 0) {
contentLength = 0;
/// \bug magic number
if(statusCode < 400) // Success
{
}
/// \bug magic number
else if(statusCode == 404) // Not Found
{
[self updateImageUsingData:UIImagePNGRepresentation([UIImage imageNamed:@"missing.png"])];
[self cancelLoading];
}
else // Other Error
{
//RMLog(@"didReceiveResponse %@ %d", _connection, statusCode);
BOOL retry = FALSE;
switch(statusCode)
{
/// \bug magic number
case 500: retry = TRUE; break;
case 503: retry = TRUE; break;
}
if(retry)
{
retryCode = statusCode;
[self requestTile];
}
else
{
[[NSNotificationCenter defaultCenter] postNotificationName:RMTileError object:[NSNumber numberWithInteger:statusCode]];
[self cancelLoading];
}
}
data = [[NSMutableData alloc] initWithCapacity:contentLength];
}
- (void)connection:(NSURLConnection *)_connection didReceiveData:(NSData *)newData
... ... @@ -192,34 +189,53 @@
[data appendData:newData];
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)_connection willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
return cachedResponse;
}
- (void)connection:(NSURLConnection *)_connection didFailWithError:(NSError *)error
{
RMLog(@"Tile could not be loaded: %@", [error localizedDescription]);
[self failCleanUp];
//RMLog(@"didFailWithError %@ %d %@", _connection, [error code], [error localizedDescription]);
BOOL retry = FALSE;
switch([error code])
{
/// \bug magic number
case -1002: retry = TRUE; break; // unsupported URL
case -1004: retry = TRUE; break; // can’t connect to host
case -1009: retry = TRUE; break;
}
if(retry)
{
retryCode = [error code];
[self requestTile];
}
else
{
[[NSNotificationCenter defaultCenter] postNotificationName:RMTileError object:[NSNumber numberWithInteger:[error code]]];
[self cancelLoading];
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)_connection
{
/// \bug magic number
if ([data length] < 512) {
RMLog(@"connectionDidFinishLoading %@ data size %d", _connection, [data length]);
RMLog(@"%@", data);
RMLog(@"%@", [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]);
//RMLog(@"connectionDidFinishLoading %@ data size %d", _connection, [data length]);
retryCode = 512;
[self requestTile];
}
else
{
[self updateImageUsingData:data];
[data release];
data = nil;
[url release];
url = nil;
[connection release];
connection = nil;
[[NSNotificationCenter defaultCenter] postNotificationName:RMTileRetrieved object:nil];
}
[self updateImageUsingData:data];
[data release];
data = nil;
[connection release];
connection = nil;
// RMLog(@"finished loading image");
//Notify whatever is interested that we have retrieved a tile
[[NSNotificationCenter defaultCenter] postNotificationName:RMTileRetrieved object:nil];
}
@end
... ...