Search code examples
iosgoogle-maps-sdk-ios

GMSSyncTileLayer tileForX:y:zoom: called only once


Summary:

I subclassed

GMSSyncTileLayer 

and overwrote

tileForX:y:zoom: 

but its only called once no matter how much I pan.

why?

DETAIL

We have implemented our own TileServer which is behind a secure webservice so we need to pass a login token to get the tiles.

The call is asynchronous POST which is quiet common for a webservice call.

Because I had to pass login token in the NSURLSession header I couldnt just pass GET urls to

GMSTileURLConstructor urls =  http://<tileserver>/gettile?x=3&y=4&zoom=5

So I subclassed GMSSyncTileLayer

@interface SNSyncTileLayer : GMSSyncTileLayer

overwrote

- (UIImage *)tileForX:(NSUInteger)x y:(NSUInteger)y zoom:(NSUInteger)zoom {

When tileForX:y:Zoom: is called the first time I call a webservice to get the tile UIImage.

The UIImage returns on a delegate and is stored in a NSDictionary with key in format TILE_x_y_Zoom.

The call to the WS is asynch so - (UIImage *)tileForX:y:Zoom: always returns nil for that tile the first time its called.

What i've noticed is that tileForX:y:Zoom: is never called again no matter how much I pan back and forth.

For instance at the current zoom I pan across europe.

I see tileForX:y:Zoom: being called once and ws calls being mad and images being stored in my dictionary.

But if i keep panning at the same zoom I come back to Europe and tileForX:y:Zoom: is not called again.

Fix one - clear cache when new tile downloaded

I tried creating a delegate on SNSyncTileLayer and everytime a new tile downloaded it called:

[self.snSyncTileLayer clearTileCache];

But this wipes out ALL tiles and reloads them so as you pan you get a terrible flashing.

My only idea next is to measure how much the map has panned and if its more than half a width or height then to call clearTileCache.

SO the big question why isnt tileForX:y:Zoom: called everytime?

My overridden class

//
//  SNSyncTileLayer.m
//

#import "SNSyncTileLayer.h"
#import "SNAppDelegate.h"

#import "GoogleTileRequest.h"
#import "SNGoogleTileRequest.h"

#import "GoogleTileImageResult.h"
@interface SNSyncTileLayer()<SNSeaNetWebServicesManagerDelegate>{
    BOOL _debugOn;
}
@property (nonatomic, retain) NSMutableDictionary * tileImageCacheDict;
@end


@implementation SNSyncTileLayer
- (instancetype)init
{
    self = [super init];
    if (self) {
        _tileImageCacheDict = [NSMutableDictionary dictionary];
        _debugOn = TRUE;
    }
    return self;
}

- (UIImage *)tileForX:(NSUInteger)x y:(NSUInteger)y zoom:(NSUInteger)zoom {
    if(_debugOn)ImportantLog(@"tileForX:(%lu) y:(%lu) zoom:(%lu)", (unsigned long)x,(unsigned long)y,(unsigned long)zoom);
    UIImage *tileImage_ = nil;
    //tileImage_ = [UIImage imageNamed:@"EmptyTile1.png"];

    NSString * keyForTile_ = [NSString stringWithFormat:@"TILE_%lu_%lu_%lu", (unsigned long)x,(unsigned long)y,(unsigned long)zoom];
    id dictObj_ = [self.tileImageCacheDict objectForKey:keyForTile_];

    if (dictObj_) {

        if([dictObj_ isMemberOfClass:[NSNull class]])
        {
            if(_debugOn)DebugLog(@"tile has been called before but image not downloaded yet:[%@]",keyForTile_);
        }
        else if([dictObj_ isMemberOfClass:[UIImage class]])
        {
            if(_debugOn)DebugLog(@"cached image found in dict_ return it:[%@]",keyForTile_);
            tileImage_ = (UIImage *)dictObj_;
        }
        else{
            ErrorLog(@"ITEM IN self.tileImageCacheDict not NSNull or UIImage:[%@]", dictObj_);
        }
    }else{
        if(_debugOn)ImportantLog(@"tileForX: CACHED IMAGE NOT FOUND: DOWNLOAD IT[%@]",keyForTile_);
        //-----------------------------------------------------------------------------------
        //add in temp object - tyring to check if tileForX:Y:Zoom is called more than once
        [self.tileImageCacheDict setObject:[NSNull null] forKey:keyForTile_];
        //-----------------------------------------------------------------------------------
        //-----------------------------------------------------------------------------------
        SNAppDelegate *appDelegate = (SNAppDelegate *)[[UIApplication sharedApplication] delegate];
        GoogleTileRequest * googleTileRequest_ = [[GoogleTileRequest alloc]init];

        googleTileRequest_.X = [NSNumber numberWithInteger:x];
        googleTileRequest_.Y = [NSNumber numberWithInteger:y];
        googleTileRequest_.Zoom = [NSNumber numberWithInteger:zoom];

        #pragma mark TODO - NOW -  thur11dec - load from settings
        googleTileRequest_.MapType = @"Dark";


        //for general errors
        appDelegate.snSeaNetWebServicesManager.delegate = self;

        //Request should know what class to return too
        googleTileRequest_.delegateForRequest = self;

        [appDelegate.snSeaNetWebServicesManager ITileController_GoogleTile:googleTileRequest_];
        //-----------------------------------------------------------------------------------
        return kGMSTileLayerNoTile;
        //-----------------------------------------------------------------------------------
    }



    return tileImage_;
}

#pragma mark -
#pragma mark SNSeaNetWebServicesManagerDelegate
#pragma mark -

-(void)     snSeaNetWebServicesManager:(SNSeaNetWebServicesManager *)SNSeaNetWebServicesManager
           wsReponseReceivedForRequest:(SNWebServiceRequest *)snWebServiceRequest_
                                 error:(NSError *)error
{    
#pragma mark TODO - NOW - thur11dec2014
    if(error){
        ErrorLog(@"error:%@",error);

    }else{

        if(snWebServiceRequest_){
            if([snWebServiceRequest_ isMemberOfClass:[SNGoogleTileRequest class]])
            {
                //Result is JSONModel ivar in Request
                if(snWebServiceRequest_.resultObject){
                    GoogleTileImageResult * googleTileImageResult_= (GoogleTileImageResult *)snWebServiceRequest_.resultObject;
                    UIImage * responseImage_ = googleTileImageResult_.responseImage;


                    if(responseImage_){

                        //-----------------------------------------------------------------------------------
                        //build the key from the parameters
                        if(snWebServiceRequest_.bodyJsonModel){
                            NSDictionary *paramsDict = [snWebServiceRequest_.bodyJsonModel toDictionary];
                            if(paramsDict){
                                NSString *keyX_ = [paramsDict objectForKey:@"X"];
                                NSString *keyY_ = [paramsDict objectForKey:@"Y"];
                                NSString *keyZoom_ = [paramsDict objectForKey:@"Zoom"];

                                if(keyX_){
                                    if(keyY_){
                                        if(keyZoom_){
                                            NSString * keyForTile_ = [NSString stringWithFormat:@"TILE_%@_%@_%@", keyX_,keyY_,keyZoom_];
                                            if(_debugOn)ImportantLog(@"TILE DOWNLOADED ADD TO CACHE[%@]",keyForTile_);
                                            [self.tileImageCacheDict setObject:responseImage_ forKey:keyForTile_];
                                            //if(_debugOn)DebugLog(@"[[self.tileImageCacheDict allKeys]count]:%lu", (unsigned long)[[self.tileImageCacheDict allKeys]count]);

                                            //-----------------------------------------------------------------------------------
                                            //I ADDED THIS SO delegate could clearTileCache but causes flashing as ALL tiles get reloaded visible ones and ones downloaded but not on map
                                            if(self.delegate){
                                                if([self.delegate respondsToSelector:@selector(snSyncTileLayer:tileDownloadedForX:Y:Zoom:)]){

                                                    [self.delegate snSyncTileLayer:self
                                                                tileDownloadedForX:keyX_
                                                                                 Y:keyY_
                                                                              Zoom:keyZoom_
                                                                    ];

                                                }else {
                                                    ErrorLog(@"<%@ %@:(%d)> %s delegate[%@] doesnt implement snSyncTileLayer:tileDownloadedForX:Y:Zoom:", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__ ,self.delegate);
                                                }
                                            }else{
                                                ErrorLog(@"<%@ %@:(%d)> %s self.delegate is nil", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__);
                                            }
                                            //-----------------------------------------------------------------------------------




                                        }else{
                                            ErrorLog(@"keyZoom_ is nil");
                                        }
                                    }else{
                                        ErrorLog(@"keyY_ is nil");
                                    }
                                }else{
                                    ErrorLog(@"keyX_ is nil");
                                }


                            }else{
                                ErrorLog(@"paramsDict is nil");
                            }
                        }else{
                            ErrorLog(@"self.downloadingTasksDictionary is nil");
                        }
                        //-----------------------------------------------------------------------------------

                    }else{
                        ErrorLog(@"responseImage_ is nil");
                    }
                }else{
                    ErrorLog(@"snWebServiceRequest_.resultJsonModel is nil");
                }
            }
            else {
                ErrorLog(@"UNHANDLED snWebServiceRequest_:%@", snWebServiceRequest_.class);
            }

        }else{
            ErrorLog(@"snWebServiceRequest_ is nil");
        }
    }
}







@end

Solution

  • I haven't done this so I'm not sure, but my guess from reading the documentation is that you should be subclassing GMSTileLayer instead of GMSSyncTileLayer.

    GMSSyncTileLayer is designed for cases where you are able to synchronously (ie immediately) return the tile for that location. By returning kGMSTileLayerNoTile, you are specifically indicating that 'there is no tile here', and so it never calls your class again for that location, as you've already responded that there is no tile there. (BTW, your description says you are returning nil, which indicates a transient error, but your code is actually returning kGMSTileLayerNoTile).

    The GMSTileLayer class is designed for the asynchronous approach that you're using. If you subclass GMSTileLayer, your requestTileForX:y:zoom:receiver: method should start the background process to fetch the tile. When the tile request succeeds, then it is passed off to the GMSTileReceiver that was provided in that method (you should keep a copy of that receiver along with your request).