mbxmapkit icon indicating copy to clipboard operation
mbxmapkit copied to clipboard

Empty tiles displayed when zooming in if an MKPolyline is present

Open maxneust opened this issue 11 years ago • 17 comments

If the map being used has an MKPolyline overlay added to it, whenever you zoom in empty tiles are displayed in the areas the polyline is covering. I expected that the tiles corresponding to the zoom level you just zoomed in from are kept on screen and replaced when the new ones can be rendered - which is the default behaviour when no MKPolyline is present.

To illustrate this, I've attached two images:

Before zooming in Before

Immediately after zooming in After

It also happens in the sample project, just add the following code in MBXViewController.m:

  • Declare a property for the polyline
@property (nonatomic) MKPolyline *polyline;
  • Add the polyline overlay after [_mapView addOverlay:_rasterOverlay];
NSArray *sampleCoordinates = @[[[CLLocation alloc] initWithLatitude:36.9794318 longitude:-122.0310751],
                               [[CLLocation alloc] initWithLatitude:36.9903995 longitude:-121.9832925]];
CLLocationCoordinate2D* coords = malloc(sampleCoordinates.count * sizeof(CLLocationCoordinate2D));
for (int i = 0; i < sampleCoordinates.count; i++) {
    coords[i] = ((CLLocation*)sampleCoordinates[i]).coordinate;
}
self.polyline = [MKPolyline polylineWithCoordinates:coords count:sampleCoordinates.count];
[_mapView addOverlay:self.polyline];
  • Configure the renderer
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay
{
    ...
    } else if ([overlay isKindOfClass:[MKPolyline class]]) {
        MKPolylineRenderer* lineView = [[MKPolylineRenderer alloc] initWithPolyline:self.polyline];
        lineView.strokeColor = [UIColor whiteColor];
        lineView.lineWidth = 1;
        return lineView;
    }
    return nil;
}

If you run the project and try to zoom in, the issue will be present. Don't add the polyline (or zoom in where the polyline is outside the viewport), and it works fine.

I reckon this might not be related to Mapbox and it's just the way that Apple handles custom tile overlays and MKPolylines, but it considerably degrades the experience so I'd really appreciate it if you guys can help me figure out what's going on.

Thanks!

maxneust avatar Jan 14 '15 03:01 maxneust

@maxneust, Something you could try is checking to see if the tiles disappear at a specific zoom level, and then compare that to the maximum zoom level provided by your tile source. For example, if it happens to be the case that the tiles disappear at the transition from zoom level 20 to zoom level 21, and it is also the case that your tile source has a maximum zoom level of 20, then the behavior you're seeing could be a result of polylines invalidating portions of MKMapView's rendered tile cache.

MapKit includes some type of internal caching for rendered tiles, but there doesn't seem to be any documentation for it. However, you can tell that caching is happening by zooming in and out beyond the maximum zoom level of your tile source and watching what happens on screen.

To check the zoom level, one way is to implement mapView:regionDidChangeAnimated: from the MKMapViewDelegate protocol, convert the MKMapView's region to a zoom level with mbx_zoomLevel from MBXMapKit.m, then NSLog it. I think that might only fire at the end of a zoom though, so you could use an NSTimer to get continuous updates.

Another thing you could try is putting an NSLog in loadTileAtPath:result: to verify which tiles are being requested.

Basically, I suspect that what you're seeing might be happening because your tile source doesn't provide tiles at a high enough zoom level for the amount of zooming you're attempting to do. Normally Apple's MKMapView render cache provides some limited overzooming, but when you do something that triggers a new render (like adding lines), the render cache gets invalidated and the pseudo-overzooming stops working.

Most of what I'm saying above is pretty speculative, and it's quite possible that I'm entirely wrong. However, my speculation does fit the observable facts reasonably well for some strange behavior that I've seen in the past, and the strange behavior you're describing seems kind of similar to that stuff.

ghost avatar Jan 18 '15 18:01 ghost

@wsnook Thanks for the tips!

The issue is present at any zoom level, so it's not related with the max zoom level available for the tile source. After the zoomed-in tiles are rendered, zooming in and out around them works and they're not requested again, so once they're loaded, they stay in the cache.

The basic problem is that overzooming is disabled for any tile that also contains a polyline, irregardless of the tile source and zoom level.

maxneust avatar Jan 21 '15 14:01 maxneust

@maxneust That's really weird. It sounds like you've found a rendering bug in MapKit rather than anything which is specific to MBXMapKit.

Two suggestions:

  1. If you can reproduce this issue with a plain MKTileOverlay and MKPolyline (i.e. no MBXMapKit in the loop), you could file a bug about it with Apple.
  2. You could take a look at what @incanus is working on over at #132. I don't know that what he's doing will help, but if the problem has to do with a bug in Apple's MKTileOverlayRenderer, it might.

ghost avatar Jan 21 '15 15:01 ghost

I ran into this problem in another project. Adapting the MBXRasterTileRenderer and code from http://stackoverflow.com/questions/4417545/calculating-tiles-to-display-in-a-maprect-when-over-zoomed-beyond-the-overlay, I modified the renderer so that it would correctly draw overzoomed tiles in this case. Here's the code:

- (NSInteger) zoomScaleToZoomLevel:(MKZoomScale)zoomScale {
    return log2(zoomScale) + 20;
}

- (CGImageRef) image:(CGImageRef)image scaledForMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale {
    MKTileOverlay *tileOverlay = (MKTileOverlay *)self.overlay;
    CGFloat factor = tileOverlay.tileSize.width / 256;

    NSInteger x = round(mapRect.origin.x * zoomScale / (tileOverlay.tileSize.width / factor));
    NSInteger y = round(mapRect.origin.y * zoomScale / (tileOverlay.tileSize.width / factor));
    NSInteger z = log2(zoomScale) + 20;

    NSInteger zoomCap = ((MKTileOverlay*)self.overlay).maximumZ;
    NSInteger overZoom = 1;

    if (z > zoomCap) {
        // overZoom progression: 1, 2, 4, 8, etc...
        overZoom = pow(2, (z - zoomCap));
    }

    CGFloat overzoomFP = overZoom;
    CGFloat tileWidth = tileOverlay.tileSize.width;
    CGFloat tileHeight = tileOverlay.tileSize.height;
    CGRect imageRect = CGRectMake((x % overZoom)/overzoomFP * tileWidth, (y % overZoom)/overzoomFP * tileHeight, tileWidth/overZoom, tileHeight/overZoom);
    return CGImageCreateWithImageInRect(image, imageRect);
}


- (MKTileOverlayPath)pathForMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale {
    MKTileOverlay *tileOverlay = (MKTileOverlay *)self.overlay;
    CGFloat factor = tileOverlay.tileSize.width / 256;

    NSInteger x = round(mapRect.origin.x * zoomScale / (tileOverlay.tileSize.width / factor));
    NSInteger y = round(mapRect.origin.y * zoomScale / (tileOverlay.tileSize.width / factor));
    NSInteger z = log2(zoomScale) + 20;

    NSInteger zoomCap = ((MKTileOverlay*)self.overlay).maximumZ;

    if (z > zoomCap) {
        // overZoom progression: 1, 2, 4, 8, etc...
        NSInteger overZoom = pow(2, (z - zoomCap));
        x /= overZoom;
        y /= overZoom;
        z = zoomCap;
    }

    MKTileOverlayPath path = {
        .x = x,
        .y = y,
        .z = z,
        .contentScaleFactor = self.contentScaleFactor
    };

    return path;
}

- (void) drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context {
    // OverZoom Mode - Detect when we are zoomed beyond the tile set.
    NSInteger z = [self zoomScaleToZoomLevel:zoomScale];
    NSInteger overZoom = 1;
    NSInteger zoomCap = ((MKTileOverlay*)self.overlay).maximumZ;

    if (z > zoomCap) {
        // overZoom progression: 1, 2, 4, 8, etc...
        overZoom = pow(2, (z - zoomCap));
    }

    MKTileOverlayPath path = [self pathForMapRect:mapRect zoomScale:zoomScale];
    NSString *xyz = [[self class] xyzForPath:path];
    if (!xyz) {
        return;
    }
    NSData *tileData = nil;

    @synchronized(self) {
        tileData = [[self class] imageDataFromRenderer:self
                                                forXYZ:xyz
                                         usingBigTiles:(((MKTileOverlay *)self.overlay).tileSize.width == 512)];

        if (!tileData) {
            return [self setNeedsDisplayInMapRect:mapRect zoomScale:zoomScale];
        }
    }

    CGImageRef imageRef = nil;

    CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)tileData);
    if (provider) {
        if ([[self class] dataIsPNG:tileData]) {
            imageRef = CGImageCreateWithPNGDataProvider(provider, nil, NO, kCGRenderingIntentDefault);
        } else if ([[self class] dataIsJPEG:tileData]) {
            imageRef = CGImageCreateWithJPEGDataProvider(provider, nil, NO, kCGRenderingIntentDefault);
        }
        CGDataProviderRelease(provider);
    }

    if (!imageRef) {
        return [self setNeedsDisplayInMapRect:mapRect zoomScale:zoomScale];
    }

    if (overZoom > 1) {
        imageRef = [self image:imageRef scaledForMapRect:mapRect zoomScale:zoomScale];
    }

    CGRect rect = [self rectForMapRect:mapRect];

    CGContextSaveGState(context);
    CGContextTranslateCTM(context, CGRectGetMinX(rect), CGRectGetMinY(rect));

    // OverZoom mode - 1 when using tiles as is, 2, 4, 8 etc when overzoomed.
    CGContextScaleCTM(context, overZoom/zoomScale, overZoom/zoomScale);

    CGContextTranslateCTM(context, 0, CGImageGetHeight(imageRef));
    CGContextScaleCTM(context, 1, -1);
    CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)), imageRef);
    CGContextRestoreGState(context);

}

troughton avatar Feb 25 '15 03:02 troughton

@wsnook Re: 1, it is indeed present in a plain setup, and it seems to be not a bug but by design - I'm assuming Apple does this having in mind that you're not replacing the whole map but just placing a non-opaque custom overlay on top of it, in that context it makes sense to remove it while zooming since the default map will still be visible. The unfortunate side effect of this is what I'm describing here.

Re: 2, I've tried it (it's part of 0.7.0 now), but didn't help.

@troughton Thanks for the contribution! However, your code doesn't seem to work. First, maximumZ always returns the max (22) for the MBX overlay, so the overzooming adjustments are never triggered; and second, the calculation of the tiles to be requested is wrong - you end up with groups of 4 equal, repeated tiles in places where tiles from different locations should be displayed. I'll spend some time trying to figure out what's the issue, but if this is working for you, can you give me a bit more context on your setup?

maxneust avatar Feb 25 '15 11:02 maxneust

@maxneust Reading back through this thread, I see that on Jan 21 you wrote:

The basic problem is that overzooming is disabled for any tile that also contains a polyline, irregardless of the tile source and zoom level.

and you mentioned overzooming again just now. If you think that overzooming would solve your problem, have you seen the overzooming code that's part of my mbtiles pull request, #142? You would need to adapt it for a tile source, but that's pretty doable, you may need to adjust for some things like whether your y-axis is flipped or not compared to the coordinate system for mbtiles. If you look around a bit, I know there are some forks where other people have done overzooming as well, but I don't remember who exactly. @lightandshadow68 might have one.

ghost avatar Feb 25 '15 16:02 ghost

@maxneust Okay, so it seems the code doesn't directly transfer across to your use case. The context of it in my project was loading an MKTileOverlay from disk and then displaying it with the modified MBXRasterTileRenderer. When adding an MKPolyline overlay and zooming, Apple's renderer ignores the maximum tile zoom specified by the MKTileOverlay and tries to load tiles at a higher detail level than exists; specifically, I was seeing error messages for it being unable to load tiles at a z of 15, when the tile set only provided tiles for up to a maximumZ of 14. Ordinarily, the map renderer will scale up the preexisting tiles, and

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

and

- (void) drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context

are not called for these overzoomed areas. I specifically worked around the edge case where these methods would be called when it was beyond the maximum zoom of the overlay; specifically, I manually loaded, cropped and scaled the parent tile. Note that I'm missing the specific rendering code for large tile sizes, and my code is dependent upon the tile overlay having a maximumZ set.

Looking back at your description, it seems that the reasons for your problem may be different; specifically,

The issue is present at any zoom level, so it's not related with the max zoom level available for the tile source. After the zoomed-in tiles are rendered, zooming in and out around them works and they're not requested again, so once they're loaded, they stay in the cache.

is different from what I encountered; in my case the tiles simply ceased to load at a certain zoom, and correctly loaded back in when you zoom back out. However, I suspect both issues are symptomatic of the same bug in MapKit.

troughton avatar Feb 25 '15 20:02 troughton

@wsnook Just to be clear, overzooming works perfectly fine without any MKPolylines added to the map, and by overzooming I mean showing the original scaled images from the already-displayed zoom levels until the newer tiles load; and this behaviour stops when polylines are present in the current viewport. I've found a couple of forks and tried them out but they didn't solve the problem. I'll check out your PR, thanks!

@troughton Thanks for the details. Yes, it might be related to the same bug (if it really is a bug, as I mentioned before it could be that this is just by design). A solution in the same line of what you've done is what I had in mind (loading and scaling the 'parent' tiles when zooming), and it might just be the implementation that I'll have to end up doing.

maxneust avatar Mar 01 '15 15:03 maxneust

@maxneust Were you ever able to find a solution to this issue? It appears to be a MapKit bug that I have experienced as well when adding MKPolylines to the map, appears to be unrelated to MBXMapKit.

ZachNagengast avatar Mar 08 '16 19:03 ZachNagengast

@ZachNagengast nope, couldn't find a solution. Are you having this issue in 'raw' MapKit as well? Because for me, MKPolylines on a 'normal' MKMapView with no other overlays/layers on top of it work just fine.

maxneust avatar Mar 11 '16 13:03 maxneust

@maxneust Yep exactly, we have a lot of overlays were trying to work with. For now we're just limiting the zoom to be just above the max zoom, but it's not ideal because the max zoom changes based on the available imagery for certain locations.

ZachNagengast avatar Mar 11 '16 22:03 ZachNagengast

@ZachNagengast can you explain how you are limiting the zoom level? thanks

etown avatar Mar 15 '16 16:03 etown

@etown Nothing too complex - using the MKMapCamera I'm checking at what altitude corresponds to the max zoom level, and the next time I set the camera I threshold it to always have an altitude slightly above the max zoom altitude. Of course this wont limit the case where the user is interacting with the camera manually, but for our purposes we didn't need that functionality.


// Limit altitude zoom so that the max zoom (287 meters) is never hit and map tile will always load
if (altitude < 300)
{
    altitude = 300;
}

MKMapCamera *currentCamera = [MKMapCamera new];
[currentCamera setAltitude:altitude];

[_mapView setCamera:currentCamera animated:YES];

As I mentioned, this isn't fool proof because the 287 max zoom altitude is somewhat arbitrary based on available tile resolution for that area of the map.

Side note - This solution required us to switch away from using [_mapView setRegion] and I outlined how we did that here: http://stackoverflow.com/a/35782159/1130983

ZachNagengast avatar Mar 15 '16 17:03 ZachNagengast

@ZachNagengast thank you for your reply. unfortunately I need to handle user interaction and I suppose there is no solution for that

etown avatar Mar 15 '16 19:03 etown

@etown None that I know of, but the one other thing I'd probably try is readjusting the zoom in - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated

ZachNagengast avatar Mar 15 '16 20:03 ZachNagengast

An old issue, but had the same problem and changing the level for the overlay removed the issue, Changed from "MKOverlayLevelAboveLabels" to [mapView addOverlay:overlay level:MKOverlayLevelAboveRoads];

oscarkockum avatar Feb 07 '18 11:02 oscarkockum

An old issue, but had the same problem and changing the level for the overlay removed the issue, Changed from "MKOverlayLevelAboveLabels" to [mapView addOverlay:overlay level:MKOverlayLevelAboveRoads];

This only works for standard view not any of the satellite or flyover views. I think this is a mapkit bug as it does not exist in standard view. Really annoying when trying to draw polygon areas over maps in satellite view.

Educatesoft-com avatar Jun 08 '20 19:06 Educatesoft-com