RL2000
RL2000

Reputation: 1011

Customizing MKTileOverlay for cached tiles

I'm trying to update my tile map code to use iOS 7's MKTileOverlay and MKTileOverlayRenderer, and I could use some pointers for making things work better.

First, here is the iOS6 code: AppleTileOverlay.m and TileOverlayView.m. This still works quite well in iOS 7 when when I replace TileOverlayView with a class that is identical in all ways except that it's a subclass of MKOverlayRenderer instead of MKOverlayView.

The new piece I'm testing is a subclass of MKTileOverlay with the only method being:

-(NSURL *)URLForTilePath:(MKTileOverlayPath)path {
    NSString *tileKey = [[NSString alloc] initWithFormat:@"%d%d%d", path.x, path.y, path.z];
    NSString *tilePath = [[NSBundle mainBundle] pathForResource:tileKey ofType:nil inDirectory:@"TileFolder"];

    NSURL *url;
    if (tilePath) {
        url = [NSURL fileURLWithPath:tilePath];
    }

    return url;
}

The map tiles load fine most of the time, but the log fills up with messages like this:

Error loading URL (null): Error Domain=NSURLErrorDomain Code=-1000 "bad URL" UserInfo=0x1b3e19e0 {NSUnderlyingError=0x1894d470 "bad URL", NSLocalizedDescription=bad URL}

from the method returning nil for the URL.

So the question is: Can I avoid those error messages, or should I just stick with the older overlay class I have?

Upvotes: 3

Views: 3934

Answers (4)

Eric
Eric

Reputation: 141

I think with Swift 2.0, URLForFilePath(...) cannot return nil as it is not an Optional.

I managed to solve this problem using the MKTileOverlay subclass to check for valid tile path as above and loading a 'dummy' transparent tile if a tile image was not available.

override func URLForTilePath(path: MKTileOverlayPath) -> NSURL {

        let tileKey = String(format:"%d/%d/%d",path.z,path.x,path.y)

        let tilePath = NSBundle.mainBundle().pathForResource(tileKey, ofType: "png", inDirectory: "Maps/Map1880")

        let blankTilePath = NSBundle.mainBundle().pathForResource("blank", ofType: "png", inDirectory: "Maps")

        var url: NSURL

        if ((tilePath) != nil)
        {
            url =  NSURL.fileURLWithPath(tilePath!)
        } else {
            url = NSURL.fileURLWithPath(blankTilePath!)
        }

        return url;
    }

This is not very elegant as it loads the blank tile for every tile that is not part of the overlay.

However, there is a better solution, thanks to User: junkpile on Apple Developer Forum, the problem with attempting to load non-existent overlay tiles is that boundingMapRect is set by default to MKMapRectWorld, i.e. the whole world.

To restrict this to the required overlay region, subclass MKTileOverlay.

Here's an example:

import MapKit
class CustomTileOverlay : MKTileOverlay {

        override var boundingMapRect: MKMapRect {
            get {
               //North-East Corner of region
                let lat1 = 53.46075
                let long1 = -1.92618
               //South-West Corner of region
                let lat2 = 53.43018
                let long2 = -1.992885

                //Convert to Coordinates
                let coord1 = CLLocationCoordinate2DMake(lat1,long1)
                let coord2 = CLLocationCoordinate2DMake(lat2,long2)

                //Convert to map points
                let p1 = MKMapPointForCoordinate (coord1);
                let p2 = MKMapPointForCoordinate (coord2);

                //Return the MKMapRect
               return MKMapRectMake(fmin(p1.x,p2.x), fmin(p1.y,p2.y), fabs(p1.x-p2.x), fabs(p1.y-p2.y)); 
            }
        }
    }

Upvotes: 1

nverinaud
nverinaud

Reputation: 1300

You can avoid it by creating a custom MKTileOverlay subclass like the following:

@interface MyTileOverlay : MKTileOverlay
@end

@implementation MyTileOverlay

- (NSURL *)URLForTilePath:(MKTileOverlayPath)path 
{
    NSString *tileKey = [[NSString alloc] initWithFormat:@"%d%d%d", path.x, path.y, path.z];
    NSString *tilePath = [[NSBundle mainBundle] pathForResource:tileKey ofType:nil inDirectory:@"TileFolder"];

    NSURL *url;
    if (tilePath) 
    {
        url = [NSURL fileURLWithPath:tilePath];
    }

    return url;
}

- (void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result
{
    NSURL *url = [self URLForTilePath:path];
    if (url)
    {
        [super loadTileAtPath:path result:result];
    }
}

@end

The idea is to not trigger the tile loading if you are sure the tile does not exists.

Upvotes: 0

Craig
Craig

Reputation: 8294

Does this happen when you are looking at areas of the map that you don't have tiles for?

Because you check if you have the file and then don't set url if the file doesn't exist, you return nil. You should return a valid NSURL to a transparent image instead.

Upvotes: 0

incanus
incanus

Reputation: 5128

My guess is you are somehow always trying to set url despite not having a true, valid tilePath. Add some debugging and see.

Upvotes: 0

Related Questions