iOS 11: MKOverlayRenderer decouples previously consistent relationship between mapRect and zoomScale

Originator:jordan.smith
Number:rdar://33638875 Date Originated:
Status:Open Resolved:
Product:iOS - MapKit Product Version:iOS 11 beta 1 - 10
Classification: Reproducible:Always
 
Area:
MapKit

Summary:
In previous version of iOS, MKOverlayRenderer has presented subclasses with a zoomScale and mapRect that are equivalent for both canDraw(_:zoomScale:) and draw(_:zoomScale:in:). Returning true when the content is available to draw results in draw(_:zoomScale:in:) being called with the same zoomScale and mapRect parameters passed to canDraw.

Although this is not a documented relationship, it would seem to make sense for several reasons. Tile content (for example, third party mapping provider images) may be cached on a per mapRect basis, and the relationship between mapRect size and zoomScale until now has remained constant. Some third party mapping providers do not support multiple tile sizes per zoom level, requiring a large amount of work to decouple the expected relationship (i.e. fetching multiple tiles, stitching them together, cropping the result, and performing this such that requests are cached and coalesced).

As of iOS 11 beta 4, the mapRect and zoomScale passed to canDraw(_:zoomScale) differs from the values passed to draw(_:zoomScale:in). The latter is called with smaller mapRects, but an equivalent zoomScale. This does not break documented logic, as areas are only drawn inside the mapRect specified by canDraw(_:zoomScale). It does however break assumptions that may have been made about the correlation between mapRect and zoomScale, and that canDraw and draw will be called with matching parameters.

Is this change intentional? It could well be speculated that this is indeed the case, for performance, pre-caching, or other internal reason - and since the behaviour is not documented should have no undue effect. However, the repercussions of such may be significant for anyone drawing content in an MKOverlayRenderer where the above assumptions were made. In some circumstances the work required to undo these assumptions is certainly non trivial.

Steps to Reproduce:
Run the attached project, and take a look at the logged output. Note how differing parameters are logged for each the canDraw(_:zoomScale) and draw(_:zoomScale:in) calls.

Expected Results:
Expected results were for the relationship between zoomLevel and mapRect to be maintained both between iOS 11 and previous versions. The parameters passed to canDraw were also expected to result in a call to draw with matching parameters, provided that true was returned from canDraw.

Observed Results:
canDraw(_:zoomLevel) is called with mapRects that are three times the size passed to draw(_:zoomLevel:in) calls, for correlating zoomLevel values.

Version:
iOS 11 beta 4

Comments

And more...

Did some deep diving of the data. On my iPad, when it calls canDrawMapRect upon launch, to tile the map for the first time, it calls canDrawMapRect 63 times, in a 9x7 grid. Each rect's origin is spaced exactly 256 points apart from the other rect origins, so they are using the grid spacing I'd expect. Unfortunately, the rect widths are 768 points, which ends up with a bunch of overlapping rects. So I think the 768 width, which is 3X larger than it should be, must indeed be a bug. They have the spacing of rect origins correct, but the rect widths are 3X too big.

I have enough evidence to file a bug with Apple -- may be closed as a duplicate of this, but hopefully it adds more info. New bug ID 35233291.

By craig.hunter.mobile at Oct. 28, 2017, 1:24 a.m. (reply...)

More information -- root problem possibly in setNeedsDisplayInMapRect

Been struggling with this bug myself. If you divide the mapRect size (width or height) by the zoomScale, you'll get the effective rect sizes in screen points. drawMapRect is getting fed 256-pt rects, which is consistent with behavior going back many years, and it's consistent with the fundamentals of tiled maps. However, canDrawMapRect is getting fired with 768-pt rects. What's more, no matter what size rect I use when calling setNeedsDisplayInMapRect, subsequent calls into canDrawMapRect see that rect scaled and snapped to the nearest 768-pt rect (or rects). So I suspect that the real issue at play here is that when sections of the MKOverlayRenderer are invalidated with setNeedsDisplayInMapRect, it's defaulting to use 768-pt rects (or at least it shows up that way when it gets to canDrawMapRect).

This is a bit infuriating, as the fundamentals of tiled maps are based on even multiples of a 256-pt (or -px) grid. Changes in zoom levels observe a well known 1:4 or 4:1 quadtree behavior. Why we'd be stuck with mapRects that are an odd multiple of 256, and will never be consistently aligned evenly with the grid, is hard to fathom. Either it's a bug or a very poor judgement call.

By craig.hunter.mobile at Oct. 27, 2017, 4:24 p.m. (reply...)

Reported the same issue - reporducible in a plain project Beta 8

I have reported the same issue affecting one of my applications & I was able to reproduce it with a new project, defining a custom tiles overlay rendered the discrepancies mentioned between draw() and canDraw() are present using a 2D base map too. As an output example:

Can draw: MKMapRect(origin: C.MKMapPoint(x: 148897792.0, y: 90177536.0), size: C.MKMapSize(width: 6291456.0, height: 6291456.0))

Drawing function : MKMapRect(origin: C.MKMapPoint(x: 148897792.0, y: 90177536.0), size: C.MKMapSize(width: 2097152.0, height: 2097152.0))

I got exactly 3X size comparing both functions.

My bug was marked as duplicated of yours in Apple Bug reporter

By barbararodeker at Aug. 29, 2017, 12:07 p.m. (reply...)

MapKit MKTileOverlayRenderer inconsistency and 3D Flyover maps

I have just run into exactly the same issue. I have an App which stores Offline Map tiles for display when out of cellular data coverage (or when the user wants to avoid cellular roaming charges).

The discrepancy between the mapRect size supplied in the call to .canDraw() and the mapRect size in the call to .draw() is exactly as you described (3x). But this appears to cause problems only when the Apple Basemap is a 3D Flyover Map. When using a 2D basemap (i.e. Apple Standard, Satellite or Hybrid), the tiles drawn in response to the .draw() method appear in the correct place and scale on the Map.

However when using a 3D Flyover Map, these tiles do not appear at the correct scale (and it seems that only even-numbered tiles are getting displayed at all by MapKit). I'll file a radar with Apple in the hope that this gets fixed before iOS 11 release (hopefully it's not already too late).

Did you get any response from Apple?

By paul.robin.manson at Aug. 17, 2017, 11:20 p.m. (reply...)

Update (or lack thereof)

No update from Apple, and it seems very much still an issue on beta 10.

By jordan.smith at Sept. 8, 2017, 4:14 a.m. (reply...)

Example Project

https://drive.google.com/file/d/0B093qXpgxyolRDF2anhjQ1lFUm8/view?usp=sharing

By jordan.smith at July 31, 2017, 11:28 p.m. (reply...)

Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!