8

I'm using a custom MKOverlay/MKOverlayView to completely cover the Google basemap with my own tiles, which are loaded asynchronously. I follow the pattern of requesting unloaded tiles when I receive a canDrawMapRect:zoomScale: call to my overlay view (and returning FALSE in that case), then calling setNeedsDisplayInMapRect:zoomScale: once the tile is available.

This all generally works, and appears to work perfectly in the simulator.

However, on the device I sometimes get a 'hole' in the overlay - a missing tile.

I can see that the tile is requested, and that the request completes. I can see that I call setNeedsDisplayInMapRect:zoomScale:, and that I am passing the original MKMapRect and MKZoomScale which were provided in canDrawMapRect:zoomScale:. But I can also see that the overlay is never asked to redraw that tile (neither canDrawMapRect:zoomScale: nor drawMapRect:zoomScale:inContext: is ever again called for that tile).

I need to understand why this is happening and how to correct it.

Here's the relevant code from my MKOverlayView subclass:

- (BOOL) canDrawMapRect: (MKMapRect) mapRect zoomScale: (MKZoomScale) zoomScale 
{
    NSUInteger zoomLevel = [self zoomLevelForZoomScale:zoomScale];
    CGPoint mercatorPoint = [self mercatorTileOriginForMapRect:mapRect];
    NSUInteger tilex = floor(mercatorPoint.x * [self worldTileWidthForZoomLevel:zoomLevel]);
    NSUInteger tiley = floor(mercatorPoint.y * [self worldTileWidthForZoomLevel:zoomLevel]);

    NSURL* tileUrl = [self tileURLForZoomLevel: zoomLevel tileX: tilex tileY: tiley];

    ASIHTTPRequest* tileRequest = [ASIHTTPRequest requestWithURL: tileUrl];
    tileRequest.downloadCache = [ASIDownloadCache sharedCache];
    [tileRequest setCacheStoragePolicy:ASICachePermanentlyCacheStoragePolicy];

    if ( NO == [[ASIDownloadCache sharedCache] isCachedDataCurrentForRequest: tileRequest] )
    {
        [tileRequest setCachePolicy: ASIAskServerIfModifiedWhenStaleCachePolicy];
        tileRequest.delegate = self;
        tileRequest.userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
                                [NSValue value: &mapRect withObjCType: @encode( MKMapRect )],       @"mapRect",
                                [NSValue value: &zoomScale withObjCType: @encode( MKZoomScale )],   @"zoomScale",
                                [NSNumber numberWithInt: tilex], @"tilex",
                                [NSNumber numberWithInt: tiley], @"tiley",
                                nil];

        [_tileRequestStack addOperation: tileRequest];

        NSLog( @"canDrawMapRect: %d, %d - REQUESTING", tilex, tiley );

        return NO;
    }

    NSLog( @"canDrawMapRect: %d, %d - READY", tilex, tiley );

    return YES;
}

- (void) drawMapRect: (MKMapRect) mapRect zoomScale: (MKZoomScale) zoomScale inContext: (CGContextRef) context 
{
    NSUInteger zoomLevel = [self zoomLevelForZoomScale:zoomScale];

    CGPoint mercatorPoint = [self mercatorTileOriginForMapRect:mapRect];

    NSUInteger tilex = floor(mercatorPoint.x * [self worldTileWidthForZoomLevel:zoomLevel]);
    NSUInteger tiley = floor(mercatorPoint.y * [self worldTileWidthForZoomLevel:zoomLevel]);

    NSLog( @"drawMapRect:  %d, %d", tilex, tiley );

    NSURL* tileUrl = [self tileURLForZoomLevel: zoomLevel tileX: tilex tileY: tiley];
    NSData* tileData = [[ASIDownloadCache sharedCache] cachedResponseDataForURL: tileUrl];

    UIGraphicsPushContext(context);

    if ( tileData != nil )
    {
        UIImage* img = [UIImage imageWithData: tileData];

        if ( img != nil )
        {
            [img drawInRect: [self rectForMapRect: mapRect] blendMode: kCGBlendModeNormal alpha: 1.0 ];
        }
        else 
        {
            NSLog( @"oops - no image" );
        }

        CGSize s = CGContextConvertSizeToUserSpace( context, CGSizeMake( 40, 1 ));

        UIFont* f = [UIFont systemFontOfSize: s.width];

        [[UIColor blackColor] setFill];

        [[NSString stringWithFormat: @"%d,%d", tilex, tiley] drawInRect: [self rectForMapRect: mapRect] withFont: f];
    }

    UIGraphicsPopContext();
}


- (void) requestFinished: (ASIHTTPRequest *) tileRequest
{
    NSValue* mapRectValue =  [tileRequest.userInfo objectForKey: @"mapRect"];
    MKMapRect mapRect;  [mapRectValue getValue: &mapRect];

    NSValue *zoomScaleValue = [tileRequest.userInfo objectForKey:@"zoomScale"];
    MKZoomScale zoomScale; [zoomScaleValue getValue: &zoomScale];

    NSLog( @"requestFinished: %d, %d, %lf", 
          [[tileRequest.userInfo objectForKey:@"tilex"] intValue], 
          [[tileRequest.userInfo objectForKey:@"tiley"] intValue], 
          zoomScale  );

    [self setNeedsDisplayInMapRect: mapRect zoomScale: zoomScale];
}

EDIT: I'm guessing that this is likely the issue.

Community
  • 1
  • 1
TomSwift
  • 39,369
  • 12
  • 121
  • 149
  • @Stanislaw - no. In fact I abandoned MapKit because of this and wrote my own tiled-map solution. I'd be curious if this problem still existed on iOS6 since MapKit is rewritten and now uses OpenGL for rendering. – TomSwift Oct 10 '12 at 22:37
  • Thanks for response. I have this problem on iOS 6 Simulator. Didn't you open-source your solution? It would be interesting to look at it. – Stanislav Pankevich Oct 11 '12 at 08:53
  • @Stanislaw - no I didn't opensource it. I wrote it for a client, sorry. – TomSwift Oct 11 '12 at 15:33
  • Could you confirm or refuse my following assumption: if inside *- (BOOL)canDrawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale {}* I would only add the strings: *[self setNeedsDisplayInMapRect:mapRect zoomScale:zoomScale]; NSLog(@"This should log again and again"); return NO;* then this method could run endlessly? I am trying to understand more details here to decide whether to write a bug report to Apple or not. Thanks! – Stanislav Pankevich Oct 11 '12 at 18:18
  • My last comment is about MapKit expected correct behaviour. – Stanislav Pankevich Oct 11 '12 at 23:23
  • @TomSwift, please see the answer I've posted. Would be nice to know, if it could solve your original issue as well. – Stanislav Pankevich Oct 26 '12 at 19:36

2 Answers2

1

I had an issue very similar to the one described here. In my case I couldn't reproduce the desired behaviour (described in http://developer.apple.com/library/ios/documentation/MapKit/Reference/MKOverlayView_class/Reference/Reference.html#//apple_ref/occ/instm/MKOverlayView/setNeedsDisplayInMapRect:zoomScale:) even having the most simple code possible:

- (BOOL)canDrawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale {
    NSLog(@"This should trace forever");

    [self setNeedsDisplayInMapRect:mapRect zoomScale:zoomScale];
    return NO;
}

or closer to my original code:

- (BOOL)canDrawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale {
    NSLog(@"This should trace forever");

    [SomeAsynchronousRequestWithCompletionHandler:^{
        [self setNeedsDisplayInMapRect:mapRect zoomScale:zoomScale];
    }];
    return NO;
}

In both cases setNeedsDisplayInMapRect:zoomScale: has never been called even once.

The situation changed, when I began running setNeedsDisplayInMapRect:zoomScale: inside a dispatch_async dispatched to the same queue that canDrawMapRect runs on, like:

- (BOOL)canDrawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale {

    dispatch_queue_t queue = dispatch_get_current_queue();

    NSLog(@"This should trace forever");

    dispatch_async(queue, ^{
        [self setNeedsDisplayInMapRect:mapRect zoomScale:zoomScale];
    });

    return NO;
}

or with asynchronous job included:

- (BOOL)canDrawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale {
    NSLog(@"This should trace forever");

    dispatch_queue_t queue = dispatch_get_current_queue();

    [SomeAsynchronousRequestWithCompletionHandler:^{
        dispatch_async(queue, ^{
            [self setNeedsDisplayInMapRect:mapRect zoomScale:zoomScale];
        });
    }];
    return NO;
}

Using dispatch_async - I can see "This should trace forever" string being traced endlessly. My original problem is also disappeared completely.

LATER UPDATE: Currently, I use dispatch_get_main_queue() to call setNeedsDisplayInMapRect:zoomScale: like

- (BOOL)canDrawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale {
    NSLog(@"This should trace forever");

    [SomeAsynchronousRequestWithCompletionHandler:^{
        dispatch_async(dispatch_get_main_queue(), ^{
            [self setNeedsDisplayInMapRect:mapRect zoomScale:zoomScale];
        });
    }];
    return NO;
}
Stanislav Pankevich
  • 11,044
  • 8
  • 69
  • 129
  • I haven't tried this myself (the problem isn't relevant to me anymore) but you solution looks good. Nice work! – TomSwift Oct 26 '12 at 21:39
  • Even if this approach works, it would still be an Apple bug. You should be able to call setNeedsDisplayInMapRect from any thread, not just the drawing thread. After all, typically you would call it from the main thread. I cannot vouch for the map interface, but I tested your approach on a normal CATiledLayer, and it did not work reliably. See http://stackoverflow.com/a/13095551/908621 – fishinear Oct 27 '12 at 14:16
  • It is not the thread you use for calling setNeedsDisplayInMapRect that is the problem, the problem is that if setNeedsDisplayInMapRect is called while the overlay is being rendered the call is ignored. This problem is not solved by calling setNeedsDisplayInMapRect on the main thread. – loomer Apr 09 '13 at 12:28
0

The answer above did not work for me. From the NSLog printout I used I could see that a different thread was being used for despite grabbing the dispatch_get_current_queue() in canDrawMapRect and storing it for later use. This was the case in the iPad 4.3 Simulator at least, I did not attempt on the device.

My solution was less satisfactory and more error prone solution of wait x time before calling.

-(void)setNeedsDisplay:(WebRequest*)webRequest
{  
   [webRequest retain];
   dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.25 * NSEC_PER_SEC), dispatch_get_main_queue(), 
   ^{
      [webRequest autorelease];
      [delegate setNeedsDisplayInMapRect:webRequest.request.requestedMapRect 
                              zoomScale:webRequest.request.requestedScale];
   });     
}
deathbytes
  • 69
  • 3
  • Please, look: I updated my answer (from "LATER UPDATE" words) - I use dispatch_get_main_queue() as well and I don't need additionally any dispatch_after. Are you sure you need this delay? – Stanislav Pankevich Nov 29 '12 at 07:05
  • the delay was required in my case. I tried it without dispatch_after to start with. – deathbytes Jan 04 '13 at 20:27